# FSD M2 · 任务与拆解

LLMS 索引： [llms.txt](/llms.txt)

---

> [← 总则](/qmtv/fsd-qmtv/00-%E6%80%BB%E5%88%99/) ｜ [附录 →](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/) ｜ 上游：[PRD §4 M2](/qmtv/product/prd-qmtv-v1/)
> 实现落点：`backend/internal/domain/task/`（model/repo/service/service_gen）+ `cmd/server/task_handlers.go`（38 handlers 全集）+ `cmd/server/task_assembly.go`（LLM/模板装配）+ `cmd/server/routes.go:324-361`（挂载）。事实基准=Go 代码；冻结版 `qmtv/server/index.js:228-366`/`routes2.js`/`routes3.js` 仅差异对拍参照。

### M2 任务与拆解

38 端点全量（基础信封见总则 §2.2）。本域是创作主链的起点：剧本 → 拆解（规则/LLM 双引擎）→ 场景/分镜 → 资产提取与生图。计费面集中在拆解（token 制/按次 20）与图片生成（动态价）；SSE 仅 `split` 一类事件；上传面 2 条（slots/assets，20MB）。属主判定全 SQL 谓词，命中失败一律 404 不用 403。分镜双表通路中本域只写 `shots`（`director_shots` 归 M10，零触及）。

#### F2.01 任务列表

> 对应 PRD：§4 M2 · GET /api/tasks ｜ 状态：实现 ｜ 实现落点：task_handlers.go:51-56、task/repo.go:120-125 ｜ UI：`/dashboard`（DashboardView 任务列表）｜ 场景挂钩：E2E-DASH-01（A1 回归锁：wire=驼峰 shotCount）/E2E-NAV-01

##### 1 功能定义

当前用户任务列表倒序读，每行附镜数（`shotCount` 驼峰 wire——二十六期 A1 回归根因，e2e 回归锁钉死）。

##### 2 触发条件与前置状态

- 进入拆解台/任务列表刷新 → `GET /api/tasks`（Bearer）；降级模式 503。

##### 3 输入与校验

无参数。身份取自 JWT claims。

##### 4 处理流程

`SELECT t.*, (子查询 COUNT shots) AS shot_count … WHERE t.user_id=? ORDER BY t.created_at DESC` → TaskWithCount 数组。宽扫描 `t.*` 依赖 0002 DDL 收紧（W8b 守卫 integration_test.go:461-484）。

##### 5 输出与结果状态

`200 {"ok":true,"data":[TaskWithCount…]}`。行 wire：`id/user_id/title/status/error/script_type/face_style/min_duration/max_duration/notes/extract_status/assets_data/split_status/style_id/extract_scope/extract_notes/created_at/updated_at`（蛇形）+ **`shotCount`（驼峰，model.go:60-63）**；`script_text` 指针 nil→JSON null。空列表 `data:null`（gorp nil slice——冻结恒 `[]`，DIF-29-1）。

##### 6 状态流转

无（纯读）。`status` 取值域见 F2.03 §6。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 无任务 | 200 `data:null`（非 `[]`，DIF-29-1） | — |
| DB 错误 | 500 | `internal server error` |

##### 8 权限与数据规则

- 权限：登录态；`WHERE user_id=?` 谓词内联。
- 数据规则：倒序 `created_at DESC`（无次级排序键，同秒行序可能抖动——冻结同构）。

#### F2.02 剧本拆解·旧版同步

> 对应 PRD：§4 M2 · POST /api/tasks ｜ 状态：实现 ｜ 实现落点：task_handlers.go:62-82、task/service.go:148-167/301-327 ｜ UI：拆解台旧版提交入口｜ 场景挂钩：无（V2 为主链，本端点为冻结 legacy 保真）

##### 1 功能定义

同步规则拆解：扣固定 20 积分 → 确定性 preprocess 切分场次/分镜/资产 → 立即返回统计。无 LLM、无异步。

##### 2 触发条件与前置状态

- 拆解台提交（legacy 形态）→ `POST /api/tasks`（Bearer）。

##### 3 输入与校验

| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| title | JSON body | 否 | 缺省「未命名」，截 60 rune（冻结 INSERT 用「未命名剧本」而台账 desc 用「未命名」——Go 把 desc 缺省同化进 title，DIF-29-13） |
| script_text | JSON body | 是 | `TrimSpace==""` → 400 `剧本内容不能为空`（冻结 index.js:236 同文案） |

##### 4 处理流程

```mermaid
flowchart TD
  A["POST /tasks"] --> B["剧本非空校验"]
  B --> C["TryDeduct 20<br/>type=preprocess<br/>「剧本拆解：<title>（-20）」"]
  C -->|不足| D["402 积分不足"]
  C --> E["INSERT tasks status=complete"]
  E --> F["fillWithMock：preprocess 确定性切分"]
  F --> G["InsertAssets + InsertShots + 每镜 slot1 故事板"]
  G --> H["200 {id,sceneCount,shotCount}"]
```

扣费在插任务前，Insert 失败不退（service.go:159-163，冻结同序）。三次 Insert 均 `_ =` 吞错（service.go:314/318/324）。INSERT 显式写 18 列零值（`script_type:''`/`min_duration:0`/`split_status:''` 等覆盖 DDL 默认——冻结走 SQLite DEFAULT，DIF-29-5）。

##### 5 输出与结果状态

`200 {"ok":true,"data":{"id","sceneCount","shotCount"}}`（**sceneCount/shotCount 驼峰**，task_handlers.go:77-81；冻结 index.js:252 同）。

##### 6 状态流转

`tasks.status`：插入即 `'complete'`（无 processing 中间态）；`split_status`：INSERT 显式写 `''`（18 列零值，DIF-29-5——非 DDL 默认 `'done'`）；本端点不消费该列。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 剧本空白 | 400 | `剧本内容不能为空` |
| 余额 <20 | 402 | `积分不足`（零台账） |
| Mock 拆解 | 确定性：场次按「第X场」标记切块（无标记按空行 >10 rune）；每场 3~4 镜；人物资产 2~3 汉字冒号启发式 ≤8；提示词走 promptcenter 模板（tpl 查库单级，景别 index%4 轮转） | — |

##### 8 权限与数据规则

- 权限：登录态；创建即本人（user_id 取 claims）。
- 数据规则：写 `tasks/shots/assets/shot_images/credit_records` 五表。

#### F2.03 剧本拆解·V2 双引擎

> 对应 PRD：§4 M2 · POST /api/tasks/v2 ｜ 状态：实现 ｜ 实现落点：task_handlers.go:140-160、task/service.go:241-299/330-366 ｜ UI：拆解台主提交（DashboardView）｜ 场景挂钩：E2E-DASH-01（Mock 同步分支：toast「拆解完成」+镜数>0）

##### 1 功能定义

token 制计费的剧本拆分：LLM 可用走异步拆解（SSE `split` 事件+站内通知，失败回退规则拆解）；LLM 不可用走规则拆解同步返回。计费 `ceil(tokens/1000)` 上限 1。

##### 2 触发条件与前置状态

- 拆解台提交（主链）→ `POST /api/tasks/v2`（Bearer）。
- LLM 可用判定 `llm.Config.Available()`（三元全非空，llm.go:24）；配置三级链 active_model_text→model_endpoints→KV llm_*（task_assembly.go:49-59）。

##### 3 输入与校验

| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| title | JSON body | 否 | 缺省「未命名」截 60 |
| script_text | JSON body | 是 | 空 → 400 `剧本内容不能为空`。**唯一校验** |
| script_type / face_style | JSON body | 否 | orDefault 缺省 `domestic`/`asian_chinese`——**无冻结二值化归一**（任意值透传，DIF-29-14） |
| min_duration / max_duration | JSON body | 否 | maxInt(入参,1)/maxInt(入参,3) |
| notes / style_id | JSON body | 否 | style_id 缺省 `realfilm` |

##### 4 处理流程

```mermaid
flowchart TD
  A["POST /tasks/v2"] --> B["剧本非空校验"]
  B --> C["token 估算：max(1000, ceil(rune数×1.6))<br/>cost=ceil(est/tokensPerCredit) <1 归 1"]
  C --> D["TryDeduct type=preprocess task_id=id<br/>「剧本拆分（约N tokens / 模型名，-cost）」"]
  D --> E["INSERT tasks status=processing<br/>split_status= llm可用?processing:done"]
  E --> F{"llm.Available()?"}
  F -->|否| G["fillWithMock + status=complete<br/>同步返回 sceneCount/shotCount"]
  F -->|是| H["go runLLMSplit（异步）<br/>响应 {id,async:true,engine,cost,tokens_estimated}"]
  H --> I["SplitTemplate 三级 → LLM Chat 240s → ParseSplit"]
  I -->|成功| J["三删重建 shots/assets + slot1<br/>SetSplitDone + 通知「剧本拆解完成」+ SSE split done"]
  I -->|失败| K["fallbackMock：三删+fillWithMock+SetSplitDone+complete<br/>通知「AI 拆解失败，已回退规则拆解」+ SSE split fallback"]
  K -->|回退也失败| L["status=failed + split_status=failed"]
```

runLLMSplit 后台 goroutine `defer recover()` 兜底（service.go:332）；重建三删序 ClearShotImages→DeleteShotsByTask→DeleteRootAssetsByTask（service.go:411-419）。

##### 5 输出与结果状态

`V2Result` 键序 `id, sceneCount(omitempty), shotCount(omitempty), async, engine, cost, tokens_estimated`（service.go:243-251）：LLM 分支无 sceneCount/shotCount 键（冻结同）；Mock 分支 `async:false` + 两计数。

##### 6 状态流转

- `tasks.status`：`processing`（仅本端点插入）→ `complete`（Mock/LLM 完成/回退成功/restart/启动恢复）｜`failed`（LLM 双重失败、mark-failed）。
- `tasks.split_status`：DDL 默认 `'done'`｜`'processing'`（LLM 可用）→ `'done'`（完成/回退）｜`'failed'`（双重失败）。**代码无 `'idle'`/`'fallback'` 写入点**（PRD :142 状态机表含此二态——PRD 该行已加注「冻结与 Go 均无写入点」，用户裁决〔三十期〕判定非系统需要不实现：前端零消费〔DashboardView 仅认 processing/failed〕+fallback 系同步内联回退无可观察窗口，勿照抄亦勿实现）。
- 服务重启悬挂行：`RecoverProcessingSplits`（service.go:444-463）扫 `split_status='processing'` → fillWithMock 直插不删基座 → 恢复完成，通知「拆解已回退完成」，**无 SSE 帧**。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 剧本空白 | 400 | `剧本内容不能为空` |
| 余额不足 | 402 | `积分不足` |
| LLM 失败 | 不回 HTTP（异步已 200）；回退规则拆解+通知+SSE fallback | — |
| 回退再失败 | status/split_status 双 failed（已扣费不退——冻结同） | — |
| token 估算 | rune 数×1.6（冻结 UTF-16 `.length`，BMP 外计数不同，DIF-29-15）；计费 ceil（冻结 round——`credits.TextCost` 正确 round 实现存续未用 billing.go:118-131，DIF-29-6） | — |
| 模型名缺配置 | 台账 desc 出现「…tokens / ，-N）」空串形态（冻结缺行回「星芒文本引擎」，DIF-29-7） | — |

##### 8 权限与数据规则

- 权限：登录态；写 `tasks/shots/assets/shot_images/credit_records/notifications`。
- 数据规则：TryDeduct 带 task_id=id（冻结 routes2.js:158 同）；通知两文案冻结逐字（service.go:352-353/359-361）。

#### F2.04 任务详情读取（:id / full / full2）

> 对应 PRD：§4 M2 · GET /api/tasks/:id、GET /api/tasks/:id/full、GET /api/tasks/:id/full2 ｜ 状态：实现 ｜ 实现落点：task_handlers.go:84-97/275-297、service_gen.go:300-427 ｜ UI：VideoView/DashboardView 任务详情｜ 场景挂钩：E2E-NAV-01（读面巡检）

##### 1 功能定义

任务详情三形态：基础（shots+原始 assets）、full（assets 带图 URL+assets_data 缓存）、full2（variants 衍生体+风格名/缩略图+组间尾帧衔接）。

##### 2 触发条件与前置状态

- 详情页/分镜工作台加载 → 三端点（Bearer）；任务不存在或非本人 → 404。

##### 3 输入与校验

路径 `:id`。无 body。

##### 4 处理流程

- **:id**：`ByID`（`WHERE id=? AND user_id=?`）→ `ShotsWithSlots`（shots ORDER index_no + 槽位按 shot 分组 ORDER slot_no + refAssetCount=COUNT assets）→ 原始 assets（**无 image_url**，task_handlers.go:95——冻结 index.js:261 同 quirk）。
- **full**：ByID → ShotsWithSlots → AssetsByTask（LEFT JOIN images 取 image_url）→ assets_data=缓存优先（坏 JSON 宽容 nil）否则从根资产现拼（**5 键 item**：id/name/description/image_id/image_url——冻结塞原始全列行，DIF-29-16）。
- **full2**：再加 VariantsByTask（parent_id<>''）合并进缓存/fallback；`tail_by_group` 组间尾帧 map（组内最后有图镜 image_id，首镜且 groupNo>1 配 `prev_tail_image_id`，service_gen.go:355-372）；`style_name`=StyleName（19 风格目录缺省「真人电影」）、`style_thumb` 幂等落盘 `style_<id>.svg`（不在目录→null）。

##### 5 输出与结果状态

- :id → `{"task","shots":[ShotView],"assets":[原始行]}`；shots 空为 `[]`（make 初始化）、slots/assets 空 `null`（DIF-29-1）。
- full → `{"task","shots","assets","assets_data"}`。
- full2 → `{"task":{…Task,"style_name","style_thumb"},"shots","assets","variants","assets_data","tail_by_group"}`——**style_name 嵌套在 task 内**（前端 curTask.style_name 消费）。

##### 6 状态流转

无（纯读）。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非本人/不存在 | 404 | `not found` |
| assets_data 缓存坏 JSON | 静默忽略走 fallback 拼装 | — |
| style_id 不在目录 | style_name=「真人电影」、style_thumb=null | — |

##### 8 权限与数据规则

- 权限：登录态 + 任务属主。
- 数据规则：ShotView 含 `slots`/`refAssetCount`（驼峰）/`prev_tail_image_id`（full2 组首镜）；variants 键映射 parent_id→item 数组。

#### F2.05 任务生命周期管理（title/delete/duplicate/mark-failed/restart）

> 对应 PRD：§4 M2 · PUT /api/tasks/:id/title、DELETE /api/tasks/:id、POST /api/tasks/:id/duplicate、POST /api/tasks/:id/mark-failed、POST /api/tasks/:id/restart ｜ 状态：实现 ｜ 实现落点：task_handlers.go:99-138、task/service.go:168-230/583-587、task/repo.go:146-175/234-248 ｜ UI：任务卡操作菜单｜ 场景挂钩：无独立 e2e

##### 1 功能定义

任务五个管理动作：改名、删除（三删序+孤儿保留）、复制（含分镜/资产快照）、标记失败、重启清图。

##### 2 触发条件与前置状态

- 任务卡操作 → 对应端点（Bearer）；title/delete/duplicate 404 面，mark-failed **无存在性检查**。

##### 3 输入与校验

| 端点 | 输入 | 校验 |
|---|---|---|
| PUT /tasks/:id/title | `{"title":string}` | 截 60 rune；无 404 面（0 行仍 200，冻结同） |
| DELETE /tasks/:id | — | ByID 谓词内联；0 行 → 404 `not found` |
| POST /tasks/:id/duplicate | — | ByID 404 面 |
| POST /tasks/:id/mark-failed | — | **无校验**（0 行也 200，冻结 routes2.js:250 同） |
| POST /tasks/:id/restart | — | ByID 404 面 |

##### 4 处理流程

- **title**：`UPDATE tasks SET title=?, updated_at=NOW(3) WHERE id=? AND user_id=?`。
- **delete**：三删序 shots→assets→tasks（子查询属主谓词）；**不删 shot_images/image_histories/images/videos（孤儿保留，repo.go:151 注释冻结 §1A）**。
- **duplicate**：新任务 title=`原题+"-副本"` 截 60（DIF-29-10），status/script_text 沿源（nil 透传）；逐镜复制（image_id/video_id 清空）；**仅复制源镜存在 slot_no=1 的槽位**（service.go:200-204，冻结每镜无条件插——DIF-29-10）；assets 全复制但 parent_id 不带（衍生变根资产，冻结 index.js:290-292 同 quirk）。
- **mark-failed**：`UPDATE tasks SET status='failed'`；不触 split_status。
- **restart**：`RestartClears` 四步——shots.image_id=''+status='idle'、shot_images.image_id=''、tasks status='complete'+extract_status=''+assets_data=''、assets.image_id=''；**保留 shots/assets/槽位行**（NULL 语义翻译 ''，model.go:4-5）。

##### 5 输出与结果状态

五者均 `{"ok":true,"data":true}`；duplicate 另返 `{"id":"<新任务>"}`。

##### 6 状态流转

- delete：行删除（级联仅 shots/assets 两层）。
- mark-failed：`complete|processing → failed`。
- restart：`任意 → complete`；shots `done → idle`；extract_status → `''`。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| title 改他人任务 | 0 行 200 true（无存在性检查） | — |
| delete/mark-failed 非本人 | delete 404；mark-failed 0 行 200 | `not found` |
| duplicate 长 title | 截 60（冻结不截，DIF-29-10） | — |

##### 8 权限与数据规则

- 权限：登录态；delete/duplicate/restart 谓词内联属主。
- 数据规则：restart 是「清成果不删结构」语义——与 delete（删结构留孤儿）互补。

#### F2.06 智能资产提取（extract / extract2 / extract-settings）

> 对应 PRD：§4 M2 · POST /api/tasks/:id/assets/extract、POST /api/tasks/:id/assets/extract2、PUT /api/tasks/:id/extract-settings ｜ 状态：实现 ｜ 实现落点：task_handlers.go:174-208、service_gen.go:34-159 ｜ UI：拆解台资产区「智能提取」｜ 场景挂钩：无 e2e（人工清单 T2 族）

##### 1 功能定义

对拆解产出的角色/场景/道具资产批量生图：v1 按资产类型模板出图并写 assets_data 缓存；v2 支持 scope 范围+自定义要求+风格前缀；extract-settings 持久化提取范围与要求。

##### 2 触发条件与前置状态

- 资产区按钮 → extract/extract2（Bearer）；设置面板保存 → extract-settings。前置：任务已有资产行。

##### 3 输入与校验

| 端点 | 输入 | 校验 |
|---|---|---|
| extract (v1) | 无 body | 资产为空 → 400 `该任务没有可提取的资产` |
| extract2 (v2) | 无 body | scope 内（根资产 ∩ extract_scope）为空 → 400 `提取范围内没有资产（可在自定义提取设置中调整范围）` |
| extract-settings | `{"scope":[...],"notes":string\|null}` | scope 白名单过滤 character/scene/prop 后 join（空数组→''，冻结同）；notes 非 null 才覆盖 |

##### 4 处理流程

```mermaid
flowchart TD
  A["POST /assets/extract(2)"] --> B["ByID 属主 404 面"]
  B --> C{"有资产?"}
  C -->|否| D["400"]
  C --> E["先 UpdateExtractResult done,''"]
  E --> F["逐资产：AssetPrompt 模板+风格前缀（v2）"]
  F --> G["images.Generate（Mock SVG/真实通道）"]
  G -->|单张失败| H["退单价 + 502 文案"]
  G -->|成功| I["InsertImage kind=asset + UpdateAssetImage"]
  I --> J["UpdateExtractResult done, assets_data JSON"]
```

- 计费：unit=ImageCost（settings `image_generation` 默认 10）；v1 cost=unit×资产数（含衍生体），desc `智能资产提取（N 个资产 × 10，-<cost>）`；v2 cost 同式，desc `智能资产提取 ×N（风格：<风格名>，-<cost>）`；type=`image`。
- **v2 prompt**=「视觉风格：<风格名>。」+AssetPrompt+（ExtractNotes?`\n[生成要求] <notes>`）。
- v1 失败 502 文案金额用**总额**（冻结 v1 用单价——退款金额本身正确，DIF-29-2）；v2 失败退全额（冻结 v2 传总额笔误的保真对齐，service_gen.go:121-123 注释）。

##### 5 输出与结果状态

`{"extract_status":"done","assets_data":{"characters":[],"scenes":[],"props":[]}}`；item 键 `id/name/description/image_id/image_url/prompt`——**v2 冻结显式带 `variants:[]`，Go omitempty 键缺失（DIF-29-3）**。

##### 6 状态流转

`tasks.extract_status`：`''` → `'done'`（成功路径先行置位，失败不回滚——下次重试覆盖）；assets_data 缓存同步覆盖。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 无资产 | 400 | `该任务没有可提取的资产` |
| scope 内为空 | 400 | `提取范围内没有资产（可在自定义提取设置中调整范围）` |
| 余额不足 | 402 | `积分不足` |
| 单张生成失败 | 退单价 continue + 最终 502 | `图片生成失败：<cause>（<cost> 积分已退回）` |

##### 8 权限与数据规则

- 权限：登录态+任务属主；写 `images/assets/tasks/credit_records`。
- 数据规则：v2 targets=根资产（parent_id=''）∩scope；衍生体在 v1 中也计费（含衍生体）。

#### F2.07 AI 重新拆解（resplit）

> 对应 PRD：§4 M2 · POST /api/tasks/:id/resplit ｜ 状态：实现 ｜ 实现落点：task_handlers.go:514-533、service.go:488-573 ｜ UI：分镜区「AI 重新拆解」｜ 场景挂钩：无 e2e

##### 1 功能定义

按自然语言指令对已有分镜做密度调整重切：`更多/细致/丰富`→每场 5 镜、`精简/减少/合并`→每场 2 镜、缺省 3~4；固定扣 20 积分。

##### 2 触发条件与前置状态

- 分镜区操作 → `POST /api/tasks/:id/resplit`（Bearer）；前置：任务存在（不要求 complete）。

##### 3 输入与校验

| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| instruction | JSON body | 否 | trim 后密度匹配；无 400 面 |

##### 4 处理流程

ByID → 扣 20（desc `AI 重新拆解（<instruction截16>）（-20）`，空指令 `AI 重新拆解（-20）`，type=preprocess）→ 三删重建（同 F2.03）→ preprocess 密度调参重切 → 重插 shots（每组首镜 independent 余 anchor_prev）+slot1 → SetSplitInstruction（存 **trim 值**）。

##### 5 输出与结果状态

`{"instruction","sceneCount","shotCount"}`——**instruction 回显的是请求原始值非 trim 值**（task_handlers.go:524-532；冻结回显 trim 后值，DIF-29-8）。

##### 6 状态流转

shots 全量重建（id 全新）；split_instruction 列更新；status/split_status 不变。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非本人 | 404 | `not found` |
| 余额 <20 | 402 | `积分不足` |
| 指令无密度词 | 缺省密度 3~4 镜/场 | — |

##### 8 权限与数据规则

- 权限：登录态+属主；写 `shots/shot_images/tasks/credit_records`。

#### F2.08 分镜图批量与续生成（continue / auto-generate / cancel / storyboard-batch）

> 对应 PRD：§4 M2 · POST /api/tasks/:id/continue、POST /api/tasks/:id/scenes/auto-generate、POST /api/tasks/:id/scenes/auto-generate/cancel、POST /api/tasks/:id/storyboard-batch ｜ 状态：实现 ｜ 实现落点：task_handlers.go:299-334/346-357/535-553、service.go:590-703、service_gen.go:634-695 ｜ UI：分镜区批量操作｜ 场景挂钩：无 e2e（人工清单 T2/T3 族）

##### 1 功能定义

四个批量/续生成入口：continue 补缺图镜、auto-generate 带连续性提示词串联生成、cancel 纯 stub 停止提示、storyboard-batch 按模式（全部/补缺/选中）批量生图。

##### 2 触发条件与前置状态

- 分镜区批量按钮 → 对应端点（Bearer）。

##### 3 输入与校验

| 端点 | 输入 | 校验 |
|---|---|---|
| continue | 无 body | 404 面；无缺图镜时 cost=0 不扣（len>0 才 TryDeduct） |
| auto-generate | 无 body | 404 面；**len=0 也 TryDeduct(0) 落 0 元台账行**（冻结 util.js:86-91 同 quirk） |
| auto-generate/cancel | 无 body | 纯 stub 无校验 |
| storyboard-batch | `{"mode":"missing"\|"selected"\|"all","shot_ids":[]}` | mode 空→missing；空列表 → 400 `没有缺失的图片`（missing）／`未选择任何分镜`（selected）；all+ids 取交集 |

##### 4 处理流程

- **continue**：pending=缺图镜（image_id=''/NULL）→ cost=unit×len（desc `继续生成未完成分镜 ×N（-<cost>）`）→ 逐镜 Generate("16:9","分镜<index_no>") → settleShotImage（InsertImage kind=storyboard+SetShotImage done+slot1 回填+InsertHistory）。
- **auto-generate**：list=pending 空则全量镜；prompt 追加连续性段——非首镜 `\n[连续性] 与上一镜头（<prevURL||前帧>）保持人物/光影/机位连续。`、首镜 `\n[起始镜头] 由智能体判断开场构图。`；desc `故事板全自动串联生成 ×N（-<cost>）`。
- **cancel**：`{"cancelled":true,"note":"已停止后续镜头的排队（已产生的费用不退）"}`，**不写任何表**。
- **storyboard-batch**：desc `批量生图（全部|仅补缺失|选中 ×N，-<cost>）`；连续性措辞与 auto-generate **不同源**（「与上一镜（<prevURL||前帧>）保持一致。」/首镜「智能体判断开场构图」）；**失败退单价**（冻结传总额系笔误，Go 统一单价——PRD v1.3 已登记，service_gen.go:632-633）。

 settle 失败（DB 错）时该张已生成不退费、仅 failed++（service.go:616-619/689-694，冻结同）。

##### 5 输出与结果状态

continue/auto-generate/storyboard-batch → `{"generated":N,"failed":N}`；cancel → 上述 stub 体。

##### 6 状态流转

成功镜：shots.status `idle → done`（SetShotImage）；失败镜 status 不动（仅计数）。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 非本人 | 404 | `not found` |
| 余额不足 | 402 | `积分不足` |
| selected 空数组 | 400 | `未选择任何分镜` |
| missing 无缺图 | 400 | `没有缺失的图片` |
| 单张失败 | 退单价 continue | 502 `图片生成失败：…` |

##### 8 权限与数据规则

- 权限：登录态+属主；写 `images/shots/shot_images/image_histories/credit_records`。
- 数据规则：三批量端点计价均为动态 unit（settings 可调）——与 F2.09 的静态价不同源。

#### F2.09 单镜故事板生成（images/storyboard/generate）

> 对应 PRD：§4 M2 · POST /api/images/storyboard/generate（v1.3 W10 归组 M2——物理在冻结 index.js 生成段）｜ 状态：实现 ｜ 实现落点：task_handlers.go:420-441、service_gen.go:581-623 ｜ UI：分镜卡「生成」/「2K 预览」｜ 场景挂钩：间接 E2E-NAV-01

##### 1 功能定义

单镜故事板图/2K 预览生成：静态价（图 10/预览 15，**无 settings 键**），可选关联 shot 回填。

##### 2 触发条件与前置状态

- 分镜卡生成按钮 → `POST /api/images/storyboard/generate`（Bearer）。

##### 3 输入与校验

| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| shot_id | JSON body | 否 | 非空→ShotByID+属主校验（err→404 `shot not found`）；**空则跳过校验也可用**（冻结 index.js:345 同） |
| prompt | JSON body | 否 | 缺省取 shot.ImagePrompt |
| ratio | JSON body | 否 | — |
| preview | JSON body | 否 | true→2K 形态 |

##### 4 处理流程

计费 cost=10（COSTS.image）/15（COSTS.image_2k **恒定无 settings 键**，billing.go:72-73）；kind='storyboard'/'preview_2k'；desc `分镜图生成（-10）`／`2K预览（-15）`（**冻结「2K预览生成（-15）」缺「生成」二字，DIF-29-9**）；task_id=shot 有则其 task 否则 ''。Generate → 失败退同额+502；成功 InsertImage（prompt=**请求原值**非解析值）→ `sh != nil && !preview` 才回填 shots/slot1/histories（linkShotImage 不重复插 images——W3 冒烟双插主键冲突修，service_gen.go:615-621 注释）。

##### 5 输出与结果状态

`{"id","url"}`。

##### 6 状态流转

关联 shot 非预览时：shots.status → done、slot1 回填、histories +1。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| shot_id 给了但不存在/非本人 | 404 | `shot not found` |
| 余额不足 | 402 | `积分不足` |
| 生成失败 | 退款+502 | `分镜图生成（-10）失败自动退回`族文案 |

##### 8 权限与数据规则

- 权限：登录态；shot 关联时校验属主。
- 数据规则：静态价与 F2.08 动态价**刻意不同源**（billing.go:72-73 注释）；Mock label=desc 全串（冻结 label「分镜图」/「2K预览」——影响 Mock SVG 文件名，DIF-29-9）。

#### F2.10 场景分组管理（scenes2 / rename / delete）

> 对应 PRD：§4 M2 · GET /api/tasks/:id/scenes2、PUT /api/tasks/:id/scenes/:groupNo、DELETE /api/tasks/:id/scenes/:groupNo ｜ 状态：实现 ｜ 实现落点：task_handlers.go:472-512、repo.go:356-371 ｜ UI：分镜区场景分组栏｜ 场景挂钩：无 e2e

##### 1 功能定义

场景分组聚合读与改名/删除：scenes2 按 group_no 聚合统计，rename/delete 按 group_no 批量作用。

##### 2 触发条件与前置状态

- 分镜区场景栏 → 三端点（Bearer）。

##### 3 输入与校验

| 端点 | 输入 | 校验 |
|---|---|---|
| GET scenes2 | — | ByID 404 面 |
| PUT scenes/:groupNo | `{"scene_title":string}` 截 60 | groupNo 非数字→**归 0**（task_handlers.go:493-494；冻结 NaN 绑定 TypeError 500——Go 200 零行，DIF-29-11） |
| DELETE scenes/:groupNo | — | 同上；0 行也 200 |

##### 4 处理流程

- scenes2：`SELECT group_no, MIN(scene_title), COUNT(*) shot_count, MIN(index_no) first_index … GROUP BY group_no ORDER BY group_no`（**MIN(scene_title) 字典序最小而非首镜标题——冻结同 SQL quirk 保真**）。
- rename：`UPDATE shots SET scene_title=? WHERE task_id=? AND group_no=?`。
- delete：`DELETE FROM shots WHERE task_id=? AND group_no=?`。

##### 5 输出与结果状态

scenes2 → `[{"group_no","scene_title","shot_count","first_index"}]`（全蛇形，空→null DIF-29-1）；rename/delete → data true。

##### 6 状态流转

delete 组删除后 shots 行消失（槽位/histories 孤儿保留）。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| groupNo=abc | 折 0 匹配零行 200（DIF-29-11） | — |
| 组不存在 | rename/delete 0 行 200 | — |

##### 8 权限与数据规则

- 权限：登录态+属主（task_id 谓词）。

#### F2.11 分镜编辑与槽位（shots / slots / shot-images / histories / shots2）

> 对应 PRD：§4 M2 · PUT /api/shots/:id、DELETE /api/shots/:id、POST /api/shots/:id/slots、DELETE /api/shot-images/:slotId、GET /api/shots/:id/histories、PUT /api/shots2/:id/params ｜ 状态：实现 ｜ 实现落点：task_handlers.go:359-470、service.go:492-501/570-578、service_gen.go:504-559、repo.go:322-354/408-437/465-470 ｜ UI：分镜卡编辑器｜ 场景挂钩：无独立 e2e

##### 1 功能定义

单镜六动作：三提示词编辑、删除、参考槽位添加（上传或生图）、槽位删除（slot1 保护）、历史读、分辨率参数。

##### 2 触发条件与前置状态

- 分镜卡操作 → 六端点（Bearer）；slots 上传限额 20MB（up20）。

##### 3 输入与校验

| 端点 | 输入 | 校验 |
|---|---|---|
| PUT /shots/:id | `image_prompt/video_prompt/transition_mode` 均**可空指针** | null=跳过该键（COALESCE 部分更新契约 §6）；属主两步查（ShotByID+task 属主）失败 404 `shot not found` |
| DELETE /shots/:id | — | 404 面；**不删槽位/histories/images** |
| POST /shots/:id/slots | multipart `file` + form `prompt/kind/ratio/label` | 无文件≠错误——**无文件走生图分支扣 10** |
| DELETE /shot-images/:slotId | — | slot_no=1 → 400 `故事板槽位不可删除`；其余 404 面 |
| GET /shots/:id/histories | — | 404 面 |
| PUT /shots2/:id/params | `{"resolution":string}` | **非 720p/1080p 静默忽略仍 200**（冻结 routes3.js:341-343 同） |

##### 4 处理流程

- **slots 双路**：有文件→images.Upload 零费；无文件→静态价 10（desc `镜头图片生成（-10）`，task_id=''）→Generate→失败退+502。成功：slotNo=MAX(slot_no)+1、InsertImage(kind=orDefault(kind,"anchor"))、InsertShotImages(label=orDefault(label,"参考图<slotNo-1>"))；生成路另 SetShotImage(done)+InsertHistory（`_ =` 吞错）。
- **shotOwned**（service.go:492-501）：ShotByID err→ErrShotNotFound；task 属主 err→同——两步均 404 `shot not found`。

##### 5 输出与结果状态

- slots → `{"slot_no":int,"image_id","url"}`；histories → `[{"id","shot_id","image_id","prompt","created_at","file_path"}]`（空→null DIF-29-1）；其余 data true。

##### 6 状态流转

- 生图路 slots/SetShotImage：shots.status → done。
- params：resolution 列更新（白名单内才生效）。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| shot 非本人 | 404 | `shot not found`（六端点同哨兵） |
| 删 slot1 | 400 | `故事板槽位不可删除` |
| slots 无文件且余额不足 | 402 | `积分不足` |
| slots 生图失败 | 退单价 | 502 |
| resolution=480p | 静默忽略 200 | — |

##### 8 权限与数据规则

- 权限：登录态+shot→task 属主链。
- 数据规则：**本域写 images 表为跨域共表直插**（10 列形态，repo.go:551-556，主归属 generation——总则 §1 术语）；histories 行 id 前缀 `si_`（冻结 `ih_`；ids.PrefixImageHistory="ih_" 仅 director 域消费（repo.go:319 InsertImageHistory），前缀双轨并存——DIF-29-4，99-附录:57）。

#### F2.12 资产图编辑与衍生（assets upload / regenerate / derive）

> 对应 PRD：§4 M2 · POST /api/assets/:id/image/upload、POST /api/assets/:id/regenerate、POST /api/assets/:id/derive ｜ 状态：实现 ｜ 实现落点：task_handlers.go:213-272、service_gen.go:162-276 ｜ UI：资产卡操作｜ 场景挂钩：无 e2e

##### 1 功能定义

资产图三动作：手动上传替换（同步 assets_data 缓存）、局部修改生图、衍生变体（新建子资产行）。

##### 2 触发条件与前置状态

- 资产卡操作 → 三端点（Bearer）；upload 限额 20MB。

##### 3 输入与校验

| 端点 | 输入 | 校验 |
|---|---|---|
| upload | multipart `file` | 无文件 → 400 `缺少文件`（冻结 routes2.js:217 同文案）；资产 404 `not found` |
| regenerate | `{"instructions":string}` | trim 空 → 400 `请填写局部修改说明` |
| derive | `{"variant_desc":string}` | trim 空 → 400 `请描述变体（如：换一套服装 / 中老年形象）` |

##### 4 处理流程

- **upload**：AssetByID（JOIN tasks 属主）→ Upload（FixFilename）→ InsertImage(kind='asset') → UpdateAssetImage → **同步 assets_data 缓存**（读改写 JSON 三组逐项改 image_id/image_url，坏 JSON 静默跳过）。
- **regenerate**：扣 unit（desc `资产图局部编辑：<名>（-<unit>）`）→ prompt=AssetPrompt+`\n[局部修改] <ins>（其余部分保持原图不变）` → Generate → 失败退+502 → InsertImage(kind='asset_edit') → UpdateAssetImage（**不写缓存**，冻结同）。
- **derive**：扣 unit（desc `角色衍生：<名>·<variant截12>（-<unit>）`）→ **先插子资产行**（name=`<名>_衍生<N>`，parent_id=源）→ prompt=AssetPrompt+`\n[衍生变体] <v>。保持角色五官与身份一致，仅按描述改变形态。视觉风格：<风格名>。` → Generate → 失败退+502（**子资产行残留无回滚**——冻结同序 routes3.js:170-172）→ InsertImage(kind='asset_variant') → UpdateAssetImage(子)。

##### 5 输出与结果状态

upload/regenerate → `{"image_id","url"}`；derive → `{"asset_id","image_id","url","name"}`。

##### 6 状态流转

- derive：assets 树新增子节点（parent_id 链）。
- upload：assets.image_id 替换+缓存同步；regenerate 仅替换 image_id。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 缺文件 | 400 | `缺少文件` |
| 说明/变体描述空 | 400 | 上述两句 |
| 余额不足 | 402 | `积分不足` |
| 生成失败 | 退单价 | 502 `图片生成失败：…` |
| derive 生成失败 | 退款但子资产行残留 | — |

##### 8 权限与数据规则

- 权限：登录态+资产→任务属主链（AssetByID JOIN tasks）。
- 数据规则：FixFilename 处理中文文件名（总则坑表）；缓存同步只覆盖命中的 name 项。

#### F2.13 任务台账与资产库（credit-records / library-apply / asset-library）

> 对应 PRD：§4 M2 · GET /api/tasks/:id/credit-records、POST /api/tasks/:id/library-apply/:libId、GET/POST /api/asset-library、POST /api/asset-library/:id/share、DELETE /api/asset-library/:id ｜ 状态：实现 ｜ 实现落点：task_handlers.go:555-631、service_gen.go:705-764、repo.go:569-605 ｜ UI：任务详情台账页/素材库页｜ 场景挂钩：E2E-CRD-01（台账键形同源 credits.Record）

##### 1 功能定义

任务维度台账对账读（LIMIT 200+汇总）、资产库条目应用到任务、素材库个人/组织共享 CRUD。

##### 2 触发条件与前置状态

- 任务详情台账 tab → credit-records；素材库页 → asset-library 四动作；「应用到任务」→ library-apply。

##### 3 输入与校验

| 端点 | 输入 | 校验 |
|---|---|---|
| GET credit-records | — | 任务 404 `not found` |
| POST library-apply/:libId | — | 任务 → 404 `task not found`；库条目 → 404 `asset not found`（双哨兵分叉） |
| GET /asset-library | — | orgIDOf 查失败按无组织 |
| POST /asset-library | `{"name","type","description","image_id","asset_id"}` | trim(name)=="" → 400 `名称必填`；asset_id 查不到静默忽略；type 缺省 `character` |
| POST …/:id/share | — | 非本人 404 `not found`；无组织 → 400 `请先加入企业组织` |
| DELETE …/:id | — | **不存在也 200**（service_gen.go:743-746 注释） |

##### 4 处理流程

- credit-records：credits.RecordsByTask（LIMIT 200 ORDER created_at DESC,id DESC）+ 汇总（负额和+by_type 分组，service.go:79-93）。
- library-apply：双 404 面 → InsertAssets（desc=lib.Description+`（来自资产库）`，image_id 沿库）。
- asset-library 列表：`(l.user_id=? OR (org_shared=1 AND l.user_id IN (SELECT id FROM users WHERE org_id=?)))` ORDER created_at DESC LIMIT 200 JOIN users 取 owner_email（**共享子查询走 users.org_id 单组织模型——冻结 org_members 双向 JOIN，语义等价登记** repo.go:574）。
- share：翻转 org_shared → 返回 `{"org_shared":0|1}`（**整数**）。

##### 5 输出与结果状态

- credit-records → `{"list":[Record…],"summary":{"total_spent","by_type"},"task_title"}`；Record wire `id,user_id,task_id,amount,balance_after,type,description,created_at`（task_id NULL 行 wire ""）。
- asset-library → `[LibraryView…]`（+`owner_email`，空→null DIF-29-1）；add → `{"id"}`；apply → `{"asset_id"}`。

##### 6 状态流转

- library-apply：assets 表新增行（任务下）。
- share：org_shared 0⇄1 翻转。

##### 7 边界与异常

| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 任务不存在 | credit-records 404 `not found`；apply 404 `task not found` | — |
| 库条目不存在 | apply 404 `asset not found` | — |
| 名称空 | 400 | `名称必填` |
| 无组织 share | 400 | `请先加入企业组织` |
| 删不存在条目 | 200 true | — |

##### 8 权限与数据规则

- 权限：登录态；asset-library 写面全部 `user_id=?` 谓词（share 的组织可见性靠 org_shared 列+列表子查询）。
- 数据规则：台账读面与 M1 F1.06 全局流水同源 credits 域；LIMIT 200。

---

### M2 端点覆盖对账

| # | 端点 | 功能节 |
|---|---|---|
| 1 | GET /api/tasks | F2.01 |
| 2 | POST /api/tasks | F2.02 |
| 3 | POST /api/tasks/v2 | F2.03 |
| 4 | GET /api/tasks/:id | F2.04 |
| 5 | PUT /api/tasks/:id/title | F2.05 |
| 6 | DELETE /api/tasks/:id | F2.05 |
| 7 | POST /api/tasks/:id/duplicate | F2.05 |
| 8 | POST /api/tasks/:id/assets/extract | F2.06 |
| 9 | POST /api/tasks/:id/assets/extract2 | F2.06 |
| 10 | PUT /api/tasks/:id/extract-settings | F2.06 |
| 11 | GET /api/tasks/:id/full | F2.04 |
| 12 | GET /api/tasks/:id/full2 | F2.04 |
| 13 | POST /api/tasks/:id/mark-failed | F2.05 |
| 14 | POST /api/tasks/:id/restart | F2.05 |
| 15 | POST /api/tasks/:id/continue | F2.08 |
| 16 | POST /api/tasks/:id/scenes/auto-generate | F2.08 |
| 17 | POST /api/tasks/:id/scenes/auto-generate/cancel | F2.08 |
| 18 | GET /api/tasks/:id/scenes2 | F2.10 |
| 19 | PUT /api/tasks/:id/scenes/:groupNo | F2.10 |
| 20 | DELETE /api/tasks/:id/scenes/:groupNo | F2.10 |
| 21 | POST /api/tasks/:id/resplit | F2.07 |
| 22 | POST /api/tasks/:id/storyboard-batch | F2.08 |
| 23 | GET /api/tasks/:id/credit-records | F2.13 |
| 24 | POST /api/tasks/:id/library-apply/:libId | F2.13 |
| 25 | PUT /api/shots/:id | F2.11 |
| 26 | DELETE /api/shots/:id | F2.11 |
| 27 | POST /api/shots/:id/slots | F2.11 |
| 28 | DELETE /api/shot-images/:slotId | F2.11 |
| 29 | GET /api/shots/:id/histories | F2.11 |
| 30 | PUT /api/shots2/:id/params | F2.11 |
| 31 | POST /api/assets/:id/image/upload | F2.12 |
| 32 | POST /api/assets/:id/regenerate | F2.12 |
| 33 | POST /api/assets/:id/derive | F2.12 |
| 34 | POST /api/images/storyboard/generate | F2.09 |
| 35 | GET /api/asset-library | F2.13 |
| 36 | POST /api/asset-library | F2.13 |
| 37 | POST /api/asset-library/:id/share | F2.13 |
| 38 | DELETE /api/asset-library/:id | F2.13 |

38/38 对账 routes.go:324-361 与 PRD §4 M2。注：`/tasks/:id/export/*` 6 条共享前缀归 M4 分册（echo radix 静态段优先语义见 [m04](/qmtv/fsd-qmtv/m04-%E5%AF%BC%E5%87%BA%E4%BA%A4%E4%BB%98/)）；`director_shots` 双表通路另一半归 M10。

**DIF 新发现（本期取证，PRD §3/§4 未登记——入 [99-附录 B](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/)）**：DIF-29-1 空列表 wire null vs 冻结 `[]`（六读面）／DIF-29-2 extract v1 失败 502 文案金额用总额（冻结单价，退款额正确）／DIF-29-3 extract2 item 缺 `variants` 键／DIF-29-4 image_histories 前缀 `si_` vs 冻结 `ih_`（ids.go:60 PrefixImageHistory 定义未用）／DIF-29-5 legacy/duplicate INSERT 18 列零值覆盖 DDL 默认／DIF-29-6 V2 计费 ceil vs 冻结 round（TextCost 正确实现并存未用）／DIF-29-7 V2 台账模型名可空串／DIF-29-8 resplit 回显原始 instruction／DIF-29-9 preview 台账 desc 缺「生成」+Mock label 偏差／DIF-29-10 duplicate 截 60+槽位条件复制／DIF-29-11 groupNo 非数字冻结 500 vs Go 200／DIF-29-12 bind 失败文案统一化／DIF-29-13 legacy 缺省标题「未命名」vs 冻结「未命名剧本」／DIF-29-14 V2 script_type/face_style 无二值化／DIF-29-15 token 估算 rune vs UTF-16／DIF-29-16 full fallback 拼装 5 键收窄。

---

反链：

- [QMTV 功能规格说明书（FSD）v1.0 · 总则](/qmtv/fsd-qmtv/00-%E6%80%BB%E5%88%99/)
- [FSD 附录 · PRD 对照 / 代码域索引 / 差异台账](/qmtv/fsd-qmtv/99-%E9%99%84%E5%BD%95/)
