feat: integrate Jellyfish production pipeline - #41
Draft
graspyourdream-sudo wants to merge 61 commits into
Draft
graspyourdream-sudo wants to merge 61 commits into
graspyourdream-sudo wants to merge 61 commits into
Conversation
第一部分:让项目主流程按顺序走通,并让所有生成入口给出真实、可理解的反馈。 一、统一步骤判定与导航 - resolveProjectStep 成为「下一步」的唯一权威:删除「已在本步则回退执行章节级 primaryCta」的隐式跳转,修复按钮写着「继续:准备资产图片」却跳进分镜工作室, 从而跳过 图片准备 → 整集提示词 → 关联绑定 的问题 - 判定完成前只显示「正在判断项目进度」,不再用空快照得出「还没有创建任何章节」 - 分镜完成后的下一步统一改为「资产提取」(工作台 CTA、章节行、分镜页、镜头编辑页), 分镜工作室降级为「单镜查看与补漏」次入口 - 项目列表默认 created_at 倒序,并在 ProjectRead 下发 created_at/updated_at: 修复「最新项目排在最下面」、卡片时间全是「打开页面那一刻」;「创建时间」不再比较 id - 新建/编辑项目隐藏无消费方的「全局种子值」(数据库字段保留,不做迁移) 二、第 2/3 步真正可操作 - 第 2 步新增「开始提取 / 重新提取」+ 候选预览 + 确认写入/忽略,全部复用既有 提取、existence-check、就地新建实体、候选关联端点,未新建任何提取后端 - 点击前显示「能力已配置,当前被演练门禁(DRY_RUN)阻止」与已配置模型, 明确「被门禁拦住 ≠ 接口没接通」 - 第 3 步资产行新增「生成 / 编辑 / 查看定版图」;生成直接进资产编辑页并自动打开 出图确认弹窗(复用既有 image-pipeline 与资产编辑器) - 资产编辑页补项目作用域守卫:缺作用域时禁用出图并提供项目下拉选择, 不再出现 status=unknown - useGenerationDraft 不再把异常吞成 null,页面显示真实原因 三、生成回执与提示词导入死路 - 新增统一五态口径(ready / dry_run / not_configured / missing_params / service_error / running)与错误分类,清理裸 Generic Error 字符串 - 删除假的「测试连接 / 测试生成 / 快速测试」,改为真实可核对的「检查配置」 - 修复视频生成把 fileId 当 taskId 轮询导致的永久 pending;区分「被门禁阻止」 与「缺少任务 ID」 - 提示词导入页遇到无章节项目时显示「该项目还没有章节」并提供内联「新建章节」, 创建后自动选中并恢复预览按钮 验收:全程 DRY_RUN,页面真实点击复核;tsc --noEmit 与 vite build 通过; 后端 606 collected / 23 failed —— 与 HEAD 基线逐条一致(既有环境基线,非本次回归), 新增 4 条排序回归测试通过。
针对第一部分验收提出的 5 条收口,逐条修复。 1. 生成门禁状态的真实性 - DRY_RUN 与模型配置**分开判断**:先判模型配置,再判门禁; DRY_RUN 开着不再显示「能力已配置」 - 模型为空 → 「模型未配置」;模型已配置 + DRY_RUN 开 → 「模型已配置,当前被演练门禁阻止」 - 图片/视频出口不再复用文本模型状态:各自读 default_image_model_id / default_video_model_id,并回查模型表(id 是否存在、类别是否匹配)与供应商表 (供应商是否存在、是否停用);查不到一律显示「配置状态无法确认」,不显示已配置 - 新增纯逻辑模块 generationStatusCore.ts(无 React 依赖,可单测)+ generationGate.ts 取数 2. 错误分类 - 不再把所有 409 归为 DRY_RUN:只有错误码为 paid_outlet_blocked,或正文明确包含 DRY_RUN 门禁信息([DRY_RUN]…拦截 / JELLYFISH_DRY_RUN 开关)才算门禁 - 其余 409 → 新增 conflict 状态「业务冲突(HTTP 409,不是演练门禁)」并显示后端真实原因 - 新增 22 条前端回归测试覆盖上述两类 409 与各状态分支(node --test,无新依赖) 3. 项目作用域选择的后续行为 - 页面顶部选择项目只设置作用域并启用出图,**不再自动发起批量生成** - 从具体生成动作触发时只恢复那一个动作(单张生成 / 批量出图) - 单张生成显式使用刚选中的 project_id(activeGenerateProjectIdRef),不依赖未刷新的 state - 项目下拉改为分页取全量,可搜索并选择第一页之外的旧项目 - 出图确认弹窗新增「出图目标:项目作用域 … · 资产 …」一行,落库前可核对 4. 内联新建章节序号 - 抽出 nextChapterIndex():现有章节**最大 index + 1**(不是数量 + 1) - PromptFlowPage 与 ChaptersTab 统一使用;回归用例覆盖 [1,3] → 4 5. 恢复可信测试基线 - 新增 backend/scripts/init_test_db.py:用当前 ORM 建全量表结构 + 最小可用配置种子 - 干净临时库全量:HEAD(28b933a) 589 passed / 13 failed(与 PR 记录基线一致); 本次 593 passed / 13 failed,失败集合逐条一致 → **新增失败 0** 验收:全程 DRY_RUN;页面完整点击「全局场景资产 → 搜索选择旧项目 → 确认 → 单张生成 → 确认弹窗显示刚选中的项目作用域 → DRY_RUN 正确拦截」;前端 typecheck / build / 22 条测试通过。
问题:`backend/scripts/init_test_db.py` 原本可以接收任意数据库路径,并会写入假供应商、 假模型、修改 ModelSettings 默认模型。误传正式 `backend/jellyfish.db` 时会在正式数据上执行。 防护规则: 1. 只创建**不存在**的目标:文件已存在 → 拒绝(不覆盖、不删除、不改名备份); 2. 拒绝正式库:任何以 `jellyfish.db` 命名的目标,以及仓库内 `backend/jellyfish.db` 路径; 3. 拒绝仓库工作树内的任何路径(测试库不应写在仓库里,建议 /tmp); 4. 安全校验是纯函数 `resolve_safe_target()`:只读文件系统存在性,不做任何写操作; 拒绝发生在建表 / 写供应商 / 改默认模型**之前**,退出码 2 并打印明确原因。 回归测试(backend/tests/test_init_test_db_script.py,9 条): - 新的临时库可正常初始化(表数量、供应商、模型、默认模型设置均符合预期); - 已存在的库被拒绝,且文件 SHA-256 与内容(表集合 / 供应商行)一字不变; - 重复运行同一路径被拒绝,文件哈希不变; - `jellyfish.db` 命名(任意目录)被拒绝且不会创建文件; - 仓库内 `backend/jellyfish.db` 与仓库内其他路径被拒绝,且不会创建文件; - 「拒绝发生在任何写操作之前」:预置用户改动(改过的供应商名与自定义默认文本模型) 在拒绝后原样保留,没有被脚本改回种子值; - `resolve_safe_target()` 对已存在路径只抛错,不产生副作用。 验证:用新临时文件全量跑后端基线 615 collected / 602 passed / 13 failed (602 = 上一提交的 593 + 本次新增 9 条),13 条失败与 HEAD 基线逐条一致 → 新增失败 0。
一、项目支持两种起点(新建项目向导) - 新建项目先选生产方式:**从剧本开始**(默认)/ **从视频提示词开始**; 再选**项目整体风格**:真人竖屏 / 真人横屏 / 2D / 3D / 其他自定义。 - 整体风格映射到既有列(`visual_style` + `style` + `default_video_ratio`), 例如「真人竖屏」= 现实 + 真人都市 + 9:16 —— 提示词与出图继续复用这三列,无需新字段。 - 新增 `projects.start_mode`(script / prompts):提示词起步的项目创建时**自动建默认章节**, 并直接落在「整集视频提示词」看板;剧本起步落在第 1 步。 - 章节内容支持导入 TXT / MD / DOCX:新增纯解析端点 `POST /studio/documents/parse` (标准库解 DOCX,不落盘、不写库、不上传对象存储、无付费守卫); 旧版 `.doc` 明确返回「请另存为 DOCX」,不静默失败。 - 「全局种子值」核对结果:项目级 seed **没有任何读取者**(前端也不写),保持隐藏; 单次请求级 seed 链路完整,与本项无关。 - 拆镜后的「下一步」全部指向第 2 步「资产准备」,不再提前进分镜工作室。 二、资产准备成为一条连续流程 - 用户看到的五步:**剧本与分镜 / 资产准备 / 整集视频提示词 / 资产与声音绑定 / 生成与交付**。 内部仍是 6 个 key,第 2 步由 `extract_assets` + `image_prep` 共同构成; 两个内部 key 都渲染同一屏:提取候选 → 审核 → 关联库资产或新建 → 图片提示词 → 上传/生成图片 → 查看定版图,全部在同一步完成(旧深链 `?step=image_prep` 仍然可用)。 - 步骤条、摘要条、顶部「当前流程位置」全部改用五步口径(`DISPLAY_STEPS`)。 三、集级提示词看板补齐起点能力(复用,未重建) - 看板新增两个业务出口:**继续准备资产 →** 与 **已有资产,直接进入绑定 →**。 - 新增「创建缺失镜头(N 个)并重新匹配」:提示词起步的项目先导入再建镜头, 建完自动重解析匹配,最后仍由用户点「确认保存」写库。 四、迁移与回滚(对称) - `start_mode` 加入共享列清单 `scripts/_llm_pipeline_columns.py`(迁移与回滚同源), DDL 为 `VARCHAR(16) NOT NULL DEFAULT 'script'`:**旧行自动回填 script**,老项目行为不变。 - `ProjectRead` 对 `start_mode=None` 做容错(迁移前写入 / 未 flush 的对象都按 script 处理)。 - 迁移测试 fixture 补上 `projects` 表并按清单动态计数,不再写死 13 列。 测试:后端 635 collected / 622 passed / 13 failed(= HEAD 既有 13 条基线,失败集合逐条一致, 新增失败 0);新增 10 条起点/迁移回滚测试 + 10 条文档解析测试; 前端 `tsc --noEmit` 通过、`vite build` 通过、`npm test` 31 passed。
一、把「从视频提示词开始」跑通(页面连续链路) - 导入抽屉补稳定测试标识(`data-testid`:来源页签 / 文本域 / 解析 / 建镜 / 保存) 与可访问名称,自动化可稳定定位;此前定位不到文本框的根因是**没有先切到「其他外部平台」页签**, 现在页签与文本域都能唯一定位。 - 修复死锁:本集还没有镜头时后端 `import-parse` 会拒绝解析(「该集还没有镜头,无法导入」), 导致「创建缺失镜头」按钮永远不出现。现在按钮按**粘贴文本段数**估算目标镜头数, 解析前也能补建,建完自动重新解析匹配。 - 建镜改为幂等:以数据库现状为准计算缺口、`id` 必填字段补齐、加入在途闸门 (状态之外的同步 ref)防连点、单个失败不中断并提示可再次点击补齐,避免重复镜头与序号冲突。 - 抽查:导入 8 条 → 解析 → 建镜(8×201,index 1..8 按顺序)→ 预览表立即显示 `S001..S008 / 镜头内容 / 外部导入 / number / 匹配正常` → 确认保存 200 → 刷新后仍在(DB:shots=8、with_prompt=8、source=external_import、index 1..8)。 二、项目角色直接复用全局演员库 - 新建/编辑角色弹窗新增「从演员库选择」:服务端搜索 + 分页 + 缩略图, 并标出「已在本项目 / 尚未关联本项目」,不必先去「项目演员」页关联再回来。 - 选中即写入 `Character.actor_id`,并复用既有 `POST /shot-links/actor` 的 upsert 保证项目关联存在 (重复选择不会建重复关系);角色名称/描述/服装仍是项目自己的字段; 演员的定版图与文件继续来自全局资产,不复制文件、不新建另一套演员数据。 - 弹窗内明确提示「项目角色」与「关联演员」是两种对象。 - 未新增角色全局表、未修改 `Character.project_id`。 三、资产准备的真实业务状态(不新增状态列) - 新增纯逻辑 `assetPrepStatus.ts`:按现有候选/关联/`image_prompts`/图片表/`is_primary` 计算五态:待确认 → 已关联待完善提示词 → 提示词已就绪待出图或上传 → 已有图片待设为定版 → 已定版, 每个状态给出**唯一明确的下一步**按钮文案。 - 资产行新增「业务状态」列(状态 + 下一步),面板顶部显示各状态数量与 `已定版 N/M`。 - 步骤判定收紧:资产准备必须**全部资产都已定版**才算就绪(此前只要有一项定版图就可能被判完成); 定版状态无法判定时不阻塞,也不谎报「已定版」。 - 定版状态来源:现有列表载荷不暴露 `is_primary`,改为按资产有界并发(≤24)查图片表, 拿不到置 null 并计入 `failedSources`。 四、用户可见文案统一为五步 - 清理可见的旧「第 3/5 步」「六步流程」文案(`ShotProductionCard`、`ProjectImagePrepPanel`、 `ProjectDevInfo`、`AssetEditPageBase`、`ChapterShotEditPage`);注释未专门清理。 - 修复章节「新建章节」弹窗存在两份实现、只有一份带导入入口的问题(现在两份都可导入 TXT/MD/DOCX)。 测试:前端 `tsc --noEmit` 通过、`vite build` 通过、`npm test` 43 passed(新增 12 条资产准备状态测试)。
一、角色引用演员:不在前端提前写关联
- 「从演员库选择」现在**只把 actor_id 写进表单**(用户可能取消创建,不该留下关联行)。
- 创建/更新角色时由后端在同一事务内幂等确保 `ProjectActorLink`
(`entity_crud.py::_ensure_character_actor_link` → `upsert_project_link`):
重复引用同一位演员只保留一条关联;任何异常都让整个请求回滚(角色与关联要么都成功、要么都不落库)。
- 修掉一个真实 UI 缺陷:演员库弹窗层级低于角色弹窗导致「选择」按钮点不动(`zIndex={1200}`)。
- 新增 5 条后端测试:创建即建关联 / 重复选择不重复 / 换演员补新关联(非破坏性)/ 失败整体回滚 / 不选演员不建关联。
二、定版状态检查全部资产 + 每种状态一个主操作
- 去掉“只查前 24 个资产”的截断:`collectPrimaryLookupTargets` 返回**全部**资产,
改为分批并发(每批 8 个)控制请求量;第 25 个及以后的资产同样参与就绪判定。
- 资产行操作列收敛为**唯一主操作**(按状态切换:确认写入 / 填提示词 / 生成图片 / 设为定版 / 查看定版图),
其余入口收进「更多」下拉;新增内联「设为定版」(复用实体图片 PATCH,写 `is_primary`)。
- 顶部统计与步骤判定共用同一口径:全部资产都已定版才显示「已就绪」并越过资产准备。
三、测试
- 新增「第 25 个及以后资产参与就绪判定」测试:`collectPrimaryLookupTargets(30)` 不截断;
30 个里第 30 个没定版 → `allDone=false`;`resolveProjectStep` 在 30 个资产只差 1 个或差 6 个定版时
仍停在资产准备;恰好差第 25 个也要拦住;定版状态无法判定时不阻塞。
- 前端 `npm test` 52 passed(较上轮 +9)、`tsc --noEmit` 通过、`vite build` 通过。
- 后端 640 collected / 627 passed / 13 failed,失败集合仍是既有 13 条基线(新增失败 0)。
一、根因(本轮唯一问题)
「资产准备」此前把状态判定建立在**偶然暴露的字段**上:角色走实体列表(读模型有
`image_prompts`),场景/道具/服装走 `project_scene_links` / `project_prop_links` 关联行
(读模型从来没有 `image_prompts`)。于是出现分裂:
**在资产准备页保存了场景提示词,返回后仍然显示「待完善提示词」**,
后面「上传图片 → 设为定版」两个状态也就推不动。
二、统一数据源(新增批量 readiness 端点;不新增数据库列,不加 alias/note)
- `app/services/studio/project_asset_readiness.py`:四类资产走**同一套口径**——
实体(name / `image_prompts`)+ 图片表(`file_id` 非空的图、其中的 `is_primary`)+ 提取候选
(本项目内同类型、同名且仍为 `pending`)。每类资产固定 3 条查询,不随资产数量增长;不截断。
- `GET /api/v1/studio/projects/{project_id}/asset-readiness`:逐项返回
`asset_type / asset_id / name / has_pending_candidate / has_image_prompt / has_image / has_primary`,
外加页面「设为定版 / 查看定版图」需要的 `thumbnail / image_id`,并附 `summary` 汇总(含 done/all_done)。
- 实体读模型补下发 `image_prompts`(`entity_crud._asset_read_payload`):填写提示词的编辑弹窗
要按现存槽位预填,这是同一链路上必需的一环。
- 空槽位(`file_id` 为空、资产编辑页自动补出的行)**不算已有图、也不算已定版**,
否则页面会跳过「上传图片」直接宣称「已定版」。
三、页面(资产表格 / 顶部统计 / 步骤判定读同一份清单)
- `useProjectStepSignals`:一次 readiness 请求替掉「角色列表 + 关联行拼资产 + 逐资产查详情与图片表」;
关联行只保留「镜头级关联」判定用途,不再用「关联行是否带 `image_prompts`」推状态。
- `assetPrepInputFromReadiness` 成为唯一映射,表格每行与顶部统计共用;
「图片提示词」列不再出现「无法判定」,并删掉为场景/道具/服装补抓名称的历史补丁。
- 状态可连续推进:已关联待完善提示词 → 提示词已就绪待出图或上传 → 已有图片待设为定版 → 已定版。
四、验证(DRY_RUN 全程守闸,未触发真实大模型 / 出图 / 出视频)
- 后端新增 8 条测试:四类同口径、空槽位不算图/不算定版、待确认候选只按同类型同名生效、
summary 与逐项标志一致、镜头级关联也算项目资产、别的项目的同名候选不影响、未知项目 404。
- 前端新增 4 条测试:用运行中后端抓下的**真实 readiness 载荷**逐行核对状态、
顶部统计与后端 `summary` 同值、四档推进且「已定版」数量 +1。
- 页面连续操作实测(同一场景):新建并关联 → 填提示词(PATCH 200,toast「已保存 1 个槽位」)
→ 上传本地 PNG(`POST /api/v1/studio/files/upload` **201**,随后 PATCH 图片 200 写入 `file_id`)
→ 设为定版(PATCH `is_primary=true` 200),状态逐级显示为
「已关联,待完善图片提示词」→「提示词已就绪,待出图或上传」→「已有图片,待设为定版」→「已定版」,
顶部「已定版」数量 5/7 → 6/7。
- 上传产物:OSS 公网**匿名**读取 200(image/png,199B);`head_object` 回读
`key=jellyfish/acceptance/files/b_upload_final.png size=199 type=image/png`。
只读核对:`scenes.image_prompts` 已写入、`scene_images` 行 `file_id` 非空且 `is_primary=1`、`files` 行齐全。
- 工程门禁:前端 typecheck / build 通过、`node --test` 56 条全通过;
后端全量 `pytest -q` → 24 failed / 624 passed,**与 HEAD 32d915f 的 24 条失败逐条一致(无新增)**,
新增的 8 条全部通过。
一、布局重排(第三部分要求一) - 三栏**相邻**:左=镜头列表(编号 + 标题 + 可执行业务状态),中=当前镜头的生产操作区(占主要空间), 右=缩小后的画面预览(默认展开、380px,能看清即可)。 - 实测(全新上下文、无本地偏好):左 320 / 中 810 / 右 380,x 顺序为 左 < 中 < 右,中间最宽。 - 删掉「覆盖模式」抽屉与「属性面板模式」设置;预览显示开关改名为「画面预览(P / Ctrl+B)」。 二、合并重复功能为一个「本镜生产」区域(要求二) 原来「手动分镜」(视频提示词 / 镜头语言 / 氛围 / 确认诊断页签)、「视频准备」(准备度面板)、 「分镜生产卡」(`ShotProductionCard`)三处各有一套提示词编辑、绑定查看、帧查看与生成入口。 现在收敛成**一个区域、按顺序七块**(步骤决定默认展开哪几块,切换镜头不动展开状态): ① 已保存的视频提示词与来源 ② 单镜编辑 / 重新生成 / 保存(镜头语言、氛围描述折叠在内) ③ 当前绑定的人物 / 场景 / 道具 / 服装与声音(编辑入口=「资产与参考帧」:绑定推荐 + 声音 + 关键帧) ④ 本次请求实际使用的首帧 / 关键帧 / 尾帧(只读回显真正会发出去的文件) ⑤ 本镜还缺什么 ⑥ 生成视频(参考方式 / 画幅 / 时长 / 模型方案 + 预检 + 单一生成入口) ⑦ 导出绑定提示词 - 另有两条折叠:**对白与镜头内容**(有对白时才出现,不再是独立主区域)与**技术详情**(默认收起)。 - 「维护设置」移出日常页签:改由工作区底部「高级设置」弹窗承载(改标题 / 备注 / 隐藏 / 删除)。 - **删掉工作室里的整集批量入口**:多选工具条不再有「批量生成」「批量视频准备度」, 批量关键帧生成的菜单项与相关代码一并移除;整集生成 / 导入仍在项目第 3 步。 - 生成判断只有一份:新增 `useShotRequestPlan`(预检 / 供应商帧可用性 / DRY_RUN 守卫 / 提交 / 落库), 旧 `ShotProductionCard`(488 行)删除,避免第二套判断。 三、绑定与参考帧(要求三) - 默认勾选规则收敛到 `bindingRecommendationRules`:**预选 + 未绑定**才默认勾选; 冲突 / 需复核 / 已丢弃一律要用户判断;新增「确认全部推荐(N)」一键写入(没有可直接确认项时给出明确说明)。 - 绑定建议表不再把 `asset_id` 当资产名显示(改用候选清单里的名字)。 - 保存后立即回读准备状态并重新推荐(`already_bound` 反映真实绑定),本镜状态随之刷新。 四、隐藏后台参数(要求四)+ 状态文案(要求五) - 主界面只留:画幅 / 时长 / 参考方式(业务说法)/ 模型方案业务名 / 缺失项 / 生成状态与错误原因。 - 供应商名、原始模型 ID、分辨率档位、`file_id` / `storage_key`、守卫状态、后端原始提示 全部移入默认折叠的「技术详情」;后端提示里内嵌的内部 ID 用 `maskInternalIds` 屏蔽后再展示。 - 新增 `shotStatusText`(唯一文案来源)替换笼统的「待确认」: 待确认资产候选 / 待保存视频提示词 / 待绑定素材 / 缺少首帧(或关键帧、尾帧)/ 已具备生成条件 / 已生成, 并**分开**给出「可生成」「可导出」两个标签;镜头列表与工作区头部共用同一判定。 - 修掉一个真实缺陷:自动定位在集级就绪数据到达前就落锁,导致「生成与交付」步骤停在第一镜; 现在两份数据都到位才判定,并且**每个步骤各定位一次**(切换镜头仍尊重用户选择)。 五、页面验收(八镜 / 九镜验收项目,DRY_RUN 守闸,未触发真实大模型 / 出图 / 出视频) 1. 项目第 3 步点「4. 资产与声音绑定」→ 落在 `/studio?studio=binding`;九镜项目按步骤自动定位到 第 2 镜(该镜缺首帧、无绑定),手动选第 5 镜后切到「生成与交付」会重新定位回第 2 镜。 2. 三栏顺序与占比符合要求(见一);中间为「本镜生产」七块,右侧为画面预览。 3. 资产推荐 → 「确认全部推荐」给出明确反馈(DRY_RUN 下建议全是「需复核」)→ 勾选未绑定项 「只保存勾选项」→ `POST /studio/shot-links/scene` **201** → 重新推荐后该行变为「已绑定」,本镜状态刷新。 4. 切换镜头后仍停留在「关联绑定」步骤与③块(展开项不变)。 5. 缺首帧时生成按钮禁用并给出原因("参考模式 first 的参考帧不可用:缺少参考帧 first")→ 参考方式切「纯文本」→ 重新预检 → 按钮恢复可用。 6. 技术详情默认收起时主界面内部 ID 计数 **0**;展开后可见供应商 / 原始模型 ID / 分辨率 / 参考帧 file_id / 声音 file_id / 已生成视频 file_id / 守卫状态。 7. 点击生成 → toast「演练模式(DRY_RUN):只展示计划,不会产生正式视频,也未写入任何库」, `POST /studio/image-pipeline/video-submit` 200(`status=dry_run`)。 8. 导出弹窗只列可导出镜头(8/8,与「可生成」分开判断)→ 实际下载 TXT: `b28a9273-…-prompt.txt`(4915 字节,8 镜)。 9. 旧项目(39 镜迁移项目)与「提示词起步」项目都能正常进入工作室;对白项目里「对白与镜头内容」 以折叠区出现(默认收起,无独立「对白状态」区域)。 六、工程门禁 - 前端:`node --test` **75 passed**(新增 19 条:状态文案 10、绑定推荐规则 5、内部 ID 屏蔽 4); `tsc --noEmit` 通过;`vite build` 通过。 - 后端:用 `init_test_db.py` 新建临时库(`/tmp/jf_test_p3/test.db`)后跑全量 → **14 failed / 634 passed**;这 14 条是既有 24 条基线的**子集**(少了 10 条依赖已初始化库/模型配置的用例), **新增失败 0**;本轮新增的 8 条后端测试全绿。本轮未改动任何后端文件。 - 未跟踪生成物检查:只提交上述前端文件;`front/node_modules`、`front/dist`、`.env`、库文件均未入库。
一、工作室不再显示旧的三步序号(第三部分遗留) - 工作室步骤改为**项目五步口径**:`3. 单镜提示词补漏` / `4. 资产与声音绑定` / `5. 生成与交付` (内部 key `video_prompt` / `binding` / `deliver` 与路由不动,只有用户可见文案变); 第一步的提示明确写出「整集生成与批量导入在项目第 3 步完成,这里只补漏与返工」。 - 项目第 3 步「整集视频提示词」仍是整集批量生成 / 批量导入的唯一主入口(工作台内), 工作室只保留单镜补漏(第 4 轮已移除工作室内的批量入口)。 二、用户可见的旧流程文案统一成五步业务语言 - 删除用户可见的「六步」表述;把「资产准备(提取 + 图片)」写成一个用户步骤, 不再描述成「资产提取 → 图片准备」两个独立步骤: `ProjectExtractCandidatesPanel`、`ProjectImagePrepPanel`(面板标题改「资产图片与定版」)、 `ChapterShotEditPage`、`ChapterShotsPage`、`ChapterShotPreparationGuide`、`chapterPreparation` 里的顺序说明统一为「剧本与分镜 → 资产准备 → 整集视频提示词 → 资产与声音绑定 → 生成与交付」。 - 资产编辑页的「第 3 步 图片准备」入口提示改回「第 2 步 资产准备」。 三、实现字段与内部 ID 不再出现在普通界面 - `shot_details.video_prompt` / `video_prompt_source` / `image_prompts` / `is_primary` / `file_id` 等实现字段,以及「来源白名单」这类内部说法,改成业务语言 (EpisodeVideoPromptBoard / ProjectImagePrepPanel / AssetImagePromptLlmPanel / ProjectStudioStepPanel / ExportScopeModal / StudioStepProgressStrip / shotReadiness / ShotAudioBindingSection / AssetEditPageBase / ChapterStudio); 工作室内的生成、上传、保存 toast 也不再打印 `file_id`(提示「内部 ID 见『技术详情』」)。 - 原始字段、供应商名、`file_id` / `storage_key`、接口参数仍只在工作室默认折叠的「技术详情」里; 「开发信息」面板保持技术口吻(它就是给排查用的技术面)。 - `1440×900` 复检:左 300 / 中 550 / 右 340,顺序与占比正确,`documentElement.scrollWidth == 1440` **无整页横向滚动**,中间仍是主要操作区。 四、修掉一条与执行目录相关的假失败(发布前检查发现) - `backend/tests/test_image_pipeline.py::test_service_relative_local_path_is_made_absolute` 用**相对 cwd** 的 `app/services/.../external_image_client.py` 读源码: 从仓库根目录跑 `pytest -q` 必然 `FileNotFoundError`,于是同一份代码在 「backend 目录」下是 13 条失败、在「仓库根目录」下是 14 条 —— 与业务无关。 - 改为相对**测试文件**解析(同文件 `test_image_pipeline_modules_contain_no_db_writes` 已是这条口径), 断言内容一字未改。修后两种 cwd 都是 13 条,失败集合与记录的基线逐条一致。 五、发布前检查结果(同一 `init_test_db.py` 全新临时库、同一环境) - `28b933a`(基线,backend cwd):**13 failed / 589 passed** - 当前 HEAD(backend cwd):**13 failed / 635 passed** → 与基线失败集合**逐条一致**(0 新增) - 当前 HEAD(仓库根目录):**13 failed / 635 passed** → 与上面同一集合(修 cwd 后与执行目录无关) - 迁移链路:`rollback → migrate → migrate(幂等)` 结构可用;`migrate` 只在**真的要加列**时写 `*.backup_before_llm_pipeline_<ts>` 备份,`rollback --restore <备份>` 可整体还原 (SQLite `DROP COLUMN` 本身会丢该列数据,这是该操作的固有语义,已在报告中写明)。 全部迁移演练都在**验收库的副本**上执行,真实验收库只读核对未受影响。 - 前端:`node --test` 75 passed、`tsc --noEmit` 通过、`vite build` 通过。 - PR 差异:`git diff --check` 在非生成物上无问题;`front/src/services/generated/**` 报的 “new blank line at EOF” 是 OpenAPI 生成器原始格式(`origin/main` 里既有的生成文件同样是 `;\n\n` 结尾,模型文件 308/308 一致),按要求保持生成器原样、不做手工修改。 - 敏感与生成物:PR 差异里没有 `.env`、数据库、图片、视频、日志、凭证或临时验收文件。
截图复核时发现同一屏出现了两套步骤段控件与两行范围统计:老的检查器头部 (分镜生成面板 + 步骤段控件 + step hint + 范围标签)和「本镜生产」工作区自带的 头部/步骤/范围重复。现在只保留工作区那一套,老头部与它专用的 getStudioStepMeta 一并删除(切换步骤、范围统计、关闭按钮都在工作区内提供)。 1440×900 复检:步骤段控件只剩一组(3. 单镜提示词补漏 / 4. 资产与声音绑定 / 5. 生成与交付),左 300 / 中 550 / 右 340,无整页横向滚动。
一、真实复测结论(按用户口径执行 1 次) - Cookie 框放**整串浏览器 Cookie**(含开头的 Authorization= 项,1185 字符)、Authorization 框留空、 授权方式=不发送(仅 Cookie)、Referer 用页面 URL。 - 结果:`getScriptPage` 仍返回 **HTTP 401**(`额外带 Authorization:否`、`授权模式 none`,凭证传递已正确)。 按用户要求到此不再猜 raw/Bearer,需以浏览器里巨日禄成功请求的真实请求头为准继续排查。 - 抓取 0 条 / 匹配 0 条 / 保存 0 条;未写库;凭证用后立即清空。 二、页面引导(两个入口都补齐) - `EpisodeVideoPromptBoard`(工作台导入抽屉):Cookie 含 `Authorization=` 时自动切「不发送(仅 Cookie)」; Authorization 框收到含 `Authorization=` 或分号的整串时**阻止提交**并提示放进 Cookie 框; Cookie 缺 `Authorization=` 时提示可能不完整;Referer 留空默认用页面 URL; 401 时显示**脱敏诊断**(接口阶段 / HTTP 状态 / 是否带 Cookie / 是否额外带 Authorization / 授权模式)。 - `PromptFlowPage`(提示词导入/交付页)此前**没有任何护栏**:补齐同样的 Cookie 提示、整串拦截、 模式自动切换与「Authorization 只填单个值」的占位说明。 - `GenerationRequestError` 带上后端信封 `meta.diagnostics`(脱敏),前端不再只拿到一句话。 三、后端硬化(`jurilu_agent_import.resolve_authorization`) - **拒收 cookie-like 的 Authorization 值**(`Authorization=` 开头或含分号/换行)→ 一律忽略该值、 只发 Cookie 头,并在 diagnostics 记为 `ignored_full_cookie`(这是「怎么粘都只发 Cookie 头」的可靠落点)。 - **未知/中文授权模式不再静默降级成 auto**:显式映射「不发送(仅 Cookie)/仅Cookie/原样发送/Bearer 发送」, 未知取值 fail-safe 成「不发送」并记 `unknown:<原值>`。 - `auto` 不再读 `JURILU_AUTHORIZATION` 环境兜底(那颗雷会凭空加一个 Authorization 头); 只有显式 raw/bearer 才读。 - **Referer 兜底搬到服务层**(`referer or source_url`,对齐原中控台 app.py:18478):一处修好所有调用方。 四、测试(新增 `backend/tests/test_jurilu_credential_passing.py`,7 条全绿) - 整串 Cookie 原样只出现在 `Cookie` 头、且 `Authorization=` 项保留; - 模式 none 时不加 Authorization 头;mode=none 绝不读环境兜底; - bearer 才加 `Bearer ` 前缀;cookie-like 的 Authorization 值被拒收; - 中文/未知模式不产生 Authorization 头;401 结果不含任何凭证内容(脱敏断言)。 前端:tsc / build 通过、`node --test` 76 passed。
真实事故:逐镜真实生成完成后草稿只存在浏览器内存,页面一中断(本次是我的工具调用被宿主机超时杀掉)
两次已付费的生成结果就丢了。这次把草稿落到服务端:
- 新增 `app/services/studio/prompt_board_drafts.py`:按 (chapter_id, shot_id) upsert 每镜草稿
(prompt / 状态 ok|failed / 错误文案 / 模型信息),**不写正式列** `shot_details.video_prompt`;
正式保存仍走既有 `prompt-board/save`(origin → source 映射不变,llm_draft → llm),保存后清掉对应草稿。
- 新增表(模型注册在 `app/models/`),并提供 **幂等迁移与回滚脚本**:
`scripts/migrate_prompt_board_drafts.py` / `scripts/rollback_prompt_board_drafts.py` /
`scripts/_prompt_board_drafts.py`(列清单单一来源,与既有 llm_pipeline 迁移同一口径)。
- 接口挂在既有 `/api/v1/studio/prompt-board/{chapter_id}` 命名空间下:
保存单镜草稿 / 读整章草稿状态(用于「已完成 / 失败 / 未开始」与只重试失败或缺失)/ 清空草稿。
- 测试 20 条全绿(`backend/tests/test_prompt_board_drafts.py`、`test_prompt_board_draft_migration.py`):
草稿 upsert 幂等、刷新后仍可读回、不污染 `video_prompt`、正式保存写 `llm` 来源并清草稿、失败态可区分。
⚠️ 前端接线(刷新恢复、暂停保留、只重试失败/缺失、防同镜并发、确认保存后清草稿)**尚未完成**,
本 commit 只是可独立验证的后端一半;页面证据与「最多 2 次真实调用」的验证在后续轮次补。
…eview 里 source_url 未定义
…ed 归一化 参考图预检:HEAD→GET 匿名探活,相对路径与内网地址不发请求即判死,不通过则整批不提交并返回中文结构化错误(含怎么修、且明确未产生付费调用);/submit、/frame-submit、/video-submit 三个真实入口全部接线,frame-submit 在写库前拦截。\n\nAI 首帧不再进 Celery 死队列:改为同进程 asyncio 内联执行,任务记录/状态/结果/取消接口照旧。\n\npartial_failed 归一化:新增 outcome / oss_ready / error_message / http_status,ok 不再对部分失败恒为 true,汇总给整数 ok_count/failed_count/oss_ready_count,旧字段全保留。\n\n说明:routes/studio/image_pipeline.py 里同时带上上一批守卫提交遗留的 _guard_blocked_envelope 复用改动。
…ardPage 根因(真实验收取证):getStoryboardPage 三个 scriptId 全部 HTTP 200,记录是 msgpack 序列化后 base64 塞在 data.payload;旧解析器只认 data.records,于是三条都「解析出 0 条」,页面还把阶段写成 getScriptPage,看着像第一步被拒。\n\n- 新增 backend/app/services/external/msgpack_lite.py:纯标准库 msgpack 解码器(不引入第三方依赖),截断/非法一律抛错,绝不返回半截数据。\n- jurilu_agent_import:识别 data.enc=msgpack + base64 payload 并解码;describe_payload_shape() 如实报「声明编码 / 解析到几条 / 字段名 / 是不是我们没解开」,区分「接口真 0 条」与「有数据没识别」。\n- 前端 juriluDiagnostics.ts:按证据判失败阶段(第一步 4xx/没取到 scriptId → getScriptPage;第一步成功但第二步 0 条/报错 → getStoryboardPage 并带第二步自己的状态码与原因)。\n- 测试:后端 32 个(解码器 + msgpack 解析 + 服务端诊断),前端 10 个,全绿;pylint 10.00/10。
用户口径:一次抓到的多个 scriptId 是不同脚本(或同一脚本不同版本),109 条不能当成同一集的连续镜头。\n\n- 选择恰好一个:HTTP 层 + 服务层双保险,去重后 >=2 一律 400「一次只能导入一个脚本组」;apply 额外要求非空;未选组只返回脚本组(entry_count=0/rows=[]/requires_script_selection)。\n- 版本提示只认可靠时间证据:只有候选集合每组都有可比较时间戳且最大者不并列才标 likely_newest,依据写明命中字段名与值;否则谁都不标,只陈述客观事实(标题归一化相同/正文重合度/记录数/序号范围/取了几页)并请用户确认。\n- 整组不截断:第二步翻页取全(pages_fetched 取证,短页/无新增/自报 total 三种收尾,安全上限 200 页并告警);sample_records 只是展示采样,不影响进匹配与写库的条数。\n- 匹配改为编号优先(seq == 镜头 index)+ 顺序兜底;plan 行带 script_id/seq/matched_by;复用既有 create_missing 与 next_index 口径,新增 missing_shot_count。\n- 修好两份按旧契约失效的自检脚本并新增 selfcheck_db.py(库解析 + 数据不足时中文跳过),selfcheck_jurilu_import 32/0、selfcheck_http_endpoints 54/0、selfcheck_jurilu_plan 27/0。
复测暴露的状态矛盾根因:把「已选中脚本组」(selectedScriptId) 当成「已匹配」用了——缺口面板用选中组的 record_count 算缺口、渲染闸门是 selectedScriptId || matchedScriptId,而同一时刻横幅用的是 matchedScriptId(仍为空),于是同时出现「尚未选择脚本组」与「缺少 28 个」;另外 matchedScriptId 只记组不记章节,换章节后旧标记会残留。\n\n- 新增 JuriluMatch(scriptId+chapterId+entryCount+matchedCount+groupRecordCount+chapterShotCount) 与 resolveJuriluFlow() 纯函数(stage=idle/selected/matching/matched/stale),只有 matched 才产出缺口与目标数;换组/换章节/匹配失败/保存成功/清空全部作废。\n- 未匹配时隐藏镜头缺口与一切创建入口;底部通用建镜按钮在巨日禄流程进行中隐藏,缺口面板 primary 成为唯一入口;建镜函数再加「必须已匹配」闸门。\n- 明确显示当前目标章节(名称 + ID 末段 + 后端真实镜头数),未匹配时不展示数字。\n- 测试 185/185(新增 7 条状态一致性用例),tsc/build 通过;页面自查回放三种状态,POST /studio/shots 计数 0/0/0。
原问题:页面保存实际走 /prompt-board/{chapter}/save,而它静默忽略前端传的
script_ids,于是真实页面路径没有执行「恰好一个 scriptId」的服务端校验
(此前只有没被实际调用的 /jurilu-import/apply 有校验)。
- BoardSaveRequest 新增单数 script_id,并显式声明已废弃的 script_ids:
任何 origin 传它都返回 400 script_ids_deprecated,不再静默忽略。
- _validate_jurilu_script_scope 在 save_board 第一行执行:origin=jurilu_import 时
缺 id → 400 missing_script_id;格式不合法或像多个 → 400 invalid_script_id;
逐条 entry 的 script_id 与请求不一致或缺失 → 400 entry_script_mismatch /
entry_missing_script_id(带 1 基下标与两边 id)。校验在任何 DB 访问之前,
失败时 0 条写入。
- 其它来源(llm_draft / external_import / manual)不受影响;成功响应新增
script_id 回显,旧字段(applied_count / skipped_count / results /
cleared_draft_count)全保留。
- 前端:savePromptBoard 改传单数 script_id,并让每条 entry 带同一组
(纯函数 buildPromptBoardSaveBody 构造 + 6 例单测),保存前另有同口径自检。
- 测试:新增 27 例——缺 id / 传 script_ids / 9 种非法格式 / 条目不一致 /
条目缺 id 全部 0 条写入;合法单组正常保存且来源全为 jurilu;monkeypatch
证明 svc.save_entries 一次都没被调用(校验先于写入);其它来源不受影响。
目标文件 54 passed、pylint 10.00/10、整仓 13 failed / 848 passed
(13 个失败名与基线逐字相同)、前端 191/191 且 tsc/build 通过。
用项目既有命令重新生成,未手工改动任何 generated 文件: corepack pnpm run openapi:fetch (curl http://127.0.0.1:8000/openapi.json -o ./openapi.json) corepack pnpm run openapi:gen (openapi-typescript-codegen 0.30.0,与 package.json 锁定版本一致) (本机 PATH 里没有 pnpm,用 corepack 调用项目脚本;openapi:gen 内部再调用 pnpm 时通过临时 PATH shim 转发到 corepack,没有改 package.json、没有改任何脚本、没有手改生成结果。) 契约核对(真实生成结果): - BoardSaveRequest 现在的属性:allow_partial / entries / mode / origin / script_id / script_ids / selected_shot_ids —— 单数 script_id(origin=jurilu_import 时必填)与 已废弃的 script_ids(传它一律 400)都在。 - BoardEntryWrite 新增 script_id(该条属于哪个脚本组)。 - 没有意外删除:paths 134 → 138(消失 0、新增 4),components.schemas 329 → 338 (消失 0、新增 9),generated/models 329 → 338、generated/services 24 → 24。 验证:npx tsc --noEmit 通过;npm test 191/191;corepack pnpm run build 成功 (仅有既有的 chunk >500kB 体积提示)。生成器产物的末尾空行按原样保留,未做任何人工格式化。 本次没有重新执行巨日禄抓取,也没有任何付费调用。
legacy 入口以前只有形态级校验(地址形状能不能给上游),不做匿名可达性探活: 本机相对路径 / 内网地址 / 404 的对象照样被当成可用参考图发给上游,上游抓不到 → 任务 failed(故障 A 原文「无法获取输入媒体 URL(404/410)」)。 - 新增 video_submit.preflight_video_input_media():**所有出视频入口共用**的提交前预检, 候选抽取(video_media_candidates:首/尾/关键帧 + 参考音频)、供应商是否吃内嵌 base64 (vendor_accepts_data_url)、探活(reference_preflight.preflight_or_raise,匿名 HEAD→GET) 各自只有一份实现;submit_video 也改走它,消除重复。 - /film/tasks/video 在建任务 / 写库之前调用:不可达 → 409 + 结构化中文错误(哪个素材、 真实 HTTP 状态、原因、怎么修、paid_call_made: false),不写 generation_tasks、 不派发任务、不出网给上游;DRY_RUN 一次都不探活。 - 测试:新增 test_legacy_video_preflight.py 11 例(不可达 → 409 且 0 行任务、写库层未被调用; 可达 → 行为不变;DRY_RUN 0 次探活;文案不含 file_id / 本机绝对路径 / 凭证)。
根因(已复现):前端分类器 classifyAssetResultStatus() 认得 partial_failed / partial_failed_by_oss / PARTIAL_FAILED / partial-failed / oss_upload_error 等, **但裸的 `partial` 返回 null**,于是这条既没进成功也没进失败,页面仍以 countsKnown=true 显示「成功 0 / 失败 0」,且没有任何说明。 - 分类器补全:含 `partial` 一律判部分失败(新增 partial / partial_ok 精确 token + 包含式规则前置);含 fail / error / denied 的 token 永不落 null; 后端 classify_status_token 同口径对齐(partial → partial_failed,不再落 unknown)。 - 不丢数硬约束:认不出的 token 从 pending 改判 unknown;汇总后对账 ok + failed + dryRun + pending + unknown == total,差额计入 unknownCount 并记 unaccountedCount(total 更小则按计数抬高 total);无法识别 / 演练占位 / 处理中的条目 一律在文案里显式写出(例如「其中 5 条状态未识别(既不算成功也不算失败)」)。 - 三个互斥桶:完整成功 / 部分成功 / 失败 —— 一条 partial_failed 现在显示 「成功 0 / 失败 1(含图片已生成但 OSS 未就绪的条目)」,不再同时进两个桶。 - 测试:前端新增 6 例(含穷举式属性测试:任何状态 token 组合下计数之和恒等于 total, 且 ok=0/failed=0 时必须给出说明或 countsKnown=false);后端新增 test_partial_failed_status_parity.py 11 例保持上下游口径一致。
真实故障 A 的成因类别:本机可读、上游 404。此前公开地址有两套拼法,且
`_build_public_url` 在没配 `s3_public_base_url` 时会退回 path-style
`{endpoint}/{bucket}/{key}` —— 在阿里云 OSS 上那是错地址(匿名 404)。
- 地址口径收敛成唯一实现 `storage.public_url_for_key()`(`_build_public_url` 删除):
配了基址 → `{public_base}/{base_path}/{key}`;**没配 → 空串 + 一次性中文 warning,
绝不猜 path-style**;本地驱动 → `/files/{key}` 本机回放地址。
新增 `storage.is_public_url()` 作为「是否公网匿名可读」的唯一判定,
`utils/files.py` 的 `resolve_vendor_image_ref`、`reference_resolver` 全部改用它
(本地回放地址不再被误判为「供应商可用」)。
- 上传后匿名可达性验证收敛成唯一实现
`reference_preflight.verify_uploaded_url_reachable()`(探活仍复用唯一的
`probe_reference_url`):DRY_RUN 一次都不出网;不可达**只告警不阻断**;
文案含真实状态码、原因与修法(含 ACL public-read / s3_public_base_url 检查)。
- 接线:`POST /api/v1/studio/files/upload` 与采纳(adopt)共用同一实现;`/upload`
响应只增字段 `url` / `url_reachable` / `url_probe` / `warnings`(与 adopt 同名同形)。
没配公网基址时 `url=""`、`url_reachable=null`、`probe.result="no_url"` + 可操作告警,
**不伪装成可用**。
- 写入路径未动(仍是既有那两条 `ACL: public-read`),没有新增第二条 OSS 写入路径。
- 测试:`test_adopt_upload_reachability.py` 6→12、`test_storage_s3_config.py` 5→8;
目标集 83 passed;整仓 13 failed / 902 passed,13 个失败名与基线逐字相同;
pylint 改过的 app 文件 10.00/10。全程零真实出网、零真实上传。
此前两条链路的判定打架:计划层只看前缀(把 `http://192.168.1.9/…` 当「可用」), 提交层只看协议(把供应商私有素材通道 `asset://` 当「本地地址、不携带」)。 - 唯一准入实现 `video_audio_input.classify_audio_input()`:内网/本机地址(即使 http:// 开头) 一律排除并给原因与修法;公网 http(s) 与 `asset://` 携带;`data:` 按供应商能力判定; 未绑定 / 文件不存在 / 供应商不支持 各自有明确 state。计划(直提 + legacy 预览)与提交 (直提 + legacy)全部经它,`reference_preflight.is_loopback_or_private_url` 复用既有唯一实现。 - 顺带修掉一处探活口径不一致:`asset://` 以前会被匿名探活按「本机/相对路径」误杀 (计划说携带、提交说不可达)→ 现在从探活候选里排除(我们探不到供应商私有通道), data URL 照旧探活。 - 请求计划新增审计字段 `audio`(只增不删):是否携带 / file_id / 公开地址 / 排除原因 / reason_code / how_to_fix / 供应商是否支持参考音频 / 术语澄清 note;legacy 预览同结构。 - 页面(`ShotAudioBindingSection` + 计划面板)用同一份纯函数文案:绑定了但供应商取不到时显示 「已绑定,但供应商无法访问」+ 真实原因 + 怎么修;未取到计划时显示「未知」而不猜。 - 术语分清:「参考音频」是输入(本轮只在**请求计划层**验证会被带进请求), 与「最终成片的音轨」(把已生成音频混流/回贴,当前未实现)不是一回事;新增 `docs/reference-audio-scope.md` 并在响应 note、页面文案、类型注释里写明。 - 测试:`test_video_audio_scope.py` 32 例(用 MockTransport 捕获**真实发出的请求体**: 公网地址进 `audio_urls`、本机相对路径不进且探活 0 次、`asset://` 携带且不被误杀、 内网不走、未绑定/不支持各有 state),前端 10 条纯函数用例。全程零真实付费调用。
用户要求:正常使用不能每次在终端手动 export。此前守卫只读 `os.environ`, `backend/.env` 里写这两个开关**完全无效**(只在文档里提了这个坑)。 - `app/config.py` 新增 `jellyfish_dry_run` / `jellyfish_real_llm_confirmed` (用 `str | None` 而不是 `bool`:`JELLYFISH_DRY_RUN=maybe` 这种取值不能让后端起不来), 并支持 `JELLYFISH_ENV_FILE` 覆盖本次要读的 `.env`(只为了能在**临时文件**上做独立进程验证, 绝不碰仓库里那份含真实凭证的 `backend/.env`)。 - `dry_run.py` 收敛成**唯一解析层**:`flag_raw / flag_source / flag_text`, 优先级 = 进程环境变量 → `backend/.env` → 默认值;未配置 = 演练; 读不到 / 读不懂 / `Settings` 抛异常一律按「未设置」处理(fail-safe,两个方向都不放行); `state()` 与状态接口只增字段:`source` / `source_label` / `dotenv_real_mode` / `switch_source` / `startup_warning`(既有字段一个没删)。 - **`.env` 打开真实模式时启动打醒目中文告警**(说明计费方式、怎么关回演练、 并提示「CI/测试请用演练模式」);告警文本与状态接口同源。 - 页面「演练模式 / 真实模式」角标新增**配置来源**只读展示 (`进程环境变量` / `backend/.env` / `默认`),由 `.env` 打开时额外标红; **普通业务页面不提供任何「一键切真实」开关** —— 切模式必须改配置并重启(有测试锁住解析结果里 不存在 set/toggle 语义字段)。`backend/.env` 继续被 git ignore。 - 测试:`test_dry_run_dotenv_switch.py`(14)+ `test_dry_run_real_mode.py`(扩到 38 总量)+ 新增 `test_dry_run_switch_subprocess.py`(6 条**独立进程**用例:默认演练 / `.env` 开启 / 环境变量覆盖 `.env` 两个方向 / 非法布尔值安全回退);`tests/conftest.py` 加 autouse 夹具 保证**整套测试默认演练**(即使本机 `.env` 写了开关也不受影响)。 - 文档与示例同步:`docs/real-run-mode.md`(优先级表 + 方式 A2)、`backend/.env.example` (旧注释「写在本文件里无效」改成正确口径,两个键仍保持注释掉)、`scripts/run_backend.sh` (提示 `.env` 里也写了开关并给行号)。 说明:同一工作树里 `main.py` 的启动告警与出站兜底改动在同一个 hunk,故随下一个 commit 提交; `dry_run.py` 里与守卫口径相关的两处(`_check_host` 放行条件、出站兜底开关)也归到下一个 commit。
上一提交把准入判定收敛成一处后,补上与验收文档 `SIX_STEP_ACCEPTANCE.md:128` 记录的 供应商契约一致性(该行写明:参考音频最多 3 条、总时长 ≤15s、需与参考图/参考视频一起用、 只收公网 URL 或 `asset://`、**与首尾帧图片互斥**): - `video_audio_input`:把「条数上限 / 总时长上限 / 与首尾帧互斥」这三条做成显式判定, 超限或与首尾帧冲突时**排除并给原因与修法**(不做静默截断),判定仍在同一处纯函数里。 - `video_submit` / legacy 预览:计划里同步给出这些排除原因(审计字段只增不删)。 - 页面声音区块与文档 `docs/reference-audio-scope.md`:把「参考音频(输入)」与 「最终成片音轨(混流/回贴,当前未实现)」的表述对齐到同一口径, 并注明「供应商真实接受参考音频并据此影响生成结果」仍未验证。 - 测试:`test_video_audio_scope.py` 扩到覆盖条数/时长/互斥三条与排除原因; 与 preflight / legacy 相关用例合计 67 passed;全程零真实付费调用。
独立入口审计(backend/tests/test_entrypoints_audit.py)查出的三条,全部修掉: 1. **`real_unconfirmed`(DRY_RUN=0 但没确认)下 LLM 编排链路会真的发出付费请求**。 根因:`llm_orchestration/client.py` 里那句出站守卫包在 `if dry_run.dry_run_enabled():` 里 —— dry_run 一关,**付费确认检查被整段跳过**(`real_call_confirmed()` 从未被检查), 而状态接口/文档承诺的是「未确认仍然不发真实请求」。修法:改成无条件过守卫 (演练 → `DryRunBlocked`;未确认 → `RealCallNotConfirmed`;已确认真实 → 放行)。 对照:image / video 出口本来就是无条件断言,所以只有这条 LLM 链路有缺口。 2. **出站兜底的放行条件同源错误**:`_check_host` 以前是 `if not dry_run_enabled(): return`, 未确认模式会绕过兜底。现在只有 `is_real_mode()`(真实且已确认)才放行本机以外的外部主机。 同时把兜底从「文档声称有、实际从未安装」改成**显式开关**: `JELLYFISH_NETWORK_GUARD=1` 时由 lifespan 安装(默认不装 —— 装上会连演练模式下的 **只读**外部读取一起拦,不能静默改变本地用法),状态接口如实回报 `guard.network_guard` / `guard.network_guard_requested`。 3. **legacy `POST /api/v1/film/tasks/video` 真实模式下无条件进 Celery 队列**: 本机既无 broker 也无 worker → 真实模式下要么 500(并留一条悬挂 pending 行)、 要么永久 pending。现在与「AI 首帧提示词」那条一致:**优先同进程内联执行** (复用同一个任务执行器,状态/结果/取消接口照旧),拿不到运行中的事件循环才退回队列。 前端本来就不再调用这个端点,页面视频生成走直链 `video-submit`。 顺带的启动告警:`main.py` 的 lifespan 现在会打「真实模式由 `backend/.env` 打开」的醒目中文告警 (与上一个 commit 的 `.env` 解析同源;该文件改动与出站兜底安装在同一 hunk,故一并提交)。 测试:`test_entrypoints_audit.py`(18 条)里两条「记录现状」的锁已按修复后的行为翻转 (未确认 → `RealCallNotConfirmed` 且零出站;legacy → 内联派发而非无条件入队,并补了 「无事件循环才回退队列」用例);新增 `test_guard_confirmation_gaps.py`(5 条:兜底按需安装、 放行条件、本机/白名单始终放行、lifespan 只在显式开启时安装)。
用项目既有命令在**当前代码**上重新生成(先重启后端加载最新代码,再 fetch + gen):
corepack pnpm run openapi:fetch
corepack pnpm run openapi:gen (openapi-typescript-codegen 0.30.0)
本机 PATH 无 pnpm,用 corepack 调项目脚本;`openapi:gen` 内部再调 pnpm 时用临时 PATH shim 转发,
**未改 package.json / 未改任何脚本 / 未手工修改任何 generated 文件 / 未做人工格式化**
(生成器末尾空行的原生格式按原样保留)。
本次把这几项新契约补进客户端:
- `BoardSaveRequest`:单数 `script_id` + 已废弃的 `script_ids`;
- `FileUploadRead`(新):`url` / `url_reachable` / `url_probe` / `warnings`;
- `VideoAudioPlanRead`(新)与 `VideoSubmitPlanRead.audio`:参考音频审计字段;
- 新增路径:`/studio/prompt-board/{chapter_id}/drafts`(GET/POST/DELETE)与
`/drafts/claim`、`/drafts/release`、`/studio/projects/{project_id}/asset-readiness`。
核对(与生成前基线逐项对比):paths **134 → 138**(消失 0 / 新增 4),
`components.schemas` **329 → 341**(消失 0 / 新增 12),生成器未删除任何既有接口或模型。
验证:`npx tsc --noEmit` 通过;`npm test` 210/210;`corepack pnpm run build` 成功。
历史段落按轮次保留,但已被取代的结论就地标注「已更新」(巨日禄 401、前端接线尚未完成), 并把 13 条既有失败的范围、参考音频未验证边界、有意未重复执行的真实调用逐条写清。
用户口径(2026-09-23): - 2D / 3D 预设的默认画幅从 9:16 改成 **16:9**(描述同步改成「16:9 横屏」); - 采用方案 B:选整体风格时把该风格的预设画幅**自动填进**「默认视频比例」输入框, 让用户看到「存进去的就是这个」;**用户手填的值优先**(真人竖屏也能改成别的比例); 输入框清空时回落到该风格预设默认值;「其他自定义」完全以用户填写为准(可留空)。 改动前是「预设静默覆盖输入框」:非自定义风格下手填的比例会被无声忽略,UI 与实际落库不一致。 新增纯函数 resolveProjectVideoRatio() 承载取值口径并有单测;ProjectLobby 的 Radio 加了 onChange 自动带入。 前端 213/213、tsc、build 通过。
用户要求(第 2 步资产准备): - 候选支持「勾选多条确认选中项」+ 逐条确认;角色可选「关联全局演员 / 新建项目角色」,场景/道具/服装可选「关联资产库 / 新建」;确认后资产立刻出现在同页生产区(复用既有刷新回调,不做跨页跳转),并明确提示下一步。 - 普通生产页面不再出现模型内部名(deepseek-chat / image2)、原始状态值(status: ready)、任务号、file_id、「演练门禁」这类开发术语;改为用户语义(可以提取 / 可以生成图片 / 正在生成 / 生成失败及原因 / 已有图片待定版 / 已定版)。 - 模型、供应商、原始状态、任务 ID、file_id、接口参数统一收进默认收起的「技术详情」(原「开发信息」面板改造);userFacingStatus.sanitizeUserText 作为最后一道闸,任何后端原始文本给用户看之前过一遍。 - 费用提示替代开发术语;「真实模式」角标保留。 前端 tsc 通过、node --test 279/279(新增 extractConfirmPlan / userFacingStatus 两组用例)。
用户目标流程:候选确认 → 生产区 → 批量生成/完善提示词 → 选择资产 → 批量出图 → 进度与失败原因 → 结果卡片 → 设为定版 → 进入下一步。全部复用既有内联端点(plan/preview、submit、task/{id}、adopt、files/external、entities/{id}/images、llm/image-prompt/preview),没有第二套生成逻辑,不碰 Celery。\n\n- 四类分页签(人物/场景/道具/服装,各带 已定版/总数)、全选/清空/只选未生成(跨页签累加)、批量生成与批量重新生成、单项生成/重新生成/编辑提示词/失败项重试、生成设置(是否用定版图当垫图、画面比例)。\n- 两条硬边界落成测试:默认只选/只提交「未生成项」且不覆盖已有图片与定版;重新生成与替换定版一律二次确认(写清替换什么、是否产生费用)。\n- 结果卡片:图/资产名+类型/用户语言状态/失败原因/提示词预览/查看详情/编辑提示词/重新生成/采纳/设为定版/重试/刷新;已定版绿标;同一资产多结果由用户选一张设版,系统不静默替换。\n- 批量前确认框:选择总数与四类分布、预计张数、未生成/已有图片/已有定版、是否带垫图、比例、当前模式,真实模式红字费用提示;一次点击只产生一轮任务;前端在途闸门 + 后端 source_task_id 幂等 + 重试用 attempt 换新键。\n- 「停止后续」只停排队项、已完成结果保留(后端无取消接口,界面写明);进度数字全部来自真实状态(总数/已完成/生成中/排队中/失败/已停止/演练占位)。\n- 服装在提交前明确拦住(出图服务契约只接受人物/场景/道具),不是静默跳过;文字统一过 maskInternalIds 脱敏。\n\n前端 tsc 通过、node --test 282/282(新增 47 例)、build 通过;页面自查用网络桩跑通提交/进度/停止/卡片,付费动作只到确认框并点取消,零付费调用。
用户要求「不得静默替换定版」「失败项可重试」:\n\n- 定版保护唯一实现 primary_protection.py:定版图 = is_primary=true 且 file_id 非空;本次写入要顶掉它(换定版行,或覆盖定版行的 file_id —— 即使 set_primary=false)都必须显式 confirm_replace_primary=true,否则结构化 409 且发生在任何写入之前(连下载都不发生)。\n- /adopt 的 set_primary 默认改成 false;响应只增 replaced_primary 回显(槽位 id / 文件名 / 是否公网,不含 file_id、完整地址、storage_key、凭证);entity_images 的 create/update 走同一判定。\n- build_source_task_id(attempt=0) 在 attempt=0 时与旧实现逐字一致(测试写死期望值),attempt>0 得到新键;/submit 新增可选 attempt 并回显本次使用的键 —— 否则同资产同提示词的「重试」会被上游幂等去重吃掉。\n- 顺带修掉 /adopt 的顺序问题:先定位槽位再下载,错误的 image_id 不会再先写一行 files。\n- 前端对齐:/adopt 与 entity_images 的新 409 会让「设为定版」「采纳并设为定版」「批量采纳」变成死路,故在这三处加「确认替换定版」弹窗并带确认字段,另加 409 兜底。\n\n后端 89 passed(新增 21 例)、pylint 10.00/10、整仓 13 failed/986 passed 且失败名逐字一致(无新增失败);前端 tsc + 282/282。全程零真实付费调用。
补齐上一笔提交遗漏的新模块(否则 6c16d14 单独 checkout 会 ImportError)。\n\n- 新增 POST /studio/image-pipeline/reference-regenerate(可选返工流程):只有该资产已有参考图且用户明确要求一致性时才用;解析参考图公网地址(唯一口径)→ 非公网形态直接 409(不发探测)→ DRY_RUN 占位不出网 → 守卫 → 复用匿名可达性预检(不可达 409 + 真实状态码 + 修法 + paid_call_made:false,零写入零出网)→ 幂等登记 → 走 Jellyfish 自有 APIMart 图片通道(请求体字段 image_urls,与本仓首帧链路已验证的适配器逐字段对齐)。\n- 响应与既有出图结果同形(results[]/summary/outcome/status/outcome/ok/image_url/oss_url/error_message/http_status/detail),前端可复用同一套结果卡片;另回显 reference_url/reference_label/reference_source/attempt/source_task_id/deduplicated/paid_call_made。\n- 与默认主流程分清:默认「生成参考图」= 按提示词直接生成(/submit,不要求已有图);本端点 = 使用已有参考图重新生成(必需一张公网可用的已有参考图)。\n- 全仓文案清理:app 内用户可见文案与 docstring 里的「垫图」全部改成「参考图」,openapi 全量 JSON 里「垫图」「图生图」0 次(同步了 2 条断言用户可见文案的既有测试)。\n\n测试:新增 16 例(本机相对路径/内网/404 → 409 且付费通道 0 请求、0 写库;公网可达 → 捕获真实请求体断言 image_urls;DRY_RUN 零出网;同 attempt 幂等只发一次、attempt 递增得新键;非 apimart 供应商 409;供应商失败同形失败结果);指定集 105 passed;pylint 10.00/10;整仓 13 failed/1002 passed 且失败名逐字一致。零真实付费调用。
…e)+ 补交漏提交的 asset_strategies.py 上一笔 d12af90 引用了未入库的 asset_strategies.py,单独 checkout 会 ImportError —— 本次一并提交,并用干净 checkout 验证。\n\n- 新增 6 例:人物参考图 16:9(参数化 9:16 / 1:1 / 3:4 都不影响),且断言这个画幅不是 default_video_ratio;场景/道具/服装按各自类型打标签(不得出现 characterReference)。\n- 变异测试证明用例有牙:把画幅改成跟随项目比例 / 把 scene 标签挂 characterReference,两条新用例分别变红,随后逐字还原。\n- 零真实出网:除传输桩外,另用 socket 层封锁(先自证封锁有效)再跑,仍 22 passed。
…ce 人物专用、参考图批量只对人物 用户口径:人物默认直接生成 16:9 人物参考图/设定图;场景=场景资产图;道具=道具资产图;服装=服装设定图;「批量生成参考图」只对人物开放;混选时后端必须按 asset_type 分流;场景/道具/服装不得被写成 characterReference;人物参考图 16:9 ≠ 项目最终视频画幅。\n\n- 唯一分流表 asset_strategies.py(上笔已提交):四类型的提示词槽位/模板、结果类型 kind+中文标签、画幅、参考图批量闸门;/submit 与 /reference-regenerate 两条链路都只调这张表。\n- 人物画幅写死 16:9 并注释「这是人物参考图的画幅,不是项目最终视频画幅」,显式传入非 16:9 → 忽略 + 如实警告(不静默);场景/道具/服装按各自口径,未被顺手改成 16:9。\n- 只增不删地暴露 result_kind/result_label/aspect_ratio_source/prompt_template(计划预览的 strategy、每条目标、提交结果、重生成响应)。\n- 更新 2 条既有行为断言(人物参考图 9:16→16:9 + 来源标记;场景走 reference_batch 改为只对人物开放)。\n\n测试:test_asset_type_dispatch.py 19 例 + 受影响批次 157 passed;pylint 14 个 app 文件 10.00/10;13 条基线失败逐字复现。整仓一次性对照未跑完(本机 IO 阻塞:新进程固定开销 25–55s,import hashlib 实测 wall 36s),这条我会在机器空闲时补跑。
D2(高危):演练模式下 POST /studio/files/upload 仍会真实写 OSS(实测返回 201 + 公网可取地址),
而状态接口却把 oss 出口标成拦截 —— 口径自相矛盾。现在两条真实对象存储写入路径
(app/services/studio/files.py::upload_file、app/utils/files.py::create_file_from_url_or_b64)
统一过既有闸门 outlet=OSS,且判定在**下载/写存储之前**:演练模式 409 + 结构化信封
(reason=dry_run、outlet=oss、how_to_enable、paid_call_made=false),**零出站、零上传、零落库**;
本地驱动(is_local_storage)不拦;真实模式照旧可用。blocked_payload 只增 paid_call_made 字段。
D4:对已有 (quality_level, view_angle) 槽位重复 POST /studio/entities/{type}/{id}/images
原为 500(UNIQUE constraint)。现在用 SAVEPOINT 只回滚这一次失败插入并转结构化 409
(entity_image_slot_exists + 槽位信息 + how_to_fix),其它完整性错误原样抛,不吞异常。
测试:新增 test_oss_outlet_guard_and_slot_conflict.py 13 例;改写 1 条把缺陷测试化的既有断言;
pylint 5 个 app 文件 10.00/10;整仓 13 failed / 1040 passed,13 条失败名与基线逐字 diff 一致
(本机负载下耗时 299s,但这次完整跑完了);零真实出网/付费/OSS 写入。
…7 文案 - D1(高危既有 bug):大模型提示词面板 setRows 整行替换 → 资产一旦存过提示词就变 undefined(undefined)且无法再生成;改为按 key 合并,并用「复刻旧实现」的单测证明改前确有此现象。 - D2/D3:前端唯一分流表 assetResultKind.ts 消费后端 result_kind/result_label/ aspect_ratio/aspect_ratio_source —— 卡片加「图片类型」标签(人物参考图/场景资产图/ 道具资产图/服装设定图),所有按钮与确认框按类型取词(场景/道具不出现「参考图」), 「批量生成参考图」只对人物出现,混选提交按 asset_type 分组逐组各起一轮。 - 用户点名:批次确认框 + 生产区各一处写明「人物参考图固定 16:9,不等于项目最终视频画幅」, 同句给出项目自己的 default_video_ratio(真机原文:本项目是 9:16,与参考图的 16:9 不一样)。 - D5:刷新后结果与进度不丢(assetRoundStore.ts 按 project+chapter 分键、上限 60 条、TTL 7 天), 恢复纯只读(shouldSubmit:false,在途项标「已停止」并说明不会重新提交),整条路径无提交/轮询入口。 - D7:ProjectWorkbench/** 与 assets/** 的非测试源码(含注释)不再出现「垫图/图生图」, 并新增源码级文案黑名单测试(范围外残留逐条登记,新出现即失败)。 门禁:tsc 0;node --test 330/330(基线 293);build 成功;页面自查 28/28 PASS 且 paid_requests_total = 0(确认框只打开后取消,未触发 submit/adopt)。
五层追踪结论(真实请求体证据):资料**从第 1→2 层就开始丢** —— 候选落库只留 id/file_id/thumbnail/description 四个 key;确认写资产时 description 成空串;图片提示词接口只读 description + 单镜头摘录(章节全文装载器 从未被调用);且模板要求模型**逐字使用**画像卡 → 第 5 层必然输出「外观信息不足,需人工补充」。只改页面文案无效。 - 整章结构化提取(复用既有 LLM 编排层/守卫/Provider,不新建 client):四类资产字段规格(角色/场景/道具/服装 按用户点名的维度)+ 别名与重复候选合并(保留每条来源证据)+ 幻觉/错类型丢弃 + 冲突才需人工 (同名异类型 / 别名指向两个资产 / 同章跨类型重名),无冲突可直接确认。 - 落库零数据库结构改动:结构化资料/别名/剧本片段/镜头依据写进 shot_extracted_candidates.payload; 资产 description 只作画像入口。 - **章节隔离(用户新边界)**:角色=项目内资产(仅空描述时补写);场景/道具/服装=全局资产**一个字都不写全局列**, 剧情身份/出场依据/本章特有字段只进 payload.chapter_overlay;全局更新走差异预览 + 显式确认 (409 global_asset_update_required 且回带差异摘要),image_prompts 还需 confirm_replace_image_prompt。 - **质量拦截(后端三条写入路径兜底)**:empty/vague_filler/name_only_generic → 422; 跨资产 duplicate/near_duplicate(≥0.9)+ 覆盖未确认 → 409;清单响应分 user_flow / technical_detail 两桶。 - **道具正式槽位**:prop_image_front / prop_image_other 进注册表(修掉「请求传了、响应 0 个、无 warning」的静默丢弃), 默认九槽位口径不变;混合批量按 asset_type 路由。 - 验收彩排(桩模型,零出网):两角色相似度 0.60、场景含空间/时代/陈设/事件/氛围、道具含材质/外形/所属人物/剧情用途、 四条 savable=True 且无空话、既有人工提示词/图片/定版图逐字未变。 - 另附仅用脚本:acceptance_chapter_setup.py / acceptance_real_llm_run.py(5 次调用计划、演练自检、失败即停不重试、 报告脱敏、结束恢复安全状态)+运行守卫测试。 测试:新增 95 例;整仓 13 failed / 1136 passed,13 条与基线逐字一致(零新增失败);pylint 10.00/10 (含 5 个非本任务文件的静态清理,其一为真 bug:quick_skill.py 使用未导入的 JSONResponse)。
- 「生成依据」默认收起,按验收五项对号:① 原始剧本与分镜(镜头标题 + 原文摘录)② 规范化资料(含别名合并结果) ③-a 全局资产通用资料(标注「不属于本章依据」)/ ③-b 本章按项目+章节隔离保存的依据 ④ 本次脱敏请求结构 ⑤ 最终提示词与同批差异(相似度 ≥90% 点名「高度重复:模板套用」);拿不到的行一律「本次未提供」并列出缺项,不编造。 - 数据隔离可见:资产行打「全局资产/项目内资产」标签;**全局资产写回一律先确认**(含逐槽位 before→after 差异), 角色仅在覆盖时确认;合并写回保证不抹掉其它槽位;三处保存入口统一。 - 质量拦截:不可用四情形绝不显示「提示词已就绪」,被拦项一个都不提交;不可用提示词不许保存。 - 不得自动覆盖:人工提示词需显式确认;保存/生成不碰图片与定版字段。 - 道具:prop_image_front 平权可用,不再出现「不支持(无槽位)」;批量按类型分组逐组提交。 - 批量生成失败即停、不自动重试(保留「重试失败项」为人工动作)。 门禁:tsc 0;node --test 377/377;build 成功;只读页面自查无黑名单字段、无付费请求。
用户口径(正式使用不可接受的三件事):清单只活在进程内缓存里 → 重启就要再花一次钱;
章节资料依附候选行 payload → 重新提取(replace_for_shot 先删后建)把它清空;
已经付费生成并人工确认的结构资料会因此丢失。
- 专用表(新增表结构,按项目 + 章节隔离):chapter_asset_profiles(资产类型/规范名/别名/结构化画像/
人工修改/用户补充/剧本片段与分镜依据/已关联真实资产 ID/剧本来源签名/生成状态与时间)、
chapter_asset_profile_runs(内容签名 + 运行元信息 + 技术详情)。数据库是事实来源,进程内缓存降级为性能优化。
- 逐条对账(不删除、不静默覆盖):新资产插入;未确认且未人工改过的行更新;**已确认/人工改过的行只把新结果
落到 pending_* 并标 pending_change**,由用户显式决定 覆盖 / 合并 / 保留(POST .../decisions);
本次分析没再提到的行保留并标 missing_in_latest。剧本/分镜变化 → 标「内容已变化,建议重新分析」+ 中文提示。
- 读写路径换轨:confirm 直接读库(重启后不再 409、不再让用户重复付费分析);图片提示词的资产资料装载
(load_asset_profile_enrichments)改为专用表优先、旧候选 payload 兜底;章节隔离层(overlay)读入口同口径。
GET /asset-profiles 变为**只读**:库里没有就如实给「未生成 + 中文引导」,绝不偷偷花钱。
- 新接口:GET .../asset-profiles/records(事实来源读入口)、PATCH .../records/{id}(人工修改进
manual_overrides / user_notes,模型结果永不覆盖)、POST .../asset-profiles/decisions(覆盖/合并/保留)。
- 迁移/回滚/幂等/自动备份:scripts/{_chapter_asset_records,migrate_chapter_asset_records,
rollback_chapter_asset_records}.py,DDL 与 ORM 逐列一致(测试断言),--check 只读,执行前 SQLite backup,
并把旧结构(候选 payload 的 chapter_overlay / asset_profile)搬进新表(同一资产多镜头合并、依据取并集)。
- 出口白名单(验收安全):JELLYFISH_ALLOWED_OUTLETS=llm 让"只授权文本模型"变成代码层面的拦截,
图片/视频/OSS 出口返回 409 outlet_not_allowed,不依赖操作者自觉;未设置时行为与之前完全一致。
- 前端:「生成依据」来源码补齐(chapter_record 系列 + 旧 overlay 兜底),未登记的内部标识不再直接摊给用户。
- 测试:新增持久化 9 例(文件型库真·模拟重启、注入"一调用就报错"的模型桩做证伪式验证)、迁移 8 例、
出口白名单 5 例、路由 7 例;受影响既有用例按新语义更新。
真实验收(第 1 次整章分析 + 确认)暴露的真实缺陷:**两个角色一个都没建出来**, 随后按资产的图片提示词直接 400「实体画像里找不到:['姜岁欢']」。 根因(一层一层查下来的): - 上游存在性检测 `entity_existence.check_names_existence` 用的是**子串匹配** (`name ILIKE '%查询名%'`,服务「新建资产」时的"是不是已经有了"提醒); - 库里存在服装「姜岁欢常服」「秦老夫人常服」,于是查询「姜岁欢」命中了一件**服装**; - 本章冲突判定把它当成 `same_name_other_type`(同名异类型)→ `needs_review` → 人工项, 而脚本按"冲突项需人工"的纪律**不会自动建**,也不会关联,于是角色永远出不来。 修复:命中一律回查库里资产的**真实名称**,只有归一化名称**完全相等**才算"同名": - 完全同名(异类型)→ 仍是冲突(回归用例锁住,修复不能顺手放过真冲突); - 子串命中 → 降级为**提示** `partial_name_match_in_library`(如实点名库里那件资产的名字与类型), 不冲突、不关联、不阻塞自动确认(同类子串命中同样只提示、不自动关联到别的东西)。 另:验收脚本新增 `--skip-analysis` 续跑模式(失败后不重复花钱)。 第 1 次分析结果已持久化在库里,续跑时先只读断言 `persistence.generated=true` 且 `llm_called=false`(证明用的是数据库、没有再调模型), 库里没有就**直接退出**;报告新增 `calls_issued` / `authorized_total`, 避免把"续跑 4 次"误读成"又花了 5 次"。缺 `--confirm-authorized` 仍然一次都不发。 测试:新增 3 例完全/子串同名判定 + 3 例续跑闸门与计数;受影响既有用例不变。
- docs/real-run-mode.md 新增「5.1 只授权部分出口:JELLYFISH_ALLOWED_OUTLETS」: 取值口径(逗号分隔 / none 全禁 / 未设置=不额外限制 / 空值按未设置)、 判定顺序(真实模式 → 白名单)、状态接口怎么取证、被拦时的 409 形状(paid_call_made=false)。 - 验收脚本:删掉未用 import、为守卫注册 import 与"多失败分支如实 return"加上明确的 pylint 说明(scripts/ 不在既有 lint 范围内,但既然改了就把话说明白)。
页面验收发现的缺口:「生成依据」面板的数据只来自**一次提示词生成响应**,
所以"还没生成"时只能显示「本次未提供生成依据」——用户看不到这份资产有什么资料,
也没法在花钱之前核对(而这一轮恰恰要求"不点真实生成也要能看到依据")。
- 只读出图计划(`POST /image-pipeline/plan/preview`)新增可选 `chapter_id`:
给了就按**项目 + 章节**装配每个 target 的 `generation_basis`(前端本来就认这个字段名,
不必改读取逻辑):项目风格 / 该资产结构化资料(**含人工修改与用户补充**)/ 剧本片段与
出场分镜 / 资料来自哪里(`structured_source`)/ 类型出图要求 / 本次会用的提示词。
- 依据与图片提示词链路**同源**:读的就是 `chapter_asset_profiles`(按项目 + 章节持久化那份),
所以页面看到的和模型拿到的是同一份资料,不是页面自己拼的另一套。
- 没给 `chapter_id` → 不装配(`generation_basis={}`),**旧调用方行为一个字不变**;
本章没有该资产资料行 → `asset_profile` 留空 + 如实说明来源,**不编造**。
- 前端:计划预览请求带上当前集(`?chapter=`)。
测试:新增 3 例(给了章节带依据且人工修改生效 / 没给章节不带 / 没有资料行不编造)。
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Jellyfish 视频生产流程:集级提示词 / 资产绑定 / 交付出口 与真实链路收口
一、已完成(代码 + 测试 + 页面或接口证据)
主流程
http(s):///asset://的引用才算可用),集级就绪接口、计划预检与生成按钮共用;提交端有第二层兜底。EpisodeVideoPromptBoard):批量生成逐镜队列、批量导入(巨日禄 / 其他外部平台)、同一张预览确认表、覆盖三模式、保存来源白名单。真实链路(已真实验收,含任务号/文件 ID)
gpt-image-2)→ 采纳 → 设为定版(character_images.id=42)。task_01M2Z8V3VPWMSJ9QC5XVQNGMM1,55.5 s,落shot_frame_images.first,OSS 匿名 200 / 3,181,915 B,视频方案usable=true/ref_kind=public。shots.generated_video_file_id。2933350、镜头 1–31 连续、31 条提示词非空、来源全部jurilu、无另外两组混入、零付费调用。第二步真正的编码是 msgpack(
data.enc/payload),已用纯标准库解码器修好(原先「解析 0 条」被误读成接口拒绝)。JELLYFISH_DRY_RUN/JELLYFISH_REAL_LLM_CONFIRMED现支持 进程环境变量 →backend/.env→ 默认 的优先级;未配置默认演练;.env打开真实模式时启动打醒目中文告警;状态接口与页面角标显示配置来源(只读,普通业务页面没有一键切换真实模式的开关);独立进程测试覆盖 4 种情形。s3_public_base_url时不再拼 path-style 假公网地址,而是显式告警 + 修法);上传后做匿名可达性验证,不可达不阻断上传但如实告诉用户「已入库、但该地址上游会被拒」。/film/tasks/video在建任务/写库之前复用同一套素材可达性预检(不可达 → 409 + 真实状态码 + 修法 +paid_call_made=false),派发改为同进程内联(不再无条件进 Celery 死队列);AI 首帧提示词任务同样内联,不再留下永久 pending。partial_failed现在显示为「完整成功 0 / 部分成功 1 / 失败 0」;无法识别的状态显式写「N 条状态未识别」,不再出现有结果却显示 0/0。/prompt-board/{chapter}/save正式接收单数script_id,origin=jurilu_import时强制「缺少/非法/像多个 → 400、本批每条必须同组、校验先于任何写库」,并明确拒绝已废弃的script_ids(不再静默忽略);OpenAPI 客户端已重新生成并核对无删除。real_unconfirmed(关演练但没确认)下 studio LLM 编排链路原先会跳过付费确认真的发出请求,已修成无条件过守卫;出站兜底改为显式开关JELLYFISH_NETWORK_GUARD=1并按状态接口如实回报(默认不装,避免静默改变本地只读用法)。二、仍未验证(只保留确实没有证据的)
http(s)与asset://携带、本机/内网地址排除并给原因);音色/口型/是否真按参考音频改变未验证;asset://无真实素材跑通样本。partial_failed的真机渲染:DRY_RUN 下 submit 只回占位,该行真机颜色只有单测 + fixture 证据。image_task_runner)未加匿名可达性探活:有意不加(worker 落库无用户可见告警通道,且会给持久化事务引入出网依赖)。refresh_cache:true绕过缓存、九槽位占位文本可被保存成正式提示词、入口 6/7 服务端缺幂等、4 个/studio/image-tasks/*仍默认入队、前端共享 API 层丢how_to_enable、/adopt无 host 白名单/无幂等 —— 确认存在,本轮未改。三、有意未重复执行(零新增付费)
真实大模型调用、真实出图、真实首帧、真实 Seedance 视频、巨日禄真实抓取与整组保存:本轮没有再次花费,只沿用既有任务号 / 文件 ID / 数据库行 / 页面证据 + 接口负例 + 代码检查。本轮唯一新增的真实调用是此前已授权范围内的 3 次(逐镜视频提示词草稿 2 次文本 LLM + 首帧出图 1 次),此后全部验证在 DRY_RUN 下完成。
四、既有非本分支问题
ApiResponse恒带meta: None,而旧断言要求恰好三键(test_api_response_envelopes/test_entities_api_responses/test_entity_existence_api_responses/test_files_api_responses/test_shot_character_links_api_responses/test_shot_subresource_api_responses/test_skills_integration::test_health_returns_ok)。已用全新临时库单独复现,确认与本 PR 无关;本 PR 保证不新增失败。urllib,而出站兜底只 patchhttpx(DRY_RUN 下仍会真实访问第三方)——既有行为,本轮只把兜底变成显式开关并写清局限。五、验证方式(可复核)
tsc --noEmit通过、npm test210/210、npm run build成功。pylint --max-line-length=10010.00/10。--check」。git diff --check:只有生成器原生文件的 EOF 空行提示,按约定不手工格式化。dry_run=true / real_call_confirmed=false(四出口全部拦截),验收数据未清理。