vault backup: 2026-06-09 23:15:17
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
---
|
||||
title: "自定义 Agent - 从 Markdown 到运行时的完整链路"
|
||||
description: "揭秘 Claude Code 自定义 Agent 完整链路:Agent 定义的 Markdown 数据模型、三种加载来源、工具过滤策略和与 AgentTool 的联动机制。"
|
||||
keywords: ["自定义 Agent", "Agent 定义", "Markdown Agent", "Agent 配置", "角色定制"]
|
||||
tags: [自定义Agent, Agent定义, Markdown, Agent配置, 角色定制]
|
||||
create time: 2026-06-09 22:30
|
||||
---
|
||||
|
||||
{/* 本章目标:揭示 Agent 定义的完整数据模型、加载发现机制、工具过滤和与 AgentTool 的联动 */}
|
||||
# 自定义 Agent - 从 Markdown 到运行时的完整链路
|
||||
|
||||
## Agent 定义的三种来源
|
||||
## 概述
|
||||
|
||||
揭秘 Claude Code 自定义 Agent 完整链路:Agent 定义的 Markdown 数据模型、三种加载来源、工具过滤策略和与 AgentTool 的联动机制。
|
||||
|
||||
## 正文
|
||||
|
||||
### Agent 定义的三种来源
|
||||
|
||||
Claude Code 的 Agent 不仅仅来自用户自定义——系统有三类来源,按优先级合并:
|
||||
|
||||
@@ -18,7 +23,7 @@ Claude Code 的 Agent 不仅仅来自用户自定义——系统有三类来源
|
||||
|
||||
合并逻辑在 `getActiveAgentsFromList()` 中:按 `agentType` 去重,后者覆盖前者。这意味着你可以在 `.claude/agents/` 中放一个 `Explore.md` 来完全替换内置的 Explore Agent。
|
||||
|
||||
## Markdown Agent 文件的完整格式
|
||||
### Markdown Agent 文件的完整格式
|
||||
|
||||
```markdown
|
||||
---
|
||||
@@ -69,7 +74,7 @@ color: "blue" # 终端中的 Agent 颜色标识
|
||||
(正文内容 = system prompt)
|
||||
```
|
||||
|
||||
### 字段解析细节
|
||||
#### 字段解析细节
|
||||
|
||||
- **`tools`**:通过 `parseAgentToolsFromFrontmatter()` 解析,支持逗号分隔字符串或数组
|
||||
- **`model: "inherit"`**:使用主线程的模型(区分大小写,只有小写 "inherit" 有效)
|
||||
@@ -77,46 +82,44 @@ color: "blue" # 终端中的 Agent 颜色标识
|
||||
- **`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
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["1. 加载 Markdown 文件"] --> B["loadMarkdownFilesForSubdir('agents', cwd)"]
|
||||
B --> C["~/.claude/agents/*.md 用户级 source='userSettings'"]
|
||||
B --> D[".claude/agents/*.md 项目级 source='projectSettings'"]
|
||||
B --> E["managed/policy sources 策略级 source='policySettings'"]
|
||||
A --> F["每个 .md 文件"]
|
||||
F --> G["解析 YAML frontmatter"]
|
||||
F --> H["正文作为 system prompt"]
|
||||
F --> I["校验必需字段 name/description"]
|
||||
F --> J["静默跳过无 frontmatter 的 .md 文件"]
|
||||
F --> K["解析失败 -> 记录到 failedFiles,不阻塞其他 Agent"]
|
||||
A --> L["2. 并行加载 Plugin Agents"]
|
||||
L --> M["loadPluginAgents() memoized"]
|
||||
A --> N["3. 初始化 Memory Snapshots"]
|
||||
N --> O["initializeAgentMemorySnapshots()"]
|
||||
A --> P["4. 合并 Built-in + Plugin + Custom"]
|
||||
P --> Q["getActiveAgentsFromList() 按 agentType 去重,后者覆盖前者"]
|
||||
Q --> R["5. 分配颜色"]
|
||||
R --> S["setAgentColor(agentType, color)"]
|
||||
```
|
||||
|
||||
## 工具过滤的实现
|
||||
### 工具过滤的实现
|
||||
|
||||
当 Agent 被派生时,`AgentTool` 根据定义中的 `tools` / `disallowedTools` 过滤可用工具列表:
|
||||
|
||||
```
|
||||
全部工具
|
||||
↓ disallowedTools 移除
|
||||
↓ tools 白名单过滤(如果指定)
|
||||
可用工具
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["全部工具"] --> B["disallowedTools 移除"]
|
||||
B --> C{"tools 指定?"}
|
||||
C -->|"是"| D["tools 白名单过滤"]
|
||||
C -->|"否"| E["全部保留"]
|
||||
D --> F["可用工具"]
|
||||
E --> F
|
||||
```
|
||||
|
||||
- **`tools` 未指定**:Agent 可以使用所有工具(默认全能)
|
||||
@@ -137,7 +140,7 @@ disallowedTools: [
|
||||
]
|
||||
```
|
||||
|
||||
## System Prompt 的注入方式
|
||||
### System Prompt 的注入方式
|
||||
|
||||
Agent 的 system prompt 通过 `getSystemPrompt()` 闭包延迟生成:
|
||||
|
||||
@@ -158,36 +161,27 @@ getSystemPrompt: () => {
|
||||
|
||||
对于 Built-in Agent,`getSystemPrompt` 接受 `toolUseContext` 参数,可以根据运行时状态(如是否使用嵌入式搜索工具)动态调整 prompt 内容。
|
||||
|
||||
## 与 AgentTool 的联动
|
||||
### 与 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)
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["AgentTool.call({ subagent_type: 'reviewer', ... })"] --> B["从 agentDefinitions.activeAgents 查找 agentType === 'reviewer'"]
|
||||
B --> C["检查 requiredMcpServers"]
|
||||
C --> D["过滤工具列表 tools/disallowedTools"]
|
||||
D --> E{"解析模型"}
|
||||
E -->|"'inherit'"| F["使用主线程模型"]
|
||||
E -->|"具体模型名"| G["直接使用"]
|
||||
E -->|"未指定"| F
|
||||
E --> H["解析权限模式 permissionMode"]
|
||||
H --> I["构建隔离环境 isolation === 'worktree'"]
|
||||
I --> J["注入 system prompt getSystemPrompt()"]
|
||||
J --> K["注入 initialPrompt 如果定义了"]
|
||||
K --> L["启动子 Agent 循环 forkSubagent/runAgent"]
|
||||
```
|
||||
|
||||
## 内置 Agent 参考
|
||||
### 内置 Agent 参考
|
||||
|
||||
| Agent | agentType | 角色 | 工具限制 | 模型 |
|
||||
|-------|-----------|------|---------|------|
|
||||
@@ -198,9 +192,10 @@ AgentTool.call({ subagent_type: "reviewer", ... })
|
||||
| **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 用户提供空白画布。
|
||||
> [!tip]
|
||||
> SDK 入口(`sdk-ts`/`sdk-py`/`sdk-cli`)不加载 Code Guide Agent。环境变量 `CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS` 可以完全禁用内置 Agent,给 SDK 用户提供空白画布。
|
||||
|
||||
## Agent Memory:持久化的 Agent 状态
|
||||
### Agent Memory:持久化的 Agent 状态
|
||||
|
||||
当 `memory` 字段启用时,Agent 获得跨会话的持久记忆:
|
||||
|
||||
@@ -209,3 +204,9 @@ SDK 入口(`sdk-ts`/`sdk-py`/`sdk-cli`)不加载 Code Guide Agent。环境
|
||||
- **`user`**:所有项目共享
|
||||
|
||||
Memory 通过 `loadAgentMemoryPrompt()` 注入到 system prompt 末尾,包含读写记忆的指令。Agent Memory Snapshot 机制在项目间同步 `user` 级记忆。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[claude-code-best/docs/features/extensibility/hooks]]
|
||||
- [[claude-code-best/docs/features/extensibility/skills]]
|
||||
- [[claude-code-best/docs/features/fork-subagent]]
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
---
|
||||
title: "Hooks 生命周期钩子 - 执行引擎与拦截协议"
|
||||
description: "从源码角度解析 Claude Code Hooks 系统:27 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。"
|
||||
keywords: ["Hooks", "生命周期钩子", "拦截器", "PreToolUse", "Hook 协议"]
|
||||
tags: [Hooks, 生命周期钩子, 拦截器, PreToolUse, Hook协议]
|
||||
create time: 2026-06-09 22:30
|
||||
---
|
||||
|
||||
{/* 本章目标:从源码角度揭示 Hook 的执行引擎、匹配机制、返回值协议和生命周期管理 */}
|
||||
# Hooks 生命周期钩子 - 执行引擎与拦截协议
|
||||
|
||||
## 27 种 Hook 事件
|
||||
## 概述
|
||||
|
||||
从源码角度解析 Claude Code Hooks 系统:27 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。
|
||||
|
||||
## 正文
|
||||
|
||||
### 27 种 Hook 事件
|
||||
|
||||
Claude Code 定义了 27 种 Hook 事件(`HOOK_EVENTS` 数组,`src/entrypoints/sdk/coreTypes.ts`),覆盖完整的 Agent 生命周期:
|
||||
|
||||
@@ -39,11 +44,11 @@ Claude Code 定义了 27 种 Hook 事件(`HOOK_EVENTS` 数组,`src/entrypoin
|
||||
| | `InstructionsLoaded` | 指令加载 | `load_reason` |
|
||||
| | `WorktreeCreate` / `WorktreeRemove` | Worktree 操作 | — |
|
||||
|
||||
## 6 种 Hook 类型
|
||||
### 6 种 Hook 类型
|
||||
|
||||
Hooks 配置支持 6 种执行方式,类型定义分布在 3 个文件中:
|
||||
|
||||
- **可持久化类型**(`command`、`prompt`、`agent`、`http`)— Zod schema 定义在 `src/schemas/hooks.ts`,通过 `z.discriminatedUnion('type', [...])` 声明
|
||||
- **可持久化类型**(`command`、`prompt`、`agent`、`http`)— Zod schema 定义在 `src/schemas/hooks.ts`
|
||||
- **callback 类型** — TypeScript 接口定义在 `src/types/hooks.ts`,用于 SDK 注册的内部 JS 函数
|
||||
- **function 类型** — 定义在 `src/utils/hooks/sessionHooks.ts`,用于运行时动态注册的函数 Hook
|
||||
|
||||
@@ -56,31 +61,31 @@ Hooks 配置支持 6 种执行方式,类型定义分布在 3 个文件中:
|
||||
| `callback` | 内部 JS 函数 | 系统内置 Hook |
|
||||
| `function` | 运行时注册的函数 Hook | Agent/Skill 内部使用 |
|
||||
|
||||
## 执行引擎:execCommandHook
|
||||
### 执行引擎:execCommandHook
|
||||
|
||||
`execCommandHook()`(`src/utils/hooks.ts`,`execCommandHook` 函数)是命令型 Hook 的执行核心:
|
||||
`execCommandHook()`(`src/utils/hooks.ts`)是命令型 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}
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["execCommandHook(hook, hookEvent, hookName, jsonInput, signal)"] --> B{"Shell 选择"}
|
||||
B -->|"bash"| C["spawn(cmd, [], { shell: gitBashPath | true })"]
|
||||
B -->|"powershell"| D["spawn(pwsh, ['-NoProfile', '-NonInteractive', '-Command', cmd])"]
|
||||
A --> E["变量替换"]
|
||||
E --> F["${CLAUDE_PLUGIN_ROOT} -> pluginRoot 路径"]
|
||||
E --> G["${CLAUDE_PLUGIN_DATA} -> plugin 数据目录"]
|
||||
E --> H["${user_config.X} -> 用户配置值"]
|
||||
A --> I["环境变量注入"]
|
||||
I --> J["CLAUDE_PROJECT_DIR"]
|
||||
I --> K["CLAUDE_ENV_FILE SessionStart/Setup/CwdChanged/FileChanged"]
|
||||
I --> L["CLAUDE_PLUGIN_OPTION_* plugin options"]
|
||||
A --> M["stdin 写入: jsonInput + '\\n'"]
|
||||
A --> N["超时: hook.timeout * 1000 ?? 600000ms 10分钟"]
|
||||
A --> O["异步检测: 检查 stdout 首行是否为 {async:true}"]
|
||||
```
|
||||
|
||||
### 异步 Hook 的检测协议
|
||||
#### 异步 Hook 的检测协议
|
||||
|
||||
Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务(`isAsyncHookJSONOutput` 检测 + `executeInBackground` 调用):
|
||||
Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务:
|
||||
|
||||
```typescript
|
||||
const firstLine = firstLineOf(stdout).trim()
|
||||
@@ -95,13 +100,13 @@ if (isAsyncHookJSONOutput(parsed)) {
|
||||
|
||||
后台 Hook 通过 `registerPendingAsyncHook()` 注册到 `AsyncHookRegistry`,完成后通过 `enqueuePendingNotification()` 通知主线程。
|
||||
|
||||
### asyncRewake:Hook 唤醒模型
|
||||
#### asyncRewake:Hook 唤醒模型
|
||||
|
||||
`asyncRewake` 模式的 Hook 绕过 `AsyncHookRegistry`。当 Hook 退出码为 2 时,通过 `enqueuePendingNotification()` 以 `task-notification` 模式注入消息,唤醒空闲的模型(通过 `useQueueProcessor`)或在忙碌时注入 `queued_command` 附件。
|
||||
`asyncRewake` 模式的 Hook 绕过 `AsyncHookRegistry`。当 Hook 退出码为 2 时,通过 `enqueuePendingNotification()` 以 `task-notification` 模式注入消息,唤醒空闲的模型或在忙碌时注入 `queued_command` 附件。
|
||||
|
||||
## Hook 输出的 JSON Schema
|
||||
### Hook 输出的 JSON Schema
|
||||
|
||||
同步 Hook 的输出遵循严格的 Zod schema(`syncHookResponseSchema`,定义在 `src/types/hooks.ts`,`hookJSONOutputSchema` 定义在 `src/schemas/hooks.ts`):
|
||||
同步 Hook 的输出遵循严格的 Zod schema:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -121,7 +126,7 @@ if (isAsyncHookJSONOutput(parsed)) {
|
||||
}
|
||||
```
|
||||
|
||||
### 各事件的 hookSpecificOutput
|
||||
#### 各事件的 hookSpecificOutput
|
||||
|
||||
| 事件 | 专有字段 | 作用 |
|
||||
|------|---------|------|
|
||||
@@ -141,34 +146,34 @@ if (isAsyncHookJSONOutput(parsed)) {
|
||||
| `FileChanged` | `watchPaths` | 文件变更后更新监控路径 |
|
||||
| `WorktreeCreate` | `worktreePath` | Worktree 创建通知 |
|
||||
|
||||
## Hook 匹配机制:getMatchingHooks
|
||||
### Hook 匹配机制:getMatchingHooks
|
||||
|
||||
`getMatchingHooks()`(`src/utils/hooks.ts`,`getMatchingHooks` 函数)负责从所有来源中查找匹配的 Hook:
|
||||
`getMatchingHooks()`(`src/utils/hooks.ts`)负责从所有来源中查找匹配的 Hook:
|
||||
|
||||
### 多来源合并
|
||||
#### 多来源合并
|
||||
|
||||
```
|
||||
getHooksConfig()
|
||||
├── getHooksConfigFromSnapshot() ← settings.json 中的 Hook(user/project/local)
|
||||
├── getRegisteredHooks() ← SDK 注册的 callback Hook
|
||||
├── getSessionHooks() ← Agent/Skill 前置注册的 session Hook
|
||||
└── getSessionFunctionHooks() ← 运行时 function Hook
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["getHooksConfig()"] --> B["getHooksConfigFromSnapshot() settings.json 中的 Hook user/project/local"]
|
||||
A --> C["getRegisteredHooks() SDK 注册的 callback Hook"]
|
||||
A --> D["getSessionHooks() Agent/Skill 前置注册的 session Hook"]
|
||||
A --> E["getSessionFunctionHooks() 运行时 function Hook"]
|
||||
```
|
||||
|
||||
### 匹配规则
|
||||
#### 匹配规则
|
||||
|
||||
`matcher` 字段支持三种模式(`matchesPattern()` 函数,`src/utils/hooks.ts`):
|
||||
`matcher` 字段支持三种模式:
|
||||
|
||||
```
|
||||
"Write" → 精确匹配
|
||||
"Write|Edit" → 管道分隔的多值匹配
|
||||
"^Bash(git.*)" → 正则匹配
|
||||
"*" 或 "" → 通配(匹配所有)
|
||||
"Write" -> 精确匹配
|
||||
"Write|Edit" -> 管道分隔的多值匹配
|
||||
"^Bash(git.*)" -> 正则匹配
|
||||
"*" 或 "" -> 通配(匹配所有)
|
||||
```
|
||||
|
||||
### if 条件过滤
|
||||
#### if 条件过滤
|
||||
|
||||
Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()`(`src/utils/hooks.ts`,`prepareIfConditionMatcher` 函数)预编译匹配器:
|
||||
Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()` 预编译匹配器:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -181,13 +186,13 @@ Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditio
|
||||
|
||||
`if` 条件使用 `permissionRuleValueFromString` 解析,支持与权限规则相同的语法(工具名 + 参数模式)。Bash 工具还会使用 tree-sitter 进行 AST 级别的命令解析。
|
||||
|
||||
### Hook 去重
|
||||
#### Hook 去重
|
||||
|
||||
同一个 Hook 命令在不同配置层级(user/project/local)可能重复。系统按四部分复合键做 Map 去重:`${pluginRoot}\0${shell}\0${command}\0${ifCondition}`(由 `hookDedupKey()` 函数构建),保留**最后合并的层级**。
|
||||
同一个 Hook 命令在不同配置层级(user/project/local)可能重复。系统按四部分复合键做 Map 去重:`${pluginRoot}\0${shell}\0${command}\0${ifCondition}`,保留**最后合并的层级**。
|
||||
|
||||
## 工作区信任检查
|
||||
### 工作区信任检查
|
||||
|
||||
**所有 Hook 都要求工作区信任**(`shouldSkipHookDueToTrust()` 函数,`src/utils/hooks.ts`)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。
|
||||
**所有 Hook 都要求工作区信任**(`shouldSkipHookDueToTrust()` 函数)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。
|
||||
|
||||
```typescript
|
||||
// 交互模式下,所有 Hook 要求信任
|
||||
@@ -195,11 +200,12 @@ const hasTrust = checkHasTrustDialogAccepted()
|
||||
return !hasTrust
|
||||
```
|
||||
|
||||
SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 true 时跳过检查)。
|
||||
> [!tip]
|
||||
> SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 true 时跳过检查)。
|
||||
|
||||
## 四种 Hook 能力的源码映射
|
||||
### 四种 Hook 能力的源码映射
|
||||
|
||||
### 1. 拦截操作(PreToolUse)
|
||||
#### 1. 拦截操作(PreToolUse)
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -212,7 +218,7 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
|
||||
|
||||
`processHookJSONOutput()` 将 `permissionDecision` 映射为 `result.permissionBehavior = 'deny'`,并设置 `blockingError`,阻止工具执行。
|
||||
|
||||
### 2. 修改行为(updatedInput / updatedMCPToolOutput)
|
||||
#### 2. 修改行为(updatedInput / updatedMCPToolOutput)
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -225,12 +231,12 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
|
||||
|
||||
`updatedInput` 替换原始工具输入;`updatedMCPToolOutput`(PostToolUse 事件)替换 MCP 工具的返回值——可用于过滤敏感数据。
|
||||
|
||||
### 3. 注入上下文(additionalContext / systemMessage)
|
||||
#### 3. 注入上下文(additionalContext / systemMessage)
|
||||
|
||||
- `additionalContext` → 通过 `createAttachmentMessage({ type: 'hook_additional_context' })` 注入为用户消息
|
||||
- `systemMessage` → 注入为系统警告,直接显示给用户
|
||||
- `additionalContext` -> 通过 `createAttachmentMessage({ type: 'hook_additional_context' })` 注入为用户消息
|
||||
- `systemMessage` -> 注入为系统警告,直接显示给用户
|
||||
|
||||
### 4. 控制流程(continue / stopReason)
|
||||
#### 4. 控制流程(continue / stopReason)
|
||||
|
||||
```json
|
||||
{ "continue": false, "stopReason": "构建失败,停止执行" }
|
||||
@@ -238,9 +244,9 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
|
||||
|
||||
`continue: false` 设置 `preventContinuation = true`,阻止 Agent 继续执行后续操作。
|
||||
|
||||
## Session Hook 的生命周期
|
||||
### 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`)清理。
|
||||
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册,绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()` 清理。
|
||||
|
||||
```typescript
|
||||
// runAgent.ts — 注册 agent 的前置 Hook
|
||||
@@ -251,3 +257,8 @@ clearSessionHooks(rootSetAppState, agentId)
|
||||
```
|
||||
|
||||
这确保 Agent A 的 Hook 不会泄漏到 Agent B 的执行中。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[claude-code-best/docs/features/extensibility/custom-agents]]
|
||||
- [[claude-code-best/docs/features/extensibility/skills]]
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
---
|
||||
title: "MCP 配置 - 多来源合并、作用域与策略管控"
|
||||
description: "详细说明 Claude Code MCP 配置的来源层次、合并优先级、传输类型、企业策略管控、插件集成和保留名称机制。"
|
||||
keywords: ["MCP", "配置", "settings.json", ".mcp.json", "企业策略", "插件"]
|
||||
tags:
|
||||
- MCP
|
||||
- 配置
|
||||
- 企业策略
|
||||
- 插件
|
||||
create time: 2026-06-09 22:30
|
||||
---
|
||||
|
||||
## 配置来源与作用域
|
||||
# MCP 配置 - 多来源合并、作用域与策略管控
|
||||
|
||||
## 概述
|
||||
|
||||
详细说明 Claude Code MCP 配置的来源层次、合并优先级、传输类型、企业策略管控、插件集成和保留名称机制。
|
||||
|
||||
## 正文
|
||||
|
||||
### 配置来源与作用域
|
||||
|
||||
Claude Code 的 MCP 配置来自多个来源,每个来源对应一个 `scope`(作用域)。配置按优先级合并,高优先级来源的同名配置覆盖低优先级。
|
||||
|
||||
### 来源列表
|
||||
#### 来源列表
|
||||
|
||||
| 来源 | Scope | 文件/接口 | 说明 |
|
||||
|------|-------|----------|------|
|
||||
@@ -21,25 +32,20 @@ Claude Code 的 MCP 配置来自多个来源,每个来源对应一个 `scope`
|
||||
| 内置动态 | `dynamic` | 代码中注册 | Computer Use / Chrome 等内置服务器 |
|
||||
| IDE SDK | `sdk` | IDE 传入 | VS Code / JetBrains 嵌入模式 |
|
||||
|
||||
### 合并优先级(从低到高)
|
||||
#### 合并优先级(从低到高)
|
||||
|
||||
```
|
||||
claude.ai 连接器 ← 最低优先级
|
||||
↓ 去重
|
||||
插件服务器
|
||||
↓ 去重
|
||||
用户全局配置
|
||||
↓
|
||||
项目配置(.mcp.json) ← 需要用户审批
|
||||
↓
|
||||
本地项目配置
|
||||
↓
|
||||
动态配置(内置 MCP) ← 最高优先级
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["claude.ai 连接器 最低优先级"] -->|"去重"| B["插件服务器"]
|
||||
B -->|"去重"| C["用户全局配置"]
|
||||
C --> D["项目配置 .mcp.json 需要用户审批"]
|
||||
D --> E["本地项目配置"]
|
||||
E --> F["动态配置 内置 MCP 最高优先级"]
|
||||
```
|
||||
|
||||
`Object.assign({}, dedupedPluginServers, userServers, approvedProjectServers, localServers)` 实现合并——后出现的同名键覆盖前者。
|
||||
|
||||
## 企业管控模式
|
||||
### 企业管控模式
|
||||
|
||||
当 `managed-mcp.json` 文件存在时,进入 **排他模式**:
|
||||
|
||||
@@ -57,9 +63,9 @@ if (doesEnterpriseMcpConfigExist()) {
|
||||
- 仍然应用策略过滤(allowlist/denylist)
|
||||
- 无法通过 CLI 添加新服务器(`addMcpConfig` 会拒绝)
|
||||
|
||||
## 传输类型与配置 Schema
|
||||
### 传输类型与配置 Schema
|
||||
|
||||
### stdio(默认)
|
||||
#### stdio(默认)
|
||||
|
||||
启动子进程,通过 stdin/stdout JSON-RPC 通信。
|
||||
|
||||
@@ -75,9 +81,10 @@ if (doesEnterpriseMcpConfigExist()) {
|
||||
|
||||
`type` 字段可省略(默认为 `stdio`)。环境变量通过 `env` 传递给子进程,会与当前进程环境合并。
|
||||
|
||||
**Windows 注意**:使用 `npx` 需要包装为 `cmd /c npx`,否则会报错。
|
||||
> [!warning]
|
||||
> **Windows 注意**:使用 `npx` 需要包装为 `cmd /c npx`,否则会报错。
|
||||
|
||||
### SSE(Server-Sent Events)
|
||||
#### SSE(Server-Sent Events)
|
||||
|
||||
通过 HTTP SSE 连接远程 MCP 服务器。
|
||||
|
||||
@@ -97,7 +104,7 @@ if (doesEnterpriseMcpConfigExist()) {
|
||||
|
||||
支持 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,15 分钟 TTL 缓存避免重复提示。
|
||||
|
||||
### HTTP(Streamable HTTP)
|
||||
#### HTTP(Streamable HTTP)
|
||||
|
||||
HTTP 流式传输。
|
||||
|
||||
@@ -113,7 +120,7 @@ HTTP 流式传输。
|
||||
|
||||
支持与 SSE 相同的 OAuth 配置。
|
||||
|
||||
### WebSocket
|
||||
#### WebSocket
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -124,24 +131,24 @@ HTTP 流式传输。
|
||||
}
|
||||
```
|
||||
|
||||
### IDE 专用类型(内部)
|
||||
#### IDE 专用类型(内部)
|
||||
|
||||
`sse-ide` 和 `ws-ide` 是 IDE 扩展专用类型,不由用户直接配置。
|
||||
|
||||
- `sse-ide`:使用 lockfile token 认证
|
||||
- `ws-ide`:使用 `X-Claude-Code-Ide-Authorization` header
|
||||
|
||||
### SDK 类型(内部)
|
||||
#### SDK 类型(内部)
|
||||
|
||||
`type: "sdk"` 由 IDE 嵌入模式传入,不经过保留名称检查和企业管控排他限制。
|
||||
|
||||
### claude.ai 代理类型(内部)
|
||||
#### claude.ai 代理类型(内部)
|
||||
|
||||
`type: "claudeai-proxy"` 由 claude.ai 网页端配置的连接器使用,通过 OAuth bearer token 认证并支持 401 重试。
|
||||
|
||||
## 配置操作
|
||||
### 配置操作
|
||||
|
||||
### 添加 MCP 服务器
|
||||
#### 添加 MCP 服务器
|
||||
|
||||
通过 CLI 命令 `claude mcp add` 或 API 调用 `addMcpConfig()`:
|
||||
|
||||
@@ -164,19 +171,19 @@ claude mcp add my-remote -s user -t http -u https://mcp.example.com/mcp
|
||||
4. **Schema 验证**:Zod 校验配置格式
|
||||
5. **策略检查**:denylist 拒绝、allowlist 验证
|
||||
|
||||
### 移除 MCP 服务器
|
||||
#### 移除 MCP 服务器
|
||||
|
||||
```bash
|
||||
claude mcp remove my-server -s user
|
||||
```
|
||||
|
||||
### 列出 MCP 服务器
|
||||
#### 列出 MCP 服务器
|
||||
|
||||
```bash
|
||||
claude mcp list
|
||||
```
|
||||
|
||||
## 项目配置审批
|
||||
### 项目配置审批
|
||||
|
||||
`.mcp.json` 中的项目配置需要用户显式审批才能生效:
|
||||
|
||||
@@ -192,7 +199,7 @@ for (const [name, config] of Object.entries(projectServers)) {
|
||||
|
||||
首次打开项目时,Claude Code 会提示用户审批 `.mcp.json` 中的每个服务器。审批状态持久化在本地配置中。
|
||||
|
||||
## 插件 MCP 集成
|
||||
### 插件 MCP 集成
|
||||
|
||||
插件通过 manifest 中的 `.mcp.json` 或 `.mcpb` 文件声明 MCP 服务器:
|
||||
|
||||
@@ -204,11 +211,11 @@ const pluginServerResults = await Promise.all(
|
||||
)
|
||||
```
|
||||
|
||||
### 插件命名空间
|
||||
#### 插件命名空间
|
||||
|
||||
插件 MCP 服务器名格式为 `plugin:<pluginName>:<serverName>`,不会与手动配置的名称冲突。
|
||||
|
||||
### 去重机制
|
||||
#### 去重机制
|
||||
|
||||
插件服务器通过内容签名去重(`dedupPluginMcpServers`):
|
||||
|
||||
@@ -221,15 +228,15 @@ const pluginServerResults = await Promise.all(
|
||||
2. 先加载的插件优先于后加载的
|
||||
3. 被抑制的插件服务器在 `/plugin` UI 中显示提示
|
||||
|
||||
### claude.ai 连接器去重
|
||||
#### claude.ai 连接器去重
|
||||
|
||||
claude.ai 连接器使用相同的内容签名机制去重(`dedupClaudeAiMcpServers`):
|
||||
claude.ai 连接器使用相同的内容签名机制去重:
|
||||
- 仅启用的手动配置参与去重(禁用的手动配置不应抑制连接器)
|
||||
- 连接器名格式为 `claude.ai <DisplayName>`
|
||||
|
||||
## 策略管控
|
||||
### 策略管控
|
||||
|
||||
### Allowlist / Denylist
|
||||
#### Allowlist / Denylist
|
||||
|
||||
企业策略通过 allowlist 和 denylist 控制可用的 MCP 服务器:
|
||||
|
||||
@@ -248,11 +255,11 @@ for (const [name, serverConfig] of Object.entries(configs)) {
|
||||
- stdio 类型的 command + args 匹配
|
||||
- URL 类型的 URL 模式匹配(支持通配符)
|
||||
|
||||
### 插件专用模式
|
||||
#### 插件专用模式
|
||||
|
||||
`isRestrictedToPluginOnly('mcp')` 启用时,只允许插件提供的 MCP 服务器——用户/项目级配置被忽略。
|
||||
|
||||
## 环境变量展开
|
||||
### 环境变量展开
|
||||
|
||||
MCP 配置中的环境变量支持 `$VAR` 和 `${VAR}` 语法展开:
|
||||
|
||||
@@ -271,11 +278,11 @@ MCP 配置中的环境变量支持 `$VAR` 和 `${VAR}` 语法展开:
|
||||
|
||||
展开时缺失的变量会生成警告信息,但不阻止配置加载。
|
||||
|
||||
## 内置 MCP 动态注册
|
||||
### 内置 MCP 动态注册
|
||||
|
||||
内置 MCP 服务器在 `main.tsx` 启动流程中动态注入配置:
|
||||
|
||||
### Computer Use MCP
|
||||
#### Computer Use MCP
|
||||
|
||||
```typescript
|
||||
// src/utils/computerUse/setup.ts
|
||||
@@ -303,7 +310,7 @@ export function setupComputerUseMCP(): {
|
||||
- 非非交互式会话
|
||||
- GrowthBook gate `getChicagoEnabled()` 返回 true
|
||||
|
||||
### Claude in Chrome MCP
|
||||
#### Claude in Chrome MCP
|
||||
|
||||
```typescript
|
||||
// 类似 Computer Use,在 main.tsx 中注册
|
||||
@@ -315,11 +322,11 @@ dynamicMcpConfig = { ...dynamicMcpConfig, ...mcpConfig }
|
||||
- `--chrome` 参数或 `claudeInChromeDefaultEnabled` 配置
|
||||
- Chrome 扩展已安装
|
||||
|
||||
### VSCode SDK MCP
|
||||
#### VSCode SDK MCP
|
||||
|
||||
IDE 嵌入模式通过初始化消息传入 `type:'sdk'` 的配置,由 `setupVscodeSdkMcp()` 设置双向通知。
|
||||
|
||||
## 保留名称
|
||||
### 保留名称
|
||||
|
||||
以下 MCP 服务器名称被保留,用户无法手动配置同名服务器:
|
||||
|
||||
@@ -333,7 +340,7 @@ IDE 嵌入模式通过初始化消息传入 `type:'sdk'` 的配置,由 `setupV
|
||||
1. `addMcpConfig()`(`config.ts:636-648`)— 运行时拒绝
|
||||
2. `main.tsx` 启动检查(`main.tsx:2351-2368`)— 启动时退出
|
||||
|
||||
## 关键源文件索引
|
||||
### 关键源文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
@@ -344,3 +351,7 @@ IDE 嵌入模式通过初始化消息传入 `type:'sdk'` 的配置,由 `setupV
|
||||
| `src/utils/computerUse/setup.ts` | Computer Use 动态注册 |
|
||||
| `src/utils/claudeInChrome/common.ts` | Chrome MCP 保留名与工具名 |
|
||||
| `src/services/mcp/vscodeSdkMcp.ts` | VSCode SDK 双向通知 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[claude-code-best/docs/features/extensibility/mcp-protocol]]
|
||||
|
||||
@@ -1,57 +1,58 @@
|
||||
---
|
||||
title: "MCP 协议 - 连接管理、工具发现与执行链路"
|
||||
description: "从源码角度解析 Claude Code 的 MCP 集成:内置 MCP 与外部 MCP 的区别、7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。"
|
||||
keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现", "内置 MCP", "外部 MCP"]
|
||||
tags:
|
||||
- MCP
|
||||
- 工具扩展
|
||||
- MCP客户端
|
||||
- 工具发现
|
||||
- 内置MCP
|
||||
- 外部MCP
|
||||
create time: 2026-06-09 22:30
|
||||
---
|
||||
|
||||
{/* 本章目标:从源码角度揭示 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()
|
||||
从源码角度解析 Claude Code 的 MCP 集成:内置 MCP 与外部 MCP 的区别、7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。
|
||||
|
||||
## 正文
|
||||
|
||||
### 架构总览:从配置到可用工具
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["配置层 多来源合并"] --> B["getAllMcpConfigs()"]
|
||||
B --> C["useManageMCPConnections() React Hook 管理连接生命周期"]
|
||||
C --> D["connectToServer(name, config) memoize 缓存"]
|
||||
D --> E{"判断类型"}
|
||||
E -->|"内置 MCP"| F["InProcessTransport 同进程"]
|
||||
E -->|"外部 stdio"| G["StdioClientTransport 子进程"]
|
||||
E -->|"远程 SSE/HTTP/WS"| H["网络传输"]
|
||||
F --> I["MCPServerConnection connected/failed/needs-auth/pending/disabled"]
|
||||
G --> I
|
||||
H --> I
|
||||
I --> J["fetchToolsForClient(client) LRU20 缓存"]
|
||||
J --> K["client.request({ method: 'tools/list' })"]
|
||||
K --> L["每个工具包装为 MCPTool 统一 Tool 接口"]
|
||||
L --> M["assembleToolPool() 合并内置工具 + MCP 工具"]
|
||||
M --> N["工具名格式: mcp__serverName__toolName"]
|
||||
```
|
||||
|
||||
## 两种 MCP 模式:内置 vs 外部
|
||||
### 两种 MCP 模式:内置 vs 外部
|
||||
|
||||
Claude Code 的 MCP 实现区分 **内置 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` 配置 |
|
||||
| Claude in Chrome | `claude-in-chrome` | `@ant/claude-for-chrome-mcp` | — | `--chrome` 参数或配置 |
|
||||
| VSCode SDK | `claude-vscode` | — | — | IDE 嵌入模式 (type:`sdk`) |
|
||||
|
||||
#### InProcessTransport:零开销同进程通信
|
||||
##### InProcessTransport:零开销同进程通信
|
||||
|
||||
内置服务器通过 `InProcessTransport`(`src/services/mcp/InProcessTransport.ts`)运行,**不启动子进程**:
|
||||
|
||||
@@ -72,7 +73,7 @@ transport = clientTransport
|
||||
- `close()` 双向关闭,任一端关闭都会触发两端的 `onclose` 回调
|
||||
- 无网络开销、无 IPC 序列化、无进程启动时间
|
||||
|
||||
#### 动态注册流程
|
||||
##### 动态注册流程
|
||||
|
||||
内置服务器在 `main.tsx` 的启动流程中注册,注入 `dynamicMcpConfig`:
|
||||
|
||||
@@ -89,20 +90,7 @@ if (feature("CHICAGO_MCP") && getPlatform() !== "unknown" && !getIsNonInteractiv
|
||||
}
|
||||
```
|
||||
|
||||
`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` 中根据服务器名拦截内置服务器:
|
||||
|
||||
@@ -130,9 +118,9 @@ if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
|
||||
}
|
||||
```
|
||||
|
||||
#### 保留名称保护
|
||||
##### 保留名称保护
|
||||
|
||||
内置服务器的名称被保留,用户无法手动添加同名配置(`config.ts:636-648`):
|
||||
内置服务器的名称被保留,用户无法手动添加同名配置:
|
||||
|
||||
```typescript
|
||||
// 添加 MCP 配置时检查保留名
|
||||
@@ -146,7 +134,7 @@ if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
|
||||
|
||||
启动时也有全局检查(`main.tsx:2351-2368`):如果用户配置中包含保留名(非 `type:'sdk'`),直接 `process.exit(1)`。
|
||||
|
||||
#### VSCode SDK MCP
|
||||
##### VSCode SDK MCP
|
||||
|
||||
VSCode SDK MCP 是特殊的内置模式。IDE(如 VS Code、JetBrains)通过嵌入方式启动 Claude Code,并传入 `type:'sdk'` 的 MCP 配置。这类配置:
|
||||
- 不经过保留名称检查(IDE 可以使用任意名称)
|
||||
@@ -167,11 +155,11 @@ export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
|
||||
}
|
||||
```
|
||||
|
||||
### 外部 MCP 服务器
|
||||
#### 外部 MCP 服务器
|
||||
|
||||
外部 MCP 服务器由用户在配置文件中声明,通过子进程或网络连接运行。
|
||||
|
||||
#### 配置来源
|
||||
##### 配置来源
|
||||
|
||||
| 来源 | Scope | 文件位置 | 优先级 |
|
||||
|------|-------|---------|--------|
|
||||
@@ -182,10 +170,9 @@ export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
|
||||
| claude.ai | `claudeai` | 通过 API 获取 | 低 |
|
||||
| 企业管控 | `enterprise` | 系统管理路径 `managed-mcp.json` | 排他(存在时覆盖全部) |
|
||||
|
||||
#### 配置示例
|
||||
##### 配置示例
|
||||
|
||||
```json
|
||||
// settings.json / .mcp.json 中的 MCP 配置
|
||||
{
|
||||
"mcpServers": {
|
||||
// stdio 类型 — 启动子进程
|
||||
@@ -216,16 +203,16 @@ export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
|
||||
}
|
||||
```
|
||||
|
||||
#### 配置合并与去重
|
||||
##### 配置合并与去重
|
||||
|
||||
`getAllMcpConfigs()`(`config.ts`)按优先级合并多个来源的配置:
|
||||
|
||||
1. 企业管控配置存在时,**独占返回**(忽略所有其他来源)
|
||||
2. 否则合并:user → project → local → plugin → claude.ai
|
||||
3. 插件与手动配置去重:通过 `getMcpServerSignature()` 生成内容签名(基于 command/args/url),插件配置被同名手动配置抑制
|
||||
2. 否则合并:user -> project -> local -> plugin -> claude.ai
|
||||
3. 插件与手动配置去重:通过 `getMcpServerSignature()` 生成内容签名,插件配置被同名手动配置抑制
|
||||
4. `addScopeToServers()` 为每个配置项标注来源 scope
|
||||
|
||||
## 7 种传输层实现
|
||||
### 7 种传输层实现
|
||||
|
||||
`connectToServer()`(`client.ts:596-1643`)根据 `config.type` 分发到不同的 Transport 实现:
|
||||
|
||||
@@ -240,36 +227,36 @@ export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
|
||||
| `claudeai-proxy` | `StreamableHTTPClientTransport` | claude.ai 代理 | OAuth bearer + 401 重试 |
|
||||
| InProcess(内置) | `InProcessTransport` | Computer Use / Chrome | 无(同进程) |
|
||||
|
||||
### stdio 传输的进程管理
|
||||
#### stdio 传输的进程管理
|
||||
|
||||
stdio 类型的 MCP 服务器作为子进程运行,cleanup 时采用 **信号升级策略**(`client.ts:1431-1564`):
|
||||
stdio 类型的 MCP 服务器作为子进程运行,cleanup 时采用 **信号升级策略**:
|
||||
|
||||
```
|
||||
SIGINT (100ms) → SIGTERM (400ms) → SIGKILL
|
||||
SIGINT (100ms) -> SIGTERM (400ms) -> SIGKILL
|
||||
```
|
||||
|
||||
总清理时间上限 600ms,防止 MCP 服务器关闭阻塞 CLI 退出。
|
||||
|
||||
### 远程传输的认证状态机
|
||||
#### 远程传输的认证状态机
|
||||
|
||||
SSE/HTTP 类型使用 `ClaudeAuthProvider` 实现 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,并写入 15 分钟 TTL 的缓存文件(`mcp-needs-auth-cache.json`),避免重复弹出认证提示。
|
||||
SSE/HTTP 类型使用 `ClaudeAuthProvider` 实现 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,并写入 15 分钟 TTL 的缓存文件,避免重复弹出认证提示。
|
||||
|
||||
```
|
||||
连接尝试 → 401 Unauthorized
|
||||
↓
|
||||
handleRemoteAuthFailure()
|
||||
├── logEvent('tengu_mcp_server_needs_auth')
|
||||
├── setMcpAuthCacheEntry(name) ← 写入 15min TTL 缓存
|
||||
└── return { type: 'needs-auth' } ← UI 显示认证提示
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["连接尝试"] --> B["401 Unauthorized"]
|
||||
B --> C["handleRemoteAuthFailure()"]
|
||||
C --> D["logEvent('tengu_mcp_server_needs_auth')"]
|
||||
C --> E["setMcpAuthCacheEntry(name) 写入 15min TTL 缓存"]
|
||||
C --> F["return { type: 'needs-auth' } UI 显示认证提示"]
|
||||
```
|
||||
|
||||
## 连接缓存与重连机制
|
||||
### 连接缓存与重连机制
|
||||
|
||||
`connectToServer` 使用 lodash `memoize` 缓存连接对象,缓存 key 为 `${name}-${JSON.stringify(config)}`。
|
||||
|
||||
### 缓存失效触发
|
||||
#### 缓存失效触发
|
||||
|
||||
当连接关闭时(`client.onclose`),清除所有相关缓存(`client.ts:1376-1404`):
|
||||
当连接关闭时(`client.onclose`),清除所有相关缓存:
|
||||
|
||||
```typescript
|
||||
client.onclose = () => {
|
||||
@@ -281,9 +268,9 @@ client.onclose = () => {
|
||||
}
|
||||
```
|
||||
|
||||
### 连接降级检测
|
||||
#### 连接降级检测
|
||||
|
||||
远程传输有 **连续错误计数器**(`client.ts:1229`):
|
||||
远程传输有 **连续错误计数器**:
|
||||
|
||||
```typescript
|
||||
let consecutiveConnectionErrors = 0
|
||||
@@ -292,9 +279,9 @@ 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 才回收。
|
||||
每个 HTTP 请求使用独立的 `setTimeout` 超时,而非共享 `AbortSignal.timeout()`。原因是 Bun 对 AbortSignal.timeout 的 GC 是惰性的——每个请求约 2.4KB 原生内存,即使请求毫秒级完成也要等 60s 才回收。
|
||||
|
||||
```typescript
|
||||
const controller = new AbortController()
|
||||
@@ -302,7 +289,7 @@ const timer = setTimeout(c => c.abort(...), MCP_REQUEST_TIMEOUT_MS, controller)
|
||||
timer.unref?.() // 不阻止进程退出
|
||||
```
|
||||
|
||||
## 工具发现:从 MCP 到 Tool 接口
|
||||
### 工具发现:从 MCP 到 Tool 接口
|
||||
|
||||
`fetchToolsForClient()`(`client.ts:1744-2000`)使用 `memoizeWithLRU` 缓存(上限 100),将 MCP 工具转换为 Claude Code 的统一 Tool 接口:
|
||||
|
||||
@@ -311,19 +298,19 @@ const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
|
||||
// 结果: "mcp__my-database__query"
|
||||
```
|
||||
|
||||
### 内置 MCP 的工具发现
|
||||
#### 内置 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+ 个浏览器控制工具。
|
||||
- **Computer Use**:`createComputerUseMcpServerForCli()` 构建 MCP Server 对象,注册 `ListToolsRequestSchema` handler。工具描述包含平台特定的已安装应用列表(1s 超时枚举)。
|
||||
- **Claude in Chrome**:`createClaudeForChromeMcpServer()` 构建 Server,提供 17+ 个浏览器控制工具。
|
||||
- **VSCode SDK**:由 IDE 端提供工具列表,通过 SDK transport 传递。
|
||||
|
||||
### 工具描述截断
|
||||
#### 工具描述截断
|
||||
|
||||
MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`)。OpenAPI 生成的 MCP 服务器曾观察到 15-60KB 的描述文档。
|
||||
|
||||
### 工具能力标注
|
||||
#### 工具能力标注
|
||||
|
||||
每个 MCP 工具根据 `tool.annotations` 自动标注:
|
||||
|
||||
@@ -334,36 +321,36 @@ MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`)。OpenAPI
|
||||
| `openWorldHint` | `isOpenWorld()` | 开放世界(不可枚举) |
|
||||
| `title` | `userFacingName()` | 显示名称 |
|
||||
|
||||
### MCP 工具的权限检查
|
||||
#### MCP 工具的权限检查
|
||||
|
||||
MCP 工具默认返回 `{ behavior: 'passthrough' }`(`client.ts:1816-1834`),意味着它们始终进入权限确认流程。工具名使用 `mcp__` 前缀精确匹配权限规则。
|
||||
MCP 工具默认返回 `{ behavior: 'passthrough' }`,意味着它们始终进入权限确认流程。工具名使用 `mcp__` 前缀精确匹配权限规则。
|
||||
|
||||
内置 MCP 服务器的工具通过 `allowedTools` 列表自动授权——在 `main.tsx` 启动时加入,绕过普通权限提示。例如 Computer Use 工具的 `request_access` 自行处理会话级审批。
|
||||
|
||||
## MCP 工具的执行链路
|
||||
### 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 }
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["AI 生成 tool_use: { name: 'mcp__my-db__query', input: { sql: '...' } }"] --> B["MCPTool.call()"]
|
||||
B --> C["ensureConnectedClient() 确保连接有效 重连"]
|
||||
C --> D["callMCPToolWithUrlElicitationRetry() 带 Elicitation 重试"]
|
||||
D --> E["client.request({ method: 'tools/call' })"]
|
||||
D --> F["处理图片结果 resize + persist"]
|
||||
D --> G["内容截断 mcpContentNeedsTruncation"]
|
||||
E --> H{"McpSessionExpiredError?"}
|
||||
H -->|"是"| I["重试一次"]
|
||||
H -->|"否"| J["返回 { data: content, mcpMeta }"]
|
||||
```
|
||||
|
||||
### Session 过期自动重试
|
||||
#### Session 过期自动重试
|
||||
|
||||
HTTP 传输的 MCP session 可能过期。检测到 `McpSessionExpiredError` 后自动重试一次(`client.ts:1862`),因为 `ensureConnectedClient()` 已经清除了缓存并建立了新连接。
|
||||
HTTP 传输的 MCP session 可能过期。检测到 `McpSessionExpiredError` 后自动重试一次,因为 `ensureConnectedClient()` 已经清除了缓存并建立了新连接。
|
||||
|
||||
### 内容截断与持久化
|
||||
#### 内容截断与持久化
|
||||
|
||||
大型 MCP 工具输出通过 `truncateMcpContentIfNeeded` 截断,二进制内容(图片)通过 `persistBinaryContent` 写入文件并返回文件路径。图片自动 resize(`maybeResizeAndDownsampleImageBuffer`)。
|
||||
大型 MCP 工具输出通过 `truncateMcpContentIfNeeded` 截断,二进制内容(图片)通过 `persistBinaryContent` 写入文件并返回文件路径。图片自动 resize。
|
||||
|
||||
## MCP 连接的并发控制
|
||||
### MCP 连接的并发控制
|
||||
|
||||
```typescript
|
||||
// 本地服务器并发连接数
|
||||
@@ -375,12 +362,12 @@ getRemoteMcpServerConnectionBatchSize() // 默认 20
|
||||
|
||||
本地 MCP 服务器(stdio)是重量级的子进程,默认限制 3 个并发连接。远程服务器是轻量级 HTTP 请求,允许 20 个并发。
|
||||
|
||||
## 内置 vs 外部 MCP 对比总结
|
||||
### 内置 vs 外部 MCP 对比总结
|
||||
|
||||
| 维度 | 内置 MCP | 外部 MCP |
|
||||
|------|---------|---------|
|
||||
| **Transport** | `InProcessTransport`(同进程) | stdio / SSE / HTTP / WebSocket |
|
||||
| **配置来源** | `setupComputerUseMCP()` / `setupClaudeInChrome()` 等动态注册 | settings.json / .mcp.json / 插件 / claude.ai |
|
||||
| **配置来源** | `setupComputerUseMCP()` 等动态注册 | settings.json / .mcp.json / 插件 / claude.ai |
|
||||
| **Scope** | `dynamic` | `user` / `project` / `local` / `enterprise` / `claudeai` |
|
||||
| **进程模型** | 同进程,零开销 | 子进程(stdio)或网络连接 |
|
||||
| **名称保护** | 保留名,用户不可添加同名 | 自由命名(字母数字 + `-_`) |
|
||||
@@ -388,9 +375,9 @@ getRemoteMcpServerConnectionBatchSize() // 默认 20
|
||||
| **权限** | `allowedTools` 自动授权 | `passthrough` 进入权限确认 |
|
||||
| **Feature Flag** | `CHICAGO_MCP`(Computer Use)等 | 无(始终可用) |
|
||||
| **工具发现** | 与外部相同(MCP 协议) | 标准 MCP `tools/list` |
|
||||
| **清理** | `inProcessServer.close()` | 信号升级策略 SIGINT→SIGTERM→SIGKILL |
|
||||
| **清理** | `inProcessServer.close()` | 信号升级策略 SIGINT->SIGTERM->SIGKILL |
|
||||
|
||||
## 关键源文件索引
|
||||
### 关键源文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
@@ -405,3 +392,7 @@ getRemoteMcpServerConnectionBatchSize() // 默认 20
|
||||
| `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) |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[claude-code-best/docs/features/extensibility/mcp-configuration]]
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
---
|
||||
title: "Skills 技能系统 - Prompt 即能力的架构哲学"
|
||||
description: "深入剖析 Claude Code Skills 系统的完整实现:从磁盘加载、Frontmatter 解析、预算感知描述截断、双模式执行(inline/fork)、权限白名单、条件激活、动态发现到远程技能加载,揭示一条完整的 Skill 生命周期链路。"
|
||||
keywords: ["Skills", "SkillTool", "技能加载", "Frontmatter", "whenToUse", "allowedTools", "fork执行", "动态发现"]
|
||||
tags: [Skills, SkillTool, 技能加载, Frontmatter, whenToUse, allowedTools, fork执行, 动态发现]
|
||||
create time: 2026-06-09 22:30
|
||||
---
|
||||
|
||||
{/* 本章目标:揭示 Skill 系统从文件到执行的全链路实现 */}
|
||||
# Skills 技能系统 - Prompt 即能力的架构哲学
|
||||
|
||||
## Tool vs Skill:本质差异
|
||||
## 概述
|
||||
|
||||
深入剖析 Claude Code Skills 系统的完整实现:从磁盘加载、Frontmatter 解析、预算感知描述截断、双模式执行(inline/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) |
|
||||
| 注册位置 | `src/tools.ts` -> `getTools()` | `src/commands.ts` -> `getCommands()` |
|
||||
| 执行器 | 各 Tool 的 `call()` 方法 | `SkillTool.call()` -> 两条分支(inline / fork) |
|
||||
|
||||
Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Prompt 质量**。一个代码审查 Skill 不需要审查引擎,只需告诉 AI "审查什么、按什么顺序、输出什么格式"——Skill 把这种"经验"封装为可复用的 Markdown。
|
||||
> [!tip]
|
||||
> Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Prompt 质量**。一个代码审查 Skill 不需要审查引擎,只需告诉 AI "审查什么、按什么顺序、输出什么格式"——Skill 把这种"经验"封装为可复用的 Markdown。
|
||||
|
||||
## Skill 的五个来源与加载链路
|
||||
### Skill 的五个来源与加载链路
|
||||
|
||||
### 1. 内置命令(Built-in Commands)
|
||||
#### 1. 内置命令(Built-in Commands)
|
||||
|
||||
硬编码在 `src/commands.ts:299` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown,但实现了相同的 `Command` 接口(`src/types/command.ts`)。
|
||||
硬编码在 `src/commands.ts:299` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown,但实现了相同的 `Command` 接口。
|
||||
|
||||
### 2. Bundled Skills(编译时打包)
|
||||
#### 2. Bundled Skills(编译时打包)
|
||||
|
||||
通过 `registerBundledSkill()`(`src/skills/bundledSkills.ts:53`)在模块初始化时注册。关键特性:
|
||||
|
||||
- **延迟文件提取**:如果 Skill 声明了 `files`(参考文件),首次调用时才解压到临时目录(`getBundledSkillExtractDir()`),使用 `O_NOFOLLOW | O_EXCL` 防止符号链接攻击(`safeWriteFile`,第 186 行)
|
||||
- **延迟文件提取**:如果 Skill 声明了 `files`(参考文件),首次调用时才解压到临时目录,使用 `O_NOFOLLOW | O_EXCL` 防止符号链接攻击
|
||||
- **闭包级 memoize**:并发调用共享同一个 extraction promise,避免竞态写入
|
||||
- 来源标记为 `source: 'bundled'`,在 Prompt 预算中享有**不可截断**的特权
|
||||
|
||||
### 3. 磁盘 Skills(`.claude/skills/`)
|
||||
#### 3. 磁盘 Skills(`.claude/skills/`)
|
||||
|
||||
由 `loadSkillsFromSkillsDir()`(`src/skills/loadSkillsDir.ts:407`)加载,这是最重要的加载路径:
|
||||
|
||||
@@ -45,27 +51,28 @@ Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Promp
|
||||
|
||||
**加载协议**:只识别 `skill-name/SKILL.md` 目录格式,不再支持单文件 `.md`。加载流程:
|
||||
|
||||
1. `readdir` 扫描目录 → 仅保留 `isDirectory()` 或 `isSymbolicLink()` 的条目
|
||||
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 行),避免通过符号链接或重叠父目录导致的重复加载。
|
||||
**去重机制**:使用 `realpath()` 解析符号链接获得规范路径,避免通过符号链接或重叠父目录导致的重复加载。
|
||||
|
||||
### 4. MCP Skills(动态发现)
|
||||
#### 4. MCP Skills(动态发现)
|
||||
|
||||
通过 `registerMCPSkillBuilders()` 注册构建器,MCP Server 的 prompt 被 `mcpSkillBuilders.ts` 转换为 `Command` 对象。标记为 `loadedFrom: 'mcp'`。
|
||||
|
||||
**安全边界**:MCP Skills 的 Prompt 内容**禁止执行内联 shell 命令**(`loadSkillsDir.ts:374` 的 `loadedFrom !== 'mcp'` 守卫),因为远程内容不可信。
|
||||
> [!warning]
|
||||
> **安全边界**:MCP Skills 的 Prompt 内容**禁止执行内联 shell 命令**(`loadSkillsDir.ts:374` 的 `loadedFrom !== 'mcp'` 守卫),因为远程内容不可信。
|
||||
|
||||
### 5. Legacy Commands(`/commands/` 目录)
|
||||
#### 5. Legacy Commands(`/commands/` 目录)
|
||||
|
||||
向后兼容的旧格式,由 `loadSkillsFromCommandsDir()`(第 566 行)加载。同时支持 `SKILL.md` 目录格式和单 `.md` 文件格式。
|
||||
|
||||
## Frontmatter 字段全景
|
||||
### Frontmatter 字段全景
|
||||
|
||||
一个 `SKILL.md` 的完整 frontmatter(`parseSkillFrontmatterFields`,第 185 行):
|
||||
一个 `SKILL.md` 的完整 frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
@@ -96,91 +103,87 @@ shell: ["bash"] # Shell 执行环境
|
||||
|
||||
解析后有 16 个字段被提取,其中 `allowedTools`、`model`、`effort` 在执行时动态修改 `toolPermissionContext`。
|
||||
|
||||
## 两条执行路径:Inline vs Fork
|
||||
### 两条执行路径:Inline vs Fork
|
||||
|
||||
SkillTool(`packages/builtin-tools/src/tools/SkillTool/SkillTool.ts:332`)在 `call()` 中根据 `command.context` 分流:
|
||||
|
||||
### Inline 模式(默认)
|
||||
#### Inline 模式(默认)
|
||||
|
||||
Skill 的 Prompt 内容被注入为 **UserMessage**,在主对话流中继续执行:
|
||||
|
||||
1. `processPromptSlashCommand()` 处理参数替换(`$ARGUMENTS`)和 shell 命令展开(`` !`...` ``)
|
||||
1. `processPromptSlashCommand()` 处理参数替换(`$ARGUMENTS`)和 shell 命令展开
|
||||
2. `${CLAUDE_SKILL_DIR}` 被替换为 Skill 所在目录的绝对路径
|
||||
3. `${CLAUDE_SESSION_ID}` 被替换为当前会话 ID
|
||||
4. 返回 `newMessages`(注入到对话流)+ `contextModifier`(修改权限上下文)
|
||||
|
||||
`contextModifier`(第 776 行)做了三件事:
|
||||
`contextModifier` 做了三件事:
|
||||
- **工具白名单注入**:将 `allowedTools` 合并到 `alwaysAllowRules.command`
|
||||
- **模型切换**:`resolveSkillModelOverride()` 处理模型覆盖,保留 `[1m]` 后缀以避免 200K 窗口截断
|
||||
- **努力级别覆盖**:修改 `effortValue`
|
||||
|
||||
### Fork 模式(`context: fork`)
|
||||
#### Fork 模式(`context: fork`)
|
||||
|
||||
Skill 在**独立子 Agent** 中执行(`executeForkedSkill`,第 122 行):
|
||||
Skill 在**独立子 Agent** 中执行(`executeForkedSkill`):
|
||||
|
||||
1. `prepareForkedCommandContext()` 构建隔离的 Agent 定义和 Prompt
|
||||
2. `runAgent()` 启动子 Agent 循环,拥有独立的 token 预算
|
||||
3. 通过 `onProgress` 回调报告工具使用进度
|
||||
4. 结果通过 `extractResultText()` 提取,子 Agent 的全部消息在提取后被释放(`agentMessages.length = 0`)
|
||||
4. 结果通过 `extractResultText()` 提取,子 Agent 的全部消息在提取后被释放
|
||||
5. 最终通过 `clearInvokedSkillsForAgent()` 清理状态
|
||||
|
||||
Fork 模式适用于需要强隔离的场景(如长时间运行的审查任务),避免污染主对话的上下文。
|
||||
|
||||
## 权限模型:Safe Properties 白名单
|
||||
### 权限模型:Safe Properties 白名单
|
||||
|
||||
`checkPermissions()`(第 433 行)实现了一个五层权限检查:
|
||||
`checkPermissions()` 实现了一个五层权限检查:
|
||||
|
||||
```
|
||||
1. Deny 规则匹配(支持精确匹配和 prefix:* 通配符)
|
||||
↓ 未命中
|
||||
2. 远程 canonical Skill 自动放行(EXPERIMENTAL_SKILL_SEARCH + USER_TYPE === 'ant')
|
||||
↓ 未命中
|
||||
3. Allow 规则匹配
|
||||
↓ 未命中
|
||||
4. Safe Properties 白名单检查(skillHasOnlySafeProperties,第 911 行)
|
||||
↓ 有非安全属性
|
||||
5. Ask 用户确认(附带精确匹配和前缀匹配两条建议规则)
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["1. Deny 规则匹配 精确匹配和 prefix:* 通配符"] -->|"未命中"| B["2. 远程 canonical Skill 自动放行 EXPERIMENTAL_SKILL_SEARCH + USER_TYPE === 'ant'"]
|
||||
B -->|"未命中"| C["3. Allow 规则匹配"]
|
||||
C -->|"未命中"| D["4. Safe Properties 白名单检查 skillHasOnlySafeProperties"]
|
||||
D -->|"有非安全属性"| E["5. Ask 用户确认 附带精确匹配和前缀匹配两条建议规则"]
|
||||
```
|
||||
|
||||
**Safe Properties**(`SAFE_SKILL_PROPERTIES`,第 876 行)是一个包含 30 个属性名的白名单(覆盖 `PromptCommand` 和 `CommandBase` 两个类型的所有安全属性)。任何不在白名单中的**有意义的属性值**(排除 `undefined`、`null`、空数组、空对象)都会触发权限请求。这是**正向安全**设计——未来新增的属性默认需要权限。
|
||||
**Safe Properties** 是一个包含 30 个属性名的白名单(覆盖 `PromptCommand` 和 `CommandBase` 两个类型的所有安全属性)。任何不在白名单中的**有意义的属性值**都会触发权限请求。这是**正向安全**设计——未来新增的属性默认需要权限。
|
||||
|
||||
## Prompt 预算:1% 上下文窗口的截断策略
|
||||
### Prompt 预算:1% 上下文窗口的截断策略
|
||||
|
||||
Skill 列表注入 System Prompt 时有严格的字符预算(`prompt.ts`):
|
||||
Skill 列表注入 System Prompt 时有严格的字符预算:
|
||||
|
||||
- **预算计算**:`contextWindowTokens × 4 chars/token × 1%`(约 8000 字符)
|
||||
- **单条上限**:`MAX_LISTING_DESC_CHARS = 250` 字符(超出截断为 `…`)
|
||||
- **预算计算**:`contextWindowTokens x 4 chars/token x 1%`(约 8000 字符)
|
||||
- **单条上限**:`MAX_LISTING_DESC_CHARS = 250` 字符(超出截断为 `...`)
|
||||
- **Bundled Skills 不可截断**:它们始终保留完整描述,预算不足时只截断非 bundled 的
|
||||
- **降级策略**:
|
||||
1. 尝试完整描述 → 超预算?
|
||||
2. Bundled 保留完整,非 bundled 均分剩余预算 → 每条描述低于 20 字符?
|
||||
1. 尝试完整描述 -> 超预算?
|
||||
2. Bundled 保留完整,非 bundled 均分剩余预算 -> 每条描述低于 20 字符?
|
||||
3. 非 bundled 仅保留名称
|
||||
|
||||
`formatCommandsWithinBudget()`(`prompt.ts:70`)实现了这个三级降级。
|
||||
`formatCommandsWithinBudget()` 实现了这个三级降级。
|
||||
|
||||
## 动态发现与条件激活
|
||||
### 动态发现与条件激活
|
||||
|
||||
### 基于文件路径的动态发现
|
||||
#### 基于文件路径的动态发现
|
||||
|
||||
`discoverSkillDirsForPaths()`(`loadSkillsDir.ts:861`)在文件操作时触发:
|
||||
`discoverSkillDirsForPaths()` 在文件操作时触发:
|
||||
|
||||
1. 从被操作的文件路径开始,**向上遍历**至 CWD(不包含 CWD 本身)
|
||||
2. 在每层查找 `.claude/skills/` 目录
|
||||
3. 使用 `realpath` 去重,`git check-ignore` 过滤 gitignored 目录
|
||||
4. 按路径深度排序(**深层优先**),更接近文件的 Skill 优先级更高
|
||||
|
||||
### 条件激活(paths frontmatter)
|
||||
#### 条件激活(paths frontmatter)
|
||||
|
||||
带有 `paths` 模式的 Skill 在加载时不会立即可用,而是存入 `conditionalSkills` Map。当被操作的文件路径匹配某个 Skill 的 paths 模式时(使用 `ignore` 库做 gitignore 风格匹配),该 Skill 才被**激活**——从 `conditionalSkills` 移入 `dynamicSkills`。
|
||||
带有 `paths` 模式的 Skill 在加载时不会立即可用,而是存入 `conditionalSkills` Map。当被操作的文件路径匹配某个 Skill 的 paths 模式时,该 Skill 才被**激活**——从 `conditionalSkills` 移入 `dynamicSkills`。
|
||||
|
||||
这意味着一个只在 `*.test.ts` 上激活的测试 Skill,平时完全不可见,只有当 AI 读取或编辑测试文件时才会出现。
|
||||
|
||||
## 使用频率排名
|
||||
### 使用频率排名
|
||||
|
||||
`recordSkillUsage()`(`skillUsageTracking.ts`)使用指数衰减算法计算 Skill 排名分数:
|
||||
`recordSkillUsage()` 使用指数衰减算法计算 Skill 排名分数:
|
||||
|
||||
```
|
||||
score = usageCount × max(0.5^(daysSinceUse / 7), 0.1)
|
||||
score = usageCount x max(0.5^(daysSinceUse / 7), 0.1)
|
||||
```
|
||||
|
||||
- **7 天半衰期**:一周前的使用权重减半
|
||||
@@ -189,33 +192,42 @@ score = usageCount × max(0.5^(daysSinceUse / 7), 0.1)
|
||||
|
||||
排名数据持久化在全局配置的 `skillUsage` 字段中。
|
||||
|
||||
## 远程技能加载(Experimental)
|
||||
### 远程技能加载(Experimental)
|
||||
|
||||
通过 `EXPERIMENTAL_SKILL_SEARCH` feature flag 控制,支持从远程(AKI/GCS/S3)加载 `_canonical_<slug>` 格式的 Skill:
|
||||
|
||||
1. `validateInput()` 中 `stripCanonicalPrefix()` 拦截 canonical 名称
|
||||
2. `executeRemoteSkill()`(第 970 行)从远程 URL 加载 SKILL.md
|
||||
2. `executeRemoteSkill()` 从远程 URL 加载 SKILL.md
|
||||
3. 支持 `gs://`、`https://`、`s3://` 等 URL 协议
|
||||
4. 内容经过 frontmatter 剥离、`${CLAUDE_SKILL_DIR}` 替换后直接注入
|
||||
5. 通过 `addInvokedSkill()` 注册到 compaction 保留状态,确保压缩后仍可恢复
|
||||
6. 远程 Skill 不经过 `processPromptSlashCommand`——无 `!command` 替换、无 `$ARGUMENTS` 展开
|
||||
|
||||
## 完整生命周期总结
|
||||
### 完整生命周期总结
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["磁盘 SKILL.md"] --> B["parseFrontmatter()"]
|
||||
B --> C["parseSkillFrontmatterFields() 16 个字段"]
|
||||
C --> D["createSkillCommand() Command 对象"]
|
||||
D --> E["去重 realpath + seenFileIds"]
|
||||
E --> F{"条件 Skill?"}
|
||||
F -->|"是"| G["conditionalSkills Map 等待路径匹配激活"]
|
||||
F -->|"否"| H["getSkillDirCommands() memoize 缓存"]
|
||||
G --> H
|
||||
H --> I["getAllCommands() 合并 local + MCP"]
|
||||
I --> J["formatCommandsWithinBudget() 截断后的 Skill 列表注入 System Prompt"]
|
||||
J --> K["AI 选择匹配的 Skill"]
|
||||
K --> L["SkillTool.validateInput() 名称校验 + 存在性检查"]
|
||||
L --> M["SkillTool.checkPermissions() 五层权限检查"]
|
||||
M --> N["SkillTool.call() inline 或 fork 执行"]
|
||||
N --> O["contextModifier() 注入 allowedTools + model + effort"]
|
||||
O --> P["recordSkillUsage() 更新使用频率排名"]
|
||||
```
|
||||
磁盘 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() → 更新使用频率排名
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[claude-code-best/docs/features/extensibility/custom-agents]]
|
||||
- [[claude-code-best/docs/features/extensibility/hooks]]
|
||||
- [[claude-code-best/docs/features/mcp-skills]]
|
||||
- [[claude-code-best/docs/features/experimental-skill-search]]
|
||||
|
||||
Reference in New Issue
Block a user