feat(core): v13 书籍创建流程迁移 — 段落式架构稿 + 旧书升级路径 + maxTokens bug fix#207
Open
feat(core): v13 书籍创建流程迁移 — 段落式架构稿 + 旧书升级路径 + maxTokens bug fix#207
Conversation
ChatPage 通过 /api/v1/agent 走的是 pi-agent loop,工具集是 sub_agent / write_truth_file / read / edit / grep / ls 等,从来不会 返回 toolCall.name === "create_book"。BookFormCard 的渲染条件因此 永远不成立,相关的 pendingBookArgs / bookCreating / handleCreateBook / createProgress 一整套 store 状态也都是死代码。 实际建书走 BookCreate 独立表单页 → POST /api/v1/books/create,与 本次清理无关;CLI 仍在用的 develop_book / CREATE_BOOK_TOOL / developBookDraft 全都在 core 端,未触动。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
世界观/角色卡之前是 <p> 纯文本 + line-clamp,遇到 story_bible 里的 **加粗**、列表、小标题会原样显示为字面字符。现在接入 Streamdown 并带 cjk/code/math/mermaid 插件,同时用 Tailwind arbitrary variant 把标题/段落/列表的字号和间距压到卡片密度。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
之前 setRoute 只在非 hash 页面分支里 setRouteState,hash 页面完全 依赖 hashchange 事件回调触发。当用户走 services → logs → services 这类路径时,中间的 logs 不写 URL,URL 一直停在 #/services;再次 赋值同一个 hash 不触发 hashchange,React state 就永远停在 logs, 表现为"点 services 没反应"。 现在进 setRoute 一律先 setRouteState,再按需写 URL,并在写 URL 前 判重避免多余 hashchange。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
writer / continuity / reviser / chapter-analyzer / consolidator / planner / runner / memory-retrieval 里读 story_bible.md / volume_outline.md / character_matrix.md / current_state.md 的点都改成 readStoryFrame / readVolumeMap / readCharacterContext / readCurrentStateWithFallback, 让 Phase 5 新书走 outline/ + roles/ 权威路径,旧书自动 fallback 到 legacy。 runner.ts 里做 before/after 对比的 old state 读取保留原样(需要磁盘原始内容); book_rules.md 没有新路径替代,所有点也保留原 readFile 调用。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ArchitectOutput 新增 Optional 字段 storyFrame/volumeMap/rhythmPrinciples/roles,
并引入 ArchitectRole 类型。老字段保留让外部消费者向后兼容。
- buildChineseFoundationPrompt / buildEnglishFoundationPrompt 方法移植自 inkos-team
的段落式 prompt(4+5 段结构 + 一人一卡 roles),剥离 Phase 7 扩展列相关要求,
pending_hooks 保留 inkos 侧的 8 列基础版。
- parseSections 识别新 section(story_frame/volume_map/roles),legacy
section(story_bible/volume_outline)仍被接受做 fallback;新增 parseRoles
解析 ---ROLE--- 块。
- writeFoundationFiles 按 storyFrame 是否非空分两条路径:Phase 5 输出下产出
outline/story_frame.md + outline/volume_map.md + roles/{主要,次要}角色/*.md +
各个 legacy shim;legacy 输出下照老方式写扁平 md 文件。运行时 append log 文件
(particle_ledger/subplot_board/emotional_arcs)保留初始化,兼容下游。
- extractYamlFrontmatter 顶层 helper 负责把 book_rules 的 YAML frontmatter
拼到 story_frame.md 顶部,让读取点有单一权威位置。
- 文案:generateFoundationFromImport prompt 里 "叙事弧线" → "故事线"。
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ensureControlDocumentsAt 在现有 story/ + runtime/ 之外,同时创建 story/outline/、story/roles/主要角色/、story/roles/次要角色/ 三个 Phase 5 目录。这样 initBook 完成后磁盘就有完整的 Phase 5 目录结构, 架构师产出的 outline/*.md 和 roles/**/*.md 有落脚处。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Task 3.1:sub_agent 的 architect 参数新增 Optional revise + feedback 字段。 architect case 在 revise=true 时走 pipeline.reviseFoundation 分支,其他参数不变。 Task 3.2: - architect.generateFoundation 新增第 4 个 options 参数,可带 reviseFrom 把 老的条目式架构稿全文 + 用户反馈传进来。buildRevisePrompt 把这些拼成转换 前缀注入到 system prompt 头部。原 3-参调用点(initBook / initFanficBook) 保持不变。 - runner.reviseFoundation(bookId, feedback) 方法:先把 story_bible.md / volume_outline.md / book_rules.md / character_matrix.md 备份到 story/.backup-phase4-<timestamp>/,读原内容喂给 architect(reviseFrom 分支产出 Phase 5 结构),跑一次 foundation-reviewer(失败只 warn 不阻断), 最后用 writeFoundationFiles 覆盖旧文件为 shim。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
createBookContextTransform 在读 truth files 之前先用 isNewLayoutBook(bookDir)
判断当前书的架构稿是否已经是 Phase 5 段落式。如果还是旧的条目式格式,
把一段 UPGRADE_HINT 拼到注入的 user message body 前面,提示 LLM 可以调
sub_agent(architect, { revise: true, ... }) 做一次升级——同时强调不要在
作者没明确同意前主动触发。
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
此前 Phase 5 专项测试和 revise-foundation 测试按仓库历史惯例在 client.defaults 里写了 temperature: 0.7 / maxTokens: 4096 / thinkingBudget: 0 等数字。这些值: - 对测试本身无意义——chat 方法被 vi.spyOn 拦截,defaults 运行时不被读取 - 会误导阅读者以为这是生产推荐配置,尤其 maxTokens: 4096 真被抄到生产会导致 一章写到一半被截断(CLAUDE.md 明令禁止的 maxTokens 回归) 改成 `as unknown as LLMClient` 的 stub cast + 注释说明,保留类型满足度, 去掉所有具体数字。现有 architect.test.ts 其它测试里的老硬编码块不动,这次 只收敛新加的代码。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Bug createLLMClient 里 `maxTokensCap: config.maxTokens ?? null` 的实现意图是 "只有用户显式设 maxTokens 时才封顶 per-call"。但 LLMConfigSchema.maxTokens 有 zod default 8192,config.maxTokens 永远非 undefined,cap 永远等于 maxTokens。 结果 chatCompletion 里 `Math.min(perCallMax, cap)` 会把 architect 的 per-call 16384 裁到 config.maxTokens=8192,基础设定输出被截断——这正是 CLAUDE.md "防止 maxTokens 回归" 那条条款在防范的。 ## 修复 把"fallback"和"cap"这两个语义彻底分到两个字段: - `LLMConfig.maxTokens` → agent 没传 per-call 时的 fallback(不变) - `LLMConfig.maxTokensCap` → per-call 硬上限,**新字段**,默认 undefined → `client.defaults.maxTokensCap = null` = 不封顶 - createLLMClient 不再从 maxTokens 推导 cap;严禁再把两者合并 ## 测试 packages/core/src/__tests__/provider.test.ts 新增 "createLLMClient maxTokensCap regression" describe: - 只设 maxTokens → cap 为 null(不封顶) - 显式设 maxTokensCap → cap 生效 - 什么都不设 → cap 为 null - per-call 16384 原样到达下游,不被 config.maxTokens=8192 裁(回归核心) - per-call 16384 被显式 maxTokensCap=4096 裁(正常 cap 语义) ## 文档 CLAUDE.md 的 "防止 maxTokens 回归" 条款改写,把两个字段的设计规则、错误 实现的样子、回归测试的路径都写清楚,让后续改动一眼看得到边界。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1f43f6a 里给 CLAUDE.md 的"防止 maxTokens 回归"条款加了 5 行 fallback/cap 字段设计细节,违反 CLAUDE.md 的渐进式披露原则 (CLAUDE.md 是 spec 索引,设计细节应该在代码注释 / 独立 spec 里)。 LLMClient 的 maxTokens / maxTokensCap 语义规则已经在 packages/core/src/llm/provider.ts 的 type definition 注释里写清楚; 回归测试在 provider.test.ts 的 "createLLMClient maxTokensCap regression" describe block。后续改动者读到那两处即可。 CLAUDE.md 恢复到单行"防止 maxTokens 回归"的索引条款。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
新建书或 reviseFoundation 落盘时,往 logger.info 输出一行记录本次走的是 phase5 还是 legacy 分支、storyFrame/volumeMap 字符数、roles 数量。 用途:当用户发现建书后磁盘只有 story_bible.md 没有 outline/ 时,看这行 能直接判断是 LLM 没按新 prompt 输出(走 legacy 分支)还是 writeFoundationFiles 本身的问题,不用读代码排查。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## 三个 Bug
**1 (high) writeFoundationFiles 无条件重置运行时状态文件**
之前在 reviseFoundation 场景下会把 current_state.md / pending_hooks.md /
particle_ledger.md / subplot_board.md / emotional_arcs.md 全部重置成初始模板。
对已经写过章节的书这是灾难——consolidator 累积的伏笔推进、角色位置、资源
账本、情感曲线都被清零,违反 context-transform 给 LLM 的 upgrade hint 承诺
"升级只改架构稿,不动已写的章节"。
修复:writeFoundationFiles 加 mode: "init" | "revise" 参数。init 模式下
初始化所有运行时文件(现有行为),revise 模式下**完全不动**运行时文件。
runner.reviseFoundation 调用时传 mode="revise"。initBook / initFanficBook
用默认 mode="init" 向后兼容。
**2 (high) reviseFoundation 第二次使用时喂 shim 给 architect**
Phase 5 书的 story_bible.md / character_matrix.md 是 shim(只有指针 + 摘录),
原 reviseFoundation 无条件读这些 flat 文件作为"原内容"。第一次升级后再用
sub_agent(architect, { revise: true, ... }) 做细节调整时,架构师拿到的
"原书内容"已经是丢信息的 shim——容易把角色线、世界观细节洗掉。
修复:reviseFoundation 用 isNewLayoutBook 检测当前书状态:
- Phase 5 书 → 从 outline/story_frame.md + outline/volume_map.md +
roles/**/*.md(走 outline-paths helper)读权威全文
- legacy 书 → 从 story_bible.md 等 flat 文件读(现有行为)
**3 (medium) 删除/改名角色后旧 role 卡残留成幽灵**
writeFoundationFiles 只写本次 architect 输出的 role 文件,不清理
roles/**/*.md 里的旧文件。如果某次 revise 改名、删人、合并角色,旧卡会继续
被 readRoleCards 注入,planner / writer / auditor 看到 ghost 角色导致上下文
污染。
修复:writeFoundationFiles 在 mode="revise" 下,写 role 前先清空
roles/主要角色/ 和 roles/次要角色/ 两个目录。备份由 runner.reviseFoundation
在调用前完成(Phase 5 书会把 outline/ + roles/ 一并备份到 .backup-phase5-
<ts>/),清空安全。
## 测试
packages/core/src/__tests__/revise-foundation.test.ts 新增 4 个回归用例:
- revise 不重置运行时状态文件(构造已写 20 章的书,验证 5 个运行时文件
内容保持不变)
- Phase 5 书二次 revise 从权威源读(验证传给 architect 的 reviseFrom
是完整 outline + roles 内容,不含 shim 指针)
- revise 清空旧 role 文件(构造 5 个老角色,revise 后只输出 2 个新角色,
验证另外 3 个旧文件被删除)
- Phase 5 revise 备份带 phase5 tag 且包含 outline/ + roles/
全仓测试 785 passed(core 785 / studio 138 / cli 169)。
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
把 inkos-team 的 Phase 5 段落式架构稿格式迁到 inkos,让新建书籍得到段落式 story_frame / volume_map + 一人一卡 roles/ 目录;旧书零改造继续用,也支持作者显式把条目式架构稿升级成段落式。顺手修了一个生产 bug:
LLMClient的maxTokens/maxTokensCap语义混用,导致 architect per-call 16384 被用户 config 的 8192 裁到 8192。8 个 commit 按阶段组织:
21ed8f9outline-pathshelper(Phase 5 双路径 fallback)+ 13 个单测0b690ab7efd910ArchitectOutput新增storyFrame/volumeMap/roles;zh/en prompt 重写;parseSections + writeFoundationFiles 双路径落盘;剥 Phase 7 伏笔扩展列;文案 "弧线" → "角色线"9bbb362outline/和roles/{主要,次要}角色/目录e4d94casub_agent的 architect 参数新增revise+feedback;pipeline 新增reviseFoundation(bookId, feedback)把旧书升级成段落式 + 备份到.backup-phase4-<ts>/4a8c2cccf5a38das unknown as LLMClientstub1f43f6aLLMClient的maxTokens(fallback)和maxTokensCap(cap)彻底分离,5 个回归测试 + CLAUDE.md 规范关键改动
generateAndReviewFoundation已在 main 分支就绪)旧书向后兼容
outline-pathshelper 自动 fallbacksub_agent(architect, { revise: true, bookId, feedback })走pipeline.reviseFoundation,原文件备份到story/.backup-phase4-<ts>/maxTokens 生产 bug
旧实现
createLLMClient里maxTokensCap: config.maxTokens ?? null让 cap 永远等于 fallback。chatCompletion里Math.min(perCall, cap)把 architect per-call 16384 裁到config.maxTokens = 8192。基础设定输出被截断。fix 后:
LLMConfig.maxTokens= per-call 没传时的 fallbackLLMConfig.maxTokensCap= 新增的 optional 硬上限字段,默认 undefined →defaults.maxTokensCap = null= 不封顶createLLMClient maxTokensCap regressiondescribe block规模
Core 侧 +1690 大头是 architect.ts 段落式 prompt 重写(zh+en 共 ~600 行 prompt)+ outline-paths helper(275 行)+ 各类回归/专项测试。
Test plan
pnpm test全绿pnpm build全通过tsc --noEmit无错误grep -rn "Phase 7|depends_on.*hook|core_hook" packages/core/src零命中)pnpm dev新建一本书,确认outline/story_frame.md+outline/volume_map.md+roles/主要角色/*.md+ shim 文件都落盘sub_agent(architect, { revise: true, bookId, feedback }),确认旧文件备份 + 新结构产出后续
Follow-up(独立 PR):把
createLLMClient接到 pi-ai MODELS 注册表,按模型真实maxTokens查;可能再补一份 LobeHub model-bank 的中文 provider 数据(DeepSeek / 通义 / SiliconFlow 在 pi-ai 里完全没收录)。相关 spec:`docs/superpowers/specs/2026-04-19-v13-book-creation-migration-design.md`
相关 plan:`docs/superpowers/plans/2026-04-19-v13-book-creation-migration.md`
🤖 Generated with Claude Code