mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 21:05:51 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b32dd4549d | ||
|
|
0135ad99ad | ||
|
|
9018c7afdb | ||
|
|
65d7f1994c | ||
|
|
ce2f19cc48 | ||
|
|
f6fe94463e | ||
|
|
c57f5a29e8 | ||
|
|
8f6800f508 | ||
|
|
722d59b6d5 | ||
|
|
b51b2d7675 | ||
|
|
975b4876cc | ||
|
|
04c8ef2ecc | ||
|
|
b759df5b0e |
10
README.md
10
README.md
@@ -1,16 +1,22 @@
|
||||
# Claude Code V1
|
||||
# Claude Code Best V1 (CCB)
|
||||
|
||||
Anthropic 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现。
|
||||
Anthropic 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现。虽然很难绷, 但是它叫做 CCB(踩踩背)...
|
||||
|
||||
> V1 会完成跑通及基本的类型检查通过;
|
||||
>
|
||||
> V2 会完整实现工程化配套设施;
|
||||
>
|
||||
> V3 会实现多层级解耦, 很多比如 UI 包, Agent 包都可以独立优化;
|
||||
>
|
||||
> V4 会完成大量的测试文件, 以提高稳定性
|
||||
>
|
||||
> 我不知道这个项目还会存在多久, fork 不好使, git clone 或者下载 .zip 包才稳健;
|
||||
>
|
||||
> 这个项目更新很快, 后台有 Opus 持续优化, 所以你可以提 issues, 但是 PR 暂时不会接受;
|
||||
>
|
||||
> 存活记录:
|
||||
> 开源后 12 小时: 愚人节, star 破 1k, 并且牢 A 没有发邮件搞这个项目
|
||||
>
|
||||
> 如果你想要私人咨询服务, 那么可以发送邮件到 claude-code-best@proton.me, 备注咨询与联系方式即可; 由于后续工作非常多, 可能会忽略邮件, 半天没回复, 可以多发;
|
||||
|
||||
## 快速开始
|
||||
|
||||
21
SECURITY.md
Normal file
21
SECURITY.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
||||
8
TODO.md
8
TODO.md
@@ -10,10 +10,10 @@
|
||||
- [x] `color-diff-napi` — 颜色差异计算 NAPI 模块 (纯 TS 实现)
|
||||
- [x] `image-processor-napi` — 图像处理 NAPI 模块 (sharp + osascript 剪贴板)
|
||||
|
||||
<!-- - [ ] `@ant/computer-use-swift` — Computer Use Swift 原生模块
|
||||
- [ ] `@ant/computer-use-mcp` — Computer Use MCP 服务
|
||||
- [ ] `@ant/computer-use-input` — Computer Use 输入模块
|
||||
- [ ] `@ant/claude-for-chrome-mcp` — Chrome MCP 扩展 -->
|
||||
- [x] `@ant/computer-use-swift` — Computer Use Swift 原生模块 (macOS JXA/screencapture 实现)
|
||||
- [x] `@ant/computer-use-mcp` — Computer Use MCP 服务 (类型安全 stub + sentinel apps + targetImageSize)
|
||||
- [x] `@ant/computer-use-input` — Computer Use 输入模块 (macOS AppleScript/JXA 实现)
|
||||
<!-- - [ ] `@ant/claude-for-chrome-mcp` — Chrome MCP 扩展 -->
|
||||
|
||||
## 工程化能力
|
||||
|
||||
|
||||
38
biome.json
38
biome.json
@@ -9,9 +9,10 @@
|
||||
"includes": ["**", "!!**/dist", "!!**/packages/@ant"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": false,
|
||||
"indentStyle": "tab",
|
||||
"lineWidth": 120
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
@@ -75,11 +76,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"json": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"semicolons": "asNeeded",
|
||||
"arrowParentheses": "asNeeded",
|
||||
"trailingCommas": "all"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["**/*.tsx"],
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"semicolons": "always"
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"lineWidth": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["scripts/**", "packages/**", "**/*.js", "**/*.mjs", "**/*.jsx"],
|
||||
"formatter": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"assist": {
|
||||
"enabled": false
|
||||
}
|
||||
|
||||
24
bun.lock
24
bun.lock
@@ -4,7 +4,7 @@
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "claude-code",
|
||||
"dependencies": {
|
||||
"devDependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||
"@ant/claude-for-chrome-mcp": "workspace:*",
|
||||
"@ant/computer-use-input": "workspace:*",
|
||||
@@ -23,6 +23,7 @@
|
||||
"@aws-sdk/credential-provider-node": "^3.972.28",
|
||||
"@aws-sdk/credential-providers": "^3.1020.0",
|
||||
"@azure/identity": "^4.13.1",
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@commander-js/extra-typings": "^14.0.0",
|
||||
"@growthbook/growthbook": "^1.6.5",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
@@ -46,6 +47,13 @@
|
||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||
"@smithy/core": "^3.23.13",
|
||||
"@smithy/node-http-handler": "^4.5.1",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@types/cacache": "^20.0.1",
|
||||
"@types/plist": "^3.0.5",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-reconciler": "^0.33.0",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"ajv": "^8.18.0",
|
||||
"asciichart": "^1.5.25",
|
||||
"audio-capture-napi": "workspace:*",
|
||||
@@ -74,6 +82,7 @@
|
||||
"image-processor-napi": "workspace:*",
|
||||
"indent-string": "^5.0.0",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"knip": "^6.1.1",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lru-cache": "^11.2.7",
|
||||
"marked": "^17.0.5",
|
||||
@@ -96,6 +105,7 @@
|
||||
"tree-kill": "^1.2.2",
|
||||
"turndown": "^7.2.2",
|
||||
"type-fest": "^5.5.0",
|
||||
"typescript": "^6.0.2",
|
||||
"undici": "^7.24.6",
|
||||
"url-handler-napi": "workspace:*",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
@@ -108,18 +118,6 @@
|
||||
"yaml": "^2.8.3",
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@types/cacache": "^20.0.1",
|
||||
"@types/plist": "^3.0.5",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-reconciler": "^0.33.0",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"knip": "^6.1.1",
|
||||
"typescript": "^6.0.2",
|
||||
},
|
||||
},
|
||||
"packages/@ant/claude-for-chrome-mcp": {
|
||||
"name": "@ant/claude-for-chrome-mcp",
|
||||
|
||||
58
docs/agent/coordinator-and-swarm.mdx
Normal file
58
docs/agent/coordinator-and-swarm.mdx
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: "协调者与蜂群"
|
||||
description: "从单兵作战到团队协作——多 Agent 的高级编排模式"
|
||||
---
|
||||
|
||||
{/* 本章目标:介绍 Coordinator Mode 和 Agent Swarms */}
|
||||
|
||||
## 两种协作模式
|
||||
|
||||
子 Agent 是"临时帮手"——主 Agent 派出去做一件事就回来。对于更复杂的协作需求,Claude Code 提供了两种高级模式:
|
||||
|
||||
## Coordinator Mode:一个指挥,多个执行
|
||||
|
||||
就像一个团队 leader 带着几个开发者:
|
||||
|
||||
- **Coordinator**(协调者):负责理解需求、拆解任务、分配工作、汇总结果
|
||||
- **Workers**(执行者):各自领取任务独立执行,通过邮箱向 Coordinator 汇报
|
||||
|
||||
```
|
||||
┌─── Worker A (重构 API)
|
||||
│
|
||||
Coordinator ──┼─── Worker B (更新测试)
|
||||
│
|
||||
└─── Worker C (更新文档)
|
||||
```
|
||||
|
||||
Coordinator 不自己写代码,它的职责是**编排**——确保所有 Worker 的工作能拼合在一起。
|
||||
|
||||
## Agent Swarms:蜂群式协作
|
||||
|
||||
比 Coordinator 更松散的协作模式:
|
||||
|
||||
- 多个 Agent 以对等身份同时工作
|
||||
- 没有中心化的指挥者
|
||||
- 通过消息邮箱互相通信和协调
|
||||
- 适合"各自负责一块、偶尔需要沟通"的场景
|
||||
|
||||
## Teammate 机制
|
||||
|
||||
进程内的"队友"——一种更轻量的协作方式:
|
||||
|
||||
- 在同一个进程内运行,共享部分基础设施状态
|
||||
- 有独立的对话上下文和工具权限
|
||||
- 适合"我需要一个搭档帮忙看看这段代码"的场景
|
||||
|
||||
## 任务类型
|
||||
|
||||
支撑多 Agent 协作的是丰富的任务类型:
|
||||
|
||||
| 任务类型 | 用途 |
|
||||
|----------|------|
|
||||
| **LocalAgentTask** | 本地子 Agent 任务 |
|
||||
| **LocalShellTask** | 后台 shell 命令 |
|
||||
| **InProcessTeammateTask** | 进程内队友 |
|
||||
| **RemoteAgentTask** | 远程 Agent |
|
||||
| **DreamTask** | 后台自主任务 |
|
||||
|
||||
每种任务类型都有自己的生命周期管理、状态追踪和通信方式。
|
||||
69
docs/agent/sub-agents.mdx
Normal file
69
docs/agent/sub-agents.mdx
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: "子 Agent:分身术"
|
||||
description: "当一个 AI 不够用时,它会召唤更多的自己"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释子 Agent 机制的设计和应用场景 */}
|
||||
|
||||
## 为什么需要子 Agent
|
||||
|
||||
有些任务太大,一个 AI 实例忙不过来:
|
||||
|
||||
- "在 5 个不同的文件中分别找到并修复同类 bug"
|
||||
- "一边重构后端 API,一边更新前端调用"
|
||||
- "研究这个库的用法,同时修改我们的代码"
|
||||
|
||||
## 分身术的运作方式
|
||||
|
||||
Claude Code 中的 Agent 工具让 AI 能够**启动另一个 AI 实例**来处理子任务:
|
||||
|
||||
<Steps>
|
||||
<Step title="主 Agent 分析任务">
|
||||
主 Agent 判断任务可以被拆解为独立的子任务
|
||||
</Step>
|
||||
<Step title="启动子 Agent">
|
||||
通过 Agent 工具创建一个或多个子 Agent,每个子 Agent 收到一个清晰的子任务描述
|
||||
</Step>
|
||||
<Step title="并行执行">
|
||||
多个子 Agent 可以同时工作,互不干扰
|
||||
</Step>
|
||||
<Step title="结果汇总">
|
||||
子 Agent 完成后,结果返回给主 Agent,主 Agent 汇总并呈现给用户
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 子 Agent 的边界
|
||||
|
||||
子 Agent 不是和主 Agent 完全一样的——它有明确的能力边界:
|
||||
|
||||
| 特性 | 主 Agent | 子 Agent |
|
||||
|------|---------|---------|
|
||||
| 可用工具 | 全部工具 | 受限子集(不能再启动子 Agent 等) |
|
||||
| 上下文 | 完整的会话历史 | 只有主 Agent 给的任务描述 |
|
||||
| 权限 | 用户设定 | 继承主 Agent 的权限,或更严格 |
|
||||
| 状态 | 可修改全局状态 | 隔离的状态空间 |
|
||||
|
||||
## 通信方式
|
||||
|
||||
主 Agent 和子 Agent 之间通过**消息邮箱**通信:
|
||||
|
||||
- 主 Agent 通过 `Agent` 工具启动子 Agent
|
||||
- 子 Agent 通过 `SendMessage` 工具向主 Agent 报告进度
|
||||
- 这种松耦合的通信方式让 Agent 可以异步协作
|
||||
|
||||
## 适用场景
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="并行研究" icon="magnifying-glass">
|
||||
多个子 Agent 同时搜索不同方向的信息
|
||||
</Card>
|
||||
<Card title="分治修改" icon="code-branch">
|
||||
把大规模修改拆分到多个子 Agent 并行执行
|
||||
</Card>
|
||||
<Card title="前后台配合" icon="layer-group">
|
||||
一个子 Agent 在后台运行测试,主 Agent 继续写代码
|
||||
</Card>
|
||||
<Card title="隔离实验" icon="flask">
|
||||
在 worktree 中启动子 Agent 尝试一个方案,不影响主分支
|
||||
</Card>
|
||||
</CardGroup>
|
||||
55
docs/agent/worktree-isolation.mdx
Normal file
55
docs/agent/worktree-isolation.mdx
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: "Worktree 隔离"
|
||||
description: "给子 Agent 一个独立的工作空间,互不污染"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释 git worktree 在多 Agent 协作中的作用 */}
|
||||
|
||||
## 问题:多个 Agent 改同一份代码
|
||||
|
||||
当多个 Agent 同时修改项目文件时,冲突在所难免:
|
||||
|
||||
- Agent A 修改了 `config.ts`,Agent B 也在改同一个文件
|
||||
- Agent A 的测试需要某个状态,Agent B 的修改破坏了它
|
||||
- 半完成的修改混在一起,无法分辨哪些是哪个 Agent 做的
|
||||
|
||||
## 解决方案:Git Worktree
|
||||
|
||||
Git 原生支持 **worktree**(工作树)——在同一个仓库中创建多个独立的工作目录,每个目录在自己的分支上独立工作。
|
||||
|
||||
Claude Code 利用这个特性为子 Agent 提供隔离的工作空间:
|
||||
|
||||
| | 共享工作目录 | Worktree 隔离 |
|
||||
|---|---|---|
|
||||
| 文件冲突 | 多个 Agent 可能互相覆盖 | 每个 Agent 在自己的目录中工作 |
|
||||
| 分支 | 都在同一个分支上 | 每个 Agent 有自己的分支 |
|
||||
| 测试 | 互相干扰 | 完全独立 |
|
||||
| 合并 | 需要手动处理冲突 | 通过 git merge 有序合并 |
|
||||
|
||||
## 工作流程
|
||||
|
||||
<Steps>
|
||||
<Step title="创建 Worktree">
|
||||
AI 启动带隔离模式的子 Agent,系统自动在 `.claude/worktrees/` 下创建新的工作目录
|
||||
</Step>
|
||||
<Step title="独立工作">
|
||||
子 Agent 在自己的 worktree 中自由修改文件、执行命令
|
||||
</Step>
|
||||
<Step title="完成任务">
|
||||
子 Agent 完成后,变更留在 worktree 的分支上
|
||||
</Step>
|
||||
<Step title="合并或丢弃">
|
||||
主 Agent(或用户)决定:合并这些变更到主分支,还是丢弃
|
||||
</Step>
|
||||
<Step title="清理">
|
||||
不再需要的 worktree 会被自动清理
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 安全网
|
||||
|
||||
Worktree 还充当了一个安全网:
|
||||
|
||||
- 子 Agent 的实验性修改不会影响主分支
|
||||
- 如果方案不可行,整个 worktree 可以直接丢弃
|
||||
- 多个方案可以在不同 worktree 中并行尝试,最后选最好的
|
||||
63
docs/context/compaction.mdx
Normal file
63
docs/context/compaction.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "上下文压缩"
|
||||
description: "对话太长怎么办——优雅地'遗忘'不重要的信息"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释 Compaction 机制的设计和策略 */}
|
||||
|
||||
## 为什么需要压缩
|
||||
|
||||
每次 API 调用的 token 有上限(通常 200K)。一场长时间的编程对话可能产生:
|
||||
|
||||
- 大量的文件内容(AI 读了几十个文件)
|
||||
- 长篇的命令输出(构建日志、测试结果)
|
||||
- 往返的对话历史
|
||||
|
||||
不压缩的话,很快就会撞到 token 上限,对话被迫终止。
|
||||
|
||||
## 压缩的策略
|
||||
|
||||
Claude Code 提供了多层压缩机制:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="自动压缩">
|
||||
当 token 接近上限时,系统自动触发压缩。AI 生成一份当前对话的**摘要**,替换掉早期的详细消息。效果就像人类的"记忆"——记住要点,忘记细节。
|
||||
</Accordion>
|
||||
<Accordion title="手动压缩">
|
||||
用户可以随时通过 `/compact` 命令主动触发压缩。可以附带提示语(如 `/compact 聚焦在认证模块的修改上`),引导 AI 保留特定信息。
|
||||
</Accordion>
|
||||
<Accordion title="Micro Compact">
|
||||
更细粒度的局部压缩——不是压缩整个对话,而是压缩某些特别长的工具输出(比如一个 5000 行的测试日志)。
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 压缩边界
|
||||
|
||||
压缩后,系统在消息历史中插入一个"边界标记"。后续的 API 调用只发送边界之后的消息:
|
||||
|
||||
```
|
||||
[早期的 50 条消息] ← 被压缩
|
||||
[压缩摘要边界] ← 一段浓缩的摘要
|
||||
[后续的 10 条消息] ← 正常发送
|
||||
```
|
||||
|
||||
这个设计保证了:
|
||||
- 压缩后的摘要为 AI 提供了历史上下文
|
||||
- 新的对话不受旧消息的 token 负担
|
||||
- 用户无感知——对话继续自然进行
|
||||
|
||||
## 压缩前后的 Hooks
|
||||
|
||||
压缩是一个可能丢失信息的操作,因此系统允许用户在压缩前后执行自定义脚本:
|
||||
|
||||
- **Pre-compact Hook**:压缩前执行,可以标记"这些信息不能丢"
|
||||
- **Post-compact Hook**:压缩后执行,可以验证关键信息是否保留
|
||||
|
||||
## 什么信息会被保留
|
||||
|
||||
压缩不是简单的截断,AI 会智能地决定保留什么:
|
||||
|
||||
- 用户的核心需求和目标
|
||||
- 重要的决策和原因
|
||||
- 当前工作的状态(改了哪些文件、做到哪一步)
|
||||
- 之前犯过的错误(避免重蹈覆辙)
|
||||
58
docs/context/project-memory.mdx
Normal file
58
docs/context/project-memory.mdx
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: "项目记忆"
|
||||
description: "让 AI 跨对话记住你的偏好和项目上下文"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释记忆系统如何让 AI 变得'有记忆' */}
|
||||
|
||||
## AI 的记忆困境
|
||||
|
||||
大语言模型没有真正的记忆。每次新对话,它都是一张白纸。用户不得不反复解释"我的项目用 Bun 不用 Node"、"commit 消息用中文"。
|
||||
|
||||
## 记忆系统的解决方案
|
||||
|
||||
Claude Code 通过一个基于文件的持久化记忆系统来模拟"跨会话记忆":
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="用户记忆" icon="user">
|
||||
关于用户的信息:角色、偏好、技术背景
|
||||
</Card>
|
||||
<Card title="反馈记忆" icon="message">
|
||||
用户对 AI 行为的纠正和肯定
|
||||
</Card>
|
||||
<Card title="项目记忆" icon="folder">
|
||||
项目中的非代码信息:谁负责什么、截止日期
|
||||
</Card>
|
||||
<Card title="参考记忆" icon="link">
|
||||
外部资源的位置:Issue tracker、Dashboard URL
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 记忆的读写时机
|
||||
|
||||
| 时机 | 动作 |
|
||||
|------|------|
|
||||
| 每次对话开始 | 加载记忆索引(MEMORY.md),相关记忆注入 System Prompt |
|
||||
| 用户纠正 AI | AI 自动判断是否值得记住,写入反馈记忆 |
|
||||
| 用户说"记住这个" | 立即保存到对应类型的记忆文件 |
|
||||
| 用户说"忘掉这个" | 找到并删除对应的记忆条目 |
|
||||
| 记忆可能过期时 | 使用前先验证(文件还在?函数还存在?),过期则更新或删除 |
|
||||
|
||||
## 记忆 vs 代码注释 vs CLAUDE.md
|
||||
|
||||
| | 记忆 | 代码注释 | CLAUDE.md |
|
||||
|---|---|---|---|
|
||||
| 存储位置 | `~/.claude/` 目录 | 代码文件中 | 项目目录中 |
|
||||
| 谁能看到 | 只有当前用户 | 所有开发者 | 所有使用 Claude Code 的人 |
|
||||
| 适合存什么 | 个人偏好、非公开的上下文 | 代码逻辑解释 | 项目约定、开发指南 |
|
||||
| 跨项目 | 是 | 否 | 否 |
|
||||
|
||||
## 不该存什么
|
||||
|
||||
记忆系统明确规定了不应存储的内容:
|
||||
|
||||
- 代码结构和架构(读代码就知道)
|
||||
- git 历史(`git log` 就能查)
|
||||
- 调试方案(修复已在代码中)
|
||||
- CLAUDE.md 里已有的内容(避免重复)
|
||||
- 临时性任务状态(用任务系统)
|
||||
53
docs/context/system-prompt.mdx
Normal file
53
docs/context/system-prompt.mdx
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: "System Prompt 的动态组装"
|
||||
description: "AI 的'工作记忆'是如何在每次对话前被精心拼装的"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释 System Prompt 的组装过程和设计思想 */}
|
||||
|
||||
## 什么是 System Prompt
|
||||
|
||||
每次调用 AI API 时,都需要发送一个 System Prompt——它是 AI 的"人设说明书",告诉 AI:
|
||||
|
||||
- 你是谁(Claude Code,一个编程助手)
|
||||
- 你能做什么(可用工具列表)
|
||||
- 你在什么环境(操作系统、当前目录、git 状态)
|
||||
- 你需要遵守什么规则(安全规范、输出格式)
|
||||
|
||||
## 不是静态模板,而是动态组装
|
||||
|
||||
Claude Code 的 System Prompt 不是一段写死的文本,而是根据当前环境**实时组装**的:
|
||||
|
||||
| 组成部分 | 内容 | 来源 |
|
||||
|----------|------|------|
|
||||
| 基础人设 | 角色定义、行为准则 | 内置模板 |
|
||||
| 环境信息 | 操作系统、shell 类型、当前日期 | 运行时检测 |
|
||||
| Git 状态 | 当前分支、最近提交、工作区状态 | `git` 命令输出 |
|
||||
| 项目知识 | CLAUDE.md 文件内容 | 项目目录层级扫描 |
|
||||
| 记忆文件 | 用户偏好、项目约定 | 持久化记忆系统 |
|
||||
| 工具说明 | 每个可用工具的描述和参数 | 工具注册表 |
|
||||
|
||||
## CLAUDE.md:项目级知识注入
|
||||
|
||||
这是 Claude Code 最巧妙的设计之一。在项目根目录放一个 `CLAUDE.md` 文件,就能让 AI "理解" 你的项目:
|
||||
|
||||
- **项目概述**:这个项目做什么、用了什么技术栈
|
||||
- **开发约定**:代码风格、命名规范、分支策略
|
||||
- **常用命令**:怎么构建、怎么测试、怎么部署
|
||||
- **注意事项**:已知的坑、特殊的配置
|
||||
|
||||
系统会自动发现并合并多级 CLAUDE.md:
|
||||
|
||||
```
|
||||
~/.claude/CLAUDE.md ← 用户全局(个人偏好)
|
||||
└── /project/CLAUDE.md ← 项目根目录(团队共享)
|
||||
└── /project/src/CLAUDE.md ← 子目录(模块特定)
|
||||
```
|
||||
|
||||
## 缓存策略
|
||||
|
||||
System Prompt 的 token 消耗不小(可能占总量的 30%+)。为了降低成本,系统使用了缓存机制:
|
||||
|
||||
- 不变的部分(基础人设、工具说明)可以跨请求复用
|
||||
- 变化的部分(git 状态、记忆文件)每次重新生成
|
||||
- 缓存节点的位置经过精心设计,最大化缓存命中率
|
||||
55
docs/context/token-budget.mdx
Normal file
55
docs/context/token-budget.mdx
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: "Token 预算管理"
|
||||
description: "精打细算每一个 token——AI 的'注意力'是有限资源"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释 token 预算管理的思路 */}
|
||||
|
||||
## Token 是什么
|
||||
|
||||
简单理解:token 约等于一个英文单词或半个中文字。AI 处理的所有输入和输出都按 token 计费。
|
||||
|
||||
| 类型 | 说明 | 谁付费 |
|
||||
|------|------|--------|
|
||||
| 输入 token | 发给 AI 的所有内容(System Prompt + 对话历史 + 工具结果) | 用户 |
|
||||
| 输出 token | AI 生成的回复和工具调用 | 用户 |
|
||||
| 缓存 token | 重复发送的内容如果命中缓存,价格更低 | 部分用户 |
|
||||
|
||||
## 预算控制的三个层面
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="单次请求" icon="1">
|
||||
每次 API 调用的最大输入/输出 token
|
||||
</Card>
|
||||
<Card title="单轮对话" icon="arrows-rotate">
|
||||
一个 Agentic Loop 内的累计 token 消耗
|
||||
</Card>
|
||||
<Card title="整个会话" icon="clock">
|
||||
全部对话轮次的累计花费(美元)
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 工具输出的预算控制
|
||||
|
||||
工具返回的内容可能非常长(一个大文件、一段长日志),直接全部塞给 AI 会浪费大量 token。系统对此有专门的控制:
|
||||
|
||||
- **结果截断**:超过长度限制的工具输出自动截断
|
||||
- **结果替换**:已经被 AI"消化"过的旧工具结果,可以被替换为简短的摘要
|
||||
- **按需读取**:大文件不一次性读完,AI 可以指定读取范围
|
||||
|
||||
## 缓存的经济学
|
||||
|
||||
System Prompt 每次都要发送,但大部分内容不变。缓存机制让这部分"免费"(或大幅降价):
|
||||
|
||||
- 首次发送:全价
|
||||
- 后续请求命中缓存:约 1/10 的价格
|
||||
- 这就是为什么 System Prompt 的结构被精心设计——不变的部分放前面,变化的部分放后面
|
||||
|
||||
## token 警告与自动压缩
|
||||
|
||||
| token 使用率 | 系统行为 |
|
||||
|-------------|---------|
|
||||
| < 70% | 正常运行 |
|
||||
| 70% ~ 90% | 显示警告,提示用户可以手动压缩 |
|
||||
| > 90% | 自动触发压缩 |
|
||||
| 接近 100% | 强制压缩或终止当前轮次 |
|
||||
59
docs/conversation/multi-turn.mdx
Normal file
59
docs/conversation/multi-turn.mdx
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: "多轮对话管理"
|
||||
description: "一场跨越数小时的编程对话是如何被管理的"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释会话编排、持久化、成本追踪 */}
|
||||
|
||||
## 单轮 vs 多轮
|
||||
|
||||
- **单轮**(一次 Agentic Loop):用户说一句 → AI 执行一系列操作 → 回答
|
||||
- **多轮**(一个 Session):用户和 AI 来回对话几十轮,持续数小时
|
||||
|
||||
多轮对话带来的挑战远超单轮:消息越来越多、token 不断累积、上下文逐渐模糊。
|
||||
|
||||
## 会话编排器的职责
|
||||
|
||||
在单轮 Agentic Loop 之上,有一个编排器负责管理整个会话生命周期:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="对话状态管理" icon="database">
|
||||
维护完整的消息历史,包括用户消息、AI 回复、工具调用结果
|
||||
</Card>
|
||||
<Card title="会话持久化" icon="floppy-disk">
|
||||
自动保存对话记录到磁盘,支持断线重连、历史回顾
|
||||
</Card>
|
||||
<Card title="文件快照" icon="camera">
|
||||
在 AI 修改文件前自动保存快照,支持回滚
|
||||
</Card>
|
||||
<Card title="成本追踪" icon="calculator">
|
||||
精确记录每轮的 token 消耗和 API 费用
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 会话恢复
|
||||
|
||||
意外退出?网络断了?没关系:
|
||||
|
||||
- 每轮对话结束后,完整的 transcript 会被写入磁盘
|
||||
- 下次启动时,可以选择恢复之前的对话
|
||||
- 恢复时,系统重建消息历史和上下文状态
|
||||
|
||||
## 成本感知
|
||||
|
||||
AI 编程助手的一个现实问题是**费用可能失控**。Claude Code 内建了多层成本控制:
|
||||
|
||||
| 机制 | 作用 |
|
||||
|------|------|
|
||||
| Token 计数器 | 实时显示本次会话已消耗的输入/输出 token |
|
||||
| 费用估算 | 根据模型定价计算累计美元花费 |
|
||||
| 预算上限 | 用户可设定最大花费,到达后自动停止 |
|
||||
| 压缩提醒 | Token 接近上限时提示用户触发压缩 |
|
||||
|
||||
## 模型切换
|
||||
|
||||
在一个会话中,用户可以随时切换模型或调整参数:
|
||||
|
||||
- `/model` 切换到不同的模型(Sonnet / Opus / Haiku)
|
||||
- `/fast` 切换快速模式
|
||||
- 模型切换不会丢失对话历史
|
||||
50
docs/conversation/streaming.mdx
Normal file
50
docs/conversation/streaming.mdx
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: "流式响应:逐字呈现"
|
||||
description: "为什么 Claude Code 的回答是'打字机效果'而不是一整块弹出"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释流式通信的意义和它如何与工具执行交织 */}
|
||||
|
||||
## 为什么需要流式
|
||||
|
||||
想象 AI 需要 30 秒才能生成完整回答——如果等 30 秒后才一次性显示,用户体验是灾难性的。
|
||||
|
||||
流式响应让用户**实时看到 AI 的思考过程**:
|
||||
- 文字逐字出现,用户能提前判断方向是否正确
|
||||
- 工具调用的参数在生成过程中就能预览
|
||||
- 长时间任务不会让用户觉得"卡死了"
|
||||
|
||||
## 流式与工具调用的交织
|
||||
|
||||
一次 AI 响应中可能同时包含文字和工具调用。流式系统需要处理这种交织:
|
||||
|
||||
```
|
||||
AI 开始输出文字 → "我来看看这个文件的内容..."
|
||||
AI 发出工具调用 → [FileRead: src/main.ts]
|
||||
↓ 暂停流式输出
|
||||
工具执行中...
|
||||
结果返回给 AI
|
||||
↓ 恢复流式输出
|
||||
AI 继续输出 → "这个文件里有一个 bug,第 42 行..."
|
||||
AI 发出下一个工具调用 → [FileEdit: src/main.ts]
|
||||
```
|
||||
|
||||
## 流式工具执行
|
||||
|
||||
更进阶的是,**工具本身的执行也可以是流式的**:
|
||||
|
||||
- 运行一个长命令(比如 `npm install`),输出逐行显示
|
||||
- 搜索大型项目时,匹配结果逐条呈现
|
||||
- AI 在等待工具结果的同时,已经开始规划下一步
|
||||
|
||||
## 多 Provider 适配
|
||||
|
||||
Claude Code 支持多个 AI 服务提供商,每个提供商的流式协议略有不同:
|
||||
|
||||
| Provider | 特点 |
|
||||
|----------|------|
|
||||
| Anthropic Direct | 原生 SSE 流,延迟最低 |
|
||||
| AWS Bedrock | 通过 AWS SDK 包装的流式接口 |
|
||||
| Google Vertex | gRPC 流转换为事件流 |
|
||||
|
||||
系统通过统一的事件抽象层屏蔽这些差异——上层代码不需要关心底层用的是哪个 Provider。
|
||||
66
docs/conversation/the-loop.mdx
Normal file
66
docs/conversation/the-loop.mdx
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: "Agentic Loop:对话的心跳"
|
||||
description: "AI 不只回答问题,它会反复思考-行动-观察,直到任务完成"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释 Agentic Loop 这个核心机制 */}
|
||||
|
||||
## 什么是 Agentic Loop
|
||||
|
||||
传统聊天机器人:你问一句,它答一句。
|
||||
Claude Code 不一样:你说一个需求,它可能连续执行十几步操作才给你最终结果。
|
||||
|
||||
这背后的机制叫做 **Agentic Loop**(智能体循环):
|
||||
|
||||
{/* TODO: 插入 Loop 示意图 */}
|
||||
|
||||
<Steps>
|
||||
<Step title="思考">
|
||||
AI 分析当前上下文,决定下一步该做什么
|
||||
</Step>
|
||||
<Step title="行动">
|
||||
AI 发出工具调用请求(比如"读取这个文件"、"执行这条命令")
|
||||
</Step>
|
||||
<Step title="观察">
|
||||
工具执行完毕,结果回传给 AI
|
||||
</Step>
|
||||
<Step title="循环或结束">
|
||||
AI 根据观察结果决定:继续下一步操作,还是任务已完成、直接回答用户
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 一个真实的例子
|
||||
|
||||
> 用户:"帮我找到项目里所有未使用的导入语句,然后删掉它们"
|
||||
|
||||
AI 的内部过程:
|
||||
|
||||
1. **思考**:我需要先了解项目结构 → **行动**:调用 Glob 工具扫描所有源文件
|
||||
2. **观察**:拿到文件列表 → **思考**:逐个检查 → **行动**:调用 Grep 搜索 import 语句
|
||||
3. **观察**:发现 3 个文件有未使用导入 → **行动**:调用 FileEdit 逐个删除
|
||||
4. **观察**:编辑成功 → **结束**:告诉用户"已清理 3 个文件中的 5 条未使用导入"
|
||||
|
||||
整个过程可能涉及 10+ 次工具调用,但用户只需要说一句话。
|
||||
|
||||
## 为什么不是"一次规划,批量执行"
|
||||
|
||||
<Note>
|
||||
一个常见的替代方案是:AI 先生成一个完整的计划,然后一次性执行所有步骤。Claude Code 选择了逐步循环,原因是:
|
||||
</Note>
|
||||
|
||||
- **每一步都能看到真实结果**:文件内容、命令输出、错误信息——这些只有执行后才知道
|
||||
- **动态调整**:如果第 3 步发现了意外情况,AI 可以立刻改变策略
|
||||
- **错误恢复**:某步失败了,AI 可以当场诊断和修复,不需要推倒重来
|
||||
- **用户可控**:用户可以在任何一步中断,AI 的循环不会失控
|
||||
|
||||
## 循环的终止条件
|
||||
|
||||
Agentic Loop 不会无限运行,以下情况会让循环停止:
|
||||
|
||||
| 条件 | 说明 |
|
||||
|------|------|
|
||||
| AI 主动结束 | AI 判断任务完成,返回纯文本回答(不再调用工具) |
|
||||
| 用户中断 | 用户按 Ctrl+C 或 ESC 打断当前操作 |
|
||||
| Token 预算耗尽 | 单轮对话的 token 用量达到上限 |
|
||||
| 输出过长自动续写 | AI 回复被截断时,系统自动发起续写请求(有次数上限) |
|
||||
| 成本上限 | 累计 API 花费超过用户设定的预算 |
|
||||
56
docs/extensibility/custom-agents.mdx
Normal file
56
docs/extensibility/custom-agents.mdx
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: "自定义 Agent"
|
||||
description: "定义你自己的 AI 角色——不同的人格、能力和边界"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释自定义 Agent 定义的设计 */}
|
||||
|
||||
## 为什么需要自定义 Agent
|
||||
|
||||
默认的 Claude Code 是一个"全能型"助手。但有些场景需要更专门化的角色:
|
||||
|
||||
- 一个只负责代码审查、不会修改代码的 Agent
|
||||
- 一个专门处理 DevOps 任务的 Agent
|
||||
- 一个面向初学者、回答更详细的 Agent
|
||||
|
||||
## Agent 定义
|
||||
|
||||
自定义 Agent 通过 Markdown 文件定义,放在 `.claude/agents/` 目录:
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|--------|------|
|
||||
| **名称** | Agent 的标识和显示名 |
|
||||
| **描述** | 这个 Agent 的职责说明 |
|
||||
| **System Prompt** | 自定义的角色指令——替换或追加到默认 System Prompt |
|
||||
| **允许的工具** | 这个 Agent 可以使用哪些工具 |
|
||||
| **模型** | 使用哪个 AI 模型 |
|
||||
|
||||
## 与子 Agent 的关系
|
||||
|
||||
自定义 Agent 可以作为子 Agent 被启动:
|
||||
|
||||
- 主 Agent 说"这个任务需要安全审查"
|
||||
- 系统启动一个自定义的"安全审查 Agent"
|
||||
- 该 Agent 只有阅读权限,使用专门的安全审查 Prompt
|
||||
|
||||
这实现了**角色分离**——不同的任务由不同"人格"的 Agent 处理。
|
||||
|
||||
## 复用与共享
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="项目级" icon="folder">
|
||||
放在项目的 `.claude/agents/` 目录,团队所有人可用
|
||||
</Card>
|
||||
<Card title="用户级" icon="user">
|
||||
放在 `~/.claude/agents/` 目录,跨项目可用
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 实际应用
|
||||
|
||||
| Agent | 角色 | 工具限制 |
|
||||
|-------|------|---------|
|
||||
| `reviewer` | 代码审查员 | 只允许 Read、Glob、Grep |
|
||||
| `devops` | DevOps 工程师 | 允许 Bash,限制在 infra/ 目录 |
|
||||
| `tutor` | 编程导师 | 全部工具,但 Prompt 强调教学 |
|
||||
| `security` | 安全审计员 | 只允许搜索和阅读,输出安全报告 |
|
||||
72
docs/extensibility/hooks.mdx
Normal file
72
docs/extensibility/hooks.mdx
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: "Hooks:生命周期钩子"
|
||||
description: "在 AI 的关键行为节点插入你自己的逻辑"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释 Hooks 系统的设计和应用场景 */}
|
||||
|
||||
## 什么是 Hooks
|
||||
|
||||
Hooks 是用户定义的 shell 命令,在 Claude Code 生命周期的特定时刻自动执行。
|
||||
|
||||
类比:React 的 `useEffect` 让你在组件渲染后执行自定义逻辑。Claude Code 的 Hooks 让你在 AI 的关键行为前后执行自定义脚本。
|
||||
|
||||
## 可用的 Hook 事件
|
||||
|
||||
| 事件 | 触发时机 | 典型用途 |
|
||||
|------|---------|---------|
|
||||
| **PreToolUse** | 工具调用前 | 拦截危险操作、自定义审批逻辑 |
|
||||
| **PostToolUse** | 工具调用后 | 记录日志、触发通知、自动格式化 |
|
||||
| **PreCompact** | 上下文压缩前 | 标记不可丢失的信息 |
|
||||
| **PostCompact** | 上下文压缩后 | 验证关键信息是否保留 |
|
||||
| **Notification** | AI 发出通知时 | 自定义通知渠道(Slack、邮件等) |
|
||||
| **StopFailure** | AI 循环异常停止时 | 自定义错误处理 |
|
||||
|
||||
## Hook 的能力
|
||||
|
||||
Hook 脚本不仅能"观察",还能"干预":
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="拦截操作" icon="hand">
|
||||
返回特定信号可以阻止工具调用执行
|
||||
</Card>
|
||||
<Card title="修改行为" icon="pen">
|
||||
返回结构化的 JSON 输出,影响 Claude Code 的后续行为
|
||||
</Card>
|
||||
<Card title="注入上下文" icon="syringe">
|
||||
向 AI 的对话中注入额外信息
|
||||
</Card>
|
||||
<Card title="触发外部流程" icon="bolt">
|
||||
调用 CI/CD、发送通知、更新 Issue tracker
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 配置方式
|
||||
|
||||
Hooks 在 `settings.json` 中配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": { "tool_name": "Write" },
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "npx prettier --write $CLAUDE_FILE_PATH"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个例子:每当 AI 写入一个文件后,自动用 Prettier 格式化。
|
||||
|
||||
## 安全控制
|
||||
|
||||
- 托管设置(企业管理员)的 Hooks 优先级最高,用户不能覆盖
|
||||
- Hook 执行有超时限制
|
||||
- Hook 的输出会被解析和验证,防止注入攻击
|
||||
75
docs/extensibility/mcp-protocol.mdx
Normal file
75
docs/extensibility/mcp-protocol.mdx
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: "MCP:开放的工具生态"
|
||||
description: "通过标准协议对接任何外部能力——数据库、API、自定义服务"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释 MCP 协议如何扩展 AI 的能力边界 */}
|
||||
|
||||
## 内置工具的局限
|
||||
|
||||
Claude Code 内置了 50+ 工具,覆盖了通用的软件开发需求。但每个团队都有特殊需求:
|
||||
|
||||
- 连接内部数据库查询数据
|
||||
- 调用公司内部 API
|
||||
- 操作特定的 DevOps 工具
|
||||
- 访问私有的知识库
|
||||
|
||||
不可能把所有人的需求都内置进去。
|
||||
|
||||
## MCP:一个标准的"插头"
|
||||
|
||||
**Model Context Protocol**(模型上下文协议)是 Anthropic 提出的开放标准,定义了 AI 与外部工具之间的通信方式。
|
||||
|
||||
类比:USB 是电脑连接外设的标准接口。MCP 是 AI 连接外部能力的标准接口。
|
||||
|
||||
## 工作原理
|
||||
|
||||
<Steps>
|
||||
<Step title="启动 MCP Server">
|
||||
开发者编写一个 MCP Server,暴露自定义工具(比如"查询数据库"、"发送 Slack 消息")
|
||||
</Step>
|
||||
<Step title="Claude Code 连接">
|
||||
在配置文件中声明要连接的 MCP Server
|
||||
</Step>
|
||||
<Step title="工具自动发现">
|
||||
连接后,MCP Server 提供的工具自动出现在 AI 的可用工具列表中
|
||||
</Step>
|
||||
<Step title="透明调用">
|
||||
AI 像使用内置工具一样使用 MCP 工具——无需知道底层实现
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 三种连接方式
|
||||
|
||||
| 方式 | 适用场景 |
|
||||
|------|---------|
|
||||
| **stdio** | MCP Server 作为子进程运行,通过标准输入/输出通信。最简单 |
|
||||
| **SSE** | 通过 HTTP Server-Sent Events 通信。适合远程服务 |
|
||||
| **StreamableHTTP** | 基于 HTTP 流的双向通信。适合复杂的交互场景 |
|
||||
|
||||
## 权限一视同仁
|
||||
|
||||
MCP 提供的工具和内置工具一样受权限系统管控:
|
||||
|
||||
- 需要用户确认才能调用
|
||||
- 可以设置 allow/deny 规则
|
||||
- 支持沙箱限制
|
||||
|
||||
这确保了第三方工具不会绕过安全边界。
|
||||
|
||||
## 实际例子
|
||||
|
||||
```json
|
||||
// settings.json 中的 MCP 配置
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-database": {
|
||||
"command": "npx",
|
||||
"args": ["@my-org/db-mcp-server"],
|
||||
"env": { "DB_URL": "postgres://..." }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
配置后,AI 就多了"查询数据库"这个能力——用自然语言描述需求,AI 自动生成查询并执行。
|
||||
60
docs/extensibility/skills.mdx
Normal file
60
docs/extensibility/skills.mdx
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: "Skills:预制的能力包"
|
||||
description: "把常用的工作流封装成可复用的技能"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释 Skills 系统的设计思想 */}
|
||||
|
||||
## Tool vs Skill
|
||||
|
||||
| | Tool | Skill |
|
||||
|---|---|---|
|
||||
| 粒度 | 单个原子操作(读文件、执行命令) | 一套完整的工作流(代码审查、创建 PR) |
|
||||
| 触发方式 | AI 自主选择 | 用户主动调用(`/skill-name`)或 AI 根据场景推荐 |
|
||||
| 本质 | 执行逻辑 | 预制的 Prompt + 工具权限配置 |
|
||||
|
||||
## Skill 的三个来源
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="内置 Skill" icon="box">
|
||||
编译进 CLI 的技能包。如 `/commit`、`/review`、`/debug`
|
||||
</Card>
|
||||
<Card title="项目 Skill" icon="folder-open">
|
||||
项目 `.claude/skills/` 目录中的 Markdown 文件。团队共享
|
||||
</Card>
|
||||
<Card title="MCP Skill" icon="plug">
|
||||
通过 MCP Server 提供的技能。动态发现
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 一个 Skill 包含什么
|
||||
|
||||
每个 Skill 本质上是一个"AI 行为的预设":
|
||||
|
||||
| 组成部分 | 作用 |
|
||||
|----------|------|
|
||||
| **名称和描述** | 告诉 AI 和用户这个技能做什么 |
|
||||
| **whenToUse** | 什么场景下应该使用这个技能(AI 据此自动推荐) |
|
||||
| **Prompt 模板** | 注入给 AI 的详细指令——相当于"操作手册" |
|
||||
| **allowedTools** | 这个技能允许使用哪些工具(能力边界) |
|
||||
| **model** | 可选指定使用的模型 |
|
||||
|
||||
## 设计精妙之处
|
||||
|
||||
Skill 的核心洞见是:**很多复杂任务的关键不在于代码逻辑,而在于 Prompt 的质量**。
|
||||
|
||||
一个好的代码审查,不是写了什么代码来审查,而是:
|
||||
- 告诉 AI 审查的标准是什么
|
||||
- 告诉 AI 按什么顺序审查
|
||||
- 告诉 AI 输出什么格式的报告
|
||||
- 限制 AI 只能用读取类工具(不要边审查边改代码)
|
||||
|
||||
Skill 把这些"经验"封装起来,任何人都能一键调用。
|
||||
|
||||
## 技能发现
|
||||
|
||||
当可用技能很多时,AI 可以通过 **SkillTool** 搜索匹配的技能:
|
||||
|
||||
- 用户说"帮我做代码审查"
|
||||
- AI 搜索已注册的技能,发现 `code-review` 匹配
|
||||
- AI 调用该技能,按预设的流程执行
|
||||
59
docs/introduction/architecture-overview.mdx
Normal file
59
docs/introduction/architecture-overview.mdx
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: "架构全景"
|
||||
description: "五层架构,一条数据流"
|
||||
---
|
||||
|
||||
{/* 本章目标:一张图讲清楚整体架构,为后续章节建立坐标系 */}
|
||||
|
||||
## 五层架构
|
||||
|
||||
Claude Code 从上到下分为五个层次,每一层职责清晰、边界分明:
|
||||
|
||||
| 层次 | 职责 | 关键词 |
|
||||
|------|------|--------|
|
||||
| **交互层** | 终端 UI、用户输入、消息展示 | React/Ink、REPL |
|
||||
| **编排层** | 管理多轮对话、会话生命周期、成本追踪 | QueryEngine、会话持久化 |
|
||||
| **核心循环层** | 单轮对话:发请求 → 拿响应 → 执行工具 → 再发请求 | Agentic Loop |
|
||||
| **工具层** | AI 的"双手"——读写文件、执行命令、搜索代码 | Tool System、MCP |
|
||||
| **通信层** | 与 Claude API 的流式通信、多云 Provider 适配 | Streaming、Bedrock/Vertex |
|
||||
|
||||
## 一条主数据流
|
||||
|
||||
{/* TODO: 插入数据流序列图 */}
|
||||
|
||||
整个系统的运转可以浓缩为一条核心数据流:
|
||||
|
||||
<Steps>
|
||||
<Step title="用户输入">
|
||||
用户在终端键入自然语言需求
|
||||
</Step>
|
||||
<Step title="上下文组装">
|
||||
系统自动拼接项目信息、git 状态、配置文件、记忆,形成完整的 System Prompt
|
||||
</Step>
|
||||
<Step title="API 调用">
|
||||
将 System Prompt + 对话历史发送给 Claude API,流式接收响应
|
||||
</Step>
|
||||
<Step title="工具调用循环">
|
||||
若 AI 返回工具调用请求 → 权限检查 → 执行工具 → 结果回传 → AI 继续思考 → 循环
|
||||
</Step>
|
||||
<Step title="任务完成">
|
||||
AI 不再调用工具,输出最终回答,等待用户下一条输入
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 四个核心设计原则
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="流式优先 (Streaming-first)">
|
||||
所有 API 通信都是流式的——用户看到 AI "逐字打出"回答,而不是等待完整响应。工具执行结果也实时反馈。这不仅提升体验,更是处理长时间任务的工程必需。
|
||||
</Accordion>
|
||||
<Accordion title="工具即能力 (Tool as Capability)">
|
||||
AI 的每一项"动手能力"都被抽象为一个 Tool。想让 AI 能做新事情?注册一个新 Tool 就好。统一的接口让能力可插拔、可组合。
|
||||
</Accordion>
|
||||
<Accordion title="权限即边界 (Permission as Boundary)">
|
||||
AI 能操作真实环境是强大的,也是危险的。每个工具调用都经过权限系统的裁决——该放行的自动放行,该拦截的坚决拦截。
|
||||
</Accordion>
|
||||
<Accordion title="上下文即记忆 (Context as Memory)">
|
||||
AI 没有真正的记忆,但通过精心的 System Prompt 组装、对话压缩、持久化记忆文件,系统营造出"AI 理解你的项目"的效果。这是上下文工程的核心。
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
37
docs/introduction/what-is-claude-code.mdx
Normal file
37
docs/introduction/what-is-claude-code.mdx
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "什么是 Claude Code"
|
||||
description: "一个住在终端里的 AI 编程搭档"
|
||||
---
|
||||
|
||||
{/* 本章目标:让完全不了解 Claude Code 的读者在 3 分钟内建立直觉 */}
|
||||
|
||||
## 一句话定义
|
||||
|
||||
Claude Code 是一个运行在命令行终端里的 AI 编程助手。你用自然语言描述需求,它直接帮你读代码、改文件、跑命令、搜索项目——全部在你的本地环境中完成。
|
||||
|
||||
## 它能做什么
|
||||
|
||||
- **对话式编程**:像和同事聊天一样描述需求,AI 直接动手实现
|
||||
- **理解整个项目**:自动读取项目结构、git 历史、配置文件,建立项目全景认知
|
||||
- **操作你的电脑**:读写文件、执行 shell 命令、搜索代码——不只是给建议,而是真正动手
|
||||
- **保护你的安全**:每个敏感操作都需要你确认,有沙箱、有权限管控
|
||||
|
||||
## 它和 ChatGPT / 普通聊天机器人的区别
|
||||
|
||||
| | 普通聊天 AI | Claude Code |
|
||||
|---|---|---|
|
||||
| 运行环境 | 云端网页 | 你的本地终端 |
|
||||
| 能做什么 | 回答问题、生成文本 | 直接操作你的项目文件和命令行 |
|
||||
| 项目理解 | 你需要手动粘贴代码 | 自动读取整个项目上下文 |
|
||||
| 安全性 | 无需考虑 | 多层权限保护 |
|
||||
|
||||
## 一次典型的交互流程
|
||||
|
||||
{/* TODO: 插入一张交互流程示意图 */}
|
||||
|
||||
1. 你在终端输入自然语言需求
|
||||
2. Claude Code 分析你的项目上下文
|
||||
3. 它决定使用哪些工具(读文件?执行命令?)
|
||||
4. 每个操作请求你确认(或根据规则自动放行)
|
||||
5. 执行完成后,把结果反馈给 AI,AI 决定下一步
|
||||
6. 循环,直到任务完成
|
||||
40
docs/introduction/why-this-whitepaper.mdx
Normal file
40
docs/introduction/why-this-whitepaper.mdx
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "为什么写这份白皮书"
|
||||
description: "将 LLM 能力落地到真实工作流的工程范本"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释为什么这个项目的架构值得深入研究 */}
|
||||
|
||||
## 不只是一个聊天工具
|
||||
|
||||
Claude Code 解决的核心问题是:**如何让大语言模型从"能说会道"变成"能说会做"**。
|
||||
|
||||
这不是简单地给 AI 套一个 shell。它涉及一系列精巧的工程决策:
|
||||
|
||||
- AI 说"我要编辑这个文件"时,如何确保安全?
|
||||
- 对话越来越长,token 快爆了怎么办?
|
||||
- AI 需要并行处理多个子任务时,如何隔离和协调?
|
||||
- 用户想扩展 AI 的能力(接数据库、连 API),如何设计插拔机制?
|
||||
|
||||
## 这份白皮书关注什么
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="功能视角" icon="eye">
|
||||
不堆代码,从"用户能做什么、AI 如何决策"出发
|
||||
</Card>
|
||||
<Card title="设计决策" icon="lightbulb">
|
||||
每个功能背后的"为什么这样设计"
|
||||
</Card>
|
||||
<Card title="架构模式" icon="sitemap">
|
||||
可复用的模式:Agentic Loop、工具抽象、上下文工程
|
||||
</Card>
|
||||
<Card title="安全哲学" icon="shield">
|
||||
AI 操作真实环境时的信任与控制平衡
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 适合谁读
|
||||
|
||||
- 想理解 AI Agent 产品架构的开发者
|
||||
- 正在构建类似工具的团队
|
||||
- 对 LLM 应用工程化感兴趣的任何人
|
||||
84
docs/mint.json
Normal file
84
docs/mint.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/schema.json",
|
||||
"name": "Claude Code Architecture",
|
||||
"logo": {
|
||||
"dark": "/logo/dark.svg",
|
||||
"light": "/logo/light.svg"
|
||||
},
|
||||
"favicon": "/favicon.svg",
|
||||
"colors": {
|
||||
"primary": "#D97706",
|
||||
"light": "#F59E0B",
|
||||
"dark": "#B45309",
|
||||
"background": {
|
||||
"dark": "#0F172A",
|
||||
"light": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"navigation": [
|
||||
{
|
||||
"group": "开始",
|
||||
"pages": [
|
||||
"introduction/what-is-claude-code",
|
||||
"introduction/why-this-whitepaper",
|
||||
"introduction/architecture-overview"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "对话是如何运转的",
|
||||
"pages": [
|
||||
"conversation/the-loop",
|
||||
"conversation/streaming",
|
||||
"conversation/multi-turn"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "工具:AI 的双手",
|
||||
"pages": [
|
||||
"tools/what-are-tools",
|
||||
"tools/file-operations",
|
||||
"tools/shell-execution",
|
||||
"tools/search-and-navigation",
|
||||
"tools/task-management"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "安全与权限",
|
||||
"pages": [
|
||||
"safety/why-safety-matters",
|
||||
"safety/permission-model",
|
||||
"safety/sandbox",
|
||||
"safety/plan-mode"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "上下文工程",
|
||||
"pages": [
|
||||
"context/system-prompt",
|
||||
"context/project-memory",
|
||||
"context/compaction",
|
||||
"context/token-budget"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "多 Agent 协作",
|
||||
"pages": [
|
||||
"agent/sub-agents",
|
||||
"agent/worktree-isolation",
|
||||
"agent/coordinator-and-swarm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "可扩展性",
|
||||
"pages": [
|
||||
"extensibility/mcp-protocol",
|
||||
"extensibility/hooks",
|
||||
"extensibility/skills",
|
||||
"extensibility/custom-agents"
|
||||
]
|
||||
}
|
||||
],
|
||||
"footerSocials": {
|
||||
"github": "https://github.com/anthropics/claude-code"
|
||||
}
|
||||
}
|
||||
64
docs/safety/permission-model.mdx
Normal file
64
docs/safety/permission-model.mdx
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: "权限模型"
|
||||
description: "三种行为 x 多级来源 = 灵活而严谨的权限体系"
|
||||
---
|
||||
|
||||
{/* 本章目标:详解权限系统的设计 */}
|
||||
|
||||
## 三种权限行为
|
||||
|
||||
每一次工具调用,系统都会做出三种裁决之一:
|
||||
|
||||
| 行为 | 含义 | 典型场景 |
|
||||
|------|------|---------|
|
||||
| **Allow** | 自动放行,用户无感知 | Read 工具读取项目内的文件 |
|
||||
| **Ask** | 弹出确认对话框,等待用户决定 | Bash 执行一条未知命令 |
|
||||
| **Deny** | 直接拒绝,AI 收到"权限被拒"的反馈 | 尝试执行被禁止的命令 |
|
||||
|
||||
## 权限规则的层级
|
||||
|
||||
规则可以来自多个来源,优先级从高到低:
|
||||
|
||||
<Steps>
|
||||
<Step title="用户会话内设置">
|
||||
用户在当前对话中手动授权的("对这个工具始终允许")
|
||||
</Step>
|
||||
<Step title="项目配置">
|
||||
项目目录中的 `.claude/settings.json`,团队共享
|
||||
</Step>
|
||||
<Step title="用户全局配置">
|
||||
`~/.claude/settings.json`,跨项目生效
|
||||
</Step>
|
||||
<Step title="托管设置">
|
||||
企业管理员下发的策略,用户不可覆盖
|
||||
</Step>
|
||||
<Step title="默认规则">
|
||||
系统内置的基线规则
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 规则的匹配方式
|
||||
|
||||
权限规则不是简单的"允许/禁止某个工具",它支持丰富的匹配条件:
|
||||
|
||||
- **工具名匹配**:`"tool": "Bash"` → 针对所有 Bash 命令
|
||||
- **命令模式匹配**:`"command": "git *"` → 只针对 git 开头的命令
|
||||
- **路径匹配**:`"path": "src/**"` → 只允许操作 src 目录下的文件
|
||||
- **组合条件**:以上条件可以组合使用
|
||||
|
||||
## 权限模式
|
||||
|
||||
系统提供几种预设的权限模式,适应不同信任级别:
|
||||
|
||||
| 模式 | 适用场景 |
|
||||
|------|---------|
|
||||
| **Default** | 日常使用,敏感操作逐一确认 |
|
||||
| **Plan Mode** | 探索阶段,AI 只能读不能写 |
|
||||
| **Bypass** | 完全信任 AI,所有操作自动放行(需显式开启) |
|
||||
|
||||
## Denial Tracking
|
||||
|
||||
系统不仅记录"允许了什么",还追踪"拒绝了什么":
|
||||
|
||||
- 如果 AI 被连续拒绝多次同一类操作,系统会调整策略
|
||||
- 这防止 AI 陷入"反复请求同一个被拒操作"的死循环
|
||||
60
docs/safety/plan-mode.mdx
Normal file
60
docs/safety/plan-mode.mdx
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: "计划模式"
|
||||
description: "先看后做——给 AI 一个只读的探索阶段"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释 Plan Mode 的设计理念 */}
|
||||
|
||||
## 问题场景
|
||||
|
||||
你说"重构这个模块",AI 立刻开始改代码——但你还没搞清楚它打算怎么改。等改了一半发现方向不对,已经来不及了。
|
||||
|
||||
## Plan Mode 的解决方案
|
||||
|
||||
计划模式给对话加了一个"只读阶段":
|
||||
|
||||
<Steps>
|
||||
<Step title="进入计划模式">
|
||||
AI 自主判断任务需要规划(或用户主动触发),进入计划模式
|
||||
</Step>
|
||||
<Step title="探索阶段">
|
||||
在这个阶段,AI 只能使用读取和搜索类工具——不能编辑文件、不能执行命令
|
||||
</Step>
|
||||
<Step title="形成计划">
|
||||
AI 把理解和方案写入计划文件,提交给用户审阅
|
||||
</Step>
|
||||
<Step title="用户审批">
|
||||
用户阅读计划,提出修改意见或直接批准
|
||||
</Step>
|
||||
<Step title="退出计划模式">
|
||||
计划被批准后,AI 恢复全部工具权限,按计划执行
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 权限的自动收窄与恢复
|
||||
|
||||
计划模式的精妙之处在于**自动改变权限上下文**:
|
||||
|
||||
- 进入时:系统自动保存当前权限模式,切换为"只读"
|
||||
- 退出时:系统自动恢复之前的权限模式
|
||||
|
||||
AI 和用户都不需要手动调整权限设置。
|
||||
|
||||
## 什么时候该用计划模式
|
||||
|
||||
| 场景 | 是否需要计划 |
|
||||
|------|-------------|
|
||||
| 修复一个明确的 typo | 不需要,直接修 |
|
||||
| 添加一个简单的函数 | 不需要 |
|
||||
| 重构一个大模块 | 需要——先搞清楚影响范围 |
|
||||
| 涉及多个文件的 feature | 需要——先统一方案 |
|
||||
| 不确定该怎么做 | 需要——让 AI 先调研 |
|
||||
|
||||
## 计划模式 + 任务系统
|
||||
|
||||
计划模式通常与任务系统配合使用:
|
||||
|
||||
1. 在计划模式中,AI 把实施步骤创建为任务列表
|
||||
2. 用户审批计划(包含任务列表)
|
||||
3. 退出计划模式后,AI 按任务列表逐项执行
|
||||
4. 用户可以通过任务列表追踪进度
|
||||
35
docs/safety/sandbox.mdx
Normal file
35
docs/safety/sandbox.mdx
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: "沙箱机制"
|
||||
description: "即使命令被允许执行,也不意味着它可以为所欲为"
|
||||
---
|
||||
|
||||
{/* 本章目标:解释沙箱的作用和原理 */}
|
||||
|
||||
## 权限之外的第二道防线
|
||||
|
||||
权限系统决定"这条命令能不能执行",沙箱决定"执行时能做到什么程度"。
|
||||
|
||||
即使一条命令通过了权限审批,沙箱仍然可以限制它的行为:
|
||||
|
||||
| 限制维度 | 说明 |
|
||||
|----------|------|
|
||||
| **文件系统** | 只能访问项目目录及其子目录 |
|
||||
| **网络** | 可以禁止或限制网络访问 |
|
||||
| **进程** | 限制可启动的子进程 |
|
||||
| **时间** | 超时自动终止 |
|
||||
|
||||
## 何时启用沙箱
|
||||
|
||||
沙箱不是默认对所有命令生效——它根据风险评估动态决定:
|
||||
|
||||
- 用户显式请求禁用沙箱的命令(`dangerouslyDisableSandbox`)不走沙箱
|
||||
- 已通过安全规则白名单的命令可以跳过沙箱
|
||||
- 未知命令或高风险命令强制进入沙箱
|
||||
|
||||
## 沙箱的实现思路
|
||||
|
||||
不同平台使用不同的沙箱技术:
|
||||
|
||||
- **macOS**:利用系统级沙箱机制限制文件和网络访问
|
||||
- **Linux**:基于命名空间和 cgroup 的进程隔离
|
||||
- 沙箱策略由系统自动选择,用户不需要手动配置
|
||||
40
docs/safety/why-safety-matters.mdx
Normal file
40
docs/safety/why-safety-matters.mdx
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: "为什么安全至关重要"
|
||||
description: "当 AI 能操作你的电脑,信任的边界在哪里"
|
||||
---
|
||||
|
||||
{/* 本章目标:建立安全意识,解释为什么需要这么多安全机制 */}
|
||||
|
||||
## AI 动手的代价
|
||||
|
||||
Claude Code 不是在沙盒里回答问题——它在你的真实项目中修改文件、执行命令。一个失误可能意味着:
|
||||
|
||||
- 覆盖了你还没提交的工作
|
||||
- 执行了危险的 `rm -rf` 命令
|
||||
- 推送了包含 bug 的代码到远程仓库
|
||||
- 泄露了 `.env` 文件中的密钥
|
||||
|
||||
## 安全设计的核心理念
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="最小权限原则" icon="lock">
|
||||
AI 默认没有任何"动手"权限,每项能力都需要显式授予
|
||||
</Card>
|
||||
<Card title="可逆优先" icon="rotate-left">
|
||||
优先执行可逆操作(读文件、搜索),对不可逆操作(删除、推送)严格审批
|
||||
</Card>
|
||||
<Card title="人在回路" icon="user">
|
||||
关键操作必须经过人类确认,AI 不能绕过用户自行决定
|
||||
</Card>
|
||||
<Card title="纵深防御" icon="shield-halved">
|
||||
多层安全机制叠加——权限规则、沙箱、计划模式、预算上限——任何一层都能阻止危险操作
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 安全 vs 效率的平衡
|
||||
|
||||
如果每个操作都要确认,AI 就变成了一个不停弹窗的烦人助手。Claude Code 的设计在安全和效率之间找到了平衡:
|
||||
|
||||
- **低风险操作自动放行**:读取文件、搜索代码——这些不会改变任何东西
|
||||
- **中风险操作规则放行**:编辑指定目录的文件——用户可以预设"允许"规则
|
||||
- **高风险操作人工确认**:删除文件、执行未知命令——必须手动审批
|
||||
54
docs/tools/file-operations.mdx
Normal file
54
docs/tools/file-operations.mdx
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: "文件操作"
|
||||
description: "AI 如何安全、高效地读写你的代码"
|
||||
---
|
||||
|
||||
{/* 本章目标:介绍文件类工具的设计理念 */}
|
||||
|
||||
## 读、写、改——三种操作模式
|
||||
|
||||
Claude Code 把文件操作拆分为三个独立工具,而不是一个万能的"文件工具":
|
||||
|
||||
| 工具 | 功能 | 设计考量 |
|
||||
|------|------|---------|
|
||||
| **Read** | 读取文件内容 | 只读操作,权限最低,AI 可以随意使用 |
|
||||
| **Write** | 创建新文件或完全重写 | 高风险操作,需要确认 |
|
||||
| **Edit** | 精确替换文件中的特定片段 | 中等风险,但比 Write 安全——只改你指定的部分 |
|
||||
|
||||
<Tip>
|
||||
为什么 Edit 和 Write 要分开?因为"编辑一行"和"重写整个文件"的风险完全不同。分离后,权限系统可以对它们施加不同的控制策略。
|
||||
</Tip>
|
||||
|
||||
## 文件读取的智慧
|
||||
|
||||
Read 工具不是简单的 `cat` 命令,它有很多精细的设计:
|
||||
|
||||
- **分页读取**:超大文件不会一次性全部读入,支持 offset + limit 指定范围
|
||||
- **多格式支持**:除了文本文件,还能读取图片(多模态展示)、PDF、Jupyter Notebook
|
||||
- **文件状态缓存**:记住已读过的文件内容,避免重复读取浪费 token
|
||||
- **Token 感知**:文件内容计入 token 预算,系统会自动评估是否"读得起"
|
||||
|
||||
## 精确编辑 vs 全量重写
|
||||
|
||||
Edit 工具的核心设计是**精确字符串替换**:
|
||||
|
||||
- AI 指定 `old_string`(要被替换的原文)和 `new_string`(替换后的新文)
|
||||
- 系统确保 `old_string` 在文件中**唯一匹配**——如果匹配到多处或零处,操作失败
|
||||
- 这个设计确保 AI 不会"改错地方"
|
||||
|
||||
## 搜索与导航
|
||||
|
||||
在动手修改之前,AI 通常需要先"找到目标"。两个搜索工具分工明确:
|
||||
|
||||
- **Glob**:按文件名模式搜索("找到所有 `.ts` 文件"),替代 `find` 命令
|
||||
- **Grep**:按文件内容搜索("找到所有包含 `TODO` 的行"),替代 `grep/rg` 命令
|
||||
|
||||
两者都经过优化,能在大型项目中快速返回结果,并自动截断过长的输出。
|
||||
|
||||
## 文件历史快照
|
||||
|
||||
每当 AI 准备修改文件时,系统会自动保存一份快照。这意味着:
|
||||
|
||||
- 用户可以随时回滚到 AI 修改前的状态
|
||||
- 即使 AI 做了错误的编辑,原始内容不会丢失
|
||||
- 快照与 git 互补——git 追踪已提交的变更,快照保护未提交的工作
|
||||
43
docs/tools/search-and-navigation.mdx
Normal file
43
docs/tools/search-and-navigation.mdx
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: "搜索与导航"
|
||||
description: "AI 如何在百万行代码中精准定位目标"
|
||||
---
|
||||
|
||||
{/* 本章目标:介绍搜索类工具和工具搜索机制 */}
|
||||
|
||||
## 两种搜索维度
|
||||
|
||||
| 维度 | 工具 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| **按名称找文件** | Glob | "找到所有测试文件"、"找 config 开头的文件" |
|
||||
| **按内容找代码** | Grep | "哪里定义了这个函数"、"谁在调用这个 API" |
|
||||
|
||||
两者组合使用,AI 就拥有了在大型项目中"导航"的能力。
|
||||
|
||||
## 搜索结果的智能处理
|
||||
|
||||
大型项目的搜索结果可能有成千上万条,直接全部返回不现实:
|
||||
|
||||
- **结果数量限制**:默认最多返回 250 条匹配
|
||||
- **上下文行**:Grep 支持显示匹配行前后的上下文(类似 `grep -C`)
|
||||
- **按修改时间排序**:Glob 默认把最近修改的文件排在前面
|
||||
- **文件类型过滤**:按语言类型过滤(只搜 `.ts` 文件、只搜 `.py` 文件)
|
||||
|
||||
## 工具发现机制
|
||||
|
||||
当可用工具超过 50 个时,AI 可能不知道该用哪个。系统提供了 **ToolSearch** 机制:
|
||||
|
||||
- AI 可以用自然语言描述需求("我需要连接数据库")
|
||||
- 系统在所有已注册工具(包括 MCP 提供的)中搜索匹配
|
||||
- 返回最相关的工具列表及使用说明
|
||||
|
||||
这让 AI 在面对庞大的工具库时不会迷路。
|
||||
|
||||
## Web 搜索与抓取
|
||||
|
||||
AI 的信息获取不局限于本地代码:
|
||||
|
||||
- **WebSearch**:搜索互联网获取最新信息
|
||||
- **WebFetch**:抓取特定网页内容,转换为 Markdown 供 AI 阅读
|
||||
|
||||
这让 AI 可以查阅文档、搜索 Stack Overflow、阅读 GitHub issue——和人类开发者的工作方式一致。
|
||||
53
docs/tools/shell-execution.mdx
Normal file
53
docs/tools/shell-execution.mdx
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: "命令执行"
|
||||
description: "让 AI 在你的终端里运行命令——安全地"
|
||||
---
|
||||
|
||||
{/* 本章目标:介绍 Bash 工具的能力与安全设计 */}
|
||||
|
||||
## AI 能执行命令意味着什么
|
||||
|
||||
这是 Claude Code 最强大也最敏感的能力。AI 可以:
|
||||
|
||||
- 运行构建命令(`npm run build`、`cargo build`)
|
||||
- 执行测试(`pytest`、`jest`)
|
||||
- 使用 git(`git status`、`git commit`)
|
||||
- 调用系统工具(`curl`、`docker`、`kubectl`)
|
||||
|
||||
几乎你在终端里能做的事,AI 都能做。
|
||||
|
||||
## 安全设计
|
||||
|
||||
强大的能力需要严格的控制:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="权限确认">
|
||||
默认情况下,每条命令执行前都需要用户手动确认。用户可以设置白名单规则,让特定命令自动放行。
|
||||
</Accordion>
|
||||
<Accordion title="沙箱隔离">
|
||||
在支持的平台上,命令可以运行在沙箱环境中——限制文件系统访问范围、禁止网络请求、阻止危险操作。
|
||||
</Accordion>
|
||||
<Accordion title="超时控制">
|
||||
每条命令都有超时限制(默认 2 分钟,最长 10 分钟),防止 AI 启动一个永远不会结束的进程。
|
||||
</Accordion>
|
||||
<Accordion title="输出截断">
|
||||
命令输出过长时自动截断,避免把海量日志全部塞进 AI 的上下文。
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## 前台与后台
|
||||
|
||||
有些命令需要等待结果(比如 `git status`),有些适合在后台运行(比如 `npm install`):
|
||||
|
||||
- **前台执行**:AI 等待命令完成,拿到输出后继续思考
|
||||
- **后台执行**:命令在后台运行,AI 可以继续做其他事,稍后再检查结果
|
||||
|
||||
## 为什么用专用工具而不是直接调 shell
|
||||
|
||||
<Note>
|
||||
Claude Code 为文件读写、代码搜索等操作提供了专用工具(Read、Grep、Glob),而不是让 AI 用 `cat`、`grep` 等 shell 命令。原因有三:
|
||||
</Note>
|
||||
|
||||
1. **权限粒度更细**:`Read` 是只读操作可以自动放行,但 `Bash: cat file` 需要审批整条命令
|
||||
2. **输出结构化**:专用工具的返回值是结构化的,方便 UI 渲染(高亮、diff 视图等)
|
||||
3. **性能优化**:专用工具可以做缓存、分页、token 预算控制,shell 命令做不到
|
||||
50
docs/tools/task-management.mdx
Normal file
50
docs/tools/task-management.mdx
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: "任务管理"
|
||||
description: "让 AI 的工作有条理、可追踪"
|
||||
---
|
||||
|
||||
{/* 本章目标:介绍任务系统如何帮助 AI 和用户保持同步 */}
|
||||
|
||||
## 为什么需要任务管理
|
||||
|
||||
当你给 AI 一个复杂需求(比如"重构整个认证模块"),它可能需要执行几十个步骤。没有任务管理,用户只能被动等待,不知道 AI 做到哪了、还要做什么。
|
||||
|
||||
## 任务系统的运作方式
|
||||
|
||||
AI 可以自主创建和管理任务列表:
|
||||
|
||||
<Steps>
|
||||
<Step title="分解任务">
|
||||
AI 把大需求拆解为多个小任务,创建到任务列表
|
||||
</Step>
|
||||
<Step title="标记进度">
|
||||
开始某个任务时标记为"进行中",完成后标记为"已完成"
|
||||
</Step>
|
||||
<Step title="依赖管理">
|
||||
任务之间可以设定依赖关系——"任务 B 必须等任务 A 完成后才能开始"
|
||||
</Step>
|
||||
<Step title="用户可见">
|
||||
用户随时可以查看任务列表,了解整体进度
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## 任务与 Plan Mode 的配合
|
||||
|
||||
面对复杂任务,AI 可以先进入**计划模式**:
|
||||
|
||||
1. AI 进入计划模式 → 只允许使用搜索和阅读类工具(不能修改文件)
|
||||
2. AI 探索代码库、理解现有架构
|
||||
3. AI 制定实施计划,创建任务列表
|
||||
4. 用户审批计划
|
||||
5. AI 退出计划模式,按计划逐项执行
|
||||
|
||||
这种"先规划、后执行"的方式避免了 AI 盲目行动造成的返工。
|
||||
|
||||
## 状态展示
|
||||
|
||||
终端 UI 中,任务列表会实时更新:
|
||||
|
||||
- 待办任务灰色显示
|
||||
- 进行中的任务有旋转动画
|
||||
- 已完成的任务打勾标记
|
||||
- 被阻塞的任务标注依赖项
|
||||
74
docs/tools/what-are-tools.mdx
Normal file
74
docs/tools/what-are-tools.mdx
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: "工具:AI 的双手"
|
||||
description: "理解 Tool 这个核心抽象——AI 是怎么从'说'变成'做'的"
|
||||
---
|
||||
|
||||
{/* 本章目标:让读者理解 Tool 抽象的设计思想 */}
|
||||
|
||||
## AI 为什么需要工具
|
||||
|
||||
大语言模型本质上只能做一件事:**根据输入文本,生成输出文本**。
|
||||
|
||||
它不能读文件、不能执行命令、不能搜索代码。要让 AI 真正"动手",需要一个桥梁——这就是 **Tool**(工具)。
|
||||
|
||||
工具是 AI 的双手。AI 说"我想读这个文件",工具系统替它真正去读;AI 说"我想执行这条命令",工具系统替它真正去跑。
|
||||
|
||||
## 一个工具长什么样
|
||||
|
||||
每个工具都是一个标准化的"能力单元",包含四个要素:
|
||||
|
||||
| 要素 | 说明 | 示例(FileRead 工具) |
|
||||
|------|------|----------------------|
|
||||
| **名称** | 工具的唯一标识 | `Read` |
|
||||
| **描述** | 告诉 AI 这个工具能做什么(AI 据此决定是否使用) | "读取本地文件系统中的文件" |
|
||||
| **参数定义** | 工具接受什么输入 | `file_path`(必填)、`offset`、`limit` |
|
||||
| **执行逻辑** | 工具被调用时实际做什么 | 读取文件内容并返回 |
|
||||
|
||||
## AI 如何选择工具
|
||||
|
||||
AI 不是从下拉菜单里选工具——它是根据**工具描述**和**当前任务**自主决策的:
|
||||
|
||||
1. 系统把所有可用工具的名称、描述、参数告诉 AI
|
||||
2. AI 在思考过程中决定"我需要用某个工具"
|
||||
3. AI 生成一个结构化的工具调用请求(工具名 + 参数)
|
||||
4. 系统执行工具,将结果返回给 AI
|
||||
|
||||
<Note>
|
||||
工具描述的质量直接影响 AI 的决策准确性。一段好的描述不仅说明"能做什么",还说明"什么时候该用、什么时候不该用"。
|
||||
</Note>
|
||||
|
||||
## 50+ 内置工具
|
||||
|
||||
Claude Code 内置了覆盖软件开发全流程的工具集:
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="文件操作" icon="file">
|
||||
Read / Write / Edit / Glob / Grep / NotebookEdit
|
||||
</Card>
|
||||
<Card title="命令执行" icon="terminal">
|
||||
Bash / PowerShell
|
||||
</Card>
|
||||
<Card title="对话管理" icon="comments">
|
||||
Agent / SendMessage / AskUserQuestion
|
||||
</Card>
|
||||
<Card title="任务追踪" icon="list-check">
|
||||
TaskCreate / TaskUpdate / TaskList / TaskGet
|
||||
</Card>
|
||||
<Card title="Web 能力" icon="globe">
|
||||
WebFetch / WebSearch
|
||||
</Card>
|
||||
<Card title="规划与版本" icon="map">
|
||||
EnterPlanMode / Worktree / TodoWrite
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## 工具的可视化渲染
|
||||
|
||||
工具不仅能"做事",还能"展示"。每个工具可以定义自己的 UI 渲染方式:
|
||||
|
||||
- **FileEdit** → 在终端里展示语法高亮的 diff 视图
|
||||
- **Bash** → 实时显示命令输出,带进度指示
|
||||
- **Grep** → 高亮匹配结果,显示文件路径和行号链接
|
||||
- **Agent** → 显示子 Agent 的进度条和状态
|
||||
|
||||
这让用户能直观地看到"AI 在做什么、做到哪了"。
|
||||
300
package.json
300
package.json
@@ -1,140 +1,164 @@
|
||||
{
|
||||
"name": "claude-js",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"packages/@ant/*"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "bun build src/entrypoints/cli.tsx --outdir dist --target bun",
|
||||
"dev": "bun run src/entrypoints/cli.tsx",
|
||||
"prepublishOnly": "bun run build",
|
||||
"lint": "biome lint src/",
|
||||
"lint:fix": "biome lint --fix src/",
|
||||
"format": "biome format --write src/",
|
||||
"prepare": "git config core.hooksPath .githooks",
|
||||
"test": "bun test",
|
||||
"check:unused": "knip-bun",
|
||||
"health": "bun run scripts/health-check.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||
"@ant/claude-for-chrome-mcp": "workspace:*",
|
||||
"@ant/computer-use-input": "workspace:*",
|
||||
"@ant/computer-use-mcp": "workspace:*",
|
||||
"@ant/computer-use-swift": "workspace:*",
|
||||
"@anthropic-ai/bedrock-sdk": "^0.26.4",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
|
||||
"@anthropic-ai/foundry-sdk": "^0.2.3",
|
||||
"@anthropic-ai/mcpb": "^2.1.2",
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.44",
|
||||
"@anthropic-ai/sdk": "^0.80.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
||||
"@aws-sdk/client-bedrock": "^3.1020.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
|
||||
"@aws-sdk/client-sts": "^3.1020.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.28",
|
||||
"@aws-sdk/credential-providers": "^3.1020.0",
|
||||
"@azure/identity": "^4.13.1",
|
||||
"@commander-js/extra-typings": "^14.0.0",
|
||||
"@growthbook/growthbook": "^1.6.5",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@opentelemetry/api": "^1.9.1",
|
||||
"@opentelemetry/api-logs": "^0.214.0",
|
||||
"@opentelemetry/core": "^2.6.1",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
|
||||
"@opentelemetry/exporter-logs-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.214.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.214.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.214.0",
|
||||
"@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/resources": "^2.6.1",
|
||||
"@opentelemetry/sdk-logs": "^0.214.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.6.1",
|
||||
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||
"@smithy/core": "^3.23.13",
|
||||
"@smithy/node-http-handler": "^4.5.1",
|
||||
"ajv": "^8.18.0",
|
||||
"asciichart": "^1.5.25",
|
||||
"audio-capture-napi": "workspace:*",
|
||||
"auto-bind": "^5.0.1",
|
||||
"axios": "^1.14.0",
|
||||
"bidi-js": "^1.0.3",
|
||||
"cacache": "^20.0.4",
|
||||
"chalk": "^5.6.2",
|
||||
"chokidar": "^5.0.0",
|
||||
"cli-boxes": "^4.0.1",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"code-excerpt": "^4.0.0",
|
||||
"color-diff-napi": "workspace:*",
|
||||
"diff": "^8.0.4",
|
||||
"emoji-regex": "^10.6.0",
|
||||
"env-paths": "^4.0.0",
|
||||
"execa": "^9.6.1",
|
||||
"fflate": "^0.8.2",
|
||||
"figures": "^6.1.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"get-east-asian-width": "^1.5.0",
|
||||
"google-auth-library": "^10.6.2",
|
||||
"highlight.js": "^11.11.1",
|
||||
"https-proxy-agent": "^8.0.0",
|
||||
"ignore": "^7.0.5",
|
||||
"image-processor-napi": "workspace:*",
|
||||
"indent-string": "^5.0.0",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lru-cache": "^11.2.7",
|
||||
"marked": "^17.0.5",
|
||||
"modifiers-napi": "workspace:*",
|
||||
"p-map": "^7.0.4",
|
||||
"picomatch": "^4.0.4",
|
||||
"plist": "^3.1.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.4",
|
||||
"react-compiler-runtime": "^1.0.0",
|
||||
"react-reconciler": "^0.33.0",
|
||||
"semver": "^7.7.4",
|
||||
"sharp": "^0.34.5",
|
||||
"shell-quote": "^1.8.3",
|
||||
"signal-exit": "^4.1.0",
|
||||
"stack-utils": "^2.0.6",
|
||||
"strip-ansi": "^7.2.0",
|
||||
"supports-hyperlinks": "^4.4.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"turndown": "^7.2.2",
|
||||
"type-fest": "^5.5.0",
|
||||
"undici": "^7.24.6",
|
||||
"url-handler-napi": "workspace:*",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"vscode-languageserver-types": "^3.17.5",
|
||||
"wrap-ansi": "^10.0.0",
|
||||
"ws": "^8.20.0",
|
||||
"xss": "^1.0.15",
|
||||
"yaml": "^2.8.3",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@types/cacache": "^20.0.1",
|
||||
"@types/plist": "^3.0.5",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-reconciler": "^0.33.0",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"knip": "^6.1.1",
|
||||
"typescript": "^6.0.2"
|
||||
}
|
||||
"name": "claude-js",
|
||||
"version": "1.0.1",
|
||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||
"type": "module",
|
||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/claude-code-best/claude-code.git"
|
||||
},
|
||||
"homepage": "https://github.com/claude-code-best/claude-code#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/claude-code-best/claude-code/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
"anthropic",
|
||||
"cli",
|
||||
"ai",
|
||||
"coding-assistant",
|
||||
"terminal",
|
||||
"repl"
|
||||
],
|
||||
"engines": {
|
||||
"bun": ">=1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"claude-js": "dist/cli.js"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"packages/@ant/*"
|
||||
],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "bun build src/entrypoints/cli.tsx --outdir dist --target bun",
|
||||
"dev": "bun run src/entrypoints/cli.tsx",
|
||||
"prepublishOnly": "bun run build",
|
||||
"lint": "biome lint src/",
|
||||
"lint:fix": "biome lint --fix src/",
|
||||
"format": "biome format --write src/",
|
||||
"prepare": "git config core.hooksPath .githooks",
|
||||
"test": "bun test",
|
||||
"check:unused": "knip-bun",
|
||||
"health": "bun run scripts/health-check.ts"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||
"@ant/claude-for-chrome-mcp": "workspace:*",
|
||||
"@ant/computer-use-input": "workspace:*",
|
||||
"@ant/computer-use-mcp": "workspace:*",
|
||||
"@ant/computer-use-swift": "workspace:*",
|
||||
"@anthropic-ai/bedrock-sdk": "^0.26.4",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
|
||||
"@anthropic-ai/foundry-sdk": "^0.2.3",
|
||||
"@anthropic-ai/mcpb": "^2.1.2",
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.44",
|
||||
"@anthropic-ai/sdk": "^0.80.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
||||
"@aws-sdk/client-bedrock": "^3.1020.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
|
||||
"@aws-sdk/client-sts": "^3.1020.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.28",
|
||||
"@aws-sdk/credential-providers": "^3.1020.0",
|
||||
"@azure/identity": "^4.13.1",
|
||||
"@commander-js/extra-typings": "^14.0.0",
|
||||
"@growthbook/growthbook": "^1.6.5",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@opentelemetry/api": "^1.9.1",
|
||||
"@opentelemetry/api-logs": "^0.214.0",
|
||||
"@opentelemetry/core": "^2.6.1",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
|
||||
"@opentelemetry/exporter-logs-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.214.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.214.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.214.0",
|
||||
"@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/resources": "^2.6.1",
|
||||
"@opentelemetry/sdk-logs": "^0.214.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.6.1",
|
||||
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||
"@smithy/core": "^3.23.13",
|
||||
"@smithy/node-http-handler": "^4.5.1",
|
||||
"ajv": "^8.18.0",
|
||||
"asciichart": "^1.5.25",
|
||||
"audio-capture-napi": "workspace:*",
|
||||
"auto-bind": "^5.0.1",
|
||||
"axios": "^1.14.0",
|
||||
"bidi-js": "^1.0.3",
|
||||
"cacache": "^20.0.4",
|
||||
"chalk": "^5.6.2",
|
||||
"chokidar": "^5.0.0",
|
||||
"cli-boxes": "^4.0.1",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"code-excerpt": "^4.0.0",
|
||||
"color-diff-napi": "workspace:*",
|
||||
"diff": "^8.0.4",
|
||||
"emoji-regex": "^10.6.0",
|
||||
"env-paths": "^4.0.0",
|
||||
"execa": "^9.6.1",
|
||||
"fflate": "^0.8.2",
|
||||
"figures": "^6.1.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"get-east-asian-width": "^1.5.0",
|
||||
"google-auth-library": "^10.6.2",
|
||||
"highlight.js": "^11.11.1",
|
||||
"https-proxy-agent": "^8.0.0",
|
||||
"ignore": "^7.0.5",
|
||||
"image-processor-napi": "workspace:*",
|
||||
"indent-string": "^5.0.0",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lru-cache": "^11.2.7",
|
||||
"marked": "^17.0.5",
|
||||
"modifiers-napi": "workspace:*",
|
||||
"p-map": "^7.0.4",
|
||||
"picomatch": "^4.0.4",
|
||||
"plist": "^3.1.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.4",
|
||||
"react-compiler-runtime": "^1.0.0",
|
||||
"react-reconciler": "^0.33.0",
|
||||
"semver": "^7.7.4",
|
||||
"sharp": "^0.34.5",
|
||||
"shell-quote": "^1.8.3",
|
||||
"signal-exit": "^4.1.0",
|
||||
"stack-utils": "^2.0.6",
|
||||
"strip-ansi": "^7.2.0",
|
||||
"supports-hyperlinks": "^4.4.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"turndown": "^7.2.2",
|
||||
"type-fest": "^5.5.0",
|
||||
"undici": "^7.24.6",
|
||||
"url-handler-napi": "workspace:*",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"vscode-languageserver-types": "^3.17.5",
|
||||
"wrap-ansi": "^10.0.0",
|
||||
"ws": "^8.20.0",
|
||||
"xss": "^1.0.15",
|
||||
"yaml": "^2.8.3",
|
||||
"zod": "^4.3.6",
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@types/cacache": "^20.0.1",
|
||||
"@types/plist": "^3.0.5",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-reconciler": "^0.33.0",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"knip": "^6.1.1",
|
||||
"typescript": "^6.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"name": "@ant/computer-use-input",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
}
|
||||
|
||||
@@ -1,39 +1,174 @@
|
||||
/**
|
||||
* @ant/computer-use-input — macOS 键鼠模拟实现
|
||||
*
|
||||
* 使用 macOS 原生工具实现:
|
||||
* - AppleScript (osascript) — 应用信息、键盘输入
|
||||
* - CGEvent via AppleScript-ObjC bridge — 鼠标操作、位置查询
|
||||
*
|
||||
* 仅 macOS 支持。其他平台返回 { isSupported: false }
|
||||
*/
|
||||
|
||||
import { $ } from 'bun'
|
||||
|
||||
interface FrontmostAppInfo {
|
||||
bundleId: string
|
||||
appName: string
|
||||
}
|
||||
|
||||
// AppleScript key code mapping
|
||||
const KEY_MAP: Record<string, number> = {
|
||||
return: 36, enter: 36, tab: 48, space: 49, delete: 51, backspace: 51,
|
||||
escape: 53, esc: 53,
|
||||
left: 123, right: 124, down: 125, up: 126,
|
||||
f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97,
|
||||
f7: 98, f8: 100, f9: 101, f10: 109, f11: 103, f12: 111,
|
||||
home: 115, end: 119, pageup: 116, pagedown: 121,
|
||||
}
|
||||
|
||||
const MODIFIER_MAP: Record<string, string> = {
|
||||
command: 'command down', cmd: 'command down', meta: 'command down', super: 'command down',
|
||||
shift: 'shift down',
|
||||
option: 'option down', alt: 'option down',
|
||||
control: 'control down', ctrl: 'control down',
|
||||
}
|
||||
|
||||
async function osascript(script: string): Promise<string> {
|
||||
const result = await $`osascript -e ${script}`.quiet().nothrow().text()
|
||||
return result.trim()
|
||||
}
|
||||
|
||||
async function jxa(script: string): Promise<string> {
|
||||
const result = await $`osascript -l JavaScript -e ${script}`.quiet().nothrow().text()
|
||||
return result.trim()
|
||||
}
|
||||
|
||||
function jxaSync(script: string): string {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: ['osascript', '-l', 'JavaScript', '-e', script],
|
||||
stdout: 'pipe', stderr: 'pipe',
|
||||
})
|
||||
return new TextDecoder().decode(result.stdout).trim()
|
||||
}
|
||||
|
||||
function buildMouseJxa(eventType: string, x: number, y: number, btn: number, clickState?: number): string {
|
||||
let script = `ObjC.import("CoreGraphics"); var p = $.CGPointMake(${x},${y}); var e = $.CGEventCreateMouseEvent(null, $.${eventType}, p, ${btn});`
|
||||
if (clickState !== undefined) {
|
||||
script += ` $.CGEventSetIntegerValueField(e, $.kCGMouseEventClickState, ${clickState});`
|
||||
}
|
||||
script += ` $.CGEventPost($.kCGHIDEventTap, e);`
|
||||
return script
|
||||
}
|
||||
|
||||
// ---- Implementation functions ----
|
||||
|
||||
async function moveMouse(x: number, y: number, _animated: boolean): Promise<void> {
|
||||
await jxa(buildMouseJxa('kCGEventMouseMoved', x, y, 0))
|
||||
}
|
||||
|
||||
async function key(keyName: string, action: 'press' | 'release'): Promise<void> {
|
||||
if (action === 'release') return
|
||||
const lower = keyName.toLowerCase()
|
||||
const keyCode = KEY_MAP[lower]
|
||||
if (keyCode !== undefined) {
|
||||
await osascript(`tell application "System Events" to key code ${keyCode}`)
|
||||
} else {
|
||||
await osascript(`tell application "System Events" to keystroke "${keyName.length === 1 ? keyName : lower}"`)
|
||||
}
|
||||
}
|
||||
|
||||
async function keys(parts: string[]): Promise<void> {
|
||||
const modifiers: string[] = []
|
||||
let finalKey: string | null = null
|
||||
for (const part of parts) {
|
||||
const mod = MODIFIER_MAP[part.toLowerCase()]
|
||||
if (mod) modifiers.push(mod)
|
||||
else finalKey = part
|
||||
}
|
||||
if (!finalKey) return
|
||||
const lower = finalKey.toLowerCase()
|
||||
const keyCode = KEY_MAP[lower]
|
||||
const modStr = modifiers.length > 0 ? ` using {${modifiers.join(', ')}}` : ''
|
||||
if (keyCode !== undefined) {
|
||||
await osascript(`tell application "System Events" to key code ${keyCode}${modStr}`)
|
||||
} else {
|
||||
await osascript(`tell application "System Events" to keystroke "${finalKey.length === 1 ? finalKey : lower}"${modStr}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function mouseLocation(): Promise<{ x: number; y: number }> {
|
||||
const result = await jxa('ObjC.import("CoreGraphics"); var e = $.CGEventCreate(null); var p = $.CGEventGetLocation(e); p.x + "," + p.y')
|
||||
const [xStr, yStr] = result.split(',')
|
||||
return { x: Math.round(Number(xStr)), y: Math.round(Number(yStr)) }
|
||||
}
|
||||
|
||||
async function mouseButton(
|
||||
button: 'left' | 'right' | 'middle',
|
||||
action: 'click' | 'press' | 'release',
|
||||
count?: number,
|
||||
): Promise<void> {
|
||||
const pos = await mouseLocation()
|
||||
const btn = button === 'left' ? 0 : button === 'right' ? 1 : 2
|
||||
const downType = btn === 0 ? 'kCGEventLeftMouseDown' : btn === 1 ? 'kCGEventRightMouseDown' : 'kCGEventOtherMouseDown'
|
||||
const upType = btn === 0 ? 'kCGEventLeftMouseUp' : btn === 1 ? 'kCGEventRightMouseUp' : 'kCGEventOtherMouseUp'
|
||||
|
||||
if (action === 'click') {
|
||||
for (let i = 0; i < (count ?? 1); i++) {
|
||||
await jxa(buildMouseJxa(downType, pos.x, pos.y, btn, i + 1))
|
||||
await jxa(buildMouseJxa(upType, pos.x, pos.y, btn, i + 1))
|
||||
}
|
||||
} else if (action === 'press') {
|
||||
await jxa(buildMouseJxa(downType, pos.x, pos.y, btn))
|
||||
} else {
|
||||
await jxa(buildMouseJxa(upType, pos.x, pos.y, btn))
|
||||
}
|
||||
}
|
||||
|
||||
async function mouseScroll(amount: number, direction: 'vertical' | 'horizontal'): Promise<void> {
|
||||
const script = direction === 'vertical'
|
||||
? `ObjC.import("CoreGraphics"); var e = $.CGEventCreateScrollWheelEvent(null, 0, 1, ${amount}); $.CGEventPost($.kCGHIDEventTap, e);`
|
||||
: `ObjC.import("CoreGraphics"); var e = $.CGEventCreateScrollWheelEvent(null, 0, 2, 0, ${amount}); $.CGEventPost($.kCGHIDEventTap, e);`
|
||||
await jxa(script)
|
||||
}
|
||||
|
||||
async function typeText(text: string): Promise<void> {
|
||||
const escaped = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
||||
await osascript(`tell application "System Events" to keystroke "${escaped}"`)
|
||||
}
|
||||
|
||||
function getFrontmostAppInfo(): FrontmostAppInfo | null {
|
||||
try {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: ['osascript', '-e', `
|
||||
tell application "System Events"
|
||||
set frontApp to first application process whose frontmost is true
|
||||
set appName to name of frontApp
|
||||
set bundleId to bundle identifier of frontApp
|
||||
return bundleId & "|" & appName
|
||||
end tell
|
||||
`],
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
})
|
||||
const output = new TextDecoder().decode(result.stdout).trim()
|
||||
if (!output || !output.includes('|')) return null
|
||||
const [bundleId, appName] = output.split('|', 2)
|
||||
return { bundleId: bundleId!, appName: appName! }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Exports ----
|
||||
|
||||
export class ComputerUseInputAPI {
|
||||
declare moveMouse: (
|
||||
x: number,
|
||||
y: number,
|
||||
animated: boolean,
|
||||
) => Promise<void>
|
||||
|
||||
declare key: (
|
||||
key: string,
|
||||
action: 'press' | 'release',
|
||||
) => Promise<void>
|
||||
|
||||
declare moveMouse: (x: number, y: number, animated: boolean) => Promise<void>
|
||||
declare key: (key: string, action: 'press' | 'release') => Promise<void>
|
||||
declare keys: (parts: string[]) => Promise<void>
|
||||
|
||||
declare mouseLocation: () => Promise<{ x: number; y: number }>
|
||||
|
||||
declare mouseButton: (
|
||||
button: 'left' | 'right' | 'middle',
|
||||
action: 'click' | 'press' | 'release',
|
||||
count?: number,
|
||||
) => Promise<void>
|
||||
|
||||
declare mouseScroll: (
|
||||
amount: number,
|
||||
direction: 'vertical' | 'horizontal',
|
||||
) => Promise<void>
|
||||
|
||||
declare mouseButton: (button: 'left' | 'right' | 'middle', action: 'click' | 'press' | 'release', count?: number) => Promise<void>
|
||||
declare mouseScroll: (amount: number, direction: 'vertical' | 'horizontal') => Promise<void>
|
||||
declare typeText: (text: string) => Promise<void>
|
||||
|
||||
declare getFrontmostAppInfo: () => FrontmostAppInfo | null
|
||||
|
||||
declare isSupported: true
|
||||
}
|
||||
|
||||
@@ -42,3 +177,7 @@ interface ComputerUseInputUnsupported {
|
||||
}
|
||||
|
||||
export type ComputerUseInput = ComputerUseInputAPI | ComputerUseInputUnsupported
|
||||
|
||||
// Plain object with all methods as own properties — compatible with require()
|
||||
export const isSupported = process.platform === 'darwin'
|
||||
export { moveMouse, key, keys, mouseLocation, mouseButton, mouseScroll, typeText, getFrontmostAppInfo }
|
||||
|
||||
@@ -1,30 +1,163 @@
|
||||
export const API_RESIZE_PARAMS: any = {}
|
||||
/**
|
||||
* @ant/computer-use-mcp — Stub 实现
|
||||
*
|
||||
* 提供类型安全的 stub,所有函数返回合理的默认值。
|
||||
* 在 feature('CHICAGO_MCP') = false 时不会被实际调用,
|
||||
* 但确保 import 不报错且类型正确。
|
||||
*/
|
||||
|
||||
export class ComputerExecutor {}
|
||||
import type {
|
||||
ComputerUseHostAdapter,
|
||||
CoordinateMode,
|
||||
GrantFlags,
|
||||
Logger,
|
||||
} from './types'
|
||||
|
||||
export type ComputerUseSessionContext = any
|
||||
export type CuCallToolResult = any
|
||||
export type CuPermissionRequest = any
|
||||
export type CuPermissionResponse = any
|
||||
export const DEFAULT_GRANT_FLAGS: any = {}
|
||||
export type DisplayGeometry = any
|
||||
export type FrontmostApp = any
|
||||
export type InstalledApp = any
|
||||
export type ResolvePrepareCaptureResult = any
|
||||
export type RunningApp = any
|
||||
export type ScreenshotDims = any
|
||||
export type ScreenshotResult = any
|
||||
// Re-export types from types.ts
|
||||
export type { CoordinateMode, Logger } from './types'
|
||||
export type {
|
||||
ComputerUseConfig,
|
||||
ComputerUseHostAdapter,
|
||||
CuPermissionRequest,
|
||||
CuPermissionResponse,
|
||||
CuSubGates,
|
||||
} from './types'
|
||||
export { DEFAULT_GRANT_FLAGS } from './types'
|
||||
|
||||
export function bindSessionContext(..._args: any[]): any {
|
||||
return null
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types (defined here for callers that import from the main entry)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface DisplayGeometry {
|
||||
width: number
|
||||
height: number
|
||||
displayId?: number
|
||||
originX?: number
|
||||
originY?: number
|
||||
}
|
||||
|
||||
export function buildComputerUseTools(..._args: any[]): any[] {
|
||||
export interface FrontmostApp {
|
||||
bundleId: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
export interface InstalledApp {
|
||||
bundleId: string
|
||||
displayName: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface RunningApp {
|
||||
bundleId: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
export interface ScreenshotResult {
|
||||
base64: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export type ResolvePrepareCaptureResult = ScreenshotResult
|
||||
|
||||
export interface ScreenshotDims {
|
||||
width: number
|
||||
height: number
|
||||
displayWidth: number
|
||||
displayHeight: number
|
||||
displayId: number
|
||||
originX: number
|
||||
originY: number
|
||||
}
|
||||
|
||||
export interface CuCallToolResultContent {
|
||||
type: 'image' | 'text'
|
||||
data?: string
|
||||
mimeType?: string
|
||||
text?: string
|
||||
}
|
||||
|
||||
export interface CuCallToolResult {
|
||||
content: CuCallToolResultContent[]
|
||||
telemetry: {
|
||||
error_kind?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type ComputerUseSessionContext = Record<string, unknown>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// API_RESIZE_PARAMS — 默认的截图缩放参数
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const API_RESIZE_PARAMS = {
|
||||
maxWidth: 1280,
|
||||
maxHeight: 800,
|
||||
maxPixels: 1280 * 800,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ComputerExecutor — stub class
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class ComputerExecutor {
|
||||
capabilities: Record<string, boolean> = {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Functions — 返回合理默认值的 stub
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 计算目标截图尺寸。
|
||||
* 在物理宽高和 API 限制之间取最优尺寸。
|
||||
*/
|
||||
export function targetImageSize(
|
||||
physW: number,
|
||||
physH: number,
|
||||
_params?: typeof API_RESIZE_PARAMS,
|
||||
): [number, number] {
|
||||
const maxW = _params?.maxWidth ?? 1280
|
||||
const maxH = _params?.maxHeight ?? 800
|
||||
const scale = Math.min(1, maxW / physW, maxH / physH)
|
||||
return [Math.round(physW * scale), Math.round(physH * scale)]
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定会话上下文,返回工具调度函数。
|
||||
* Stub 返回一个始终返回空结果的调度器。
|
||||
*/
|
||||
export function bindSessionContext(
|
||||
_adapter: ComputerUseHostAdapter,
|
||||
_coordinateMode: CoordinateMode,
|
||||
_ctx: ComputerUseSessionContext,
|
||||
): (name: string, args: unknown) => Promise<CuCallToolResult> {
|
||||
return async (_name: string, _args: unknown) => ({
|
||||
content: [],
|
||||
telemetry: {},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Computer Use 工具定义列表。
|
||||
* Stub 返回空数组(无工具)。
|
||||
*/
|
||||
export function buildComputerUseTools(
|
||||
_capabilities?: Record<string, boolean>,
|
||||
_coordinateMode?: CoordinateMode,
|
||||
_installedAppNames?: string[],
|
||||
): Array<{ name: string; description: string; inputSchema: Record<string, unknown> }> {
|
||||
return []
|
||||
}
|
||||
|
||||
export function createComputerUseMcpServer(..._args: any[]): any {
|
||||
/**
|
||||
* 创建 Computer Use MCP server。
|
||||
* Stub 返回 null(服务未启用)。
|
||||
*/
|
||||
export function createComputerUseMcpServer(
|
||||
_adapter?: ComputerUseHostAdapter,
|
||||
_coordinateMode?: CoordinateMode,
|
||||
): null {
|
||||
return null
|
||||
}
|
||||
|
||||
export const targetImageSize: any = null
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
export const sentinelApps: string[] = []
|
||||
/**
|
||||
* Sentinel apps — 需要特殊权限警告的应用列表
|
||||
*
|
||||
* 包含终端、文件管理器、系统设置等敏感应用。
|
||||
* Computer Use 操作这些应用时会显示额外警告。
|
||||
*/
|
||||
|
||||
export function getSentinelCategory(_appName: string): string | null {
|
||||
return null
|
||||
type SentinelCategory = 'shell' | 'filesystem' | 'system_settings'
|
||||
|
||||
const SENTINEL_MAP: Record<string, SentinelCategory> = {
|
||||
// Shell / Terminal
|
||||
'com.apple.Terminal': 'shell',
|
||||
'com.googlecode.iterm2': 'shell',
|
||||
'dev.warp.Warp-Stable': 'shell',
|
||||
'io.alacritty': 'shell',
|
||||
'com.github.wez.wezterm': 'shell',
|
||||
'net.kovidgoyal.kitty': 'shell',
|
||||
'co.zeit.hyper': 'shell',
|
||||
|
||||
// Filesystem
|
||||
'com.apple.finder': 'filesystem',
|
||||
|
||||
// System Settings
|
||||
'com.apple.systempreferences': 'system_settings',
|
||||
'com.apple.SystemPreferences': 'system_settings',
|
||||
}
|
||||
|
||||
export const sentinelApps: string[] = Object.keys(SENTINEL_MAP)
|
||||
|
||||
export function getSentinelCategory(bundleId: string): SentinelCategory | null {
|
||||
return SENTINEL_MAP[bundleId] ?? null
|
||||
}
|
||||
|
||||
@@ -1,8 +1,70 @@
|
||||
export type ComputerUseConfig = any
|
||||
export type ComputerUseHostAdapter = any
|
||||
export type CoordinateMode = any
|
||||
export type CuPermissionRequest = any
|
||||
export type CuPermissionResponse = any
|
||||
export type CuSubGates = any
|
||||
export const DEFAULT_GRANT_FLAGS: any = {}
|
||||
export type Logger = any
|
||||
/**
|
||||
* @ant/computer-use-mcp — Types
|
||||
*
|
||||
* 从调用侧反推的真实类型定义,替代 any stub。
|
||||
*/
|
||||
|
||||
export type CoordinateMode = 'pixels' | 'normalized'
|
||||
|
||||
export interface CuSubGates {
|
||||
pixelValidation: boolean
|
||||
clipboardPasteMultiline: boolean
|
||||
mouseAnimation: boolean
|
||||
hideBeforeAction: boolean
|
||||
autoTargetDisplay: boolean
|
||||
clipboardGuard: boolean
|
||||
}
|
||||
|
||||
export interface Logger {
|
||||
silly(message: string, ...args: unknown[]): void
|
||||
debug(message: string, ...args: unknown[]): void
|
||||
info(message: string, ...args: unknown[]): void
|
||||
warn(message: string, ...args: unknown[]): void
|
||||
error(message: string, ...args: unknown[]): void
|
||||
}
|
||||
|
||||
export interface CuPermissionRequest {
|
||||
apps: Array<{ bundleId: string; displayName: string }>
|
||||
requestedFlags: GrantFlags
|
||||
reason: string
|
||||
tccState: { accessibility: boolean; screenRecording: boolean }
|
||||
willHide: string[]
|
||||
}
|
||||
|
||||
export interface GrantFlags {
|
||||
clipboardRead: boolean
|
||||
clipboardWrite: boolean
|
||||
systemKeyCombos: boolean
|
||||
}
|
||||
|
||||
export interface CuPermissionResponse {
|
||||
granted: string[]
|
||||
denied: string[]
|
||||
flags: GrantFlags
|
||||
}
|
||||
|
||||
export const DEFAULT_GRANT_FLAGS: GrantFlags = {
|
||||
clipboardRead: false,
|
||||
clipboardWrite: false,
|
||||
systemKeyCombos: false,
|
||||
}
|
||||
|
||||
export interface ComputerUseConfig {
|
||||
coordinateMode: CoordinateMode
|
||||
enabledTools: string[]
|
||||
}
|
||||
|
||||
export interface ComputerUseHostAdapter {
|
||||
serverName: string
|
||||
logger: Logger
|
||||
executor: ComputerExecutor
|
||||
ensureOsPermissions(): Promise<{ granted: true } | { granted: false; accessibility: boolean; screenRecording: boolean }>
|
||||
isDisabled(): boolean
|
||||
getSubGates(): CuSubGates
|
||||
getAutoUnhideEnabled(): boolean
|
||||
cropRawPatch?(base64: string, x: number, y: number, w: number, h: number): Promise<string>
|
||||
}
|
||||
|
||||
export interface ComputerExecutor {
|
||||
capabilities: Record<string, boolean>
|
||||
}
|
||||
|
||||
@@ -1,66 +1,194 @@
|
||||
interface DisplayGeometry {
|
||||
/**
|
||||
* @ant/computer-use-swift — macOS 实现
|
||||
*
|
||||
* 用 AppleScript/JXA/screencapture 替代原始 Swift 原生模块。
|
||||
* 提供显示器信息、应用管理、截图等功能。
|
||||
*
|
||||
* 仅 macOS 支持。
|
||||
*/
|
||||
|
||||
import { readFileSync, unlinkSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types (exported for callers)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface DisplayGeometry {
|
||||
width: number
|
||||
height: number
|
||||
scaleFactor: number
|
||||
displayId: number
|
||||
}
|
||||
|
||||
interface PrepareDisplayResult {
|
||||
export interface PrepareDisplayResult {
|
||||
activated: string
|
||||
hidden: string[]
|
||||
}
|
||||
|
||||
interface AppInfo {
|
||||
export interface AppInfo {
|
||||
bundleId: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
interface InstalledApp {
|
||||
export interface InstalledApp {
|
||||
bundleId: string
|
||||
displayName: string
|
||||
path: string
|
||||
iconDataUrl?: string
|
||||
}
|
||||
|
||||
interface RunningApp {
|
||||
export interface RunningApp {
|
||||
bundleId: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
interface ScreenshotResult {
|
||||
export interface ScreenshotResult {
|
||||
base64: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface ResolvePrepareCaptureResult {
|
||||
export interface ResolvePrepareCaptureResult {
|
||||
base64: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface WindowDisplayInfo {
|
||||
export interface WindowDisplayInfo {
|
||||
bundleId: string
|
||||
displayIds: number[]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function jxaSync(script: string): string {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: ['osascript', '-l', 'JavaScript', '-e', script],
|
||||
stdout: 'pipe', stderr: 'pipe',
|
||||
})
|
||||
return new TextDecoder().decode(result.stdout).trim()
|
||||
}
|
||||
|
||||
function osascriptSync(script: string): string {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: ['osascript', '-e', script],
|
||||
stdout: 'pipe', stderr: 'pipe',
|
||||
})
|
||||
return new TextDecoder().decode(result.stdout).trim()
|
||||
}
|
||||
|
||||
async function osascript(script: string): Promise<string> {
|
||||
const proc = Bun.spawn(['osascript', '-e', script], {
|
||||
stdout: 'pipe', stderr: 'pipe',
|
||||
})
|
||||
const text = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
return text.trim()
|
||||
}
|
||||
|
||||
async function jxa(script: string): Promise<string> {
|
||||
const proc = Bun.spawn(['osascript', '-l', 'JavaScript', '-e', script], {
|
||||
stdout: 'pipe', stderr: 'pipe',
|
||||
})
|
||||
const text = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
return text.trim()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DisplayAPI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface DisplayAPI {
|
||||
getSize(displayId?: number): DisplayGeometry
|
||||
listAll(): DisplayGeometry[]
|
||||
}
|
||||
|
||||
const displayAPI: DisplayAPI = {
|
||||
getSize(displayId?: number): DisplayGeometry {
|
||||
const all = this.listAll()
|
||||
if (displayId !== undefined) {
|
||||
const found = all.find(d => d.displayId === displayId)
|
||||
if (found) return found
|
||||
}
|
||||
return all[0] ?? { width: 1920, height: 1080, scaleFactor: 2, displayId: 1 }
|
||||
},
|
||||
|
||||
listAll(): DisplayGeometry[] {
|
||||
try {
|
||||
const raw = jxaSync(`
|
||||
ObjC.import("CoreGraphics");
|
||||
var displays = $.CGDisplayCopyAllDisplayModes ? [] : [];
|
||||
var active = $.CGGetActiveDisplayList(10, null, Ref());
|
||||
var countRef = Ref();
|
||||
$.CGGetActiveDisplayList(0, null, countRef);
|
||||
var count = countRef[0];
|
||||
var idBuf = Ref();
|
||||
$.CGGetActiveDisplayList(count, idBuf, countRef);
|
||||
var result = [];
|
||||
for (var i = 0; i < count; i++) {
|
||||
var did = idBuf[i];
|
||||
var w = $.CGDisplayPixelsWide(did);
|
||||
var h = $.CGDisplayPixelsHigh(did);
|
||||
var mode = $.CGDisplayCopyDisplayMode(did);
|
||||
var pw = $.CGDisplayModeGetPixelWidth(mode);
|
||||
var sf = pw > 0 && w > 0 ? pw / w : 2;
|
||||
result.push({width: w, height: h, scaleFactor: sf, displayId: did});
|
||||
}
|
||||
JSON.stringify(result);
|
||||
`)
|
||||
return (JSON.parse(raw) as DisplayGeometry[]).map(d => ({
|
||||
width: Number(d.width), height: Number(d.height),
|
||||
scaleFactor: Number(d.scaleFactor), displayId: Number(d.displayId),
|
||||
}))
|
||||
} catch {
|
||||
// Fallback: use NSScreen via JXA
|
||||
try {
|
||||
const raw = jxaSync(`
|
||||
ObjC.import("AppKit");
|
||||
var screens = $.NSScreen.screens;
|
||||
var result = [];
|
||||
for (var i = 0; i < screens.count; i++) {
|
||||
var s = screens.objectAtIndex(i);
|
||||
var frame = s.frame;
|
||||
var desc = s.deviceDescription;
|
||||
var screenNumber = desc.objectForKey($("NSScreenNumber")).intValue;
|
||||
var backingFactor = s.backingScaleFactor;
|
||||
result.push({
|
||||
width: Math.round(frame.size.width),
|
||||
height: Math.round(frame.size.height),
|
||||
scaleFactor: backingFactor,
|
||||
displayId: screenNumber
|
||||
});
|
||||
}
|
||||
JSON.stringify(result);
|
||||
`)
|
||||
return (JSON.parse(raw) as DisplayGeometry[]).map(d => ({
|
||||
width: Number(d.width),
|
||||
height: Number(d.height),
|
||||
scaleFactor: Number(d.scaleFactor),
|
||||
displayId: Number(d.displayId),
|
||||
}))
|
||||
} catch {
|
||||
return [{ width: 1920, height: 1080, scaleFactor: 2, displayId: 1 }]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AppsAPI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface AppsAPI {
|
||||
prepareDisplay(
|
||||
allowlistBundleIds: string[],
|
||||
surrogateHost: string,
|
||||
displayId?: number,
|
||||
): Promise<PrepareDisplayResult>
|
||||
previewHideSet(
|
||||
bundleIds: string[],
|
||||
displayId?: number,
|
||||
): Promise<Array<AppInfo>>
|
||||
findWindowDisplays(
|
||||
bundleIds: string[],
|
||||
): Promise<Array<WindowDisplayInfo>>
|
||||
appUnderPoint(
|
||||
x: number,
|
||||
y: number,
|
||||
): Promise<AppInfo | null>
|
||||
prepareDisplay(allowlistBundleIds: string[], surrogateHost: string, displayId?: number): Promise<PrepareDisplayResult>
|
||||
previewHideSet(bundleIds: string[], displayId?: number): Promise<AppInfo[]>
|
||||
findWindowDisplays(bundleIds: string[]): Promise<WindowDisplayInfo[]>
|
||||
appUnderPoint(x: number, y: number): Promise<AppInfo | null>
|
||||
listInstalled(): Promise<InstalledApp[]>
|
||||
iconDataUrl(path: string): string | null
|
||||
listRunning(): RunningApp[]
|
||||
@@ -68,45 +196,193 @@ interface AppsAPI {
|
||||
unhide(bundleIds: string[]): Promise<void>
|
||||
}
|
||||
|
||||
interface DisplayAPI {
|
||||
getSize(displayId?: number): DisplayGeometry
|
||||
listAll(): DisplayGeometry[]
|
||||
const appsAPI: AppsAPI = {
|
||||
async prepareDisplay(
|
||||
_allowlistBundleIds: string[],
|
||||
_surrogateHost: string,
|
||||
_displayId?: number,
|
||||
): Promise<PrepareDisplayResult> {
|
||||
return { activated: '', hidden: [] }
|
||||
},
|
||||
|
||||
async previewHideSet(
|
||||
_bundleIds: string[],
|
||||
_displayId?: number,
|
||||
): Promise<AppInfo[]> {
|
||||
return []
|
||||
},
|
||||
|
||||
async findWindowDisplays(bundleIds: string[]): Promise<WindowDisplayInfo[]> {
|
||||
// Each running app is assumed to be on display 1
|
||||
return bundleIds.map(bundleId => ({ bundleId, displayIds: [1] }))
|
||||
},
|
||||
|
||||
async appUnderPoint(_x: number, _y: number): Promise<AppInfo | null> {
|
||||
// Use JXA to find app at mouse position via accessibility
|
||||
try {
|
||||
const result = await jxa(`
|
||||
ObjC.import("CoreGraphics");
|
||||
ObjC.import("AppKit");
|
||||
var pt = $.CGPointMake(${_x}, ${_y});
|
||||
// Get frontmost app as a fallback
|
||||
var app = $.NSWorkspace.sharedWorkspace.frontmostApplication;
|
||||
JSON.stringify({bundleId: app.bundleIdentifier.js, displayName: app.localizedName.js});
|
||||
`)
|
||||
return JSON.parse(result)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
async listInstalled(): Promise<InstalledApp[]> {
|
||||
try {
|
||||
const result = await osascript(`
|
||||
tell application "System Events"
|
||||
set appList to ""
|
||||
repeat with appFile in (every file of folder "Applications" of startup disk whose name ends with ".app")
|
||||
set appPath to POSIX path of (appFile as alias)
|
||||
set appName to name of appFile
|
||||
set appList to appList & appPath & "|" & appName & "\\n"
|
||||
end repeat
|
||||
return appList
|
||||
end tell
|
||||
`)
|
||||
return result.split('\n').filter(Boolean).map(line => {
|
||||
const [path, name] = line.split('|', 2)
|
||||
// Derive bundleId from Info.plist would be ideal, but use path-based fallback
|
||||
const displayName = (name ?? '').replace(/\.app$/, '')
|
||||
return {
|
||||
bundleId: `com.app.${displayName.toLowerCase().replace(/\s+/g, '-')}`,
|
||||
displayName,
|
||||
path: path ?? '',
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
iconDataUrl(_path: string): string | null {
|
||||
return null
|
||||
},
|
||||
|
||||
listRunning(): RunningApp[] {
|
||||
try {
|
||||
const raw = jxaSync(`
|
||||
var apps = Application("System Events").applicationProcesses.whose({backgroundOnly: false});
|
||||
var result = [];
|
||||
for (var i = 0; i < apps.length; i++) {
|
||||
try {
|
||||
var a = apps[i];
|
||||
result.push({bundleId: a.bundleIdentifier(), displayName: a.name()});
|
||||
} catch(e) {}
|
||||
}
|
||||
JSON.stringify(result);
|
||||
`)
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
async open(bundleId: string): Promise<void> {
|
||||
await osascript(`tell application id "${bundleId}" to activate`)
|
||||
},
|
||||
|
||||
async unhide(bundleIds: string[]): Promise<void> {
|
||||
for (const bundleId of bundleIds) {
|
||||
await osascript(`
|
||||
tell application "System Events"
|
||||
set visible of application process (name of application process whose bundle identifier is "${bundleId}") to true
|
||||
end tell
|
||||
`)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ScreenshotAPI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ScreenshotAPI {
|
||||
captureExcluding(
|
||||
allowedBundleIds: string[],
|
||||
quality: number,
|
||||
targetW: number,
|
||||
targetH: number,
|
||||
displayId?: number,
|
||||
allowedBundleIds: string[], quality: number,
|
||||
targetW: number, targetH: number, displayId?: number,
|
||||
): Promise<ScreenshotResult>
|
||||
captureRegion(
|
||||
allowedBundleIds: string[],
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
outW: number,
|
||||
outH: number,
|
||||
quality: number,
|
||||
displayId?: number,
|
||||
x: number, y: number, w: number, h: number,
|
||||
outW: number, outH: number, quality: number, displayId?: number,
|
||||
): Promise<ScreenshotResult>
|
||||
}
|
||||
|
||||
export class ComputerUseAPI {
|
||||
declare apps: AppsAPI
|
||||
declare display: DisplayAPI
|
||||
declare screenshot: ScreenshotAPI
|
||||
async function captureScreenToBase64(args: string[]): Promise<{ base64: string; width: number; height: number }> {
|
||||
const tmpFile = join(tmpdir(), `cu-screenshot-${Date.now()}.png`)
|
||||
const proc = Bun.spawn(['screencapture', ...args, tmpFile], {
|
||||
stdout: 'pipe', stderr: 'pipe',
|
||||
})
|
||||
await proc.exited
|
||||
|
||||
declare resolvePrepareCapture: (
|
||||
try {
|
||||
const buf = readFileSync(tmpFile)
|
||||
const base64 = buf.toString('base64')
|
||||
// Parse PNG header for dimensions (bytes 16-23)
|
||||
const width = buf.readUInt32BE(16)
|
||||
const height = buf.readUInt32BE(20)
|
||||
return { base64, width, height }
|
||||
} finally {
|
||||
try { unlinkSync(tmpFile) } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
const screenshotAPI: ScreenshotAPI = {
|
||||
async captureExcluding(
|
||||
_allowedBundleIds: string[],
|
||||
_quality: number,
|
||||
_targetW: number,
|
||||
_targetH: number,
|
||||
displayId?: number,
|
||||
): Promise<ScreenshotResult> {
|
||||
const args = ['-x'] // silent
|
||||
if (displayId !== undefined) {
|
||||
args.push('-D', String(displayId))
|
||||
}
|
||||
return captureScreenToBase64(args)
|
||||
},
|
||||
|
||||
async captureRegion(
|
||||
_allowedBundleIds: string[],
|
||||
x: number, y: number, w: number, h: number,
|
||||
_outW: number, _outH: number, _quality: number,
|
||||
displayId?: number,
|
||||
): Promise<ScreenshotResult> {
|
||||
const args = ['-x', '-R', `${x},${y},${w},${h}`]
|
||||
if (displayId !== undefined) {
|
||||
args.push('-D', String(displayId))
|
||||
}
|
||||
return captureScreenToBase64(args)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ComputerUseAPI — Main export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class ComputerUseAPI {
|
||||
apps: AppsAPI = appsAPI
|
||||
display: DisplayAPI = displayAPI
|
||||
screenshot: ScreenshotAPI = screenshotAPI
|
||||
|
||||
async resolvePrepareCapture(
|
||||
allowedBundleIds: string[],
|
||||
surrogateHost: string,
|
||||
_surrogateHost: string,
|
||||
quality: number,
|
||||
targetW: number,
|
||||
targetH: number,
|
||||
displayId?: number,
|
||||
autoResolve?: boolean,
|
||||
doHide?: boolean,
|
||||
) => Promise<ResolvePrepareCaptureResult>
|
||||
_autoResolve?: boolean,
|
||||
_doHide?: boolean,
|
||||
): Promise<ResolvePrepareCaptureResult> {
|
||||
return this.screenshot.captureExcluding(allowedBundleIds, quality, targetW, targetH, displayId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env bun
|
||||
// Runtime polyfill for bun:bundle (build-time macros)
|
||||
const feature = (_name: string) => false;
|
||||
if (typeof globalThis.MACRO === "undefined") {
|
||||
|
||||
Reference in New Issue
Block a user