vault backup: 2026-06-08 23:26:18
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
---
|
||||
tags:
|
||||
- claude-code
|
||||
- mermaid
|
||||
create time: 2026-06-08 00:00
|
||||
---
|
||||
|
||||
# Agent Loop 简易流程
|
||||
|
||||
## 概述
|
||||
|
||||
Claude Code Agent 的核心执行循环:输入经过 Context 管理后交给 LLM 流式输出,若返回 `tool_use` 则执行工具并将结果回注 Context,循环往复;否则结束。
|
||||
|
||||
## 正文
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
START((输入)) --> CTX["Context 管理"]
|
||||
CTX --> LLM["LLM 流式输出"]
|
||||
LLM --> TC{"tool_use?"}
|
||||
|
||||
TC --> |是| EXEC["执行工具"]
|
||||
EXEC --> CTX
|
||||
|
||||
TC --> |否| DONE((完成))
|
||||
|
||||
classDef proc fill:#eef,stroke:#66c,color:#224
|
||||
classDef decision fill:#fee,stroke:#c66,color:#422
|
||||
classDef io fill:#eff,stroke:#6cc,color:#244
|
||||
|
||||
class CTX,LLM,EXEC proc
|
||||
class TC decision
|
||||
class START,DONE io
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
```mermaid
|
||||
flowchart TB
|
||||
START((输入)) --> CTX["Context 管理"]
|
||||
CTX --> PRE["Pre-sampling Hook"]
|
||||
PRE --> LLM["LLM 流式输出"]
|
||||
LLM --> TC{tool_use?}
|
||||
|
||||
TC --> |是| PERM{需权限?}
|
||||
PERM --> |是| USER["👤 用户审批"]
|
||||
USER --> |allow| TOOL_PRE
|
||||
USER --> |deny| DENIED["拒绝"]
|
||||
PERM --> |否| TOOL_PRE["Pre-tool Hook"]
|
||||
TOOL_PRE --> EXEC["并发执行工具"]
|
||||
EXEC --> TOOL_POST["Post-tool Hook"]
|
||||
TOOL_POST --> CTX
|
||||
DENIED --> CTX
|
||||
|
||||
TC --> |否| POST["Post-sampling Hook"]
|
||||
POST --> STOP{"Stop Hook"}
|
||||
STOP --> |不通过| CTX
|
||||
STOP --> |通过| BUDGET{"Token Budget"}
|
||||
BUDGET --> |继续| CTX
|
||||
BUDGET --> |完成| DONE((完成))
|
||||
|
||||
subgraph SUB["子 Agent"]
|
||||
FORK["AgentTool"] --> RECURSE["递归调用"]
|
||||
end
|
||||
|
||||
EXEC -.-> FORK
|
||||
|
||||
classDef proc fill:#eef,stroke:#66c,color:#224
|
||||
classDef decision fill:#fee,stroke:#c66,color:#422
|
||||
classDef hook fill:#ffe,stroke:#cc6,color:#442
|
||||
classDef io fill:#eff,stroke:#6cc,color:#244
|
||||
classDef sub fill:#efe,stroke:#6a6,color:#242
|
||||
|
||||
class CTX,LLM,EXEC proc
|
||||
class TC,PERM,STOP,BUDGET decision
|
||||
class PRE,TOOL_PRE,TOOL_POST,POST hook
|
||||
class START,DONE,USER,DENIED io
|
||||
class FORK,RECURSE sub
|
||||
```
|
||||
@@ -0,0 +1,211 @@
|
||||
---
|
||||
title: "自定义 Agent - 从 Markdown 到运行时的完整链路"
|
||||
description: "揭秘 Claude Code 自定义 Agent 完整链路:Agent 定义的 Markdown 数据模型、三种加载来源、工具过滤策略和与 AgentTool 的联动机制。"
|
||||
keywords: ["自定义 Agent", "Agent 定义", "Markdown Agent", "Agent 配置", "角色定制"]
|
||||
---
|
||||
|
||||
{/* 本章目标:揭示 Agent 定义的完整数据模型、加载发现机制、工具过滤和与 AgentTool 的联动 */}
|
||||
|
||||
## Agent 定义的三种来源
|
||||
|
||||
Claude Code 的 Agent 不仅仅来自用户自定义——系统有三类来源,按优先级合并:
|
||||
|
||||
| 来源 | 位置 | 优先级 |
|
||||
|------|------|--------|
|
||||
| **Built-in** | `packages/builtin-tools/src/tools/AgentTool/built-in/` 硬编码 | 最低(可被覆盖) |
|
||||
| **Plugin** | 通过插件系统注册 | 中 |
|
||||
| **User/Project/Policy** | `.claude/agents/*.md` 或 settings.json | 最高 |
|
||||
|
||||
合并逻辑在 `getActiveAgentsFromList()` 中:按 `agentType` 去重,后者覆盖前者。这意味着你可以在 `.claude/agents/` 中放一个 `Explore.md` 来完全替换内置的 Explore Agent。
|
||||
|
||||
## Markdown Agent 文件的完整格式
|
||||
|
||||
```markdown
|
||||
---
|
||||
# === 必需字段 ===
|
||||
name: "reviewer" # Agent 标识(agentType)
|
||||
description: "Code review specialist, read-only analysis"
|
||||
|
||||
# === 工具控制 ===
|
||||
tools: "Read,Glob,Grep,Bash" # 允许的工具列表(逗号分隔)
|
||||
disallowedTools: "Write,Edit" # 显式禁止的工具
|
||||
|
||||
# === 模型配置 ===
|
||||
model: "haiku" # 指定模型(或 "inherit" 继承主线程)
|
||||
effort: "high" # 推理努力程度:low/medium/high 或整数
|
||||
|
||||
# === 行为控制 ===
|
||||
maxTurns: 10 # 最大 agentic 轮次
|
||||
permissionMode: "plan" # 权限模式:plan/bypassPermissions 等
|
||||
background: true # 始终作为后台任务运行
|
||||
initialPrompt: "/search TODO" # 首轮用户消息前缀(支持斜杠命令)
|
||||
|
||||
# === 隔离与持久化 ===
|
||||
isolation: "worktree" # 在独立 git worktree 中运行
|
||||
memory: "project" # 持久记忆范围:user/project/local
|
||||
|
||||
# === MCP 服务器 ===
|
||||
mcpServers:
|
||||
- "slack" # 引用已配置的 MCP 服务器
|
||||
- database: # 内联定义
|
||||
command: "npx"
|
||||
args: ["mcp-db"]
|
||||
|
||||
# === Hooks ===
|
||||
hooks:
|
||||
PreToolUse:
|
||||
- command: "audit-log.sh"
|
||||
timeout: 5000
|
||||
|
||||
# === Skills ===
|
||||
skills: "code-review,security-review" # 预加载的 skills(逗号分隔)
|
||||
|
||||
# === 显示 ===
|
||||
color: "blue" # 终端中的 Agent 颜色标识
|
||||
---
|
||||
|
||||
你是代码审查专家。你的职责是...
|
||||
|
||||
(正文内容 = system prompt)
|
||||
```
|
||||
|
||||
### 字段解析细节
|
||||
|
||||
- **`tools`**:通过 `parseAgentToolsFromFrontmatter()` 解析,支持逗号分隔字符串或数组
|
||||
- **`model: "inherit"`**:使用主线程的模型(区分大小写,只有小写 "inherit" 有效)
|
||||
- **`memory`**:启用后自动注入 `Write`/`Edit`/`Read` 工具(即使 `tools` 未包含),并在 system prompt 末尾追加 memory 指令
|
||||
- **`isolation: "remote"`**:仅在 Anthropic 内部可用(`USER_TYPE === 'ant'`),外部构建只支持 `worktree`
|
||||
- **`background`**:`true` 使 Agent 始终在后台运行,主线程不等待结果
|
||||
|
||||
## 加载与发现机制
|
||||
|
||||
`getAgentDefinitionsWithOverrides()`(被 `memoize` 缓存)执行完整的发现流程:
|
||||
|
||||
```
|
||||
1. 加载 Markdown 文件
|
||||
├── loadMarkdownFilesForSubdir('agents', cwd)
|
||||
│ ├── ~/.claude/agents/*.md (用户级,source = 'userSettings')
|
||||
│ ├── .claude/agents/*.md (项目级,source = 'projectSettings')
|
||||
│ └── managed/policy sources (策略级,source = 'policySettings')
|
||||
│
|
||||
└── 每个 .md 文件:
|
||||
├── 解析 YAML frontmatter
|
||||
├── 正文作为 system prompt
|
||||
├── 校验必需字段(name, description)
|
||||
├── 静默跳过无 frontmatter 的 .md 文件(可能是参考文档)
|
||||
└── 解析失败 → 记录到 failedFiles,不阻塞其他 Agent
|
||||
|
||||
2. 并行加载 Plugin Agents
|
||||
└── loadPluginAgents() → memoized
|
||||
|
||||
3. 初始化 Memory Snapshots(如果 AGENT_MEMORY_SNAPSHOT 启用)
|
||||
└── initializeAgentMemorySnapshots()
|
||||
|
||||
4. 合并 Built-in + Plugin + Custom
|
||||
└── getActiveAgentsFromList() → 按 agentType 去重,后者覆盖前者
|
||||
|
||||
5. 分配颜色
|
||||
└── setAgentColor(agentType, color) → 终端 UI 中区分不同 Agent
|
||||
```
|
||||
|
||||
## 工具过滤的实现
|
||||
|
||||
当 Agent 被派生时,`AgentTool` 根据定义中的 `tools` / `disallowedTools` 过滤可用工具列表:
|
||||
|
||||
```
|
||||
全部工具
|
||||
↓ disallowedTools 移除
|
||||
↓ tools 白名单过滤(如果指定)
|
||||
可用工具
|
||||
```
|
||||
|
||||
- **`tools` 未指定**:Agent 可以使用所有工具(默认全能)
|
||||
- **`tools` 指定**:只能使用列出的工具
|
||||
- **`disallowedTools`**:即使 `tools` 未指定,这些工具也被禁止
|
||||
- **自动注入**:`memory` 启用时自动添加 `Write`/`Edit`/`Read`
|
||||
|
||||
以内置 Explore Agent 为例:
|
||||
|
||||
```typescript
|
||||
// packages/builtin-tools/src/tools/AgentTool/built-in/exploreAgent.ts
|
||||
disallowedTools: [
|
||||
'Agent', // 不能嵌套调用 Agent
|
||||
'ExitPlanMode', // 不需要 plan mode
|
||||
'FileEdit', // 只读
|
||||
'FileWrite', // 只读
|
||||
'NotebookEdit', // 只读
|
||||
]
|
||||
```
|
||||
|
||||
## System Prompt 的注入方式
|
||||
|
||||
Agent 的 system prompt 通过 `getSystemPrompt()` 闭包延迟生成:
|
||||
|
||||
```typescript
|
||||
// Markdown Agent
|
||||
getSystemPrompt: () => {
|
||||
if (isAutoMemoryEnabled() && memory) {
|
||||
return systemPrompt + '\n\n' + loadAgentMemoryPrompt(agentType, memory)
|
||||
}
|
||||
return systemPrompt
|
||||
}
|
||||
```
|
||||
|
||||
这意味着:
|
||||
1. **Markdown 正文 = 完整的 system prompt**——不是追加,而是替换默认 prompt
|
||||
2. **Memory 指令**在 memory 启用时自动追加到末尾
|
||||
3. **闭包延迟计算**——memory 状态可能在文件加载后才变化
|
||||
|
||||
对于 Built-in Agent,`getSystemPrompt` 接受 `toolUseContext` 参数,可以根据运行时状态(如是否使用嵌入式搜索工具)动态调整 prompt 内容。
|
||||
|
||||
## 与 AgentTool 的联动
|
||||
|
||||
当主 Agent 需要派生子 Agent 时:
|
||||
|
||||
```
|
||||
AgentTool.call({ subagent_type: "reviewer", ... })
|
||||
↓
|
||||
1. 从 agentDefinitions.activeAgents 查找 agentType === "reviewer"
|
||||
↓
|
||||
2. 检查 requiredMcpServers(如果 Agent 要求特定 MCP 服务器)
|
||||
↓
|
||||
3. 过滤工具列表(tools / disallowedTools)
|
||||
↓
|
||||
4. 解析模型:
|
||||
- "inherit" → 使用主线程模型
|
||||
- 具体模型名 → 直接使用
|
||||
- 未指定 → 主线程模型
|
||||
↓
|
||||
5. 解析权限模式(permissionMode)
|
||||
↓
|
||||
6. 构建隔离环境(如果 isolation === "worktree")
|
||||
↓
|
||||
7. 注入 system prompt(getSystemPrompt())
|
||||
↓
|
||||
8. 注入 initialPrompt(如果定义了)
|
||||
↓
|
||||
9. 启动子 Agent 循环(forkSubagent / runAgent)
|
||||
```
|
||||
|
||||
## 内置 Agent 参考
|
||||
|
||||
| Agent | agentType | 角色 | 工具限制 | 模型 |
|
||||
|-------|-----------|------|---------|------|
|
||||
| **General Purpose** | `general-purpose` | 默认子 Agent | 全部工具 | 主线程模型 |
|
||||
| **Explore** | `Explore` | 代码搜索专家 | 只读(无 Write/Edit) | haiku(外部) |
|
||||
| **Plan** | `Plan` | 规划专家 | 只读 + ExitPlanMode | inherit |
|
||||
| **Verification** | `verification` | 结果验证 | 由 feature flag 控制 | — |
|
||||
| **Code Guide** | `claude-code-guide` | Claude Code 使用指南 | 只读 | — |
|
||||
| **Statusline Setup** | `statusline-setup` | 终端状态栏配置 | 有限 | — |
|
||||
|
||||
SDK 入口(`sdk-ts`/`sdk-py`/`sdk-cli`)不加载 Code Guide Agent。环境变量 `CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS` 可以完全禁用内置 Agent,给 SDK 用户提供空白画布。
|
||||
|
||||
## Agent Memory:持久化的 Agent 状态
|
||||
|
||||
当 `memory` 字段启用时,Agent 获得跨会话的持久记忆:
|
||||
|
||||
- **`local`**:当前项目、当前用户有效
|
||||
- **`project`**:当前项目所有用户共享
|
||||
- **`user`**:所有项目共享
|
||||
|
||||
Memory 通过 `loadAgentMemoryPrompt()` 注入到 system prompt 末尾,包含读写记忆的指令。Agent Memory Snapshot 机制在项目间同步 `user` 级记忆。
|
||||
@@ -0,0 +1,253 @@
|
||||
---
|
||||
title: "Hooks 生命周期钩子 - 执行引擎与拦截协议"
|
||||
description: "从源码角度解析 Claude Code Hooks 系统:27 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。"
|
||||
keywords: ["Hooks", "生命周期钩子", "拦截器", "PreToolUse", "Hook 协议"]
|
||||
---
|
||||
|
||||
{/* 本章目标:从源码角度揭示 Hook 的执行引擎、匹配机制、返回值协议和生命周期管理 */}
|
||||
|
||||
## 27 种 Hook 事件
|
||||
|
||||
Claude Code 定义了 27 种 Hook 事件(`HOOK_EVENTS` 数组,`src/entrypoints/sdk/coreTypes.ts`),覆盖完整的 Agent 生命周期:
|
||||
|
||||
| 阶段 | 事件 | 触发时机 | 匹配字段 |
|
||||
|------|------|---------|---------|
|
||||
| **会话** | `SessionStart` | 会话启动 | `source` |
|
||||
| | `SessionEnd` | 会话结束 | `reason` |
|
||||
| | `Setup` | 初始化完成 | `trigger` |
|
||||
| **用户交互** | `UserPromptSubmit` | 用户提交消息 | — |
|
||||
| | `Stop` | Agent 停止响应 | — |
|
||||
| | `StopFailure` | Agent 停止失败 | `error` |
|
||||
| **工具执行** | `PreToolUse` | 工具调用前 | `tool_name` |
|
||||
| | `PostToolUse` | 工具调用后(成功) | `tool_name` |
|
||||
| | `PostToolUseFailure` | 工具调用后(失败) | `tool_name` |
|
||||
| **权限** | `PermissionRequest` | 权限请求 | `tool_name` |
|
||||
| | `PermissionDenied` | 权限被拒 | `tool_name` |
|
||||
| **子 Agent** | `SubagentStart` | 子 Agent 启动 | `agent_type` |
|
||||
| | `SubagentStop` | 子 Agent 停止 | `agent_type` |
|
||||
| **压缩** | `PreCompact` | 上下文压缩前 | `trigger` |
|
||||
| | `PostCompact` | 上下文压缩后 | `trigger` |
|
||||
| **协作** | `TeammateIdle` | Teammate 空闲 | — |
|
||||
| | `TaskCreated` | 任务创建 | — |
|
||||
| | `TaskCompleted` | 任务完成 | — |
|
||||
| **MCP** | `Elicitation` | MCP 服务器请求用户输入 | `mcp_server_name` |
|
||||
| | `ElicitationResult` | Elicitation 结果返回 | `mcp_server_name` |
|
||||
| **通知** | `Notification` | 系统通知事件 | `notification_type` |
|
||||
| **环境** | `ConfigChange` | 配置变更 | `source` |
|
||||
| | `CwdChanged` | 工作目录变更 | — |
|
||||
| | `FileChanged` | 文件变更 | `file_path` |
|
||||
| | `InstructionsLoaded` | 指令加载 | `load_reason` |
|
||||
| | `WorktreeCreate` / `WorktreeRemove` | Worktree 操作 | — |
|
||||
|
||||
## 6 种 Hook 类型
|
||||
|
||||
Hooks 配置支持 6 种执行方式,类型定义分布在 3 个文件中:
|
||||
|
||||
- **可持久化类型**(`command`、`prompt`、`agent`、`http`)— Zod schema 定义在 `src/schemas/hooks.ts`,通过 `z.discriminatedUnion('type', [...])` 声明
|
||||
- **callback 类型** — TypeScript 接口定义在 `src/types/hooks.ts`,用于 SDK 注册的内部 JS 函数
|
||||
- **function 类型** — 定义在 `src/utils/hooks/sessionHooks.ts`,用于运行时动态注册的函数 Hook
|
||||
|
||||
| 类型 | 执行方式 | 适用场景 |
|
||||
|------|---------|---------|
|
||||
| `command` | Shell 命令(bash/PowerShell) | 通用脚本、CI 检查 |
|
||||
| `prompt` | 注入到 AI 上下文 | 代码规范提醒 |
|
||||
| `agent` | 启动子 Agent 执行 | 复杂分析任务 |
|
||||
| `http` | HTTP 请求 | 远程服务、Webhook |
|
||||
| `callback` | 内部 JS 函数 | 系统内置 Hook |
|
||||
| `function` | 运行时注册的函数 Hook | Agent/Skill 内部使用 |
|
||||
|
||||
## 执行引擎:execCommandHook
|
||||
|
||||
`execCommandHook()`(`src/utils/hooks.ts`,`execCommandHook` 函数)是命令型 Hook 的执行核心:
|
||||
|
||||
```
|
||||
execCommandHook(hook, hookEvent, hookName, jsonInput, signal)
|
||||
├── Shell 选择: hook.shell ?? DEFAULT_HOOK_SHELL
|
||||
│ ├── bash: spawn(cmd, [], { shell: gitBashPath | true })
|
||||
│ └── powershell: spawn(pwsh, ['-NoProfile', '-NonInteractive', '-Command', cmd])
|
||||
├── 变量替换
|
||||
│ ├── ${CLAUDE_PLUGIN_ROOT} → pluginRoot 路径
|
||||
│ ├── ${CLAUDE_PLUGIN_DATA} → plugin 数据目录
|
||||
│ └── ${user_config.X} → 用户配置值
|
||||
├── 环境变量注入
|
||||
│ ├── CLAUDE_PROJECT_DIR
|
||||
│ ├── CLAUDE_ENV_FILE(SessionStart/Setup/CwdChanged/FileChanged)
|
||||
│ └── CLAUDE_PLUGIN_OPTION_*(plugin options)
|
||||
├── stdin 写入: jsonInput + '\n'
|
||||
├── 超时: hook.timeout * 1000 ?? 600000ms(10分钟)
|
||||
└── 异步检测: 检查 stdout 首行是否为 {"async":true}
|
||||
```
|
||||
|
||||
### 异步 Hook 的检测协议
|
||||
|
||||
Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务(`isAsyncHookJSONOutput` 检测 + `executeInBackground` 调用):
|
||||
|
||||
```typescript
|
||||
const firstLine = firstLineOf(stdout).trim()
|
||||
if (isAsyncHookJSONOutput(parsed)) {
|
||||
executeInBackground({
|
||||
processId: `async_hook_${child.pid}`,
|
||||
asyncResponse: parsed,
|
||||
...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
后台 Hook 通过 `registerPendingAsyncHook()` 注册到 `AsyncHookRegistry`,完成后通过 `enqueuePendingNotification()` 通知主线程。
|
||||
|
||||
### asyncRewake:Hook 唤醒模型
|
||||
|
||||
`asyncRewake` 模式的 Hook 绕过 `AsyncHookRegistry`。当 Hook 退出码为 2 时,通过 `enqueuePendingNotification()` 以 `task-notification` 模式注入消息,唤醒空闲的模型(通过 `useQueueProcessor`)或在忙碌时注入 `queued_command` 附件。
|
||||
|
||||
## Hook 输出的 JSON Schema
|
||||
|
||||
同步 Hook 的输出遵循严格的 Zod schema(`syncHookResponseSchema`,定义在 `src/types/hooks.ts`,`hookJSONOutputSchema` 定义在 `src/schemas/hooks.ts`):
|
||||
|
||||
```json
|
||||
{
|
||||
"continue": false, // 是否继续执行
|
||||
"suppressOutput": true, // 隐藏 stdout
|
||||
"stopReason": "安全检查失败", // continue=false 时的原因
|
||||
"decision": "approve" | "block", // 全局决策
|
||||
"reason": "原因说明", // 决策原因
|
||||
"systemMessage": "警告内容", // 注入到上下文的系统消息
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "allow" | "deny" | "ask",
|
||||
"permissionDecisionReason": "匹配了安全规则",
|
||||
"updatedInput": { ... }, // 修改后的工具输入
|
||||
"additionalContext": "额外上下文" // 注入到对话
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 各事件的 hookSpecificOutput
|
||||
|
||||
| 事件 | 专有字段 | 作用 |
|
||||
|------|---------|------|
|
||||
| `PreToolUse` | `permissionDecision`, `permissionDecisionReason`, `updatedInput`, `additionalContext` | 拦截/修改工具输入 |
|
||||
| `PostToolUse` | `additionalContext`, `updatedMCPToolOutput` | 修改 MCP 工具输出 |
|
||||
| `PostToolUseFailure` | `additionalContext` | 失败后注入上下文 |
|
||||
| `UserPromptSubmit` | `additionalContext` | 注入额外上下文 |
|
||||
| `SessionStart` | `additionalContext`, `initialUserMessage`, `watchPaths` | 设置初始消息和文件监控 |
|
||||
| `PermissionRequest` | `decision`(含 `allow`/`deny` 子字段) | 权限请求的 Hook 决策 |
|
||||
| `PermissionDenied` | `retry` | 指示是否重试 |
|
||||
| `SubagentStart` | `additionalContext` | 子 Agent 启动时注入上下文 |
|
||||
| `Elicitation` | `action`, `content` | 控制用户输入对话框 |
|
||||
| `ElicitationResult` | `action`, `content` | Elicitation 结果处理 |
|
||||
| `Notification` | `additionalContext` | 通知事件注入上下文 |
|
||||
| `Setup` | `additionalContext` | 初始化时注入上下文 |
|
||||
| `CwdChanged` | `watchPaths` | 目录变更后更新监控路径 |
|
||||
| `FileChanged` | `watchPaths` | 文件变更后更新监控路径 |
|
||||
| `WorktreeCreate` | `worktreePath` | Worktree 创建通知 |
|
||||
|
||||
## Hook 匹配机制:getMatchingHooks
|
||||
|
||||
`getMatchingHooks()`(`src/utils/hooks.ts`,`getMatchingHooks` 函数)负责从所有来源中查找匹配的 Hook:
|
||||
|
||||
### 多来源合并
|
||||
|
||||
```
|
||||
getHooksConfig()
|
||||
├── getHooksConfigFromSnapshot() ← settings.json 中的 Hook(user/project/local)
|
||||
├── getRegisteredHooks() ← SDK 注册的 callback Hook
|
||||
├── getSessionHooks() ← Agent/Skill 前置注册的 session Hook
|
||||
└── getSessionFunctionHooks() ← 运行时 function Hook
|
||||
```
|
||||
|
||||
### 匹配规则
|
||||
|
||||
`matcher` 字段支持三种模式(`matchesPattern()` 函数,`src/utils/hooks.ts`):
|
||||
|
||||
```
|
||||
"Write" → 精确匹配
|
||||
"Write|Edit" → 管道分隔的多值匹配
|
||||
"^Bash(git.*)" → 正则匹配
|
||||
"*" 或 "" → 通配(匹配所有)
|
||||
```
|
||||
|
||||
### if 条件过滤
|
||||
|
||||
Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()`(`src/utils/hooks.ts`,`prepareIfConditionMatcher` 函数)预编译匹配器:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": [{
|
||||
"command": "check-git-branch.sh",
|
||||
"if": "Bash(git push*)"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
`if` 条件使用 `permissionRuleValueFromString` 解析,支持与权限规则相同的语法(工具名 + 参数模式)。Bash 工具还会使用 tree-sitter 进行 AST 级别的命令解析。
|
||||
|
||||
### Hook 去重
|
||||
|
||||
同一个 Hook 命令在不同配置层级(user/project/local)可能重复。系统按四部分复合键做 Map 去重:`${pluginRoot}\0${shell}\0${command}\0${ifCondition}`(由 `hookDedupKey()` 函数构建),保留**最后合并的层级**。
|
||||
|
||||
## 工作区信任检查
|
||||
|
||||
**所有 Hook 都要求工作区信任**(`shouldSkipHookDueToTrust()` 函数,`src/utils/hooks.ts`)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。
|
||||
|
||||
```typescript
|
||||
// 交互模式下,所有 Hook 要求信任
|
||||
const hasTrust = checkHasTrustDialogAccepted()
|
||||
return !hasTrust
|
||||
```
|
||||
|
||||
SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 true 时跳过检查)。
|
||||
|
||||
## 四种 Hook 能力的源码映射
|
||||
|
||||
### 1. 拦截操作(PreToolUse)
|
||||
|
||||
```json
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`processHookJSONOutput()` 将 `permissionDecision` 映射为 `result.permissionBehavior = 'deny'`,并设置 `blockingError`,阻止工具执行。
|
||||
|
||||
### 2. 修改行为(updatedInput / updatedMCPToolOutput)
|
||||
|
||||
```json
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"updatedInput": { "command": "npm test -- --bail" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`updatedInput` 替换原始工具输入;`updatedMCPToolOutput`(PostToolUse 事件)替换 MCP 工具的返回值——可用于过滤敏感数据。
|
||||
|
||||
### 3. 注入上下文(additionalContext / systemMessage)
|
||||
|
||||
- `additionalContext` → 通过 `createAttachmentMessage({ type: 'hook_additional_context' })` 注入为用户消息
|
||||
- `systemMessage` → 注入为系统警告,直接显示给用户
|
||||
|
||||
### 4. 控制流程(continue / stopReason)
|
||||
|
||||
```json
|
||||
{ "continue": false, "stopReason": "构建失败,停止执行" }
|
||||
```
|
||||
|
||||
`continue: false` 设置 `preventContinuation = true`,阻止 Agent 继续执行后续操作。
|
||||
|
||||
## Session Hook 的生命周期
|
||||
|
||||
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(调用位置:`packages/builtin-tools/src/tools/AgentTool/runAgent.ts`;定义位置:`src/utils/hooks/registerFrontmatterHooks.ts`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()`(定义位置:`src/utils/hooks/sessionHooks.ts`)清理。
|
||||
|
||||
```typescript
|
||||
// runAgent.ts — 注册 agent 的前置 Hook
|
||||
registerFrontmatterHooks(rootSetAppState, agentId, agentDefinition.hooks, ...)
|
||||
|
||||
// runAgent.ts — finally 块清理
|
||||
clearSessionHooks(rootSetAppState, agentId)
|
||||
```
|
||||
|
||||
这确保 Agent A 的 Hook 不会泄漏到 Agent B 的执行中。
|
||||
@@ -0,0 +1,346 @@
|
||||
---
|
||||
title: "MCP 配置 - 多来源合并、作用域与策略管控"
|
||||
description: "详细说明 Claude Code MCP 配置的来源层次、合并优先级、传输类型、企业策略管控、插件集成和保留名称机制。"
|
||||
keywords: ["MCP", "配置", "settings.json", ".mcp.json", "企业策略", "插件"]
|
||||
---
|
||||
|
||||
## 配置来源与作用域
|
||||
|
||||
Claude Code 的 MCP 配置来自多个来源,每个来源对应一个 `scope`(作用域)。配置按优先级合并,高优先级来源的同名配置覆盖低优先级。
|
||||
|
||||
### 来源列表
|
||||
|
||||
| 来源 | Scope | 文件/接口 | 说明 |
|
||||
|------|-------|----------|------|
|
||||
| 企业管控 | `enterprise` | 系统管理路径 `managed-mcp.json` | **排他模式**:存在时忽略所有其他来源 |
|
||||
| 本地项目 | `local` | `<project>/.claude/settings.local.json` | 项目级私有配置(不提交到 VCS) |
|
||||
| 项目配置 | `project` | `<project>/.mcp.json` | 项目级共享配置(可提交到 VCS) |
|
||||
| 用户全局 | `user` | `~/.claude/settings.json` | 用户级配置,所有项目共享 |
|
||||
| 插件 | `dynamic` | 插件 manifest 中 `.mcp.json` / `.mcpb` | 插件提供的 MCP 服务器 |
|
||||
| claude.ai | `claudeai` | 通过 API 获取 | claude.ai 网页端配置的连接器 |
|
||||
| 内置动态 | `dynamic` | 代码中注册 | Computer Use / Chrome 等内置服务器 |
|
||||
| IDE SDK | `sdk` | IDE 传入 | VS Code / JetBrains 嵌入模式 |
|
||||
|
||||
### 合并优先级(从低到高)
|
||||
|
||||
```
|
||||
claude.ai 连接器 ← 最低优先级
|
||||
↓ 去重
|
||||
插件服务器
|
||||
↓ 去重
|
||||
用户全局配置
|
||||
↓
|
||||
项目配置(.mcp.json) ← 需要用户审批
|
||||
↓
|
||||
本地项目配置
|
||||
↓
|
||||
动态配置(内置 MCP) ← 最高优先级
|
||||
```
|
||||
|
||||
`Object.assign({}, dedupedPluginServers, userServers, approvedProjectServers, localServers)` 实现合并——后出现的同名键覆盖前者。
|
||||
|
||||
## 企业管控模式
|
||||
|
||||
当 `managed-mcp.json` 文件存在时,进入 **排他模式**:
|
||||
|
||||
```typescript
|
||||
// config.ts:1084
|
||||
if (doesEnterpriseMcpConfigExist()) {
|
||||
// 只返回企业配置,忽略所有用户/项目/插件/claude.ai 配置
|
||||
return { servers: filtered, errors: [] }
|
||||
}
|
||||
```
|
||||
|
||||
特性:
|
||||
- 路径由系统管理决定(`getManagedFilePath()` + `managed-mcp.json`)
|
||||
- 覆盖所有用户级、项目级、插件和 claude.ai 配置
|
||||
- 仍然应用策略过滤(allowlist/denylist)
|
||||
- 无法通过 CLI 添加新服务器(`addMcpConfig` 会拒绝)
|
||||
|
||||
## 传输类型与配置 Schema
|
||||
|
||||
### stdio(默认)
|
||||
|
||||
启动子进程,通过 stdin/stdout JSON-RPC 通信。
|
||||
|
||||
```json
|
||||
{
|
||||
"my-server": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@my-org/mcp-server"],
|
||||
"env": { "API_KEY": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`type` 字段可省略(默认为 `stdio`)。环境变量通过 `env` 传递给子进程,会与当前进程环境合并。
|
||||
|
||||
**Windows 注意**:使用 `npx` 需要包装为 `cmd /c npx`,否则会报错。
|
||||
|
||||
### SSE(Server-Sent Events)
|
||||
|
||||
通过 HTTP SSE 连接远程 MCP 服务器。
|
||||
|
||||
```json
|
||||
{
|
||||
"my-remote": {
|
||||
"type": "sse",
|
||||
"url": "https://mcp.example.com/sse",
|
||||
"headers": { "Authorization": "Bearer ..." },
|
||||
"oauth": {
|
||||
"clientId": "...",
|
||||
"authServerMetadataUrl": "https://auth.example.com/.well-known/oauth-authorization-server"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
支持 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,15 分钟 TTL 缓存避免重复提示。
|
||||
|
||||
### HTTP(Streamable HTTP)
|
||||
|
||||
HTTP 流式传输。
|
||||
|
||||
```json
|
||||
{
|
||||
"my-http": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.example.com/mcp",
|
||||
"headers": { "X-API-Key": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
支持与 SSE 相同的 OAuth 配置。
|
||||
|
||||
### WebSocket
|
||||
|
||||
```json
|
||||
{
|
||||
"my-ws": {
|
||||
"type": "ws",
|
||||
"url": "wss://mcp.example.com/ws"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IDE 专用类型(内部)
|
||||
|
||||
`sse-ide` 和 `ws-ide` 是 IDE 扩展专用类型,不由用户直接配置。
|
||||
|
||||
- `sse-ide`:使用 lockfile token 认证
|
||||
- `ws-ide`:使用 `X-Claude-Code-Ide-Authorization` header
|
||||
|
||||
### SDK 类型(内部)
|
||||
|
||||
`type: "sdk"` 由 IDE 嵌入模式传入,不经过保留名称检查和企业管控排他限制。
|
||||
|
||||
### claude.ai 代理类型(内部)
|
||||
|
||||
`type: "claudeai-proxy"` 由 claude.ai 网页端配置的连接器使用,通过 OAuth bearer token 认证并支持 401 重试。
|
||||
|
||||
## 配置操作
|
||||
|
||||
### 添加 MCP 服务器
|
||||
|
||||
通过 CLI 命令 `claude mcp add` 或 API 调用 `addMcpConfig()`:
|
||||
|
||||
```bash
|
||||
# 添加到用户配置
|
||||
claude mcp add my-server -s user -- npx @my-org/mcp-server
|
||||
|
||||
# 添加到项目配置
|
||||
claude mcp add my-server -s project -- npx @my-org/mcp-server
|
||||
|
||||
# 添加 HTTP 类型
|
||||
claude mcp add my-remote -s user -t http -u https://mcp.example.com/mcp
|
||||
```
|
||||
|
||||
添加时的验证流程:
|
||||
|
||||
1. **名称校验**:只允许字母、数字、连字符和下划线
|
||||
2. **保留名检查**:`claude-in-chrome` 和 `computer-use` 被保留
|
||||
3. **企业管控检查**:企业模式下拒绝添加
|
||||
4. **Schema 验证**:Zod 校验配置格式
|
||||
5. **策略检查**:denylist 拒绝、allowlist 验证
|
||||
|
||||
### 移除 MCP 服务器
|
||||
|
||||
```bash
|
||||
claude mcp remove my-server -s user
|
||||
```
|
||||
|
||||
### 列出 MCP 服务器
|
||||
|
||||
```bash
|
||||
claude mcp list
|
||||
```
|
||||
|
||||
## 项目配置审批
|
||||
|
||||
`.mcp.json` 中的项目配置需要用户显式审批才能生效:
|
||||
|
||||
```typescript
|
||||
// config.ts:1166
|
||||
const approvedProjectServers: Record<string, ScopedMcpServerConfig> = {}
|
||||
for (const [name, config] of Object.entries(projectServers)) {
|
||||
if (getProjectMcpServerStatus(name) === 'approved') {
|
||||
approvedProjectServers[name] = config
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
首次打开项目时,Claude Code 会提示用户审批 `.mcp.json` 中的每个服务器。审批状态持久化在本地配置中。
|
||||
|
||||
## 插件 MCP 集成
|
||||
|
||||
插件通过 manifest 中的 `.mcp.json` 或 `.mcpb` 文件声明 MCP 服务器:
|
||||
|
||||
```typescript
|
||||
// 插件 MCP 加载流程
|
||||
const pluginResult = await loadAllPluginsCacheOnly()
|
||||
const pluginServerResults = await Promise.all(
|
||||
pluginResult.enabled.map(plugin => getPluginMcpServers(plugin, mcpErrors))
|
||||
)
|
||||
```
|
||||
|
||||
### 插件命名空间
|
||||
|
||||
插件 MCP 服务器名格式为 `plugin:<pluginName>:<serverName>`,不会与手动配置的名称冲突。
|
||||
|
||||
### 去重机制
|
||||
|
||||
插件服务器通过内容签名去重(`dedupPluginMcpServers`):
|
||||
|
||||
- **stdio 类型**:签名 = `stdio:` + JSON.stringify([command, ...args])
|
||||
- **URL 类型**:签名 = `url:` + 原始 URL(unwrap CCR proxy URL)
|
||||
- **sdk 类型**:签名为 null,不去重
|
||||
|
||||
去重规则:
|
||||
1. 手动配置优先于插件配置
|
||||
2. 先加载的插件优先于后加载的
|
||||
3. 被抑制的插件服务器在 `/plugin` UI 中显示提示
|
||||
|
||||
### claude.ai 连接器去重
|
||||
|
||||
claude.ai 连接器使用相同的内容签名机制去重(`dedupClaudeAiMcpServers`):
|
||||
- 仅启用的手动配置参与去重(禁用的手动配置不应抑制连接器)
|
||||
- 连接器名格式为 `claude.ai <DisplayName>`
|
||||
|
||||
## 策略管控
|
||||
|
||||
### Allowlist / Denylist
|
||||
|
||||
企业策略通过 allowlist 和 denylist 控制可用的 MCP 服务器:
|
||||
|
||||
```typescript
|
||||
// config.ts:1243 - 最终策略过滤
|
||||
for (const [name, serverConfig] of Object.entries(configs)) {
|
||||
if (!isMcpServerAllowedByPolicy(name, serverConfig)) {
|
||||
continue // 跳过策略禁止的服务器
|
||||
}
|
||||
filtered[name] = serverConfig
|
||||
}
|
||||
```
|
||||
|
||||
策略检查考虑:
|
||||
- 服务器名称匹配
|
||||
- stdio 类型的 command + args 匹配
|
||||
- URL 类型的 URL 模式匹配(支持通配符)
|
||||
|
||||
### 插件专用模式
|
||||
|
||||
`isRestrictedToPluginOnly('mcp')` 启用时,只允许插件提供的 MCP 服务器——用户/项目级配置被忽略。
|
||||
|
||||
## 环境变量展开
|
||||
|
||||
MCP 配置中的环境变量支持 `$VAR` 和 `${VAR}` 语法展开:
|
||||
|
||||
```json
|
||||
{
|
||||
"my-server": {
|
||||
"command": "npx",
|
||||
"args": ["@my-org/mcp-server"],
|
||||
"env": {
|
||||
"API_KEY": "$MY_API_KEY",
|
||||
"DB_URL": "${DATABASE_URL}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
展开时缺失的变量会生成警告信息,但不阻止配置加载。
|
||||
|
||||
## 内置 MCP 动态注册
|
||||
|
||||
内置 MCP 服务器在 `main.tsx` 启动流程中动态注入配置:
|
||||
|
||||
### Computer Use MCP
|
||||
|
||||
```typescript
|
||||
// src/utils/computerUse/setup.ts
|
||||
export function setupComputerUseMCP(): {
|
||||
mcpConfig: Record<string, ScopedMcpServerConfig>
|
||||
allowedTools: string[]
|
||||
} {
|
||||
return {
|
||||
mcpConfig: {
|
||||
"computer-use": {
|
||||
type: "stdio",
|
||||
command: process.execPath,
|
||||
args: ["--computer-use-mcp"],
|
||||
scope: "dynamic",
|
||||
}
|
||||
},
|
||||
allowedTools: ["mcp__computer-use__screenshot", ...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
启用条件:
|
||||
- Feature flag `CHICAGO_MCP` 开启
|
||||
- `getPlatform() !== "unknown"`(macOS/Windows/Linux)
|
||||
- 非非交互式会话
|
||||
- GrowthBook gate `getChicagoEnabled()` 返回 true
|
||||
|
||||
### Claude in Chrome MCP
|
||||
|
||||
```typescript
|
||||
// 类似 Computer Use,在 main.tsx 中注册
|
||||
const { mcpConfig, allowedTools, systemPrompt } = setupClaudeInChrome()
|
||||
dynamicMcpConfig = { ...dynamicMcpConfig, ...mcpConfig }
|
||||
```
|
||||
|
||||
启用条件:
|
||||
- `--chrome` 参数或 `claudeInChromeDefaultEnabled` 配置
|
||||
- Chrome 扩展已安装
|
||||
|
||||
### VSCode SDK MCP
|
||||
|
||||
IDE 嵌入模式通过初始化消息传入 `type:'sdk'` 的配置,由 `setupVscodeSdkMcp()` 设置双向通知。
|
||||
|
||||
## 保留名称
|
||||
|
||||
以下 MCP 服务器名称被保留,用户无法手动配置同名服务器:
|
||||
|
||||
| 名称 | 用途 | 检查条件 |
|
||||
|------|------|---------|
|
||||
| `claude-in-chrome` | Chrome 浏览器控制 | 始终检查 |
|
||||
| `computer-use` | 桌面自动化 | `CHICAGO_MCP` feature flag 开启时检查 |
|
||||
| `claude-vscode` | VSCode IDE 集成 | 由 SDK 传入,不经过名称检查 |
|
||||
|
||||
保留名检查在两个位置:
|
||||
1. `addMcpConfig()`(`config.ts:636-648`)— 运行时拒绝
|
||||
2. `main.tsx` 启动检查(`main.tsx:2351-2368`)— 启动时退出
|
||||
|
||||
## 关键源文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/services/mcp/config.ts` | 配置管理核心:合并、去重、策略、添加/删除 |
|
||||
| `src/services/mcp/types.ts` | Zod Schema 定义、类型声明 |
|
||||
| `src/services/mcp/client.ts` | 连接管理、传输层选择 |
|
||||
| `src/utils/plugins/mcpPluginIntegration.ts` | 插件 MCP 配置加载 |
|
||||
| `src/utils/computerUse/setup.ts` | Computer Use 动态注册 |
|
||||
| `src/utils/claudeInChrome/common.ts` | Chrome MCP 保留名与工具名 |
|
||||
| `src/services/mcp/vscodeSdkMcp.ts` | VSCode SDK 双向通知 |
|
||||
@@ -0,0 +1,407 @@
|
||||
---
|
||||
title: "MCP 协议 - 连接管理、工具发现与执行链路"
|
||||
description: "从源码角度解析 Claude Code 的 MCP 集成:内置 MCP 与外部 MCP 的区别、7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。"
|
||||
keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现", "内置 MCP", "外部 MCP"]
|
||||
---
|
||||
|
||||
{/* 本章目标:从源码角度揭示 MCP 客户端的两种运行模式(内置/外部)、连接管理、工具发现协议和执行链路 */}
|
||||
|
||||
## 架构总览:从配置到可用工具
|
||||
|
||||
```
|
||||
配置层(多来源合并)
|
||||
├── settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } } ← 外部
|
||||
├── .mcp.json: 项目级 MCP 配置 ← 外部
|
||||
├── 插件 manifest (.mcp.json / .mcpb) ← 外部(插件)
|
||||
├── claude.ai connectors ← 外部(远程)
|
||||
├── enterprise managed-mcp.json ← 外部(企业管控)
|
||||
├── setupComputerUseMCP() / setupClaudeInChrome() ← 内置(动态注册)
|
||||
└── SDK 传入 (type:'sdk') ← 内置(IDE 嵌入)
|
||||
↓
|
||||
getAllMcpConfigs() ← enterprise 独占 或 合并 user/project/local + plugin + claude.ai
|
||||
↓
|
||||
useManageMCPConnections() ← React Hook 管理连接生命周期
|
||||
↓
|
||||
connectToServer(name, config) ← memoize 缓存(lodash memoize)
|
||||
├── 判断:内置 MCP → InProcessTransport(同进程)
|
||||
├── 判断:外部 stdio → StdioClientTransport(子进程)
|
||||
├── 判断:远程 SSE/HTTP/WS → 网络传输
|
||||
└── 返回 MCPServerConnection ← { connected | failed | needs-auth | pending | disabled }
|
||||
↓
|
||||
fetchToolsForClient(client) ← LRU(20) 缓存
|
||||
├── client.request({ method: 'tools/list' })
|
||||
└── 每个工具包装为 MCPTool ← 统一 Tool 接口
|
||||
↓
|
||||
assembleToolPool() ← 合并内置工具 + MCP 工具
|
||||
↓
|
||||
工具名格式: mcp__<serverName>__<toolName> ← buildMcpToolName()
|
||||
```
|
||||
|
||||
## 两种 MCP 模式:内置 vs 外部
|
||||
|
||||
Claude Code 的 MCP 实现区分 **内置 MCP 服务器** 和 **外部 MCP 服务器**。两者使用相同的客户端协议和工具发现机制,但在连接方式、生命周期管理和配置来源上完全不同。
|
||||
|
||||
### 内置 MCP 服务器
|
||||
|
||||
内置 MCP 服务器由 Claude Code 自身提供,无需用户手动配置。它们在启动时自动注册为 `dynamic` scope 的配置,并在同进程内运行。
|
||||
|
||||
| 服务器 | 名称 | 包路径 | Feature Flag | 启用方式 |
|
||||
|--------|------|--------|-------------|---------|
|
||||
| Computer Use | `computer-use` | `@ant/computer-use-mcp` | `CHICAGO_MCP` | GrowthBook gate + macOS + interactive |
|
||||
| Claude in Chrome | `claude-in-chrome` | `@ant/claude-for-chrome-mcp` | — | `--chrome` 参数或 `claudeInChromeDefaultEnabled` 配置 |
|
||||
| VSCode SDK | `claude-vscode` | — | — | IDE 嵌入模式 (type:`sdk`) |
|
||||
|
||||
#### InProcessTransport:零开销同进程通信
|
||||
|
||||
内置服务器通过 `InProcessTransport`(`src/services/mcp/InProcessTransport.ts`)运行,**不启动子进程**:
|
||||
|
||||
```typescript
|
||||
// 创建一对 linked transport —— 消息在两端之间直接传递
|
||||
const [clientTransport, serverTransport] = createLinkedTransportPair()
|
||||
|
||||
// server 端连接到 serverTransport
|
||||
inProcessServer = createComputerUseMcpServerForCli()
|
||||
await inProcessServer.connect(serverTransport)
|
||||
|
||||
// client 端使用 clientTransport(与外部 MCP 的 Client 相同接口)
|
||||
transport = clientTransport
|
||||
```
|
||||
|
||||
`InProcessTransport` 的核心设计:
|
||||
- `send()` 通过 `queueMicrotask()` 异步投递消息到对端,避免同步请求/响应的栈深度问题
|
||||
- `close()` 双向关闭,任一端关闭都会触发两端的 `onclose` 回调
|
||||
- 无网络开销、无 IPC 序列化、无进程启动时间
|
||||
|
||||
#### 动态注册流程
|
||||
|
||||
内置服务器在 `main.tsx` 的启动流程中注册,注入 `dynamicMcpConfig`:
|
||||
|
||||
```typescript
|
||||
// main.tsx: Computer Use MCP 动态注册
|
||||
if (feature("CHICAGO_MCP") && getPlatform() !== "unknown" && !getIsNonInteractiveSession()) {
|
||||
const { getChicagoEnabled } = await import("src/utils/computerUse/gates.js")
|
||||
if (getChicagoEnabled()) {
|
||||
const { setupComputerUseMCP } = await import("src/utils/computerUse/setup.js")
|
||||
const { mcpConfig, allowedTools } = setupComputerUseMCP()
|
||||
dynamicMcpConfig = { ...dynamicMcpConfig, ...mcpConfig }
|
||||
allowedTools.push(...cuTools)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`setupComputerUseMCP()` 返回的配置(`src/utils/computerUse/setup.ts`):
|
||||
|
||||
```typescript
|
||||
{
|
||||
"computer-use": {
|
||||
type: "stdio", // 类型标记为 stdio(但 client.ts 会拦截为 InProcessTransport)
|
||||
command: process.execPath,
|
||||
args: ["--computer-use-mcp"],
|
||||
scope: "dynamic", // 动态作用域,不持久化
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 连接时拦截
|
||||
|
||||
`connectToServer()` 在 `client.ts:906-944` 中根据服务器名拦截内置服务器:
|
||||
|
||||
```typescript
|
||||
// Chrome MCP — 在 process 内运行,避免 ~325MB 子进程
|
||||
if (isClaudeInChromeMCPServer(name)) {
|
||||
const { createChromeContext } = await import('../../utils/claudeInChrome/mcpServer.js')
|
||||
const { createClaudeForChromeMcpServer } = await import('@ant/claude-for-chrome-mcp')
|
||||
const { createLinkedTransportPair } = await import('./InProcessTransport.js')
|
||||
const context = createChromeContext(config.env)
|
||||
inProcessServer = createClaudeForChromeMcpServer(context)
|
||||
const [clientTransport, serverTransport] = createLinkedTransportPair()
|
||||
await inProcessServer.connect(serverTransport)
|
||||
transport = clientTransport
|
||||
}
|
||||
|
||||
// Computer Use MCP — 同理
|
||||
if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
|
||||
const { createComputerUseMcpServerForCli } = await import('../../utils/computerUse/mcpServer.js')
|
||||
const { createLinkedTransportPair } = await import('./InProcessTransport.js')
|
||||
inProcessServer = await createComputerUseMcpServerForCli()
|
||||
const [clientTransport, serverTransport] = createLinkedTransportPair()
|
||||
await inProcessServer.connect(serverTransport)
|
||||
transport = clientTransport
|
||||
}
|
||||
```
|
||||
|
||||
#### 保留名称保护
|
||||
|
||||
内置服务器的名称被保留,用户无法手动添加同名配置(`config.ts:636-648`):
|
||||
|
||||
```typescript
|
||||
// 添加 MCP 配置时检查保留名
|
||||
if (isClaudeInChromeMCPServer(name)) {
|
||||
throw new Error(`Cannot add MCP server "${name}": this name is reserved.`)
|
||||
}
|
||||
if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
|
||||
throw new Error(`Cannot add MCP server "${name}": this name is reserved.`)
|
||||
}
|
||||
```
|
||||
|
||||
启动时也有全局检查(`main.tsx:2351-2368`):如果用户配置中包含保留名(非 `type:'sdk'`),直接 `process.exit(1)`。
|
||||
|
||||
#### VSCode SDK MCP
|
||||
|
||||
VSCode SDK MCP 是特殊的内置模式。IDE(如 VS Code、JetBrains)通过嵌入方式启动 Claude Code,并传入 `type:'sdk'` 的 MCP 配置。这类配置:
|
||||
- 不经过保留名称检查(IDE 可以使用任意名称)
|
||||
- 不参与 enterprise MCP 的排他控制
|
||||
- 通过 VSCode SDK transport 连接
|
||||
- 支持双向通知(如 `file_updated`、`experiment_gates`)
|
||||
|
||||
```typescript
|
||||
// src/services/mcp/vscodeSdkMcp.ts
|
||||
export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
|
||||
const client = sdkClients.find(client => client.name === 'claude-vscode')
|
||||
if (client && client.type === 'connected') {
|
||||
// 注册 log_event 通知处理器
|
||||
client.client.setNotificationHandler(LogEventNotificationSchema(), ...)
|
||||
// 发送实验门控到 VSCode
|
||||
client.client.notification({ method: 'experiment_gates', params: { gates } })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 外部 MCP 服务器
|
||||
|
||||
外部 MCP 服务器由用户在配置文件中声明,通过子进程或网络连接运行。
|
||||
|
||||
#### 配置来源
|
||||
|
||||
| 来源 | Scope | 文件位置 | 优先级 |
|
||||
|------|-------|---------|--------|
|
||||
| 项目配置 | `project` | `<project>/.mcp.json` | 最高(同名覆盖) |
|
||||
| 本地配置 | `local` | `<project>/.claude/settings.local.json` | 高 |
|
||||
| 用户配置 | `user` | `~/.claude/settings.json` | 中 |
|
||||
| 插件 | `dynamic` | 插件 manifest 中 `.mcp.json` | 中 |
|
||||
| claude.ai | `claudeai` | 通过 API 获取 | 低 |
|
||||
| 企业管控 | `enterprise` | 系统管理路径 `managed-mcp.json` | 排他(存在时覆盖全部) |
|
||||
|
||||
#### 配置示例
|
||||
|
||||
```json
|
||||
// settings.json / .mcp.json 中的 MCP 配置
|
||||
{
|
||||
"mcpServers": {
|
||||
// stdio 类型 — 启动子进程
|
||||
"my-database": {
|
||||
"command": "npx",
|
||||
"args": ["@my-org/db-mcp-server"],
|
||||
"env": { "DB_URL": "postgres://..." }
|
||||
},
|
||||
|
||||
// HTTP 流类型 — 远程服务器
|
||||
"remote-api": {
|
||||
"type": "http",
|
||||
"url": "https://api.example.com/mcp"
|
||||
},
|
||||
|
||||
// SSE 类型 — Server-Sent Events
|
||||
"realtime-feed": {
|
||||
"type": "sse",
|
||||
"url": "https://feed.example.com/sse"
|
||||
},
|
||||
|
||||
// WebSocket 类型
|
||||
"ws-service": {
|
||||
"type": "ws",
|
||||
"url": "wss://ws.example.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 配置合并与去重
|
||||
|
||||
`getAllMcpConfigs()`(`config.ts`)按优先级合并多个来源的配置:
|
||||
|
||||
1. 企业管控配置存在时,**独占返回**(忽略所有其他来源)
|
||||
2. 否则合并:user → project → local → plugin → claude.ai
|
||||
3. 插件与手动配置去重:通过 `getMcpServerSignature()` 生成内容签名(基于 command/args/url),插件配置被同名手动配置抑制
|
||||
4. `addScopeToServers()` 为每个配置项标注来源 scope
|
||||
|
||||
## 7 种传输层实现
|
||||
|
||||
`connectToServer()`(`client.ts:596-1643`)根据 `config.type` 分发到不同的 Transport 实现:
|
||||
|
||||
| 传输类型 | Transport 类 | 适用场景 | 认证方式 |
|
||||
|----------|-------------|---------|---------|
|
||||
| `stdio`(默认) | `StdioClientTransport` | 外部本地子进程 | 无 |
|
||||
| `sse` | `SSEClientTransport` | 远程 SSE 服务 | `ClaudeAuthProvider` + OAuth |
|
||||
| `http` | `StreamableHTTPClientTransport` | HTTP 流 | `ClaudeAuthProvider` + OAuth |
|
||||
| `sse-ide` | `SSEClientTransport` | IDE 集成 | lockfile token |
|
||||
| `ws-ide` | `WebSocketTransport` | IDE WebSocket | `X-Claude-Code-Ide-Authorization` |
|
||||
| `ws` | `WebSocketTransport` | WebSocket 服务 | session ingress token |
|
||||
| `claudeai-proxy` | `StreamableHTTPClientTransport` | claude.ai 代理 | OAuth bearer + 401 重试 |
|
||||
| InProcess(内置) | `InProcessTransport` | Computer Use / Chrome | 无(同进程) |
|
||||
|
||||
### stdio 传输的进程管理
|
||||
|
||||
stdio 类型的 MCP 服务器作为子进程运行,cleanup 时采用 **信号升级策略**(`client.ts:1431-1564`):
|
||||
|
||||
```
|
||||
SIGINT (100ms) → SIGTERM (400ms) → SIGKILL
|
||||
```
|
||||
|
||||
总清理时间上限 600ms,防止 MCP 服务器关闭阻塞 CLI 退出。
|
||||
|
||||
### 远程传输的认证状态机
|
||||
|
||||
SSE/HTTP 类型使用 `ClaudeAuthProvider` 实现 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,并写入 15 分钟 TTL 的缓存文件(`mcp-needs-auth-cache.json`),避免重复弹出认证提示。
|
||||
|
||||
```
|
||||
连接尝试 → 401 Unauthorized
|
||||
↓
|
||||
handleRemoteAuthFailure()
|
||||
├── logEvent('tengu_mcp_server_needs_auth')
|
||||
├── setMcpAuthCacheEntry(name) ← 写入 15min TTL 缓存
|
||||
└── return { type: 'needs-auth' } ← UI 显示认证提示
|
||||
```
|
||||
|
||||
## 连接缓存与重连机制
|
||||
|
||||
`connectToServer` 使用 lodash `memoize` 缓存连接对象,缓存 key 为 `${name}-${JSON.stringify(config)}`。
|
||||
|
||||
### 缓存失效触发
|
||||
|
||||
当连接关闭时(`client.onclose`),清除所有相关缓存(`client.ts:1376-1404`):
|
||||
|
||||
```typescript
|
||||
client.onclose = () => {
|
||||
const key = getServerCacheKey(name, serverRef)
|
||||
fetchToolsForClient.cache.delete(name) // 工具缓存
|
||||
fetchResourcesForClient.cache.delete(name) // 资源缓存
|
||||
fetchCommandsForClient.cache.delete(name) // 命令缓存
|
||||
connectToServer.cache.delete(key) // 连接缓存
|
||||
}
|
||||
```
|
||||
|
||||
### 连接降级检测
|
||||
|
||||
远程传输有 **连续错误计数器**(`client.ts:1229`):
|
||||
|
||||
```typescript
|
||||
let consecutiveConnectionErrors = 0
|
||||
const MAX_ERRORS_BEFORE_RECONNECT = 3
|
||||
```
|
||||
|
||||
遇到终端错误(ECONNRESET、ETIMEDOUT、EPIPE 等)连续 3 次后,主动关闭 transport 触发重连。对于 HTTP 传输,还检测 session 过期(404 + JSON-RPC code -32001)。
|
||||
|
||||
### 请求级超时保护
|
||||
|
||||
每个 HTTP 请求使用独立的 `setTimeout` 超时(`wrapFetchWithTimeout`,`client.ts:493`),而非共享 `AbortSignal.timeout()`。原因是 Bun 对 AbortSignal.timeout 的 GC 是惰性的——每个请求约 2.4KB 原生内存,即使请求毫秒级完成也要等 60s 才回收。
|
||||
|
||||
```typescript
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(c => c.abort(...), MCP_REQUEST_TIMEOUT_MS, controller)
|
||||
timer.unref?.() // 不阻止进程退出
|
||||
```
|
||||
|
||||
## 工具发现:从 MCP 到 Tool 接口
|
||||
|
||||
`fetchToolsForClient()`(`client.ts:1744-2000`)使用 `memoizeWithLRU` 缓存(上限 100),将 MCP 工具转换为 Claude Code 的统一 Tool 接口:
|
||||
|
||||
```typescript
|
||||
const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
|
||||
// 结果: "mcp__my-database__query"
|
||||
```
|
||||
|
||||
### 内置 MCP 的工具发现
|
||||
|
||||
内置 MCP 服务器虽然使用 InProcessTransport,但工具发现流程与外部服务器完全一致:
|
||||
|
||||
- **Computer Use**:`createComputerUseMcpServerForCli()` 在 `src/utils/computerUse/mcpServer.ts` 中构建 MCP Server 对象,注册 `ListToolsRequestSchema` handler。工具描述包含平台特定的已安装应用列表(1s 超时枚举)。
|
||||
- **Claude in Chrome**:`createClaudeForChromeMcpServer()` 在 `@ant/claude-for-chrome-mcp` 包中构建 Server,提供 17+ 个浏览器控制工具。
|
||||
- **VSCode SDK**:由 IDE 端提供工具列表,通过 SDK transport 传递。
|
||||
|
||||
### 工具描述截断
|
||||
|
||||
MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`)。OpenAPI 生成的 MCP 服务器曾观察到 15-60KB 的描述文档。
|
||||
|
||||
### 工具能力标注
|
||||
|
||||
每个 MCP 工具根据 `tool.annotations` 自动标注:
|
||||
|
||||
| 注解 | 映射到 | 含义 |
|
||||
|------|--------|------|
|
||||
| `readOnlyHint` | `isReadOnly()` + `isConcurrencySafe()` | 只读,可并行 |
|
||||
| `destructiveHint` | `isDestructive()` | 破坏性操作 |
|
||||
| `openWorldHint` | `isOpenWorld()` | 开放世界(不可枚举) |
|
||||
| `title` | `userFacingName()` | 显示名称 |
|
||||
|
||||
### MCP 工具的权限检查
|
||||
|
||||
MCP 工具默认返回 `{ behavior: 'passthrough' }`(`client.ts:1816-1834`),意味着它们始终进入权限确认流程。工具名使用 `mcp__` 前缀精确匹配权限规则。
|
||||
|
||||
内置 MCP 服务器的工具通过 `allowedTools` 列表自动授权——在 `main.tsx` 启动时加入,绕过普通权限提示。例如 Computer Use 工具的 `request_access` 自行处理会话级审批。
|
||||
|
||||
## MCP 工具的执行链路
|
||||
|
||||
```
|
||||
AI 生成 tool_use: { name: "mcp__my-db__query", input: { sql: "..." } }
|
||||
↓
|
||||
MCPTool.call() ← client.ts:1835
|
||||
├── ensureConnectedClient() ← 确保连接有效(重连)
|
||||
├── callMCPToolWithUrlElicitationRetry() ← 带 Elicitation 重试
|
||||
│ ├── client.request({ method: 'tools/call' })
|
||||
│ ├── 处理图片结果(resize + persist)
|
||||
│ └── 内容截断(mcpContentNeedsTruncation)
|
||||
├── McpSessionExpiredError → 重试一次
|
||||
└── 返回 { data: content, mcpMeta }
|
||||
```
|
||||
|
||||
### Session 过期自动重试
|
||||
|
||||
HTTP 传输的 MCP session 可能过期。检测到 `McpSessionExpiredError` 后自动重试一次(`client.ts:1862`),因为 `ensureConnectedClient()` 已经清除了缓存并建立了新连接。
|
||||
|
||||
### 内容截断与持久化
|
||||
|
||||
大型 MCP 工具输出通过 `truncateMcpContentIfNeeded` 截断,二进制内容(图片)通过 `persistBinaryContent` 写入文件并返回文件路径。图片自动 resize(`maybeResizeAndDownsampleImageBuffer`)。
|
||||
|
||||
## MCP 连接的并发控制
|
||||
|
||||
```typescript
|
||||
// 本地服务器并发连接数
|
||||
getMcpServerConnectionBatchSize() // 默认 3
|
||||
|
||||
// 远程服务器并发连接数
|
||||
getRemoteMcpServerConnectionBatchSize() // 默认 20
|
||||
```
|
||||
|
||||
本地 MCP 服务器(stdio)是重量级的子进程,默认限制 3 个并发连接。远程服务器是轻量级 HTTP 请求,允许 20 个并发。
|
||||
|
||||
## 内置 vs 外部 MCP 对比总结
|
||||
|
||||
| 维度 | 内置 MCP | 外部 MCP |
|
||||
|------|---------|---------|
|
||||
| **Transport** | `InProcessTransport`(同进程) | stdio / SSE / HTTP / WebSocket |
|
||||
| **配置来源** | `setupComputerUseMCP()` / `setupClaudeInChrome()` 等动态注册 | settings.json / .mcp.json / 插件 / claude.ai |
|
||||
| **Scope** | `dynamic` | `user` / `project` / `local` / `enterprise` / `claudeai` |
|
||||
| **进程模型** | 同进程,零开销 | 子进程(stdio)或网络连接 |
|
||||
| **名称保护** | 保留名,用户不可添加同名 | 自由命名(字母数字 + `-_`) |
|
||||
| **生命周期** | 随 CLI 启停 | 连接缓存 + 按需重连 |
|
||||
| **权限** | `allowedTools` 自动授权 | `passthrough` 进入权限确认 |
|
||||
| **Feature Flag** | `CHICAGO_MCP`(Computer Use)等 | 无(始终可用) |
|
||||
| **工具发现** | 与外部相同(MCP 协议) | 标准 MCP `tools/list` |
|
||||
| **清理** | `inProcessServer.close()` | 信号升级策略 SIGINT→SIGTERM→SIGKILL |
|
||||
|
||||
## 关键源文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/services/mcp/client.ts` | 核心客户端:connectToServer、fetchToolsForClient、MCPTool.call |
|
||||
| `src/services/mcp/config.ts` | 配置管理:getAllMcpConfigs、addMcpConfig、removeMcpConfig |
|
||||
| `src/services/mcp/types.ts` | 类型定义:配置 Schema、连接状态类型 |
|
||||
| `src/services/mcp/InProcessTransport.ts` | 内置 MCP 传输层:linked transport pair |
|
||||
| `src/services/mcp/vscodeSdkMcp.ts` | VSCode SDK MCP:双向通知、实验门控 |
|
||||
| `src/services/mcp/useManageMCPConnections.ts` | React Hook:连接生命周期、重连 |
|
||||
| `src/utils/computerUse/mcpServer.ts` | Computer Use MCP Server 构建 |
|
||||
| `src/utils/computerUse/setup.ts` | Computer Use 动态注册 |
|
||||
| `src/utils/claudeInChrome/mcpServer.ts` | Chrome MCP Server 构建 + Bridge 配置 |
|
||||
| `src/tools/MCPTool/MCPTool.ts` | MCP 工具包装:统一 Tool 接口 |
|
||||
| `src/entrypoints/mcp.ts` | MCP server 入口(Claude Code 作为 MCP server) |
|
||||
@@ -0,0 +1,221 @@
|
||||
---
|
||||
title: "Skills 技能系统 - Prompt 即能力的架构哲学"
|
||||
description: "深入剖析 Claude Code Skills 系统的完整实现:从磁盘加载、Frontmatter 解析、预算感知描述截断、双模式执行(inline/fork)、权限白名单、条件激活、动态发现到远程技能加载,揭示一条完整的 Skill 生命周期链路。"
|
||||
keywords: ["Skills", "SkillTool", "技能加载", "Frontmatter", "whenToUse", "allowedTools", "fork执行", "动态发现"]
|
||||
---
|
||||
|
||||
{/* 本章目标:揭示 Skill 系统从文件到执行的全链路实现 */}
|
||||
|
||||
## Tool vs Skill:本质差异
|
||||
|
||||
| | Tool | Skill |
|
||||
|---|---|---|
|
||||
| 粒度 | 单个原子操作(读文件、执行命令) | 一套完整的工作流(代码审查、创建 PR) |
|
||||
| 触发方式 | AI 自主选择 | 用户 `/skill-name` 或 AI 通过 `SkillTool` 自动匹配 |
|
||||
| 本质 | TypeScript 执行逻辑 | **Prompt + 权限配置**的声明式封装 |
|
||||
| 注册位置 | `src/tools.ts` → `getTools()` | `src/commands.ts` → `getCommands()` |
|
||||
| 执行器 | 各 Tool 的 `call()` 方法 | `SkillTool.call()` → 两条分支(inline / fork) |
|
||||
|
||||
Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Prompt 质量**。一个代码审查 Skill 不需要审查引擎,只需告诉 AI "审查什么、按什么顺序、输出什么格式"——Skill 把这种"经验"封装为可复用的 Markdown。
|
||||
|
||||
## Skill 的五个来源与加载链路
|
||||
|
||||
### 1. 内置命令(Built-in Commands)
|
||||
|
||||
硬编码在 `src/commands.ts:299` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown,但实现了相同的 `Command` 接口(`src/types/command.ts`)。
|
||||
|
||||
### 2. Bundled Skills(编译时打包)
|
||||
|
||||
通过 `registerBundledSkill()`(`src/skills/bundledSkills.ts:53`)在模块初始化时注册。关键特性:
|
||||
|
||||
- **延迟文件提取**:如果 Skill 声明了 `files`(参考文件),首次调用时才解压到临时目录(`getBundledSkillExtractDir()`),使用 `O_NOFOLLOW | O_EXCL` 防止符号链接攻击(`safeWriteFile`,第 186 行)
|
||||
- **闭包级 memoize**:并发调用共享同一个 extraction promise,避免竞态写入
|
||||
- 来源标记为 `source: 'bundled'`,在 Prompt 预算中享有**不可截断**的特权
|
||||
|
||||
### 3. 磁盘 Skills(`.claude/skills/`)
|
||||
|
||||
由 `loadSkillsFromSkillsDir()`(`src/skills/loadSkillsDir.ts:407`)加载,这是最重要的加载路径:
|
||||
|
||||
```
|
||||
管理策略: $MANAGED_DIR/.claude/skills/ (policySettings)
|
||||
用户全局: ~/.claude/skills/ (userSettings)
|
||||
项目级: .claude/skills/ (projectSettings, 向上遍历至 home)
|
||||
附加目录: --add-dir 指定的路径下 .claude/skills/
|
||||
```
|
||||
|
||||
**加载协议**:只识别 `skill-name/SKILL.md` 目录格式,不再支持单文件 `.md`。加载流程:
|
||||
|
||||
1. `readdir` 扫描目录 → 仅保留 `isDirectory()` 或 `isSymbolicLink()` 的条目
|
||||
2. 在每个子目录中查找 `SKILL.md`,未找到则跳过
|
||||
3. `parseFrontmatter()` 解析 YAML 头部,提取 `whenToUse`、`allowedTools`、`context` 等字段
|
||||
4. `parseSkillFrontmatterFields()`(第 185 行)统一解析 16 个 frontmatter 字段
|
||||
5. `createSkillCommand()`(第 270 行)构造 `Command` 对象
|
||||
|
||||
**去重机制**:使用 `realpath()` 解析符号链接获得规范路径(`getFileIdentity`,第 118 行),避免通过符号链接或重叠父目录导致的重复加载。
|
||||
|
||||
### 4. MCP Skills(动态发现)
|
||||
|
||||
通过 `registerMCPSkillBuilders()` 注册构建器,MCP Server 的 prompt 被 `mcpSkillBuilders.ts` 转换为 `Command` 对象。标记为 `loadedFrom: 'mcp'`。
|
||||
|
||||
**安全边界**:MCP Skills 的 Prompt 内容**禁止执行内联 shell 命令**(`loadSkillsDir.ts:374` 的 `loadedFrom !== 'mcp'` 守卫),因为远程内容不可信。
|
||||
|
||||
### 5. Legacy Commands(`/commands/` 目录)
|
||||
|
||||
向后兼容的旧格式,由 `loadSkillsFromCommandsDir()`(第 566 行)加载。同时支持 `SKILL.md` 目录格式和单 `.md` 文件格式。
|
||||
|
||||
## Frontmatter 字段全景
|
||||
|
||||
一个 `SKILL.md` 的完整 frontmatter(`parseSkillFrontmatterFields`,第 185 行):
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: code-review # 显示名称(覆盖目录名)
|
||||
description: 系统性代码审查 # 描述(或从 Markdown 首段提取)
|
||||
when_to_use: "用户说审查代码、找 bug" # AI 自动匹配依据
|
||||
allowed-tools: # 工具白名单
|
||||
- Read
|
||||
- Grep
|
||||
- Glob
|
||||
argument-hint: "<file-or-directory>" # 参数提示
|
||||
arguments: [path] # 声明式参数名(用于 $ARGUMENTS 替换)
|
||||
model: opus # 模型覆盖
|
||||
effort: high # 努力级别
|
||||
context: fork # 执行模式:inline(默认)| fork
|
||||
agent: code-reviewer # 指定 Agent 定义文件
|
||||
user-invocable: true # 用户是否可 /调用
|
||||
disable-model-invocation: false # 禁止 AI 自主调用
|
||||
version: "1.0" # 版本号
|
||||
paths: # 条件激活的文件路径模式
|
||||
- "src/**/*.ts"
|
||||
hooks: # Hook 配置
|
||||
PreToolUse:
|
||||
- command: ["echo", "checking"]
|
||||
shell: ["bash"] # Shell 执行环境
|
||||
---
|
||||
```
|
||||
|
||||
解析后有 16 个字段被提取,其中 `allowedTools`、`model`、`effort` 在执行时动态修改 `toolPermissionContext`。
|
||||
|
||||
## 两条执行路径:Inline vs Fork
|
||||
|
||||
SkillTool(`packages/builtin-tools/src/tools/SkillTool/SkillTool.ts:332`)在 `call()` 中根据 `command.context` 分流:
|
||||
|
||||
### Inline 模式(默认)
|
||||
|
||||
Skill 的 Prompt 内容被注入为 **UserMessage**,在主对话流中继续执行:
|
||||
|
||||
1. `processPromptSlashCommand()` 处理参数替换(`$ARGUMENTS`)和 shell 命令展开(`` !`...` ``)
|
||||
2. `${CLAUDE_SKILL_DIR}` 被替换为 Skill 所在目录的绝对路径
|
||||
3. `${CLAUDE_SESSION_ID}` 被替换为当前会话 ID
|
||||
4. 返回 `newMessages`(注入到对话流)+ `contextModifier`(修改权限上下文)
|
||||
|
||||
`contextModifier`(第 776 行)做了三件事:
|
||||
- **工具白名单注入**:将 `allowedTools` 合并到 `alwaysAllowRules.command`
|
||||
- **模型切换**:`resolveSkillModelOverride()` 处理模型覆盖,保留 `[1m]` 后缀以避免 200K 窗口截断
|
||||
- **努力级别覆盖**:修改 `effortValue`
|
||||
|
||||
### Fork 模式(`context: fork`)
|
||||
|
||||
Skill 在**独立子 Agent** 中执行(`executeForkedSkill`,第 122 行):
|
||||
|
||||
1. `prepareForkedCommandContext()` 构建隔离的 Agent 定义和 Prompt
|
||||
2. `runAgent()` 启动子 Agent 循环,拥有独立的 token 预算
|
||||
3. 通过 `onProgress` 回调报告工具使用进度
|
||||
4. 结果通过 `extractResultText()` 提取,子 Agent 的全部消息在提取后被释放(`agentMessages.length = 0`)
|
||||
5. 最终通过 `clearInvokedSkillsForAgent()` 清理状态
|
||||
|
||||
Fork 模式适用于需要强隔离的场景(如长时间运行的审查任务),避免污染主对话的上下文。
|
||||
|
||||
## 权限模型:Safe Properties 白名单
|
||||
|
||||
`checkPermissions()`(第 433 行)实现了一个五层权限检查:
|
||||
|
||||
```
|
||||
1. Deny 规则匹配(支持精确匹配和 prefix:* 通配符)
|
||||
↓ 未命中
|
||||
2. 远程 canonical Skill 自动放行(EXPERIMENTAL_SKILL_SEARCH + USER_TYPE === 'ant')
|
||||
↓ 未命中
|
||||
3. Allow 规则匹配
|
||||
↓ 未命中
|
||||
4. Safe Properties 白名单检查(skillHasOnlySafeProperties,第 911 行)
|
||||
↓ 有非安全属性
|
||||
5. Ask 用户确认(附带精确匹配和前缀匹配两条建议规则)
|
||||
```
|
||||
|
||||
**Safe Properties**(`SAFE_SKILL_PROPERTIES`,第 876 行)是一个包含 30 个属性名的白名单(覆盖 `PromptCommand` 和 `CommandBase` 两个类型的所有安全属性)。任何不在白名单中的**有意义的属性值**(排除 `undefined`、`null`、空数组、空对象)都会触发权限请求。这是**正向安全**设计——未来新增的属性默认需要权限。
|
||||
|
||||
## Prompt 预算:1% 上下文窗口的截断策略
|
||||
|
||||
Skill 列表注入 System Prompt 时有严格的字符预算(`prompt.ts`):
|
||||
|
||||
- **预算计算**:`contextWindowTokens × 4 chars/token × 1%`(约 8000 字符)
|
||||
- **单条上限**:`MAX_LISTING_DESC_CHARS = 250` 字符(超出截断为 `…`)
|
||||
- **Bundled Skills 不可截断**:它们始终保留完整描述,预算不足时只截断非 bundled 的
|
||||
- **降级策略**:
|
||||
1. 尝试完整描述 → 超预算?
|
||||
2. Bundled 保留完整,非 bundled 均分剩余预算 → 每条描述低于 20 字符?
|
||||
3. 非 bundled 仅保留名称
|
||||
|
||||
`formatCommandsWithinBudget()`(`prompt.ts:70`)实现了这个三级降级。
|
||||
|
||||
## 动态发现与条件激活
|
||||
|
||||
### 基于文件路径的动态发现
|
||||
|
||||
`discoverSkillDirsForPaths()`(`loadSkillsDir.ts:861`)在文件操作时触发:
|
||||
|
||||
1. 从被操作的文件路径开始,**向上遍历**至 CWD(不包含 CWD 本身)
|
||||
2. 在每层查找 `.claude/skills/` 目录
|
||||
3. 使用 `realpath` 去重,`git check-ignore` 过滤 gitignored 目录
|
||||
4. 按路径深度排序(**深层优先**),更接近文件的 Skill 优先级更高
|
||||
|
||||
### 条件激活(paths frontmatter)
|
||||
|
||||
带有 `paths` 模式的 Skill 在加载时不会立即可用,而是存入 `conditionalSkills` Map。当被操作的文件路径匹配某个 Skill 的 paths 模式时(使用 `ignore` 库做 gitignore 风格匹配),该 Skill 才被**激活**——从 `conditionalSkills` 移入 `dynamicSkills`。
|
||||
|
||||
这意味着一个只在 `*.test.ts` 上激活的测试 Skill,平时完全不可见,只有当 AI 读取或编辑测试文件时才会出现。
|
||||
|
||||
## 使用频率排名
|
||||
|
||||
`recordSkillUsage()`(`skillUsageTracking.ts`)使用指数衰减算法计算 Skill 排名分数:
|
||||
|
||||
```
|
||||
score = usageCount × max(0.5^(daysSinceUse / 7), 0.1)
|
||||
```
|
||||
|
||||
- **7 天半衰期**:一周前的使用权重减半
|
||||
- **最低 0.1 保底**:避免老但高频使用的 Skill 完全沉底
|
||||
- **60 秒去抖**:同一 Skill 在 1 分钟内的多次调用只计一次,减少文件 I/O
|
||||
|
||||
排名数据持久化在全局配置的 `skillUsage` 字段中。
|
||||
|
||||
## 远程技能加载(Experimental)
|
||||
|
||||
通过 `EXPERIMENTAL_SKILL_SEARCH` feature flag 控制,支持从远程(AKI/GCS/S3)加载 `_canonical_<slug>` 格式的 Skill:
|
||||
|
||||
1. `validateInput()` 中 `stripCanonicalPrefix()` 拦截 canonical 名称
|
||||
2. `executeRemoteSkill()`(第 970 行)从远程 URL 加载 SKILL.md
|
||||
3. 支持 `gs://`、`https://`、`s3://` 等 URL 协议
|
||||
4. 内容经过 frontmatter 剥离、`${CLAUDE_SKILL_DIR}` 替换后直接注入
|
||||
5. 通过 `addInvokedSkill()` 注册到 compaction 保留状态,确保压缩后仍可恢复
|
||||
6. 远程 Skill 不经过 `processPromptSlashCommand`——无 `!command` 替换、无 `$ARGUMENTS` 展开
|
||||
|
||||
## 完整生命周期总结
|
||||
|
||||
```
|
||||
磁盘 SKILL.md
|
||||
↓ parseFrontmatter()
|
||||
↓ parseSkillFrontmatterFields() → 16 个字段
|
||||
↓ createSkillCommand() → Command 对象
|
||||
↓ 去重(realpath + seenFileIds)
|
||||
↓ 条件 Skill → conditionalSkills Map(等待路径匹配激活)
|
||||
↓ getSkillDirCommands() memoize 缓存
|
||||
↓ getAllCommands() 合并 local + MCP
|
||||
↓ formatCommandsWithinBudget() → 截断后的 Skill 列表注入 System Prompt
|
||||
↓ AI 选择匹配的 Skill
|
||||
↓ SkillTool.validateInput() → 名称校验 + 存在性检查
|
||||
↓ SkillTool.checkPermissions() → 五层权限检查
|
||||
↓ SkillTool.call() → inline 或 fork 执行
|
||||
↓ contextModifier() → 注入 allowedTools + model + effort
|
||||
↓ recordSkillUsage() → 更新使用频率排名
|
||||
```
|
||||
Reference in New Issue
Block a user