Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/18-MCP协议实现.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ if (doesEnterpriseMcpConfigExist()) {

需要注意的是,enterprise 独占模式并不完全排斥所有外部配置。SDK 类型的服务器(`type: 'sdk'`)在策略过滤时被豁免(`filterMcpServersByPolicy` 中 `c.type === 'sdk'` 直接放行),因为 SDK 服务器是进程内传输的占位符,CLI 不会为它们 spawn 进程或打开网络连接,URL/command 形式的 allowlist 对它们也无意义。

### 2.2 Project 配置的向上遍历
### 2.4 Project 配置的向上遍历

Project 级别的 `.mcp.json` 有一个特殊行为:**从 CWD 开始,向上遍历到文件系统根目录**,越靠近 CWD 的配置优先级越高:

Expand Down Expand Up @@ -306,7 +306,7 @@ case 'project': {

这意味着 monorepo 的根目录可以定义通用的 MCP 服务器,子项目目录可以覆盖或添加自己的。

### 2.3 插件去重:基于签名的内容比对
### 2.5 插件去重:基于签名的内容比对

当多个来源定义了指向同一个底层服务的 MCP 服务器时(例如,用户手动配置了 Slack MCP,插件也提供了 Slack MCP),需要智能去重。

Expand All @@ -332,7 +332,7 @@ export function getMcpServerSignature(config: McpServerConfig): string | null {
- **插件内先到先得**:多个插件提供相同服务器时,先加载的赢
- **手动配置 > claude.ai 连接器**:用户手动配置表达了更强的意图

### 2.4 环境变量展开
### 2.6 环境变量展开

MCP 配置支持 `${VAR}` 和 `${VAR:-default}` 语法的环境变量展开:

Expand All @@ -356,7 +356,7 @@ export function expandEnvVarsInString(value: string): {

这个展开会递归应用到 stdio 服务器的 `command`、`args`、`env`,以及远程服务器的 `url`、`headers` 上。

### 2.5 企业策略过滤:Allowlist 与 Denylist
### 2.7 企业策略过滤:Allowlist 与 Denylist

企业管理员可以通过 `allowedMcpServers` 和 `deniedMcpServers` 控制哪些 MCP 服务器可以使用。策略支持三种匹配方式:

Expand Down Expand Up @@ -548,7 +548,7 @@ const timeoutPromise = new Promise<never>((_, reject) => {
const timeoutId = setTimeout(() => {
if (inProcessServer) inProcessServer.close().catch(() => {})
transport.close().catch(() => {})
reject(new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS(
reject(new TelemetrySafeError(
`MCP server "${name}" connection timed out after ${getConnectionTimeoutMs()}ms`,
'MCP connection timeout',
))
Expand Down
8 changes: 4 additions & 4 deletions docs/19-权限系统与远程权限回灌.md
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,7 @@ Auto 模式不是对每个操作都调用昂贵的 Classifier API,而是先通

前面八节都默认"问"和"答"发生在同一台电脑上:模型想跑工具,本地 CLI 弹个对话框,用户在终端按 yes/no。新版多了一个更有意思的场景——你在咖啡店掏出手机,claude.ai 推来一条通知:"是否允许在你笔记本上执行 `rm node_modules`?"你点一下允许,几秒之后家里那台电脑上的工具就真的跑起来了。

把这条链路想象成一个对讲机:本地 CLI 想问问题,远程那头的人有权回答。能架起这部对讲机,靠的是新增的三个落点,本节就来逐个拆开。
把这条链路想象成一条专用的内部通道:本地 CLI 把权限问题发出去,远程那头的人有权回答。能架起这条通道,靠的是新增的三个落点,本节就来逐个拆开。

### 9.1 三个新落点:接口层、本地协调层、回灌入口

Expand All @@ -804,9 +804,9 @@ flowchart LR

三个落点对应的源码很短,可以这样理解它们的角色:

第一处是**接口层**,落在 `bridge/bridgePermissionCallbacks.ts:1-43`。这个文件只有 43 行,给"对讲机"画了一份最小的协议:怎么把问题发出去、怎么收回答、怎么取消提问。值得一提的是它没有用裸 `as` 把消息强转成业务类型,而是写了一个类型守卫专门校验回包合不合法——跨进程消息一旦相信"对面会发对的",运行时就会无声无息出 bug,这份小心是必要的。
第一处是**接口层**,落在 `bridge/bridgePermissionCallbacks.ts:1-43`。这个文件只有 43 行,给这条通道画了一份最小的协议:怎么把问题发出去、怎么收回答、怎么取消提问。值得一提的是它没有用裸 `as` 把消息强转成业务类型,而是写了一个类型守卫专门校验回包合不合法——跨进程消息一旦相信"对面会发对的",运行时就会无声无息出 bug,这份小心是必要的。

第二处是**本地协调层**,落在 `hooks/useReplBridge.tsx:369-585`。它负责"在本地这边坐镇":每个发出去的问题分一个 `requestId`,远端回包了再按 id 找回原来等着的那段代码。它做完之后会把对讲机的引用挂到全局状态里,权限内核需要时直接取。
第二处是**本地协调层**,落在 `hooks/useReplBridge.tsx:369-585`。它负责"在本地这边坐镇":每个发出去的问题分一个 `requestId`,远端回包了再按 id 找回原来等着的那段代码。它做完之后会把通道的引用挂到全局状态里,权限内核需要时直接取。

第三处是**反向回灌入口**,落在 `remote/remotePermissionBridge.ts:1-78`。前两个文件解决的是"本地工具想问远端用户";这个文件解决的是反过来的麻烦:远端模型在云端调了一个工具,但本地 CLI 必须帮它弹出确认对话框——而本地此刻既不知道这工具长什么样,也没有完整的对话上下文。它的解法是"现场捏一个最小可信对象"塞给本地确认队列,§9.5 会展开。

Expand Down Expand Up @@ -874,7 +874,7 @@ sequenceDiagram
Local--xKernel: 用户后按的回答被丢弃
```

注意图里两条"撤销"线很重要。任何一条赛道赢下之后,必须主动通知**所有还在等待的赛道**:"别问了,已经有答案了。" 否则远端用户那边会留下一个孤儿提示框,本地工具明明已经跑完,他还在那盯着是否允许——这是最让用户困惑的"幽灵提示"问题。`interactiveHandler.ts:92-298` 把这条规矩落到代码里:六条赢家路径(本地三个回调、async hook、async classifier、recheckPermission),每一条都显式做了一次撤销
注意图里两条"撤销"线很重要。任何一条赛道赢下之后,必须主动通知**所有还在等待的赛道**:"别问了,已经有答案了。" 否则远端用户那边会留下一个孤儿提示框,本地工具明明已经跑完,他还在那盯着是否允许——这是最让用户困惑的"幽灵提示"问题。`interactiveHandler.ts:92-298` 把这条规矩落到代码里:上一节列出的五条赛道再加上后面 §9.4 要讲的 `recheckPermission`,总共六个可能"赢"的入口,每一个都显式做了一次撤销

### 9.4 一个反直觉的小动作:recheckPermission 也要 claim

Expand Down
81 changes: 75 additions & 6 deletions docs/21-Skill-Plugin-OutputStyle三扩展点.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Review the code changes on the given branch...

| 字段 | 类型 | 默认值 | 源码位置 |
|------|------|--------|---------|
| `name` | string | undefined(显示名,不影响 Skill 标识) | `loadSkillsDir.ts:238-240` — `displayName` |
| `name` | string | undefined(显示名 `displayName`,不影响 Skill 标识;Skill 标识始终是目录名 `entry.name`) | `loadSkillsDir.ts:238-240` — `displayName` |
| `description` | string | 从 Markdown 正文首行提取 | `loadSkillsDir.ts:208-214` |
| `allowed-tools` | string/string[] | `[]` | `loadSkillsDir.ts:242-245` |
| `argument-hint` | string | undefined | `loadSkillsDir.ts:246-249` |
Expand Down Expand Up @@ -520,6 +520,8 @@ Plugin 中的命令自动带有 Plugin 名称前缀。命名逻辑在 `getComman
// 例:my-plugin:sub:deploy
```

嵌套目录形式只有在 plugin 把命令文件按 namespace 分层组织时才会出现 —— 例如 `commands/sub/deploy.md` 会被注册成 `my-plugin:sub:deploy`,相对路径里的 `/` 会按层级转换为 `:`。普通的扁平 `commands/build.md` 不会带 namespace。

### 3.4 Plugin 变量替换

Plugin 命令支持特有的变量替换(`utils/plugins/loadPluginCommands.ts:340-377`):
Expand Down Expand Up @@ -787,7 +789,7 @@ MCP Skill 有一个关键安全限制:**不执行 Shell 命令嵌入**(`load

---

## Output Style:体验层的第三条扩展路径
## 六、Output Style:体验层的第三条扩展路径

前面五节一直在围着 Skill / Agent / Plugin / Hook 这条"行为面"主线转 —— 谁注入 prompt、谁调工具、谁拦截事件。但 Claude Code 还藏着另一条扩展路径,叫 Output Style。它跟 Skill 看起来都是 `.md` 文件、都吃 frontmatter,第一眼很容易混为一谈;实际上两者切入对话的位置完全不同 —— Skill 是把内容注入到 user/assistant 这一侧,Output Style 则是在 system prompt 末端追加一段 `# Output Style: ...` 风格指令(`constants/prompts.ts:151-157`),并可选地省略 `getSimpleDoingTasksSection()` 那段默认任务清单(`constants/prompts.ts:564-567`),intro / system / actions / tools / tone / efficiency 以及 memory / env / language / MCP 等其余 system sections 一概照常保留。前者在"一次回合里要做什么"上加料,后者只是在原有 system prompt 之上追加风格指令、调整输出腔调,并不会顶替或换掉 system 那一侧。

Expand Down Expand Up @@ -866,7 +868,7 @@ Plugin 则是这三者的"打包发行单元"。一个完整的 plugin 可以同

---

## 、实战示例
## 、实战示例

### 示例 1:代码审查 Skill

Expand Down Expand Up @@ -969,9 +971,78 @@ Important:
1. 每次文件被编辑/写入后,从 stdin JSON 中提取 `file_path` 字段,自动运行 ESLint 修复
2. 当 Agent 即将停止时,异步运行测试套件;如果测试失败(退出码 2),唤醒模型继续修复(注意 `Stop` 事件的退出码 2 含义是"继续对话")

### 示例 4:完整 Plugin 包

把前面 Skill / Agent / Hook / Output Style 一起打包成一个 plugin,就是开发者交付给团队的最小完整单元:

```
team-toolkit/
├── plugin.json
├── commands/
│ └── stand-up.md
├── skills/
│ └── review-pr/
│ └── SKILL.md
├── agents/
│ └── test-fixer.md
├── hooks/
│ └── hooks.json
└── output-styles/
└── concise-zh.md
```

`plugin.json` 把所有相对路径写死,并通过 `userConfig` 暴露一项最小用户配置:

```json
{
"name": "team-toolkit",
"version": "1.0.0",
"description": "团队内部共用的 review / test / output 套件",
"commands": "./commands",
"skills": "./skills",
"agents": "./agents",
"hooks": "./hooks/hooks.json",
"outputStyles": "./output-styles",
"mcpServers": {
"internal-search": {
"type": "stdio",
"command": "node",
"args": ["./mcp-server/index.js"]
}
},
"userConfig": {
"githubToken": {
"type": "string",
"title": "GitHub Token",
"description": "用于 review-pr Skill 调用 GitHub API",
"required": true,
"sensitive": true
}
}
}
```

`output-styles/concise-zh.md` 给 plugin 用户默认套上简洁风格(注意 `force-for-plugin` 只对 plugin 来源生效,会覆盖用户在 `/output-style` 里的偏好):

```markdown
---
name: "concise-zh"
description: "简洁中文回答,没有客套"
force-for-plugin: true
---

用简短、直接的中文回答。先给结论再展开理由,不要客气话。
```

> Plugin 来源的 Output Style 由 `utils/plugins/loadPluginOutputStyles.ts:36-85` 的 `loadOutputStyleFromFile()` 加载,frontmatter 只解析 `name`、`description`、`force-for-plugin` 三个字段(外加正文 prompt),其它键会被忽略 —— 比如 `keep-coding-instructions` 在 plugin 路径上不会生效,要保留 system-reminder 行为请直接在正文里写明。
>
> 同时第 55 行会把样式名命名空间化为 `${pluginName}:${baseStyleName}`,所以下面 `concise-zh` 安装后真正注册的名字是 `team-toolkit:concise-zh`。

安装后,Plugin 会把 `commands/stand-up.md` 暴露为 `/team-toolkit:stand-up`、把 `skills/review-pr/` 注册为 `/team-toolkit:review-pr`、把 `agents/test-fixer.md` 加入可调用的 Agent 池、把 `hooks/hooks.json` 中的 hooks 接进事件总线,并自动把 `team-toolkit:concise-zh` 作为输出腔调启用 —— 这就是前面四档扩展点合在一起、走完整 Plugin 发行流程的样子。

---

## 、可迁移的设计模式
## 、可迁移的设计模式

### 模式 1:Markdown-as-Config + Frontmatter 约定

Expand Down Expand Up @@ -1000,8 +1071,6 @@ Claude Code 的扩展系统(Agent、Skill、Plugin)都遵循同一模式:

---

---

## 下一章预告

[第 22 章:Feature Flag 与编译期优化 — 同一份代码构建两个产品](./22-FeatureFlag与编译期优化.md)
Expand Down
8 changes: 6 additions & 2 deletions docs/22-FeatureFlag与编译期优化.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ grep -rhoE "feature\(['\"]([A-Z_0-9]+)['\"]\)" --include="*.ts" --include="*.tsx
| `HISTORY_SNIP` | 16 | 历史片段剪辑 |
| `CHICAGO_MCP` | 16 | Computer Use MCP |

这些 flag 中,`KAIROS`(希腊语「恰当时机」)出现 154 次,几乎是第二名的 1.5 倍 —— 它对应的是 Claude Code 的「助手」(Kairos Assistant)模式,本质上是一个把 Claude Code 内核当成"主动型聊天助手"使用的内部大型实验功能:相比默认的请求-响应循环,它会在更多触发点主动开口(如长任务结束、idle 提醒、定时简报),并依赖 `KAIROS_BRIEF`、`KAIROS_CHANNELS`、`KAIROS_GITHUB_WEBHOOKS` 等一系列同前缀的子 flag 协同。所以严格来说 `KAIROS` 不是"助手模式"这么宽泛,而是"主动型助手"的总开关。

#### 1.3.1 剩余 74 个 flag 的分类速查表

按主题域分组(**逐个列出**,不再让读者自行 grep)。"次"指 `feature('X')` 在源码中的出现次数。下方各组合计 74 个唯一 flag(`BUDDY` 已在 Top 16 中、未在此重复计数)。
Expand Down Expand Up @@ -327,7 +329,7 @@ const contextCollapse = feature('CONTEXT_COLLAPSE')

### 1.5 编译期 + 运行时双重门控:Ablation Baseline

一个特别精巧的用法是 `cli.tsx` 中的 Ablation Baseline(消融实验基线)。它展示了编译期 `feature()` 和运行时环境变量**组合使用**的模式:
一个特别精巧的用法是 `cli.tsx` 中的 Ablation Baseline。**先解释一下名字** —— 在内部实验流水线里,开发者需要一个"什么花哨功能都关掉"的基线版本来对比一个新功能到底带来了多大效果,这种"去掉某条件作为对照组"的做法在机器学习里叫"消融实验"(Ablation Study),所以这里的"基线"指的就是"实验对照组"。对外部读者来说,它的意义在于:这是一个**编译期 `feature()` 和运行时环境变量组合使用**的范本,外部构建里这整段会被 DCE 删掉,所以你不会真的在你的 `claude` 里遇到它,但模式本身可以借鉴。

```typescript
// entrypoints/cli.tsx:16-26
Expand Down Expand Up @@ -728,7 +730,9 @@ function isScratchpadGateEnabled(): boolean {
}
```

这展示了三层如何嵌套:`feature()` 决定 coordinator 代码是否存在 → 环境变量决定 coordinator 是否激活 → GrowthBook 决定 coordinator 内部的 scratchpad 子功能是否启用。
这里调用名带 `Statsig`、所在文件却叫 `growthbook.ts`,并不是命名错误,而是**迁移期的兼容层**:项目历史上用 Statsig 做实验平台,现在正在迁到 GrowthBook,`services/analytics/growthbook.ts:792-836` 的注释明确写道这个函数是"MIGRATION ONLY"——它先查 GrowthBook 缓存,未命中再回退到 `config.cachedStatsigGates`。也就是说,Statsig 是"上一代"实验平台、GrowthBook 是"这一代",两者通过这种命名前缀 + 旧缓存兜底的方式共存在同一个文件里,直到所有 gate 完成迁移。

这展示了三层如何嵌套:`feature()` 决定 coordinator 代码是否存在 → 环境变量决定 coordinator 是否激活 → GrowthBook(含 Statsig 兼容回退)决定 coordinator 内部的 scratchpad 子功能是否启用。

```mermaid
graph LR
Expand Down
14 changes: 8 additions & 6 deletions docs/23-客户端传输与API重试.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,11 @@ Claude Code 的 API 调用默认走流式(SSE),失败时自动降级到非

> 这部分内容已迁移到[第 25 章:DirectConnect 与上游代理](./25-DirectConnect-与上游代理.md)。本节作为占位锚点保留,便于从历史目录直接跳转,并提示读者:流式降级与本章的 `withRetry` 主循环、传输层长连接是三条互不相同的代码路径,请勿混淆。

## 客户端传输层:WebSocket / SSE / Hybrid 三态
---

## 五、客户端传输层:WebSocket / SSE / Hybrid 三态

到此为止讨论的都是面向模型 API(`api.anthropic.com/messages`)的请求-响应往返。但 Claude Code 还有一条独立的传输路径 —— **客户端与会话服务的长连接**。BridgeTeleport、远程容器都依赖它在浏览器、容器、CLI 之间双向同步事件。它的失败模式和模型 API 完全不同:断网不再是"重发一次 POST"那么简单,而是要面对**断线重连、事件重放、token 刷新、休眠唤醒、批量上传与背压、多副本切换**。
到此为止讨论的都是面向模型 API(`api.anthropic.com/messages`)的请求-响应往返。但 Claude Code 还有一条独立的传输路径 —— **客户端与会话服务的长连接**。这条路径承载的就是 **CCR**(Claude Cloud Runtime,会话云端运行时)—— 它把每个 Claude Code 会话托管在云侧 worker 里,再通过本节的 WebSocket / SSE / Hybrid 把事件流双向同步到本地 CLI 和远端浏览器/手机端(Bridge / Teleport)。后文出现的 `CCR v2`、`worker`、`worker_epoch`、`Bridge`、`Teleport` 都是这套架构里的角色,下一章 C24 会专门拆解;本节只把它们当作"长连接对侧"的代号使用。它的失败模式和模型 API 完全不同:断网不再是"重发一次 POST"那么简单,而是要面对**断线重连、事件重放、token 刷新、休眠唤醒、批量上传与背压、多副本切换**。

`cli/transports/` 目录维护了三个共享 `Transport` 接口的实现,由一个轻量级 dispatcher 根据环境变量挑选:

Expand Down Expand Up @@ -744,7 +746,7 @@ if (response.status === 409) {

---

## 、连接错误分类与用户友好提示
## 、连接错误分类与用户友好提示

模型 API 重试层和传输层共享同一套底层错误工具,集中在 `services/api/errorUtils.ts`。

Expand Down Expand Up @@ -842,7 +844,7 @@ type NestedAPIError = {

---

## 、资源泄漏防护
## 、资源泄漏防护

### 7.1 流资源释放

Expand Down Expand Up @@ -889,7 +891,7 @@ return (input, init) => {

---

## 、完整的 API 调用生命周期
## 、完整的 API 调用生命周期

把模型 API 重试链与传输层放在同一张图里,可以看到 Claude Code 完整的对外通信形态:

Expand Down Expand Up @@ -948,7 +950,7 @@ sequenceDiagram

---

## 、可迁移的设计模式
## 、可迁移的设计模式

### 模式 1:AsyncGenerator 重试层

Expand Down
Loading
Loading