第 52 章:稳定历史与未来

个人公众号

源码验证日期:2026-05-15,基于 commit 0d81bb6

你在第 51 章看到安全防御如何用六层防线对抗四个攻击面。但安全不是唯一需要”层”的东西。插件系统需要在灵活与稳定之间搭桥,代码库需要和历史痕迹共存,而整个项目需要一个面向未来的方向。

这是最后一章。我们把三个维度——插件系统的稳定与灵活、代码库的历史痕迹、项目的未来方向——编织在一起,讨论一个活的代码库如何在变化中保持自己的形状。


本章路线图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
graph LR
CH41["第41章<br/>为什么是TypeScript"] --> CH42["第42章<br/>为什么是React/Ink"]
CH42["第42章<br/>为什么是React/Ink"] --> CH43["第43章<br/>为什么用Zod"]
CH43["第43章<br/>为什么用Zod"] --> CH44["第44章<br/>工具系统的演进"]
CH44["第44章<br/>工具系统的演进"] --> CH45["第45章<br/>安全与便利"]
CH45["第45章<br/>安全与便利"] --> CH46["第46章<br/>有限窗口"]
CH46["第46章<br/>有限窗口"] --> CH47["第47章<br/>大AsyncGenerator"]
CH47["第47章<br/>大AsyncGenerator"] --> CH48["第48章<br/>Agent架构"]
CH48["第48章<br/>Agent架构"] --> CH49["第49章<br/>开放协议"]
CH49["第49章<br/>开放协议"] --> CH50["第50章<br/>性能的故事"]
CH50["第50章<br/>性能的故事"] --> CH51["第51章<br/>纵深防御"]
CH51["第51章<br/>纵深防御"] --> CH52["第52章<br/>稳定、历史与未来"]

style CH41 fill:#e0e0e0,stroke:#999
style CH42 fill:#e0e0e0,stroke:#999
style CH43 fill:#e0e0e0,stroke:#999
style CH44 fill:#e0e0e0,stroke:#999
style CH45 fill:#e0e0e0,stroke:#999
style CH46 fill:#e0e0e0,stroke:#999
style CH47 fill:#e0e0e0,stroke:#999
style CH48 fill:#e0e0e0,stroke:#999
style CH49 fill:#e0e0e0,stroke:#999
style CH50 fill:#e0e0e0,stroke:#999
style CH51 fill:#e0e0e0,stroke:#999
style CH52 fill:#4a90d9,stroke:#2c5f8a,color:#fff,stroke-width:3px

第一部分:插件系统的稳定与灵活

核心矛盾

插件系统的核心矛盾可以用一句话概括:第三方需要稳定的 API 来构建可靠的功能,但平台需要不断演化来添加新能力——每一次演化都可能破坏已有的插件。这是软件工程里最古老的张力之一。

三层架构

Claude Code 的插件系统用三层架构来应对这个矛盾。

第一层:Manifest 与 Schema 验证。 插件的生命周期始于一个 plugin.json 文件。这个 manifest 定义了插件的元数据——名字、描述、版本、它提供的组件(commands、agents、skills、hooks、output-styles、MCP 服务器)。schemas.ts 是这个系统的基础——1681 行的 Zod schema 定义了 manifest 的完整结构。

1
2
3
const MARKETPLACE_ONLY_MANIFEST_FIELDS = new Set([
'category', 'source', 'tags', 'strict', 'id',
])

这个集合解决了一个常见的混淆问题:插件作者经常把 marketplace.json 的字段复制到 plugin.json 里,或者反过来。Schema 的严格性是一个精心设计的平衡——结构必须正确(schema 验证),但语义可以宽松(未知字段被忽略而不是报错)。

第二层:版本化与缓存。 pluginVersioning.ts 定义了版本计算的优先级:

1
2
3
4
1. plugin.json 中的 version 字段(最高优先级)
2. marketplace entry 提供的版本
3. Git commit SHA
4. 'unknown' 作为兜底

版本不只是标签——它决定了缓存路径。每个版本有自己的缓存目录。更新时,updatePluginOp 执行”非原地更新”:下载到临时目录 -> 计算新版本 -> 复制到版本化缓存 -> 更新安装记录。任何步骤失败,旧安装不受影响。

第三层:作用域与策略。 插件有四个作用域:user -> project -> local -> managedisPluginBlockedByPolicy 只有五行代码,但它是企业安全的基石:

1
2
3
4
export function isPluginBlockedByPolicy(pluginId: string): boolean {
const policyEnabled = getSettingsForSource('policySettings')?.enabledPlugins
return policyEnabled?.[pluginId] === false
}

五种组件的声明式设计

PluginComponent 类型定义了五种组件:

1
2
3
4
5
6
export type PluginComponent =
| 'commands'
| 'agents'
| 'skills'
| 'hooks'
| 'output-styles'

关键的设计选择是:所有组件都是声明式的,不是命令式的。插件作者不需要写 JavaScript 代码来注册组件——只需要在 plugin.json 里声明文件路径,Claude Code 负责加载和执行。这降低了进入门槛,也减少了与平台内部实现的耦合。

为什么不用进程内插件

像 VS Code 那样,插件作为 JavaScript 模块加载到宿主进程中——集成度高,性能好。但耦合度也高——宿主的任何内部 API 变化都可能破坏插件。而且安全风险更大——进程内的 JavaScript 可以访问文件系统、网络和进程内存。Claude Code 选择的是声明式插件 + MCP 服务器的组合,避免了进程内执行。

为什么不用固定版本范围

像 npm 的 package.json 那样,声明插件兼容的宿主版本范围。理论上最安全,但实践中 Claude Code 发布周期很短(通常每周一个版本),如果每次发布都需要所有插件作者更新兼容性声明,插件生态会被拖慢。当前策略是保持 manifest schema 的向后兼容,而不是强制版本匹配。


第二部分:代码里的化石

化石一:commands 变 skills

Claude Code 的用户自定义技能存放在 .claude/commands/ 目录里,但代码内部叫它们 skills。加载函数叫 loadSkillsDir,UI 组件叫 SkillsMenu,但磁盘上的目录叫 commands

loadSkillsDir.ts 里的代码坦率地记录了这个过渡:

1
loadedFrom: 'commands_DEPRECATED',

commandsskills 的重命名不是一天发生的。它涉及磁盘目录结构、用户习惯、UI 显示、内部 API——每一层都需要更新,但不是所有层都能同时更新。commands_DEPRECATED 成了两个世界之间的桥梁:新的代码用 skills,旧的磁盘路径还是 commandsloadedFrom 标记告诉每一层该怎么处理。

化石二:bashCommandIsSafe 的 2600 行

bashSecurity.ts 里的 bashCommandIsSafe_DEPRECATED 函数有 2592 行。它被标记为过时,但仍然被广泛使用。替代方案是基于 tree-sitter AST 的新解析器,但 tree-sitter 不是所有环境都可用——它需要 native binding,在某些系统上可能安装失败。

于是出现了双轨制:新代码用 tree-sitter 解析,旧代码继续用正则。bashPermissions.ts 里的这两行是双轨制的缩影:

1
2
const bashCommandIsSafeAsync = bashCommandIsSafeAsync_DEPRECATED
const splitCommand = splitCommand_DEPRECATED

注释 CC-643 记录了一个已知的 bug:旧的命令分割器在处理复杂复合命令时可能出错。这个 bug 被记录、被跟踪、被接受为已知限制——因为修复的正确方式是完成 tree-sitter 迁移,而不是修补即将被替换的实现。

化石三:11 个迁移文件

src/migrations/ 目录里有 11 个迁移文件,603 行代码。每一个对应一次模型名称的变更:

  • migrateFennecToOpus.ts:内部代号 “fennec” 重命名为 “opus”
  • migrateLegacyOpusToCurrent.ts:旧的 opus 字符串迁移到新格式
  • migrateSonnet45ToSonnet46.ts:sonnet 4.5 迁移到 4.6

这些迁移是幂等的——只有在检测到旧字符串时才修改,重复运行不会产生副作用。

migrateSonnet45ToSonnet46.ts 的注释解释了为什么需要这种迁移:

1
2
3
Users may have been pinned to explicit Sonnet 4.5 strings by:
- The earlier migrateSonnet1mToSonnet45 migration
- Manually selecting it via /model

这不是理想的设计。理想的设计是用别名系统(sonnet -> 当前最新 sonnet 模型),而不是硬编码的模型字符串。但别名系统需要更多的架构工作,而迁移函数只需要几十行代码。在”现在能工作”和”以后会更好”之间,团队选择了前者。

化石四:ANT-ONLY 的双面人生

代码库里有大量 [ANT-ONLY] 标记。--delegate-permissions--dangerously-skip-permissions-with-classifiers--afk--tasks--agent-teams——这些 CLI 选项都被标记为”仅限内部”。

一些还被标记为废弃但未移除:

1
2
3
new Option('--afk',
'[ANT-ONLY] Deprecated alias for --permission-mode auto.')
.hideHelp()

.hideHelp() 是向后兼容的优雅实现——旧的 CLI 选项继续工作(不破坏内部自动化脚本),但不暴露给新用户。这是一种软着陆。

化石五:getSettings_DEPRECATED 的广泛使用

getSettings_DEPRECATED 出现在十几个文件里。它被标记为过时,建议使用新的作用域感知的设置 API,但迁移工作还没有完成。新的 API 需要显式指定设置的来源(userSettings、projectSettings、localSettings),而不是返回一个合并后的全局对象。这个改变在语义上是正确的,但在实践中需要逐个检查每个调用点。

“够用就好”的工程哲学

这些化石不是疏忽。它们是软件演化的自然痕迹——活的代码库不会像教科书那样干净。每一个 _DEPRECATED 后缀都是一个故事:曾经它是对的,后来需求变了,但替换它的工作还没完成(或者不值得完成)。

关键问题是:什么时候偿还技术债务? 答案不是”越早越好”。太早偿还是浪费——你可能为一个即将被替代的模块花了两天重构,结果下个月那个模块被整个移除了。太晚偿还也是浪费——当你花了三天追踪一个 bug,结果发现根因是一个五年前的 _DEPRECATED 函数的已知限制。

正确的时机是在债务开始影响开发速度的时候。commandsskills 的迁移、splitCommand 到 tree-sitter 的迁移——这些正在进行中。在工程里,”还没做”和”不打算做”是两件不同的事。


第三部分:代码里写着的未来

路线图的线索

源代码里散落着指向未来的线索。schemas.ts 里的 TODO 注释标记了插件系统尚未实现的能力:

1
2
3
// TODO (future work): allow globs?
// TODO (future work) gist
// TODO (future work) single file?

插件 schema 的 glob 支持意味着未来的插件可能不仅能声明单个文件路径,还能用模式匹配来引用文件集合。gist 和单文件支持意味着插件的分发方式可能更加灵活——不一定要一个完整的 Git 仓库,一个 GitHub Gist 或单个文件也能成为插件。

PluginError 类型定义了 26 种错误变体,但注释承认只有 2 种在生产中使用。这些”预留”的错误类型是未来工作的路线图——git-auth-failednetwork-errormanifest-validation-error——每一种都对应一个需要更好错误处理的场景。

Multi-Agent 的萌芽

src/utils/swarm/ 目录已经存在。spawnUtils.tsteamHelpers.tsteammateModel.ts——这些文件暗示了一个正在成型的多 Agent 协作系统。当前的 Claude Code 主要是一个单 Agent 交互——一个对话、一个 AI。但 --agent-teams 选项(标记为 [ANT-ONLY])和 swarm 相关的工具函数表明,多 Agent 协作是一个活跃的开发方向。

多 Agent 架构带来新的挑战:Agent 之间怎么通信?怎么共享上下文?怎么避免冲突?怎么分配任务?这些问题的答案将塑造 Claude Code 的下一个架构阶段。

上下文窗口的持续扩展

transcriptTooLong 字段出现在 YoloClassifierResult 类型中,注释说:

1
2
3
API returned "prompt is too long" — the classifier transcript exceeded
the context window. Deterministic (same transcript -> same error), so
callers should fall back to normal prompting rather than retry/fail-closed.

上下文溢出是一个持续存在的挑战。随着对话的进行,token 累积,最终会触及模型的上下文上限。压缩是当前的解决方案,但它有成本和精度损失。如果未来模型的上下文窗口继续增长(从 200K 到 1M,再到可能的 10M+),压缩的触发频率会降低,但不会完全消失——因为更多的上下文意味着更贵的 API 调用。

Away Summary——用户不在时的自主性

awaySummary.ts 服务处理用户离开后回来的场景。当用户离开终端再回来时,系统生成一个简短的摘要,帮助他们快速恢复上下文:

1
2
The user stepped away and is coming back. Write exactly 1-3 short sentences.
Start by stating the high-level task. Next: the concrete next step.

这个功能指向一个更广阔的方向:AI 助手在用户不主动操作时的自主性。当前,Claude Code 主要在用户输入后响应。但 away summary、后台 Agent、定时任务这些功能暗示了一种”AI 在你需要时已经在工作”的未来。


第四部分:如果从零开始重新设计

现在我们有了四卷书的视野,可以问一个更大的问题:如果从零开始重新设计 Claude Code,会有什么不同?

更统一的命令解析

splitCommand_DEPRECATED 的 2600 行代码,根源是 shell 命令解析的复杂性。如果从一开始就基于 tree-sitter 或类似的 AST 级解析器,就不需要后来花大量时间在正则匹配和边界情况上。

从第一天就考虑压缩

上下文压缩是后来加的,这导致了一些设计上的妥协——比如消息格式需要兼容压缩前和压缩后的状态。如果从一开始就把上下文管理作为核心设计约束,消息格式和对话模型可能更干净。

别名系统替代迁移文件

如果从一开始就用别名(sonnet -> 当前最优 sonnet 模型),就不需要每次发布新模型时写一个迁移函数。

当然,这些都是事后诸葛亮。真实的软件开发不是在白纸上画蓝图,而是在变化的需求和有限的时间中做出最好的选择。Claude Code 的架构不是完美设计的产物,而是务实迭代的结果——这正是它值得研究的原因。


第五部分:四卷旅程的回顾

让我们回顾一下这四卷书走过的路。

卷一:消息的旅程。 我们追踪了一条消息从用户输入到模型响应的完整路径。你看到了消息的格式化、系统提示的构建、API 调用的发送、流式响应的接收。消息是 Claude Code 的血液——理解了消息的流向,就理解了系统的主干。

卷二:引擎室的秘密。 我们打开了机器的外壳,拆开了每一个组件。工具系统的 DNA(Tool 接口)、权限系统的门卫(permissions)、对话的记忆术(compact)、外部世界的入口(MCP)、Agent 的克隆和协作(fork)、跨越会话的记忆(memory)、API 通信的暗面(retry、error handling)。这些组件构成了 Claude Code 的引擎。

卷三:造物主的工坊。 我们从读者变成了创作者。你学会了搭建开发环境、修改源码、创建自己的工具、处理用户输入、配置权限规则、接入 MCP 服务器、构建多 Agent 协作。这三卷不只是知识传递——它们是能力传递。

卷四:架构师的棋盘。 我们退后一步,从组件级别提升到架构级别。为什么是 TypeScript?为什么是 React Ink?工具系统怎么演进的?安全与便利怎么平衡?上下文窗口怎么管理?Agent 架构怎么取舍?MCP 为什么是协议?性能怎么优化?安全怎么纵深防御?插件怎么设计?技术债务怎么处理?未来怎么演化?

这四个层次——消息、组件、创作、架构——构成了一个完整的理解框架。你从最具体的(一条消息怎么发送)走到了最抽象的(一个架构决策背后的权衡)。


检查点

  • 插件系统的三层架构:Manifest/Schema 验证 -> 版本化缓存 -> 作用域与策略
  • 声明式优于命令式plugin.json 是声明,不是程序——安全、稳定、可审计
  • 技术债务是活代码的自然状态:commands_DEPRECATED、splitCommand_DEPRECATED、11 个迁移文件
  • “够用就好”的工程判断:有限时间花在最有价值的地方
  • 未来方向:Multi-Agent、上下文扩展、用户自主性、MCP 生态飞轮
  • 代码是流动的:理解框架比静态文档更有价值——给你足够基础自己跟上变化

结语

这本书叫《跟着消息走:Claude Code 源码的旅程》。消息是线索,代码是地图,架构是全景。我们一起走过了这个旅程。

代码是流动的。你读到的这些源码分析,在你读到的时候可能已经不完全准确了——Claude Code 在持续更新,新的功能在被添加,旧的代码在被重构,新的架构决策在被做出。这不是这本书的缺陷,而是它的特征。一个好的源码分析不是一份静态的文档,而是一个理解框架——给你足够的基础知识,让你能够自己跟上代码的变化。

在这个框架里,你学到了:

  • 怎么追踪一条消息在系统中的完整路径
  • 怎么拆开一个工具、一个权限系统、一个压缩引擎
  • 怎么创建自己的工具、自己的 MCP 服务器、自己的插件
  • 怎么评估一个架构决策的权衡——为什么选了这个而不是那个
  • 怎么识别技术债务,判断它是否需要偿还
  • 怎么理解安全的纵深防御和插件的灵活与稳定
  • 怎么思考一个快速发展的项目的未来方向

这些不只是关于 Claude Code 的知识。它们是关于如何理解、评估和构建复杂软件系统的通用能力。

感谢你走完了这个旅程。无论你是为了学习、为了贡献、还是纯粹的好奇心——我希望这四卷书给了你一些有价值的东西。

代码还在演化。旅程没有终点。


导航

上一章:第 51 章:安全的纵深防御

下一章:第 52.5 章:Token 经济学