Skip to content

docs(c04): 新增 第26章 配置迁移即代码#32

Merged
luyao618 merged 4 commits into
mainfrom
agent/cc-dev/d8a21641
May 24, 2026
Merged

docs(c04): 新增 第26章 配置迁移即代码#32
luyao618 merged 4 commits into
mainfrom
agent/cc-dev/d8a21641

Conversation

@luyao618
Copy link
Copy Markdown
Owner

@luyao618 luyao618 commented May 23, 2026

概要

新增第 26 章《配置迁移即代码》,覆盖 migrations/ 目录 11 个迁移文件与 main.tsxrunMigrations() 调度。全新增章节(v1 无对应),交付 100% 新增内容。

  • chapter_id: c04
  • is_new_chapter: true
  • source_commit: 290fdc9481a70612bc5823aa4ed225c52c52aad3
  • estimated_words: ~7.8k 字
  • 风格双亲: docs/03-状态管理.mddocs/17-Settings-系统.md

R-1 风格双亲实证

风格双亲 1:docs/03-状态管理.md(v1 原文摘抄)

Claude Code 面临一个独特的状态管理难题:它既是一个 React 应用,又不完全是

终端 UI 用 Ink(React for CLI)渲染,组件需要响应式的状态更新。但核心业务逻辑 —— API 调用、工具执行、Agent 编排 —— 运行在 React 树之外。一次工具调用的结果需要同时:

  1. 更新 React 组件(显示在终端 UI 上)
  2. 被非 React 的 query.ts 对话循环读取
  3. 被 Agent 子系统使用(可能运行在隔离的上下文中)

如果用 Redux/Zustand 这类库?太重了。React 内置的 useState/useReducer?无法从 React 树外部访问。模块级全局变量?无法触发 React 重渲染。

Claude Code 的答案是:三层状态架构 + 一个 35 行的自研 Store

在深入代码之前,先建立全局认知。Claude Code 的状态分布在三个层次,各有明确的职责边界……这三层的设计原则是向下依赖,向上隔离:ToolUseContext 引用 AppState 的 getter/setter;AppState 可以读取 bootstrap/state 的值;但反过来不成立。

继承的写法要点:用「为什么 X 值得单独讲?」型场景式破题,先把"这件事到底有多复杂 / 为什么常规手段不顶用"摆出来,再给一句粗体一句话答案,然后再进入正文铺细节;每一层 / 每一条都拿到一个可命名的小标题,散文里穿插 file:line 锚点而不是把锚点堆成开头列表。

风格双亲 2:docs/17-Settings-系统.md(v1 原文摘抄)

一个 CLI 工具的配置需求看似简单 —— 用户写一个 JSON 文件就行了。但 Claude Code 面对的现实远比这复杂:

  1. 个人偏好 —— 用户想全局设置自己偏好的模型、权限规则
  2. 团队共享 —— 项目组要把 MCP 服务器、Hook 脚本提交到 Git 仓库共享
  3. 本地覆盖 —— 个人本地调试需要覆盖项目设置,但不能提交到 Git
  4. 企业管控 —— 安全团队需要强制启用沙箱、禁用危险权限,且用户不能覆盖
  5. 远程策略 —— 企业管理员通过 API 远程下发配置,无需触碰每台机器
  6. 平台差异 —— macOS 用 plist + MDM、Windows 用注册表、Linux 用文件

这些需求层层叠加,任何单一配置文件都无法满足。Claude Code 的 Settings 系统通过多层配置源 + 优先级合并 + 变更检测热更新的架构,优雅地解决了这个问题。

Settings 系统的核心设计是一条明确的优先级链。配置从多个来源读取,按照优先级从低到高逐层合并,高优先级覆盖低优先级……

继承的写法要点:用"看似简单 / 实际多场景"对照拉开张力,再用一条粗体结构化短句把全章答案概括成一个名词短语;正文里同一组相邻文件 / 相邻字段用编号小节平行展开("5 + 1 层"对应到本章「3 + 5 + 3」三组迁移),并在每一段尾巴用一句话归纳"这一类做的事到底是什么"。

本章新写摘抄 1(开篇与破题,对应风格双亲 1 的"为什么单独讲")

当你打开 migrations/ 目录会看到什么?

如果你拉下 Claude Code 的源码,进到根目录敲一行 ls migrations/,迎面会蹦出 11 个文件:……11 个文件全部是顶层文件——没有子目录、没有公共基类、没有"框架",每个文件导出一个同名的无参函数,函数名就是它做的那件事。这种朴素到近乎乡土的组织方式背后有一个很清楚的意图:每一次破坏性的配置改动,都是一段可以被独立 review、独立删除、独立写测试的小代码片段。你不需要去理解一个"迁移引擎"——你只需要打开你感兴趣的那个文件,从头读到尾,三五十行内它就讲完了自己的故事。

为什么这件事值得单独讲一章?因为这是 Claude Code 处理**"产品在用户身后偷偷换零件"**这件事的全部答案。模型默认值会换(Pro 用户从 Sonnet 默认翻成 Opus),模型别名会重映射(sonnet 现在指 Sonnet 4.6,不再是 4.5),字段会从 ~/.claude.json 搬到 ~/.claude/settings.json,旧的实现细节键名会被改成更适合写进用户文档的名字……这些动作如果直接做、不管历史,老用户的体验就会在某次升级后突然错乱:模型莫名其妙变了、自动更新莫名其妙打开了、/model 显示的是一串再也对不上的 ID。migrations/ 里这 11 个函数,就是把每一次这种动作的"历史债"都写下来、按版本号一次性兑现的地方。

本章新写摘抄 2(小节归纳,对应风格双亲 2 的"分层平行展开 + 一句话归纳")

这三个迁移合在一起,演示了 Claude Code 处理"字段搬家"这件事的全部套路:判断老地方有没有 / 检查新地方有没有覆盖风险 / 数组型字段做去重合并 / 单值型字段做存在性短路 / 写新地方 / 删老地方 / 发事件 / 失败不抛。下次再有什么字段要搬,照这个模板写一个新文件、加进流水线、把版本号 +1,就完事了。

把 6 段模型迁移连起来读,你能看到 Claude Code 处理"模型字符串演化"这件事的一整套思路:别名升级时,先把老用户从旧别名映射到当时对应的显式串(保留意图)→ 等下一个别名变动时,再把符合条件的人卷回新别名(跟随升级)→ 期间一直只读写 userSettings,不动其它层,不读 merged。同样的模式可以扩到 fast mode 等正交维度。

OC-R Round-2 修订

针对 OC-R Round-1 verdict 的 3 个 blocking 已修复:

  1. R-1 风格双亲实证段(本 PR 描述上方已补齐):v1 原文摘抄 2 段(各 > 200 字)+ 本章新写摘抄 2 段(各 > 200 字)。
  2. §五-第四「迁移绝不抛」表述按源码修正:原文宣称"所有跟磁盘/配置打交道的写入都包在 try/catch 里"——事实 11 个迁移里只有 4 个真有 try/catch(migrateAutoUpdatesToSettings:25-60migrateBypassPermissionsAcceptedToSettings:21-39migrateEnableAllProjectMcpServersToSettings:33-117resetAutoModeOptInForDefaultOffer:31-49),另外 7 个 (migrateSonnet1mToSonnet45migrateLegacyOpusToCurrentmigrateSonnet45ToSonnet46migrateOpusToOpus1mmigrateFennecToOpusmigrateReplBridgeEnabledToRemoteControlAtStartupresetProToOpusDefault) 没有 try/catch,靠"读写同源 / completion flag 早退 + 外层版本号失败不写"兜底。文案改为**「显式 try/catch(4 段)+ 幂等早退靠版本号兜底(7 段)」** 的精确两套机制。
  3. §3.1 / §五-第五「只读写 userSettings、不读 merged」全局结论按源码修正resetProToOpusDefault.ts:26-30 走的是 getSettings_DEPRECATED()(merged)。修正为:「只读写 userSettings」是给 5 段改写模型字符串的迁移(migrateSonnet1mToSonnet45 / migrateLegacyOpusToCurrent / migrateSonnet45ToSonnet46 / migrateOpusToOpus1m / migrateFennecToOpus)的纪律;resetProToOpusDefault 是例外——它不改 model 字段、只决定横幅时间戳,所以读 merged 不会触发 silent global promotion。§3.2 同步补充了它走 getSettings_DEPRECATED() 这一事实与解释。

章节结构

  1. 一条流水线:runMigrations() 与 set-rerun 守门员
  2. 字段搬家:autoUpdates / bypassPermissions / enableAllProjectMcpServers
  3. 模型字符串的版本编年史:legacyOpus / Opus→Opus1m / Sonnet1m→Sonnet45→Sonnet46 / Fennec / ProToOpus
  4. 改名、清理与一次性重置:replBridge 改名 / TRANSCRIPT_CLASSIFIER 重置
  5. 把它们串起来:3 套幂等模式、顺序即事实、版本号契约

CI 自检(Round-2 重跑)

  • C-3 源码块占比:22.8%(OK,< 25%)
  • C-4 章节标题:20 个全部合规
  • C-5 frontmatter:无
  • no-spec-jargon:OK
  • no-fuzzy:OK
  • no-revision-codenames:OK
  • check-orphans:OK
  • check:docs 全套:全绿

备注

  • 仅提交 PR,不执行 gh pr merge,等尧哥手动合。

Yao Lu and others added 4 commits May 24, 2026 00:36
11 个 migration 串成一条 set-rerun 流水线,三类搬迁(设置层级 / 模型字符串 / 改名清理重置)的幂等契约与顺序事实。

风格双亲:docs/03-状态管理.md, docs/17-Settings-系统.md
source_commit: 290fdc9481a70612bc5823aa4ed225c52c52aad3

Co-authored-by: multica-agent <github@multica.ai>
- §3.1 / §五-第五: 「只读写 userSettings、不读 merged」改为针对 5 段改写
  字符串的迁移;resetProToOpusDefault 走 getSettings_DEPRECATED() 是例
  外,因为它不改 model 字段、仅决定横幅时间戳。
- §3.2: 显式补充 resetProToOpusDefault 用 merged settings 判断默认值,
  以及为什么这里读 merged 安全。
- §五-第四: 「迁移绝不抛 / 所有写入都包在 try/catch 里」修正为「显式
  try/catch(4 段)+ 幂等早退靠版本号兜底(7 段)」,逐段列出锚点。

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
@luyao618 luyao618 merged commit 719b916 into main May 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant