# 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:描述}}}}` - `` - 任意 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:描述}}` 这类过渡协议。