fix: 同步制作接口并修复浅色主题层次

This commit is contained in:
GouJ
2026-09-14 10:08:54 +08:00
parent 2eea0ea72e
commit 7ea3f2a0b4
19 changed files with 641 additions and 93 deletions
+14
View File
@@ -10,6 +10,9 @@ import type {
ProjectVideoStatus,
PromptReadiness,
ProductionPipelineResult,
ProductionPlan,
ProviderInputSpec,
ProjectProductionStatus,
RetryVideosResult,
ShotKeyframe,
ShotVideo,
@@ -32,6 +35,17 @@ function shotPath(id: string) {
/** 生产链 API;生成请求不设置客户端短超时,也不自动重试。 */
export const productionApi = {
/** 根据显式指定的模型查询最终输入,不能用通用规格推断授权素材是否生效。 */
providerInputSpec: (shotId: string, provider: string, signal?: AbortSignal) =>
request<ProviderInputSpec>(`${shotPath(shotId)}/provider-input-spec?provider=${encodeURIComponent(provider)}`, {
signal
}),
/** 查询当前计划,不生成图片、视频或提示词。 */
plan: (projectId: string, signal?: AbortSignal) =>
request<ProductionPlan>(`${projectPath(projectId)}/production/plan`, { signal }),
/** 读取真实主资产完成状态,不能用任务提交回执代替。 */
status: (projectId: string, signal?: AbortSignal) =>
request<ProjectProductionStatus>(`${projectPath(projectId)}/production/status`, { signal }),
startPipeline: (projectId: string) =>
request<ProductionPipelineResult>(`${projectPath(projectId)}/production/start`, {
method: 'POST',
@@ -4,6 +4,7 @@ import { computed, ref, watch } from 'vue'
import ConfirmAction from '../../workflows/ConfirmAction.vue'
import { downloadText, formatDate } from '../../../lib/format'
import { useAdvancedProduction } from '../useAdvancedProduction'
import ProductionPlanPanel from './ProductionPlanPanel.vue'
/** 高级入口明确真实流程边界,默认折叠且不在挂载时触发预检或生成。 */
const { id, operation, session, checking, error, check, blocked, preflight, start } = useAdvancedProduction()
@@ -31,12 +32,10 @@ function exportReceipt() {
<NCollapse v-model:expanded-names="expanded" class="mt-5"
><NCollapseItem name="advanced" title="高级:串联生产(全项目)">
<NAlert type="warning" :show-icon="false"
>依次补齐形态提示词形态参考图视频提示词并提交视频任务不会生成视觉风格身份母版导演设计即时状态或首帧请先在各阶段完成验收</NAlert
>依次补齐形态提示词参考图首帧质量校验与修复视频提示词提交视频任务视觉风格身份母版导演设计和即时状态仍需提前准备</NAlert
>
<p class="mt-3 text-xs leading-6 text-muted">
范围固定为全项目跳过已有结果不使用上方并发覆盖数量上限和尺寸配置后端固定文字并发
3图片和视频并发 2可能产生模型费用此流程没有持久运行锁或恢复
checkpoint请确认其他页面终端没有任务断网后先查资产勿直接重试
范围固定为全项目跳过已有结果不使用上方并发覆盖数量上限和尺寸配置可能产生文字图片首帧校验与修复视频模型费用模型与并发由服务端配置决定请确认其他页面没有任务断网后先刷新制作状态勿直接重试
</p>
<div class="mt-4 flex flex-wrap items-center gap-3">
<NButton :disabled="blocked || checking" :loading="checking" @click="preflight"
@@ -46,7 +45,7 @@ function exportReceipt() {
label="启动串联生产"
:disabled="blocked || checking || !check || !!check.issues.length"
acknowledgement
:description="`项目 ${id}:全项目串联生产,不生成首帧,也不等待视频完成。确认后会重新检查前置条件;可能产生文字、图片和视频模型费用。`"
:description="`项目 ${id}:全项目串联生产,包含首帧生成、校验与有限修复,不等待异步视频完成。确认后会重新读取计划;有阻塞时先处理可执行部分,成功资产会保留。可能产生文字、图片、校验和视频模型费用。`"
@confirm="start"
/>
</div>
@@ -63,8 +62,9 @@ function exportReceipt() {
<ul v-if="check.issues.length" class="mt-2 space-y-2 text-xs text-danger">
<li v-for="issue in check.issues" :key="issue">{{ issue }}</li>
</ul>
<p v-else class="mt-2 text-sm">前置检查通过提交前仍会再次校验</p>
<p v-else class="mt-2 text-sm">可以执行计划中的任务提交前仍会再次校验</p>
</div>
<ProductionPlanPanel v-if="check" :plan="check.plan" :status="check.status" />
<p v-if="operation.pending && operation.label === '高级串联生产'" role="status" class="mt-3 text-sm">
请求进行中请勿重复提交
</p>
@@ -73,7 +73,13 @@ function exportReceipt() {
<h3 class="text-sm font-semibold">
串联生产回执 ·
{{
receipt.needsManualReview ? '需要人工处理' : receipt.completed ? '流程已返回' : '流程未完成'
receipt.needsManualReview
? '需要人工处理'
: receipt.completed
? '制作已完成'
: receipt.dispatchCompleted
? '任务已分发,等待视频完成'
: '本轮未完成'
}}
</h3>
<NButton text size="small" @click="exportReceipt">导出回执</NButton>
@@ -87,11 +93,23 @@ function exportReceipt() {
失败 {{ row.result.failed }}</template
><template v-else>未执行或未返回</template>
</p>
<p class="mt-2 text-xs">
首帧质量<template v-if="receipt.keyframeQualityResult">
{{ receipt.keyframeQualityResult.total }} · 校验通过
{{ receipt.keyframeQualityResult.passed }} · 跳过 {{ receipt.keyframeQualityResult.skipped }} ·
阻塞 {{ receipt.keyframeQualityResult.blocked }} · 修复未通过
{{ receipt.keyframeQualityResult.repairFailed }} · 执行失败
{{ receipt.keyframeQualityResult.failed }} </template
><template v-else>未执行或未返回</template>
</p>
<p class="mt-2 text-xs">
视频任务<template v-if="receipt.videoGenerationResult"
> {{ receipt.videoGenerationResult.total }} · 已提交
{{ receipt.videoGenerationResult.created }} · 跳过 {{ receipt.videoGenerationResult.skipped }} ·
提交失败 {{ receipt.videoGenerationResult.failed }}</template
{{ receipt.videoGenerationResult.created }} · 跳过
{{ receipt.videoGenerationResult.skipped }} 含进行中
{{ receipt.videoGenerationResult.inProgress }}阻塞
{{ receipt.videoGenerationResult.blocked }} · 提交失败
{{ receipt.videoGenerationResult.failed }}</template
><template v-else>未执行或未返回</template>
</p>
<NAlert
@@ -0,0 +1,125 @@
<script setup lang="ts">
import { computed } from 'vue'
import { NAlert, NCollapse, NCollapseItem, NScrollbar } from 'naive-ui'
import type { ProductionPlan, ProjectProductionStatus } from '../types'
/** 展示后端只读执行计划与真实资产状态,不自行推断阻塞是否可自动解除。 */
const props = defineProps<{ plan: ProductionPlan; status: ProjectProductionStatus }>()
const rows = computed(() => [
{ label: '主体参考图', stage: props.plan.subjectImages },
{ label: '首帧质量', stage: props.plan.keyframes },
{ label: '视频提示词', stage: props.plan.videoPrompts },
{ label: '视频任务', stage: props.plan.videos }
])
const stageNames = { subject_images: '主体参考图', keyframes: '首帧', videos: '视频', completed: '全部完成' }
/** 保留全部后端问题,包括预计可由前置阶段解决的问题,便于核对计划。 */
const problems = computed(() => [
...props.plan.details.subjectImages
.filter(item => item.issues.length)
.map(item => ({
key: `form:${item.subjectFormId}`,
label: `主体参考图 · ${item.subjectName} / ${item.formName}${item.subjectRef}`,
issues: item.issues
})),
...[
{ label: '首帧', key: 'keyframe', items: props.plan.details.keyframes },
{ label: '视频提示词', key: 'prompt', items: props.plan.details.videoPrompts },
{ label: '视频', key: 'video', items: props.plan.details.videos }
].flatMap(group =>
group.items
.filter(item => item.issues.length)
.map(item => ({
key: `${group.key}:${item.shotId}`,
label: `${group.label} · 镜头 ${item.shotNo}${item.shotId}`,
issues: item.issues
}))
)
])
</script>
<template>
<section class="surface-inset mt-3 p-4" aria-label="制作执行计划">
<h3 class="text-sm font-semibold">执行计划 · 仅检查不产生生成费用</h3>
<p class="mt-2 text-xs" role="status">
当前制作状态{{ status.completed ? '制作已完成' : `待完成${stageNames[status.stage]}` }} · 有效主体图
{{ status.subjectImages.current }}/{{ status.subjectImages.total }} · 有效首帧
{{ status.keyframes.skipped }}/{{ status.keyframes.total }} · 有效主视频 {{ status.videos.skipped }}/{{
status.videos.total
}}
· 视频进行中 {{ status.videos.inProgress }}
</p>
<NScrollbar x-scrollable class="mt-3">
<table class="production-plan-table">
<caption class="sr-only">
各阶段当前执行数量
</caption>
<thead>
<tr>
<th scope="col">阶段</th>
<th scope="col">总数</th>
<th scope="col">可执行</th>
<th scope="col">跳过</th>
<th scope="col">前置完成后可执行</th>
<th scope="col">需人工处理</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.label">
<th scope="row">{{ row.label }}</th>
<td>{{ row.stage.total }}</td>
<td>{{ row.stage.planned }}</td>
<td>{{ row.stage.skipped }}</td>
<td>{{ row.stage.upstreamResolvable }}</td>
<td>{{ row.stage.manualBlocked }}</td>
</tr>
</tbody>
</table>
</NScrollbar>
<p class="mt-2 text-xs text-muted">
计划按当前资产估算前置生成后会重新检查视频跳过包含已有有效视频和进行中的任务已有过期主体主图生成新候选后仍需在形态图库确认新主图
</p>
<NAlert v-if="plan.hasManualBlockers" type="warning" :show-icon="false" class="mt-3">
各阶段合计
{{ plan.manualBlocked }}
项需人工处理同一镜头可能重复计数可先生成就绪的主体参考图成功结果会保留遇到阻塞时后续阶段可能暂停
</NAlert>
<NCollapse v-if="problems.length" class="mt-3">
<NCollapseItem name="issues" title="查看前置条件与阻塞原因">
<ul class="space-y-3 text-xs">
<li v-for="item in problems" :key="item.key">
<strong class="break-all">{{ item.label }}</strong>
<p v-for="(issue, index) in item.issues" :key="index" class="mt-1 break-words">
{{ issue.reason
}}<span v-if="issue.missingSubjects?.length">
· 关联主体{{ issue.missingSubjects.join('') }}</span
>
</p>
</li>
</ul>
</NCollapseItem>
</NCollapse>
</section>
</template>
<style scoped>
.production-plan-table {
width: 100%;
min-width: 540px;
border-collapse: collapse;
font-size: 12px;
}
.production-plan-table th,
.production-plan-table td {
padding: 9px 12px;
text-align: left;
}
.production-plan-table thead {
background: var(--app-control);
}
.production-plan-table tbody {
background: var(--app-surface);
}
.production-plan-table tbody tr:nth-child(even) {
background: var(--app-subtle);
}
</style>
@@ -0,0 +1,89 @@
<script setup lang="ts">
import { computed, onScopeDispose, ref, watch } from 'vue'
import { NAlert, NButton, NInput } from 'naive-ui'
import { productionApi } from '../api'
import { errorMessage } from '../../../lib/http'
import type { ProviderInputSpec } from '../types'
/** 最终输入检查仅发 GET,切换模型或镜头后清除旧回执,不修改授权绑定。 */
const props = defineProps<{ shotId: string }>()
const provider = ref('')
const targetProvider = computed(() => provider.value.trim().toLowerCase())
const result = ref<ProviderInputSpec | null>(null)
const error = ref('')
const loading = ref(false)
let version = 0
let controller: AbortController | undefined
/** 使晚到响应失效,避免把另一镜头或模型的授权信息展示到当前页面。 */
function clear() {
version++
controller?.abort()
result.value = null
error.value = ''
loading.value = false
}
watch([() => props.shotId, targetProvider], clear, { flush: 'sync' })
onScopeDispose(clear)
/** 查询实际输入并核对镜头及模型归属。 */
async function inspect() {
if (!props.shotId || !targetProvider.value || loading.value) return
clear()
const current = version
const shotId = props.shotId
const providerName = targetProvider.value
controller = new AbortController()
loading.value = true
try {
const data = await productionApi.providerInputSpec(shotId, providerName, controller.signal)
if (current !== version) return
if (data.shotId !== shotId || data.targetProvider !== providerName)
throw new Error('最终输入与当前镜头或模型不匹配,请重新查询。')
result.value = data
} catch (cause) {
if (current === version) error.value = errorMessage(cause)
} finally {
if (current === version) loading.value = false
}
}
</script>
<template>
<section class="surface-inset mt-4 p-4" aria-label="视频模型最终输入检查">
<h4 class="text-sm font-medium">按模型检查实际输入</h4>
<p class="mt-2 text-xs text-muted">
填写后端使用的视频模型标识检查实际首帧与授权素材只读查询不产生生成费用
</p>
<div class="mt-3 flex flex-wrap gap-2">
<NInput
v-model:value="provider"
placeholder="例如 seedance"
class="min-w-0 flex-1"
:input-props="{ 'aria-label': '视频模型标识' }"
/>
<NButton :disabled="!targetProvider || loading" :loading="loading" @click="inspect">检查实际输入</NButton>
</div>
<NAlert v-if="error" type="error" :show-icon="false" class="mt-3">{{ error }}</NAlert>
<div v-if="result" class="mt-3 space-y-2 break-all text-xs">
<p>目标模型{{ result.targetProvider }}</p>
<p>业务主首帧{{ result.businessKeyframe.id }}</p>
<p>
实际输入首帧{{ result.providerInputKeyframe.id }} ·
{{ result.providerInputKeyframe.source === 'provider_variant' ? '模型专用变体' : '业务主首帧' }}
</p>
<p v-if="result.providerInputKeyframe.variantReason">
变体原因{{ result.providerInputKeyframe.variantReason }}
</p>
<ul class="space-y-2">
<li v-for="reference in result.references" :key="`${reference.subjectId}:${reference.subjectFormId}`">
{{ reference.subjectName }} / {{ reference.subjectFormName }}{{ reference.subjectRef }}
<template v-if="reference.providerAsset"
>授权素材 {{ reference.providerAsset.assetId }} ·
{{ reference.providerAsset.provider }}</template
>
<template v-else>未返回该模型的有效授权素材</template>
</li>
</ul>
</div>
</section>
</template>
@@ -1,4 +1,5 @@
<script setup lang="ts">
import ProviderInputInspector from './ProviderInputInspector.vue'
import { NAlert, NButton, NCollapse, NCollapseItem, NTag } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { ExternalLink, ImagePlus, RefreshCw } from '@lucide/vue'
@@ -502,6 +503,7 @@ async function inspectSpecs() {
<NAlert v-if="specError" role="alert" type="error" :show-icon="false" class="mt-4">{{ specError }}</NAlert>
<p v-if="inspecting" role="status" class="mt-4 text-xs text-muted">正在读取生成规格…</p>
<ModelCapabilities :shot-id="shot.shotId" />
<ProviderInputInspector :shot-id="shot.shotId" />
<NCollapse v-if="keyframeSpec || videoSpec" class="mt-5 pt-4 text-xs" :default-expanded-names="['details']"
><NCollapseItem name="details"
><template #header>本镜确定性生成规格</template>
@@ -530,7 +532,7 @@ async function inspectSpecs() {
业务主首帧 ID · {{ videoSpec.keyframe.id }}
</p>
<p v-if="videoSpec.providerInputKeyframe" class="mt-2 break-all text-[10px] text-muted">
模型输入首帧 ID · {{ videoSpec.providerInputKeyframe.id }}
通用规格首帧 ID(未指定模型) · {{ videoSpec.providerInputKeyframe.id }}
{{
videoSpec.providerInputKeyframe.source === 'provider_variant'
? '(专用变体)'
+124 -4
View File
@@ -148,6 +148,8 @@ export interface KeyframeSpec {
/** 视频生成规格中的主体参考图。 */
export interface VideoReference {
/** 当前目标视频模型的授权素材,不拼接模型私有地址。 */
providerAsset?: { provider: string; assetId: string }
subjectId: string
subjectRef: string
subjectName: string
@@ -302,15 +304,20 @@ export interface ProductionSession {
pipelineReceipt?: ProductionPipelineResult | null
}
/** 串联生产只汇总节点结果completed 不代表视频已完成。 */
/** 串联生产回执completed 由后端核对全部当前有效主资产后给出。 */
export interface ProductionPipelineResult {
projectId: string
imageProvider: string
videoProvider: string
visualPromptResult?: PipelineBatchResult
subjectImageResult?: PipelineBatchResult
videoPromptResult?: PipelineBatchResult
videoGenerationResult?: Omit<PipelineBatchResult, 'generated'> & { created: number }
keyframeQualityResult?: PipelineKeyframeQualityResult
videoGenerationResult?: Omit<PipelineBatchResult, 'generated'> & {
created: number
inProgress: number
blocked: number
}
/** 同步任务分发结束;异步视频仍可能正在生成和校验。 */
dispatchCompleted: boolean
completed: boolean
needsManualReview: boolean
stopReason: string
@@ -335,3 +342,116 @@ export interface VideoInputKeyframe {
/** 后端公开的模型输入恢复状态,不能当作普通重试状态。 */
export type ProviderInputRecoveryStatus = 'not_required' | 'required' | 'variant_ready' | 'exhausted'
/** 项目当前制作阶段,与剧本创作状态分别展示。 */
export type ProductionStage = 'subject_images' | 'keyframes' | 'videos' | 'completed'
/** 主体参考图的公开就绪明细,阻塞原因由后端提供。 */
export interface SubjectImageReadinessItem {
subjectFormId: string
subjectRef: string
subjectName: string
module: string
formName: string
status: 'ready' | 'skipped' | 'blocked'
primaryImageId: string | null
primaryImageStale: boolean
issues: ProductionIssue[]
}
/** 无消耗计划中每个阶段的数量;不同阶段的阻塞数不可当作去重镜头数。 */
export interface ProductionPlanStage {
total: number
planned: number
skipped: number
upstreamResolvable: number
manualBlocked: number
}
/** 后端执行计划只读取资产,不触发模型生成。 */
export interface ProductionPlan {
projectId: string
completed: boolean
stage: ProductionStage
hasManualBlockers: boolean
manualBlocked: number
subjectImages: ProductionPlanStage
keyframes: ProductionPlanStage
videoPrompts: ProductionPlanStage
videos: ProductionPlanStage
details: {
subjectImages: SubjectImageReadinessItem[]
keyframes: KeyframeReadinessItem[]
videoPrompts: PromptReadinessItem[]
videos: VideoReadinessItem[]
}
}
/** 真实制作状态;只有主体主图、主首帧和主视频均有效才完成。 */
export interface ProjectProductionStatus {
projectId: string
completed: boolean
stage: ProductionStage
subjectImages: {
total: number
current: number
missing: number
stale: number
complete: boolean
items: {
subjectFormId: string
subjectRef: string
subjectName: string
module: string
formName: string
primaryImageId: string | null
status: 'missing' | 'current' | 'stale'
reason: string
}[]
}
keyframes: Pick<KeyframeReadiness, 'total' | 'ready' | 'skipped' | 'blocked' | 'stalePrimaryKeyframe' | 'items'> & {
complete: boolean
}
videos: Pick<
VideoReadiness,
| 'total'
| 'ready'
| 'skipped'
| 'inProgress'
| 'blocked'
| 'stalePrimaryVideo'
| 'providerInputRecovery'
| 'items'
> & {
complete: boolean
tasks: ProjectVideoStatus
}
}
/** 首帧质量流程保留未通过和异常失败计数,不将已有主图视作本轮校验通过。 */
export interface PipelineKeyframeQualityResult {
total: number
passed: number
repairFailed: number
failed: number
skipped: number
blocked: number
stalePrimaryKeyframe: number
}
/** 指定模型的最终输入查询回执;此接口不会生成图片或视频。 */
export interface ProviderInputSpec {
shotId: string
targetProvider: string
businessKeyframe: Pick<VideoInputKeyframe, 'id' | 'imageUrl'>
providerInputKeyframe: Pick<
VideoGenerationSpec['providerInputKeyframe'],
'id' | 'imageUrl' | 'source' | 'targetProvider' | 'variantReason'
>
references: (Pick<
VideoReference,
'subjectId' | 'subjectRef' | 'subjectName' | 'module' | 'subjectFormId' | 'subjectFormName'
> & {
providerAsset: { provider: string; assetId: string } | null
})[]
}
@@ -2,66 +2,59 @@ import { computed, onScopeDispose, ref, watch } from 'vue'
import { projectsApi } from '../projects/api'
import { useProjectContext } from '../projects/context'
import { subjectImagesApi } from '../subject-images/api'
import { hasRunningImages, primaryImage } from '../subject-images/model'
import { hasRunningImages } from '../subject-images/model'
import { getOperation, runOperation } from '../workflows/operations'
import { hasRunningWorkflow } from '../workflows/selectors'
import { errorMessage } from '../../lib/http'
import { productionApi } from './api'
import { getProductionSession } from './model'
/** 高级串联入口只能消费已验图的主资产,不替用户跳过选角、设计和首帧环节。 */
/** 使用后端计划区分可自动补齐和人工阻塞,保留项目归属及活动任务检查。 */
export async function checkPipeline(projectId: string) {
const [project, checkpoints, forms, keyframes, prompts, videos] = await Promise.all([
const [project, checkpoints, forms, plan, status] = await Promise.all([
projectsApi.detail(projectId),
projectsApi.checkpoints(projectId),
subjectImagesApi.listForms(projectId),
productionApi.keyframeReadiness(projectId, true),
productionApi.promptReadiness(projectId, true),
productionApi.videoReadiness(projectId, false)
productionApi.plan(projectId),
productionApi.status(projectId)
])
if (project.id !== projectId || forms.some(form => form.subject.projectId !== projectId))
if (
project.id !== projectId ||
plan.projectId !== projectId ||
status.projectId !== projectId ||
forms.some(form => form.subject.projectId !== projectId)
)
throw new Error('预检返回了其他项目的数据,请重新读取。')
const issues: string[] = []
const shotIds = new Set(keyframes.items.map(item => item.shotId))
const shotIds = new Set(plan.details.keyframes.map(item => item.shotId))
if (
shotIds.size !== keyframes.total ||
[prompts, videos].some(
data =>
new Set(data.items.map(item => item.shotId)).size !== shotIds.size ||
data.items.some(item => !shotIds.has(item.shotId))
shotIds.size !== plan.keyframes.total ||
plan.details.keyframes.length !== plan.keyframes.total ||
plan.subjectImages.total !== forms.length ||
plan.details.subjectImages.length !== forms.length ||
plan.details.subjectImages.some(item => !forms.some(form => form.id === item.subjectFormId)) ||
[plan.details.videoPrompts, plan.details.videos, status.keyframes.items, status.videos.items].some(
items =>
items.length !== shotIds.size ||
new Set(items.map(item => item.shotId)).size !== shotIds.size ||
items.some(item => !shotIds.has(item.shotId))
)
)
issues.push('各阶段镜头列表不一致,可能正在重新拆解,请刷新后重试。')
issues.push('各阶段资产列表不一致,可能正在重新拆解,请刷新后重试。')
if (project.status !== 'completed') issues.push('剧本尚未完成,请先完成剧本创作。')
if (hasRunningWorkflow(checkpoints)) issues.push('项目仍有后台任务或工作流进行中。')
if (!forms.length) issues.push('尚无正式形态,请先完成拆解。')
if (!forms.length || !plan.keyframes.total) issues.push('尚无正式形态或镜头,请先完成拆解和分镜。')
if (forms.some(form => hasRunningImages(form.images))) issues.push('形态图片仍在生成中。')
if (forms.some(form => !primaryImage(form.images)?.imageUrl))
issues.push('请先补齐并确认所有形态主图,更新过期母版引用,再生成首帧。')
if (
!keyframes.total ||
keyframes.items.length !== keyframes.total ||
keyframes.items.some(
item =>
!item.primaryKeyframeId || item.primaryKeyframeStale || item.issues.length || item.status === 'blocked'
)
status.videos.inProgress ||
status.videos.items.some(item => item.status === 'in_progress' || item.activeVideoId)
)
issues.push('请先为全部镜头完成设计、身份检查和有效主首帧;串联流程不会生成首帧。')
if (
prompts.total !== keyframes.total ||
prompts.items.length !== prompts.total ||
prompts.items.some(item => item.issues.length || item.status === 'blocked')
)
issues.push('视频提示词的生成规格或参考图不完整,请到分镜页修复。')
if (
videos.total !== keyframes.total ||
videos.items.length !== videos.total ||
videos.items.some(item => item.issues.some(issue => issue.code !== 'missing_prompt'))
)
issues.push('视频前置检查未通过,请检查参考图和主首帧。')
if (videos.inProgress || videos.items.some(item => item.status === 'in_progress' || item.activeVideoId))
issues.push('存在活动视频任务,请等待结束后再操作。')
return { projectId, issues, shotCount: keyframes.total, checkedAt: new Date().toISOString() }
if (status.completed || plan.completed) issues.push('当前项目制作已完成,无需再次启动。')
else if (![plan.subjectImages, plan.keyframes, plan.videoPrompts, plan.videos].some(stage => stage.planned > 0))
issues.push('当前没有可执行的制作任务,请先处理计划中的阻塞项。')
// 人工阻塞在计划中独立展示;不拦截其余 ready 形态的部分成功生产。
return { projectId, issues, plan, status, shotCount: plan.keyframes.total, checkedAt: new Date().toISOString() }
}
/** 预检不付费;确认后再次预检并锁住本浏览器项目,长请求不自动重发。 */
@@ -396,7 +396,7 @@ const rules = { concurrency: integerRule('批量并发'), limit: integerRule('
:description="
force
? '对项目全部形态调用图片模型新增一张图片。已有主图不会被替换,生成后可在图片记录中手动选择主图。'
: '仅对没有主图的形态调用图片模型生图,已有主图的形态跳过。操作面向整个项目,不受列表筛选影响。'
: '对缺少主图或主图已过期的形态调用图片模型,仍有效的主图跳过。已有过期主图不会被自动替换,生成后请在图片记录中确认新主图。操作面向整个项目,不受列表筛选影响。'
"
@confirm="generateProject"
/>
@@ -846,7 +846,7 @@ const rules = { concurrency: integerRule('批量并发'), limit: integerRule('
/* 外层使用浅灰分组,输入与下拉恢复为表面色,避免同色后失去控件边界。 */
--app-field: var(--app-surface);
--app-field-hover: var(--app-surface);
background: color-mix(in srgb, var(--app-subtle) 96%, white);
background: var(--app-subtle);
container-type: inline-size;
}
.form-image-toolbar {