Compare commits

...

3 Commits

Author SHA1 Message Date
a10cf6e7fb docs: add orchestration logic overview (PDF + HTML source)
13-page A4 PDF reverse-engineered from current code: 4-stage serial pack
flow with 3 gates, intra-pack topo + 4-concurrency, L0-L3 derivation,
Seedance anchor priority, and spec-vs-implementation gap callouts.

Source HTML kept for future re-render via Chrome headless.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 12:06:05 +08:00
f613cbb921 auto-save 2026-05-23 11:33 (~2) 2026-05-23 11:33:07 +08:00
e51962717d auto-save 2026-05-23 11:22 (~2) 2026-05-23 11:22:17 +08:00
4 changed files with 2617 additions and 1643 deletions

View File

@@ -1,6 +1,6 @@
# 项目接力
- 生成时间May 22, 2026 at 12:48
- 生成时间May 23, 2026 at 11:30
- 项目AI玩具专利生成工作流
- 路径:/Users/kangwan/Projects/code/20260518-ai-toy-patent-workflow
- 状态active
@@ -9,7 +9,7 @@
## 最近助手会话概览
- Claudedf7c3755-a4d2-4e32-b68b-42bbaebc2fda · 时间未知
- Codex019e4ac6-ab79-74c1-8fc2-26545c4863df · 时间未知
- Codex019e52d9-22f1-77d3-9897-146889dd551a · 时间未知
- Cursor未找到匹配当前项目的最近会话
## Claude 最近会话
@@ -49,52 +49,41 @@
## Codex 最近会话
- Session ID019e4ac6-ab79-74c1-8fc2-26545c4863df
- Transcript/Users/kangwan/.codex/sessions/2026/05/21/rollout-2026-05-21T21-43-14-019e4ac6-ab79-74c1-8fc2-26545c4863df.jsonl
- Session ID019e52d9-22f1-77d3-9897-146889dd551a
- Transcript/Users/kangwan/.codex/sessions/2026/05/23/rollout-2026-05-23T11-20-22-019e52d9-22f1-77d3-9897-146889dd551a.jsonl
- 工作目录:/Users/kangwan/Projects/code/20260518-ai-toy-patent-workflow
- 分支master
- 敏感字段:已对 token / key / password / secret 做脱敏
### 最近用户要求
- 可以改原始的文件名
- OK 开始出一套吧 这应该是混合式ip了
- 全称是这个“有你家族”猪小懂IP系列家族
- # AGENTS.md instructions for /Users/kangwan/Projects/code/20260518-ai-toy-patent-workflow <INSTRUCTIONS> # AI玩具专利生成工作流 Agent Rules ## Must Read First - 全项目规则真源:`/Users/kangwan/Projects/code/20260317-rules-dashboard/RULES.md` - 全项目元数据规范:`/Users/kangwan/Projects/code/20260317-rules-dashboard/SCHEMA.md` - `.project.json` 是机器真源:公网链接、快捷登录、凭证引用都以它为准 - `RULES.md` 是人工规则和部署事实:启动命令、平台、域名、注意事项都写这里 - 不允许编造不存在的域名、账号、密码;未知就保持空白并明确标记待补充 ## Deployment Metadata Contract - 任何任务只要新增、删除或修改公网地址,必须在同一次任务里更新 `.project.json` - `urls[]` 推荐显式写 `type``app``backend``docs``admin``repo` - 项目专属的网页登录信息,如果允许放进仓库,就写 …
- 你现在是在 Codex CLI 里的项目承接会话。 这不是对话立项台;项目目录已经存在,当前目标是接住上下文并等待用户下一步。 项目路径:/Users/kangwan/Projects/code/20260518-ai-toy-patent-workflow 承接步骤: - 先读取 `.memory/assistant-handoff.md` - 再阅读 `.project.json``RULES.md``AGENTS.md` - 结合当前仓库状态建立上下文 要求: - 不要要求用户重新粘贴 Claude / Codex / Cursor 的总结 - 先理解最近进度、未完成项和下一步候选 - 不要立即修改代码、继续上一个任务或自行执行命令 - 等待用户给出下一条明确指令后再开始动手 - 如果后续需要 Cursor提醒用户从看板打开 Cursor GUI 到当前项目目录
### Codex 最近回复
> 文字模板生成也在跑 GPT 文本模型,通常比图片快,等返回后我会做最终汇总验收
> 我看到当前交接文件比它自己记录的状态更新:现在 `.memory/assistant-handoff.md` 和 `.memory/worklog.json` 都是本地修改。再补一轮只读检查最近提交、diff 概览和全局规则真源,避免只信旧交接文本
> 出完一套,生产项目是 `s_mpfnrr3k_f8e03a`,名称已统一为 `“有你家族”猪小懂IP系列家族`
> 接住上下文,当前只做了只读同步,没有改代码或续跑旧任务
>
> 完成内容:
> - 图片包:专利 12 张、配件 13 张、生产打样 18 张、宣发 21 张,全部 `complete`
> - 文字模板18 个,已生成
> - 视频5 条成片,均约 `60.3s`
> - 360 度旋转展示
> - 开箱短片
> - 触感细节
> - 角色故事介绍
> - 工厂预览短片
> 项目当前状态:`master` 与 `origin/master` 对齐在 `f38c524 docs: record video panel deploy`,前一条功能提交是 `7abbb7d fix: dedupe suffixed video tasks`。最新部署事实是 2026-05-22 的视频面板修复:`video_turntable_60s` 这类真实成片会映射回默认视频卡,不再重复显示不可播放空卡。生产入口是 `https://ai-toy.kang-kang.com`,本地固定端口 `4560`,推荐 `npm run docker:up`。
>
> 视频文件名也改成好识别的形式,例如
> `youni_zhuxiaodong_360_turntable_60s.mp4`、`youni_zhuxiaodong_unboxing_60s.mp4` 等。
> 当前未提交变更只有接力/日志类文件
>
> 验证过:
> - 5 条视频 `ffprobe` 都是约 60 秒
> - 登录状态下视频 Range 返回 `206`,线上拖动/预览应该正常
> - 抽帧联系表不是黑屏5 条都有画面
> - 没嵌入权志龙真实歌曲;视频提示里用的是“原创非版权 K-pop/electro 氛围”,后期要上 GD 热门歌需要你在剪辑平台里用已授权音源替换
>
> 线上看这里:`https://ai-toy.kang-kang.com`
>
> 最终 `git status -sb`
> ```text
> ## master...origin/master
> M .memory/assistant-handoff.md
> M .memory/worklog.json
> ```
>
> 这次没有改仓库代码,所以没有做代码 commit只是在生产数据里生成并回填了这一套素材
> 下一步候选我先不执行:可以验证线上/本地当前页面、继续查视频播放问题、处理 Claude 交接里提到的本地 dev server chunk 问题或做路线里的导出专利包、ZIP/PDF、Seedance 轮询 UI。后续如果需要 Cursor请从看板打开 Cursor GUI 到当前项目根目录 `/Users/kangwan/Projects/code/20260518-ai-toy-patent-workflow`,不要打开父目录
>
> <oai-mem-citation>
> <citation_entries>
> MEMORY.md:503-511|note=[handoff read order and no-recap protocol]
> MEMORY.md:517-520|note=[AI toy repo runtime and provider caveats]
> MEMORY.md:49-58|note=[AI toy continuation scope and prior rollout pointer]
>
## Cursor 最近会话
@@ -104,7 +93,7 @@
- 当前分支master
- 未提交变更1 项
- 最近提交:auto-save 2026-05-22 09:01 (~2)
- 最近提交:docs: record video panel deploy
- 变更文件:
- M .memory/worklog.json

View File

@@ -2322,6 +2322,329 @@
"message": "启动 Codex 接力会话 · 已载入 Claude / Codex 最近会话,等待下一条指令 · 分支 master · 2 项未提交变更 · 最近提交auto-save 2026-05-22 09:01 (~2)",
"ts": "2026-05-22T04:48:44Z",
"type": "assistant-session"
},
{
"files_changed": 2,
"hash": "20d2d8f",
"message": "auto-save 2026-05-22 12:50 (~2)",
"ts": "2026-05-22T12:50:09+08:00",
"type": "commit"
},
{
"files_changed": 2,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 2 项未提交变更 · 最近提交auto-save 2026-05-22 12:50 (~2)",
"ts": "2026-05-22T04:58:47Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"hash": "7abbb7d",
"message": "fix: dedupe suffixed video tasks",
"ts": "2026-05-22T12:59:12+08:00",
"type": "commit"
},
{
"files_changed": 1,
"hash": "f38c524",
"message": "docs: record video panel deploy",
"ts": "2026-05-22T13:02:12+08:00",
"type": "commit"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T05:08:47Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T05:18:47Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T05:28:47Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T05:38:47Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T05:48:47Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T05:58:47Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T06:08:47Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T06:18:47Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T06:28:48Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T06:38:48Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T06:48:48Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T06:58:48Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T07:08:48Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T07:18:48Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T07:28:48Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T07:38:48Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T07:48:48Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T07:58:49Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T08:08:49Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T08:18:49Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T08:28:49Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T08:38:49Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T08:48:49Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T08:58:49Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T09:08:49Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T09:18:50Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T09:28:50Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T09:38:50Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T09:48:50Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T09:58:50Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T10:08:50Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T10:18:50Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T10:28:50Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T10:38:50Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T10:48:50Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T10:58:51Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T11:08:51Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T11:18:51Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T11:28:51Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T11:38:51Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话结束 · 持续 0 秒 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-22T11:41:52Z",
"type": "session-end"
},
{
"files_changed": 2,
"message": "启动 Codex 接力会话 · 已载入 Claude / Codex 最近会话,等待下一条指令 · 分支 master · 2 项未提交变更 · 最近提交docs: record video panel deploy",
"ts": "2026-05-23T03:20:18Z",
"type": "assistant-session"
},
{
"files_changed": 2,
"hash": "e519627",
"message": "auto-save 2026-05-23 11:22 (~2)",
"ts": "2026-05-23T11:22:17+08:00",
"type": "commit"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-23 11:22 (~2)",
"ts": "2026-05-23T03:30:20Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"message": "Codex 会话结束 · 持续 0 秒 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-23 11:22 (~2)",
"ts": "2026-05-23T03:30:20Z",
"type": "session-end"
},
{
"files_changed": 1,
"message": "Codex 会话结束 · 持续 0 秒 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-23 11:22 (~2)",
"ts": "2026-05-23T03:30:20Z",
"type": "session-end"
},
{
"files_changed": 2,
"message": "启动 Claude 接力会话 · 已载入 Claude / Codex 最近会话,等待下一条指令 · 分支 master · 2 项未提交变更 · 最近提交auto-save 2026-05-23 11:22 (~2)",
"ts": "2026-05-23T03:30:31Z",
"type": "assistant-session"
},
{
"ts": "2026-05-23T11:33:07+08:00",
"type": "commit",
"message": "auto-save 2026-05-23 11:33 (~2)",
"hash": "f613cbb",
"files_changed": 2
},
{
"ts": "2026-05-23T03:40:33Z",
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-23 11:33 (~2)",
"files_changed": 1
}
]
}

662
docs/orchestration.html Normal file
View File

@@ -0,0 +1,662 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>AI 玩具专利生成工作流 · 编排逻辑(代码真源版)</title>
<style>
@page { size: A4; margin: 18mm 16mm 18mm 16mm; }
:root {
--bg: #ffffff;
--ink: #1a1a1f;
--muted: #5e6473;
--line: #d8dde6;
--hairline: #ecf0f6;
--accent: #4f46e5;
--accent-soft: #eef2ff;
--warn: #b45309;
--warn-soft: #fef3c7;
--ok: #047857;
--ok-soft: #d1fae5;
--code-bg: #f5f6fa;
--code-ink: #1f2333;
--pack-patent: #4f46e5;
--pack-acc: #0e7490;
--pack-prod: #b45309;
--pack-mkt: #be185d;
}
html, body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--ink);
font-family: "PingFang SC", "Hiragino Sans GB", "Source Han Sans CN", "Microsoft YaHei", "STHeiti", "Helvetica Neue", Arial, sans-serif;
font-size: 11pt;
line-height: 1.55;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
main { padding: 0 0 24pt 0; }
h1, h2, h3, h4 { color: var(--ink); margin: 0; line-height: 1.25; font-weight: 700; }
h1 { font-size: 22pt; margin: 0 0 6pt; letter-spacing: -0.5px; }
h2 { font-size: 15pt; margin: 22pt 0 8pt; padding-top: 6pt; border-top: 1px solid var(--line); }
h2:first-of-type { border-top: none; padding-top: 0; }
h3 { font-size: 12pt; margin: 14pt 0 6pt; color: var(--accent); }
h4 { font-size: 11pt; margin: 10pt 0 4pt; color: var(--muted); }
p { margin: 6pt 0; }
ul, ol { margin: 4pt 0 6pt 18pt; padding: 0; }
li { margin: 2pt 0; }
code {
font-family: "JetBrains Mono", "Menlo", "Consolas", "Source Han Mono SC", monospace;
font-size: 9.5pt;
background: var(--code-bg);
color: var(--code-ink);
padding: 1pt 4pt;
border-radius: 3pt;
word-break: break-word;
}
pre {
font-family: "JetBrains Mono", "Menlo", "Consolas", monospace;
background: var(--code-bg);
color: var(--code-ink);
border: 1px solid var(--hairline);
border-radius: 4pt;
padding: 8pt 10pt;
font-size: 9pt;
line-height: 1.45;
overflow: hidden;
white-space: pre-wrap;
word-break: break-word;
page-break-inside: avoid;
}
pre.ascii {
background: #0f1117;
color: #c8d0e0;
border: none;
font-size: 8.5pt;
line-height: 1.35;
white-space: pre;
overflow-x: auto;
}
.meta {
color: var(--muted);
font-size: 9.5pt;
margin: 0 0 14pt;
display: flex;
flex-wrap: wrap;
gap: 4pt 14pt;
}
.meta span { white-space: nowrap; }
.grid { display: grid; gap: 8pt; }
.grid-2 { grid-template-columns: 1fr 1fr; }
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
.card {
border: 1px solid var(--line);
border-radius: 5pt;
padding: 8pt 10pt;
background: #fff;
page-break-inside: avoid;
}
.card h4 { margin: 0 0 4pt; color: var(--ink); font-size: 10.5pt; }
.card .sub { color: var(--muted); font-size: 9pt; margin: 0 0 4pt; }
table {
width: 100%;
border-collapse: collapse;
margin: 6pt 0 8pt;
font-size: 9.5pt;
page-break-inside: auto;
}
th, td {
border: 1px solid var(--line);
padding: 4pt 6pt;
text-align: left;
vertical-align: top;
}
th { background: var(--code-bg); font-weight: 600; }
tr { page-break-inside: avoid; }
.tag {
display: inline-block;
padding: 1pt 6pt;
border-radius: 10pt;
font-size: 8.5pt;
font-weight: 600;
line-height: 1.5;
}
.tag-patent { background: #eef2ff; color: var(--pack-patent); }
.tag-acc { background: #ecfeff; color: var(--pack-acc); }
.tag-prod { background: #fef3c7; color: var(--pack-prod); }
.tag-mkt { background: #fce7f3; color: var(--pack-mkt); }
.tag-warn { background: var(--warn-soft); color: var(--warn); }
.tag-ok { background: var(--ok-soft); color: var(--ok); }
.tag-info { background: var(--accent-soft); color: var(--accent); }
.callout {
border-left: 3pt solid var(--accent);
background: var(--accent-soft);
padding: 8pt 10pt;
border-radius: 0 4pt 4pt 0;
margin: 8pt 0;
page-break-inside: avoid;
}
.callout.warn { border-color: var(--warn); background: var(--warn-soft); }
.callout.ok { border-color: var(--ok); background: var(--ok-soft); }
.callout h4 { color: var(--ink); margin: 0 0 4pt; }
.src {
font-family: "JetBrains Mono", "Menlo", "Consolas", monospace;
font-size: 8.5pt;
color: var(--muted);
}
.toc { columns: 2; column-gap: 18pt; font-size: 10pt; }
.toc ol { margin: 0; padding-left: 18pt; }
.pagebreak { page-break-before: always; }
.small { font-size: 9pt; color: var(--muted); }
hr.split { border: none; border-top: 1px dashed var(--hairline); margin: 8pt 0; }
</style>
</head>
<body>
<main>
<header>
<h1>AI 玩具专利生成工作流 · 编排逻辑</h1>
<div class="meta">
<span>项目:<code>20260518-ai-toy-patent-workflow</code></span>
<span>分支:<code>master</code> @ <code>e519627</code></span>
<span>文档生成2026-05-23</span>
<span>真源:仓库当前代码 + RULES.md</span>
</div>
<p>本文件是从源代码反向归纳的编排说明,不是规约。出现差异时以 <code>src/lib/templates.ts</code><code>PACK_ORDER</code><code>PACK_TEMPLATES</code><code>VIDEO_TEMPLATES</code> 以及 <code>src/app/api/**</code> 的路由实现为准。</p>
</header>
<h2>0 · 目录</h2>
<div class="toc">
<ol>
<li>顶层一图4 阶段串行 + 平行视频</li>
<li>数据真源与冻结版本</li>
<li>阶段 A输入 → 候选图</li>
<li>阶段 B九宫格选中</li>
<li>阶段 C角色锁定CharacterSpec + L1</li>
<li>阶段 D四个图片包串行</li>
<li>阶段 E文案模板18 条)</li>
<li>阶段 F视频任务Seedance, 5 条)</li>
<li>横切:持久化、审计、鉴权、轮询</li>
<li>编排约束与"规约 vs 实现"差异</li>
<li>已落地导出 / 未落地路线</li>
</ol>
</div>
<h2>1 · 顶层一图4 阶段串行 + 平行视频</h2>
<p>整个工作流是一条<strong>带 gate 的状态机</strong>,一个 <code>GenSession</code> 串起所有阶段的产物。横向四个图片包<strong>严格串行</strong>,包内单图<strong>4 并发 + 拓扑排序</strong>,文案 / 视频在 <code>characterSpec</code> 锁定后即可触发,但前端按"四包完成后再开"做 UX 引导。</p>
<pre class="ascii">
┌─────────── A. 输入入口 ───────────┐
│ idea POST /api/generate │──┐
│ remix POST /api/projects/from-… │ │ → GenSession 落盘data/sessions/
│ replicate / extend ↑ │ │
└────────────────────────────────────┘ │
┌──── B. 九宫格选中 ────┐
│ POST /api/select │
│ 选中图复制到 selected/│
└───────────┬───────────┘
┌──── C. 角色锁定gate #1────┐
│ POST /api/character/lock │ → CharacterSpec
│ (replicate/extend 走 strict)│ + cleanReferenceImageUrl
└───────────┬──────────────────┘ (L1 白底净化锚图)
┌────────── D. 四个图片包(严格串行)──────────┐
│ ① patent ▶ ② accessories ▶ ③ production ▶ │
│ ④ marketing │
│ gate #2前一包 status='complete' 才解锁 │
│ gate #3同 session+image+kind 并发锁 │
│ 包内:拓扑排序 + 4 并发 + 增量回写 │
└───────────┬──────────────────────────────────┘
▼ (前端 UX四包齐了再开下一段
┌────────── E. 文案 18 模板 ──────────┐
│ POST /api/text/generate │
│ gatecharacterSpec 必须存在 │
└─────────────────────────────────────┘
┌────────── F. 视频 5 模板Seedance─────────┐
│ POST /api/video/generate异步任务
│ GET /api/video/status/[taskId](轮询 15s
│ 锚图优先级mkt_white_front → patent_front │
│ → cleanReferenceImageUrl → L0 │
└──────────────────────────────────────────────┘
导出已落地ZIP路线图PDF
</pre>
<div class="callout">
<h4>一句话总结</h4>
<p>选中图 (L0) → 净化为 L1 → 用 L1 作为根锚图生成各包根模板 (L2) → 包内其它模板基于 L2 派生 (L3) → 全程通过 GPT image edit 而不是文本拼 URL保证角色一致。</p>
</div>
<h2>2 · 数据真源与冻结版本</h2>
<table>
<thead><tr><th width="22%">符号</th><th width="35%">代码位置</th><th>值 / 含义</th></tr></thead>
<tbody>
<tr><td><code>PACK_ORDER</code></td><td><code>src/lib/templates.ts:13</code></td><td><code>['patent', 'accessories', 'production', 'marketing']</code> — gate 校验唯一来源</td></tr>
<tr><td><code>PACK_LABELS</code></td><td><code>src/lib/templates.ts:6</code></td><td>patent=专利包 / accessories=配件包 / production=生产打样包 / marketing=宣发包</td></tr>
<tr><td><code>TEMPLATE_FREEZE_VERSION</code></td><td><code>src/lib/templates.ts:4</code></td><td><code>toy-pack-templates-v01</code> — 写入每个 ToyAsset.meta 和 ExportManifest</td></tr>
<tr><td><code>FILENAME_SCHEMA</code></td><td><code>src/lib/templates.ts:3</code></td><td><code>{sessionId}_{characterSlug}_{pack}_{view}_{version}.{ext}</code></td></tr>
<tr><td><code>PACK_TEMPLATES</code></td><td><code>src/lib/templates.ts:1094</code></td><td>4 个包各自的模板数组,每个包指定<strong>根模板</strong>(其它模板的 anchorTemplateId 全部指向根)</td></tr>
<tr><td><code>PACK_ASSET_CONCURRENCY</code></td><td><code>src/lib/packGenerator.ts:155</code></td><td>4 — 包内单图并发上限</td></tr>
<tr><td><code>VIDEO_TEMPLATES</code></td><td><code>src/lib/templates.ts:15</code></td><td>5 条:旋转 / 开箱 / 触感 / 角色故事 / 工厂预览</td></tr>
<tr><td><code>TEXT_TEMPLATES</code></td><td><code>src/lib/templates.ts:106</code></td><td>18 条:项目 / 专利 / 生产 / 配件 / 宣发 / 视频脚本</td></tr>
</tbody>
</table>
<h4>各包模板规模与根锚</h4>
<table>
<thead><tr><th></th><th>kind</th><th>根模板L2 锚)</th><th>模板总数</th><th>必需</th><th>可选</th></tr></thead>
<tbody>
<tr><td><span class="tag tag-patent">专利包</span></td><td><code>patent</code></td><td><code>patent_front</code></td><td>12</td><td>7</td><td>5</td></tr>
<tr><td><span class="tag tag-acc">配件包</span></td><td><code>accessories</code></td><td><code>acc_inventory_sheet</code></td><td>13</td><td>12</td><td>1</td></tr>
<tr><td><span class="tag tag-prod">生产打样包</span></td><td><code>production</code></td><td><code>prod_front_spec</code></td><td>19</td><td>15</td><td>4</td></tr>
<tr><td><span class="tag tag-mkt">宣发包</span></td><td><code>marketing</code></td><td><code>mkt_white_front</code></td><td>22</td><td>11</td><td>11</td></tr>
</tbody>
</table>
<p class="small">规模来源 <code>PACK_TEMPLATE_SUMMARY</code><code>src/lib/templates.ts:1101</code>)。宣发包末尾 5 条 <code>video_*</code> 是分镜板(图片),与 <code>VIDEO_TEMPLATES</code> 的真实视频任务同名但不同源。</p>
<h2>3 · 阶段 A输入 → 候选图</h2>
<h3>3.1 三种输入模式(<code>ProjectInputMode</code></h3>
<table>
<thead><tr><th>模式</th><th>API</th><th>九宫格生成</th><th>L0 是什么</th><th>角色锁定</th></tr></thead>
<tbody>
<tr>
<td><code>idea</code><br/><span class="tag tag-info">想法</span></td>
<td><code>POST /api/generate</code></td>
<td>GPT images/generations × N4/8/12ref 图作为文本提示拼接</td>
<td>用户从九宫格选中的图</td>
<td>用户手动点 <code>/api/character/lock</code>normal 净化</td>
</tr>
<tr>
<td><code>remix</code><br/><span class="tag tag-info">二创</span></td>
<td><code>POST /api/projects/from-upload</code></td>
<td>GPT images/edits 基于上传图 × N强制"原创化"提示</td>
<td>用户从九宫格选中的图</td>
<td>同 idea</td>
</tr>
<tr>
<td><code>replicate</code><br/><span class="tag tag-info">复刻</span></td>
<td><code>POST /api/projects/from-upload</code></td>
<td><strong>跳过</strong>,上传图直接作为 L0 selected</td>
<td>上传的主体图</td>
<td><strong>自动</strong>调 buildCharacterSpec + <strong>strict</strong> 净化</td>
</tr>
<tr>
<td><code>extend</code><br/><span class="tag tag-info">扩展</span></td>
<td><code>POST /api/projects/from-upload</code></td>
<td>同 replicate</td>
<td>同 replicate</td>
<td>同 replicate且把上传图按 <code>role</code> 预填到专利六视图槽位(<code>preFilledSlots</code></td>
</tr>
</tbody>
</table>
<h3>3.2 上传 role → 专利槽位映射extend 模式)</h3>
<p class="src">src/app/api/projects/from-upload/route.ts:19</p>
<table>
<thead><tr><th>UploadedImageRole</th><th>映射到 AssetTemplate.id</th></tr></thead>
<tbody>
<tr><td><code>view-front</code></td><td><code>patent_front</code></td></tr>
<tr><td><code>view-back</code></td><td><code>patent_back</code></td></tr>
<tr><td><code>view-left</code></td><td><code>patent_left</code></td></tr>
<tr><td><code>view-right</code></td><td><code>patent_right</code></td></tr>
<tr><td><code>view-top</code></td><td><code>patent_top</code></td></tr>
<tr><td><code>view-bottom</code></td><td><code>patent_bottom</code></td></tr>
</tbody>
</table>
<p class="small">命中预填槽的 pack asset 不会调 GPT直接复用上传 URL<code>packGenerator.ts:326-356</code>)。</p>
<h3>3.3 Provider 选择</h3>
<pre>// src/lib/providers.ts:10
export function detectProvider(): Provider {
return process.env.OPENAI_API_KEY ? 'gpt' : 'mock';
}</pre>
<ul>
<li><strong>gpt</strong>:图片生图走 <code>POST {GPT_API_BASE}/images/generations</code><code>/images/edits</code>;文本结构化走 <code>/responses</code> + <code>format: json_object</code></li>
<li><strong>mock</strong>:返回 SVG 占位图(笑脸 + 渐变背景),仅用于跑通流程,不能生产用</li>
<li><strong>视频不 mock</strong>Seedance 缺 Key 时直接 503</li>
</ul>
<h2>4 · 阶段 B九宫格选中</h2>
<p><code>POST /api/select</code><code>src/app/api/select/route.ts</code>)支持 <code>action: 'select' | 'reject' | 'reset'</code><code>select</code> 时把图从 <code>data/generated/</code> 复制到 <code>data/selected/</code> 并把新 URL 写回 <code>img.meta.selectedUrl</code></p>
<p>前端键盘约定(<code>src/components/PromptPanel.tsx</code><code>1-9</code> 选中,<code>Shift+1-9</code> 打叉。被打叉的图保留可见,不会进入后续阶段,但仍在 audit DB 留痕。</p>
<h2>5 · 阶段 C角色锁定CharacterSpec + L1 锚图)</h2>
<h3>5.1 两条路径</h3>
<div class="grid grid-2">
<div class="card">
<h4>路径 1 — 普通锁定</h4>
<p class="sub"><code>POST /api/character/lock</code></p>
<ol>
<li>幂等:未 force 且当前 spec.sourceImageId == imageId直接返回缓存</li>
<li><code>buildCharacterSpec()</code>:调 GPT JSON 结构化输出</li>
<li><code>cleanupCharacterAnchor()</code><strong>normal</strong> prompt 净化为白底</li>
<li>写入 <code>characterSpec.cleanReferenceImageUrl</code> = L1 锚图 URL</li>
</ol>
</div>
<div class="card">
<h4>路径 2 — 上传/复刻锁定</h4>
<p class="sub"><code>POST /api/character/lock-from-upload</code><code>from-upload</code> 自动触发</p>
<ol>
<li>有 userHint 时覆盖 <code>session.prompt</code></li>
<li><code>buildCharacterSpec()</code> 在 replicate/extend/upload 分支走 <code>inferCharacterSpecFromImage()</code>Vision 推断)</li>
<li><code>cleanupCharacterAnchor()</code><strong>strict</strong> prompt仅抽取最大最完整的单一主角色丢弃多宫格 / 包装 / 海报版式</li>
<li>强制 force=true每次都重算并覆盖 L1</li>
</ol>
</div>
</div>
<h3>5.2 CharacterSpec 字段(<code>src/lib/types.ts:76</code></h3>
<p>15 个语义字段 + 3 个图像引用 + lockedAt。详见 <code>CHARACTER_SPEC_FIELDS</code><code>templates.ts:58</code>)。关键三项:</p>
<ul>
<li><code>sourceImageId / sourceImageUrl</code> — L0用户选中或上传的图</li>
<li><code>cleanReferenceImageUrl</code> — L1净化后的白底锚图是后续所有 pack 生成的根锚)</li>
<li><code>negativePrompt</code> — 写入每张 pack 图的 prompt 后缀,防角色漂移</li>
</ul>
<h3>5.3 strict 净化的关键约束(节选)</h3>
<p class="src">src/lib/packGenerator.ts:171-200</p>
<ul>
<li>多宫格 / 品牌手册 / 包装展示 → 只抽取最大最清楚的单一主角色,不保留版式 / 分割线 / 标题 / 包装平铺</li>
<li>必须保留:玩具本体的设计标识、衣服图案、帽标、面罩声波图案等用户上传的原创品牌符号</li>
<li>背景纯白,去水印 / 价格 / 网页 UI</li>
<li>不改五官、配色、配件位置、材质纹理</li>
</ul>
<h2>6 · 阶段 D四个图片包串行</h2>
<h3>6.1 三道 gate</h3>
<div class="callout warn">
<h4>每次 POST /api/packs/generate 前后端都过的 gate</h4>
<ol>
<li><strong>characterSpec 必须存在</strong> — 否则 409 "请先锁定角色设定"<code>packs/generate/route.ts:43</code></li>
<li><strong>前一包必须 complete</strong><code>PACK_ORDER</code> 中前一项必须满足 <code>pack.status === 'complete'</code> 且模板覆盖率 100%<code>packs/generate/route.ts:25-58</code></li>
<li><strong>并发互斥</strong> — 同一 <code>session:image:kind</code> 已在跑则返回 202 running<code>generationLocks.ts</code></li>
<li>额外约束:源图 <code>status</code> 必须 = <code>selected</code></li>
</ol>
</div>
<h3>6.2 包内编排(<code>generateAssetPack</code><code>packGenerator.ts:276</code></h3>
<pre class="ascii">
sortTemplatesByAnchor(getPackTemplates(kind)) // 拓扑排序
取/建 CharacterSpec → cleanupCharacterAnchor // 兜底确保 L1 存在
existingPack 合并:从断点续生(按 templateId 去重)
takeReadyTemplate() // 依赖已就绪的模板进入候选
inFlight ≤ PACK_ASSET_CONCURRENCY (=4) // 并发槽
对每张模板:
· 若命中 preFilledSlot → 直接复用上传图,不调 GPT
· 否则 generateAssetImage()
· anchorImageUrl = anchorAsset.url // L3基于已生成根模板
?? L1.cleanReferenceImageUrl // L2用净化锚图
?? L0.url
· GPT images/edits 真正的图生图(读 anchor 字节 → multipart
· data: 开头则落盘到 data/packs/{packId}_{assetId}.{ext}
async onProgress(pack) → persistPackProgress (每张都回写 session JSON)
全部就绪后 pack.status = 'complete',写 ExportManifest 到 data/exports/
</pre>
<h3>6.3 派生层级(<code>ToyAsset.derivationLevel</code></h3>
<table>
<thead><tr><th></th><th>含义</th><th>来源 URL</th><th>触发条件</th></tr></thead>
<tbody>
<tr><td>L0</td><td>用户选中 / 上传主体图</td><td><code>img.url</code></td><td>选中 / 复刻</td></tr>
<tr><td>L1</td><td>白底净化锚图</td><td><code>characterSpec.cleanReferenceImageUrl</code></td><td>角色锁定</td></tr>
<tr><td>L2</td><td>每个包的根模板图</td><td><code>data/packs/...</code></td><td>包内 <code>anchorTemplateId == undefined</code> 的模板(每包仅一张:<code>patent_front</code> / <code>acc_inventory_sheet</code> / <code>prod_front_spec</code> / <code>mkt_white_front</code></td></tr>
<tr><td>L3</td><td>包内其它图</td><td>同上basedOn = L2</td><td>所有 <code>anchorTemplateId</code> 指向根的模板</td></tr>
</tbody>
</table>
<p class="small">代码里 <code>derivationLevel</code> 只被赋值 <code>2</code>(无 anchorAsset<code>3</code>(有 anchorAsset<code>0/1</code> 出现在类型定义中,运行时由 L0 图片本身和 cleanReferenceImageUrl 隐式承担。</p>
<h3>6.4 单张重做(<code>POST /api/assets/[assetId]/regenerate</code></h3>
<div class="callout warn">
<h4>双重 gate</h4>
<ul>
<li><strong>confirmCost === true</strong> 才放行(前端必须二次确认),否则 400</li>
<li><code>session:asset</code> 并发锁,已在跑返回 429</li>
<li>沿用同一 anchor优先该 asset 的 anchorAsset → cleanReferenceImageUrl → sourceImageUrl → L0</li>
<li>支持 <code>userRefinement</code> 文本追加到 prompt 末尾</li>
</ul>
</div>
<h3>6.5 增量回写与断点续跑</h3>
<p><code>onProgress</code> 在每张生成完成后 reload session JSON、用最新 pack 替换旧版本(按 <code>kind + sourceImageId</code> 匹配),再写回。<code>generateAssetPack</code> 启动时会取出未完成的 <code>existingPack</code>,按已落地的 templateId 跳过、只生成剩余项 → 断网或失败可重试。</p>
<h2>7 · 阶段 E文案模板18 条)</h2>
<h3>7.1 路由</h3>
<p><code>POST /api/text/generate</code>body <code>{sessionId, templateIds?}</code><strong>后端唯一 gate</strong><code>session.characterSpec</code> 必须存在(<code>text/generate/route.ts:18</code>),不强制四包完成。</p>
<h3>7.2 实现</h3>
<p class="src">src/lib/textGenerator.ts</p>
<ul>
<li>未传 templateIds 时生成<strong>全部</strong> 18 条;传了则只生成子集</li>
<li>一次 GPT <code>/responses</code> JSON 调用,要求返回 <code>{items: [{templateId, content}]}</code></li>
<li>未配 GPT Key 时每条用 <code>fallbackContent()</code> 生成占位稿,标注"未配置文本模型时生成占位稿"</li>
<li>结果按 templateId 去重后写入 <code>session.textAssets[]</code></li>
</ul>
<h3>7.3 18 条文案模板按 kind 分组</h3>
<table>
<thead><tr><th>kind</th><th>条数</th><th>典型 templateId必需打 ★)</th></tr></thead>
<tbody>
<tr><td><code>project</code></td><td>2</td><td>★ text_project_design_brief · ★ text_character_setting</td></tr>
<tr><td><code>patent</code></td><td>7</td><td>★ product_name · ★ product_use · ★ design_points · ★ representative_view · ★ view_brief · color_claim</td></tr>
<tr><td><code>production</code></td><td>4</td><td>★ brief · ★ cmf · ★ bom · ★ qc</td></tr>
<tr><td><code>accessories</code></td><td>2</td><td>★ accessory_brief · ★ accessory_bom</td></tr>
<tr><td><code>marketing</code></td><td>3</td><td>★ core_copy · ★ detail_page · social_posts</td></tr>
<tr><td><code>video</code></td><td>1</td><td>video_script_pack脚本文字包</td></tr>
</tbody>
</table>
<h2>8 · 阶段 F视频任务Seedance</h2>
<h3>8.1 五条视频模板(<code>VIDEO_TEMPLATES</code></h3>
<table>
<thead><tr><th>id</th><th>标题</th><th>比例</th><th>时长</th></tr></thead>
<tbody>
<tr><td><code>video_turntable</code></td><td>360 度旋转展示</td><td>16:9</td><td>6 s</td></tr>
<tr><td><code>video_unboxing</code></td><td>开箱短片</td><td>9:16</td><td>8 s</td></tr>
<tr><td><code>video_touch_detail</code></td><td>触感细节</td><td>9:16</td><td>6 s</td></tr>
<tr><td><code>video_story_intro</code></td><td>角色故事介绍</td><td>16:9</td><td>8 s</td></tr>
<tr><td><code>video_factory_preview</code></td><td>工厂预览短片</td><td>16:9</td><td>8 s</td></tr>
</tbody>
</table>
<h3>8.2 提交 + 轮询</h3>
<pre class="ascii">
POST /api/video/generate GET /api/video/status/[taskId]
│ ▲
▼ │ 前端每 15 s 轮询
generateSeedanceVideo() │ 最多 30 次
↓ │
POST {SEEDANCE_API_BASE} │
/contents/generations/tasks │
↓ task_id, status='submitted' │
保存到 session.videoTasks[] ──────────────┘
status='succeeded' 时 videoUrl 用 saveRemoteVideo() 拉到 data/videos/
返回 /api/video-file/{filename} 本地路径
</pre>
<h3>8.3 锚图优先级(<code>page.tsx:580-589</code></h3>
<ol>
<li><code>mkt_white_front</code> — 宣发白底正面图(最稳定)</li>
<li><code>patent_front</code> — 专利主视图</li>
<li><code>characterSpec.cleanReferenceImageUrl</code> — L1 净化锚图</li>
<li>当前选中意向图 L0</li>
</ol>
<h3>8.4 PUBLIC_APP_URL 注入</h3>
<p>Seedance 需要从公网拉参考图,所以 <code>publicUrlOrUndefined()</code><code>/api/img/...</code><code>PUBLIC_APP_URL</code>(生产 = <code>https://ai-toy.kang-kang.com</code>)转成绝对 URL。localhost / 127.0.0.1 / 私有 IP 一律丢弃。</p>
<h3>8.5 视频任务去重</h3>
<p>每次新提交按 <code>templateId</code> 去重覆盖(<code>video/generate/route.ts:46</code>),保证 5 个模板各最多一个最新任务。<code>fix: dedupe suffixed video tasks</code><code>7abbb7d</code>)专门处理 <code>video_turntable_60s</code> 等带后缀的真实成片回流到默认模板卡。</p>
<h2>9 · 横切:持久化、审计、鉴权、轮询</h2>
<h3>9.1 八个存储桶(<code>src/lib/storage.ts</code></h3>
<table>
<thead><tr><th></th><th>URL 前缀</th><th>放什么</th></tr></thead>
<tbody>
<tr><td><code>data/sessions/</code></td><td></td><td>每个 session 一个 JSON含 images / packs / textAssets / videoTasks / exports 全量</td></tr>
<tr><td><code>data/generated/</code></td><td><code>/api/img/generated/</code></td><td>九宫格候选图原图</td></tr>
<tr><td><code>data/selected/</code></td><td><code>/api/img/selected/</code></td><td>选中后复制一份(保留生成版本不被覆盖)</td></tr>
<tr><td><code>data/refs/</code></td><td><code>/api/img/refs/</code></td><td>idea 模式上传的参考图</td></tr>
<tr><td><code>data/uploads/</code></td><td><code>/api/img/uploads/</code></td><td>remix / replicate / extend 的上传图</td></tr>
<tr><td><code>data/anchors/</code></td><td><code>/api/img/anchors/</code></td><td>L1 净化锚图 <code>{sessionId}_{imageId}_clean.{ext}</code></td></tr>
<tr><td><code>data/packs/</code></td><td><code>/api/img/packs/</code></td><td>四个包的所有 ToyAsset 图片</td></tr>
<tr><td><code>data/videos/</code></td><td><code>/api/video-file/</code></td><td>Seedance 成片从公网拉回的本地副本</td></tr>
<tr><td><code>data/exports/</code></td><td><code>/api/export/</code></td><td><code>ExportManifest</code> JSON每个 pack 一份)</td></tr>
</tbody>
</table>
<h3>9.2 审计SQLite + 兜底 JSONL</h3>
<p><code>src/lib/auditDb.ts</code>。每个 API 路由的关键节点started / completed / failed / blocked / saved都调 <code>recordEvent()</code>,落到 <code>data/app.db</code>。Docker 镜像内置 <code>sqlite3</code>;非 Docker 本地缺 sqlite3 时降级写 <code>data/audit-fallback.jsonl</code>,不阻断流程。</p>
<p>每张图也通过 <code>upsertImageAsset()</code> 写入 <code>image_assets</code> 表,包含 bucket / width / height / sizeBytes / kind / templateId / origin<code>/api/gallery/[sessionId]</code> 的真源。</p>
<h3>9.3 鉴权(<code>src/middleware.ts</code></h3>
<ul>
<li>Cookie 名:<code>WEB_AUTH_COOKIE_NAME</code>(默认 <code>ai_toy_session</code></li>
<li>HMAC-SHA256 签名 <code>body.signature</code><code>body</code> 是 base64url 编码的 <code>{u, exp}</code></li>
<li>公开路径:<code>/login</code> / <code>/_next/</code> / <code>/api/auth/</code> / <code>/api/img/</code> / <code>/favicon.ico</code> / <code>/robots.txt</code> / <code>/sitemap.xml</code></li>
<li>未鉴权HTML 路径 302 到 <code>/login?next=...</code>;非 HTML API 返回 <code>401 {error: 'unauthorized'}</code></li>
<li><code>/api/img/*</code> 故意保持公开 —— Seedance 必须能从公网拉参考图</li>
</ul>
<h3>9.4 轮询节奏(前端)</h3>
<table>
<thead><tr><th>对象</th><th>间隔</th><th>最大次数</th><th>终止条件</th></tr></thead>
<tbody>
<tr><td>pack 生成(<code>scheduleSessionRefresh</code></td><td>5 s</td><td>90</td><td>无 status='draft' 的 pack前 6 次无论如何都跑</td></tr>
<tr><td>视频任务(<code>scheduleVideoRefresh</code></td><td>15 s</td><td>30</td><td>status 不再是 submitted/processing</td></tr>
</tbody>
</table>
<h2>10 · 编排约束与"规约 vs 实现"差异</h2>
<div class="callout warn">
<h4>差异 1RULES.md 说"四个图片包完成后才解锁文案和视频"</h4>
<p>后端实际只校验 <code>session.characterSpec</code> 存在:</p>
<ul>
<li><code>/api/text/generate</code>:只 check characterSpec<code>text/generate/route.ts:18</code></li>
<li><code>/api/video/generate</code>:完全不 check pack 完成度,直接打 Seedance</li>
</ul>
<p>这条规约靠前端 UX 引导执行,不是后端 enforce。绕过前端可以在锁定角色后立刻发文案/视频请求。</p>
</div>
<div class="callout warn">
<h4>差异 2视频不 mock</h4>
<p>没配 <code>SEEDANCE_API_KEY</code><code>/api/video/generate</code><code>/api/video/status</code> 返回 <strong>503</strong>,不会回退到占位视频。文档和 RULES.md 一致。</p>
</div>
<div class="callout warn">
<h4>差异 3宣发包里 5 条 <code>video_*</code> 模板是分镜板(图片),不是真实视频</h4>
<p><code>marketing</code> 包模板列表里 <code>video_turntable</code> / <code>video_unboxing</code> 等 5 条<strong>id 与 VIDEO_TEMPLATES 重名</strong>,但 kind=<code>marketing</code>、aspectRatio=<code>16:9</code><code>9:16</code>,走的是 GPT image edit产出 PNG 分镜板。真实视频由 Seedance 异步任务单独产出,存 <code>session.videoTasks[]</code>。两者完全独立,前端按 templateId 关联展示。</p>
</div>
<div class="callout">
<h4>差异 4派生层级运行时只用 2 / 3</h4>
<p>类型定义 <code>derivationLevel: 0 | 1 | 2 | 3</code> 给出了完整四级,但 <code>generateAssetPack</code> 只赋值 2包根模板和 3包内其它。L0/L1 由 GenImage 和 CharacterSpec.cleanReferenceImageUrl 隐式承担,不写入 ToyAsset.derivationLevel。</p>
</div>
<div class="callout">
<h4>差异 5preFilledSlot 命中后 derivationLevel</h4>
<p>命中预填上传图时仍按 anchor 存在与否赋 2/3<code>packGenerator.ts:347</code>),但实际生成 URL 是上传桶 URL不是 packs 桶。导出 ZIP 时 <code>extFromAsset</code> 会从 URL 抓扩展名,<code>readImageUrl</code> 回到 uploads 桶读字节。</p>
</div>
<h2>11 · 已落地导出 / 未落地路线</h2>
<h3>11.1 已落地</h3>
<ul>
<li><strong>ExportManifest JSON</strong>:每包生成结束自动写 <code>data/exports/{sessionId}_{kind}_{version}_manifest.json</code>,含 files[]asset_id, templateId, filename, url, anchor, derivation, checklist</li>
<li><strong>ZIP 下载</strong><code>GET /api/packs/download?sessionId=&amp;kind=</code>,纯 Node Buffer 拼装 ZIP含 CRC32文件名 <code>{characterSlug}_{kind}_{N}张.zip</code>,按 templateId 顺序编号 <code>01_xxx.png</code></li>
</ul>
<h3>11.2 未落地RULES.md 路线)</h3>
<ul>
<li><strong>PNG 高清导出 + PDF 合订</strong>ExportManifest 已预留 <code>exportTargets: ['zip', 'pdf', 'manifest-json']</code>,只实现了 zip + manifestpdf 未生成</li>
<li><strong>Seedance 任务轮询 UI</strong>:现状是被动 15s 间隔静默 refresh没有进度条 / 失败重试按钮的完整 UI</li>
</ul>
<h2>12 · 关键 API 速查</h2>
<table>
<thead><tr><th>方法</th><th>路径</th><th>gate / 关键行为</th></tr></thead>
<tbody>
<tr><td>POST</td><td><code>/api/uploads</code></td><td>multipartrole 必传</td></tr>
<tr><td>POST</td><td><code>/api/generate</code></td><td>idea 模式批量生图4/8/12</td></tr>
<tr><td>POST</td><td><code>/api/projects/from-upload</code></td><td>mode ∈ {remix, replicate, extend}replicate/extend 自动锁定 strict</td></tr>
<tr><td>POST</td><td><code>/api/select</code></td><td>action ∈ {select, reject, reset}select 时复制到 selected/</td></tr>
<tr><td>POST</td><td><code>/api/character/lock</code></td><td>普通净化force 控制是否重算</td></tr>
<tr><td>POST</td><td><code>/api/character/lock-from-upload</code></td><td>strict 净化force 总是 true</td></tr>
<tr><td>POST</td><td><code>/api/character/cleanup</code></td><td>独立触发 cleanupCharacterAnchor</td></tr>
<tr><td>POST</td><td><code>/api/packs/generate</code></td><td>三道 gatebackground=true 时返 202 异步跑</td></tr>
<tr><td>POST</td><td><code>/api/assets/[assetId]/regenerate</code></td><td>必传 confirmCost=true并发锁</td></tr>
<tr><td>GET</td><td><code>/api/packs/download?sessionId=&amp;kind=</code></td><td>按选中图找该 kind 的 pack 打 ZIP</td></tr>
<tr><td>POST</td><td><code>/api/text/generate</code></td><td>必须 characterSpec可传 templateIds 子集</td></tr>
<tr><td>POST</td><td><code>/api/video/generate</code></td><td>必须 Seedance Key按 templateId 去重覆盖</td></tr>
<tr><td>GET</td><td><code>/api/video/status/[taskId]?sessionId=</code></td><td>查 Seedance + 写回本地副本</td></tr>
<tr><td>GET</td><td><code>/api/sessions</code></td><td>按 createdAt desc 列全部 session 元信息</td></tr>
<tr><td>GET</td><td><code>/api/templates</code></td><td>把 PACK_TEMPLATES / TEXT / VIDEO 暴露给前端</td></tr>
<tr><td>GET</td><td><code>/api/gallery/[sessionId]</code></td><td>从 image_assets 表 + filesystem 拼图库</td></tr>
<tr><td>GET</td><td><code>/api/audit/[sessionId]</code></td><td>读 events 表事件流水</td></tr>
<tr><td>GET</td><td><code>/api/img/[bucket]/[filename]</code></td><td><strong>公开</strong>Seedance 拉参考图依赖</td></tr>
<tr><td>GET</td><td><code>/api/video-file/[filename]</code></td><td>本地视频副本</td></tr>
<tr><td>POST</td><td><code>/api/auth/login</code> / <code>/logout</code></td><td>HMAC HttpOnly Cookie</td></tr>
</tbody>
</table>
<h2>附录 · 文件锚点</h2>
<table>
<thead><tr><th>关键概念</th><th>代码位置</th></tr></thead>
<tbody>
<tr><td>串行顺序 PACK_ORDER</td><td><code>src/lib/templates.ts:13</code></td></tr>
<tr><td>包模板冻结版本</td><td><code>src/lib/templates.ts:4</code></td></tr>
<tr><td>包内并发上限</td><td><code>src/lib/packGenerator.ts:155</code></td></tr>
<tr><td>包 gate 三道</td><td><code>src/app/api/packs/generate/route.ts:42-91</code></td></tr>
<tr><td>包内拓扑 + 并发调度</td><td><code>src/lib/packGenerator.ts:392-424</code></td></tr>
<tr><td>L1 strict / normal prompt</td><td><code>src/lib/packGenerator.ts:171-200</code></td></tr>
<tr><td>L1 净化路径</td><td><code>src/lib/packGenerator.ts:157</code></td></tr>
<tr><td>L0/L1/L2/L3 派生</td><td><code>src/lib/packGenerator.ts:316-389</code></td></tr>
<tr><td>preFilledSlot 映射</td><td><code>src/app/api/projects/from-upload/route.ts:19</code></td></tr>
<tr><td>视频锚图优先级</td><td><code>src/app/page.tsx:580-589</code></td></tr>
<tr><td>视频任务 templateId 去重</td><td><code>src/app/api/video/generate/route.ts:46</code></td></tr>
<tr><td>pack 进度轮询</td><td><code>src/app/page.tsx:536-543</code></td></tr>
<tr><td>video 状态轮询</td><td><code>src/app/page.tsx:545-557</code></td></tr>
<tr><td>generationLocks 全局并发锁</td><td><code>src/lib/generationLocks.ts</code></td></tr>
<tr><td>ZIP 打包</td><td><code>src/app/api/packs/download/route.ts</code></td></tr>
<tr><td>HMAC Cookie 鉴权</td><td><code>src/middleware.ts</code></td></tr>
<tr><td>审计写库</td><td><code>src/lib/auditDb.ts</code></td></tr>
</tbody>
</table>
<p class="small" style="margin-top: 18pt;">— 文档生成基于 commit <code>e519627</code>。结构性改动后请重跑 <code>npm run docs:orchestration</code>(如已配脚本)或重新执行 docs/orchestration.html 的生成命令。</p>
</main>
</body>
</html>

BIN
docs/orchestration.pdf Normal file

Binary file not shown.