From 13bb6dc83b221fb238503f730d65db32dd59f148 Mon Sep 17 00:00:00 2001 From: GouJ Date: Thu, 3 Sep 2026 19:38:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8C=E6=AD=A5=E8=B4=A8=E9=87=8F?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E4=BF=AE=E5=A4=8D=E5=B9=B6=E7=B2=BE=E7=AE=80?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 26 +- docs/backend-coverage.md | 12 +- docs/frontend-simplification.md | 62 +++ src/admin.css | 45 ++ src/components/ui/ActionMenu.test.ts | 90 +++ src/components/ui/ActionMenu.vue | 32 ++ src/components/ui/DetailDisclosure.vue | 23 + src/features/breakdown/BreakdownPage.test.ts | 5 + src/features/breakdown/BreakdownPage.vue | 27 +- .../breakdown/components/SubjectList.vue | 37 +- src/features/create-drama/CreateDramaPage.vue | 20 +- src/features/production/ProductionPage.vue | 62 ++- src/features/production/api.ts | 6 +- src/features/production/capabilities.test.ts | 8 +- .../production/components/KeyframeDialog.vue | 5 +- .../components/ModelCapabilities.vue | 76 +++ .../components/ProductionReceipt.vue | 2 +- .../production/components/QualityDialog.vue | 261 +++++++++ .../production/components/QualityResult.vue | 142 +++++ .../components/ShotProductionAssets.vue | 184 +++++-- src/features/production/production.test.ts | 7 +- src/features/production/quality-api.ts | 52 ++ src/features/production/quality.test.ts | 515 ++++++++++++++++++ src/features/production/quality.ts | 106 ++++ src/features/production/quality.types.ts | 150 +++++ src/features/production/types.ts | 7 +- src/features/production/useProduction.ts | 3 +- src/features/production/useQuality.ts | 248 +++++++++ src/features/storyboard/StoryboardPage.vue | 23 +- .../subject-identity/SubjectIdentityPage.vue | 4 +- .../components/CastingCandidateDialog.vue | 3 +- .../components/IdentityEditor.vue | 32 +- .../components/IdentityImageDialog.vue | 5 +- .../subject-identity/identity.test.ts | 7 +- src/features/subject-identity/types.ts | 3 - .../subject-identity/useSubjectIdentity.ts | 1 - .../subject-images/SubjectImagesPage.vue | 36 +- .../components/GenerateImageDialog.vue | 3 +- src/features/subject-images/images.test.ts | 6 +- src/features/subject-images/prompts.test.ts | 5 +- src/features/subject-images/types.ts | 4 +- .../subject-images/useSubjectImages.ts | 2 - src/features/visual-style/VisualStylePage.vue | 18 +- .../visual-style/components/StyleEditor.vue | 81 +-- src/features/workflows/pages.test.ts | 35 +- src/lib/http.test.ts | 7 +- src/testing/naive.ts | 16 + 47 files changed, 2244 insertions(+), 260 deletions(-) create mode 100644 docs/frontend-simplification.md create mode 100644 src/components/ui/ActionMenu.test.ts create mode 100644 src/components/ui/ActionMenu.vue create mode 100644 src/components/ui/DetailDisclosure.vue create mode 100644 src/features/production/components/ModelCapabilities.vue create mode 100644 src/features/production/components/QualityDialog.vue create mode 100644 src/features/production/components/QualityResult.vue create mode 100644 src/features/production/quality-api.ts create mode 100644 src/features/production/quality.test.ts create mode 100644 src/features/production/quality.ts create mode 100644 src/features/production/quality.types.ts create mode 100644 src/features/production/useQuality.ts diff --git a/README.md b/README.md index 9b51ffd..52ee9cd 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,14 @@ 短剧 Agent 的前端工作台。支持剧本创作、拆解、视觉风格、角色选角、主体形态、分镜设计、镜头首帧与视频生产,使用真实后端 API,不包含演示数据或浏览器端模型调用。 +## 最新同步(2026-09-03) + +- 对齐后端 `dev@05bb9e8b7e0a0e85303b16eb3a2ac634e01dd7eb`:图片/视频模型由后端统一配置,前端不再发送固定 Provider。 +- 新增首帧视觉校验、有限自动修复、当前集小批量质量生成、视频抽样帧质量检查、视频修复候选与复检晋升、只读模型能力查询。默认不调用模型,付费操作需确认,结果可定位和导出。 +- 保留主要布局和业务能力,导出/执行记录/提示词等次要入口收进菜单,长介绍和高级规则按需展开;关键错误、过期、未保存与费用提示继续直显。 + +入口和边界见 [前端简化与质量功能同步](docs/frontend-simplification.md),接口覆盖见 [后端功能覆盖说明](docs/backend-coverage.md)。 + ## 启动 需要 Node.js >= 22.18 和 pnpm 11。 @@ -33,13 +41,13 @@ API_PROXY_TARGET=http://localhost:3412 | 拆解恢复 | 失败抽取重试、缺失剧集镜头补齐、分镜引用与 Form 绑定修复 | | 视觉风格 | AI 生成、人工编辑、分类 Prompt、JSON 硬约束、锁定;风格参考图地址登记、启停和删除 | | 主体身份 | 正式主体目录、单个/批量身份文本、人工编辑与锁定;无形态 Character 也可先完成身份 | -| 角色选角 | 选角就绪检查、角色身份专用批量生成、Seedream 候选小批量生成、确认演员并原子锁定 Identity | +| 角色选角 | 选角就绪检查、角色身份专用批量生成、后端图片模型 候选小批量生成、确认演员并原子锁定 Identity | | 身份母版 | 普通身份图与辅助视角、实际 Prompt 和来源追溯;Character 母版通过确认选角确定 | | 分镜检查 | 按剧集、Beat、正式 Shot ID 浏览;导演设计、环境与主体即时状态;单集覆盖率与 JSON 导出 | | 分镜生成 | Direction / VisualState 单集生成、项目批量补齐、覆盖重生成;并发和自动修复次数;部分失败诊断 | | 镜头工具 | 主体参考图与缺失原因、确定性 GenerationSpec 查询与导出、单镜头/批量视频提示词 | -| 形态图片 | 正式形态图库、主图/候选图预览、图片历史与失败记录、Seedream 单图/批量生图、切换主参考图 | -| 镜头生产 | Prompt/首帧/视频就绪诊断、Seedream 首帧候选、Seedance 任务、成片播放、主资产切换与失败重试 | +| 形态图片 | 正式形态图库、主图/候选图预览、图片历史与失败记录、后端图片模型 单图/批量生图、切换主参考图 | +| 镜头生产 | Prompt/首帧/视频就绪诊断、后端图片模型 首帧候选、后端视频模型 任务、成片播放、主资产切换与失败重试 | 尚未实现:手工编辑剧本或分镜设计(后端暂无对应写接口)、用户登录。模型输出按纯文本显示,避免执行不可信 HTML。图片和视频只接受 HTTP(S) 或后端 `/storage/` 路径。 @@ -169,15 +177,15 @@ Oxfmt 不负责代码质量,Oxlint 不负责 Vue 的完整类型推导;Vue S | POST | `/projects/:id/character-identities/generate` | 只批量生成 Character Identity 文本 | | GET | `/projects/:id/character-casting/readiness` | 角色身份、候选、Anchor 与锁定状态汇总 | | POST | `/projects/:id/character-casting/candidates/generate` | 为缺少 Anchor 的角色小批量生成停用候选 | -| GET / POST | `/subjects/:subjectId/identity/images` | 查询带 `isAnchor` 的身份图片/Seedream 生图 | +| GET / POST | `/subjects/:subjectId/identity/images` | 查询带 `isAnchor` 的身份图片/后端图片模型 生图 | | POST | `/subjects/:subjectId/identity/casting-candidates` | 单角色生成 primary 选角候选,不自动成为 Anchor | | PUT | `/subjects/:subjectId/identity/images/:imageId/anchor` | 将成功的 primary 候选设为身份母版 | | PUT | `/subjects/:subjectId/identity/images/:imageId/casting` | 确认 Character 选角:切换 Anchor 并锁定 Identity | | GET | `/projects/:id/video-prompts/readiness` | 项目视频提示词前置条件检查 | -| GET / POST | `/projects/:id/keyframes/readiness`、`/keyframes/generate` | 首帧就绪检查与批量 Seedream 生图 | +| GET / POST | `/projects/:id/keyframes/readiness`、`/keyframes/generate` | 首帧就绪检查与批量 后端图片模型 生图 | | GET / POST | `/storyboard-shots/:shotId/keyframes`、`/keyframe` | 单镜头首帧历史与生图 | | PUT | `/storyboard-shots/:shotId/keyframes/:keyframeId/primary` | 切换主首帧 | -| GET / POST | `/projects/:id/videos/readiness`、`/videos/generate` | 视频前置检查与批量 Seedance 任务 | +| GET / POST | `/projects/:id/videos/readiness`、`/videos/generate` | 视频前置检查与批量 后端视频模型 任务 | | GET | `/projects/:id/videos/status` | 项目最近视频任务状态 | | POST | `/projects/:id/videos/retry-failed` | 重试最近失败的视频任务 | | GET / POST | `/storyboard-shots/:shotId/videos` | 单镜头视频历史与任务创建 | @@ -215,7 +223,7 @@ API 层也提供 `state` 与 `breakdown/latest` 方法。页面通过共享 chec 1. 入口为 `/projects/:projectId/subject-images`;项目导航、拆解主体列表和分镜参考图缺失提示均可进入。图库自动读取后端已生成图片,不需要重新生图才能显示。 2. 生图使用正式数据库 `SubjectForm.id`。Checkpoint 的领域 `formId` 不是数据库 ID,不能直接调用图片接口。项目列表包含 Identity Anchor 及图片的 `rawJson` 追溯字段,用于识别母版过期;完整实际 Prompt 在历史弹窗读取。 3. 单图明确发送 `provider: 'seedream'`,避免后端单图入口默认使用 mock。模型和凭证仅配置在后端,前端不保存 API Key。自定义 Prompt 优先,其次正式 `generationPrompt`;缺少时后端先调用文本模型生成并保存。`appearancePrompt` 仅为原始素材,不能直接充当最终 Prompt。可在形态卡片“提示词与生成”先生成、检查与导出正式提示词;后端尚无手工编辑保存接口。 -4. 单图宽高同时留空使用后端默认 2K,自定义时必须同时提供正整数,具体尺寸限制仍由模型校验。当前 Seedream Provider 没有使用 negativePrompt,所以页面不提供无效的负向提示词输入。 +4. 单图宽高同时留空使用后端默认 2K,自定义时必须同时提供正整数,具体尺寸限制仍由模型校验。当前 后端图片模型 Provider 没有使用 negativePrompt,所以页面不提供无效的负向提示词输入。 5. 单图生成新增历史记录,不删除旧图。无主图时默认勾选“生成成功后设为主图”;已有主图时默认不替换。候选图可以在历史弹窗二次确认后通过 PUT 设为主图,只允许已完成图片。 6. 批量默认跳过已有主图的形态;勾选“已有主图也新增候选图”发送 `force: true`。后端批量 force 只生成新图,**不会替换已有主图**。批量作用于整个项目,不受类型/文字/缺图筛选影响,也不接收单图的尺寸与自定义 Prompt。 7. 图库每 6 秒读取一次真实记录,弹窗只在打开时查询该形态图片。数据库存在 pending / generating 时暂停再次生图;网络错误不自动重试有费用的请求。HTTP 200 中的批量部分失败单独展示。 @@ -309,8 +317,8 @@ server { 11. 进入“视觉风格”,保存或生成风格,检查分类 Prompt 和硬约束;锁定后 AI 入口禁用,人工编辑仍可保存。登记一张已有风格图,检查启停和移除确认。 12. 进入“主体身份”,先补齐 Character Identity,再小批量生成选角候选。确认候选不会自动成为 Anchor,人工确认演员后 Identity 同时锁定且 readiness 变为 ready。 13. 为已确认演员生成 front / three-quarter / full-body 辅助图;确认辅助图不能直接设为母版。角色需要更多演员候选时使用单角色候选入口。 -14. 对人物和场景形态分别新增图片,在图片详情检查当次母版 ID;旧主图应保持不变,手动切换后再刷新分镜参考图。道具不应显示自动继承母版已生效。 -15. 在镜头生产页生成单镜头首帧,确认规格同时列出 `identity-anchor` 与 `form-primary`。切换主首帧后再创建 Seedance 任务,等待完成后播放并选择主视频。 +14. 对人物和场景形态分别新增图片,在图片详情检查当次母版 ID;旧主图应保持不变,手动切换后再刷新分镜参考图。人物、场景、道具均应在身份已锁定时继承当前母版。 +15. 在镜头生产页生成单镜头首帧,确认规格同时列出 `identity-anchor` 与 `form-primary`。切换主首帧后再创建 后端视频模型 任务,等待完成后播放并选择主视频。 本轮新增测试覆盖风格 JSON/锁定/图片 DELETE、身份归并/前置条件/尺寸/显式参考图/母版切换、部分失败、旧响应、草稿保护与费用确认。模型调用均使用模拟 API。当前执行环境未能通过浏览器访问本地开发服务,**未完成浏览器视觉验收及真实模型联调**,请按上述步骤在本地验收。 diff --git a/docs/backend-coverage.md b/docs/backend-coverage.md index 7f492bc..15939c4 100644 --- a/docs/backend-coverage.md +++ b/docs/backend-coverage.md @@ -1,6 +1,6 @@ # 后端功能覆盖说明 -核对日期:2026-09-02。后端:`qianlanse/short-drama-agent`,`dev@537dba89a18d571a6df24dae17494b09ce635c15`。以 API 路由和实际 service/workflow 实现为依据,不只根据接口名称推断功能。本轮不修改后端。 +核对日期:2026-09-03。后端:`qianlanse/short-drama-agent`,`dev@05bb9e8b7e0a0e85303b16eb3a2ac634e01dd7eb`。以 API 路由和实际 service/workflow 实现为依据,不只根据接口名称推断功能。本轮不修改后端。 ## 页面与接口能力 @@ -25,6 +25,10 @@ | 视频任务与主视频 | 镜头生产 | 项目 videos readiness/generate/status/retry-failed,逐镜 videos,单任务 status,主视频 PUT | 保留任务提交、轮询、播放、失败重试与主视频切换;不把 HTTP 完成当成片完成 | | 高级串联生产 | 镜头生产批量面板底部(默认折叠) | POST `/projects/:id/production/start` | 新增预检、再次校验、费用确认、分阶段回执与诊断导出;明确不是全链自动制片 | +## 新增质量检查与修复 + +已接入:首帧 `validate`、`repair`,项目 `keyframes/generate-quality`,视频 `validate`/`repair`,公开图片模型能力及逐镜能力 GET。生成请求不再传固定 Provider。质量接口的详细参数、默认一镜范围、费用确认、主图晋升、三点视频抽样和会话回执限制见 [前端简化与质量功能同步](frontend-simplification.md)。首帧校验历史尚无 GET,不虚构读取接口;视频已有结果从 `rawJson.videoValidation` 读取。 + ## 高级串联生产边界 实际后端顺序:形态正式提示词 → 形态图片 → 视频提示词 → 视频任务提交。**没有视觉风格、身份、导演设计、即时状态或首帧生成节点,也不会等待成片。** @@ -32,7 +36,7 @@ - 仅剧本 `completed` 可启动;先检查项目/工作流任务、形态活动图片、当前母版引用、镜头规格、主首帧有效性和活动视频。 - 为避免中途生成主图使既有首帧失效,前端要求先确认全部形态主图并补齐有效主首帧;因此该入口通常跳过形态生图,只串联仍缺少的文本与视频任务。 - 点击预检只有 GET;付费确认后再次 GET 校验。查询失败或条件变更不提交 POST。单次项目锁仅覆盖本浏览器,不能代替服务器事务和跨客户端互斥。 -- 参数固定为 `imageProvider: seedream`、`videoProvider: seedance`。后端内部文字并发 3、图片与视频并发 2;不接受批量面板的覆盖、数量上限或尺寸配置,不应让用户误以为可小批量执行。 +- 图片与视频模型由后端统一配置,前端不传请求级 Provider。旧串联路由仍解析默认字段,但实际执行节点已改用统一配置;不据此显示虚假模型选择。后端内部文字并发 3、图片与视频并发 2,不接受普通批量面板的覆盖、上限和尺寸配置。 - 图工作流 `completed` 仅说明经过 finalize,可能仍有错误;`needsManualReview`、`stopReason` 和所有 `errors` 原样显示。视频统计用“已提交”,不标“成片已完成”。 - 此工作流没有持久 checkpoint/恢复/取消接口。网络断开不能推断后端停止,禁止自动重发。已提交的长请求回执按原项目隔离;页面刷新后需从资产状态核对。 @@ -48,7 +52,7 @@ - 图库始终读取 keyframes 与 videos 的 readiness,显示下游过期镜头数量;选择来源镜头后才查询其参考图,避免遍历全部镜头的 N+1 请求。只允许定位当前项目列表中的镜头。 - 图库深链接支持 `sourceShotId`、`subjectFormId` 和 `subjectRef`;精确形态优先,切换定位清除旧筛选,丢失的形态不自动回退其他形态。返回生产页携带 `episodeNo` 与正式 `shotId`。 - “身份主图过期”与“下游首帧过期”独立展示。素材有效而首帧过期时,只需核对后重建首帧;定位、刷新、筛选不触发生成或切换主资产。 -- 当前后端基线存在检查口径不一致风险:`video-readiness.service.ts` 与 `video-generation-compiler.service.ts` 调用 `shotKeyframeCurrentnessService.check` 时仅传形态引用,遗漏第三个当前身份母版参数。默认空列表会将快照里的身份母版视为已移除;而 `shot-keyframe-readiness.service.ts` 会正确传入当前母版。前端因此合并两种检查,并将“首帧未标过期、视频报告过期”单列为“状态待核对”,不据此判定素材失效或建议直接重复生图。本次仅修改前端,不绕过视频阻塞;后端调用链需另行修复与联调。 +- 本次后端基线已修复视频 readiness/compiler 缺少当前身份母版参数引起的过期误报。前端保留异常时的两种检查结果对照和“状态待核对”提示,不绕过后端阻塞或自动重复生图。 ## 不添加无效或重复入口 @@ -65,5 +69,5 @@ ## 回归验证 - 自动测试全部模拟 API,覆盖正式 ID/参数、提示词与图片职责分离、批量部分失败、数量和尺寸校验、候选不覆盖、未完成剧本禁用、费用确认、预检后条件变化、项目切换与旧响应隔离。 -- 已通过 lint、格式、TypeScript、Vitest(23 个文件、233 项测试)和生产构建;定位回归见 `src/features/subject-images/asset-impact.test.ts`。测试不触发真实模型或生产任务。 +- 已通过 lint、格式、TypeScript、Vitest(含质量链路及菜单/折叠交互回归)和生产构建;定位回归见 `src/features/subject-images/asset-impact.test.ts`。测试不触发真实模型或生产任务。 - 当前环境无法打开应用预览,未进行浏览器视觉验收;需在可访问的开发环境检查浅/暗主题和窄屏布局,并由用户明确批准后进行真实小批量模型联调。 diff --git a/docs/frontend-simplification.md b/docs/frontend-simplification.md new file mode 100644 index 0000000..5d5209e --- /dev/null +++ b/docs/frontend-simplification.md @@ -0,0 +1,62 @@ +# 前端简化与质量功能同步 + +核对日期:2026-09-03。后端 dev 基线:`05bb9e8b7e0a0e85303b16eb3a2ac634e01dd7eb`。本次只改前端,不更改数据库、后端配置或生成资产。 + +## 页面保留什么、收起什么 + +沿用现有路由、左侧导航、剧集/主体/镜头目录、详情分栏和灰阶主题,不另建工作台。主操作、正式内容和阻塞提示直接显示;低频入口采用 Naive UI 菜单、折叠区和弹窗。 + +| 工作区 | 默认显示 | 按需查看 | +| --- | --- | --- | +| 剧本创作 | 正文、角色/世界观/审核标签、进入拆解 | Tabs suffix 的更多菜单:执行记录、导出 | +| 剧本拆解 | 主体、形态、分镜、任务与失败提醒 | 更多菜单:执行记录、JSON 导出;长主体介绍可展开 | +| 视觉风格 | 名称、整体风格、锁定、保存和 AI 生成 | 分类风格、硬约束、生成规则;收起不丢失草稿 | +| 主体身份 | 身份编辑、锁定、选角、母版及图片 | 身份填写规则;批量功能仍在原抽屉 | +| 形态图片 | 图片、主图状态、看图与生成 | 卡片更多菜单:提示词、主体身份与母版;批量配置在抽屉 | +| 分镜设计 | 剧集、目录、设计与状态 | 更多菜单导出本集,原抽屉生成与恢复 | +| 镜头生产 | 提示词、首帧、视频和主要操作 | 资产更多菜单查看生成规格;质量检查打开独立面板,细参数/逐轮回执可展开 | + +少边框、无圆角的布局保持不变;浅色 Tabs 分隔线仍为 `#dadada`。新增刷新与更多按钮为正方形并保留可访问名称;规则折叠标题使用可键盘激活的按钮。摘要只改变显示,不截断底层内容或导出结果。 + +不收起:读取失败、任务失败、素材过期、未保存修改、关键禁用条件、费用说明、主图替换后果。原来的素材精确定位/返回原镜头功能保留。重复分镜前置问题只在对应生产阶段显示。 + +## 与新后端同步 + +### 模型配置 + +图片与视频实际使用后端 `IMAGE_*`、`VIDEO_*` 配置。身份/选角/形态/首帧/视频请求不再发送固定 `provider`;串联生产也不再传固定 `imageProvider`、`videoProvider`。旧串联路由仍保留默认字段,但新执行节点不再消费这些请求级模型值,因此界面不能把它们当实际模型展示。 + +质量面板或资产规格弹窗可按需读取 `/image-providers/capabilities` 和当前镜头的 `keyframe-provider-capability`。此入口只读,不提供会被后端忽略的“前端切换模型”。未声明参考图上限显示“上限未声明”,不声称无限制。 + +### 质量入口 + +| 入口 | 实际 API | 结果与边界 | +| --- | --- | --- | +| 已完成首帧 → 质量检查 → 仅校验 | POST `/storyboard-shots/:shotId/keyframes/:keyframeId/validate` | 调用视觉模型,不重生成、不切换主首帧 | +| 首帧 → 质量检查 → 校验并修复 | POST 同路径 `/repair` | 有限重生成;新候选通过才晋升主首帧;原图直接通过不会自动晋升 | +| 生产工具栏 → 首帧质量生成 | POST `/projects/:projectId/keyframes/generate-quality` | 当前集、默认最多一镜、并发 1、最多修复 1 次;每镜候选通过后才设主图 | +| 已完成视频 → 质量检查 | POST `/storyboard-shots/:shotId/videos/:videoId/validate` | 三点抽帧视觉检查,不检测音频/口型/完整连续运动;普通候选不切换主视频,修复候选通过后自动晋升 | + +批量质量配置与普通批量面板独立。后端非 force 先根据全项目是否有过期项决定筛选,再筛当前集;因此其他集存在过期项而当前集无过期项时,本集不会先补缺失首帧。前端明确提示切换剧集,不悄悄扩大范围。 + +允许文字按行输入并清理空白/重复;未填写不自行授权文字。宽高同时留空或填写正整数,修复次数可为 0。确认区显示本次最多生图/视觉校验调用次数,修改参数或目标后撤销费用确认。 + +### 视频修复与复检晋升(本轮后端追加更新) + +视频质量面板新增“生成修复候选”,使用 POST `/storyboard-shots/:shotId/videos/:videoId/repair`,只接受 `maxRepairAttempts`(默认 2,正整数)。该参数是修复链的上限,不是本次自动生成次数。本次只提交一个视频候选,不自动等待、复检或循环重试;修复约束和允许文字沿用已保存的失败校验。 + +确认后重新读取视频,未校验、校验已通过、链路到达上限或镜头有活动视频时不提交。候选完成后仍不是主视频,需要对新候选显式执行视觉校验;后端通过后自动晋升,界面在付费确认前说明后果,并显示实际晋升结果。未通过复检的修复候选不提供普通“设为主视频”按钮,不能绕过验收。 + +### 状态与风险控制 + +- 打开页面、菜单、结果和模型能力不会自动调用生成/视觉模型。质量 POST 只在确认后发生,不自动超时重试。 +- 确认后再次 GET 项目完成状态、活动任务、项目镜头列表与目标资产归属。视频必须有完成状态、地址和有效时长;首帧必须完成且有图片。修复和小批量质量生成先检查模型参考图能力,批次检查当前镜头是否仍在生图。 +- 预检期间关闭面板或换目标不发送 POST。已提交请求继续运行,回执按原项目/镜头/资产保存,不能写入新项目。当前浏览器项目互斥不能代替后端跨客户端锁;仍需确认外部任务停止。 +- 结果分别显示视觉通过、修复未通过、调用失败、未执行;不会把 HTTP 200 当质量通过。批量回执提供定位镜头,完整主体检查/逐轮耗时/原始回执可展开和导出。 +- 视频历史结果直接从 `rawJson.videoValidation` 读取,不为“查看结果”重新调用视觉模型。它是历史抽样结论,不保证当前引用仍一致。 +- 后端持久化首帧校验,但尚无历史读取 GET。前端只显示当前会话确实收到的首帧回执并支持导出,不虚构历史接口。 +- 后端已修复视频校验漏传身份母版导致的过期误报。前端保留两种就绪结果冲突时的防御性提示,不绕过后端阻塞、不自动重复生成。 + +## 验证边界 + +单元与组件测试全部使用模拟 API,验证参数、正式 ID、禁用、费用确认、范围、归属、回执隔离和菜单访问,不触发真实模型任务。浏览器视觉与实际模型联调需要在可访问的开发环境进行;不将 DOM 测试视为视觉验收。 diff --git a/src/admin.css b/src/admin.css index d191e88..0cabdb5 100644 --- a/src/admin.css +++ b/src/admin.css @@ -1472,6 +1472,51 @@ body { .form-image-card-focused { box-shadow: inset 3px 0 0 var(--app-accent-text); } +/* 低频记录收起后归还正文宽度,不留下历史列的占位。 */ +.breakdown-results-section > .content-with-history.history-hidden { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr); +} +/* 补充说明只按需展开,继续使用平直背景层级,不增加边框或圆角。 */ +.detail-disclosure.n-collapse { + font-size: 12px; + color: var(--app-muted); +} +.detail-disclosure-content { + padding: 12px 14px; + background: var(--app-subtle); + color: var(--app-ink); + line-height: 1.8; +} +.quality-fields { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); + align-items: start; + gap: 16px; +} +.quality-result { + margin-top: 24px; + padding: 16px; + background: var(--app-subtle); +} +.quality-result-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 12px; + margin-top: 10px; + padding: 12px; + background: var(--app-control); + font-size: 12px; + overflow-wrap: anywhere; +} +.quality-json { + margin-top: 16px; + white-space: pre-wrap; + overflow-wrap: anywhere; + font-size: 11px; + line-height: 1.7; +} .select-control { min-width: 130px; } diff --git a/src/components/ui/ActionMenu.test.ts b/src/components/ui/ActionMenu.test.ts new file mode 100644 index 0000000..90f9bb8 --- /dev/null +++ b/src/components/ui/ActionMenu.test.ts @@ -0,0 +1,90 @@ +import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { NInput, NDropdown } from 'naive-ui' +import ActionMenu from './ActionMenu.vue' +import DetailDisclosure from './DetailDisclosure.vue' +import StyleEditor from '../../features/visual-style/components/StyleEditor.vue' +import SubjectList from '../../features/breakdown/components/SubjectList.vue' +import { selectMenu } from '../../testing/naive' +import type { SubjectCandidate } from '../../features/breakdown/types' + +let wrapper: VueWrapper | undefined +afterEach(() => { + wrapper?.unmount() + wrapper = undefined + document.body.innerHTML = '' + vi.unstubAllGlobals() +}) + +describe('精简入口保留功能', () => { + it('菜单按点击展开,禁用项不能绕过,触发器有可访问名称', async () => { + wrapper = mount(ActionMenu, { + attachTo: document.body, + props: { + label: '测试更多操作', + items: [ + { key: 'history', label: '执行记录' }, + { key: 'export', label: '导出', disabled: true } + ] + } + }) + expect(wrapper.get('button').attributes('aria-haspopup')).toBe('menu') + expect(wrapper.get('button').attributes('aria-expanded')).toBe('false') + expect(document.querySelector('.n-dropdown-option-body')).toBeNull() + await selectMenu('测试更多操作', '执行记录') + expect(wrapper.emitted('select')).toEqual([['history']]) + wrapper.getComponent(NDropdown).vm.$emit('select', 'export') + expect(wrapper.emitted('select')).toHaveLength(1) + }) + + it('补充说明默认不展开,键盘可聚焦的标题可查看原文', async () => { + wrapper = mount(DetailDisclosure, { props: { title: '规则说明' }, slots: { default: '完整规则仍然保留' } }) + expect(wrapper.text()).not.toContain('完整规则仍然保留') + await wrapper.get('.n-collapse-item__header-main').trigger('click') + await flushPromises() + expect(wrapper.text()).toContain('完整规则仍然保留') + }) + + it('风格高级字段收起不丢失保存值,展开后仍可修改', async () => { + wrapper = mount(StyleEditor, { props: { visualStyle: null, disabled: false } }) + expect(wrapper.find('#style-constraints').exists()).toBe(false) + await wrapper.get('.n-collapse-item__header-main').trigger('click') + await flushPromises() + await wrapper.get('#style-constraints').setValue('["保留硬约束"]') + const fields = wrapper.findAllComponents(NInput) + fields.find(item => item.props('placeholder')?.includes('人物质感'))!.vm.$emit('update:value', '人物风格') + await flushPromises() + await wrapper.get('.n-collapse-item__header-main').trigger('click') + await wrapper.get('form').trigger('submit') + expect(wrapper.emitted('save')).toMatchObject([ + [{ characterPrompt: '人物风格', hardConstraints: ['保留硬约束'] }] + ]) + }) + + it('主体长文收起仅影响显示,完整内容与图片入口不丢失', async () => { + const description = '主体完整经历。'.repeat(30) + wrapper = mount(SubjectList, { + props: { + projectId: 'p', + subjects: [ + { + profileId: 'p1', + ref: '@CH0001', + name: '人物', + module: 'character', + description, + appearance_prompt: '外观', + aliases: [] + } as SubjectCandidate + ], + forms: [] + }, + global: { stubs: { RouterLink: true } } + }) + const text = wrapper.get('.line-clamp-3') + expect(text.text()).toBe(description) + await wrapper.get('button').trigger('click') + expect(wrapper.find('.line-clamp-3').exists()).toBe(false) + expect(wrapper.findAll('router-link-stub')).toHaveLength(2) + }) +}) diff --git a/src/components/ui/ActionMenu.vue b/src/components/ui/ActionMenu.vue new file mode 100644 index 0000000..e1ba72e --- /dev/null +++ b/src/components/ui/ActionMenu.vue @@ -0,0 +1,32 @@ + + diff --git a/src/components/ui/DetailDisclosure.vue b/src/components/ui/DetailDisclosure.vue new file mode 100644 index 0000000..b75340c --- /dev/null +++ b/src/components/ui/DetailDisclosure.vue @@ -0,0 +1,23 @@ + + diff --git a/src/features/breakdown/BreakdownPage.test.ts b/src/features/breakdown/BreakdownPage.test.ts index 5d16167..eaae85b 100644 --- a/src/features/breakdown/BreakdownPage.test.ts +++ b/src/features/breakdown/BreakdownPage.test.ts @@ -8,6 +8,7 @@ import { projectContextKey, type useProjectData } from '../projects/context' import type { ProjectDetail } from '../projects/types' import type { Checkpoint } from '../workflows/types' import type { BreakdownModule, EpisodePlan } from './types' +import { selectMenu } from '../../testing/naive' let wrapper: VueWrapper | undefined afterEach(() => { @@ -195,6 +196,8 @@ describe('拆解页内容滚动', () => { it.each(['人物', '场景', '道具'])('%s 使用共用固定斑马纹样式,搜索后保持列表条目结构', async label => { mountPage() await selectTab(label) + expect(wrapper!.find('.history-panel').exists()).toBe(false) + await selectMenu('拆解更多操作', '执行记录') const list = wrapper!.get('.subject-record-list') expect(list.classes()).toContain('record-list') expect(list.findAll(':scope > article')).toHaveLength(30) @@ -364,6 +367,7 @@ describe('拆解页内容滚动', () => { ])('%s 的末项始终放在独立结果滚动容器内', async (label, lastItem) => { mountPage() await selectTab(label) + await selectMenu('拆解更多操作', '执行记录') expect(wrapper!.find('.workspace-scroll').exists()).toBe(false) const results = wrapper!.get('.breakdown-results-scroll .n-scrollbar-container') expect(results.text()).toContain(lastItem) @@ -378,6 +382,7 @@ describe('拆解页内容滚动', () => { const fetcher = vi.fn() vi.stubGlobal('fetch', fetcher) const provided = mountPage() + await selectMenu('拆解更多操作', '执行记录') const results = wrapper!.get('.breakdown-results-scroll .n-scrollbar-container').element const history = wrapper!.get('.history-panel .n-scrollbar-container').element results.scrollTop = 900 diff --git a/src/features/breakdown/BreakdownPage.vue b/src/features/breakdown/BreakdownPage.vue index da0c281..0708fb1 100644 --- a/src/features/breakdown/BreakdownPage.vue +++ b/src/features/breakdown/BreakdownPage.vue @@ -1,8 +1,9 @@ + diff --git a/src/features/production/components/ProductionReceipt.vue b/src/features/production/components/ProductionReceipt.vue index 8a457fb..b209e2c 100644 --- a/src/features/production/components/ProductionReceipt.vue +++ b/src/features/production/components/ProductionReceipt.vue @@ -47,7 +47,7 @@ function exportReceipt() { :show-icon="false" class="mt-3" > - 视频任务已经创建,但尚未代表成片完成。后台会继续查询 Seedance,以下方任务状态和视频资产为准。 + 视频任务已经创建,但尚未代表成片完成。后台会继续查询 视频模型,以下方任务状态和视频资产为准。 本次存在部分失败,成功提交或生成的结果不会回滚。请查看逐镜错误后再重试。 diff --git a/src/features/production/components/QualityDialog.vue b/src/features/production/components/QualityDialog.vue new file mode 100644 index 0000000..0042126 --- /dev/null +++ b/src/features/production/components/QualityDialog.vue @@ -0,0 +1,261 @@ + + + diff --git a/src/features/production/components/QualityResult.vue b/src/features/production/components/QualityResult.vue new file mode 100644 index 0000000..af9ea48 --- /dev/null +++ b/src/features/production/components/QualityResult.vue @@ -0,0 +1,142 @@ + + + diff --git a/src/features/production/components/ShotProductionAssets.vue b/src/features/production/components/ShotProductionAssets.vue index a51bf9c..0d39099 100644 --- a/src/features/production/components/ShotProductionAssets.vue +++ b/src/features/production/components/ShotProductionAssets.vue @@ -1,7 +1,7 @@