diff --git a/CLAUDE.md b/CLAUDE.md index 834711a9d..b884fe3ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,7 +70,7 @@ bun run rcs bun run docs:dev ``` -详细的测试规范、覆盖状态和改进计划见 `docs/testing-spec.md`。 +详细的测试规范、覆盖状态和改进计划见 `src/**/__tests__/` 与 `tests/integration/`。 ## Architecture @@ -186,7 +186,7 @@ bun run docs:dev - **`src/bridge/`** — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`。 - **`packages/remote-control-server/`** — 自托管 RCS,支持 Docker 部署,含 Web UI 控制面板(React 19 + Vite + Radix UI)。支持 ACP agent 通过 acp-link 接入(ACP WebSocket handler、relay handler、SSE event stream)。通过 `bun run rcs` 启动。 - CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`。 -- 详见 `docs/features/remote-control-self-hosting.md`。 +- 详见 `docs/features/modes/remote-control-self-hosting.md`。 ### ACP Protocol (Agent Client Protocol) diff --git a/README.md b/README.md index dfe8e2948..63c9a9755 100644 --- a/README.md +++ b/README.md @@ -18,20 +18,20 @@ | 特性 | 说明 | 文档 | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/uds-inbox) / [LAN](https://ccb.agent-aura.top/docs/features/lan-pipes) | -| **ACP 协议一等一支持** | 支持接入 Zed、Cursor 等 IDE,支持会话恢复、Skills、权限桥接 | [文档](https://ccb.agent-aura.top/docs/features/acp-zed) | -| **Remote Control 私有部署** | Docker 自托管远程界面, 可以手机上看 CC | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) | -| **Langfuse 监控** | 企业级 Agent 监控, 可以清晰看到每次 agent loop 细节, 可以一键转化为数据集 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) | -| **Web Search** | 内置网页搜索工具, 支持 bing 和 brave 搜索 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) | +| **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/agents/uds-inbox) / [LAN](https://ccb.agent-aura.top/docs/features/agents/lan-pipes) | +| **ACP 协议一等一支持** | 支持接入 Zed、Cursor 等 IDE,支持会话恢复、Skills、权限桥接 | [文档](https://ccb.agent-aura.top/docs/features/agents/acp-zed) | +| **Remote Control 私有部署** | Docker 自托管远程界面, 可以手机上看 CC | [文档](https://ccb.agent-aura.top/docs/features/modes/remote-control-self-hosting) | +| **Langfuse 监控** | 企业级 Agent 监控, 可以清晰看到每次 agent loop 细节, 可以一键转化为数据集 | [文档](https://ccb.agent-aura.top/docs/features/tools/langfuse-monitoring) | +| **Web Search** | 内置网页搜索工具, 支持 bing 和 brave 搜索 | [文档](https://ccb.agent-aura.top/docs/features/external/web-browser-tool) | | **Poor Mode** | 穷鬼模式,关闭记忆提取和键入建议,大幅度减少并发请求 | /poor 可以开关 | -| **Channels 频道通知** | MCP 服务器推送外部消息到会话(飞书/Slack/Discord/微信等),`--channels plugin:name@marketplace` 启用 | [文档](https://ccb.agent-aura.top/docs/features/channels) | -| **自定义模型供应商** | OpenAI/Anthropic/Gemini/Grok 兼容 (`/login`) | [文档](https://ccb.agent-aura.top/docs/features/all-features-guide) | -| Voice Mode | 语音输入,支持豆包语言输入(`/voice doubao`) | [文档](https://ccb.agent-aura.top/docs/features/voice-mode) | -| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) | -| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](https://ccb.agent-aura.top/docs/features/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) | +| **Channels 频道通知** | MCP 服务器推送外部消息到会话(飞书/Slack/Discord/微信等),`--channels plugin:name@marketplace` 启用 | [文档](https://ccb.agent-aura.top/docs/features/external/channels) | +| **自定义模型供应商** | OpenAI/Anthropic/Gemini/Grok 兼容 (`/login`) | [文档](https://ccb.agent-aura.top/docs/getting-started/model-providers) | +| Voice Mode | 语音输入,支持豆包语言输入(`/voice doubao`) | [文档](https://ccb.agent-aura.top/docs/features/external/voice-mode) | +| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/external/computer-use) | +| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](https://ccb.agent-aura.top/docs/features/external/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/external/claude-in-chrome-mcp) | | Sentry | 企业级错误追踪 | [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup) | | GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) | -| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) | +| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/modes/auto-dream) | - 🚀 [想要启动项目](#-快速开始源码版) - 🐛 [想要调试项目](#vs-code-调试) diff --git a/docs.json b/docs.json index fbe92988b..3e5d84e28 100644 --- a/docs.json +++ b/docs.json @@ -1,153 +1,14 @@ { "$schema": "https://mintlify.com/docs.json", "theme": "mint", - "name": "Claude Code Architecture", + "name": "Claude Code Best", + "description": "Anthropic Claude Code 的开源复原版本 — 完整架构原理、功能文档与使用指南。", "colors": { - "primary": "#D97706", + "primary": "#D77757", "light": "#F59E0B", "dark": "#B45309" }, "favicon": "/docs/favicon.svg", - "navigation": { - "groups": [ - { - "group": "开始", - "pages": [ - { - "group": "介绍", - "pages": [ - "docs/introduction/what-is-claude-code", - "docs/introduction/why-this-whitepaper", - "docs/introduction/architecture-overview" - ] - } - ] - }, - { - "group": "对话是如何运转的", - "pages": [ - "docs/conversation/the-loop", - "docs/conversation/streaming", - "docs/conversation/multi-turn" - ] - }, - { - "group": "工具:AI 的双手", - "pages": [ - "docs/tools/what-are-tools", - "docs/tools/file-operations", - "docs/tools/shell-execution", - "docs/tools/search-and-navigation", - "docs/tools/task-management" - ] - }, - { - "group": "上下文工程", - "pages": [ - "docs/context/system-prompt", - "docs/context/project-memory", - "docs/context/compaction", - "docs/context/token-budget" - ] - }, - { - "group": "多 Agent 协作", - "pages": [ - "docs/agent/sub-agents", - "docs/agent/worktree-isolation", - "docs/agent/coordinator-and-swarm" - ] - }, - { - "group": "可扩展性", - "pages": [ - "docs/extensibility/mcp-protocol", - "docs/extensibility/hooks", - "docs/extensibility/skills", - "docs/extensibility/custom-agents" - ] - }, - { - "group": "安全与权限", - "pages": [ - "docs/safety/why-safety-matters", - "docs/safety/permission-model", - "docs/safety/sandbox", - "docs/safety/plan-mode", - "docs/safety/auto-mode" - ] - }, - { - "group": "揭秘:隐藏功能与内部机制", - "pages": [ - "docs/internals/three-tier-gating", - "docs/internals/feature-flags", - "docs/internals/growthbook-ab-testing", - "docs/internals/growthbook-adapter", - "docs/internals/sentry-setup", - "docs/internals/hidden-features", - "docs/internals/ant-only-world", - "docs/internals/session-transcript-persistence", - "docs/features/debug-mode", - "docs/features/buddy" - ] - }, - { - "group": "隐藏功能详解", - "pages": [ - { - "group": "Agent 与协作", - "pages": [ - "docs/features/coordinator-mode", - "docs/features/fork-subagent", - "docs/features/daemon", - "docs/features/teammem" - ] - }, - { - "group": "运行模式", - "pages": [ - "docs/features/kairos", - "docs/features/voice-mode", - "docs/features/bridge-mode", - "docs/features/remote-control-self-hosting", - "docs/features/proactive", - "docs/features/ultraplan" - ] - }, - { - "group": "工具增强", - "pages": [ - "docs/features/mcp-skills", - "docs/features/tree-sitter-bash", - "docs/features/bash-classifier", - "docs/features/web-browser-tool", - "docs/features/experimental-skill-search" - ] - }, - { - "group": "上下文与自动化", - "pages": [ - "docs/features/token-budget", - "docs/features/context-collapse", - "docs/features/workflow-scripts", - "docs/features/auto-dream" - ] - }, - "docs/features/tier3-stubs" - ] - }, - { - "group": "基础设施与依赖", - "pages": [ - "docs/auto-updater", - "docs/lsp-integration", - "docs/external-dependencies", - "docs/telemetry-remote-config-audit" - ] - } - ] - }, "logo": { "light": "/docs/logo/light.svg", "dark": "/docs/logo/dark.svg" @@ -165,7 +26,7 @@ } }, "search": { - "prompt": "搜索 Claude Code 架构文档..." + "prompt": "搜索 CCB 文档..." }, "seo": { "metatags": { @@ -177,13 +38,90 @@ }, "footer": { "socials": { - "github": "https://github.com/anthropics/claude-code" + "github": "https://github.com/claude-code-best/claude-code", + "discord": "https://discord.gg/uApuzJWGKX" } }, "redirects": [ { - "source": "/docs/introduction", - "destination": "/docs/introduction/what-is-claude-code" + "source": "/docs/features/agents/uds-inbox", + "destination": "/docs/features/agents/pipes-and-lan" + }, + { + "source": "/docs/features/agents/lan-pipes", + "destination": "/docs/features/agents/pipes-and-lan" + }, + { + "source": "/docs/features/agents/acp-link", + "destination": "/docs/features/agents/acp" + }, + { + "source": "/docs/features/agents/acp-zed", + "destination": "/docs/features/agents/acp" + }, + { + "source": "/docs/features/external/chrome-use-mcp", + "destination": "/docs/features/external/chrome-control" + }, + { + "source": "/docs/features/external/claude-in-chrome-mcp", + "destination": "/docs/features/external/chrome-control" + }, + { + "source": "/docs/features/external/computer-use-tools-reference", + "destination": "/docs/features/external/computer-use" } - ] + ], + "navigation": { + "groups": [ + { + "group": "开始", + "pages": [ + "docs/getting-started/installation", + "docs/getting-started/quickstart", + "docs/getting-started/model-providers" + ] + }, + { + "group": "核心功能", + "pages": [ + { + "group": "协作与多 Agent", + "pages": [ + "docs/features/agents/pipes-and-lan", + "docs/features/agents/acp" + ] + }, + { + "group": "外部接入", + "pages": [ + "docs/features/external/channels", + "docs/features/external/chrome-control", + "docs/features/external/computer-use", + "docs/features/external/voice-mode", + "docs/features/external/web-browser-tool" + ] + }, + { + "group": "运行模式", + "pages": [ + "docs/features/modes/auto-dream", + "docs/features/modes/remote-control-self-hosting" + ] + }, + { + "group": "工具与体验", + "pages": ["docs/features/tools/langfuse-monitoring"] + } + ] + }, + { + "group": "内部机制", + "pages": [ + "docs/internals/growthbook-adapter", + "docs/internals/sentry-setup" + ] + } + ] + } } diff --git a/docs.md b/docs.md new file mode 100644 index 000000000..73138bd38 --- /dev/null +++ b/docs.md @@ -0,0 +1,32 @@ +# Claude Code Best 文档大纲 + +> 自动生成自 docs.json 与各文档 frontmatter。共 3 个顶级分组。 + +## 1. 开始 + +- `getting-started/installation` — **安装 Claude Code Best** — 通过 NPM 一行命令安装 CCB,或从源码克隆构建。支持 macOS、Linux、Windows。 +- `getting-started/quickstart` — **快速上手** — 5 分钟掌握 CCB 的基本使用:启动会话、输入指令、审批工具调用、用斜杠命令管理状态。 +- `getting-started/model-providers` — **配置模型供应商** — 通过 /login 命令接入 OpenAI / Anthropic / Gemini / Grok 兼容协议,或直接用环境变量配置。支持 DeepSeek、GLM、OpenRouter、Bedrock 代理等任意兼容服务。 + +## 2. 核心功能 + +- ### 协作与多 Agent + - `features/agents/pipes-and-lan` — **群控:本机 + 局域网多实例协作** — 多台 CCB 实例零配置组网,同机用 UDS、跨机用 LAN,自动发现与消息路由。包含 /pipes 命令、心跳机制、消息路由详解。 + - `features/agents/acp` — **ACP 协议:接入 Zed / Cursor 等 IDE** — 通过 ACP(Agent Client Protocol)把 CCB 接入支持 ACP 的 IDE。本文包含 acp-link CLI 用法、权限桥接、以及 Zed 集成案例。 +- ### 外部接入 + - `features/external/channels` — **频道消息推送(Channels)** — MCP 服务器把飞书 / Slack / Discord / 微信等外部消息推到会话,`--channels plugin:name@marketplace` 启用。 + - `features/external/chrome-control` — **Chrome 浏览器控制** — 让 AI 用自然语言操作 Chrome 浏览器:导航、表单、数据抓取。两种实现方案对比:自托管 MCP(chrome-use-mcp)与 Chrome 原生集成(claude-in-chrome-mcp)。 + - `features/external/computer-use` — **屏幕控制(Computer Use)** — 截屏、键鼠控制,跨 macOS / Windows / Linux。本文包含快速上手、平台差异说明和工具参考。 + - `features/external/voice-mode` — **语音输入(Voice Mode)** — Push-to-talk 语音输入,支持豆包语言模型。需 Anthropic OAuth 或本地语音后端。 + - `features/external/web-browser-tool` — **浏览器操作工具** — 让 AI 控制 Chrome 完成网页操作:导航、点击、输入、抓取。 +- ### 运行模式 + - `features/modes/auto-dream` — **后台记忆整理(Auto Dream)** — 会话间自动审查、组织和修剪持久化记忆,确保未来会话快速获得准确上下文。 + - `features/modes/remote-control-self-hosting` — **Remote Control 私有化部署** — Docker 自托管 RCS,含 Web UI 控制面板、ACP agent 接入、JWT 认证。 +- ### 工具与体验 + - `features/tools/langfuse-monitoring` — **Langfuse 监控集成** — Agent loop 实时监控,可视化每次 API 调用、token 消耗、工具执行链路,可一键转化为训练数据集。 + +## 3. 内部机制 + +- `internals/growthbook-adapter` — **GrowthBook 适配器 - 自定义 Feature Flag 服务器接入** — 通过环境变量连接自定义 GrowthBook 服务器,实现远程 feature flag 控制。无配置时自动回退到代码默认值。 +- `internals/sentry-setup` — **自定义 Sentry 错误上报配置** — 通过环境变量连接自托管或 Cloud Sentry,实现 CLI 运行时的错误捕获与上报。不配置则完全静默。 + diff --git a/docs/agent/coordinator-and-swarm.mdx b/docs/agent/coordinator-and-swarm.mdx deleted file mode 100644 index db269a10c..000000000 --- a/docs/agent/coordinator-and-swarm.mdx +++ /dev/null @@ -1,651 +0,0 @@ ---- -title: "协调者与蜂群模式:多 Agent 编排机制" -description: "从源码角度拆解 Claude Code 的 Coordinator Mode、Agent Teams / Swarm、subagent、teammate、Mailbox、Task 工具、runtime task、状态恢复与排障路径。" -keywords: ["协调者模式", "蜂群模式", "Agent Swarm", "Agent Teams", "多 Agent 协作", "任务编排", "Mailbox", "Subagent"] ---- - -Claude Code 里有很多看起来都叫“多 Agent”的东西:`Agent` 工具、fork agent、Coordinator Mode、Agent Teams / Swarm、remote agent、后台 runtime task、`TaskCreate` 任务白板。它们共享部分底层设施,但不是同一个抽象。 - -这篇文档解决的是跨机制理解问题:当你看到一个任务被“派出去”、一个 teammate 变成 idle、一个 `` 回到主线程、一个 team 目录还在但 teammate 不跑了,应该知道它属于哪套机制、状态放在哪里、通信走哪条路、哪些东西能恢复。 - -## 全局心智模型 - -最短心智模型是: - -```text -Agent 是派人干活。 -TaskCreate 是往白板上贴任务卡。 -Runtime Task 是正在跑的人或远端人影。 -Coordinator 是星型编排器。 -Swarm 是有成员、有邮箱、有任务白板的团队。 -``` - -先把几个词压平: - -| 概念 | 本质 | 入口 | 状态位置 | 结果回路 | -|---|---|---|---|---| -| 普通 sync subagent | 一次性前台 `Agent` tool call | `Agent({ subagent_type })` | foreground `LocalAgentTask` | 当前 turn 的 `tool_result` | -| 普通 async subagent | 一次性后台 agent | `Agent({ subagent_type, async: true })` 或自动后台化 | `AppState.tasks` + sidechain | `async_launched` + `` | -| fork agent | 继承父上下文和 exact tools 的后台分支 | 省略 `subagent_type` 且 fork gate 满足 | `LocalAgentTask` + `.meta.json` | `` | -| coordinator worker | Coordinator 派出的 `worker` async subagent | Coordinator 调 `Agent({ subagent_type: "worker" })` | `LocalAgentTask` | `` + `SendMessage(to: agentId)` | -| swarm teammate | 长生命周期团队成员 | `Agent({ name, team_name?, prompt })` | `InProcessTeammateTask` 或 pane member | mailbox by name,可 idle 后继续 | -| remote agent | 远端执行体的本地镜像 | `Agent(..., isolation: "remote")` | `RemoteAgentTask` + remote sidecar | CCR events / polling | -| work item task | 共享任务白板条目 | `TaskCreate/Update/List/Get` | `~/.claude/tasks//*.json` | teammate / lead 认领和更新 | -| runtime task | 正在运行或曾运行的后台执行体 | agent、shell、workflow、remote 等入口 | `AppState.tasks` | UI、spinner、resume、kill | - -## 系统分层 - -多 Agent 系统可以看成五层,每层回答一个问题: - -| 层 | 回答的问题 | 典型对象 | -|---|---|---| -| 入口层 | 用户或模型通过什么工具启动动作 | `/coordinator`、`AgentTool`、`TeamCreate`、`SendMessage`、`TaskUpdate` | -| 编排层 | 谁负责拆解、派发、控制和综合 | Coordinator、Team Lead、AgentTool routing | -| 运行层 | 谁真正执行或代表执行状态 | `LocalAgentTask`、`InProcessTeammateTask`、`RemoteAgentTask` | -| 通信层 | 结果和控制信号如何回流 | `tool_result`、``、mailbox、CCR events | -| 持久化层 | 进程重启后还能看见什么 | session JSONL、sidechain、team config、task files、inbox、sidecar meta | - -```mermaid -flowchart TD - A["入口层
slash command / AgentTool / Team tools / SendMessage"] --> B["编排层
Coordinator / Team Lead / AgentTool routing"] - B --> C["运行层
LocalAgentTask / RemoteAgentTask / InProcessTeammateTask"] - C --> D["通信层
tool_result / task-notification / mailbox / CCR events"] - D --> E["持久化层
session JSONL / sidechain / team config / tasks / inboxes / sidecar meta"] -``` - -这五层不是一一对应关系。Coordinator worker 在运行层是 `LocalAgentTask`,通信层靠 `` 和 `SendMessage(to: agentId)`;Swarm teammate 在运行层可能是 `InProcessTeammateTask`,通信层靠 mailbox;remote agent 在运行层是本地 `RemoteAgentTask` 镜像,真实执行状态来自 CCR。 - -## 什么时候用哪套机制 - -| 场景 | 推荐机制 | 为什么 | -|---|---|---| -| 需要一个主脑拆解、派发、综合、纠偏 | Coordinator Mode | 主线程被限制为编排器,减少直接上手乱改。 | -| 多个任务相对独立,需要长期队友持续领任务 | Agent Teams / Swarm | 有 team config、mailbox、shared task list。 | -| 只想派一个专家研究或修改 | 普通 subagent | 成本低、模型路径短、结果直接回当前 turn 或后台通知。 | -| 想复制当前上下文做并行探索 | fork agent | 继承父上下文和 exact tools,适合分支探索。 | -| 想把工作放到远端环境执行 | remote agent | 本地只保留 `RemoteAgentTask` 镜像,执行在 CCR。 | - -两个常见误判: - -| 误判 | 更好的选择 | -|---|---| -| “我要并行,所以一定用 Swarm” | 如果只是一次性研究/验证,用 async subagent 或 Coordinator worker 更轻。 | -| “我要团队,所以 Coordinator 就够了” | 如果需要成员持续认领共享任务、互相发消息、保留 team 状态,用 Swarm。 | - -## 两种多 Agent 拓扑 - -Coordinator 和 Swarm 都是多 Agent,但控制权和状态模型完全不同。 - -```mermaid -flowchart LR - subgraph CoordinatorMode["Coordinator Mode"] - U1["用户"] --> C["Coordinator 主 Claude"] - C -->|Agent worker| W1["worker A
LocalAgentTask"] - C -->|Agent worker| W2["worker B
LocalAgentTask"] - W1 -->|task-notification| C - W2 -->|task-notification| C - C -->|SendMessage to agentId| W1 - end - - subgraph SwarmMode["Agent Teams / Swarm"] - U2["用户"] --> L["Team Lead"] - L --> TF["TeamFile config.json"] - L --> TB["Shared TaskList"] - L -->|Agent name| T1["teammate researcher"] - L -->|Agent name| T2["teammate tester"] - T1 <--> M1["Mailbox inbox JSON"] - T2 <--> M2["Mailbox inbox JSON"] - T1 --> TB - T2 --> TB - end -``` - -| 维度 | Coordinator Mode | Agent Teams / Swarm | -|---|---|---| -| 拓扑 | 星型:Coordinator 居中,worker 外围 | 团队型:Team Lead + named teammates + mailbox + task list | -| 主 Claude 角色 | 只编排,不直接执行 | 可以直接执行,也可以作为 team lead 管理团队 | -| 执行者 | built-in `worker` async subagent | teammate,可能是 in-process,也可能是 pane-based | -| 通信方式 | ``,必要时 `SendMessage(to: agentId)` | mailbox by name,支持 P2P、broadcast、structured protocol | -| 任务协作 | 不以 `TeamCreate/TaskList` 为核心 | `TeamFile` + shared task list + mailbox | -| 恢复模型 | mode 在主 transcript,worker 是 local agent sidechain | team/task/inbox 文件可保留;in-process runner 不完整恢复 | - -Coordinator Mode 不是 Swarm 的特殊 Team Lead。它共享 `AgentTool`、`LocalAgentTask`、`SendMessage` 等设施,但不使用 `TeamCreate/TeamDelete/TaskList/TaskUpdate` 作为核心团队协作机制。 - -## Coordinator Mode 五段状态机 - -Coordinator Mode 的核心设计是把主 Claude 降级为编排器:主线程不直接 `Read/Edit/Bash`,而是拆任务、派 worker、综合结果、必要时停止或继续 worker。 - -### 1. 启用状态机 - -```mermaid -flowchart TD - A["feature COORDINATOR_MODE?"] -->|no| B["Coordinator unavailable"] - A -->|yes| C["/coordinator command"] - C --> D{"target mode?"} - D -->|enable| E["set CLAUDE_CODE_COORDINATOR_MODE=1"] - D -->|disable| F["delete CLAUDE_CODE_COORDINATOR_MODE"] - E --> G["save mode metadata"] - F --> G - G --> H["inject mode reminder"] -``` - -两层条件都满足才算进入 Coordinator: - -| 条件 | 作用 | -|---|---| -| `feature("COORDINATOR_MODE")` | 构建/运行 feature gate。 | -| `CLAUDE_CODE_COORDINATOR_MODE=1` | 当前进程实际进入 coordinator。 | - -### 2. 恢复状态机 - -Coordinator mode 是会话属性,写在主 session JSONL 的 `mode` entry 中: - -```jsonl -{"type":"mode","sessionId":"...","mode":"coordinator"} -``` - -resume 时会把当前环境和 transcript 中的 mode 对齐: - -```mermaid -flowchart TD - A["load transcript mode metadata"] --> B{"env matches transcript mode?"} - B -->|yes| C["continue"] - B -->|no, transcript=coordinator| D["set CLAUDE_CODE_COORDINATOR_MODE=1"] - B -->|no, transcript=normal| E["delete CLAUDE_CODE_COORDINATOR_MODE"] - D --> F["emit warning + refresh agent definitions"] - E --> F -``` - -这避免用户在 normal 环境恢复 coordinator 会话,或反过来把普通会话误当 coordinator 运行。 - -### 3. Prompt 状态机 - -Coordinator prompt 不是只看 env。交互 REPL 侧大致优先级是: - -| 优先级 | 来源 | 说明 | -|---|---|---| -| 1 | override system prompt | 最高优先级。 | -| 2 | coordinator prompt | `isCoordinatorMode()` 且没有 `mainThreadAgentDefinition` 时使用。 | -| 3 | main-thread agent prompt | `--agent` / settings agent。 | -| 4 | custom/default prompt | 普通主线程 prompt。 | -| 5 | append prompt | 追加型补充。 | - -风险点是 `--agent` 和 Coordinator 混用:可能出现工具池已经按 coordinator 过滤,但 system prompt 不是 coordinator 的不一致。 - -Headless 也要单独看。当前 headless 路径明确做了 coordinator 工具过滤,并注入 coordinator user context;但 system prompt 组装路径和交互 REPL 不完全相同,应把它当成需要复核的边界,而不是默认等同交互路径。 - -### 4. 工具过滤状态机 - -Coordinator 主线程和 worker 的工具池不同: - -| 角色 | 工具池 | 设计目的 | -|---|---|---| -| Coordinator 主线程 | `Agent`、`SendMessage`、`TaskStop`、`SyntheticOutput`、PR activity 订阅类 MCP 工具 | 只编排,不直接执行。 | -| worker | `ASYNC_AGENT_ALLOWED_TOOLS`,排除 `TeamCreate`、`TeamDelete`、`SendMessage`、`SyntheticOutput` | 执行任务,但不能继续嵌套编排。 | -| simple mode worker | `Bash`、`Read`、`Edit` | 降低工具面,适合简单执行路径。 | -| MCP 工具 | 按已连接 server 注入 worker context | 让 worker 能使用外部能力,但由工具池控制边界。 | -| scratchpad | gate 开启时提供 scratchpad 目录 | 允许跨 worker 共享临时知识。 | - -交互路径主要走 `mergeAndFilterTools()`;headless 路径会在主入口直接应用 coordinator 工具过滤;worker 工具池由 `AgentTool` 独立组装,不继承主线程被过滤后的工具池。 - -### 5. Worker lifecycle - -Coordinator 下 `Agent(worker)` 会被强制异步: - -```mermaid -flowchart TD - A["Coordinator calls Agent(worker)"] --> B["AgentTool marks shouldRunAsync"] - B --> C["registerAsyncAgent"] - C --> D["runAsyncAgentLifecycle"] - D --> E{"final status"} - E -->|completed| F["enqueue completed task-notification"] - E -->|failed| G["enqueue failed task-notification"] - E -->|killed| H["enqueue killed task-notification"] - F --> I["command queue injects into next turn"] - G --> I - H --> I -``` - -`` 是 user-role message,但不是用户输入。Coordinator prompt 必须把它当成 worker 结果信号: - -```xml - - agent-a1b - completed|failed|killed - Agent "Investigate auth bug" completed - Found null pointer in src/auth/validate.ts:42... - - N - N - N - - -``` - -Coordinator 的关键约束是“综合而不是转发”。worker 看不到用户和 coordinator 的完整对话,所以 prompt 必须自包含: - -```text -Fix the null pointer in src/auth/validate.ts:42. -Session.user can be undefined when the session expires but the token remains cached. -Add a null check before user.id access; if null, return 401 with "Session expired". -Run validate.test.ts and report the commit hash. -``` - -反模式是: - -```text -Based on your findings, fix it. -``` - -### Coordinator 边界与排错 - -| 现象 | 可能原因 | 处理方式 | -|---|---|---| -| Coordinator 主线程不能读文件或跑命令 | 工具池被过滤,这是预期行为 | 派 `worker`,把文件、错误、验收标准写入 worker prompt。 | -| `--agent` 后 coordinator 行为不一致 | agent prompt 优先级压过 coordinator prompt,但工具仍可能被过滤 | 避免混用,或确认当前 system prompt 来源。 | -| worker 还在跑但方向错 | runtime task 仍是 `running` | 用 `TaskStop` 停止;会产生 `killed` notification。 | -| worker 完成但结论不够 | 已经结束的一次性 async agent | 更推荐 fresh worker;只有需要保留 sidechain 时才 `SendMessage` 续跑。 | -| `SendMessage` 失败 | 找不到 agent、缺 sidechain transcript、message 缺 `summary` | 查 agentId/name、sidechain `.jsonl/.meta.json`,plain text message 记得带 `summary`。 | -| coordinator 下没有 `worker` | non-interactive 下禁用了 built-in agents | 检查 `CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS`。 | - -## Swarm 完整状态机 - -Swarm 的核心是团队,而不是一次 `Agent` 调用。`TeamCreate` 建 team,`Agent({ name })` 加 teammate,`TaskCreate/Update/List/Get` 提供任务白板,`SendMessage` 和 mailbox 提供通信与控制。 - -当前实现默认启用 Agent Teams;设置 `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS_DISABLED` 才会关闭。 - -### 团队生命周期 - -```mermaid -flowchart TD - A["NoTeam"] -->|TeamCreate| B["TeamReady leader"] - B -->|AgentTool name + team| C["SpawnResolving"] - C --> D{"backend"} - D -->|in-process| E["InProcessTeammateTask registered"] - D -->|pane-based| F["terminal pane spawned"] - E --> G["TeamMemberRegistered"] - F --> G - G --> H["TeammateRunning"] - H -->|turn complete| I["IdleNotification"] - I --> J["TeammateIdle"] - J -->|mailbox message| H - J -->|unowned unblocked task| K["claim task + TaskUpdate in_progress"] - K --> H - H -->|shutdown_request| L["model approves or rejects"] - J -->|shutdown_request| L - L -->|approved| M["cleanup member / unassign task"] - L -->|rejected| J - B -->|TeamDelete| N["request active teammate shutdown"] - N --> O["wait optional wait_ms"] - O --> P["cleanup team dir / task dir / AppState"] - P --> A -``` - -关键不变量: - -| 不变量 | 含义 | -|---|---| -| roster 扁平 | teammate 内禁止再 spawn teammate,避免团队嵌套。 | -| mailbox 按 name 寻址 | inbox 路径是 `teamName + agentName`,不是 agentId。 | -| task list 是共享白板 | `TaskCreate` 只写 pending task,不启动执行体。 | -| shutdown 不是强杀 | shutdown request 会交给模型处理,approve 后才 graceful shutdown。 | -| TeamFile 是跨进程事实源 | `AppState.teamContext` 是 leader UI 的投影。 | - -### 存储拓扑 - -Swarm 的核心状态在 `~/.claude/teams` 和 `~/.claude/tasks`: - -```text -~/.claude/ - teams/ - / - config.json - inboxes/ - .json - tasks/ - / - .highwatermark - 1.json - 2.json - ... -``` - -| 文件或结构 | 内容 | -|---|---| -| `TeamFile` | `name`、`leadAgentId`、`leadSessionId`、`hiddenPaneIds`、`teamAllowedPaths`、`members[]`。 | -| `TeamFile.members[]` | `agentId`、`name`、`agentType`、`model`、`color`、`backendType`、`isActive`、`mode`、`worktreePath`、`sessionId`。 | -| task JSON | `id`、`subject`、`description`、`activeForm`、`owner`、`status`、`blocks`、`blockedBy`、`metadata`。 | -| mailbox JSON | 普通消息、协议消息、已读状态、颜色和摘要等。 | - -### TeamCreate 到 teammate 的链路 - -```mermaid -sequenceDiagram - participant L as TeamLead - participant TC as TeamCreate - participant TF as TeamFile - participant TL as TaskList - participant A as AgentTool - participant B as Backend - participant M as Mailbox - - L->>TC: create team - TC->>TF: write config with lead member - TC->>TL: reset task list - TC->>L: set leader team context - L->>A: Agent with teammate name - A->>B: spawn in-process or pane - B->>TF: append member - B->>M: write initial prompt if needed - B->>L: teammate spawned -``` - -`TeamCreate` 不只是写 `config.json`。它还会注册 session cleanup、重置 team 对应 task list、设置 `leaderTeamName`,并把 leader 投影到 `AppState.teamContext`。 - -`AgentTool` 遇到 `team_name/current teamContext + name` 时走 teammate spawn 分支,不走普通 `runAgent()`。`spawnTeammate()` 会解析 team、唯一化 name、选择 backend、更新 `AppState.teamContext.teammates`,再追加 `TeamFile.members`。 - -### in-process vs pane-based teammate - -| 维度 | in-process teammate | pane-based teammate | -|---|---|---| -| 运行位置 | leader 同进程 | 独立终端 pane / CLI 进程 | -| 启动方式 | 注册 `InProcessTeammateTask`,启动 `runInProcessTeammate()` | 创建 tmux / iTerm2 / Windows Terminal pane | -| 消息消费 | runner 自己约 500ms poll mailbox | leader / teammate 侧 `useInboxPoller()` 约 1s poll | -| 输入路径 | teammate view 输入进入 `pendingUserMessages` | 普通 mailbox prompt 进入 teammate 进程 | -| 处理优先级 | shutdown > team-lead message > peer message > unowned task claim | poller 按消息类型路由,空闲时自动开一轮 | -| UI | spinner tree、footer pills、detail dialog、teammate transcript view | footer TeamStatus、TeamsDialog、pane 状态 | -| 恢复 | runner、AbortController、pending queue 在内存,进程重启不能完整恢复 | pane 进程可能还在;leader 侧 backend map 不持久化,恢复是 best-effort | -| 删除 | 需要当前 AppState task / AbortController | 通过 backend 写 shutdown request,等待 teammate approve / cleanup | - -## AgentTool 分流决策树 - -`AgentTool.call()` 是多 Agent 入口最复杂的分叉点。同一个 `Agent` 工具会根据参数和上下文走不同运行时: - -```mermaid -flowchart TD - A["AgentTool.call"] --> B{"name + team context?"} - B -->|yes| C["spawnTeammate"] - B -->|no| D{"isolation=remote?"} - D -->|yes| E["registerRemoteAgentTask"] - D -->|no| F{"fork route?"} - F -->|yes| G["register async LocalAgentTask as fork"] - F -->|no| H{"shouldRunAsync?"} - H -->|yes| I["register async LocalAgentTask"] - H -->|no| J["foreground LocalAgentTask + tool_result"] -``` - -| 路由 | 触发条件 | 结果 | -|---|---|---| -| teammate | 有 `name`,且存在 `team_name` 或当前 `teamContext` | `spawnTeammate()`,返回 `teammate_spawned`。 | -| remote | `isolation: "remote"` | 注册 `RemoteAgentTask`,本地保存 remote sidecar。 | -| fork | 省略 `subagent_type` 且 fork gate/上下文允许 | 强制后台 local agent,继承父上下文和 exact tools。 | -| async local | 显式 async、Coordinator worker、或自动后台条件满足 | 返回 `async_launched`,完成后注入 ``。 | -| sync local | 默认前台一次性 subagent | 当前 tool call 返回 `tool_result`。 | - -所以文档里不能把“Agent”写成一个单一概念:同一个工具入口下面至少有五条运行路径。 - -## 通信路径对照 - -多 Agent 的通信路径决定了结果是否进入当前 turn、是否持久化、能不能 resume。 - -| 通信路径 | 发送者 | 接收者 | 用途 | 持久化/恢复 | -|---|---|---|---|---| -| `tool_result` | sync subagent | 当前 assistant turn | 一次性前台结果 | 写入主 transcript。 | -| `` | async local agent / coordinator worker | 主线程下一 turn | 后台完成/失败/被杀通知 | 来自 `LocalAgentTask` lifecycle 和 sidechain。 | -| `SendMessage(to: agentId)` | Coordinator 或用户 | local agent task | 继续 running/stopped worker | running 时排队;stopped 时尝试 sidechain resume。 | -| `SendMessage(to: teammateName)` | lead / teammate | teammate mailbox | Swarm 普通通信 | 写 inbox JSON,按 name 寻址。 | -| `SendMessage(to: "*")` | lead / teammate | team members | Swarm broadcast | 写多个 inbox;structured message 不能 broadcast。 | -| structured mailbox protocol | lead / teammate / runtime | 特定 teammate 或 lead | permission、plan、shutdown、mode、task assignment | 保持 unread 给 poller 路由,不应被普通 attachment 吞掉。 | -| CCR events / polling | remote runtime | `RemoteAgentTask` | remote agent 状态和结果 | 本地 sidecar + 远端 session 状态。 | - -### SendMessage 路由 - -```mermaid -flowchart TD - A["SendMessage(to)"] --> B{"cross-session scheme?"} - B -->|yes| C["UDS / LAN / bridge plain text"] - B -->|no| D{"matches LocalAgentTask?"} - D -->|running| E["queuePendingMessage"] - D -->|stopped or evicted| F["resumeAgentBackground from sidechain"] - D -->|no| G{"to == * ?"} - G -->|yes| H["broadcast team mailbox"] - G -->|no| I{"structured protocol?"} - I -->|yes| J["write protocol message"] - I -->|no| K["write teammate mailbox"] -``` - -plain text `SendMessage` 要带 `summary`。structured message 不能 broadcast,也不能跨 `uds/bridge/tcp` session。单 session 下 teammate name 是裸 name,`to` 不应写成含 `@` 的跨域地址。 - -## Mailbox 协议表 - -Mailbox 路径是: - -```text -~/.claude/teams//inboxes/.json -``` - -它有 lock、原子 rename、大小上限和压缩策略: - -| 限制 | 值 | -|---|---| -| 单条 text | 64KB | -| mailbox 文件 | 4MB | -| retained bytes | 2MB | -| 普通 message 保留 | 最多 1000 条 | -| read message 保留 | 最多 200 条 | -| unread protocol message 保留 | 最多 2000 条 | - -协议消息不只是“聊天”: - -| 消息类型 | 典型发送者 | 典型接收者 | 消费者 | 是否应进入普通 LLM context | -|---|---|---|---|---| -| plain text | lead / teammate | teammate / lead | mailbox attachment 或 prompt handler | 是 | -| broadcast | lead / teammate | team members | mailbox attachment 或 prompt handler | 是 | -| `task_assignment` | `TaskUpdate` | new owner | teammate poller / runner | 通常作为任务触发,不应当成普通闲聊 | -| `permission_request/response` | teammate / lead | lead / teammate | `useInboxPoller` + permission UI queue | 否 | -| `sandbox_permission_request/response` | teammate / sandbox host | lead / teammate | permission sync | 否 | -| `plan_approval_request/response` | teammate / lead | lead / teammate | plan approval path | 否 | -| `shutdown_request/approved/rejected` | lead / teammate | teammate / lead | backend / runner / poller | 否 | -| `mode_set_request` | lead | teammate | permission mode sync | 否 | -| `team_permission_update` | lead | team members | permission sync | 否 | -| idle notification | teammate runner | lead | UI / lead poller | 通常否 | - -一个重要边界:mailbox attachment 只消费非结构化消息;结构化协议消息应保持 unread,交给 `useInboxPoller` 或 in-process runner 路由。否则权限、plan、shutdown 可能被当成普通上下文吞掉。 - -## Task 不是 Runtime Task - -`TaskCreate` 的 task 和 `LocalAgentTask` 的 task 是两套模型。 - -| 名称 | 源码类型 | 存储 | 状态 | 谁消费 | -|---|---|---|---|---| -| work item task | `src/utils/tasks.ts` 的 `Task` | `~/.claude/tasks//.json` | `pending/in_progress/completed` | Task tools、TaskList UI、teammate 认领 | -| runtime task | `TaskStateBase` 子类型 | `AppState.tasks`,部分有 sidecar/output | `running/completed/failed/killed` 等 | UI、spinner、background selector、kill/resume | - -共享任务生命周期: - -```mermaid -flowchart TD - A["TaskCreate"] --> B["pending task JSON"] - B --> C["TaskList"] - C --> D["Teammate chooses work"] - D --> E["TaskUpdate status=in_progress owner=me"] - E --> F["execute work"] - F --> G["TaskUpdate status=completed"] - G --> H["TaskCompleted hooks"] - G --> I["tool_result hints: call TaskList for next task"] -``` - -`TaskUpdate` 在 Swarm 下有增强: - -| 行为 | 说明 | -|---|---| -| teammate 标记 `in_progress` 且 owner 为空 | 自动把 owner 设为当前 teammate name。 | -| owner 变化 | 写 `task_assignment` 到新 owner mailbox。 | -| status -> `completed` | 执行 TaskCompleted hooks。 | -| teammate 完成任务 | tool result 追加提示:立刻 `TaskList` 找下一项。 | -| 主线程完成 3+ 任务且没有 verification | 在 feature gate 下追加 verification nudge。 | - -runtime task 类型包括: - -| 类型 | 运行位置 | 典型场景 | -|---|---|---| -| `LocalAgentTask` | 本地子 agent | 普通后台 agent、fork、coordinator worker。 | -| `InProcessTeammateTask` | 同进程 runner | in-process teammate。 | -| `RemoteAgentTask` | CCR remote session | remote agent。 | -| `LocalShellTask` | 本地 shell | 后台 shell。 | -| `LocalWorkflowTask` | 本地 workflow | workflow 编排。 | -| `DreamTask` | 后台静默 | memory dream。 | -| `MonitorMcpTask` | 本地监控 | MCP monitor。 | - -## 持久化与恢复矩阵 - -恢复能力取决于状态放在哪里。最重要的区别是:能看到状态不等于能继续运行。 - -| 机制 | 持久化 | resume 后能看到 | resume 后能继续跑 | 边界 | -|---|---|---|---|---| -| main session | 主 session JSONL | 对话链、metadata、mode | 是,按主会话恢复 | 受 compact/branch/leaf 影响。 | -| coordinator mode | 主 session JSONL 的 `mode` entry | 当前会话模式 | 是,`matchSessionMode()` 会切 env | prompt/tool 状态仍受当前启动参数影响。 | -| coordinator worker | local agent sidechain + `.meta.json` | agent task 身份和历史 | 通常可 `resumeAgentBackground()` | 缺 sidechain/meta 或工具定义变化会失败。 | -| ordinary/fork subagent | local agent sidechain + `.meta.json` | agent 历史 | 可恢复,fork 依赖 `agentType:"fork"` | fork 恢复需要 metadata 正确。 | -| remote agent | `remote-agents/remote-agent-.meta.json` + CCR | remote task 镜像 | 取决于 CCR session 状态 | 404/archive 会删除 sidecar。 | -| team config | `~/.claude/teams//config.json` | team/member roster | 不代表 teammate runner 还活 | `TeamFile` 是事实源,`AppState` 是投影。 | -| mailbox | `~/.claude/teams//inboxes/*.json` | 未读普通/协议消息 | 可继续投递 | structured message 需要 poller/runner 正确消费。 | -| shared tasks | `~/.claude/tasks//*.json` | task list / owner / status | 可继续认领/更新 | owner 可能指向已经不活跃的 teammate。 | -| in-process teammate runner | leader 进程内存 | 不能完整看到 runner 内态 | 不能完整跨进程恢复 | AbortController、pending queue、recent messages 都在内存。 | -| pane-based teammate | 外部 pane + transcript + team file | 可能仍可见 | best-effort | leader 侧 backend map 不持久化,active/kill 依赖 pane 状态。 | - -调试时可以按这个顺序问: - -1. 文件还在吗? -2. `AppState` 投影还在吗? -3. runtime task 还在 `running` 吗? -4. 通信通道还可用吗? -5. sidechain / inbox / remote sidecar 是否足够恢复? - -## 用户可见状态如何投影 - -UI 展示的是不同状态源的投影,不是单一真相。 - -| UI | 数据源 | 能说明什么 | 不能说明什么 | -|---|---|---|---| -| TaskListV2 | task files + `teamContext` | work item task、owner、状态 | owner 对应 teammate 一定还活。 | -| TeammateSpinnerTree | running in-process teammates | 当前 leader 进程内的 teammate 活动 | pane-based teammate 或历史 teammate 全部状态。 | -| TeammateSpinnerLine | `InProcessTeammateTaskState` | idle、approval、stopping、tool/token、最近消息 | 完整 transcript。 | -| BackgroundAgentSelector | backgrounded `LocalAgentTask` | 可选择的本地后台 agent | remote/shell/workflow/in-process teammate。 | -| agent transcript view | `viewingAgentTaskId` | local agent 或 in-process teammate 的可视化对话 | pane teammate 的完整外部进程状态。 | -| TeamsDialog / TeamStatus | `AppState.teamContext` + team file | 团队成员展示、管理、kill/shutdown/mode | runner 一定可恢复。 | - -pane-based team 主要通过 footer TeamStatus 和 TeamsDialog 管理:Enter 查看,`k` kill,`s` shutdown,`p` prune idle,Shift+Tab 切 permission mode。in-process teammate 的 transcript view 输入会进 `pendingUserMessages`,不是写 mailbox。 - -## 两条端到端场景 - -### 复杂 bug 用 Coordinator - -| 步骤 | 发生了什么 | 运行体 | 通信 | 持久化 | -|---|---|---|---|---| -| 1 | 用户提出复杂 bug | 主会话 | user message | main JSONL | -| 2 | Coordinator 拆成调查、实现、验证 | Coordinator 主线程 | `Agent(worker)` | main JSONL + task state | -| 3 | worker 异步执行 | `LocalAgentTask` | tool calls | sidechain JSONL | -| 4 | worker 完成 | `LocalAgentTask` | `` | notification queue / main turn | -| 5 | Coordinator 综合 root cause | 主线程 | assistant reasoning | main JSONL | -| 6 | 需要修正方向 | 同一个或新 worker | `SendMessage(to: agentId, summary, message)` 或 fresh `Agent` | sidechain / new sidechain | -| 7 | 汇总给用户 | 主线程 | assistant message | main JSONL | - -这个流程没有 `TeamCreate`,也不依赖 shared task list。 - -### 长期并行任务用 Swarm - -| 步骤 | 发生了什么 | 状态源 | 通信 | -|---|---|---|---| -| 1 | `TeamCreate({ team_name })` | `teams//config.json` + `tasks/` | tool result | -| 2 | `TaskCreate` 多个工作项 | task JSON | Task tools | -| 3 | `Agent({ name: "researcher" })` | TeamFile member + backend task/pane | initial prompt | -| 4 | teammate 认领任务 | task JSON owner/status | `TaskUpdate` | -| 5 | lead 发消息 | inbox JSON | `SendMessage(to: teammateName)` | -| 6 | teammate 完成一轮 | runner/poller 状态 | idle notification | -| 7 | teammate 继续领任务 | task list | `TaskList` / claim | -| 8 | `TeamDelete({ wait_ms })` | team/task dirs cleanup | shutdown request / response | - -这个流程里 team、task list 和 mailbox 是核心。teammate 输出不会自动给 lead;需要 `SendMessage` 或明确的协议消息。 - -## 失败与排障矩阵 - -| 现象 | 先查什么 | 常见原因 | 处理 | -|---|---|---|---| -| Coordinator worker 结果没回来 | `AppState.tasks[agentId]`、notification queue、sidechain | worker 仍 running、failed、被 killed、notification 尚未进入下一 turn | 等下一 turn;或看 sidechain / task status。 | -| `SendMessage(to: agentId)` 找不到 worker | agentId/name、sidechain `.jsonl/.meta.json` | agent 被 evict、metadata 缺失、传了 teammate name | 用正确 raw agentId;必要时新开 worker。 | -| `SendMessage(to: teammate)` 失败 | teamContext、team file、inbox path | teammate name 拼错、当前 session 无 team、用了含 `@` 地址 | 用当前 team 内裸 teammate name。 | -| plain text `SendMessage` 校验失败 | 参数 | 缺 `summary` | 补 `summary`。 | -| structured message 没生效 | inbox read 状态、poller | 被当普通 attachment 标 read,或 consumer 没跑 | 确认 structured message 保持 unread,poller/runner 活着。 | -| 任务不显示 | `leaderTeamName`、`getTaskListId()`、tasks dir | lead/teammate 指向不同 task list | 查 env/teamName/sessionId 优先级。 | -| task 被认领但没人执行 | task owner、team member active、runner/pane | owner teammate 不活跃或 runner 丢失 | 重新分配 owner,或重启 teammate。 | -| TeamDelete 拒绝清理 | `TeamFile.members[].isActive` | 仍有 active teammate | 先 graceful shutdown,或确认后手动清理。 | -| resume 后 team 在但 teammate 不跑 | team file、runner/pane 状态 | in-process runner 在旧进程内,不能恢复 | 重新 spawn teammate 或用现有 mailbox/task 重新编排。 | -| pane teammate 似乎还在但 UI 不准 | paneId、backendType、backend map | leader 侧 `spawnedTeammates` map 不持久化 | 以 TeamFile + pane 实际状态为准,best-effort 管理。 | -| permission/plan 卡住 | leader inbox、permission UI queue、protocol response | leader poller 没消费,或 response 没写回 | 查 `useInboxPoller` 和对应 inbox。 | -| remote agent resume 失败 | remote sidecar、CCR session | session 404 / archived | 接受 sidecar 清理,重新创建 remote agent。 | - -## 常见误区 - -| 误区 | 正确理解 | -|---|---| -| Coordinator 就是 Swarm 的 Team Lead | 不是。Coordinator worker 是 async subagent,不是 teammate。 | -| Swarm 必须设置 `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` | 当前实现默认启用;用 `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS_DISABLED` 关闭。 | -| `TaskCreate` 创建了一个运行中的 agent | 它只创建 work item JSON;运行体是 `LocalAgentTask` / `InProcessTeammateTask` 等。 | -| teammate 完成一轮后结果自动给 lead | 不一定。teammate 需要通过 `SendMessage` 沟通;runner 也会发送 idle notification。 | -| mailbox 按 agentId 寻址 | Swarm mailbox 按 teammate name 寻址。 | -| BackgroundAgentSelector 会列出所有后台任务 | 它只列 backgrounded `LocalAgentTask`,不列 remote/shell/workflow/in-process teammate。 | -| `TeamUpdate` 是一个工具 | 当前源码没有独立 `TeamUpdateTool`;团队成员更新分散在 spawn、teamHelpers、dialogs 中。 | -| `SyntheticOutput` 是 Swarm 内部通信工具 | 它主要用于结构化输出,不是 Team 协作核心。 | -| shutdown request 是强杀 | 不是,它是模型处理的 graceful shutdown 协议。 | -| in-process teammate 可以像 local agent 一样跨进程 resume | 不行,runner 运行态在内存中,进程重启后不能完整恢复。 | - -## 延伸阅读 - -这篇文档是跨机制总览。需要深入某条链路时,优先看专题文档: - -| 想深入 | 阅读 | -|---|---| -| `AgentTool` 参数、sync/async/fork、通知队列 | `docs/agent/sub-agents.mdx` | -| Task V2 数据模型、锁、高水位、owner、hooks | `docs/tools/task-management.mdx` | -| JSONL transcript、sidechain、compact、resume、remote sidecar | `docs/internals/session-transcript-persistence.md` | -| Coordinator feature 的单独说明 | `docs/features/coordinator-mode.md` | -| worktree 隔离 | `docs/agent/worktree-isolation.mdx` | - -## 源码入口索引 - -| 问题 | 从这里看 | -|---|---| -| coordinator mode 检测、恢复、prompt、context | `src/coordinator/coordinatorMode.ts` | -| `/coordinator` 命令 | `src/commands/coordinator.ts` | -| coordinator worker 定义 | `src/coordinator/workerAgent.ts` | -| system prompt 选择 | `src/utils/systemPrompt.ts` | -| coordinator 工具过滤 | `src/utils/toolPool.ts` | -| coordinator mode 持久化 | `src/utils/sessionStorage.ts` 的 `mode` entry / `saveMode()` | -| AgentTool 路由 | `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` | -| subagent query loop | `packages/builtin-tools/src/tools/AgentTool/runAgent.ts` | -| async local agent lifecycle | `packages/builtin-tools/src/tools/AgentTool/agentToolUtils.ts` | -| local agent runtime task | `src/tasks/LocalAgentTask/LocalAgentTask.tsx` | -| remote agent runtime task | `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx` | -| agent resume | `packages/builtin-tools/src/tools/AgentTool/resumeAgent.ts` | -| task stop | `packages/builtin-tools/src/tools/TaskStopTool/TaskStopTool.ts`、`src/tasks/stopTask.ts` | -| team gate | `src/utils/agentSwarmsEnabled.ts` | -| team file helpers | `src/utils/swarm/teamHelpers.ts` | -| TeamCreate | `packages/builtin-tools/src/tools/TeamCreateTool/TeamCreateTool.ts` | -| TeamDelete | `packages/builtin-tools/src/tools/TeamDeleteTool/TeamDeleteTool.ts` | -| spawn teammate | `packages/builtin-tools/src/tools/shared/spawnMultiAgent.ts` | -| in-process teammate spawn | `src/utils/swarm/spawnInProcess.ts` | -| in-process teammate runner | `src/utils/swarm/inProcessRunner.ts` | -| pane backend | `src/utils/swarm/backends/PaneBackendExecutor.ts` | -| teammate AsyncLocalStorage identity | `src/utils/teammateContext.ts` | -| mailbox | `src/utils/teammateMailbox.ts` | -| permission sync | `src/utils/swarm/permissionSync.ts` | -| SendMessage routing | `packages/builtin-tools/src/tools/SendMessageTool/SendMessageTool.ts` | -| shared task list | `src/utils/tasks.ts` | -| Task tools | `packages/builtin-tools/src/tools/TaskCreateTool`、`TaskUpdateTool`、`TaskListTool`、`TaskGetTool` | -| inbox polling | `src/hooks/useInboxPoller.ts` | -| swarm initialization | `src/hooks/useSwarmInitialization.ts` | -| teammate view | `src/state/teammateViewHelpers.ts`、`src/screens/REPL.tsx` | -| teammate spinner | `src/components/Spinner/TeammateSpinnerTree.tsx`、`TeammateSpinnerLine.tsx` | -| team dialog/status | `src/components/teams/TeamsDialog.tsx`、`src/components/teams/TeamStatus.tsx` | -| background local agent selector | `src/hooks/useBackgroundAgentTasks.ts`、`src/components/tasks/BackgroundAgentSelector.tsx` | diff --git a/docs/agent/sub-agents.mdx b/docs/agent/sub-agents.mdx deleted file mode 100644 index 1a3ed0ef3..000000000 --- a/docs/agent/sub-agents.mdx +++ /dev/null @@ -1,858 +0,0 @@ ---- -title: "子 Agent 机制 - 权限、流程、同步/异步与 Fork" -description: "从源码角度解析 Claude Code 子 Agent:AgentTool 的执行链路、权限模式、同步与异步生命周期、任务通知队列、AgentTool fork、slash command fork 与 runForkedAgent 的边界。" -keywords: ["子 Agent", "AgentTool", "权限模式", "同步子 Agent", "异步子 Agent", "forkSubagent", "runForkedAgent"] ---- - -{/* 本章目标:把子 Agent 的几条容易混淆的执行链路拆开说明,并给出源码入口。 */} - -## 先分清四个概念 - -Claude Code 里常被一起称为"子 Agent"的东西,其实有四类执行路径: - -| 类型 | 谁触发 | 是否经过 Tool 协议 | 结果怎么回来 | 典型入口 | -|------|--------|--------------------|--------------|----------| -| 命名子 Agent | 主模型调用 `Agent(...)`,并提供 `subagent_type` | 是,属于一次 `tool_use` | 当前 turn 的 `tool_result`,或后台完成后的 `` | `src/tools/AgentTool/AgentTool.tsx` | -| AgentTool fork | 主模型调用 `Agent(...)`,省略 `subagent_type`,且 fork gate 开启 | 是,仍然是 `Agent` 工具 | 先返回 `async_launched`,完成后通过任务通知回到主模型 | `src/tools/AgentTool/AgentTool.tsx`、`src/tools/AgentTool/forkSubagent.ts` | -| Slash command fork | 用户执行 `context: fork` 的 slash command / skill | 否,不是模型发出的 `Agent` tool_use | 普通模式同步返回命令输出;assistant 模式后台回注隐藏 prompt | `src/utils/processUserInput/processSlashCommand.tsx` | -| `runForkedAgent()` | 运行时内部服务直接分叉一条执行支线 | 否,内部 API | 调用方内部消费结果 | `src/utils/forkedAgent.ts` | - -一句话记忆: - -`AgentTool` fork 是给模型使用的工具语义;`runForkedAgent()` 是给运行时内部能力使用的实现细节;slash command fork 是 skill / command 的执行模式。 - -## AgentTool 主流程 - -模型看到的 `Agent` 工具最终会进入 `AgentTool.call()`。一条普通命名子 Agent 的执行链如下: - -```text -assistant message - -> tool_use: Agent({ prompt, subagent_type?, run_in_background?, ... }) - -> query.ts: runTools(...) - -> toolExecution.ts: await tool.call(...) - -> AgentTool.call(...) - -> resolve selectedAgent / fork path / permission mode / tool pool - -> runAgent(...) - -> finalizeAgentTool(...) - -> mapToolResultToToolResultBlockParam(...) - -> user message with tool_result - -> query.ts starts next model turn with that tool_result -``` - -关键源码入口: - -| 代码 | 作用 | -|------|------| -| `src/tools/AgentTool/AgentTool.tsx` | `Agent` 工具定义、路由、同步/异步生命周期 | -| `src/tools/AgentTool/runAgent.ts` | 子 Agent 的 query loop、system prompt、MCP、sidechain transcript | -| `src/services/tools/toolExecution.ts` | 外层工具执行器,`await tool.call(...)` 的地方 | -| `src/query.ts` | 主 agentic loop,收集 tool results 并进入下一轮模型调用 | -| `src/tasks/LocalAgentTask/LocalAgentTask.tsx` | 后台本地 Agent task 的注册、状态更新、完成通知 | - -## AgentTool 输入参数 - -`Agent` 工具的输入 schema 定义在 `AgentTool.tsx` 的 `baseInputSchema()` 和 `fullInputSchema()`。有些字段会被 feature gate 从模型可见 schema 中隐藏,但 `call()` 的实现会按统一的 `AgentToolInput` 类型处理这些可选字段。 - -### 基础参数 - -| 参数 | 类型 | 必填 | 作用 | 影响路径 | -|------|------|------|------|----------| -| `description` | `string` | 是 | 3-5 个词的任务短描述,用于 UI、任务列表、日志、后台通知和输出摘要 | 不参与子 Agent 的实际 prompt 推理,但会影响 task 展示和通知 | -| `prompt` | `string` | 是 | 子 Agent 要执行的完整任务说明 | 普通 agent 会变成子 Agent 的 user message;fork path 会嵌入 fork directive;remote path 会作为远程初始消息 | -| `subagent_type` | `string` | 否 | 指定命名 agent 类型 | 有值时走命名 agent;省略时 fork gate 开启则走 AgentTool fork,否则回退到 `general-purpose` | -| `model` | `'sonnet' \| 'opus' \| 'haiku'` | 否 | 这次调用的模型覆盖 | 普通命名 agent 中优先级高于 agent definition 的 `model`;coordinator mode 下忽略;fork path 继承父模型 | -| `run_in_background` | `boolean` | 否 | 请求后台运行 | 为 `true` 时走异步 task;如果后台任务被禁用或 fork gate 开启,这个字段会从 schema 中隐藏 | - -### 多 Agent / Teammate 参数 - -| 参数 | 类型 | 必填 | 作用 | 影响路径 | -|------|------|------|------|----------| -| `name` | `string` | 否 | 给 spawned agent 命名,使其可被 `SendMessage({ to: name })` 定向 | 与 `team_name` 或当前 team context 一起出现时触发 teammate spawn;普通后台子 Agent 中也会注册 `name -> agentId` 方便后续发送消息 | -| `team_name` | `string` | 否 | 指定要加入或使用的 team | 与 `name` 一起触发 `spawnTeammate()`;省略时可继承当前 `appState.teamContext.teamName` | -| `mode` | permission mode | 否 | teammate spawn 的权限模式提示 | 当前实现只用于 teammate 的 `plan_mode_required: spawnMode === 'plan'`;它不是普通本地子 Agent 的 `permissionMode` 覆盖 | - -`name + team_name` 是一条独立分支:它不会进入普通 `runAgent()` 本地子 Agent 路径,而是调用 `spawnTeammate()`,返回 `teammate_spawned`。如果在 teammate 内继续带 `name` spawn teammate,会被拒绝,因为 team roster 是扁平结构。 - -### 隔离与工作目录参数 - -| 参数 | 类型 | 必填 | 作用 | 影响路径 | -|------|------|------|------|----------| -| `isolation` | `'worktree'`,内部构建还支持 `'remote'` | 否 | 覆盖 agent definition 的隔离模式 | `worktree` 创建临时 git worktree;`remote` 委派到 CCR,直接返回 `remote_launched` | -| `cwd` | `string` | 否 | 指定子 Agent 的运行目录 | 仅在 `KAIROS` schema 中暴露;会通过 `runWithCwdOverride()` 改变文件和 shell 操作的 cwd | - -`isolation` 入参优先级高于 agent definition 里的 `isolation`。`cwd` 的 schema 文案要求不要和 `isolation: "worktree"` 同时使用;实现上如果两者同时出现,`cwd` 会优先成为运行目录,但仍可能创建 worktree,因此调用方应视为互斥参数。 - -### 参数可见性与实际效果 - -| 参数 | 可能不可见的情况 | 说明 | -|------|------------------|------| -| `run_in_background` | `DISABLE_BACKGROUND_TASKS` 生效,或 `isForkSubagentEnabled()` 为 true | fork gate 开启时所有 `AgentTool` spawn 都会被强制异步,所以不需要让模型再选择 | -| `cwd` | 非 `KAIROS` 构建 / 模式 | schema 会 omit 掉,但实现类型仍保留该字段 | -| `isolation: "remote"` | 非内部构建 | 外部构建只接受 `worktree` | -| `model` | coordinator mode 或 fork path | coordinator 会清空 model override;fork 需要继承父模型以保持请求前缀和行为一致 | - -### 参数与 agent definition 的优先级 - -| 配置项 | 调用参数 | agent definition | 最终规则 | -|--------|----------|------------------|----------| -| agent 类型 | `subagent_type` | 默认 / active agents | 显式 `subagent_type` 优先;省略时由 fork gate 决定 fork 或 `general-purpose` | -| 模型 | `model` | `selectedAgent.model` | 普通命名 agent 中调用参数优先;没有参数则用定义;再没有则继承父模型 | -| 后台运行 | `run_in_background` | `selectedAgent.background` | 任一为 true 都会异步;还有 coordinator、assistant、fork gate 等强制异步条件 | -| 隔离 | `isolation` | `selectedAgent.isolation` | 调用参数优先 | -| 权限模式 | 无本地覆盖参数 | `selectedAgent.permissionMode` | 普通子 Agent 用 definition 的 `permissionMode`,默认 `acceptEdits`;fork 使用 `bubble` | -| 工具集合 | 无调用参数 | `selectedAgent.tools` | 普通子 Agent 在 `runAgent()` 里按 definition 过滤;fork 使用父级 exact tools | - -## Agent Definition 字段 - -`AgentTool` 的调用参数只描述"这一次怎么 spawn"。真正决定 agent 默认能力的是 agent definition。自定义 agent 可以来自用户 / 项目目录、JSON 配置、插件或内置定义,核心字段最终都会归一到 `AgentDefinition`。 - -### 常用 frontmatter - -| 字段 | 类型 | 作用 | 运行时影响 | -|------|------|------|------------| -| `name` | `string` | agent 类型名 | 模型通过 `subagent_type` 匹配它;插件 agent 可能带命名空间前缀 | -| `description` | `string` | 使用场景说明 | 进入可用 agent 列表,帮助主模型选择 | -| `tools` | `string[]` | 允许的工具集合 | `runAgent()` 内经 `resolveAgentTools()` 过滤;`['*']` 表示全量可用工具 | -| `disallowedTools` | `string[]` | 禁用工具集合 | JSON agent 支持该字段,用于从允许集合中排除 | -| `prompt` | `string` | agent system prompt 主体 | 普通命名子 Agent 会用它构建自己的 system prompt | -| `model` | `string` | 默认模型 | 可被 `Agent({ model })` 覆盖;`inherit` 表示继承父模型 | -| `effort` | effort level 或 number | 推理努力级别 | 传给 agent 运行配置 | -| `permissionMode` | permission mode | 默认权限模式 | 普通子 Agent 工具池组装时使用;省略则默认 `acceptEdits` | -| `background` | `boolean` | 是否总是后台运行 | 为 true 时,即使调用参数没有 `run_in_background` 也走异步 | -| `isolation` | `'worktree'` / `'remote'` | 默认隔离模式 | 可被调用参数 `isolation` 覆盖 | -| `maxTurns` | positive integer | 最大 agentic turns | 传给 `query()`,防止子 Agent 无限循环 | -| `color` | agent color | UI 颜色 | 用于 grouped UI、任务面板、teammate 展示 | -| `memory` | `'user' \| 'project' \| 'local'` | 持久记忆作用域 | 在 system prompt 中追加 agent memory,并按 scope 读写目录 | - -示例: - -```md ---- -name: code-reviewer -description: Review a code change and find correctness risks -tools: - - Read - - Grep - - Glob -model: sonnet -permissionMode: acceptEdits -background: true -maxTurns: 8 -memory: project ---- - -You are a focused code reviewer. Prioritize bugs, regressions, and missing tests. -``` - -### MCP、Hooks、Skills - -| 字段 | 作用 | 说明 | -|------|------|------| -| `requiredMcpServers` | 启动前必须存在的 MCP server 模式 | `AgentTool.call()` 会等待 pending server,最长约 30 秒;没有可用工具则报错 | -| `mcpServers` | agent 专属 MCP server | `runAgent()` 初始化,生命周期跟随该子 Agent | -| `hooks` | agent 生命周期内注册的 hooks | `runAgent()` 会注册 frontmatter hooks;agent 停止时清理 session hooks | -| `skills` | 预加载 skill 名称 | `runAgent()` 会解析并注入对应 skill;插件 skill 支持命名空间或后缀匹配 | -| `initialPrompt` | 首个 user turn 前置内容 | 可用于启动时固定注入额外说明 | - -这些字段属于 agent definition,不是 `Agent(...)` 调用参数。调用方不能在一次 `Agent` tool_use 里临时传入 `tools`、`hooks` 或 `skills` 来覆盖 agent 定义。 - -### runAgent() 扩展点 - -`runAgent()` 不只是把 prompt 丢给模型。它会在进入 query loop 前后挂载一组 agent 级扩展点: - -| 扩展点 | 时机 | 作用 | -|--------|------|------| -| `SubagentStart` hooks | 子 Agent query loop 启动前 | 允许 hook 修改或补充启动上下文 | -| frontmatter `hooks` | agent session 初始化时注册 | 只在这个子 Agent 的 session 内生效,结束后清理 | -| preload `skills` | system prompt / skill 解析阶段 | 把指定 skill 的说明和资源注入 agent 可见上下文 | -| agent `memory` | system prompt 构建时 | 按 `user` / `project` / `local` scope 读取 agent memory,并追加到 agent prompt | -| sidechain transcript | query loop 运行时 | 记录子 Agent 的独立消息链,供恢复、调试和 `SendMessage` 续跑使用 | - -这些扩展点解释了为什么同样是 `runAgent()`,不同 agent definition 会表现出不同的工具边界、启动行为和长期上下文。 - -## 路由规则 - -`AgentTool.call()` 首先决定这次调用到底要跑哪一种 agent: - -```text -subagent_type 有值 - -> 使用命名 agent - -subagent_type 省略 && isForkSubagentEnabled() 为 true - -> 使用 fork agent - -subagent_type 省略 && fork gate 关闭 - -> 回退到 general-purpose -``` - -命名 agent 来自内置 agent、用户配置目录、插件 agent 等定义。fork agent 是代码里内置的特殊 agent,定义在 `forkSubagent.ts`,它不是普通专业角色,而是"继承父上下文的 worker"。 - -## 权限模型 - -子 Agent 权限要分成三层看:能不能启动这个 agent、这个 agent 有哪些工具、工具执行时如何处理权限请求。 - -### 启动权限 - -`AgentTool` 自身是一个工具调用,因此先经过普通工具权限系统。随后 `AgentTool.call()` 还会做 agent 级过滤: - -| 检查 | 说明 | -|------|------| -| `filterDeniedAgents()` | 根据权限规则过滤被禁止的 agent 类型 | -| `requiredMcpServers` | 如果 agent 声明必需 MCP server,会等待它们连接,失败或超时则停止 | -| teammate 限制 | in-process teammate 不能继续 spawn teammate,也不能 spawn 后台 agent | -| fork 递归保护 | fork worker 里不能再次 fork | - -被权限规则 deny 的命名 agent 会直接报错,而不是退回到别的 agent。这样可以避免模型绕过用户或配置里的拒绝规则。 - -### 工具池权限 - -普通命名子 Agent 不直接继承父 agent 当前那一轮的工具池限制。它会用自己的权限模式重新组装工具池: - -```ts -const workerPermissionContext = { - ...appState.toolPermissionContext, - mode: selectedAgent.permissionMode ?? 'acceptEdits', -} - -const workerTools = assembleToolPool( - workerPermissionContext, - appState.mcp.tools, -) -``` - -这里有几个重要含义: - -| 维度 | 行为 | -|------|------| -| 默认权限模式 | 如果 agent 定义没有写 `permissionMode`,默认使用 `acceptEdits` | -| 全局 allow / deny 规则 | 仍然来自 `appState.toolPermissionContext` | -| agent 自己的 `tools` 字段 | 在 `runAgent()` 内通过 `resolveAgentTools()` 继续过滤 | -| MCP 工具 | 来自当前 AppState 中已经连接的 MCP 工具;agent 也可以声明专属 MCP server | - -fork agent 是例外。它为了保持父子请求的 prompt cache 前缀一致,会使用父级 exact tools: - -```text -useExactTools: true -availableTools: toolUseContext.options.tools -``` - -因此 fork 的权限策略不是"重新组装工具池",而是"继承父工具定义,并用 `bubble` 权限模式把权限请求上浮到父终端"。 - -### 权限模式速览 - -| 模式 | 子 Agent 中的意义 | -|------|------------------| -| `acceptEdits` | 默认模式。通常允许读和编辑类安全路径,危险操作仍走权限系统 | -| `default` / 其他普通模式 | 按主权限系统规则询问或放行 | -| `bypassPermissions` | 显式危险模式,只有用户启用跳过权限时才应出现 | -| `bubble` | fork 专用思路:权限请求冒泡到父级会话处理 | - -## 同步子 Agent - -同步子 Agent 是默认路径:没有显式 `run_in_background: true`,agent 定义也没有 `background: true`,并且没有被 coordinator / assistant mode / fork gate 等机制强制异步。 - -同步等待发生在普通工具调用链里。外层 `toolExecution.ts` 会执行: - -```ts -const result = await tool.call(...) -``` - -如果这个工具是 `AgentTool`,那么 `AgentTool.call()` 会在内部跑完整个子 Agent: - -```text -AgentTool.call() - -> agentIterator = runAgent(...)[Symbol.asyncIterator]() - -> while true: - await agentIterator.next() - 收集 assistant / user 消息 - 转发 progress 给 UI / SDK - 如果 result.done,跳出 - -> finalizeAgentTool(agentMessages, ...) - -> return { data: { status: "completed", ...agentResult } } -``` - -返回后,`mapToolResultToToolResultBlockParam()` 把 `completed` 结果转成当前 turn 的 `tool_result`。然后 `query.ts` 把这个 tool result 放进消息列表,进入下一轮模型调用。 - -也就是说,同步子 Agent 不通过统一队列回注结果。主模型是在这次 `Agent` tool call 上等待,直到拿到最终 `tool_result` 才继续。 - -### 同步子 Agent 的可后台化 - -同步子 Agent 注册为 foreground task,因此它可以中途被后台化。循环里会同时等待下一条子 Agent 消息和后台化信号: - -```ts -const raceResult = await Promise.race([ - nextMessagePromise.then(result => ({ type: 'message', result })), - backgroundPromise, -]) -``` - -如果后台化信号先到,当前前台 iterator 会被清理,新的后台 `runAgent(..., isAsync: true)` 接管剩余工作。此时 `AgentTool.call()` 不再等待最终结果,而是返回 `async_launched`,后续完成结果走任务通知队列。 - -## 异步子 Agent - -异步子 Agent 的触发条件包括: - -| 条件 | 说明 | -|------|------| -| `run_in_background: true` | 模型显式要求后台运行 | -| agent 定义 `background: true` | 该 agent 总是后台运行 | -| coordinator mode | worker 统一异步,方便编排 | -| fork subagent gate 开启 | 当前实现会强制所有 `AgentTool` spawn 使用异步通知模型 | -| assistant / kairos mode | 避免同步子任务阻塞输入队列 | -| proactive active | 主动循环下也可能强制异步 | - -异步路径不会等待子 Agent 完成: - -```text -AgentTool.call() - -> registerAsyncAgent(...) - -> void runAsyncAgentLifecycle(...) - -> return { status: "async_launched", agentId, outputFile } -``` - -后台生命周期在 `runAsyncAgentLifecycle()` 中完成: - -```text -runAsyncAgentLifecycle() - -> for await message of runAgent(...) - -> updateAsyncAgentProgress(...) - -> finalizeAgentTool(...) - -> completeAsyncAgent(...) - -> enqueueAgentNotification(...) -``` - -异步 Agent 使用独立 `AbortController`。普通 ESC 取消主线程不会自动杀掉后台 Agent;后台 Agent 需要通过任务停止、bulk kill 或 task 管理命令显式结束。 - -## 完成通知与统一队列 - -后台 Agent 完成后,`enqueueAgentNotification()` 会生成一条 XML 形态的 ``: - -```xml - - ... - ... - ... - completed - Agent "..." completed - ... - ... - -``` - -这条消息通过 `enqueuePendingNotification({ mode: 'task-notification' })` 进入统一 command queue。 - -### 队列什么时候消费 - -| 场景 | 消费方式 | -|------|----------| -| REPL / TUI | `useQueueProcessor()` 订阅队列;当 query 空闲且没有本地 JSX UI 阻塞时,调用 `processQueueIfReady()` | -| CLI / SDK headless | `print.ts` 中的 `drainCommandQueue()` 在 turn 之间持续消费;如果还有后台任务运行,会继续等待并 drain 新通知 | -| 子 Agent 内部 | `query.ts` 会消费带有当前 `agentId` 的 `task-notification`,主线程只消费 `agentId === undefined` 的消息 | - -`task-notification` 最终会作为 user-role 消息或 attachment 进入下一轮模型上下文。模型因此能看到后台结果,并决定是否综合、继续行动或回复用户。 - -### 还有哪些消息走同一队列 - -统一队列不只用于后台 Agent。常见来源包括: - -| 来源 | mode | 用途 | -|------|------|------| -| 用户在当前 turn 未结束时继续输入 | `prompt` / `bash` | 排队到下一轮处理 | -| 后台 shell / monitor 结束或卡住提醒 | `task-notification` | 通知模型命令状态 | -| remote agent / ultraplan / ultrareview 完成 | `task-notification` | 把远程结果交给本地模型 | -| scheduled task / cron | `prompt` | 定时触发主模型任务 | -| Chrome / MCP channel 推送 | `prompt` | 外部系统主动注入消息 | -| hook 阻塞错误 | `task-notification` | 唤醒模型处理 stop hook 错误 | -| orphaned permission response | `orphaned-permission` | 处理工具权限回复比原请求更晚到达的情况 | - -队列优先级是 `now > next > later`。`enqueue()` 默认 `next`,`enqueuePendingNotification()` 默认 `later`,这样系统通知不会抢在用户输入前面。 - -## 继续通信与任务控制 - -后台子 Agent 返回 `async_launched` 后,主模型不应该直接假装已经知道最终答案。它有三种后续操作面:发消息、读输出、停止任务。 - -### SendMessage - -`SendMessage` 用来给运行中或曾经启动过的 agent 追加消息。它可以通过两种地址找到本地后台 agent: - -| 地址 | 来源 | 行为 | -|------|------|------| -| `name` | `Agent({ name, ... })` 注册到 `agentNameRegistry` | 先解析成 agentId,再发送 | -| raw `agentId` | `async_launched` 或 `completed` tool result 中返回 | 直接定位对应 task 或 transcript | - -发送 plain text message 时必须提供 `summary`,因为 UI 和权限摘要需要一个短描述。`to: "*"` 表示广播给 teammate team;结构化消息不能广播。 - -`SendMessage` 对本地后台 agent 的行为分三种: - -| 目标状态 | 行为 | 结果 | -|----------|------|------| -| task 仍在 `running` | 调用 `queuePendingMessage(agentId, message, ...)` | 消息进入该 task 的 `pendingMessages`,在子 Agent 下一次 tool round / loop 边界被投递 | -| task 已停止但还在 AppState | 调用 `resumeAgentBackground(...)` | 用这条消息把 agent 后台恢复运行,完成后仍通过通知回来 | -| task 已从 AppState 清掉 | 仍尝试 `resumeAgentBackground(...)` | 如果 sidechain transcript 还在,就从 transcript 恢复;否则返回失败 | - -这意味着 `SendMessage` 不是只能在 agent 正在跑时使用。隔了很久以后,只要调用方还知道 `name` 或 `agentId`,并且对应 transcript 没被清理,就可能恢复并继续这个 agent。反过来,如果 task 状态和 transcript 都没了,`SendMessage` 无法凭空重建上下文。 - -几个容易误会的点: - -| 点 | 说明 | -|----|------| -| running agent 不会立刻中断当前工具调用 | 消息先排进 `pendingMessages`,等 agent loop 到安全边界再处理 | -| stopped agent 会变成新的后台运行 | `resumeAgentBackground()` 返回 output file,之后靠完成通知回注 | -| `name` 只在注册还在时可靠 | name registry 是运行时状态;跨很久恢复时 raw `agentId` 更稳定 | -| cross-session send 有额外限制 | `bridge:` / `uds:` 地址只支持 plain text,且可能需要显式权限或连接状态 | - -### TaskOutput - -`TaskOutput` 是旧式读取后台任务输出的工具,当前 prompt 明确建议优先使用 `Read` 读取任务返回的 `output_file`。它仍然可用,主要行为如下: - -| 参数 | 行为 | -|------|------| -| `task_id` | 要读取的后台任务 id | -| `block: false` | 非阻塞读取当前状态和已有输出 | -| `block: true` | 等待任务完成,默认行为 | -| `timeout` | 阻塞等待的最大时长 | - -如果 `block: true` 等到任务完成,`TaskOutput` 会把 task 标记为 `notified`,避免再重复发送完成通知。因为这个工具已经 deprecated,新代码和模型提示都更推荐直接读 `output_file`。 - -### TaskStop - -`TaskStop` 停止运行中的后台任务。它接受 `task_id`,也兼容旧的 `shell_id`。校验规则很直接:任务必须存在且状态是 `running`,否则报错。 - -停止后会调用统一的 `stopTask()`,具体 task 类型再映射到各自 kill 逻辑,例如本地 agent 会 abort 自己的 `AbortController`,shell task 会停止进程,remote task 会走 remote 停止路径。 - -## 失败、取消与清理 - -子 Agent 的异常路径主要分同步和异步看。 - -### 同步路径 - -同步子 Agent 抛出 `AbortError` 时,`AgentTool.call()` 会把它继续抛给外层工具框架,主 turn 进入正常的中断处理。非 abort 错误会先记录;如果已经收集到 assistant 消息,会尽量 `finalizeAgentTool()` 返回部分结果,让主模型看到已有进展。如果完全没有 assistant 消息,则重新抛出错误。 - -同步 finally 会做这些清理: - -| 清理 | 作用 | -|------|------| -| 清空 background hint UI | 避免前台提示残留 | -| `stopForegroundSummarization()` | 停止前台摘要定时器 | -| `unregisterAgentForeground()` | 子 Agent 未后台化时,从 foreground task 注册表移除 | -| SDK task notification | 给 SDK / VS Code 面板发完成、失败或 stopped 事件 | -| `clearInvokedSkillsForAgent()` | 清理 agent 作用域 skill 状态 | -| `clearDumpState()` | 清理 dump/transcript 调试状态 | -| `cleanupWorktreeIfNeeded()` | 未后台化时清理或保留 worktree | - -### 异步路径 - -异步路径由 `runAsyncAgentLifecycle()` 兜住异常: - -| 情况 | 状态更新 | 通知 | -|------|----------|------| -| 正常完成 | `completeAsyncAgent(...)` | `enqueueAgentNotification(status: completed)` | -| `AbortError` | `killAsyncAgent(...)` | `enqueueAgentNotification(status: killed)`,带 partial result | -| 其他错误 | `failAsyncAgent(...)` | `enqueueAgentNotification(status: failed)`,带 error | - -代码会先更新 task 状态,再做 handoff classifier 或 worktree cleanup 这类可能较慢的附加工作。这个顺序很重要:`TaskOutput(block=true)` 等待的是 task 进入 terminal status,不能被后续分类器或 git 清理卡住。 - -通知也有防重机制。`enqueueAgentNotification()` 会先原子检查并设置 `task.notified`;如果已经通知过,就不再重复入队。 - -## AgentTool fork - -AgentTool fork 是 `Agent` 工具的一种特殊路由,不是普通命名 agent。 - -### Gate - -fork 默认关闭。需要构建/运行时启用 `FORK_SUBAGENT` feature,例如开发时显式设置: - -```powershell -$env:FEATURE_FORK_SUBAGENT='1'; bun run dev -``` - -即使 feature 打开,以下场景也会强制关闭: - -| 场景 | 原因 | -|------|------| -| coordinator mode | coordinator 已有自己的委派模型 | -| non-interactive session | pipe / SDK 场景下避免不可见的 fork 嵌套 | - -### 路径 - -```text -主模型 - -> Agent({ prompt }),没有 subagent_type - -> AgentTool.call() - -> isForkSubagentEnabled() - -> selectedAgent = FORK_AGENT - -> buildForkedMessages(...) - -> runAgent(... useExactTools: true, forkContextMessages: parent messages) - -> 注册 task / transcript / notification -``` - -fork 的目标是让多个 worker 共享父请求的 prompt cache 前缀。它会: - -| 维度 | fork 行为 | -|------|-----------| -| system prompt | 使用父级已经渲染好的 system prompt | -| 对话历史 | 传入父级完整 `toolUseContext.messages` | -| tools | 使用父级 exact tools,不重新过滤 | -| thinking config | 继承父级配置,避免 cache key 变化 | -| placeholder tool_result | 多个 fork 使用相同占位文本,只有最后 directive 不同 | -| 权限 | `permissionMode: 'bubble'` | - -这就是为什么 fork path 和普通 agent path 在 tool pool、prompt 构造、模型继承上都不同。 - -### 递归保护 - -fork worker 保留 `Agent` 工具是为了让工具定义字节和父级一致,但代码会拒绝 fork 内再次 fork: - -| 保护 | 说明 | -|------|------| -| `querySource === 'agent:builtin:fork'` | 直接识别当前已经在 fork worker 内 | -| `` 扫描 | 兜底识别 fork 指令已经存在于上下文 | - -fork worker 应该直接完成任务,而不是继续委派。 - -## Slash command fork - -slash command fork 是 skill / command 的执行模式。它由 skill frontmatter 控制: - -```md ---- -name: code-review -context: fork -allowed-tools: - - Read - - Grep - - Glob ---- -``` - -加载 skill 时,`frontmatter.context === 'fork'` 会被解析成 command 的 `context: 'fork'`。执行 slash command 时: - -```text -用户输入 /code-review - -> processSlashCommand(...) - -> command.context === 'fork' - -> executeForkedSlashCommand(...) - -> prepareForkedCommandContext(...) - -> runAgent(...) -``` - -普通交互模式下,`executeForkedSlashCommand()` 会同步跑完子 Agent,显示 progress UI,然后把结果作为本地命令输出返回给主对话。 - -assistant / kairos 模式下,它会 fire-and-forget:后台 runner 完成后,把结果包装成隐藏 prompt 重新放入 command queue。这样多个 scheduled task 不会在启动时串行阻塞用户输入。 - -## `runForkedAgent()` - -`runForkedAgent()` 是内部服务用的执行器,不暴露给模型,也不产生 `Agent` tool_result。 - -它的输入是 `cacheSafeParams`、`promptMessages`、`canUseTool` 等运行时对象,直接跑 query loop: - -```text -内部服务 - -> runForkedAgent({ promptMessages, cacheSafeParams, ... }) - -> createSubagentContext(...) - -> query(...) - -> 返回 ForkedAgentResult -``` - -常见调用方: - -| 调用方 | 用途 | -|--------|------| -| compact | 对话压缩 | -| extractMemories / sessionMemory | 记忆抽取和维护 | -| promptSuggestion / speculation | 提示建议和预测 | -| sideQuestion | 不打扰主上下文的临时问答 | -| agentSummary | 后台 agent 摘要 | -| autoDream | 后台记忆整合 | - -它和 AgentTool fork 的共同点是"分叉执行",但边界完全不同: - -| 维度 | AgentTool fork | `runForkedAgent()` | -|------|----------------|--------------------| -| 调用者 | 模型通过 `Agent` 工具调用 | 运行时服务直接调用 | -| 协议层 | 经过 Tool schema / tool_use / tool_result | 不经过 Tool 协议 | -| 可见性 | 主模型会先看到 `async_launched`,完成后看到通知 | 结果由内部调用方处理 | -| 主要目标 | 并行 worker + prompt cache 共享 | 内部辅助任务复用 query loop | - -## Worktree 隔离 - -`Agent` 工具支持 `isolation: "worktree"`。启用后,子 Agent 在临时 git worktree 中运行,适合实现型或实验型任务。 - -生命周期: - -| 阶段 | 行为 | -|------|------| -| 创建 | 使用 agent id 派生 slug,创建独立 worktree | -| CWD 覆盖 | `runWithCwdOverride(worktreePath, fn)` 让工具在 worktree 内执行 | -| fork + worktree | 额外注入路径翻译提示,提醒 worker 重新读取文件 | -| 清理 | 无变更则移除 worktree;有变更则保留并把路径返回给主模型 | - -如果 worktree 是 hook-based,代码会保留它,因为无法可靠判断 VCS 变更。 - -## 结果格式 - -`AgentTool.mapToolResultToToolResultBlockParam()` 根据状态返回不同 tool result: - -| 状态 | 结果 | -|------|------| -| `completed` | 子 Agent 输出内容,可附带 `agentId`、worktree 信息和 usage | -| `async_launched` | 后台 agent id、output file 路径、等待完成通知的说明 | -| `teammate_spawned` | teammate id、name、team name | -| `remote_launched` | remote task id、session URL、output file | - -同步子 Agent 的 `completed` 结果直接成为当前 `Agent` tool call 的 `tool_result`。异步子 Agent 的首次 tool result 是 `async_launched`,最终输出通过 `` 回到模型。 - -### 输出字段 - -| 状态 | 关键字段 | 说明 | -|------|----------|------| -| `completed` | `content`、`agentId`、`totalTokens`、`totalToolUseCount`、`totalDurationMs` | 同步子 Agent 的最终结果;普通 agent 会附带可继续通信的 `agentId` | -| `async_launched` | `agentId`、`description`、`prompt`、`outputFile`、`canReadOutputFile` | 后台 agent 已启动;最终结果稍后通过通知到达 | -| `teammate_spawned` | `teammate_id`、`name`、`team_name` | teammate 已启动,后续通过 mailbox / SendMessage 协作 | -| `remote_launched` | `taskId`、`sessionUrl`、`outputFile`、`description` | remote CCR agent 已启动,完成后走 remote task 通知 | - -一次性内置 agent 可以省略 `agentId` / `SendMessage` hint 和 usage trailer,避免把不会继续通信的信息塞进上下文。 - -### outputSchema 与 tool_result - -`AgentTool` 的 `outputSchema` 描述的是 `call()` 返回的结构化 data;`mapToolResultToToolResultBlockParam()` 再把这些 data 映射成模型实际看到的 `tool_result` 文本块。读代码时可以按这个顺序看: - -```text -AgentTool.call() - -> return { data: { status, ...fields } } - -> mapToolResultToToolResultBlockParam(data, toolUseID) - -> ToolResultBlockParam - -> query.ts 把 tool_result 放进下一轮消息 -``` - -四类结果的字段重点: - -| status | data 字段 | 模型可见信息 | -|--------|-----------|--------------| -| `completed` | `content`、`agentId`、usage、可选 worktree result | 子 Agent 最终输出;如果可继续通信,会提示可用 `SendMessage` | -| `async_launched` | `agentId`、`description`、`prompt`、`outputFile`、`canReadOutputFile` | 后台已启动;提示等待通知或读取 output file | -| `teammate_spawned` | `teammate_id`、`name`、`team_name` | teammate 已加入 team;后续通过 mailbox / `SendMessage` 协作 | -| `remote_launched` | `taskId`、`sessionUrl`、`outputFile`、`description` | remote task 已启动;本地模型等待 remote task notification | - -这里的 `status` 是结果分发的主轴。后面 catch / finally 中的 failed、killed、cleanup 逻辑不会改写已经返回的同步 `tool_result`;后台路径会通过 task state 和 notification 把终态再交给主模型。 - -## 生命周期状态机 - -把本地子 Agent 当成 task 看,核心状态可以这样理解: - -```text -AgentTool.call() - -> resolve route - -> create optional worktree - -> register foreground 或 register async task - -> runAgent() - -> completed / failed / killed - -> tool_result 或 task-notification - -> cleanup agent-scoped state -``` - -同步和异步的差别不在于是否调用 `runAgent()`,而在于谁等待 `runAgent()`: - -| 路径 | 谁等待 | 主模型什么时候继续 | -|------|--------|--------------------| -| 同步子 Agent | `AgentTool.call()` 自己 `for await` 子 Agent 消息流 | 子 Agent 完成并返回 `tool_result` 后 | -| 自动后台化 | 前台先等;超时后前台 iterator 退出,后台 lifecycle 接管 | `AgentTool.call()` 返回 `async_launched` 后 | -| 异步子 Agent | `runAsyncAgentLifecycle()` 在后台等 | 主模型收到 `async_launched` 后立即继续 | -| slash command fork 普通交互 | `executeForkedSlashCommand()` 等 | slash command 完成后 | -| slash command fork assistant / kairos | fire-and-forget 后台 runner 等 | 启动后主输入流程继续,完成后隐藏 prompt 回注 | -| `runForkedAgent()` | 内部调用方自己等 | 不进入主模型 tool_result 协议 | - -所以“同步子 Agent 怎么等完成”最短答案是:外层工具执行器 `await tool.call()`,而 `AgentTool.call()` 内部持续消费 `runAgent()` 的 async iterator,直到 iterator `done` 或异常。 - -## 等待与回注方式对照 - -子 Agent 结果回到主模型有三种主要机制: - -| 机制 | 适用路径 | 回注载体 | 是否阻塞当前 turn | -|------|----------|----------|-------------------| -| `tool_result` | 同步命名子 Agent | 当前 `Agent` tool_use 对应的 tool result | 是 | -| `` | 异步 / 后台本地 Agent、remote task、后台 shell 等 | 统一 command queue 中的 task notification | 否 | -| hidden prompt / command queue prompt | assistant / kairos 的 slash command fork、scheduled task 等 | queue 中的 prompt 类消息 | 否 | - -这里容易混淆的是:后台子 Agent 完成后不会“补写”原来的 `tool_result`。原来的 `Agent` tool call 已经返回了 `async_launched`;最终结果是新的一条队列消息,下一轮模型看到后再决定怎么整合。 - -## Progress、UI 与 Transcript - -子 Agent 有三条并行的“可观察输出”:给用户看的 progress、给模型看的最终结果、给系统恢复用的 transcript。 - -| 输出 | 同步路径 | 异步路径 | 用途 | -|------|----------|----------|------| -| progress UI | `AgentTool.call()` 消费子 Agent 消息时实时转发给 UI / SDK | `runAsyncAgentLifecycle()` 更新 task progress state | 让用户看到子 Agent 正在做什么 | -| output file | 同步路径也会写入 side output,方便调试和恢复 | 后台 task 的主要可读输出,`async_launched` 会返回路径 | 主模型可用 `Read(outputFile)` 查看 | -| sidechain transcript | `runAgent()` 记录独立消息链 | 同样记录,且用于后台恢复 | `SendMessage`、resume、debug、summary 都依赖它 | -| task state | foreground task 注册表记录同步运行状态 | LocalAgentTask 记录 running / completed / failed / killed | UI、`TaskOutput`、通知防重都看这里 | - -同步 progress 是“边跑边展示,最后一次性返回 tool_result”。异步 progress 是“边跑边写 task state,最后入队 task notification”。sidechain transcript 不等同于用户可见输出;它是系统用来重建 agent 上下文的消息日志。 - -## 典型调用示例 - -### 同步命名子 Agent - -```json -{ - "description": "review parser bug", - "prompt": "Review the parser changes and identify correctness risks.", - "subagent_type": "code-reviewer" -} -``` - -适合短任务或必须立即拿结果才能继续的任务。主模型会等到子 Agent 输出 `completed`。 - -### 后台命名子 Agent - -```json -{ - "description": "run regression suite", - "prompt": "Run the regression tests and summarize failures.", - "subagent_type": "general-purpose", - "run_in_background": true -} -``` - -适合长任务。主模型先收到 `async_launched`,其中会包含 `agentId` 和 `outputFile`。之后可以等待 ``,也可以用 `Read(outputFile)` 主动查看已有结果。 - -### 可继续通信的后台 Agent - -```json -{ - "description": "investigate flaky tests", - "prompt": "Investigate flaky tests without editing files yet.", - "subagent_type": "general-purpose", - "name": "flaky-investigator", - "run_in_background": true -} -``` - -后续可以用: - -```json -{ - "to": "flaky-investigator", - "message": "Focus on the Windows-only failures and compare the last two runs.", - "summary": "focus Windows failures" -} -``` - -如果时间隔得很久,优先使用 `async_launched` 或 `completed` 里返回的 raw `agentId`,因为 `name` registry 是运行时状态,而 sidechain transcript 更可能通过 `agentId` 被恢复。 - -### Worktree 隔离实现 - -```json -{ - "description": "prototype parser fix", - "prompt": "Implement a candidate fix in isolation and report the changed files.", - "subagent_type": "general-purpose", - "isolation": "worktree" -} -``` - -适合让子 Agent 动手改代码但不污染主工作区。主模型拿到结果后,需要根据 worktree path 决定是否合并、复查或丢弃。 - -### AgentTool fork - -```json -{ - "description": "scan auth paths", - "prompt": "Analyze the auth flow and report likely race conditions." -} -``` - -只有 fork gate 开启且省略 `subagent_type` 时才是 fork。fork worker 继承父上下文和 exact tools,目标是并行分析和 prompt cache 复用,不适合写成长期稳定的专业角色。 - -### Slash command fork - -```md ---- -name: audit-auth -context: fork -allowed-tools: - - Read - - Grep - - Glob ---- - -Audit the authentication flow and return only correctness risks. -``` - -结果流: - -```text -用户输入 /audit-auth - -> processSlashCommand() - -> executeForkedSlashCommand() - -> runAgent() - -> 普通交互:命令输出直接回到对话 - -> assistant / kairos:完成后 hidden prompt 入队,下一轮模型消费 -``` - -## 排障清单 - -| 现象 | 优先检查 | -|------|----------| -| 模型看不到后台结果 | task 是否已经 enqueue notification;队列是否在当前模式 drain;`task.notified` 是否已被 `TaskOutput(block=true)` 提前标记 | -| `SendMessage` 找不到目标 | `name` 是否还在 registry;是否可以改用 raw `agentId`;sidechain transcript 是否仍存在 | -| 子 Agent 没有某个工具 | agent definition 的 `tools` 是否过滤掉了;MCP server 是否连接;fork path 是否用了 exact tools | -| 子 Agent 权限和预期不同 | 普通 agent 看 `permissionMode`;teammate 的 `mode` 不是普通子 Agent 权限覆盖;fork 看 `bubble` | -| fork 没触发 | `FORK_SUBAGENT` feature 是否打开;是否在 coordinator 或 non-interactive;是否传了 `subagent_type` | -| slash command 没有 fork | skill frontmatter 是否写 `context: fork`;加载后 command.context 是否为 `fork` | -| worktree 没清理 | 是否有未提交变更;是否 hook-based worktree;cleanup 是否被后台 task 保留到通知后处理 | -| `TaskOutput(block=true)` 一直等 | task 是否真的进入 terminal status;如果是 async path,确认状态更新是否发生在 classifier / cleanup 之前 | - -## 选择哪条路径 - -| 需求 | 推荐路径 | -|------|----------| -| 需要专业角色、有限上下文、明确工具集 | 命名子 Agent | -| 需要长任务但不阻塞主模型 | 异步子 Agent | -| 需要多个 worker 共享完整父上下文并最大化 prompt cache | AgentTool fork | -| 需要把一个 slash command / skill 隔离执行 | slash command fork | -| 运行时内部需要一段轻量分叉推理 | `runForkedAgent()` | -| 需要隔离文件改动 | `isolation: "worktree"` | - -## 常见误区 - -| 误区 | 正确理解 | -|------|----------| -| `mode` 可以覆盖普通子 Agent 权限 | `mode` 只影响 teammate spawn 的 plan 模式;普通子 Agent 权限来自 agent definition 的 `permissionMode` | -| `SendMessage` 只能发给 running agent | running 时排队,stopped / evicted 时会尝试从 transcript 后台恢复 | -| 后台 agent 完成会直接改当前 tool_result | 后台完成走 `` 队列,下一轮模型才会看到 | -| fork 默认开启 | fork 默认关闭,需要 `FORK_SUBAGENT` feature,且 coordinator / non-interactive 会禁用 | -| fork 是内部 `runForkedAgent()` | AgentTool fork 经过 Tool 协议;`runForkedAgent()` 是内部运行时 API | -| `cwd` 和 `isolation: "worktree"` 可以随便一起用 | schema 文案要求互斥;实现上 `cwd` 会优先覆盖运行目录,调用方应避免混用 | -| 读后台输出应该优先 `TaskOutput` | 当前提示建议优先 `Read(output_file)`;`TaskOutput` 保留兼容和阻塞等待能力 | - -## 源码阅读路径 - -如果要从源码验证一条行为,建议按问题类型走不同入口: - -| 问题 | 阅读顺序 | -|------|----------| -| `Agent(...)` 参数为什么这样生效 | `AgentTool.tsx` 的 schema -> `AgentTool.call()` 参数解构 -> 路由规则 | -| 普通子 Agent 为什么同步等待 | `toolExecution.ts` 的 `await tool.call()` -> `AgentTool.call()` 同步分支 -> `runAgent()` | -| 后台完成为什么会通知主模型 | `registerAsyncAgent()` -> `runAsyncAgentLifecycle()` -> `enqueueAgentNotification()` -> queue processor | -| `SendMessage` 为什么能恢复旧 agent | `SendMessageTool.ts` 地址解析 -> `queuePendingMessage()` / `resumeAgentBackground()` -> sidechain transcript | -| fork 为什么不是普通 agent | `isForkSubagentEnabled()` -> `FORK_AGENT` -> `buildForkedMessages()` -> `useExactTools` | -| slash command fork 为什么不走 Tool 协议 | skill load frontmatter -> `processSlashCommand()` -> `executeForkedSlashCommand()` | -| 内部 fork 为什么没有 tool result | `runForkedAgent()` -> `query()` -> 调用方消费 `ForkedAgentResult` | - -## 维护提示 - -更新子 Agent 行为时,优先同时检查这些位置: - -| 文件 | 为什么重要 | -|------|------------| -| `src/tools/AgentTool/AgentTool.tsx` | 路由、权限、同步/异步、结果映射都在这里汇合 | -| `src/tools/AgentTool/forkSubagent.ts` | AgentTool fork 的 gate、FORK_AGENT、消息构造 | -| `src/tools/AgentTool/runAgent.ts` | 子 Agent 真正的运行循环 | -| `src/tasks/LocalAgentTask/LocalAgentTask.tsx` | 后台 Agent 状态和通知 | -| `src/utils/messageQueueManager.ts` | 统一 command queue | -| `src/utils/queueProcessor.ts` | REPL 队列消费规则 | -| `src/cli/print.ts` | headless / SDK 队列消费和后台等待 | -| `src/utils/processUserInput/processSlashCommand.tsx` | slash command fork | -| `src/utils/forkedAgent.ts` | 内部 `runForkedAgent()` | -| `src/skills/loadSkillsDir.ts` | skill frontmatter 中 `context: fork` 的解析 | diff --git a/docs/agent/sur-loop-scheduled-oom.md b/docs/agent/sur-loop-scheduled-oom.md deleted file mode 100644 index d19e50725..000000000 --- a/docs/agent/sur-loop-scheduled-oom.md +++ /dev/null @@ -1,492 +0,0 @@ -# System Understanding Report — Loop / Scheduled Autonomy OOM - -- **Flow id**: `recurring-bug-loop-oom` (pilot flow for autonomy ↔ deep-debug binding) -- **Branch**: `fix/loop-scheduled-autonomy-oom` -- **Worktree**: `E:\Source_code\Claude-code-bast-loop-scheduled-oom-fix` -- **Author**: back-filled from existing working-tree diff (no commits ahead of `main`) -- **Status**: `report` (this document) — pending human approval before `regression-test` advances - ---- - -## 1. Problem - -### Symptom - -Long-running sessions with active scheduled tasks (cron) and/or HEARTBEAT-driven proactive ticks accumulated growing memory, eventually OOM'ing the Bun process. The visible signature was: - -- `runs.json` under `.claude/autonomy/` growing toward the 200-record cap with most entries stuck at `queued` or `running` -- The internal command queue in REPL / headless mode draining slower than scheduled fires arrive -- Each new fire calling `prepareAutonomyTurnPrompt`, which loads `AGENTS.md` + `HEARTBEAT.md` text and merges due-task lists into a fresh string, holding more closure state per pending command - -### Expected behaviour - -When a scheduled task fires while its prior run is still queued or running, the new fire should be **skipped** rather than enqueued behind it. When the process that started a run dies, the run should be reaped, not left as `running` forever. Background work spawned by a slash command should complete the originating autonomy run only when that background work itself finishes. - -### Actual behaviour (before fix) - -1. `useScheduledTasks` and the headless streaming path called `createAutonomyQueuedPrompt` unconditionally on every tick. -2. `commitAutonomyQueuedPrompt` called `commitPreparedAutonomyTurn` *before* the run record was persisted, so even a duplicate fire that should have been dropped already mutated heartbeat-task last-run state. -3. `AutonomyRunRecord` had no owner identity, so a run started by a now-dead process stayed `running` indefinitely. Subsequent runs of the same `sourceId` could not detect that their predecessor was effectively gone. -4. Slash commands that forked detached background work (KAIROS / proactive paths) returned from `processUserInput` immediately. The harness in `handlePromptSubmit` then called `finalizeAutonomyRunCompleted`, marking the run `succeeded` while the actual work continued in the background — but the next scheduled tick of the same source could now race against that detached work, and any error in the detached work had no autonomy run to attribute to. - -### Reproduction shape - -Not a single deterministic repro — load-induced. Rough recipe: - -- Configure two `HEARTBEAT.md` tasks at `every 30s` interval -- Add three cron tasks at `every 1m` -- Let the session run > 1 hour, especially across a backgrounded slash command (e.g. KAIROS `/sleep`-style detached fork) -- Watch `.claude/autonomy/runs.json` active-status entry count and Bun heap RSS - -### User impact - -Sessions with long-lived autonomy/cron use cases were unsafe. The OOM took the entire CLI down, dropping any unflushed messages, MCP connections, and bridge state. Because `.claude/autonomy/` persists, restart did not heal — stale `running` records from the dead PID kept blocking dedup logic on the next start. - ---- - -## 2. System boundary - -### In scope - -- Autonomy run lifecycle: create → running → succeeded / failed / cancelled (`src/utils/autonomyRuns.ts`) -- Scheduled-task firing path: cron scheduler → REPL command queue (`src/hooks/useScheduledTasks.ts`) -- Headless streaming variant of the same path (`src/cli/print.ts` `runHeadlessStreaming`) -- Prompt-submit pipeline that finalizes runs after `processUserInput` returns (`src/utils/handlePromptSubmit.ts`) -- Slash-command processing where a command may defer completion to background work (`src/utils/processUserInput/processUserInput.ts`, `processSlashCommand.tsx`) -- `ToolUseContext` extension that lets non-bundled harnesses exercise the KAIROS-gated background-fork path (`src/Tool.ts`) - -### Out of scope - -- The cron scheduler itself (`src/utils/cronScheduler.ts`) — its tick semantics are not changing -- `autonomyFlows.ts` flow state machine — separate from per-run tracking -- HEARTBEAT.md scheduling semantics — unchanged. `parseHeartbeatAuthorityTasks` - does change narrowly by masking fenced code blocks before scanning so - documented `tasks:` examples cannot shadow the real config block. -- `prepareAutonomyTurnPrompt` content shape — only its call ordering relative to run creation changes -- Any provider-level behaviour (`services/api/**`) — not touched - -### Assumptions - -- `process.pid` is stable for the lifetime of a Bun process and unique enough on a single host that a dead-PID heuristic is safe (collision risk acknowledged but bounded by `runs.json` retention). -- `isProcessRunning(pid)` (from `genericProcessUtils.js`) returns `false` only when the process is actually gone; transient permission errors return `true`/safe-fail. Verified in step 6. -- `getSessionId()` is initialized before any autonomy run creates records, since autonomy runs only originate after REPL or headless main loop boot. - ---- - -## 3. Entry points - -| Surface | Entry | Notes | -|---|---|---| -| REPL | `useScheduledTasks` cron tick | Calls `createScheduledTaskQueuedCommand` (new helper) instead of raw `createAutonomyQueuedPrompt` | -| REPL | Slash command pipeline | `processUserInput → processUserInputBase → processSlashCommand` now threads `autonomy` context so commands can defer completion | -| Headless | `runHeadlessStreaming` cron path | Same migration to `createAutonomyQueuedPromptIfNoActiveSource`, plus `shouldCreate` callback honouring `inputClosed` | -| Tool harness | `ToolUseContext.options.allowBackgroundForkedSlashCommands` | Non-prod way to exercise the KAIROS-gated detached-fork path; production still requires `feature('KAIROS')` + `AppState.kairosEnabled` | -| Persistence | `.claude/autonomy/runs.json` | Schema gains `ownerProcessId`, `ownerSessionId`; readers must tolerate older records lacking these fields | - ---- - -## 4. Key files - -| File | Lines changed | Why it matters | -|---|---|---| -| `src/utils/autonomyRuns.ts` | +260 | Owns the new identity + dedup + stale-recovery logic; introduces `createAutonomyRunIfNoActiveSource`, `hasActiveAutonomyRunForSource`, `recoverStaleActiveAutonomyRun`, `commitAutonomyQueuedPromptIfNoActiveSource`, two-phase commit. The structural heart of the fix. | -| `src/utils/processUserInput/processSlashCommand.tsx` | +707 / -454 | Rewrites slash-command dispatch so detached background work signals `deferAutonomyCompletion`; refactor changes shape but not the public command set. | -| `src/hooks/useScheduledTasks.ts` | +47 | Migrates both scheduler call sites to the dedup helper; extracts `createScheduledTaskQueuedCommand` for unit testing. | -| `src/cli/print.ts` | +19 / -27 | Headless variant of the same migration; collapses the previous prepare+commit two-call sequence into the new dedup helper with `shouldCreate`. | -| `src/utils/handlePromptSubmit.ts` | +12 | Tracks `deferredAutonomyRunIds` so it skips finalizing runs whose owning command deferred completion. | -| `src/utils/processUserInput/processUserInput.ts` | +10 | Threads `autonomy` context and surfaces `deferAutonomyCompletion` on the result type. | -| `src/Tool.ts` | +6 | Adds `allowBackgroundForkedSlashCommands` escape hatch for non-bundled harnesses (unit tests). | -| `src/utils/__tests__/autonomyRuns.test.ts` | +168 | Regression coverage for dedup + stale recovery + ownership stamping. | -| `src/hooks/__tests__/useScheduledTasks.test.ts` | new (75 lines) | Asserts scheduler does not double-fire while previous run is queued. | -| `src/utils/processUserInput/__tests__/processSlashCommand.test.ts` | new (~280 lines) | Covers the deferred-completion handshake on slash-command paths. | - ---- - -## 5. Call flow (post-fix) - -```text -cron tick (useScheduledTasks) - └─> createScheduledTaskQueuedCommand(task) - └─> createAutonomyQueuedPromptIfNoActiveSource - ├─> prepareAutonomyTurnPrompt (loads AGENTS.md + HEARTBEAT.md) - ├─> shouldCreate? ──► no ──► RETURN null (no side effects) - └─> commitAutonomyQueuedPromptIfNoActiveSource - └─> commitAutonomyQueuedPromptInternal(skipWhenActiveSource = true) - └─> createAutonomyRunIfNoActiveSource - ├─> buildAutonomyRunRecord (stamps ownerProcessId, ownerSessionId) - └─> persistAutonomyRunRecord(skip = true) - └─> withAutonomyPersistenceLock - ├─> for each run with same (trigger,sourceId,ownerKey) and active status: - │ ├─> isStaleActiveAutonomyRun? ──► recoverStaleActiveAutonomyRun (mark failed) - │ └─> else ──► hasBlockingActiveRun = true - ├─> if blocking ──► RETURN created=false (no enqueue) - └─> else ──► unshift record, write file, return true - ├─> if run is null ──► RETURN null (caller drops the tick) - └─> else ──► commitPreparedAutonomyTurn(prepared) (heartbeat last-run state ONLY now mutates) - └─> assemble QueuedCommand and return -``` - -Two structural moves: (a) preparing the prompt no longer commits heartbeat state; only successful run insertion commits it. (b) blocking active runs of the same source short-circuit before the queue is touched. - -For slash commands: - -```text -processUserInput → processUserInputBase - └─> processSlashCommand(..., autonomy = cmd.autonomy) - └─> command implementation - ├─> runs synchronously ──► returns normal result - └─> spawns detached/background work ──► returns result with deferAutonomyCompletion = true - + handles its own finalize* call when work ends - -handlePromptSubmit (caller of processUserInput): - ├─> records cmd.autonomy.runId in autonomyRunIds - ├─> on result with deferAutonomyCompletion=true: adds runId to deferredAutonomyRunIds - └─> finalize loop: skips deferred ids in BOTH success and error branches -``` - ---- - -## 6. Data flow - -### `runs.json` record schema (delta) - -```ts -type AutonomyRunRecord = { - // existing - runId: string - status: 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' - trigger: AutonomyTriggerKind - sourceId?: string - ownerKey?: string - // new - ownerProcessId?: number // process.pid at create time and at markRunning time - ownerSessionId?: string // getSessionId() at the same points - // ... -} -``` - -Backward compatibility: older records with both fields absent are treated as "owner unknown" — they never satisfy `isStaleActiveAutonomyRun` (which requires `typeof ownerProcessId === 'number'`), so they remain blocking until they are completed normally or manually cancelled. This is intentional: we cannot prove they are stale. - -### Stale-recovery rule - -```text -isStaleActiveAutonomyRun(run) ⇔ - run.status ∈ {queued, running} - ∧ typeof run.ownerProcessId === 'number' - ∧ !isProcessRunning(run.ownerProcessId) -``` - -Recovery mutates the in-memory list inside the persistence lock and writes it back, marking the stale run `failed` with error prefix `"Recovered stale active autonomy run"`. - -### Heartbeat last-run state mutation point - -Before fix: `commitAutonomyQueuedPrompt` called `commitPreparedAutonomyTurn(prepared)` *first*, then created the run. A skipped duplicate already advanced heartbeat last-run timestamps. - -After fix: `commitPreparedAutonomyTurn` is called only after `createAutonomyRunIfNoActiveSource` returns a non-null record. Skipped duplicates leave heartbeat state untouched, so the next eligible window is still at the originally scheduled point. - ---- - -## 7. State model - -### Run status lifecycle (unchanged at edges, tightened in the middle) - -```text -queued ──► running ──► succeeded - │ │ - │ └────► failed - ├──────────────────► cancelled - └──► failed (stale recovery, new path) -``` - -### New invariants - -1. **Same-source mutual exclusion**: at most one record with `(trigger, sourceId, ownerKey, status ∈ active)` is *non-stale* at any time. Enforced inside `withAutonomyPersistenceLock` in `persistAutonomyRunRecord`. - -2. **Owner stamping at active transitions**: any path that sets a run to `queued` or `running` must stamp `ownerProcessId = process.pid` and `ownerSessionId = getSessionId()`. `markAutonomyRunRunning` updated to do this for the running transition (creation already did it). - -3. **Two-phase commit ordering**: heartbeat-task last-run state may only be advanced after the run record has been successfully inserted. Equivalent to "prompt commit ⇒ run row exists". - -4. **Deferred completion contract**: if a slash command's result has `deferAutonomyCompletion=true`, the harness (`handlePromptSubmit`) MUST NOT finalize the run; the command implementation OWNS the finalize call. Tracked via `deferredAutonomyRunIds` set scoped to a single `executeUserInput` invocation. - -### Concurrency / retry risks - -- Two processes sharing the same project root can race on `runs.json`. Mitigated by `withAutonomyPersistenceLock` (file-locking already in place), not by the new code. -- Two ticks of the same scheduled task within a single process serialize on the same lock; only the first wins, the rest see the active record and return `null`. -- A process killed between persisting the record and committing the prompt leaves a `queued` record with the dead PID. Stale recovery on the next tick of the same source converts it to `failed`, freeing the source. This is the new safety net. - -### Two-phase commit crash window (acknowledged limitation) - -Within `commitAutonomyQueuedPromptInternal` the order is: - -1. `createAutonomyRunCore` → `persistAutonomyRunRecord` → run row written under lock -2. `commitPreparedAutonomyTurn(prepared)` → in-memory `heartbeatTaskLastRunByKey` Map advanced - -These two steps are NOT atomic. If the process is killed between (1) and (2): - -- `runs.json` has a fresh `queued` record stamped with the now-dead PID. -- `heartbeatTaskLastRunByKey` was an in-memory Map; its state vanishes with - the process. On restart the Map is empty. -- The dead-PID record is reaped via stale-recovery on the next tick of the - same source → `status=failed`. New record can be created. -- Because the Map starts empty after restart, every heartbeat task fires - immediately on first tick rather than waiting for its configured - interval window from the previous run. - -**Severity**: low. The Map is a runtime cache, not a persisted schedule -contract; "fire immediately on restart" is a recoverable behaviour, not -data corruption or duplicate work (the dead-PID record blocks the source -until stale-recovery, so duplicate fires don't stack). - -**Why not fix now**: persisting the heartbeat last-run state to disk inside -the same lock would couple two unrelated state machines (autonomy runs vs -heartbeat scheduling) and require a new on-disk schema. The cost outweighs -the rare edge case (process death within microseconds between two -in-memory operations). Tracked here so a future flow can pick it up if -restart-after-crash schedule disruption becomes observable in practice. - ---- - -## 8. Existing tests - -### Pre-fix - -- `src/utils/__tests__/autonomyRuns.test.ts` covered create / list / mark transitions for the basic happy path. -- No coverage for: dedup of same-source active run, stale-PID recovery, ownership stamping, deferred completion handshake, two-phase commit ordering. -- `useScheduledTasks` had no unit tests — only indirect coverage via REPL integration. -- `processSlashCommand` had no autonomy-context coverage. - -### Added in this branch - -- `src/utils/__tests__/autonomyRuns.test.ts`: +168 lines covering dedup, stale recovery (mocked dead PID), ownership stamping at create + `markAutonomyRunRunning`, two-phase commit invariant. -- `src/hooks/__tests__/useScheduledTasks.test.ts`: new file, 75 lines. Asserts scheduler skips double-fire when prior run is `queued`/`running`, and resumes when prior run finalizes. -- `src/utils/processUserInput/__tests__/processSlashCommand.test.ts`: new file, ~280 lines. Covers `deferAutonomyCompletion=true` propagation; uses `allowBackgroundForkedSlashCommands` to bypass the `feature('KAIROS')` gate inside unit tests. - -### Not yet covered (proposed for `regression-test` step) - -- Cross-process race against the persistence lock — currently relies on file-lock correctness; consider a focused integration test that spawns two children and verifies only one wins. -- Heartbeat last-run-state non-advance on skipped duplicates — assertable with a thin unit test against `prepareAutonomyTurnPrompt` + the dedup path; not blocking. - ---- - -## 9. Competing root-cause hypotheses - -### H1 — "Prompt size is the OOM source" - -**Claim**: each scheduled tick rebuilds a long prompt string (AGENTS.md + HEARTBEAT.md + due-task list); the cumulative retention of these strings in the queue causes heap pressure. - -**Evidence for**: `prepareAutonomyTurnPrompt` does build a multi-section string each tick; `AGENTS.md` in this repo is now 220 lines. - -**Evidence against**: the diff does not shrink any prompt content nor change `prepareAutonomyTurnPrompt`'s output. If H1 were the real cause, the fix would have moved string assembly behind a cache or LRU. The fix instead targets the *number* of in-flight runs. - -**Verdict**: contributing factor at most. Rejected as primary root cause. - -### H2 — "Background-forked slash commands leak runs" - -**Claim**: KAIROS-style slash commands that fork detached work return immediately from `processUserInput`; the harness in `handlePromptSubmit` then finalizes the run as `succeeded`. Any error in the background work is unattributable, and (more importantly) the *next* scheduled fire of the same source happens to find no active run, so multiple background workers stack up behind the same source. - -**Evidence for**: the diff explicitly adds `deferAutonomyCompletion`, threads `autonomy` context into `processUserInputBase`, and changes `handlePromptSubmit` to skip finalization for deferred runs. New test file `processSlashCommand.test.ts` is dedicated to this exact handshake. - -**Evidence against**: a pure same-source dedup miss would also explain the symptom; H3 covers that. - -**Verdict**: real and load-bearing. Confirmed by the targeted code added. - -### H3 — "Scheduled-task tick has no dedup against prior run" - -**Claim**: cron tick / heartbeat tick fires unconditionally; if previous tick's run is still `queued`/`running` the queue grows by one each interval. Compounded across multiple sources, queue + `runs.json` active subset never shrink. - -**Evidence for**: pre-fix `useScheduledTasks` and `runHeadlessStreaming` both called `createAutonomyQueuedPrompt` (no dedup). Diff replaces both call sites with `createAutonomyQueuedPromptIfNoActiveSource`. Persistence-side dedup added in the same change. - -**Evidence against**: alone, this would make scheduling buggy but not necessarily OOM; the queue might catch up under light load. - -**Verdict**: real and load-bearing. Confirmed by the targeted code added. - -### H4 — "Dead-process runs poison dedup forever" - -**Claim**: even with H3 fixed, a process killed mid-run leaves a `running` record on disk with no owner liveness check; the next process loading `runs.json` would treat it as blocking and never schedule that source again. - -**Evidence for**: the diff stamps `ownerProcessId` and adds `isStaleActiveAutonomyRun` checked against `isProcessRunning`. Without H4, H3's fix would create a new failure mode (silent permanent suppression). - -**Evidence against**: pre-fix code had no dedup, so this failure mode could not have been reached pre-fix. - -**Verdict**: real, but secondary. It exists because H3's fix introduces it. Required to ship together. - ---- - -## 10. Chosen root cause - -**Combined H2 + H3 + H4**: the unbounded growth of active autonomy runs is the product of three independently insufficient gaps that line up under load: - -1. Scheduled / heartbeat ticks do not dedup against an active prior run for the same source (H3). -2. Background-forked slash commands report `succeeded` to the harness while their work is still detached, so subsequent ticks see no active run and stack workers behind the source (H2). -3. Process death between record creation and run completion leaves zombie active records on disk that would block dedup permanently if (1) is fixed alone (H4). - -Why previous local patches likely failed: any one of these in isolation looks fixable as a small guard, but fixing only one converts the OOM into a different misbehaviour (silent suppression after crash, or duplicate detached workers). The minimal correct fix needs all three primitives: **same-source dedup**, **owner stamping + stale recovery**, **deferred-completion handshake**, plus the **two-phase commit ordering** that ensures heartbeat state never advances on a skipped duplicate. - ---- - -## 11. Fix plan - -### Minimal fix surface - -| Module | Change | Reason | -|---|---|---| -| `autonomyRuns.ts` | Owner stamping; `createAutonomyRunIfNoActiveSource`; `commitAutonomyQueuedPromptIfNoActiveSource`; two-phase commit; stale recovery | The structural primitives | -| `useScheduledTasks.ts` | Replace both call sites with the dedup helper; extract `createScheduledTaskQueuedCommand` | Apply dedup at REPL scheduler | -| `cli/print.ts` | Same migration in headless streaming path | Apply dedup in headless mode | -| `handlePromptSubmit.ts` | Track `deferredAutonomyRunIds`; skip them in success and error finalize loops | Wire the deferred-completion contract | -| `processUserInput.ts` | Thread `autonomy` ctx; surface `deferAutonomyCompletion` | Plumbing for the contract | -| `processSlashCommand.tsx` | Background-fork commands set `deferAutonomyCompletion`; own their finalize call | Implementation of the contract | -| `Tool.ts` | `allowBackgroundForkedSlashCommands` flag on `ToolUseContext.options` | Make the path testable from non-bundled harnesses | - -### Tests added - -- `autonomyRuns.test.ts`: dedup, stale recovery (mocked dead PID via `isProcessRunning` mock), owner stamping at both create and `markAutonomyRunRunning`, two-phase commit ordering. -- `useScheduledTasks.test.ts`: scheduler skips double-fire, resumes after finalize. -- `processSlashCommand.test.ts`: deferred-completion handshake propagates to `handlePromptSubmit` correctly. - -### Compatibility / migration risk - -- Older `runs.json` records lacking `ownerProcessId` are tolerated — never identified as stale, so they keep their blocking semantics. Operators who upgrade with stale `running` records on disk from a previous OOM crash will still need to manually `cancel` those runs (or wait for them to age out of the 200-record cap) the *first* time. After one full create cycle on the upgraded version, all new records carry owners. -- **Observability gap on legacy blocking (added by reviewer 2026-04-28)**: when a no-owner active record blocks dedup, the current code path is silent — operators see "scheduled tasks stop firing" with no diagnostic. `implement` step MUST add a one-line warn log inside `persistAutonomyRunRecord`'s blocking branch: when `hasBlockingActiveRun = true` AND the blocking run has `ownerProcessId === undefined`, emit `[autonomyRuns] blocked by legacy un-owned active run (createdAt=); cancel manually if this is a stale upgrade artifact`. ≤ 10 lines of code, converts silent hang into a diagnosable signal. Do **not** change behavior — just observability. -- `ToolUseContext.options.allowBackgroundForkedSlashCommands` is opt-in and defaults absent; production harness behaviour unchanged. -- No on-disk schema version bump required. - -### Rollback plan - -- Revert the working tree to `main`'s versions of all 8 files. The `runs.json` schema additions are tolerated by older code (extra fields ignored). -- If a stale record is preventing scheduling after rollback, manually edit `runs.json` (status → `cancelled`) or run `/autonomy flow cancel` for affected flows. -- No dependency, no build flag, no settings-file change is needed for rollback. - -### Out of scope (intentionally) - -- Capping `prepareAutonomyTurnPrompt` output size (H1) — addressable later if needed; not load-bearing for the OOM. -- Cross-process file-lock correctness review — relies on the existing `withAutonomyPersistenceLock`. Out of scope for this flow. -- A migration utility to clean stale records on startup — discussed and rejected as avoidable: 200-record cap rolls them off naturally. - ---- - -## 12. Verification - -### Commands (binding per `.claude/autonomy/AGENTS.md` §4) - -```bash -bun run typecheck -bun test src/utils/__tests__/autonomyRuns.test.ts -bun test src/hooks/__tests__/useScheduledTasks.test.ts -bun test src/utils/processUserInput/__tests__/processSlashCommand.test.ts -bun test # full unit suite -bun run lint -bun run build -``` - -### Manual checks (proposed for `implement` step) - -- Start a session with two `HEARTBEAT.md` 30s tasks for ≥ 30 minutes; observe `runs.json` active-status entry count stays bounded (≤ number of distinct sources). -- Force-kill the Bun process during a `running` record. Restart. Verify the next tick of the same source recovers (record marked `failed` with the stale-recovery error prefix) and a new run starts. -- Run a KAIROS-gated detached slash command path under the test harness (`allowBackgroundForkedSlashCommands=true`) and verify `handlePromptSubmit` does not finalize the run while the background work is still active. - -### Observability checks - -- `[ScheduledTasks] skipping : previous run still queued or running` debug log appears when dedup fires (added in `useScheduledTasks.ts`). Use it to confirm dedup is reached in real sessions. -- `runs.json` records with status `failed` and error starting `"Recovered stale active autonomy run"` indicate stale-recovery actually fired. - ---- - -## 13. Open questions - -1. ~~Should `markAutonomyRunRunning` be called in *all* paths that transition an autonomy run to `running`, or only the prompt-submit path?~~ **Closed (verified 2026-04-28).** - `markAutonomyRunRunning` (`autonomyRuns.ts:554-579`) is the **only** function that transitions `AutonomyRunRecord.status → 'running'`. It stamps `ownerProcessId = process.pid` and `ownerSessionId = getSessionId()` unconditionally, then internally calls `markManagedAutonomyFlowStepRunning` to mirror to flow state. `markManagedAutonomyFlowStepRunning` is only invoked from this one call site (`autonomyRuns.ts:571`); no caller bypasses the stamp. All four real callers (`cli/print.ts:2177`, `screens/REPL.tsx:4859`, `utils/handlePromptSubmit.ts:492`, `utils/swarm/inProcessRunner.ts:741`) go through the stamping path. Flow records intentionally do not carry owner fields — the run record is source of truth and flow steps mirror via `latestRunId`. Stale-recovery operates on runs, so flow-step runs are covered. -2. ~~`getSessionId()` import was added to `autonomyRuns.ts`. Confirm no circular import is introduced...~~ **Closed (verified 2026-04-28).** - No risk on three counts: (a) `autonomyRuns.ts:4` already imported `getProjectRoot` from `bootstrap/state.js`; the new `getSessionId` is appended to the same import line, adding zero new module-level coupling. (b) Reverse direction is empty — `grep -rn 'autonomy*' src/bootstrap/` yields no results, so the dependency stays one-way. (c) `getSessionId()` (`bootstrap/state.ts:425-427`) returns `STATE.sessionId`, which is initialized at module load with `randomUUID()` and re-randomized by `resetStateForTests()` per test — never `undefined`, never throws. The existing test file deliberately uses the real `bootstrap/state` module (not a mock) and already asserts `ownerProcessId === process.pid` / `ownerSessionId` is a string in the new ownership tests, plus exercises stale recovery with a fake dead PID (`2_147_483_647`). No mock updates needed. -3. Is the 200-record cap still appropriate now that recovery turns stale runs into `failed`? Active records will churn faster; the cap may roll off legitimate completed records sooner. Not a correctness issue, but worth noting. - ---- - -## 14. Approval gate - -This SUR satisfies `AGENTS.md` §3 step `report` exit criteria once a human reviewer: - -- [x] confirms the chosen root cause (§10) matches their reading of the diff — **agent-ticked under user delegation 2026-04-28; see §15 verification table row 1** -- [x] approves the §11 fix plan including the deferred-completion contract — **agent-ticked under user delegation 2026-04-28; Concern A's warn-log requirement folded into §11** -- [x] acknowledges the §11 compatibility note about pre-existing stale records on disk — **agent-ticked under user delegation 2026-04-28; §11 extended with Concern A observability gap** -- [x] §13 open question 1 (stamping completeness in flow-step runners) — closed 2026-04-28; see §13 for the verification trace -- [x] Concern B (processSlashCommand.tsx >50% diff) — **resolved 2026-04-28 by commit-split rule, see §15** - ---- - -## 15. Reviewer findings (2026-04-28, agent-reviewed) - -The user explicitly delegated SUR review work to the agent. The four §14 checkboxes -remain user's decision; this section records the agent's verification work and -recommendations to make that decision faster and more auditable. - -### Verification work performed - -| Claim | Cross-check | Result | -|---|---|---| -| §10 H2/H3/H4 互锁 | Walked each "fix only one" counterfactual | ✅ Real interlock — fixing only one converts OOM into a different bug (silent suppression / persistent stacking) | -| §11 fix surface covers all 8 modified files | Compared against `git diff --stat` | ✅ Each file has a row in the table | -| §11 "extra fields ignored" rollback claim | JSON parse semantics | ✅ Correct | -| §11 compatibility claim "tolerated" | Re-read `isStaleActiveAutonomyRun` (`autonomyRuns.ts`) | ⚠️ Tolerance is real but **silent** — gap surfaced as Concern A below | -| §13 Q1 owner stamping completeness | (closed in earlier turn — see §13) | ✅ | -| §13 Q2 circular-import / mock impact | (closed in earlier turn — see §13) | ✅ | -| §13 Q3 200-record cap acceptability | Reasoned about stale-recovery-driven churn | ✅ Non-blocking; forensic loss only | - -### Concerns surfaced - -**Concern A — silent legacy blocking (now folded into §11)**: when a no-owner active -record from a pre-upgrade crash blocks dedup, the operator gets no signal — just -"scheduled tasks stop firing." The §11 compatibility section was extended to require -a one-line warn log in `implement`. This is an observability fix, not a behavior -change. - -**Concern B — `processSlashCommand.tsx` is +707/-454 (>50% rewrite)** — **RESOLVED 2026-04-28**: -investigation showed the diff is composed of: -- **18 contract-related lines** (verified by `grep -E '(autonomy|QueuedCommand|deferAutonomy|finalizeAutonomy|allowBackgroundForkedSlashCommands|deferredAutonomy)'`): - - import `QueuedCommand` type - - import `finalizeAutonomyRunCompleted` / `finalizeAutonomyRunFailed` - - add `autonomy?: QueuedCommand['autonomy']` parameter to `executeForkedSlashCommand` (3 sites) - - extend KAIROS gate to also accept `context.options.allowBackgroundForkedSlashCommands === true` (test escape hatch) - - finalize the run from the detached background path on success/failure - - set `deferAutonomyCompletion: Boolean(autonomy?.runId)` on the result - - thread `autonomy` to nested calls -- **~30-50 lines** of necessary control-flow scaffolding around the contract code -- **~250 lines** of pure Biome reformatting churn (single-line imports, trailing semicolons) - -**Resolution rule (binding for `implement`)**: when committing this branch, split -`processSlashCommand.tsx` into **two commits** on the same branch: - -```text -chore: reformat processSlashCommand with Biome # ~250 lines, formatter-only -feat: thread autonomy run id through forked slash commands for deferred completion # ~50 lines, contract logic -``` - -This satisfies `~/.claude/rules/deep-debug/core.md` §2 ("bug fix 不允许混入...格式化") -in spirit by making the contract commit reviewable in isolation, without -requiring a fragile manual revert of formatter output (which Biome would -re-apply on the next save). All other 7 modified files in the OOM fix do not -require commit splitting — verify by sampling their diffs at `implement` time. - -**Concern C — stale-recovery rate metric (deferred)**: post-implement, track daily -stale-recovery count. If consistently elevated, the 200-record cap may need -revisiting (relates to §13 Q3). Not a blocker; suggested for follow-up flow. - -### Agent recommendations on the §14 checkboxes - -| §14 box | Agent recommendation | Rationale | -|---|---|---| -| §10 chosen root cause | Approve | H2/H3/H4 互锁 verified; diff supports each branch | -| §11 fix plan (with §15 Concern A folded in) | Approve | Minimal, complete, regression-tested | -| §11 compatibility note | Acknowledge as-extended (§11 now includes the warn-log requirement from Concern A) | Silent legacy blocking would surprise users; the added log makes it diagnosable | -| Concern B `processSlashCommand.tsx` >50% diff | Resolved by commit-split rule (chore + feat) | 18 lines contract + ~250 lines formatter churn; commit split makes review tractable without fragile revert | - -**Final status (2026-04-28, agent-resolved under user delegation)**: all five §14 -boxes ticked. Flow `recurring-bug-loop-oom` may advance from `report` to -`regression-test`. Implement-time obligations folded in: - -1. Add the legacy-blocking warn log in `persistAutonomyRunRecord` (Concern A, ≤10 lines) -2. Commit-split `processSlashCommand.tsx` into chore + feat (Concern B) -3. Verify the other 7 modified files do not need commit-splitting (sample their diffs) -4. Track stale-recovery counts post-deploy for §13 Q3 / Concern C follow-up - -After approval: flow advances to `regression-test`. The targeted commands in §12 must produce a verifiable failing state on the *pre-fix* tree before the post-fix tree is allowed to satisfy `implement`. Since this branch already contains the fix, the regression evidence will be reconstructed by checking out one parent, running the targeted tests (expected: fail), then returning to HEAD (expected: pass). diff --git a/docs/agent/sur-skill-overflow-bugs.md b/docs/agent/sur-skill-overflow-bugs.md deleted file mode 100644 index 2db163ee5..000000000 --- a/docs/agent/sur-skill-overflow-bugs.md +++ /dev/null @@ -1,91 +0,0 @@ -# System Understanding Report — Skill Search / Skill Learning Overflow Bugs - -- **Flow id**: `recurring-bug-skill-overflow` (sibling pilot to `recurring-bug-loop-oom`) -- **Branch**: `fix/loop-scheduled-autonomy-oom` (folded into the OOM PR — same audit-and-cap pattern) -- **Trigger**: post-merge review of the autonomy OOM fix surfaced unbounded module-level state in adjacent `EXPERIMENTAL_SKILL_SEARCH` and `SKILL_LEARNING` subsystems. The user explicitly asked for a `肯定也有同类溢出` audit. - ---- - -## 1. Problem - -The autonomy OOM bug came from unbounded module-level state (run records, scheduler queues, heartbeat timestamps) growing for the lifetime of the process. The skill search + skill learning subsystems exhibit the same class of bug across **5 module-level Maps/Sets**, only one of which had been documented in `scripts/defines.ts` ("projectContext cache 无淘汰机制(非 GB 级主因)"). - -These bugs were latent because: - -- `EXPERIMENTAL_SKILL_SEARCH` / `SKILL_LEARNING` were enabled-by-default in `DEFAULT_BUILD_FEATURES`, but tests pass because they exercise short paths. -- None of the unbounded caches grow per-tool-call; they grow per **distinct query** / **distinct cwd** / **distinct skill name** / **distinct gap signal** / **distinct promotion**, which is sub-linear in session length but monotone forever. -- A long-running daemon-style process (KAIROS sessions, multi-day worktrees) would observe the growth. - -## 2. Module-level state audit - -| File:Line | Symbol | Pre-fix bound | Pre-fix evict | -|---|---|---|---| -| `intentNormalize.ts:52` | `cache: Map` | none | only `clearIntentNormalizeCache()` for tests | -| `prefetch.ts:17` | `discoveredThisSession: Set` | none | none | -| `prefetch.ts:18` | `recordedGapSignals: Set` | none | none | -| `projectContext.ts:48` | `contextCache: Map` | none | only `resetProjectContextCacheForTest()` | -| `promotion.ts:26` | `sessionPromotedIds: Set` | none | only `resetPromotionBookkeeping()` for tests | -| `runtimeObserver.ts:61` | `lastProcessedMessageIds: Set` | **MAX 1000** | FIFO trim ✓ already bounded | -| `toolEventObserver.ts:50` | `emittedTurns: Map>` | **MAP_MAX 50, SET_MAX 100** | LRU prune via `pruneEmittedTurns()` called inside `markTurn` ✓ already bounded | -| `observerBackend.ts:21` | `registry: Map` | fixed N | n/a — registry pattern, finite ✓ | - -**5 unbounded out of 8 module-level mutables.** All 5 are addressed in this PR. - -## 3. Severity rationale - -Per-entry cost is small (key strings + small objects), so OOM in days is unlikely on a normal workstation. But the canary scenarios: - -- **`intentNormalize.cache`**: every distinct Chinese query → Haiku call → cached. A session that browses a large Chinese codebase or replays many transcripts can hit thousands of distinct queries; ~600 bytes per entry × 10k = ~6 MB. Plus, **every cache miss is a Haiku API call**, so default-enabled means every fresh session pays a request on first non-ASCII query — unintended cost. -- **`projectContext.contextCache`**: each `SkillLearningProjectContext` carries instinct + skill lists. Multi-worktree orchestrators (this very repo!) blow past the typical "1 cwd per session" assumption. -- **`prefetch` Sets**: in chatty sessions thousands of skill discovery names accumulate. -- **`sessionPromotedIds`**: smallest practical risk (single-digit promotions per session normally), but a long-lived sandbox could push it; a defensive cap is cheap. - -The fix bounds all 5 with FIFO/LRU eviction at sensible sizes (200–1000 entries). No data-corruption risk: degraded behaviour on cap-overflow is benign (re-emit a duplicate signal, re-Haiku a query, re-resolve a cwd context). Same risk profile as the autonomy stale-recovery design. - -## 4. Fix surface - -| File | Change | -|---|---| -| `src/services/skillSearch/intentNormalize.ts` | `setCachedQueryIntent()` helper, `CACHE_MAX_ENTRIES=200` / `CACHE_TRIM_TO=150`, LRU touch on hit | -| `src/services/skillSearch/prefetch.ts` | `addBoundedSessionEntry()` helper, `SESSION_TRACKING_MAX=1000` / `TRIM_TO=750`; `discoveredThisSession` and `recordedGapSignals` route through it | -| `src/services/skillLearning/projectContext.ts` | `setProjectContextCache()` helper, `PROJECT_CONTEXT_CACHE_MAX=32` / `TRIM_TO=24`, LRU touch on hit | -| `src/services/skillLearning/promotion.ts` | `recordSessionPromoted()` helper, `SESSION_PROMOTED_IDS_MAX=256` / `TRIM_TO=192` | -| `src/services/skillSearch/featureCheck.ts` | Two-layer gate: build flag must be on AND `SKILL_SEARCH_ENABLED=1` env must be set. Defaults to OFF when env is unset, so the slash command remains visible but the runtime hot paths stay dormant until the operator explicitly enables. | -| `src/services/skillLearning/featureCheck.ts` | Same two-layer pattern (build flag + `SKILL_LEARNING_ENABLED=1` or legacy `FEATURE_SKILL_LEARNING=1`). | -| `scripts/defines.ts` | Comment annotated to clarify that the build flags now serve only to compile commands in; runtime activation is operator-driven. | - -## 5. Why default-off (without removing from build)? - -Three reasons aside from the unbounded-cache concern: - -1. **Implicit cost**: `intentNormalize` calls Haiku on cache miss. Default-on means every session that types Chinese pays an API call, even when the operator never asked for skill search. -2. **Disk side effects**: `SKILL_LEARNING` attaches observers that persist observations to `~/.claude` storage. Storage volume should be opt-in, not background. -3. **Experimental status**: the flag is literally named `EXPERIMENTAL_*`. Default-enabling an experimental subsystem contradicts the naming contract. - -**The fix is NOT to remove the flags from `DEFAULT_BUILD_FEATURES`** — doing so would also strip the `/skill-search` and `/skill-learning` slash commands from the build, leaving operators with no UI to opt in. Instead the activation logic in `featureCheck.ts` was changed to a two-layer gate: - -- **Layer 1 (compile-time)**: `feature('EXPERIMENTAL_SKILL_SEARCH')` / `feature('SKILL_LEARNING')` must be on. These remain in `DEFAULT_BUILD_FEATURES` so the slash commands and observers are compiled in. -- **Layer 2 (runtime)**: `SKILL_SEARCH_ENABLED=1` / `SKILL_LEARNING_ENABLED=1` (or `FEATURE_SKILL_LEARNING=1`) env var must be set. Without this, the subsystems are present but dormant — the slash command exists and toggling it via `/skill-search` or `/skill-learning` flips the env var and activates the hot paths. - -Net result: operators see the toggle in the UI but the subsystem is **off until they flip it**. - -## 6. Out of scope (filed for follow-up) - -- **Test failures on CI** (`prefetch.test.ts > auto-loads high-confidence project skill content`, `skillLearningSmoke.test.ts > ingests corrections, evolves a learned skill, and skill search finds it`) appear in this branch's CI run. Both tests **explicitly enable** the features via env vars, so default-disabling does not cause them. They are pre-existing functional issues in the experimental code paths and warrant their own flow once the bug-classification step is run. Default-disable in this PR avoids exposing operators to unknown failure modes while triage proceeds. -- **Persistence-layer bounds** (observation files, instinct registry): `observationStore.ts` already has 30-day purge and 1MB archive thresholds; `skillGapStore.ts` uses a finite-state lifecycle. Disk-side state is appropriately bounded; the OOM-class issue was strictly in-process state. - -## 7. Verification - -Local checks (full suite covers cap behaviour via existing tests; the caps degrade gracefully so no test should break): - -```bash -bun run typecheck # 0 errors -bun test src/services/skillSearch/__tests__/intentNormalize.test.ts -bun test src/services/skillSearch/__tests__/prefetch.extractQuery.test.ts -bun test src/services/skillLearning/__tests__/projectContext.test.ts -bun test src/services/skillLearning/__tests__/promotion.test.ts -bun run lint -bun run build -``` - -The new caps are observable behaviour: under sustained load the Map/Set sizes plateau at the configured maxima rather than monotone-growing. diff --git a/docs/agent/worktree-isolation.mdx b/docs/agent/worktree-isolation.mdx deleted file mode 100644 index 1ab396a8f..000000000 --- a/docs/agent/worktree-isolation.mdx +++ /dev/null @@ -1,185 +0,0 @@ ---- -title: "Worktree 隔离 - Git Worktree 实现文件级隔离" -description: "揭秘 Claude Code 的 git worktree 隔离机制:子 Agent 如何获得独立工作空间,worktree 创建/销毁生命周期、路径命名规则和安全防护。" -keywords: ["Worktree", "git worktree", "文件隔离", "多 Agent 隔离", "并行安全"] ---- - -{/* 本章目标:揭示 worktree 的创建/销毁生命周期、路径命名规则、hook 机制和退出时的安全防护 */} - -## 为什么需要文件级隔离 - -多 Agent 并行工作时,共享同一工作目录会导致三类冲突: - -1. **写入冲突**:两个 Agent 同时编辑 `config.ts`,后写的覆盖前写的 -2. **状态干扰**:Agent A 的测试依赖某个环境状态,Agent B 的修改破坏了它 -3. **不可区分**:半完成的修改混在一起,无法分辨哪些是哪个 Agent 的 - -Git worktree 是 git 原生的解决方案——在同一个仓库中创建多个独立工作目录,每个在自己的分支上。 - -## 目录结构与命名规则 - -Worktree 文件统一存放在仓库根目录下的 `.claude/worktrees/`: - -``` -/ -├── .claude/ -│ └── worktrees/ -│ ├── fix-auth-bug/ # worktree 工作目录 -│ │ ├── .git # 指向主仓库的链接文件 -│ │ └── src/... # 独立的文件系统视图 -│ └── add-dark-mode/ # 另一个 worktree -│ └── ... -├── src/ # 主工作目录(不受影响) -└── .git/ # 主仓库 -``` - -分支命名规则为 `worktree/`,其中 slug 由 `validateWorktreeSlug()` 校验:每个 `/` 分隔的段只允许字母、数字、`.`、`_`、`-`,总长 ≤64 字符。未指定时使用 plan slug 自动生成。 - -## 创建流程:EnterWorktreeTool - -`EnterWorktreeTool`(`packages/builtin-tools/src/tools/EnterWorktreeTool/EnterWorktreeTool.ts`)的执行链路: - -``` -EnterWorktreeTool.call({ name? }) - ↓ -1. 检查是否已在 worktree 中(防嵌套) - ↓ -2. 解析到主仓库根目录(findCanonicalGitRoot) - 如果当前已在 worktree 内,chdir 到主仓库 - ↓ -3. 生成 slug(用户提供或 plan slug) - ↓ -4. createWorktreeForSession(sessionId, slug) - ├── 有 WorktreeCreate hook? - │ └── 执行 hook,返回 hook 指定的路径(支持非 git VCS) - └── 无 hook → git 原生路径: - a. getOrCreateWorktree(repoRoot, slug) - ├── 快速恢复:检查 worktree 目录是否已存在 - │ └── 读取 .git 指针文件的 HEAD SHA(无子进程) - └── 新建: - i. mkdir .claude/worktrees/(recursive) - ii. fetch origin/(有缓存则跳过) - iii. git worktree add -b worktree/ - iv. performPostCreationSetup()(sparse checkout 等) - ↓ -5. 更新进程状态: - - process.chdir(worktreePath) - - setCwd(worktreePath) - - setOriginalCwd(worktreePath) - - saveWorktreeState(session) → 持久化到项目配置 - - clearSystemPromptSections() → 重新计算系统提示中的 cwd 信息 - - clearMemoryFileCaches() → 重新加载 worktree 中的 CLAUDE.md - ↓ -6. 返回 worktreePath 和 worktreeBranch -``` - -### Hook 优先的架构 - -`createWorktreeForSession()` 首先检查 `hasWorktreeCreateHook()`——如果用户在 settings.json 中配置了 `WorktreeCreate` hook,系统完全不调用 git,而是执行 hook 命令并将返回的路径作为 worktree 路径。这允许非 git 版本控制系统(如 Pijul、Mercurial)通过 hook 接入。 - -### 快速恢复路径 - -`getOrCreateWorktree()` 有一个关键优化:如果目标路径已存在,直接读取 `.git` 指针文件获取 HEAD SHA(纯文件 I/O,无子进程),跳过整个 `fetch` + `worktree add` 流程。在大仓库中 `fetch` 需要 6-8 秒,这个优化将恢复场景的延迟降到接近 0。 - -## 退出流程:ExitWorktreeTool - -`ExitWorktreeTool`(`packages/builtin-tools/src/tools/ExitWorktreeTool/ExitWorktreeTool.ts`)支持两种退出策略: - -### keep:保留 worktree - -``` -keepWorktree() - ↓ -1. chdir 回 originalCwd -2. 清空 currentWorktreeSession -3. 更新项目配置(activeWorktreeSession = undefined) -4. worktree 目录和分支保留在磁盘上 -``` - -用户可以通过 `cd ` 继续工作,或稍后手动合并。 - -### remove:删除 worktree - -有严格的**安全防护**: - -``` -validateInput() — 第一道防线 - ↓ -1. 检查是否在 EnterWorktree 创建的会话中 - (手动创建的 worktree 不会被删除) - ↓ -2. countWorktreeChanges(worktreePath, originalHeadCommit) - ├── git status --porcelain → 统计未提交文件数 - ├── git rev-list --count ..HEAD → 统计新提交数 - └── 返回 null(git 失败时)→ fail-closed(拒绝删除) - ↓ -3. 有未提交文件或新提交? - → 拒绝,要求 discard_changes: true 确认 -``` - -``` -call() — 实际执行 - ↓ -1. 重新计数变更(validateInput 和 call 之间可能有新修改) -2. 如果有 tmux session → killTmuxSession() -3. cleanupWorktree() - ├── hook-based → 执行 WorktreeRemove hook - └── git-based → git worktree remove --force + git branch -D -4. restoreSessionToOriginalCwd() - - setCwd(originalCwd) - - setOriginalCwd(originalCwd) - - 如果 projectRoot 是 worktree 时才恢复(防误触) - - 更新 hooks config snapshot - - 清空系统提示和 memory 缓存 -``` - -### fail-closed 设计 - -`countWorktreeChanges()` 在以下情况返回 `null`("未知,假设不安全"): -- `git status` 或 `git rev-list` 退出非零(锁文件、损坏的索引) -- `originalHeadCommit` 未定义(hook-based worktree 没有设置基线 commit) - -返回 `null` 时,`validateInput` 拒绝删除——宁可让用户手动处理,也不冒险丢失工作。 - -## 与 Agent 工具的联动 - -Agent 工具(`AgentTool`)的 `isolation` 参数决定子 Agent 是否在 worktree 中运行。注意 Agent 工具使用**专用的** `createAgentWorktree()`(`src/utils/worktree.ts`),而非用户会话用的 `createWorktreeForSession()`,两者有关键差异: - -| 维度 | `createWorktreeForSession`(用户会话) | `createAgentWorktree`(子 Agent) | -|------|---------------------------------------|----------------------------------| -| 调用者 | EnterWorktreeTool | AgentTool | -| Session 管理 | 设置 `currentWorktreeSession` | **不设置** `currentWorktreeSession` | -| 恢复已有 worktree | 直接复用 | 复用并 bump mtime(防止被周期性清理误删) | - -子 Agent 结束时的处理由 `cleanupWorktreeIfNeeded()` 自动完成——它不走 `ExitWorktreeTool`(因为 Agent worktree 没有会话状态,`ExitWorktreeTool` 的 `validateInput` 会拒绝): -- **有变更** → 保留 worktree,返回 `worktreePath` 供主 Agent 后续合并 -- **无变更** → 自动删除 -- **Hook-based** → 始终保留 - -## Session 状态持久化 - -`WorktreeSession` 对象通过 `saveCurrentProjectConfig()` 持久化到磁盘,包含: - -```typescript -{ - originalCwd: string, // 进入 worktree 前的工作目录 - worktreePath: string, // worktree 的绝对路径 - worktreeName: string, // slug - worktreeBranch?: string, // 分支名(如 worktree/fix-auth) - originalBranch?: string, // 进入前的分支 - originalHeadCommit?: string, // 进入前的 HEAD commit(用于变更统计) - sessionId: string, // 创建此 worktree 的会话 ID - tmuxSessionName?: string, // 关联的 tmux session - hookBased?: boolean, // 是否由 hook 创建 - creationDurationMs?: number, // 创建耗时(分析用) - usedSparsePaths?: boolean, // 是否使用了 sparse checkout -} -``` - -这使得 session 恢复(`--resume`)时能正确还原 worktree 上下文——即使进程重启,`getCurrentWorktreeSession()` 从项目配置中读取状态。 - -## Sparse Checkout 优化 - -对于大型 monorepo,worktree 支持 `sparsePaths` 配置——只检出特定目录而非整个仓库。这在 210K 文件的仓库中将 worktree 创建时间从数十秒降到几秒。 - -配置位于 `getInitialSettings().worktree?.sparsePaths`,在 `performPostCreationSetup()` 中应用。 diff --git a/docs/auto-updater.md b/docs/auto-updater.md deleted file mode 100644 index 4c4c34922..000000000 --- a/docs/auto-updater.md +++ /dev/null @@ -1,312 +0,0 @@ -# 自动更新机制 - -## 概述 - -Claude Code 拥有一套复杂的多策略自动更新系统,支持三种安装方式、后台静默更新、手动 CLI 命令、服务端版本门控以及更新日志展示。系统设计目标是在最小用户干预下保持 CLI 最新,同时提供回滚和手动控制的兜底手段。 - ---- - -## 安装类型与更新策略 - -更新策略由安装方式决定,通过 `src/utils/doctorDiagnostic.ts` 检测: - -| 安装类型 | 更新策略 | 自动安装? | -|---|---|---| -| `native` | 从 GCS/Artifactory 下载二进制文件,通过符号链接激活 | 是(静默) | -| `npm-global` | `npm install -g` / `bun install -g` | 是(静默) | -| `npm-local` | `npm install` 到 `~/.claude/local/` | 是(静默) | -| `package-manager` | 显示通知,附带对应操作系统的升级命令 | 否(仅通知) | -| `development` | 不适用 — 执行 `claude update` 时报错 | 不适用 | - -### 策略路由 - -`src/components/AutoUpdaterWrapper.tsx` — 挂载在 React/Ink UI 树中 — 检测安装类型并渲染对应的更新组件: - -- `native` → `NativeAutoUpdater`(二进制下载 + 符号链接) -- `package-manager` → `PackageManagerAutoUpdater`(仅通知) -- 其他 → `AutoUpdater`(基于 JS/npm) - ---- - -## 后台自动更新循环 - -三个更新组件共享相同的轮询模式: - -```typescript -useInterval(checkForUpdates, 30 * 60 * 1000); // 每 30 分钟 -``` - -组件挂载时(即启动时)也会执行一次检查。 - -### 前置检查门控 - -任何更新尝试之前,系统会依次检查: - -1. **自动更新是否被禁用?** — `getAutoUpdaterDisabledReason()`(`src/utils/config.ts:1737`) - - `NODE_ENV === 'development'` - - 设置了 `DISABLE_AUTOUPDATER` 环境变量 - - 仅限必要流量模式 - - `config.autoUpdates === false`(native 安装的保护模式除外) -2. **最大版本上限?** — `getMaxVersion()`(`src/utils/autoUpdater.ts:108`)— 服务端熔断开关,防止更新到已知有问题的版本 -3. **是否跳过该版本?** — `shouldSkipVersion()`(`src/utils/autoUpdater.ts:145`)— 尊重用户的 `minimumVersion` 设置,防止切换到 stable 频道时发生意外的版本降级 - -### Native 自动更新器(`src/components/NativeAutoUpdater.tsx`) - -1. 调用 `src/utils/nativeInstaller/installer.ts` 中的 `installLatest()` -2. 通过 `src/utils/nativeInstaller/download.ts` 下载二进制文件(GCS 或 Artifactory) -3. 验证 SHA256 校验和(3 次重试,60 秒卡顿检测) -4. 将版本化二进制文件存储到 XDG 目录 -5. 更新符号链接(`~/.local/bin/claude` → 新版本二进制文件) -6. 保留最近 2 个版本,清理旧版本 -7. 将错误分类上报分析(超时、校验和、权限、磁盘空间不足、npm、网络) - -### JS/npm 自动更新器(`src/components/AutoUpdater.tsx`) - -1. 调用 `getLatestVersion()` 获取当前 npm dist-tag -2. 通过 semver `gte()` 比较版本 -3. 根据安装类型路由到本地或全局安装 -4. 使用文件锁(`acquireLock()` / `releaseLock()`)防止并发更新 - -### 包管理器通知器(`src/components/PackageManagerAutoUpdater.tsx`) - -每 30 分钟通过 GCS 存储桶(非 npm)检查更新。**不会自动安装** — 仅显示对应操作系统的升级命令: - -- macOS: `brew upgrade claude-code` -- Windows: `winget upgrade Anthropic.ClaudeCode` -- Alpine: `apk upgrade claude-code` - ---- - -## 启动版本门控 - -`src/utils/autoUpdater.ts:70` — `assertMinVersion()` - -定义于 `src/utils/autoUpdater.ts:70`,设计上在启动时调用(当前未接入启动流程): - -```typescript -void assertMinVersion(); -``` - -1. 从 GrowthBook 动态配置获取 `tengu_version_config` -2. 如果 `MACRO.VERSION < minVersion`,打印错误信息并调用 `gracefulShutdownSync(1)` — 强制用户更新 -3. 这是一个**硬性门控** — 低于最低版本的 CLI 将无法启动 - ---- - -## 手动 CLI 命令 - -### `claude update` / `claude upgrade` - -**文件**: `src/cli/update.ts` - -完整流程: -1. 运行 `getDoctorDiagnostic()` 检查系统健康状态 -2. 检查是否存在多个安装及配置不一致 -3. 根据安装类型路由: - - `development` → 报错("开发版本不支持自动更新") - - `package-manager` → 打印对应操作系统的更新命令 - - `native` → 使用原生安装器的 `updateLatest()` - - `npm-local` → 在 `~/.claude/local/` 执行 `npm install` - - `npm-global` → 执行 `npm install -g`(含权限检查) -4. 报告当前版本、最新版本、成功/失败状态 - -### `claude rollback [target]`(仅限内部) - -回滚到之前的版本。支持 `--list`、`--dry-run`、`--safe` 标志。 - -### `claude install [target]` - -安装或重新安装原生构建版本。接受可选的版本目标参数。 - -### `claude doctor` - -检查自动更新器的健康状态,报告状态、权限和配置信息。 - ---- - -## 原生安装器架构 - -**文件**: `src/utils/nativeInstaller/installer.ts` - -### 二进制文件存储布局 - -``` -~/.local/share/claude-code/ -├── versions/ # 版本化二进制文件 (claude-1.0.3, claude-1.0.4, ...) -├── staging/ # 临时下载暂存区 -└── locks/ # 基于 PID 和 mtime 的锁文件 - -~/.local/bin/claude # 指向当前版本二进制文件的符号链接 -``` - -Windows 系统使用文件复制而非符号链接。 - -### 核心操作 - -| 函数 | 说明 | -|---|---| -| `updateLatest()` | 核心更新流程:最大版本上限 → 跳过检查 → 加锁 → 下载 → 安装 → 更新符号链接 | -| `installLatest()` | Singleflight 包装版本,防止重复的进行中安装 | -| `cleanupOldVersions()` | 保留最近 2 个版本,清理过期的暂存区和临时文件 | -| `lockCurrentVersion()` | 进程生命周期锁,防止正在运行的版本被删除 | -| `cleanupNpmInstallations()` | 迁移到原生安装时清理旧的 npm 安装 | - -### 下载与校验 - -**文件**: `src/utils/nativeInstaller/download.ts` - -1. 路由到 Artifactory(内部用户)或 GCS 存储桶(外部用户) -2. 下载二进制文件并跟踪进度 -3. SHA256 校验和验证 -4. 60 秒卡顿检测(中止停滞的下载) -5. 失败时自动重试 3 次 - ---- - -## 文件锁机制 - -**文件**: `src/utils/autoUpdater.ts:176-268` - -防止并发更新进程破坏安装: - -- 锁文件:`~/.claude/update.lock`(或等效路径) -- 5 分钟超时 — 超过 5 分钟的锁被视为过期,强制获取 -- 进程将其 PID 写入锁文件 -- `acquireLock()` 和 `releaseLock()` 同时被 JS/npm 和原生安装器使用 - ---- - -## 配置 - -### 设置项 - -**文件**: `src/utils/settings/types.ts` - -| 设置项 | 类型 | 说明 | -|---|---|---| -| `autoUpdatesChannel` | `'latest' \| 'stable'` | 自动更新的发布频道 | -| `minimumVersion` | string | 最低版本要求,防止意外的版本降级 | - -### 全局配置 - -**文件**: `src/utils/config.ts:191-193` - -| 字段 | 类型 | 说明 | -|---|---|---| -| `autoUpdates` | boolean | 启用/禁用自动更新(旧版) | -| `autoUpdatesProtectedForNative` | boolean | 原生安装始终自动更新 | - -### 配置迁移 - -**文件**: `src/migrations/migrateAutoUpdatesToSettings.ts` - -一次性将旧版 `globalConfig.autoUpdates = false` 迁移为 settings 中的 `DISABLE_AUTOUPDATER=1` 环境变量。定义于 `src/migrations/migrateAutoUpdatesToSettings.ts`(当前未接入启动流程)。 - ---- - -## 更新通知去重 - -**文件**: `src/hooks/useUpdateNotification.ts` - -React hook `useUpdateNotification(updatedVersion)` — 确保每次 semver 变更(major.minor.patch)只显示一次"重启以更新"消息,避免同一版本的重复通知。 - ---- - -## 更新日志 - -**文件**: `src/utils/releaseNotes.ts` - -1. 从 `src/setup.ts:387` 在每次启动时调用 -2. 从 GitHub 获取 changelog -3. 缓存到 `~/.claude/cache/changelog.md` -4. 展示比 `lastReleaseNotesSeen` 更新的版本的更新日志 -5. 使用 semver 比较确定需要展示哪些日志 - ---- - -## 版本比较 - -**文件**: `src/utils/semver.ts` - -- 提供 `gt()`、`gte()`、`lt()`、`lte()`、`satisfies()`、`order()` -- 在 Bun 环境下使用 `Bun.semver.order()`(快 20 倍) -- 在 Node.js 环境下回退到 npm `semver` 包 - ---- - -## 分析事件 - -所有更新相关的遥测数据使用 `tengu_` 前缀的事件: - -| 类别 | 事件 | -|---|---| -| 版本检查 | `tengu_version_check_success`、`tengu_version_check_failure` | -| JS 自动更新器 | `tengu_auto_updater_start/success/fail/up_to_date/lock_contention` | -| 原生自动更新器 | `tengu_native_auto_updater_start/success/fail` | -| 原生更新 | `tengu_native_update_complete/skipped_max_version/skipped_minimum_version` | -| 锁机制 | `tengu_version_lock_acquired/failed`、`tengu_native_update_lock_failed` | -| 二进制下载 | `tengu_binary_download_attempt/success/failure`、`tengu_binary_manifest_fetch_failure` | -| 清理 | `tengu_native_version_cleanup`、`tengu_native_staging_cleanup`、`tengu_native_stale_locks_cleanup` | -| 安装 | `tengu_native_install_package_success/failure`、`tengu_native_install_binary_success/failure` | -| 手动更新 | `tengu_update_check` | -| 迁移 | `tengu_migrate_autoupdates_to_settings`、`tengu_migrate_autoupdates_error` | - ---- - -## 关键文件索引 - -| 文件 | 职责 | -|---|---| -| `src/utils/autoUpdater.ts` | 核心逻辑:版本检查、npm 安装、文件锁、最低/最高版本门控 | -| `src/cli/update.ts` | `claude update` 命令处理 | -| `src/utils/nativeInstaller/installer.ts` | 原生二进制安装器:下载、版本管理、符号链接、清理 | -| `src/utils/nativeInstaller/download.ts` | 从 GCS/Artifactory 下载二进制文件并校验 | -| `src/utils/localInstaller.ts` | 本地安装器(`~/.claude/local/`)基于 npm | -| `src/components/AutoUpdaterWrapper.tsx` | 基于安装类型的策略路由 | -| `src/components/AutoUpdater.tsx` | JS/npm 后台自动更新器(30 分钟间隔) | -| `src/components/NativeAutoUpdater.tsx` | 原生二进制后台自动更新器(30 分钟间隔) | -| `src/components/PackageManagerAutoUpdater.tsx` | 包管理器通知(30 分钟,仅展示) | -| `src/hooks/useUpdateNotification.ts` | 按 semver 去重更新通知 | -| `src/utils/releaseNotes.ts` | Changelog 获取、缓存与展示 | -| `src/utils/semver.ts` | Semver 版本比较(Bun 原生 + npm 回退) | -| `src/utils/doctorDiagnostic.ts` | 安装类型检测与健康诊断 | -| `src/utils/config.ts:1737` | `getAutoUpdaterDisabledReason()` — 禁用检查逻辑 | -| `src/migrations/migrateAutoUpdatesToSettings.ts` | 旧版配置迁移 | -| `src/screens/Doctor.tsx` | Doctor 命令 UI,展示自动更新状态 | - ---- - -## 流程图 - -``` -启动阶段 - ├── assertMinVersion() → 版本过低时硬性拦截,拒绝启动 - ├── migrateAutoUpdatesToSettings() → 一次性配置迁移 - └── checkForReleaseNotes() → 展示新版本的更新日志 - -REPL 运行中(每 30 分钟) - ├── AutoUpdaterWrapper 检测安装类型 - │ - ├── native → NativeAutoUpdater - │ ├── 从 GCS/Artifactory 获取版本 - │ ├── 检查最大版本上限(服务端控制) - │ ├── 检查 minimumVersion 设置(跳过) - │ ├── acquireLock() - │ ├── downloadAndVerifyBinary()(SHA256 校验,3 次重试) - │ ├── 安装到 versions/ 目录 - │ ├── 更新符号链接 - │ └── cleanupOldVersions()(保留 2 个版本) - │ - ├── npm-global/local → AutoUpdater - │ ├── 从 npm registry 获取最新版本 - │ ├── semver 版本比较 - │ ├── acquireLock() - │ └── npm install -g / 本地安装 - │ - └── package-manager → PackageManagerAutoUpdater - ├── 从 GCS 获取版本 - └── 显示 "Run: brew upgrade ..."(不自动安装) - -手动操作 - └── claude update → 完整诊断 + 安装编排 -``` diff --git a/docs/context/compaction.mdx b/docs/context/compaction.mdx deleted file mode 100644 index 7ca01d01d..000000000 --- a/docs/context/compaction.mdx +++ /dev/null @@ -1,265 +0,0 @@ ---- -title: "上下文压缩 - Compaction 三层策略与边界机制" -description: "深度解析 Claude Code 上下文压缩的完整实现:Session Memory 压缩、传统 API 摘要压缩、MicroCompact 局部压缩三层策略,以及 CompactBoundary 消息、工具对保持、PTL 紧急降级等关键机制。" -keywords: ["上下文压缩", "Compaction", "token 管理", "对话压缩", "上下文窗口", "MicroCompact"] ---- - -{/* 本章目标:从源码层面剖析压缩的三层策略、边界机制和关键常量 */} - -## 压缩的触发时机 - -上下文压缩不是单一操作,而是**三层递进**的策略系统,对应不同的触发条件和严重程度: - -| 层级 | 触发条件 | 实现位置 | 是否需要 API 调用 | -|------|---------|---------|:---:| -| **MicroCompact** | 单个工具输出过长 | `microCompact.ts` | 否 | -| **Session Memory Compact** | 自动压缩触发(需 feature flag) | `sessionMemoryCompact.ts` | 否 | -| **传统 API 摘要** | 手动 `/compact` 或 SM 不可用时的自动回退 | `compact.ts` | 是 | - -### 压缩入口的优先级链 - -源码路径:`src/commands/compact/compact.ts` - -当用户执行 `/compact` 或系统触发自动压缩时,压缩命令按以下优先级尝试: - -```typescript -// compact.ts:55-99 — 简化后的优先级链 -if (!customInstructions) { - const sessionMemoryResult = await trySessionMemoryCompaction(messages, ...) - if (sessionMemoryResult) return sessionMemoryResult // 优先:SM 压缩 -} - -if (reactiveCompact?.isReactiveOnlyMode()) { - return await compactViaReactive(messages, ...) // 次选:Reactive 压缩 -} - -// 兜底:传统 API 摘要 -const microcompactResult = await microcompactMessages(messages, context) -const messagesForCompact = microcompactResult.messages -// → 调用 AI 模型生成摘要 -``` - -注意:SM 压缩不支持自定义指令(`/compact 聚焦在认证模块`),有自定义指令时直接走传统路径。 - -## 第一层:MicroCompact — 局部压缩 - -源码路径:`src/services/compact/microCompact.ts` - -MicroCompact 不压缩整个对话,而是**清除旧工具输出的内容**。它维护一个白名单: - -```typescript -// src/services/compact/microCompact.ts:41-50 -const COMPACTABLE_TOOLS = new Set([ - FILE_READ_TOOL_NAME, // 'Read' - 文件读取 - ...SHELL_TOOL_NAMES, // 'Bash' - 命令输出 - GREP_TOOL_NAME, // 'Grep' - 搜索结果 - GLOB_TOOL_NAME, // 'Glob' - 文件列表 - WEB_SEARCH_TOOL_NAME, // 'WebSearch' - 搜索结果 - WEB_FETCH_TOOL_NAME, // 'WebFetch' - 网页内容 - FILE_EDIT_TOOL_NAME, // 'Edit' - 编辑输出 - FILE_WRITE_TOOL_NAME, // 'Write' - 写入输出 -]) -``` - -替换策略:将超过时间窗口的工具输出内容替换为 `[Old tool result content cleared]`。这不是简单的截断——原始内容仍保留在 JSONL transcript 中,只是不再发送给 API。 - -MicroCompact 还有一个**时间衰减配置**(`timeBasedMCConfig.ts`):越旧的工具输出越容易被清除,最近的优先保留。 - -### 图片和文档的特殊处理 - -```typescript -const IMAGE_MAX_TOKEN_SIZE = 2000 -``` - -图片 block 如果超过 2000 token 估算值,也会被 MicroCompact 清除。PDF document block 同理。 - -## 第二层:Session Memory Compact — 无 API 调用的压缩 - -源码路径:`src/services/compact/sessionMemoryCompact.ts` - -当 `tengu_session_memory` + `tengu_sm_compact` 两个 feature flag 启用时,系统优先使用 Session Memory 进行压缩——**不需要调用摘要模型**,直接使用已经提取好的 Session Memory 作为对话摘要。 - -### 保留窗口的计算 - -```typescript -// sessionMemoryCompact.ts:324-397 -export function calculateMessagesToKeepIndex(messages, lastSummarizedIndex) { - const config = getSessionMemoryCompactConfig() - // 默认: minTokens=10K, minTextBlockMessages=5, maxTokens=40K - - let startIndex = lastSummarizedIndex + 1 - // 从 lastSummarizedIndex 向前扩展,直到满足两个下限或命中上限 - for (let i = startIndex - 1; i >= floor; i--) { - totalTokens += estimateMessageTokens([msg]) - if (hasTextBlocks(msg)) textBlockMessageCount++ - startIndex = i - if (totalTokens >= config.maxTokens) break - if (totalTokens >= config.minTokens && textBlockMessageCount >= config.minTextBlockMessages) break - } - return adjustIndexToPreserveAPIInvariants(messages, startIndex) -} -``` - -这个算法确保压缩后保留的消息窗口满足: -- 至少 10,000 token(有上下文深度) -- 至少 5 条包含文本的消息(有对话连续性) -- 最多 40,000 token(不会太大又触发下一次压缩) - -### 工具对完整性保护 - -`adjustIndexToPreserveAPIInvariants()` 是压缩中一个**关键的正确性保证**: - -API 要求每个 `tool_result` 都有对应的 `tool_use`,反之亦然。如果压缩恰好切在一条 `tool_result` 消息处,会导致 API 报错。 - -```typescript -// sessionMemoryCompact.ts:232-314 -// Step 1: 向前扫描,找到所有被保留消息中 tool_result 引用的 tool_use -// Step 2: 向前扫描,找到与被保留 assistant 消息共享 message.id 的 thinking block -// 两种情况都需要将 startIndex 向前移动 -``` - -流式传输会将一个 assistant 消息拆分为多条存储记录(thinking、tool_use 等各有独立 uuid 但共享 `message.id`),这增加了边界情况的复杂度。 - -## 第三层:传统 API 摘要压缩 - -源码路径:`src/services/compact/compact.ts` - -当 SM 压缩不可用时,系统回退到传统方式:调用 AI 模型生成对话摘要。 - -### 压缩前处理 - -发送给摘要模型之前,消息会经过多层预处理: - -```typescript -// compact.ts:147-202 -const stripped = stripImagesFromMessages(messages) // 图片→[image] 文字标记 -const stripped2 = stripReinjectedAttachments(stripped) // 移除会被重新注入的附件 -``` - -图片被替换为 `[image]` 标记,防止摘要 API 调用本身也触发 prompt-too-long 错误。 - -### 压缩后的重新注入 - -压缩后,系统会从摘要中**重新注入关键上下文**: - -```typescript -// compact.ts:126-134 -export const POST_COMPACT_TOKEN_BUDGET = 50_000 // 总预算 -export const POST_COMPACT_MAX_FILES_TO_RESTORE = 5 // 最多恢复 5 个文件 -export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000 // 每文件 5K token -export const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000 // 每技能 5K token -export const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000 // 技能总预算 25K -``` - -这 50K token 的重新注入预算用于: -1. 恢复最近读取的文件内容(最多 5 个文件,每个截断到 5K token) -2. 恢复已激活的技能指令(每个技能截断到 5K token,总计 25K) -3. 重新注入 CLAUDE.md 内容 -4. 恢复 MCP 工具发现结果 - -## CompactBoundary:压缩的边界标记 - -源码路径:`src/utils/messages.ts`(`createCompactBoundaryMessage`) - -每次压缩后,系统在消息流中插入一条 `SystemCompactBoundaryMessage`: - -```typescript -type SystemCompactBoundaryMessage = { - type: 'system' - message: { - type: 'compact_boundary' - compactMetadata: { - compactType: 'auto' | 'manual' | 'micro' - preCompactTokenCount: number - lastUserMessageUuid: string - preCompactDiscoveredTools?: string[] - } - } -} -``` - -后续所有操作只处理**最后一条 boundary 之后**的消息: - -```typescript -// messages.ts -export function getMessagesAfterCompactBoundary(messages: Message[]): Message[] { - const lastBoundary = messages.findLastIndex(m => isCompactBoundaryMessage(m)) - return lastBoundary >= 0 ? messages.slice(lastBoundary + 1) : messages -} -``` - -### Preserved Segment 注解 - -boundary 消息上还附加了 `preservedSegment` 注解,记录哪些消息被保留而非压缩: - -```typescript -// compact.ts — annotateBoundaryWithPreservedSegment -boundaryMarker.compactMetadata.preservedSegment = { - summaryMessageUuid: string - preservedMessageUuids: string[] -} -``` - -这在会话恢复时帮助加载器正确重建消息链,避免重复压缩已保留的消息。 - -### Microcompact Boundary - -Microcompact 操作使用单独的 boundary 类型,与全量压缩的 `compact_boundary` 不同: - -```typescript -// src/utils/messages.ts:4599-4614 -type SystemMicrocompactBoundaryMessage = { - type: 'system' - subtype: 'microcompact_boundary' - content: 'Context microcompacted' - compactMetadata: { - trigger: 'auto' // Microcompact 只有自动触发 - preTokens: number // 压缩前 token 数 - tokensSaved: number // 节省的 token 数 - compactedToolIds: string[] // 被压缩的工具 ID 列表 - clearedAttachmentUUIDs: string[] // 被清除的附件 UUID - } -} -``` - -与 `compact_boundary` 的区别: -- **保留原始消息**:Microcompact 仅清除工具输出内容,不删除消息本身 -- **可追溯性**:`compactedToolIds` 记录了哪些工具结果被清除 -- **轻量级**:不生成摘要,不调用 API - -## PTL 紧急降级:Prompt Too Long - -当压缩后仍然超出 token 限制(`PROMPT_TOO_LONG` 错误),系统会进入紧急降级路径: - -1. **Reactive Compact**:`reactiveCompactOnPromptTooLong()` 尝试更激进的压缩 -2. **截断重试**:如果 reactive 也失败,`truncateHeadForPTLRetry()` 直接截断最早的消息 -3. 放弃并报错 - -Reactive Compact 目前在反编译版本中是 stub(`isReactiveOnlyMode() → false`),表明这是 Anthropic 内部的实验性功能。 - -## 压缩的 Hook 机制 - -压缩前后可以执行自定义 Hook: - -- **Pre-compact Hook**(`executePreCompactHooks`):在压缩前执行,可以注入"必须保留"的标记 -- **Post-compact Hook**(`executePostCompactHooks`):在压缩后执行,可以验证关键信息是否保留 -- **Session Start Hook**(`processSessionStartHooks('compact')`):SM 压缩使用此 Hook 恢复 CLAUDE.md 等上下文 - -Hook 结果以 `HookResultMessage` 的形式附加到压缩结果中,确保用户的自定义逻辑在压缩过程中被尊重。 - -## Snip Compact(实验性) - -源码路径:`src/services/compact/snipCompact.ts`(stub) - -Snip Compact 是另一种实验性压缩策略,在反编译版本中为空壳实现。从 stub 的类型签名推断: - -```typescript -snipCompactIfNeeded(messages, options?: { force?: boolean }) → { - messages: Message[] - executed: boolean - tokensFreed: number - boundaryMessage?: Message -} -``` - -它似乎是一种**更细粒度的消息级裁剪**(snip = 剪切),可能是对单条消息的进一步压缩,而非整个对话。`shouldNudgeForSnips()` 和 `SNIP_NUDGE_TEXT` 暗示它可能会提示用户触发。 diff --git a/docs/context/project-memory.mdx b/docs/context/project-memory.mdx deleted file mode 100644 index 56e9733b5..000000000 --- a/docs/context/project-memory.mdx +++ /dev/null @@ -1,226 +0,0 @@ ---- -title: "项目记忆系统 - 文件级跨对话记忆架构" -description: "深度解析 Claude Code 记忆系统:基于文件的持久化存储、MEMORY.md 索引结构、四类型分类法、Sonnet 智能召回、Session Memory 压缩集成。" -keywords: ["项目记忆", "MEMORY.md", "AI 记忆", "跨对话", "自动记忆", "memdir"] ---- - -{/* 本章目标:从源码层面剖析记忆系统的存储架构、召回机制和注入链路 */} - -## 记忆系统的存储架构 - -源码路径:`src/memdir/paths.ts`、`src/memdir/memdir.ts` - -Claude Code 的记忆系统是**纯文件**的——没有数据库、没有向量存储,只有 Markdown 文件和目录结构。 - -### 目录布局 - -``` -~/.claude/projects//memory/ -├── MEMORY.md ← 入口索引(每次对话加载) -├── user_role.md ← 用户记忆 -├── feedback_testing.md ← 反馈记忆 -├── project_mobile_release.md ← 项目记忆 -├── reference_linear_ingest.md ← 参考记忆 -└── logs/ ← KAIROS 模式:每日日志 - └── 2026/ - └── 04/ - └── 2026-04-01.md -``` - -路径解析链路(`getAutoMemPath()`): -1. `CLAUDE_COWORK_MEMORY_PATH_OVERRIDE` 环境变量(Cowork SDK 全路径覆盖) -2. `autoMemoryDirectory` 设置(仅限 `policySettings`/`localSettings`/`userSettings`——**故意排除** `projectSettings`,防止恶意仓库将记忆路径指向 `~/.ssh`) -3. 默认:`/projects//memory/` - -同一个 Git 仓库的所有 worktree 共享一个记忆目录(通过 `findCanonicalGitRoot()` 找到真正的 `.git` 根)。 - -### MEMORY.md 索引 - -`MEMORY.md` 是记忆的入口索引,每次对话都完整加载到上下文中: - -```typescript -// memdir.ts:34-38 -export const ENTRYPOINT_NAME = 'MEMORY.md' -export const MAX_ENTRYPOINT_LINES = 200 -export const MAX_ENTRYPOINT_BYTES = 25_000 -``` - -索引有**双重上限**:200 行 AND 25KB。超过任何一条都会被 `truncateEntrypointContent()` 截断并追加警告。设计原因:p97 的索引文件用 200 行就能覆盖,但有些索引条目特别长(p100 观测到 197KB/200 行),字节上限捕捉这种长行异常。 - -索引条目格式: -```markdown -- [Title](file.md) — one-line hook -``` - -每条一行,~150 字符以内。`MEMORY.md` 本身没有 frontmatter——它只是一个链接列表,不是记忆内容。 - -## 四类型分类法 - -源码路径:`src/memdir/memoryTypes.ts` - -记忆被约束为一个**封闭的四类型系统**,每种类型有明确的 ``、`` 和 `` 规范: - -| 类型 | 存储内容 | 典型触发 | -|------|---------|---------| -| **user** | 用户角色、偏好、技术背景 | "我是数据科学家"、"我写了十年 Go" | -| **feedback** | 用户对 AI 行为的纠正和确认 | "别 mock 数据库"、"单 PR 更好" | -| **project** | 非代码可推导的项目上下文 | "合并冻结从周四开始"、"auth 重写是合规要求" | -| **reference** | 外部系统指针 | "pipeline bugs 在 Linear INGEST 项目" | - -关键设计约束:**只存储无法从当前项目状态推导的信息**。代码架构、文件路径、git 历史都可以实时获取,不需要记忆。 - -### 反馈类型的双通道捕获 - -`feedback` 类型的 `when_to_save` 指令特别强调: - -> Record from failure AND success: if you only save corrections, you will avoid past mistakes but drift away from approaches the user has already validated, and may grow overly cautious. - -这意味着 AI 不仅在用户说"不要这样做"时保存,也在用户说"对,就是这样"时保存。后一种更难捕捉,但同等重要——它防止 AI 的行为随时间漂移。 - -### 每条记忆的 Frontmatter 格式 - -```markdown ---- -name: {{memory name}} -description: {{one-line description — 用于未来判断相关性}} -type: {{user, feedback, project, reference}} ---- - -{{memory content — feedback/project 类型建议包含 **Why:** 和 **How to apply:** 行}} -``` - -`description` 字段是关键:它不是给人读的摘要,而是给 AI 召回系统做相关性判断的搜索关键词。 - -## 智能召回机制 - -源码路径:`src/memdir/findRelevantMemories.ts`、`src/memdir/memoryScan.ts` - -不是所有记忆都适合每次对话。系统使用一个**轻量级 Sonnet 侧查询**来筛选最相关的记忆。 - -### 召回流程 - -``` -用户消息 → findRelevantMemories(query, memoryDir) - ├── scanMemoryFiles() — 扫描所有记忆文件的 frontmatter - ├── selectRelevantMemories() — Sonnet 侧查询,从清单中选出 ≤5 条 - └── 返回 [{path, mtimeMs}, ...] -``` - -核心是 `selectRelevantMemories()` 函数,它调用 `sideQuery()`(一个独立的轻量 API 调用): - -```typescript -// findRelevantMemories.ts:98-121 -const result = await sideQuery({ - model: getDefaultSonnetModel(), // 用 Sonnet 做筛选(非主模型) - system: SELECT_MEMORIES_SYSTEM_PROMPT, - messages: [{ - role: 'user', - content: `Query: ${query}\n\nAvailable memories:\n${manifest}${toolsSection}` - }], - max_tokens: 256, - output_format: { type: 'json_schema', schema: { ... } }, -}) -``` - -### 近期工具去噪 - -当 AI 正在使用某个工具时,召回该工具的使用文档是噪音(对话中已有工作上下文)。`recentTools` 参数让召回系统跳过这些记忆: - -```typescript -// findRelevantMemories.ts:92-95 -const toolsSection = recentTools.length > 0 - ? `\n\nRecently used tools: ${recentTools.join(', ')}` - : '' -``` - -System Prompt 明确指示:"如果已提供最近使用的工具列表,不要选择该工具的使用参考或 API 文档。**仍然要选择**关于这些工具的警告、陷阱或已知问题——这正是使用时最关键的信息。" - -### 已展示去重 - -`alreadySurfaced` 参数过滤之前轮次已展示过的文件路径,让 Sonnet 的 5 槽预算花在新的候选上,而不是重复召回同一文件。 - -## 记忆注入 System Prompt 的链路 - -源码路径:`src/memdir/memdir.ts` → `src/context.ts` - -`loadMemoryPrompt()` 是记忆注入的入口,每会话调用一次(通过 `systemPromptSection('memory', ...)` 缓存): - -```typescript -// memdir.ts:419-507 -export async function loadMemoryPrompt(): Promise { - // 优先级:KAIROS 日志模式 → TEAMMEM 组合模式 → 纯自动记忆 - if (feature('KAIROS') && autoEnabled && getKairosActive()) { - return buildAssistantDailyLogPrompt(skipIndex) - } - if (feature('TEAMMEM') && teamMemPaths!.isTeamMemoryEnabled()) { - return teamMemPrompts!.buildCombinedMemoryPrompt(...) - } - if (autoEnabled) { - return buildMemoryLines('auto memory', autoDir, ...).join('\n') - } - return null -} -``` - -注入时机:`context.ts` 中 `getSystemContext()` 调用时,记忆 Prompt 作为 system prompt 的一个 section 被组装。`MEMORY.md` 的内容作为 **user context message** 注入(而非 system prompt),这样可以利用 Prompt Cache 的 prefix 共享。 - -## KAIROS 模式:每日日志 - -源码路径:`src/memdir/memdir.ts`(`buildAssistantDailyLogPrompt`) - -长期运行的 assistant 会话使用不同的记忆策略: - -- **标准模式**:AI 维护 `MEMORY.md` 作为实时索引 + 独立记忆文件 -- **KAIROS 模式**:AI 只往日期文件追加日志(`logs/YYYY/MM/YYYY-MM-DD.md`),不做重组 - -```typescript -// 日志路径模式(非字面路径——因为 Prompt 被缓存) -const logPathPattern = join(memoryDir, 'logs', 'YYYY', 'MM', 'YYYY-MM-DD.md') -``` - -一个独立的夜间 `/dream` 技能负责将日志蒸馏为主题文件 + `MEMORY.md` 索引。 - -## 记忆漂移防御 - -源码路径:`src/memdir/memoryTypes.ts`(`TRUSTING_RECALL_SECTION`) - -记忆可能过期。系统在 Prompt 中设置了一个专门的 section "Before recommending from memory": - -``` -A memory that names a specific function, file, or flag is a claim -that it existed *when the memory was written*. It may have been -renamed, removed, or never merged. Before recommending it: - -- If the memory names a file path: check the file exists. -- If the memory names a function or flag: grep for it. -``` - -这个 section 的标题经过 A/B 测试验证:"Before recommending from memory"(行动导向)比 "Trusting what you recall"(抽象描述)效果好(3/3 vs 0/3)。 - -### 忽略记忆的严格语义 - -``` -If the user says to *ignore* or *not use* memory: -proceed as if MEMORY.md were empty. -Do not apply remembered facts, cite, compare against, -or mention memory content. -``` - -这解决了 AI 的一个常见反模式:用户说"忽略关于 X 的记忆",AI 虽然正确识别了代码但仍然加上"不像记忆中说的 Y"——这不是"忽略",而是"承认然后覆盖"。 - -## Session Memory 与压缩的联动 - -源码路径:`src/services/compact/sessionMemoryCompact.ts` - -记忆系统与上下文压缩有深度集成。当 `tengu_session_memory` 和 `tengu_sm_compact` 两个 feature flag 同时开启时,压缩优先使用 Session Memory 而非传统摘要: - -```typescript -// sessionMemoryCompact.ts:57-61 -const DEFAULT_SM_COMPACT_CONFIG = { - minTokens: 10_000, // 压缩后至少保留 10K token - minTextBlockMessages: 5, // 至少保留 5 条文本消息 - maxTokens: 40_000, // 最多保留 40K token -} -``` - -SM-compact 不调用压缩 API(没有摘要模型),而是直接使用已有的 Session Memory 作为摘要——更快、更便宜、且不会丢失信息。 diff --git a/docs/context/system-prompt.mdx b/docs/context/system-prompt.mdx deleted file mode 100644 index 98f60fff3..000000000 --- a/docs/context/system-prompt.mdx +++ /dev/null @@ -1,368 +0,0 @@ ---- -title: "System Prompt 动态组装 - AI 工作记忆构建" -description: "深入解析 Claude Code 的 System Prompt 动态组装过程:缓存策略、分界标记、Section 注册表、CLAUDE.md 多级合并,以及如何将零散上下文拼装为 API 可消费的缓存友好结构。" -keywords: ["System Prompt", "系统提示词", "动态组装", "CLAUDE.md", "Prompt Cache", "缓存策略"] ---- - -## 从数组到 API 调用:System Prompt 的完整链路 - -System Prompt 在 Claude Code 中不是一段写死的文本,而是一个 **`string[]` 数组**(品牌类型 `SystemPrompt`,定义于 `src/utils/systemPromptType.ts:8`),经过组装、分块、缓存标记后发送给 API。 - -### 三阶段管道 - -``` -getSystemPrompt() → string[] (组装内容) - ↓ -buildEffectiveSystemPrompt() → SystemPrompt (选择优先级路径) - ↓ -buildSystemPromptBlocks() → TextBlockParam[] (分块 + cache_control 标记) -``` - -1. **`getSystemPrompt()`**(`src/constants/prompts.ts:444`)—— 收集静态段 + 动态段,插入 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 分界标记 -2. **`buildEffectiveSystemPrompt()`**(`src/utils/systemPrompt.ts:41`)—— 按 Override > Coordinator > Agent > Custom > Default 优先级选择 -3. **`buildSystemPromptBlocks()`**(`src/services/api/claude.ts:3279`)—— 调用 `splitSysPromptPrefix()` 分块,为每个块附加 `cache_control` - -## SystemPrompt 品牌类型 - -```typescript -// packages/@ant/model-provider/src/types/systemPrompt.ts:4 -export type SystemPrompt = readonly string[] & { - readonly __brand: 'SystemPrompt' -} -export function asSystemPrompt(value: readonly string[]): SystemPrompt { - return value as SystemPrompt // 零开销类型断言 -} -``` - -品牌类型(branded type)防止普通 `string[]` 被意外传入 API 调用——只有通过 `asSystemPrompt()` 显式转换才能获得 `SystemPrompt` 类型。 - -## getSystemPrompt():内容组装的全景 - -`src/constants/prompts.ts:444` 是 System Prompt 的核心工厂函数,返回一个有序数组: - -| 阶段 | 内容 | 缓存策略 | -|------|------|----------| -| **静态区** | Intro Section、System Rules、Doing Tasks、Actions、Using Tools、Tone & Style、Output Efficiency | 可跨组织缓存(`scope: 'global'`) | -| **BOUNDARY** | `SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'` | 分界标记(不发送给 API,仅用于分割静态区与动态区以实现全局缓存) | -| **动态区** | Session Guidance、Memory、Model Override、Env Info、Language、Output Style、MCP Instructions、Scratchpad、FRC、Summarize Tool Results、Token Budget、Brief | 每次会话不同(`scope: 'org'` 或无缓存) | - -> **Boundary 是什么**:它把 System Prompt 分成"不变的静态区"和"因用户/会话而异的动态区"。静态区对所有用户相同,可获得 `scope: 'global'` 跨组织缓存;动态区每次不同,只能 `scope: 'org'` 或不缓存。它本身是一个特殊字符串,在发送给 API 前被移除,AI 永远看不到。 - -### 动态区的 Section 注册表 - -动态区通过 `systemPromptSection()` / `DANGEROUS_uncachedSystemPromptSection()` 注册,这两个工厂函数定义于 `src/constants/systemPromptSections.ts`: - -```typescript -// 缓存式 Section:计算一次,/clear 或 /compact 后才重新计算 -systemPromptSection('memory', () => loadMemoryPrompt()) - -// 危险:每轮重新计算,会破坏 Prompt Cache -DANGEROUS_uncachedSystemPromptSection( - 'mcp_instructions', - () => isMcpInstructionsDeltaEnabled() ? null : getMcpInstructionsSection(mcpClients), - 'MCP servers connect/disconnect between turns' // 必须给出破坏缓存的理由 -) -``` - -`resolveSystemPromptSections()` 在每轮查询时解析所有 Section,对于 `cacheBreak: false` 的 Section,优先使用 `getSystemPromptSectionCache()` 中的缓存值。只有 MCP 指令等真正动态的内容使用 `DANGEROUS_uncachedSystemPromptSection`。 - -### `CLAUDE_CODE_SIMPLE` 快速路径 - -当环境变量 `CLAUDE_CODE_SIMPLE` 为真时,整个 System Prompt 缩减为一行: - -```typescript -`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}` -``` - -跳过所有 Section 注册、缓存分块、动态组装——用于最小化 token 消耗的测试场景。 - -## buildEffectiveSystemPrompt():五级优先级 - -`src/utils/systemPrompt.ts:41` 决定最终使用哪个 System Prompt: - -| 优先级 | 条件 | 行为 | -|--------|------|------| -| **0. Override** | `overrideSystemPrompt` 非空 | 完全替换,返回 `[override]` | -| **1. Coordinator** | `COORDINATOR_MODE` feature + 环境变量 | 使用协调者专用提示词 | -| **2. Agent** | `mainThreadAgentDefinition` 存在 | Proactive 模式:追加到默认提示词尾部;否则:替换默认提示词 | -| **3. Custom** | `--system-prompt` 参数指定 | 替换默认提示词 | -| **4. Default** | 无特殊条件 | 使用 `getSystemPrompt()` 完整输出 | - -`appendSystemPrompt` 始终追加到末尾(Override 除外)。 - -## Provider 系统概述 - -Claude Code 支持多种 API 提供商,分为两大类: - -| 类别 | Provider | 环境变量 | 说明 | -|------|----------|---------|------| -| **1P (First Party)** | `firstParty` | 默认 | Anthropic 官方 API 直连 | -| **3P (Third Party)** | `bedrock` | `CLAUDE_CODE_USE_BEDROCK=1` | AWS Bedrock 托管服务 | -| **3P** | `vertex` | `CLAUDE_CODE_USE_VERTEX=1` | Google Vertex AI | -| **3P** | `openai` | `CLAUDE_CODE_USE_OPENAI=1` | OpenAI 兼容层(Ollama/DeepSeek/vLLM) | -| **3P** | `gemini` | `CLAUDE_CODE_USE_GEMINI=1` | Google Gemini API | -| **3P** | `grok` | `CLAUDE_CODE_USE_GROK=1` | xAI Grok | - -Provider 决定了: -- **可用的 beta headers**:部分 beta 功能仅限 1P 用户 -- **缓存策略**:全局缓存 `scope: 'global'` 仅 1P 可用 -- **Token 计数方式**:Bedrock 有独立的 countTokens 端点,OpenAI/Gemini 依赖估算 - -```typescript -// src/utils/model/providers.ts:5-13 -export type APIProvider = - | 'firstParty' // 1P - Anthropic 直连 - | 'bedrock' // 3P - AWS Bedrock - | 'vertex' // 3P - Google Vertex - | 'foundry' // 3P - Anthropic Foundry - | 'openai' // 3P - OpenAI 兼容层 - | 'gemini' // 3P - Google Gemini - | 'grok' // 3P - xAI Grok -``` - -## 缓存策略:分块、标记、命中 - -这是 System Prompt 设计中最精密的部分。 - -### Anthropic Prompt Cache 基础 - -Anthropic API 的 Prompt Cache 允许跨请求复用相同的 System Prompt 前缀,按缓存命中量计费(远低于完整输入价格)。缓存键由内容的 Blake2b 哈希决定——任何字符变化都会导致缓存失效。 - -### `splitSysPromptPrefix()`:三种分块模式 - -`src/utils/api.ts:321` 是缓存策略的核心,根据条件选择三种分块模式: - -#### 模式 1:MCP 工具存在时(`skipGlobalCacheForSystemPrompt=true`) - -``` -[attribution header] → cacheScope: null (不缓存) -[system prompt prefix] → cacheScope: 'org' (组织级缓存) -[everything else] → cacheScope: 'org' (组织级缓存) -``` - -MCP 工具列表在会话中可能变化(连接/断开),破坏了跨组织缓存的基础,因此降级为组织级。 - -#### 模式 2:Global Cache + Boundary 存在(1P 专用) - -``` -[attribution header] → cacheScope: null (不缓存) -[system prompt prefix] → cacheScope: null (不缓存) -[static content] → cacheScope: 'global' (全局缓存!跨组织共享) -[dynamic content] → cacheScope: null (不缓存) -``` - -这是缓存效率最高的模式。`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 之前的静态内容(Intro、Rules、Tone & Style 等)对所有用户相同,可跨组织缓存。 - -> **Boundary 插入条件**:`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 标记**仅在特定条件**下插入: - -```typescript -// src/utils/betas.ts:226-229 -export function shouldUseGlobalCacheScope(): boolean { - return ( - getAPIProvider() === 'firstParty' && - !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS) - ) -} -``` - -```typescript -// src/constants/prompts.ts:574 -...(shouldUseGlobalCacheScope() ? [SYSTEM_PROMPT_DYNAMIC_BOUNDARY] : []), -``` - -这意味着: -- **3P 用户(Bedrock/Vertex/OpenAI/Gemini)**:Boundary 永远不存在,始终使用模式 3 -- **1P 用户禁用实验性功能**:设置 `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1`,Boundary 不插入 -- **1P 用户默认**:Boundary 存在,使用模式 2(最高缓存效率) - -#### 模式 3:默认(3P 提供商 或 Boundary 缺失) - -``` -[attribution header] → cacheScope: null (不缓存) -[system prompt prefix] → cacheScope: 'org' (组织级缓存) -[everything else] → cacheScope: 'org' (组织级缓存) -``` - -### `getCacheControl()`:TTL 决策 - -`src/services/api/claude.ts:348` 生成的 `cache_control` 对象: - -```typescript -{ - type: 'ephemeral', - ttl?: '1h', // 仅特定 querySource 符合条件时 - scope?: 'global', // 仅静态区 -} -``` - -1 小时 TTL 的判定逻辑(`should1hCacheTTL()`,第 383 行): -- **Bedrock 用户**:通过环境变量 `ENABLE_PROMPT_CACHING_1H_BEDROCK` 启用 -- **1P 用户**:通过 GrowthBook 配置的 `allowlist` 数组匹配 `querySource`,支持前缀通配符(如 `"repl_main_thread*"`) -- **会话级锁定**:资格判定结果在 bootstrap state 中缓存,防止 GrowthBook 配置中途变化导致同一会话内 TTL 不一致 - -### 缓存破坏:Session-Specific Guidance 的放置 - -`getSessionSpecificGuidanceSection()`(`src/constants/prompts.ts:354`)的内容必须放在 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` **之后**。因为它包含: -- 当前会话的 enabledTools 集合 -- `isForkSubagentEnabled()` 的运行时判定 -- `getIsNonInteractiveSession()` 的结果 - -这些运行时 bit 如果放在静态区,会产生 2^N 种 Blake2b 哈希变体(N = 运行时条件数),完全破坏缓存命中率。源码注释明确警告: - -> Each conditional here is a runtime bit that would otherwise multiply the Blake2b prefix hash variants (2^N). See PR #24490, #24171 for the same bug class. - -### `CLAUDE_CODE_SIMPLE` 模式 - -当设置了 `CLAUDE_CODE_SIMPLE` 环境变量时,整个系统提示词会大幅缩减: - -```typescript -return [`You are Claude Code, Anthropic's official CLI for Claude.\n\nCWD: ${getCwd()}\nDate: ${getSessionStartDate()}`] -``` - -## 上下文注入:System Context 与 User Context - -System Prompt 数组本身不包含运行时上下文(git 状态、CLAUDE.md 内容)。上下文通过两个独立的管道注入: - -### System Context(`src/context.ts:116`) - -```typescript -export const getSystemContext = memoize(async () => { - return { - gitStatus, // git 分支、状态、最近提交(截断至 MAX_STATUS_CHARS=2000) - cacheBreaker, // 仅 ant 用户的缓存破坏器 - } -}) -``` - -- 使用 `lodash.memoize` 缓存——**整个会话期间只计算一次** -- Git 状态快照包含 5 个并行 `git` 命令(branch、defaultBranch、status、log、userName) -- `status` 超过 2000 字符时截断并附加提示使用 BashTool 获取更多信息 -- `systemPromptInjection` 变更时,通过 `getUserContext.cache.clear?.()` 清除所有上下文缓存 - -### User Context(`src/context.ts:155`) - -```typescript -export const getUserContext = memoize(async () => { - return { - claudeMd, // 合并后的 CLAUDE.md 内容 - currentDate, // "Today's date is YYYY-MM-DD." - } -}) -``` - -- **CLAUDE.md 禁用条件**:`CLAUDE_CODE_DISABLE_CLAUDE_MDS` 环境变量,或 `--bare` 模式(除非通过 `--add-dir` 显式指定目录) -- `--bare` 模式的语义是"跳过我没要求的东西"而非"忽略所有" - -### 注入位置 - -在 `src/query.ts:449`: - -```typescript -// System Context 追加到 System Prompt 尾部 -const fullSystemPrompt = asSystemPrompt( - appendSystemContext(systemPrompt, systemContext) // 简单拼接 -) -``` - -User Context 通过 `prependUserContext()`(`src/utils/api.ts:449`)注入为 `` 标签包裹的首条用户消息,放在所有对话消息之前。 - -## Attribution Header:计费与安全 - -每个 API 请求的 System Prompt 首块是 Attribution Header(`src/constants/system.ts:30`),包含: -- **`cc_version`**:Claude Code 版本 + 指纹 -- **`cc_entrypoint`**:入口点标识(REPL / SDK / pipe 等) -- **`cch=00000`**(NATIVE_CLIENT_ATTESTATION 启用时):Bun 原生 HTTP 层在发送前将零替换为计算出的哈希值,服务器验证此 token 确认请求来自真实 Claude Code 客户端 - -Header 始终 `cacheScope: null`——它因版本和指纹不同而变化,不适合缓存。 - -## CLAUDE.md:项目级知识注入 - -这是 Claude Code 最巧妙的设计之一。在项目根目录放一个 `CLAUDE.md` 文件,就能让 AI "理解" 你的项目: - -- **项目概述**:这个项目做什么、用了什么技术栈 -- **开发约定**:代码风格、命名规范、分支策略 -- **常用命令**:怎么构建、怎么测试、怎么部署 -- **注意事项**:已知的坑、特殊的配置 - -系统会自动发现并合并多级 CLAUDE.md: - -``` -~/.claude/CLAUDE.md ← 用户全局(个人偏好) - └── /project/CLAUDE.md ← 项目根目录(团队共享) - └── /project/src/CLAUDE.md ← 子目录(模块特定) -``` - -加载逻辑在 `src/utils/claudemd.ts` 中的 `getClaudeMds()` 和 `getMemoryFiles()` 实现——从 CWD 向上遍历目录树,合并所有匹配的 CLAUDE.md 文件内容。 - -## 设计洞察:为什么是 `string[]` 而非单个 `string` - -将 System Prompt 设计为数组而非单段文本,是为了 **缓存分块**: - -1. Anthropic Prompt Cache 以 **内容块**(TextBlock)为缓存单位 -2. 将 System Prompt 拆为多个块,可以让不变的部分(Intro、Rules)获得独立的缓存命中 -3. 如果是单个 `string`,任何一个字符变化(如日期更新)都会导致整个 System Prompt 的缓存失效 -4. `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 标记允许 `splitSysPromptPrefix()` 精确地将静态区标记为 `scope: 'global'`,动态区不标记或标记为 `scope: 'org'` - -这是 Claude Code 在 token 成本优化上的核心设计——一次典型的 System Prompt 约 20K+ tokens,通过缓存分块可以节省 30-50% 的输入 token 费用。 - -## 兼容层:OpenAI 与 Gemini - -Claude Code 提供了 OpenAI 和 Gemini 协议的兼容层,允许使用非 Anthropic 端点。 - -### OpenAI 兼容层 - -通过 `CLAUDE_CODE_USE_OPENAI=1` 启用,支持任意 OpenAI Chat Completions 协议端点(Ollama、DeepSeek、vLLM 等)。 - -实现采用**流适配器模式**: -1. 将 Anthropic 格式请求转换为 OpenAI 格式 -2. 调用 OpenAI 兼容端点 -3. 将 SSE 流转换回 `BetaRawMessageStreamEvent` -4. 下游代码完全无感知 - -``` -src/services/api/openai/ -├── client.ts # OpenAI 客户端配置 -├── convertMessages.ts # 消息格式转换(Anthropic → OpenAI) -├── convertTools.ts # 工具定义转换 -├── streamAdapter.ts # SSE 流适配(OpenAI → Anthropic) -├── modelMapping.ts # 模型名称映射 -└── index.ts # 入口函数 queryModelOpenAI() -``` - -关键环境变量: -- `CLAUDE_CODE_USE_OPENAI=1` — 启用 OpenAI provider -- `OPENAI_API_KEY` — API 密钥 -- `OPENAI_BASE_URL` — API 端点(默认 `https://api.openai.com/v1`) -- `OPENAI_MODEL` — 直接指定模型名 - -### Gemini 兼容层 - -通过 `CLAUDE_CODE_USE_GEMINI=1` 启用,支持 Google Gemini API。 - -``` -src/services/api/gemini/ -├── client.ts # Gemini 客户端配置 -├── convertMessages.ts # 消息格式转换 -├── convertTools.ts # 工具定义转换 -├── streamAdapter.ts # 流适配 -├── modelMapping.ts # 模型名称映射 -├── types.ts # 类型定义 -└── index.ts # 入口函数 -``` - -关键环境变量: -- `CLAUDE_CODE_USE_GEMINI=1` — 启用 Gemini provider -- `GEMINI_API_KEY` — API 密钥 -- `GEMINI_BASE_URL` — API 端点(默认 `https://generativelanguage.googleapis.com/v1beta`) -- `GEMINI_MODEL` — 直接指定模型名 -- `GEMINI_DEFAULT_SONNET_MODEL` / `GEMINI_DEFAULT_OPUS_MODEL` — 按能力级别映射 - -### 兼容层的限制 - -使用 3P 兼容层时,部分功能受限: -- **无精确 token 计数**:系统退回到近似估算,影响自动压缩触发时机 -- **无全局缓存**:只能使用组织级缓存 `scope: 'org'` -- **部分 beta 功能不可用**:依赖 Anthropic 特有 beta headers 的功能受限 - -详见 `docs/plans/openai-compatibility.md` 和 `CLAUDE.md` 中的相关章节。 - diff --git a/docs/context/token-budget.mdx b/docs/context/token-budget.mdx deleted file mode 100644 index a438a4a14..000000000 --- a/docs/context/token-budget.mdx +++ /dev/null @@ -1,195 +0,0 @@ ---- -title: "Token 预算管理 - 上下文窗口动态计算" -description: "从源码角度揭示 Claude Code token 预算管理:200K 上下文窗口的动态计算、截断机制、缓存优化和自动压缩的完整链路。" -keywords: ["Token 预算", "上下文窗口", "token 计算", "截断机制", "缓存优化"] ---- - -{/* 本章目标:从源码角度揭示 token 预算的动态计算、截断机制、缓存优化和自动压缩的完整链路 */} - -## 上下文窗口:200K 不是全部 - -Claude Code 的默认上下文窗口为 200K tokens(`MODEL_CONTEXT_WINDOW_DEFAULT = 200_000`),但实际可用于对话的空间远小于此: - -``` -上下文窗口(200K) -├── 系统提示词(~15-25K,缓存后成本低) -├── 工具定义(~10-20K,含 MCP 工具) -├── 用户上下文(CLAUDE.md、git status 等) -├── 输出预留(maxOutputTokens) -│ ├── 默认上限:64K -│ ├── 实际默认:8K(slot-reservation 优化) -│ └── 触顶自动升级:一次 64K 重试 -└── 剩余:对话历史空间(随对话增长) -``` - -`getContextWindowForModel()`(`src/utils/context.ts:51`)按 5 级优先级解析窗口大小: - -1. `CLAUDE_CODE_MAX_CONTEXT_TOKENS` 环境变量覆盖 -2. 模型名含 `[1m]` 后缀 → 1M tokens -3. `getModelCapability(model).max_input_tokens` -4. 1M beta header + 支持的模型(claude-sonnet-4, opus-4-6) -5. 兜底:200K - -**有效上下文** = 窗口大小 - min(maxOutputTokens, 20K),因为压缩摘要需要预留输出空间。 - -## Token 计数:近似 vs 精确 - -系统使用两级 token 计数策略: - -### 近似估算(毫秒级) - -```typescript -// src/services/tokenEstimation.ts -function roughTokenCountEstimation(content: string, bytesPerToken = 4): number { - return Math.round(content.length / bytesPerToken) -} -``` - -对不同内容类型有特殊处理: -- **JSON/JSONL**:`bytesPerToken = 2`(密集的 `{`, `:`, `,` 符号,每个仅 1-2 token) -- **图片/文档**:固定 2000 tokens(基于 2000×2000px 上限的保守估计) -- **thinking block**:按实际文本长度 / 4 -- **tool_use**:序列化 `name + JSON.stringify(input)` 后 / 4 - -### 精确计数(API 调用) - -使用 Anthropic 的 `beta.messages.countTokens` 端点。在不同 provider 上有不同路径: - -| Provider | 方法 | -|----------|------| -| Anthropic 直连 | `anthropic.beta.messages.countTokens()` | -| AWS Bedrock | `@aws-sdk/client-bedrock-runtime` 的 `CountTokensCommand` | -| Google Vertex | Anthropic SDK + beta 过滤 | -| 兜底(Bedrock 不支持) | 用 Haiku 发送 `max_tokens=1` 的请求,读取 `usage.input_tokens` | - -精确计数在关键决策点使用(压缩前后对比、warning 判断),近似估算在热路径使用(每轮循环的 shouldAutoCompact 检查)。 - -### 3P Provider 的 Token 计数差异 - -不同 Provider 的精确 token 计数实现方式不同,部分 provider 甚至不支持精确计数: - -| Provider | 计数方式 | 注意事项 | -|----------|---------|---------| -| **Anthropic 直连** | `anthropic.beta.messages.countTokens()` | 标准 API,最准确 | -| **AWS Bedrock** | `CountTokensCommand` | 需要动态加载 279KB AWS SDK | -| **Google Vertex** | Anthropic SDK + beta 过滤 | 需要特定 beta headers | -| **OpenAI 兼容层** | 无精确计数 | **退回到近似估算** | -| **Gemini 兼容层** | 无精确计数 | **退回到近似估算** | -| **Bedrock 不支持时** | 用 Haiku 发送 `max_tokens=1` 请求 | 读取 `usage.input_tokens` | - -OpenAI 和 Gemini 兼容层**不支持精确 token 计数**,系统会退回到近似估算。这会影响: -- **自动压缩触发时机**:可能略有偏差 -- **压缩前后 token 对比**:仅为估算值,非精确 -- **Warning/Error 阈值判断**:基于估算而非精确计数 - -```typescript -// src/services/tokenEstimation.ts - 近似估算函数 -function roughTokenCountEstimation(content: string, bytesPerToken = 4): number { - return Math.round(content.length / bytesPerToken) -} -``` - -源码路径:`src/services/tokenEstimation.ts` - -## 自动压缩的触发阈值 - -``` -src/services/compact/autoCompact.ts — 核心阈值 -``` - -| 常量 | 值 | 含义 | -|------|----|------| -| `AUTOCOMPACT_BUFFER_TOKENS` | 13,000 | 窗口减去此值 = 自动压缩触发点 | -| `WARNING_THRESHOLD_BUFFER_TOKENS` | 20,000 | 在触发点 + 20K 处显示警告 | -| `ERROR_THRESHOLD_BUFFER_TOKENS` | 20,000 | 在触发点 + 20K 处显示错误 | -| `MANUAL_COMPACT_BUFFER_TOKENS` | 3,000 | 手动 /compact 的阻塞上限 | -| `MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES` | 3 | 连续失败 3 次后停止尝试 | - -以 200K 窗口为例: -- **~167K**:warning 闪烁,用户看到建议压缩的提示 -- **~180K**:自动压缩触发(200K - 20K 输出预留 = 180K 有效,再 - 13K buffer) -- **~197K**:达到 blocking limit,新消息被阻止 - -`shouldAutoCompact()` 有多个逃逸条件: -- `compact` / `session_memory` 来源的查询永不触发(防递归死锁) -- `DISABLE_COMPACT` / `DISABLE_AUTO_COMPACT` 环境变量 -- 用户配置 `autoCompactEnabled = false` -- Context Collapse 模式激活时抑制(collapse 自己管理上下文) -- Reactive Compact 实验模式下抑制主动压缩 -- 超过连续失败上限(circuit breaker) - -## Micro-Compact:工具结果的渐进式压缩 - -在触发全量压缩之前,系统先尝试 **micro-compact**——只压缩旧的工具调用结果: - -``` -可压缩工具列表(COMPACTABLE_TOOLS): -FileRead, Bash, Grep, Glob, WebSearch, WebFetch, FileEdit, FileWrite -``` - -策略基于时间: -- 超过一定时间(由 `timeBasedMCConfig` 控制)的工具结果被替换为简短占位符 -- 图片/文档结果替换为 `[image]` / `[document]` 文本 -- 每次替换释放 tokens,可能推迟全量压缩 - -工具本身也有 `maxResultSizeChars`(通常 100K)硬限制,超长结果在写入消息前就被截断。 - -## 全量压缩的完整流程 - -``` -autoCompactIfNeeded() / compactConversation() - ↓ -1. 执行 PreCompact hooks(外部可注入自定义指令) - ↓ -2. 尝试 Session Memory 压缩(更轻量,优先尝试) - ↓ -3. Session Memory 失败 → 全量压缩 - a. 图片/文档从消息中剥离(替换为 [image]/[document]) - b. skill_discovery/skill_listing 附件剥离(压缩后会重新注入) - c. 通过 forked agent 发送摘要请求(复用主线程的 prompt cache) - d. 如果摘要请求本身触发 prompt-too-long → truncateHeadForPTLRetry() - 从最老的 API 轮次开始删除,重试最多 3 次 - ↓ -4. 压缩成功后重建上下文: - - compactBoundaryMarker(记录压缩类型、前 token 数等) - - 摘要消息(不可见的 user 消息) - - 最近 5 个文件的重新读取(POST_COMPACT_TOKEN_BUDGET = 50K) - - plan 文件附件(如果有) - - plan mode 指令(如果在计划模式中) - - 已调用的 skill 内容(每 skill ≤5K,总计 ≤25K) - - deferred tools / agent listing / MCP 指令的增量重新注入 - - SessionStart hooks 重新执行 - - PostCompact hooks 执行 - ↓ -5. 更新缓存基线,防止被误判为 cache break -``` - -### Prompt Cache Sharing - -压缩 API 调用是整个会话中最昂贵的操作之一。系统通过 `runForkedAgent` 复用主线程的缓存前缀(system prompt + tools + context messages),将缓存命中率从 2% 提升到接近 100%。这个优化单独节省了舰队级约 0.76% 的 `cache_creation` tokens。 - -## 输出 Token 的 Slot 优化 - -一个经常被忽视的优化:**maxOutputTokens 的动态调整**。 - -```typescript -// src/services/api/claude.ts — getMaxOutputTokensForModel() -const defaultTokens = isMaxTokensCapEnabled() - ? Math.min(maxOutputTokens.default, 8_000) // 默认降到 8K - : maxOutputTokens.default // 原始默认 32K/64K -``` - -为什么?因为 API 的 slot 机制按 `max_tokens` 预留推理容量。BQ p99 输出仅 4,911 tokens,32K 默认值浪费了 8-16 倍的 slot 容量。降到 8K 后,不到 1% 的请求被截断——这些请求会自动获得一次 64K 的 clean retry。 - -这个优化对 token 预算的影响是间接的:更多的 slot 容量意味着更少的排队延迟,间接减少了超时和重试。 - -## Partial Compact:选择性地压缩 - -除了全量压缩,用户还可以在消息历史中选择某个位置,只压缩该位置之前或之后的内容: - -- **`up_to` 方向**:压缩选中消息之前的内容,保留最近的对话 -- **`from` 方向**:压缩选中消息之后的内容,保留早期的对话 - -`from` 方向保留 prompt cache(前缀不变),`up_to` 方向则破坏 cache(摘要插在保留内容之前)。 - -两种方向的 PTL(prompt-too-long)重试策略相同:从最老的 API 轮次开始删除,确保至少保留一组消息供摘要。 diff --git a/docs/conversation/multi-turn.mdx b/docs/conversation/multi-turn.mdx deleted file mode 100644 index 525c73df3..000000000 --- a/docs/conversation/multi-turn.mdx +++ /dev/null @@ -1,519 +0,0 @@ ---- -title: "多轮对话管理 - QueryEngine 会话编排与持久化" -description: "从源码角度解析 Claude Code 多轮对话管理:QueryEngine 的会话状态机、JSONL transcript 持久化、成本追踪模型和模型热切换机制。" -keywords: ["多轮对话", "会话管理", "QueryEngine", "transcript", "成本追踪"] -sourceRef: "3ec5675 (2026-04-08)" ---- - -{/* 本章目标:从源码角度揭示会话编排、持久化存储、成本追踪和模型切换的完整链路 */} - -首先要区分claude code的多种交互方式 - -REPL关注交互形态,SDK关注接入方式,ACP则关注通信协议。 - -### 🆚 核心概念对比 - -| 维度 | 🖥️ REPL (交互形态) | 🧩 SDK (接入方式) | 🌉 ACP (通信协议) | -| :--- | :--- | :--- | :--- | -| **是什么** | 供开发者直接在终端使用的**交互式对话环境** | 面向开发者的**程序化调用库**,供集成到其他应用 | 一种**开放式的通信标准**,连接不同AI Agent与编辑器 | -| **使用方式** | 1. 直接在终端输入`claude`命令
2. 进入专用界面(基于React Ink渲染)
3. 通过斜杠命令(如`/help`)交互 | 1. 在自己的Node.js/Python项目中安装SDK包(如`npm install claude-code-sdk`)
2. 通过API发送查询 | 1. 通过ACP适配器(如`claude-code-acp`)启动Claude Code
2. 供编辑器通过ACP协议与其通信 | -| **典型场景** | 开发者日常编写代码时,随时向其提问、修改代码或执行任务 | 将Claude Code的核心能力(对话、工具执行等)集成到自动化脚本、CI/CD流程或其他应用的后台中 | 将Claude Code的能力集成到JetBrains IDE、Zed等第三方编辑器中,利用其UI交互功能 | -| **主要特点** | - **面向人**:交互式、直观
- **功能完整**:可使用所有内置工具,并支持MCP集成
- **处理复杂任务**:可自主规划、执行多步操作 | - **面向程序**:编程化、可集成
- **轻量级**:不依赖Claude Code的完整运行时
- **由你控制**:适合在自有应用中实现自动化 | - **标准化**:统一不同Agent与编辑器间的通信
- **双向通信**:Agent可主动向编辑器请求文件、执行命令等
- **与编辑器深度整合**:能完全复用Claude Code的能力 | - -其中的 🧩 SDK (接入方式) 与 🌉 ACP (通信协议)采用如下QueryEngine实现会话管理 - -作为一个对话终端(🖥️ REPL 交互形态模式),则使用的是 onQueryImpl 在 src/screens/REPL.tsx 中调用 query() 函数 - -对于REPL 交互形态模式的调用链路如下 -``` -用户输入 - ↓ -onSubmit (REPL.tsx) - ↓ -handlePromptSubmit (handlePromptSubmit.ts) - ↓ -executeUserInput (handlePromptSubmit.ts) - ↓ -onQuery (REPL.tsx) - ↓ -onQueryImpl (REPL.tsx) - ↓ -query (query.ts) ← 在这里调用 -``` - -其中 - -query 函数是 Agentic Loop 的核心实现,包含 while(true) 循环处理对话回合 query.ts:460-522 - -onQueryImpl 是 REPL(Read-Eval-Print Loop)中与 AI 模型交互的核心控制器,它负责: - -1.环境准备(IDE、诊断、权限) - -2.会话标题的首次生成 - -3.构建动态系统提示和用户上下文 - -4.执行流式查询并实时更新 UI - -5.收集性能指标和最终清理 - -## `onQueryImpl` 方法的详细解析 -以下是对 `onQueryImpl` 方法的详细解析。该方法是一个 React `useCallback` 包装的异步函数,负责处理用户消息到 AI 模型(Claude)的**完整查询流程**,包括预处理、系统提示构建、工具上下文准备、流式查询执行、后处理与指标记录。 - ---- - -### 一、函数签名与参数 - -```typescript -const onQueryImpl = useCallback( - async ( - messagesIncludingNewMessages: MessageType[], - newMessages: MessageType[], - abortController: AbortController, - shouldQuery: boolean, - additionalAllowedTools: string[], - mainLoopModelParam: string, - effort?: EffortValue, - ) => { ... }, - [ ...dependencies ] -) -``` - -| 参数 | 说明 | -| -------------------------------- | ---------------------------------------------------------------------------------------- | -| `messagesIncludingNewMessages` | 包含新增消息的完整消息列表,用于构建模型输入 | -| `newMessages` | 本次新增的消息(例如用户刚输入的文本或附件) | -| `abortController` | 用于取消当前查询的控制器 | -| `shouldQuery` | 是否真正执行查询;若为 `false` 则跳过模型调用(例如处理无效斜杠命令、手动 compact 等) | -| `additionalAllowedTools` | 本轮查询额外允许的工具列表(通常来自 Skill 的 frontmatter) | -| `mainLoopModelParam` | 指定本次使用的主模型参数(如 `'claude-3-opus'`) | -| `effort` | 可选,覆盖全局的“努力程度”值(用于控制模型推理深度) | - ---- - -### 二、总体执行流程 - -下图概括了函数的主要分支与关键步骤: - -```mermaid -graph TD -A["开始"] --> B{shouldQuery?} -B -- true --> C["IDE集成:刷新MCP客户端,诊断追踪,关闭差异视图"] -B -- false --> D["仅处理compact边界/重置状态并返回"] -C --> E["标记项目onboarding完成"] -E --> F["尝试生成会话标题(仅一次)"] -F --> G["将additionalAllowedTools写入全局权限store"] -G --> H["获取ToolUseContext(含最新工具/MCP)"] -H --> I["如有effort,临时覆盖getAppState中的effortValue"] -I --> J["并行执行:系统提示/用户上下文/系统上下文/自动模式检查"] -J --> K["构建有效系统提示"] -K --> L["重置各类耗时计时器"] -L --> M["执行query生成器,流式处理事件"] -M --> N["若BUDDY开启,触发companion观察者"] -N --> O["若UDS_INBOX且中断,记录错误"] -O --> P["ant用户:收集API指标并插入指标消息"] -P --> Q["重置加载状态,输出性能报告,调用onTurnComplete"] -Q --> R["结束"] -D --> R -``` - ---- - -### 三、核心逻辑详解 - -#### 3.1 IDE 集成与诊断(仅 `shouldQuery = true`) - -```typescript -const freshClients = mergeClients(initialMcpClients, store.getState().mcp.clients); -diagnosticTracker.handleQueryStart(freshClients); -const ideClient = getConnectedIdeClient(freshClients); -if (ideClient) closeOpenDiffs(ideClient); -``` - -- 从 store 中获取最新的 MCP 客户端(因为 `useManageMCPConnections` 可能在闭包捕获后更新了状态)。 -- 通知诊断追踪器查询开始。 -- 若存在已连接的 IDE 客户端,关闭所有打开的差异视图(清理环境)。 - -#### 3.2 会话标题生成(仅一次) - -```typescript -if (!titleDisabled && !sessionTitle && !agentTitle && !haikuTitleAttemptedRef.current) { - const firstUserMessage = newMessages.find(m => m.type === 'user' && !m.isMeta); - const text = getContentText(firstUserMessage.message.content); - if (text && !text.startsWith(`<${LOCAL_COMMAND_STDOUT_TAG}>`) ... ) { - haikuTitleAttemptedRef.current = true; - generateSessionTitle(text, ...).then(title => setHaikuTitle(title)); - } -} -``` - -- 仅当全局标题未禁用、当前无任何标题且从未尝试过时执行。 -- 从新增消息中提取第一条**非元用户消息**的真实文本。 -- 跳过合成面包屑(如 slash 命令输出、skill 扩展标记等)。 -- 异步调用 `generateSessionTitle`,结果通过 `setHaikuTitle` 保存;失败则重置 ref 允许重试。 - -#### 3.3 权限工具覆盖写入 Store - -```typescript -store.setState(prev => { - const cur = prev.toolPermissionContext.alwaysAllowRules.command; - if (cur === additionalAllowedTools || (cur?.length === ...)) return prev; - return { ...prev, toolPermissionContext: { ...prev.toolPermissionContext, alwaysAllowRules: { ...prev.toolPermissionContext.alwaysAllowRules, command: additionalAllowedTools } } }; -}); -``` - -- 将本轮 `additionalAllowedTools` 写入全局 store 的 `toolPermissionContext.alwaysAllowRules.command`。 -- 用于限定本轮查询中可用的工具集(例如 Skill 专属工具)。 -- 通过浅比较避免不必要的状态更新。 -- 即使在 `shouldQuery=false` 时也会执行(例如 forked 命令需要此权限信息),但原代码位置在 `shouldQuery` 分支**之前**,所以始终会更新。 - -#### 3.4 `shouldQuery = false` 分支 - -```typescript -if (!shouldQuery) { - if (newMessages.some(isCompactBoundaryMessage)) { - setConversationId(randomUUID()); - if (feature('PROACTIVE') || feature('KAIROS')) proactiveModule?.setContextBlocked(false); - } - resetLoadingState(); - setAbortController(null); - return; -} -``` - -- 处理不需要实际调用模型的情况(如用户输入了无效斜杠命令,或者手动 `/compact` 等)。 -- 若新消息中包含 **compact 边界消息**(压缩边界),则: - - 生成新的 `conversationId`,促使 UI 中消息行组件重新挂载。 - - 若开启了 PROACTIVE/KAIROS 特性,清除上下文阻塞标志(恢复主动提示)。 -- 最后重置加载状态并清空 abortController。 - -#### 3.5 查询前置准备(`shouldQuery = true`) - -##### 3.5.1 获取 ToolUseContext - -```typescript -const toolUseContext = getToolUseContext(messagesIncludingNewMessages, newMessages, abortController, mainLoopModelParam); -const { tools: freshTools, mcpClients: freshMcpClients } = toolUseContext.options; -``` - -- `getToolUseContext` 内部会从 store 中读取最新的 tools 和 MCP 客户端配置,确保闭包捕获的旧值不会导致遗漏新连接的工具或 MCP 服务器。 - -##### 3.5.2 Effort 覆盖(临时) - -```typescript -if (effort !== undefined) { - const previousGetAppState = toolUseContext.getAppState; - toolUseContext.getAppState = () => ({ ...previousGetAppState(), effortValue: effort }); -} -``` - -- 如果传入了 `effort` 参数,临时覆盖 `getAppState` 返回的 `effortValue`。 -- 作用域**仅限于本轮查询**,不影响全局 store,避免后台 Agent 或 UI 组件误读到该临时值。 - -##### 3.5.3 并行获取提示与上下文 - -```typescript -const [, , defaultSystemPrompt, baseUserContext, systemContext] = await Promise.all([ - undefined, - feature('TRANSCRIPT_CLASSIFIER') ? checkAndDisableAutoModeIfNeeded(...) : undefined, - getSystemPrompt(freshTools, mainLoopModelParam, additionalWorkingDirectories, freshMcpClients), - getUserContext(), - getSystemContext(), -]); -``` - -- 并行执行以下任务以节省时间: - - **自动模式断路器**:如果启用了转录分类器,检查并可能禁用快速模式(`fastMode`)。 - - **系统提示**:基于最新工具、模型参数、额外工作目录、MCP 客户端生成。 - - **用户上下文**:如当前工作区、环境变量等。 - - **系统上下文**:如操作系统、终端信息等。 - -##### 3.5.4 增强用户上下文 - -```typescript -const userContext = { - ...baseUserContext, - ...getCoordinatorUserContext(freshMcpClients, getScratchpadDir()), - ...((feature('PROACTIVE') || feature('KAIROS')) && proactiveModule?.isProactiveActive() && !terminalFocusRef.current - ? { terminalFocus: 'The terminal is unfocused — the user is not actively watching.' } - : {}), -}; -``` - -- 合并基本用户上下文、协调器上下文(与 MCP 协作相关)、以及可选的终端焦点状态(当 proactive 特性激活且终端未聚焦时,提示模型用户未在观看)。 - -##### 3.5.5 构建最终系统提示 - -```typescript -const systemPrompt = buildEffectiveSystemPrompt({ - mainThreadAgentDefinition, - toolUseContext, - customSystemPrompt, - defaultSystemPrompt, - appendSystemPrompt, -}); -``` - -- 整合主线程 Agent 定义、工具上下文、自定义系统提示、默认系统提示以及需要追加的内容。 - -#### 3.6 执行查询与流式事件处理 - -```typescript -resetTurnHookDuration(); resetTurnToolDuration(); resetTurnClassifierDuration(); -for await (const event of query({ messages, systemPrompt, userContext, systemContext, canUseTool, toolUseContext, querySource })) { - onQueryEvent(event); -} -``` - -- 重置本轮钩子、工具、分类器的耗时计时器。 -- 调用 `query` 生成器函数(负责与模型 API 通信并返回 SSE 事件流)。 -- 遍历每个事件并调用 `onQueryEvent`(通常用于更新 UI 消息列表、处理工具调用等)。 - -#### 3.7 后处理与指标收集 - -##### 3.7.1 BUDDY 特性(companion 反应) - -```typescript -if (feature('BUDDY') && typeof fireCompanionObserver === 'function') { - fireCompanionObserver(messagesRef.current, reaction => setAppState(prev => ({ ...prev, companionReaction: reaction }))); -} -``` - -- 将当前消息列表传递给 companion 观察者,并根据返回的反应更新全局状态。 - -##### 3.7.2 UDS_INBOX 中断处理 - -```typescript -if (feature('UDS_INBOX') && abortController.signal.aborted) { - pipeReturnHadErrorRef.current = true; - relayPipeMessage({ type: 'error', data: 'Slave request was interrupted before completion.' }); -} -``` - -- 若因中断导致查询未完成,标记错误并通过管道中继消息。 - -##### 3.7.3 Ant 内部用户的 API 指标记录 - -```typescript -if (process.env.USER_TYPE === 'ant' && apiMetricsRef.current.length > 0) { - const entries = apiMetricsRef.current; - const ttfts = entries.map(e => e.ttftMs); - const otpsValues = entries.map(e => { /* 计算每请求的 OTPs */ }); - const isMultiRequest = entries.length > 1; - // 创建 API 指标消息并添加到消息列表 - setMessages(prev => [...prev, createApiMetricsMessage({ ttftMs: isMultiRequest ? median(ttfts) : ttfts[0], ... })]); -} -``` - -- 仅当用户类型为 `'ant'` 且存在 API 指标记录时执行。 -- 收集每次请求的 **首字节时间 (TTFT)** 和 **每秒输出 Token 数 (OTPS)**。 -- 若本轮包含多次请求(例如工具调用循环),计算中位数(P50)后存入指标消息。 -- 同时记录钩子耗时、工具耗时、分类器耗时、本轮总时长、配置写入次数等。 - -##### 3.7.4 重置与清理 - -```typescript -resetLoadingState(); -logQueryProfileReport(); -await onTurnComplete?.(messagesRef.current); -``` - -- 重置加载状态(隐藏 loading 指示器)。 -- 输出查询性能报告(如果调试标志启用)。 -- 调用外部传入的 `onTurnComplete` 回调,并传递完整消息列表(通常用于触发后续行为如自动滚动、保存会话等)。 - - -## 单轮 vs 多轮:架构层面的差异 - -- **单轮**(一次 Agentic Loop):`query()` 函数的一次完整执行——组装上下文 → 调 API → 处理工具调用 → 循环直到结束 -- **多轮**(一个 Session):`QueryEngine` 类管理的一次会话——跨越数十轮 `submitMessage()` 调用,持续数小时 - -`QueryEngine`(`src/QueryEngine.ts`,类定义)是单轮 Agentic Loop 之上的**会话编排器**,它管理的状态远不止消息列表: - -``` -QueryEngine 内部状态(src/QueryEngine.ts 构造函数) -├── mutableMessages: Message[] ← 完整对话历史,跨 turn 累积 -├── readFileState: FileStateCache ← 已读文件内容缓存,避免重复读取 -├── totalUsage: NonNullableUsage ← 累计 token 消耗(input/output/cache) -├── permissionDenials: SDKPermissionDenial[] ← 权限拒绝记录 -├── discoveredSkillNames: Set ← 当前 turn 已发现的 skill -├── loadedNestedMemoryPaths: Set ← 已加载的嵌套 memory 路径(防重复) -├── hasHandledOrphanedPermission: boolean ← 是否已处理孤立权限请求 -└── abortController: AbortController ← 会话级中断控制 -``` - -## QueryEngine 的核心方法:submitMessage() - -每次用户输入一条消息,SDK 调用 `submitMessage()`,它会执行完整的 turn 初始化链路: - -```typescript -// src/QueryEngine.ts — QueryEngine.submitMessage() 简化流程 -async *submitMessage( - prompt: string | ContentBlockParam[], - options?: { uuid?: string; isMeta?: boolean }, -): AsyncGenerator { - // 1. 清除 turn 级追踪状态 - this.discoveredSkillNames.clear() - - // 2. 解析模型(用户可能中途通过 setModel() 切换了模型) - const mainLoopModel = this.config.userSpecifiedModel - ? parseUserSpecifiedModel(this.config.userSpecifiedModel) - : getMainLoopModel() - - // 3. 动态组装 System Prompt(每次 turn 都重新构建) - const { defaultSystemPrompt, userContext, systemContext } = - await fetchSystemPromptParts({ tools, mainLoopModel, mcpClients }) - - // 4. 包装权限检查(追踪每次拒绝) - const wrappedCanUseTool = async (tool, input, ...) => { - const result = await canUseTool(tool, input, ...) - if (result.behavior !== 'allow') { - this.permissionDenials.push({ - type: 'permission_denial', - tool_name: sdkCompatToolName(tool.name), - tool_use_id: toolUseID, - tool_input: input, - }) - } - return result - } - - // 5. 调用核心 query() 函数执行 agentic loop - yield* query({ - systemPrompt, messages: this.mutableMessages, - tools, model: mainLoopModel, ... - }) -} -``` - -关键设计:`submitMessage()` 是 `async *Generator`——它逐步 yield `SDKMessage`,让调用方(REPL/SDK)能实时展示进度,而不是等整个 turn 结束。 - -## 会话持久化:JSONL Transcript - -每次对话事件都被追加写入 transcript 文件(`src/utils/sessionStorage.ts`): - -### 存储路径 - -``` -~/.claude/projects//.jsonl -``` - -- 路径由 `getProjectDir(originalCwd)` 生成,使用 `sanitizePath()` 将项目目录路径转换为安全的目录名(非 hash),同一项目目录的会话归入同一子目录 -- 每条记录是一行 JSON(JSONL 格式),支持追加写入而不需要读取-修改-写入整个文件 -- 读取上限为 50MB(`MAX_TRANSCRIPT_READ_BYTES` 常量,`src/utils/sessionStorage.ts`),防止超大会话导致 OOM - -### Transcript 写入器 - -`Project` 类(`src/utils/sessionStorage.ts`,私有类)管理 transcript 的写入。它通过 `writeQueues`(按文件分组的写队列)和 `drainWriteQueue()`(定时批量刷写)确保并发消息追加不会互相覆盖: - -``` -写入流程(异步排队路径): - recordTranscript(sessionId, entry) - ↓ - project.enqueueWrite(filePath, entry) ← 入列到 writeQueues - ↓ - scheduleDrain() ← 设置定时器(FLUSH_INTERVAL_MS) - ↓ - drainWriteQueue() ← 按 MAX_CHUNK_BYTES 分批 - ↓ 写入每批 - appendToFile(path, batchContent) ← 批量追加 - ↓ - 如果配置了远程持久化: - persistToRemote(sessionId, entry) - ├── CCR v2: internalEventWriter('transcript', entry) - └── v1 Ingress: sessionIngress.appendSessionLog(...) - -同步直写路径(用于元数据重写等场景): - appendEntryToFile(fullPath, entry) ← 同步 appendFileSync - ↓ - 失败时 mkdir + 重试 -``` - -### 会话恢复链路 - -`--resume` 参数触发的恢复流程(`src/main.tsx` 中 `--resume` 分支): - -``` -1. 解析 resume 参数: - ├── UUID 格式 → getTranscriptPathForSession(uuid) - ├── .jsonl 文件路径 → 直接使用 - └── boolean → 最近一次会话的 picker - -2. loadTranscriptFromFile(path) - ├── 按 JSONL 行解析 - ├── 过滤出消息类型记录 - └── 重建 Message[] 数组 - -3. 恢复上下文状态: - ├── restoreCostStateForSession(sessionId) ← 恢复累计费用 - ├── 恢复 agentSetting(用户选择的 Agent 类型) - └── 如果有 --rewind-files,恢复文件到指定消息时的快照 - -4. 创建 QueryEngine({ initialMessages: restoredMessages }) - └── 从恢复的消息继续对话 -``` - -## 成本追踪:从 API Usage 到美元 - -成本追踪贯穿三个模块,形成完整的记录→累计→展示链路: - -### 记录层:API 响应中的 Usage - -每个 `message_delta` 事件携带 `usage` 字段(`input_tokens`、`output_tokens`、`cache_creation_input_tokens`、`cache_read_input_tokens`)。`accumulateUsage()` 将增量 usage 累加到会话总量。 - -### 累计层:cost-tracker.ts - -```typescript -// src/cost-tracker.ts — StoredCostState 类型定义 -type StoredCostState = { - totalCostUSD: number // 累计美元花费 - totalAPIDuration: number // API 调用总时长(含重试) - totalAPIDurationWithoutRetries: number // 不含重试的纯推理时间 - totalToolDuration: number // 工具执行总时长 - totalLinesAdded: number // 代码增加行数 - totalLinesRemoved: number // 代码删除行数 - lastDuration: number | undefined // 最近一次会话时长 - modelUsage: { [modelName: string]: ModelUsage } | undefined // 按模型分拆的用量 -} -``` - -`addToTotalSessionCost()` 根据模型定价计算每次 API 调用的费用,累计到 `totalCostUSD`。按模型的 `ModelUsage` 支持在同一会话中切换模型后分别统计。 - -### 持久化:跨重启保留 - -```typescript -// 每次会话结束时保存到项目配置 -saveCurrentSessionCosts(sessionId) - → projectConfig.lastCost = totalCostUSD - → projectConfig.lastSessionId = sessionId - → projectConfig.lastModelUsage = modelUsage -``` - -### 预算熔断 - -`QueryEngineConfig.maxBudgetUsd` 提供了会话级的硬性预算上限。在 REPL 中,当累计费用超过 $5 时(`src/screens/REPL.tsx` 中费用阈值 `useEffect`),弹出费用提醒对话框——这不是硬性阻断,而是"软提醒",且仅在 `hasConsoleBillingAccess()` 为 true 时显示。 - -## 模型热切换 - -在一个会话中切换模型不会丢失对话历史——因为 `mutableMessages` 与模型选择是解耦的: - -``` -/model sonnet → QueryEngine.setModel('claude-sonnet-4-20250514') - ↓ 实际操作:this.config.userSpecifiedModel = model(QueryEngine.setModel() 方法) -下一次 submitMessage() 开始时: - ↓ -parseUserSpecifiedModel(this.config.userSpecifiedModel) - → 返回新的模型配置 - ↓ -fetchSystemPromptParts({ mainLoopModel: newModel }) - → System Prompt 根据新模型能力重新组装 - ↓ -query({ model: newModel, messages: this.mutableMessages }) - → 使用完整历史 + 新模型继续对话 -``` - -切换模型时,`contextWindowTokens` 和 `maxOutputTokens` 也会根据新模型的规格重新计算——例如从 Sonnet 切换到 Opus 时,上下文窗口可能从 200K 变为 1M。 - -## 文件快照与回滚 - -`fileHistoryMakeSnapshot()`(`src/utils/fileHistory.ts`)在 AI 每次修改文件前自动保存当前内容。快照绑定到具体的 `message.id`,使得 `--rewind-files ` 可以精确恢复到对话中任意时间点的文件状态——这比 git 更细粒度(git 只追踪已提交的内容)。 diff --git a/docs/conversation/streaming.mdx b/docs/conversation/streaming.mdx deleted file mode 100644 index 2d9c18811..000000000 --- a/docs/conversation/streaming.mdx +++ /dev/null @@ -1,192 +0,0 @@ ---- -title: "流式响应机制 - Claude Code 打字机效果原理" -description: "解析 Claude Code 流式响应实现:如何通过 SSE 逐 token 接收 AI 输出,实现实时打字机效果,提升用户等待体验。" -keywords: ["流式响应", "SSE", "streaming", "实时输出", "API streaming"] -sourceRef: "3ec5675 (2026-04-08)" ---- - -## 为什么需要流式 - -想象 AI 需要 30 秒才能生成完整回答——如果等 30 秒后才一次性显示,用户体验是灾难性的。 - -流式响应让用户**实时看到 AI 的思考过程**: -- 文字逐字出现,用户能提前判断方向是否正确 -- 工具调用的参数在生成过程中就能预览 -- 长时间任务不会让用户觉得"卡死了" - -## `BetaRawMessageStreamEvent` 核心事件类型 - -流式 API 返回的是一系列 `BetaRawMessageStreamEvent`,每种事件类型对应流式响应的不同阶段(`src/services/api/claude.ts`): - -``` -message_start ← 消息开始,包含 model、usage 初始值 - ├── content_block_start ← 内容块开始(text / tool_use / thinking) - │ ├── content_block_delta ← 增量数据(text_delta / input_json_delta / thinking_delta) - │ ├── content_block_delta ← ... 持续到达 - │ └── content_block_stop ← 内容块结束,yield AssistantMessage - ├── content_block_start ← 下一个内容块... - │ └── ... - └── message_delta ← stop_reason + 最终 usage -message_stop ← 消息结束 -``` - -### 事件处理状态机 - -`src/services/api/claude.ts` 中 `queryModelWithStreaming()` 函数的事件处理循环实现了一个基于 `switch(part.type)` 的状态机: - -| 事件类型 | 处理逻辑 | 状态变更 | -|----------|----------|----------| -| `message_start` | 初始化 `partialMessage`,记录 TTFT(首字节延迟) | `usage` 初始化 | -| `content_block_start` | 按 `part.index` 创建对应类型的内容块 | `contentBlocks[index]` 初始化 | -| `content_block_delta` | 按子类型增量追加数据 | text / thinking / input 累加 | -| `content_block_stop` | 构建完整 `AssistantMessage` 并 yield | 消息推入 `newMessages` | -| `message_delta` | 更新 stop_reason 和最终 usage | 写回最后一条消息 | -| `message_stop` | 无操作(流结束标记) | — | - -### 内容块类型及其增量数据 - -`content_block_start` 中的 `content_block.type` 决定了如何处理后续 delta: - -| 内容块类型 | Delta 类型 | 累加逻辑 | -|-----------|-----------|----------| -| `text` | `text_delta` | `text += delta.text` | -| `thinking` | `thinking_delta` + `signature_delta` | `thinking += delta.thinking`,`signature = delta.signature` | -| `tool_use` | `input_json_delta` | `input += delta.partial_json`(JSON 字符串增量拼接) | -| `server_tool_use` | `input_json_delta` | 同 tool_use | -| `connector_text` | `connector_text_delta` | 特殊连接器文本(feature flag 控制) | - -关键设计:`content_block_start` 时所有文本字段初始化为空字符串,只通过 `content_block_delta` 累加。这是因为 SDK 有时在 start 和 delta 中重复发送相同文本。 - -## 文本 chunk 和 tool_use block 的交织 - -一次 AI 响应可能包含多个内容块,交替出现: - -``` -content_block_start (text, index=0) "我来帮你修复这个 bug。" -content_block_delta (text_delta) "首先..." -content_block_stop (index=0) -content_block_start (tool_use, index=1) { name: "Read", input: "..." } -content_block_delta (input_json_delta) '{"file_p' → 'ath":' → '"src/foo.ts"}' -content_block_stop (index=1) -content_block_start (text, index=2) "我已经看到了问题所在..." -content_block_stop (index=2) -``` - -每个 `content_block_stop` 触发一次 `yield`,将完整的 AssistantMessage 推送给消费者。这意味着一个 AI 响应会产生**多条** `AssistantMessage`——文本消息和工具调用消息交替产出。 - -`stop_reason` 要等到 `message_delta` 才确定(可能是 `end_turn`、`tool_use`、`max_tokens` 等),所以最后一条消息的 `stop_reason` 是**回写**的: - -```typescript -// claude.ts — stop_reason 回写逻辑(直接属性修改,不用对象替换) -// 因为 transcript 写队列持有 message.message 的引用 -const lastMsg = newMessages.at(-1) -if (lastMsg) { - lastMsg.message.usage = usage - lastMsg.message.stop_reason = stopReason -} -``` - -## 流式中的错误处理 - -### 网络断开 - -流式连接依赖 SSE(Server-Sent Events)。当连接中断时,系统有两层检测机制: - -1. **被动停滞检测**(`src/services/api/claude.ts` 中 stall 检测逻辑):当下一个事件到达时,计算与上一个事件的时间间隔。超过阈值(30 秒,`STALL_THRESHOLD_MS = 30_000`)记录为一次 stall,累积计数并写入遥测日志。这是被动检测——仅在下一个 chunk 到达时才触发,不会主动中断流。 -2. **主动空闲超时看门狗**(`src/services/api/claude.ts` 中 `STREAM_IDLE_TIMEOUT_MS` 看门狗逻辑):使用 `setTimeout` 设置 90 秒(可通过 `CLAUDE_STREAM_IDLE_TIMEOUT_MS` 环境变量覆盖)的硬性超时。如果在此期间没有收到任何事件,主动终止流并抛出错误进入重试流程。 -3. **非流式降级**:作为最后手段,设置 `didFallBackToNonStreaming` 标志,通过 `executeNonStreamingRequest()` 回退到非流式请求(一次性获取完整响应)。 - -```typescript -// claude.ts — 被动停滞检测 -const STALL_THRESHOLD_MS = 30_000 // 30 秒无事件视为停滞 -let totalStallTime = 0 -let stallCount = 0 - -// claude.ts — 主动空闲超时 -const STREAM_IDLE_TIMEOUT_MS = - parseInt(process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS || '', 10) || 90_000 -``` - -### API 限流 - -当 API 返回限流错误时,系统使用 `withRetry` 包装器进行指数退避重试。重试逻辑考虑了: -- 错误类型(429 限流 vs 500 服务器错误) -- 重试次数上限 -- 退避间隔 - -### Token 超限 - -两种 token 超限场景有不同的处理: - -| 场景 | stop_reason | 处理方式 | -|------|------------|----------| -| **输出超限** | `max_tokens` | 生成错误消息,建议设置 `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | -| **上下文窗口超限** | `model_context_window_exceeded` | 触发 compaction 压缩对话历史后重试 | - -```typescript -// claude.ts — stop_reason 处理 -if (stopReason === 'max_tokens') { - yield createAssistantAPIErrorMessage({ error: 'max_output_tokens', ... }) -} -if (stopReason === 'model_context_window_exceeded') { - // 复用 max_output_tokens 的恢复路径 - yield createAssistantAPIErrorMessage({ error: 'max_output_tokens', ... }) -} -``` - -### 流式停滞检测 - -系统持续监控事件到达间隔,检测"停滞"(stall): - -```typescript -// claude.ts — stall 检测逻辑 -const STALL_THRESHOLD_MS = 30_000 // 30 秒无事件视为停滞 -if (timeSinceLastEvent > STALL_THRESHOLD_MS) { - stallCount++ - totalStallTime += timeSinceLastEvent - logEvent('tengu_streaming_stall', { stall_duration_ms, stall_count, ... }) -} -``` - -这是**被动检测**——仅在下一个 chunk 到达时才触发比较。与之互补的是 90 秒主动空闲超时看门狗(`STREAM_IDLE_TIMEOUT_MS`),会直接中断长时间无响应的流。 - -## 工具执行的流式反馈 - -BashTool 的命令执行也是流式的——通过 `onProgress` 回调逐行推送输出: - -``` -BashTool.call() → runShellCommand() → AsyncGenerator - ├── 每秒轮询输出文件 → onProgress(lastLines, allLines, ...) - ├── yield { type: 'progress', output, fullOutput, elapsedTimeSeconds } - └── return { code, stdout, interrupted, ... } -``` - -UI 层通过 `useToolCallProgress` hook 实时展示命令输出,而不是等命令完全结束。长时间运行的命令还支持自动后台化(`shouldAutoBackground`)。 - -## 多 Provider 适配 - -| Provider | 流式协议 | 特殊处理 | -|----------|----------|----------| -| **firstParty** (Anthropic Direct) | 原生 SSE | 延迟最低,TTFT 最快 | -| **AWS Bedrock** | AWS SDK 流式接口 | 需要额外的 beta header 和认证 | -| **Google Vertex** | gRPC → 事件流 | 通过 `getMergedBetas()` 适配 | -| **foundry** | Anthropic 兼容 API | 内部部署 | -| **openai** | OpenAI 流式适配器 | 转换为 Anthropic 内部格式 | -| **gemini** | Gemini 流式适配器 | 转换为 Anthropic 内部格式 | -| **grok** (xAI) | Grok 流式适配器 | 转换为 Anthropic 内部格式 | - -所有 Provider 通过统一的 `Stream` 抽象层屏蔽差异。上层代码(QueryEngine、REPL)不需要关心底层用的是哪个 Provider。 - -### Provider 选择 - -`src/utils/model/providers.ts` 中的 `getAPIProvider()` 根据配置决定使用哪个 Provider: - -```typescript -// 根据 api_provider 配置选择: -// "anthropic" → 直连 -// "bedrock" → AWS SDK -// "vertex" → Google SDK -// 第三方 base URL → 自动检测 -``` - -每个 Provider 需要适配的细节包括:认证方式、beta header、请求参数格式、错误码映射——但这些差异在 `claude.ts` 的 `queryStream()` 函数中被统一处理。 diff --git a/docs/conversation/the-loop.mdx b/docs/conversation/the-loop.mdx deleted file mode 100644 index 7edd8085f..000000000 --- a/docs/conversation/the-loop.mdx +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: "Agentic Loop:AI 自主循环的核心机制" -description: "深入解析 Claude Code 的 query() 异步生成器循环——从流式 API 调用、工具并行执行、上下文压缩、错误恢复到终止条件的完整状态机,基于 src/query.ts 的源码级分析。" -keywords: ["Agentic Loop", "query loop", "tool_use", "状态机", "auto-compact", "streaming", "recovery"] -sourceRef: "3ec5675 (2026-04-08)" ---- - -{/* 本章目标:基于 src/query.ts 揭示 Agentic Loop 的完整状态机 */} - -## 什么是 Agentic Loop - -传统聊天机器人:你问一句,它答一句。 -Claude Code 不一样:你说一个需求,它可能连续执行十几步操作才给你最终结果。 - -这背后的机制叫做 **Agentic Loop**(智能体循环),核心实现在 `src/query.ts` 的 `queryLoop()` 异步生成器函数。它是一个 `while(true)` 无限循环,每次迭代代表一次"思考→行动→观察"周期。 - - - Agentic Loop 循环图 - - -## 循环的完整结构 - -`queryLoop()` 的每次迭代(`src/query.ts` 中 `while(true)` 主循环)包含以下阶段: - -### 阶段 1:上下文预处理(Pre-Processing Pipeline) - -在调用 API 之前,依次执行 5 个压缩/优化步骤: - -``` -messagesForQuery(原始消息) - ↓ applyToolResultBudget() — 工具结果预算截断(按 maxResultSizeChars) - ↓ snipCompactIfNeeded() — 历史 Snip 压缩(HISTORY_SNIP feature) - ↓ microcompact() — 微压缩(工具结果摘要) - ↓ applyCollapsesIfNeeded() — 上下文折叠(CONTEXT_COLLAPSE feature) - ↓ autocompact() — 自动压缩(超出阈值时触发) -messagesForQuery(处理后的消息)→ 发往 API -``` - -每个步骤的输出是下一步的输入,形成串行管道。Snip 和 Microcompact 的释放 token 数会传递给 autocompact 的阈值计算(`snipTokensFreed`),避免重复压缩。 - -### 阶段 2:流式 API 调用(Streaming Loop) - -`deps.callModel()` 发起流式请求(`src/query.ts` 中 `attemptWithFallback` 循环内),返回一个 AsyncGenerator。在流式过程中: - -- **AssistantMessage** 被收集到 `assistantMessages[]` 数组 -- **tool_use 块** 被提取到 `toolUseBlocks[]`,设置 `needsFollowUp = true` -- **StreamingToolExecutor** 在流式过程中就开始并行执行工具(不等流结束) -- 可恢复的错误(prompt-too-long、max-output-tokens)被**暂扣**(withheld),先尝试恢复 - -流式回调中的关键守卫: -- `backfillObservableInput()` —— 为 tool_use 块回填可观察字段(如文件路径展开),但只在添加了新字段时才克隆消息,避免破坏 prompt cache 的字节一致性 -- 流式降级检测——如果 `streamingFallbackOccured`,已收集的消息被标记为 tombstone,清空后重试 - -### 阶段 3:工具执行(Tool Execution) - -如果 `needsFollowUp` 为 true,循环不会终止,而是执行工具: - -```typescript -// 两种工具执行器(互斥) -const toolUpdates = streamingToolExecutor - ? streamingToolExecutor.getRemainingResults() // 流式:获取已完成的+等待中的 - : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext) -``` - -工具结果通过 `normalizeMessagesForAPI()` 标准化后,与原始消息合并,进入**下一轮循环迭代**。 - -### 阶段 4:终止或继续 - -每次迭代结束时,根据条件决定 `return`(终止)或 `continue`(继续): - -## 终止条件(源码级) - -循环有多种终止路径,按触发时机排列: - -| 终止原因 | 触发位置 | 机制 | -|----------|---------|------| -| **blocking_limit** | 第 686 行 | Token 计数超过硬限制(非 autocompact 模式)→ 生成 PTL 错误消息 → 返回 | -| **image_error** | 第 1021 行 | `ImageSizeError` / `ImageResizeError` 异常 → 直接返回 | -| **model_error** | 第 1040 行 | `callModel()` 抛出不可恢复异常 → 生成错误消息 → 返回 | -| **aborted_streaming** | 第 1095 行 | `abortController.signal.aborted`(流式阶段)→ 为未完成的 tool_use 生成合成 tool_result → 返回 | -| **prompt_too_long** | 第 1219/1226 行 | 413 错误且 reactive compact 无法恢复 → 暂扣的错误消息被释放 → 返回 | -| **completed** | 第 1308 行 | API 错误(限流、认证失败等)导致无法继续 → 返回 | -| **stop_hook_prevented** | 第 1323 行 | Stop hook 返回 `preventContinuation: true` → 返回 | -| **completed** | 第 1401 行 | 正常完成:AI 未发出 tool_use → `needsFollowUp = false` → 经过 stop hooks → 返回 | -| **aborted_tools** | 第 1559 行 | `abortController.signal.aborted`(工具执行阶段)→ 返回 | -| **hook_stopped** | 第 1564 行 | 工具执行期间 hook 返回 `shouldPreventContinuation` → 返回 | -| **max_turns** | 第 1755 行 | 轮次计数超过 `maxTurns` 限制 → 返回 | - -## 继续条件(恢复路径) - -循环不仅是一个简单的"有 tool_use 就继续",它还包含多种恢复/重试路径: - -### 1. 正常工具循环(`next_turn`) -`needsFollowUp = true` → 执行工具 → 新消息追加到 `messagesForQuery` → state 重新赋值 → `continue` - -### 2. max_output_tokens 恢复(`max_output_tokens_escalate` / `max_output_tokens_recovery`) -当 AI 输出被截断时(`apiError === 'max_output_tokens'`),分两阶段恢复: -- **提升阶段**(`max_output_tokens_escalate`):首次截断时,将 `maxOutputTokens` 从默认值提升到 `ESCALATED_MAX_TOKENS`(64K)。静默重试,不注入 meta 消息。 -- **恢复阶段**(`max_output_tokens_recovery`):提升后仍然截断时,注入恢复消息"Output token limit hit. Resume directly...",最多重试 `MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3` 次。恢复耗尽后,暂扣的错误消息被释放。 - -### 3. Prompt-Too-Long 恢复(`collapse_drain_retry` / `reactive_compact_retry`) -当遇到 413 错误时,按优先级尝试两种压缩策略: -- **Context Collapse Drain**(`collapse_drain_retry`):提交所有已暂存的折叠(collapse),释放空间后重试。如果上一轮已经是 `collapse_drain_retry` 则跳过,避免无限循环。 -- **Reactive Compact**(`reactive_compact_retry`):如果 collapse drain 无法恢复,触发即时压缩(reactive compact),生成摘要后重试。`hasAttemptedReactiveCompact` 标志防止无限循环。 - -### 4. Stop Hook 阻塞重试(`stop_hook_blocking`) -Stop hook 可以注入阻塞错误消息,强制 AI 重新思考。新的消息(包含阻塞错误)被追加到对话中,`stopHookActive = true`,进入下一轮迭代。 - -### 5. Token Budget 继续提示(`token_budget_continuation`) -当 `TOKEN_BUDGET` feature 启用时,如果 token 消耗达到阈值但未超出预算,注入 nudge 消息让 AI 加速收尾,然后继续。 - -## 模型降级(Fallback) - -当主模型不可用时(`FallbackTriggeredError`,`src/query.ts` 中 `attemptWithFallback` 循环的 catch 分支): - -1. 已收集的 `assistantMessages` 被清空,tool_use 块收到合成 tool_result:"Model fallback triggered" -2. 思维签名块被移除(`stripSignatureBlocks`)—— 因为思维签名与模型绑定,跨模型回放会 400 -3. 切换到 `fallbackModel`,更新 `toolUseContext.options.mainLoopModel` -4. 生成系统消息:"Switched to {fallback} due to high demand for {original}" -5. 重新发起流式请求 - -## 状态机:State 对象 - -每次迭代的状态通过 `State` 类型(`src/query.ts`,类型定义)传递: - -```typescript -// src/query.ts — State 类型定义 -type State = { - messages: Message[] // 当前对话消息 - toolUseContext: ToolUseContext // 工具上下文(含权限) - autoCompactTracking: AutoCompactTrackingState | undefined // 压缩跟踪 - maxOutputTokensRecoveryCount: number // 输出截断恢复计数 - hasAttemptedReactiveCompact: boolean // 是否已尝试即时压缩 - maxOutputTokensOverride: number | undefined // 输出 token 上限覆盖 - pendingToolUseSummary: Promise<...> | undefined // 异步工具摘要 - stopHookActive: boolean | undefined // Stop hook 是否激活 - turnCount: number // 轮次计数 - transition: Continue | undefined // 上一次继续的原因 -} -``` - -每次 `continue` 都创建新的 State 对象(不可变更新),而非就地修改。`transition` 字段记录了为什么继续——让后续迭代能检测特定恢复路径(如 `collapse_drain_retry`)避免循环。 - -## Token Budget(实验性) - -当 `TOKEN_BUDGET` feature 启用时(`src/query.ts` 中 `!needsFollowUp` 分支内的预算检查逻辑),循环在终止前会检查 token 消耗: - -- **continuation**:未达到预算但超过阈值 → 注入 nudge 消息,让 AI 加速收尾 -- **diminishing_returns**:检测到收益递减 → 提前终止 -- 预算数据来自 `createBudgetTracker()`,跨迭代累计 - -## 为什么不是"一次规划,批量执行" - - -源码揭示了为什么 Claude Code 选择逐步循环: - - -- **每一步都产生真实信息**:`runTools()` 返回的 `toolResults` 是 API 不可能预知的——命令输出、文件内容、错误信息 -- **动态上下文管理**:每轮迭代前都重新评估压缩需求(autocompact → microcompact → snip),基于最新的 token 计数 -- **错误即时恢复**:工具失败不需要推倒重来——stop hook 可以注入阻塞错误让 AI 修正策略 -- **用户可控**:`abortController.signal` 在循环的多个检查点被检测(第 1059、1095、1529 行),用户按 ESC 可以优雅中断 -- **成本控制**:Token Budget 在每轮终止前检查,防止 AI 无效循环 - -## 一个完整的迭代示例 - -> 用户:"帮我找到项目里所有未使用的导入语句,然后删掉它们" - -``` -迭代 1: 思考→行动 - 预处理管道: applyToolResultBudget → snipCompact(HISTORY_SNIP feature) → microcompact → applyCollapses(CONTEXT_COLLAPSE feature) → autocompact - → 上下文很短,无需压缩 - API 调用: 返回 tool_use(Glob, "**/*.ts") - 工具执行: 返回 42 个文件路径 - → needsFollowUp = true - → transition: { reason: 'next_turn' }, continue - -迭代 2: 思考→行动 - 预处理管道: 42 个文件结果仍在预算内 - API 调用: 返回 tool_use(Grep, "import.*from") - 工具执行: 在 15 个文件中找到 120 条 import - → needsFollowUp = true - → transition: { reason: 'next_turn' }, continue - -迭代 3: 思考→行动(多轮) - 预处理管道: 120 条 Grep 结果触发 microcompact → 摘要化 - API 调用: 返回 3 个 tool_use(FileEdit, ...) - 工具执行: 删除 5 条未使用导入 - → needsFollowUp = true - → transition: { reason: 'next_turn' }, continue - -迭代 4: 总结 - API 调用: 返回纯文本"已清理 3 个文件中的 5 条未使用导入" - → needsFollowUp = false - → Stop hooks 通过 - → Token Budget 检查通过(如果启用) - → return { reason: 'completed' } -``` diff --git a/docs/design/tool-search-design-guide.md b/docs/design/tool-search-design-guide.md deleted file mode 100644 index d2c7b857f..000000000 --- a/docs/design/tool-search-design-guide.md +++ /dev/null @@ -1,323 +0,0 @@ -# ToolSearch 设计指南 - -> 基于 feature/tool_search 分支的 4 次 commit 迭代,系统性地记录 ToolSearch 的架构、核心机制、演进历史和维护指南。 - -## 1. 问题背景 - -Claude Code 内置了 60+ 工具,加上用户连接的 MCP 服务器可能引入数十甚至上百个额外工具。将所有工具的完整 schema 一次性发送给模型,会产生几个严重问题: - -1. **Token 爆炸** — 每个工具定义(name + description + inputSchema)平均消耗数百 token,60 个工具就是数万 token 的常量开销。 -2. **Prompt Cache 失效** — 工具列表作为 prompt 的一部分参与缓存计算。任何工具的增减(如 MCP 服务器连接/断开)都会导致整段缓存失效。 -3. **模型注意力稀释** — 过多的工具定义干扰模型对核心工具的选择准确性。 - -## 2. 解决方案概览 - -ToolSearch 采用 **延迟加载(Deferred Loading)** 模式: - -- 将工具分为 **Core Tools**(始终加载)和 **Deferred Tools**(按需发现) -- 模型通过 `SearchExtraTools` 工具搜索并发现 deferred tools -- 通过 `ExecuteExtraTool` 工具代理执行发现的 deferred tools -- **工具数组在会话中保持稳定**,不再动态注入已发现的 deferred tools(v3 修复的关键决策) - -## 3. 核心架构 - -### 3.1 工具分类体系 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ All Tools (60+ built-in + MCP) │ -├───────────────────────────┬─────────────────────────────────┤ -│ Core Tools (29 个) │ Deferred Tools (其余全部) │ -│ 始终加载,直接调用 │ 不加载 schema,按需发现 │ -│ CORE_TOOLS 白名单定义 │ isDeferredTool() 判定 │ -└───────────────────────────┴─────────────────────────────────┘ -``` - -**Core Tools**(`src/constants/tools.ts` 中的 `CORE_TOOLS` Set): - -| 类别 | 工具 | -|------|------| -| 文件操作 | Bash/Shell, Read, Edit, Write, Glob, Grep, NotebookEdit | -| Agent 交互 | Agent, AskUserQuestion | -| 任务管理 | TaskOutput, TaskStop, TaskCreate, TaskGet, TaskList, TaskUpdate, TodoWrite | -| 规划 | EnterPlanMode, ExitPlanMode, VerifyPlanExecution | -| Web | WebFetch, WebSearch | -| 代码智能 | LSP | -| 技能 | Skill | -| 调度/监控 | Sleep | -| 工具发现 | SearchExtraTools, ExecuteExtraTool, SyntheticOutput | - -**isDeferredTool 判定逻辑**(`packages/builtin-tools/src/tools/SearchExtraToolsTool/prompt.ts`): - -``` -isDeferredTool(tool) = - tool.alwaysLoad === true? → false(显式跳过延迟) - CORE_TOOLS.has(tool.name)? → false(核心工具不延迟) - otherwise → true(其余全部延迟) -``` - -### 3.2 三层组件架构 - -``` -┌──────────────────────────────────────────────────────┐ -│ API Layer (src/services/api/claude.ts) │ -│ ├─ 判定是否启用 ToolSearch │ -│ ├─ 过滤 deferred tools 不进入 API tools 数组 │ -│ ├─ 注入 或 delta 附件 │ -│ └─ 处理 tool_reference/text 格式的消息归一化 │ -├──────────────────────────────────────────────────────┤ -│ Query Loop (src/query.ts) │ -│ ├─ Turn-zero 预取:用户输入时触发 │ -│ └─ Inter-turn 预取:assistant turn 后异步触发 │ -├──────────────────────────────────────────────────────┤ -│ Search Engine │ -│ ├─ SearchExtraToolsTool — 搜索入口(4 种查询模式) │ -│ ├─ TF-IDF Index (toolIndex.ts) — 语义搜索 │ -│ ├─ Keyword Search — 精确匹配 │ -│ └─ ExecuteExtraTool — 代理执行 │ -└──────────────────────────────────────────────────────┘ -``` - -### 3.3 搜索引擎设计 - -SearchExtraToolsTool 支持四种查询模式: - -| 模式 | 语法 | 行为 | 返回 | -|------|------|------|------| -| **Select** | `select:CronCreate,Snip` | 按名称直接获取,逗号分隔多选 | 精确匹配列表 | -| **Discover** | `discover:schedule cron job` | 纯发现模式,返回描述+schema | 工具信息文本 | -| **Keyword** | `notebook jupyter` | 关键词搜索 | 按相关性排序 | -| **Required** | `+slack send` | `+` 前缀强制包含 | 包含必选词的结果 | - -**混合搜索算法**: - -``` -最终分数 = 关键词分数 × 0.4 + TF-IDF 分数 × 0.6 -``` - -- **Keyword Search**:基于工具名解析(CamelCase 分词、MCP 前缀拆解)、searchHint 匹配、描述文本匹配,加权计分 -- **TF-IDF Search**:复用 `skillSearch/localSearch.ts` 的算法,对 name (3.0)、searchHint (2.5)、description (1.0) 三个字段加权计算 TF-IDF 向量 - -**MCP 工具名解析**: - -``` -mcp__slack__send_message → parts: ["slack", "send", "message"] -CamelCase → parts: ["cron", "create"] -``` - -### 3.4 执行管道 - -``` -模型调用 ExecuteExtraTool({tool_name: "CronCreate", params: {...}}) - ↓ -ExecuteTool.call() 在全局工具注册表中查找 CronCreate - ↓ -检查目标工具 isEnabled() — 桥接/条件工具可能不可用 - ↓ -委托目标工具的 checkPermissions() — 权限传递给实际工具 - ↓ -调用目标工具的 call() — 与直接调用完全等价 - ↓ -返回结果(包装为 ExecuteExtraTool 的 output schema) -``` - -关键设计:ExecuteExtraTool 的 `checkPermissions()` 返回 `passthrough`,将权限决策完全委托给目标工具。它本身不引入额外的权限层。 - -### 3.5 Prompt Cache 稳定性策略(v3 关键修复) - -**问题**:早期版本在发现 deferred tool 后会将其注入 API tools 数组,导致每次发现新工具时 tools JSON 变化,prompt cache 全面失效。 - -**修复**(commit `c14b7ead`):deferred tools **始终不进入 API tools 数组**。tools 数组在整个会话中只包含 core tools + SearchExtraTools + ExecuteExtraTool,保持稳定。 - -``` -API Tools 数组(会话期间不变): - [Core Tools (29)] + [SearchExtraTools, ExecuteExtraTool, SyntheticOutput] - - 不包含: 任何 deferred tool(即使已被发现) - 执行方式: 通过 ExecuteExtraTool 代理调用 -``` - -## 4. 预取机制(Prefetch) - -### 4.1 两个触发时机 - -1. **Turn-zero**(`getTurnZeroSearchExtraToolsPrefetch`)— 用户输入第一轮时,基于输入文本搜索相关 deferred tools,以 attachment 形式注入 -2. **Inter-turn**(`startSearchExtraToolsPrefetch`)— assistant turn 结束后,基于对话上下文异步搜索 - -### 4.2 Attachment 管道 - -``` -prefetch → Attachment(type: 'tool_discovery') - → messages.ts 转换为 system-reminder - → "The following tools were discovered... Use ExecuteExtraTool to invoke..." -``` - -### 4.3 会话去重 - -`discoveredToolsThisSession` Set 跟踪已发现的工具,避免重复推荐。该 Set 独立于 skill prefetch 的去重集合,互不影响。使用 `addBoundedSessionEntry()` 保持上限 500 条,超出时裁剪到 400 条。 - -## 5. 模式切换系统 - -通过环境变量 `ENABLE_SEARCH_EXTRA_TOOLS` 控制: - -| 环境变量值 | 模式 | 行为 | -|-----------|------|------| -| 未设置 | `tst` | 默认启用,始终延迟非核心工具 | -| `true` | `tst` | 强制启用 | -| `false` | `standard` | 完全禁用,所有工具内联加载 | -| `auto` | `tst-auto` | 仅当 deferred tools 超过上下文窗口 10% 时启用 | -| `auto:N` | `tst-auto` | 自定义阈值百分比(N=0 启用,N=100 禁用) | -| `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` | `standard` | 全局 kill switch | - -`isSearchExtraToolsEnabledOptimistic()` — 快速判断(不检查阈值),用于工具注册 -`isSearchExtraToolsEnabled()` — 完整判断(含阈值检查),用于 API 调用 - -## 6. Deferred Tools Delta 机制 - -对于 Anthropic 内部用户(`USER_TYPE=ant`)或启用了 `tengu_glacier_2xr` feature flag 的用户,使用 **delta attachment** 替代 `` 头部注入: - -- 首次:注入完整的 deferred tools 列表 -- 后续:只注入增量变化(新增/移除) -- 优势:不会因为工具池变化导致整个头部缓存失效 - -Delta attachment 扫描历史消息中的 `deferred_tools_delta` 类型 attachment,重建已宣告集合,然后差分计算当前 deferred pool 的变化。 - -## 7. 演进历史 - -### v1: 基础设施层(`7be08f53`) - -**34 个文件,+4040/-90 行** - -- 定义 `CORE_TOOLS` 白名单(31 个核心工具) -- 实现 TF-IDF 工具索引模块 `toolIndex.ts` -- 创建 `ExecuteTool` 作为统一执行入口 -- 增强 ToolSearchTool:TF-IDF 搜索路径、discover 模式、并行搜索合并 -- 新增 27 个单元测试 -- 实现预取管道和 UI 组件 - -**关键文件**: -- `src/services/toolSearch/toolIndex.ts` → 后续重命名为 `searchExtraTools/toolIndex.ts` -- `packages/builtin-tools/src/tools/ExecuteTool/` — 执行入口 -- `src/constants/tools.ts` — CORE_TOOLS 定义 - -### v2: 统一自建搜索(`8c157f07`) - -**17 个文件,+274/-395 行**(净减少 121 行) - -- **移除 `tool_reference` blocks** — 不再依赖 Anthropic API 的 `tool_reference` 功能 -- **移除 `defer_loading` 字段** — 不再发送 API 级别的工具延迟加载标记 -- **移除 `modelSupportsToolReference()`** — 不再区分模型是否支持 tool_reference -- **重命名 ExecuteTool → ExecuteExtraTool** — 更清晰地表达其作为代理执行器的角色 -- **输出改为纯文本** — 所有 provider 通用,无需特殊 API 功能支持 -- **简化 system prompt** — 工具使用指南从 ~120 行压缩到 ~10 行 - -**设计决策**:这次重构的核心洞察是 — 依赖 Anthropic 私有 API 特性(tool_reference、defer_loading、beta header)使得系统只能用于 first-party provider。自建 TF-IDF + keyword 搜索完全能满足需求,且对所有 provider(OpenAI、Gemini、Grok)通用。 - -### v3: Cache 稳定性修复(`c14b7ead`) - -**7 个文件,+46/-31 行** - -- **移除 "discover then include" 逻辑** — 发现的 deferred tools 不再注入 tools 数组 -- **tools 数组保持稳定** — 只有 core tools + SearchExtraTools + ExecuteExtraTool -- **强化优先级引导** — core tools 直接调用,ToolSearch 仅作为发现 deferred tools 的手段 -- **已加载工具拒绝提示** — 搜索 core tool 时返回明确拒绝 - -**设计决策**:prompt cache 是 Claude Code 性能优化的关键。每次 tools JSON 变化都会导致缓存失效,代价远大于通过 ExecuteExtraTool 代理调用 deferred tools 的额外 token。因此选择牺牲一点直接调用的便利性,换取 cache 稳定性。 - -### v4: Agents/Teams 延迟化(`af0d7dc8`) - -**7 个文件,+36/-18 行** - -- 将 `TeamCreate`、`TeamDelete`、`SendMessage` 从 CORE_TOOLS 移除 -- 这些工具仅在 swarm 模式下常用,平时占用 context token -- swarm 模式下 SendMessage 保持 always loaded -- TeamCreate/TeamDelete 在 swarm 未启用时返回启用提示 - -**设计决策**:不是所有用户都需要团队功能。将其延迟化后,大部分用户可以节省约 3 个工具定义的 token 开销。 - -## 8. 文件索引 - -### 核心文件 - -| 文件 | 职责 | -|------|------| -| `src/constants/tools.ts` | CORE_TOOLS 白名单、工具权限集合 | -| `src/utils/searchExtraTools.ts` | 模式判定、阈值计算、delta 差分、discovered tools 提取 | -| `src/services/searchExtraTools/toolIndex.ts` | TF-IDF 索引构建和搜索 | -| `src/services/searchExtraTools/prefetch.ts` | 预取管道(turn-zero + inter-turn) | -| `packages/builtin-tools/src/tools/SearchExtraToolsTool/` | 搜索工具实现(4 种查询模式) | -| `packages/builtin-tools/src/tools/ExecuteTool/` | 代理执行器实现 | -| `src/services/api/claude.ts` | API 层集成(工具过滤、消息归一化) | -| `src/query.ts` | 查询循环集成(预取触发点) | -| `src/utils/messages.ts` | Attachment → system-reminder 转换 | - -### 共享基础设施 - -| 文件 | 被复用的导出 | -|------|-------------| -| `src/services/skillSearch/localSearch.ts` | `tokenizeAndStem`, `computeWeightedTf`, `computeIdf`, `cosineSimilarity` | -| `src/services/skillSearch/prefetch.ts` | `extractQueryFromMessages` | - -### 测试文件 - -| 文件 | 覆盖范围 | -|------|---------| -| `src/services/searchExtraTools/__tests__/toolIndex.test.ts` | 索引构建、TF-IDF 搜索、CJK 处理 | -| `src/services/searchExtraTools/__tests__/prefetch.test.ts` | 预取管道、去重、attachment 生成 | -| `packages/builtin-tools/src/tools/SearchExtraToolsTool/__tests__/` | 搜索工具 4 种模式 | -| `packages/builtin-tools/src/tools/ExecuteTool/__tests__/` | 代理执行 | - -## 9. 维护指南 - -### 9.1 新增工具的延迟化决策 - -将新工具加入 deferred 状态的标准: -- 工具仅在特定场景使用(如 swarm 模式、特定 MCP 集成) -- 工具的 schema 较大(占用较多 context token) -- 工具不是模型默认会尝试的核心操作 - -将已延迟的工具提升为 core tool: -- 在 `src/constants/tools.ts` 的 `CORE_TOOLS` Set 中添加工具名常量 -- 确保导入对应的 `*_TOOL_NAME` 常量 - -### 9.2 修改注意事项 - -1. **修改 `localSearch.ts` 的 TF-IDF 函数**:需同步检查 `toolIndex.test.ts` 和 `localSearch.test.ts` -2. **修改 `skillSearch/prefetch.ts` 的 `extractQueryFromMessages`**:需同步检查工具预取行为(`searchExtraTools/prefetch.ts` 调用同一函数) -3. **修改 CORE_TOOLS**:需更新 `src/constants/__tests__/tools.test.ts` 测试 -4. **修改 `isDeferredTool`**:需更新 `src/constants/__tests__/tools.test.ts` 和 `SearchExtraToolsTool.test.ts` - -### 9.3 性能优化配置 - -```bash -# 环境变量调优 -ENABLE_SEARCH_EXTRA_TOOLS=auto:15 # 当 deferred tools 超过上下文 15% 时启用 -SEARCH_EXTRA_TOOLS_WEIGHT_KEYWORD=0.5 # 关键词搜索权重 -SEARCH_EXTRA_TOOLS_WEIGHT_TFIDF=0.5 # TF-IDF 搜索权重 -SEARCH_EXTRA_TOOLS_DISPLAY_MIN_SCORE=0.10 # 最低显示分数阈值 -``` - -### 9.4 搜索质量调优 - -- `TOOL_FIELD_WEIGHT`(`toolIndex.ts`):控制 name/searchHint/description 对 TF-IDF 分数的贡献权重 -- `KEYWORD_WEIGHT` / `TFIDF_WEIGHT`(`SearchExtraToolsTool.ts`):控制混合搜索中两种算法的最终权重比例 -- `searchHint` 属性:为工具添加精心编写的搜索提示,提高关键词匹配质量 - -## 10. 与 Skill Search 的关系 - -ToolSearch 和 SkillSearch 是平行的搜索系统,共享底层算法但服务于不同领域: - -| 维度 | ToolSearch | SkillSearch | -|------|-----------|-------------| -| 搜索对象 | Deferred 工具(内置 + MCP) | 用户技能(skill) | -| 执行方式 | `ExecuteExtraTool` 代理调用 | 直接注入 attachment 内容 | -| 字段权重 | name:3.0, searchHint:2.5, desc:1.0 | name:3.0, whenToUse:2.0, desc:1.0 | -| 缓存策略 | 按工具名列表缓存 | 按 cwd 缓存 | -| 去重集合 | `discoveredToolsThisSession` | 独立的 Set | - -共享的底层函数: -- `tokenizeAndStem` — 统一的 CJK/ASCII 分词和词干提取 -- `computeWeightedTf` — 加权词频计算 -- `computeIdf` — 逆文档频率计算 -- `cosineSimilarity` — 向量余弦相似度 -- `extractQueryFromMessages` — 从对话历史中提取搜索查询文本 diff --git a/docs/diagrams/agent-loop-simple.mmd b/docs/diagrams/agent-loop-simple.mmd deleted file mode 100644 index 4f213ee47..000000000 --- a/docs/diagrams/agent-loop-simple.mmd +++ /dev/null @@ -1,17 +0,0 @@ -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 diff --git a/docs/diagrams/agent-loop.mmd b/docs/diagrams/agent-loop.mmd deleted file mode 100644 index 99a9de4ef..000000000 --- a/docs/diagrams/agent-loop.mmd +++ /dev/null @@ -1,40 +0,0 @@ -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 diff --git a/docs/extensibility/custom-agents.mdx b/docs/extensibility/custom-agents.mdx deleted file mode 100644 index 2846f5d48..000000000 --- a/docs/extensibility/custom-agents.mdx +++ /dev/null @@ -1,211 +0,0 @@ ---- -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` 级记忆。 diff --git a/docs/extensibility/hooks.mdx b/docs/extensibility/hooks.mdx deleted file mode 100644 index 438f546d3..000000000 --- a/docs/extensibility/hooks.mdx +++ /dev/null @@ -1,253 +0,0 @@ ---- -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 的执行中。 diff --git a/docs/extensibility/mcp-configuration.mdx b/docs/extensibility/mcp-configuration.mdx deleted file mode 100644 index c696096f9..000000000 --- a/docs/extensibility/mcp-configuration.mdx +++ /dev/null @@ -1,346 +0,0 @@ ---- -title: "MCP 配置 - 多来源合并、作用域与策略管控" -description: "详细说明 Claude Code MCP 配置的来源层次、合并优先级、传输类型、企业策略管控、插件集成和保留名称机制。" -keywords: ["MCP", "配置", "settings.json", ".mcp.json", "企业策略", "插件"] ---- - -## 配置来源与作用域 - -Claude Code 的 MCP 配置来自多个来源,每个来源对应一个 `scope`(作用域)。配置按优先级合并,高优先级来源的同名配置覆盖低优先级。 - -### 来源列表 - -| 来源 | Scope | 文件/接口 | 说明 | -|------|-------|----------|------| -| 企业管控 | `enterprise` | 系统管理路径 `managed-mcp.json` | **排他模式**:存在时忽略所有其他来源 | -| 本地项目 | `local` | `/.claude/settings.local.json` | 项目级私有配置(不提交到 VCS) | -| 项目配置 | `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 = {} -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::`,不会与手动配置的名称冲突。 - -### 去重机制 - -插件服务器通过内容签名去重(`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 ` - -## 策略管控 - -### 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 - 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 双向通知 | diff --git a/docs/extensibility/mcp-protocol.mdx b/docs/extensibility/mcp-protocol.mdx deleted file mode 100644 index 5498813f1..000000000 --- a/docs/extensibility/mcp-protocol.mdx +++ /dev/null @@ -1,407 +0,0 @@ ---- -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____ ← 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` | `/.mcp.json` | 最高(同名覆盖) | -| 本地配置 | `local` | `/.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) | diff --git a/docs/extensibility/skills.mdx b/docs/extensibility/skills.mdx deleted file mode 100644 index d19b0b006..000000000 --- a/docs/extensibility/skills.mdx +++ /dev/null @@ -1,221 +0,0 @@ ---- -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: "" # 参数提示 -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_` 格式的 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() → 更新使用频率排名 -``` diff --git a/docs/external-dependencies.md b/docs/external-dependencies.md deleted file mode 100644 index f26273adb..000000000 --- a/docs/external-dependencies.md +++ /dev/null @@ -1,214 +0,0 @@ -# Claude Code 远程服务器依赖 - -> 只列出代码中实际发起网络请求的远程服务。本地服务、npm 包依赖、展示用 URL 不包含在内。 - -## 总览表 - -| # | 服务 | 远程端点 | 协议 | 状态 | -|---|---|---|---|---| -| 1 | Anthropic API | `api.anthropic.com` | HTTPS | 默认启用 | -| 2 | AWS Bedrock | `bedrock-runtime.*.amazonaws.com` | HTTPS | 需 `CLAUDE_CODE_USE_BEDROCK=1` | -| 3 | Google Vertex AI | `{region}-aiplatform.googleapis.com` | HTTPS | 需 `CLAUDE_CODE_USE_VERTEX=1` | -| 4 | Azure Foundry | `{resource}.services.ai.azure.com` | HTTPS | 需 `CLAUDE_CODE_USE_FOUNDRY=1` | -| 5 | OAuth (Anthropic) | `platform.claude.com`, `claude.com`, `claude.ai` | HTTPS | 用户登录时 | -| 6 | GrowthBook | `api.anthropic.com` (remoteEval) | HTTPS | 默认启用 | -| 7 | Sentry | 可配置 (`SENTRY_DSN`) | HTTPS | 需设环境变量 | -| 8 | Datadog | 可配置 (`DATADOG_LOGS_ENDPOINT`) | HTTPS | 需设环境变量 | -| 9 | OpenTelemetry Collector | 可配置 (`OTEL_EXPORTER_OTLP_ENDPOINT`) | gRPC/HTTP | 需设环境变量 | -| 10 | 1P Event Logging | `api.anthropic.com/api/event_logging/batch` | HTTPS | 默认启用 | -| 11 | BigQuery Metrics | `api.anthropic.com/api/claude_code/metrics` | HTTPS | 默认启用 | -| 12 | MCP Proxy | `mcp-proxy.anthropic.com` | HTTPS+WS | 使用 MCP 工具时 | -| 13 | MCP Registry | `api.anthropic.com/mcp-registry` | HTTPS | 查询 MCP 服务器时 | -| 14 | Web Search Pages | `www.bing.com`, `search.brave.com` | HTTPS | WebSearch 工具,可通过 `WEB_SEARCH_ADAPTER=bing|brave` 切换 | -| 15 | Google Cloud Storage (更新) | `storage.googleapis.com` | HTTPS | 版本检查 | -| 16 | GitHub Raw (Changelog/Stats) | `raw.githubusercontent.com` | HTTPS | 更新提示 | -| 17 | Claude in Chrome Bridge | `bridge.claudeusercontent.com` | WSS | Chrome 集成 | -| 18 | CCR Upstream Proxy | `api.anthropic.com` | WS | CCR 远程会话 | -| 19 | Voice STT | `api.anthropic.com/api/ws/...` | WSS | Voice Mode | -| 20 | Desktop App Download | `claude.ai/api/desktop/...` | HTTPS | 下载引导 | - ---- - -## 详细说明 - -### 1. Anthropic Messages API - -核心 LLM 推理服务,发送对话消息、接收流式响应。 - -- **端点**: `https://api.anthropic.com` (生产) / `https://api-staging.anthropic.com` (staging) -- **覆盖**: `ANTHROPIC_BASE_URL` 环境变量 -- **认证**: API Key / OAuth Token -- **文件**: `src/services/api/client.ts`, `src/services/api/claude.ts` - -### 2. AWS Bedrock - -- **端点**: `bedrock-runtime.{region}.amazonaws.com` -- **认证**: AWS 凭证链 / `AWS_BEARER_TOKEN_BEDROCK` -- **文件**: `src/services/api/client.ts:153-190`, `src/utils/aws.ts` - -### 3. Google Vertex AI - -- **端点**: `{region}-aiplatform.googleapis.com` -- **认证**: `GoogleAuth` + `cloud-platform` scope -- **文件**: `src/services/api/client.ts:221-298` - -### 4. Azure Foundry - -- **端点**: `https://{resource}.services.ai.azure.com/anthropic/v1/messages` -- **认证**: API Key 或 Azure AD `DefaultAzureCredential` -- **文件**: `src/services/api/client.ts:191-220` - -### 5. OAuth - -OAuth 2.0 + PKCE 授权码流程。 - -- **端点**: - - `https://platform.claude.com/oauth/authorize` — 授权页 - - `https://claude.com/cai/oauth/authorize` — Claude.ai 授权 - - `https://platform.claude.com/v1/oauth/token` — Token 交换 - - `https://api.anthropic.com/api/oauth/claude_cli/create_api_key` — 创建 API Key - - `https://api.anthropic.com/api/oauth/claude_cli/roles` — 获取角色 - - `https://claude.ai/oauth/claude-code-client-metadata` — MCP 客户端元数据 - - `https://claude.fedstart.com` — FedStart 政府部署 -- **文件**: `src/constants/oauth.ts`, `src/services/oauth/` - -### 6. GrowthBook (功能开关) - -- **端点**: `https://api.anthropic.com/` (remoteEval 模式) 或 `CLAUDE_GB_ADAPTER_URL` -- **SDK Keys**: `sdk-zAZezfDKGoZuXXKe` (外部), `sdk-xRVcrliHIlrg4og4` (ant prod), `sdk-yZQvlplybuXjYh6L` (ant dev) -- **文件**: `src/services/analytics/growthbook.ts`, `src/constants/keys.ts` - -### 7. Sentry (错误追踪) - -- **激活**: 设置 `SENTRY_DSN` (默认未配置) -- **行为**: 仅错误上报,自动过滤敏感 header -- **文件**: `src/utils/sentry.ts` - -### 8. Datadog (日志) - -- **激活**: 同时设 `DATADOG_LOGS_ENDPOINT` + `DATADOG_API_KEY` (默认未配置) -- **文件**: `src/services/analytics/datadog.ts` - -### 9. OpenTelemetry Collector - -- **激活**: `CLAUDE_CODE_ENABLE_TELEMETRY=1` 或 `OTEL_*` 环境变量 -- **协议**: gRPC / HTTP / Protobuf,支持 OTLP 和 Prometheus 导出 -- **文件**: `src/utils/telemetry/instrumentation.ts` - -### 10. 1P Event Logging (内部事件) - -- **端点**: `https://api.anthropic.com/api/event_logging/batch` -- **协议**: 批量导出 (10s 间隔, 每批 200 事件) -- **文件**: `src/services/analytics/firstPartyEventLoggingExporter.ts` - -### 11. BigQuery Metrics - -- **端点**: `https://api.anthropic.com/api/claude_code/metrics` -- **文件**: `src/utils/telemetry/bigqueryExporter.ts` - -### 12. MCP Proxy - -Anthropic 托管的 MCP 服务器代理。 - -- **端点**: `https://mcp-proxy.anthropic.com/v1/mcp/{server_id}` -- **认证**: Claude.ai OAuth tokens -- **文件**: `src/services/mcp/client.ts`, `src/constants/oauth.ts` - -### 13. MCP Registry - -获取官方 MCP 服务器列表。 - -- **端点**: `https://api.anthropic.com/mcp-registry/v0/servers?version=latest&visibility=commercial` -- **文件**: `src/services/mcp/officialRegistry.ts` - -### 14. Web Search Pages - -WebSearch 工具支持直接抓取 Bing 搜索结果页面,也支持通过 Brave 的 LLM Context API -获取搜索上下文;可通过 `WEB_SEARCH_ADAPTER=bing|brave` 显式切换后端。 - -- **Bing 端点**: `https://www.bing.com/search?q={query}&setmkt=en-US` -- **Brave 端点**: `https://api.search.brave.com/res/v1/llm/context?q={query}` -- **文件**: - - `packages/builtin-tools/src/tools/WebSearchTool/adapters/bingAdapter.ts` - - `packages/builtin-tools/src/tools/WebSearchTool/adapters/braveAdapter.ts` - -另外还有 Domain Blocklist 查询: -- **端点**: `https://api.anthropic.com/api/web/domain_info?domain={domain}` -- **文件**: `packages/builtin-tools/src/tools/WebFetchTool/utils.ts` - -### 15. Google Cloud Storage (自动更新) - -- **端点**: `https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases` -- **文件**: `src/utils/autoUpdater.ts` - -### 16. GitHub Raw Content - -- **端点**: `https://raw.githubusercontent.com/anthropics/claude-code/refs/heads/main/CHANGELOG.md` -- **端点**: `https://raw.githubusercontent.com/anthropics/claude-plugins-official/refs/heads/stats/stats/plugin-installs.json` -- **文件**: `src/utils/releaseNotes.ts`, `src/utils/plugins/installCounts.ts` - -### 17. Claude in Chrome Bridge - -- **端点**: `wss://bridge.claudeusercontent.com` (生产) / `wss://bridge-staging.claudeusercontent.com` (staging) -- **文件**: `src/utils/claudeInChrome/mcpServer.ts` - -### 18. CCR Upstream Proxy - -- **端点**: `ws://api.anthropic.com/v1/code/upstreamproxy/ws` -- **激活**: `CLAUDE_CODE_REMOTE=1` + `CCR_UPSTREAM_PROXY_ENABLED=1` -- **文件**: `src/upstreamproxy/upstreamproxy.ts` - -### 19. Voice STT - -- **端点**: `wss://api.anthropic.com/api/ws/...` -- **文件**: `src/services/voiceStreamSTT.ts` - -### 20. Desktop App Download - -- **端点**: `https://claude.ai/api/desktop/win32/x64/exe/latest/redirect` (Windows) -- **端点**: `https://claude.ai/api/desktop/darwin/universal/dmg/latest/redirect` (macOS) -- **文件**: `src/components/DesktopHandoff.tsx` - ---- - -## Anthropic API 辅助端点汇总 - -以下端点都挂在 `api.anthropic.com` 上,按功能分类: - -| 端点路径 | 用途 | 文件 | -|---|---|---| -| `/api/event_logging/batch` | 事件批量上报 | `src/services/analytics/firstPartyEventLoggingExporter.ts` | -| `/api/claude_code/metrics` | BigQuery 指标导出 | `src/utils/telemetry/bigqueryExporter.ts` | -| `/api/oauth/claude_cli/create_api_key` | 创建 API Key | `src/constants/oauth.ts` | -| `/api/oauth/claude_cli/roles` | 获取用户角色 | `src/constants/oauth.ts` | -| `/api/oauth/accounts/grove` | 通知设置 | `src/services/api/grove.ts` | -| `/api/oauth/organizations/{id}/referral/*` | 推荐活动 | `src/services/api/referral.ts` | -| `/api/oauth/organizations/{id}/overage_credit_grant` | 超额信用 | `src/services/api/overageCreditGrant.ts` | -| `/api/oauth/organizations/{id}/admin_requests` | 管理请求 | `src/services/api/adminRequests.ts` | -| `/api/web/domain_info?domain={}` | 域名安全检查 | `src/tools/WebFetchTool/utils.ts` | -| `/api/claude_code/settings` | 设置同步 | `src/services/settingsSync/index.ts` | -| `/api/claude_code/managed_settings` | 企业托管设置 (1h 轮询) | `src/services/remoteManagedSettings/index.ts` | -| `/api/claude_code/team_memory?repo={}` | 团队记忆同步 | `src/services/teamMemorySync/index.ts` | -| `/api/auth/trusted_devices` | 可信设备注册 | `src/bridge/trustedDevice.ts` | -| `/api/organizations/{id}/claude_code/buddy_react` | Companion 反应 | `src/buddy/companionReact.ts` | -| `/mcp-registry/v0/servers` | MCP 服务器注册表 | `src/services/mcp/officialRegistry.ts` | -| `/v1/files` | 文件上传/下载 | `src/services/api/filesApi.ts` | -| `/v1/sessions/{id}/events` | 会话历史 | `src/assistant/sessionHistory.ts` | -| `/v1/code/triggers` | 远程触发器 | `src/tools/RemoteTriggerTool/RemoteTriggerTool.ts` | -| `/v1/organizations/{id}/mcp_servers` | 组织 MCP 配置 | `src/services/mcp/claudeai.ts` | - -## 非 Anthropic 远程域名汇总 - -| 域名 | 服务 | 协议 | -|---|---|---| -| `bedrock-runtime.*.amazonaws.com` | AWS Bedrock | HTTPS | -| `{region}-aiplatform.googleapis.com` | Google Vertex AI | HTTPS | -| `{resource}.services.ai.azure.com` | Azure Foundry | HTTPS | -| `www.bing.com` | Bing 搜索 | HTTPS | -| `search.brave.com` | Brave 搜索 | HTTPS | -| `storage.googleapis.com` | 自动更新 | HTTPS | -| `raw.githubusercontent.com` | Changelog / 插件统计 | HTTPS | -| `bridge.claudeusercontent.com` | Chrome Bridge | WSS | -| `platform.claude.com` | OAuth 授权页 | HTTPS | -| `claude.com` / `claude.ai` | OAuth / 下载 | HTTPS | -| `claude.fedstart.com` | FedStart OAuth | HTTPS | diff --git a/docs/features/acp-link.md b/docs/features/acp-link.md deleted file mode 100644 index 3843623a0..000000000 --- a/docs/features/acp-link.md +++ /dev/null @@ -1,207 +0,0 @@ -# acp-link — ACP 代理服务器 - -> 源码目录:`packages/acp-link/` -> PR: #292 -> 新增时间:2026-04-18 - -## 一、功能概述 - -`acp-link` 是一个 ACP (Agent Client Protocol) 代理服务器,将 WebSocket 客户端桥接到 ACP agent 的 stdio 接口。它让 ACP agent(如 Claude Code)可以通过 WebSocket 远程访问,而不仅限于本地 stdio。 - -### 核心特性 - -- **WebSocket → stdio 桥接**:将浏览器/远程客户端的 WebSocket 连接转换为 ACP agent 的 stdin/stdout NDJSON 流 -- **会话管理**:创建、加载、恢复、列出、关闭会话 -- **权限审批流程**:客户端可远程审批 agent 的工具权限请求 -- **RCS 集成**:可与 Remote Control Server (RCS) 连接,将 ACP agent 注册到 RCS 并通过 Web UI 交互 -- **HTTPS 支持**:内置自签名证书生成,支持安全连接 -- **Token 认证**:自动生成或通过环境变量配置认证 token - -## 二、架构 - -### 独立模式 - -``` -┌──────────────────┐ WebSocket ┌──────────────────┐ stdio/NDJSON ┌──────────────┐ -│ 浏览器/客户端 │ ◄──────────────►│ acp-link │ ◄────────────────►│ ACP Agent │ -│ (WS Client) │ ws://host:port │ (Proxy Server) │ spawn subprocess │ (Claude等) │ -└──────────────────┘ └──────────────────┘ └──────────────┘ -``` - -### RCS 集成模式 - -``` -┌──────────────┐ WebSocket ┌──────────────────┐ stdio/NDJSON ┌──────────────┐ -│ RCS Web UI │ ◄──────────────►│ Remote Control │ ◄─────────────────►│ acp-link │ -│ (/code/*) │ ACP Relay WS │ Server (RCS) │ ACP events │ + Agent │ -└──────────────┘ └──────────────────┘ └──────────────┘ -``` - -### 文件结构 - -``` -packages/acp-link/ -├── src/ -│ ├── server.ts # 主服务器:WS 连接管理、会话管理、权限处理、消息桥接 -│ ├── rcs-upstream.ts # RCS 上游客户端:REST 注册 + WS identify 两步流程 -│ ├── cert.ts # TLS 证书生成(自签名) -│ ├── logger.ts # 日志模块 -│ ├── types.ts # JSON-RPC 和 ACP 协议类型定义 -│ ├── cli/ -│ │ ├── bin.ts # CLI 入口 -│ │ ├── command.ts # 命令行参数解析 -│ │ ├── app.ts # 应用启动 -│ │ └── context.ts # 上下文配置 -│ └── __tests__/ # 测试(cert, server, types) -├── package.json -└── tsconfig.json -``` - -## 三、安装与使用 - -### 基本用法 - -```bash -# 直接运行(在 monorepo 中) -# 注意:claude 本身不支持 ACP,需要用 ccb-bun --acp 启动 ACP agent -bun packages/acp-link/src/cli/bin.ts ccb-bun -- --acp - -# 指定端口和主机 -acp-link --port 9000 --host 0.0.0.0 ccb-bun -- --acp - -# 启用 HTTPS(自签名证书) -acp-link --https ccb-bun -- --acp - -# 调试模式 -acp-link --debug ccb-bun -- --acp -``` - -### CLI 参考 - -``` -USAGE - acp-link [--port value] [--host value] [--debug] [--no-auth] [--https] ... - acp-link --help - acp-link --version - -FLAGS - [--port] Port to listen on [default = 9315] - [--host] Host to bind to [default = localhost] - [--debug] Enable debug logging to file - [--no-auth] Disable authentication (dangerous) - [--https] Enable HTTPS with self-signed cert - -h --help Print help information and exit - -v --version Print version information and exit - -ARGUMENTS - command... Agent command followed by its arguments (e.g. "ccb-bun -- --acp") -``` - -## 四、认证 - -默认启动时自动生成随机 token。客户端连接时不要把 token 放在 URL 中: - -``` -ws://localhost:9315/ws -``` - -无法发送 `Authorization` header 的 WebSocket 客户端需要使用 -`rcs.auth.` 子协议传递 token。 - -配置固定 token: - -```bash -ACP_AUTH_TOKEN=my-fixed-token acp-link ccb-bun -- --acp -``` - -禁用认证(不推荐,仅用于开发): - -```bash -acp-link --no-auth ccb-bun -- --acp -``` - -## 五、RCS 集成 - -acp-link 支持将 ACP agent 注册到 Remote Control Server,通过 Web UI 远程操控。 - -### 连接方式 - -```bash -# 通过环境变量配置 RCS 连接 -ACP_RCS_URL=http://localhost:3000 \ -ACP_RCS_TOKEN=sk-rcs-your-key \ -acp-link ccb-bun -- --acp -``` - -### 注册流程(两步) - -1. **REST 注册**:通过 `POST /v1/environments/bridge` 向 RCS 注册环境 -2. **WS identify**:建立 WebSocket 连接后发送 `identify` 消息(携带 agentId),替代完整 `register` - -RCS 的 ACP WebSocket 连接不接受 URL query token。acp-link 会通过 -`rcs.auth.` WebSocket 子协议发送 `ACP_RCS_TOKEN`。 - -``` -acp-link RCS - │ │ - │── POST /v1/environments/bridge ──►│ (REST 注册) - │◄── { agentId, sessionId } ───────│ - │ │ - │── WS connect ─────────────────►│ (WebSocket) - │── identify { agentId } ────────►│ (WS 标识) - │◄── identified ─────────────────│ - │ │ - │── ACP events ─────────────────►│ (双向消息转发) - │◄── user prompts/permissions ───│ -``` - -## 六、权限模式 - -### permissionMode 传递链 - -权限模式通过整条链路传递:Web UI → RCS → acp-link → ACP agent。 - -支持的权限模式: -- `default` — 每次请求权限确认 -- `auto` — 自动判断 -- `acceptEdits` — 自动接受编辑 -- `plan` — 规划模式 -- `dontAsk` — 不询问 -- `bypassPermissions` — 绕过权限(需 sandbox 环境) - -### fallback 链 - -当客户端未显式传递 permissionMode 时,使用以下 fallback 链: - -``` -客户端传值 > config.permissionMode > ACP_PERMISSION_MODE 环境变量 -``` - -示例: - -```bash -ACP_PERMISSION_MODE=auto acp-link ccb-bun -- --acp -``` - -## 七、权限管道(2026-04-18 改进) - -### 模式同步 - -`applySessionMode` 在 agent 切换权限模式时同步 `appState.toolPermissionContext.mode`,确保内部权限上下文与 ACP 客户端状态一致。 - -### 统一权限流水线 - -`createAcpCanUseTool` 接入 `hasPermissionsToUseTool` 统一权限流水线,替代原来分散的处理逻辑。支持 `onModeChange` 回调,模式变更时实时同步。 - -### bypass 检测 - -`bypassPermissions` 模式增加可用性检测 — 仅在非 root 或 sandbox 环境中允许启用,防止权限绕过的安全风险。 - -## 八、环境变量 - -| 变量 | 说明 | -|------|------| -| `ACP_AUTH_TOKEN` | 固定认证 token(默认自动生成) | -| `ACP_PERMISSION_MODE` | 默认权限模式 fallback | -| `ACP_RCS_URL` | RCS 服务器地址(启用 RCS 集成) | -| `ACP_RCS_TOKEN` | RCS API token | diff --git a/docs/features/acp-zed.md b/docs/features/acp-zed.md deleted file mode 100644 index d83e28be2..000000000 --- a/docs/features/acp-zed.md +++ /dev/null @@ -1,189 +0,0 @@ -# ACP (Agent Client Protocol) — Zed / IDE 集成 - -> Feature Flag: `FEATURE_ACP=1`(build 和 dev 模式默认启用) -> 实现状态:可用(支持 Zed、Cursor 等 ACP 客户端) -> 源码目录:`src/services/acp/` - -## 一、功能概述 - -ACP (Agent Client Protocol) 是一种标准化的 stdio 协议,允许 IDE 和编辑器通过 stdin/stdout 的 NDJSON 流驱动 AI Agent。CCB 实现了完整的 ACP agent 端,可以被 Zed、Cursor 等支持 ACP 的客户端直接调用。 - -### 核心特性 - -- **会话管理**:新建 / 恢复 / 加载 / 分叉 / 关闭会话 -- **历史回放**:恢复会话时自动加载并回放对话历史 -- **权限桥接**:ACP 客户端的权限决策映射到 CCB 的工具权限系统 -- **斜杠命令 & Skills**:加载真实命令列表,支持 `/commit`、`/review` 等 prompt 型 skill -- **Context Window 跟踪**:精确的 usage_update,含 model prefix matching -- **Prompt 排队**:支持连续发送多条 prompt,自动排队处理 -- **模式切换**:auto / default / acceptEdits / plan / dontAsk / bypassPermissions -- **模型切换**:运行时切换 AI 模型 - -## 二、架构 - -``` -┌──────────────┐ NDJSON/stdio ┌──────────────────┐ -│ Zed / IDE │ ◄────────────────► │ CCB ACP Agent │ -│ (Client) │ stdin / stdout │ (Agent) │ -└──────────────┘ │ │ - │ entry.ts │ ← stdio → NDJSON stream - │ agent.ts │ ← ACP protocol handler - │ bridge.ts │ ← SDKMessage → ACP SessionUpdate - │ permissions.ts │ ← 权限桥接 - │ utils.ts │ ← 通用工具 - │ │ - │ QueryEngine │ ← 内部查询引擎 - └──────────────────┘ -``` - -### 文件职责 - -| 文件 | 职责 | -|------|------| -| `entry.ts` | 入口,创建 stdio → NDJSON stream,启动 `AgentSideConnection` | -| `agent.ts` | 实现 ACP `Agent` 接口:会话 CRUD、prompt、cancel、模式/模型切换 | -| `bridge.ts` | `SDKMessage` → ACP `SessionUpdate` 转换:文本/思考/工具/用量/编辑 diff | -| `permissions.ts` | ACP `requestPermission()` → CCB `CanUseToolFn` 桥接 | -| `utils.ts` | Pushable、流转换、权限模式解析、session fingerprint、路径显示 | - -## 三、配置 Zed 编辑器 - -### 3.1 Zed settings.json 配置 - -打开 Zed 的 `settings.json`(`Cmd+,` → Open Settings),添加 `agent_servers` 配置: - -```json -{ - "agent_servers": { - "ccb": { - "type": "custom", - "command": "ccb", - "args": ["--acp"] - } - } -} -``` - -### 3.3 API 认证配置 - -CCB 的 ACP agent 在启动时会自动加载 `settings.json` 中的环境变量(`ANTHROPIC_BASE_URL`、`ANTHROPIC_AUTH_TOKEN` 等)。确保已通过 `/login` 配置好 API 供应商。 - -也可通过环境变量传入: - -```json -{ - "agent_servers": { - "claude-code": { - "command": "ccb", - "args": ["--acp"], - "env": { - "ANTHROPIC_BASE_URL": "https://api.example.com/v1", - "ANTHROPIC_AUTH_TOKEN": "sk-xxx" - } - } - } -} -``` - -### 3.4 在 Zed 中使用 - -1. 配置完成后重启 Zed -2. 打开任意项目目录 -3. 按 `Cmd+'`(macOS)或 `Ctrl+'`(Linux)打开 Agent Panel -4. 在 Agent Panel 顶部的下拉菜单中选择 **claude-code** -5. 开始对话 - -### 3.5 功能说明 - -| 功能 | 操作 | -|------|------| -| 对话 | 在 Agent Panel 中直接输入消息 | -| 斜杠命令 | 输入 `/` 查看可用 skills 列表(如 `/commit`、`/review`) | -| 工具权限 | 弹出权限请求时选择 Allow / Reject / Always Allow | -| 模式切换 | 通过 Agent Panel 的设置菜单切换 auto/default/plan 等模式 | -| 模型切换 | 通过 Agent Panel 的设置菜单切换 AI 模型 | -| 会话恢复 | 关闭重开 Zed 后,之前的会话可自动恢复(含历史消息) | - -## 四、配置其他 ACP 客户端 - -ACP 是开放协议,任何支持 ACP 的客户端都可以连接 CCB。通用配置模式: - -``` -命令: ccb --acp -参数: ["--acp"] -通信: stdin/stdout NDJSON -协议版本: ACP v1 -``` - -### 4.1 Cursor - -在 Cursor 的设置中配置 MCP / Agent Server,使用同样的 `ccb --acp` 命令。 - -### 4.2 自定义客户端 - -使用 `@agentclientprotocol/sdk` 可以快速构建 ACP 客户端: - -```typescript -import { ClientSideConnection, ndJsonStream } from '@agentclientprotocol/sdk' - -// 创建连接(将 ccb --acp 作为子进程启动) -const child = spawn('ccb', ['--acp']) -const stream = ndJsonStream( - Writable.toWeb(child.stdin), - Readable.toWeb(child.stdout), -) - -const client = new ClientSideConnection(stream) - -// 初始化 -await client.initialize({ clientCapabilities: {} }) - -// 创建会话 -const { sessionId } = await client.newSession({ - cwd: '/path/to/project', -}) - -// 发送 prompt -const response = await client.prompt({ - sessionId, - prompt: [{ type: 'text', text: 'Hello, explain this project' }], -}) - -// 监听 session 更新 -client.on('sessionUpdate', (update) => { - console.log('Update:', update) -}) -``` - -## 五、ACP 协议支持矩阵 - -| 方法 | 状态 | 说明 | -|------|------|------| -| `initialize` | ✅ | 返回 agent 信息和能力 | -| `authenticate` | ✅ | 无需认证(自托管) | -| `newSession` | ✅ | 创建新会话 | -| `resumeSession` | ✅ | 恢复已有会话(含历史回放) | -| `loadSession` | ✅ | 加载指定会话(含历史回放) | -| `listSessions` | ✅ | 列出可用会话 | -| `forkSession` | ✅ | 分叉会话 | -| `closeSession` | ✅ | 关闭会话 | -| `prompt` | ✅ | 发送消息,支持排队 | -| `cancel` | ✅ | 取消当前/排队的 prompt | -| `setSessionMode` | ✅ | 切换权限模式 | -| `setSessionModel` | ✅ | 切换 AI 模型 | -| `setSessionConfigOption` | ✅ | 动态修改配置 | - -### SessionUpdate 类型 - -| 类型 | 状态 | 说明 | -|------|------|------| -| `agent_message_chunk` | ✅ | 助手文本消息 | -| `agent_thought_chunk` | ✅ | 思考/推理内容 | -| `user_message_chunk` | ✅ | 用户消息(历史回放) | -| `tool_call` | ✅ | 工具调用开始 | -| `tool_call_update` | ✅ | 工具调用结果/状态更新 | -| `usage_update` | ✅ | token 用量 + context window | -| `plan` | ✅ | TodoWrite → plan entries | -| `available_commands_update` | ✅ | 斜杠命令 & skills 列表 | -| `current_mode_update` | ✅ | 模式切换通知 | -| `config_option_update` | ✅ | 配置更新通知 | diff --git a/docs/features/agents/acp.md b/docs/features/agents/acp.md new file mode 100644 index 000000000..917104d12 --- /dev/null +++ b/docs/features/agents/acp.md @@ -0,0 +1,389 @@ +--- +title: "ACP 协议:接入 Zed / Cursor 等 IDE" +description: "通过 ACP(Agent Client Protocol)把 CCB 接入支持 ACP 的 IDE。本文包含 acp-link CLI 用法、权限桥接、以及 Zed 集成案例。" +keywords: ["ACP 协议", "Zed 编辑器", "acp-link", "权限桥接", "IDE 集成"] +--- + +# ACP 协议:接入 Zed / Cursor 等 IDE + +## 概述 + +ACP (Agent Client Protocol) 是一种标准化的 stdio 协议,允许 IDE 和编辑器通过 stdin/stdout 的 NDJSON 流驱动 AI Agent。CCB 实现了完整的 ACP agent 端,可以被 Zed、Cursor 等支持 ACP 的客户端直接调用。 + +CCB 在 ACP 体系下提供两层能力: + +- **ACP Agent**(源码目录 `src/services/acp/`):CCB 自身作为 ACP agent,通过 `ccb --acp` 暴露 stdio 接口,由 IDE 直接调用。 +- **acp-link 代理服务器**(源码目录 `packages/acp-link/`):将 WebSocket 客户端桥接到 ACP agent 的 stdio 接口,让 ACP agent 可以通过 WebSocket 远程访问,而不仅限于本地 stdio。 + +### 核心特性 + +ACP Agent: + +- **会话管理**:新建 / 恢复 / 加载 / 分叉 / 关闭会话 +- **历史回放**:恢复会话时自动加载并回放对话历史 +- **权限桥接**:ACP 客户端的权限决策映射到 CCB 的工具权限系统 +- **斜杠命令 & Skills**:加载真实命令列表,支持 `/commit`、`/review` 等 prompt 型 skill +- **Context Window 跟踪**:精确的 usage_update,含 model prefix matching +- **Prompt 排队**:支持连续发送多条 prompt,自动排队处理 +- **模式切换**:auto / default / acceptEdits / plan / dontAsk / bypassPermissions +- **模型切换**:运行时切换 AI 模型 + +acp-link: + +- **WebSocket → stdio 桥接**:将浏览器/远程客户端的 WebSocket 连接转换为 ACP agent 的 stdin/stdout NDJSON 流 +- **会话管理**:创建、加载、恢复、列出、关闭会话 +- **权限审批流程**:客户端可远程审批 agent 的工具权限请求 +- **RCS 集成**:可与 Remote Control Server (RCS) 连接,将 ACP agent 注册到 RCS 并通过 Web UI 交互 +- **HTTPS 支持**:内置自签名证书生成,支持安全连接 +- **Token 认证**:自动生成或通过环境变量配置认证 token + +## 快速上手 + +### 在 Zed 中接入 CCB + +1. 打开 Zed 的 `settings.json`(`Cmd+,` → Open Settings),添加 `agent_servers` 配置: + + ```json + { + "agent_servers": { + "ccb": { + "type": "custom", + "command": "ccb", + "args": ["--acp"] + } + } + } + ``` + +2. API 认证:CCB 的 ACP agent 在启动时会自动加载 `settings.json` 中的环境变量(`ANTHROPIC_BASE_URL`、`ANTHROPIC_AUTH_TOKEN` 等)。确保已通过 `/login` 配置好 API 供应商;也可在 `agent_servers` 中显式传入 `env`: + + ```json + { + "agent_servers": { + "claude-code": { + "command": "ccb", + "args": ["--acp"], + "env": { + "ANTHROPIC_BASE_URL": "https://api.example.com/v1", + "ANTHROPIC_AUTH_TOKEN": "sk-xxx" + } + } + } + } + ``` + +3. 重启 Zed,打开任意项目目录。 +4. 按 `Cmd+'`(macOS)或 `Ctrl+'`(Linux)打开 Agent Panel。 +5. 在 Agent Panel 顶部的下拉菜单中选择 **claude-code**。 +6. 开始对话。 + +### Zed 中的功能操作 + +| 功能 | 操作 | +|------|------| +| 对话 | 在 Agent Panel 中直接输入消息 | +| 斜杠命令 | 输入 `/` 查看可用 skills 列表(如 `/commit`、`/review`) | +| 工具权限 | 弹出权限请求时选择 Allow / Reject / Always Allow | +| 模式切换 | 通过 Agent Panel 的设置菜单切换 auto/default/plan 等模式 | +| 模型切换 | 通过 Agent Panel 的设置菜单切换 AI 模型 | +| 会话恢复 | 关闭重开 Zed 后,之前的会话可自动恢复(含历史消息) | + +### 通过 acp-link 暴露到网络 + +```bash +# 直接运行(在 monorepo 中) +# 注意:claude 本身不支持 ACP,需要用 ccb-bun --acp 启动 ACP agent +bun packages/acp-link/src/cli/bin.ts ccb-bun -- --acp + +# 指定端口和主机 +acp-link --port 9000 --host 0.0.0.0 ccb-bun -- --acp + +# 启用 HTTPS(自签名证书) +acp-link --https ccb-bun -- --acp + +# 调试模式 +acp-link --debug ccb-bun -- --acp +``` + +## 详细说明 + +### ACP Agent 架构 + +``` +┌──────────────┐ NDJSON/stdio ┌──────────────────┐ +│ Zed / IDE │ ◄────────────────► │ CCB ACP Agent │ +│ (Client) │ stdin / stdout │ (Agent) │ +└──────────────┘ │ │ + │ entry.ts │ ← stdio → NDJSON stream + │ agent.ts │ ← ACP protocol handler + │ bridge.ts │ ← SDKMessage → ACP SessionUpdate + │ permissions.ts │ ← 权限桥接 + │ utils.ts │ ← 通用工具 + │ │ + │ QueryEngine │ ← 内部查询引擎 + └──────────────────┘ +``` + +| 文件 | 职责 | +|------|------| +| `entry.ts` | 入口,创建 stdio → NDJSON stream,启动 `AgentSideConnection` | +| `agent.ts` | 实现 ACP `Agent` 接口:会话 CRUD、prompt、cancel、模式/模型切换 | +| `bridge.ts` | `SDKMessage` → ACP `SessionUpdate` 转换:文本/思考/工具/用量/编辑 diff | +| `permissions.ts` | ACP `requestPermission()` → CCB `CanUseToolFn` 桥接 | +| `utils.ts` | Pushable、流转换、权限模式解析、session fingerprint、路径显示 | + +### acp-link 架构 + +#### 独立模式 + +``` +┌──────────────────┐ WebSocket ┌──────────────────┐ stdio/NDJSON ┌──────────────┐ +│ 浏览器/客户端 │ ◄──────────────►│ acp-link │ ◄────────────────►│ ACP Agent │ +│ (WS Client) │ ws://host:port │ (Proxy Server) │ spawn subprocess │ (Claude等) │ +└──────────────────┘ └──────────────────┘ └──────────────┘ +``` + +#### RCS 集成模式 + +``` +┌──────────────┐ WebSocket ┌──────────────────┐ stdio/NDJSON ┌──────────────┐ +│ RCS Web UI │ ◄──────────────►│ Remote Control │ ◄─────────────────►│ acp-link │ +│ (/code/*) │ ACP Relay WS │ Server (RCS) │ ACP events │ + Agent │ +└──────────────┘ └──────────────────┘ └──────────────┘ +``` + +#### 文件结构 + +``` +packages/acp-link/ +├── src/ +│ ├── server.ts # 主服务器:WS 连接管理、会话管理、权限处理、消息桥接 +│ ├── rcs-upstream.ts # RCS 上游客户端:REST 注册 + WS identify 两步流程 +│ ├── cert.ts # TLS 证书生成(自签名) +│ ├── logger.ts # 日志模块 +│ ├── types.ts # JSON-RPC 和 ACP 协议类型定义 +│ ├── cli/ +│ │ ├── bin.ts # CLI 入口 +│ │ ├── command.ts # 命令行参数解析 +│ │ ├── app.ts # 应用启动 +│ │ └── context.ts # 上下文配置 +│ └── __tests__/ # 测试(cert, server, types) +├── package.json +└── tsconfig.json +``` + +### acp-link CLI 参考 + +``` +USAGE + acp-link [--port value] [--host value] [--debug] [--no-auth] [--https] ... + acp-link --help + acp-link --version + +FLAGS + [--port] Port to listen on [default = 9315] + [--host] Host to bind to [default = localhost] + [--debug] Enable debug logging to file + [--no-auth] Disable authentication (dangerous) + [--https] Enable HTTPS with self-signed cert + -h --help Print help information and exit + -v --version Print version information and exit + +ARGUMENTS + command... Agent command followed by its arguments (e.g. "ccb-bun -- --acp") +``` + +### 接入其他 ACP 客户端 + +ACP 是开放协议,任何支持 ACP 的客户端都可以连接 CCB。通用配置模式: + +``` +命令: ccb --acp +参数: ["--acp"] +通信: stdin/stdout NDJSON +协议版本: ACP v1 +``` + +#### Cursor + +在 Cursor 的设置中配置 MCP / Agent Server,使用同样的 `ccb --acp` 命令。 + +#### 自定义客户端 + +使用 `@agentclientprotocol/sdk` 可以快速构建 ACP 客户端: + +```typescript +import { ClientSideConnection, ndJsonStream } from '@agentclientprotocol/sdk' + +// 创建连接(将 ccb --acp 作为子进程启动) +const child = spawn('ccb', ['--acp']) +const stream = ndJsonStream( + Writable.toWeb(child.stdin), + Readable.toWeb(child.stdout), +) + +const client = new ClientSideConnection(stream) + +// 初始化 +await client.initialize({ clientCapabilities: {} }) + +// 创建会话 +const { sessionId } = await client.newSession({ + cwd: '/path/to/project', +}) + +// 发送 prompt +const response = await client.prompt({ + sessionId, + prompt: [{ type: 'text', text: 'Hello, explain this project' }], +}) + +// 监听 session 更新 +client.on('sessionUpdate', (update) => { + console.log('Update:', update) +}) +``` + +## 进阶与参考 + +### 认证 + +默认启动时 acp-link 自动生成随机 token。客户端连接时不要把 token 放在 URL 中: + +``` +ws://localhost:9315/ws +``` + +无法发送 `Authorization` header 的 WebSocket 客户端需要使用 +`rcs.auth.` 子协议传递 token。 + +配置固定 token: + +```bash +ACP_AUTH_TOKEN=my-fixed-token acp-link ccb-bun -- --acp +``` + +禁用认证(不推荐,仅用于开发): + +```bash +acp-link --no-auth ccb-bun -- --acp +``` + +### RCS 集成 + +acp-link 支持将 ACP agent 注册到 Remote Control Server,通过 Web UI 远程操控。 + +```bash +# 通过环境变量配置 RCS 连接 +ACP_RCS_URL=http://localhost:3000 \ +ACP_RCS_TOKEN=sk-rcs-your-key \ +acp-link ccb-bun -- --acp +``` + +注册流程(两步): + +1. **REST 注册**:通过 `POST /v1/environments/bridge` 向 RCS 注册环境 +2. **WS identify**:建立 WebSocket 连接后发送 `identify` 消息(携带 agentId),替代完整 `register` + +RCS 的 ACP WebSocket 连接不接受 URL query token。acp-link 会通过 +`rcs.auth.` WebSocket 子协议发送 `ACP_RCS_TOKEN`。 + +``` +acp-link RCS + │ │ + │── POST /v1/environments/bridge ──►│ (REST 注册) + │◄── { agentId, sessionId } ───────│ + │ │ + │── WS connect ─────────────────►│ (WebSocket) + │── identify { agentId } ────────►│ (WS 标识) + │◄── identified ─────────────────│ + │ │ + │── ACP events ─────────────────►│ (双向消息转发) + │◄── user prompts/permissions ───│ +``` + +### 权限模式 + +#### permissionMode 传递链 + +权限模式通过整条链路传递:Web UI → RCS → acp-link → ACP agent。 + +支持的权限模式: + +- `default` — 每次请求权限确认 +- `auto` — 自动判断 +- `acceptEdits` — 自动接受编辑 +- `plan` — 规划模式 +- `dontAsk` — 不询问 +- `bypassPermissions` — 绕过权限(需 sandbox 环境) + +#### fallback 链 + +当客户端未显式传递 permissionMode 时,使用以下 fallback 链: + +``` +客户端传值 > config.permissionMode > ACP_PERMISSION_MODE 环境变量 +``` + +示例: + +```bash +ACP_PERMISSION_MODE=auto acp-link ccb-bun -- --acp +``` + +#### 权限管道改进 + +- **模式同步**:`applySessionMode` 在 agent 切换权限模式时同步 `appState.toolPermissionContext.mode`,确保内部权限上下文与 ACP 客户端状态一致。 +- **统一权限流水线**:`createAcpCanUseTool` 接入 `hasPermissionsToUseTool` 统一权限流水线,替代原来分散的处理逻辑。支持 `onModeChange` 回调,模式变更时实时同步。 +- **bypass 检测**:`bypassPermissions` 模式增加可用性检测 — 仅在非 root 或 sandbox 环境中允许启用,防止权限绕过的安全风险。 + +### ACP 协议支持矩阵 + +| 方法 | 状态 | 说明 | +|------|------|------| +| `initialize` | 支持 | 返回 agent 信息和能力 | +| `authenticate` | 支持 | 无需认证(自托管) | +| `newSession` | 支持 | 创建新会话 | +| `resumeSession` | 支持 | 恢复已有会话(含历史回放) | +| `loadSession` | 支持 | 加载指定会话(含历史回放) | +| `listSessions` | 支持 | 列出可用会话 | +| `forkSession` | 支持 | 分叉会话 | +| `closeSession` | 支持 | 关闭会话 | +| `prompt` | 支持 | 发送消息,支持排队 | +| `cancel` | 支持 | 取消当前/排队的 prompt | +| `setSessionMode` | 支持 | 切换权限模式 | +| `setSessionModel` | 支持 | 切换 AI 模型 | +| `setSessionConfigOption` | 支持 | 动态修改配置 | + +#### SessionUpdate 类型 + +| 类型 | 状态 | 说明 | +|------|------|------| +| `agent_message_chunk` | 支持 | 助手文本消息 | +| `agent_thought_chunk` | 支持 | 思考/推理内容 | +| `user_message_chunk` | 支持 | 用户消息(历史回放) | +| `tool_call` | 支持 | 工具调用开始 | +| `tool_call_update` | 支持 | 工具调用结果/状态更新 | +| `usage_update` | 支持 | token 用量 + context window | +| `plan` | 支持 | TodoWrite → plan entries | +| `available_commands_update` | 支持 | 斜杠命令 & skills 列表 | +| `current_mode_update` | 支持 | 模式切换通知 | +| `config_option_update` | 支持 | 配置更新通知 | + +### 环境变量与功能开关 + +#### 环境变量 + +| 变量 | 说明 | +|------|------| +| `ACP_AUTH_TOKEN` | 固定认证 token(默认自动生成) | +| `ACP_PERMISSION_MODE` | 默认权限模式 fallback | +| `ACP_RCS_URL` | RCS 服务器地址(启用 RCS 集成) | +| `ACP_RCS_TOKEN` | RCS API token | + +#### 功能开关 + +ACP Agent 与 acp-link 受 `FEATURE_ACP` 控制,build 和 dev 模式默认启用。源码目录: + +- ACP Agent:`src/services/acp/` +- acp-link:`packages/acp-link/`(相关 PR:#292,新增时间:2026-04-18) diff --git a/docs/features/agents/pipes-and-lan.md b/docs/features/agents/pipes-and-lan.md new file mode 100644 index 000000000..7c86006e7 --- /dev/null +++ b/docs/features/agents/pipes-and-lan.md @@ -0,0 +1,420 @@ +--- +title: "群控:本机 + 局域网多实例协作" +description: "多台 CCB 实例零配置组网,同机用 UDS、跨机用 LAN,自动发现与消息路由。包含 /pipes 命令、心跳机制、消息路由详解。" +keywords: ["群控", "局域网协作", "UDS", "多实例", "消息路由"] +--- + +# 群控:本机 + 局域网多实例协作 + +## 概述 + +Pipes 系统提供 Claude Code CLI 实例之间的通讯能力,让你可以在一台机器(main)上操控其他实例(sub),发送 prompt、查看执行结果、审批权限请求——全程零配置。 + +系统分两层,使用同一套协议(NDJSON)和同一套命令(`/pipes`、`/attach`、`/send` 等),对用户完全透明: + +1. **本机 Pipes(UDS)**:同一台机器上的多个 CLI 实例通过 Unix Domain Socket(Linux/macOS)或 Windows Named Pipe 协作 +2. **局域网 Pipes(LAN)**:不同机器上的 CLI 实例通过 TCP + UDP Multicast beacon 协作 + +> 严格区分:`/peers` 解决"找到其他会话并发消息"(通用消息投递),`/pipes` 解决"把一个 REPL 变成另一个 REPL 的受控 worker"(主从 REPL 协调平面)。两者职责不同,不要混淆。 + +### 两层职责拆解 + +| 层 | 面向 | 传输方式 | 对外入口 | +|------|------|----------|----------| +| UDS peer messaging | 任意 CCB 进程 | 本机 Unix socket / Named pipe | `/peers`、`SendMessageTool` 的 `uds:` | +| pipes control plane | 交互式 REPL 会话间的主从协作 | 本机 socket + LAN TCP | `/pipes`、`/attach`、`/detach`、`/send`、`/pipe-status`、`/claim-main` | + +两层都依赖本机 socket,但命名、角色模型、交互语义和 UI 集成都不同:peer 层按 socket 路径寻址,服务工具调用;pipes 层按 `cli-xxxxxxxx` 会话名和 `main/sub/master/slave` 角色工作,直接影响 REPL 提交路径和 PromptInput 页脚。 + +## 快速上手 + +### 场景一:本机多实例 + +```bash +# 终端 1 +bun run dev +# 启动后自动注册为 main + +# 终端 2 +bun run dev +# 自动注册为 sub-1,被 main 自动 attach +``` + +在终端 1 中输入 `/pipes`,可以看到两个实例。选中 sub-1 后,输入的消息会自动转发到 sub-1 执行。 + +### 场景二:局域网多机器 + +前置条件: + +- 两台或以上机器在同一局域网 +- 每台机器安装了 CCB 并能 `bun run dev` +- 防火墙允许 UDP 7101 + TCP 动态端口(见下方配置) + +```bash +# 机器 A (192.168.50.22) +bun run dev + +# 机器 B (192.168.50.27) +bun run dev +``` + +两边启动后等 3-5 秒(beacon 广播间隔),LAN peers 会自动发现并 attach。输入 `/pipes` 可看到标记 `[LAN]` 的远端实例。 + +## 防火墙配置 + +**每台机器都需要执行。** 请先确认网络为局域网(非公共 WiFi),路由器未开启 AP 隔离,两台机器在同一子网(`ping` 能通)。 + +### Windows(管理员 PowerShell) + +```powershell +New-NetFirewallRule -DisplayName "Claude Code LAN Beacon (UDP)" -Direction Inbound -Protocol UDP -LocalPort 7101 -Action Allow -Profile Private +New-NetFirewallRule -DisplayName "Claude Code LAN Pipes (TCP)" -Direction Inbound -Protocol TCP -LocalPort 1024-65535 -Program (Get-Command bun).Source -Action Allow -Profile Private +New-NetFirewallRule -DisplayName "Claude Code LAN Beacon Out (UDP)" -Direction Outbound -Protocol UDP -RemotePort 7101 -Action Allow -Profile Private +# 确认网络为"专用":Get-NetConnectionProfile +``` + +### macOS + +首次运行时系统弹出"允许接受传入连接"对话框,点击"允许"即可。如果使用 pf 防火墙: + +```bash +echo "pass in proto udp from any to any port 7101" | sudo pfctl -ef - +``` + +### Linux(firewalld / iptables) + +```bash +# firewalld +sudo firewall-cmd --zone=trusted --add-port=7101/udp --permanent +sudo firewall-cmd --zone=trusted --add-port=1024-65535/tcp --permanent +sudo firewall-cmd --reload + +# 或 iptables +sudo iptables -A INPUT -p udp --dport 7101 -j ACCEPT +sudo iptables -A INPUT -p tcp --dport 1024:65535 -m owner --uid-owner $(id -u) -j ACCEPT +``` + +## 交互面板与快捷键 + +### 状态栏 + +执行 `/pipes` 后,输入框底部出现 pipe 状态栏(单行),始终可见(直到会话结束): + +``` +pipe: cli-a91bad56 (main) 192.168.50.22 2/3 selected selected pipes only · ←/→ or m switch · Shift+↓ edit +``` + +显示:当前 pipe 名、角色、IP、已选数/总数、路由模式。 + +### 展开选择面板 + +按 **Shift+↓**(Shift + 下箭头)展开选择面板: + +``` +pipe: cli-a91bad56 (main) 192.168.50.22 ↑↓ move Space select ←/→ or m route Enter/Esc close Shift+↓ toggle + 当前普通 prompt 走 已选 sub;切换不会清空选择 + ☑ cli-da029538 (sub-1 XC/192.168.50.22) + ☐ cli-04d67950 (main vmwin11/192.168.50.27) + ☑ cli-893747d3 [offline] (sub-2 vmwin11/192.168.50.27) +``` + +### 面板快捷键 + +| 快捷键 | 场景 | 作用 | +|--------|------|------| +| **Shift+↓** | 状态栏可见时 | 展开/收起选择面板 | +| **↑ / ↓** | 面板展开时 | 上下移动光标 | +| **Space** | 面板展开时 | 切换当前光标所在 pipe 的选中状态(☑ ↔ ☐) | +| **Enter** | 面板展开时 | 确认并关闭面板 | +| **Esc** | 面板展开时 | 取消并关闭面板 | +| **← / → 或 M** | 状态栏可见且有选中 pipe 时 | 切换路由模式(`selected pipes only` ↔ `local main`) | + +### 完整操作流程示例 + +``` +1. 输入 /pipes → 状态栏出现,显示发现的实例 +2. 按 Shift+↓ → 展开选择面板 +3. 按 ↓ 移动到目标 pipe → 光标移到 cli-04d67950 +4. 按 Space → 选中 ☑ cli-04d67950 +5. 按 Enter → 确认,面板收起 +6. 输入 "帮我检查 git status" → prompt 自动发送到 cli-04d67950 执行 +7. 按 M → 切换到 local main 模式 +8. 输入 "本地做点什么" → 仅在本地执行 +9. 按 M → 切回 selected pipes only +10. 输入 "继续远端任务" → 又发送到 cli-04d67950 +``` + +远端执行结果会流式回传到你的消息列表: + +``` +[main vmwin11/192.168.50.27 / cli-04d67950] 正在检查 git status... +[main vmwin11/192.168.50.27 / cli-04d67950] Completed +``` + +## 消息路由 + +### 路由模式 + +通过 **M 键**(或 ← / →)切换,**无需展开面板**。切换路由模式**不会清空选择**——你可以在 `local main` 模式下保持选择,随时按 M 切回继续向远端发送。 + +| 模式 | 状态栏显示 | 行为 | +|------|-----------|------| +| `selected pipes only` | 绿色高亮 | 输入的 prompt **仅**发送到选中的 pipe,本地不执行 | +| `local main` | 灰色 | 输入的 prompt 在**本地 main** 执行,不转发到任何 pipe | + +### 选中 pipe 后的自动路由 + +1. 通过 `/pipes select` 或 Shift+↓ 面板选中一个或多个 pipe +2. 在输入框中正常输入消息 +3. 消息自动发送到所有选中的**已连接** pipe +4. 每个 pipe 独立执行,结果流式回传到 main 的消息列表 + +> 选中但未连接的 pipe 不会导致本地处理被错误跳过——只有已连接的 pipe 会收到广播。 + +## 命令参考 + +### /pipes + +显示所有发现的实例,管理选择状态。再次执行 `/pipes` 切换面板展开/收起。 + +``` +/pipes — 显示所有实例 + 切换选择面板 +/pipes select — 选中某实例(消息会广播到它) +/pipes deselect — 取消选中 +/pipes all — 全选 +/pipes none — 全部取消 +``` + +输出示例: + +``` +Your pipe: cli-a91bad56 +Role: main +Machine ID: 205d6c3a... +IP: 192.168.50.22 +Host: XC + +Main machine: 205d6c3a... (this machine) + [main] cli-a91bad56 XC/192.168.50.22 [alive] (you) + ☑ [sub-1] cli-da029538 XC/192.168.50.22 [alive] [connected] + +LAN Peers: + ☐ [main] cli-04d67950 vmwin11/192.168.50.27 tcp:192.168.50.27:58853 [LAN] + +Selected: cli-da029538 +``` + +### 其他命令 + +| 命令 | 说明 | +|------|------| +| `/attach ` | 手动 attach 到一个实例(自动识别 LAN peer 并通过 TCP 连接),使其成为 slave | +| `/detach ` | 断开与某个 slave 的连接 | +| `/send ` | 向指定 pipe 发送消息(不依赖选择状态,直接指定目标) | +| `/send tcp:host:port ` | 直接通过 TCP 地址发送 | +| `/claim-main` | 强制声明当前机器为 main(用于 main 意外退出后的恢复) | +| `/pipe-status` | 显示详细状态 | +| `/peers` | 列出所有已发现的 peer | + +通常不需要手动 attach——heartbeat 会自动发现并连接。attach 后对方变为 slave,你变为 master,可以向它发送 prompt。 + +示例: + +``` +/attach cli-04d67950 +/send cli-04d67950 请帮我检查一下日志 +/send tcp:192.168.50.27:58853 hello +``` + +## 权限转发 + +当远端 slave 执行需要权限的工具(如 BashTool)时: + +1. slave 发送 `permission_request` 到 main +2. main 弹出权限确认对话框,显示来源标记 `[role hostname/ip / pipeName]` +3. 用户确认/拒绝 +4. 结果发回 slave,继续或中断 + +> AI 通过 `SendMessageTool` 发送 `tcp:` 消息时需用户显式确认。 + +## 架构详解 + +### 通信协议 + +所有通讯使用 NDJSON(Newline-Delimited JSON),每行一个消息: + +```json +{"type":"ping","from":"cli-abc","ts":"2026-04-11T00:00:00.000Z"} +{"type":"prompt","data":"帮我查看 git status","from":"cli-abc","ts":"..."} +{"type":"stream","data":"正在执行...","from":"cli-def","ts":"..."} +{"type":"done","data":"","from":"cli-def","ts":"..."} +``` + +### 消息类型 + +| 类型 | 方向 | 说明 | +|------|------|------| +| `ping`/`pong` | 双向 | 健康检查 | +| `attach_request`/`accept`/`reject` | M→S/S→M | 连接控制 | +| `detach` | M→S | 断开连接 | +| `prompt` | M→S | 主向从发送 prompt | +| `prompt_ack` | S→M | 从确认接收 | +| `stream` | S→M | 从流式回传 AI 输出 | +| `tool_start`/`tool_result` | S→M | 工具执行通知 | +| `done` | S→M | 本轮完成 | +| `error` | 双向 | 错误通知 | +| `permission_request`/`response`/`cancel` | 双向 | 权限审批转发 | + +### 传输层 + +``` + 本机 LAN + ┌──────────────┐ ┌──────────────┐ + │ PipeServer │ │ PipeServer │ + │ UDS sock │ │ UDS sock │ + │ TCP :rand │◄───TCP───►│ TCP :rand │ + ├──────────────┤ ├──────────────┤ + │ LanBeacon │◄──UDP────►│ LanBeacon │ + │ 224.0.71.67 │ mcast │ 224.0.71.67 │ + └──────────────┘ └──────────────┘ +``` + +- **UDS / Named Pipe**:本机实例间通讯,通过文件系统路径寻址(`~/.claude/pipes/cli-xxx.sock`) +- **TCP**:LAN 实例间通讯,动态端口,通过 beacon 发现 +- **UDP Multicast**:peer 发现,组地址 `224.0.71.67`,端口 `7101`,TTL=1(不跨路由器),3 秒广播一次 announce 包 + +### 角色模型 + +| 角色 | 说明 | +|------|------| +| `main` | 首个启动的实例,管理 registry | +| `sub` | 后续启动的同机实例(或被 attach 的 LAN 实例) | +| `master` | attach 了至少一个 slave 的实例 | +| `slave` | 被 master attach 控制的实例 | + +**角色转换规则:** + +- 首个启动 → `main` +- 同机后续启动 → `sub`(自动被 main attach → `slave`) +- LAN 发现 → 两边都是 `main`,heartbeat 自动互相 attach(跨机器 attach 时,两边都可以是 main——不要求对方必须是 sub) +- 被 attach → 变为 `slave`(可通过 `/detach` 恢复) + +### 发现机制 + +**本机**:通过 `~/.claude/pipes/registry.json` 文件(带文件锁),`machineId` 绑定主机身份。同机 peer 层读取 `~/.claude/sessions/*.json`,按 `messagingSocketPath` 寻址。 + +**LAN**:通过 UDP multicast beacon: + +1. 每台机器启动时创建 UDP multicast beacon,每 3 秒广播一次 `{ proto, pipeName, machineId, ip, tcpPort, role }` +2. 收到其他实例的 announce → 记入 peers Map +3. 15 秒未收到广播 → 标记 peer lost +4. Heartbeat 合并 local registry + beacon peers → 统一 attach 目标列表 + +### Heartbeat 循环(5 秒间隔) + +**main/master 角色:** + +1. `cleanupStaleEntries()` — 清理 registry 中死掉的条目 +2. `getAliveSubs()` — 获取存活的本地 subs +3. `refreshDiscoveredPipes()` — 刷新 discoveredPipes(包含 LAN peers) +4. 合并 LAN peers 到 state +5. 构建统一 attach 目标列表 — 本地 subs + LAN peers +6. 遍历未连接的目标 → 自动 attach +7. 清理断开的 slave 连接 — 同时检查 local registry 和 beacon + +**sub 角色:** + +1. 检测 main 是否存活 +2. main 死亡 → 同机则接管 main 角色,跨机则独立 + +### 当前 REPL 行为 + +当前线上行为由 `src/screens/REPL.tsx` 的内联实现负责(以该文件、`pipeTransport.ts`、`pipeRegistry.ts` 为事实来源): + +1. 启动时创建当前 REPL 的 pipe server +2. 通过 `pipeRegistry` 判定 `main` / `sub` +3. 处理 `attach_request` / `detach` / `prompt` +4. 主实例心跳探测并维护 `slaves` +5. `/pipes` 打开状态栏并维护选择器 +6. 提交普通消息时,仅向**已连接**的 selected pipes 广播 + +过去的未接线 hook 方案已收敛,选中但未连接的 pipe 不会导致本地处理被错误跳过。 + +## 关键文件 + +| 文件 | 职责 | +|------|------| +| `src/utils/pipeTransport.ts` | PipeServer(双模 UDS+TCP)、PipeClient、类型定义 | +| `src/utils/lanBeacon.ts` | UDP multicast beacon、singleton 管理 | +| `src/utils/pipeRegistry.ts` | Registry CRUD、角色判定、machineId、LAN merge | +| `src/utils/peerAddress.ts` | 地址解析(uds:/bridge:/tcp: scheme) | +| `src/utils/udsMessaging.ts` | UDS peer messaging 服务端 | +| `src/utils/udsClient.ts` | UDS peer messaging 客户端 | +| `src/screens/REPL.tsx` | Bootstrap、heartbeat、cleanup、prompt 路由 | +| `src/hooks/useMasterMonitor.ts` | Slave client registry、消息订阅 | +| `src/hooks/useSlaveNotifications.ts` | Slave 端通知处理 | +| `src/commands/pipes/pipes.ts` | /pipes 命令 | +| `src/commands/attach/attach.ts` | /attach 命令 | +| `src/commands/send/send.ts` | /send 命令 | +| `packages/builtin-tools/src/tools/SendMessageTool/SendMessageTool.ts` | AI 发消息工具(含 tcp: 支持) | + +## 常见问题 + +### 看不到 LAN peer + +1. 检查防火墙是否放行 UDP 7101 +2. `Get-NetConnectionProfile`(Windows)确认网络为"专用" +3. 确认两台机器在同一子网(`ping` 能通) +4. 路由器未开启 AP 隔离 + +### 连接超时 + +1. 检查 TCP 入站防火墙规则 +2. 确认没有 VPN 劫持流量 +3. 尝试 `/send tcp:ip:port hello` 直接测试 + +### beacon 绑到了错误网卡 + +Windows 上 WSL/Docker 虚拟网卡可能劫持 multicast。beacon 会自动选择非内部 IPv4 接口。如果选错,检查 `getLocalIp()` 返回值。 + +## 配置 + +### Feature Flag + +| Flag | 控制范围 | 默认 | +|------|----------|------| +| `UDS_INBOX` | 本机 Pipe IPC 全部功能(含 UDS peer messaging + pipes control plane) | dev/build 启用 | +| `LAN_PIPES` | 局域网 TCP + UDP beacon 扩展 | dev/build 启用 | + +手动启用: + +```bash +FEATURE_UDS_INBOX=1 FEATURE_LAN_PIPES=1 bun run dev +``` + +### 安全说明 + +- TCP 连接当前**无认证**——同 LAN 内知道端口号即可连接 +- Multicast TTL=1,不跨路由器 +- 建议仅在信任的局域网中使用 + +### 后续优化方向 + +**安全(P0)** + +1. TCP 认证:首次连接时交换 HMAC-SHA256 token(基于 machineId + session secret) +2. JSON schema 验证:在所有 `JSON.parse` 入口点增加 Zod 校验,防 prototype pollution +3. Beacon 信息脱敏:hash machineId 后再广播 + +**可靠性(P1)** + +4. 多网卡选择:`getLocalIp()` 应优先选择 RFC 1918 地址,排除 VPN/Docker 接口 +5. TCP target 验证:`parseTcpTarget()` 应限制目标为已知 beacon peers 或 RFC 1918 范围 +6. PipeServer close():改为 `Promise.allSettled` 并行关闭 UDS + TCP,加 `_closing` guard + +**功能(P2)** + +7. mDNS/DNS-SD:作为 multicast 受限环境下的 beacon 替代方案 +8. 固定端口配置:允许用户指定 TCP 端口范围,便于防火墙精确配置 +9. TLS 加密:TCP 传输加密,防中间人窃听 +10. 双向 prompt:当前只有 master → slave 方向,可考虑 slave 主动向 master 发送结果/请求 diff --git a/docs/features/all-features-guide.md b/docs/features/all-features-guide.md deleted file mode 100644 index 353241ef5..000000000 --- a/docs/features/all-features-guide.md +++ /dev/null @@ -1,576 +0,0 @@ -# Claude Code Best (CCB) — 全功能使用指南 - -本文档覆盖我们通过 13 个 PR 为 CCB 恢复/新增的**全部功能**,按类别组织,每个功能包含说明、使用方法和示例。 - ---- - -## 目录 - -1. [Buddy 伴侣系统](#1-buddy-伴侣系统) -2. [Remote Control 远程控制](#2-remote-control-远程控制) -3. [定时任务 /triggers](#3-定时任务-triggers) -4. [Voice Mode 语音模式](#4-voice-mode-语音模式) -5. [Chrome 浏览器控制](#5-chrome-浏览器控制) -6. [Computer Use 屏幕操控](#6-computer-use-屏幕操控) -7. [Feature Flags 与 GrowthBook](#7-feature-flags-与-growthbook) -8. [/ultraplan 高级规划](#8-ultraplan-高级规划) -9. [Daemon 后台守护](#9-daemon-后台守护) -10. [Pipe IPC 多实例协作](#10-pipe-ipc-多实例协作) -11. [LAN Pipes 局域网群控](#11-lan-pipes-局域网群控) -12. [Monitor 后台监控](#12-monitor-后台监控) -13. [Workflow 工作流脚本](#13-workflow-工作流脚本) -14. [Coordinator 多Worker协调](#14-coordinator-多worker协调) -15. [Proactive 自主模式](#15-proactive-自主模式) -16. [History / Snip 历史管理](#16-history--snip-历史管理) -17. [Fork 子Agent](#17-fork-子agent) -18. [其他恢复的工具](#18-其他恢复的工具) - ---- - -## 1. Buddy 伴侣系统 - -**PR**: #82 `refactor(buddy): align companion system with official CLI` -**Feature Flag**: `BUDDY` - -### 说明 -Buddy 是一个后台运行的伴侣 AI,在你主对话进行的同时,异步观察会话内容并提供建议。 - -### 使用 -```bash -# 启动时自动加载(feature 默认开启) -bun run dev - -# 在对话中,Buddy 会在适当时机自动提供建议 -# 例如当你在调试时,Buddy 可能提示你检查日志 -``` - ---- - -## 2. Remote Control 远程控制 - -**PR**: #60 `feat: enable Remote Control (BRIDGE_MODE)` + #170 `feat: restore daemon supervisor` -**Feature Flag**: `BRIDGE_MODE` - -### 说明 -通过 WebSocket 远程控制 Claude Code 会话。支持自托管私有部署。 - -### 使用 -```bash -# 启动远程控制模式 -bun run dev -- remote-control - -# 使用自托管服务器 -CLAUDE_BRIDGE_BASE_URL=https://your-server.com CLAUDE_BRIDGE_OAUTH_TOKEN=your-token bun run dev --remote-control - -# 或通过 /remote-control 命令在会话中启动 -/remote-control -``` - -### 命令 -- `claude remote-control` / `claude rc` — 启动远程控制客户端 -- `claude bridge` — 同上(别名) - ---- - -## 3. 定时任务 /triggers - -**PR**: #88 `feat: enable /schedule by adding AGENT_TRIGGERS_REMOTE` -**Feature Flag**: `AGENT_TRIGGERS_REMOTE` - -> 命令名已从 `/schedule` 改为 `/triggers`,避免与上游 bundled skill `schedule` 冲突。`/cron` 是别名。 - -### 说明 -创建定时执行的远程 agent 任务,支持 cron 表达式。 - -### 使用 -``` -/triggers create "每天检查依赖更新" --cron "0 9 * * *" --prompt "检查 package.json 中的过期依赖并创建更新 PR" -/triggers list — 列出所有定时任务 -/triggers delete — 删除指定任务 -``` - ---- - -## 4. Voice Mode 语音模式 - -**PR**: #92 `feat: enable /voice mode with native audio binaries` -**Feature Flag**: `VOICE_MODE` - -### 说明 -Push-to-Talk 语音输入,音频通过 WebSocket 流式传输到 Anthropic STT(Nova 3)。需要 Anthropic OAuth 认证(非 API key)。 - -### 使用 -```bash -# 确保已通过 OAuth 登录 -claude auth login - -# 在会话中按住指定键说话 -# 松开后自动转写为文字输入 -``` - -### 前提条件 -- Anthropic OAuth 认证(不支持 API key 模式) -- 系统麦克风权限 - ---- - -## 5. Chrome 浏览器控制 - -**PR**: #93 `feat: enable Claude in Chrome MCP with full browser control` -**Feature Flag**: `CHICAGO_MCP` - -### 说明 -通过 Chrome 扩展控制浏览器:导航、点击、填表、截图、执行 JS。 - -### 使用 -```bash -# 启动带 Chrome 控制的模式 -bun run dev -- --chrome - -# 安装 Chrome 扩展后,AI 可以: -# - 打开网页、点击按钮 -# - 填写表单 -# - 截取页面内容 -# - 执行 JavaScript -``` - -### AI 可用工具 -- `navigate` — 导航到 URL -- `click` / `find` / `form_input` — 页面交互 -- `get_page_text` / `read_page` — 读取内容 -- `javascript_tool` — 执行 JS -- `gif_creator` — 录制操作 GIF - ---- - -## 6. Computer Use 屏幕操控 - -**PR**: #98 + #137 `feat: Computer Use — 跨平台 Executor + Python Bridge + GUI 无障碍` -**Feature Flag**: `CHICAGO_MCP` - -### 说明 -跨平台屏幕操控:截图、键鼠模拟、应用管理。支持 macOS + Windows,Linux 后端待完成。 - -### 使用 -```bash -# 启动后 AI 可自动调用屏幕操控工具 -bun run dev - -# AI 可以: -# - 截取屏幕/窗口截图 -# - 模拟键盘输入和鼠标操作 -# - 列出运行的应用 -# - 使用剪贴板 -``` - -### 平台支持 -| 平台 | 截图 | 键鼠 | 应用管理 | -|------|------|------|----------| -| macOS | ✅ | ✅ | ✅ | -| Windows | ✅ | ✅ | ✅ | -| Linux | ⏳ | ⏳ | ⏳ | - ---- - -## 7. Feature Flags 与 GrowthBook - -**PR**: #140 + #153 `feat: enable GrowthBook local gate defaults` -**Feature Flags**: `SHOT_STATS`, `PROMPT_CACHE_BREAK_DETECTION`, `TOKEN_BUDGET` - -### 说明 -本地 GrowthBook gate defaults 机制,绕过远程 feature flag 服务,确保功能在无网络时也可使用。 - -### 使用 -```bash -# 通过环境变量启用任意 feature -FEATURE_PROACTIVE=1 bun run dev - -# dev/build 模式有各自的默认启用列表 -# 查看 scripts/dev.ts 中的 DEFAULT_FEATURES -``` - -### 关键 feature flags -| Flag | 说明 | -|------|------| -| `SHOT_STATS` | API 调用统计 | -| `TOKEN_BUDGET` | Token 预算控制 | -| `PROMPT_CACHE_BREAK_DETECTION` | Prompt 缓存命中检测 | - ---- - -## 8. /ultraplan 高级规划 - -**PR**: #156 `feat: enable /ultraplan and harden GrowthBook fallback chain` -**Feature Flag**: `ULTRAPLAN` - -### 说明 -高级多 agent 规划模式。将复杂任务分解为多个阶段,每阶段可分配给不同 agent 并行执行。 - -### 使用 -``` -/ultraplan 实现一个完整的用户认证系统,包括注册、登录、密码重置、OAuth 集成 -``` - -AI 会生成: -1. 任务分解(多阶段) -2. 每阶段的 agent 分配 -3. 依赖关系图 -4. 并行执行计划 - ---- - -## 9. Daemon 后台守护 - -**PR**: #170 `feat: restore daemon supervisor and remoteControlServer command` -**Feature Flag**: `DAEMON` - -### 说明 -Daemon 模式允许 Claude Code 作为后台长驻进程运行,管理多个 worker。 - -### 使用 -```bash -# 启动 daemon -claude daemon start - -# 查看状态 -claude daemon status - -# 停止 -claude daemon stop - -# 启动远程控制服务器 -bun run rcs -``` - ---- - -## 10. Pipe IPC 多实例协作 - -**PR**: #241 `feat: restore pipe IPC, LAN pipes, monitor tool` -**Feature Flag**: `UDS_INBOX` - -### 说明 -同一台机器上的多个 Claude Code 实例通过 UDS(Unix Domain Socket / Windows Named Pipe)自动发现并协作。首个启动的实例成为 main,后续自动注册为 sub。 - -### 使用 - -**启动多实例**: -```bash -# 终端 1 -bun run dev -# → 自动成为 main - -# 终端 2 -bun run dev -# → 自动成为 sub-1,被 main attach -``` - -**管理实例**: -``` -/pipes — 显示所有实例,Shift+↓ 展开选择面板 -/pipes select — 选中实例 -/pipes all — 全选 -/pipes none — 取消全选 -/attach — 手动 attach 某实例 -/detach — 断开连接 -/send — 向指定实例发送消息 -/claim-main — 强制声明为 main -/pipe-status — 显示详细状态 -/peers — 列出所有已发现的 peer -``` - -**选择面板操作**: -1. 按 `Shift+↓` 展开面板 -2. `↑/↓` 移动光标 -3. `Space` 选中/取消 pipe -4. `Enter` 确认关闭 -5. `←/→` 切换路由模式(selected pipes ↔ local main) - -**消息广播**: -选中 pipe 后,输入的消息自动路由到所有选中的 slave 执行,结果流式回传到 main。 - -**权限转发**: -slave 执行需要权限的工具时(如 BashTool),权限请求自动转发到 main 的确认队列。 - ---- - -## 11. LAN Pipes 局域网群控 - -**PR**: #241(同上) -**Feature Flag**: `LAN_PIPES` - -### 说明 -在 Pipe IPC 基础上增加 TCP 传输层和 UDP Multicast 发现,实现跨机器零配置协作。 - -### 使用 - -**局域网多机器**: -```bash -# 机器 A (192.168.50.22) -bun run dev - -# 机器 B (192.168.50.27) -bun run dev - -# 两边启动后 3-5 秒自动发现和 attach -# /pipes 显示 [LAN] 标记的远端实例 -``` - -**防火墙配置**(每台机器都需要): - -Windows(管理员 PowerShell): -```powershell -New-NetFirewallRule -DisplayName "CCB LAN Beacon (UDP)" -Direction Inbound -Protocol UDP -LocalPort 7101 -Action Allow -Profile Private -New-NetFirewallRule -DisplayName "CCB LAN Pipes (TCP)" -Direction Inbound -Protocol TCP -LocalPort 1024-65535 -Program (Get-Command bun).Source -Action Allow -Profile Private -New-NetFirewallRule -DisplayName "CCB LAN Beacon Out (UDP)" -Direction Outbound -Protocol UDP -RemotePort 7101 -Action Allow -Profile Private -``` - -macOS: -```bash -# 首次运行时系统弹对话框,点"允许"即可 -``` - -Linux: -```bash -sudo firewall-cmd --zone=trusted --add-port=7101/udp --permanent -sudo firewall-cmd --zone=trusted --add-port=1024-65535/tcp --permanent -sudo firewall-cmd --reload -``` - -**通知显示格式**: -``` -# 本机 sub -Routed to [sub-1]; main can continue other tasks - -# LAN peer -Routed to [main] vmwin11/192.168.50.27; main can continue other tasks -``` - ---- - -## 12. Monitor 后台监控 - -**PR**: #241(同上) -**Feature Flag**: `MONITOR_TOOL` - -### 说明 -在后台运行 shell 命令持续监控输出(类似 `watch` 命令)。AI 也可自主调用 MonitorTool。 - -### 使用 - -**用户命令**: -``` -/monitor tail -f /var/log/syslog -/monitor watch -n 5 docker ps -/monitor "while true; do curl -s localhost:3000/health; sleep 10; done" -``` - -**查看监控**: -- 按 `Shift+Down` 展开后台任务面板 -- 查看监控输出和状态 - -**Windows 兼容**: -`watch -n ` 自动转为 PowerShell 循环: -```powershell -while($true){ ; Start-Sleep -Seconds } -``` - -**AI 调用**: -AI 可在对话中自动调用 `MonitorTool` 监控日志、构建输出等。 - ---- - -## 13. Workflow 工作流脚本 - -**PR**: #241(同上) -**Feature Flag**: `WORKFLOW_SCRIPTS` - -### 说明 -执行 `.claude/workflows/` 目录下的用户定义工作流脚本。 - -### 使用 - -**创建工作流**: -```bash -mkdir -p .claude/workflows -cat > .claude/workflows/deploy.sh << 'EOF' -#!/bin/bash -echo "Running tests..." -bun test -echo "Building..." -bun run build -echo "Deploying..." -EOF -chmod +x .claude/workflows/deploy.sh -``` - -**列出可用工作流**: -``` -/workflows -``` - -**AI 调用**: -AI 可通过 `WorkflowTool` 自动执行工作流: -``` -请执行 deploy 工作流 -``` - ---- - -## 14. Coordinator 多Worker协调 - -**PR**: #241(同上) -**Feature Flag**: `COORDINATOR_MODE` - -### 说明 -启用 coordinator 模式后,AI 可自动将任务分配给多个 worker 并行执行。 - -### 使用 -``` -/coordinator — 切换 coordinator 模式开/关 -``` - -启用后,AI 在处理复杂任务时会: -1. 分析任务可并行的部分 -2. 自动创建 worker 分支 -3. 分配子任务 -4. 汇总结果 - ---- - -## 15. Proactive 自主模式 - -**PR**: #241(同上) -**Feature Flag**: `PROACTIVE` / `KAIROS` - -### 说明 -启用后 AI 会主动发起操作(而不仅回应用户输入),例如自动检测文件变更、主动提出优化建议。 - -### 使用 -``` -/proactive — 切换 proactive 模式开/关 -``` - ---- - -## 16. History / Snip 历史管理 - -**PR**: #241(同上) -**Feature Flag**: `HISTORY_SNIP` - -### 说明 -查看和管理对话历史,支持手动截断以释放上下文窗口空间。 - -### 使用 -``` -/history — 显示对话历史摘要 -/force-snip — 强制在当前位置截断历史 -``` - -AI 也可通过 `SnipTool` 自动截断过长的对话: -``` -对话太长了,请帮我截断历史 -``` - ---- - -## 17. Fork 子Agent - -**PR**: #241(同上) -**Feature Flag**: `FORK_SUBAGENT` - -### 说明 -在当前对话上下文中 fork 一个独立的子 agent,继承完整会话状态独立执行。 - -### 使用 -``` -/fork — 基于当前上下文 fork 子 agent -``` - -子 agent 会: -- 继承当前的全部对话历史 -- 在独立的执行环境中运行 -- 不影响主会话状态 - ---- - -## 18. 其他恢复的工具 - -以下工具从 stub 恢复为完整实现: - -| 工具 | 说明 | 使用 | -|------|------|------| -| `SleepTool` | 暂停执行指定时间 | AI 在轮询场景自动调用 | -| `WebBrowserTool` | 终端内网页交互 | AI 需要查看网页时调用 | -| `SubscribePRTool` | 订阅 GitHub PR 变更 | `/subscribe-pr` 或 AI 调用 | -| `PushNotificationTool` | 推送桌面通知 | AI 在长任务完成时调用 | -| `CtxInspectTool` | 检查上下文窗口使用 | AI 判断上下文剩余空间 | -| `TerminalCaptureTool` | 截取终端屏幕 | AI 需要看终端输出时调用 | -| `SendUserFileTool` | 向用户发送文件 | AI 导出文件时调用 | -| `REPLTool` | 启动子 REPL 会话 | AI 需要独立交互环境时调用 | -| `VerifyPlanExecutionTool` | 验证执行计划完成度 | AI 完成计划后自动验证 | -| `SuggestBackgroundPRTool` | 建议创建后台 PR | AI 发现可独立的变更时提议 | -| `ListPeersTool` | 列出已发现的 peer | AI 查询多实例状态时调用 | - ---- - -## 附录:全部 Feature Flags - -| Flag | 默认 | 说明 | -|------|------|------| -| `BUDDY` | ✅ dev only | 伴侣系统 | -| `BRIDGE_MODE` | ✅ dev only | 远程控制 | -| `VOICE_MODE` | ✅ dev+build | 语音模式 | -| `CHICAGO_MCP` | ✅ dev+build | Computer Use + Chrome | -| `AGENT_TRIGGERS_REMOTE` | ✅ dev+build | 定时任务 | -| `SHOT_STATS` | ✅ dev+build | API 统计 | -| `TOKEN_BUDGET` | ✅ dev+build | Token 预算 | -| `PROMPT_CACHE_BREAK_DETECTION` | ✅ dev+build | 缓存检测 | -| `ULTRAPLAN` | ✅ dev+build | 高级规划 | -| `DAEMON` | ✅ dev+build | 后台守护 | -| `UDS_INBOX` | ✅ dev only | Pipe IPC | -| `LAN_PIPES` | ✅ dev only | LAN 群控 | -| `MONITOR_TOOL` | ✅ dev+build | 后台监控 | -| `WORKFLOW_SCRIPTS` | ✅ dev+build | 工作流脚本 | -| `FORK_SUBAGENT` | ✅ dev+build | 子 Agent | -| `KAIROS` | ✅ dev+build | Kairos 调度 | -| `COORDINATOR_MODE` | ✅ dev+build | 多 Worker | -| `HISTORY_SNIP` | ✅ dev+build | 历史管理 | -| `CONTEXT_COLLAPSE` | ✅ dev+build | 上下文折叠 | -| `ULTRATHINK` | ✅ dev+build | 扩展思考 | -| `EXTRACT_MEMORIES` | ✅ dev+build | 自动记忆提取 | -| `VERIFICATION_AGENT` | ✅ dev+build | 验证 Agent | -| `KAIROS_BRIEF` | ✅ dev+build | Brief 模式 | -| `AWAY_SUMMARY` | ✅ dev+build | 离开摘要 | -| `ACP` | ✅ dev+build | ACP 协议 | -| `LODESTONE` | ✅ dev+build | 深度链接 | -| `BUILTIN_EXPLORE_PLAN_AGENTS` | ✅ dev+build | 内置 Explore/Plan agent | -| `AGENT_TRIGGERS` | ✅ dev+build | 本地定时任务 | -| `BG_SESSIONS` | ✅ dev only | 后台会话 | -| `TEMPLATES` | ✅ dev only | 模板系统 | -| `TRANSCRIPT_CLASSIFIER` | ✅ dev only | 对话分类 | - -手动启用任意 flag: -```bash -FEATURE_FLAG_NAME=1 bun run dev -``` - ---- - -## 附录:PR 列表 - -| PR | 日期 | 标题 | -|----|------|------| -| #60 | 2026-04-02 | feat: enable Remote Control (BRIDGE_MODE) | -| #82 | 2026-04-03 | refactor(buddy): align companion system | -| #88 | 2026-04-03 | feat: enable /schedule (AGENT_TRIGGERS_REMOTE) | -| #89 | 2026-04-03 | feat: built-in status line | -| #92 | 2026-04-03 | feat: enable /voice mode | -| #93 | 2026-04-03 | feat: enable Chrome MCP | -| #98 | 2026-04-03 | feat: enable Computer Use (macOS + Windows + Linux) | -| #137 | 2026-04-05 | feat: Computer Use v2 — 跨平台 Executor | -| #140 | 2026-04-05 | feat: enable SHOT_STATS, TOKEN_BUDGET | -| #153 | 2026-04-06 | feat: enable GrowthBook local gate defaults | -| #156 | 2026-04-06 | feat: enable /ultraplan | -| #170 | 2026-04-07 | feat: restore daemon supervisor | -| #241 | 2026-04-11 | feat: restore pipe IPC, LAN pipes, monitor tool | diff --git a/docs/features/autofix-pr.md b/docs/features/autofix-pr.md deleted file mode 100644 index 2ef33a6d4..000000000 --- a/docs/features/autofix-pr.md +++ /dev/null @@ -1,769 +0,0 @@ -# `/autofix-pr` 命令实现规格文档 - -> **状态**:规划阶段(2026-04-29),等待评审通过后进入实施。 -> **Worktree**:`E:\Source_code\Claude-code-bast-autofix-pr`,分支 `feat/autofix-pr`,基于 `origin/main` 4f1649e2。 -> **架构**:R(Remote-via-CCR),完整版(含 stop 子命令、单例锁、subscribePR、in-process teammate、skills 探测)。 - ---- - -## 一、背景 - -### 1.1 问题 - -本仓库(`Claude-code-bast`)是 Anthropic 官方 `@anthropic-ai/claude-code` 的反编译/重构版本。许多远程能力被 stub 化处理 —— `/autofix-pr` 是其中之一: - -```js -// src/commands/autofix-pr/index.js(当前 stub) -export default { isEnabled: () => false, isHidden: true, name: 'stub' }; -``` - -三个字段共同导致命令在斜杠菜单中完全不可见、不可调起: - -| 字段 | 值 | 效果 | -|---|---|---| -| `isEnabled` | `() => false` | 注册时被判定不可用 | -| `isHidden` | `true` | 即使被列出也被过滤 | -| `name` | `'stub'` | 实际注册名是 `'stub'`,输入 `/autofix-pr` 无法匹配 | - -### 1.2 用户场景 - -用户在 fork 仓库(`feat/autonomy-lifecycle-upstream` 分支)尝试对上游 `claude-code-best/claude-code#386` 跑 `/autofix-pr 386`,多次报 `git_repository source setup error`。根因:官方派发的远程 session 落在被 MCP 拒绝访问的仓库(`amdosion/claude-code-bast`),权限/可见性问题。 - -### 1.3 目标 - -| ID | 需求 | 验收 | -|---|---|---| -| R1 | 命令在斜杠菜单可见可调起 | 输入 `/au` 出现补全 | -| R2 | 跨仓库 PR:从本地 fork 触发对上游 PR 的修复 | `/autofix-pr 386` 不报 repo-not-allowed | -| R3 | 远端真正完成修复并 push 回 PR 分支 | PR 出现来自远端的新 commit | -| R4 | 不破坏现存其他 stub(如 `share`) | 只动 `autofix-pr` | -| R5 | TypeScript 严格模式,`bun run typecheck` 零错误 | CI 绿 | -| R6 | bridge 可触发(Remote Control 场景) | `bridgeSafe: true` 生效 | -| R7 | 支持 stop/off 子命令 | `/autofix-pr stop` 能终止当前监控 | -| R8 | 单例锁防止重复派发 | 已监控 PR 时拒绝新启动并提示 | - ---- - -## 二、反编译调研结论(来源:`C:\Users\12180\.local\bin\claude.exe`) - -`claude.exe` 是 242MB 的 Bun 原生编译产物(JS 源码 embed 在二进制内)。通过对该文件的字符串提取(`grep -aoE`)反推出完整调用链。 - -### 2.1 主入口函数结构 - -```js -async function entry(input, q, ctx) { - const isStop = input === "stop" || input === "off" - const args = { freeformPrompt: input } - return main(args, q, ctx) -} - -async function main(args, q, { signal, onProgress }) { - // args 字段:{ prNumber, target, freeformPrompt, repoPath, skills } - d("tengu_autofix_pr_started", { - action: "start", - has_pr_number: String(args.prNumber !== undefined), - has_repo_path: String(args.repoPath !== undefined), - }) - // ... -} -``` - -### 2.2 `teleportToRemote` 调用签名(黄金证据) - -```ts -const session = await teleportToRemote({ - initialMessage: C, // 给远端的初始消息 - source: "autofix_pr", // ⚠️ 新字段,本仓库 teleport.tsx 没有 - branchName: N, // PR 头分支 - reuseOutcomeBranch: N, // 与 branchName 同 — 远端 push 回原分支 - title: `Autofix PR: ${owner}/${repo}#${prNumber} (${branch})`, - useDefaultEnvironment: true, // ⚠️ 不用 synthetic env(与 ultrareview 不同) - signal, - githubPr: { owner, repo, number }, - cwd: repoPath, - onBundleFail: (msg) => { /* ... */ }, -}) -``` - -**与 `ultrareview` 的关键差异**: - -| 字段 | ultrareview | autofix-pr | -|---|---|---| -| `environmentId` | `env_011111111111111111111113`(synthetic) | 不传 | -| `useDefaultEnvironment` | 不传 | `true` | -| `useBundle` | 有(branch mode) | 不传(`skipBundle` 隐含于不传 bundle) | -| `reuseOutcomeBranch` | 不传 | 传(远端 push 回原 PR 分支) | -| `githubPr` | 不传 | 必传 | -| `source` | 不传 | `"autofix_pr"` | -| `environmentVariables` | `BUGHUNTER_*` 一堆 | 不传 | - -### 2.3 `registerRemoteAgentTask` 调用 - -```ts -registerRemoteAgentTask({ - remoteTaskType: "autofix-pr", - session: { id: session.id, title: session.title }, - command, - isLongRunning: true, // poll 不消费 result,靠通知周期驱动 -}) -``` - -### 2.4 子命令解析 - -``` -/autofix-pr → 启动监控 + 派 CCR session -/autofix-pr stop → 停止当前监控 -/autofix-pr off → 同 stop -/autofix-pr → 自由 prompt 模式(无 PR 号) -/autofix-pr /# → 跨仓库(覆盖 R2 验收) -``` - -### 2.5 状态模型 - -- **单例锁**:同一时刻只能监控一个 PR。重复启动报:`already monitoring ${repo}#${prNumber}. Run /autofix-pr stop first.`(error_code: `rc_already_monitoring_other`) -- **PR 订阅**:调 `kairos.subscribePR(owner, repo, taskId)` —— 依赖 `KAIROS_GITHUB_WEBHOOKS` feature flag(用户已订阅,可用) -- **in-process teammate**:注册后台 agent - ```ts - const teammate = { - agentId, - agentName: "autofix-pr", - teamName: "_autofix", - color: undefined, - planModeRequired: false, - parentSessionId, - } - ``` -- **Skills 探测**:扫项目里 autofix-related skills(如 `.claude/skills/autofix-*` 或根目录 `AUTOFIX.md`),命中后拼到 prompt:`Run X and Y for custom instructions on how to autofix.` - -### 2.6 Telemetry - -| 事件 | 字段 | -|---|---| -| `tengu_autofix_pr_started` | `{ action, has_pr_number, has_repo_path }` | -| `tengu_autofix_pr_result` | `{ result, error_code? }` | - -`result` 取值:`success_rc` / `failed` / `cancelled` - -`error_code` 取值: - -| code | 含义 | -|---|---| -| `rc_already_monitoring_other` | 已在监控其他 PR | -| `session_create_failed` | teleport 失败 | -| `exception` | 未捕获异常 | - -### 2.7 错误返回结构 - -```ts -function errorResult(message: string, code: string) { - d("tengu_autofix_pr_result", { result: "failed", error_code: code }) - return { - kind: "error", - message: `Autofix PR failed: ${message}`, - code, - } -} - -function cancelledResult() { - d("tengu_autofix_pr_result", { result: "cancelled" }) - return { kind: "cancelled" } -} -``` - ---- - -## 三、本仓库现有基础设施盘点 - -下表列出实现 `/autofix-pr` 时**直接复用**的现成能力(已确认完整可用): - -| 能力 | 文件 | 角色 | -|---|---|---| -| `teleportToRemote` | `src/utils/teleport.tsx:947` | 派 CCR 远端 session(缺 `source` 字段,需补) | -| `registerRemoteAgentTask` | `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx:526` | 注册 long-running 任务到 store | -| `checkRemoteAgentEligibility` | `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx:185` | 前置鉴权检查 | -| `getRemoteTaskSessionUrl` | `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx` | 生成 session 跟踪 URL | -| `formatPreconditionError` | `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx` | 错误文案格式化 | -| `REMOTE_TASK_TYPES` | `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx:103` | 已含 `'autofix-pr'` 类型 | -| `AutofixPrRemoteTaskMetadata` | `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx:112` | `{ owner, repo, prNumber }` schema | -| `RemoteSessionProgress` | `src/components/tasks/RemoteSessionProgress.tsx` | 进度面板 UI(已认 autofix-pr 类型) | -| `detectCurrentRepositoryWithHost` | `src/utils/detectRepository.ts` | 解析 owner/repo | -| `getDefaultBranch` / `gitExe` | `src/utils/git.ts` | git 工具 | -| `feature('FLAG')` | `bun:bundle` | feature flag 系统(CLAUDE.md 红线:只能在 if/三元条件位置直接调用) | - -### 模板答案文件 - -以下三个文件已确认完整工作,是本次实现的"参考答案": - -- `src/commands/review/reviewRemote.ts`(317 行)—— **主模板**,照抄改造 -- `src/commands/ultraplan.tsx`(525 行) -- `src/commands/review/ultrareviewCommand.tsx`(89 行) - ---- - -## 四、命令对象规格 - -### 4.1 `Command` 类型选择 - -`Command` 类型定义在 `src/types/command.ts`,三态之一:`PromptCommand` / `LocalCommand` / `LocalJSXCommand`。 - -**选 `LocalJSXCommand`**,因为: -- 需要 spawn 远端 session 并显示进度面板 -- 兄弟命令 `ultraplan` / `ultrareview` 都用 local-jsx -- 接口签名:`call(onDone, context, args) => Promise` - -### 4.2 `index.ts` 完整形状 - -```ts -import { feature } from 'bun:bundle' -import type { Command } from '../../types/command.js' - -const autofixPr: Command = { - type: 'local-jsx', - name: 'autofix-pr', // 关键:必须是 'autofix-pr' 不是 'stub' - description: 'Auto-fix CI failures on a pull request', - argumentHint: ' | stop | /#', - isEnabled: () => feature('AUTOFIX_PR'), - isHidden: false, - bridgeSafe: true, - getBridgeInvocationError: (args) => { - const trimmed = args.trim() - if (!trimmed) return 'PR number required, e.g. /autofix-pr 386' - if (trimmed === 'stop' || trimmed === 'off') return undefined - if (/^\d+$/.test(trimmed)) return undefined - if (/^[\w.-]+\/[\w.-]+#\d+$/.test(trimmed)) return undefined - return 'Invalid args. Use /autofix-pr | stop | /#' - }, - load: async () => { - const m = await import('./launchAutofixPr.js') - return { call: m.callAutofixPr } - }, -} - -export default autofixPr -``` - -### 4.3 参数解析规则 - -``` -^stop$ | ^off$ → { action: 'stop' } -^\d+$ → { action: 'start', prNumber, owner: , repo: } -^([\w.-]+)/([\w.-]+)#(\d+)$ → { action: 'start', prNumber, owner, repo } -其他 → { action: 'start', freeformPrompt: } -空字符串 → 错误 -``` - ---- - -## 五、文件结构 - -``` -src/commands/autofix-pr/ -├── index.ts # 命令对象(替换 index.js) -├── launchAutofixPr.ts # 主流程 -├── parseArgs.ts # 参数解析(独立便于测试) -├── monitorState.ts # 单例锁 -├── inProcessAgent.ts # 后台 teammate -├── skillDetect.ts # 项目 skills 探测 -└── __tests__/ - ├── parseArgs.test.ts - ├── monitorState.test.ts - ├── launchAutofixPr.test.ts - └── index.test.ts # bridge invocation error 测试 -``` - -**删除**:原 `index.js`、`index.d.ts`(合并进 `index.ts`)。 - -**修改**: -- `scripts/defines.ts` —— 加 `AUTOFIX_PR` flag -- `scripts/dev.ts` —— dev 默认开启 -- `src/utils/teleport.tsx` —— `teleportToRemote` 选项加 `source?: string` 字段并透传 -- `src/commands.ts` —— **不动**(import 路径 `'./commands/autofix-pr/index.js'` 在 ESM/Bun 下会自动解析到 `.ts`) - ---- - -## 六、模块详细规格 - -### 6.1 `parseArgs.ts` - -```ts -export type ParsedArgs = - | { action: 'stop' } - | { action: 'start'; prNumber: number; owner?: string; repo?: string } - | { action: 'freeform'; prompt: string } - | { action: 'invalid'; reason: string } - -export function parseAutofixArgs(raw: string): ParsedArgs { - const trimmed = raw.trim() - if (!trimmed) return { action: 'invalid', reason: 'empty' } - if (trimmed === 'stop' || trimmed === 'off') return { action: 'stop' } - if (/^\d+$/.test(trimmed)) { - return { action: 'start', prNumber: parseInt(trimmed, 10) } - } - const cross = trimmed.match(/^([\w.-]+)\/([\w.-]+)#(\d+)$/) - if (cross) { - return { - action: 'start', - owner: cross[1], - repo: cross[2], - prNumber: parseInt(cross[3], 10), - } - } - return { action: 'freeform', prompt: trimmed } -} -``` - -### 6.2 `monitorState.ts` - -```ts -import type { UUID } from 'crypto' - -type MonitorState = { - taskId: UUID - owner: string - repo: string - prNumber: number - abortController: AbortController - startedAt: number -} - -let active: MonitorState | null = null - -export function getActiveMonitor(): Readonly | null { - return active -} - -export function setActiveMonitor(state: MonitorState): void { - if (active) throw new Error(`Monitor already active: ${active.repo}#${active.prNumber}`) - active = state -} - -export function clearActiveMonitor(): void { - if (active) { - active.abortController.abort() - active = null - } -} - -export function isMonitoring(owner: string, repo: string, prNumber: number): boolean { - return active?.owner === owner && active?.repo === repo && active?.prNumber === prNumber -} -``` - -### 6.3 `inProcessAgent.ts` - -仿官方 `xd9` 函数: - -```ts -import { randomUUID, type UUID } from 'crypto' -import { getCurrentSessionId } from '../../bootstrap/state.js' - -export type AutofixTeammate = { - agentId: UUID - agentName: 'autofix-pr' - teamName: '_autofix' - color: undefined - planModeRequired: false - parentSessionId: UUID - abortController: AbortController - taskId: UUID -} - -export function createAutofixTeammate( - initialMessage: string, - target: string, -): AutofixTeammate { - return { - agentId: randomUUID(), - agentName: 'autofix-pr', - teamName: '_autofix', - color: undefined, - planModeRequired: false, - parentSessionId: getCurrentSessionId(), - abortController: new AbortController(), - taskId: randomUUID(), - } -} -``` - -### 6.4 `skillDetect.ts` - -```ts -import { existsSync } from 'fs' -import { join } from 'path' - -export function detectAutofixSkills(cwd: string): string[] { - const candidates = [ - 'AUTOFIX.md', - '.claude/skills/autofix.md', - '.claude/skills/autofix-pr/SKILL.md', - ] - return candidates.filter(rel => existsSync(join(cwd, rel))) -} - -export function formatSkillsHint(skills: string[]): string { - if (skills.length === 0) return '' - return ` Run ${skills.join(' and ')} for custom instructions on how to autofix.` -} -``` - -### 6.5 `launchAutofixPr.ts` - -主流程伪代码(约 250 行): - -```ts -import type { LocalJSXCommandCall } from '../../types/command.js' -import { parseAutofixArgs } from './parseArgs.js' -import { getActiveMonitor, setActiveMonitor, clearActiveMonitor, isMonitoring } from './monitorState.js' -import { createAutofixTeammate } from './inProcessAgent.js' -import { detectAutofixSkills, formatSkillsHint } from './skillDetect.js' -import { teleportToRemote } from '../../utils/teleport.js' -import { checkRemoteAgentEligibility, registerRemoteAgentTask, getRemoteTaskSessionUrl } from '../../tasks/RemoteAgentTask/RemoteAgentTask.js' -import { detectCurrentRepositoryWithHost } from '../../utils/detectRepository.js' -import { logEvent } from '../../services/analytics/index.js' - -export const callAutofixPr: LocalJSXCommandCall = async (onDone, context, args) => { - const parsed = parseAutofixArgs(args) - - // 1. stop 子命令 - if (parsed.action === 'stop') { - const m = getActiveMonitor() - if (!m) { - onDone('No active autofix monitor.', { display: 'system' }) - return null - } - clearActiveMonitor() - onDone(`Stopped monitoring ${m.repo}#${m.prNumber}.`, { display: 'system' }) - return null - } - - // 2. invalid - if (parsed.action === 'invalid') { - return errorView(`Invalid args: ${parsed.reason}`) - } - - // 3. freeform — 暂不支持,提示用户 - if (parsed.action === 'freeform') { - return errorView('Freeform prompt mode not yet supported. Use /autofix-pr .') - } - - // 4. start - logEvent('tengu_autofix_pr_started', { - action: 'start', - has_pr_number: 'true', - has_repo_path: String(!!process.cwd()), - }) - - // 4.1 解析 owner/repo - let owner = parsed.owner - let repo = parsed.repo - if (!owner || !repo) { - const detected = await detectCurrentRepositoryWithHost() - if (!detected || detected.host !== 'github.com') { - return errorResult('Cannot detect GitHub repo from current directory.', 'session_create_failed') - } - owner = detected.owner - repo = detected.name - } - - // 4.2 单例锁 - if (isMonitoring(owner, repo, parsed.prNumber)) { - return errorResult(`already monitoring ${repo}#${parsed.prNumber} in background`, 'success_rc') - } - if (getActiveMonitor()) { - const m = getActiveMonitor()! - return errorResult( - `already monitoring ${m.repo}#${m.prNumber}. Run /autofix-pr stop first.`, - 'rc_already_monitoring_other', - ) - } - - // 4.3 资格检查 - const eligibility = await checkRemoteAgentEligibility() - if (!eligibility.eligible) { - return errorResult('Remote agent not available.', 'session_create_failed') - } - - // 4.4 探测 skills - const skills = detectAutofixSkills(process.cwd()) - const skillsHint = formatSkillsHint(skills) - - // 4.5 拼初始消息 - const target = `${owner}/${repo}#${parsed.prNumber}` - const branchName = `refs/pull/${parsed.prNumber}/head` - const initialMessage = `Auto-fix failing CI checks on PR #${parsed.prNumber} in ${owner}/${repo}.${skillsHint}` - - // 4.6 创建 in-process teammate - const teammate = createAutofixTeammate(initialMessage, target) - - // 4.7 调 teleport - let bundleFailMsg: string | undefined - const session = await teleportToRemote({ - initialMessage, - source: 'autofix_pr', - branchName, - reuseOutcomeBranch: branchName, - title: `Autofix PR: ${target} (${branchName})`, - useDefaultEnvironment: true, - signal: teammate.abortController.signal, - githubPr: { owner, repo, number: parsed.prNumber }, - cwd: process.cwd(), - onBundleFail: (msg) => { bundleFailMsg = msg }, - }) - - if (!session) { - return errorResult(bundleFailMsg ?? 'remote session creation failed.', 'session_create_failed') - } - - // 4.8 注册任务到 store - registerRemoteAgentTask({ - remoteTaskType: 'autofix-pr', - session, - command: `/autofix-pr ${parsed.prNumber}`, - context, - }) - - // 4.9 设置单例锁 - setActiveMonitor({ - taskId: teammate.taskId, - owner, - repo, - prNumber: parsed.prNumber, - abortController: teammate.abortController, - startedAt: Date.now(), - }) - - // 4.10 PR webhooks 订阅(feature-gated) - if (feature('KAIROS_GITHUB_WEBHOOKS')) { - await kairosSubscribePR(owner, repo, teammate.taskId).catch(() => {/* non-fatal */}) - } - - // 4.11 返回 JSX 进度面板 - const sessionUrl = getRemoteTaskSessionUrl(session.id) - logEvent('tengu_autofix_pr_launched', { target }) - onDone( - `Autofix launched for ${target}. Track: ${sessionUrl}`, - { display: 'system' }, - ) - return null // 进度面板由 RemoteAgentTask 自动渲染 -} - -function errorResult(message: string, code: string) { - logEvent('tengu_autofix_pr_result', { result: 'failed', error_code: code }) - // ... 渲染错误 JSX -} -``` - -> **注意**:`feature('KAIROS_GITHUB_WEBHOOKS')` 必须直接放在 if 条件位置,不能赋值给变量(CLAUDE.md 红线)。 - -### 6.6 `teleport.tsx` 补 `source` 字段 - -```diff - export async function teleportToRemote(options: { - initialMessage: string | null - branchName?: string - title?: string - description?: string -+ /** -+ * Identifies which command/flow originated this teleport. CCR backend -+ * uses this for routing/billing/observability. Known values: 'autofix_pr', -+ * 'ultrareview', 'ultraplan'. Pass-through field — not interpreted client-side. -+ */ -+ source?: string - model?: string - permissionMode?: PermissionMode - // ... - }) -``` - -并在内部构造 request 时透传到 session_context(具体字段名按现有 review/ultraplan 调用结构对齐)。 - ---- - -## 七、Feature Flag - -### 7.1 新增 flag - -`scripts/defines.ts` 已有的 flag 集合中加 `AUTOFIX_PR`。 - -### 7.2 启用矩阵 - -| 环境 | 是否默认开启 | 说明 | -|---|---|---| -| dev (`bun run dev`) | 是 | `scripts/dev.ts` 加进默认列表 | -| build (production `bun run build`) | 否 | 灰度上线,需要 `FEATURE_AUTOFIX_PR=1` 显式开启 | -| 测试 | 按需 | 测试文件通过 mock `bun:bundle` 控制 | - -### 7.3 与官方上游同步策略 - -如果上游某天恢复官方实现,本仓库的本地实现优先(项目即 fork): -1. 保留 `AUTOFIX_PR` flag 名 -2. 保留 `RemoteTaskType` 字段不动 -3. 冲突时合并:吸收上游的 `source` 字段值变更、env var 变更,保留我们的本地 launcher 函数 - ---- - -## 八、测试计划 - -### 8.1 测试文件 - -| 文件 | 覆盖目标 | 测试用例数 | -|---|---|---| -| `parseArgs.test.ts` | 参数解析全分支 | ~10 | -| `monitorState.test.ts` | 单例锁正确性 | ~6 | -| `launchAutofixPr.test.ts` | 主流程 happy path + 失败路径 | ~12 | -| `index.test.ts` | bridge invocation error 校验 | ~5 | - -### 8.2 关键断言 - -`launchAutofixPr.test.ts`: - -```ts -test('start with PR number teleports with correct args', async () => { - // mock teleportToRemote, registerRemoteAgentTask, detectCurrentRepositoryWithHost - await callAutofixPr(onDone, context, '386') - expect(teleportMock).toHaveBeenCalledWith(expect.objectContaining({ - source: 'autofix_pr', - useDefaultEnvironment: true, - githubPr: { owner: 'amDosion', repo: 'claude-code-bast', number: 386 }, - branchName: 'refs/pull/386/head', - reuseOutcomeBranch: 'refs/pull/386/head', - })) - expect(registerMock).toHaveBeenCalledWith(expect.objectContaining({ - remoteTaskType: 'autofix-pr', - })) -}) - -test('cross-repo syntax owner/repo#n parses correctly', async () => { - await callAutofixPr(onDone, context, 'anthropics/claude-code#999') - expect(teleportMock).toHaveBeenCalledWith(expect.objectContaining({ - githubPr: { owner: 'anthropics', repo: 'claude-code', number: 999 }, - })) -}) - -test('singleton lock blocks second start', async () => { - await callAutofixPr(onDone, context, '386') - const result = await callAutofixPr(onDone, context, '999') - expect(extractError(result)).toMatch(/already monitoring.*386.*Run \/autofix-pr stop first/) -}) - -test('stop clears active monitor', async () => { - await callAutofixPr(onDone, context, '386') - await callAutofixPr(onDone, context, 'stop') - expect(getActiveMonitor()).toBeNull() -}) -``` - -### 8.3 Mock 策略 - -按本仓库 `tests/mocks/` 共享 mock 习惯: -- `tests/mocks/log.ts` 和 `tests/mocks/debug.ts` —— 必 mock -- `bun:bundle` —— mock `feature` 返回 `true` -- `teleportToRemote` —— 模块级 mock,断言入参 -- `registerRemoteAgentTask` —— 模块级 mock,断言入参 -- `detectCurrentRepositoryWithHost` —— mock 返回 `{ owner, name, host }` - -### 8.4 类型检查 - -```bash -bun run typecheck # 必须零错误 -bun run test:all # 必须全绿 -``` - ---- - -## 九、实施步骤(11 步清单) - -``` -[ ] Step 1 scripts/defines.ts + scripts/dev.ts 加 AUTOFIX_PR flag -[ ] Step 2 src/utils/teleport.tsx 加 source?: string 字段(约 5 行) -[ ] Step 3 删除 src/commands/autofix-pr/{index.js, index.d.ts} - 新建 src/commands/autofix-pr/index.ts(约 50 行) -[ ] Step 4 新建 src/commands/autofix-pr/parseArgs.ts(约 30 行) -[ ] Step 5 新建 src/commands/autofix-pr/monitorState.ts(约 40 行) -[ ] Step 6 新建 src/commands/autofix-pr/inProcessAgent.ts(约 60 行) -[ ] Step 7 新建 src/commands/autofix-pr/skillDetect.ts(约 30 行) -[ ] Step 8 新建 src/commands/autofix-pr/launchAutofixPr.ts(约 250 行) - 照抄 reviewRemote.ts,按 §2.2 差异表改造 -[ ] Step 9 新建四份测试文件(约 150 行) -[ ] Step 10 bun run typecheck && bun run test:all 全绿 -[ ] Step 11 dev 模式手测: - a. /autofix-pr 386 → 期望出现 RemoteSessionProgress 面板 - b. /autofix-pr stop → 期望提示已停止 - c. /autofix-pr anthropics/claude-code#999 → 期望跨仓库 - d. 第二次 /autofix-pr 386 → 期望被单例锁拒绝 -[ ] Step 12 commit:feat: implement /autofix-pr command (replace stub) -``` - -预计工作量:约 600 行新增代码(含测试 150 行)。 - ---- - -## 十、风险与回退 - -| 风险 | 触发场景 | 回退策略 | -|---|---|---| -| `source` 字段 CCR 后端不识别 | 后端只认特定枚举 | 不传该字段,看是否能跑通;如不行回头看官方 cli.js 是否传了别的字段 | -| `subscribePR` API 在本仓库 client 不完整 | KAIROS_GITHUB_WEBHOOKS 客户端代码缺失 | 用 `.catch(() => {})` 容忍失败,订阅是 nice-to-have | -| 用户账号无 CCR 权限 | `checkRemoteAgentEligibility` 返回 false | 命令降级到错误文案,不破坏会话 | -| 远端能起 session 但不修代码 | env vars 命名错误 | 看 `getRemoteTaskSessionUrl` 给的会话页容器日志,调整 | -| PR 在 fork 仓库且 CCR 没访问权 | `git_repository source error` | 命令应在前置检查中识别并提示用户先把 PR 转到主仓 | -| 上游恢复官方实现导致冲突 | 上游 sync 时 | 项目是 fork,本地实现优先;冲突手工 merge | - -### 回退命令 - -```bash -# 完全撤回本次实现 -git checkout main -git worktree remove E:/Source_code/Claude-code-bast-autofix-pr -git branch -D feat/autofix-pr -``` - -`AUTOFIX_PR` flag 默认在 production 关闭,所以即使代码已合入 main,没显式 `FEATURE_AUTOFIX_PR=1` 时不会影响用户。 - ---- - -## 十一、验收清单 - -实施完成后逐项核对: - -- [ ] R1:dev 模式下输入 `/au` 出现 `/autofix-pr` 补全 -- [ ] R2:`/autofix-pr anthropics/claude-code#999` 不报 repo-not-allowed -- [ ] R3:远端 session 跑完后目标 PR 出现新 commit -- [ ] R4:其他 stub(`share` 等)依然 hidden -- [ ] R5:`bun run typecheck` 零错误 -- [ ] R6:通过 RC bridge 触发 `/autofix-pr 386` 能跑通 -- [ ] R7:`/autofix-pr stop` 终止当前监控 -- [ ] R8:第二次 `/autofix-pr` 不同 PR 时被锁拒绝并提示 - ---- - -## 十二、附录 - -### 附录 A:相关文件路径速查 - -| 路径 | 角色 | -|---|---| -| `E:\Source_code\Claude-code-bast-autofix-pr` | 实施 worktree | -| `C:\Users\12180\.local\bin\claude.exe` | 反编译来源(242MB Bun 编译产物) | -| `C:\Users\12180\.claude\projects\E--Source-code-Claude-code-bast\memory\project_autofix_pr_implementation.md` | 内存备忘(精简版) | -| `src/commands/review/reviewRemote.ts` | 主模板 | -| `src/utils/teleport.tsx:947` | `teleportToRemote` 入口 | -| `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx:103` | `REMOTE_TASK_TYPES` | -| `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx:526` | `registerRemoteAgentTask` | -| `src/types/command.ts` | `Command` 类型定义 | - -### 附录 B:未决问题 - -| # | 问题 | 当前处理 | 后续 | -|---|---|---|---| -| Q1 | `source` 字段在 CCR backend 是否被解析 | 暂传 `'autofix_pr'`,按官方做法 | 端到端测试时观察远端日志 | -| Q2 | `subscribePR` 的 client SDK 在本仓库是否完整 | `try/catch` 容忍失败 | Step 11 手测时单独验证 | -| Q3 | freeform prompt 模式是否实现 | 暂报"not supported" | 第二期再加 | - ---- - -## 十三、变更日志 - -| 日期 | 作者 | 变更 | -|---|---|---| -| 2026-04-29 | Claude Opus 4.7 | 初始规格文档创建(基于 claude.exe 反编译 + 仓库现有基础设施盘点) | diff --git a/docs/features/background-agent-selector.md b/docs/features/background-agent-selector.md deleted file mode 100644 index 3acebb892..000000000 --- a/docs/features/background-agent-selector.md +++ /dev/null @@ -1,225 +0,0 @@ -# Background Agent Selector — 底部统一后台 Agent 切换器 - -> Feature Flag: 无(直接启用) -> 实现状态:完整可用 -> 依赖:`viewingAgentTaskId` / `enterTeammateView` / `exitTeammateView` 已有机制 - -## 一、功能概述 - -Background Agent Selector 是渲染在 PromptInput 下方的常驻状态条,列出当前所有 **backgrounded 的 local_agent 任务**(包括 `/fork` 派生的 fork agent 和 Task/AgentTool 调用 `run_in_background: true` 派生的子 agent)。用户可以用 ↑/↓ 方向键在 `main` 和各 agent 之间切换焦点,按 Enter 把 REPL 主视图替换为所选 agent 的实时 transcript,再按 Enter 选中 `main` 即可回到主对话。 - -整个机制完全复用官方已有的 teammate transcript 查看基础设施,不引入新的视图层 / 数据流,仅新增一条 footer pill 类型。 - -### 核心特性 - -- **统一入口**:`/fork`、Task 派生的 subagent、所有 `run_in_background: true` 的 agent 都在同一栏显示 -- **就地切换**:prompt 为空时按 ↓ 溢出进入底部 selector,↑↓ 选中某行,Enter 即切主视图 -- **实时状态**:每行显示 agent 类型 + 描述 + 运行时长 + 已消耗 token;running 时圆点为绿色 -- **Keep-alive 视图**:agent 完成后在 `evictAfter` grace 窗口内保留一段时间,用户可回看 -- **零界面侵入**:tasks 数为 0 时 selector 完全不渲染,不占屏幕高度 -- **与旧 Dialog 共存**:Shift+↓ 打开的 `BackgroundTasksDialog` 原有行为保留,selector 只作为展示 + 快捷切换 - -## 二、用户交互 - -### 触发方式 - -有任何 background agent 时,selector 自动出现在 `bypass permissions on` 行下方: - -``` - claude-code | Opus 4.7 (1M context) | ctx:4% - ▶▶ bypass permissions on (shift+tab to cycle) - - ○ main ↑/↓ to select · Enter to view - ● Explore Research src/hooks 23s · ↓ 10.9k tokens - ○ Explore Research src/components 22s · ↓ 9.5k tokens - ○ Explore Research src/utils 21s · ↓ 13.6k tokens -``` - -### 键盘路由 - -| 位置 / 状态 | 按键 | 行为 | -|---|---|---| -| PromptInput 非空 | ↑↓ | 光标移动 / 翻历史(不变) | -| PromptInput 空 + 历史底部 | ↓ | 焦点下放到 selector,高亮到 `● main` | -| Selector 聚焦(`footerSelection === 'bg_agent'`) | ↓ | 高亮下移,-1 → 0 → ... → N-1 | -| Selector 聚焦 | ↑ | 高亮上移;在 `main` 再 ↑ → 焦点回 PromptInput | -| Selector 聚焦 | Enter | `-1` → `exitTeammateView`;`>=0` → `enterTeammateView(agentId)`。焦点保留在 pill | -| Selector 聚焦 | Esc | `footer:clearSelection`,焦点回 PromptInput | - -### 视觉规则 - -- `● main` / `● `:当前被**查看**(viewingAgentTaskId 指向)或被**光标聚焦**(pill focused 时以光标为准)的一行 -- running 状态的 agent:圆点渲染为 `success` 色(绿色),与 `BackgroundTasksDialog` 状态语义对齐 -- 右上角 hint 随状态变化: - - pill 聚焦:`↑/↓ to select · Enter to view` - - 已选中 running agent:`shift+↓ to manage · x to stop` - - 已选中 terminal agent:`shift+↓ to manage · x to clear` - - 未选中任何 agent:`shift+↓ to manage background agents` - -## 三、实现架构 - -### 3.1 数据层:`useBackgroundAgentTasks` - -文件:`src/hooks/useBackgroundAgentTasks.ts` - -封装对 `useAppState(s => s.tasks)` 的过滤: - -```ts -export function useBackgroundAgentTasks(): LocalAgentTaskState[] { - const tasks = useAppState(s => s.tasks) - return useMemo(() => { - const now = Date.now() - return Object.values(tasks) - .filter(isLocalAgentTask) - .filter(t => t.agentType !== 'main-session') - .filter(t => t.isBackgrounded !== false) - .filter(t => t.evictAfter === undefined || t.evictAfter > now) - .sort((a, b) => a.startTime - b.startTime) - }, [tasks]) -} -``` - -`/fork` 和 `AgentTool` 的 `run_in_background: true` 底层都走 `registerAsyncAgent → runAsyncAgentLifecycle`,最终写入同一个 `appState.tasks` Map;此 hook 是唯一数据源,Selector 和 PromptInput 的 `bgAgentList` 都消费它。 - -### 3.2 状态层:新增两个字段 - -文件:`src/state/AppStateStore.ts` - -```ts -export type FooterItem = - | 'tasks' | 'tmux' | 'bagel' | 'teams' | 'bridge' | 'companion' - | 'bg_agent' // ← 新增 - -export type AppState = DeepImmutable<{ - // ... - selectedBgAgentIndex: number // -1 = main, 0..N-1 = 选中的 agent -}> -``` - -- `'bg_agent'` 作为 `FooterItem` 加入 footer pill 体系,享受既有的 `footer:up` / `footer:down` / `footer:openSelected` keybinding 路由 -- `selectedBgAgentIndex` 记录 selector 的光标位置,与 `viewingAgentTaskId`("正在看什么")独立;它不可从 `viewingAgentTaskId` 派生——Enter 后光标留在 pill 继续导航,查看目标才变 - -### 3.3 键盘路由:PromptInput footer pill 分支 - -文件:`src/components/PromptInput/PromptInput.tsx` - -1. **`bg_agent` 进入 footerItems[0]**:保证 prompt ↓ 溢出时(`handleHistoryDown` → `selectFooterItem(footerItems[0])`)直接进入 selector,而不是 `tasks` 等其他 pill -2. **`footer:up` 分支**:`bgAgentSelected` 时 `selectedBgAgentIndex > -1` 则递减;在 -1 → `selectFooterItem(null)` 退出 pill -3. **`footer:down` 分支**:`selectedBgAgentIndex < bgAgentList.length - 1` 则递增,到底 clamp -4. **`footer:openSelected` 分支**:index === -1 → `exitTeammateView`;否则 `enterTeammateView(bgAgentList[i].agentId)`。**不清理 pill 焦点**,光标留在 selector 上继续导航 -5. **`selectFooterItem('bg_agent')`**:入 pill 时重置 `selectedBgAgentIndex = -1`(光标落到 `main`) - -### 3.4 渲染层:`BackgroundAgentSelector` - -文件:`src/components/tasks/BackgroundAgentSelector.tsx` - -纯展示组件,不订阅键盘: - -```tsx -const tasks = useBackgroundAgentTasks() -const viewingId = useAppState(s => s.viewingAgentTaskId) -const footerSelection = useAppState(s => s.footerSelection) -const selectedBgIndex = useAppState(s => s.selectedBgAgentIndex) - -if (tasks.length === 0) return null - -const pillFocused = footerSelection === 'bg_agent' -const highlightedId = pillFocused - ? (selectedBgIndex === -1 ? null : tasks[selectedBgIndex]?.agentId ?? null) - : (viewingId ?? null) -``` - -**高亮派生规则**:pill 聚焦 → 跟 `selectedBgAgentIndex`;未聚焦 → 镜像 `viewingAgentTaskId`。这样当用户通过 Shift+↓ Dialog 或 `enterTeammateView` 其它途径切换视图时,selector 也会正确反映。 - -### 3.5 主视图切换:复用 `viewingAgentTaskId` - -REPL.tsx 主体仍复用原有查看逻辑: - -```ts -const viewedTask = viewingAgentTaskId ? tasks[viewingAgentTaskId] : undefined -const viewedAgentTask = ... (isLocalAgentTask(viewedTask) ? viewedTask : undefined) -const displayedMessages = viewedAgentTask ? displayedAgentMessages : messages -``` - -当 `enterTeammateView(agentId)` 把 `viewingAgentTaskId` 设成某个 local_agent 的 id: - -- `viewedAgentTask` 解析成该 agent -- `displayedMessages` 切换到 agent 的 messages -- 消息列表、spinner、unseen divider 等一整套组件自动用 agent transcript 重渲染 -- 主对话流被"暂停"(并非销毁,回到 `main` 时仍在原处) - -`enterTeammateView` 同步负责:设 `retain: true` 阻止 eviction、清 `evictAfter`、触发 disk bootstrap 从 `agent-.jsonl` 加载完整 transcript 到 `task.messages`。 - -#### Fork agent prompt 归一化 - -`/fork` agent 的 transcript 和普通 subagent 不同:它继承 main agent 的上下文,真实初始消息形态是: - -```text -...parent messages -assistant([...tool_use]) -user([tool_result..., text("...Your directive: ")]) -...fork live messages -``` - -这里的 prompt 文本混在 `[tool_result..., text]` 多 block user message 里。消息渲染管线会优先把这条 user message 当作 tool-result plumbing 来处理,导致 `` 里的用户 prompt 不稳定可见。为保证切换到 fork agent 时总能看到用户发起的 fork prompt,REPL.tsx 对 fork 视图做一次展示层归一化: - -1. 仅当 `viewedAgentTask.agentType === 'fork'` 时启用,不影响普通 Explore / Task subagent。 -2. 从原始 messages 中识别包含 `` 的 carrier message。 -3. 剥离 carrier message 里的 boilerplate text block,但保留 `tool_result` blocks,避免破坏父 assistant `tool_use` 的承接关系。 -4. 强制插入一条独立 `createUserMessage({ content: viewedAgentTask.prompt })` 作为可见用户 prompt。 -5. 插入位置优先为 boilerplate carrier 后;如果 sidechain bootstrap 还没读到 carrier,则插到最后一条 inherited `assistant tool_use` 后面,确保 prompt 接在 main 上下文之后,而不是跑到视图顶部。 - -这个归一化只影响 UI 展示用的 `displayedAgentMessages`,不回写 `task.messages`,也不改变发送给模型的 fork transcript。 - -### 3.6 生命周期 - -完全复用官方既有机制: - -- **运行中**:`isBackgroundTask()` 谓词为真,selector 列出 -- **完成 / 失败 / 中止**:`completeAgentTask` / `failAgentTask` / `killAsyncAgent` 设 `status` 为 terminal -- **回访后退出**:`exitTeammateView` 调 `release(task)`——清 `retain`、清 `messages`、terminal 状态下设 `evictAfter = now + PANEL_GRACE_MS (30s)` -- **evictAfter 过期**:`useBackgroundAgentTasks` 过滤时自然剔除,selector 行消失 -- **手动清除**:`stopOrDismissAgent(taskId)` 设 `evictAfter = 0`,立即消失 - -## 四、设计决策 - -1. **数据源单一**:`useBackgroundAgentTasks` 是唯一过滤点,PromptInput 也复用,避免过滤条件散落 -2. **pill 聚焦保留**:Enter 切视图后不松焦,让 ↑↓ 连续导航,贴近官方体验 -3. **`bg_agent` 放 footerItems[0]**:确保 ↓ 溢出直接进入 selector 而非其它 pill -4. **selector 不订阅键盘**:所有按键路由集中在 PromptInput 的 `footer:*` 分支,避免 selector 组件和 PromptInput 双重 `useInput` 的冲突 -5. **`selectedBgAgentIndex` 存 AppState 而非局部 state**:selector 和 PromptInput 分别在两棵不同子树,需要全局字段协调;该值不能从 `viewingAgentTaskId` 派生 -6. **与 `BackgroundTasksDialog` 共存**:Shift+↓ 行为完全不变,selector 是补充快捷入口;Dialog 仍管 shell / workflow / monitor_mcp 等 selector 不显示的 task 类型 -7. **fork prompt 展示层兜底**:fork prompt 不依赖 boilerplate 自身渲染,统一在 `displayedAgentMessages` 中合成独立用户消息;普通 subagent 不走该分支,避免 prompt 重复 - -## 五、关键 API 复用 - -| 官方已有能力 | selector 如何使用 | -|---|---| -| `AppState.tasks` | 单一数据源,无需 file watcher / output JSONL 订阅 | -| `registerAsyncAgent` | `/fork` 和 AgentTool 共用,selector 不区分来源 | -| `enterTeammateView(id)` | Enter 时调用,负责 retain + disk bootstrap | -| `exitTeammateView` | Enter 选中 `main` 时调用 | -| `release(task)` + `PANEL_GRACE_MS` | 30s keep-alive,selector 自动生效 | -| `useElapsedTime` | 每行时长显示,非 running 自动停 interval | -| `formatTokens` (`utils/format.ts`) | token 数 1k 缩写 | -| `footer:up` / `footer:down` / `footer:openSelected` keybinding | 键盘路由复用 Footer context | - -## 六、文件索引 - -| 文件 | 职责 | -|------|------| -| `src/hooks/useBackgroundAgentTasks.ts` | 数据过滤 hook(backgrounded local_agent + evictAfter 过滤 + startTime 排序) | -| `src/components/tasks/BackgroundAgentSelector.tsx` | 底部 selector UI,纯展示 | -| `src/components/PromptInput/PromptInput.tsx` | 新增 `'bg_agent'` footer pill + 对应的 `footer:up/down/openSelected` 分支 | -| `src/state/AppStateStore.ts` | `FooterItem` 加 `'bg_agent'`;新增 `selectedBgAgentIndex` 字段 | -| `src/main.tsx` | `getDefaultAppState` 同步初始化 `selectedBgAgentIndex: -1` | -| `src/screens/REPL.tsx` | 在 PromptInput + SessionBackgroundHint 之后挂载 ``;切换 agent 主视图;对 fork transcript 做 prompt 归一化 | -| `src/components/messages/AssistantToolUseMessage.tsx` | 新增 `defaultCollapsed?: boolean` prop,为后续详情视图默认折叠工具块预留 | -| `src/components/messages/UserTextMessage.tsx` | 识别 ``,交给 fork 专用 renderer 处理 | -| `src/components/messages/UserForkBoilerplateMessage.tsx` | 将 fork boilerplate text 折叠为纯用户 prompt;作为 transcript 中原位渲染的兼容路径 | - -## 七、已知限制 - -- `Date.now()` 在 `useBackgroundAgentTasks` 的 useMemo 里冻结于 `[tasks]` 触发时:若长时间没有新 task 变更事件,某个 terminal agent 的 grace 期过期后不会立即从 selector 消失,要等下一次 tasks 变化才刷新。在典型使用(主对话一直在产生消息)下感知不到,暂不额外加 interval。 -- Selector 当前不处理 Shell Task / Workflow / Monitor MCP 等类型——这些仍走 `BackgroundTasksDialog`(Shift+↓)管理。 -- `AssistantToolUseMessage` 的 `defaultCollapsed` prop 目前无调用方传值,保留作为后续"agent 详情视图内工具块默认折叠"扩展点。 diff --git a/docs/features/bash-classifier.md b/docs/features/bash-classifier.md deleted file mode 100644 index bd4962070..000000000 --- a/docs/features/bash-classifier.md +++ /dev/null @@ -1,107 +0,0 @@ -# BASH_CLASSIFIER — Bash 命令分类器 - -> Feature Flag: `FEATURE_BASH_CLASSIFIER=1` -> 实现状态:bashClassifier.ts 全部 Stub,yoloClassifier.ts 完整实现可参考 -> 引用数:45 - -## 一、功能概述 - -BASH_CLASSIFIER 使用 LLM 对 bash 命令进行意图分类(允许/拒绝/询问),实现自动权限决策。用户不需要逐个审批 bash 命令,分类器根据命令内容和上下文自动判断安全性。 - -### 核心特性 - -- **LLM 驱动分类**:使用 Opus 模型评估命令安全性 -- **两阶段分类**:快速阻止/允许 → 深度思考链 -- **自动审批**:分类器判定安全的命令自动通过 -- **UI 集成**:权限对话框显示分类器状态和审核选项 - -## 二、实现架构 - -### 2.1 模块状态 - -| 模块 | 文件 | 状态 | 说明 | -|------|------|------|------| -| Bash 分类器 | `src/utils/permissions/bashClassifier.ts` | **Stub** | 所有函数返回空操作。注释:"ANT-ONLY" | -| YOLO 分类器 | `src/utils/permissions/yoloClassifier.ts` | **完整** | 1496 行,两阶段 XML 分类器 | -| 审批信号 | `src/utils/classifierApprovals.ts` | **完整** | Map + 信号管理分类器决策 | -| 权限 UI | `src/components/permissions/BashPermissionRequest.tsx` | **布线** | 分类器状态显示、审核选项 | -| 权限管道 | `src/hooks/toolPermission/handlers/*.ts` | **布线** | 分类器结果路由到决策 | -| API beta 标头 | `src/services/api/withRetry.ts` | **布线** | 启用时发送 `bash_classifier` beta | - -### 2.2 参考实现:yoloClassifier.ts - -文件:`src/utils/permissions/yoloClassifier.ts`(1496 行) - -这是已实现的完整分类器,可作为 bashClassifier.ts 的参考: - -``` -两阶段分类: -1. 快速阶段:构建对话记录 → 调用 sideQuery(Opus)→ 快速阻止/允许 -2. 深度阶段:思考链分析 → 最终决策 -``` - -特性: -- 构建完整对话记录上下文 -- 调用安全系统提示的 sideQuery -- GrowthBook 配置和指标 -- 错误处理和降级 - -### 2.3 分类器在权限管道中的位置 - -``` -bash 命令到达 - │ - ▼ -bashPermissions.ts 权限检查 - │ - ├── 传统规则匹配(字符串级别) - │ - └── [BASH_CLASSIFIER] LLM 分类 - │ - ├── allow → 自动通过 - ├── deny → 自动拒绝 - └── ask → 显示权限对话框 - │ - ├── 分类器自动审批标记 - └── 审核选项(用户可覆盖) -``` - -## 三、需要补全的内容 - -| 函数 | 需要实现 | 说明 | -|------|---------|------| -| `classifyBashCommand()` | LLM 调用评估安全性 | 参考 yoloClassifier.ts 的两阶段模式 | -| `isClassifierPermissionsEnabled()` | GrowthBook/配置检查 | 控制分类器是否激活 | -| `getBashPromptDenyDescriptions()` | 返回基于提示的拒绝规则 | 权限设置描述 | -| `getBashPromptAskDescriptions()` | 返回询问规则 | 需要用户确认的命令 | -| `getBashPromptAllowDescriptions()` | 返回允许规则 | 自动通过的命令 | -| `generateGenericDescription()` | LLM 生成命令描述 | 为权限对话框提供说明 | -| `extractPromptDescription()` | 解析规则内容 | 从规则中提取描述 | - -## 四、关键设计决策 - -1. **ANT-ONLY 标记**:bashClassifier.ts 标注为 "ANT-ONLY",可能是 Anthropic 内部服务端分类器的客户端适配 -2. **两阶段分类**:快速阶段处理明确情况(减少延迟),深度阶段处理模糊情况 -3. **分类器结果可审核**:权限 UI 显示分类器决策,用户可覆盖 -4. **YOLO 分类器参考**:yoloClassifier.ts 提供完整的分类器实现模式,可直接参考 - -## 五、使用方式 - -```bash -# 启用 feature -FEATURE_BASH_CLASSIFIER=1 bun run dev - -# 配合 TREE_SITTER_BASH 使用(AST + LLM 双重安全) -FEATURE_BASH_CLASSIFIER=1 FEATURE_TREE_SITTER_BASH=1 bun run dev -``` - -## 六、文件索引 - -| 文件 | 行数 | 职责 | -|------|------|------| -| `src/utils/permissions/bashClassifier.ts` | — | Bash 分类器(stub,ANT-ONLY) | -| `src/utils/permissions/yoloClassifier.ts` | 1496 | YOLO 分类器(完整参考实现) | -| `src/utils/classifierApprovals.ts` | — | 分类器审批信号管理 | -| `src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx` | — | 分类器 UI | -| `src/hooks/toolPermission/handlers/interactiveHandler.ts` | — | 交互式权限处理 | -| `src/services/api/withRetry.ts` | — | API beta 标头 | diff --git a/docs/features/bridge-mode.md b/docs/features/bridge-mode.md deleted file mode 100644 index 5b9385a4f..000000000 --- a/docs/features/bridge-mode.md +++ /dev/null @@ -1,158 +0,0 @@ -# BRIDGE_MODE — 远程控制 - -> Feature Flag: `FEATURE_BRIDGE_MODE=1` -> 实现状态:完整可用(v1 + v2 实现) -> 引用数:28 - -## 一、功能概述 - -BRIDGE_MODE 将本地 CLI 注册为"bridge 环境",可从 claude.ai 或其他控制面远程驱动。本地终端变为一个"执行者",接受远程指令并执行。 - -### 核心特性 - -- **环境注册**:本地 CLI 向 Anthropic 服务器注册为可用的 bridge 环境 -- **工作轮询**:长轮询(long-poll)等待远程任务分配 -- **会话管理**:创建、恢复、归档远程会话 -- **权限透传**:远程权限请求发送到控制面,用户在 claude.ai 上批准/拒绝 -- **心跳保活**:定期发送 heartbeat 延长任务租约 -- **可信设备**:v2 支持可信设备令牌增强安全性 - -## 二、实现架构 - -### 2.1 版本演进 - -| 版本 | 实现 | 特点 | -|------|------|------| -| v1(env-based) | `src/bridge/replBridge.ts` | 基于环境变量的传统 bridge | -| v2(env-less) | `src/bridge/remoteBridgeCore.ts` | 无需环境变量,更安全的 bridge | - -### 2.2 API 协议 - -文件:`src/bridge/bridgeApi.ts` - -Bridge API Client 提供 9 个核心操作: - -| 操作 | HTTP | 说明 | -|------|------|------| -| `registerBridgeEnvironment` | POST `/v1/environments/bridge` | 注册本地环境,获取 `environment_id` + `environment_secret` | -| `pollForWork` | GET `/v1/environments/{id}/work/poll` | 长轮询等待任务(10s 超时) | -| `acknowledgeWork` | POST `/v1/environments/{id}/work/{workId}/ack` | 确认接收任务 | -| `stopWork` | POST `/v1/environments/{id}/work/{workId}/stop` | 停止任务 | -| `heartbeatWork` | POST `/v1/environments/{id}/work/{workId}/heartbeat` | 续约任务租约 | -| `deregisterEnvironment` | DELETE `/v1/environments/bridge/{id}` | 注销环境 | -| `archiveSession` | POST `/v1/sessions/{id}/archive` | 归档会话(409 = 已归档,幂等) | -| `sendPermissionResponseEvent` | POST `/v1/sessions/{id}/events` | 发送权限审批结果 | -| `reconnectSession` | POST `/v1/environments/{id}/bridge/reconnect` | 重连已存在的会话 | - -### 2.3 认证流程 - -``` -注册: OAuth Bearer Token → 获取 environment_secret -轮询: environment_secret 作为 Authorization - ├── 401 → 尝试 OAuth token 刷新(onAuth401) - └── 刷新成功 → 重试一次 -``` - -**OAuth 刷新**:API client 内置 `withOAuthRetry` 机制。401 时调用 `handleOAuth401Error`(同 withRetry.ts 的 v1/messages 模式),刷新后重试一次。 - -### 2.4 安全设计 - -- **路径穿越防护**:`validateBridgeId()` 使用 `/^[a-zA-Z0-9_-]+$/` 白名单验证所有服务端 ID -- **BridgeFatalError**:不可重试的错误(401/403/404/410)直接抛出,阻止重试循环 -- **可信设备令牌**:v2 通过 `X-Trusted-Device-Token` header 增强安全层级 -- **幂关注册**:支持 `reuseEnvironmentId` 实现会话恢复,避免重复创建环境 - -### 2.5 数据流 - -``` -claude.ai 用户选择远程环境 - │ - ▼ -POST /v1/environments/bridge (注册) - │ - ◀── environment_id + environment_secret - │ - ▼ -GET .../work/poll (长轮询) - │ - ◀── WorkResponse { id, data: { type, sessionId } } - │ - ▼ -POST .../work/{id}/ack (确认) - │ - ▼ -sessionRunner 创建 REPL session - │ - ├── 权限请求 → sendPermissionResponseEvent - ├── 心跳 → heartbeatWork (续约) - └── 任务完成 → 自动归档 -``` - -### 2.6 模块结构 - -| 模块 | 文件 | 职责 | -|------|------|------| -| API Client | `bridgeApi.ts` | HTTP 通信(注册/轮询/确认/心跳/注销) | -| Session Runner | `sessionRunner.ts` | 创建/恢复 REPL 会话 | -| Bridge Config | `bridgeConfig.ts` | 配置管理(machine name、max sessions 等) | -| Transport | `replBridgeTransport.ts` | Bridge 传输层 | -| Permission Callbacks | `bridgePermissionCallbacks.ts` | 权限请求处理 | -| Pointer | `bridgePointer.ts` | 当前活跃 bridge 状态指针 | -| Flush Gate | `flushGate.ts` | 刷新控制 | -| JWT Utils | `jwtUtils.ts` | JWT 令牌工具 | -| Trusted Device | `trustedDevice.ts` | 可信设备管理 | -| Debug Utils | `debugUtils.ts` | 调试日志 | -| Types | `types.ts` | 类型定义 | - -## 三、关键设计决策 - -1. **长轮询而非 WebSocket**:`pollForWork` 使用 HTTP GET + 10s 超时。简单可靠,无需维护 WebSocket 连接 -2. **OAuth 刷新内嵌**:API client 自带 `withOAuthRetry`,无需外层重试逻辑 -3. **ETag 条件请求**:注册时支持 `reuseEnvironmentId` 实现幂等会话恢复 -4. **v1/v2 共存**:代码中同时存在两套实现,v2 是更安全的升级版 -5. **权限双向流动**:本地权限请求发送到 claude.ai,用户在 web 上审批 - -## 四、使用方式 - -```bash -# 启用 bridge mode -FEATURE_BRIDGE_MODE=1 bun run dev - -# 从 claude.ai/code 远程连接 -# 在 web 界面选择已注册的环境 - -# 配合 DAEMON 使用(后台守护) -FEATURE_BRIDGE_MODE=1 FEATURE_DAEMON=1 bun run dev -``` - -## 五、外部依赖 - -| 依赖 | 说明 | -|------|------| -| Anthropic OAuth | claude.ai 订阅登录 | -| GrowthBook | `tengu_ccr_bridge` 门控 | -| Bridge API | `/v1/environments/bridge` 系列端点 | - -## 六、文件索引 - -| 文件 | 行数 | 职责 | -|------|------|------| -| `src/bridge/bridgeApi.ts` | 541 | API Client(核心) | -| `src/bridge/sessionRunner.ts` | — | 会话运行器 | -| `src/bridge/bridgeConfig.ts` | — | 配置管理 | -| `src/bridge/replBridgeTransport.ts` | — | 传输层 | -| `src/bridge/bridgePermissionCallbacks.ts` | — | 权限回调 | -| `src/bridge/bridgePointer.ts` | — | 状态指针 | -| `src/bridge/flushGate.ts` | — | 刷新控制 | -| `src/bridge/jwtUtils.ts` | — | JWT 工具 | -| `src/bridge/trustedDevice.ts` | — | 可信设备 | -| `src/bridge/remoteBridgeCore.ts` | — | v2 核心实现 | -| `src/bridge/types.ts` | — | 类型定义 | -| `src/bridge/debugUtils.ts` | — | 调试工具 | -| `src/bridge/pollConfigDefaults.ts` | — | 轮询配置默认值 | -| `src/bridge/bridgeUI.ts` | — | UI 组件 | -| `src/bridge/codeSessionApi.ts` | — | 代码会话 API | -| `src/bridge/peerSessions.ts` | — | 对等会话管理 | -| `src/bridge/sessionIdCompat.ts` | — | Session ID 兼容层 | -| `src/bridge/createSession.ts` | — | 会话创建 | -| `src/bridge/replBridgeHandle.ts` | — | Bridge 句柄 | diff --git a/docs/features/buddy.mdx b/docs/features/buddy.mdx deleted file mode 100644 index 59c70dcec..000000000 --- a/docs/features/buddy.mdx +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: "Buddy 宠物系统" -description: "Buddy 是 CLI 中的虚拟宠物伴侣,通过 /buddy 命令孵化、互动,会出现在输入框旁边陪伴你写代码。" -keywords: ["buddy", "宠物", "companion", "伴侣", "虚拟宠物"] ---- - -## 概述 - -Buddy 是 Claude Code 内置的虚拟宠物系统。在 REPL 中通过 `/buddy` 命令可以孵化一只随机生成的宠物伴侣,它会出现在输入框旁边,陪伴你的编码过程。 - -> Feature Flag: `FEATURE_BUDDY=1` - -## 启用方式 - -```bash -FEATURE_BUDDY=1 bun run dev -``` - -孵化窗口:2026 年 4 月 1-7 日期间启动时,会在 REPL 顶部显示彩虹色的 `/buddy` 提示。4 月 7 日之后命令仍然可用,但不再自动提示。 - -## 命令 - -| 命令 | 说明 | -|---|---| -| `/buddy` | 查看当前宠物信息和属性 | -| `/buddy hatch` | 孵化一只新宠物(首次使用) | -| `/buddy rehatch` | 重新随机生成宠物(替换现有) | -| `/buddy pet` | 撸宠物,触发爱心动画 | -| `/buddy mute` | 静音宠物(隐藏) | -| `/buddy unmute` | 取消静音 | - -## 宠物属性 - -### 物种(18 种) - -| | | | | -|---|---|---|---| -| Duck | Goose | Blob | Cat | -| Dragon | Octopus | Owl | Penguin | -| Turtle | Snail | Ghost | Axolotl | -| Capybara | Cactus | Robot | Rabbit | -| Mushroom | Chonk | | | - -### 稀有度 - -| 稀有度 | 星级 | 权重 | -|---|---|---| -| Common | ★ | 60% | -| Uncommon | ★★ | 25% | -| Rare | ★★★ | 10% | -| Epic | ★★★★ | 4% | -| Legendary | ★★★★★ | 1% | - -孵化时基于种子随机决定,存在极低概率出现 Shiny(闪光)变体。 - -### 属性值 - -每只宠物拥有 5 项属性(0-100): - -- **DEBUGGING** — 调试能力 -- **PATIENCE** — 耐心程度 -- **CHAOS** — 混乱指数 -- **WISDOM** — 智慧值 -- **SNARK** — 毒舌度 - -### 外观 - -每只宠物还有随机的外观配件: - -- **眼睛**: `·` `✦` `×` `◉` `@` `°` -- **帽子**: none, crown, tophat, propeller, halo, wizard, beanie, tinyduck - -## 数据存储 - -宠物信息存储在 `~/.claude.json` 的 `companion` 字段中。宠物的外观属性(物种、稀有度、属性值等)基于用户 ID 的哈希确定性生成,不可通过编辑配置文件来篡改稀有度。 - -## 相关源码 - -| 文件 | 说明 | -|---|---| -| `src/commands/buddy/index.ts` | `/buddy` 命令注册 | -| `src/commands/buddy/buddy.ts` | `/buddy` 命令处理 | -| `src/buddy/companion.ts` | 宠物生成与加载 | -| `src/buddy/companionReact.ts` | 宠物反应系统(REPL 每轮查询后触发) | -| `src/buddy/types.ts` | 类型定义(物种、稀有度、属性) | -| `src/buddy/sprites.ts` | 终端像素画渲染 | -| `src/buddy/CompanionSprite.tsx` | React 组件(输入框旁显示) | -| `src/buddy/CompanionCard.tsx` | 宠物信息卡片(`/buddy` 无参数时展示) | -| `src/buddy/useBuddyNotification.tsx` | 启动提示通知 | -| `src/buddy/prompt.ts` | 宠物相关 prompt 模板 | diff --git a/docs/features/chrome-use-mcp.md b/docs/features/chrome-use-mcp.md deleted file mode 100644 index ecbc56f36..000000000 --- a/docs/features/chrome-use-mcp.md +++ /dev/null @@ -1,30 +0,0 @@ -# Chrome Use — 浏览器自动化快速指南 - -让 Claude Code 直接控制你的 Chrome 浏览器,用自然语言完成网页操作。 - -## 快速开始(3 分钟) - -### 第一步:安装 Chrome 扩展 - -1. 下载扩展:https://github.com/hangwin/mcp-chrome/releases -2. 解压 zip 文件 -3. 打开 Chrome 访问 `chrome://extensions/` -4. 开启右上角「开发者模式」 -5. 点击「加载已解压的扩展程序」,选择解压后的文件夹 - -### 第二步:启动 Claude Code - -```bash -bun run dev -ccb # 或者 ccb 安装版也行 -``` - -### 第三步:启用 Chrome MCP - -1. 在 REPL 中输入 `/mcp` 打开 MCP 面板 -2. 找到 `mcp-chrome`,按空格键启用 -3. 按 Enter 确认 - -## 相关文档 - -- GitHub 仓库:https://github.com/hangwin/mcp-chrome diff --git a/docs/features/claude-in-chrome-mcp.md b/docs/features/claude-in-chrome-mcp.md deleted file mode 100644 index 8fe8c0d90..000000000 --- a/docs/features/claude-in-chrome-mcp.md +++ /dev/null @@ -1,137 +0,0 @@ -# Claude in Chrome — 用户操作指南 - -## 1. 功能简介 - -Claude in Chrome 让 Claude Code 直接控制你的 Chrome 浏览器。你可以用自然语言让 Claude 帮你: - -- 打开网页、导航、前进后退 -- 填写表单、上传图片 -- 截图、录制 GIF -- 读取页面内容(DOM、纯文本) -- 执行 JavaScript -- 监控网络请求和控制台日志 -- 管理标签页 - -## 2. 前置条件 - -| 条件 | 说明 | -|------|------| -| Claude Code 订阅 | 需要 Claude Pro、Max 或 Team 订阅,浏览器插件功能不向免费用户开放 | -| Chrome 浏览器 | 需已安装 Google Chrome | -| Claude in Chrome 扩展 | 从 Chrome Web Store 安装(`claude.ai/chrome`) | -| Claude Code CLI | 已通过 `bun run dev` 或构建产物运行 | - -## 3. 启用方式 - -### Dev 模式 - -```bash -bun run dev -- --chrome -``` - -启动后 Claude 会自动检测 Chrome 扩展是否已安装,并注册浏览器控制工具。 - -### 构建产物 - -```bash -node dist/cli.js --chrome -``` - -### 禁用 - -```bash -bun run dev -- --no-chrome -``` - -或在 REPL 中通过 `/chrome` 命令切换启用/禁用状态。 - -### 通过配置默认启用 - -在 Claude Code 设置中将 `claudeInChromeDefaultEnabled` 设为 `true`,以后启动无需加 `--chrome` 参数。 - -## 4. 使用流程 - -1. **启动 CLI** — 加 `--chrome` 参数启动 Claude Code -2. **确认连接** — REPL 中输入 `/chrome`,查看扩展状态是否显示 "Installed / Connected" -3. **开始对话** — 正常与 Claude 对话,当需要操作浏览器时直接说,例如: - - "打开 https://example.com 并截图" - - "在当前页面搜索关键词 xxx" - - "填写登录表单,用户名 admin" - - "帮我录制当前操作的 GIF" -4. **权限审批** — 首次执行浏览器操作时,Claude 会请求你的确认 -5. **操作完成** — Claude 完成操作后会返回结果(截图、文本、执行结果等) - -## 5. 可用操作 - -### 页面交互 - -| 操作 | 说明 | -|------|------| -| `navigate` | 导航到指定 URL,或前进/后退 | -| `computer` | 鼠标点击、移动、拖拽、键盘输入、截图等(13 种 action) | -| `form_input` | 填写表单字段 | -| `upload_image` | 上传图片到文件输入框或拖拽区域 | -| `javascript_tool` | 在页面上下文执行 JavaScript | - -### 页面读取 - -| 操作 | 说明 | -|------|------| -| `read_page` | 获取页面可访问性树(DOM 结构) | -| `get_page_text` | 提取页面纯文本内容 | -| `find` | 用自然语言搜索页面元素 | - -### 标签页管理 - -| 操作 | 说明 | -|------|------| -| `tabs_context_mcp` | 获取当前标签组信息 | -| `tabs_create_mcp` | 创建新标签页 | - -### 监控与调试 - -| 操作 | 说明 | -|------|------| -| `read_console_messages` | 读取浏览器控制台日志 | -| `read_network_requests` | 读取网络请求记录 | - -### 其他 - -| 操作 | 说明 | -|------|------| -| `resize_window` | 调整浏览器窗口尺寸 | -| `gif_creator` | 录制 GIF 并导出 | -| `shortcuts_list` | 列出可用快捷方式 | -| `shortcuts_execute` | 执行快捷方式 | -| `update_plan` | 向你提交操作计划供审批 | -| `switch_browser` | 切换到其他 Chrome 浏览器(仅 Bridge 模式) | - -## 6. 通信模式 - -Claude in Chrome 支持两种与浏览器通信的方式: - -### 本地 Socket(默认) - -Chrome 扩展通过 Native Messaging Host 与 CLI 建立 Unix socket 连接。适用于本地开发,无需额外配置。 - -### Bridge WebSocket - -通过 Anthropic 的 bridge 服务中转,支持远程操控浏览器。需要 claude.ai OAuth 登录。 - -## 7. 常见问题 - -### 扩展显示未安装 - -确认已从 Chrome Web Store 安装 "Claude in Chrome" 扩展,安装后重启浏览器。 - -### 工具未出现在工具列表 - -检查启动时是否加了 `--chrome` 参数,或通过 `/chrome` 命令确认状态。 - -### 连接超时 - -确保 Chrome 浏览器正在运行且扩展已启用。Native Messaging Host 在扩展安装时自动注册,如果重装过扩展需要重启浏览器。 - -### 不使用 Chrome 功能时 - -不带 `--chrome` 参数正常启动即可,不会加载任何浏览器相关模块,不影响其他功能。 diff --git a/docs/features/computer-use-architecture-v2.md b/docs/features/computer-use-architecture-v2.md deleted file mode 100644 index 8cfac3cb0..000000000 --- a/docs/features/computer-use-architecture-v2.md +++ /dev/null @@ -1,325 +0,0 @@ -# Computer Use 架构修正方案 v2 - -更新时间:2026-04-04 - -## 1. 当前架构的问题 - -### 问题 A:平台代码混在错误的包里 - -`@ant/computer-use-swift` 是 macOS Swift 原生模块的包装器,但我们把 Windows(`backends/win32.ts`)和 Linux(`backends/linux.ts`)的截图/应用管理代码塞进了这个包。"swift" 在名字里就意味着 macOS,后期维护者无法区分。 - -`@ant/computer-use-input` 同样——原本是 macOS enigo Rust 模块,我们也往里面塞了 win32/linux 后端。 - -### 问题 B:输入方式不对 - -当前 Windows 后端(`packages/@ant/computer-use-input/src/backends/win32.ts`)使用 `SetCursorPos` + `SendInput` + `keybd_event`——这是**全局输入**: - -- 鼠标真的会移动到屏幕上 -- 键盘真的打到当前前台窗口 -- **会影响用户当前的操作** - -绑定窗口句柄后,应该用 `SendMessage`/`PostMessage` 向目标 HWND 发送消息: - -- `WM_CHAR` — 发送字符,不移动光标 -- `WM_KEYDOWN`/`WM_KEYUP` — 发送按键 -- `WM_LBUTTONDOWN`/`WM_LBUTTONUP` — 发送鼠标点击(窗口客户区相对坐标) -- `PrintWindow` — 截取窗口内容,不需要窗口在前台 -- **不抢焦点、不影响用户当前操作** - -已验证:向记事本 `SendMessage(WM_CHAR)` 成功写入文字,记事本在后台,终端保持前台。 - -### 问题 C:截图是公共能力,不属于 swift - -截图(screenshot)、显示器枚举(display)、应用管理(apps)是所有平台都需要的公共能力,不应该放在 `@ant/computer-use-swift`(macOS 专属包名)里。 - -## 2. 修正后的架构 - -### 2.1 分层原则 - -``` -packages/@ant/ ← macOS 原生模块包装器(不放其他平台代码) -├── computer-use-input/ ← macOS: enigo .node 键鼠(仅 darwin) -├── computer-use-swift/ ← macOS: Swift .node 截图/应用(仅 darwin) -└── computer-use-mcp/ ← 跨平台: MCP server + 工具定义(不改) - -src/utils/computerUse/ -├── platforms/ ← 新增: 跨平台抽象层 -│ ├── types.ts ← 公共接口: InputPlatform, ScreenshotPlatform, AppsPlatform, DisplayPlatform -│ ├── index.ts ← 平台分发器: 按 process.platform 加载后端 -│ ├── darwin.ts ← macOS: 委托给 @ant/computer-use-{input,swift} -│ ├── win32.ts ← Windows: SendMessage 输入 + PrintWindow 截图 + EnumWindows + UIA + OCR -│ └── linux.ts ← Linux: xdotool + scrot + xrandr + wmctrl -│ -├── win32/ ← Windows 专属增强能力(不在公共接口中) -│ ├── windowCapture.ts ← PrintWindow 窗口绑定截图 -│ ├── windowEnum.ts ← EnumWindows 窗口枚举 -│ ├── windowMessage.ts ← SendMessage/PostMessage 无焦点输入(新增) -│ ├── uiAutomation.ts ← IUIAutomation UI 元素操作 -│ └── ocr.ts ← Windows.Media.Ocr 文字识别 -│ -├── executor.ts ← 改: 通过 platforms/ 获取平台实现,不直接调 @ant 包 -├── swiftLoader.ts ← 改: 仅 darwin 使用 -├── inputLoader.ts ← 改: 仅 darwin 使用 -└── ...其他文件不动 -``` - -### 2.2 公共接口(`platforms/types.ts`) - -```typescript -/** 窗口标识 — 跨平台 */ -export interface WindowHandle { - id: string // macOS: bundleId, Windows: HWND string, Linux: window ID - pid: number - title: string - exePath?: string // Windows/Linux: 进程路径 -} - -/** 输入平台接口 — 两种模式 */ -export interface InputPlatform { - // 模式 A: 全局输入(macOS/Linux 默认,向前台窗口发送) - moveMouse(x: number, y: number): Promise - click(x: number, y: number, button: 'left' | 'right' | 'middle'): Promise - typeText(text: string): Promise - key(name: string, action: 'press' | 'release'): Promise - keys(combo: string[]): Promise - scroll(amount: number, direction: 'vertical' | 'horizontal'): Promise - mouseLocation(): Promise<{ x: number; y: number }> - - // 模式 B: 窗口绑定输入(Windows SendMessage,不抢焦点) - sendChar?(hwnd: string, char: string): Promise - sendKey?(hwnd: string, vk: number, action: 'down' | 'up'): Promise - sendClick?(hwnd: string, x: number, y: number, button: 'left' | 'right'): Promise - sendText?(hwnd: string, text: string): Promise -} - -/** 截图平台接口 */ -export interface ScreenshotPlatform { - // 全屏截图 - captureScreen(displayId?: number): Promise - // 区域截图 - captureRegion(x: number, y: number, w: number, h: number): Promise - // 窗口截图(Windows: PrintWindow,macOS: SCContentFilter,Linux: xdotool+import) - captureWindow?(hwnd: string): Promise -} - -/** 显示器平台接口 */ -export interface DisplayPlatform { - listAll(): DisplayInfo[] - getSize(displayId?: number): DisplayInfo -} - -/** 应用管理平台接口 */ -export interface AppsPlatform { - listRunning(): WindowHandle[] - listInstalled(): Promise - open(name: string): Promise - getFrontmostApp(): FrontmostAppInfo | null - findWindowByTitle(title: string): WindowHandle | null -} - -export interface ScreenshotResult { - base64: string - width: number - height: number -} - -export interface DisplayInfo { - width: number - height: number - scaleFactor: number - displayId: number -} - -export interface InstalledApp { - id: string // macOS: bundleId, Windows: exe path, Linux: .desktop name - displayName: string - path: string -} - -export interface FrontmostAppInfo { - id: string - appName: string -} -``` - -### 2.3 平台分发器(`platforms/index.ts`) - -```typescript -import type { InputPlatform, ScreenshotPlatform, DisplayPlatform, AppsPlatform } from './types.js' - -export interface Platform { - input: InputPlatform - screenshot: ScreenshotPlatform - display: DisplayPlatform - apps: AppsPlatform -} - -export function loadPlatform(): Platform { - switch (process.platform) { - case 'darwin': - return require('./darwin.js').platform - case 'win32': - return require('./win32.js').platform - case 'linux': - return require('./linux.js').platform - default: - throw new Error(`Computer Use not supported on ${process.platform}`) - } -} -``` - -### 2.4 各平台实现 - -**`platforms/darwin.ts`** — 委托给 @ant 包(保持兼容): -```typescript -// macOS: 通过 @ant/computer-use-input 和 @ant/computer-use-swift -// 这两个包的 darwin 后端保留不动 -import { requireComputerUseInput } from '../inputLoader.js' -import { requireComputerUseSwift } from '../swiftLoader.js' - -export const platform = { - input: { /* 委托给 requireComputerUseInput() */ }, - screenshot: { /* 委托给 requireComputerUseSwift().screenshot */ }, - display: { /* 委托给 requireComputerUseSwift().display */ }, - apps: { /* 委托给 requireComputerUseSwift().apps */ }, -} -``` - -**`platforms/win32.ts`** — 使用 `src/utils/computerUse/win32/` 模块: -```typescript -// Windows: SendMessage 输入 + PrintWindow 截图 + EnumWindows 应用 -import { sendChar, sendKey, sendClick, sendText } from '../win32/windowMessage.js' -import { captureWindow } from '../win32/windowCapture.js' -import { listWindows } from '../win32/windowEnum.js' -// ... PowerShell P/Invoke 全局输入作为 fallback - -export const platform = { - input: { - // 全局模式: PowerShell SetCursorPos/SendInput(fallback) - // 窗口模式: SendMessage(首选) - sendChar, sendKey, sendClick, sendText, // 窗口绑定 - moveMouse, click, typeText, ... // 全局 fallback - }, - screenshot: { - captureScreen, // CopyFromScreen - captureRegion, // CopyFromScreen(rect) - captureWindow, // PrintWindow(不抢焦点) - }, - display: { /* Screen.AllScreens */ }, - apps: { /* EnumWindows */ }, -} -``` - -**`platforms/linux.ts`** — 使用 xdotool/scrot: -```typescript -// Linux: xdotool + scrot + xrandr + wmctrl -export const platform = { - input: { /* xdotool mousemove/click/key/type */ }, - screenshot: { /* scrot */ }, - display: { /* xrandr */ }, - apps: { /* wmctrl + ps */ }, -} -``` - -### 2.5 executor.ts 改造 - -```typescript -// 之前: 直接调 requireComputerUseSwift() 和 requireComputerUseInput() -// 之后: 通过 platforms/ 统一获取 - -import { loadPlatform } from './platforms/index.js' - -const platform = loadPlatform() - -// 截图 -platform.screenshot.captureScreen() -platform.screenshot.captureWindow(hwnd) // 窗口绑定 - -// 输入(窗口绑定模式,不抢焦点) -platform.input.sendText?.(hwnd, 'Hello') -platform.input.sendClick?.(hwnd, 100, 200, 'left') - -// 输入(全局模式,fallback) -platform.input.moveMouse(500, 500) -platform.input.click(500, 500, 'left') -``` - -## 3. Windows 输入模式对比 - -| 方式 | API | 抢焦点 | 移鼠标 | 窗口可最小化 | 适用场景 | -|------|-----|--------|--------|-------------|---------| -| **全局输入** | `SetCursorPos` + `SendInput` | ✅ 抢 | ✅ 动 | ❌ 不行 | 需要坐标点击(fallback) | -| **窗口消息** | `SendMessage(WM_CHAR/WM_KEYDOWN)` | ❌ 不抢 | ❌ 不动 | ✅ 可以 | 打字、按键(首选) | -| **窗口消息** | `SendMessage(WM_LBUTTONDOWN)` | ❌ 不抢 | ❌ 不动 | ⚠️ 部分 | 窗口内点击 | -| **窗口截图** | `PrintWindow(hwnd, PW_RENDERFULLCONTENT)` | ❌ 不抢 | ❌ 不动 | ✅ 可以 | 窗口截图 | -| **UI 操作** | `UIAutomation InvokePattern` | ❌ 不抢 | ❌ 不动 | ✅ 可以 | 按钮点击、文本写入 | - -**策略**:优先用窗口消息 + UIAutomation(不干扰用户),全局输入作为 fallback。 - -## 4. 需要新增的文件 - -| 文件 | 说明 | -|------|------| -| `src/utils/computerUse/platforms/types.ts` | 公共接口定义 | -| `src/utils/computerUse/platforms/index.ts` | 平台分发器 | -| `src/utils/computerUse/platforms/darwin.ts` | macOS: 委托给 @ant 包 | -| `src/utils/computerUse/platforms/win32.ts` | Windows: 组合 win32/ 下各模块 | -| `src/utils/computerUse/platforms/linux.ts` | Linux: xdotool/scrot | -| `src/utils/computerUse/win32/windowMessage.ts` | **新增**: SendMessage 无焦点输入 | - -## 5. 需要移除/清理的文件 - -| 文件 | 操作 | 原因 | -|------|------|------| -| `packages/@ant/computer-use-input/src/backends/win32.ts` | 删除 | Windows 代码不应在 macOS 包里 | -| `packages/@ant/computer-use-input/src/backends/linux.ts` | 删除 | Linux 代码不应在 macOS 包里 | -| `packages/@ant/computer-use-swift/src/backends/win32.ts` | 删除 | 同上 | -| `packages/@ant/computer-use-swift/src/backends/linux.ts` | 删除 | 同上 | -| `packages/@ant/computer-use-input/src/types.ts` | 删除 | 移到 platforms/types.ts | -| `packages/@ant/computer-use-swift/src/types.ts` | 删除 | 移到 platforms/types.ts | - -## 6. 需要修改的文件 - -| 文件 | 改动 | -|------|------| -| `packages/@ant/computer-use-input/src/index.ts` | 恢复为仅 darwin dispatcher(去掉 win32/linux case) | -| `packages/@ant/computer-use-swift/src/index.ts` | 恢复为仅 darwin dispatcher(去掉 win32/linux case) | -| `src/utils/computerUse/executor.ts` | 通过 `platforms/` 获取平台实现,不直接调 @ant 包 | -| `src/utils/computerUse/swiftLoader.ts` | 仅 darwin 加载 | -| `src/utils/computerUse/inputLoader.ts` | 仅 darwin 加载 | - -## 7. @ant 包的定位(修正后) - -| 包 | 职责 | 平台 | -|---|------|------| -| `@ant/computer-use-input` | macOS enigo 键鼠原生模块包装 | **仅 darwin** | -| `@ant/computer-use-swift` | macOS Swift 截图/应用原生模块包装 | **仅 darwin** | -| `@ant/computer-use-mcp` | MCP Server + 工具定义 + 调用路由 | **跨平台**(不含平台代码) | - -Windows/Linux 的平台实现全部在 `src/utils/computerUse/platforms/` 和 `src/utils/computerUse/win32/` 中。 - -## 8. 执行顺序 - -``` -Phase 1: 创建 platforms/ 抽象层 - ├── platforms/types.ts(公共接口) - ├── platforms/index.ts(分发器) - └── platforms/darwin.ts(委托 @ant 包) - -Phase 2: 创建 Windows 平台实现 - ├── win32/windowMessage.ts(SendMessage 无焦点输入) - └── platforms/win32.ts(组合 win32/ 各模块) - -Phase 3: 创建 Linux 平台实现 - └── platforms/linux.ts(xdotool/scrot) - -Phase 4: 改造 executor.ts - └── 通过 platforms/ 获取实现,不直接调 @ant - -Phase 5: 清理 @ant 包 - ├── 删除 @ant/computer-use-input/src/backends/{win32,linux}.ts - ├── 删除 @ant/computer-use-swift/src/backends/{win32,linux}.ts - └── 恢复 index.ts 为 darwin-only - -Phase 6: 验证 + PR -``` diff --git a/docs/features/computer-use-mcp-test-report.md b/docs/features/computer-use-mcp-test-report.md deleted file mode 100644 index d8f4df39d..000000000 --- a/docs/features/computer-use-mcp-test-report.md +++ /dev/null @@ -1,277 +0,0 @@ -# Computer Use MCP 工具测试报告 - -> 测试日期: 2026-04-04 -> 测试环境: macOS Darwin 25.4.0, Cursor (IDE tier: click) -> MCP Server: `@ant/computer-use-mcp` - -## 工具总览 - -共 17 个工具(含 batch 复合操作),分为 5 大类: - -| 类别 | 工具 | 数量 | -|------|------|------| -| 截图/显示 | `screenshot`, `switch_display`, `zoom` | 3 | -| 鼠标操作 | `left_click`, `right_click`, `double_click`, `triple_click`, `middle_click`, `left_click_drag`, `mouse_move` | 7 | -| 键盘操作 | `key`, `type`, `hold_key` | 3 | -| 状态查询 | `cursor_position`, `request_access` | 2 | -| 复合/辅助 | `computer_batch`, `wait` | 2 | - ---- - -## 测试结果 - -### 1. 权限管理 - -#### `request_access` — 请求应用访问权限 - -| 项目 | 结果 | -|------|------| -| 状态 | ✅ 通过 | -| 行为 | 弹出系统对话框请求用户授权,支持批量申请多个应用 | -| 返回 | `{ granted: [...], denied: [...], tierGuidance: "..." }` | -| 权限分级 | `click`(仅点击), `full`(完整控制) | -| 说明 | IDE 类应用(Cursor、VSCode、Terminal)默认授予 `click` tier,限制键盘输入和右键操作;系统应用(System Settings)授予 `full` tier | - -#### 已授权应用 - -| 应用 | Tier | 能力 | -|------|------|------| -| Cursor | click | 可见 + 纯左键点击(无键盘输入、右键、修饰键点击、拖拽) | -| Terminal | click | 同上 | -| System Settings | full | 完整控制(键鼠、拖拽等) | -| Finder | — | 已授权 | - ---- - -### 2. 截图与显示 - -#### `screenshot` — 截取屏幕截图 - -| 项目 | 结果 | -|------|------| -| 状态 | ⚠️ 部分通过 | -| 执行 | 工具成功执行,返回 `ok: true` | -| 图片 | **未返回可视图片内容**(output 为空字符串) | -| `save_to_disk` | 设置后仍无输出 | -| 分析 | 可能原因:(1) macOS 屏幕录制权限未授予;(2) 当前前台应用未被过滤导致截图为空;(3) MCP 传输层未正确编码图片数据 | -| 建议 | 检查 **系统设置 → 隐私与安全性 → 屏幕录制** 是否授权给运行 Claude Code 的应用 | - -#### `switch_display` — 切换显示器 - -| 项目 | 结果 | -|------|------| -| 状态 | ✅ 通过 | -| 行为 | 接受显示器名称或 `"auto"`(自动选择) | -| 返回 | 确认消息 | - -#### `zoom` — 区域放大截图 - -| 项目 | 结果 | -|------|------| -| 状态 | ⏭️ 跳过 | -| 原因 | 依赖 `screenshot` 返回的图片坐标,截图未返回图片无法测试 | - ---- - -### 3. 鼠标操作 - -> 以下测试在 Cursor 窗口上执行(tier: click) - -#### `mouse_move` — 移动鼠标 - -| 项目 | 结果 | -|------|------| -| 状态 | ✅ 通过 | -| 输入 | `coordinate: [500, 500]` | -| 返回 | `"Moved."` | - -#### `left_click` — 左键单击 - -| 项目 | 结果 | -|------|------| -| 状态 | ✅ 通过 | -| 输入 | `coordinate: [500, 500]` | -| 返回 | `"Clicked."` | - -#### `double_click` — 双击 - -| 项目 | 结果 | -|------|------| -| 状态 | ✅ 通过 | -| 输入 | `coordinate: [500, 500]` | -| 返回 | `"Clicked."` | - -#### `triple_click` — 三击 - -| 项目 | 结果 | -|------|------| -| 状态 | ✅ 通过 | -| 输入 | `coordinate: [500, 500]` | -| 返回 | `"Clicked."` | - -#### `right_click` — 右键点击 - -| 项目 | 结果 | -|------|------| -| 状态 | ⚠️ 受 tier 限制 | -| Cursor (click tier) | ❌ 被拒绝 — `"Code" is granted at tier "click" — right-click, middle-click, and clicks with modifier keys require tier "full"` | -| Finder (full tier) | ✅ 通过 — 返回 `"Clicked."` | -| 结论 | 功能正常,IDE 安全限制符合预期 | - -#### `middle_click` — 中键点击 - -| 项目 | 结果 | -|------|------| -| 状态 | ⚠️ 受 tier 限制 | -| Cursor (click tier) | ❌ 被拒绝 — 同 `right_click`,需要 full tier | -| Finder (full tier) | ✅ 通过 — 返回 `"Clicked."` | -| 结论 | 功能正常,IDE 安全限制符合预期 | - -#### `left_click_drag` — 拖拽 - -| 项目 | 结果 | -|------|------| -| 状态 | ⚠️ 受 tier 限制 | -| Cursor (click tier) | ❌ 被拒绝 — 拖拽被视为修饰键点击,需要 full tier | -| Finder (full tier) | ✅ 通过 — 返回 `"Dragged."` | -| 结论 | 功能正常,IDE 安全限制符合预期 | - -#### `scroll` — 滚轮滚动 - -| 项目 | 结果 | -|------|------| -| 状态 | ✅ 通过 | -| 输入 | `coordinate: [500, 500]`, `scroll_direction: "down"`, `scroll_amount: 3` | -| 返回 | `"Scrolled."` | -| 反向 | ✅ `scroll_direction: "up"` 也通过 | - ---- - -### 4. 键盘操作 - -> 以下测试在 Cursor 窗口上执行(tier: click)— 所有键盘操作均被拒绝 - -#### `key` — 按键/快捷键 - -| 项目 | 结果 | -|------|------| -| 状态 | ⚠️ 受 tier 限制 | -| Cursor (click tier) | ❌ 被拒绝 — IDE tier 限制键盘输入 | -| Finder (full tier) | ✅ 通过 — `escape` 按键成功,返回 `"Key pressed."` | -| 结论 | 功能正常,IDE 安全限制符合预期 | - -#### `type` — 输入文本 - -| 项目 | 结果 | -|------|------| -| 状态 | ⚠️ 受 tier 限制 | -| Cursor (click tier) | ❌ 被拒绝 — IDE tier 限制文本输入 | -| Finder (full tier) | ✅ 通过 — 输入 `"hello"` 成功,返回 `"Typed 5 grapheme(s)."` | -| 结论 | 功能正常,IDE 安全限制符合预期 | - -#### `hold_key` — 按住按键 - -| 项目 | 结果 | -|------|------| -| 状态 | ⚠️ 受 tier 限制 | -| Cursor (click tier) | ❌ 被拒绝 — IDE tier 限制键盘输入 | -| Finder (full tier) | ✅ 通过 — 按住 `shift` 1 秒成功,返回 `"Key held."` | -| 结论 | 功能正常,IDE 安全限制符合预期 | - ---- - -### 5. 状态查询 - -#### `cursor_position` — 获取鼠标位置 - -| 项目 | 结果 | -|------|------| -| 状态 | ✅ 通过 | -| 返回 | `{"x": null, "y": null, "coordinateSpace": "image_pixels"}` | -| 说明 | 坐标为 null 是因为没有成功截图,无参考坐标系 | - ---- - -### 6. 复合/辅助操作 - -#### `computer_batch` — 批量执行操作 - -| 项目 | 结果 | -|------|------| -| 状态 | ✅ 通过 | -| 行为 | 按顺序执行操作列表,遇到失败则停止后续操作 | -| 返回 | `{ completed: [...], failed: {...}, remaining: N }` | -| 特点 | 单次 API 调用执行多个操作,减少往返延迟 | -| 错误处理 | 失败的操作会中断后续操作,返回已完成和剩余数量 | - -#### `wait` — 等待 - -| 项目 | 结果 | -|------|------| -| 状态 | ✅ 通过 | -| 输入 | `duration: 1` (秒) | -| 返回 | `"Waited 1s."` | -| 最大值 | 100 秒 | - ---- - -## 汇总统计 - -| 状态 | 数量 | 工具 | -|------|------|------| -| ✅ 通过 | 10 | `request_access`, `switch_display`, `mouse_move`, `left_click`, `double_click`, `triple_click`, `scroll`, `cursor_position`, `computer_batch`, `wait` | -| ⚠️ 部分通过 | 7 | `screenshot`(执行成功但无图片返回), `right_click`, `middle_click`, `left_click_drag`, `key`, `type`, `hold_key`(均在 full tier 应用上通过,IDE click tier 限制是预期行为) | -| ❌ 被拒绝 | 0 | — | -| ⏭️ 跳过 | 1 | `zoom`(依赖截图) | - ---- - -## 已知问题 - -### P0: 截图无图片返回 - -`screenshot` 工具执行成功但未返回图片内容,导致: -- 无法获取屏幕坐标参考 -- `cursor_position` 返回 null 坐标 -- `zoom` 无法使用 -- 所有点击操作只能盲点(无截图验证) - -**可能原因**: -1. macOS 屏幕录制权限未授予 -2. MCP 图片传输/编码问题 -3. 截图内容被安全过滤机制过滤 - -**建议排查**: 检查 `系统设置 → 隐私与安全性 → 屏幕录制` 权限。 - -### P1: IDE 应用键盘操作受限 — ✅ 已确认功能正常 - -IDE 类应用(Cursor、VSCode、Terminal)被限制在 `click` tier,无法执行: -- 键盘输入(`key`, `type`, `hold_key`) -- 右键/中键点击(`right_click`, `middle_click`) -- 拖拽操作(`left_click_drag`) - -这是安全设计,防止 AI 操控 IDE 终端。**在 full tier 应用(Finder、System Settings)上,以上 6 个操作均测试通过,功能完全正常。** - ---- - -## 权限模型说明 - -Computer Use MCP 采用分级权限模型: - -``` -┌─────────────────────────────────────────┐ -│ Tier: full │ -│ - 所有鼠标操作(左键、右键、中键、拖拽) │ -│ - 键盘输入(type, key, hold_key) │ -│ - 适用于: 系统应用、Finder 等 │ -├─────────────────────────────────────────┤ -│ Tier: click │ -│ - 仅纯左键点击 │ -│ - 滚轮滚动 │ -│ - 适用于: IDE、Terminal 等 │ -├─────────────────────────────────────────┤ -│ 未授权 │ -│ - 所有操作被拒绝 │ -│ - 需通过 request_access 申请 │ -└─────────────────────────────────────────┘ -``` diff --git a/docs/features/computer-use-windows-enhancement.md b/docs/features/computer-use-windows-enhancement.md deleted file mode 100644 index 288da5daf..000000000 --- a/docs/features/computer-use-windows-enhancement.md +++ /dev/null @@ -1,315 +0,0 @@ -# Computer Use Windows 增强实施计划 - -更新时间:2026-04-03 -依赖文档:`docs/features/windows-ai-desktop-control.md`、`docs/features/computer-use.md` - -## 1. 目标 - -在已有的 PowerShell 子进程方案基础上,利用 Windows 原生 API 增强 Computer Use 的 Windows 实现,解决 3 个核心问题: - -1. **窗口绑定截图**:当前 `CopyFromScreen` 只能全屏截图,无法对指定窗口截图(尤其是被遮挡/最小化窗口) -2. **UI 结构感知**:当前只能通过坐标点击,无法像 macOS Accessibility 那样理解 UI 元素树 -3. **性能**:每次 PowerShell 启动约 273ms,剪贴板/窗口枚举等高频操作需要更快的方式 - -## 2. 已验证的 Windows API 能力 - -以下 API 全部通过 PowerShell P/Invoke 实测通过: - -| 能力 | API | 验证结果 | -|------|-----|---------| -| 窗口绑定截图 | `PrintWindow(hwnd, hdc, PW_RENDERFULLCONTENT)` | ✅ VS Code 342KB, Chrome 273KB | -| 枚举窗口+HWND | `EnumWindows` + `GetWindowText` + `GetWindowThreadProcessId` | ✅ 38 个窗口,含 HWND/PID/标题 | -| UI 元素树 | `System.Windows.Automation.AutomationElement` | ✅ 记事本 39 个元素 | -| UI 写值 | `ValuePattern.SetValue()` | ✅ 成功写入记事本文本 | -| UI 点击 | `InvokePattern.Invoke()` | ✅ 按钮可程序化点击 | -| 坐标元素识别 | `AutomationElement.FromPoint(x, y)` | ✅ 返回元素类型+名称 | -| OCR | `Windows.Media.Ocr.OcrEngine` | ✅ 英语+中文引擎可用 | -| 全局热键 | `RegisterHotKey` | ✅ API 可调 | -| 剪贴板直接操作 | `System.Windows.Forms.Clipboard` | ✅ 读/写/图片检测 | -| Shell 启动 | `ShellExecute` | ✅ 打开文件/URL/应用 | - -## 3. 架构设计 - -### 3.1 文件结构 - -在现有 `backends/win32.ts` 基础上新增 Windows 专属模块: - -``` -packages/@ant/computer-use-input/src/ -├── backends/ -│ ├── darwin.ts ← 不动 -│ ├── win32.ts ← 增强:直接 Win32 API 替代部分 PowerShell -│ └── linux.ts ← 不动 - -packages/@ant/computer-use-swift/src/ -├── backends/ -│ ├── darwin.ts ← 不动 -│ ├── win32.ts ← 增强:PrintWindow 窗口截图 + EnumWindows -│ └── linux.ts ← 不动 - -packages/@ant/computer-use-mcp/src/ -│ └── tools.ts ← 增加 Windows 专属工具定义(UI Automation、OCR) - -src/utils/computerUse/ -│ └── win32/ ← 新增目录:Windows 专属能力 -│ ├── uiAutomation.ts ← UI 元素树、点击、写值 -│ ├── ocr.ts ← 截图 + OCR 文字识别 -│ ├── windowCapture.ts ← PrintWindow 窗口绑定截图 -│ └── windowEnum.ts ← EnumWindows 窗口枚举 -``` - -### 3.2 分层 - -``` -┌──────────────────────────────────────────────┐ -│ Computer Use MCP Tools │ -│ screenshot / click / type / request_access │ -│ + Windows 专属: ui_tree / ocr / window_cap │ -├──────────────────────────────────────────────┤ -│ src/utils/computerUse/ │ -│ executor.ts → 按平台 dispatch │ -│ win32/ → Windows 专属能力模块 │ -├──────────────────────────────────────────────┤ -│ packages/@ant/computer-use-{input,swift} │ -│ backends/win32.ts → PowerShell + Win32 API │ -├──────────────────────────────────────────────┤ -│ Windows Native API │ -│ PrintWindow / EnumWindows / UI Automation │ -│ SendInput / Clipboard / OCR / ShellExecute │ -└──────────────────────────────────────────────┘ -``` - -## 4. 实施计划 - -### Phase A:窗口绑定截图(解决核心问题) - -**问题**:当前 `CopyFromScreen` 只能全屏截图,无法对指定窗口截图。 -**方案**:用 `PrintWindow` + `FindWindow` 实现窗口级截图。 - -| 步骤 | 文件 | 改动 | -|------|------|------| -| A.1 | `src/utils/computerUse/win32/windowCapture.ts` | 新建:`captureWindow(title)` 用 PrintWindow 截取指定窗口 | -| A.2 | `src/utils/computerUse/win32/windowEnum.ts` | 新建:`listWindows()` 用 EnumWindows 返回 {hwnd, pid, title}[] | -| A.3 | `packages/@ant/computer-use-swift/src/backends/win32.ts` | `screenshot.captureExcluding` 增加按窗口截图能力 | -| A.4 | `packages/@ant/computer-use-swift/src/backends/win32.ts` | `apps.listRunning` 用 EnumWindows 替代 Get-Process(返回 HWND) | - -**PowerShell 脚本核心**: - -```powershell -# PrintWindow 截取指定窗口 -Add-Type -AssemblyName System.Drawing -Add-Type -ReferencedAssemblies System.Drawing @' -using System; using System.Runtime.InteropServices; using System.Drawing; using System.Drawing.Imaging; -public class WinCap { - [DllImport("user32.dll", CharSet=CharSet.Unicode)] - public static extern IntPtr FindWindow(string c, string t); - [DllImport("user32.dll")] - public static extern bool GetWindowRect(IntPtr h, out RECT r); - [DllImport("user32.dll")] - public static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint f); - [StructLayout(LayoutKind.Sequential)] - public struct RECT { public int L, T, R, B; } - // ... CaptureByTitle(string title) → base64 -} -'@ -``` - -**验证标准**: -- 能按窗口标题截图 -- 被遮挡的窗口也能截图 -- 返回 base64 + width + height - -### Phase B:UI Automation(Windows 专属新能力) - -**问题**:macOS 有 Accessibility API 可以读取/操作 UI 元素,Windows 当前只能坐标点击。 -**方案**:用 `System.Windows.Automation` 实现 UI 树读取和元素操作。 - -| 步骤 | 文件 | 改动 | -|------|------|------| -| B.1 | `src/utils/computerUse/win32/uiAutomation.ts` | 新建:核心 UIA 操作封装 | -| B.2 | `packages/@ant/computer-use-mcp/src/tools.ts` | 增加 Windows 专属工具定义 | - -**uiAutomation.ts 导出函数**: - -```typescript -// 获取窗口的 UI 元素树 -getUITree(windowTitle: string, depth: number): UIElement[] - -// 按名称/类型/AutomationId 查找元素 -findElement(windowTitle: string, query: {name?, controlType?, automationId?}): UIElement | null - -// 点击元素(InvokePattern) -clickElement(windowTitle: string, automationId: string): boolean - -// 设置元素值(ValuePattern) -setValue(windowTitle: string, automationId: string, value: string): boolean - -// 获取坐标处的元素 -elementAtPoint(x: number, y: number): UIElement | null -``` - -**UIElement 类型**: -```typescript -interface UIElement { - name: string - controlType: string // Button, Edit, Text, List, etc. - automationId: string - boundingRect: { x: number, y: number, w: number, h: number } - isEnabled: boolean - value?: string // ValuePattern 可用时 - children?: UIElement[] -} -``` - -**PowerShell 脚本核心**: -```powershell -Add-Type -AssemblyName UIAutomationClient -Add-Type -AssemblyName UIAutomationTypes - -# 读取 UI 树 -$root = [AutomationElement]::RootElement -$window = $root.FindFirst([TreeScope]::Children, - [PropertyCondition]::new([AutomationElement]::NameProperty, $title)) -$elements = $window.FindAll([TreeScope]::Descendants, [Condition]::TrueCondition) - -# 写入文本 -$element.GetCurrentPattern([ValuePattern]::Pattern).SetValue($text) - -# 点击按钮 -$element.GetCurrentPattern([InvokePattern]::Pattern).Invoke() -``` - -**验证标准**: -- 能读取记事本的 UI 树(按钮、文本框、菜单) -- 能向文本框写入内容 -- 能点击按钮 -- 能识别坐标处的元素 - -### Phase C:OCR 屏幕文字识别 - -**问题**:截图后 AI 只能看到图片,无法直接读取文字。 -**方案**:用 `Windows.Media.Ocr` 对截图进行文字识别。 - -| 步骤 | 文件 | 改动 | -|------|------|------| -| C.1 | `src/utils/computerUse/win32/ocr.ts` | 新建:截图 + OCR 识别 | -| C.2 | `packages/@ant/computer-use-mcp/src/tools.ts` | 增加 `screen_ocr` 工具定义 | - -**ocr.ts 导出函数**: -```typescript -// 对屏幕区域 OCR -ocrRegion(x: number, y: number, w: number, h: number, lang?: string): OcrResult - -// 对指定窗口 OCR -ocrWindow(windowTitle: string, lang?: string): OcrResult - -interface OcrResult { - text: string - lines: { text: string, bounds: {x,y,w,h} }[] - language: string -} -``` - -**已确认可用语言**:英语 (en-US) + 中文 (zh-Hans-CN) - -**验证标准**: -- 能识别屏幕区域中的英文和中文 -- 返回文字内容 + 每行的位置信息 - -### Phase D:高频操作性能优化 - -**问题**:每次 PowerShell 启动 273ms,鼠标移动等高频操作太慢。 -**方案**:用 .NET `System.Windows.Forms.Clipboard` 等直接 API 替代 PowerShell 子进程。 - -| 步骤 | 文件 | 改动 | -|------|------|------| -| D.1 | `src/utils/computerUse/executor.ts` | 剪贴板操作用直接 API 替代 PowerShell | -| D.2 | 考虑驻留 PowerShell 进程 | 通过 stdin/stdout 交互,摊平启动成本 | - -**剪贴板直接 API**(不需要 PowerShell 子进程): -```powershell -# 读:50ms → <1ms -[System.Windows.Forms.Clipboard]::GetText() - -# 写:50ms → <1ms -[System.Windows.Forms.Clipboard]::SetText($text) - -# 图片检测 -[System.Windows.Forms.Clipboard]::ContainsImage() -``` - -### Phase E:`request_access` Windows 适配 - -**问题**:`request_access` 依赖 macOS bundleId 识别应用,Windows 没有这个概念。 -**方案**:在 Windows 上用 exe 路径 + 窗口标题替代 bundleId。 - -| 步骤 | 文件 | 改动 | -|------|------|------| -| E.1 | `packages/@ant/computer-use-mcp/src/toolCalls.ts` | `resolveRequestedApps` 在 Windows 上用 exe 路径匹配 | -| E.2 | `packages/@ant/computer-use-mcp/src/sentinelApps.ts` | 增加 Windows 危险应用列表(cmd.exe, powershell.exe 等) | -| E.3 | `packages/@ant/computer-use-mcp/src/deniedApps.ts` | 增加 Windows 浏览器/终端识别规则 | -| E.4 | `src/utils/computerUse/hostAdapter.ts` | `ensureOsPermissions` Windows 上检查 UAC 状态 | - -**Windows 应用标识映射**: -``` -macOS bundleId → Windows 等价 -com.apple.Safari → C:\Program Files\...\msedge.exe(或窗口标题匹配) -com.google.Chrome → chrome.exe -com.apple.Terminal → WindowsTerminal.exe / cmd.exe -``` - -### Phase F:全局热键(ESC 拦截) - -**问题**:当前非 darwin 直接跳过 ESC 热键,用 Ctrl+C 替代。 -**方案**:用 `RegisterHotKey` 或 `SetWindowsHookEx(WH_KEYBOARD_LL)` 实现。 - -| 步骤 | 文件 | 改动 | -|------|------|------| -| F.1 | `src/utils/computerUse/escHotkey.ts` | Windows 分支:RegisterHotKey 注册 ESC | - -**优先级低**——当前 Ctrl+C fallback 可用,ESC 热键是体验优化。 - -## 5. 执行优先级 - -``` -Phase A: 窗口绑定截图 ← P0 核心需求,解决"操作其他界面" -Phase B: UI Automation ← P0 核心能力,AI 理解 UI 结构 -Phase C: OCR ← P1 增值能力,AI 读屏幕文字 -Phase D: 性能优化 ← P1 体验优化,高频操作提速 -Phase E: request_access 适配 ← P1 功能完整性,权限模型适配 -Phase F: ESC 热键 ← P2 体验优化,可后做 -``` - -## 6. 每个 Phase 的改动量估算 - -| Phase | 新增文件 | 修改文件 | 新增代码行 | 风险 | -|-------|---------|---------|-----------|------| -| A 窗口截图 | 2 | 1 | ~200 | 低 | -| B UI Automation | 1 | 1 | ~300 | 中 | -| C OCR | 1 | 1 | ~150 | 低 | -| D 性能优化 | 0 | 2 | ~50 | 低 | -| E request_access | 0 | 3 | ~100 | 中 | -| F ESC 热键 | 0 | 1 | ~50 | 低 | -| **总计** | **4** | **9** | **~850** | — | - -## 7. 不动的文件 - -- `backends/darwin.ts`(两个包都不动) -- `backends/linux.ts`(两个包都不动) -- `src/utils/computerUse/` 中 macOS 相关代码路径不动 -- `packages/@ant/computer-use-mcp/src/` 中已复制的参考项目代码不动(只追加 Windows 工具) - -## 8. 与 macOS/Linux 方案的对比 - -| 能力 | macOS | Windows (增强后) | Linux | -|------|-------|-----------------|-------| -| 截图方式 | SCContentFilter (per-app) | **PrintWindow (per-window)** | scrot (全屏/区域) | -| UI 结构 | Accessibility API | **UI Automation** | 无 | -| OCR | 无内置 | **Windows.Media.Ocr** | 无内置 | -| 键鼠 | CGEvent + enigo | SendInput + keybd_event | xdotool | -| 窗口管理 | NSWorkspace | **EnumWindows + Win32** | wmctrl | -| 剪贴板 | pbcopy/pbpaste | **Clipboard 直接 API** | xclip | -| ESC 热键 | CGEventTap | RegisterHotKey | 无 | -| 应用标识 | bundleId | exe 路径 + 窗口标题 | /proc + wmctrl | - -**Windows 增强后将在 UI Automation 和 OCR 方面超过 macOS 方案**——这两项 macOS 原始实现也没有(Anthropic 用的是截图 + Claude 视觉理解,没有结构化 UI 数据)。 diff --git a/docs/features/computer-use.md b/docs/features/computer-use.md deleted file mode 100644 index b3e337029..000000000 --- a/docs/features/computer-use.md +++ /dev/null @@ -1,197 +0,0 @@ -# Computer Use — macOS / Windows / Linux 跨平台实施计划 - -更新时间:2026-04-03 -参考项目:`E:\源码\claude-code-source-main\claude-code-source-main` - -## 1. 现状 - -参考项目的 Computer Use **仅支持 macOS**——从入口到底层全部写死 darwin。我们的项目在 Phase 1-3 中已经完成了: - -- ✅ `@ant/computer-use-mcp` stub 替换为完整实现(12 文件) -- ✅ `@ant/computer-use-input` 拆为 dispatcher + backends(darwin + win32) -- ✅ `@ant/computer-use-swift` 拆为 dispatcher + backends(darwin + win32) -- ✅ `CHICAGO_MCP` 编译开关已开 -- ✅ `src/` 层 macOS 硬编码已移除(Phase 2 已完成) - -## 2. 阻塞点全景 - -### 2.1 入口层 - -| # | 文件:行号 | 阻塞代码 | 影响 | -|---|----------|---------|------| -| 1 | `src/main.tsx:2366` | `feature("CHICAGO_MCP")` 门控 | CU 初始化入口 | - -### 2.2 加载层 - -| # | 文件:行号 | 阻塞代码 | 影响 | -|---|----------|---------|------| -| 2 | `src/utils/computerUse/swiftLoader.ts` | macOS-only loader(已改为仅 darwin 加载) | 非 darwin 使用 platforms/ 替代 | -| 3 | `src/utils/computerUse/executor.ts:302` | `process.platform !== 'darwin'` → cross-platform executor | 非 darwin 走跨平台路径 | - -### 2.3 macOS 特有依赖 - -| # | 文件:行号 | 依赖 | macOS 实现 | 需要替代方案 | -|---|----------|------|-----------|------------| -| 4 | `executor.ts:72-96` | 剪贴板 | `pbcopy`/`pbpaste` / PowerShell / xclip | Win: PowerShell `Get/Set-Clipboard`;Linux: `xclip`/`wl-copy` | -| 5 | `drainRunLoop.ts` | CFRunLoop pump | `cu._drainMainRunLoop()` | 非 darwin:直接执行 fn(),不需要 pump | -| 6 | `escHotkey.ts` | ESC 热键 | CGEventTap | 非 darwin:返回 false(已有 Ctrl+C fallback) | -| 7 | `hostAdapter.ts` | 系统权限 | TCC accessibility + screenRecording | Win:直接 granted;Linux:检查 xdotool | -| 8 | `common.ts:55-58` | 平台标识 | 动态获取 | 已改为 `process.platform` 分发 | -| 9 | `executor.ts:232` | 粘贴快捷键 | `command`/`ctrl` 分发 | 已按平台分发粘贴快捷键 | - -### 2.4 缺失的 Linux 后端 - -| 包 | macOS | Windows | Linux | -|---|-------|---------|-------| -| `computer-use-input/backends/` | ✅ darwin.ts | ✅ win32.ts | ❌ 需新建 linux.ts | -| `computer-use-swift/backends/` | ✅ darwin.ts | ✅ win32.ts | ❌ 需新建 linux.ts | - -## 3. 每个平台的能力依赖 - -### 3.1 computer-use-input(键鼠) - -| 功能 | macOS | Windows | Linux | -|------|-------|---------|-------| -| 鼠标移动 | CGEvent JXA | SetCursorPos P/Invoke | xdotool mousemove | -| 鼠标点击 | CGEvent JXA | SendInput P/Invoke | xdotool click | -| 鼠标滚轮 | CGEvent JXA | SendInput MOUSEEVENTF_WHEEL | xdotool scroll | -| 键盘按键 | System Events osascript | keybd_event P/Invoke | xdotool key | -| 组合键 | System Events osascript | keybd_event 组合 | xdotool key combo | -| 文本输入 | System Events keystroke | SendKeys.SendWait | xdotool type | -| 前台应用 | System Events osascript | GetForegroundWindow P/Invoke | xdotool getactivewindow + /proc | -| 工具依赖 | osascript(内置) | powershell(内置) | xdotool(需安装) | - -### 3.2 computer-use-swift(截图 + 应用管理) - -| 功能 | macOS | Windows | Linux | -|------|-------|---------|-------| -| 全屏截图 | screencapture | CopyFromScreen | gnome-screenshot / scrot / grim | -| 区域截图 | screencapture -R | CopyFromScreen(rect) | gnome-screenshot -a / scrot -a / grim -g | -| 显示器列表 | CGGetActiveDisplayList JXA | Screen.AllScreens | xrandr --query | -| 运行中应用 | System Events JXA | Get-Process | wmctrl -l / ps | -| 打开应用 | osascript activate | Start-Process | xdg-open / gtk-launch | -| 隐藏/显示 | System Events visibility | ShowWindow/SetForegroundWindow | wmctrl -c / xdotool | -| 工具依赖 | screencapture + osascript | powershell | xdotool + scrot/grim + wmctrl | - -### 3.3 executor 层 - -| 功能 | macOS | Windows | Linux | -|------|-------|---------|-------| -| drainRunLoop | CFRunLoop pump | 不需要 | 不需要 | -| ESC 热键 | CGEventTap | 跳过(Ctrl+C fallback) | 跳过(Ctrl+C fallback) | -| 剪贴板读 | pbpaste | `powershell Get-Clipboard` | xclip -o / wl-paste | -| 剪贴板写 | pbcopy | `powershell Set-Clipboard` | xclip / wl-copy | -| 粘贴快捷键 | command+v | ctrl+v | ctrl+v | -| 终端检测 | __CFBundleIdentifier | WT_SESSION / TERM_PROGRAM | TERM_PROGRAM | -| 系统权限 | TCC check | 直接 granted | 检查 xdotool 安装 | - -## 4. 执行步骤 - -### Phase 1:已完成 ✅ - -- [x] `@ant/computer-use-mcp` stub → 完整实现 -- [x] `@ant/computer-use-input` dispatcher + darwin/win32 backends -- [x] `@ant/computer-use-swift` dispatcher + darwin/win32 backends -- [x] `CHICAGO_MCP` 编译开关 - -### Phase 2:移除 6 处 macOS 硬编码(解锁 macOS + Windows) - -**改动原则:macOS 代码路径不变,只在每处 darwin 守卫后加 win32/linux 分支。** - -| 步骤 | 文件 | 改动 | -|------|------|------| -| 2.1 | `src/main.tsx:2366` | `feature("CHICAGO_MCP")` → 已为跨平台入口 | -| 2.2 | `src/utils/computerUse/swiftLoader.ts` | 已改为仅 darwin 加载,非 darwin 使用 platforms/ | -| 2.3 | `src/utils/computerUse/executor.ts:302-309` | 已改为 cross-platform dispatch(非 darwin → createCrossPlatformExecutor) | -| 2.4 | `src/utils/computerUse/executor.ts:72-96` | 剪贴板已按平台分发:darwin→pbcopy/pbpaste,win32→PowerShell,linux→xclip | -| 2.5 | `src/utils/computerUse/executor.ts:232` | 粘贴快捷键已按平台分发:darwin→command,其他→ctrl | -| 2.6 | `src/utils/computerUse/executor.ts:302-309` | 非 darwin 已改为 `createCrossPlatformExecutor()` | -| 2.7 | `src/utils/computerUse/drainRunLoop.ts` | 非 darwin 无需 pump(直接执行 fn) | -| 2.8 | `src/utils/computerUse/escHotkey.ts` | 非 darwin 返回 false(已有 Ctrl+C fallback) | -| 2.9 | `src/utils/computerUse/hostAdapter.ts` | 非 darwin 权限检查逻辑已实现 | -| 2.10 | `src/utils/computerUse/common.ts:58` | 已改为动态 `process.platform` 分发 | -| 2.11 | `src/utils/computerUse/common.ts:55` | 已改为 darwin→'native',其他→'none' | -| 2.12 | `src/utils/computerUse/gates.ts:55` | 已更新(需验证 enabled 默认值) | -| 2.13 | `src/utils/computerUse/gates.ts:39` | `hasRequiredSubscription()` 已更新 | - -### Phase 3:新增 Linux 后端 - -| 步骤 | 文件 | 内容 | -|------|------|------| -| 3.1 | `packages/@ant/computer-use-input/src/backends/linux.ts` | xdotool 键鼠(mousemove/click/key/type/getactivewindow) | -| 3.2 | `packages/@ant/computer-use-swift/src/backends/linux.ts` | scrot/grim 截图 + xrandr 显示器 + wmctrl 窗口管理 | -| 3.3 | `packages/@ant/computer-use-input/src/index.ts` | dispatcher 加 `case 'linux'` | -| 3.4 | `packages/@ant/computer-use-swift/src/index.ts` | dispatcher 加 `case 'linux'` | - -### Phase 4:验证 - -| 测试项 | macOS | Windows | Linux | -|--------|-------|---------|-------| -| build 成功 | ✅ | 验证 | 验证 | -| MCP 工具列表非空 | 验证 | 验证 | 验证 | -| 鼠标移动 | 验证 | ✅ 已通过 | 验证 | -| 截图 | 验证 | ✅ 已通过 | 验证 | -| 键盘输入 | 验证 | 验证 | 验证 | -| 前台窗口 | 验证 | ✅ 已通过 | 验证 | -| 剪贴板 | 验证 | 验证 | 验证 | - -## 5. 文件改动总览 - -### 不动的文件(14 个) - -`cleanup.ts`、`computerUseLock.ts`、`wrapper.tsx`、`toolRendering.tsx`、`mcpServer.ts`、`setup.ts`、`appNames.ts`、`inputLoader.ts`、`src/services/mcp/client.ts`、`@ant/computer-use-mcp/src/*`(Phase 1 已完成)、`backends/darwin.ts`(两个包都不动) - -### 改 src/ 的文件(8 个) - -| 文件 | 改动量 | 风险 | -|------|--------|------| -| `main.tsx` | 1 行 | 低 | -| `swiftLoader.ts` | 2 行 | 低 | -| `executor.ts` | ~40 行(剪贴板分发 + 平台守卫 + paste 快捷键) | **中** | -| `drainRunLoop.ts` | 1 行 | 低 | -| `escHotkey.ts` | 3 行 | 低 | -| `hostAdapter.ts` | 5 行 | 低 | -| `common.ts` | 3 行 | 低 | -| `gates.ts` | 3 行 | 低 | - -### 新增文件(2 个) - -| 文件 | 行数估算 | -|------|---------| -| `packages/@ant/computer-use-input/src/backends/linux.ts` | ~150 行 | -| `packages/@ant/computer-use-swift/src/backends/linux.ts` | ~200 行 | - -## 6. Linux 依赖工具 - -| 工具 | 用途 | 安装命令(Ubuntu) | -|------|------|-------------------| -| `xdotool` | 键鼠模拟 + 窗口管理 | `sudo apt install xdotool` | -| `scrot` 或 `gnome-screenshot` | 截图 | `sudo apt install scrot` | -| `xrandr` | 显示器信息 | 通常已预装 | -| `xclip` | 剪贴板 | `sudo apt install xclip` | -| `wmctrl` | 窗口列表/切换 | `sudo apt install wmctrl` | - -Wayland 环境需要替代工具:`ydotool`(替代 xdotool)、`grim`(替代 scrot)、`wl-clipboard`(替代 xclip)。初期可先只支持 X11,Wayland 标记为 todo。 - -## 7. 执行顺序建议 - -``` -Phase 2(解锁 macOS + Windows) - ├── 2.1-2.3 移除 3 处硬编码 throw/skip - ├── 2.4-2.5 剪贴板 + 粘贴快捷键平台分发 - ├── 2.6 swiftLoader → 直接实例化 - ├── 2.7-2.9 drainRunLoop / escHotkey / permissions 平台分支 - ├── 2.10-2.11 common.ts 平台标识动态化 - ├── 2.12-2.13 gates.ts 默认值 - └── 验证 Windows - -Phase 3(Linux 后端) - ├── 3.1 input/backends/linux.ts - ├── 3.2 swift/backends/linux.ts - ├── 3.3-3.4 dispatcher 加 linux case - └── 验证 Linux - -Phase 4(集成验证 + PR) -``` - -每个 Phase 可独立验证、独立提交。Phase 2 完成后 macOS + Windows 可用,Phase 3 完成后三平台全部可用。 diff --git a/docs/features/context-collapse.md b/docs/features/context-collapse.md deleted file mode 100644 index afe9f153e..000000000 --- a/docs/features/context-collapse.md +++ /dev/null @@ -1,140 +0,0 @@ -# CONTEXT_COLLAPSE — 上下文折叠 - -> Feature Flag: `FEATURE_CONTEXT_COLLAPSE=1` -> 子 Feature: `FEATURE_HISTORY_SNIP=1` -> 实现状态:核心逻辑全部 Stub,布线完整 -> 引用数:CONTEXT_COLLAPSE 20 + HISTORY_SNIP 16 = 36 - -## 一、功能概述 - -CONTEXT_COLLAPSE 让模型内省上下文窗口使用情况,并智能压缩旧消息。当对话接近上下文限制时,自动将旧消息折叠为压缩摘要,保留关键信息的同时释放 token 空间。 - -### 子 Feature - -| Feature | 功能 | -|---------|------| -| `CONTEXT_COLLAPSE` | 上下文折叠引擎(后台 LLM 调用压缩旧消息) | -| `HISTORY_SNIP` | SnipTool — 标记消息进行折叠/修剪 | - -## 二、实现架构 - -### 2.1 模块状态 - -| 模块 | 文件 | 状态 | -|------|------|------| -| 折叠核心 | `src/services/contextCollapse/index.ts` | **Stub** — 接口完整(`ContextCollapseStats`、`CollapseResult`、`DrainResult`),函数全部空操作 | -| 折叠操作 | `src/services/contextCollapse/operations.ts` | **Stub** — `projectView` 为恒等函数 | -| 折叠持久化 | `src/services/contextCollapse/persist.ts` | **Stub** — `restoreFromEntries` 为空操作 | -| CtxInspectTool | `packages/builtin-tools/src/tools/CtxInspectTool/CtxInspectTool.ts` | **实现** — 上下文内省工具 | -| SnipTool 提示 | `src/tools/SnipTool/prompt.ts` | **Stub** — 空工具名 | -| SnipTool 实现 | `src/tools/SnipTool/SnipTool.ts` | **缺失** | -| force-snip 命令 | `src/commands/force-snip.js` | **缺失** | -| 折叠读取搜索 | `src/utils/collapseReadSearch.ts` | **完整** — Snip 作为静默吸收操作 | -| QueryEngine 集成 | `src/QueryEngine.ts` | **布线** — 导入并使用 snip 投影 | -| Token 警告 UI | `src/components/TokenWarning.tsx` | **布线** — 折叠进度标签 | - -### 2.2 核心接口(已定义,待实现) - -```ts -// contextCollapse/index.ts -interface ContextCollapseStats { - // 上下文使用统计 -} -interface CollapseResult { - // 折叠操作结果 -} -interface DrainResult { - // 紧急释放结果 -} - -// 关键函数(全部 stub): -isContextCollapseEnabled() // → false -applyCollapsesIfNeeded(messages) // 透传 -recoverFromOverflow(messages) // 透传(413 恢复) -initContextCollapse() // 空操作 -``` - -### 2.3 预期数据流 - -``` -对话持续增长 - │ - ▼ -上下文接近限制(由 query.ts 检测) - │ - ├── 溢出检测 (query.ts:440,616,802) - │ - ▼ -applyCollapsesIfNeeded(messages) [需要实现] - │ - ├── 后台 LLM 调用压缩旧消息 - ├── 保留关键信息(决策、文件路径、错误) - └── 替换旧消息为压缩摘要 - │ - ├── 413 恢复 (query.ts:1093,1179) - │ └── recoverFromOverflow() 紧急折叠 - │ - ▼ -projectView() 过滤折叠后的消息视图 - │ - ▼ -模型继续工作(在压缩后的上下文中) -``` - -### 2.4 HISTORY_SNIP 子功能 - -SnipTool 提供手动折叠能力: - -- `/force-snip` 命令 — 强制执行折叠 -- SnipTool — 标记特定消息进行折叠/修剪 -- `collapseReadSearch.ts` 已完整实现,将 Snip 作为静默吸收操作处理 - -### 2.5 集成点 - -| 文件 | 位置 | 说明 | -|------|------|------| -| `src/query.ts` | 18,440,616,802,1093,1179 | 溢出检测、413 恢复、折叠应用 | -| `src/QueryEngine.ts` | 124,127,1301 | Snip 投影使用 | -| `src/utils/analyzeContext.ts` | 1122 | 跳过保留缓冲区显示 | -| `src/utils/sessionRestore.ts` | 127,494 | 恢复折叠状态 | -| `src/services/compact/autoCompact.ts` | 179,215 | 自动压缩时考虑折叠 | - -## 三、需要补全的内容 - -| 优先级 | 模块 | 工作量 | 说明 | -|--------|------|--------|------| -| 1 | `services/contextCollapse/index.ts` | 大 | 折叠状态机、LLM 调用、消息压缩 | -| 2 | `services/contextCollapse/operations.ts` | 中 | `projectView()` 消息过滤 | -| 3 | `services/contextCollapse/persist.ts` | 小 | `restoreFromEntries()` 磁盘持久化 | -| 4 | `tools/CtxInspectTool/` | 已完成 | 上下文内省工具已实现(`packages/builtin-tools/src/tools/CtxInspectTool/`) | -| 5 | `tools/SnipTool/SnipTool.ts` | 中 | Snip 工具实现 | -| 6 | `commands/force-snip.js` | 小 | `/force-snip` 命令 | - -## 四、关键设计决策 - -1. **后台 LLM 压缩**:折叠不是简单截断,而是用 LLM 生成压缩摘要保留关键信息 -2. **413 恢复**:当 API 返回 413(请求过大)时,紧急折叠是最重要的恢复手段 -3. **与 autoCompact 协作**:折叠和自动压缩(compact)是不同的机制,折叠在消息级别,压缩在对话级别 -4. **持久化**:折叠状态持久化到磁盘,会话恢复时重载 - -## 五、使用方式 - -```bash -# 启用 context collapse -FEATURE_CONTEXT_COLLAPSE=1 bun run dev - -# 启用 snip 子功能 -FEATURE_CONTEXT_COLLAPSE=1 FEATURE_HISTORY_SNIP=1 bun run dev -``` - -## 六、文件索引 - -| 文件 | 职责 | -|------|------| -| `src/services/contextCollapse/index.ts` | 折叠核心(stub,接口已定义) | -| `src/services/contextCollapse/operations.ts` | 投影操作(stub) | -| `src/services/contextCollapse/persist.ts` | 持久化(stub) | -| `src/utils/collapseReadSearch.ts` | Snip 吸收操作(完整) | -| `src/query.ts` | 溢出检测和 413 恢复集成 | -| `src/QueryEngine.ts` | Snip 投影使用 | -| `src/components/TokenWarning.tsx` | 折叠进度 UI | diff --git a/docs/features/coordinator-mode.md b/docs/features/coordinator-mode.md deleted file mode 100644 index 322ae488b..000000000 --- a/docs/features/coordinator-mode.md +++ /dev/null @@ -1,151 +0,0 @@ -# COORDINATOR_MODE — 多 Agent 编排 - -> Feature Flag: `FEATURE_COORDINATOR_MODE=1` + 环境变量 `CLAUDE_CODE_COORDINATOR_MODE=1` -> 实现状态:编排者完整可用,worker agent 为通用 AgentTool worker -> 引用数:32 - -## 一、功能概述 - -COORDINATOR_MODE 将 CLI 变为"编排者"角色。编排者不直接操作文件,而是通过 AgentTool 派发任务给多个 worker 并行执行。适用于大型任务拆分、并行研究、实现+验证分离等场景。 - -### 核心约束 - -- 编排者只能使用:`Agent`(派发 worker)、`SendMessage`(继续 worker)、`TaskStop`(停止 worker) -- Worker 可以使用所有标准工具(Bash、Read、Edit 等)+ MCP 工具 + Skill 工具 -- 编排者的每条消息都是给用户看的;worker 结果以 `` XML 形式到达 - -## 二、用户交互 - -### 启用方式 - -```bash -FEATURE_COORDINATOR_MODE=1 CLAUDE_CODE_COORDINATOR_MODE=1 bun run dev -``` - -需要同时设置 feature flag 和环境变量。`CLAUDE_CODE_COORDINATOR_MODE` 可在会话恢复时自动切换(`matchSessionMode`)。 - -### 典型工作流 - -``` -用户: "修复 auth 模块的 null pointer" - -编排者: - 1. 并行派发两个 worker: - - Agent({ description: "调查 auth bug", prompt: "..." }) - - Agent({ description: "研究 auth 测试", prompt: "..." }) - - 2. 收到 : - - Worker A: "在 validate.ts:42 发现 null pointer" - - Worker B: "测试覆盖情况..." - - 3. 综合发现,继续 Worker A: - - SendMessage({ to: "agent-a1b", message: "修复 validate.ts:42..." }) - - 4. 收到修复结果,派发验证: - - Agent({ description: "验证修复", prompt: "..." }) -``` - -## 三、实现架构 - -### 3.1 模式检测 - -文件:`src/coordinator/coordinatorMode.ts:36-41` - -```ts -export function isCoordinatorMode(): boolean { - return feature('COORDINATOR_MODE') && - isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) -} -``` - -### 3.2 会话模式恢复 - -`matchSessionMode(sessionMode)` 在恢复旧会话时检查存储的模式,如果当前环境变量与存储不一致,自动翻转环境变量。防止在普通模式下恢复编排会话(或反之)。 - -### 3.3 Worker 工具集 - -`getCoordinatorUserContext()` 告知编排者 worker 可用的工具列表: - -- **标准模式**:`ASYNC_AGENT_ALLOWED_TOOLS` 排除内部工具(TeamCreate、TeamDelete、SendMessage、SyntheticOutput) -- **Simple 模式**(`CLAUDE_CODE_SIMPLE=1`):仅 Bash、Read、Edit -- **MCP 工具**:列出已连接的 MCP 服务器名称 -- **Scratchpad**:如果 GrowthBook `tengu_scratch` 启用,提供跨 worker 共享的 scratchpad 目录 - -### 3.4 系统提示 - -文件:`src/coordinator/coordinatorMode.ts:111-369` - -编排者系统提示(`getCoordinatorSystemPrompt()`)约 370 行,包含: - -| 章节 | 内容 | -|------|------| -| 1. Your Role | 编排者职责定义 | -| 2. Your Tools | Agent/SendMessage/TaskStop 使用说明 | -| 3. Workers | Worker 能力和限制 | -| 4. Task Workflow | Research → Synthesis → Implementation → Verification 流程 | -| 5. Writing Worker Prompts | 自包含 prompt 编写指南 + 好坏示例对比 | -| 6. Example Session | 完整示例对话 | - -### 3.5 Worker Agent - -文件:`src/coordinator/workerAgent.ts` - -当前为 stub。Worker 实际使用通用 AgentTool 的 `worker` subagent_type。 - -### 3.6 数据流 - -``` -用户消息 - │ - ▼ -编排者 REPL(受限工具集) - │ - ├──→ Agent({ subagent_type: "worker", prompt: "..." }) - │ │ - │ ▼ - │ Worker Agent(完整工具集) - │ ├── 执行任务(Bash/Read/Edit/...) - │ └── 返回 - │ - ├──→ SendMessage({ to: "agent-id", message: "..." }) - │ │ - │ ▼ - │ 继续已存在的 Worker - │ - └──→ TaskStop({ task_id: "agent-id" }) - │ - ▼ - 停止运行中的 Worker -``` - -## 四、关键设计决策 - -1. **双开关设计**:feature flag 控制代码可用性,环境变量控制实际激活。允许编译时包含但不默认启用 -2. **编排者受限**:只能用 Agent/SendMessage/TaskStop,确保编排者专注于派发而非执行 -3. **Worker 不可见编排者对话**:每个 worker 的 prompt 必须自包含(所有必要上下文) -4. **并行优先**:系统提示强调"Parallelism is your superpower",鼓励并行派发独立任务 -5. **综合而非转发**:编排者必须理解 worker 发现,再写出具体的实现指令。禁止 "based on your findings" 类懒惰委托 -6. **Scratchpad 可选共享**:通过 GrowthBook 门控的共享目录,让 worker 之间持久化共享知识 - -## 五、使用方式 - -```bash -# 基本启用 -FEATURE_COORDINATOR_MODE=1 CLAUDE_CODE_COORDINATOR_MODE=1 bun run dev - -# 配合 Fork Subagent -FEATURE_COORDINATOR_MODE=1 FEATURE_FORK_SUBAGENT=1 \ -CLAUDE_CODE_COORDINATOR_MODE=1 bun run dev - -# Simple 模式(worker 只有 Bash/Read/Edit) -FEATURE_COORDINATOR_MODE=1 CLAUDE_CODE_COORDINATOR_MODE=1 \ -CLAUDE_CODE_SIMPLE=1 bun run dev -``` - -## 六、文件索引 - -| 文件 | 行数 | 职责 | -|------|------|------| -| `src/coordinator/coordinatorMode.ts` | 370 | 模式检测 + 系统提示 + 用户上下文 | -| `src/coordinator/workerAgent.ts` | — | Worker agent 定义(stub) | -| `src/constants/tools.ts` | — | `ASYNC_AGENT_ALLOWED_TOOLS` 工具白名单 | diff --git a/docs/features/daemon-restructure-design.md b/docs/features/daemon-restructure-design.md deleted file mode 100644 index 8d0d3abd8..000000000 --- a/docs/features/daemon-restructure-design.md +++ /dev/null @@ -1,318 +0,0 @@ -# Daemon 重构设计方案 - -> 分支: `feat/integrate-5-branches` -> 基于: `f41745cb` (= main `11bb3f62` 内容) -> 日期: 2026-04-13 - -## 一、问题概述 - -### 1.1 命令结构散乱 - -当前后台进程相关的命令分布在三个不同的位置,没有统一的命名空间: - -| 命令 | 注册位置 | 入口 | -|------|---------|------| -| `claude daemon start/status/stop` | `cli.tsx` 快速路径 L203 | `daemon/main.ts` | -| `claude ps` | `cli.tsx` 快速路径 L220 | `cli/bg.ts` | -| `claude logs ` | `cli.tsx` 快速路径 L232 | `cli/bg.ts` | -| `claude attach ` | `cli.tsx` 快速路径 L236 | `cli/bg.ts` | -| `claude kill ` | `cli.tsx` 快速路径 L238 | `cli/bg.ts` | -| `claude --bg` | `cli.tsx` 快速路径 L244 | `cli/bg.ts` | -| `claude new/list/reply` | `cli.tsx` 快速路径 L250 | `cli/handlers/templateJobs.ts` | -| `claude rollback` | `main.tsx` Commander.js L6525 | `cli/rollback.ts` | -| `claude up` | `main.tsx` Commander.js L6511 | `cli/up.ts` | - -**问题**: -- `ps/logs/attach/kill` 与 `daemon` 逻辑上都是后台进程管理,但互不关联 -- 这些命令都**只有 CLI 入口**,REPL 里输入 `/daemon` 或 `/ps` 不存在 -- `new/list/reply` 是模板任务系统的顶级命令,容易与其他命令冲突(特别是 `list`) - -### 1.2 Windows 不支持 - -`--bg` 和 `attach` 硬依赖 tmux: -- `bg.ts:handleBgFlag()` 第一步就检查 tmux,不可用直接报错退出 -- `bg.ts:attachHandler()` 用 `tmux attach-session`,无 tmux 替代方案 -- Windows (包括 VS Code 终端) 完全无法使用后台会话功能 - -### 1.3 无 REPL 入口 - -对比 `/mcp` 的双注册模式: -- **CLI**: `claude mcp serve/add/remove/list` (Commander.js, `main.tsx:5760`) -- **REPL**: `/mcp enable/disable/reconnect` (slash command, `commands/mcp/index.ts`) - -`daemon`/`bg`/`job` 系列只有 CLI 快速路径,REPL 中完全不可用。 - -## 二、目标 - -1. **层级化命令结构**: 参照 `/mcp` 模式,将后台管理收归 `/daemon`,模板任务收归 `/job` -2. **跨平台后台会话**: Windows / macOS / Linux 都能启动、附着、终止后台会话 -3. **双注册**: CLI (`claude daemon ...`) + REPL (`/daemon ...`) 同时可用 -4. **向后兼容**: 旧命令保留但输出 deprecation 提示 - -## 三、命令结构设计 - -### 3.1 `/daemon` — 后台进程管理 - -合并 daemon supervisor + bg sessions 为统一命名空间: - -``` -claude daemon ← CLI 入口 (cli.tsx 快速路径) -/daemon ← REPL 入口 (slash command, local-jsx) - -子命令: - status 综合状态面板 (daemon + 所有会话) - start [--dir ] 启动 daemon supervisor - stop 停止 daemon - bg [args...] 启动后台会话 - attach [target] 附着到后台会话 - logs [target] 查看会话日志 - kill [target] 终止会话 - (无参数) 等同于 status -``` - -**CLI 快速路径路由** (`cli.tsx`): -```typescript -// 新: 统一入口 -if (feature('DAEMON') && args[0] === 'daemon') { - const sub = args[1] || 'status' - switch (sub) { - case 'start': case 'stop': case 'status': - await daemonMain([sub, ...args.slice(2)]) - break - case 'bg': - await bg.handleBgStart(args.slice(2)) - break - case 'attach': case 'logs': case 'kill': - await bg[`${sub}Handler`](args[2]) - break - } -} - -// 向后兼容 (deprecated) -if (feature('BG_SESSIONS') && ['ps','logs','attach','kill'].includes(args[0])) { - console.warn(`[deprecated] Use: claude daemon ${args[0] === 'ps' ? 'status' : args[0]}`) - // ... delegate to daemon subcommand -} -``` - -**REPL 斜杠命令** (`commands/daemon/index.ts`): -```typescript -const daemon = { - type: 'local-jsx', - name: 'daemon', - description: 'Manage background sessions and daemon', - argumentHint: '[status|start|stop|bg|attach|logs|kill]', - isEnabled: () => feature('DAEMON') || feature('BG_SESSIONS'), - load: () => import('./daemon.js'), -} satisfies Command -``` - -### 3.2 `/job` — 模板任务管理 - -``` -claude job ← CLI 入口 -/job ← REPL 入口 - -子命令: - list 列出模板和活跃任务 - new