重构回忆录为 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:
Kevin
2026-03-20 10:30:07 +08:00
parent 13e3124b85
commit 7f57f96c25
67 changed files with 4751 additions and 832 deletions

View 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. 正文中的图片只允许最终引用形式,例如 `![caption](asset://image_id)`
一句话概括新流程:
`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
![奶奶坐在院子里的藤椅上](asset://img_123)
```
### 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:描述}}` 这类过渡协议。