本次 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 映射等回归测试,并加入图片占位符退役设计文档。
10 KiB
Image Intent 化与占位符退役设计
日期:2026-03-19 前提:
story-first + markdown-first总体重构已完成。 目标:彻底移除{{IMAGE:描述}}这类正文占位符,把图片生成与回填改造成结构化 image intent 流程。
1. 结论
本设计的核心决策如下:
{{IMAGE:描述}}不再是正文协议。- story 正文只保留最终可阅读的 markdown。
- 每个 story 必须且仅有一张主插图。
- 插图属于 story,封面属于 chapter。
- 图片“待生成意图”以结构化数据存储,不再嵌入正文。
- 正文中的图片只允许最终引用形式,例如
。
一句话概括新流程:
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 中只允许最终图片引用:

5.2 禁止形式
以下形式全部退出线上正文:
{{IMAGE:描述}}{{{{IMAGE:描述}}}}<!-- image-intent: ... -->- 任意 HTML 媒体占位标记
5.3 解释
正文只服务阅读与导出,不再承载“待生成意图”。
待生成意图只存在于结构化表中。
6. 流程设计
6.1 Story 主插图流程
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"]
步骤:
StorySynthesisAgent生成或更新 story canonical markdown。StoryImageIntentExtractor从 story markdown 或 AST 中提取唯一主图意图。- 写入
story_image_intents,状态为pending。 - 异步图片任务读取 intent,生成资产。
- 成功后写入
assets。 - 创建新的
story_version,把 markdown 中对应位置回填成asset://图片引用。 - 更新
stories.current_version_id。
6.2 Chapter 封面流程
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"]
步骤:
ChapterComposerOrchestrator完成章节编排。- 聚合本章 stories 的人物、地点、时间、情绪、时代背景。
- 生成唯一
chapter_cover_intent。 - 生成封面资源并绑定到
chapters.cover_asset_id。
说明:
- 封面不回写进正文 markdown。
- 阅读页顶部可单独展示封面 asset。
7. Image Intent 提取策略
7.1 规则
每个 story 必须且仅有一张主插图,因此 extractor 不做多图候选池。
优先级:
- 最具画面感的场景段落
- 具有人物 + 动作 + 场景 + 时代细节的段落
- 故事转折点或记忆锚点段落
- 若 story 过于抽象,则退化为“人物/地点/时代感”概括图
7.2 输出
输出结构至少包含:
captionprompt_briefsource_spanstyle_profile
7.3 失败兜底
如果规则和 agent 都未提取到高质量意图,则使用最小兜底策略:
- story title
- story stage
- time refs
- place refs
- people refs
- story summary
即使降级,也必须生成 1 条 primary intent。
8. 版本回填策略
8.1 原则
图片生成成功后,不能原地覆盖 story 正文。
必须:
- 基于当前 story version 创建新版本
- 将最终图片引用回填到 markdown
- 写入
change_summary - 更新当前生效版本指针
8.2 回填位置
建议由 source_span 或 block id 决定回填位置。
如果定位失败:
- 退化为在 story 开头或相关段落后插入图片引用
- 但仍需创建新版本,不可丢图
9. 状态机
9.1 Story 状态建议
content_pendingcontent_ready_image_pendingcontent_ready_image_processingpublishedimage_failed
9.2 约束
publishedstory 必须有 resolved primary image assetcontent_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_templateinject_placeholdersparse_image_placeholderssplit_narrative_to_sectionsparse_narrative_to_sections- 基于 placeholder 创建段落配图的逻辑
- “每 3 段 1 图”的旧 fallback
注意:
- 这些逻辑可短期保留在离线迁移脚本中读取历史数据
- 但不允许继续出现在线上写路径和读路径
13. 一次性实施步骤
- 新增
story_image_intents - 新增
chapter_cover_intents - 统一资源表为
assets - 删除 prompt 中对
{{IMAGE:描述}}的输出要求 - 重写 story 生成链:正文生成后提取 primary image intent
- 重写图片任务:读取 intent,不读取正文占位符
- 重写 story version 回填逻辑:写入
asset:// - 重写 chapter 封面聚合逻辑
- app / PDF 渲染只认
asset:// - 迁移历史正文与旧图片记录
- 删除旧占位符相关线上逻辑与测试
- 补齐新 intent / asset / cover 测试
14. 最终判断
这次“修复占位符”本质上不是字符串格式修复,而是把旧的正文 DSL 彻底退役。
正确的长期模型应当是:
- story 有且只有一张主插图
- chapter 有一张聚合封面
- 图片意图是结构化资产流程
- markdown 只保存最终可阅读结果
只要这四点成立,未来无论是运营润色、重新生成图片、替换封面、审计版本还是导出 PDF,都不需要再碰 {{IMAGE:描述}} 这类过渡协议。