重构回忆录为 story-first / markdown-first 架构并整合图片意图与前端 UI 修复
本次 squash merge 将 codex-story-first-image-intent 的整体改动合入 development,核心内容包括: 1. 后端数据与迁移:新增 stories、story_versions、story_image_intents、chapter_cover_intents、assets 等模型与 Alembic 迁移,建立 story-first、markdown-first、asset-first 的主数据链路。 2. 生成与任务链:引入 StoryBuilderOrchestrator、ChapterComposerOrchestrator、story_image_tasks、chapter_cover_tasks,图片生成从正文占位符改为结构化 intent -> asset -> markdown 回填。 3. 并发与一致性:为 story/chapter intent 增加 claim_token、claimed_at、attempt_count,采用数据库原子 claim 为主、Redis 锁为辅,避免重复生成、锁误删和 processing 卡死。 4. Memoir 读写路径:章节 canonical_markdown 成为正文真源,列表/详情接口补齐 markdown、cover_asset、word_count 等字段,PDF 与 asset 解析链路同步升级。 5. Memory / Retrieval:扩展 transcript ingest、chunking、evidence 检索与 story 聚合基础设施,为后续 story-first RAG 与多 agent 编排提供底座。 6. App 端体验:章节页继续走 MarkdownRenderer 阅读链,同时吸收 fix3-19 的跨平台 UI glitch 修复;更新对话页、首页、文案资源与章节列表映射逻辑。 7. 测试与文档:补充 asset resolver、story image task、章节封面派发、markdown 映射等回归测试,并加入图片占位符退役设计文档。
This commit is contained in:
352
docs/plans/2026-03-19-image-intent-placeholder-removal-design.md
Normal file
352
docs/plans/2026-03-19-image-intent-placeholder-removal-design.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# Image Intent 化与占位符退役设计
|
||||
|
||||
> 日期:2026-03-19
|
||||
> 前提:`story-first + markdown-first` 总体重构已完成。
|
||||
> 目标:彻底移除 `{{IMAGE:描述}}` 这类正文占位符,把图片生成与回填改造成结构化 image intent 流程。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
本设计的核心决策如下:
|
||||
|
||||
1. `{{IMAGE:描述}}` 不再是正文协议。
|
||||
2. story 正文只保留最终可阅读的 markdown。
|
||||
3. 每个 story 必须且仅有一张主插图。
|
||||
4. 插图属于 story,封面属于 chapter。
|
||||
5. 图片“待生成意图”以结构化数据存储,不再嵌入正文。
|
||||
6. 正文中的图片只允许最终引用形式,例如 ``。
|
||||
|
||||
一句话概括新流程:
|
||||
|
||||
`story markdown -> extract image intent -> generate asset -> write new story version with asset:// reference`
|
||||
|
||||
## 2. 问题定义
|
||||
|
||||
旧占位符方案存在以下问题:
|
||||
|
||||
- 生成意图与正文内容耦合,污染 markdown 真源。
|
||||
- 占位符兼容双层、四层、多层花括号,协议不稳定。
|
||||
- 图片样式模板被直接拼进占位符字符串,数据边界混乱。
|
||||
- 旧链路依赖 `section` 拆分,和 `story-first` 架构冲突。
|
||||
- 占位符错误、残留或格式偏差会直接泄漏到阅读层。
|
||||
- “每 3 段 1 图”这类旧 fallback 是技术债,不应继续存在。
|
||||
|
||||
因此,新系统必须彻底移除正文占位符,把图片生成升级成 story/chapter 的结构化资产流程。
|
||||
|
||||
## 3. 目标与非目标
|
||||
|
||||
### 3.1 目标
|
||||
|
||||
- 让图片生成链路与 story/chapter 正文解耦。
|
||||
- 保证每个 story 恰好一张主插图。
|
||||
- 让 chapter 封面从章节内全部 stories 聚合生成。
|
||||
- 让 app、PDF、未来运营端消费统一的 markdown 图片协议。
|
||||
- 建立可重试、可审计、可回填的图片版本链。
|
||||
|
||||
### 3.2 非目标
|
||||
|
||||
- 不保留 `{{IMAGE:描述}}` 作为线上兼容格式。
|
||||
- 不支持 story 多张正文插图的一期能力。
|
||||
- 不在本阶段做通用媒体编辑器。
|
||||
- 不把封面也写回 chapter 正文 markdown。
|
||||
|
||||
## 4. 核心模型
|
||||
|
||||
### 4.1 Story 插图意图
|
||||
|
||||
建议新增 `story_image_intents`:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `id` | string | 主键 |
|
||||
| `story_id` | string | 所属 story |
|
||||
| `story_version_id` | string | 提取意图时对应的正文版本 |
|
||||
| `intent_role` | string | 固定为 `primary` |
|
||||
| `source_span` | json/null | 对应正文中的段落或块位置信息 |
|
||||
| `caption` | string | 最终图注候选 |
|
||||
| `prompt_brief` | text | 供出图使用的结构化场景摘要 |
|
||||
| `style_profile` | string/null | 风格策略键 |
|
||||
| `status` | string | pending / processing / completed / failed |
|
||||
| `asset_id` | string/null | 生成成功后的资产 |
|
||||
| `error` | text/null | 错误信息 |
|
||||
| `created_at` | datetime | 创建时间 |
|
||||
| `updated_at` | datetime | 更新时间 |
|
||||
|
||||
关键约束:
|
||||
|
||||
- 每个 active story 只能有 1 条 `intent_role=primary` 的有效 intent。
|
||||
|
||||
### 4.2 Chapter 封面意图
|
||||
|
||||
建议新增 `chapter_cover_intents`:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `id` | string | 主键 |
|
||||
| `chapter_id` | string | 所属 chapter |
|
||||
| `chapter_version_id` | string | 封面生成时对应的章节版本 |
|
||||
| `story_ids` | json | 参与聚合的 stories |
|
||||
| `prompt_brief` | text | 章节封面摘要 |
|
||||
| `status` | string | pending / processing / completed / failed |
|
||||
| `asset_id` | string/null | 封面资产 |
|
||||
| `error` | text/null | 错误信息 |
|
||||
| `created_at` | datetime | 创建时间 |
|
||||
| `updated_at` | datetime | 更新时间 |
|
||||
|
||||
### 4.3 统一资源表
|
||||
|
||||
建议统一使用 `assets` 或在现有图片表基础上重构:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `id` | string | 主键 |
|
||||
| `asset_type` | string | story_image / chapter_cover |
|
||||
| `storage_key` | string | 对象存储键 |
|
||||
| `url` | string/null | 可访问地址 |
|
||||
| `provider` | string | 生成 provider |
|
||||
| `style_profile` | string/null | 风格配置 |
|
||||
| `prompt_final` | text | 最终发送给模型的 prompt |
|
||||
| `status` | string | completed / failed / deleted |
|
||||
| `width` | int/null | 宽 |
|
||||
| `height` | int/null | 高 |
|
||||
| `created_at` | datetime | 创建时间 |
|
||||
|
||||
## 5. 新的正文协议
|
||||
|
||||
### 5.1 允许形式
|
||||
|
||||
正文 markdown 中只允许最终图片引用:
|
||||
|
||||
```md
|
||||

|
||||
```
|
||||
|
||||
### 5.2 禁止形式
|
||||
|
||||
以下形式全部退出线上正文:
|
||||
|
||||
- `{{IMAGE:描述}}`
|
||||
- `{{{{IMAGE:描述}}}}`
|
||||
- `<!-- image-intent: ... -->`
|
||||
- 任意 HTML 媒体占位标记
|
||||
|
||||
### 5.3 解释
|
||||
|
||||
正文只服务阅读与导出,不再承载“待生成意图”。
|
||||
|
||||
待生成意图只存在于结构化表中。
|
||||
|
||||
## 6. 流程设计
|
||||
|
||||
### 6.1 Story 主插图流程
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["StorySynthesisAgent"] --> B["story canonical markdown"]
|
||||
B --> C["StoryImageIntentExtractor"]
|
||||
C --> D["story_image_intents"]
|
||||
D --> E["ImageGenerationTask"]
|
||||
E --> F["assets"]
|
||||
F --> G["Create new story_version"]
|
||||
G --> H["story markdown includes asset:// reference"]
|
||||
```
|
||||
|
||||
步骤:
|
||||
|
||||
1. `StorySynthesisAgent` 生成或更新 story canonical markdown。
|
||||
2. `StoryImageIntentExtractor` 从 story markdown 或 AST 中提取唯一主图意图。
|
||||
3. 写入 `story_image_intents`,状态为 `pending`。
|
||||
4. 异步图片任务读取 intent,生成资产。
|
||||
5. 成功后写入 `assets`。
|
||||
6. 创建新的 `story_version`,把 markdown 中对应位置回填成 `asset://` 图片引用。
|
||||
7. 更新 `stories.current_version_id`。
|
||||
|
||||
### 6.2 Chapter 封面流程
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["ChapterComposerOrchestrator"] --> B["chapter markdown"]
|
||||
B --> C["Aggregate chapter stories"]
|
||||
C --> D["chapter_cover_intent"]
|
||||
D --> E["Cover image generation"]
|
||||
E --> F["cover asset"]
|
||||
F --> G["chapters.cover_asset_id"]
|
||||
```
|
||||
|
||||
步骤:
|
||||
|
||||
1. `ChapterComposerOrchestrator` 完成章节编排。
|
||||
2. 聚合本章 stories 的人物、地点、时间、情绪、时代背景。
|
||||
3. 生成唯一 `chapter_cover_intent`。
|
||||
4. 生成封面资源并绑定到 `chapters.cover_asset_id`。
|
||||
|
||||
说明:
|
||||
|
||||
- 封面不回写进正文 markdown。
|
||||
- 阅读页顶部可单独展示封面 asset。
|
||||
|
||||
## 7. Image Intent 提取策略
|
||||
|
||||
### 7.1 规则
|
||||
|
||||
每个 story 必须且仅有一张主插图,因此 extractor 不做多图候选池。
|
||||
|
||||
优先级:
|
||||
|
||||
1. 最具画面感的场景段落
|
||||
2. 具有人物 + 动作 + 场景 + 时代细节的段落
|
||||
3. 故事转折点或记忆锚点段落
|
||||
4. 若 story 过于抽象,则退化为“人物/地点/时代感”概括图
|
||||
|
||||
### 7.2 输出
|
||||
|
||||
输出结构至少包含:
|
||||
|
||||
- `caption`
|
||||
- `prompt_brief`
|
||||
- `source_span`
|
||||
- `style_profile`
|
||||
|
||||
### 7.3 失败兜底
|
||||
|
||||
如果规则和 agent 都未提取到高质量意图,则使用最小兜底策略:
|
||||
|
||||
- story title
|
||||
- story stage
|
||||
- time refs
|
||||
- place refs
|
||||
- people refs
|
||||
- story summary
|
||||
|
||||
即使降级,也必须生成 1 条 primary intent。
|
||||
|
||||
## 8. 版本回填策略
|
||||
|
||||
### 8.1 原则
|
||||
|
||||
图片生成成功后,不能原地覆盖 story 正文。
|
||||
|
||||
必须:
|
||||
|
||||
1. 基于当前 story version 创建新版本
|
||||
2. 将最终图片引用回填到 markdown
|
||||
3. 写入 `change_summary`
|
||||
4. 更新当前生效版本指针
|
||||
|
||||
### 8.2 回填位置
|
||||
|
||||
建议由 `source_span` 或 block id 决定回填位置。
|
||||
|
||||
如果定位失败:
|
||||
|
||||
- 退化为在 story 开头或相关段落后插入图片引用
|
||||
- 但仍需创建新版本,不可丢图
|
||||
|
||||
## 9. 状态机
|
||||
|
||||
### 9.1 Story 状态建议
|
||||
|
||||
- `content_pending`
|
||||
- `content_ready_image_pending`
|
||||
- `content_ready_image_processing`
|
||||
- `published`
|
||||
- `image_failed`
|
||||
|
||||
### 9.2 约束
|
||||
|
||||
- `published` story 必须有 resolved primary image asset
|
||||
- `content_ready_image_pending` 允许正文已就绪但图片仍在处理中
|
||||
- `image_failed` 允许重试,但不允许伪装成已发布完整内容
|
||||
|
||||
## 10. 失败处理
|
||||
|
||||
### 10.1 意图提取失败
|
||||
|
||||
- 走 deterministic fallback
|
||||
- 必须产出 intent
|
||||
|
||||
### 10.2 图片生成失败
|
||||
|
||||
- intent 状态置为 `failed`
|
||||
- story 状态置为 `content_ready_image_pending` 或 `image_failed`
|
||||
- 支持后台重试
|
||||
|
||||
### 10.3 回填失败
|
||||
|
||||
- asset 保留
|
||||
- intent 状态可为 `completed_but_unapplied`
|
||||
- 创建修复任务重新生成 story version
|
||||
|
||||
### 10.4 Chapter 封面失败
|
||||
|
||||
- 不影响章节正文阅读
|
||||
- 允许章节无封面但正文可读
|
||||
|
||||
## 11. 测试计划
|
||||
|
||||
### 11.1 单元测试
|
||||
|
||||
- 每个 story 只能生成一个 primary intent
|
||||
- abstract story 走 fallback 也能生成 intent
|
||||
- 回填后 markdown 只含 `asset://`,不含 placeholder
|
||||
|
||||
### 11.2 集成测试
|
||||
|
||||
- story markdown -> image intent -> asset -> new story version
|
||||
- chapter stories -> cover intent -> cover asset
|
||||
|
||||
### 11.3 迁移测试
|
||||
|
||||
- 旧 `{{IMAGE:描述}}` 正文可被正确提取为 intent
|
||||
- 旧图片记录可被映射为 asset
|
||||
- 迁移后正文不再含 placeholder
|
||||
|
||||
### 11.4 渲染测试
|
||||
|
||||
- app 阅读页正确渲染 `asset://`
|
||||
- PDF 正确渲染 story 图片与 chapter 封面
|
||||
- 未解析外链或非法资源时安全失败
|
||||
|
||||
## 12. 旧链路退役清单
|
||||
|
||||
以下逻辑应退出线上主链路:
|
||||
|
||||
- `inject_image_placeholder_template`
|
||||
- `inject_placeholders`
|
||||
- `parse_image_placeholders`
|
||||
- `split_narrative_to_sections`
|
||||
- `parse_narrative_to_sections`
|
||||
- 基于 placeholder 创建段落配图的逻辑
|
||||
- “每 3 段 1 图”的旧 fallback
|
||||
|
||||
注意:
|
||||
|
||||
- 这些逻辑可短期保留在离线迁移脚本中读取历史数据
|
||||
- 但不允许继续出现在线上写路径和读路径
|
||||
|
||||
## 13. 一次性实施步骤
|
||||
|
||||
1. 新增 `story_image_intents`
|
||||
2. 新增 `chapter_cover_intents`
|
||||
3. 统一资源表为 `assets`
|
||||
4. 删除 prompt 中对 `{{IMAGE:描述}}` 的输出要求
|
||||
5. 重写 story 生成链:正文生成后提取 primary image intent
|
||||
6. 重写图片任务:读取 intent,不读取正文占位符
|
||||
7. 重写 story version 回填逻辑:写入 `asset://`
|
||||
8. 重写 chapter 封面聚合逻辑
|
||||
9. app / PDF 渲染只认 `asset://`
|
||||
10. 迁移历史正文与旧图片记录
|
||||
11. 删除旧占位符相关线上逻辑与测试
|
||||
12. 补齐新 intent / asset / cover 测试
|
||||
|
||||
## 14. 最终判断
|
||||
|
||||
这次“修复占位符”本质上不是字符串格式修复,而是把旧的正文 DSL 彻底退役。
|
||||
|
||||
正确的长期模型应当是:
|
||||
|
||||
- story 有且只有一张主插图
|
||||
- chapter 有一张聚合封面
|
||||
- 图片意图是结构化资产流程
|
||||
- markdown 只保存最终可阅读结果
|
||||
|
||||
只要这四点成立,未来无论是运营润色、重新生成图片、替换封面、审计版本还是导出 PDF,都不需要再碰 `{{IMAGE:描述}}` 这类过渡协议。
|
||||
Reference in New Issue
Block a user