Compare commits

...

84 Commits

Author SHA1 Message Date
claude-code-best
d2b66d9d2c docs: update contributors 2026-04-17 12:45:56 +00:00
claude-code-best
d70e7f7f05 feat: 支持 langfuse 工具调用映射 2026-04-17 20:45:14 +08:00
Cheng Zi Feng
72a2093cd6 feat(remote-control): 优化 Web 展示、状态同步与桥接控制流程 (#288)
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
2026-04-17 16:21:27 +08:00
claude-code-best
b5c299f5d2 build: CI 添加通过过滤 2026-04-17 11:33:57 +08:00
claude-code-best
ac42ce2d67 fix: 解决 node 下 loading 按钮计算错误问题 2026-04-17 10:42:40 +08:00
claude-code-best
c659912517 docs: 更新说明 2026-04-17 10:22:56 +08:00
claude-code-best
a14b7f352b test: 修正 mock 的滥用情况 2026-04-17 10:13:09 +08:00
claude-code-best
c5ab83a3fc fix: 修复 linux 端的安装问题 2026-04-17 09:51:59 +08:00
claude-code-best
03b7f9b453 chore: 1.4.1 2026-04-17 09:45:36 +08:00
claude-code-best
bddd146f25 feat: 重构供应商层次 (#286)
* refactor: 创建 @anthropic-ai/model-provider 包骨架与类型定义

- 新建 workspace 包 packages/@anthropic-ai/model-provider
- 定义 ModelProviderHooks 接口(依赖注入:分析、成本、日志等)
- 定义 ClientFactories 接口(Anthropic/OpenAI/Gemini/Grok 客户端工厂)
- 搬入核心类型:Message 体系、NonNullableUsage、EMPTY_USAGE、SystemPrompt、错误常量
- 主项目 src/types/message.ts 等改为 re-export,保持向后兼容

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 提升 OpenAI 转换器和模型映射到 model-provider 包

- 搬入 OpenAI 消息转换(convertMessages)、工具转换(convertTools)、流适配(streamAdapter)
- 搬入 OpenAI 和 Grok 模型映射(resolveOpenAIModel、resolveGrokModel)
- 主项目文件改为 thin re-export proxy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 搬入 Gemini 兼容层到 model-provider 包

- 搬入 Gemini 类型定义、消息转换、工具转换、流适配、模型映射
- 主项目 gemini/ 目录下文件改为 thin re-export proxy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 搬入 errorUtils 并迁移消费者导入到 model-provider

- 搬入 formatAPIError、extractConnectionErrorDetails 等 errorUtils
- 迁移 10 个消费者文件直接从 @anthropic-ai/model-provider 导入
- 更新 emptyUsage、sdkUtilityTypes、systemPromptType 为 re-export proxy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: compact 模型降级为 -1 模式(Opus→Sonnet, Sonnet→Haiku)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 添加 agent-loop 绘图

* Revert "feat: compact 模型降级为 -1 模式(Opus→Sonnet, Sonnet→Haiku)"

This reverts commit e458d6391d.

* docs: 添加简化版 agent loop

* fix: 修复 n 快捷键导致关闭的问题

* fix: 修复 node 下 ws 没打包问题

* docs: 修复链接

* test: 添加测试支持

* fix: 修复类型问题(#267) (#271)

* fix: 修复 Bun 的 polyfill 问题

* fix: 类型修复完成

* feat: 统一所有包的类型文件

* fix: 修复构建问题

* test: 修复类型校验 (#279)

* fix: 修复 Bun 的 polyfill 问题

* fix: 类型修复完成

* feat: 统一所有包的类型文件

* fix: 修复构建问题

* fix(remote-control): harden self-hosted session flows (#278)

Co-authored-by: chengzifeng <chengzifeng@meituan.com>

* docs: update contributors

* build: 新增 vite 构建流程

* feat: 添加环境变量支持以覆盖 max_tokens 设置

* feat(langfuse): LLM generation 记录工具定义

将 Anthropic 格式的工具定义转换为 Langfuse 兼容的 OpenAI 格式,
并在 generation 的 input 中以 { messages, tools } 结构传入,
以便在 Langfuse UI 中查看完整的工具定义信息。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: 添加对 ACP 协议的支持 (#284)

* feat: 适配 zed acp 协议

* docs: 完善 acp 文档

* chore: 1.4.0

* conflict: 解决冲突

* feat: 添加测试覆盖率上报

* style: 改名加移动文件夹位置

* refactor: 移动测试用例及实现

* test: 修复测试用例完成

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Cheng Zi Feng <1154238323@qq.com>
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
Co-authored-by: claude-code-best <272536312+claude-code-best@users.noreply.github.com>
2026-04-17 09:33:14 +08:00
claude-code-best
c8d08d235b Feat/integrate lint preview (#285)
* feat: 适配 zed acp 协议

* docs: 完善 acp 文档

* feat: integrate feature branches + daemon/job 命令层级化 + 跨平台后台引擎

Cherry-picked from origin/lint/preview (637c908), excluding lint-only changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct detectMimeFromBase64 to decode raw bytes from base64

Cherry-picked from origin/lint/preview (ee36954).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: daemon 子进程 spawn 跨平台修复 + CliLaunchSpec 集中化重构

Cherry-picked from origin/lint/preview (c5f52cd), excluding lint-only formatting changes.

- 新建 src/utils/cliLaunch.ts: 集中化 CLI 子进程启动层
- 修复 --daemon-worker=kind 等号格式解析
- 修复 daemon/bg fast path 缺少 setShellIfWindows()
- 修复 checkPathExists 用 existsSync 替代 execSync('dir')
- 7 个 spawn 站点迁移到 CliLaunchSpec

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: merge tsconfig.base.json into tsconfig.json with full compiler options

The cherry-pick from 637c908 dropped jsx/strict/etc settings when removing
tsconfig.base.json. This commit restores them in a single tsconfig.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: merge tsconfig.base.json into tsconfig.json with full compiler options

The cherry-pick from 637c908 dropped jsx/strict/etc settings when removing
tsconfig.base.json. This commit restores them in a single tsconfig.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 20:59:29 +08:00
claude-code-best
a02dc0bded chore: 1.4.0 2026-04-16 20:48:09 +08:00
claude-code-best
3cb1e50b25 feat: 添加对 ACP 协议的支持 (#284)
* feat: 适配 zed acp 协议

* docs: 完善 acp 文档
2026-04-16 20:31:50 +08:00
claude-code-best
cfab161e28 feat(langfuse): LLM generation 记录工具定义
将 Anthropic 格式的工具定义转换为 Langfuse 兼容的 OpenAI 格式,
并在 generation 的 input 中以 { messages, tools } 结构传入,
以便在 Langfuse UI 中查看完整的工具定义信息。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 15:31:18 +08:00
claude-code-best
90027279e6 feat: 添加环境变量支持以覆盖 max_tokens 设置 2026-04-16 13:01:07 +08:00
claude-code-best
3470783ced build: 新增 vite 构建流程 2026-04-16 12:39:19 +08:00
claude-code-best
8169b96250 docs: update contributors 2026-04-16 02:46:47 +00:00
Cheng Zi Feng
fe08cacf8d fix(remote-control): harden self-hosted session flows (#278)
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
2026-04-16 10:46:31 +08:00
claude-code-best
5a4c820e1d test: 修复类型校验 (#279)
* fix: 修复 Bun 的 polyfill 问题

* fix: 类型修复完成

* feat: 统一所有包的类型文件

* fix: 修复构建问题
2026-04-16 10:28:47 +08:00
claude-code-best
1a4e9702c2 fix: 修复类型问题(#267) (#271)
* fix: 修复 Bun 的 polyfill 问题

* fix: 类型修复完成

* feat: 统一所有包的类型文件

* fix: 修复构建问题
2026-04-15 10:54:00 +08:00
claude-code-best
2273a0bcfe docs: 修复链接 2026-04-14 21:19:36 +08:00
claude-code-best
b80483c23e fix: 修复 node 下 ws 没打包问题 2026-04-14 21:19:25 +08:00
claude-code-best
8442aaadd2 fix: 修复 n 快捷键导致关闭的问题 2026-04-14 21:18:36 +08:00
claude-code-best
dad3ad2b8d docs: 添加浏览器说明支持 2026-04-13 21:22:41 +08:00
claude-code-best
b5b81dfe49 chore: 更改 chrome 的依赖 2026-04-13 20:55:04 +08:00
claude-code-best
ecbd5a93e4 fix: 修复 Bun.hash 不存在的问题 2026-04-13 20:21:14 +08:00
claude-code-best
be80da4ce0 fix: 修复缓存 2026-04-13 20:09:23 +08:00
claude-code-best
fce40fed1f feat: 加上 userId 的传递 2026-04-13 19:04:51 +08:00
claude-code-best
a7e03a5b30 fix: 修复 interrupt 日志不上传 2026-04-13 18:12:23 +08:00
claude-code-best
05cabbbd73 feat: langfuse 工具调用显示为嵌套结构 2026-04-13 18:09:12 +08:00
claude-code-best
d4b30d32c3 fix: 修复 chrome 链接版本 2026-04-13 17:30:47 +08:00
claude-code-best
e0484e2817 fix: 使用简化版本的 chrome 桥接器 2026-04-13 16:03:47 +08:00
claude-code-best
2fb1c9dcd8 feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构

* fix: 修复类型问题

* chore: 更新版本到 1.3.2

* Add brave as alternative WebSearchTool

* fix: 修正顺序

* fix: 修复对穷鬼模式的 auto dream 和 session memory 越过

* feat: 穷鬼模式去除 session-summary

* feat: 创建 builtin-tools 包,搬运所有工具实现

将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/,
内部导入路径已更新为 src/ alias 模式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/

- src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/
- 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock

- tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射
- 新增 packages/builtin-tools/src 至 include

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀

所有包名及 import 路径统一添加 @claude-code-best/ 前缀:
- builtin-tools → @claude-code-best/builtin-tools
- mcp-client → @claude-code-best/mcp-client
- agent-tools → @claude-code-best/agent-tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复 node 环境没有 bun 的问题

---------

Co-authored-by: Eric-Guo <eric.guocz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:52:05 +08:00
claude-code-best
bbb8b613a9 docs: update contributors 2026-04-13 01:51:00 +00:00
claude-code-best
c63b875ae3 chore: 1.3.3 版本 2026-04-13 09:50:38 +08:00
claude-code-best
9b8503d13d fix: 修复 node 环境没有 bun 的问题 2026-04-13 09:47:33 +08:00
claude-code-best
3cf94fbda0 fix: 修复对穷鬼模式的 auto dream 和 session memory 越过 2026-04-12 23:24:12 +08:00
claude-code-best
9a3081dff6 Merge branch 'pr/Eric-Guo/245' 2026-04-12 23:12:23 +08:00
claude-code-best
bd6448ecda fix: 修正顺序 2026-04-12 23:12:09 +08:00
claude-code-best
1071270ce3 chore: 更新版本到 1.3.2 2026-04-12 22:47:03 +08:00
Eric-Guo
711440474c Add brave as alternative WebSearchTool 2026-04-12 22:23:11 +08:00
claude-code-best
8399d9ed20 fix: 修复类型问题 2026-04-12 22:19:54 +08:00
claude-code-best
513ccc3003 fix: 修复需要鉴权的问题 2026-04-12 21:45:22 +08:00
claude-code-best
e770f1ef9d feat: 完成第一个 mcp-chrome 接入版本 2026-04-12 21:32:09 +08:00
claude-code-best
227083d31f fix: 修复截图 MIME 类型硬编码导致 API 拒绝的问题
macOS screencapture 输出 PNG,但代码硬编码 mimeType 为 image/jpeg,
导致 API 报错 "specified using image/jpeg but appears to be image/png"。
改为通过 magic bytes 检测实际图片格式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 17:32:28 +08:00
claude-code-best
14c46df881 docs: 清理垃圾文档 2026-04-12 17:32:28 +08:00
Dosion
e0e4ee41c2 docs: add complete features guide and rewrite LAN Pipes docs (#246)
- Add docs/features/all-features-guide.md — comprehensive guide covering
  all 18 features across 13 PRs (Buddy, Remote Control, Voice, Chrome,
  Computer Use, GrowthBook, Ultraplan, Daemon, Pipe IPC, LAN Pipes,
  Monitor, Workflow, Coordinator, Proactive, History/Snip, Fork, etc.)

- Rewrite docs/features/lan-pipes.md — user-facing guide with step-by-step
  quickstart, firewall config (Windows/macOS/Linux), command reference,
  keyboard shortcuts, routing modes, permission forwarding, FAQ

- Rewrite docs/features/lan-pipes-implementation.md — developer reference
  with updated architecture (hook extraction, ndjsonFramer, module singletons),
  corrected code references, NDJSON protocol spec, attach flow sequence

Co-authored-by: unraid <local@unraid.local>
2026-04-12 11:37:51 +08:00
claude-code-best
e9861415c0 fix: 修复穷鬼模式的写入问题 2026-04-12 09:15:12 +08:00
claude-code-best
423f114db6 docs: 更新 langfuse 文档 2026-04-11 23:53:21 +08:00
claude-code-best
c8a502f81f chore: 删除重复 feature 定义 2026-04-11 23:27:28 +08:00
claude-code-best
09fc515edb feat: 远程群控 (#243)
* feat: restore pipe IPC, LAN pipes, monitor tool, and PR-package features

Core IPC system (UDS_INBOX):
- PipeServer/PipeClient with UDS + TCP dual transport, NDJSON protocol
- PipeRegistry: machineId-based role assignment, file locking
- Master/slave attach, prompt relay, permission forwarding
- Heartbeat lifecycle with parallel isPipeAlive probes
- Commands: /pipes, /attach, /detach, /send, /claim-main, /pipe-status

LAN Pipes (LAN_PIPES):
- UDP multicast beacon (224.0.71.67:7101) for zero-config LAN discovery
- PipeServer TCP listener, PipeClient TCP connect mode
- Heartbeat auto-attaches LAN peers via TCP
- Cross-machine attach allowed regardless of role
- /pipes shows [LAN] peers with role + hostname/IP
- SendMessageTool supports tcp: scheme with user consent

Architecture — extracted hooks from REPL.tsx (~830 lines → ~20 lines):
- usePipeIpc: lifecycle (bootstrap, handlers, heartbeat, cleanup)
- usePipeRelay: slave→master message relay via module singleton
- usePipePermissionForward: permission request/cancel forwarding
- usePipeRouter: selected pipe input routing with role+IP labels
- Shared ndjsonFramer.ts replaces 3 duplicate NDJSON parsers

Key fixes applied during development:
- Multicast binds to correct LAN interface (not WSL/Docker)
- Beacon ref stored as module singleton (not Zustand state mutation)
- Heartbeat preserves LAN peers in discoveredPipes and selectedPipes
- Disconnect handler calls removeSlaveClient (fixes listener leak)
- cleanupStaleEntries probes without lock, writes briefly under lock
- getMachineId uses async execFile (not blocking execSync)
- globalThis.__pipeSendToMaster replaced with setPipeRelay singleton
- M key only toggles route mode when selector panel is expanded
- User prompt displayed in message list on pipe broadcast
- Broadcast notifications show [role] + hostname/IP for LAN peers

Other restored features:
- Monitor tool: /monitor command, MonitorTool, MonitorMcpTask lifecycle
- Daemon supervisor and remoteControlServer command
- Tools: SnipTool, SleepTool, ListPeersTool, SendUserFileTool,
  WebBrowserTool, WorkflowTool, and 10+ stub→implementation rewrites
- Feature flags: UDS_INBOX, LAN_PIPES, MONITOR_TOOL, FORK_SUBAGENT,
  KAIROS, COORDINATOR_MODE, WORKFLOW_SCRIPTS, HISTORY_SNIP

Tests: 2190 pass / 0 fail (15 new: lanBeacon 7, peerAddress 8)

* fix: resolve merge conflicts and fix all tsc/test errors after main merge

- Export ToolResultBlockParam from Tool.ts (14 tool files fixed)
- Migrate ink imports from ../../ink.js to @anthropic/ink (7 files)
- Fix toolUseID → toolUseId typo in monitor.ts and MonitorTool.tsx
- Add fallback values for string|undefined type errors (8 locations)
- Fix AppState type in assistant.ts, add NewInstallWizard stubs
- Fix ParsedRepository.repo → .name in subscribe-pr.ts
- Fix AgentId/string type mismatch in BackgroundTasksDialog.tsx
- Fix PipeRelayFn return type in pipePermissionRelay.ts
- Use PipeMessage type in usePipeRelay.ts
- Fix lanBeacon.test.ts mock type assertions
- Create missing MouseActionEvent class for ink package
- Use ansi: color format instead of bare "green"/"red"
- Resolve theme.permission access via getTheme()

Result: 0 tsc errors, 2496 tests pass, 0 fail

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 恢复 /poor 的说明

---------

Co-authored-by: unraid <local@unraid.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 23:22:55 +08:00
claude-code-best
2fea429dc6 feat: 添加对 langfuse 监控的支持 (#242)
* docs: 更新类型检查的 CLAUDE.md

* feat: 添加模型 1M 上下文切换

* chore: remove prefetchOfficialMcpUrls call on startup

* docs: 添加 git commit 规范

* feat: 第一次接入 langfuse

* fix: 修复 generation 的计时的错误

* feat: 添加多 agent 的监控

* feat: 添加 /poor 省流模式,toggle 关闭 extract_memories 和 prompt_suggestion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 修复 lock 文件

* chore: 更新类型依赖

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 22:07:38 +08:00
claude-code-best
6a9da9d546 docs: 添加 git commit 规范 2026-04-11 15:13:41 +08:00
claude-code-best
d27c6cbc64 chore: remove prefetchOfficialMcpUrls call on startup 2026-04-11 15:13:41 +08:00
claude-code-best
ffd1c366eb feat: 添加模型 1M 上下文切换 2026-04-11 15:13:41 +08:00
claude-code-best
5beeebad59 docs: 更新类型检查的 CLAUDE.md 2026-04-11 15:13:41 +08:00
claude-code-best
c676ac4693 docs: update contributors 2026-04-11 05:19:38 +00:00
claude-code-best
eeb0f2776e docs: update contributors 2026-04-11 02:24:22 +00:00
claude-code-best
6a70056910 feat: 全部类型问题解决 2026-04-11 10:24:00 +08:00
claude-code-best
7088fe3c8b Merge remote-tracking branch 'guunergooner/fix/openai-stop-reason-usage' 2026-04-10 22:27:16 +08:00
claude-code-best
b060eabda9 chore: 添加类型测试 2026-04-10 22:24:17 +08:00
sobird
9da7345f8e Add Ultraplan Feature for Advanced Multi-Agent Planning (#232)
* feat: add ultraplan feature for advanced multi-agent planning

Implement ultraplan command with web-based planning interface,
supporting multiple prompt modes and interactive plan approval.

* chore: add semi

* chore: add semi
2026-04-10 22:17:00 +08:00
claude-code-best
8137b66a46 fix: 修复初次登陆的校验问题 2026-04-10 22:06:02 +08:00
claude-code-best
b681139b63 docs: update contributors 2026-04-10 10:14:34 +00:00
claude-code-best
0b1e678fb7 fix: 修复 mintlify ignore, 修复侧边栏 2026-04-10 18:14:07 +08:00
CyberScrubber
81073135e2 docs: 审校 Agent 文档术语与架构描述准确性(docs/agent) (#231)
基于源码验证和官方文档对照,修正三份 Agent 文档的内容准确性:

coordinator-and-swarm.mdx:
- 对齐官方术语:Leader→Team Lead, teammate→Teammate, 引入 Mailbox 消息系统
- 修正 Swarm 门控为 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1(原文"默认启用"不准确)
- 拓扑描述改为"星型+P2P混合"(官方有 Team Lead 角色,非纯网状)
- 新增架构组件表、Mailbox 消息系统、Hook 事件(TeammateIdle/TaskCreated/TaskCompleted)、限制说明
- 新增持久化存储路径(~/.claude/teams/, ~/.claude/tasks/)
- 移除虚构的"高水位标记"(claimTask 仅使用文件锁)
- 修正 Scratchpad 描述(Workers 获得,非 Coordinator 拥有)
- 修复引号不匹配

sub-agents.mdx:
- 修正为三路径路由(命名 Agent / Fork / GP 回退),原文仅两种
- 补充 isForkSubagentEnabled() 三前提(feature flag + 非Coordinator + 非非交互式)
- 新增模型解析优先级(4级链 + inherit 运行时解析)
- 新增内置 Agent 表(Explore/Plan/GP/statusline-setup/claude-code-guide)
- 新增 Hook 事件表(frontmatter + settings.json 双级别)
- 补充异步生命周期完整链路(finalizeAgentTool → getWorktreeResult)
- 补充 transcript 存储路径和结果格式细节

worktree-isolation.mdx:
- 区分 createWorktreeForSession vs createAgentWorktree 两种函数
- 修正清理机制为 cleanupWorktreeIfNeeded(非 ExitWorktreeTool 手动操作)
- 补充 usedSparsePaths 字段
2026-04-10 18:00:42 +08:00
claude-code-best
ff03fe7fcb fix: 修复类型问题 2026-04-10 17:34:01 +08:00
guunergooner
c82f59943c fix(openai): fix stop_reason null, zero usage fields and max_tokens forwarding
- Fix stop_reason always null in assembled AssistantMessage by applying
  the value captured from message_delta event
- Reset partialMessage to null after message_stop to prevent duplicate
  AssistantMessage emission causing doubled content in next API request
- Forward computed maxTokens into buildOpenAIRequestBody as max_tokens
  so OpenAI-compatible endpoints receive the intended output cap
- Extract assembleFinalAssistantOutputs helper to deduplicate message
  assembly logic between message_stop handler and post-loop fallback
- Fix test helper to use events parameter instead of hidden global
- Add regression test for max_tokens request forwarding

Signed-off-by: guunergooner <tongchao0923@gmail.com>
2026-04-10 12:17:52 +08:00
claude-code-best
e70319e8f5 docs: 更新远程控制及 rg 下载 2026-04-10 12:11:04 +08:00
claude-code-best
609e91143f docs: 尝试 docs 文档更新 2026-04-10 10:15:19 +08:00
claude-code-best
637531f81f fix(types): simplify bridge transport message type
Replace StdoutMessageWithSession conditional type with simpler
TransportMessage intersection type. The conditional type was
over-engineered for what is just StdoutMessage & { session_id? }.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 23:55:54 +08:00
claude-code-best
875510e1eb docs: update CLAUDE.md with architecture details and type safety guidelines
- Update build/dev mode descriptions to reflect current feature flags
- Add workspace packages table and multi-API compatibility layer docs
- Add Type Safety Guidelines section: prohibit `as any` in non-test code,
  document proper alternatives (Record<string, unknown>, dual assertion,
  type guards, specific SDK types)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 23:55:05 +08:00
claude-code-best
34bbc1d403 fix(types): replace all as any with proper type assertions
Eliminate unsafe `as any` casts across 21 non-test source files,
replacing them with specific type annotations:

- Bridge transport: use StdoutMessage type for write/writeBatch calls
- print.ts: type msg.request as Record<string, unknown> for unknown
  SDK control subtypes; use StdoutMessage for output.enqueue()
- API providers (openai/grok/gemini): import ChatCompletion types,
  type streams as AsyncIterable<ChatCompletionChunk>, type request
  bodies as ChatCompletionCreateParamsStreaming
- Computer use executor: use Partial<ResolvePrepareCaptureResult>
  for cross-platform screenshot result
- Components: replace Ink color string casts with proper typing
- Win32 bridge: type stdin as Writable after null check

All 2453 tests pass with 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 23:51:33 +08:00
claude-code-best
a14d3dc8f0 fix(types): clean type fixes across 92 files
Apply proper TypeScript type corrections without any unsafe casts:
- Fix unknown/never/{} types from decompilation
- Correct function signatures and parameter types
- Add missing type declarations and interfaces
- Fix Ink component prop types
- Update API client/provider type annotations

Test files with mock data casts are included as-is (acceptable pattern).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 23:45:56 +08:00
claude-code-best
ab3d8ef87e docs: update contributors 2026-04-09 14:04:36 +00:00
claude-code-best
dfce6d02f9 docs: 更新私有部署文档 2026-04-09 22:03:54 +08:00
claude-code-best
01cf45f4ac fix: 修复 permission 面板 2026-04-09 22:03:54 +08:00
HitMargin
e6affc7053 1. 修复 src/commands/fork/index.ts — 将空 stub 改为有效的 Command 定义 2. 新增 src/commands/fork/fork.tsx — 完整实现 /fork 命令,复用了 AgentTool 的 fork 子 agent 逻辑 (#221)
* 1. 修复 src/commands/fork/index.ts — 将空 stub 改为有效的 Command 定义
  2. 新增 src/commands/fork/fork.tsx — 完整实现 /fork 命令,复用了 AgentTool 的 fork 子 agent 逻辑

* Update src/commands/fork/fork.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix

---------

Co-authored-by: HitMargin <hitmargin@qq.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-09 21:53:07 +08:00
Alan
bb07836231 fix: support CRLF SSE frame parsing (#223) 2026-04-09 21:52:28 +08:00
claude-code-best
87230cf3bf docs: update contributors 2026-04-09 12:47:35 +00:00
claude-code-best
8c619a215c build: 修复构建报错 2026-04-09 20:47:13 +08:00
claude-code-best
2b0d31aaca feat: 对齐构建时的目标 2026-04-09 20:30:53 +08:00
bonerush
562e9daadd fix: Handle undefined command names in getCommandName function (#217)
* fix: reorder tool and user messages for OpenAI API compatibility (#168)
Fixes #168
OpenAI requires that an assistant message with tool_calls be immediately
followed by tool messages. Previously, convertInternalUserMessage
output user content before tool results, causing 400 errors.
Now tool messages are pushed first.

* fix: 修复OpenAI兼容层中deferred tools处理问题

  提交描述:
  修复了在使用OpenAI兼容API时TaskCreate工具调用失败的问题。

  问题:
  - 当使用OpenAI兼容API模型时,调用TaskCreate工具出现"InputValidationError: The required
  parameter `subject` is missing"错误
  - OpenAI兼容层没有正确处理deferred tools的过滤逻辑,导致工具schema没有被正确发送给模型

  修复:
  1. 在OpenAI兼容层中添加了与Anthropic API路径一致的deferred tools处理逻辑
  2. 导入必要的工具搜索相关函数: isToolSearchEnabled, extractDiscoveredToolNames,
  isDeferredTool等
  3. 实现工具过滤逻辑:
     - 检查工具搜索是否启用
     - 构建deferred tools集合
     - 过滤工具列表: 只包含非deferred工具或已发现的deferred工具
     - 为deferred tools设置deferLoading标志
  4. 修正了extractDiscoveredToolNames函数的导入路径错误

  影响:
  - 解决了TaskCreate工具调用时的参数验证错误
  - 确保OpenAI兼容层与Anthropic API路径在处理deferred tools时行为一致
  - 支持工具搜索功能在OpenAI兼容模式下正常工作

  修改的文件:
  - src/services/api/openai/index.ts - 主要修复文件

  测试建议:
  1. 使用OpenAI兼容API模型时,TaskCreate工具应该可以正常调用
  2. 如果工具搜索功能启用,可能需要先使用ToolSearchTool来发现TaskCreate工具
  3. 验证工具调用时不再出现"InputValidationError"错误

  这个修复确保了当使用OpenAI兼容API(如Ollama、DeepSeek、vLLM等)时,deferred
  tools(如TaskCreate)能够被正确处理,解决了工具调用失败的问题。

* fix: 更新未发送工具架构提示,提供OpenAI兼容模型的使用步骤

* fix: Handle undefined command names in getCommandName function

- Modified getCommandName in src/types/command.ts to return empty string instead of undefined when cmd.name is undefined
- Added null checks in src/hooks/useTypeahead.tsx to safely handle command names
- Prevents "undefined is not an object" error when FEATURE_BUDDY=1 and FEATURE_FORK_SUBAGENT=1 are enabled

The error occurred because getCommandName(cmd) could return undefined when cmd.name was undefined, causing .length access to fail.
2026-04-09 17:53:43 +08:00
CyberScrubber
8b2532a9c1 docs: fix documentation deviations from source code (#220)
* docs: 修正 docs/conversation 文档与源码的偏差(multi-turn/streaming/the-loop)

- multi-turn: TranscriptWriter→Project 私有类, 会话路径改用 sanitized-cwd,
  补充 StoredCostState.lastDuration 字段, 模型切换改为 setModel(),
  QueryEngine 状态补全 loadedNestedMemoryPaths/hasHandledOrphanedPermission,
  行号改为符号引用
- streaming: STALL_THRESHOLD_MS 10s→30s, 新增 90s 主动空闲看门狗描述,
  非流式降级补充 didFallBackToNonStreaming/executeNonStreamingRequest,
  行号改为符号引用
- the-loop: 终止条件 7→11, 继续条件重整为 5 组层级结构,
  max_output_tokens 拆分 escalate/recovery 子阶段,
  prompt-too-long 拆分 collapse_drain/reactive_compact 子策略,
  State 类型修正 autoCompactTracking 为可选, 行号改为符号引用
- 全部: 添加 sourceRef 版本锚定(3ec5675)

* docs: 修正 docs/extensibility 文档与源码的偏差(custom-agents/hooks/skills)

- custom-agents: Verification 模型修正为 inherit, 补充 Plugin Agent 字段限制
  (permissionMode/hooks/mcpServers 被安全忽略, isolation 仅 worktree),
  加载流程修正为 6 层优先级, 补充 memory snapshot 门控条件
- hooks: 事件数 22→27(补充 Notification), Hook 类型定义位置修正为 3 个文件,
  行号改为符号引用, Zod schema 范围修正, 去重键修正为四部分复合键,
  registerFrontmatterHooks/clearSessionHooks 区分定义位置和调用位置
- skills: 字段数 17→16, 权限层级 4→5(补充 remote canonical auto-allow),
  SAFE_SKILL_PROPERTIES 28→30, skillUsageTracking 路径修正,
  行号改为符号引用
- mcp-protocol: 全部验证通过, 无需修改
- 全部: 添加 sourceRef 版本锚定(3ec5675)

* Revert "docs: 修正 docs/extensibility 文档与源码的偏差(custom-agents/hooks/skills)"

* docs: 修正 docs/extensibility 文档与源码的偏差(hooks/skills/mcp-protocol)

hooks:
- 事件数 22→27(补充 Notification 事件)
- Hook 类型定义位置修正为 3 个文件分布
  (schemas/hooks.ts / types/hooks.ts / utils/hooks/sessionHooks.ts)
- Zod schema 引用从硬编码行号改为符号引用
- hookSpecificOutput 表从 6 扩展至 15 个事件
  (补全 permissionDecisionReason / PostToolUseFailure / SubagentStart 等)
- 去重键从 pluginRoot\0command 修正为四部分复合键
  (pluginRoot\0shell\0command\0ifCondition)
- 全部硬编码行号改为符号引用以避免版本漂移

skills:
- parseSkillFrontmatterFields 字段数 17→16
- SAFE_SKILL_PROPERTIES 属性数 28→30
- checkPermissions 层级 4→5
- 第 2 层描述从"官方市场"修正为"远程 canonical"

mcp-protocol:
- 配置层级从"三级"修正为
  "enterprise 独占或合并 user/project/local + plugin + claude.ai"

* docs: 修正 system-prompt.mdx 中 Boundary 章节的层级与可读性

- Boundary 插入条件从 ### 降为 blockquote,不再打断三种分块模式的并列结构
- 表格中 Boundary 缓存策略列补充说明其分割作用
- 新增 Boundary 概念释义(blockquote),解释其分割静态区/动态区以实现全局缓存的设计意图
2026-04-09 17:53:11 +08:00
1128 changed files with 51476 additions and 15331 deletions

View File

@@ -8,7 +8,7 @@ on:
jobs:
ci:
runs-on: macos-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -20,8 +20,19 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Test
run: bun test
- name: Type check
run: bunx tsc --noEmit
- name: Test with Coverage
run: |
set -o pipefail
bun test --coverage --coverage-reporter=lcov 2>&1 | grep -vE '^\s*(\(pass\)|\(skip\))' | sed '/^.*\/__tests__\/.*:$/d' | cat -s
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
file: ./coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
- name: Build
run: bun run build
run: bun run build:vite

13
.gitignore vendored
View File

@@ -12,9 +12,9 @@ src/utils/vendor/
# AI tool runtime directories
.agents/
.codex/
.claude/
.omx/
.docs/task/
# Binary / screenshot files (root only)
/*.png
*.bmp
@@ -29,3 +29,12 @@ __pycache__/
logs
data
.omc
.codex/*
!.codex/agents/
!.codex/agents/**
!.codex/skills/
!.codex/skills/**
.codex/skills/.system/**
!.codex/prompts/
!.codex/prompts/**

2
.mintignore Normal file
View File

@@ -0,0 +1,2 @@
src/
packages/

238
CLAUDE.md
View File

@@ -4,7 +4,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
This is a **reverse-engineered / decompiled** version of Anthropic's official Claude Code CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. The codebase has ~1341 tsc errors from decompilation (mostly `unknown`/`never`/`{}` types) — these do **not** block Bun runtime execution.
This is a **reverse-engineered / decompiled** version of Anthropic's official Claude Code CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. TypeScript strict mode is enforced — **`bunx tsc --noEmit` must pass with zero errors**.
## Git Commit Message Convention
使用 **Conventional Commits** 规范:
```
<type>: <描述>
```
常见 type`feat``fix``docs``chore``refactor`
示例:
- `feat: 添加模型 1M 上下文切换`
- `fix: 修复初次登陆的校验问题`
- `chore: remove prefetchOfficialMcpUrls call on startup`
## Commands
@@ -21,11 +36,11 @@ bun run dev:inspect
# Pipe mode
echo "say hello" | bun run src/entrypoints/cli.tsx -p
# Build (code splitting, outputs dist/cli.js + ~450 chunk files)
# Build (code splitting, outputs dist/cli.js + chunk files)
bun run build
# Test
bun test # run all tests
bun test # run all tests (2453 tests / 137 files / 0 fail)
bun test src/utils/__tests__/hash.test.ts # run single file
bun test --coverage # with coverage report
@@ -40,6 +55,11 @@ bun run health
# Check unused exports
bun run check:unused
bun run typecheck
# Remote Control Server
bun run rcs
# Docs dev server (Mintlify)
bun run docs:dev
```
@@ -51,26 +71,30 @@ bun run docs:dev
### Runtime & Build
- **Runtime**: Bun (not Node.js). All imports, builds, and execution use Bun APIs.
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。默认启用 `AGENT_TRIGGERS_REMOTE``CHICAGO_MCP``VOICE_MODE` feature。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines运行 `src/entrypoints/cli.tsx`。默认启用 `BUDDY``TRANSCRIPT_CLASSIFIER``BRIDGE_MODE``AGENT_TRIGGERS_REMOTE``CHICAGO_MCP``VOICE_MODE` 六个 feature。
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。Build 默认启用 19 个 feature见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
- **Monorepo**: Bun workspaces — internal packages live in `packages/` resolved via `workspace:*`.
- **Monorepo**: Bun workspaces — 14 个 internal packages in `packages/` resolved via `workspace:*`
- **Lint/Format**: Biome (`biome.json`)。`bun run lint` / `bun run lint:fix` / `bun run format`
- **Defines**: 集中管理在 `scripts/defines.ts`。当前版本 `2.1.888`
- **CI**: GitHub Actions — `ci.yml`(构建+测试)、`release-rcs.yml`RCS 发布)、`update-contributors.yml`(自动更新贡献者)。
### Entry & Bootstrap
1. **`src/entrypoints/cli.tsx`** — True entrypoint。`main()` 函数按优先级处理多条快速路径:
1. **`src/entrypoints/cli.tsx`** (323 行) — True entrypoint。`main()` 函数按优先级处理多条快速路径:
- `--version` / `-v` — 零模块加载
- `--dump-system-prompt` — feature-gated (DUMP_SYSTEM_PROMPT)
- `--claude-in-chrome-mcp` / `--chrome-native-host`
- `--computer-use-mcp` — 独立 MCP server 模式
- `--daemon-worker=<kind>` — feature-gated (DAEMON)
- `remote-control` / `rc` / `bridge` — feature-gated (BRIDGE_MODE)
- `daemon` — feature-gated (DAEMON)
- `remote-control` / `rc` / `remote` / `sync` / `bridge` — feature-gated (BRIDGE_MODE)
- `daemon` [subcommand] — feature-gated (DAEMON)
- `ps` / `logs` / `attach` / `kill` / `--bg` — feature-gated (BG_SESSIONS)
- `new` / `list` / `reply` — Template job commands
- `environment-runner` / `self-hosted-runner` — BYOC runner
- `--tmux` + `--worktree` 组合
- 默认路径:加载 `main.tsx` 启动完整 CLI
2. **`src/main.tsx`** (~4680 行) — Commander.js CLI definition。注册大量 subcommands`mcp` (serve/add/remove/list...)、`server``ssh``open``auth``plugin``agents``auto-mode``doctor``update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。
2. **`src/main.tsx`** (~6970 行) — Commander.js CLI definition。注册大量 subcommands`mcp` (serve/add/remove/list...)、`server``ssh``open``auth``plugin``agents``auto-mode``doctor``update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。
3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog)。
### Core Loop
@@ -82,21 +106,28 @@ bun run docs:dev
### API Layer
- **`src/services/api/claude.ts`** — Core API client. Builds request params (system prompt, messages, tools, betas), calls the Anthropic SDK streaming endpoint, and processes `BetaRawMessageStreamEvent` events.
- Supports multiple providers: Anthropic direct, AWS Bedrock, Google Vertex, Azure.
- Provider selection in `src/utils/model/providers.ts`.
- **7 providers**: `firstParty` (Anthropic direct), `bedrock` (AWS), `vertex` (Google Cloud), `foundry`, `openai`, `gemini`, `grok` (xAI)。
- Provider selection in `src/utils/model/providers.ts`。优先级modelType 参数 > 环境变量 > 默认 firstParty。
### Tool System
- **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`).
- **`src/tools.ts`** — Tool registry. Assembles the tool list; some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
- **`src/tools/<ToolName>/`** — 61 个 tool 目录(如 BashTool, FileEditTool, GrepTool, AgentTool, WebFetchTool, LSPTool, MCPTool 等)。每个 tool 包含 `name``description``inputSchema``call()` 及可选的 React 渲染组件
- **`src/tools.ts`** (387 行) — Tool registry. Assembles the tool list; some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
- **`src/tools/<ToolName>/`** — 55 个 tool 目录。主要分类:
- **文件操作**: FileEditTool, FileReadTool, FileWriteTool, GlobTool, GrepTool
- **Shell/执行**: BashTool, PowerShellTool, REPLTool
- **Agent 系统**: AgentTool, TaskCreateTool, TaskUpdateTool, TaskListTool, TaskGetTool
- **规划**: EnterPlanModeTool, ExitPlanModeV2Tool, VerifyPlanExecutionTool
- **Web/MCP**: WebFetchTool, WebSearchTool, MCPTool, McpAuthTool
- **调度**: CronCreateTool, CronDeleteTool, CronListTool
- **其他**: LSPTool, ConfigTool, SkillTool, EnterWorktreeTool, ExitWorktreeTool 等
- **`src/tools/shared/`** — Tool 共享工具函数。
### UI Layer (Ink)
- **`src/ink.ts`** — Ink render wrapper with ThemeProvider injection.
- **`src/ink/`** — Custom Ink framework (forked/internal): custom reconciler, hooks (`useInput`, `useTerminalSize`, `useSearchHighlight`), virtual list rendering.
- **`src/components/`** — 大量 React 组件170+ 项),渲染于终端 Ink 环境中。关键组件:
- **`packages/@ant/ink/`** — Custom Ink frameworkforked/internal),包含 components、core、hooks、keybindings、theme、utils。注意不是 `src/ink/`
- **`src/components/`** — 149 个组件目录/文件,渲染于终端 Ink 环境中。关键组件:
- `App.tsx` — Root provider (AppState, Stats, FpsMetrics)
- `Messages.tsx` / `MessageRow.tsx` — Conversation message rendering
- `PromptInput/` — User input handling
@@ -112,10 +143,30 @@ bun run docs:dev
- **`src/state/selectors.ts`** — State selectors.
- **`src/bootstrap/state.ts`** — Module-level singletons for session-global state (session ID, CWD, project root, token counts, model overrides, client type, permission mode).
### Workspace Packages
| Package | 说明 |
|---------|------|
| `packages/@ant/ink/` | Forked Ink 框架components、hooks、keybindings、theme |
| `packages/@ant/computer-use-mcp/` | Computer Use MCP server截图/键鼠/剪贴板/应用管理) |
| `packages/@ant/computer-use-input/` | 键鼠模拟dispatcher + darwin/win32/linux backend |
| `packages/@ant/computer-use-swift/` | 截图 + 应用管理dispatcher + per-platform backend |
| `packages/@ant/claude-for-chrome-mcp/` | Chrome 浏览器控制(通过 `--chrome` 启用) |
| `packages/remote-control-server/` | 自托管 Remote Control ServerDocker 部署,含 Web UI |
| `packages/swarm/` | Swarm 解耦模块 |
| `packages/shell/` | Shell 抽象 |
| `packages/audio-capture-napi/` | 原生音频捕获(已恢复) |
| `packages/color-diff-napi/` | 颜色差异计算完整实现11 tests |
| `packages/image-processor-napi/` | 图像处理(已恢复) |
| `packages/modifiers-napi/` | 键盘修饰键检测stub |
| `packages/url-handler-napi/` | URL scheme 处理stub |
### Bridge / Remote Control
- **`src/bridge/`** (~35 files) — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`
- **`src/bridge/`** (~37 files) — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`
- **`packages/remote-control-server/`** — 自托管 RCS支持 Docker 部署,含 Web UI 控制面板。通过 `bun run rcs` 启动。
- CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`
- 详见 `docs/features/remote-control-self-hosting.md`
### Daemon Mode
@@ -128,91 +179,64 @@ bun run docs:dev
### Feature Flag System
Feature flags control which functionality is enabled at runtime:
Feature flags control which functionality is enabled at runtime. 代码中统一通过 `import { feature } from 'bun:bundle'` 导入,调用 `feature('FLAG_NAME')` 返回 `boolean`
- **在代码中使用**: 统一通过 `import { feature } from 'bun:bundle'` 导入,调用 `feature('FLAG_NAME')` 返回 `boolean`。**不要**在 `cli.tsx` 或其他文件里自己定义 `feature` 函数或覆盖这个 import
- **启用方式**: 通过环境变量 `FEATURE_<FLAG_NAME>=1`。例如 `FEATURE_BUDDY=1 bun run dev` 启用 BUDDY 功能。
- **Dev 默认 features**: `BUDDY``TRANSCRIPT_CLASSIFIER``BRIDGE_MODE``AGENT_TRIGGERS_REMOTE``CHICAGO_MCP``VOICE_MODE`(见 `scripts/dev.ts`
- **Build 默认 features**: `AGENT_TRIGGERS_REMOTE``CHICAGO_MCP``VOICE_MODE`(见 `build.ts`)。
- **常见 flag**: `BUDDY`, `DAEMON`, `BRIDGE_MODE`, `BG_SESSIONS`, `PROACTIVE`, `KAIROS`, `VOICE_MODE`, `FORK_SUBAGENT`, `SSH_REMOTE`, `DIRECT_CONNECT`, `TEMPLATES`, `CHICAGO_MCP`, `BYOC_ENVIRONMENT_RUNNER`, `SELF_HOSTED_RUNNER`, `COORDINATOR_MODE`, `UDS_INBOX`, `LODESTONE`, `ABLATION_BASELINE` 等。
- **类型声明**: `src/types/internal-modules.d.ts` 中声明了 `bun:bundle` 模块的 `feature` 函数签名。
**启用方式**: 环境变量 `FEATURE_<FLAG_NAME>=1`。例如 `FEATURE_BUDDY=1 bun run dev`
**Build 默认 features**19 个,见 `build.ts`:
- 基础: `BUDDY`, `TRANSCRIPT_CLASSIFIER`, `BRIDGE_MODE`, `AGENT_TRIGGERS_REMOTE`, `CHICAGO_MCP`, `VOICE_MODE`
- 统计/缓存: `SHOT_STATS`, `PROMPT_CACHE_BREAK_DETECTION`, `TOKEN_BUDGET`
- P0 本地: `AGENT_TRIGGERS`, `ULTRATHINK`, `BUILTIN_EXPLORE_PLAN_AGENTS`, `LODESTONE`
- P1 API 依赖: `EXTRACT_MEMORIES`, `VERIFICATION_AGENT`, `KAIROS_BRIEF`, `AWAY_SUMMARY`, `ULTRAPLAN`
- P2: `DAEMON`
**Dev mode 默认**: 全部启用(见 `scripts/dev.ts`)。
**类型声明**: `src/types/internal-modules.d.ts` 中声明了 `bun:bundle` 模块的 `feature` 函数签名。
**新增功能的正确做法**: 保留 `import { feature } from 'bun:bundle'` + `feature('FLAG_NAME')` 的标准模式,在运行时通过环境变量或配置控制,不要绕过 feature flag 直接 import。
### Multi-API 兼容层
所有兼容层均采用流适配器模式:将第三方 API 格式转为 Anthropic 内部格式,下游代码完全不改。
#### OpenAI 兼容层
通过 `CLAUDE_CODE_USE_OPENAI=1` 启用,支持 Ollama/DeepSeek/vLLM 等任意 OpenAI Chat Completions 协议端点。含 DeepSeek thinking mode 支持。
- **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射
- 关键环境变量:`CLAUDE_CODE_USE_OPENAI``OPENAI_API_KEY``OPENAI_BASE_URL``OPENAI_MODEL`
#### Gemini 兼容层
通过 `CLAUDE_CODE_USE_GEMINI=1` 启用。独立环境变量体系。
- **`src/services/api/gemini/`** — client、模型映射、类型定义
- 关键环境变量:`GEMINI_API_KEY`(必填)、`GEMINI_MODEL`(直接指定)、`GEMINI_DEFAULT_SONNET_MODEL`/`GEMINI_DEFAULT_OPUS_MODEL`(按能力映射)
- 模型映射优先级:`GEMINI_MODEL` > `GEMINI_DEFAULT_*_MODEL` > `ANTHROPIC_DEFAULT_*_MODEL`(已废弃) > 原样返回
#### Grok 兼容层
通过 `CLAUDE_CODE_USE_GROK=1` 启用。自定义模型映射支持 xAI Grok API。
- **`src/services/api/grok/`** — client、模型映射
详见各兼容层的 docs 文档。
### Stubbed/Deleted Modules
| Module | Status |
|--------|--------|
| Computer Use (`@ant/*`) | Restored — `computer-use-swift`, `computer-use-input`, `computer-use-mcp`, `claude-for-chrome-mcp` 均有完整实现,macOS + Windows 可用,Linux 后端待完成 |
| `*-napi` packages | `audio-capture-napi``image-processor-napi` 已恢复实现`color-diff-napi` 完整实现`url-handler-napi``modifiers-napi` 仍为 stub |
| Voice Mode | Restored — `src/voice/``src/hooks/useVoiceIntegration.tsx``src/services/voiceStreamSTT.ts` 等,Push-to-Talk 语音输入(需 Anthropic OAuth |
| OpenAI 兼容层 | Restored`src/services/api/openai/`,支持 Ollama/DeepSeek/vLLM 等任意 OpenAI 协议端点,通过 `CLAUDE_CODE_USE_OPENAI=1` 启用 |
| Computer Use (`@ant/*`) | Restored — macOS + Windows + Linux后端完整度不一) |
| `*-napi` packages | `audio-capture-napi``image-processor-napi` 已恢复;`color-diff-napi` 完整;`modifiers-napi``url-handler-napi` 仍为 stub |
| Voice Mode | Restored — Push-to-Talk 语音输入(需 Anthropic OAuth |
| OpenAI/Gemini/Grok 兼容层 | Restored |
| Remote Control Server | Restored — 自托管 RCS + Web UI |
| Analytics / GrowthBook / Sentry | Empty implementations |
| Magic Docs / LSP Server | Removed |
| Plugins / Marketplace | Removed |
| MCP OAuth | Simplified |
### Computer Use
Feature flag `CHICAGO_MCP`dev/build 默认启用。实现跨平台屏幕操控macOS + Windows 可用Linux 待完成)。
- **`packages/@ant/computer-use-mcp/`** — MCP server注册截图/键鼠/剪贴板/应用管理工具
- **`packages/@ant/computer-use-input/`** — 键鼠模拟dispatcher + per-platform backend`backends/darwin.ts``win32.ts``linux.ts`
- **`packages/@ant/computer-use-swift/`** — 截图 + 应用管理,同样 dispatcher + per-platform backend
- **`packages/@ant/claude-for-chrome-mcp/`** — Chrome 浏览器控制(独立于 Computer Use通过 `--chrome` CLI 参数启用)
详见 `docs/features/computer-use.md`
### Voice Mode
Feature flag `VOICE_MODE`dev/build 默认启用。Push-to-Talk 语音输入,音频通过 WebSocket 流式传输到 Anthropic STTNova 3。需要 Anthropic OAuth非 API key
- **`src/voice/voiceModeEnabled.ts`** — 三层门控feature flag + GrowthBook + OAuth auth
- **`src/hooks/useVoice.ts`** — React hook 管理录音状态和 WebSocket 连接
- **`src/services/voiceStreamSTT.ts`** — STT WebSocket 流式传输
详见 `docs/features/voice-mode.md`
### OpenAI 兼容层
通过 `CLAUDE_CODE_USE_OPENAI=1` 环境变量启用,支持任意 OpenAI Chat Completions 协议端点Ollama、DeepSeek、vLLM 等)。流适配器模式:在 `queryModel()` 中将 Anthropic 格式请求转为 OpenAI 格式,再将 SSE 流转换回 `BetaRawMessageStreamEvent`,下游代码完全不改。
- **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射
- **`src/utils/model/providers.ts`** — 添加 `'openai'` provider 类型(最高优先级)
关键环境变量:`CLAUDE_CODE_USE_OPENAI``OPENAI_API_KEY``OPENAI_BASE_URL``OPENAI_MODEL``OPENAI_DEFAULT_OPUS_MODEL``OPENAI_DEFAULT_SONNET_MODEL``OPENAI_DEFAULT_HAIKU_MODEL`。详见 `docs/plans/openai-compatibility.md`
### Gemini 兼容层
通过 `CLAUDE_CODE_USE_GEMINI=1` 环境变量或 `modelType: "gemini"` 设置启用,支持 Google Gemini API。独立的环境变量体系不与 OpenAI 或 Anthropic 配置混杂。
- **`src/services/api/gemini/`** — client、模型映射、类型定义
- **`src/utils/model/providers.ts`** — 添加 `'gemini'` provider 类型
- **`src/utils/managedEnvConstants.ts`** — Gemini 专用的 managed env vars
关键环境变量:
- `CLAUDE_CODE_USE_GEMINI` - 启用 Gemini provider
- `GEMINI_API_KEY` - API 密钥(必填)
- `GEMINI_BASE_URL` - API 端点(可选,默认 `https://generativelanguage.googleapis.com/v1beta`
- `GEMINI_MODEL` - 直接指定模型(最高优先级)
- `GEMINI_DEFAULT_HAIKU_MODEL` / `GEMINI_DEFAULT_SONNET_MODEL` / `GEMINI_DEFAULT_OPUS_MODEL` - 按能力级别映射
- `GEMINI_DEFAULT_HAIKU_MODEL_NAME` / `DESCRIPTION` / `SUPPORTED_CAPABILITIES` - 显示名称和描述
- `GEMINI_SMALL_FAST_MODEL` - 快速任务使用的模型(可选)
模型映射优先级(`src/services/api/gemini/modelMapping.ts`
1. `GEMINI_MODEL` - 直接覆盖
2. `GEMINI_DEFAULT_*_MODEL` - 独立配置(推荐)
3. `ANTHROPIC_DEFAULT_*_MODEL` - 向后兼容 fallback已废弃
4. 原样返回 Anthropic 模型名
使用示例:
```bash
export CLAUDE_CODE_USE_GEMINI=1
export GEMINI_API_KEY="your-api-key"
export GEMINI_DEFAULT_SONNET_MODEL="gemini-2.5-flash"
export GEMINI_DEFAULT_OPUS_MODEL="gemini-2.5-pro"
```
### Key Type Files
- **`src/types/global.d.ts`** — Declares `MACRO`, `BUILD_TARGET`, `BUILD_ENV` and internal Anthropic-only identifiers.
@@ -223,20 +247,48 @@ export GEMINI_DEFAULT_OPUS_MODEL="gemini-2.5-pro"
## Testing
- **框架**: `bun:test`(内置断言 + mock
- **当前状态**: 2992 tests / 188 files / 0 fail
- **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `<module>.test.ts`
- **集成测试**: `tests/integration/` — 4 个文件cli-arguments, context-build, message-pipeline, tool-chain
- **共享 mock/fixture**: `tests/mocks/`api-responses, file-system, fixtures/
- **命名**: `describe("functionName")` + `test("behavior description")`,英文
- **Mock 模式**: 对重依赖模块使用 `mock.module()` + `await import()` 解锁(必须内联在测试文件中,不能从共享 helper 导入
- **当前状态**: ~1623 tests / 114 files (110 unit + 4 integration) / 0 fail详见 `docs/testing-spec.md`
- **包测试**: `packages/` 下各包也有独立测试(如 `color-diff-napi` 11 tests
### Mock 使用规范
**只 mock 有副作用的依赖链,不 mock 纯函数/纯数据模块。**
被迫 mock 的根源:`log.ts` / `debug.ts``bootstrap/state.ts`(模块级 `realpathSync` / `randomUUID` 副作用)。必须 mock 的模块:`log.ts``debug.ts``bun:bundle``settings/settings.js``config.ts``auth.ts`、第三方网络库。
不要 mock纯函数模块`errors.ts``stringUtils.js`、mock 值与真实实现相同的模块、mock 路径与实际 import 不匹配的模块。
路径规则:统一用 `.ts` 扩展名 + `src/*` 别名路径,禁止双重 mock 同一模块。
### 类型检查
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:
```bash
bunx tsc --noEmit
```
**类型规范**
- 生产代码禁止 `as any`;测试文件中 mock 数据可用 `as any`
- 类型不匹配优先用 `as unknown as SpecificType` 双重断言,或补充 interface
- 未知结构对象用 `Record<string, unknown>` 替代 `any`
- 联合类型用类型守卫type guard收窄不要强转
- `msg.request` 属性访问:`const req = msg.request as Record<string, unknown>`
- Ink `color` prop`as keyof Theme` 而非 `as any`
## Working with This Codebase
- **Don't try to fix all tsc errors** — they're from decompilation and don't affect runtime.
- **tsc must pass** — `bunx tsc --noEmit` 必须零错误,任何修改都不能引入新的类型错误。
- **Feature flags** — 默认全部关闭(`feature()` 返回 `false`。Dev/build 各有自己的默认启用列表。不要在 `cli.tsx` 中重定义 `feature` 函数。
- **React Compiler output** — Components have decompiled memoization boilerplate (`const $ = _c(N)`). This is normal.
- **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。
- **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。**`feature()` 只能直接用在 `if` 语句或三元表达式的条件位置**Bun 编译器限制),不能赋值给变量、不能放在箭头函数体里、不能作为 `&&` 链的一部分。正确:`if (feature('X')) {}``feature('X') ? a : b`
- **`src/` path alias** — tsconfig maps `src/*` to `./src/*`. Imports like `import { ... } from 'src/utils/...'` are valid.
- **MACRO defines** — 集中管理在 `scripts/defines.ts`。Dev mode 通过 `bun -d` 注入build 通过 `Bun.build({ define })` 注入。修改版本号等常量只改这个文件。
- **构建产物兼容 Node.js** — `build.ts` 会自动后处理 `import.meta.require`,产物可直接用 `node dist/cli.js` 运行。
- **Biome 配置** — 大量 lint 规则被关闭decompiled 代码不适合严格 lint`.tsx` 文件用 120 行宽 + 强制分号;其他文件 80 行宽 + 按需分号。
- **Ink 框架在 `packages/@ant/ink/`** — 不是 `src/ink/`该目录不存在。Ink 相关的组件、hooks、keybindings 都在 packages 中。
- **Provider 优先级** — `modelType` 参数 > 环境变量 > 默认 `firstParty`。新增 provider 需在 `src/utils/model/providers.ts` 注册。

View File

@@ -1,5 +1,194 @@
# DEV-LOG
## /poor 省流模式 (2026-04-11)
新增 `/poor` 命令toggle 关闭 `extract_memories``prompt_suggestion`,省 token。
- 新增 `POOR` feature flagbuild.ts + dev.ts
- `src/commands/poor/` — 命令定义 + toggle 实现 + 状态管理
- `src/query/stopHooks.ts` — POOR 模式激活时跳过 extract_memories 和 prompt_suggestion
---
## Pipe IPC + LAN Pipes + Monitor Tool + 工具恢复 (2026-04-08 ~ 2026-04-11)
**分支**: `feat/pr-package-adapt`
### 背景
从 decompiled 代码恢复大量 stub 为完整实现,同时新增 LAN 跨机器通讯能力。本次 PR 覆盖Pipe IPC 系统、LAN Pipes、Monitor Tool、20+ 工具/组件<E7BB84><E4BBB6><EFBFBD>复、REPL hook 架构重构。
### 实现
#### 1. PipeServer TCP 双模式(`src/utils/pipeTransport.ts`
从原始的纯 UDS 服务器扩展为 UDS + TCP 双模式:
- 提取 `setupSocket()` 共享方法UDS 和 TCP 的 socket 处理逻辑完全一致
- `start(options?: PipeServerOptions)` 新增可选参数 `{ enableTcp, tcpPort }`
- 内部维护两个 `net.Server`UDS + TCP共享同一组 `clients: Set<Socket>``handlers`
- TCP server 绑定 `0.0.0.0` + 动态端口port=0 由 OS 分配)
- `tcpAddress` getter 暴露 TCP 端口信息
- `close()` 同时关闭两个 server
- 新增类型:`PipeTransportMode``TcpEndpoint``PipeServerOptions`
PipeClient 对应扩展:
- 构造函数新增可选 `TcpEndpoint` 参数
- `connect()` 根据是否有 TCP endpoint 分派到 `connectTcp()``connectUds()`
- TCP 连接不需要文件存在轮询,直接建立连接
#### 2. LAN Beacon — UDP Multicast 发现(`src/utils/lanBeacon.ts`,新文件)
零配置局域网 peer 发现:
- **协议**UDP multicast 组 `224.0.71.67`"CC" ASCII端口 `7101`TTL=1
- **Announce 包**JSON `{ proto, pipeName, machineId, hostname, ip, tcpPort, role, ts }`
- **广播间隔**3 秒,首次在 socket bind 完成后立即发送
- **Peer 超时**15 秒无 announce 视为 lost
- **事件**`peer-discovered``peer-lost`
- **存储**module-level singleton `getLanBeacon()`/`setLanBeacon()`,不挂在 Zustand state 上
关键修复:
- `addMembership(group, localIp)` + `setMulticastInterface(localIp)` 指定 LAN 网卡,解决 Windows 上 WSL/Docker 虚拟网卡劫持 multicast 的问题
- announce/cleanup 定时器移入 `bind()` 回调内,修复 socket 未就绪时发送的竞态
#### 3. Registry 扩展(`src/utils/pipeRegistry.ts`
- `PipeRegistryEntry` 新增 `tcpPort?``lanVisible?` 字段
- `mergeWithLanPeers(registry, lanPeers)` 合并本地 registry 和 LAN beacon peers本地优先
#### 4. Peer Address 扩展(`src/utils/peerAddress.ts`
- `parseAddress()` 新增 `tcp` scheme`tcp:192.168.1.20:7100`
- 新增 `parseTcpTarget()` 解析 `host:port` 字符串
#### 5. REPL 集成(`src/screens/REPL.tsx`
三个阶段的改动:
**Bootstrap**`createPipeServer()` 时根据 `feature('LAN_PIPES')` 传入 TCP 选项 → 启动 `LanBeacon` → 注册 entry 携带 tcpPort
**Heartbeat**(每 5 秒):
- `refreshDiscoveredPipes()` 同时包含本地 subs 和 LAN beacon peers防止 LAN peer 状态被覆盖
- auto-attach 循环统一遍历本地 subs + LAN peersLAN peers 通过 TCP endpoint 连接
- cleanup 检查 LAN beacon peers 列表,避免误删存活的 LAN 连接
- attach 请求携带 `machineId`,接收方区分 LAN peer不要求 sub 角色)
**Cleanup**:通过 `getLanBeacon()` 获取并 `stop()``setLanBeacon(null)` 清除
#### 6. 命令更新
- `/pipes``src/commands/pipes/pipes.ts`):显示 `[LAN]` 标记的远端实例
- `/attach``src/commands/attach/attach.ts`):自动查找 LAN beacon 获取 TCP endpoint
- `SendMessageTool``src/tools/SendMessageTool/SendMessageTool.ts`):支持 `tcp:` scheme权限检查要求用户确认
#### 7. Feature Flag
`LAN_PIPES` — 在 `scripts/dev.ts``build.ts` 的默认 features 列表中启用。所有 LAN 代码路径均通过 `feature('LAN_PIPES')` 门控。
#### 8. Pipe IPC 基础系统(`UDS_INBOX` feature
- `PipeServer`/`PipeClient`UDS 传输NDJSON 协议(共享 `ndjsonFramer.ts`
- `PipeRegistry`machineId 绑定的角色分配main/sub文件锁并行探测
- Master/slave attach 流程、prompt 转发、permission 转发
- Heartbeat 生命周期5s 间隔stale entry 清理busy flag 防重叠)
- 命令:`/pipes``/attach``/detach``/send``/claim-main``/pipe-status`
#### 9. Monitor Tool`MONITOR_TOOL` feature
- `MonitorTool`AI 可调用的后台 shell 监控工具
- `/monitor` 命令用户快捷入口Windows 兼容watch → PowerShell 循环)
- `MonitorMcpTask`:从 stub 恢复完整生命周期register/complete/fail/kill
- `MonitorPermissionRequest`React 权限确认 UI
- `MonitorMcpDetailDialog`Shift+Down 详情面板
#### 10. 工具恢复stub → 实现)
- SnipTool、SleepTool、ListPeersTool、SendUserFileTool
- WebBrowserTool、SubscribePRTool、PushNotificationTool
- CtxInspectTool、TerminalCaptureTool、WorkflowTool
- REPLTool (.js → .ts)、VerifyPlanExecutionTool (.js → .ts)、SuggestBackgroundPRTool (.js → .ts)
- 组件 .ts → .tsx 重写MonitorPermissionRequest、ReviewArtifactPermissionRequest、MonitorMcpDetailDialog、WorkflowDetailDialog、WorkflowPermissionRequest
#### 11. REPL Hook 架构重构
从 REPL.tsx 提取 ~830 行 Pipe IPC 内联代码为 4 个独立 hook
| Hook | 行数 | 职责 |
|------|------|------|
| `usePipeIpc` | 623 | 生命周期bootstrap、handlers、heartbeat、cleanup |
| `usePipeRelay` | 38 | slave→master 消息回传(通过 `setPipeRelay` singleton |
| `usePipePermissionForward` | 159 | 权限请求转发 + 流式通知显示 |
| `usePipeRouter` | 130 | selected pipe 输入路由 + role/IP 标签显示 |
共享工具:`ndjsonFramer.ts` 替换 3 份重复的 NDJSON 解析。
#### 12. Feature Flags 新增启用
UDS_INBOX、LAN_PIPES、MONITOR_TOOL、FORK_SUBAGENT、KAIROS、COORDINATOR_MODE、WORKFLOW_SCRIPTS、HISTORY_SNIP、CONTEXT_COLLAPSE
### 踩坑记录
1. **Multicast 绑错网卡**Windows 上 `addMembership(group)` 不指定本地接口时,默认绑到 WSL/Docker 虚拟网卡(`172.19.112.1`LAN 上的真实机器收不到。必须 `addMembership(group, localIp)` + `setMulticastInterface(localIp)`
2. **Beacon ref 丢失**:最初用 `(store.getState() as any)._lanBeacon` 挂载 beacon 引用,但 Zustand `setState` 展开 `prev` 时不包含 `_lanBeacon` 属性,下次读取就是 `undefined`。改为 module-level singleton 解决。
3. **Heartbeat 清洗 LAN 连接**`refreshDiscoveredPipes()` 每 5 秒用仅含本地 registry subs 的列表完全覆盖 `discoveredPipes` + `selectedPipes`LAN peer 的发现和选择状态被持续清空。必须在 refresh 中同时包含 beacon peers。
4. **Heartbeat cleanup 误删**`!aliveSubNames.has(slaveName)` 导致 LAN peer不在本地 registry被判定为死连接每 5 秒清除一次。需要同时检查 beacon peers 列表。
5. **跨机器 attach 被拒**:两台机器各自为 `main`attach handler 硬编码 `role !== 'sub'` 拒绝。通过 attach_request 携带 `machineId`,接收方对不同 machineId 的请求放行。
6. **`feature()` 使用约束**Bun 的 `feature()` 是编译时常量,只能在 `if` 语句或三元条件中直接使用,不能赋值给变量(如 `const x = feature('...')`),否则构建报错。
### 已知限制
- TCP 无认证:同 LAN 内任何设备知道端口号即可连接
- JSON.parse 无 schema 验证code review 建议增加 Zod 校验
- Beacon 明文广播 IP/hostname/machineId建议后续 hash 处理
- `getLocalIp()` 可能返回 VPN 地址:多网卡环境需更精确的接口选择
### 测试
- `src/utils/__tests__/lanBeacon.test.ts`7 个测试mock dgram
- `src/utils/__tests__/peerAddress.test.ts`8 个测试(纯函数)
- 全量2190 pass / 0 fail
### 防火墙配置
**Windows**(管理员 PowerShell
```powershell
New-NetFirewallRule -DisplayName "Claude Code LAN Beacon (UDP)" -Direction Inbound -Protocol UDP -LocalPort 7101 -Action Allow -Profile Private
New-NetFirewallRule -DisplayName "Claude Code LAN Pipes (TCP)" -Direction Inbound -Protocol TCP -LocalPort 1024-65535 -Program (Get-Command bun).Source -Action Allow -Profile Private
New-NetFirewallRule -DisplayName "Claude Code LAN Beacon Out (UDP)" -Direction Outbound -Protocol UDP -RemotePort 7101 -Action Allow -Profile Private
```
**macOS**(首次运行时系统会弹出"允许接受传入连接"对话框,点击允许即可。手动放行):
```bash
# 如果使用 pf <20><><EFBFBD>火墙添加规则
echo "pass in proto udp from any to any port 7101" | sudo pfctl -ef -
# 或<><E68896>接在 System Settings → Network → Firewall 中允许 bun 进程
```
**Linux**firewalld
```bash
sudo firewall-cmd --zone=trusted --add-port=7101/udp --permanent
sudo firewall-cmd --zone=trusted --add-port=1024-65535/tcp --permanent
sudo firewall-cmd --reload
```
**Linux**iptables
```bash
sudo iptables -A INPUT -p udp --dport 7101 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 1024:65535 -m owner --uid-owner $(id -u) -j ACCEPT
sudo iptables-save | sudo tee /etc/iptables/rules.v4
```
**通用验证**:确认网络为局域网(非公共 WiFi<EFBC8C><E8B7AF><EFBFBD>器未开启 AP 隔离。
---
## Daemon + Remote Control Server 还原 (2026-04-07)
**分支**: `feat/daemon-remote-control-server`

View File

@@ -10,13 +10,25 @@
> Which Claude do you like? The open source one is the best.
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)...
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)... 而且, 我们实现了企业版或者需要登陆 Claude 账号才能使用的特性, 实现技术普惠
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/qZU6zS7Q)
- ✅ [x] V4 — 测试补全、[Buddy](https://ccb.agent-aura.top/docs/features/buddy)、[Auto Mode](https://ccb.agent-aura.top/docs/safety/auto-mode)、环境变量 Feature 开关
- ✅ [x] V5 — [Sentry](https://ccb.agent-aura.top/docs/internals/sentry-setup) / [GrowthBook](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) 企业监控、[自定义 Login](https://ccb.agent-aura.top/docs/features/custom-platform-login)、[OpenAI 兼容](https://ccb.agent-aura.top/docs/plans/openai-compatibility)、[Web Search](https://ccb.agent-aura.top/docs/features/web-browser-tool)、[Computer Use](https://ccb.agent-aura.top/docs/features/computer-use) / [Chrome Use](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp)、[Voice Mode](https://ccb.agent-aura.top/docs/features/voice-mode)、[Bridge Mode](https://ccb.agent-aura.top/docs/features/bridge-mode)、[/dream 记忆整理](https://ccb.agent-aura.top/docs/features/auto-dream)
- 🔮 [ ] V6 — 大规模重构石山代码全面模块分包全新分支main 封存为历史版本)
| 特性 | 说明 | 文档 |
|------|------|------|
| **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/pipes-and-lan) / [LAN](https://ccb.agent-aura.top/docs/features/lan-pipes) |
| **ACP 协议一等一支持** | 支持接入 Zed、Cursor 等 IDE支持会话恢复、Skills、权限桥接 | [文档](https://ccb.agent-aura.top/docs/features/acp-zed) |
| **Remote Control 私有部署** | Docker 自托管远程界面, 可以手机上看 CC | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) |
| **Langfuse 监控** | 企业级 Agent 监控, 可以清晰看到每次 agent loop 细节, 可以一键转化为数据集 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
| **Web Search** | 内置网页搜索工具, 支持 bing 和 brave 搜索 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
| **Poor Mode** | 穷鬼模式,关闭记忆提取和键入建议,大幅度减少并发请求 | /poor 可以开关 |
| **自定义模型供应商** | OpenAI/Anthropic/Gemini/Grok 兼容 | [文档](https://ccb.agent-aura.top/docs/features/custom-platform-login) |
| Voice Mode | Push-to-Talk 语音输入 | [文档](https://ccb.agent-aura.top/docs/features/voice-mode) |
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](https://ccb.agent-aura.top/docs/features/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) |
| Sentry | 企业级错误追踪 | [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup) |
| GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
- 🚀 [想要启动项目](#快速开始源码版)
- 🐛 [想要调试项目](#vs-code-调试)
@@ -30,13 +42,9 @@
```sh
bun i -g claude-code-best
bun pm -g trust claude-code-best
ccb # 直接打开 claude code
```
⚠️ 国内对 github 网络较差的, 需要先设置这个环境变量
```bash
DEFAULT_RELEASE_BASE=https://ghproxy.net/https://github.com/microsoft/ripgrep-prebuilt/releases/download/v15.0.1
ccb # 以 nodejs 打开 claude code
ccb-bun # 以 bun 形态打开
CLAUDE_BRIDGE_BASE_URL=https://remote-control.claude-code-best.win/ CLAUDE_BRIDGE_OAUTH_TOKEN=test-my-key ccb --remote-control # 我们有自部署的远程控制
```
## ⚡ 快速开始(源码版)
@@ -54,12 +62,6 @@ DEFAULT_RELEASE_BASE=https://ghproxy.net/https://github.com/microsoft/ripgrep-pr
bun install
```
⚠️ 国内对 github 网络较差的,可以使用这个环境变量
```bash
DEFAULT_RELEASE_BASE=https://ghproxy.net/https://github.com/microsoft/ripgrep-prebuilt/releases/download/v15.0.1
```
### ▶️ 运行
```bash

View File

@@ -30,6 +30,23 @@ const DEFAULT_BUILD_FEATURES = [
'ULTRAPLAN',
// P2: daemon + remote control server
'DAEMON',
// ACP (Agent Client Protocol) agent mode
'ACP',
// PR-package restored features
'WORKFLOW_SCRIPTS',
'HISTORY_SNIP',
'CONTEXT_COLLAPSE',
'MONITOR_TOOL',
'FORK_SUBAGENT',
// 'UDS_INBOX',
'KAIROS',
'COORDINATOR_MODE',
'LAN_PIPES',
'BG_SESSIONS',
'TEMPLATES',
// 'REVIEW_ARTIFACT', // API 请求无响应,需进一步排查 schema 兼容性
// P3: poor mode (disable extract_memories + prompt_suggestion)
'POOR',
]
// Collect FEATURE_* env vars → Bun.build features
@@ -75,8 +92,27 @@ for (const file of files) {
}
}
// Also patch unguarded globalThis.Bun destructuring from third-party deps
// (e.g. @anthropic-ai/sandbox-runtime) so Node.js doesn't crash at import time.
let bunPatched = 0
const BUN_DESTRUCTURE = /var \{([^}]+)\} = globalThis\.Bun;?/g
const BUN_DESTRUCTURE_SAFE = 'var {$1} = typeof globalThis.Bun !== "undefined" ? globalThis.Bun : {};'
for (const file of files) {
if (!file.endsWith('.js')) continue
const filePath = join(outdir, file)
const content = await readFile(filePath, 'utf-8')
if (BUN_DESTRUCTURE.test(content)) {
await writeFile(
filePath,
content.replace(BUN_DESTRUCTURE, BUN_DESTRUCTURE_SAFE),
)
bunPatched++
}
}
BUN_DESTRUCTURE.lastIndex = 0
console.log(
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for Node.js compat)`,
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for import.meta.require, ${bunPatched} for Bun destructure)`,
)
// Step 4: Copy native .node addon files (audio-capture)
@@ -99,3 +135,18 @@ if (!rgScript.success) {
} else {
console.log(`Bundled download-ripgrep script to ${outdir}/`)
}
// Step 6: Generate cli-bun and cli-node executable entry points
const cliBun = join(outdir, 'cli-bun.js')
const cliNode = join(outdir, 'cli-node.js')
await writeFile(cliBun, '#!/usr/bin/env bun\nimport "./cli.js"\n')
await writeFile(cliNode, '#!/usr/bin/env node\nimport "./cli.js"\n')
// Make both executable
const { chmodSync } = await import('fs')
chmodSync(cliBun, 0o755)
chmodSync(cliNode, 0o755)
console.log(`Generated ${cliBun} (shebang: bun) and ${cliNode} (shebang: node)`)

688
bun.lock

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

188
docs.json Normal file
View File

@@ -0,0 +1,188 @@
{
"$schema": "https://mintlify.com/docs.json",
"theme": "mint",
"name": "Claude Code Architecture",
"colors": {
"primary": "#D97706",
"light": "#F59E0B",
"dark": "#B45309"
},
"favicon": "/docs/favicon.svg",
"navigation": {
"groups": [
{
"group": "开始",
"pages": [
{
"group": "介绍",
"pages": [
"docs/introduction/what-is-claude-code",
"docs/introduction/why-this-whitepaper",
"docs/introduction/architecture-overview"
]
}
]
},
{
"group": "对话是如何运转的",
"pages": [
"docs/conversation/the-loop",
"docs/conversation/streaming",
"docs/conversation/multi-turn"
]
},
{
"group": "工具AI 的双手",
"pages": [
"docs/tools/what-are-tools",
"docs/tools/file-operations",
"docs/tools/shell-execution",
"docs/tools/search-and-navigation",
"docs/tools/task-management"
]
},
{
"group": "上下文工程",
"pages": [
"docs/context/system-prompt",
"docs/context/project-memory",
"docs/context/compaction",
"docs/context/token-budget"
]
},
{
"group": "多 Agent 协作",
"pages": [
"docs/agent/sub-agents",
"docs/agent/worktree-isolation",
"docs/agent/coordinator-and-swarm"
]
},
{
"group": "可扩展性",
"pages": [
"docs/extensibility/mcp-protocol",
"docs/extensibility/hooks",
"docs/extensibility/skills",
"docs/extensibility/custom-agents"
]
},
{
"group": "安全与权限",
"pages": [
"docs/safety/why-safety-matters",
"docs/safety/permission-model",
"docs/safety/sandbox",
"docs/safety/plan-mode",
"docs/safety/auto-mode"
]
},
{
"group": "揭秘:隐藏功能与内部机制",
"pages": [
"docs/internals/three-tier-gating",
"docs/internals/feature-flags",
"docs/internals/growthbook-ab-testing",
"docs/internals/growthbook-adapter",
"docs/internals/sentry-setup",
"docs/internals/hidden-features",
"docs/internals/ant-only-world",
"docs/features/debug-mode",
"docs/features/buddy"
]
},
{
"group": "隐藏功能详解",
"pages": [
{
"group": "Agent 与协作",
"pages": [
"docs/features/coordinator-mode",
"docs/features/fork-subagent",
"docs/features/daemon",
"docs/features/teammem"
]
},
{
"group": "运行模式",
"pages": [
"docs/features/kairos",
"docs/features/voice-mode",
"docs/features/bridge-mode",
"docs/features/remote-control-self-hosting",
"docs/features/proactive",
"docs/features/ultraplan"
]
},
{
"group": "工具增强",
"pages": [
"docs/features/mcp-skills",
"docs/features/tree-sitter-bash",
"docs/features/bash-classifier",
"docs/features/web-browser-tool",
"docs/features/experimental-skill-search"
]
},
{
"group": "上下文与自动化",
"pages": [
"docs/features/token-budget",
"docs/features/context-collapse",
"docs/features/workflow-scripts",
"docs/features/auto-dream"
]
},
"docs/features/tier3-stubs"
]
},
{
"group": "基础设施与依赖",
"pages": [
"docs/auto-updater",
"docs/lsp-integration",
"docs/external-dependencies",
"docs/telemetry-remote-config-audit"
]
}
]
},
"logo": {
"light": "/docs/logo/light.svg",
"dark": "/docs/logo/dark.svg"
},
"background": {
"color": {
"light": "#FFFFFF",
"dark": "#0F172A"
}
},
"navbar": {
"primary": {
"type": "github",
"href": "https://github.com/claude-code-best/claude-code"
}
},
"search": {
"prompt": "搜索 Claude Code 架构文档..."
},
"seo": {
"metatags": {
"og:image": "https://ccb.agent-aura.top/docs/images/og-cover.png",
"twitter:image": "https://ccb.agent-aura.top/docs/images/og-cover.png",
"twitter:card": "summary_large_image"
},
"indexing": "navigable"
},
"footer": {
"socials": {
"github": "https://github.com/anthropics/claude-code"
}
},
"redirects": [
{
"source": "/docs/introduction",
"destination": "/docs/introduction/what-is-claude-code"
}
]
}

View File

@@ -1,128 +0,0 @@
# 文档修正计划
> 目标:补充源码级洞察,让每篇文档从"概念科普"升级为"逆向工程白皮书"水准。
---
## 第一梯队:空壳页,需要大幅重写
### 1. `safety/sandbox.mdx` — 沙箱机制 ✅ DONE
**现状**35 行,只列了"文件系统/网络/进程/时间"四个维度,没有任何实现细节。
**修正方向**
- 补充 macOS `sandbox-exec` 的实际调用方式,展示沙箱 profile 的关键片段
- 说明 `getSandboxConfig()` 的判定逻辑:哪些命令走沙箱、哪些跳过
- 补充 `dangerouslyDisableSandbox` 参数的设计权衡
- 加入 Linux 平台的沙箱差异对比seatbelt vs namespace
- 展示一次命令执行从权限检查→沙箱包裹→实际执行的完整链路
---
### 2. `introduction/what-is-claude-code.mdx` — 什么是 Claude Code ✅ DONE
**现状**39 行,纯营销文案,和"普通聊天 AI"的对比表太低级。
**修正方向**
- 砍掉"能做什么"的泛泛列表,改为一个具体的端到端示例(从用户输入→系统处理→最终输出)
- 用一张简化架构图替代文字描述,让读者 30 秒建立直觉
- 补充 Claude Code 的技术定位:不是 IDE 插件、不是 Web Chat而是 terminal-native agentic system
- 加入与 Cursor / Copilot / Aider 等工具的定位差异(架构层面而非功能清单)
---
### 3. `introduction/why-this-whitepaper.mdx` — 为什么写这份白皮书 ✅ DONE
**现状**40 行,全是空话,四张 Card 只是后续章节标题的预告。
**修正方向**
- 明确定位:这是对 Anthropic 官方 CLI 的逆向工程分析,不是官方文档
- 列出逆向过程中发现的 3-5 个最意外/最精妙的设计决策(吊住读者胃口)
- 说明白皮书的阅读路线图:推荐的阅读顺序和每个章节解决什么问题
- 补充"这份白皮书不是什么"——不是使用教程,不是 API 文档
---
### 4. `safety/why-safety-matters.mdx` — 为什么安全至关重要 ✅ DONE
**现状**40 行,只列了显而易见的风险,"安全 vs 效率的平衡"只有 3 个 bullet。
**修正方向**
- 从源码角度展示安全体系的全景图:权限规则 → 沙箱 → Plan Mode → 预算上限 → Hooks 的纵深防御链
- 补充 Claude 自身 System Prompt 中的安全指令("执行前确认"、"优先可逆操作"等),展示 AI 端的安全约束
- 用真实场景说明"安全 vs 效率"的工程权衡:比如 Read 工具为什么免审批、Bash 工具为什么要逐条确认
- 加入 Prompt Injection 防御的简要说明tool result 中的恶意内容如何被系统标记)
---
## 第二梯队:有骨架但太浅,需要补肉
### 5. `conversation/streaming.mdx` — 流式响应 ✅ DONE
**现状**43 行,只说了"流式好"和 3 行 provider 表。
**修正方向**
- 补充 `BetaRawMessageStreamEvent` 的核心事件类型及其含义
- 展示文本 chunk 和 tool_use block 交织的状态机流转
- 说明流式中的错误处理网络断开、API 限流、token 超限时的重试/降级策略
- 补充 `processStreamEvents()` 的核心逻辑如何从事件流中分离出文本、工具调用、usage 统计
---
### 6. `tools/search-and-navigation.mdx` — 搜索与导航 ✅ DONE
**现状**43 行,只说 Glob 和 Grep 存在。
**修正方向**
- 补充 ripgrep 二进制的内嵌方式vendor 目录、平台适配)
- 说明搜索结果的 head_limit 默认 250 的设计原因token 预算)
- 展示 ToolSearch 的实现:如何用语义匹配在 50+ 工具(含 MCP中找到最相关的
- 补充 Glob 按修改时间排序的意义:最近修改的文件最可能与当前任务相关
---
### 7. `tools/task-management.mdx` — 任务管理 ✅ DONE
**现状**50 行,只有流程 Steps 和状态展示的 4 个 bullet。
**修正方向**
- 补充任务的数据模型id / subject / description / status / blockedBy / blocks / owner
- 说明依赖管理的实现blockedBy 如何阻止任务被认领、完成一个任务后如何自动解锁下游
- 展示任务与 Agent 工具的联动:子 Agent 如何认领任务、报告进度
- 补充 activeForm 字段的 UX 设计:进行中任务的 spinner 动画文案
---
### 8. `context/token-budget.mdx` — Token 预算管理 ✅ DONE
**现状**55 行,预算控制只有 3 张 Card 各一句话。
**修正方向**
- 补充 `contextWindowTokens``maxOutputTokens` 的动态计算逻辑
- 说明缓存 breakpoint 的放置策略System Prompt 中不变内容在前、变化内容在后的原因
- 展示工具输出截断的具体机制:超长结果如何被 truncate、何时触发 micro-compact
- 补充 token 计数的实现:`countTokens` 的调用时机和近似 vs 精确计数的权衡
---
### 9. `agent/worktree-isolation.mdx` — Worktree 隔离 ✅ DONE
**现状**55 行,只描述了 git worktree 的概念。
**修正方向**
- 展示 `.claude/worktrees/` 的目录结构和分支命名规则
- 说明 worktree 的生命周期:创建时机(`isolation: "worktree"`)→ 子 Agent 执行 → 完成/放弃 → 自动清理
- 补充 worktree 与子 Agent 的绑定关系Agent 结束时如何判断 keep or remove
- 加入 EnterWorktree / ExitWorktree 工具的交互设计
---
### 10. `extensibility/custom-agents.mdx` — 自定义 Agent ✅ DONE
**现状**56 行,只有配置表和示例表。
**修正方向**
- 展示 agent markdown 文件的完整 frontmatter 格式name / description / model / allowedTools 等)
- 说明 agent 如何被加载和注入 System Prompt`loadAgentDefinitions()` 的发现和合并逻辑
- 展示工具限制的实现allowedTools 如何过滤工具列表
- 补充 agent 与 subagent_type 参数的关联Agent 工具如何指定使用自定义 Agent

View File

@@ -10,13 +10,13 @@ keywords: ["协调者模式", "蜂群模式", "Agent Swarm", "多 Agent 协作",
| 维度 | Coordinator Mode | Agent Swarms |
|------|-----------------|--------------|
| **门控** | `feature('COORDINATOR_MODE')` + `CLAUDE_CODE_COORDINATOR_MODE=1` | 任务系统 V2默认启用 |
| **拓扑** | 星型Coordinator 居中Worker 外围 | 网状:对等 Agent 共享任务列表 |
| **角色** | 明确分工Coordinator 编排、Worker 执行 | 模糊:每个 Agent 自主认领任务 |
| **通信** | `SendMessage` 定向通信 + `<task-notification>` | 任务文件系统 + 邮箱广播 |
| **适用** | 需要集中决策的复杂任务 | 并行度高的独立子任务 |
| **门控** | `feature('COORDINATOR_MODE')` + `CLAUDE_CODE_COORDINATOR_MODE=1` | `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` 环境变量 |
| **拓扑** | 星型Coordinator 居中Worker 外围 | 星型+P2P 混合Team Lead 协调Teammate 间可直接通信 |
| **角色** | 明确分工Coordinator 编排、Worker 执行 | Team Lead 协调 + Teammate 自主认领任务 |
| **通信** | `SendMessage` 定向通信 + `<task-notification>` | Mailbox 消息系统message / broadcast |
| **适用** | 需要集中决策的复杂任务 | 并行度高、需要 Teammate 间直接协作的任务 |
两者不是互斥的——Coordinator Mode 可以在 Swarm 架构之上运行,将 Coordinator 作为特殊的 Leader Agent
两者不是互斥的——理论上 Coordinator Mode 可以在 Agent Teams 架构之上运行(概念层叠加,非嵌套团队),将 Coordinator 作为特殊的 Team Lead但这部分集成`workerAgent.ts` 中的 `getCoordinatorAgents`)目前为 stub 实现,尚未完整落地
## Coordinator Mode星型编排架构
@@ -45,7 +45,7 @@ Coordinator 被剥夺了所有"动手"工具,只保留编排能力:
| **TaskStop** | 中途停止走错方向的 Worker |
| **subscribe_pr_activity** | 订阅 GitHub PR 事件review comments、CI 结果) |
Coordinator **不写代码、不读文件、不执行命令**——它只做三件事:理解需求、分配任务、综合结果。
Coordinator **不写代码、不读文件、不执行命令**——它的核心职责是:理解需求、分配任务、综合结果,以及在无需工具时直接回答用户问题
### Worker 的工具权限
@@ -53,7 +53,7 @@ Worker 的可用工具由 `getCoordinatorUserContext()``coordinatorMode.ts:80
```typescript
// 简化模式下:只有 Bash + Read + Edit
const workerTools = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE')
const workerTools = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)
? [BASH_TOOL_NAME, FILE_READ_TOOL_NAME, FILE_EDIT_TOOL_NAME]
: Array.from(ASYNC_AGENT_ALLOWED_TOOLS)
.filter(name => !INTERNAL_WORKER_TOOLS.has(name))
@@ -63,7 +63,7 @@ const workerTools = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE')
### Scratchpad跨 Worker 的共享知识库
当 `tengu_scratch` feature flag 启用时Coordinator 拥有一个 Scratchpad 目录
当 `isScratchpadGateEnabled()`(内部检查 `tengu_scratch` feature gate启用时Workers 获得一个 Scratchpad 目录Coordinator 通过其系统上下文知晓该目录的存在
```
Scratchpad 目录:
@@ -113,32 +113,84 @@ Coordinator System Prompt`coordinatorMode.ts:111-369`,约 260 行)明确
这是 Coordinator Mode 最核心的设计约束Coordinator 必须先理解,再分配。
## Agent Swarms:蜂群式协作
## Agent Teams (Swarm):蜂群式协作
Swarm 模式基于任务系统 V2详见[任务管理](../tools/task-management.mdx)),核心机制是**共享任务列表 + 竞争认领**
Swarm 模式基于任务系统 V2详见[任务管理](../tools/task-management.mdx)),核心机制是**共享任务列表 + 竞争认领 + Mailbox 消息系统**
### 团队初始化
```
Leader 创建团队TeamCreateTool
Team Lead 创建团队TeamCreateTool
设置 teamName → setLeaderTeamName()
所有 teammate 自动获得相同的 taskListId
所有 Teammate 自动获得相同的 taskListId
teammate 启动时:
Teammate 启动时:
1. CLAUDE_CODE_TASK_LIST_ID 环境变量(显式覆盖)
2. teammate 上下文的 teamName共享 leader 的任务列表)
2. Teammate 上下文的 teamName共享 Lead 的任务列表)
3. CLAUDE_CODE_TEAM_NAME 环境变量
4. leader 设置的 teamName
4. Lead 设置的 teamName
5. getSessionId()(兜底)
```
多级优先级确保了 Leader 和所有 Teammate 指向同一个任务列表,无需额外协调。
多级优先级确保了 Team Lead 和所有 Teammate 指向同一个任务列表,无需额外协调。
### 架构组件
官方 Agent Teams 架构定义了四个核心组件:
| 组件 | 角色 |
|------|------|
| **Team Lead** | 创建团队、分配任务、综合结果的主 Claude Code 会话 |
| **Teammate** | 独立的 Claude Code 实例,各自拥有独立的上下文窗口 |
| **Task List** | 共享的任务列表Teammate 竞争认领和完成 |
| **Mailbox** | 消息系统,支持 Teammate 间直接通信 |
### Mailbox 消息系统
官方架构中的 Mailbox 是 Teammate 间通信的核心原语,支持两种消息模式(`broadcast` 模式来自源码推断,官方文档未明确细分):
| 模式 | 作用 | 场景 |
|------|------|------|
| **message** | 定向发送给指定 Teammate | 传递具体指令、请求协作 |
| **broadcast** | 广播给所有 Teammate | 全局通知、状态同步 |
Mailbox 的关键特性:
- **自动投递**:消息自动送达目标 Teammate 的对话上下文
- **空闲通知**TeammateIdleTeammate 完成当前任务进入空闲时,自动通过 Mailbox 通知 Team Lead
- **直接通信**:与 Coordinator Mode 不同Teammate 之间可以直接通信,无需经过 Lead 中转
### Hook 事件
Agent Teams 提供三个关键 Hook 事件,用于在团队生命周期中注入自定义逻辑:
| Hook | 触发时机 | 典型用途 |
|------|---------|---------|
| **TaskCreated** | 新任务添加到任务列表时 | 自动分配、优先级排序 |
| **TaskCompleted** | 任务标记为完成时 | 结果通知、依赖解锁 |
| **TeammateIdle** | Teammate 完成所有任务进入空闲时 | Lead 重新分配、动态扩缩容 |
### 限制
当前 Agent Teams 实现的限制:
- **不支持嵌套团队**Teammate 不能再创建子团队
- **每 session 一个团队**:一个会话只能属于一个团队
- **Lead 固定**Team Lead 创建后不可更换
- **不支持 in-process Teammate 的会话恢复**:进程重启后 in-process 类型 Teammate 的状态丢失
### 持久化存储
团队状态通过文件系统持久化,确保进程重启后可恢复:
```
~/.claude/teams/{team-name}/config.json ← 团队配置
~/.claude/tasks/{team-name}/ ← 共享任务列表(文件锁保护)
```
### 任务认领与竞争
`claimTask()` 是 Swarm 的核心并发原语:
`claimTask()` 是 Agent Teams 的核心并发原语:
```
Teammate A 调用 TaskList → 发现 task #3 是 pending
@@ -146,7 +198,7 @@ Teammate B 同时发现 task #3 是 pending
两者同时尝试 TaskUpdate(task #3, {status: "in_progress"})
文件锁 + 高水位标记保证原子性:
文件锁保证原子性:
- 第一个写入者获得 owner 锁定
- 第二个写入者收到 already_claimed 错误
@@ -166,8 +218,11 @@ unassignTeammateTasks()
→ 扫描任务列表,找到 owner === teammateName 的未完成任务
→ 重置为 pending + owner=undefined
Leader 通过 mailbox 收到通知
→ 重新分配或创建新 Teammate
Team Lead 感知途径:
1. 任务状态变化pending 重置)—— 通过共享任务列表
2. Mailbox 空闲通知TeammateIdle hook—— Teammate 停止时自动通知 Lead
Team Lead 重新分配任务或创建新 Teammate
```
## 任务类型全景
@@ -186,11 +241,11 @@ Leader 通过 mailbox 收到通知
`InProcessTeammateTask` 与 `LocalAgentTask` 的关键差异:前者共享进程的内存空间和基础设施状态(如 MCP 连接池),但有独立的对话上下文和工具权限;后者是完全隔离的子进程,启动开销更大但更安全。
## Coordinator vs Swarm 的选择
## Coordinator vs Agent Teams 的选择
| 场景 | 推荐模式 | 原因 |
|------|---------|------|
| "重构认证系统,需要多模块协调" | Coordinator | 需要集中决策Worker 间有依赖 |
| "修复 10 个独立的 lint 警告" | Swarm | 任务独立,可完全并行 |
| "修复 10 个独立的 lint 警告" | Agent Teams | 任务独立Teammate 可完全并行 |
| "研究方案 A 和方案 B然后选一个实现" | Coordinator | 先并行研究,再集中决策 |
| "在大仓库中搜索所有 TODO 并分类" | Swarm | 无依赖,各自领任务即可 |
| "在大仓库中搜索所有 TODO 并分类" | Agent Teams | 无依赖,各自领任务即可 |

View File

@@ -14,8 +14,8 @@ keywords: ["子 Agent", "AgentTool", "任务委派", "forkSubagent", "子进程
AI 生成 tool_use: { prompt: "修复 bug", subagent_type: "Explore" }
AgentTool.call() ← 入口AgentTool.tsx:239
├── 解析 effectiveTypefork vs 命名 agent
├── filterDeniedAgents() ← 权限过滤
├── 解析 effectiveTypefork vs 命名 agent vs GP 回退
├── filterDeniedAgents() ← 仅命名 Agent 路径执行:权限过滤
├── 检查 requiredMcpServers ← MCP 依赖验证(最长等 30s
├── assembleToolPool(workerPermissionContext) ← 独立组装工具池
├── createAgentWorktree() ← 可选 worktree 隔离
@@ -26,26 +26,30 @@ runAgent() ← 核心执行runAgent.ts:248
├── executeSubagentStartHooks() ← Hook 注入
├── query() ← 进入标准 agentic loop
│ ├── 消息流逐条 yield
│ └── recordSidechainTranscript() ← JSONL 持久化
│ └── recordSidechainTranscript() ← JSONL 持久化~/.claude/projects/{project}/{session}/subagents/
finalizeAgentTool() ← 结果汇总
├── 提取文本内容 + usage 统计
└── mapToolResultToToolResultBlockParam() ← 格式化为 tool_result
```
## 两种子 Agent 路径:命名 Agent vs Fork
## 子 Agent 的三种路径
`AgentTool.call()` 根据是否提供 `subagent_type` 走两条完全不同的路径(`AgentTool.tsx:322-356`
`AgentTool.call()` 根据 `subagent_type` 参数和 Fork 实验开关,走三条不同的路径
| 维度 | 命名 Agent`subagent_type` 指定) | Fork 子进程(`subagent_type` 省略) |
|------|-------------------------------------|--------------------------------------|
| **触发条件** | `subagent_type` 有值 | `isForkSubagentEnabled()` && 未指定类型 |
| **System Prompt** | Agent 自身的 `getSystemPrompt()` | 继承父 Agent 的完整 System Prompt |
| **工具池** | `assembleToolPool()` 独立组装 | 父 Agent 的原始工具池(`useExactTools: true` |
| **上下文** | 仅任务描述 | 父 Agent 的完整对话历史(`forkContextMessages` |
| **模型** | 可独立指定 | 继承父模型(`model: 'inherit'` |
| **权限模式** | Agent 定义的 `permissionMode` | `'bubble'`(上浮到父终端) |
| **目的** | 专业任务委派 | Prompt Cache 命中率优化 |
| 维度 | 命名 Agent`subagent_type` 指定) | Fork 子进程(Fork 启用 + 类型省略) | General-purpose 回退Fork 关闭 + 类型省略) |
|------|-------------------------------------|--------------------------------------|---------------------------------------------|
| **触发条件** | `subagent_type` 有值 | `isForkSubagentEnabled() === true` 且未指定类型 | `isForkSubagentEnabled() === false` 且未指定类型 |
| **System Prompt** | Agent 自身的 `getSystemPrompt()` | 继承父 Agent 的完整 System Prompt | General-purpose Agent 的 `getSystemPrompt()` |
| **工具池** | `assembleToolPool()` 独立组装 | 父 Agent 的原始工具池(`useExactTools: true` | `assembleToolPool()` 独立组装 |
| **上下文** | 仅任务描述 | 父 Agent 的完整对话历史(`forkContextMessages` | 仅任务描述 |
| **模型** | 可独立指定 | 继承父模型(`model: 'inherit'` | 可独立指定 |
| **权限模式** | Agent 定义的 `permissionMode` | `'bubble'`(上浮到父终端) | Agent 定义的 `permissionMode` |
| **目的** | 专业任务委派 | Prompt Cache 命中率优化 | 通用任务处理 |
<Note>
Fork 实验的门控函数 `isForkSubagentEnabled()` 需要同时满足三个前提:`FORK_SUBAGENT` feature flag 已启用、当前不在 Coordinator 模式中、且不是非交互式会话。任一条件不满足时,省略 `subagent_type` 会静默降级为 General-purpose Agent而非触发 Fork。
</Note>
Fork 路径的设计核心是 **Prompt Cache 共享**:所有 fork 子进程共享父 Agent 的完整 `assistant` 消息(所有 `tool_use` 块),用相同的占位符 `tool_result` 填充,只有最后一个 `text` 块包含各自的指令。这使得 API 请求前缀字节完全一致,最大化缓存命中。
@@ -64,9 +68,41 @@ Fork 子进程保留 Agent 工具(为了 cache-identical tool defs但通
1. **`querySource` 检查**(压缩安全):`context.options.querySource === 'agent:builtin:fork'`
2. **消息扫描**(降级兜底):检测 `<fork-boilerplate>` 标签
## 工具池的独立组装
### 模型解析优先级
子 Agent 不继承父 Agent 的工具限制——它的工具池完全独立组装(`AgentTool.tsx:573-577`
子 Agent 的模型选择遵循严格的优先级链(`src/utils/model/agent.ts`
```
1. CLAUDE_CODE_SUBAGENT_MODEL 环境变量 ← 全局覆盖
↓(未设置时)
2. 每次调用的 model 参数 ← AgentTool 入参
↓(未指定时)
3. Agent 定义的 model frontmatter ← 如 "sonnet", "haiku", "inherit"
↓(未定义时)
4. 继承父对话模型conversation model ← getDefaultSubagentModel() 返回 "inherit"
```
其中 `inherit` 不是简单的模型传递——它经过 `getRuntimeMainLoopModel()` 解析,确保 plan mode 下的 `opusplan→Opus` 等运行时映射正确生效。当 Agent 指定的模型族(如 `haiku`)与父模型同族时,直接复用父模型的精确 ID避免跨 provider 降级。
## 命名 Agent 的工具池独立组装
### 内置 Agent
系统预定义了几个内置 Agent`src/tools/AgentTool/builtinAgents.ts`),各有明确的职责和模型配置:
| Agent | 模型 | 权限 | 用途 |
|-------|------|------|------|
| **Explore** | Haiku轻量快速 | 只读Read/Grep/Glob | 代码库搜索与探索 |
| **Plan** | 继承父模型 | 只读 | 为 Plan Mode 收集研究信息 |
| **General-purpose** | 继承父模型 | 全部工具 | 复杂的通用任务处理 |
| **statusline-setup** | 继承父模型 | 受限 | 配置状态栏设置 |
| **claude-code-guide** | 继承父模型 | 受限 | 解答 Claude Code 使用问题 |
用户还可通过 `.claude/agents/` 目录或 settings 定义自定义 Agent作用域优先级为managed settings > CLI `--agents` > 项目级 `.claude/agents/` > 用户级 `~/.claude/agents/` > plugin。
命名 Agent包括 General-purpose 回退)不继承父 Agent 的工具限制——它的工具池完全独立组装。Fork 子进程则通过 `useExactTools: true` 直接继承父 Agent 的原始工具池,以保持 Prompt Cache 中工具定义的字节一致性。
命名 Agent 的工具池组装逻辑:
```typescript
const workerPermissionContext = {
@@ -93,6 +129,17 @@ const resolvedTools = useExactTools
`resolveAgentTools()` 会根据 Agent 定义中的 `tools` 字段过滤可用工具,将 `['*']` 映射为全量工具。
### Hook 事件
子 Agent 支持 Agent 定义 frontmatter 和全局 settings.json 两种级别的 Hook
| 来源 | 事件 | 说明 |
|------|------|------|
| Agent frontmatter `hooks` | `PreToolUse` / `PostToolUse` | 工具调用前后拦截 |
| Agent frontmatter `hooks` | `Stop` | 自动转换为 `SubagentStop``registerFrontmatterHooks` 传入 `isAgent=true` |
| settings.json | `SubagentStart` | 子 Agent 启动时触发(`executeSubagentStartHooks()` |
| settings.json | `SubagentStop` | 子 Agent 停止时触发 |
## Worktree 隔离机制
`isolation: "worktree"` 参数让子 Agent 在独立的 git worktree 中工作(`AgentTool.tsx:590-593`
@@ -115,19 +162,23 @@ Worktree 生命周期:
### 异步 Agent后台运行
当 `run_in_background=true``selectedAgent.background=true` Agent 立即返回 `async_launched` 状态`AgentTool.tsx:686-764`
当 `run_in_background=true``selectedAgent.background=true`、或系统判定应强制异步(如 `assistantForceAsync`、`proactiveModule` 激活)Agent 立即返回 `async_launched` 状态:
```
registerAsyncAgent(agentId, ...) ← 注册到 AppState.tasks
↓ (void — 火后不管)
runAsyncAgentLifecycle() ← 后台执行
runAsyncAgentLifecycle() ← 后台执行agentToolUtils.ts
├── runAgent().onCacheSafeParams ← 进度摘要初始化
├── 消息流迭代
├── completeAsyncAgent() ← 标记完成
├── classifyHandoffIfNeeded() ← 安全检查
├── finalizeAgentTool() 结果汇总(提取文本 + usage 统计)
├── completeAsyncAgent() ← 标记完成(先于通知,确保 TaskOutput 尽快解除阻塞)
├── classifyHandoffIfNeeded() ← 安全分类(需 TRANSCRIPT_CLASSIFIER feature
├── getWorktreeResult() ← Worktree 清理(如有隔离)
└── enqueueAgentNotification() ← 通知主 Agent
```
如果异步 Agent 提供了 `name` 参数,还会注册到 `agentNameRegistry`,支撑 `SendMessage` 工具通过名称路由到该 Agent。
异步 Agent 获得独立的 `AbortController`,不与父 Agent 共享——用户按 ESC 取消主线程不会杀掉后台 Agent。
### 同步 Agent前台运行
@@ -154,16 +205,16 @@ const raceResult = await Promise.race([
## 结果回传格式
`mapToolResultToToolResultBlockParam()` 根据状态返回不同格式`AgentTool.tsx:1298-1375`
`mapToolResultToToolResultBlockParam()` 根据状态返回不同格式:
| 状态 | 返回内容 |
|------|---------|
| `completed` | 内容 + `<usage>` 块token/tool_calls/duration |
| `async_launched` | agentId + outputFile 路径 + 操作指引 |
| `completed` | 内容 + `<usage>` 块token/tool_calls/duration;无内容时插入占位文本 `"(Subagent completed but returned no output.)"` 防止模型误判为空 |
| `async_launched` | agentId + outputFile 路径 + 操作指引(指引内容取决于 `canReadOutputFile`:有读取权限时提示通过 Read/Bash 查看进度,否则仅告知已启动) |
| `teammate_spawned` | agent_id + name + team_name |
| `remote_launched` | taskId + sessionUrl + outputFile |
对于一次性内置 AgentExplore、Plan`<usage>` 块被省略——每周节省约 1-2 Gtok 的上下文窗口
对于一次性内置 AgentExplore、Plan当**不存在** worktree 隔离时,`<usage>` 块和 agentId 尾部被省略——每周节省约 1-2 Gtok 的上下文窗口。存在 worktree 时仍需返回 `worktreePath` 和 `worktreeBranch` 信息
## MCP 依赖的等待机制
@@ -174,7 +225,7 @@ const MAX_WAIT_MS = 30_000 // 最长等 30 秒
const POLL_INTERVAL_MS = 500 // 每 500ms 轮询
```
早期退出条件:任何必需服务器进入 `failed` 状态时立即停止等待。工具可用性通过 `mcp__` 前缀工具名解析(`mcp__serverName__toolName`)判断。
早期退出条件:任何必需服务器进入 `failed` 状态时立即停止等待。工具可用性通过 `mcp__` 前缀工具名解析(`mcp__serverName__toolName`)判断。等待结束后如果仍有必需服务器未就绪,`call()` 会抛出错误并明确列出缺失的服务器名称。
## 适用场景

View File

@@ -143,14 +143,18 @@ call() — 实际执行
## 与 Agent 工具的联动
Agent 工具(`AgentTool`)的 `isolation` 参数决定子 Agent 是否在 worktree 中运行:
Agent 工具(`AgentTool`)的 `isolation` 参数决定子 Agent 是否在 worktree 中运行。注意 Agent 工具使用**专用的** `createAgentWorktree()``src/utils/worktree.ts`),而非用户会话用的 `createWorktreeForSession()`,两者有关键差异
- `isolation: "worktree"` → 调用 `createWorktreeForSession()`,子 Agent 在独立 worktree 中执行
- 无 isolation → 子 Agent 共享主工作目录
| 维度 | `createWorktreeForSession`(用户会话) | `createAgentWorktree`(子 Agent |
|------|---------------------------------------|----------------------------------|
| 调用者 | EnterWorktreeTool | AgentTool |
| Session 管理 | 设置 `currentWorktreeSession` | **不设置** `currentWorktreeSession` |
| 恢复已有 worktree | 直接复用 | 复用并 bump mtime防止被周期性清理误删 |
子 Agent 结束时的处理:
- **成功**:主 Agent 通过 `ExitWorktreeTool(action: "keep")` 保留 worktree然后手动合并
- **失败/放弃**:主 Agent 通过 `ExitWorktreeTool(action: "remove", discard_changes: true)` 清理
子 Agent 结束时的处理由 `cleanupWorktreeIfNeeded()` 自动完成——它不走 `ExitWorktreeTool`(因为 Agent worktree 没有会话状态,`ExitWorktreeTool` 的 `validateInput` 会拒绝)
- **有变更** → 保留 worktree返回 `worktreePath` 供主 Agent 后续合并
- **无变更** → 自动删除
- **Hook-based** → 始终保留
## Session 状态持久化
@@ -168,6 +172,7 @@ Agent 工具(`AgentTool`)的 `isolation` 参数决定子 Agent 是否在 wor
tmuxSessionName?: string, // 关联的 tmux session
hookBased?: boolean, // 是否由 hook 创建
creationDurationMs?: number, // 创建耗时(分析用)
usedSparsePaths?: boolean, // 是否使用了 sparse checkout
}
```

View File

@@ -43,9 +43,11 @@ export function asSystemPrompt(value: readonly string[]): SystemPrompt {
| 阶段 | 内容 | 缓存策略 |
|------|------|----------|
| **静态区** | Intro Section、System Rules、Doing Tasks、Actions、Using Tools、Tone & Style、Output Efficiency | 可跨组织缓存(`scope: 'global'` |
| **BOUNDARY** | `SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'` | 分界标记(不发送给 API |
| **BOUNDARY** | `SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'` | 分界标记(不发送给 API,仅用于分割静态区与动态区以实现全局缓存 |
| **动态区** | Session Guidance、Memory、Model Override、Env Info、Language、Output Style、MCP Instructions、Scratchpad、FRC、Summarize Tool Results、Token Budget、Brief | 每次会话不同(`scope: 'org'` 或无缓存) |
> **Boundary 是什么**:它把 System Prompt 分成"不变的静态区"和"因用户/会话而异的动态区"。静态区对所有用户相同,可获得 `scope: 'global'` 跨组织缓存;动态区每次不同,只能 `scope: 'org'` 或不缓存。它本身是一个特殊字符串,在发送给 API 前被移除AI 永远看不到。
### 动态区的 Section 注册表
动态区通过 `systemPromptSection()` / `DANGEROUS_uncachedSystemPromptSection()` 注册,这两个工厂函数定义于 `src/constants/systemPromptSections.ts`
@@ -151,9 +153,7 @@ MCP 工具列表在会话中可能变化(连接/断开),破坏了跨组织
这是缓存效率最高的模式。`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 之前的静态内容Intro、Rules、Tone & Style 等)对所有用户相同,可跨组织缓存。
### Boundary 插入条件
`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 标记**仅在特定条件**下插入:
> **Boundary 插入条件**`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 标记**仅在特定条件**下插入:
```typescript
// src/utils/betas.ts:226-229

View File

@@ -2,6 +2,7 @@
title: "多轮对话管理 - QueryEngine 会话编排与持久化"
description: "从源码角度解析 Claude Code 多轮对话管理QueryEngine 的会话状态机、JSONL transcript 持久化、成本追踪模型和模型热切换机制。"
keywords: ["多轮对话", "会话管理", "QueryEngine", "transcript", "成本追踪"]
sourceRef: "3ec5675 (2026-04-08)"
---
{/* 本章目标:从源码角度揭示会话编排、持久化存储、成本追踪和模型切换的完整链路 */}
@@ -11,15 +12,17 @@ keywords: ["多轮对话", "会话管理", "QueryEngine", "transcript", "成本
- **单轮**(一次 Agentic Loop`query()` 函数的一次完整执行——组装上下文 → 调 API → 处理工具调用 → 循环直到结束
- **多轮**(一个 Session`QueryEngine` 类管理的一次会话——跨越数十轮 `submitMessage()` 调用,持续数小时
`QueryEngine``src/QueryEngine.ts:186`)是单轮 Agentic Loop 之上的**会话编排器**,它管理的状态远不止消息列表:
`QueryEngine``src/QueryEngine.ts`,类定义)是单轮 Agentic Loop 之上的**会话编排器**,它管理的状态远不止消息列表:
```
QueryEngine 内部状态
QueryEngine 内部状态src/QueryEngine.ts 构造函数)
├── mutableMessages: Message[] ← 完整对话历史,跨 turn 累积
├── readFileState: FileStateCache ← 已读文件内容缓存,避免重复读取
├── totalUsage: NonNullableUsage ← 累计 token 消耗input/output/cache
├── permissionDenials: SDKPermissionDenial[] ← 权限拒绝记录
├── discoveredSkillNames: Set<string> ← 当前 turn 已发现的 skill
├── loadedNestedMemoryPaths: Set<string> ← 已加载的嵌套 memory 路径(防重复)
├── hasHandledOrphanedPermission: boolean ← 是否已处理孤立权限请求
└── abortController: AbortController ← 会话级中断控制
```
@@ -28,29 +31,37 @@ QueryEngine 内部状态
每次用户输入一条消息REPL 或 SDK 调用 `submitMessage()`,它会执行完整的 turn 初始化链路:
```typescript
// src/QueryEngine.ts:211 — 简化的 submitMessage 流程
async *submitMessage(prompt, options?): AsyncGenerator<SDKMessage> {
// src/QueryEngine.ts — QueryEngine.submitMessage() 简化流程
async *submitMessage(
prompt: string | ContentBlockParam[],
options?: { uuid?: string; isMeta?: boolean },
): AsyncGenerator<SDKMessage> {
// 1. 清除 turn 级追踪状态
this.discoveredSkillNames.clear()
// 2. 解析模型(用户可能中途切换了模型)
const mainLoopModel = userSpecifiedModel
? parseUserSpecifiedModel(userSpecifiedModel)
// 2. 解析模型(用户可能中途通过 setModel() 切换了模型)
const mainLoopModel = this.config.userSpecifiedModel
? parseUserSpecifiedModel(this.config.userSpecifiedModel)
: getMainLoopModel()
// 3. 动态组装 System Prompt每次 turn 都重新构建)
const { defaultSystemPrompt, userContext, systemContext } =
await fetchSystemPromptParts({ tools, mainLoopModel, mcpClients })
// 4. 包装权限检查(追踪每次拒绝)
const wrappedCanUseTool = async (tool, input, ...) => {
const result = await canUseTool(tool, input, ...)
if (result.behavior !== 'allow') {
this.permissionDenials.push({ tool_name: tool.name, ... })
this.permissionDenials.push({
type: 'permission_denial',
tool_name: sdkCompatToolName(tool.name),
tool_use_id: toolUseID,
tool_input: input,
})
}
return result
}
// 5. 调用核心 query() 函数执行 agentic loop
yield* query({
systemPrompt, messages: this.mutableMessages,
@@ -68,36 +79,43 @@ async *submitMessage(prompt, options?): AsyncGenerator<SDKMessage> {
### 存储路径
```
~/.claude/projects/<project-hash>/<session-id>.jsonl
~/.claude/projects/<sanitized-cwd>/<session-uuid>.jsonl
```
- `project-hash` 由 `getProjectDir(originalCwd)` 生成,同一项目目录的会话归入同一子目录
- 路径由 `getProjectDir(originalCwd)` 生成,使用 `sanitizePath()` 将项目目录路径转换为安全的目录名(非 hash同一项目目录的会话归入同一子目录
- 每条记录是一行 JSONJSONL 格式),支持追加写入而不需要读取-修改-写入整个文件
- 读取上限为 50MB`MAX_TRANSCRIPT_READ_BYTES`),防止超大会话导致 OOM
- 读取上限为 50MB`MAX_TRANSCRIPT_READ_BYTES` 常量,`src/utils/sessionStorage.ts`),防止超大会话导致 OOM
### Transcript 写入器
`TranscriptWriter``src/utils/sessionStorage.ts:1200+`)是一个写队列,确保并发消息追加不会互相覆盖:
`Project` 类`src/utils/sessionStorage.ts`,私有类)管理 transcript 的写入。它通过 `writeQueues`(按文件分组的写队列)和 `drainWriteQueue()`(定时批量刷写)确保并发消息追加不会互相覆盖:
```
写入流程:
appendEntryToFile(sessionId, entry)
写入流程(异步排队路径)
recordTranscript(sessionId, entry)
ensureCurrentSessionFile() ← 懒初始化:首次写入时才创建文件
project.enqueueWrite(filePath, entry) 入列到 writeQueues
序列化为 JSON + 换行符
scheduleDrain() ← 设置定时器FLUSH_INTERVAL_MS
appendFile(path, line) ← 原子追加
drainWriteQueue() ← 按 MAX_CHUNK_BYTES 分批
↓ 写入每批
appendToFile(path, batchContent) ← 批量追加
如果配置了远程持久化:
persistToRemote(sessionId, entry)
├── CCR v2: internalEventWriter('transcript', entry)
└── v1 Ingress: sessionIngress.appendSessionLog(...)
同步直写路径(用于元数据重写等场景):
appendEntryToFile(fullPath, entry) ← 同步 appendFileSync
失败时 mkdir + 重试
```
### 会话恢复链路
`--resume` 参数触发的恢复流程(`src/main.tsx:3620+`
`--resume` 参数触发的恢复流程(`src/main.tsx` 中 `--resume` 分支
```
1. 解析 resume 参数:
@@ -130,7 +148,7 @@ async *submitMessage(prompt, options?): AsyncGenerator<SDKMessage> {
### 累计层cost-tracker.ts
```typescript
// src/cost-tracker.ts — StoredCostState 数据模型
// src/cost-tracker.ts — StoredCostState 类型定义
type StoredCostState = {
totalCostUSD: number // 累计美元花费
totalAPIDuration: number // API 调用总时长(含重试)
@@ -138,7 +156,8 @@ type StoredCostState = {
totalToolDuration: number // 工具执行总时长
totalLinesAdded: number // 代码增加行数
totalLinesRemoved: number // 代码删除行数
modelUsage: { [modelName: string]: ModelUsage } // 按模型分拆的用量
lastDuration: number | undefined // 最近一次会话时长
modelUsage: { [modelName: string]: ModelUsage } | undefined // 按模型分拆的用量
}
```
@@ -156,18 +175,18 @@ saveCurrentSessionCosts(sessionId)
### 预算熔断
`QueryEngineConfig.maxBudgetUsd` 提供了会话级的硬性预算上限。在 REPL 中,当累计费用超过 $5 时(`src/screens/REPL.tsx:2208`),弹出费用提醒对话框——这不是硬性阻断,而是"软提醒"。
`QueryEngineConfig.maxBudgetUsd` 提供了会话级的硬性预算上限。在 REPL 中,当累计费用超过 $5 时(`src/screens/REPL.tsx` 中费用阈值 `useEffect`),弹出费用提醒对话框——这不是硬性阻断,而是"软提醒",且仅在 `hasConsoleBillingAccess()` 为 true 时显示
## 模型热切换
在一个会话中切换模型不会丢失对话历史——因为 `mutableMessages` 与模型选择是解耦的:
```
/model sonnet → setMainLoopModelOverride('claude-sonnet-4-20250514')
/model sonnet → QueryEngine.setModel('claude-sonnet-4-20250514')
实际操作this.config.userSpecifiedModel = modelQueryEngine.setModel() 方法)
下一次 submitMessage() 开始时:
parseUserSpecifiedModel(userSpecifiedModel)
parseUserSpecifiedModel(this.config.userSpecifiedModel)
→ 返回新的模型配置
fetchSystemPromptParts({ mainLoopModel: newModel })

View File

@@ -2,6 +2,7 @@
title: "流式响应机制 - Claude Code 打字机效果原理"
description: "解析 Claude Code 流式响应实现:如何通过 SSE 逐 token 接收 AI 输出,实现实时打字机效果,提升用户等待体验。"
keywords: ["流式响应", "SSE", "streaming", "实时输出", "API streaming"]
sourceRef: "3ec5675 (2026-04-08)"
---
## 为什么需要流式
@@ -31,7 +32,7 @@ message_stop ← 消息结束
### 事件处理状态机
`src/services/api/claude.ts:1980-2298` 实现了一个基于 `switch(part.type)` 的状态机:
`src/services/api/claude.ts` 中 `queryStreamRaw()` 函数的事件处理循环实现了一个基于 `switch(part.type)` 的状态机:
| 事件类型 | 处理逻辑 | 状态变更 |
|----------|----------|----------|
@@ -76,7 +77,7 @@ content_block_stop (index=2)
`stop_reason` 要等到 `message_delta` 才确定(可能是 `end_turn`、`tool_use`、`max_tokens` 等),所以最后一条消息的 `stop_reason` 是**回写**的:
```typescript
// claude.ts:2246 — 直接属性修改,不用对象替换
// claude.ts — stop_reason 回写逻辑(直接属性修改,不用对象替换
// 因为 transcript 写队列持有 message.message 的引用
const lastMsg = newMessages.at(-1)
if (lastMsg) {
@@ -89,16 +90,21 @@ if (lastMsg) {
### 网络断开
流式连接依赖 SSEServer-Sent Events。当连接中断时
流式连接依赖 SSEServer-Sent Events。当连接中断时,系统有两层检测机制
1. **Stream idle watchdog**:定时检测事件间隔超过阈值(stall触发告警和重试
2. **Stream abort**:如果 watchdog 检测到长时间无事件,抛出错误进入重试流程
3. **非流式降级**:作为最后手段,回退到非流式请求(一次性获取完整响应)
1. **被动停滞检测**`src/services/api/claude.ts` 中 stall 检测逻辑):当下一个事件到达时,计算与上一个事件的时间间隔超过阈值(30 秒,`STALL_THRESHOLD_MS = 30_000`)记录为一次 stall累积计数并写入遥测日志。这是被动检测——仅在下一个 chunk 到达时才触发,不会主动中断流。
2. **主动空闲超时看门狗**`src/services/api/claude.ts` 中 `STREAM_IDLE_TIMEOUT_MS` 看门狗逻辑):使用 `setTimeout` 设置 90 秒(可通过 `CLAUDE_STREAM_IDLE_TIMEOUT_MS` 环境变量覆盖)的硬性超时。如果在此期间没有收到任何事件,主动终止流并抛出错误进入重试流程
3. **非流式降级**:作为最后手段,设置 `didFallBackToNonStreaming` 标志,通过 `executeNonStreamingRequest()` 回退到非流式请求(一次性获取完整响应)
```typescript
// claude.ts:2338-2355 — 检测空流
// 1. 完全没有事件 → 代理返回了非 SSE 响应
// 2. 有 message_start 但没有 content_block_stop → 流被截断
// claude.ts — 被动停滞检测
const STALL_THRESHOLD_MS = 30_000 // 30 秒无事件视为停滞
let totalStallTime = 0
let stallCount = 0
// claude.ts — 主动空闲超时
const STREAM_IDLE_TIMEOUT_MS =
parseInt(process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS || '', 10) || 90_000
```
### API 限流
@@ -118,7 +124,7 @@ if (lastMsg) {
| **上下文窗口超限** | `model_context_window_exceeded` | 触发 compaction 压缩对话历史后重试 |
```typescript
// claude.ts:2267-2293
// claude.ts — stop_reason 处理
if (stopReason === 'max_tokens') {
yield createAssistantAPIErrorMessage({ error: 'max_output_tokens', ... })
}
@@ -133,8 +139,8 @@ if (stopReason === 'model_context_window_exceeded') {
系统持续监控事件到达间隔,检测"停滞"stall
```typescript
// claude.ts:1940-1966
const STALL_THRESHOLD_MS = 10_000 // 10 秒无事件视为停滞
// claude.ts — stall 检测逻辑
const STALL_THRESHOLD_MS = 30_000 // 30 秒无事件视为停滞
if (timeSinceLastEvent > STALL_THRESHOLD_MS) {
stallCount++
totalStallTime += timeSinceLastEvent
@@ -142,7 +148,7 @@ if (timeSinceLastEvent > STALL_THRESHOLD_MS) {
}
```
多个 stall 累积后watchdog 可能决定中断流并触发重试
这是**被动检测**——仅在下一个 chunk 到达时才触发比较。与之互补的是 90 秒主动空闲超时看门狗(`STREAM_IDLE_TIMEOUT_MS`),会直接中断长时间无响应的流
## 工具执行的流式反馈

View File

@@ -2,6 +2,7 @@
title: "Agentic LoopAI 自主循环的核心机制"
description: "深入解析 Claude Code 的 query() 异步生成器循环——从流式 API 调用、工具并行执行、上下文压缩、错误恢复到终止条件的完整状态机,基于 src/query.ts 的源码级分析。"
keywords: ["Agentic Loop", "query loop", "tool_use", "状态机", "auto-compact", "streaming", "recovery"]
sourceRef: "3ec5675 (2026-04-08)"
---
{/* 本章目标:基于 src/query.ts 揭示 Agentic Loop 的完整状态机 */}
@@ -11,7 +12,7 @@ keywords: ["Agentic Loop", "query loop", "tool_use", "状态机", "auto-compact"
传统聊天机器人:你问一句,它答一句。
Claude Code 不一样:你说一个需求,它可能连续执行十几步操作才给你最终结果。
这背后的机制叫做 **Agentic Loop**(智能体循环),核心实现在 `src/query.ts` 的 `queryLoop()` 异步生成器函数(第 241 行)。它是一个 `while(true)` 无限循环,每次迭代代表一次"思考→行动→观察"周期。
这背后的机制叫做 **Agentic Loop**(智能体循环),核心实现在 `src/query.ts` 的 `queryLoop()` 异步生成器函数。它是一个 `while(true)` 无限循环,每次迭代代表一次"思考→行动→观察"周期。
<Frame caption="Agentic Loop 循环示意">
<img src="/docs/images/agentic-loop.png" alt="Agentic Loop 循环图" />
@@ -19,7 +20,7 @@ Claude Code 不一样:你说一个需求,它可能连续执行十几步操
## 循环的完整结构
`queryLoop()` 的每次迭代(`src/query.ts:307` `while(true)`)包含以下阶段:
`queryLoop()` 的每次迭代(`src/query.ts` 中 `while(true)` 主循环)包含以下阶段:
### 阶段 1上下文预处理Pre-Processing Pipeline
@@ -39,7 +40,7 @@ messagesForQuery处理后的消息→ 发往 API
### 阶段 2流式 API 调用Streaming Loop
`deps.callModel()` 发起流式请求(第 659 行),返回一个 AsyncGenerator。在流式过程中
`deps.callModel()` 发起流式请求(`src/query.ts` 中 `attemptWithFallback` 循环内),返回一个 AsyncGenerator。在流式过程中
- **AssistantMessage** 被收集到 `assistantMessages[]` 数组
- **tool_use 块** 被提取到 `toolUseBlocks[]`,设置 `needsFollowUp = true`
@@ -47,8 +48,8 @@ messagesForQuery处理后的消息→ 发往 API
- 可恢复的错误prompt-too-long、max-output-tokens被**暂扣**withheld先尝试恢复
流式回调中的关键守卫:
- `backfillObservableInput()`(第 763 行)—— 为 tool_use 块回填可观察字段(如文件路径展开),但只在添加了新字段时才克隆消息,避免破坏 prompt cache 的字节一致性
- 流式降级检测——如果 `streamingFallbackOccured`,已收集的消息被标记为 tombstone(第 717 行),清空后重试
- `backfillObservableInput()` —— 为 tool_use 块回填可观察字段(如文件路径展开),但只在添加了新字段时才克隆消息,避免破坏 prompt cache 的字节一致性
- 流式降级检测——如果 `streamingFallbackOccured`,已收集的消息被标记为 tombstone清空后重试
### 阶段 3工具执行Tool Execution
@@ -67,42 +68,50 @@ const toolUpdates = streamingToolExecutor
每次迭代结束时,根据条件决定 `return`(终止)或 `continue`(继续):
## 7 种终止条件(源码级)
## 终止条件(源码级)
循环有多种终止路径,按触发时机排列:
| 终止原因 | 触发位置 | 机制 |
|----------|---------|------|
| **completed** | 第 1360 行 | AI 未发出 tool_use → `needsFollowUp = false` → 经过 stop hooks → 返回 |
| **blocking_limit** | 第 646 行 | Token 计数超过硬限制(非 autocompact 模式)→ 生成 PTL 错误消息 → 返回 |
| **aborted_streaming** | 第 1054 行 | `abortController.signal.aborted` → 为未完成的 tool_use 生成合成 tool_result → 返回 |
| **model_error** | 第 999 行 | `callModel()` 抛出异常 → 生成错误消息 → 返回 |
| **prompt_too_long** | 第 1178 行 | 413 错误且 reactive compact 无法恢复 → 暂扣的错误消息被释放 → 返回 |
| **image_error** | 第 980/1178 行 | 图片尺寸/大小错误 → 直接返回 |
| **image_error** | 第 980 行 | `ImageSizeError` / `ImageResizeError` 异常直接返回 |
| **model_error** | 第 999 行 | `callModel()` 抛出不可恢复异常 → 生成错误消息 → 返回 |
| **aborted_streaming** | 第 1054 行 | `abortController.signal.aborted`(流式阶段)→ 为未完成的 tool_use 生成合成 tool_result → 返回 |
| **prompt_too_long** | 第 1178/1185 行 | 413 错误且 reactive compact 无法恢复 → 暂扣的错误消息被释放 → 返回 |
| **completed** | 第 1267 行 | API 错误(限流、认证失败等)导致无法继续 → 返回 |
| **stop_hook_prevented** | 第 1282 行 | Stop hook 返回 `preventContinuation: true` → 返回 |
| **completed** | 第 1360 行 | 正常完成AI 未发出 tool_use → `needsFollowUp = false` → 经过 stop hooks → 返回 |
| **aborted_tools** | 第 1518 行 | `abortController.signal.aborted`(工具执行阶段)→ 返回 |
| **hook_stopped** | 第 1523 行 | 工具执行期间 hook 返回 `shouldPreventContinuation` → 返回 |
| **max_turns** | 第 1714 行 | 轮次计数超过 `maxTurns` 限制 → 返回 |
## 4 种继续条件(恢复路径)
## 继续条件(恢复路径)
循环不仅是一个简单的"有 tool_use 就继续",它还包含多种恢复/重试路径:
### 1. 正常工具循环
`needsFollowUp = true` → 执行工具 → 新消息追加到 `messagesForQuery` → `continue`
### 1. 正常工具循环`next_turn`
`needsFollowUp = true` → 执行工具 → 新消息追加到 `messagesForQuery` → state 重新赋值 → `continue`
### 2. max_output_tokens 恢复(第 1191-1255 行
当 AI 输出被截断时(`apiError === 'max_output_tokens'`
- **首次**:尝试将 `maxOutputTokens` 从默认值提升到 `ESCALATED_MAX_TOKENS`64K,无 meta 消息,静默重试
- **后续**注入恢复消息"Output token limit hit. Resume directly...",最多重试 `MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3` 次
- 恢复耗尽后,暂扣的错误消息被释放
### 2. max_output_tokens 恢复(`max_output_tokens_escalate` / `max_output_tokens_recovery`
当 AI 输出被截断时(`apiError === 'max_output_tokens'`,分两阶段恢复
- **提升阶段**`max_output_tokens_escalate`):首次截断时,将 `maxOutputTokens` 从默认值提升到 `ESCALATED_MAX_TOKENS`64K。静默重试,不注入 meta 消息
- **恢复阶段**`max_output_tokens_recovery`):提升后仍然截断时,注入恢复消息"Output token limit hit. Resume directly...",最多重试 `MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3` 次。恢复耗尽后,暂扣的错误消息被释放。
### 3. Prompt-Too-Long 恢复(第 1088-1186 行
当遇到 413 错误时,有两个恢复阶段
- **Context Collapse Drain**第 1097 行):提交所有已暂存的折叠,释放空间后重试。如果上一轮已经是 collapse_drain_retry 则跳过
- **Reactive Compact**第 1123 行):触发即时压缩,生成摘要后重试。`hasAttemptedReactiveCompact` 防止无限循环
### 3. Prompt-Too-Long 恢复(`collapse_drain_retry` / `reactive_compact_retry`
当遇到 413 错误时,按优先级尝试两种压缩策略
- **Context Collapse Drain**`collapse_drain_retry`):提交所有已暂存的折叠collapse,释放空间后重试。如果上一轮已经是 `collapse_drain_retry` 则跳过,避免无限循环。
- **Reactive Compact**`reactive_compact_retry`):如果 collapse drain 无法恢复触发即时压缩reactive compact,生成摘要后重试。`hasAttemptedReactiveCompact` 标志防止无限循环
### 4. Stop Hook 阻塞重试(第 1285-1308 行
### 4. Stop Hook 阻塞重试(`stop_hook_blocking`
Stop hook 可以注入阻塞错误消息,强制 AI 重新思考。新的消息(包含阻塞错误)被追加到对话中,`stopHookActive = true`,进入下一轮迭代。
### 5. Token Budget 继续提示(`token_budget_continuation`
当 `TOKEN_BUDGET` feature 启用时,如果 token 消耗达到阈值但未超出预算,注入 nudge 消息让 AI 加速收尾,然后继续。
## 模型降级Fallback
当主模型不可用时(`FallbackTriggeredError`第 897 行
当主模型不可用时(`FallbackTriggeredError``src/query.ts` 中 `attemptWithFallback` 循环的 catch 分支
1. 已收集的 `assistantMessages` 被清空tool_use 块收到合成 tool_result"Model fallback triggered"
2. 思维签名块被移除(`stripSignatureBlocks`)—— 因为思维签名与模型绑定,跨模型回放会 400
@@ -112,13 +121,14 @@ Stop hook 可以注入阻塞错误消息,强制 AI 重新思考。新的消息
## 状态机State 对象
每次迭代的状态通过 `State` 类型(第 204 行)传递:
每次迭代的状态通过 `State` 类型(`src/query.ts`,类型定义)传递:
```typescript
// src/query.ts — State 类型定义
type State = {
messages: Message[] // 当前对话消息
toolUseContext: ToolUseContext // 工具上下文(含权限)
autoCompactTracking: AutoCompactTrackingState // 压缩跟踪
autoCompactTracking: AutoCompactTrackingState | undefined // 压缩跟踪
maxOutputTokensRecoveryCount: number // 输出截断恢复计数
hasAttemptedReactiveCompact: boolean // 是否已尝试即时压缩
maxOutputTokensOverride: number | undefined // 输出 token 上限覆盖
@@ -133,7 +143,7 @@ type State = {
## Token Budget实验性
当 `TOKEN_BUDGET` feature 启用时(第 1311 行),循环在终止前会检查 token 消耗:
当 `TOKEN_BUDGET` feature 启用时(`src/query.ts` 中 `!needsFollowUp` 分支内的预算检查逻辑),循环在终止前会检查 token 消耗:
- **continuation**:未达到预算但超过阈值 → 注入 nudge 消息,让 AI 加速收尾
- **diminishing_returns**:检测到收益递减 → 提前终止
@@ -157,26 +167,31 @@ type State = {
```
迭代 1: 思考→行动
预处理: 无需压缩(上下文很短)
预处理管道: applyToolResultBudget → snipCompact(HISTORY_SNIP feature) → microcompact → applyCollapses(CONTEXT_COLLAPSE feature) → autocompact
→ 上下文很短,无需压缩
API 调用: 返回 tool_use(Glob, "**/*.ts")
工具执行: 返回 42 个文件路径
→ needsFollowUp = true, continue
→ needsFollowUp = true
→ transition: { reason: 'next_turn' }, continue
迭代 2: 思考→行动
预处理: 42 个文件结果仍在预算内
预处理管道: 42 个文件结果仍在预算内
API 调用: 返回 tool_use(Grep, "import.*from")
工具执行: 在 15 个文件中找到 120 条 import
→ needsFollowUp = true, continue
→ needsFollowUp = true
→ transition: { reason: 'next_turn' }, continue
迭代 3: 思考→行动(多轮)
预处理: 120 条 Grep 结果触发 microcompact → 摘要化
预处理管道: 120 条 Grep 结果触发 microcompact → 摘要化
API 调用: 返回 3 个 tool_use(FileEdit, ...)
工具执行: 删除 5 条未使用导入
→ needsFollowUp = true, continue
→ needsFollowUp = true
→ transition: { reason: 'next_turn' }, continue
迭代 4: 总结
API 调用: 返回纯文本"已清理 3 个文件中的 5 条未使用导入"
→ needsFollowUp = false
→ Stop hooks 通过
→ Token Budget 检查通过(如果启用)
→ return { reason: 'completed' }
```

View File

@@ -0,0 +1,17 @@
flowchart TB
START((输入)) --> CTX["Context 管理"]
CTX --> LLM["LLM 流式输出"]
LLM --> TC{tool_use?}
TC --> |是| EXEC["执行工具"]
EXEC --> CTX
TC --> |否| DONE((完成))
classDef proc fill:#eef,stroke:#66c,color:#224
classDef decision fill:#fee,stroke:#c66,color:#422
classDef io fill:#eff,stroke:#6cc,color:#244
class CTX,LLM,EXEC proc
class TC decision
class START,DONE io

View File

@@ -0,0 +1,40 @@
flowchart TB
START((输入)) --> CTX["Context 管理"]
CTX --> PRE["Pre-sampling Hook"]
PRE --> LLM["LLM 流式输出"]
LLM --> TC{tool_use?}
TC --> |是| PERM{需权限?}
PERM --> |是| USER["👤 用户审批"]
USER --> |allow| TOOL_PRE
USER --> |deny| DENIED["拒绝"]
PERM --> |否| TOOL_PRE["Pre-tool Hook"]
TOOL_PRE --> EXEC["并发执行工具"]
EXEC --> TOOL_POST["Post-tool Hook"]
TOOL_POST --> CTX
DENIED --> CTX
TC --> |否| POST["Post-sampling Hook"]
POST --> STOP{"Stop Hook"}
STOP --> |不通过| CTX
STOP --> |通过| BUDGET{"Token Budget"}
BUDGET --> |继续| CTX
BUDGET --> |完成| DONE((完成))
subgraph SUB["子 Agent"]
FORK["AgentTool"] --> RECURSE["递归调用"]
end
EXEC -.-> FORK
classDef proc fill:#eef,stroke:#66c,color:#224
classDef decision fill:#fee,stroke:#c66,color:#422
classDef hook fill:#ffe,stroke:#cc6,color:#442
classDef io fill:#eff,stroke:#6cc,color:#244
classDef sub fill:#efe,stroke:#6a6,color:#242
class CTX,LLM,EXEC proc
class TC,PERM,STOP,BUDGET decision
class PRE,TOOL_PRE,TOOL_POST,POST hook
class START,DONE,USER,DENIED io
class FORK,RECURSE sub

View File

@@ -1,14 +1,14 @@
---
title: "Hooks 生命周期钩子 - 执行引擎与拦截协议"
description: "从源码角度解析 Claude Code Hooks 系统22 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。"
description: "从源码角度解析 Claude Code Hooks 系统27 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。"
keywords: ["Hooks", "生命周期钩子", "拦截器", "PreToolUse", "Hook 协议"]
---
{/* 本章目标:从源码角度揭示 Hook 的执行引擎、匹配机制、返回值协议和生命周期管理 */}
## 22 种 Hook 事件
## 27 种 Hook 事件
Claude Code 定义了 22 种 Hook 事件(`coreTypes.ts:25-53`),覆盖完整的 Agent 生命周期:
Claude Code 定义了 27 种 Hook 事件(`HOOK_EVENTS` 数组,`src/entrypoints/sdk/coreTypes.ts`),覆盖完整的 Agent 生命周期:
| 阶段 | 事件 | 触发时机 | 匹配字段 |
|------|------|---------|---------|
@@ -32,6 +32,7 @@ Claude Code 定义了 22 种 Hook 事件(`coreTypes.ts:25-53`),覆盖完
| | `TaskCompleted` | 任务完成 | — |
| **MCP** | `Elicitation` | MCP 服务器请求用户输入 | `mcp_server_name` |
| | `ElicitationResult` | Elicitation 结果返回 | `mcp_server_name` |
| **通知** | `Notification` | 系统通知事件 | `notification_type` |
| **环境** | `ConfigChange` | 配置变更 | `source` |
| | `CwdChanged` | 工作目录变更 | — |
| | `FileChanged` | 文件变更 | `file_path` |
@@ -40,7 +41,11 @@ Claude Code 定义了 22 种 Hook 事件(`coreTypes.ts:25-53`),覆盖完
## 6 种 Hook 类型
Hooks 配置支持 6 种执行方式`src/types/hooks.ts`
Hooks 配置支持 6 种执行方式,类型定义分布在 3 个文件中
- **可持久化类型**`command`、`prompt`、`agent`、`http`)— Zod schema 定义在 `src/schemas/hooks.ts`,通过 `z.discriminatedUnion('type', [...])` 声明
- **callback 类型** — TypeScript 接口定义在 `src/types/hooks.ts`,用于 SDK 注册的内部 JS 函数
- **function 类型** — 定义在 `src/utils/hooks/sessionHooks.ts`,用于运行时动态注册的函数 Hook
| 类型 | 执行方式 | 适用场景 |
|------|---------|---------|
@@ -53,7 +58,7 @@ Hooks 配置支持 6 种执行方式(`src/types/hooks.ts`
## 执行引擎execCommandHook
`execCommandHook()``src/utils/hooks.ts:829-1417`)是命令型 Hook 的执行核心:
`execCommandHook()``src/utils/hooks.ts``execCommandHook` 函数)是命令型 Hook 的执行核心:
```
execCommandHook(hook, hookEvent, hookName, jsonInput, signal)
@@ -75,7 +80,7 @@ execCommandHook(hook, hookEvent, hookName, jsonInput, signal)
### 异步 Hook 的检测协议
Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务(`hooks.ts:1199-1246`
Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务(`isAsyncHookJSONOutput` 检测 + `executeInBackground` 调用
```typescript
const firstLine = firstLineOf(stdout).trim()
@@ -96,7 +101,7 @@ if (isAsyncHookJSONOutput(parsed)) {
## Hook 输出的 JSON Schema
同步 Hook 的输出遵循严格的 Zod schema`src/types/hooks.ts:49-567`
同步 Hook 的输出遵循严格的 Zod schema`syncHookResponseSchema`,定义在 `src/types/hooks.ts``hookJSONOutputSchema` 定义在 `src/schemas/hooks.ts`
```json
{
@@ -120,16 +125,25 @@ if (isAsyncHookJSONOutput(parsed)) {
| 事件 | 专有字段 | 作用 |
|------|---------|------|
| `PreToolUse` | `permissionDecision`, `updatedInput`, `additionalContext` | 拦截/修改工具输入 |
| `UserPromptSubmit` | `additionalContext` | 注入额外上下文 |
| `PreToolUse` | `permissionDecision`, `permissionDecisionReason`, `updatedInput`, `additionalContext` | 拦截/修改工具输入 |
| `PostToolUse` | `additionalContext`, `updatedMCPToolOutput` | 修改 MCP 工具输出 |
| `SessionStart` | `initialUserMessage`, `watchPaths` | 设置初始消息和文件监控 |
| `PostToolUseFailure` | `additionalContext` | 失败后注入上下文 |
| `UserPromptSubmit` | `additionalContext` | 注入额外上下文 |
| `SessionStart` | `additionalContext`, `initialUserMessage`, `watchPaths` | 设置初始消息和文件监控 |
| `PermissionRequest` | `decision`(含 `allow`/`deny` 子字段) | 权限请求的 Hook 决策 |
| `PermissionDenied` | `retry` | 指示是否重试 |
| `SubagentStart` | `additionalContext` | 子 Agent 启动时注入上下文 |
| `Elicitation` | `action`, `content` | 控制用户输入对话框 |
| `ElicitationResult` | `action`, `content` | Elicitation 结果处理 |
| `Notification` | `additionalContext` | 通知事件注入上下文 |
| `Setup` | `additionalContext` | 初始化时注入上下文 |
| `CwdChanged` | `watchPaths` | 目录变更后更新监控路径 |
| `FileChanged` | `watchPaths` | 文件变更后更新监控路径 |
| `WorktreeCreate` | `worktreePath` | Worktree 创建通知 |
## Hook 匹配机制getMatchingHooks
`getMatchingHooks()``hooks.ts:1685-1956`)负责从所有来源中查找匹配的 Hook
`getMatchingHooks()``src/utils/hooks.ts``getMatchingHooks` 函数)负责从所有来源中查找匹配的 Hook
### 多来源合并
@@ -143,7 +157,7 @@ getHooksConfig()
### 匹配规则
`matcher` 字段支持三种模式(`matchesPattern()`, `hooks.ts:1428-1463`
`matcher` 字段支持三种模式(`matchesPattern()` 函数,`src/utils/hooks.ts`
```
"Write" → 精确匹配
@@ -154,7 +168,7 @@ getHooksConfig()
### if 条件过滤
Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()``hooks.ts:1472-1503`)预编译匹配器:
Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()``src/utils/hooks.ts``prepareIfConditionMatcher` 函数)预编译匹配器:
```json
{
@@ -169,11 +183,11 @@ Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditio
### Hook 去重
同一个 Hook 命令在不同配置层级user/project/local可能重复。系统按 `pluginRoot\0command` 做 Map 去重,保留**最后合并的层级**。
同一个 Hook 命令在不同配置层级user/project/local可能重复。系统按四部分复合键做 Map 去重:`${pluginRoot}\0${shell}\0${command}\0${ifCondition}`(由 `hookDedupKey()` 函数构建),保留**最后合并的层级**。
## 工作区信任检查
**所有 Hook 都要求工作区信任**`shouldSkipHookDueToTrust()`, `hooks.ts:286-296`)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。
**所有 Hook 都要求工作区信任**`shouldSkipHookDueToTrust()` 函数,`src/utils/hooks.ts`)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。
```typescript
// 交互模式下,所有 Hook 要求信任
@@ -226,13 +240,13 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
## Session Hook 的生命周期
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(`runAgent.ts:567-575`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()` 清理。
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(调用位置:`src/tools/AgentTool/runAgent.ts`;定义位置:`src/utils/hooks/registerFrontmatterHooks.ts`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()`(定义位置:`src/utils/hooks/sessionHooks.ts`清理。
```typescript
// runAgent.ts:567 — 注册 agent 的前置 Hook
// runAgent.ts — 注册 agent 的前置 Hook
registerFrontmatterHooks(rootSetAppState, agentId, agentDefinition.hooks, ...)
// runAgent.ts:820 — finally 块清理
// runAgent.ts — finally 块清理
clearSessionHooks(rootSetAppState, agentId)
```

View File

@@ -0,0 +1,346 @@
---
title: "MCP 配置 - 多来源合并、作用域与策略管控"
description: "详细说明 Claude Code MCP 配置的来源层次、合并优先级、传输类型、企业策略管控、插件集成和保留名称机制。"
keywords: ["MCP", "配置", "settings.json", ".mcp.json", "企业策略", "插件"]
---
## 配置来源与作用域
Claude Code 的 MCP 配置来自多个来源,每个来源对应一个 `scope`(作用域)。配置按优先级合并,高优先级来源的同名配置覆盖低优先级。
### 来源列表
| 来源 | Scope | 文件/接口 | 说明 |
|------|-------|----------|------|
| 企业管控 | `enterprise` | 系统管理路径 `managed-mcp.json` | **排他模式**:存在时忽略所有其他来源 |
| 本地项目 | `local` | `<project>/.claude/settings.local.json` | 项目级私有配置(不提交到 VCS |
| 项目配置 | `project` | `<project>/.mcp.json` | 项目级共享配置(可提交到 VCS |
| 用户全局 | `user` | `~/.claude/settings.json` | 用户级配置,所有项目共享 |
| 插件 | `dynamic` | 插件 manifest 中 `.mcp.json` / `.mcpb` | 插件提供的 MCP 服务器 |
| claude.ai | `claudeai` | 通过 API 获取 | claude.ai 网页端配置的连接器 |
| 内置动态 | `dynamic` | 代码中注册 | Computer Use / Chrome 等内置服务器 |
| IDE SDK | `sdk` | IDE 传入 | VS Code / JetBrains 嵌入模式 |
### 合并优先级(从低到高)
```
claude.ai 连接器 ← 最低优先级
↓ 去重
插件服务器
↓ 去重
用户全局配置
项目配置(.mcp.json ← 需要用户审批
本地项目配置
动态配置(内置 MCP ← 最高优先级
```
`Object.assign({}, dedupedPluginServers, userServers, approvedProjectServers, localServers)` 实现合并——后出现的同名键覆盖前者。
## 企业管控模式
当 `managed-mcp.json` 文件存在时,进入 **排他模式**
```typescript
// config.ts:1084
if (doesEnterpriseMcpConfigExist()) {
// 只返回企业配置,忽略所有用户/项目/插件/claude.ai 配置
return { servers: filtered, errors: [] }
}
```
特性:
- 路径由系统管理决定(`getManagedFilePath()` + `managed-mcp.json`
- 覆盖所有用户级、项目级、插件和 claude.ai 配置
- 仍然应用策略过滤allowlist/denylist
- 无法通过 CLI 添加新服务器(`addMcpConfig` 会拒绝)
## 传输类型与配置 Schema
### stdio默认
启动子进程,通过 stdin/stdout JSON-RPC 通信。
```json
{
"my-server": {
"command": "npx",
"args": ["-y", "@my-org/mcp-server"],
"env": { "API_KEY": "..." }
}
}
```
`type` 字段可省略(默认为 `stdio`)。环境变量通过 `env` 传递给子进程,会与当前进程环境合并。
**Windows 注意**:使用 `npx` 需要包装为 `cmd /c npx`,否则会报错。
### SSEServer-Sent Events
通过 HTTP SSE 连接远程 MCP 服务器。
```json
{
"my-remote": {
"type": "sse",
"url": "https://mcp.example.com/sse",
"headers": { "Authorization": "Bearer ..." },
"oauth": {
"clientId": "...",
"authServerMetadataUrl": "https://auth.example.com/.well-known/oauth-authorization-server"
}
}
}
```
支持 OAuth 认证流程。认证失败时进入 `needs-auth` 状态15 分钟 TTL 缓存避免重复提示。
### HTTPStreamable HTTP
HTTP 流式传输。
```json
{
"my-http": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": { "X-API-Key": "..." }
}
}
```
支持与 SSE 相同的 OAuth 配置。
### WebSocket
```json
{
"my-ws": {
"type": "ws",
"url": "wss://mcp.example.com/ws"
}
}
```
### IDE 专用类型(内部)
`sse-ide` 和 `ws-ide` 是 IDE 扩展专用类型,不由用户直接配置。
- `sse-ide`:使用 lockfile token 认证
- `ws-ide`:使用 `X-Claude-Code-Ide-Authorization` header
### SDK 类型(内部)
`type: "sdk"` 由 IDE 嵌入模式传入,不经过保留名称检查和企业管控排他限制。
### claude.ai 代理类型(内部)
`type: "claudeai-proxy"` 由 claude.ai 网页端配置的连接器使用,通过 OAuth bearer token 认证并支持 401 重试。
## 配置操作
### 添加 MCP 服务器
通过 CLI 命令 `claude mcp add` 或 API 调用 `addMcpConfig()`
```bash
# 添加到用户配置
claude mcp add my-server -s user -- npx @my-org/mcp-server
# 添加到项目配置
claude mcp add my-server -s project -- npx @my-org/mcp-server
# 添加 HTTP 类型
claude mcp add my-remote -s user -t http -u https://mcp.example.com/mcp
```
添加时的验证流程:
1. **名称校验**:只允许字母、数字、连字符和下划线
2. **保留名检查**`claude-in-chrome` 和 `computer-use` 被保留
3. **企业管控检查**:企业模式下拒绝添加
4. **Schema 验证**Zod 校验配置格式
5. **策略检查**denylist 拒绝、allowlist 验证
### 移除 MCP 服务器
```bash
claude mcp remove my-server -s user
```
### 列出 MCP 服务器
```bash
claude mcp list
```
## 项目配置审批
`.mcp.json` 中的项目配置需要用户显式审批才能生效:
```typescript
// config.ts:1166
const approvedProjectServers: Record<string, ScopedMcpServerConfig> = {}
for (const [name, config] of Object.entries(projectServers)) {
if (getProjectMcpServerStatus(name) === 'approved') {
approvedProjectServers[name] = config
}
}
```
首次打开项目时Claude Code 会提示用户审批 `.mcp.json` 中的每个服务器。审批状态持久化在本地配置中。
## 插件 MCP 集成
插件通过 manifest 中的 `.mcp.json` 或 `.mcpb` 文件声明 MCP 服务器:
```typescript
// 插件 MCP 加载流程
const pluginResult = await loadAllPluginsCacheOnly()
const pluginServerResults = await Promise.all(
pluginResult.enabled.map(plugin => getPluginMcpServers(plugin, mcpErrors))
)
```
### 插件命名空间
插件 MCP 服务器名格式为 `plugin:<pluginName>:<serverName>`,不会与手动配置的名称冲突。
### 去重机制
插件服务器通过内容签名去重(`dedupPluginMcpServers`
- **stdio 类型**:签名 = `stdio:` + JSON.stringify([command, ...args])
- **URL 类型**:签名 = `url:` + 原始 URLunwrap CCR proxy URL
- **sdk 类型**:签名为 null不去重
去重规则:
1. 手动配置优先于插件配置
2. 先加载的插件优先于后加载的
3. 被抑制的插件服务器在 `/plugin` UI 中显示提示
### claude.ai 连接器去重
claude.ai 连接器使用相同的内容签名机制去重(`dedupClaudeAiMcpServers`
- 仅启用的手动配置参与去重(禁用的手动配置不应抑制连接器)
- 连接器名格式为 `claude.ai <DisplayName>`
## 策略管控
### Allowlist / Denylist
企业策略通过 allowlist 和 denylist 控制可用的 MCP 服务器:
```typescript
// config.ts:1243 - 最终策略过滤
for (const [name, serverConfig] of Object.entries(configs)) {
if (!isMcpServerAllowedByPolicy(name, serverConfig)) {
continue // 跳过策略禁止的服务器
}
filtered[name] = serverConfig
}
```
策略检查考虑:
- 服务器名称匹配
- stdio 类型的 command + args 匹配
- URL 类型的 URL 模式匹配(支持通配符)
### 插件专用模式
`isRestrictedToPluginOnly('mcp')` 启用时,只允许插件提供的 MCP 服务器——用户/项目级配置被忽略。
## 环境变量展开
MCP 配置中的环境变量支持 `$VAR` 和 `${VAR}` 语法展开:
```json
{
"my-server": {
"command": "npx",
"args": ["@my-org/mcp-server"],
"env": {
"API_KEY": "$MY_API_KEY",
"DB_URL": "${DATABASE_URL}"
}
}
}
```
展开时缺失的变量会生成警告信息,但不阻止配置加载。
## 内置 MCP 动态注册
内置 MCP 服务器在 `main.tsx` 启动流程中动态注入配置:
### Computer Use MCP
```typescript
// src/utils/computerUse/setup.ts
export function setupComputerUseMCP(): {
mcpConfig: Record<string, ScopedMcpServerConfig>
allowedTools: string[]
} {
return {
mcpConfig: {
"computer-use": {
type: "stdio",
command: process.execPath,
args: ["--computer-use-mcp"],
scope: "dynamic",
}
},
allowedTools: ["mcp__computer-use__screenshot", ...]
}
}
```
启用条件:
- Feature flag `CHICAGO_MCP` 开启
- `getPlatform() !== "unknown"`macOS/Windows/Linux
- 非非交互式会话
- GrowthBook gate `getChicagoEnabled()` 返回 true
### Claude in Chrome MCP
```typescript
// 类似 Computer Use在 main.tsx 中注册
const { mcpConfig, allowedTools, systemPrompt } = setupClaudeInChrome()
dynamicMcpConfig = { ...dynamicMcpConfig, ...mcpConfig }
```
启用条件:
- `--chrome` 参数或 `claudeInChromeDefaultEnabled` 配置
- Chrome 扩展已安装
### VSCode SDK MCP
IDE 嵌入模式通过初始化消息传入 `type:'sdk'` 的配置,由 `setupVscodeSdkMcp()` 设置双向通知。
## 保留名称
以下 MCP 服务器名称被保留,用户无法手动配置同名服务器:
| 名称 | 用途 | 检查条件 |
|------|------|---------|
| `claude-in-chrome` | Chrome 浏览器控制 | 始终检查 |
| `computer-use` | 桌面自动化 | `CHICAGO_MCP` feature flag 开启时检查 |
| `claude-vscode` | VSCode IDE 集成 | 由 SDK 传入,不经过名称检查 |
保留名检查在两个位置:
1. `addMcpConfig()``config.ts:636-648`)— 运行时拒绝
2. `main.tsx` 启动检查(`main.tsx:2351-2368`)— 启动时退出
## 关键源文件索引
| 文件 | 职责 |
|------|------|
| `src/services/mcp/config.ts` | 配置管理核心:合并、去重、策略、添加/删除 |
| `src/services/mcp/types.ts` | Zod Schema 定义、类型声明 |
| `src/services/mcp/client.ts` | 连接管理、传输层选择 |
| `src/utils/plugins/mcpPluginIntegration.ts` | 插件 MCP 配置加载 |
| `src/utils/computerUse/setup.ts` | Computer Use 动态注册 |
| `src/utils/claudeInChrome/common.ts` | Chrome MCP 保留名与工具名 |
| `src/services/mcp/vscodeSdkMcp.ts` | VSCode SDK 双向通知 |

View File

@@ -1,25 +1,32 @@
---
title: "MCP 协议 - 连接管理、工具发现与执行链路"
description: "从源码角度解析 Claude Code 的 MCP 集成7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。"
keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现"]
description: "从源码角度解析 Claude Code 的 MCP 集成:内置 MCP 与外部 MCP 的区别、7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。"
keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现", "内置 MCP", "外部 MCP"]
---
{/* 本章目标:从源码角度揭示 MCP 客户端的连接管理、工具发现协议和执行链路 */}
{/* 本章目标:从源码角度揭示 MCP 客户端的两种运行模式(内置/外部)、连接管理、工具发现协议和执行链路 */}
## 架构总览:从配置到可用工具
```
settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } }
配置层(多来源合并)
├── settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } } ← 外部
├── .mcp.json: 项目级 MCP 配置 ← 外部
├── 插件 manifest (.mcp.json / .mcpb) ← 外部(插件)
├── claude.ai connectors ← 外部(远程)
├── enterprise managed-mcp.json ← 外部(企业管控)
├── setupComputerUseMCP() / setupClaudeInChrome() ← 内置(动态注册)
└── SDK 传入 (type:'sdk') ← 内置IDE 嵌入)
getAllMcpConfigs() ← 合并 user/project/local 三级配置
getAllMcpConfigs() ← enterprise 独占 或 合并 user/project/local + plugin + claude.ai
useManageMCPConnections() ← React Hook 管理连接生命周期
connectToServer(name, config) ← memoize 缓存lodash memoize
├── 创建 Transportstdio/sse/http/...
├── new Client() ← @modelcontextprotocol/sdk
├── client.connect(transport) ← 超时控制MCP_TIMEOUT, 默认 30s
└── 返回 MCPServerConnection ← { connected | failed | needs-auth | pending }
├── 判断:内置 MCP → InProcessTransport同进程
├── 判断:外部 stdio → StdioClientTransport子进程
├── 判断:远程 SSE/HTTP/WS → 网络传输
└── 返回 MCPServerConnection ← { connected | failed | needs-auth | pending | disabled }
fetchToolsForClient(client) ← LRU(20) 缓存
├── client.request({ method: 'tools/list' })
@@ -30,19 +37,208 @@ assembleToolPool() ← 合并内置工具 + MCP 工具
工具名格式: mcp__<serverName>__<toolName> ← buildMcpToolName()
```
## 两种 MCP 模式:内置 vs 外部
Claude Code 的 MCP 实现区分 **内置 MCP 服务器** 和 **外部 MCP 服务器**。两者使用相同的客户端协议和工具发现机制,但在连接方式、生命周期管理和配置来源上完全不同。
### 内置 MCP 服务器
内置 MCP 服务器由 Claude Code 自身提供,无需用户手动配置。它们在启动时自动注册为 `dynamic` scope 的配置,并在同进程内运行。
| 服务器 | 名称 | 包路径 | Feature Flag | 启用方式 |
|--------|------|--------|-------------|---------|
| Computer Use | `computer-use` | `@ant/computer-use-mcp` | `CHICAGO_MCP` | GrowthBook gate + macOS + interactive |
| Claude in Chrome | `claude-in-chrome` | `@ant/claude-for-chrome-mcp` | — | `--chrome` 参数或 `claudeInChromeDefaultEnabled` 配置 |
| VSCode SDK | `claude-vscode` | — | — | IDE 嵌入模式 (type:`sdk`) |
#### InProcessTransport零开销同进程通信
内置服务器通过 `InProcessTransport``src/services/mcp/InProcessTransport.ts`)运行,**不启动子进程**
```typescript
// 创建一对 linked transport —— 消息在两端之间直接传递
const [clientTransport, serverTransport] = createLinkedTransportPair()
// server 端连接到 serverTransport
inProcessServer = createComputerUseMcpServerForCli()
await inProcessServer.connect(serverTransport)
// client 端使用 clientTransport与外部 MCP 的 Client 相同接口)
transport = clientTransport
```
`InProcessTransport` 的核心设计:
- `send()` 通过 `queueMicrotask()` 异步投递消息到对端,避免同步请求/响应的栈深度问题
- `close()` 双向关闭,任一端关闭都会触发两端的 `onclose` 回调
- 无网络开销、无 IPC 序列化、无进程启动时间
#### 动态注册流程
内置服务器在 `main.tsx` 的启动流程中注册,注入 `dynamicMcpConfig`
```typescript
// main.tsx: Computer Use MCP 动态注册
if (feature("CHICAGO_MCP") && getPlatform() !== "unknown" && !getIsNonInteractiveSession()) {
const { getChicagoEnabled } = await import("src/utils/computerUse/gates.js")
if (getChicagoEnabled()) {
const { setupComputerUseMCP } = await import("src/utils/computerUse/setup.js")
const { mcpConfig, allowedTools } = setupComputerUseMCP()
dynamicMcpConfig = { ...dynamicMcpConfig, ...mcpConfig }
allowedTools.push(...cuTools)
}
}
```
`setupComputerUseMCP()` 返回的配置(`src/utils/computerUse/setup.ts`
```typescript
{
"computer-use": {
type: "stdio", // 类型标记为 stdio但 client.ts 会拦截为 InProcessTransport
command: process.execPath,
args: ["--computer-use-mcp"],
scope: "dynamic", // 动态作用域,不持久化
}
}
```
#### 连接时拦截
`connectToServer()` 在 `client.ts:906-944` 中根据服务器名拦截内置服务器:
```typescript
// Chrome MCP — 在 process 内运行,避免 ~325MB 子进程
if (isClaudeInChromeMCPServer(name)) {
const { createChromeContext } = await import('../../utils/claudeInChrome/mcpServer.js')
const { createClaudeForChromeMcpServer } = await import('@ant/claude-for-chrome-mcp')
const { createLinkedTransportPair } = await import('./InProcessTransport.js')
const context = createChromeContext(config.env)
inProcessServer = createClaudeForChromeMcpServer(context)
const [clientTransport, serverTransport] = createLinkedTransportPair()
await inProcessServer.connect(serverTransport)
transport = clientTransport
}
// Computer Use MCP — 同理
if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
const { createComputerUseMcpServerForCli } = await import('../../utils/computerUse/mcpServer.js')
const { createLinkedTransportPair } = await import('./InProcessTransport.js')
inProcessServer = await createComputerUseMcpServerForCli()
const [clientTransport, serverTransport] = createLinkedTransportPair()
await inProcessServer.connect(serverTransport)
transport = clientTransport
}
```
#### 保留名称保护
内置服务器的名称被保留,用户无法手动添加同名配置(`config.ts:636-648`
```typescript
// 添加 MCP 配置时检查保留名
if (isClaudeInChromeMCPServer(name)) {
throw new Error(`Cannot add MCP server "${name}": this name is reserved.`)
}
if (feature('CHICAGO_MCP') && isComputerUseMCPServer(name)) {
throw new Error(`Cannot add MCP server "${name}": this name is reserved.`)
}
```
启动时也有全局检查(`main.tsx:2351-2368`):如果用户配置中包含保留名(非 `type:'sdk'`),直接 `process.exit(1)`。
#### VSCode SDK MCP
VSCode SDK MCP 是特殊的内置模式。IDE如 VS Code、JetBrains通过嵌入方式启动 Claude Code并传入 `type:'sdk'` 的 MCP 配置。这类配置:
- 不经过保留名称检查IDE 可以使用任意名称)
- 不参与 enterprise MCP 的排他控制
- 通过 VSCode SDK transport 连接
- 支持双向通知(如 `file_updated`、`experiment_gates`
```typescript
// src/services/mcp/vscodeSdkMcp.ts
export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void {
const client = sdkClients.find(client => client.name === 'claude-vscode')
if (client && client.type === 'connected') {
// 注册 log_event 通知处理器
client.client.setNotificationHandler(LogEventNotificationSchema(), ...)
// 发送实验门控到 VSCode
client.client.notification({ method: 'experiment_gates', params: { gates } })
}
}
```
### 外部 MCP 服务器
外部 MCP 服务器由用户在配置文件中声明,通过子进程或网络连接运行。
#### 配置来源
| 来源 | Scope | 文件位置 | 优先级 |
|------|-------|---------|--------|
| 项目配置 | `project` | `<project>/.mcp.json` | 最高(同名覆盖) |
| 本地配置 | `local` | `<project>/.claude/settings.local.json` | 高 |
| 用户配置 | `user` | `~/.claude/settings.json` | 中 |
| 插件 | `dynamic` | 插件 manifest 中 `.mcp.json` | 中 |
| claude.ai | `claudeai` | 通过 API 获取 | 低 |
| 企业管控 | `enterprise` | 系统管理路径 `managed-mcp.json` | 排他(存在时覆盖全部) |
#### 配置示例
```json
// settings.json / .mcp.json 中的 MCP 配置
{
"mcpServers": {
// stdio 类型 — 启动子进程
"my-database": {
"command": "npx",
"args": ["@my-org/db-mcp-server"],
"env": { "DB_URL": "postgres://..." }
},
// HTTP 流类型 — 远程服务器
"remote-api": {
"type": "http",
"url": "https://api.example.com/mcp"
},
// SSE 类型 — Server-Sent Events
"realtime-feed": {
"type": "sse",
"url": "https://feed.example.com/sse"
},
// WebSocket 类型
"ws-service": {
"type": "ws",
"url": "wss://ws.example.com/mcp"
}
}
}
```
#### 配置合并与去重
`getAllMcpConfigs()``config.ts`)按优先级合并多个来源的配置:
1. 企业管控配置存在时,**独占返回**(忽略所有其他来源)
2. 否则合并user → project → local → plugin → claude.ai
3. 插件与手动配置去重:通过 `getMcpServerSignature()` 生成内容签名(基于 command/args/url插件配置被同名手动配置抑制
4. `addScopeToServers()` 为每个配置项标注来源 scope
## 7 种传输层实现
`connectToServer()``client.ts:596-1643`)根据 `config.type` 分发到不同的 Transport 实现:
| 传输类型 | Transport 类 | 适用场景 | 认证方式 |
|----------|-------------|---------|---------|
| `stdio`(默认) | `StdioClientTransport` | 本地子进程 | 无 |
| `stdio`(默认) | `StdioClientTransport` | 外部本地子进程 | 无 |
| `sse` | `SSEClientTransport` | 远程 SSE 服务 | `ClaudeAuthProvider` + OAuth |
| `http` | `StreamableHTTPClientTransport` | HTTP 流 | `ClaudeAuthProvider` + OAuth |
| `sse-ide` | `SSEClientTransport` | IDE 集成 | lockfile token |
| `ws-ide` | `WebSocketTransport` | IDE WebSocket | `X-Claude-Code-Ide-Authorization` |
| `ws` | `WebSocketTransport` | WebSocket 服务 | session ingress token |
| `claudeai-proxy` | `StreamableHTTPClientTransport` | claude.ai 代理 | OAuth bearer + 401 重试 |
| InProcess内置 | `InProcessTransport` | Computer Use / Chrome | 无(同进程) |
### stdio 传输的进程管理
@@ -112,9 +308,17 @@ timer.unref?.() // 不阻止进程退出
```typescript
const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
// 结果: "mcp__my-db__query"
// 结果: "mcp__my-database__query"
```
### 内置 MCP 的工具发现
内置 MCP 服务器虽然使用 InProcessTransport但工具发现流程与外部服务器完全一致
- **Computer Use**`createComputerUseMcpServerForCli()` 在 `src/utils/computerUse/mcpServer.ts` 中构建 MCP Server 对象,注册 `ListToolsRequestSchema` handler。工具描述包含平台特定的已安装应用列表1s 超时枚举)。
- **Claude in Chrome**`createClaudeForChromeMcpServer()` 在 `@ant/claude-for-chrome-mcp` 包中构建 Server提供 17+ 个浏览器控制工具。
- **VSCode SDK**:由 IDE 端提供工具列表,通过 SDK transport 传递。
### 工具描述截断
MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`。OpenAPI 生成的 MCP 服务器曾观察到 15-60KB 的描述文档。
@@ -134,6 +338,8 @@ MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`。OpenAPI
MCP 工具默认返回 `{ behavior: 'passthrough' }``client.ts:1816-1834`),意味着它们始终进入权限确认流程。工具名使用 `mcp__` 前缀精确匹配权限规则。
内置 MCP 服务器的工具通过 `allowedTools` 列表自动授权——在 `main.tsx` 启动时加入,绕过普通权限提示。例如 Computer Use 工具的 `request_access` 自行处理会话级审批。
## MCP 工具的执行链路
```
@@ -169,23 +375,33 @@ getRemoteMcpServerConnectionBatchSize() // 默认 20
本地 MCP 服务器stdio是重量级的子进程默认限制 3 个并发连接。远程服务器是轻量级 HTTP 请求,允许 20 个并发。
## 实际配置示例
## 内置 vs 外部 MCP 对比总结
```json
// settings.json 中的 MCP 配置
{
"mcpServers": {
"my-database": {
"command": "npx",
"args": ["@my-org/db-mcp-server"],
"env": { "DB_URL": "postgres://..." }
},
"remote-api": {
"type": "http",
"url": "https://api.example.com/mcp"
}
}
}
```
| 维度 | 内置 MCP | 外部 MCP |
|------|---------|---------|
| **Transport** | `InProcessTransport`(同进程) | stdio / SSE / HTTP / WebSocket |
| **配置来源** | `setupComputerUseMCP()` / `setupClaudeInChrome()` 等动态注册 | settings.json / .mcp.json / 插件 / claude.ai |
| **Scope** | `dynamic` | `user` / `project` / `local` / `enterprise` / `claudeai` |
| **进程模型** | 同进程,零开销 | 子进程stdio或网络连接 |
| **名称保护** | 保留名,用户不可添加同名 | 自由命名(字母数字 + `-_` |
| **生命周期** | 随 CLI 启停 | 连接缓存 + 按需重连 |
| **权限** | `allowedTools` 自动授权 | `passthrough` 进入权限确认 |
| **Feature Flag** | `CHICAGO_MCP`Computer Use等 | 无(始终可用) |
| **工具发现** | 与外部相同MCP 协议) | 标准 MCP `tools/list` |
| **清理** | `inProcessServer.close()` | 信号升级策略 SIGINT→SIGTERM→SIGKILL |
配置后AI 的工具列表中会出现 `mcp__my-database__query` 和 `mcp__remote-api__*` 工具——与内置工具使用相同的权限检查链路和 UI 渲染。
## 关键源文件索引
| 文件 | 职责 |
|------|------|
| `src/services/mcp/client.ts` | 核心客户端connectToServer、fetchToolsForClient、MCPTool.call |
| `src/services/mcp/config.ts` | 配置管理getAllMcpConfigs、addMcpConfig、removeMcpConfig |
| `src/services/mcp/types.ts` | 类型定义:配置 Schema、连接状态类型 |
| `src/services/mcp/InProcessTransport.ts` | 内置 MCP 传输层linked transport pair |
| `src/services/mcp/vscodeSdkMcp.ts` | VSCode SDK MCP双向通知、实验门控 |
| `src/services/mcp/useManageMCPConnections.ts` | React Hook连接生命周期、重连 |
| `src/utils/computerUse/mcpServer.ts` | Computer Use MCP Server 构建 |
| `src/utils/computerUse/setup.ts` | Computer Use 动态注册 |
| `src/utils/claudeInChrome/mcpServer.ts` | Chrome MCP Server 构建 + Bridge 配置 |
| `src/tools/MCPTool/MCPTool.ts` | MCP 工具包装:统一 Tool 接口 |
| `src/entrypoints/mcp.ts` | MCP server 入口Claude Code 作为 MCP server |

View File

@@ -48,7 +48,7 @@ Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Promp
1. `readdir` 扫描目录 → 仅保留 `isDirectory()` 或 `isSymbolicLink()` 的条目
2. 在每个子目录中查找 `SKILL.md`,未找到则跳过
3. `parseFrontmatter()` 解析 YAML 头部,提取 `whenToUse`、`allowedTools`、`context` 等字段
4. `parseSkillFrontmatterFields()`(第 185 行)统一解析 17 个 frontmatter 字段
4. `parseSkillFrontmatterFields()`(第 185 行)统一解析 16 个 frontmatter 字段
5. `createSkillCommand()`(第 270 行)构造 `Command` 对象
**去重机制**:使用 `realpath()` 解析符号链接获得规范路径(`getFileIdentity`,第 118 行),避免通过符号链接或重叠父目录导致的重复加载。
@@ -94,7 +94,7 @@ shell: ["bash"] # Shell 执行环境
---
```
解析后有 17 个字段被提取,其中 `allowedTools`、`model`、`effort` 在执行时动态修改 `toolPermissionContext`。
解析后有 16 个字段被提取,其中 `allowedTools`、`model`、`effort` 在执行时动态修改 `toolPermissionContext`。
## 两条执行路径Inline vs Fork
@@ -128,12 +128,12 @@ Fork 模式适用于需要强隔离的场景(如长时间运行的审查任务
## 权限模型Safe Properties 白名单
`checkPermissions()`(第 433 行)实现了一个层权限检查:
`checkPermissions()`(第 433 行)实现了一个层权限检查:
```
1. Deny 规则匹配(支持精确匹配和 prefix:* 通配符)
↓ 未命中
2. 官方市场 Skill 自动放行plugin + isOfficialMarketplaceName
2. 远程 canonical Skill 自动放行EXPERIMENTAL_SKILL_SEARCH + USER_TYPE === 'ant'
↓ 未命中
3. Allow 规则匹配
↓ 未命中
@@ -142,7 +142,7 @@ Fork 模式适用于需要强隔离的场景(如长时间运行的审查任务
5. Ask 用户确认(附带精确匹配和前缀匹配两条建议规则)
```
**Safe Properties**`SAFE_SKILL_PROPERTIES`,第 876 行)是一个包含 28 个属性名的白名单。任何不在白名单中的**有意义的属性值**(排除 `undefined`、`null`、空数组、空对象)都会触发权限请求。这是**正向安全**设计——未来新增的属性默认需要权限。
**Safe Properties**`SAFE_SKILL_PROPERTIES`,第 876 行)是一个包含 30 个属性名的白名单(覆盖 `PromptCommand` 和 `CommandBase` 两个类型的所有安全属性)。任何不在白名单中的**有意义的属性值**(排除 `undefined`、`null`、空数组、空对象)都会触发权限请求。这是**正向安全**设计——未来新增的属性默认需要权限。
## Prompt 预算1% 上下文窗口的截断策略
@@ -205,7 +205,7 @@ score = usageCount × max(0.5^(daysSinceUse / 7), 0.1)
```
磁盘 SKILL.md
↓ parseFrontmatter()
↓ parseSkillFrontmatterFields() → 17 个字段
↓ parseSkillFrontmatterFields() → 16 个字段
↓ createSkillCommand() → Command 对象
↓ 去重realpath + seenFileIds
↓ 条件 Skill → conditionalSkills Map等待路径匹配激活
@@ -214,7 +214,7 @@ score = usageCount × max(0.5^(daysSinceUse / 7), 0.1)
↓ formatCommandsWithinBudget() → 截断后的 Skill 列表注入 System Prompt
↓ AI 选择匹配的 Skill
↓ SkillTool.validateInput() → 名称校验 + 存在性检查
↓ SkillTool.checkPermissions() → 层权限检查
↓ SkillTool.checkPermissions() → 层权限检查
↓ SkillTool.call() → inline 或 fork 执行
↓ contextModifier() → 注入 allowedTools + model + effort
↓ recordSkillUsage() → 更新使用频率排名

View File

@@ -19,7 +19,7 @@
| 11 | BigQuery Metrics | `api.anthropic.com/api/claude_code/metrics` | HTTPS | 默认启用 |
| 12 | MCP Proxy | `mcp-proxy.anthropic.com` | HTTPS+WS | 使用 MCP 工具时 |
| 13 | MCP Registry | `api.anthropic.com/mcp-registry` | HTTPS | 查询 MCP 服务器时 |
| 14 | Bing Search | `www.bing.com` | HTTPS | WebSearch 工具 |
| 14 | Web Search Pages | `www.bing.com`, `search.brave.com` | HTTPS | WebSearch 工具,可通过 `WEB_SEARCH_ADAPTER=bing|brave` 切换 |
| 15 | Google Cloud Storage (更新) | `storage.googleapis.com` | HTTPS | 版本检查 |
| 16 | GitHub Raw (Changelog/Stats) | `raw.githubusercontent.com` | HTTPS | 更新提示 |
| 17 | Claude in Chrome Bridge | `bridge.claudeusercontent.com` | WSS | Chrome 集成 |
@@ -121,12 +121,16 @@ Anthropic 托管的 MCP 服务器代理。
- **端点**: `https://api.anthropic.com/mcp-registry/v0/servers?version=latest&visibility=commercial`
- **文件**: `src/services/mcp/officialRegistry.ts`
### 14. Bing Search
### 14. Web Search Pages
WebSearch 工具的默认适配器,抓取 Bing 搜索结果
WebSearch 工具支持直接抓取 Bing 搜索结果页面,也支持通过 Brave 的 LLM Context API
获取搜索上下文;可通过 `WEB_SEARCH_ADAPTER=bing|brave` 显式切换后端。
- **端点**: `https://www.bing.com/search?q={query}&setmkt=en-US`
- **文件**: `src/tools/WebSearchTool/adapters/bingAdapter.ts`
- **Bing 端点**: `https://www.bing.com/search?q={query}&setmkt=en-US`
- **Brave 端点**: `https://api.search.brave.com/res/v1/llm/context?q={query}`
- **文件**:
- `src/tools/WebSearchTool/adapters/bingAdapter.ts`
- `src/tools/WebSearchTool/adapters/braveAdapter.ts`
另外还有 Domain Blocklist 查询:
- **端点**: `https://api.anthropic.com/api/web/domain_info?domain={domain}`
@@ -201,6 +205,7 @@ WebSearch 工具的默认适配器,抓取 Bing 搜索结果。
| `{region}-aiplatform.googleapis.com` | Google Vertex AI | HTTPS |
| `{resource}.services.ai.azure.com` | Azure Foundry | HTTPS |
| `www.bing.com` | Bing 搜索 | HTTPS |
| `search.brave.com` | Brave 搜索 | HTTPS |
| `storage.googleapis.com` | 自动更新 | HTTPS |
| `raw.githubusercontent.com` | Changelog / 插件统计 | HTTPS |
| `bridge.claudeusercontent.com` | Chrome Bridge | WSS |

View File

@@ -1,457 +0,0 @@
# Feature 探索计划书
> 生成日期2026-04-02
> 代码库中已识别 89 个 feature flag本文档按实现完整度和探索价值分级制定探索优先级和路线图。
>
> **已完成**BUDDY✅ 2026-04-02、TRANSCRIPT_CLASSIFIER / Auto Mode✅ 2026-04-02
---
## 一、总览
### 按实现状态分类
| 状态 | 数量 | 说明 |
|------|------|------|
| 已实现/可用 | 11 | 代码完整,开启 feature 后可运行(可能需要 OAuth 等外部依赖) |
| 部分实现 | 8 | 核心逻辑存在但关键模块为 stub需要补全 |
| 纯 Stub | 15 | 所有函数/工具返回空值,需要从零实现 |
| N/A | 55+ | 内部基础设施、低引用量辅助功能,或反编译丢失过多 |
### 启用方式
所有 feature 通过环境变量启用:
```bash
# 单个 feature
FEATURE_BUDDY=1 bun run dev
# 多个 feature 组合
FEATURE_KAIROS=1 FEATURE_PROACTIVE=1 FEATURE_FORK_SUBAGENT=1 bun run dev
```
---
## 二、Tier 1 — 已实现/可用(优先探索)
### 2.1 KAIROS常驻助手模式⭐ 最高优先级
- **引用数**154全库最大
- **功能**:将 CLI 变为常驻后台助手,支持:
- 持久化 bridge 会话(跨重启复用 session
- 后台执行任务(用户离开终端时继续工作)
- 推送通知到移动端(任务完成/需要输入时)
- 每日记忆日志 + `/dream` 知识蒸馏
- 外部频道消息接入Slack/Discord/Telegram
- **子 Feature**
| 子 Feature | 引用 | 功能 |
|-----------|------|------|
| `KAIROS_BRIEF` | 39 | Brief 工具(`SendUserMessage`),结构化消息输出 |
| `KAIROS_CHANNELS` | 19 | 外部频道消息接入 |
| `KAIROS_PUSH_NOTIFICATION` | 4 | 移动端推送通知 |
| `KAIROS_GITHUB_WEBHOOKS` | 3 | GitHub PR webhook 订阅 |
| `KAIROS_DREAM` | 1 | 夜间记忆蒸馏 |
- **关键文件**`src/assistant/``src/tools/BriefTool/``src/services/mcp/channelNotification.ts``src/memdir/memdir.ts`
- **外部依赖**Anthropic OAuthclaude.ai 订阅、GrowthBook 特性门控
- **探索命令**`FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 FEATURE_PROACTIVE=1 bun run dev`
**探索步骤**
1. 开启 feature观察启动行为变化
2. 测试 `/assistant``/brief` 命令
3. 验证 BriefTool 输出模式
4. 尝试频道消息接入
5. 测试 `/dream` 记忆蒸馏
---
### ~~2.2 TRANSCRIPT_CLASSIFIERAuto Mode 分类器)~~ ✅ 已完成
- **引用数**108
- **功能**:使用 LLM 对用户意图进行分类,实现 auto mode自动决定工具权限
- **状态**:✅ prompt 模板已重建功能完整可用2026-04-02 完成)
---
### 2.3 VOICE_MODE语音输入
- **引用数**46
- **功能**按键说话Push-to-Talk音频流式传输到 Anthropic STT 端点Nova 3实时转录显示
- **当前状态****完整实现**包括录音、WebSocket 流、转录插入
- **关键文件**`src/voice/voiceModeEnabled.ts``src/hooks/useVoice.ts``src/services/voiceStreamSTT.ts`
- **外部依赖**Anthropic OAuth非 API key、macOS 原生音频或 SoX
- **探索命令**`FEATURE_VOICE_MODE=1 bun run dev`
- **默认快捷键**:长按空格键录音
**探索步骤**
1. 确认 OAuth token 可用
2. 测试按住空格录音 → 释放后转录
3. 验证实时中间转录显示
4. 测试 `/voice` 命令切换
---
### 2.4 TEAMMEM团队共享记忆
- **引用数**51
- **功能**:基于 GitHub 仓库的团队共享记忆系统,`memory/team/` 目录双向同步到 Anthropic 服务器
- **当前状态****完整实现**,包括增量同步、冲突解决、密钥扫描、路径穿越防护
- **关键文件**`src/services/teamMemorySync/`index、watcher、secretScanner`src/memdir/teamMemPaths.ts`
- **外部依赖**Anthropic OAuth + GitHub remote`getGithubRepo()`
- **探索命令**`FEATURE_TEAMMEM=1 bun run dev`
**探索步骤**
1. 确认项目有 GitHub remote
2. 开启后观察 `memory/team/` 目录创建
3. 测试团队记忆写入和同步
4. 验证密钥扫描防护
---
### 2.5 COORDINATOR_MODE多 Agent 编排)
- **引用数**32
- **功能**CLI 变为编排者,通过 AgentTool 派发任务给多个 worker 并行执行
- **当前状态**核心逻辑实现worker agent 模块为 stub
- **关键文件**`src/coordinator/coordinatorMode.ts`(系统 prompt 完整)、`src/coordinator/workerAgent.ts`stub
- **限制**:编排者只能使用 AgentTool/TaskStop/SendMessage不能直接操作文件
- **探索命令**`FEATURE_COORDINATOR_MODE=1 CLAUDE_CODE_COORDINATOR_MODE=1 bun run dev`
**探索步骤**
1. 补全 `workerAgent.ts` stub
2. 测试多 worker 并行任务派发
3. 验证 worker 结果汇总
---
### 2.6 BRIDGE_MODE远程控制
- **引用数**28
- **功能**:本地 CLI 注册为 bridge 环境,可从 claude.ai 或其他控制面远程驱动
- **当前状态**v1env-based和 v2env-less实现均存在
- **关键文件**`src/bridge/bridgeEnabled.ts``src/bridge/replBridge.ts`v1`src/bridge/remoteBridgeCore.ts`v2
- **外部依赖**claude.ai OAuth、GrowthBook 门控 `tengu_ccr_bridge`
- **探索命令**`FEATURE_BRIDGE_MODE=1 bun run dev`
---
### 2.7 FORK_SUBAGENT上下文继承子 Agent
- **引用数**4
- **功能**AgentTool 生成 fork 子 agent继承父级完整对话上下文优化 prompt cache
- **当前状态****完整实现**`forkSubagent.ts`),支持 worktree 隔离通知、递归防护
- **关键文件**`src/tools/AgentTool/forkSubagent.ts`
- **探索命令**`FEATURE_FORK_SUBAGENT=1 bun run dev`
---
### 2.8 TOKEN_BUDGETToken 预算控制)
- **引用数**9
- **功能**:解析用户指定的 token 预算(如 "spend 2M tokens"),自动持续工作直到达到目标
- **当前状态**:解析器**完整实现**支持简写和详细语法QueryEngine 中的周转逻辑已连接
- **关键文件**`src/utils/tokenBudget.ts``src/QueryEngine.ts`
- **探索命令**`FEATURE_TOKEN_BUDGET=1 bun run dev`
---
### 2.9 MCP_SKILLSMCP 技能发现)
- **引用数**9
- **功能**:将 MCP 服务器提供的 prompt 类型命令筛选为可调用技能
- **当前状态****功能性实现**config 门控筛选器)
- **关键文件**`src/commands.ts``getMcpSkillCommands()`
- **探索命令**`FEATURE_MCP_SKILLS=1 bun run dev`
---
### 2.10 TREE_SITTER_BASHBash AST 解析)
- **引用数**3
- **功能**:纯 TypeScript bash 命令 AST 解析器,用于 fail-closed 权限匹配
- **当前状态****完整实现**`bashParser.ts` ~2000行 + `ast.ts` ~400行
- **关键文件**`src/utils/vendor/tree-sitter-bash/`
- **探索命令**`FEATURE_TREE_SITTER_BASH=1 bun run dev`
---
### ~~2.11 BUDDY虚拟伙伴~~ ✅ 已完成
- **引用数**16
- **功能**`/buddy` 命令,支持 hatch/rehatch/pet/mute/unmute
- **状态**:✅ 已合入功能完整可用2026-04-02 完成)
---
## 三、Tier 2 — 部分实现(需要补全)
### 3.1 PROACTIVE主动模式
- **引用数**37
- **功能**Tick 驱动的自主代理,定时唤醒执行工作,配合 SleepTool 控制节奏
- **当前状态**:核心模块 `src/proactive/index.ts` **全部 stub**activate/deactivate/pause 返回 false 或空操作)
- **依赖**:与 KAIROS 强绑定(所有检查都是 `feature('PROACTIVE') || feature('KAIROS')`
- **补全工作量**:中等 — 需要实现 tick 生成、SleepTool 集成、暂停/恢复逻辑
### 3.2 BASH_CLASSIFIERBash 命令分类器)
- **引用数**45
- **功能**LLM 驱动的 bash 命令意图分类(允许/拒绝/询问)
- **当前状态**`bashClassifier.ts` **全部 stub**`matches: false`
- **补全工作量**:大 — 需要 LLM 调用实现、prompt 设计
### 3.3 ULTRAPLAN增强规划
- **引用数**10
- **功能**:关键字触发增强计划模式,输入 "ultraplan" 自动转为 plan
- **当前状态**:关键字检测**完整实现**`/ultraplan` 命令**为 stub**
- **补全工作量**:小 — 只需实现命令处理逻辑
### 3.4 EXPERIMENTAL_SKILL_SEARCH技能语义搜索
- **引用数**21
- **功能**DiscoverSkills 工具,根据当前任务语义搜索可用技能
- **当前状态**:布线完整,核心搜索逻辑 stub
- **补全工作量**:中等 — 需要实现搜索引擎和索引
### 3.5 CONTEXT_COLLAPSE上下文折叠
- **引用数**20
- **功能**CtxInspectTool 让模型内省上下文窗口大小,优化压缩决策
- **当前状态**:工具 stubHISTORY_SNIP 子功能也 stub
- **补全工作量**:中等
### 3.6 WORKFLOW_SCRIPTS工作流自动化
- **引用数**10
- **功能**:基于文件的自动化工作流 + `/workflows` 命令
- **当前状态**WorkflowTool、命令、加载器全部 stub
- **补全工作量**:大 — 需要从零设计工作流 DSL
### 3.7 WEB_BROWSER_TOOL浏览器工具
- **引用数**4
- **功能**:模型可调用浏览器工具导航和交互网页
- **当前状态**:工具注册存在,实现 stub
- **补全工作量**:大
### 3.8 DAEMON后台守护进程
- **引用数**3
- **功能**:后台守护进程 + 远程控制服务器
- **当前状态**:只有条件导入布线,无实现
- **补全工作量**:极大
---
## 四、Tier 3 — 纯 Stub / N/A低优先级
| Feature | 引用 | 状态 | 说明 |
|---------|------|------|------|
| CHICAGO_MCP | 16 | N/A | Anthropic 内部 MCP 基础设施 |
| UDS_INBOX | 17 | Stub | Unix 域套接字对等消息 |
| MONITOR_TOOL | 13 | Stub | 文件/进程监控工具 |
| BG_SESSIONS | 11 | Stub | 后台会话管理 |
| SHOT_STATS | 10 | 无实现 | 逐 prompt 统计 |
| EXTRACT_MEMORIES | 7 | 无实现 | 自动记忆提取 |
| TEMPLATES | 6 | Stub | 项目/提示模板 |
| LODESTONE | 6 | N/A | 内部基础设施 |
| STREAMLINED_OUTPUT | 1 | — | 精简输出模式 |
| HOOK_PROMPTS | 1 | — | Hook 提示词 |
| CCR_AUTO_CONNECT | 3 | — | CCR 自动连接 |
| CCR_MIRROR | 4 | — | CCR 镜像模式 |
| CCR_REMOTE_SETUP | 1 | — | CCR 远程设置 |
| NATIVE_CLIPBOARD_IMAGE | 2 | — | 原生剪贴板图片 |
| CONNECTOR_TEXT | 7 | — | 连接器文本 |
以及其余 40+ 个低引用量 feature。
---
## 五、探索路线图
### Phase 1快速验证无外部依赖
> 目标:确认代码可以正常运行,体验基本功能
| 优先级 | Feature | 命令 | 预期效果 |
|--------|---------|------|----------|
| 1 | BUDDY | `FEATURE_BUDDY=1 bun run dev` | `/buddy hatch` 生成伙伴 |
| 2 | FORK_SUBAGENT | `FEATURE_FORK_SUBAGENT=1 bun run dev` | Agent 可生成上下文继承的子任务 |
| 3 | TOKEN_BUDGET | `FEATURE_TOKEN_BUDGET=1 bun run dev` | 输入 "spend 500k tokens" 测试自动持续 |
| 4 | TREE_SITTER_BASH | `FEATURE_TREE_SITTER_BASH=1 bun run dev` | 更精确的 bash 权限匹配 |
| 5 | MCP_SKILLS | `FEATURE_MCP_SKILLS=1 bun run dev` | MCP 服务器 prompt 提升为技能 |
### Phase 2核心功能探索需要 OAuth
> 目标:体验 KAIROS 全套能力
| 优先级 | Feature | 命令 | 预期效果 |
|--------|---------|------|----------|
| 1 | TRANSCRIPT_CLASSIFIER | `FEATURE_TRANSCRIPT_CLASSIFIER=1 bun run dev` | Auto mode 自动激活 |
| 2 | KAIROS 全套 | `FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 FEATURE_KAIROS_CHANNELS=1 FEATURE_PROACTIVE=1 bun run dev` | 常驻助手 + Brief 输出 + 频道消息 |
| 3 | VOICE_MODE | `FEATURE_VOICE_MODE=1 bun run dev` | 按空格说话 |
| 4 | TEAMMEM | `FEATURE_TEAMMEM=1 bun run dev` | 团队记忆同步 |
| 5 | COORDINATOR_MODE | `FEATURE_COORDINATOR_MODE=1 CLAUDE_CODE_COORDINATOR_MODE=1 bun run dev` | 多 agent 编排 |
### Phase 3Stub 补全开发
> 目标:将高价值 stub 实现为可用功能
| 优先级 | Feature | 补全难度 | 价值 |
|--------|---------|----------|------|
| 1 | PROACTIVE | 中 | 自主工作能力 |
| 2 | ULTRAPLAN | 小 | 增强规划 |
| 3 | CONTEXT_COLLAPSE | 中 | 长对话优化 |
| 4 | EXPERIMENTAL_SKILL_SEARCH | 中 | 技能发现 |
| 5 | BASH_CLASSIFIER | 大 | 安全增强 |
---
## 六、推荐组合方案
### "全功能助手"组合
```bash
FEATURE_KAIROS=1 \
FEATURE_KAIROS_BRIEF=1 \
FEATURE_KAIROS_CHANNELS=1 \
FEATURE_KAIROS_PUSH_NOTIFICATION=1 \
FEATURE_PROACTIVE=1 \
FEATURE_FORK_SUBAGENT=1 \
FEATURE_TOKEN_BUDGET=1 \
FEATURE_TRANSCRIPT_CLASSIFIER=1 \
FEATURE_BUDDY=1 \
bun run dev
```
### "多 Agent 协作"组合
```bash
FEATURE_COORDINATOR_MODE=1 \
FEATURE_FORK_SUBAGENT=1 \
FEATURE_BRIDGE_MODE=1 \
FEATURE_BG_SESSIONS=1 \
CLAUDE_CODE_COORDINATOR_MODE=1 \
bun run dev
```
### "开发者增强"组合
```bash
FEATURE_TRANSCRIPT_CLASSIFIER=1 \
FEATURE_TREE_SITTER_BASH=1 \
FEATURE_TOKEN_BUDGET=1 \
FEATURE_MCP_SKILLS=1 \
FEATURE_CONTEXT_COLLAPSE=1 \
bun run dev
```
---
## 七、风险与注意事项
1. **OAuth 依赖**KAIROS、VOICE_MODE、TEAMMEM、BRIDGE_MODE 需要 Anthropic OAuth 认证claude.ai 订阅API key 用户无法使用
2. **GrowthBook 门控**部分功能VOICE_MODE 的 `tengu_cobalt_frost`、TEAMMEM 的 `tengu_herring_clock`)即使 feature flag 开启,还需要服务端 GrowthBook 开关
3. **反编译不完整**:所有"已实现"功能均为反编译产物,可能存在运行时错误,需要逐个验证
4. **Proactive stub**KAIROS 的自主工作能力依赖 PROACTIVE但 PROACTIVE 核心是 stub需先补全
5. **tsc 错误**:代码库有 ~1341 个 TypeScript 编译错误(来自反编译),不影响 Bun 运行时但在 IDE 中会有大量红线
---
## 附录Feature Flag 完整列表
共 89 个 feature flag按引用数降序
| Feature | 引用 | Tier |
|---------|------|------|
| KAIROS | 154 | 1 |
| TRANSCRIPT_CLASSIFIER | 108 | 1 |
| TEAMMEM | 51 | 1 |
| VOICE_MODE | 46 | 1 |
| BASH_CLASSIFIER | 45 | 2 |
| KAIROS_BRIEF | 39 | 1 |
| PROACTIVE | 37 | 2 |
| COORDINATOR_MODE | 32 | 1 |
| BRIDGE_MODE | 28 | 1 |
| EXPERIMENTAL_SKILL_SEARCH | 21 | 2 |
| CONTEXT_COLLAPSE | 20 | 2 |
| KAIROS_CHANNELS | 19 | 1 |
| UDS_INBOX | 17 | 3 |
| CHICAGO_MCP | 16 | 3 |
| BUDDY | 16 | 1 |
| HISTORY_SNIP | 15 | 2 |
| MONITOR_TOOL | 13 | 3 |
| COMMIT_ATTRIBUTION | 12 | — |
| CACHED_MICROCOMPACT | 12 | — |
| BG_SESSIONS | 11 | 3 |
| WORKFLOW_SCRIPTS | 10 | 2 |
| ULTRAPLAN | 10 | 2 |
| SHOT_STATS | 10 | 3 |
| TOKEN_BUDGET | 9 | 1 |
| PROMPT_CACHE_BREAK_DETECTION | 9 | — |
| MCP_SKILLS | 9 | 1 |
| EXTRACT_MEMORIES | 7 | 3 |
| CONNECTOR_TEXT | 7 | — |
| TEMPLATES | 6 | 3 |
| LODESTONE | 6 | 3 |
| TREE_SITTER_BASH_SHADOW | 5 | — |
| QUICK_SEARCH | 5 | — |
| MESSAGE_ACTIONS | 5 | — |
| DOWNLOAD_USER_SETTINGS | 5 | — |
| DIRECT_CONNECT | 5 | — |
| WEB_BROWSER_TOOL | 4 | 2 |
| VERIFICATION_AGENT | 4 | — |
| TERMINAL_PANEL | 4 | — |
| SSH_REMOTE | 4 | — |
| REVIEW_ARTIFACT | 4 | — |
| REACTIVE_COMPACT | 4 | — |
| KAIROS_PUSH_NOTIFICATION | 4 | 1 |
| HISTORY_PICKER | 4 | — |
| FORK_SUBAGENT | 4 | 1 |
| CCR_MIRROR | 4 | — |
| TREE_SITTER_BASH | 3 | 1 |
| MEMORY_SHAPE_TELEMETRY | 3 | — |
| MCP_RICH_OUTPUT | 3 | — |
| KAIROS_GITHUB_WEBHOOKS | 3 | 1 |
| FILE_PERSISTENCE | 3 | — |
| DAEMON | 3 | 2 |
| CCR_AUTO_CONNECT | 3 | — |
| UPLOAD_USER_SETTINGS | 2 | — |
| POWERSHELL_AUTO_MODE | 2 | — |
| OVERFLOW_TEST_TOOL | 2 | — |
| NEW_INIT | 2 | — |
| NATIVE_CLIPBOARD_IMAGE | 2 | — |
| HARD_FAIL | 2 | — |
| ENHANCED_TELEMETRY_BETA | 2 | — |
| COWORKER_TYPE_TELEMETRY | 2 | — |
| BREAK_CACHE_COMMAND | 2 | — |
| AWAY_SUMMARY | 2 | — |
| AUTO_THEME | 2 | — |
| ALLOW_TEST_VERSIONS | 2 | — |
| AGENT_TRIGGERS_REMOTE | 2 | — |
| AGENT_MEMORY_SNAPSHOT | 2 | — |
| UNATTENDED_RETRY | 1 | — |
| ULTRATHINK | 1 | — |
| TORCH | 1 | — |
| STREAMLINED_OUTPUT | 1 | — |
| SLOW_OPERATION_LOGGING | 1 | — |
| SKILL_IMPROVEMENT | 1 | — |
| SELF_HOSTED_RUNNER | 1 | — |
| RUN_SKILL_GENERATOR | 1 | — |
| PERFETTO_TRACING | 1 | — |
| NATIVE_CLIENT_ATTESTATION | 1 | — |
| KAIROS_DREAM | 1 | 1 |
| IS_LIBC_MUSL | 1 | — |
| IS_LIBC_GLIBC | 1 | — |
| HOOK_PROMPTS | 1 | — |
| DUMP_SYSTEM_PROMPT | 1 | — |
| COMPACTION_REMINDERS | 1 | — |
| CCR_REMOTE_SETUP | 1 | — |
| BYOC_ENVIRONMENT_RUNNER | 1 | — |
| BUILTIN_EXPLORE_PLAN_AGENTS | 1 | — |
| BUILDING_CLAUDE_APPS | 1 | — |
| ANTI_DISTILLATION_CC | 1 | — |
| AGENT_TRIGGERS | 1 | — |
| ABLATION_BASELINE | 1 | — |

File diff suppressed because it is too large Load Diff

189
docs/features/acp-zed.md Normal file
View File

@@ -0,0 +1,189 @@
# ACP (Agent Client Protocol) — Zed / IDE 集成
> Feature Flag: `FEATURE_ACP=1`build 和 dev 模式默认启用)
> 实现状态:可用(支持 Zed、Cursor 等 ACP 客户端)
> 源码目录:`src/services/acp/`
## 一、功能概述
ACP (Agent Client Protocol) 是一种标准化的 stdio 协议,允许 IDE 和编辑器通过 stdin/stdout 的 NDJSON 流驱动 AI Agent。CCB 实现了完整的 ACP agent 端,可以被 Zed、Cursor 等支持 ACP 的客户端直接调用。
### 核心特性
- **会话管理**:新建 / 恢复 / 加载 / 分叉 / 关闭会话
- **历史回放**:恢复会话时自动加载并回放对话历史
- **权限桥接**ACP 客户端的权限决策映射到 CCB 的工具权限系统
- **斜杠命令 & Skills**:加载真实命令列表,支持 `/commit``/review` 等 prompt 型 skill
- **Context Window 跟踪**:精确的 usage_update含 model prefix matching
- **Prompt 排队**:支持连续发送多条 prompt自动排队处理
- **模式切换**auto / default / acceptEdits / plan / dontAsk / bypassPermissions
- **模型切换**:运行时切换 AI 模型
## 二、架构
```
┌──────────────┐ NDJSON/stdio ┌──────────────────┐
│ Zed / IDE │ ◄────────────────► │ CCB ACP Agent │
│ (Client) │ stdin / stdout │ (Agent) │
└──────────────┘ │ │
│ entry.ts │ ← stdio → NDJSON stream
│ agent.ts │ ← ACP protocol handler
│ bridge.ts │ ← SDKMessage → ACP SessionUpdate
│ permissions.ts │ ← 权限桥接
│ utils.ts │ ← 通用工具
│ │
│ QueryEngine │ ← 内部查询引擎
└──────────────────┘
```
### 文件职责
| 文件 | 职责 |
|------|------|
| `entry.ts` | 入口,创建 stdio → NDJSON stream启动 `AgentSideConnection` |
| `agent.ts` | 实现 ACP `Agent` 接口:会话 CRUD、prompt、cancel、模式/模型切换 |
| `bridge.ts` | `SDKMessage` → ACP `SessionUpdate` 转换:文本/思考/工具/用量/编辑 diff |
| `permissions.ts` | ACP `requestPermission()` → CCB `CanUseToolFn` 桥接 |
| `utils.ts` | Pushable、流转换、权限模式解析、session fingerprint、路径显示 |
## 三、配置 Zed 编辑器
### 3.1 Zed settings.json 配置
打开 Zed 的 `settings.json``Cmd+,` → Open Settings添加 `agent_servers` 配置:
```json
{
"agent_servers": {
"ccb": {
"type": "custom",
"command": "ccb",
"args": ["--acp"]
}
}
}
```
### 3.3 API 认证配置
CCB 的 ACP agent 在启动时会自动加载 `settings.json` 中的环境变量(`ANTHROPIC_BASE_URL``ANTHROPIC_AUTH_TOKEN` 等)。确保已通过 `/login` 配置好 API 供应商。
也可通过环境变量传入:
```json
{
"agent_servers": {
"claude-code": {
"command": "ccb",
"args": ["--acp"],
"env": {
"ANTHROPIC_BASE_URL": "https://api.example.com/v1",
"ANTHROPIC_AUTH_TOKEN": "sk-xxx"
}
}
}
}
```
### 3.4 在 Zed 中使用
1. 配置完成后重启 Zed
2. 打开任意项目目录
3.`Cmd+'`macOS`Ctrl+'`Linux打开 Agent Panel
4. 在 Agent Panel 顶部的下拉菜单中选择 **claude-code**
5. 开始对话
### 3.5 功能说明
| 功能 | 操作 |
|------|------|
| 对话 | 在 Agent Panel 中直接输入消息 |
| 斜杠命令 | 输入 `/` 查看可用 skills 列表(如 `/commit``/review` |
| 工具权限 | 弹出权限请求时选择 Allow / Reject / Always Allow |
| 模式切换 | 通过 Agent Panel 的设置菜单切换 auto/default/plan 等模式 |
| 模型切换 | 通过 Agent Panel 的设置菜单切换 AI 模型 |
| 会话恢复 | 关闭重开 Zed 后,之前的会话可自动恢复(含历史消息) |
## 四、配置其他 ACP 客户端
ACP 是开放协议,任何支持 ACP 的客户端都可以连接 CCB。通用配置模式
```
命令: ccb --acp
参数: ["--acp"]
通信: stdin/stdout NDJSON
协议版本: ACP v1
```
### 4.1 Cursor
在 Cursor 的设置中配置 MCP / Agent Server使用同样的 `ccb --acp` 命令。
### 4.2 自定义客户端
使用 `@agentclientprotocol/sdk` 可以快速构建 ACP 客户端:
```typescript
import { ClientSideConnection, ndJsonStream } from '@agentclientprotocol/sdk'
// 创建连接(将 ccb --acp 作为子进程启动)
const child = spawn('ccb', ['--acp'])
const stream = ndJsonStream(
Writable.toWeb(child.stdin),
Readable.toWeb(child.stdout),
)
const client = new ClientSideConnection(stream)
// 初始化
await client.initialize({ clientCapabilities: {} })
// 创建会话
const { sessionId } = await client.newSession({
cwd: '/path/to/project',
})
// 发送 prompt
const response = await client.prompt({
sessionId,
prompt: [{ type: 'text', text: 'Hello, explain this project' }],
})
// 监听 session 更新
client.on('sessionUpdate', (update) => {
console.log('Update:', update)
})
```
## 五、ACP 协议支持矩阵
| 方法 | 状态 | 说明 |
|------|------|------|
| `initialize` | ✅ | 返回 agent 信息和能力 |
| `authenticate` | ✅ | 无需认证(自托管) |
| `newSession` | ✅ | 创建新会话 |
| `resumeSession` | ✅ | 恢复已有会话(含历史回放) |
| `loadSession` | ✅ | 加载指定会话(含历史回放) |
| `listSessions` | ✅ | 列出可用会话 |
| `forkSession` | ✅ | 分叉会话 |
| `closeSession` | ✅ | 关闭会话 |
| `prompt` | ✅ | 发送消息,支持排队 |
| `cancel` | ✅ | 取消当前/排队的 prompt |
| `setSessionMode` | ✅ | 切换权限模式 |
| `setSessionModel` | ✅ | 切换 AI 模型 |
| `setSessionConfigOption` | ✅ | 动态修改配置 |
### SessionUpdate 类型
| 类型 | 状态 | 说明 |
|------|------|------|
| `agent_message_chunk` | ✅ | 助手文本消息 |
| `agent_thought_chunk` | ✅ | 思考/推理内容 |
| `user_message_chunk` | ✅ | 用户消息(历史回放) |
| `tool_call` | ✅ | 工具调用开始 |
| `tool_call_update` | ✅ | 工具调用结果/状态更新 |
| `usage_update` | ✅ | token 用量 + context window |
| `plan` | ✅ | TodoWrite → plan entries |
| `available_commands_update` | ✅ | 斜杠命令 & skills 列表 |
| `current_mode_update` | ✅ | 模式切换通知 |
| `config_option_update` | ✅ | 配置更新通知 |

View File

@@ -0,0 +1,562 @@
# Claude Code Best (CCB) — 全功能使用指南
本文档覆盖我们通过 13 个 PR 为 CCB 恢复/新增的**全部功能**,按类别组织,每个功能包含说明、使用方法和示例。
---
## 目录
1. [Buddy 伴侣系统](#1-buddy-伴侣系统)
2. [Remote Control 远程控制](#2-remote-control-远程控制)
3. [定时任务 /schedule](#3-定时任务-schedule)
4. [Voice Mode 语音模式](#4-voice-mode-语音模式)
5. [Chrome 浏览器控制](#5-chrome-浏览器控制)
6. [Computer Use 屏幕操控](#6-computer-use-屏幕操控)
7. [Feature Flags 与 GrowthBook](#7-feature-flags-与-growthbook)
8. [/ultraplan 高级规划](#8-ultraplan-高级规划)
9. [Daemon 后台守护](#9-daemon-后台守护)
10. [Pipe IPC 多实例协作](#10-pipe-ipc-多实例协作)
11. [LAN Pipes 局域网群控](#11-lan-pipes-局域网群控)
12. [Monitor 后台监控](#12-monitor-后台监控)
13. [Workflow 工作流脚本](#13-workflow-工作流脚本)
14. [Coordinator 多Worker协调](#14-coordinator-多worker协调)
15. [Proactive 自主模式](#15-proactive-自主模式)
16. [History / Snip 历史管理](#16-history--snip-历史管理)
17. [Fork 子Agent](#17-fork-子agent)
18. [其他恢复的工具](#18-其他恢复的工具)
---
## 1. Buddy 伴侣系统
**PR**: #82 `refactor(buddy): align companion system with official CLI`
**Feature Flag**: `BUDDY`
### 说明
Buddy 是一个后台运行的伴侣 AI在你主对话进行的同时异步观察会话内容并提供建议。
### 使用
```bash
# 启动时自动加载feature 默认开启)
bun run dev
# 在对话中Buddy 会在适当时机自动提供建议
# 例如当你在调试时Buddy 可能提示你检查日志
```
---
## 2. Remote Control 远程控制
**PR**: #60 `feat: enable Remote Control (BRIDGE_MODE)` + #170 `feat: restore daemon supervisor`
**Feature Flag**: `BRIDGE_MODE`
### 说明
通过 WebSocket 远程控制 Claude Code 会话。支持自托管私有部署。
### 使用
```bash
# 启动远程控制模式
bun run dev -- remote-control
# 使用自托管服务器
CLAUDE_BRIDGE_BASE_URL=https://your-server.com CLAUDE_BRIDGE_OAUTH_TOKEN=your-token bun run dev --remote-control
# 或通过 /remote-control 命令在会话中启动
/remote-control
```
### 命令
- `claude remote-control` / `claude rc` — 启动远程控制客户端
- `claude bridge` — 同上(别名)
---
## 3. 定时任务 /schedule
**PR**: #88 `feat: enable /schedule by adding AGENT_TRIGGERS_REMOTE`
**Feature Flag**: `AGENT_TRIGGERS_REMOTE`
### 说明
创建定时执行的远程 agent 任务,支持 cron 表达式。
### 使用
```
/schedule create "每天检查依赖更新" --cron "0 9 * * *" --prompt "检查 package.json 中的过期依赖并创建更新 PR"
/schedule list — 列出所有定时任务
/schedule delete <id> — 删除指定任务
```
---
## 4. Voice Mode 语音模式
**PR**: #92 `feat: enable /voice mode with native audio binaries`
**Feature Flag**: `VOICE_MODE`
### 说明
Push-to-Talk 语音输入,音频通过 WebSocket 流式传输到 Anthropic STTNova 3。需要 Anthropic OAuth 认证(非 API key
### 使用
```bash
# 确保已通过 OAuth 登录
claude auth login
# 在会话中按住指定键说话
# 松开后自动转写为文字输入
```
### 前提条件
- Anthropic OAuth 认证(不支持 API key 模式)
- 系统麦克风权限
---
## 5. Chrome 浏览器控制
**PR**: #93 `feat: enable Claude in Chrome MCP with full browser control`
**Feature Flag**: `CHICAGO_MCP`
### 说明
通过 Chrome 扩展控制浏览器:导航、点击、填表、截图、执行 JS。
### 使用
```bash
# 启动带 Chrome 控制的模式
bun run dev -- --chrome
# 安装 Chrome 扩展后AI 可以:
# - 打开网页、点击按钮
# - 填写表单
# - 截取页面内容
# - 执行 JavaScript
```
### AI 可用工具
- `navigate` — 导航到 URL
- `click` / `find` / `form_input` — 页面交互
- `get_page_text` / `read_page` — 读取内容
- `javascript_tool` — 执行 JS
- `gif_creator` — 录制操作 GIF
---
## 6. Computer Use 屏幕操控
**PR**: #98 + #137 `feat: Computer Use — 跨平台 Executor + Python Bridge + GUI 无障碍`
**Feature Flag**: `CHICAGO_MCP`
### 说明
跨平台屏幕操控:截图、键鼠模拟、应用管理。支持 macOS + WindowsLinux 后端待完成。
### 使用
```bash
# 启动后 AI 可自动调用屏幕操控工具
bun run dev
# AI 可以:
# - 截取屏幕/窗口截图
# - 模拟键盘输入和鼠标操作
# - 列出运行的应用
# - 使用剪贴板
```
### 平台支持
| 平台 | 截图 | 键鼠 | 应用管理 |
|------|------|------|----------|
| macOS | ✅ | ✅ | ✅ |
| Windows | ✅ | ✅ | ✅ |
| Linux | ⏳ | ⏳ | ⏳ |
---
## 7. Feature Flags 与 GrowthBook
**PR**: #140 + #153 `feat: enable GrowthBook local gate defaults`
**Feature Flags**: `SHOT_STATS`, `PROMPT_CACHE_BREAK_DETECTION`, `TOKEN_BUDGET`
### 说明
本地 GrowthBook gate defaults 机制,绕过远程 feature flag 服务,确保功能在无网络时也可使用。
### 使用
```bash
# 通过环境变量启用任意 feature
FEATURE_PROACTIVE=1 bun run dev
# dev/build 模式有各自的默认启用列表
# 查看 scripts/dev.ts 中的 DEFAULT_FEATURES
```
### 关键 feature flags
| Flag | 说明 |
|------|------|
| `SHOT_STATS` | API 调用统计 |
| `TOKEN_BUDGET` | Token 预算控制 |
| `PROMPT_CACHE_BREAK_DETECTION` | Prompt 缓存命中检测 |
---
## 8. /ultraplan 高级规划
**PR**: #156 `feat: enable /ultraplan and harden GrowthBook fallback chain`
**Feature Flag**: `ULTRAPLAN`
### 说明
高级多 agent 规划模式。将复杂任务分解为多个阶段,每阶段可分配给不同 agent 并行执行。
### 使用
```
/ultraplan 实现一个完整的用户认证系统包括注册、登录、密码重置、OAuth 集成
```
AI 会生成:
1. 任务分解(多阶段)
2. 每阶段的 agent 分配
3. 依赖关系图
4. 并行执行计划
---
## 9. Daemon 后台守护
**PR**: #170 `feat: restore daemon supervisor and remoteControlServer command`
**Feature Flag**: `DAEMON`
### 说明
Daemon 模式允许 Claude Code 作为后台长驻进程运行,管理多个 worker。
### 使用
```bash
# 启动 daemon
claude daemon start
# 查看状态
claude daemon status
# 停止
claude daemon stop
# 启动远程控制服务器
bun run rcs
```
---
## 10. Pipe IPC 多实例协作
**PR**: #241 `feat: restore pipe IPC, LAN pipes, monitor tool`
**Feature Flag**: `UDS_INBOX`
### 说明
同一台机器上的多个 Claude Code 实例通过 UDSUnix Domain Socket / Windows Named Pipe自动发现并协作。首个启动的实例成为 main后续自动注册为 sub。
### 使用
**启动多实例**
```bash
# 终端 1
bun run dev
# → 自动成为 main
# 终端 2
bun run dev
# → 自动成为 sub-1被 main attach
```
**管理实例**
```
/pipes — 显示所有实例Shift+↓ 展开选择面板
/pipes select <name> — 选中实例
/pipes all — 全选
/pipes none — 取消全选
/attach <name> — 手动 attach 某实例
/detach <name> — 断开连接
/send <name> <msg> — 向指定实例发送消息
/claim-main — 强制声明为 main
/pipe-status — 显示详细状态
/peers — 列出所有已发现的 peer
```
**选择面板操作**
1.`Shift+↓` 展开面板
2. `↑/↓` 移动光标
3. `Space` 选中/取消 pipe
4. `Enter` 确认关闭
5. `←/→` 切换路由模式selected pipes ↔ local main
**消息广播**
选中 pipe 后,输入的消息自动路由到所有选中的 slave 执行,结果流式回传到 main。
**权限转发**
slave 执行需要权限的工具时(如 BashTool权限请求自动转发到 main 的确认队列。
---
## 11. LAN Pipes 局域网群控
**PR**: #241(同上)
**Feature Flag**: `LAN_PIPES`
### 说明
在 Pipe IPC 基础上增加 TCP 传输层和 UDP Multicast 发现,实现跨机器零配置协作。
### 使用
**局域网多机器**
```bash
# 机器 A (192.168.50.22)
bun run dev
# 机器 B (192.168.50.27)
bun run dev
# 两边启动后 3-5 秒自动发现和 attach
# /pipes 显示 [LAN] 标记的远端实例
```
**防火墙配置**(每台机器都需要):
Windows管理员 PowerShell
```powershell
New-NetFirewallRule -DisplayName "CCB LAN Beacon (UDP)" -Direction Inbound -Protocol UDP -LocalPort 7101 -Action Allow -Profile Private
New-NetFirewallRule -DisplayName "CCB LAN Pipes (TCP)" -Direction Inbound -Protocol TCP -LocalPort 1024-65535 -Program (Get-Command bun).Source -Action Allow -Profile Private
New-NetFirewallRule -DisplayName "CCB LAN Beacon Out (UDP)" -Direction Outbound -Protocol UDP -RemotePort 7101 -Action Allow -Profile Private
```
macOS
```bash
# 首次运行时系统弹对话框,点"允许"即可
```
Linux
```bash
sudo firewall-cmd --zone=trusted --add-port=7101/udp --permanent
sudo firewall-cmd --zone=trusted --add-port=1024-65535/tcp --permanent
sudo firewall-cmd --reload
```
**通知显示格式**
```
# 本机 sub
Routed to [sub-1]; main can continue other tasks
# LAN peer
Routed to [main] vmwin11/192.168.50.27; main can continue other tasks
```
---
## 12. Monitor 后台监控
**PR**: #241(同上)
**Feature Flag**: `MONITOR_TOOL`
### 说明
在后台运行 shell 命令持续监控输出(类似 `watch` 命令。AI 也可自主调用 MonitorTool。
### 使用
**用户命令**
```
/monitor tail -f /var/log/syslog
/monitor watch -n 5 docker ps
/monitor "while true; do curl -s localhost:3000/health; sleep 10; done"
```
**查看监控**
-`Shift+Down` 展开后台任务面板
- 查看监控输出和状态
**Windows 兼容**
`watch -n <sec> <cmd>` 自动转为 PowerShell 循环:
```powershell
while($true){ <cmd>; Start-Sleep -Seconds <sec> }
```
**AI 调用**
AI 可在对话中自动调用 `MonitorTool` 监控日志、构建输出等。
---
## 13. Workflow 工作流脚本
**PR**: #241(同上)
**Feature Flag**: `WORKFLOW_SCRIPTS`
### 说明
执行 `.claude/workflows/` 目录下的用户定义工作流脚本。
### 使用
**创建工作流**
```bash
mkdir -p .claude/workflows
cat > .claude/workflows/deploy.sh << 'EOF'
#!/bin/bash
echo "Running tests..."
bun test
echo "Building..."
bun run build
echo "Deploying..."
EOF
chmod +x .claude/workflows/deploy.sh
```
**列出可用工作流**
```
/workflows
```
**AI 调用**
AI 可通过 `WorkflowTool` 自动执行工作流:
```
请执行 deploy 工作流
```
---
## 14. Coordinator 多Worker协调
**PR**: #241(同上)
**Feature Flag**: `COORDINATOR_MODE`
### 说明
启用 coordinator 模式后AI 可自动将任务分配给多个 worker 并行执行。
### 使用
```
/coordinator — 切换 coordinator 模式开/关
```
启用后AI 在处理复杂任务时会:
1. 分析任务可并行的部分
2. 自动创建 worker 分支
3. 分配子任务
4. 汇总结果
---
## 15. Proactive 自主模式
**PR**: #241(同上)
**Feature Flag**: `PROACTIVE` / `KAIROS`
### 说明
启用后 AI 会主动发起操作(而不仅回应用户输入),例如自动检测文件变更、主动提出优化建议。
### 使用
```
/proactive — 切换 proactive 模式开/关
```
---
## 16. History / Snip 历史管理
**PR**: #241(同上)
**Feature Flag**: `HISTORY_SNIP`
### 说明
查看和管理对话历史,支持手动截断以释放上下文窗口空间。
### 使用
```
/history — 显示对话历史摘要
/force-snip — 强制在当前位置截断历史
```
AI 也可通过 `SnipTool` 自动截断过长的对话:
```
对话太长了,请帮我截断历史
```
---
## 17. Fork 子Agent
**PR**: #241(同上)
**Feature Flag**: `FORK_SUBAGENT`
### 说明
在当前对话上下文中 fork 一个独立的子 agent继承完整会话状态独立执行。
### 使用
```
/fork — 基于当前上下文 fork 子 agent
```
子 agent 会:
- 继承当前的全部对话历史
- 在独立的执行环境中运行
- 不影响主会话状态
---
## 18. 其他恢复的工具
以下工具从 stub 恢复为完整实现:
| 工具 | 说明 | 使用 |
|------|------|------|
| `SleepTool` | 暂停执行指定时间 | AI 在轮询场景自动调用 |
| `WebBrowserTool` | 终端内网页交互 | AI 需要查看网页时调用 |
| `SubscribePRTool` | 订阅 GitHub PR 变更 | `/subscribe-pr` 或 AI 调用 |
| `PushNotificationTool` | 推送桌面通知 | AI 在长任务完成时调用 |
| `CtxInspectTool` | 检查上下文窗口使用 | AI 判断上下文剩余空间 |
| `TerminalCaptureTool` | 截取终端屏幕 | AI 需要看终端输出时调用 |
| `SendUserFileTool` | 向用户发送文件 | AI 导出文件时调用 |
| `REPLTool` | 启动子 REPL 会话 | AI 需要独立交互环境时调用 |
| `VerifyPlanExecutionTool` | 验证执行计划完成度 | AI 完成计划后自动验证 |
| `SuggestBackgroundPRTool` | 建议创建后台 PR | AI 发现可独立的变更时提议 |
| `ListPeersTool` | 列出已发现的 peer | AI 查询多实例状态时调用 |
---
## 附录:全部 Feature Flags
| Flag | 默认 | 说明 |
|------|------|------|
| `BUDDY` | ✅ dev/build | 伴侣系统 |
| `BRIDGE_MODE` | ✅ dev/build | 远程控制 |
| `VOICE_MODE` | ✅ dev/build | 语音模式 |
| `CHICAGO_MCP` | ✅ dev/build | Computer Use + Chrome |
| `AGENT_TRIGGERS_REMOTE` | ✅ dev/build | 定时任务 |
| `SHOT_STATS` | ✅ dev/build | API 统计 |
| `TOKEN_BUDGET` | ✅ dev/build | Token 预算 |
| `PROMPT_CACHE_BREAK_DETECTION` | ✅ dev/build | 缓存检测 |
| `ULTRAPLAN` | ✅ dev/build | 高级规划 |
| `DAEMON` | ✅ dev/build | 后台守护 |
| `UDS_INBOX` | ✅ dev/build | Pipe IPC |
| `LAN_PIPES` | ✅ dev/build | LAN 群控 |
| `MONITOR_TOOL` | ✅ dev/build | 后台监控 |
| `WORKFLOW_SCRIPTS` | ✅ dev/build | 工作流脚本 |
| `FORK_SUBAGENT` | ✅ dev/build | 子 Agent |
| `KAIROS` | ✅ dev/build | Kairos 调度 |
| `COORDINATOR_MODE` | ✅ dev/build | 多 Worker |
| `HISTORY_SNIP` | ✅ dev/build | 历史管理 |
| `CONTEXT_COLLAPSE` | ✅ dev/build | 上下文折叠 |
手动启用任意 flag
```bash
FEATURE_FLAG_NAME=1 bun run dev
```
---
## 附录PR 列表
| PR | 日期 | 标题 |
|----|------|------|
| #60 | 2026-04-02 | feat: enable Remote Control (BRIDGE_MODE) |
| #82 | 2026-04-03 | refactor(buddy): align companion system |
| #88 | 2026-04-03 | feat: enable /schedule (AGENT_TRIGGERS_REMOTE) |
| #89 | 2026-04-03 | feat: built-in status line |
| #92 | 2026-04-03 | feat: enable /voice mode |
| #93 | 2026-04-03 | feat: enable Chrome MCP |
| #98 | 2026-04-03 | feat: enable Computer Use (macOS + Windows + Linux) |
| #137 | 2026-04-05 | feat: Computer Use v2 — 跨平台 Executor |
| #140 | 2026-04-05 | feat: enable SHOT_STATS, TOKEN_BUDGET |
| #153 | 2026-04-06 | feat: enable GrowthBook local gate defaults |
| #156 | 2026-04-06 | feat: enable /ultraplan |
| #170 | 2026-04-07 | feat: restore daemon supervisor |
| #241 | 2026-04-11 | feat: restore pipe IPC, LAN pipes, monitor tool |

View File

@@ -0,0 +1,30 @@
# Chrome Use — 浏览器自动化快速指南
让 Claude Code 直接控制你的 Chrome 浏览器,用自然语言完成网页操作。
## 快速开始3 分钟)
### 第一步:安装 Chrome 扩展
1. 下载扩展https://github.com/hangwin/mcp-chrome/releases
2. 解压 zip 文件
3. 打开 Chrome 访问 `chrome://extensions/`
4. 开启右上角「开发者模式」
5. 点击「加载已解压的扩展程序」,选择解压后的文件夹
### 第二步:启动 Claude Code
```bash
bun run dev
ccb # 或者 ccb 安装版也行
```
### 第三步:启用 Chrome MCP
1. 在 REPL 中输入 `/mcp` 打开 MCP 面板
2. 找到 `mcp-chrome`,按空格键启用
3. 按 Enter 确认
## 相关文档
- GitHub 仓库https://github.com/hangwin/mcp-chrome

View File

@@ -0,0 +1,318 @@
# Daemon 重构设计方案
> 分支: `feat/integrate-5-branches`
> 基于: `f41745cb` (= main `11bb3f62` 内容)
> 日期: 2026-04-13
## 一、问题概述
### 1.1 命令结构散乱
当前后台进程相关的命令分布在三个不同的位置,没有统一的命名空间:
| 命令 | 注册位置 | 入口 |
|------|---------|------|
| `claude daemon start/status/stop` | `cli.tsx` 快速路径 L203 | `daemon/main.ts` |
| `claude ps` | `cli.tsx` 快速路径 L220 | `cli/bg.ts` |
| `claude logs <x>` | `cli.tsx` 快速路径 L232 | `cli/bg.ts` |
| `claude attach <x>` | `cli.tsx` 快速路径 L236 | `cli/bg.ts` |
| `claude kill <x>` | `cli.tsx` 快速路径 L238 | `cli/bg.ts` |
| `claude --bg` | `cli.tsx` 快速路径 L244 | `cli/bg.ts` |
| `claude new/list/reply` | `cli.tsx` 快速路径 L250 | `cli/handlers/templateJobs.ts` |
| `claude rollback` | `main.tsx` Commander.js L6525 | `cli/rollback.ts` |
| `claude up` | `main.tsx` Commander.js L6511 | `cli/up.ts` |
**问题**:
- `ps/logs/attach/kill``daemon` 逻辑上都是后台进程管理,但互不关联
- 这些命令都**只有 CLI 入口**REPL 里输入 `/daemon``/ps` 不存在
- `new/list/reply` 是模板任务系统的顶级命令,容易与其他命令冲突(特别是 `list`
### 1.2 Windows 不支持
`--bg``attach` 硬依赖 tmux
- `bg.ts:handleBgFlag()` 第一步就检查 tmux不可用直接报错退出
- `bg.ts:attachHandler()``tmux attach-session`,无 tmux 替代方案
- Windows (包括 VS Code 终端) 完全无法使用后台会话功能
### 1.3 无 REPL 入口
对比 `/mcp` 的双注册模式:
- **CLI**: `claude mcp serve/add/remove/list` (Commander.js, `main.tsx:5760`)
- **REPL**: `/mcp enable/disable/reconnect` (slash command, `commands/mcp/index.ts`)
`daemon`/`bg`/`job` 系列只有 CLI 快速路径REPL 中完全不可用。
## 二、目标
1. **层级化命令结构**: 参照 `/mcp` 模式,将后台管理收归 `/daemon`,模板任务收归 `/job`
2. **跨平台后台会话**: Windows / macOS / Linux 都能启动、附着、终止后台会话
3. **双注册**: CLI (`claude daemon ...`) + REPL (`/daemon ...`) 同时可用
4. **向后兼容**: 旧命令保留但输出 deprecation 提示
## 三、命令结构设计
### 3.1 `/daemon` — 后台进程管理
合并 daemon supervisor + bg sessions 为统一命名空间:
```
claude daemon <subcommand> ← CLI 入口 (cli.tsx 快速路径)
/daemon <subcommand> ← REPL 入口 (slash command, local-jsx)
子命令:
status 综合状态面板 (daemon + 所有会话)
start [--dir <path>] 启动 daemon supervisor
stop 停止 daemon
bg [args...] 启动后台会话
attach [target] 附着到后台会话
logs [target] 查看会话日志
kill [target] 终止会话
(无参数) 等同于 status
```
**CLI 快速路径路由** (`cli.tsx`):
```typescript
// 新: 统一入口
if (feature('DAEMON') && args[0] === 'daemon') {
const sub = args[1] || 'status'
switch (sub) {
case 'start': case 'stop': case 'status':
await daemonMain([sub, ...args.slice(2)])
break
case 'bg':
await bg.handleBgStart(args.slice(2))
break
case 'attach': case 'logs': case 'kill':
await bg[`${sub}Handler`](args[2])
break
}
}
// 向后兼容 (deprecated)
if (feature('BG_SESSIONS') && ['ps','logs','attach','kill'].includes(args[0])) {
console.warn(`[deprecated] Use: claude daemon ${args[0] === 'ps' ? 'status' : args[0]}`)
// ... delegate to daemon subcommand
}
```
**REPL 斜杠命令** (`commands/daemon/index.ts`):
```typescript
const daemon = {
type: 'local-jsx',
name: 'daemon',
description: 'Manage background sessions and daemon',
argumentHint: '[status|start|stop|bg|attach|logs|kill]',
isEnabled: () => feature('DAEMON') || feature('BG_SESSIONS'),
load: () => import('./daemon.js'),
} satisfies Command
```
### 3.2 `/job` — 模板任务管理
```
claude job <subcommand> ← CLI 入口
/job <subcommand> ← REPL 入口
子命令:
list 列出模板和活跃任务
new <template> [args] 从模板创建任务
reply <id> <text> 回复任务
status <id> 查看任务状态
(无参数) 等同于 list
```
### 3.3 独立命令 (不变)
```
claude up 保持顶级 (简短的 bootstrap 命令)
claude rollback [target] 保持顶级 (低频运维命令)
```
## 四、跨平台后台引擎
### 4.1 引擎抽象
```typescript
// src/cli/bg/engine.ts
export interface BgEngine {
readonly name: string
/** 当前平台是否可用 */
available(): Promise<boolean>
/** 启动后台会话 */
start(opts: BgStartOptions): Promise<BgStartResult>
/** 附着到后台会话blocking */
attach(session: SessionEntry): Promise<void>
}
export interface BgStartOptions {
sessionName: string
args: string[]
env: Record<string, string | undefined>
logPath: string
cwd: string
}
export interface BgStartResult {
pid: number
sessionName: string
logPath: string
engineUsed: string
}
```
### 4.2 三种引擎实现
| 引擎 | 平台 | 启动方式 | attach 方式 |
|------|------|---------|------------|
| TmuxEngine | macOS/Linux (有 tmux) | `tmux new-session -d` | `tmux attach-session` |
| DetachedEngine | Windows / 无 tmux 的 macOS/Linux | `spawn({ detached, stdio→logFile })` | `tail -f` 日志文件 |
#### DetachedEngine 详细设计
**启动 (`start`)**:
```typescript
// 1. 打开日志文件 fd
const logFd = fs.openSync(logPath, 'a')
// 2. detached spawn, stdout/stderr 重定向到日志
const child = spawn(process.execPath, execArgs, {
detached: true,
stdio: ['ignore', logFd, logFd],
env,
cwd,
})
child.unref()
fs.closeSync(logFd)
// 3. 写 sessions/<PID>.json
```
**附着 (`attach`)**:
```typescript
// 跨平台 tail -f 实现
// 1. 读取已有日志内容输出到 stdout
// 2. fs.watch(logPath) 监听变化
// 3. 每次变化读取新增内容
// 4. Ctrl+C 退出 tail不杀后台进程
```
#### 引擎选择逻辑
```typescript
// src/cli/bg/engines/index.ts
export async function selectEngine(): Promise<BgEngine> {
if (process.platform === 'win32') {
return new DetachedEngine()
}
const tmux = new TmuxEngine()
if (await tmux.available()) {
return tmux
}
return new DetachedEngine()
}
```
### 4.3 SessionEntry 扩展
```typescript
interface SessionEntry {
// ... 现有字段
engine: 'tmux' | 'detached' // 新增: 记录使用的引擎
tmuxSessionName?: string // tmux 引擎才有
logPath?: string // 两种引擎都有
}
```
`attach` 时根据 `session.engine` 选择对应的 attach 策略。
## 五、文件变更清单
### 新增文件 (10 个)
```
src/cli/bg/engine.ts BgEngine 接口定义
src/cli/bg/engines/tmux.ts TmuxEngine (从 bg.ts 提取)
src/cli/bg/engines/detached.ts DetachedEngine (新实现)
src/cli/bg/engines/index.ts 引擎选择 + re-export
src/cli/bg/tail.ts 跨平台日志 tail (用于 detached attach)
src/commands/daemon/index.ts /daemon REPL 斜杠命令注册
src/commands/daemon/daemon.tsx /daemon 子命令路由 + status UI
src/commands/job/index.ts /job REPL 斜杠命令注册
src/commands/job/job.tsx /job 子命令路由 + UI
docs/features/daemon-restructure-design.md 本设计文档
```
### 修改文件 (6 个)
```
src/cli/bg.ts 重构: handler 函数改为调用 BgEngine
src/entrypoints/cli.tsx 快速路径: daemon 统一入口 + 向后兼容
src/commands.ts 注册 /daemon 和 /job 斜杠命令
src/daemon/main.ts daemonMain() 增加 bg/ps/logs 子命令分发
src/main.tsx Commander.js: 可选注册 daemon/job 子命令
src/cli/handlers/templateJobs.ts 适配 /job 入口 (可能不需改)
```
### 不动的文件
```
src/daemon/state.ts daemon PID 状态管理 (无需改)
src/jobs/state.ts job 状态管理 (无需改)
src/jobs/templates.ts 模板发现 (无需改)
src/jobs/classifier.ts 任务分类器 (无需改)
src/cli/rollback.ts 保持顶级命令 (无需改)
src/cli/up.ts 保持顶级命令 (无需改)
```
## 六、可行性分析
### 6.1 风险评估
| 风险 | 级别 | 缓解措施 |
|------|------|---------|
| cli.tsx 快速路径修改影响启动性能 | 低 | 仅改路由逻辑import 仍然 lazy |
| DetachedEngine 的 attach 在 Windows 上 fs.watch 不可靠 | 中 | 使用轮询 fallback (setInterval + fs.stat) |
| 向后兼容的 deprecation 可能破坏脚本 | 低 | 旧命令保持可用,仅输出 stderr 警告 |
| REPL 中 /daemon bg 需要 spawn 子进程 | 中 | 参考 /assistant 的 NewInstallWizard (已有 spawn 先例) |
| tsc 类型兼容 | 低 | 接口定义清晰,不引入 any |
### 6.2 工作量估计
| Task | 文件数 | 复杂度 |
|------|--------|--------|
| Task 013: BgEngine 抽象 + 引擎实现 | 5 新增 + 1 修改 | 中 |
| Task 014: /daemon 命令层级化 | 3 新增 + 3 修改 | 中 |
| Task 015: /job 命令层级化 | 2 新增 + 2 修改 | 低 |
| Task 016: 向后兼容 + 测试 | 0 新增 + 2 修改 | 低 |
### 6.3 依赖关系
```
Task 013 (BgEngine) ← 无依赖,可独立开发
Task 014 (/daemon) ← 依赖 Task 013 (引擎选择)
Task 015 (/job) ← 无依赖,可与 013 并行
Task 016 (兼容) ← 依赖 Task 014 + 015
```
## 七、设计决策记录
### D1: 为什么 daemon + bg sessions 合为一个命名空间?
用户视角:都是"后台运行的东西"。分开会导致 `claude daemon status` 看 supervisor + `claude ps` 看会话,割裂感强。合并后 `claude daemon status` 一次性展示 supervisor 状态 + 所有会话列表。
### D2: 为什么 rollback/up 不收入 daemon
它们本质是**版本管理/环境初始化**,不是后台进程管理。`claude up` 是同步阻塞的 setup 脚本,不涉及 daemon 或后台会话。保持顶级更直观。
### D3: 为什么 DetachedEngine 的 attach 用 tail 而不是 IPC
1. 日志文件是最简单的跨平台方案,无需额外依赖
2. UDS Pipe IPC 系统 (usePipeIpc) 设计用于实例间通信,不是终端附着
3. tmux attach 的体验(完整 PTY无法在纯 detached 模式下复制tail 是最诚实的替代
### D4: 为什么不用 Windows Terminal 的 tab/pane API
Windows Terminal 的 `wt.exe` 新窗口/标签功能不够通用——用户可能在 VS Code、ConEmu、cmder 等终端中。detached + log 是唯一跨终端方案。

View File

@@ -1011,38 +1011,32 @@ src/utils/swarm/ 目录22 个文件):
## 28. UDS_INBOX
**编译时引用次数**: 18单引号 17 + 双引号 1
**功能描述**: UDSUnix Domain Socket收件箱。允许 Claude Code 实例之间通过 Unix 套接字发送消息。
**分类**: PARTIAL
**缺失原因**: `src/utils/udsMessaging.ts` 仅 1 行,`src/utils/udsClient.ts` 仅 3 行(空壳),命令入口缺失
**编译时引用次数**: 18历史快照
**功能描述**: 本机进程间通信能力。当前由两层组成:
1. `udsMessaging` / `udsClient`:通用 UDS 消息层,供 `SendMessageTool``/peers` 使用。
2. `pipeTransport` / `pipeRegistry`:会话级 named-pipe 协调层,供 `/pipes``/attach``/detach``/send``/pipe-status``/history``/claim-main` 使用。
**当前分类**: IMPLEMENTED / EXPERIMENTAL
**当前事实**:
- `src/utils/udsMessaging.ts``src/utils/udsClient.ts` 已实现,不再是空壳。
- `src/utils/pipeTransport.ts` 使用本机 named pipe / Unix socket`localIp` / `hostname` / `machineId` 仅用于注册表展示与身份判定,不是已上线的局域网传输层。
- `src/screens/REPL.tsx` 内联承载当前有效的 pipe 控制平面;早期 hook 试验路径已清理。
**核心实现文件**:
| 文件路径 | 行数 | 功能说明 |
|----------|------|----------|
| src/tools/SendMessageTool/SendMessageTool.ts | 917 行 | 发送消息工具(完整实现) |
| src/tools/SendMessageTool/prompt.ts | 49 行 | 消息工具提示词 |
| src/utils/udsClient.ts | 3 行 | UDS 客户端(桩) |
| src/utils/udsMessaging.ts | 1 行 | UDS 消息(桩) |
| 文件路径 | 功能说明 |
|----------|----------|
| src/utils/udsMessaging.ts | 通用 UDS server / inbox |
| src/utils/udsClient.ts | 通用 peer 发现、探活、发送 |
| src/utils/pipeTransport.ts | named-pipe server/client、探活、AppState 扩展 |
| src/utils/pipeRegistry.ts | main/sub 注册表、machineId、claim-main |
| src/commands/peers/peers.ts | UDS peer 可达性检查 |
| src/commands/pipes/pipes.ts | pipe 注册表检查与选择器入口 |
| src/commands/attach/attach.ts | master -> slave attach |
| src/screens/REPL.tsx | 当前生效的 REPL pipe bootstrap 与心跳 |
**引用该标志的文件10 个)**:
1. src/cli/print.ts — CLI 输出
2. src/commands.ts — 命令注册(引用 `commands/peers/index.js`
3. src/components/messages/UserTextMessage.tsx — 用户消息
4. src/main.tsx — 主入口
5. src/setup.ts — 初始化
6. src/tools.ts — 工具注册
7. src/tools/SendMessageTool/SendMessageTool.ts — 发送消息工具
8. src/tools/SendMessageTool/prompt.ts — 提示词
9. src/utils/concurrentSessions.ts — 并发会话
10. src/utils/messages/systemInit.ts — 系统初始化消息
**缺失文件**:
- src/commands/peers/index.ts — 命令入口缺失
- src/utils/udsMessaging.ts — 仅 1 行空壳
- src/utils/udsClient.ts — 仅 3 行空壳
**启用所需修复**: 需要实现 UDS 客户端和消息模块,并创建命令入口。
**备注**: 如需真实局域网通信,需要单独引入 TCP/WebSocket 传输、认证与发现机制;现有代码尚未实现该层。详见 `docs/features/uds-inbox.md`
---

View File

@@ -1,7 +1,7 @@
# KAIROS — 常驻助手模式
> Feature Flag: `FEATURE_KAIROS=1`(及子 Feature
> 实现状态:核心框架完整,部分子模块为 stub
> 实现状态:核心框架完整,部分子模块为 stubproactive/sleep 节奏控制已可用
> 引用数154全库最大
## 一、功能概述
@@ -74,8 +74,9 @@ KAIROS 在系统提示中注入两大段落:
SleepTool 是 KAIROS/Proactive 的节奏控制核心。工具描述让模型理解"休眠"概念:
- 工具名:`Sleep`
- 功能:等待指定时间后响应 tick prompt
- 功能:等待指定时间后响应 tick prompt;若队列出现新工作或 proactive 被关闭,会提前唤醒
-`<tick_tag>` 配合实现心跳式自主工作
- 远程控制 surfaces 可通过 `automation_state` 看到 `standby` / `sleeping` 两种状态
### 3.3 Bridge 集成
@@ -172,8 +173,10 @@ FEATURE_KAIROS=1 FEATURE_TOKEN_BUDGET=1 bun run dev
| `src/assistant/AssistantSessionChooser.ts` | — | Session 选择 UIstub |
| `src/tools/BriefTool/` | — | BriefTool 实现stub |
| `src/tools/SleepTool/prompt.ts` | ~30 | SleepTool 工具提示 |
| `src/tools/SleepTool/SleepTool.ts` | ~200 | 休眠/唤醒与 automation metadata |
| `src/services/mcp/channelNotification.ts` | 5 | 频道消息接入stub |
| `src/memdir/memdir.ts` | — | 记忆目录管理stub |
| `src/constants/prompts.ts:552-554,843-914` | 72 | 系统提示注入 |
| `src/components/tasks/src/tasks/DreamTask/` | 3 | Dream 任务stub |
| `src/proactive/index.ts` | — | Proactive 核心(stubKAIROS 共享) |
| `src/proactive/index.ts` | — | Proactive 核心KAIROS 共享) |
| `src/utils/sessionState.ts` | — | 向 bridge/CCR 暴露 automation 状态 |

View File

@@ -0,0 +1,321 @@
# LAN Pipes — 技术实现文档
面向开发者的实现细节。用户指南见 [lan-pipes.md](./lan-pipes.md)。
---
## 架构
```
Machine A (192.168.50.22) Machine B (192.168.50.27)
┌───────────────────────────┐ ┌───────────────────────────┐
│ PipeServer │ │ PipeServer │
│ UDS: ~/.claude/pipes/ │ │ UDS: ~/.claude/pipes/ │
│ cli-abc.sock │ │ cli-def.sock │
│ TCP: 0.0.0.0:<random> │◄──TCP───►│ TCP: 0.0.0.0:<random> │
├───────────────────────────┤ ├───────────────────────────┤
│ LanBeacon │ │ LanBeacon │
│ UDP 224.0.71.67:7101 │◄──UDP───►│ UDP 224.0.71.67:7101 │
├───────────────────────────┤ ├───────────────────────────┤
│ usePipeIpc (hook) │ │ usePipeIpc (hook) │
│ initPipeServer │ │ initPipeServer │
│ registerMessageHandlers │ │ registerMessageHandlers │
│ runMainHeartbeat │ │ runSubHeartbeat │
│ cleanupPipeIpc │ │ cleanupPipeIpc │
└───────────────────────────┘ └───────────────────────────┘
```
## Feature Flag
`LAN_PIPES` — 在 `scripts/dev.ts``build.ts``DEFAULT_FEATURES` 中启用。
所有 LAN 代码路径通过 `feature('LAN_PIPES')` 编译时门控。`feature()` 只能在 `if` 或三元中使用Bun 编译时常量约束)。
---
## 核心文件
| 文件 | 说明 |
|------|------|
| `src/utils/pipeTransport.ts` | PipeServer/PipeClientUDS + TCP 双模式) |
| `src/utils/lanBeacon.ts` | UDP multicast beacon + module singleton |
| `src/utils/ndjsonFramer.ts` | 共享 NDJSON socket 帧解析 |
| `src/utils/pipeRegistry.ts` | 文件注册表 + `mergeWithLanPeers()` |
| `src/utils/peerAddress.ts` | 地址解析uds/bridge/tcp scheme |
| `src/utils/pipePermissionRelay.ts` | 权限转发 + `setPipeRelay`/`getPipeRelay` singleton |
| `src/hooks/usePipeIpc.ts` | 生命周期 hook从 REPL.tsx 提取) |
| `src/hooks/usePipeRelay.ts` | 消息回传 hook |
| `src/hooks/usePipePermissionForward.ts` | 权限转发 hook |
| `src/hooks/usePipeRouter.ts` | 输入路由 hook |
| `src/hooks/useMasterMonitor.ts` | slave 注册表 + 消息订阅 |
---
## PipeServer TCP 扩展
`src/utils/pipeTransport.ts`
### 类型
```typescript
export type PipeTransportMode = 'uds' | 'tcp'
export type TcpEndpoint = { host: string; port: number }
export type PipeServerOptions = { enableTcp?: boolean; tcpPort?: number }
```
### PipeServer 变更
- `setupSocket(socket)` — 从 start() 提取的共享方法UDS 和 TCP 共用
- `start(options?)` — 可选启用 TCPport=0 让 OS 分配
- 内部维护两个 `net.Server`,共享同一组 `clients: Set<Socket>``handlers`
- `tcpAddress` getter 暴露 TCP 端口
- `close()` 同时关闭两个 server
socket 帧解析使用 `attachNdjsonFramer()` from `ndjsonFramer.ts`(替代原先 3 份重复代码)。
### PipeClient 变更
- 构造函数新增可选 `TcpEndpoint` 参数
- `connect()` 根据 tcpEndpoint 分派到 `connectTcp()``connectUds()`
- TCP 不需要文件存在轮询,直接建连
---
## LAN Beacon
`src/utils/lanBeacon.ts`
### 协议参数
| 参数 | 值 |
|------|-----|
| Multicast 组 | `224.0.71.67` |
| 端口 | `7101` |
| 广播间隔 | `3000ms` |
| Peer 超时 | `15000ms` |
| TTL | `1` |
### Announce 包
```typescript
type LanAnnounce = {
proto: 'claude-pipe-v1'
pipeName: string
machineId: string
hostname: string
ip: string
tcpPort: number
role: 'main' | 'sub'
ts: number
}
```
### API
```typescript
class LanBeacon extends EventEmitter {
constructor(announce: Omit<LanAnnounce, 'proto' | 'ts'>)
start(): void
stop(): void
getPeers(): Map<string, LanAnnounce> // 防御性拷贝
updateAnnounce(partial): void // 使用 spread不可变更新
on('peer-discovered', (peer: LanAnnounce) => void)
on('peer-lost', (pipeName: string) => void)
}
```
### 存储
module-level singleton`getLanBeacon()` / `setLanBeacon()`。不挂在 Zustand state 上(避免 `setState` 展开时丢失引用)。
### 网卡绑定
`addMembership(group, localIp)` + `setMulticastInterface(localIp)` 指定 LAN 网卡。解决 Windows 上 WSL/Docker 虚拟网卡劫持 multicast 的问题。
---
## Hook 架构
从 REPL.tsx 提取的 ~830 行 Pipe IPC 代码:
### usePipeIpc生命周期
`src/hooks/usePipeIpc.ts`623 行)
在 REPL.tsx 顶层通过 feature-gated require 加载:
```typescript
const usePipeIpc = feature('UDS_INBOX')
? require('../hooks/usePipeIpc.js').usePipeIpc
: () => undefined;
// 组件内
usePipeIpc({ store, handleIncomingPrompt });
```
内部使用 **lazy getter** 函数加载依赖(避免循环依赖导致 Bun 运行时崩溃):
```typescript
const pt = () => require('../utils/pipeTransport.js')
const pr = () => require('../utils/pipeRegistry.js')
const mm = () => require('./useMasterMonitor.js')
// ...
```
`import type` 用于静态类型(不会触发模块加载)。
### 四个阶段函数
| 函数 | 职责 |
|------|------|
| `initPipeServer` | 角色判定 + server 创建 + beacon 启动 |
| `registerMessageHandlers` | ping、attach、prompt、permission、detach 五个 handler |
| `runMainHeartbeat` | cleanup + 发现 + auto-attach + 清理死连接 |
| `runSubHeartbeat` | 检测 main 是否存活,死亡则接管或独立 |
### usePipeRelay消息回传
`src/hooks/usePipeRelay.ts`38 行)
提供 `relayPipeMessage()``pipeReturnHadErrorRef`。relay 函数通过 `getPipeRelay()` module singleton 读取(替代 `globalThis.__pipeSendToMaster`)。
### usePipePermissionForward权限转发
`src/hooks/usePipePermissionForward.ts`159 行)
订阅 `subscribePipeEntries()`,处理:
- `permission_request` → 解析 payload → 查找 tool → 加入确认队列
- `permission_cancel` → 从队列移除
- `stream/error/done` → 转为系统消息显示(含 role + IP 标签)
### usePipeRouter输入路由
`src/hooks/usePipeRouter.ts`130 行)
提供 `routeToSelectedPipes(input): boolean`。读取 `selectedPipes` + `routeMode`,逐个发送到已连接目标。通知显示 `[role] hostname/ip`LAN peer`[role]`(本机)。
---
## Registry 并行探测
`src/utils/pipeRegistry.ts`
### getAliveSubs()
```typescript
export async function getAliveSubs(): Promise<PipeRegistrySub[]> {
const registry = await readRegistry()
const results = await Promise.all(
registry.subs.map(sub =>
isPipeAlive(sub.pipeName, 1000).then(alive => alive ? sub : null)
)
)
return results.filter(Boolean)
}
```
### cleanupStaleEntries()
两阶段:
1. **无锁并行探测**`Promise.all` 探测 main + 所有 subs
2. **短暂持锁写入**`acquireLock()` → 重新读取 → 应用变更 → 写入 → `releaseLock()`
持锁时间从 N 秒降至 ~10ms。
### getMachineId()
Windows/macOS 使用 `execFile`(异步),不阻塞主线程。结果缓存,仅首次调用执行。
---
## NDJSON 协议
### 消息类型
| 类型 | 方向 | 数据 |
|------|------|------|
| `ping` / `pong` | 双向 | 无 |
| `attach_request` | M→S | `meta: { machineId }` |
| `attach_accept` / `attach_reject` | S→M | `data: reason` |
| `detach` | M→S | 无 |
| `prompt` | M→S | `data: prompt_text` |
| `prompt_ack` | S→M | `data: 'accepted'` |
| `stream` | S→M | `data: partial_text` |
| `done` | S→M | 无 |
| `error` | 双向 | `data: error_message` |
| `permission_request` | S→M | `data: JSON(PipePermissionRequestPayload)` |
| `permission_response` | M→S | `data: JSON(PipePermissionResponsePayload)` |
| `permission_cancel` | M→S | `data: JSON({ requestId, reason })` |
### 帧格式
每行一个 JSON 对象,`\n` 分隔:
```
{"type":"ping","from":"cli-abc","ts":"2026-04-11T00:00:00.000Z"}\n
{"type":"prompt","data":"检查 git status","from":"cli-abc"}\n
```
---
## 跨机器 Attach 流程
```
CLI-B (192.168.50.27) 心跳循环
→ beacon.getPeers() 发现 CLI-A (192.168.50.22)
→ connectToPipe(pName, myName, 3000, { host: '192.168.50.22', port: 58853 })
→ PipeClient.connectTcp() → net.createConnection({ host, port })
→ client.send({ type: 'attach_request', meta: { machineId } })
→ CLI-A 收到:
isLanPeer = (msg.meta.machineId !== myMachineId) → true
→ 不检查 role直接 reply({ type: 'attach_accept' })
→ setPipeRelay(socket.write)
→ CLI-B 收到 attach_accept
→ addSlaveClient(pName, client)
→ store.setState: role='master', slaves[pName] = { status: 'idle' }
```
关键:跨机器 attach 不要求对方是 sub 角色。通过 `machineId` 区分 LAN peer。
---
## SendMessageTool TCP 支持
`src/tools/SendMessageTool/SendMessageTool.ts`
- `to` 字段支持 `tcp:host:port` 格式
- `checkPermissions``tcp:` scheme 返回 `behavior: 'ask'``classifierApprovable: false`
- `call()`:创建临时 `PipeClient` → connect → send → disconnect
---
## 测试
| 文件 | 测试数 | 覆盖 |
|------|--------|------|
| `lanBeacon.test.ts` | 7 | socket 初始化、announce、peer 发现/过滤/清理 |
| `peerAddress.test.ts` | 8 | scheme 解析、parseTcpTarget、端口范围验证 |
| `pipePermissionRelay.test.ts` | 2 | setPipeRelay singleton、权限请求/响应 |
| `pipeTransport.test.ts` | 2 | UDS 基础行为 |
| `useMasterMonitor.test.ts` | 5 | slave 注册/移除、事件发射 |
全量2190 pass / 0 fail
---
## 已知限制
1. **TCP 无认证** — 同 LAN 内知道端口号即可连接
2. **Beacon 明文广播** — IP/hostname/machineId 未 hash
3. **单网卡选择**`getLocalIp()` 返回首个非内部 IPv4可能选到 VPN
4. **端口随机** — 每次启动不同端口,依赖 beacon 发现
5. **SendMessageTool 每次创建新连接** — 未复用已有 slave client
## 后续改进方向
1. HMAC-SHA256 TCP 握手认证
2. machineId hash 后再广播
3. 多网卡选择(优先 RFC 1918 地址)
4. 固定端口范围配置
5. TLS 加密传输
6. SendMessageTool 复用已连接的 slave client

193
docs/features/lan-pipes.md Normal file
View File

@@ -0,0 +1,193 @@
# LAN Pipes — 局域网多机器群控指南
## 什么是 LAN Pipes
LAN Pipes 让多台机器上的 Claude Code 实例通过局域网自动发现并协作。你可以在一台机器main上操控其他机器sub上的 Claude Code发送 prompt、查看执行结果、审批权限请求——全程零配置。
基于本机 Pipe IPC`UDS_INBOX`)扩展,新增 TCP 传输层 + UDP Multicast 发现。
## 前置条件
- 两台或以上机器在同一局域网
- 每台机器安装了 CCB 并能 `bun run dev`
- Feature flag `LAN_PIPES`dev/build 默认开启)
- 防火墙允许 UDP 7101 + TCP 动态端口(见下方配置)
## 快速开始
### 第一步:配置防火墙
**每台机器都需要执行。**
**Windows**(管理员 PowerShell
```powershell
New-NetFirewallRule -DisplayName "CCB LAN Beacon (UDP)" -Direction Inbound -Protocol UDP -LocalPort 7101 -Action Allow -Profile Private
New-NetFirewallRule -DisplayName "CCB LAN Pipes (TCP)" -Direction Inbound -Protocol TCP -LocalPort 1024-65535 -Program (Get-Command bun).Source -Action Allow -Profile Private
New-NetFirewallRule -DisplayName "CCB LAN Beacon Out (UDP)" -Direction Outbound -Protocol UDP -RemotePort 7101 -Action Allow -Profile Private
```
验证网络为"专用"(非公共):`Get-NetConnectionProfile`
**macOS**
首次运行时系统弹出"允许接受传入连接"对话框,点击"允许"。
如果使用 pf 防火墙:
```bash
echo "pass in proto udp from any to any port 7101" | sudo pfctl -ef -
```
**Linux**firewalld
```bash
sudo firewall-cmd --zone=trusted --add-port=7101/udp --permanent
sudo firewall-cmd --zone=trusted --add-port=1024-65535/tcp --permanent
sudo firewall-cmd --reload
```
**Linux**iptables
```bash
sudo iptables -A INPUT -p udp --dport 7101 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 1024:65535 -m owner --uid-owner $(id -u) -j ACCEPT
```
### 第二步:启动
```bash
# 机器 A例如 192.168.50.22
bun run dev
# 机器 B例如 192.168.50.27
bun run dev
```
启动后等待 3-5 秒beacon 广播间隔),两边自动发现并连接。
### 第三步:查看和操作
在任一台机器上:
```
/pipes
```
输出示例:
```
pipe: cli-a91bad56 (main) 192.168.50.22 2/3 selected
Main machine: 205d6c3a... (this machine)
[main] cli-a91bad56 XC/192.168.50.22 [alive] (you)
☑ [sub-1] cli-da029538 XC/192.168.50.22 [alive] [connected]
LAN Peers:
☐ [main] cli-04d67950 vmwin11/192.168.50.27 tcp:192.168.50.27:58853 [LAN]
```
### 第四步:选中目标并发送任务
1.`Shift+↓` 展开选择面板
2. `↑↓` 移动到 LAN peer
3. `Space` 选中
4. `Enter` 确认
5. 输入 prompt自动路由到远端执行
远端执行结果会流式回传到你的消息列表:
```
[main vmwin11/192.168.50.27 / cli-04d67950] 正在检查 git status...
[main vmwin11/192.168.50.27 / cli-04d67950] Completed
```
## 完整命令参考
| 命令 | 说明 |
|------|------|
| `/pipes` | 显示所有实例(本机 + LANShift+↓ 展开选择面板 |
| `/pipes select <name>` | 选中某实例 |
| `/pipes all` | 全选 |
| `/pipes none` | 取消全选 |
| `/attach <name>` | 手动 attach自动识别 LAN peer 并通过 TCP 连接) |
| `/detach <name>` | 断开连接 |
| `/send <name> <msg>` | 向指定 pipe 发送消息 |
| `/send tcp:host:port <msg>` | 直接通过 TCP 地址发送 |
| `/claim-main` | 强制声明为 main |
| `/pipe-status` | 显示详细状态 |
| `/peers` | 列出所有已发现的 peer |
## 快捷键
| 快捷键 | 场景 | 作用 |
|--------|------|------|
| `Shift+↓` | 状态栏可见时 | 展开/收起选择面板 |
| `↑ / ↓` | 面板展开时 | 移动光标 |
| `Space` | 面板展开时 | 选中/取消 |
| `Enter` | 面板展开时 | 确认关闭 |
| `Esc` | 面板展开时 | 取消关闭 |
| `← / →` | 有选中 pipe 时 | 切换路由模式 |
| `M` | 面板展开时 | 同 ←/→ 切换路由模式 |
## 路由模式
| 模式 | 显示 | 行为 |
|------|------|------|
| `selected pipes only` | 绿色 | prompt 仅发送到选中的 pipe本地不执行 |
| `local main` | 灰色 | prompt 仅在本地执行,不转发 |
切换路由模式不会清空选择。
## 权限转发
当远端 slave 执行需要权限的工具(如 BashTool
1. slave 发送 `permission_request` 到 main
2. main 弹出权限确认对话框,显示 `[role hostname/ip / pipeName]`
3. 用户确认/拒绝
4. 结果发回 slave继续或中断
## 工作原理
### 发现机制
- 每台机器启动时创建 UDP multicast beacon
- 组地址 `224.0.71.67`,端口 `7101`TTL=1不跨路由器
- 每 3 秒广播一次自身信息pipeName、IP、TCP 端口、角色)
- 15 秒未收到广播则标记 peer 丢失
### 通信机制
- 本机实例UDSUnix Domain Socket / Named Pipe
- 跨机器TCP动态端口通过 beacon 发现)
- 协议NDJSON每行一个 JSON 对象)
- 消息类型ping/pong、attach/detach、prompt/stream/done/error、permission
### 角色模型
| 角色 | 说明 |
|------|------|
| `main` | 首个启动的实例 |
| `sub` | 同机后续启动的实例 |
| `master` | attach 了至少一个 slave 的实例 |
| `slave` | 被 master attach 的实例 |
跨机器 attach 时,两边都可以是 main——不要求对方必须是 sub。
## 常见问题
### 看不到 LAN peer
1. 检查防火墙是否放行 UDP 7101
2. `Get-NetConnectionProfile`Windows确认网络为"专用"
3. 确认两台机器在同一子网(`ping` 能通)
4. 路由器未开启 AP 隔离
### 连接超时
1. 检查 TCP 入站防火墙规则
2. 确认没有 VPN 劫持流量
3. 尝试 `/send tcp:ip:port hello` 直接测试
### beacon 绑到了错误网卡
Windows 上 WSL/Docker 虚拟网卡可能劫持 multicast。beacon 会自动选择非内部 IPv4 接口。如果选错,检查 `getLocalIp()` 返回值。
## 安全说明
- TCP 连接当前**无认证**——同 LAN 内知道端口号即可连接
- Multicast TTL=1不跨路由器
- AI 通过 `SendMessageTool` 发送 `tcp:` 消息时需**用户显式确认**
- 建议仅在信任的局域网中使用

View File

@@ -0,0 +1,205 @@
# Langfuse 监控集成
> 实现状态:已完成,通过环境变量启用
> 依赖:`@langfuse/otel`、`@langfuse/tracing`、`@opentelemetry/sdk-trace-base`
## 一、功能概述
Langfuse 是一个开源的 LLM 可观测性平台,用于追踪、监控和调试 AI 应用的请求链路。CCB 通过 OpenTelemetry (OTel) 桥接层将 Langfuse 集成到查询流程中,实现:
- **LLM 调用追踪** — 记录每次 API 请求的模型、Provider、输入/输出、Token 用量
- **工具执行追踪** — 记录每个工具调用的名称、输入、输出、耗时和错误
- **多 Agent 追踪** — 主 Agent 和子 Agent 各自独立的 Trace 链路
- **数据脱敏** — 自动遮蔽敏感信息API Key、文件内容、Shell 输出等)
## 二、启用方式
Langfuse 是开源项目,你可以 **自部署**Docker / Kubernetes也可以使用官方提供的 **[Langfuse Cloud](https://cloud.langfuse.com)** 免费测试。注册后在 Project Settings → API Keys 页面获取密钥。
核心只需要三个环境变量:
| 环境变量 | 说明 |
|---------|------|
| `LANGFUSE_PUBLIC_KEY` | Langfuse 公钥(必填) |
| `LANGFUSE_SECRET_KEY` | Langfuse 密钥(必填) |
| `LANGFUSE_BASE_URL` | 服务地址,默认 `https://cloud.langfuse.com`;自部署时改为你的地址(必填) |
未配置时所有追踪函数为 no-op零开销。
### 通过 settings.json 配置(推荐)
`.claude/settings.json``env` 字段中添加,这样每次启动自动生效:
```json
{
"env": {
"LANGFUSE_PUBLIC_KEY": "pk-xxx",
"LANGFUSE_SECRET_KEY": "sk-xxx",
"LANGFUSE_BASE_URL": "https://cloud.langfuse.com"
}
}
```
### 其他可选参数
| 环境变量 | 默认值 | 说明 |
|---------|--------|------|
| `LANGFUSE_TRACING_ENVIRONMENT` | `development` | 环境标签,用于 Langfuse 面板筛选 |
| `LANGFUSE_FLUSH_AT` | `20` | 批量发送的 span 数量阈值 |
| `LANGFUSE_FLUSH_INTERVAL` | `10` | 定时刷新间隔(秒) |
| `LANGFUSE_EXPORT_MODE` | `batched` | 导出模式:`batched`(批量)或 `immediate`(即时) |
| `LANGFUSE_TIMEOUT` | `5` | 请求超时(秒) |
## 四、架构
### 4.1 模块结构
```
src/services/langfuse/
├── index.ts # 统一导出
├── client.ts # OTel Provider + LangfuseSpanProcessor 初始化
├── tracing.ts # Trace/Span 创建、LLM 和工具观察记录
├── convert.ts # 内部 Message 类型 → Langfuse OpenAI 兼容格式转换
└── sanitize.ts # 数据脱敏(敏感字段、文件路径、工具输出)
```
### 4.2 追踪层级
```
Trace (Agent Span) ← createTrace() / createSubagentTrace()
├── Generation (LLM 调用) ← recordLLMObservation()
├── Tool Observation (工具调用) ← recordToolObservation()
├── Tool Observation (工具调用) ← recordToolObservation()
└── ...
```
### 4.3 数据流
```
query.ts ──→ createTrace() # 每个 query turn 创建根 trace
├── claude.ts ──→ recordLLMObservation() # API 调用完成后记录 LLM 观察
├── toolExecution.ts ──→ recordToolObservation() # 每个工具执行记录
└── query.ts ──→ endTrace() # turn 结束时关闭 trace
runAgent.ts ──→ createSubagentTrace() # 子 Agent 有独立 trace
```
## 五、追踪详情
### 5.1 主 Agent Trace
每次 `query()` 调用(即用户一次对话 turn创建一个类型为 `agent` 的根 Span
- **名称**: `agent-run``agent-run:<querySource>`
- **元数据**: `provider``model``agentType: "main"`
- **Session ID**: 关联到 Langfuse 的 Session 功能,支持按会话聚合
### 5.2 子 Agent Trace
通过 `AgentTool` 启动的子 Agent 创建独立 Trace
- **名称**: `agent:<agentType>`
- **元数据**: `provider``model``agentType``agentId`
- 独立于主 Trace有自己的 Session 关联
### 5.3 LLM Generation
每次 API 调用记录为一个 `generation` 类型的 Span
- **名称**: 按 Provider 映射(如 `ChatAnthropic``ChatOpenAI``ChatBedrockAnthropic` 等)
- **记录内容**: 输入消息、输出消息、Token 用量input/output
- **时间**: 精确记录 `startTime``endTime``completionStartTime`TTFT 指标)
Provider 名称映射:
| Provider | Generation 名称 |
|----------|-----------------|
| `firstParty` | `ChatAnthropic` |
| `bedrock` | `ChatBedrockAnthropic` |
| `vertex` | `ChatVertexAnthropic` |
| `foundry` | `ChatFoundry` |
| `openai` | `ChatOpenAI` |
| `gemini` | `ChatGoogleGenerativeAI` |
| `grok` | `ChatXAI` |
### 5.4 工具执行
每个工具调用记录为一个 `tool` 类型的 Span
- **名称**: 工具名(如 `FileEditTool``BashTool`
- **记录内容**: 输入(经脱敏)、输出(经脱敏)、`toolUseId`
- **错误标记**: `isError` 标志 + `level: ERROR`
## 六、数据脱敏
所有上传到 Langfuse 的数据都会经过脱敏处理(`sanitize.ts`),确保敏感信息不会泄露:
### 6.1 全局脱敏(`sanitizeGlobal`
- **Home 路径替换** — `/Users/xxx``~`
- **敏感字段遮蔽** — 匹配 `api_key``token``secret``password``credential``auth_header` 等关键字的字段值替换为 `[REDACTED]`
### 6.2 工具输入脱敏(`sanitizeToolInput`
- 敏感字段遮蔽(同全局)
- `file_path``path``directory` 路径中的 Home 目录替换
### 6.3 工具输出脱敏(`sanitizeToolOutput`
| 工具 | 脱敏策略 |
|------|---------|
| `FileReadTool``FileWriteTool``FileEditTool` | 完全遮蔽,仅保留字符数:`[file content redacted, N chars]` |
| `BashTool``PowerShellTool` | 截断至 500 字符 |
| `ConfigTool``MCPTool` | 完全遮蔽 |
| 其他工具 | 原样保留 |
## 七、消息格式转换
`convert.ts` 将 CCB 内部的 Message 类型转换为 Langfuse 期望的 OpenAI 兼容格式:
- **输入**: `UserMessage | AssistantMessage[]` + 可选 system prompt → `{ role, content }[]`
- **输出**: `AssistantMessage[]``{ role: 'assistant', content }`
- **Content Block 映射**:
- `text``{ type: 'text', text }`
- `thinking` / `redacted_thinking``{ type: 'thinking', thinking }`
- `tool_use``{ type: 'tool_use', id, name, input }`
- `tool_result``{ type: 'tool_result', tool_use_id, content }`
- `image` / `document` → 占位标记 `[image]` / `[document: name]`
## 八、生命周期
1. **初始化**`initLangfuse()``src/entrypoints/init.ts` 启动时调用,创建 `LangfuseSpanProcessor``BasicTracerProvider`
2. **运行时** — 各追踪函数通过 `isLangfuseEnabled()` 检查,未配置时直接返回 `null`/跳过
3. **关闭**`shutdownLangfuse()` 在进程退出时调用,强制 flush 并关闭 Processor
## 九、自部署 Langfuse
Langfuse 是开源项目,支持 Docker / Kubernetes 自部署:
```bash
docker run -d \
--name langfuse \
-p 3000:3000 \
-e DATABASE_URL=postgresql://... \
langfuse/langfuse:latest
```
自部署后,将 `LANGFUSE_BASE_URL` 指向你的实例地址即可。详见 [Langfuse 自部署文档](https://langfuse.com/docs/deployment/self-host)。
如果没有自部署需求,可以直接使用 [Langfuse Cloud](https://cloud.langfuse.com),提供免费额度可用于测试。
## 十、相关文件
| 文件 | 说明 |
|------|------|
| `src/services/langfuse/client.ts` | OTel Provider 初始化、生命周期管理 |
| `src/services/langfuse/tracing.ts` | Trace/Span 创建和观察记录 |
| `src/services/langfuse/convert.ts` | Message 格式转换 |
| `src/services/langfuse/sanitize.ts` | 数据脱敏 |
| `src/services/langfuse/__tests__/langfuse.test.ts` | 测试568 行) |
| `src/query.ts` | 主查询流程中的 Trace 集成 |
| `src/services/tools/toolExecution.ts` | 工具执行中的观察记录 |
| `src/tools/AgentTool/runAgent.ts` | 子 Agent Trace 创建 |

View File

@@ -0,0 +1,342 @@
# Pipes + LAN Pipes 完整功能指南
## 概述
Pipes 系统提供 Claude Code CLI 实例之间的通讯能力,分两层:
1. **Pipes本机**:同一台机器上的多个 CLI 实例通过 UDSUnix Domain Socket / Windows Named Pipe协作
2. **LAN Pipes局域网**:不同机器上的 CLI 实例通过 TCP + UDP Multicast 协作
两层使用同一套协议NDJSON和同一套命令`/pipes``/attach``/send` 等),对用户透明。
## Feature Flags
| Flag | 控制范围 | 默认 |
|------|----------|------|
| `UDS_INBOX` | 本机 Pipe IPC 全部功能 | dev/build 启用 |
| `LAN_PIPES` | 局域网 TCP + beacon 扩展 | dev/build 启用 |
手动启用:`FEATURE_UDS_INBOX=1 FEATURE_LAN_PIPES=1 bun run dev`
## 快速上手
### 本机多实例
```bash
# 终端 1
bun run dev
# 启动后自动注册为 main
# 终端 2
bun run dev
# 自动注册为 sub-1被 main 自动 attach
```
在终端 1 中输入 `/pipes`,可以看到两个实例。选中 sub-1 后,输入的消息会自动转发到 sub-1 执行。
### 局域网多机器
```bash
# 机器 A (192.168.50.22)
bun run dev
# 机器 B (192.168.50.27)
bun run dev
```
两边启动后等 3-5 秒beacon 广播间隔LAN peers 会自动发现并 attach。输入 `/pipes` 可看到标记 `[LAN]` 的远端实例。
### 防火墙配置(两台机器都需要)
**Windows**(管理员 PowerShell
```powershell
New-NetFirewallRule -DisplayName "Claude Code LAN Beacon (UDP)" -Direction Inbound -Protocol UDP -LocalPort 7101 -Action Allow -Profile Private
New-NetFirewallRule -DisplayName "Claude Code LAN Pipes (TCP)" -Direction Inbound -Protocol TCP -LocalPort 1024-65535 -Program (Get-Command bun).Source -Action Allow -Profile Private
New-NetFirewallRule -DisplayName "Claude Code LAN Beacon Out (UDP)" -Direction Outbound -Protocol UDP -RemotePort 7101 -Action Allow -Profile Private
# 确认网络为"专用"Get-NetConnectionProfile
```
**macOS**(首次运行时系统弹出对话框,点击"允许"即可):
```bash
# 如果需要手动放行 pf 防火墙:
echo "pass in proto udp from any to any port 7101" | sudo pfctl -ef -
```
**Linux**firewalld / iptables
```bash
# firewalld
sudo firewall-cmd --zone=trusted --add-port=7101/udp --permanent
sudo firewall-cmd --zone=trusted --add-port=1024-65535/tcp --permanent
sudo firewall-cmd --reload
# 或 iptables
sudo iptables -A INPUT -p udp --dport 7101 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 1024:65535 -m owner --uid-owner $(id -u) -j ACCEPT
```
确认:网络为局域网(非公共 WiFi路由器未开启 AP 隔离。
## 交互面板与快捷键
### 状态栏
执行 `/pipes` 后,输入框底部出现 pipe 状态栏(单行):
```
pipe: cli-a91bad56 (main) 192.168.50.22 2/3 selected selected pipes only · ←/→ or m switch · Shift+↓ edit
```
状态栏始终可见(直到会话结束),显示:当前 pipe 名、角色、IP、已选数/总数、路由模式。
### 展开选择面板
**Shift+↓**Shift + 下箭头)展开选择面板:
```
pipe: cli-a91bad56 (main) 192.168.50.22 ↑↓ move Space select ←/→ or m route Enter/Esc close Shift+↓ toggle
当前普通 prompt 走 已选 sub切换不会清空选择
☑ cli-da029538 (sub-1 XC/192.168.50.22)
☐ cli-04d67950 (main vmwin11/192.168.50.27)
☑ cli-893747d3 [offline] (sub-2 vmwin11/192.168.50.27)
```
### 面板内快捷键
| 快捷键 | 场景 | 作用 |
|--------|------|------|
| **Shift+↓** | 状态栏可见时 | 展开/收起选择面板 |
| **↑ / ↓** | 面板展开时 | 上下移动光标 |
| **Space** | 面板展开时 | 切换当前光标所在 pipe 的选中状态(☑ ↔ ☐) |
| **Enter** | 面板展开时 | 确认并关闭面板 |
| **Esc** | 面板展开时 | 取消并关闭面板 |
| **← / → 或 M** | 状态栏可见且有选中 pipe 时 | 切换路由模式(`selected pipes only``local main` |
### M 键 — 路由模式切换
M 键(或 ← / →)用于在两种路由模式之间切换,**无需展开面板**
| 模式 | 状态栏显示 | 行为 |
|------|-----------|------|
| `selected pipes only` | 绿色高亮 | 输入的 prompt **仅**发送到选中的 pipe本地不执行 |
| `local main` | 灰色 | 输入的 prompt 在**本地 main** 执行,不转发到任何 pipe |
切换路由模式**不会清空选择**。你可以在 `local main` 模式下保持选择,随时按 M 切回 `selected pipes only` 继续向远端发送。
### 完整操作流程示例
```
1. 输入 /pipes → 状态栏出现,显示发现的实例
2. 按 Shift+↓ → 展开选择面板
3. 按 ↓ 移动到目标 pipe → 光标移到 cli-04d67950
4. 按 Space → 选中 ☑ cli-04d67950
5. 按 Enter → 确认,面板收起
6. 输入 "帮我检查 git status" → prompt 自动发送到 cli-04d67950 执行
7. 按 M → 切换到 local main 模式
8. 输入 "本地做点什么" → 仅在本地执行
9. 按 M → 切回 selected pipes only
10. 输入 "继续远端任务" → 又发送到 cli-04d67950
```
## 命令参考
### /pipes
显示所有发现的实例,管理选择状态。再次执行 `/pipes` 切换面板展开/收起。
```
/pipes — 显示所有实例 + 切换选择面板
/pipes select <name> — 选中某实例(消息会广播到它)
/pipes deselect <name> — 取消选中
/pipes all — 全选
/pipes none — 全部取消
```
输出示例:
```
Your pipe: cli-a91bad56
Role: main
Machine ID: 205d6c3a...
IP: 192.168.50.22
Host: XC
Main machine: 205d6c3a... (this machine)
[main] cli-a91bad56 XC/192.168.50.22 [alive] (you)
☑ [sub-1] cli-da029538 XC/192.168.50.22 [alive] [connected]
LAN Peers:
☐ [main] cli-04d67950 vmwin11/192.168.50.27 tcp:192.168.50.27:58853 [LAN]
Selected: cli-da029538
```
### /attach <name>
手动 attach 到一个实例,使其成为你的 slave。
```
/attach cli-04d67950 — 连接到指定 pipe自动解析 LAN TCP 端点)
```
attach 后,对方变为 slave你变为 master。可以向它发送 prompt。通常不需要手动 attach——heartbeat 会自动发现并连接。
### /detach <name>
断开与某个 slave 的连接。
```
/detach cli-04d67950
```
### /send <name> <message>
向指定 pipe 发送消息(不依赖选择状态,直接指定目标)。
```
/send cli-04d67950 请帮我检查一下日志
/send tcp:192.168.50.27:58853 hello — 直接通过 TCP 地址发送
```
### /claim-main
强制声明当前机器为 main用于 main 意外退出后的恢复)。
## 消息路由
### 选中 pipe 后的自动路由
1. 通过 `/pipes select` 或 Shift+Down 面板选中一个或多个 pipe
2. 在输入框中正常输入消息
3. 消息自动发送到所有选中的已连接 pipe
4. 每个 pipe 独立执行,结果流式回传到 main 的消息列表
### 路由模式
| 模式 | 行为 |
|------|------|
| `selected`(默认) | 消息发送到选中的 pipe |
| `local` | 消息仅在本地执行,不转发 |
## 架构
### 通信协议
所有通讯使用 NDJSONNewline-Delimited JSON每行一个消息
```json
{"type":"ping","from":"cli-abc","ts":"2026-04-11T00:00:00.000Z"}
{"type":"prompt","data":"帮我查看 git status","from":"cli-abc","ts":"..."}
{"type":"stream","data":"正在执行...","from":"cli-def","ts":"..."}
{"type":"done","data":"","from":"cli-def","ts":"..."}
```
### 消息类型
| 类型 | 方向 | 说明 |
|------|------|------|
| `ping`/`pong` | 双向 | 健康检查 |
| `attach_request`/`accept`/`reject` | M→S/S→M | 连接控制 |
| `detach` | M→S | 断开连接 |
| `prompt` | M→S | 主向从发送 prompt |
| `prompt_ack` | S→M | 从确认接收 |
| `stream` | S→M | 从流式回传 AI 输出 |
| `tool_start`/`tool_result` | S→M | 工具执行通知 |
| `done` | S→M | 本轮完成 |
| `error` | 双向 | 错误通知 |
| `permission_request`/`response`/`cancel` | 双向 | 权限审批转发 |
### 传输层
```
本机 LAN
┌──────────────┐ ┌──────────────┐
│ PipeServer │ │ PipeServer │
│ UDS sock │ │ UDS sock │
│ TCP :rand │◄───TCP───►│ TCP :rand │
├──────────────┤ ├──────────────┤
│ LanBeacon │◄──UDP────►│ LanBeacon │
│ 224.0.71.67 │ mcast │ 224.0.71.67 │
└──────────────┘ └──────────────┘
```
- **UDS**:本机实例间通讯,通过文件系统路径寻址(`~/.claude/pipes/cli-xxx.sock`
- **TCP**LAN 实例间通讯,动态端口,通过 beacon 发现
- **UDP Multicast**peer 发现3 秒广播一次 announce 包
### 角色模型
| 角色 | 说明 |
|------|------|
| `main` | 首个启动的实例,管理 registry |
| `sub` | 后续启动的同机实例(或被 attach 的 LAN 实例) |
| `master` | attach 了至少一个 slave 的实例 |
| `slave` | 被 master attach 控制的实例 |
角色转换:
- 首个启动 → `main`
- 同机后续启动 → `sub`(自动被 main attach → `slave`
- LAN 发现 → 两边都是 `main`heartbeat 自动互相 attach
- 被 attach → 变为 `slave`(可通过 `/detach` 恢复)
### 发现机制
**本机**:通过 `~/.claude/pipes/registry.json` 文件(带文件锁),`machineId` 绑定主机身份。
**LAN**:通过 UDP multicast beacon
1. 每 3 秒广播 `{ proto, pipeName, machineId, ip, tcpPort, role }`
2. 收到其他实例的 announce → 记入 peers Map
3. 15 秒未收到 → 标记 peer lost
4. Heartbeat 合并 local registry + beacon peers → 统一 attach 目标列表
### Heartbeat 循环5 秒间隔)
```
main/master 角色:
1. cleanupStaleEntries() — 清理 registry 中死掉的条目
2. getAliveSubs() — 获取存活的本地 subs
3. refreshDiscoveredPipes() — 刷新 discoveredPipes包含 LAN peers
4. 合并 LAN peers 到 state
5. 构建统一 attach 目标列表 — 本地 subs + LAN peers
6. 遍历未连接的目标 → 自动 attach
7. 清理断开的 slave 连接 — 同时检查 local registry 和 beacon
sub 角色:
1. 检测 main 是否存活
2. main 死亡 → 同机则接管 main 角色,跨机则独立
```
## 关键文件
| 文件 | 职责 |
|------|------|
| `src/utils/pipeTransport.ts` | PipeServer双模 UDS+TCP、PipeClient、类型定义 |
| `src/utils/lanBeacon.ts` | UDP multicast beacon、singleton 管理 |
| `src/utils/pipeRegistry.ts` | Registry CRUD、角色判定、machineId、LAN merge |
| `src/utils/peerAddress.ts` | 地址解析uds:/bridge:/tcp: scheme |
| `src/screens/REPL.tsx` | Bootstrap、heartbeat、cleanup、prompt 路由 |
| `src/hooks/useMasterMonitor.ts` | Slave client registry、消息订阅 |
| `src/hooks/useSlaveNotifications.ts` | Slave 端通知处理 |
| `src/commands/pipes/pipes.ts` | /pipes 命令 |
| `src/commands/attach/attach.ts` | /attach 命令 |
| `src/commands/send/send.ts` | /send 命令 |
| `src/tools/SendMessageTool/SendMessageTool.ts` | AI 发消息工具(含 tcp: 支持) |
## 后续优化方向
### 安全P0
1. **TCP 认证**:首次连接时交换 HMAC-SHA256 token基于 machineId + session secret防止未授权设备连接
2. **JSON schema 验证**:在所有 `JSON.parse` 入口点增加 Zod 校验,防止 prototype pollution
3. **Beacon 信息脱敏**hash machineId 后再广播,不暴露硬件序列号
### 可靠性P1
4. **多网卡选择**`getLocalIp()` 应优先选择 RFC 1918 地址,排除 VPN/Docker 接口
5. **TCP target 验证**`parseTcpTarget()` 应限制目标为已知 beacon peers 或 RFC 1918 范围
6. **PipeServer close()**:改为 `Promise.allSettled` 并行关闭 UDS + TCP`_closing` guard
### 功能P2
7. **mDNS/DNS-SD**:作为 multicast 受限环境下的 beacon 替代方案
8. **固定端口配置**:允许用户指定 TCP 端口范围,便于防火墙精确配置
9. **TLS 加密**TCP 传输加密,防中间人窃听
10. **双向 prompt**:当前只有 master → slave 方向,可考虑 slave 主动向 master 发送结果/请求

View File

@@ -1,7 +1,7 @@
# PROACTIVE — 主动模式
> Feature Flag: `FEATURE_PROACTIVE=1`(与 `FEATURE_KAIROS=1` 共享功能)
> 实现状态:核心模块全部 Stub布线完整
> 实现状态:核心循环与 SleepTool 已落地,部分外围文档仍在补齐
> 引用数37
## 一、功能概述
@@ -21,13 +21,13 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
| 模块 | 文件 | 状态 | 说明 |
|------|------|------|------|
| 核心逻辑 | `src/proactive/index.ts` | **Stub** | `activateProactive()``deactivateProactive()``isProactiveActive() => false` |
| 核心逻辑 | `src/proactive/index.ts` | **已实现** | `activateProactive()``deactivateProactive()``pause/resume``nextTickAt` 调度状态 |
| SleepTool 提示 | `src/tools/SleepTool/prompt.ts` | **完整** | 工具提示定义(工具名:`Sleep` |
| 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` |
| 工具注册 | `src/tools.ts:26-28` | **布线** | SleepTool 动态加载 |
| REPL 集成 | `src/screens/REPL.tsx` | **布线** | tick 驱动逻辑、占位符、页脚 UI |
| REPL 集成 | `src/screens/REPL.tsx` | **已实现** | tick 驱动、standby/sleeping 状态、页脚与 bridge automation metadata 上报 |
| 系统提示 | `src/constants/prompts.ts:860-914` | **完整** | 自主工作行为指令(~55 行详细 prompt |
| 会话存储 | `src/utils/sessionStorage.ts:4892-4912` | **布线** | tick 消息注入对话流 |
| 远控状态镜像 | `src/utils/sessionState.ts` | **已实现** | 向 remote-control/CCR 暴露 `automation_state` 元数据 |
### 2.2 系统提示内容
@@ -46,7 +46,7 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
### 2.3 数据流
```
activateProactive() [需要实现]
activateProactive()
Tick 调度器启动
@@ -62,20 +62,22 @@ Tick 调度器启动
└── 无事可做 → 必须调用 SleepTool
SleepTool 等待 [需要实现]
SleepTool 等待
├── 用户插入新工作 / 队列中有命令 → 立即唤醒
├── proactive 被关闭 → 立即中断
└── 进入休眠时向远端 surfaces 上报 `automation_state = sleeping`
下一个 tick 到达
```
## 三、需要补全的内容
## 三、当前行为补充
| 优先级 | 模块 | 工作量 | 说明 |
|--------|------|--------|------|
| 1 | `src/proactive/index.ts` | 中 | Tick 调度器、activate/deactivate 状态机、pause/resume |
| 2 | `src/tools/SleepTool/SleepTool.ts` | 小 | 工具执行(等待指定时间后触发 tick |
| 3 | `src/commands/proactive.js` | 小 | `/proactive` 斜杠命令处理器 |
| 4 | `src/hooks/useProactive.ts` | 中 | React hookREPL 引用但不存在) |
- `standby`proactive 已开启,当前没有执行中的 turn且已调度下一个 tick。
- `sleeping`:模型显式调用 `SleepTool` 进入等待窗口。
- remote-control/CCR 通过 `external_metadata.automation_state` 接收这两个状态,用于 Web UI 的 Autopilot 状态显示。
- `SleepTool` 现在不是纯定时器;它会在共享命令队列出现新工作时提前醒来。
## 四、关键设计决策
@@ -101,9 +103,11 @@ FEATURE_PROACTIVE=1 FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 bun run dev
| 文件 | 职责 |
|------|------|
| `src/proactive/index.ts` | 核心逻辑stub |
| `src/proactive/index.ts` | 核心逻辑与 next-tick 状态 |
| `src/tools/SleepTool/prompt.ts` | SleepTool 工具提示 |
| `src/tools/SleepTool/SleepTool.ts` | 休眠/唤醒执行逻辑 |
| `src/constants/prompts.ts:860-914` | 自主工作系统提示 |
| `src/screens/REPL.tsx` | REPL tick 集成 |
| `src/screens/REPL.tsx` | REPL tick 集成与 automation 状态上报 |
| `src/utils/sessionStorage.ts:4892-4912` | Tick 消息注入 |
| `src/utils/sessionState.ts` | bridge/CCR metadata 镜像 |
| `src/components/PromptInput/PromptInputFooterLeftSide.tsx` | 页脚 UI 状态 |

View File

@@ -0,0 +1,293 @@
# Remote Control Server 私有化部署指南
本指南说明如何将 Remote Control Server (RCS) 部署到私有环境,并通过 Claude Code CLI 连接使用。
## 架构概览
```
┌──────────────────┐ ┌──────────────────────┐
│ Claude Code CLI │ ◄── HTTP/SSE/WS ─►│ Remote Control │
│ (Bridge Worker) │ 长轮询 + 心跳 │ Server (RCS) │
└──────────────────┘ │ │
│ ┌──────────────┐ │
┌──────────────────┐ HTTP/SSE │ │ In-Memory │ │
│ Web UI 控制面板 │ ◄─────────────── │ │ Store │ │
│ (/code/*) │ │ └──────────────┘ │
└──────────────────┘ │ ┌──────────────┐ │
│ │ JWT Auth │ │
│ └──────────────┘ │
└──────────────────────┘
```
**RCS 是一个纯内存的中间服务**,它的职责是:
- 接收 Claude Code CLI 的环境注册和工作轮询
- 提供 Web UI 供操作者远程监控和审批
- 通过 WebSocket/SSE 双向传输消息
- 管理会话、环境、权限请求
## 前置条件
- 一台可被 Claude Code CLI 和 Web 浏览器同时访问的服务器物理机、VM、容器均可
- [Docker](https://www.docker.com/)
- 启用 `BRIDGE_MODE` feature flag 的 Claude Code 构建
## 部署
### 构建 Docker 镜像
在项目根目录执行:
```bash
docker build -t rcs:latest -f packages/remote-control-server/Dockerfile .
```
### 启动容器
```bash
docker run -d \
--name rcs \
-p 3000:3000 \
-e RCS_API_KEYS=sk-rcs-your-secret-key-here \
-e RCS_BASE_URL=https://rcs.example.com \
-v rcs-data:/app/data \
--restart unless-stopped \
rcs:latest
```
### Docker Compose
```yaml
version: "3.8"
services:
rcs:
build:
context: .
dockerfile: packages/remote-control-server/Dockerfile
args:
VERSION: "0.1.0"
ports:
- "3000:3000"
environment:
- RCS_API_KEYS=sk-rcs-your-secret-key-here
- RCS_BASE_URL=https://rcs.example.com
volumes:
- rcs-data:/app/data
restart: unless-stopped
volumes:
rcs-data:
```
启动:
```bash
docker compose up -d
```
## 环境变量参考
### 服务器端
| 变量 | 必填 | 默认值 | 说明 |
|------|------|--------|------|
| `RCS_API_KEYS` | **是** | _(空)_ | API 密钥列表,逗号分隔。用于客户端认证和 JWT 签名。**务必设置强密钥** |
| `RCS_PORT` | 否 | `3000` | 服务监听端口 |
| `RCS_HOST` | 否 | `0.0.0.0` | 服务监听地址 |
| `RCS_BASE_URL` | 否 | `http://localhost:3000` | 外部访问 URL。用于生成 WebSocket 连接地址,必须与客户端实际访问的地址一致 |
| `RCS_VERSION` | 否 | `0.1.0` | 版本号,显示在 `/health` 响应中 |
| `RCS_POLL_TIMEOUT` | 否 | `8` | V1 工作轮询超时(秒) |
| `RCS_HEARTBEAT_INTERVAL` | 否 | `20` | 心跳间隔(秒) |
| `RCS_JWT_EXPIRES_IN` | 否 | `3600` | JWT 令牌有效期(秒) |
| `RCS_DISCONNECT_TIMEOUT` | 否 | `300` | 断线判定超时(秒) |
### 客户端Claude Code CLI
| 变量 | 必填 | 说明 |
|------|------|------|
| `CLAUDE_BRIDGE_BASE_URL` | **是** | RCS 服务器地址,例如 `https://rcs.example.com`。设置此变量即启用自托管模式,跳过 GrowthBook 门控 |
| `CLAUDE_BRIDGE_OAUTH_TOKEN` | **是** | 认证令牌,必须与服务器端 `RCS_API_KEYS` 中的某个值匹配 |
| `CLAUDE_BRIDGE_SESSION_INGRESS_URL` | 否 | WebSocket 入口地址(默认与 `CLAUDE_BRIDGE_BASE_URL` 相同) |
| `CLAUDE_CODE_REMOTE` | 否 | 设为 `1` 时标记为远程执行模式 |
## Claude Code 客户端连接
### 1. 设置环境变量
在运行 Claude Code 的机器上设置:
```bash
export CLAUDE_BRIDGE_BASE_URL="https://rcs.example.com"
export CLAUDE_BRIDGE_OAUTH_TOKEN="sk-rcs-your-secret-key-here"
```
### 2. 启动 Claude Code
```bash
# 使用 dev 模式BRIDGE_MODE 默认启用)
bun run dev
# 或使用构建产物
bun run dist/cli.js
```
### 3. 执行 /remote-control 命令
在 Claude Code 的 REPL 中输入:
```
/remote-control
```
环境型 Remote Control例如 `claude remote-control` 子命令)会向 RCS 注册环境,注册成功后在终端显示连接 URL
```
https://rcs.example.com/code?bridge=<environmentId>
```
交互式 REPL 方式(`--remote-control``/remote-control`)在某些桥接模式下也可能直接给出会话 URL
```
https://rcs.example.com/code/session_<id>
```
两种 URL 都可以直接在浏览器打开并远程操控当前会话;只有 environment 模式才会出现在 Web UI 的环境列表中。
若已连接,再次执行 `/remote-control` 会显示对话框,包含以下选项:
- **Disconnect this session** — 断开远程连接
- **Show QR code** — 显示/隐藏二维码
- **Continue** — 保持连接,继续使用
也可通过 CLI 参数直接启动:
```bash
claude remote-control
# 或简写
claude rc
# 或
claude bridge
```
## Web UI 控制面板
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。功能:
- 查看已注册的运行环境environment 模式)
- 创建和管理会话
- 实时查看对话消息和工具调用
- 查看 Autopilot 状态(`standby` / `sleeping`)和自动运行指示
- 查看 authoritative task snapshots 驱动的 Tasks 面板
- 审批 Claude Code 的工具权限请求
Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境。
## 工作流程详解
```
┌──────────────────────────────────────────────────────────┐
│ 完整工作流程 │
└──────────────────────────────────────────────────────────┘
1. Claude Code CLI 启动,设置环境变量指向自托管 RCS
2. 用户执行 /remote-control 命令
3. 注册环境
CLI ──POST /v1/environments/bridge──► RCS
CLI ◄── { environment_id, environment_secret } ── RCS
4. 终端显示连接 URL
https://rcs.example.com/code?bridge=<environmentId>
5. 开始工作轮询(循环)
CLI ──GET /v1/environments/:id/work/poll──► RCS
(长轮询,等待任务分配,超时 8 秒后重试)
6. 浏览器打开 URL → Web UI 创建任务
Browser ──POST /web/sessions──► RCS
RCS 分配 work 给正在轮询的 CLI
7. CLI 收到任务并确认
CLI ◄── { id, data: { type, sessionId } } ── RCS
CLI ──POST /v1/environments/:id/work/:workId/ack──► RCS
8. 建立会话连接
CLI ──WebSocket /v1/session_ingress──► RCS
(或使用 V2 的 SSE + HTTP POST
9. 双向通信
CLI ──消息/工具调用结果──► RCS ──► Browser
CLI ◄──权限审批/指令───── RCS ◄──── Browser
CLI ──automation_state / task_state──► RCS ──► Browser
10. 心跳保活(每 20 秒)
CLI ──POST /v1/environments/:id/work/:workId/heartbeat──► RCS
11. 任务完成 → 归档会话 → 注销环境
```
## 故障排查
### Web UI 看不到当前 Autopilot 状态
- `standby`proactive 已开启,正在等待下一个 tick
- `sleeping`:模型正在 `SleepTool` 等待窗口中
这两个状态通过 worker `external_metadata.automation_state` 上报。如果页面只显示普通 working spinner优先检查 CLI 和 RCS 之间的 worker metadata PUT 是否成功。
### CLI 无法连接
```
Error: Remote Control is not available in this build.
```
**原因**`BRIDGE_MODE` feature flag 未启用。
**解决**:使用 dev 模式(默认启用)或确保构建时包含 `BRIDGE_MODE` flag。
### 认证失败 (401)
```
Error: Unauthorized
```
**检查项**
1. `CLAUDE_BRIDGE_OAUTH_TOKEN` 是否与 `RCS_API_KEYS` 中的值匹配
2. API Key 是否包含多余的空格或换行
3. 两个环境变量是否都已正确设置
### WebSocket 连接中断
**检查项**
1. 如果使用反向代理,确认已正确配置 WebSocket 升级(`Upgrade` / `Connection` 头)
2. 代理的 `proxy_read_timeout` 是否足够大(建议 86400 秒)
3. 网络防火墙是否允许 WebSocket 流量
### 健康检查
```bash
curl https://rcs.example.com/health
# 预期: {"status":"ok","version":"0.1.0"}
```
## 限制与注意事项
| 项目 | 说明 |
|------|------|
| 存储 | 纯内存存储Map服务器重启后所有会话和环境数据丢失 |
| 扩展 | 不支持水平扩展(无共享状态),单实例部署 |
| 并发 | 适合中小规模使用,大量并发会话可能需要性能调优 |
| 数据持久化 | `/app/data` 卷已预留但当前未使用,未来可能用于持久化 |
| Web UI 认证 | 基于 UUID无用户账户系统适合受信任网络环境 |
## 与云端模式对比
| 特性 | 云端 (Anthropic CCR) | 自托管 (RCS) |
|------|---------------------|--------------|
| 认证方式 | claude.ai OAuth 订阅 | API Key |
| GrowthBook 门控 | 需要 `tengu_ccr_bridge` 通过 | 自动跳过 |
| 功能标志 | 需要 `BRIDGE_MODE=1` | 同样需要 |
| 部署位置 | Anthropic 云端 | 用户自有服务器 |
| 数据流经 | Anthropic 基础设施 | 用户私有网络 |
| 依赖 | claude.ai 订阅 + OAuth | 仅需 API Key |
自托管模式的核心优势是:设置 `CLAUDE_BRIDGE_BASE_URL` 后,代码自动调用 `isSelfHostedBridge()` 返回 `true`,跳过所有 GrowthBook 和订阅检查,无需 claude.ai 账户即可使用。

View File

@@ -0,0 +1,310 @@
# Stub 恢复设计 1-4
> 日期2026-04-12
> 目标:基于当前代码边界,为下一阶段 4 个 stub/半 stub 命令面给出可实施的设计方案。
> 排序原则:按建议实施顺序排序,不按问题严重性排序。
## 设计原则
- 先做能独立闭环、收益明确、改动边界清晰的项。
- 大项拆成 `MVP``Phase 2+`,避免一次性掉进大范围恢复。
- 优先复用已有状态、传输层、日志与配置能力,不重造协议。
- 设计以当前仓库实际代码为准,不以旧文档的理想状态为准。
## 1. `claude daemon status` / `claude daemon stop`
### 现状
- `start` 路径已有完整 supervisor + worker 生命周期:
[src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:1>)
[src/daemon/workerRegistry.ts](</e:/Source_code/Claude-code-bast/src/daemon/workerRegistry.ts:1>)
- `status` / `stop` 目前只是占位输出:
[src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:49>)
- `/remote-control-server` 有自己的命令内 UI 状态,但只维护当前进程内的 `daemonProcess`,并不适合作为跨进程 CLI 管理基础:
[src/commands/remoteControlServer/remoteControlServer.tsx](</e:/Source_code/Claude-code-bast/src/commands/remoteControlServer/remoteControlServer.tsx:32>)
### 目标
-`claude daemon status``claude daemon stop` 在另一个 CLI 进程中也能正确工作。
- 不依赖 TUI 内存态,不要求当前命令进程就是启动 daemon 的那个进程。
### MVP 方案
- 新增 daemon 状态文件,例如:
`~/.claude/daemon/remote-control.json`
- `start` 时写入:
- supervisor pid
- cwd
- startedAt
- worker kinds
- 最近状态
- `status`
- 读取状态文件
- 用现有进程探测能力验证 pid 是否存活
- 输出 `running / stopped / stale`
- stale 时自动清理状态文件
- `stop`
- 读取 pid
- 发送 `SIGTERM`
- 等待退出
- 超时后 `SIGKILL`
- 清理状态文件
### 代码范围
- 新增 `src/daemon/state.ts`
- 修改 [src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:1>)
- 轻量修改 [src/commands/remoteControlServer/remoteControlServer.tsx](</e:/Source_code/Claude-code-bast/src/commands/remoteControlServer/remoteControlServer.tsx:32>),让 UI 尽量读取同一份状态文件
### 验证
1. `claude daemon start`
2. 新开终端执行 `claude daemon status`
3. 执行 `claude daemon stop`
4. 再次执行 `claude daemon status`,确认返回 `stopped` 或清晰的 `stale cleaned`
### 风险
- Windows 信号模型和 Unix 不同,`stop` 需要超时兜底。
- 当前设计默认单 supervisor不处理多实例并发。
### 工作量判断
-
- 适合作为下一步的首选实现项
## 2. `BG_SESSIONS`
### 现状
- fast-path 已接好:
[src/entrypoints/cli.tsx](</e:/Source_code/Claude-code-bast/src/entrypoints/cli.tsx:218>)
- session registry 已有真实实现:
[src/utils/concurrentSessions.ts](</e:/Source_code/Claude-code-bast/src/utils/concurrentSessions.ts:1>)
- `exit` 在 bg session 内已会 `tmux detach-client`
[src/commands/exit/exit.tsx](</e:/Source_code/Claude-code-bast/src/commands/exit/exit.tsx:20>)
- 但 CLI handler 仍全空:
[src/cli/bg.ts](</e:/Source_code/Claude-code-bast/src/cli/bg.ts:1>)
- task summary 仍然是 stub
[src/utils/taskSummary.ts](</e:/Source_code/Claude-code-bast/src/utils/taskSummary.ts:1>)
### 目标
- 先把 `ps` / `logs` / `kill` 做成真正有用的 session 管理命令。
- 不在第一阶段就强行补完 `attach` / `--bg`
### Phase 2AMVP
- 实现 `ps`
- 从 registry 读取 live sessions
- 展示 pid、kind、sessionId、cwd、name、startedAt、bridgeSessionId
- 如果有 activity/status则一并展示
- 实现 `logs`
- 支持按 `sessionId / pid / name` 查找
- 优先复用本地 transcript/log 读取能力
- 如果 registry 里存在 `logPath`,支持 tail 文件
- 实现 `kill`
- 解析目标 session
- 发退出信号
- 清理 stale registry
### Phase 2B后续
- 实现 `attach`
- 实现 `--bg`
- 实现 `taskSummary` 的中途状态更新
### 为什么要拆
- 现有 registry 记录了 `pid / sessionId / name / logPath`
- 但没有可靠的 tmux attach target
- 所以 `attach``--bg` 不是简单补 handler而是需要补启动/附着元数据设计
### 代码范围
- 修改 [src/cli/bg.ts](</e:/Source_code/Claude-code-bast/src/cli/bg.ts:1>)
- 修改 [src/utils/concurrentSessions.ts](</e:/Source_code/Claude-code-bast/src/utils/concurrentSessions.ts:1>) 以便后续 attach/--bg 扩展
- 修改 [src/utils/taskSummary.ts](</e:/Source_code/Claude-code-bast/src/utils/taskSummary.ts:1>)
- 复用:
[src/utils/sessionStorage.ts](</e:/Source_code/Claude-code-bast/src/utils/sessionStorage.ts:3870>)
[src/utils/udsClient.ts](</e:/Source_code/Claude-code-bast/src/utils/udsClient.ts:1>)
### 验证
1. `ps` 能列出 live sessions
2. `logs <sessionId|pid|name>` 能输出对应日志
3. `kill <sessionId|pid|name>` 能结束目标 session
### 风险
- `attach` / `--bg` 第二阶段需要 tmux 元数据设计
- Windows 下 tmux 路径需要明确降级策略
### 工作量判断
- `ps/logs/kill` 中等
- `attach/--bg` 明显更大,应分阶段
## 3. `TEMPLATES`
### 现状
- 命令入口只有 fast-path
[src/entrypoints/cli.tsx](</e:/Source_code/Claude-code-bast/src/entrypoints/cli.tsx:249>)
- handler 是空的:
[src/cli/handlers/templateJobs.ts](</e:/Source_code/Claude-code-bast/src/cli/handlers/templateJobs.ts:1>)
- `markdownConfigLoader` 已把 `templates` 纳入配置目录:
[src/utils/markdownConfigLoader.ts](</e:/Source_code/Claude-code-bast/src/utils/markdownConfigLoader.ts:29>)
- `query / stopHooks` 已预留 job classifier 链路:
[src/query/stopHooks.ts](</e:/Source_code/Claude-code-bast/src/query/stopHooks.ts:103>)
- `jobs/classifier.ts` 仍是 stub
[src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
### 目标
-`new / list / reply` 做成可用的模板任务系统。
- 第一阶段不碰复杂的自动分类与自动执行。
### MVP 方案
- 模板来源:
`.claude/templates/*.md`
- 模板格式:
复用现有 markdown + frontmatter 解析,不另外设计 DSL
- `list`
- 列出所有模板
- 显示模板名、description、路径
- `new <template> [args...]`
- 解析模板
-`~/.claude/jobs/<job-id>/` 下创建 job 目录
- 写入 `template.md``input.txt``state.json`
- 返回 job id 与目录
- `reply <job-id> <text>`
- 将回复写入 `replies.jsonl``input.txt`
- 更新 `state.json`
### Phase 2
- 恢复 [src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
- 让带 `CLAUDE_JOB_DIR` 的 job session 在 turn 完成后自动更新 `state.json`
- 再决定是否补自动 job runner
### 为什么要拆
- 当前证据表明这是“template job commands”不是单纯模板列表
- 但自动 job 运行链路没有足够现成实现,先做文件系统 job lifecycle 更稳
### 代码范围
- 修改 [src/cli/handlers/templateJobs.ts](</e:/Source_code/Claude-code-bast/src/cli/handlers/templateJobs.ts:1>)
- 新增 `src/jobs/state.ts`
- 新增 `src/jobs/templates.ts`
- Phase 2 再改 [src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
### 验证
1. `list` 能列出 `.claude/templates`
2. `new` 能创建 job 目录和状态文件
3. `reply` 能更新 job 内容和状态
4. Phase 2 再验证 classifier 写状态
### 风险
- frontmatter schema 需要先定义最小字段集
- 一旦扩展到“自动运行 job”范围会明显膨胀
### 工作量判断
- MVP 中等
- 完整 job 系统偏大
## 4. `assistant [sessionId]`
### 现状
- attach 主流程其实已经存在:
[src/main.tsx](</e:/Source_code/Claude-code-bast/src/main.tsx:4708>)
- 远端 viewer 所需基础模块已存在:
[src/remote/RemoteSessionManager.ts](</e:/Source_code/Claude-code-bast/src/remote/RemoteSessionManager.ts:1>)
[src/hooks/useAssistantHistory.ts](</e:/Source_code/Claude-code-bast/src/hooks/useAssistantHistory.ts:1>)
[src/assistant/sessionHistory.ts](</e:/Source_code/Claude-code-bast/src/assistant/sessionHistory.ts:1>)
- 真正 stub 的主要是:
[src/assistant/sessionDiscovery.ts](</e:/Source_code/Claude-code-bast/src/assistant/sessionDiscovery.ts:1>)
[src/assistant/AssistantSessionChooser.ts](</e:/Source_code/Claude-code-bast/src/assistant/AssistantSessionChooser.ts:1>)
[src/commands/assistant/assistant.ts](</e:/Source_code/Claude-code-bast/src/commands/assistant/assistant.ts:7>)
[src/assistant/index.ts](</e:/Source_code/Claude-code-bast/src/assistant/index.ts:1>)
### 目标
- 不一次性恢复整个 KAIROS 助手系统。
- 先做“明确 sessionId 的 viewer attach 可用”,再逐步补 discovery / chooser / install。
### Phase 4AMVP
- 只支持 `claude assistant <sessionId>`
-`claude assistant` 无参数模式,先返回明确提示:
- 当前版本需要显式 `sessionId`
- discovery 尚未启用
- 这样可以直接复用现有 attach 分支,不必先恢复 chooser/install wizard
### Phase 4B
- 恢复 `discoverAssistantSessions()`
- 数据来源优先复用现有 sessions / bridge / teleport API而不是新协议
-`claude assistant` 无参数时能拿到候选 session 列表
### Phase 4C
- 恢复 `AssistantSessionChooser`
- 多 session 时可交互选择
### Phase 4D
- 最后考虑 install wizard 辅助函数
- 这部分属于“没有 session 时如何引导”,不是 attach 核心路径
### 为什么要拆
- attach 渲染层与远端消息通道大部分已经在
- 真正缺的是“如何发现目标 session”和“如何交互选择”
- 如果把 `src/assistant/index.ts` 的整套 KAIROS 正常模式也一起拉进来,范围会失控
### 代码范围
- Phase 4A
- [src/main.tsx](</e:/Source_code/Claude-code-bast/src/main.tsx:4708>)
- [src/commands/assistant/index.ts](</e:/Source_code/Claude-code-bast/src/commands/assistant/index.ts:1>)
- Phase 4B
- [src/assistant/sessionDiscovery.ts](</e:/Source_code/Claude-code-bast/src/assistant/sessionDiscovery.ts:1>)
- Phase 4C
- [src/assistant/AssistantSessionChooser.ts](</e:/Source_code/Claude-code-bast/src/assistant/AssistantSessionChooser.ts:1>)
- Phase 4D
- [src/commands/assistant/assistant.ts](</e:/Source_code/Claude-code-bast/src/commands/assistant/assistant.ts:7>)
### 验证
1. `claude assistant <sessionId>` 能进入 remote viewer
2. 历史懒加载工作正常
3. 无参数模式先给出明确提示
4. 后续阶段再分别验证 discovery / chooser / install
### 风险
- 这是四项里范围最大的
- 一旦把 KAIROS 正常模式整体拉入会从“viewer attach”膨胀成“完整 assistant mode 恢复”
### 工作量判断
- Phase 4A 中等
- 4A-4D 全做完很大
## 建议执行顺序
1. `claude daemon status` / `claude daemon stop`
2. `BG_SESSIONS` 先做 `ps/logs/kill`
3. `TEMPLATES` 先做 job 文件系统 MVP
4. `assistant [sessionId]` 先做显式 sessionId attach再补 discovery/chooser/install
## 简短结论
这四项里,最适合立刻实现的是 `daemon status/stop``BG_SESSIONS``TEMPLATES` 适合按 MVP 先补 handler 与文件系统闭环。`assistant [sessionId]` 不能整块硬上应该按“attach → discovery → chooser → install”拆开恢复。

View File

@@ -8,7 +8,6 @@
| Feature | 引用 | 状态 | 类别 | 简要说明 |
|---------|------|------|------|---------|
| CHICAGO_MCP | 16 | N/A | 内部基础设施 | Anthropic 内部 MCP 基础设施,非外部可用 |
| UDS_INBOX | 17 | Stub | 消息通信 | Unix 域套接字对等消息,进程间消息传递 |
| MONITOR_TOOL | 13 | Stub | 工具 | 文件/进程监控工具,检测变更并通知 |
| BG_SESSIONS | 11 | Stub | 会话管理 | 后台会话管理,支持多会话并行 |
| SHOT_STATS | 10 | 无实现 | 统计 | 逐 prompt 统计信息收集 |
@@ -68,7 +67,7 @@ BUILDING_CLAUDE_APPS, ANTI_DISTILLATION_CC, AGENT_TRIGGERS, ABLATION_BASELINE
这些 feature 被列为 Tier 3 的原因:
1. **内部基础设施**CHICAGO_MCP, LODESTONEAnthropic 内部使用,外部无法运行
2. **纯 Stub 且引用低**UDS_INBOX, MONITOR_TOOL, BG_SESSIONS需要大量工作才能实现
2. **纯 Stub 且引用低**MONITOR_TOOL, BG_SESSIONS需要大量工作才能实现
3. **实验性功能**SHOT_STATS, EXTRACT_MEMORIES尚在概念阶段
4. **辅助功能**STREAMLINED_OUTPUT, HOOK_PROMPTS影响范围小
5. **CCR 系列**:依赖远程控制基础设施,需要 BRIDGE_MODE 先完善

114
docs/features/uds-inbox.md Normal file
View File

@@ -0,0 +1,114 @@
# UDS_INBOX / pipes
## 概述
`UDS_INBOX` 现在不是一个“空壳 flag”而是一套已经落地的本机 IPC 能力。但它同时承载了两层不同目标,必须拆开理解:
1. **UDS peer messaging**
- 面向任意 Claude Code 进程。
- 使用 `src/utils/udsMessaging.ts``src/utils/udsClient.ts`
- 对外入口是 `/peers``SendMessageTool``uds:<socket-path>` 地址。
2. **pipes control plane**
- 面向交互式 REPL 会话之间的主从协作。
- 使用 `src/utils/pipeTransport.ts``src/utils/pipeRegistry.ts``src/screens/REPL.tsx` 中的内联 bootstrap。
- 对外入口是 `/pipes``/attach``/detach``/send``/pipe-status``/history``/claim-main`
这两层都依赖本机 socket但职责不同。`/peers` 解决“找到其他会话并发消息”,`/pipes` 解决“把一个 REPL 变成另一个 REPL 的受控 worker”。
## 为什么要有单独的 `pipes`
单独的 `pipes` 层有三个实际理由:
1. **命名与角色模型不同**
- UDS peer 层按 `messagingSocketPath` 寻址。
- pipes 层按 `cli-xxxxxxxx` 会话名、`main/sub/master/slave` 角色和 `machineId` 注册表工作。
2. **交互语义不同**
- peer 层是通用消息投递。
- pipes 层需要 attach、detach、历史收集、选择性广播、状态栏和 REPL 快捷键。
3. **UI 集成不同**
- peer 层主要服务工具调用。
- pipes 层直接影响 REPL 提交路径和 PromptInput 页脚。
如果把两者硬合并,`SendMessageTool` 的通用寻址和 REPL 的主从控制会互相污染,命令语义也会变得混乱。
## 当前通信模型
### 1. UDS peer messaging
- 服务端:`src/utils/udsMessaging.ts`
- 客户端:`src/utils/udsClient.ts`
- 发现方式:读取 `~/.claude/sessions/*.json`
- 地址方式:`uds:<socket-path>`
- 传输方式:**本机 Unix socket / Windows named pipe**
这层是真正的“通用收件箱”。
### 2. pipes control plane
- 服务端/客户端:`src/utils/pipeTransport.ts`
- 注册表:`src/utils/pipeRegistry.ts`
- 生效入口:`src/screens/REPL.tsx`
- 发现方式:扫描 `~/.claude/pipes/` + `registry.json`
- 会话名:`cli-${sessionId.slice(0, 8)}`
- 传输方式:**本机 Unix socket / Windows named pipe**
这层是真正的“主从 REPL 协调平面”。
## 关于“局域网通信”的事实
当前实现**不是**真正的局域网传输。
代码里虽然保存了这些字段:
- `localIp`
- `hostname`
- `machineId`
- `mac`
但这些字段当前只用于:
1. 注册表展示
2. main/sub 身份判定
3. `claim-main` 的机器级归属切换
4. 状态输出与排障信息
它们**没有**被用于创建 TCP/WebSocket 连接。真正的传输仍然是 `getPipePath(name)` 返回的本机 socket 路径。
所以目前更准确的描述应该是:
- `pipes` 支持 **本机多实例协作**
- `registry` 带有 **机器身份元数据**
-**尚未实现跨机器局域网 transport**
如果未来要做真局域网版本,至少还需要:
1. TCP/WebSocket transport
2. 认证与会话授权
3. 发现与地址交换
4. 超时、重连和安全边界
## 当前 REPL 行为
当前线上行为由 `src/screens/REPL.tsx` 的内联实现负责:
1. 启动时创建当前 REPL 的 pipe server
2. 通过 `pipeRegistry` 判定 `main` / `sub`
3. 处理 `attach_request` / `detach` / `prompt`
4. 主实例心跳探测并维护 `slaves`
5. `/pipes` 打开状态栏并维护选择器
6. 提交普通消息时,仅向**已连接**的 selected pipes 广播
最近的收敛点:
- 过去遗留了一套未接线的 hook 方案
- 当前已明确以 `REPL.tsx` 内联 bootstrap 为唯一生效实现
- 选中但未连接的 pipe 不再导致本地处理被错误跳过
## 文档与代码对齐约定
后续关于 `UDS_INBOX` / `pipes` 的说明应遵守以下表述:
1. 默认称为“本机 IPC / 本机多实例协作”
2. 不把 `localIp` / `hostname` 元数据表述成已完成的 LAN transport
3. 明确区分 `/peers``/pipes` 的两层职责
4.`src/screens/REPL.tsx``src/utils/pipeTransport.ts``src/utils/pipeRegistry.ts` 为事实来源

View File

@@ -1,11 +1,11 @@
# WEB_SEARCH_TOOL — 网页搜索工具
> 实现状态:适配器架构完成,Bing 适配器为当前默认后端
> 实现状态:适配器架构完成,支持 API / Bing / Brave 三种后端
> 引用数:核心工具,无 feature flag 门控(始终启用)
## 一、功能概述
WebSearchTool 让模型可以搜索互联网获取最新信息。原始实现仅支持 Anthropic API 服务端搜索(`web_search_20250305` server tool在第三方代理端点下不可用。现已重构为适配器架构新增 Bing 搜索页面解析作为 fallback,确保任何 API 端点都能使用搜索功能。
WebSearchTool 让模型可以搜索互联网获取最新信息。原始实现仅支持 Anthropic API 服务端搜索(`web_search_20250305` server tool在第三方代理端点下不可用。现已重构为适配器架构支持 API 服务端搜索,以及 Bing / Brave 两个 HTML 解析后端,确保任何 API 端点都能使用搜索功能。
## 二、实现架构
@@ -21,9 +21,13 @@ WebSearchTool.call()
│ └── 使用 web_search_20250305 server tool
│ 通过 queryModelWithStreaming 二次调用 API
── BingSearchAdapter — Bing HTML 抓取 + 正则提取(当前默认)
└── 直接抓取 Bing 搜索页 HTML
正则提取 b_algo 块中的标题/URL/摘要
── BingSearchAdapter — Bing HTML 抓取 + 正则提取
└── 直接抓取 Bing 搜索页 HTML
正则提取 b_algo 块中的标题/URL/摘要
└── BraveSearchAdapter — Brave LLM Context API
└── 调用 Brave HTTPS GET 接口
将 grounding payload 映射为标题/URL/摘要
```
### 2.2 模块结构
@@ -37,8 +41,9 @@ WebSearchTool.call()
| 适配器工厂 | `src/tools/WebSearchTool/adapters/index.ts` | `createAdapter()` 工厂函数,选择后端 |
| API 适配器 | `src/tools/WebSearchTool/adapters/apiAdapter.ts` | 封装原有 `queryModelWithStreaming` 逻辑,使用 server tool |
| Bing 适配器 | `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 抓取 + 正则解析 |
| 单元测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 32 个测试用例 |
| 集成测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 真实网络请求验证 |
| Brave 适配器 | `src/tools/WebSearchTool/adapters/braveAdapter.ts` | Brave LLM Context API 适配与结果映射 |
| 单元测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts`, `src/tools/WebSearchTool/__tests__/braveAdapter*.test.ts`, `src/tools/WebSearchTool/__tests__/adapterFactory.test.ts` | Bing / Brave 解析与工厂逻辑测试 |
| 集成测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts`, `src/tools/WebSearchTool/__tests__/braveAdapter.integration.ts` | 真实网络请求验证 |
### 2.3 数据流
@@ -49,20 +54,18 @@ WebSearchTool.call()
validateInput() — 校验 query 非空、allowed/block 不共存
createAdapter() → BingSearchAdapter当前硬编码
createAdapter() → ApiSearchAdapter | BingSearchAdapter | BraveSearchAdapter
adapter.search(query, { allowedDomains, blockedDomains, signal, onProgress })
├── onProgress({ type: 'query_update', query })
├── axios.get(bing.com/search?q=...&setmkt=en-US)
│ └── 13 个 Edge 浏览器请求头
├── axios.get(search-engine-url)
│ └── API 鉴权请求头
├── extractBingResults(html) — 正则提取 <li class="b_algo"> 块
── resolveBingUrl() — 解码 base64 重定向 URL
│ ├── extractSnippet() — 三级降级摘要提取
│ └── decodeHtmlEntities() — he.decode
├── extractResults(payload) — 按后端提取结果
── grounding → SearchResult[] 映射
├── 客户端域名过滤 (allowedDomains / blockedDomains)
@@ -117,19 +120,18 @@ Bing 返回的重定向 URL 格式:`bing.com/ck/a?...&u=a1aHR0cHM6Ly9...`
## 四、适配器选择逻辑
当前 `createAdapter()` 硬编码返回 `BingSearchAdapter`,原逻辑已注释保留
`createAdapter()` 按以下优先级选择后端,并按选中的后端 key 缓存适配器实例
```typescript
export function createAdapter(): WebSearchAdapter {
return new BingSearchAdapter()
// 注释保留的选择逻辑:
// 1. WEB_SEARCH_ADAPTER 环境变量强制指定 api|bing
// 2. isFirstPartyAnthropicBaseUrl() → API 适配器
// 3. 第三方端点 → Bing 适配器
// 1. WEB_SEARCH_ADAPTER=api|bing|brave 显式指定
// 2. Anthropic 官方 API Base URL → ApiSearchAdapter
// 3. 第三方代理 / 非官方端点 → BingSearchAdapter
}
```
恢复自动选择:取消 `index.ts` 中的注释即可。
显式指定 `WEB_SEARCH_ADAPTER=brave` 时,会改用 Brave LLM Context API 后端,并要求
`BRAVE_SEARCH_API_KEY``BRAVE_API_KEY`
## 五、接口定义

View File

@@ -1,190 +0,0 @@
# OpenAI兼容模型中task工具使用指南
## 问题描述
当使用OpenAI兼容模型如DeepSeek、Ollama、vLLM等调用task工具TaskGet、TaskCreate、TaskUpdate、TaskList可能会出现以下错误
```
Error: InputValidationError: TaskGet failed due to the following issues:
The required parameter `taskId` is missing
An unexpected parameter `task_id` was provided
This tool's schema was not sent to the API — it was not in the discovered-tool set derived from message history. Without the schema in your prompt, typed parameters (arrays, numbers, booleans) get emitted as strings and the client-side parser rejects them. Load the tool first: call ToolSearch with query "select:TaskGet", then retry this call.
```
## 问题原因
### 1. 延迟加载工具Deferred Tools
task工具都是延迟加载的`shouldDefer: true`),这意味着:
- 工具的模式schema不会在初始API调用中发送
- 需要先通过`ToolSearch`工具发现
- 只有在被发现后工具模式才会被发送给API
### 2. 参数名转换问题
- task工具使用驼峰命名`taskId`
- OpenAI兼容模型可能输出蛇形命名`task_id`
- 当工具模式没有被发送时,模型会猜测参数名,可能导致不匹配
## 解决方案
### 方案1先使用ToolSearch推荐
在使用task工具之前先调用`ToolSearch`工具:
```javascript
// 第一步发现task工具
ToolSearch("select:TaskGet,TaskCreate,TaskUpdate,TaskList")
// 第二步正常使用task工具
TaskCreate({ subject: "任务标题", description: "任务描述" })
TaskGet({ taskId: "1" })
TaskUpdate({ taskId: "1", status: "completed" })
TaskList()
```
### 方案2批量发现所有task工具
```javascript
// 一次性发现所有task工具
ToolSearch("select:TaskGet,TaskCreate,TaskUpdate,TaskList")
// 然后可以任意使用task工具
const task = await TaskCreate({ subject: "新任务", description: "任务描述" })
console.log(`创建的任务ID: ${task.id}`)
const taskList = await TaskList()
console.log(`当前有 ${taskList.tasks.length} 个任务`)
```
### 方案3单独发现特定工具
```javascript
// 只发现需要的工具
ToolSearch("select:TaskGet")
// 然后使用该工具
TaskGet({ taskId: "1" })
```
## 参数名注意事项
在使用OpenAI兼容模型时请注意参数名格式
### ✅ 正确(驼峰命名)
```javascript
TaskGet({ taskId: "1" })
TaskCreate({ subject: "标题", description: "描述" })
TaskUpdate({ taskId: "1", status: "completed" })
```
### ❌ 错误(蛇形命名)
```javascript
TaskGet({ task_id: "1" }) // 错误应该使用taskId
TaskCreate({ subject: "标题", description: "描述" }) // 正确
TaskUpdate({ task_id: "1", status: "completed" }) // 错误应该使用taskId
```
## 常见问题解答
### Q1: 为什么需要先使用ToolSearch
A: task工具是延迟加载的它们的模式只有在被`ToolSearch`工具发现后才会发送给API。没有工具模式模型无法知道正确的参数名和类型。
### Q2: 每次会话都需要使用ToolSearch吗
A: 是的每次新的会话都需要先使用ToolSearch发现工具。工具发现状态不会在会话之间保留。
### Q3: 使用Anthropic官方模型也需要这样吗
A: 通常不需要。Anthropic官方模型对延迟加载工具的处理更智能但为了兼容性建议在使用task工具前都先使用ToolSearch。
### Q4: 可以一次性发现所有工具吗?
A: 可以,使用`ToolSearch("select:TaskGet,TaskCreate,TaskUpdate,TaskList")`可以一次性发现所有task工具。
### Q5: 如果忘记使用ToolSearch会怎样
A: 会收到参数验证错误提示需要先使用ToolSearch。按照错误信息的指导操作即可。
## 最佳实践
1. **会话开始时发现工具**在开始使用task工具前先调用ToolSearch
2. **批量发现**一次性发现所有需要的task工具
3. **检查参数名**:确保使用正确的驼峰命名参数
4. **查看错误信息**:如果遇到错误,仔细阅读错误信息中的指导
## 示例工作流
```javascript
// 1. 开始新会话
// 2. 发现task工具
ToolSearch("select:TaskGet,TaskCreate,TaskUpdate,TaskList")
// 3. 创建任务
const newTask = await TaskCreate({
subject: "修复OpenAI兼容性问题",
description: "解决task工具在OpenAI兼容模型下的参数名问题"
})
// 4. 获取任务详情
const taskDetails = await TaskGet({ taskId: newTask.id })
// 5. 更新任务状态
await TaskUpdate({
taskId: newTask.id,
status: "in_progress",
activeForm: "修复OpenAI兼容性问题"
})
// 6. 查看所有任务
const allTasks = await TaskList()
console.log(`当前有 ${allTasks.tasks.length} 个任务`)
// 7. 完成任务
await TaskUpdate({
taskId: newTask.id,
status: "completed"
})
```
## 故障排除
### 错误:参数名不匹配
**症状**`taskId`参数缺失,发现`task_id`参数
**解决**:确保使用驼峰命名的`taskId`,而不是蛇形命名的`task_id`
### 错误:工具模式未发送
**症状**`This tool's schema was not sent to the API`
**解决**:先使用`ToolSearch("select:工具名")`发现工具
### 错误:工具不可用
**症状**:工具调用失败,没有具体错误信息
**解决**:检查工具是否启用(通过`isTodoV2Enabled()`),确保环境变量设置正确
## 相关配置
### 环境变量
```bash
# 启用OpenAI兼容模式
export CLAUDE_CODE_USE_OPENAI=1
export OPENAI_API_KEY=your-api-key
export OPENAI_BASE_URL=https://api.deepseek.com
# 配置模型映射
export OPENAI_DEFAULT_SONNET_MODEL=deepseek-chat
export OPENAI_DEFAULT_OPUS_MODEL=deepseek-chat
export OPENAI_DEFAULT_HAIKU_MODEL=deepseek-chat
```
### 设置文件
通过`/login`命令配置OpenAI兼容模式后设置会保存在`~/.claude/settings.json`
```json
{
"modelType": "openai",
"openai": {
"baseURL": "https://api.deepseek.com",
"apiKey": "your-api-key",
"models": {
"haiku": "deepseek-chat",
"sonnet": "deepseek-chat",
"opus": "deepseek-chat"
}
}
}
```
## 总结
在使用OpenAI兼容模型时task工具需要先通过`ToolSearch`发现才能正常使用。遵循"先发现,后使用"的原则并注意参数名的正确格式驼峰命名可以确保task工具在OpenAI兼容模型下正常工作。

View File

@@ -1,425 +0,0 @@
# OpenAI 协议兼容层
## 概述
claude-code 支持通过 OpenAI Chat Completions API`/v1/chat/completions`)兼容任意 OpenAI 协议端点,包括 Ollama、DeepSeek、vLLM、One API、LiteLLM 等。
核心策略为**流适配器模式**:在 `queryModel()` 中插入提前返回分支,将 Anthropic 格式请求转为 OpenAI 格式,调用 OpenAI SDK再将 SSE 流转换回 `BetaRawMessageStreamEvent` 格式。下游代码流处理循环、query.ts、QueryEngine.ts、REPL**完全不改**。
## 环境变量
| 变量 | 必需 | 说明 |
|---|---|---|
| `CLAUDE_CODE_USE_OPENAI` | 是 | 设为 `1` 启用 OpenAI 后端 |
| `OPENAI_API_KEY` | 是 | API keyOllama 等可设为任意值) |
| `OPENAI_BASE_URL` | 推荐 | 端点 URL`http://localhost:11434/v1` |
| `OPENAI_MODEL` | 可选 | 覆盖所有请求的模型名(跳过映射) |
| `OPENAI_DEFAULT_OPUS_MODEL` | 可选 | 覆盖 opus 家族对应的模型(如 `o3`, `o3-mini`, `o1-pro` |
| `OPENAI_DEFAULT_SONNET_MODEL` | 可选 | 覆盖 sonnet 家族对应的模型(如 `gpt-4o`, `gpt-4.1` |
| `OPENAI_DEFAULT_HAIKU_MODEL` | 可选 | 覆盖 haiku 家族对应的模型(如 `gpt-4o-mini`, `gpt-4.0-mini` |
| `OPENAI_ORG_ID` | 可选 | Organization ID |
| `OPENAI_PROJECT_ID` | 可选 | Project ID |
### 使用示例
```bash
# Ollama
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=ollama \
OPENAI_BASE_URL=http://localhost:11434/v1 \
OPENAI_MODEL=qwen2.5-coder-32b \
bun run dev
# DeepSeek自动支持 Thinking
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=sk-xxx \
OPENAI_BASE_URL=https://api.deepseek.com/v1 \
OPENAI_MODEL=deepseek-chat \
bun run dev
# vLLM
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=token-abc123 \
OPENAI_BASE_URL=http://localhost:8000/v1 \
OPENAI_MODEL=Qwen/Qwen2.5-Coder-32B-Instruct \
bun run dev
# One API / LiteLLM
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=sk-your-key \
OPENAI_BASE_URL=https://your-one-api.example.com/v1 \
OPENAI_MODEL=gpt-4o \
bun run dev
# 自定义模型映射(使用家族变量)
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=sk-xxx \
OPENAI_BASE_URL=https://my-gateway.example.com/v1 \
OPENAI_DEFAULT_SONNET_MODEL="gpt-4o-2024-11-20" \
OPENAI_DEFAULT_HAIKU_MODEL="gpt-4o-mini" \
bun run dev
```
## 架构
### 请求流程
```
queryModel() [claude.ts]
├── 共享预处理(消息归一化、工具过滤、媒体裁剪)
└── if (getAPIProvider() === 'openai')
└── queryModelOpenAI() [openai/index.ts]
├── resolveOpenAIModel() → 解析模型名
├── normalizeMessagesForAPI() → 共享消息预处理
├── toolToAPISchema() → 构建工具 schema
├── anthropicMessagesToOpenAI() → 消息格式转换
├── anthropicToolsToOpenAI() → 工具格式转换
├── openai.chat.completions.create({ stream: true })
└── adaptOpenAIStreamToAnthropic() → 流格式转换
├── delta.reasoning_content → thinking 块
├── delta.content → text 块
├── delta.tool_calls → tool_use 块
├── usage.cached_tokens → cache_read_input_tokens
└── yield BetaRawMessageStreamEvent
```
### 模型名解析优先级
`resolveOpenAIModel()` 的解析顺序:
1. `OPENAI_MODEL` 环境变量 → 直接使用,覆盖所有
2. `OPENAI_DEFAULT_{FAMILY}_MODEL` 变量(如 `OPENAI_DEFAULT_SONNET_MODEL`)→ 按模型家族覆盖
3. `ANTHROPIC_DEFAULT_{FAMILY}_MODEL` 变量(向后兼容)
4. 内置默认映射(见下表)
5. 以上都不匹配 → 原名透传
### 内置模型映射
| Anthropic 模型 | OpenAI 映射 |
|---|---|
| `claude-sonnet-4-6` | `gpt-4o` |
| `claude-sonnet-4-5-20250929` | `gpt-4o` |
| `claude-sonnet-4-20250514` | `gpt-4o` |
| `claude-3-7-sonnet-20250219` | `gpt-4o` |
| `claude-3-5-sonnet-20241022` | `gpt-4o` |
| `claude-opus-4-6` | `o3` |
| `claude-opus-4-5-20251101` | `o3` |
| `claude-opus-4-1-20250805` | `o3` |
| `claude-opus-4-20250514` | `o3` |
| `claude-haiku-4-5-20251001` | `gpt-4o-mini` |
| `claude-3-5-haiku-20241022` | `gpt-4o-mini` |
同时会自动剥离 `[1m]` 后缀Claude 特有的 modifier
## 文件结构
### 新增文件
```
src/services/api/openai/
├── client.ts # OpenAI SDK 客户端工厂(~50 行)
├── convertMessages.ts # Anthropic → OpenAI 消息格式转换(~190 行)
├── convertTools.ts # Anthropic → OpenAI 工具格式转换(~70 行)
├── streamAdapter.ts # SSE 流转换核心,含 thinking + caching~270 行)
├── modelMapping.ts # 模型名解析(~60 行)
├── index.ts # 公共入口 queryModelOpenAI()~110 行)
└── __tests__/
├── convertMessages.test.ts # 10 个测试
├── convertTools.test.ts # 7 个测试
├── modelMapping.test.ts # 6 个测试
└── streamAdapter.test.ts # 14 个测试(含 thinking + caching
```
### 修改文件
| 文件 | 改动 |
|---|---|
| `src/utils/model/providers.ts` | 添加 `'openai'` provider 类型 + `CLAUDE_CODE_USE_OPENAI` 检查(最高优先级) |
| `src/utils/model/configs.ts` | 每个 ModelConfig 添加 `openai` 键 |
| `src/services/api/claude.ts` | 在 `stripExcessMediaItems()` 后插入 OpenAI 提前返回分支(~8 行) |
| `package.json` | 添加 `"openai": "^4.73.0"` 依赖 |
## 消息转换规则
### Anthropic → OpenAI
| Anthropic | OpenAI |
|---|---|
| `system` prompt`string[]` | `role: "system"` 消息(`\n\n` 拼接) |
| `user` + `text` 块 | `role: "user"` 消息 |
| `assistant` + `text` 块 | `role: "assistant"` + `content` |
| `assistant` + `tool_use` 块 | `role: "assistant"` + `tool_calls[]` |
| `user` + `tool_result` 块 | `role: "tool"` + `tool_call_id` |
| `thinking` 块 | 静默丢弃(请求侧) |
### 工具转换
| Anthropic | OpenAI |
|---|---|
| `{ name, description, input_schema }` | `{ type: "function", function: { name, description, parameters } }` |
| `cache_control`, `defer_loading` 等字段 | 剥离 |
| `tool_choice: { type: "auto" }` | `"auto"` |
| `tool_choice: { type: "any" }` | `"required"` |
| `tool_choice: { type: "tool", name }` | `{ type: "function", function: { name } }` |
### 消息转换示例
```
Anthropic: OpenAI:
[
system: ["You are helpful."], [
{ role: "system",
{ role: "user", content: "You are helpful." },
content: [ { role: "user",
{ type: "text", text: "Run ls" } content: "Run ls"
] },
}, { role: "assistant",
{ role: "assistant", content: "I'll check.",
content: [ tool_calls: [{
{ type: "text", text: "I'll check."}, id: "tu_123",
{ type: "tool_use", type: "function",
id: "tu_123", name: "bash", function: {
input: { command: "ls" } } name: "bash",
] arguments: '{"command":"ls"}'
}, }] }
{ role: "user", { role: "tool",
content: [ tool_call_id: "tu_123",
{ type: "tool_result", content: "file1\nfile2"
tool_use_id: "tu_123", }
content: "file1\nfile2" ]
]
}
]
```
## 流转换规则
### SSE Chunk → Anthropic Event 映射
| OpenAI Chunk | Anthropic Event |
|---|---|
| 首个 chunk | `message_start`(含 usage |
| `delta.reasoning_content` | `content_block_start(thinking)` + `thinking_delta` |
| `delta.content` | `content_block_start(text)` + `text_delta` |
| `delta.tool_calls` | `content_block_start(tool_use)` + `input_json_delta` |
| `finish_reason: "stop"` | `message_delta(stop_reason: "end_turn")` |
| `finish_reason: "tool_calls"` | `message_delta(stop_reason: "tool_use")` |
| `finish_reason: "length"` | `message_delta(stop_reason: "max_tokens")` |
### 块顺序
当模型返回 `reasoning_content` 时(如 DeepSeek块顺序与 Anthropic 一致:
```
thinking block (index 0) ← delta.reasoning_content
text block (index 1) ← delta.content
```
或:
```
thinking block (index 0) ← delta.reasoning_content
tool_use block (index 1) ← delta.tool_calls
```
`reasoning_content` 时:
```
text block (index 0) ← delta.content
tool_use block (index 1) ← delta.tool_calls如果有
```
### finish_reason 映射
| OpenAI | Anthropic |
|---|---|
| `stop` | `end_turn` |
| `tool_calls` | `tool_use` |
| `length` | `max_tokens` |
| `content_filter` | `end_turn` |
### 事件序列示例
**纯文本响应**
```
OpenAI chunks:
delta.content = "Hello"
delta.content = " world"
finish_reason = "stop"
→ Anthropic events:
message_start { message: { id, role: 'assistant', usage: {...} } }
content_block_start { index: 0, content_block: { type: 'text' } }
content_block_delta { index: 0, delta: { type: 'text_delta', text: 'Hello' } }
content_block_delta { index: 0, delta: { type: 'text_delta', text: ' world' } }
content_block_stop { index: 0 }
message_delta { delta: { stop_reason: 'end_turn' } }
message_stop
```
**Thinking + 文本DeepSeek 风格)**
```
OpenAI chunks:
delta.reasoning_content = "Let me think..."
delta.reasoning_content = " step by step."
delta.content = "The answer is 42."
finish_reason = "stop"
→ Anthropic events:
message_start { ... }
content_block_start { index: 0, content_block: { type: 'thinking', signature: '' } }
content_block_delta { index: 0, delta: { type: 'thinking_delta', thinking: 'Let me think...' } }
content_block_delta { index: 0, delta: { type: 'thinking_delta', thinking: ' step by step.' } }
content_block_stop { index: 0 }
content_block_start { index: 1, content_block: { type: 'text' } }
content_block_delta { index: 1, delta: { type: 'text_delta', text: 'The answer is 42.' } }
content_block_stop { index: 1 }
message_delta { delta: { stop_reason: 'end_turn' } }
message_stop
```
**工具调用**
```
OpenAI chunks:
delta.tool_calls[0] = { id: 'call_xxx', function: { name: 'bash', arguments: '' } }
delta.tool_calls[0].function.arguments = '{"comm'
delta.tool_calls[0].function.arguments = 'and":"ls"}'
finish_reason = "tool_calls"
→ Anthropic events:
message_start { ... }
content_block_start { index: 0, content_block: { type: 'tool_use', id: 'call_xxx', name: 'bash' } }
content_block_delta { index: 0, delta: { type: 'input_json_delta', partial_json: '{"comm' } }
content_block_delta { index: 0, delta: { type: 'input_json_delta', partial_json: 'and":"ls"}' } }
content_block_stop { index: 0 }
message_delta { delta: { stop_reason: 'tool_use' } }
message_stop
```
## 功能支持
### Thinking思维链
**请求侧**不需要显式配置。支持思维链的模型DeepSeek 等)会自动返回 `delta.reasoning_content`
**响应侧**`delta.reasoning_content` 被转换为 Anthropic `thinking` content block
```ts
// content_block_start
{ type: 'content_block_start', index: 0,
content_block: { type: 'thinking', thinking: '', signature: '' } }
// content_block_delta
{ type: 'content_block_delta', index: 0,
delta: { type: 'thinking_delta', thinking: 'Let me analyze...' } }
```
thinking block 在 text/tool_use block 之前自动关闭,保持 Anthropic 的块顺序。
### Prompt Caching
**请求侧**OpenAI 端点使用自动缓存,无需显式设置 `cache_control`
**响应侧**OpenAI 的 `usage.prompt_tokens_details.cached_tokens` 被映射到 Anthropic 的 `cache_read_input_tokens`
```
OpenAI: usage.prompt_tokens_details.cached_tokens = 800
Anthropic: message_start.message.usage.cache_read_input_tokens = 800
```
`message_start` 的 usage 中报告缓存命中量。
### 工具调用Tool Use
完整支持 OpenAI function calling 格式。所有本地工具Bash、FileEdit、Grep、Glob、Agent 等)透明工作——它们通过 JSON 输入输出通信,格式无关。
工具参数以 `input_json_delta` 形式流式传输,由下游代码拼接解析。
### 不支持的功能
| 功能 | 策略 |
|---|---|
| Beta Headers | 不发送 |
| Server Tools (advisor) | 不发送 |
| Structured Output | 不发送 |
| Fast Mode / Effort | 不发送 |
| Tool Search / defer_loading | 不启用,所有工具直接发送 |
| Anthropic Signature | thinking block 的 `signature` 字段为空字符串 |
| cache_creation_input_tokens | 始终为 0OpenAI 不区分创建/读取) |
## 测试
```bash
# 运行所有 OpenAI 适配层测试
bun test src/services/api/openai/__tests__/
# 单独运行
bun test src/services/api/openai/__tests__/streamAdapter.test.ts # 14 tests含 thinking + caching
bun test src/services/api/openai/__tests__/convertMessages.test.ts # 10 tests
bun test src/services/api/openai/__tests__/convertTools.test.ts # 7 tests
bun test src/services/api/openai/__tests__/modelMapping.test.ts # 6 tests
```
当前测试覆盖:**39 tests / 73 assertions / 0 fail**。
### 测试覆盖矩阵
| 功能 | convertMessages | convertTools | streamAdapter | modelMapping |
|---|---|---|---|---|
| 文本消息转换 | ✅ | | | |
| tool_use 转换 | ✅ | | | |
| tool_result 转换 | ✅ | | | |
| thinking 剥离 | ✅ | | | |
| 完整对话流程 | ✅ | | | |
| 工具 schema 转换 | | ✅ | | |
| tool_choice 映射 | | ✅ | | |
| 纯文本流 | | | ✅ | |
| 工具调用流 | | | ✅ | |
| 混合文本+工具 | | | ✅ | |
| finish_reason 映射 | | | ✅ | |
| thinking 流 | | | ✅ | |
| thinking+text 切换 | | | ✅ | |
| thinking+tool_use 切换 | | | ✅ | |
| 块索引正确性 | | | ✅ | |
| cached_tokens 映射 | | | ✅ | |
| OPENAI_MODEL 覆盖 | | | | ✅ |
| 默认模型映射 | | | | ✅ |
| 未知模型透传 | | | | ✅ |
| [1m] 后缀剥离 | | | | ✅ |
## 端到端验证
```bash
# 1. 安装依赖
bun install
# 2. 运行单元测试
bun test src/services/api/openai/__tests__/
# 3. 连接实际端点(以 Ollama 为例)
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=ollama \
OPENAI_BASE_URL=http://localhost:11434/v1 \
OPENAI_MODEL=qwen2.5-coder-32b \
bun run dev
# 4. 连接 DeepSeek测试 thinking 支持)
CLAUDE_CODE_USE_OPENAI=1 \
OPENAI_API_KEY=sk-xxx \
OPENAI_BASE_URL=https://api.deepseek.com/v1 \
OPENAI_MODEL=deepseek-reasoner \
bun run dev
# 5. 确认现有测试不受影响
bun test # 无 CLAUDE_CODE_USE_OPENAI 时走原有路径
```
## 代码统计
| 类别 | 行数 |
|---|---|
| 新增源码 | ~620 行 |
| 新增测试 | ~450 行 |
| 改动现有代码 | ~25 行 |
| **总计** | **~1100 行** |

View File

@@ -1,35 +0,0 @@
# 社区项目 & Blog 合集
> 每日更新,欢迎自荐!
## 工具 & 应用
| 项目 | 描述 | 作者 |
|------|------|------|
| [4qtask.vercel.app](https://4qtask.vercel.app/) | 免费四象限时间管理工具 | @kevinhuky |
| [kaying.studio](https://kaying.studio/) | 个人 AI 工具箱 | @kayingai |
| [supsub.ai](https://supsub.ai/) | 高效阅读工具 | @hidumou |
| [x-video-download.net](https://x-video-download.net/) | 视频下载工具 | @syakadou |
| [1openapi.com](https://1openapi.com/) | API 中转站 | @thinker007 |
| [claw-z.com](https://claw-z.com/) | 一键部署 OpenClaw AI Agent场景驱动、全面管理 | @uhhc |
| [gemini-watermark-remover.net](https://gemini-watermark-remover.net/) | Gemini 水印移除工具 | @syakadou |
## GitHub 开源项目
| 项目 | 描述 | 作者 |
|------|------|------|
| [VersperClaw](https://github.com/versperai/VersperClaw) | 全自动科研流 | @versperai |
| [claude-reviews-claude](https://github.com/openedclaude/claude-reviews-claude) | 原汤化原食——Claude 如何看待眼中的老己 | @openedclaude |
| [agentica](https://github.com/shibing624/agentica) | 自研 Agent 框架,借鉴 claude-code 多 Agent 处理 | @shibing624 |
| [macman](https://github.com/tonngw/macman) | Mac 从 0 到 1 保姆级配置教程 | @tonngw |
| [SuperSpec](https://github.com/asasugar/SuperSpec) | SDD / Spec-Driven Development | @asasugar |
| [adnify](https://github.com/adnaan-worker/adnify) | 高颜值高定制化 AI 编辑器 | @adnaan-worker |
| [another-rule-engine](https://github.com/eatmoreduck/another-rule-engine) | 基于 Groovy 的开源多功能决策引擎 | @eatmoreduck |
| [creative_master](https://github.com/chatabc/creative_master) | AI 驱动的创意灵感管理工具 | @chatabc |
| [RapidDoc](https://github.com/RapidAI/RapidDoc) | Office 文件解析工具转 Markdown支持 PDF/Image/Word/PPT/Excel | @hzkitt |
## Blog
| 链接 | 作者 |
|------|------|
| [blog.xiaohuangyu.space](https://blog.xiaohuangyu.space/) | @eatmoreduck |

View File

@@ -0,0 +1,77 @@
# Task 001: daemon status / stop
> 来源: [stub-recovery-design-1-4.md](../features/stub-recovery-design-1-4.md) 第 1 项
> 优先级: P0 (首选实现项)
> 工作量: 小
> 状态: DONE
## 目标
`claude daemon status``claude daemon stop` 在任意 CLI 进程中都能正确工作,不依赖 TUI 内存态。
## 背景
- `start` 路径已有完整 supervisor + worker 生命周期 (`src/daemon/main.ts`, `src/daemon/workerRegistry.ts`)
- `status` / `stop` 目前只是占位输出 (`src/daemon/main.ts:49`)
- `/remote-control-server` 有自己的命令内 UI 状态,但只维护当前进程内的 `daemonProcess`,不适合跨进程管理
## 实现方案
### 新增文件
| 文件 | 说明 |
|------|------|
| `src/daemon/state.ts` | daemon 状态文件读写模块 |
### 修改文件
| 文件 | 改动 |
|------|------|
| `src/daemon/main.ts` | `start` 写入状态文件;`status`/`stop` 调用 state 模块 |
| `src/commands/remoteControlServer/remoteControlServer.tsx` | 读取同一份状态文件(轻量改动) |
### 状态文件
路径: `~/.claude/daemon/remote-control.json`
```json
{
"pid": 12345,
"cwd": "/path/to/project",
"startedAt": "2026-04-12T10:00:00Z",
"workerKinds": ["bridge", "rcs"],
"lastStatus": "running"
}
```
### status 逻辑
1. 读取状态文件
2. 用进程探测验证 pid 是否存活
3. 输出 `running` / `stopped` / `stale`
4. stale 时自动清理状态文件
### stop 逻辑
1. 读取 pid
2. 发送 `SIGTERM`
3. 等待退出(超时兜底)
4. 超时后 `SIGKILL`
5. 清理状态文件
## 验证步骤
- [ ] `claude daemon start` 正常启动并写入状态文件
- [ ] 新开终端执行 `claude daemon status`,显示 `running`
- [ ] 执行 `claude daemon stop`daemon 正常退出
- [ ] 再次执行 `claude daemon status`,返回 `stopped``stale cleaned`
- [ ] Windows 下 stop 超时兜底正常工作
## 风险
- Windows 信号模型和 Unix 不同,`stop` 需要超时兜底
- 当前设计默认单 supervisor不处理多实例并发
## 依赖
无外部依赖,可独立实施。

View File

@@ -0,0 +1,80 @@
# Task 002: BG_SESSIONS — ps / logs / kill
> 来源: [stub-recovery-design-1-4.md](../features/stub-recovery-design-1-4.md) 第 2 项
> 优先级: P1
> 工作量: 中等
> 状态: DONE
> 阶段: Phase 2A (MVP)
## 目标
`ps` / `logs` / `kill` 做成真正有用的 session 管理命令。不在第一阶段补完 `attach` / `--bg`
## 背景
- fast-path 已接好 (`src/entrypoints/cli.tsx:218`)
- session registry 已有真实实现 (`src/utils/concurrentSessions.ts`)
- `exit` 在 bg session 内已会 `tmux detach-client` (`src/commands/exit/exit.tsx:20`)
- CLI handler 仍全空 (`src/cli/bg.ts`)
- task summary 仍然是 stub (`src/utils/taskSummary.ts`)
## 实现方案
### 修改文件
| 文件 | 改动 |
|------|------|
| `src/cli/bg.ts` | 实现 `ps` / `logs` / `kill` handler |
| `src/utils/concurrentSessions.ts` | 扩展以便后续 attach/--bg 使用 |
| `src/utils/taskSummary.ts` | 补充基础实现 |
### 复用模块
- `src/utils/sessionStorage.ts` — session 存储
- `src/utils/udsClient.ts` — UDS 通信
### ps 命令
- 从 registry 读取 live sessions
- 展示: pid, kind, sessionId, cwd, name, startedAt, bridgeSessionId
- 如果有 activity/status一并展示
### logs 命令
- 支持按 `sessionId` / `pid` / `name` 查找
- 优先复用本地 transcript/log 读取能力
- 如果 registry 里存在 `logPath`,支持 tail 文件
### kill 命令
- 解析目标 session
- 发退出信号
- 清理 stale registry
## 验证步骤
- [ ] `ps` 能列出当前 live sessions
- [ ] `logs <sessionId|pid|name>` 能输出对应日志
- [ ] `kill <sessionId|pid|name>` 能结束目标 session 并清理 registry
- [ ] 无 live session 时各命令有明确提示
## Phase 2B (后续)
- [ ] 实现 `attach`
- [ ] 实现 `--bg`
- [ ] 实现 `taskSummary` 的中途状态更新
### 为什么拆分
- 现有 registry 记录了 `pid / sessionId / name / logPath`
- 但没有可靠的 tmux attach target
- `attach``--bg` 需要补启动/附着元数据设计,不是简单补 handler
## 风险
- `attach` / `--bg` 第二阶段需要 tmux 元数据设计
- Windows 下 tmux 路径需要明确降级策略
## 依赖
- Task 001 (daemon 状态管理可复用模式,但非硬性依赖)

View File

@@ -0,0 +1,87 @@
# Task 003: TEMPLATES — job 文件系统 MVP
> 来源: [stub-recovery-design-1-4.md](../features/stub-recovery-design-1-4.md) 第 3 项
> 优先级: P2
> 工作量: 中等
> 状态: DONE
> 阶段: MVP
## 目标
`new` / `list` / `reply` 做成可用的模板任务系统。第一阶段不碰复杂的自动分类与自动执行。
## 背景
- 命令入口只有 fast-path (`src/entrypoints/cli.tsx:249`)
- handler 是空的 (`src/cli/handlers/templateJobs.ts`)
- `markdownConfigLoader` 已把 `templates` 纳入配置目录 (`src/utils/markdownConfigLoader.ts:29`)
- `query/stopHooks` 已预留 job classifier 链路 (`src/query/stopHooks.ts:103`)
- `jobs/classifier.ts` 仍是 stub (`src/jobs/classifier.ts`)
## 实现方案
### 新增文件
| 文件 | 说明 |
|------|------|
| `src/jobs/state.ts` | job 状态管理 |
| `src/jobs/templates.ts` | 模板解析与列表 |
### 修改文件
| 文件 | 改动 |
|------|------|
| `src/cli/handlers/templateJobs.ts` | 实现 `new` / `list` / `reply` handler |
### 模板来源
`.claude/templates/*.md`
### 模板格式
复用现有 markdown + frontmatter 解析,不另外设计 DSL。
### list 命令
- 列出所有模板
- 显示: 模板名, description, 路径
### new 命令
- 解析模板
-`~/.claude/jobs/<job-id>/` 下创建 job 目录
- 写入 `template.md`, `input.txt`, `state.json`
- 返回 job id 与目录路径
### reply 命令
- 将回复写入 `replies.jsonl``input.txt`
- 更新 `state.json`
## 验证步骤
- [ ] `list` 能列出 `.claude/templates` 下的所有模板
- [ ] `new <template> [args...]` 能创建 job 目录和状态文件
- [ ] `reply <job-id> <text>` 能更新 job 内容和状态
- [ ] frontmatter schema 最小字段集已定义
## Phase 2 (后续)
- [ ] 恢复 `src/jobs/classifier.ts`
- [ ] 让带 `CLAUDE_JOB_DIR` 的 job session 在 turn 完成后自动更新 `state.json`
- [ ] 再决定是否补自动 job runner
### 为什么拆分
- 当前是 "template job commands",不是单纯模板列表
- 自动 job 运行链路没有足够现成实现
- 先做文件系统 job lifecycle 更稳
## 风险
- frontmatter schema 需要先定义最小字段集
- 一旦扩展到"自动运行 job",范围会明显膨胀
## 依赖
无硬性依赖,可独立实施。

View File

@@ -0,0 +1,103 @@
# Task 004: assistant [sessionId] — 分阶段恢复
> 来源: [stub-recovery-design-1-4.md](../features/stub-recovery-design-1-4.md) 第 4 项
> 优先级: P3
> 工作量: Phase 4A 中等4A-4D 全做完很大
> 状态: Phase 4A DONE, 4B-4D TODO
## 目标
不一次性恢复整个 KAIROS 助手系统。先做"明确 sessionId 的 viewer attach 可用",再逐步补 discovery / chooser / install。
## 背景
- attach 主流程已存在 (`src/main.tsx:4708`)
- 远端 viewer 所需基础模块已存在:
- `src/remote/RemoteSessionManager.ts`
- `src/hooks/useAssistantHistory.ts`
- `src/assistant/sessionHistory.ts`
- 真正 stub 的主要是:
- `src/assistant/sessionDiscovery.ts`
- `src/assistant/AssistantSessionChooser.ts`
- `src/commands/assistant/assistant.ts:7`
- `src/assistant/index.ts`
## 分阶段实现
### Phase 4A: MVP — 显式 sessionId attach
**修改文件:**
| 文件 | 改动 |
|------|------|
| `src/main.tsx` | 确保 attach 分支可用 |
| `src/commands/assistant/index.ts` | 实现显式 sessionId 参数入口 |
**行为:**
- `claude assistant <sessionId>` — 进入 remote viewer
- `claude assistant` (无参数) — 返回明确提示: 当前版本需要显式 sessionIddiscovery 尚未启用
**验证:**
- [ ] `claude assistant <sessionId>` 能进入 remote viewer
- [ ] 历史懒加载工作正常
- [ ] 无参数模式给出明确提示
### Phase 4B: session discovery
**修改文件:**
| 文件 | 改动 |
|------|------|
| `src/assistant/sessionDiscovery.ts` | 恢复 `discoverAssistantSessions()` |
**行为:**
- 数据来源优先复用现有 sessions / bridge / teleport API不新增协议
- `claude assistant` 无参数时能拿到候选 session 列表
**验证:**
- [ ] 无参数调用能列出可用 sessions
- [ ] 数据来源复用现有通道
### Phase 4C: session chooser
**修改文件:**
| 文件 | 改动 |
|------|------|
| `src/assistant/AssistantSessionChooser.ts` | 恢复交互式选择器 |
**行为:**
- 多 session 时可交互选择
**验证:**
- [ ] 多个 session 时弹出选择器
- [ ] 选择后正确 attach
### Phase 4D: install wizard
**修改文件:**
| 文件 | 改动 |
|------|------|
| `src/commands/assistant/assistant.ts` | 恢复 install wizard 辅助函数 |
**行为:**
- 没有 session 时如何引导用户
**验证:**
- [ ] 无可用 session 时引导用户创建/连接
## 为什么拆分
- attach 渲染层与远端消息通道大部分已在
- 真正缺的是"如何发现目标 session"和"如何交互选择"
- 如果把 `src/assistant/index.ts` 的整套 KAIROS 正常模式也一起拉进来,范围会失控
## 风险
- 这是四项里范围最大的
- 一旦把 KAIROS 正常模式整体拉入,会从"viewer attach"膨胀成"完整 assistant mode 恢复"
## 依赖
- Task 002 的 session registry 模式可复用

View File

@@ -0,0 +1,196 @@
# Task 013: BgEngine 跨平台后台引擎抽象
> 设计文档: [daemon-restructure-design.md](../features/daemon-restructure-design.md) § 四
> 依赖: 无
> 分支: `feat/integrate-5-branches`
## 目标
`src/cli/bg.ts` 中硬编码的 tmux 逻辑提取为引擎抽象层,实现 TmuxEngine + DetachedEngine使后台会话功能在 Windows / macOS / Linux 上都能工作。
## 背景
当前 `bg.ts``handleBgFlag()``attachHandler()` 直接调用 tmux 命令。Windows 上 `--bg` 直接报错退出。需要一个引擎抽象层,根据平台和可用工具自动选择最佳方案。
## 文件清单
### 新增
| 文件 | 说明 |
|------|------|
| `src/cli/bg/engine.ts` | BgEngine 接口 + BgStartOptions/BgStartResult 类型 |
| `src/cli/bg/engines/tmux.ts` | TmuxEngine: 从 `bg.ts` 提取 tmux 相关逻辑 |
| `src/cli/bg/engines/detached.ts` | DetachedEngine: spawn({ detached }) + logFile 重定向 |
| `src/cli/bg/engines/index.ts` | selectEngine() 自动选择 + re-export |
| `src/cli/bg/tail.ts` | 跨平台日志 tail: fs.watch + 轮询 fallback |
### 修改
| 文件 | 变更 |
|------|------|
| `src/cli/bg.ts` | `handleBgFlag()` 改为调用 `selectEngine().start()``attachHandler()` 改为调用 `engine.attach()` |
## 实现方案
### 1. BgEngine 接口 (`src/cli/bg/engine.ts`)
```typescript
export interface BgEngine {
readonly name: string
available(): Promise<boolean>
start(opts: BgStartOptions): Promise<BgStartResult>
attach(session: SessionEntry): Promise<void>
}
export interface BgStartOptions {
sessionName: string
args: string[] // CLI args (去除 --bg)
env: Record<string, string | undefined>
logPath: string
cwd: string
}
export interface BgStartResult {
pid: number
sessionName: string
logPath: string
engineUsed: 'tmux' | 'detached'
}
```
### 2. TmuxEngine (`src/cli/bg/engines/tmux.ts`)
`bg.ts:handleBgFlag()``bg.ts:attachHandler()` 提取:
- `available()`: `execFileNoThrow('tmux', ['-V'])` 返回 code === 0
- `start()`: `tmux new-session -d -s <name> <cmd>`
- `attach()`: `tmux attach-session -t <session.tmuxSessionName>`
### 3. DetachedEngine (`src/cli/bg/engines/detached.ts`)
```typescript
export class DetachedEngine implements BgEngine {
readonly name = 'detached'
async available(): Promise<boolean> {
return true // 总是可用
}
async start(opts: BgStartOptions): Promise<BgStartResult> {
const logFd = openSync(opts.logPath, 'a')
const child = spawn(process.execPath, [process.argv[1]!, ...opts.args], {
detached: true,
stdio: ['ignore', logFd, logFd],
env: opts.env,
cwd: opts.cwd,
})
child.unref()
closeSync(logFd)
return {
pid: child.pid!,
sessionName: opts.sessionName,
logPath: opts.logPath,
engineUsed: 'detached',
}
}
async attach(session: SessionEntry): Promise<void> {
// 委托给 tail.ts
await tailLog(session.logPath!)
}
}
```
### 4. 日志 Tail (`src/cli/bg/tail.ts`)
```typescript
/**
* 跨平台实时日志输出。Ctrl+C 退出,不杀后台进程。
*
* 策略:
* 1. 读取已有内容输出
* 2. fs.watch() 监听文件变化 (主方案)
* 3. 如果 fs.watch 不可靠 (某些 Windows 网络驱动器)fallback 到 500ms 轮询
*/
export async function tailLog(logPath: string): Promise<void>
```
### 5. 引擎选择 (`src/cli/bg/engines/index.ts`)
```typescript
export async function selectEngine(): Promise<BgEngine> {
if (process.platform === 'win32') {
return new DetachedEngine()
}
const tmux = new TmuxEngine()
if (await tmux.available()) {
return tmux
}
return new DetachedEngine()
}
```
### 6. bg.ts 重构
`handleBgFlag()` 改名为 `handleBgStart()`,内部逻辑:
```typescript
export async function handleBgStart(args: string[]): Promise<void> {
const engine = await selectEngine()
const sessionName = `claude-bg-${randomUUID().slice(0, 8)}`
const logPath = join(getClaudeConfigHomeDir(), 'sessions', 'logs', `${sessionName}.log`)
const result = await engine.start({
sessionName,
args: filteredArgs,
env: { ...process.env, CLAUDE_CODE_SESSION_KIND: 'bg', ... },
logPath,
cwd: process.cwd(),
})
console.log(`Background session started: ${result.sessionName}`)
console.log(` Engine: ${result.engineUsed}`)
console.log(` Log: ${result.logPath}`)
console.log(` Use \`claude daemon attach ${result.sessionName}\` to reconnect.`)
}
```
`attachHandler()` 根据 `session.engine` 字段选择引擎:
```typescript
export async function attachHandler(target: string | undefined): Promise<void> {
// ... 找到 session
if (session.engine === 'tmux' && session.tmuxSessionName) {
const tmux = new TmuxEngine()
await tmux.attach(session)
} else {
const detached = new DetachedEngine()
await detached.attach(session)
}
}
```
## SessionEntry 扩展
`sessions/<PID>.json` 新增 `engine` 字段:
```json
{
"pid": 12345,
"engine": "detached",
"logPath": "~/.claude/sessions/logs/claude-bg-a1b2c3d4.log",
"sessionId": "...",
"cwd": "..."
}
```
兼容旧格式: 如果 `engine` 字段缺失,检查 `tmuxSessionName` 存在则为 `tmux`,否则为 `detached`
## 验证清单
- [ ] Windows: `claude daemon bg` 启动后台会话,无 tmux 依赖
- [ ] Windows: `claude daemon attach <name>` 以 tail 模式附着Ctrl+C 退出不杀进程
- [ ] macOS/Linux (有 tmux): 行为与当前一致
- [ ] macOS/Linux (无 tmux): 自动 fallback 到 detached 引擎
- [ ] `claude daemon status` 正确显示 engine 类型
- [ ] 旧格式 session JSON (无 engine 字段) 兼容
- [ ] tsc --noEmit 零错误
- [ ] bun test 通过

View File

@@ -0,0 +1,275 @@
# Task 014: /daemon 命令层级化
> 设计文档: [daemon-restructure-design.md](../features/daemon-restructure-design.md) § 三.1
> 依赖: Task 013 (BgEngine 抽象)
> 分支: `feat/integrate-5-branches`
## 目标
将散落的 `daemon start/stop/status` + `ps/logs/attach/kill` + `--bg` 统一收归 `/daemon` 命名空间,实现 CLI + REPL 双注册。
## 背景
当前这些命令注册在两个互不关联的位置:
- `cli.tsx:203-212`: `daemon [start|status|stop]``daemon/main.ts`
- `cli.tsx:217-246`: `ps|logs|attach|kill|--bg``cli/bg.ts`
需要合并为统一的 `claude daemon <subcommand>` 入口,并新增 REPL `/daemon` 斜杠命令。
## 文件清单
### 新增
| 文件 | 说明 |
|------|------|
| `src/commands/daemon/index.ts` | `/daemon` REPL 斜杠命令注册 (type: local-jsx) |
| `src/commands/daemon/daemon.tsx` | `/daemon` 子命令路由 + status UI 组件 |
### 修改
| 文件 | 变更 |
|------|------|
| `src/entrypoints/cli.tsx` | 统一 daemon 快速路径: `daemon <sub>` 路由到对应 handler。旧命令 `ps/logs/attach/kill` 保留但输出 deprecation 警告后代理 |
| `src/commands.ts` | 注册 `/daemon` 斜杠命令 (feature-gated: DAEMON \|\| BG_SESSIONS) |
| `src/daemon/main.ts` | `daemonMain()` 扩展: 支持 `bg/attach/logs/kill/ps` 子命令 (委托给 bg.ts handlers) |
## 实现方案
### 1. CLI 快速路径统一 (`cli.tsx`)
**改前** (两段独立路由):
```typescript
// 段 1: daemon
if (feature('DAEMON') && args[0] === 'daemon') {
await daemonMain(args.slice(1))
}
// 段 2: bg sessions
if (feature('BG_SESSIONS') && ['ps','logs','attach','kill'].includes(args[0])) {
// ...switch/case
}
```
**改后** (统一入口):
```typescript
// 统一 daemon 入口 — 合并 daemon supervisor + bg sessions
if (
(feature('DAEMON') || feature('BG_SESSIONS')) &&
args[0] === 'daemon'
) {
profileCheckpoint('cli_daemon_path')
const { enableConfigs } = await import('../utils/config.js')
enableConfigs()
const { initSinks } = await import('../utils/sinks.js')
initSinks()
const { daemonMain } = await import('../daemon/main.js')
await daemonMain(args.slice(1))
return
}
// --bg 快捷方式 → daemon bg
if (
feature('BG_SESSIONS') &&
(args.includes('--bg') || args.includes('--background'))
) {
profileCheckpoint('cli_daemon_path')
const { enableConfigs } = await import('../utils/config.js')
enableConfigs()
const bg = await import('../cli/bg.js')
await bg.handleBgStart(args.filter(a => a !== '--bg' && a !== '--background'))
return
}
// 向后兼容: ps/logs/attach/kill → daemon <sub> (deprecated)
if (
feature('BG_SESSIONS') &&
['ps', 'logs', 'attach', 'kill'].includes(args[0] ?? '')
) {
const mapped = args[0] === 'ps' ? 'status' : args[0]
console.error(`[deprecated] Use: claude daemon ${mapped} ${args.slice(1).join(' ')}`.trim())
const { enableConfigs } = await import('../utils/config.js')
enableConfigs()
const { daemonMain } = await import('../daemon/main.js')
await daemonMain([args[0]!, ...args.slice(1)])
return
}
```
### 2. daemonMain 扩展 (`daemon/main.ts`)
```typescript
export async function daemonMain(args: string[]): Promise<void> {
const subcommand = args[0] || 'status'
switch (subcommand) {
// --- Supervisor 管理 ---
case 'start':
await runSupervisor(args.slice(1))
break
case 'stop':
await handleDaemonStop()
break
// --- 会话管理 (委托给 bg.ts) ---
case 'status':
case 'ps':
await showUnifiedStatus() // 新: daemon 状态 + 会话列表
break
case 'bg':
const bg = await import('../cli/bg.js')
await bg.handleBgStart(args.slice(1))
break
case 'attach':
const bg2 = await import('../cli/bg.js')
await bg2.attachHandler(args[1])
break
case 'logs':
const bg3 = await import('../cli/bg.js')
await bg3.logsHandler(args[1])
break
case 'kill':
const bg4 = await import('../cli/bg.js')
await bg4.killHandler(args[1])
break
case '--help': case '-h': case 'help':
printHelp()
break
default:
console.error(`Unknown daemon subcommand: ${subcommand}`)
printHelp()
process.exitCode = 1
}
}
```
### 3. 统一状态面板 (`showUnifiedStatus`)
```typescript
async function showUnifiedStatus(): Promise<void> {
// 1. Daemon supervisor 状态
const daemonResult = queryDaemonStatus()
console.log('=== Daemon Supervisor ===')
switch (daemonResult.status) {
case 'running':
console.log(` Status: running (PID: ${daemonResult.state!.pid})`)
console.log(` Workers: ${daemonResult.state!.workerKinds.join(', ')}`)
break
case 'stopped':
console.log(' Status: stopped')
break
case 'stale':
console.log(' Status: stale (cleaned up)')
break
}
// 2. 后台会话列表
console.log('\n=== Background Sessions ===')
const bg = await import('../cli/bg.js')
await bg.psHandler([])
}
```
### 4. REPL 斜杠命令注册
**`src/commands/daemon/index.ts`**:
```typescript
import type { Command } from '../../commands.js'
import { feature } from 'bun:bundle'
const daemon = {
type: 'local-jsx',
name: 'daemon',
description: 'Manage background sessions and daemon',
argumentHint: '[status|start|stop|bg|attach|logs|kill]',
isEnabled: () => {
if (feature('DAEMON')) return true
if (feature('BG_SESSIONS')) return true
return false
},
load: () => import('./daemon.js'),
} satisfies Command
export default daemon
```
**`src/commands/daemon/daemon.tsx`**:
```typescript
export async function call(
onDone: LocalJSXCommandOnDone,
context: LocalJSXCommandContext,
args: string,
): Promise<React.ReactNode> {
const parts = args.trim().split(/\s+/)
const sub = parts[0] || 'status'
switch (sub) {
case 'status':
case 'ps':
// 调用 showUnifiedStatus捕获输出
// 返回文本结果
break
case 'bg':
// REPL 中启动后台会话
break
case 'start':
case 'stop':
case 'attach':
case 'logs':
case 'kill':
// 委托给对应 handler
break
default:
onDone(`Unknown: ${sub}. Use: status|start|stop|bg|attach|logs|kill`)
return null
}
}
```
**`src/commands.ts`** 添加:
```typescript
// 条件导入
const daemonCmd =
feature('DAEMON') || feature('BG_SESSIONS')
? require('./commands/daemon/index.js').default
: null
// COMMANDS 数组中添加
...(daemonCmd ? [daemonCmd] : []),
```
### 5. 更新 help 文本 (`daemon/main.ts`)
```
Claude Code Daemon — background process management
USAGE
claude daemon [subcommand]
SUBCOMMANDS
status Show daemon and session status (default)
start Start the daemon supervisor
stop Stop the daemon
bg Start a background session
attach Attach to a background session
logs Show session logs
kill Kill a session
help Show this help
REPL
/daemon [subcommand] Same commands available in interactive mode
```
## 验证清单
- [ ] `claude daemon` (无参数) 显示统一状态面板
- [ ] `claude daemon status` 显示 supervisor + 会话列表
- [ ] `claude daemon start/stop` 与当前行为一致
- [ ] `claude daemon bg` 启动后台会话 (调用 BgEngine)
- [ ] `claude daemon attach/logs/kill <target>` 功能正常
- [ ] `claude ps` 输出 deprecation 警告 + 正常工作
- [ ] `claude logs/attach/kill` 同上
- [ ] `claude --bg` 快捷方式正常
- [ ] REPL 中 `/daemon` 可用tab 补全显示
- [ ] REPL 中 `/daemon status` 显示状态信息
- [ ] tsc --noEmit 零错误
- [ ] bun test 通过

View File

@@ -0,0 +1,177 @@
# Task 015: /job 命令层级化
> 设计文档: [daemon-restructure-design.md](../features/daemon-restructure-design.md) § 三.2
> 依赖: 无 (可与 Task 013 并行)
> 分支: `feat/integrate-5-branches`
## 目标
`claude new/list/reply` 收归 `/job` 命名空间,实现 CLI + REPL 双注册。
## 背景
当前 `new`, `list`, `reply` 是顶级 CLI 命令 (`cli.tsx:250-261`),容易与其他命令冲突(特别是 `list` 这种通用词)。需要收归 `claude job <subcommand>` 并新增 REPL `/job` 入口。
## 文件清单
### 新增
| 文件 | 说明 |
|------|------|
| `src/commands/job/index.ts` | `/job` REPL 斜杠命令注册 |
| `src/commands/job/job.tsx` | `/job` 子命令路由 |
### 修改
| 文件 | 变更 |
|------|------|
| `src/entrypoints/cli.tsx` | 新增 `job` 快速路径 + 旧 `new/list/reply` deprecation 代理 |
| `src/commands.ts` | 注册 `/job` 斜杠命令 |
### 不动
| 文件 | 说明 |
|------|------|
| `src/cli/handlers/templateJobs.ts` | 内部 handler 不变,只是被调用方式变了 |
| `src/jobs/state.ts` | job 状态管理不变 |
| `src/jobs/templates.ts` | 模板发现不变 |
| `src/jobs/classifier.ts` | 任务分类器不变 |
## 实现方案
### 1. CLI 快速路径 (`cli.tsx`)
**改后**:
```typescript
// 新: claude job <subcommand>
if (
feature('TEMPLATES') &&
args[0] === 'job'
) {
profileCheckpoint('cli_templates_path')
const { templatesMain } = await import('../cli/handlers/templateJobs.js')
await templatesMain(args.slice(1))
process.exit(0)
}
// 向后兼容 (deprecated)
if (
feature('TEMPLATES') &&
(args[0] === 'new' || args[0] === 'list' || args[0] === 'reply')
) {
console.error(`[deprecated] Use: claude job ${args[0]} ${args.slice(1).join(' ')}`.trim())
profileCheckpoint('cli_templates_path')
const { templatesMain } = await import('../cli/handlers/templateJobs.js')
await templatesMain(args)
process.exit(0)
}
```
### 2. templateJobs.ts 新增 status 子命令
在现有 `switch` 中增加:
```typescript
case 'status':
handleStatus(args.slice(1))
break
```
```typescript
function handleStatus(args: string[]): void {
const jobId = args[0]
if (!jobId) {
console.error('Usage: claude job status <job-id>')
process.exitCode = 1
return
}
const state = readJobState(jobId)
if (!state) {
console.error(`Job not found: ${jobId}`)
process.exitCode = 1
return
}
console.log(`Job: ${state.jobId}`)
console.log(` Template: ${state.templateName}`)
console.log(` Status: ${state.status}`)
console.log(` Created: ${state.createdAt}`)
console.log(` Updated: ${state.updatedAt}`)
}
```
### 3. REPL 斜杠命令
**`src/commands/job/index.ts`**:
```typescript
import type { Command } from '../../commands.js'
import { feature } from 'bun:bundle'
const job = {
type: 'local-jsx',
name: 'job',
description: 'Manage template jobs',
argumentHint: '[list|new|reply|status]',
isEnabled: () => {
if (feature('TEMPLATES')) return true
return false
},
load: () => import('./job.js'),
} satisfies Command
export default job
```
**`src/commands/job/job.tsx`**:
```typescript
export async function call(
onDone: LocalJSXCommandOnDone,
_context: LocalJSXCommandContext,
args: string,
): Promise<React.ReactNode> {
const parts = args.trim().split(/\s+/)
const sub = parts[0] || 'list'
// 委托给 templatesMain
const { templatesMain } = await import('../../cli/handlers/templateJobs.js')
// 捕获 console.log 输出作为结果返回给 REPL
const lines: string[] = []
const origLog = console.log
const origError = console.error
console.log = (...a: unknown[]) => lines.push(a.join(' '))
console.error = (...a: unknown[]) => lines.push(a.join(' '))
try {
await templatesMain([sub, ...parts.slice(1)])
} finally {
console.log = origLog
console.error = origError
}
onDone(lines.join('\n') || 'Done.', { display: 'system' })
return null
}
```
### 4. commands.ts 注册
```typescript
const jobCmd = feature('TEMPLATES')
? require('./commands/job/index.js').default
: null
// COMMANDS 数组:
...(jobCmd ? [jobCmd] : []),
```
## 验证清单
- [ ] `claude job list` 列出模板
- [ ] `claude job new <template>` 创建任务
- [ ] `claude job reply <id> <text>` 回复任务
- [ ] `claude job status <id>` 显示任务状态
- [ ] `claude job` (无参数) 等同于 `claude job list`
- [ ] `claude new/list/reply` 输出 deprecation 警告 + 正常工作
- [ ] REPL 中 `/job` 可用
- [ ] REPL 中 `/job list` 显示模板列表
- [ ] tsc --noEmit 零错误
- [ ] bun test 通过

View File

@@ -0,0 +1,123 @@
# Task 016: 向后兼容 + 测试
> 设计文档: [daemon-restructure-design.md](../features/daemon-restructure-design.md) § 五
> 依赖: Task 014, Task 015
> 分支: `feat/integrate-5-branches`
## 目标
确保旧命令向后兼容 (deprecation 警告 + 正常代理),并为重构后的命令结构编写测试。
## 文件清单
### 新增
| 文件 | 说明 |
|------|------|
| `src/daemon/__tests__/daemonMain.test.ts` | daemonMain 子命令路由测试 |
| `src/cli/bg/__tests__/engine.test.ts` | BgEngine 选择逻辑测试 |
| `src/cli/bg/__tests__/detached.test.ts` | DetachedEngine 启动/停止测试 |
| `src/cli/bg/__tests__/tail.test.ts` | 日志 tail 功能测试 |
### 修改
| 文件 | 变更 |
|------|------|
| `src/entrypoints/cli.tsx` | 确认 deprecation 路径正确代理 |
## 实现方案
### 1. 向后兼容矩阵
| 旧命令 | 新命令 | 处理方式 |
|--------|--------|---------|
| `claude ps` | `claude daemon status` | stderr 输出 `[deprecated] Use: claude daemon status`,然后执行 |
| `claude logs <x>` | `claude daemon logs <x>` | 同上 |
| `claude attach <x>` | `claude daemon attach <x>` | 同上 |
| `claude kill <x>` | `claude daemon kill <x>` | 同上 |
| `claude --bg` | `claude daemon bg` | 保留为快捷方式,**不** deprecate (太常用) |
| `claude new <t>` | `claude job new <t>` | stderr deprecation + 执行 |
| `claude list` | `claude job list` | stderr deprecation + 执行 |
| `claude reply <id>` | `claude job reply <id>` | stderr deprecation + 执行 |
**关键**: deprecation 输出到 stderr 而非 stdout不影响脚本管道。
### 2. 测试计划
#### 2.1 daemonMain 路由测试
```typescript
describe('daemonMain', () => {
test('无参数默认 status', async () => { ... })
test('start 调用 runSupervisor', async () => { ... })
test('stop 调用 handleDaemonStop', async () => { ... })
test('bg 委托给 bg.handleBgStart', async () => { ... })
test('attach 委托给 bg.attachHandler', async () => { ... })
test('logs 委托给 bg.logsHandler', async () => { ... })
test('kill 委托给 bg.killHandler', async () => { ... })
test('未知子命令设置 exitCode=1', async () => { ... })
})
```
#### 2.2 引擎选择测试
```typescript
describe('selectEngine', () => {
test('win32 返回 DetachedEngine', async () => { ... })
test('darwin + tmux 可用返回 TmuxEngine', async () => { ... })
test('darwin + tmux 不可用返回 DetachedEngine', async () => { ... })
test('linux + tmux 可用返回 TmuxEngine', async () => { ... })
})
```
#### 2.3 DetachedEngine 测试
```typescript
describe('DetachedEngine', () => {
test('available 始终返回 true', async () => { ... })
test('start 创建 detached 子进程并写入日志', async () => { ... })
test('start 返回的 PID 文件存在', async () => { ... })
})
```
#### 2.4 Tail 测试
```typescript
describe('tailLog', () => {
test('输出已有日志内容', async () => { ... })
test('追加内容时实时输出', async () => { ... })
test('SIGINT 退出 tail', async () => { ... })
})
```
### 3. 集成验证脚本
可选: 在 `scripts/` 下添加一个手动验证脚本:
```bash
#!/bin/bash
# scripts/verify-daemon-restructure.sh
echo "=== 1. claude daemon status ==="
bun run dev -- daemon status
echo "=== 2. claude daemon bg (should start) ==="
bun run dev -- daemon bg --help
echo "=== 3. claude ps (deprecated) ==="
bun run dev -- ps 2>&1 | head -1
echo "=== 4. claude job list ==="
bun run dev -- job list
echo "=== 5. claude list (deprecated) ==="
bun run dev -- list 2>&1 | head -1
```
## 验证清单
- [ ] 旧命令全部正常工作 (仅多一行 stderr 警告)
- [ ] `--bg` 保持无警告
- [ ] 所有新增测试通过
- [ ] 现有 2695 个测试无回归
- [ ] tsc --noEmit 零错误
- [ ] 手动在 Windows + macOS/Linux 上验证关键路径

View File

@@ -1,147 +0,0 @@
# Tool 系统测试计划
## 概述
Tool 系统是 Claude Code 的核心,负责工具的定义、注册、发现和过滤。本计划覆盖 `src/Tool.ts` 中的工具接口与工具函数、`src/tools.ts` 中的注册/过滤逻辑,以及各工具目录下可独立测试的纯函数。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/Tool.ts` | `buildTool`, `toolMatchesName`, `findToolByName`, `getEmptyToolPermissionContext`, `filterToolProgressMessages` |
| `src/tools.ts` | `parseToolPreset`, `filterToolsByDenyRules`, `getAllBaseTools`, `getTools`, `assembleToolPool` |
| `src/tools/shared/gitOperationTracking.ts` | `parseGitCommitId`, `detectGitOperation` |
| `src/tools/shared/spawnMultiAgent.ts` | `resolveTeammateModel`, `generateUniqueTeammateName` |
| `src/tools/GrepTool/GrepTool.ts` | `applyHeadLimit`, `formatLimitInfo`(内部辅助函数) |
| `src/tools/FileEditTool/utils.ts` | 字符串匹配/补丁相关纯函数 |
---
## 测试用例
### src/Tool.ts
#### describe('buildTool')
- test('fills in default isEnabled as true') — 不传 isEnabled 时,构建的 tool.isEnabled() 应返回 true
- test('fills in default isConcurrencySafe as false') — 默认值应为 falsefail-closed
- test('fills in default isReadOnly as false') — 默认假设有写操作
- test('fills in default isDestructive as false') — 默认非破坏性
- test('fills in default checkPermissions as allow') — 默认 checkPermissions 应返回 `{ behavior: 'allow', updatedInput }`
- test('fills in default userFacingName from tool name') — userFacingName 默认应返回 tool.name
- test('preserves explicitly provided methods') — 传入自定义 isEnabled 等方法时应覆盖默认值
- test('preserves all non-defaultable properties') — name, inputSchema, call, description 等属性原样保留
#### describe('toolMatchesName')
- test('returns true for exact name match') — `{ name: 'Bash' }` 匹配 'Bash'
- test('returns false for non-matching name') — `{ name: 'Bash' }` 不匹配 'Read'
- test('returns true when name matches an alias') — `{ name: 'Bash', aliases: ['BashTool'] }` 匹配 'BashTool'
- test('returns false when aliases is undefined') — `{ name: 'Bash' }` 不匹配 'BashTool'
- test('returns false when aliases is empty') — `{ name: 'Bash', aliases: [] }` 不匹配 'BashTool'
#### describe('findToolByName')
- test('finds tool by primary name') — 从 tools 列表中按 name 找到工具
- test('finds tool by alias') — 从 tools 列表中按 alias 找到工具
- test('returns undefined when no match') — 找不到时返回 undefined
- test('returns first match when duplicates exist') — 多个同名工具时返回第一个
#### describe('getEmptyToolPermissionContext')
- test('returns default permission mode') — mode 应为 'default'
- test('returns empty maps and arrays') — additionalWorkingDirectories 为空 Maprules 为空对象
- test('returns isBypassPermissionsModeAvailable as false')
#### describe('filterToolProgressMessages')
- test('filters out hook_progress messages') — 移除 type 为 hook_progress 的消息
- test('keeps tool progress messages') — 保留非 hook_progress 的消息
- test('returns empty array for empty input')
- test('handles messages without type field') — data 不含 type 时应保留
---
### src/tools.ts
#### describe('parseToolPreset')
- test('returns "default" for "default" input') — 精确匹配
- test('returns "default" for "Default" input') — 大小写不敏感
- test('returns null for unknown preset') — 未知字符串返回 null
- test('returns null for empty string')
#### describe('filterToolsByDenyRules')
- test('returns all tools when no deny rules') — 空 deny 规则不过滤任何工具
- test('filters out tools matching blanket deny rule') — deny rule `{ toolName: 'Bash' }` 应移除 Bash
- test('does not filter tools with content-specific deny rules') — deny rule `{ toolName: 'Bash', ruleContent: 'rm -rf' }` 不移除 Bash只在运行时阻止特定命令
- test('filters MCP tools by server name prefix') — deny rule `mcp__server` 应移除该 server 下所有工具
- test('preserves tools not matching any deny rule')
#### describe('getAllBaseTools')
- test('returns a non-empty array of tools') — 至少包含核心工具
- test('each tool has required properties') — 每个工具应有 name, inputSchema, call 等属性
- test('includes BashTool, FileReadTool, FileEditTool') — 核心工具始终存在
- test('includes TestingPermissionTool when NODE_ENV is test') — 需设置 env
#### describe('getTools')
- test('returns filtered tools based on permission context') — 根据 deny rules 过滤
- test('returns simple tools in CLAUDE_CODE_SIMPLE mode') — 仅返回 Bash/Read/Edit
- test('filters disabled tools via isEnabled') — isEnabled 返回 false 的工具被排除
---
### src/tools/shared/gitOperationTracking.ts
#### describe('parseGitCommitId')
- test('extracts commit hash from git commit output') — 从 `[main abc1234] message` 中提取 `abc1234`
- test('returns null for non-commit output') — 无法解析时返回 null
- test('handles various branch name formats') — `[feature/foo abc1234]`
#### describe('detectGitOperation')
- test('detects git commit operation') — 命令含 `git commit` 时识别为 commit
- test('detects git push operation') — 命令含 `git push` 时识别
- test('returns null for non-git commands') — 非 git 命令返回 null
- test('detects git merge operation')
- test('detects git rebase operation')
---
### src/tools/shared/spawnMultiAgent.ts
#### describe('resolveTeammateModel')
- test('returns specified model when provided')
- test('falls back to default model when not specified')
#### describe('generateUniqueTeammateName')
- test('generates a name when no existing names') — 无冲突时返回基础名
- test('appends suffix when name conflicts') — 与已有名称冲突时添加后缀
- test('handles multiple conflicts') — 多次冲突时递增后缀
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| `bun:bundle` (feature) | 已 polyfill 为 `() => false` | 不需额外 mock |
| `process.env` | `bun:test` mock | 测试 `USER_TYPE``NODE_ENV``CLAUDE_CODE_SIMPLE` |
| `getDenyRuleForTool` | mock module | `filterToolsByDenyRules` 测试中需控制返回值 |
| `isToolSearchEnabledOptimistic` | mock module | `getAllBaseTools` 中条件加载 |
## 集成测试场景
放在 `tests/integration/tool-chain.test.ts`
### describe('Tool registration and discovery')
- test('getAllBaseTools returns tools that can be found by findToolByName') — 注册 → 查找完整链路
- test('filterToolsByDenyRules + getTools produces consistent results') — 过滤管线一致性
- test('assembleToolPool deduplicates built-in and MCP tools') — 合并去重逻辑

View File

@@ -1,416 +0,0 @@
# 工具函数(纯函数)测试计划
## 概述
覆盖 `src/utils/` 下所有可独立单元测试的纯函数。这些函数无外部依赖,输入输出确定性强,是测试金字塔的底层基石。
## 被测文件
| 文件 | 状态 | 关键导出 |
|------|------|----------|
| `src/utils/array.ts` | **已有测试** | intersperse, count, uniq |
| `src/utils/set.ts` | **已有测试** | difference, intersects, every, union |
| `src/utils/xml.ts` | 待测 | escapeXml, escapeXmlAttr |
| `src/utils/hash.ts` | 待测 | djb2Hash, hashContent, hashPair |
| `src/utils/stringUtils.ts` | 待测 | escapeRegExp, capitalize, plural, firstLineOf, countCharInString, normalizeFullWidthDigits, normalizeFullWidthSpace, safeJoinLines, truncateToLines, EndTruncatingAccumulator |
| `src/utils/semver.ts` | 待测 | gt, gte, lt, lte, satisfies, order |
| `src/utils/uuid.ts` | 待测 | validateUuid, createAgentId |
| `src/utils/format.ts` | 待测 | formatFileSize, formatSecondsShort, formatDuration, formatNumber, formatTokens, formatRelativeTime, formatRelativeTimeAgo |
| `src/utils/json.ts` | 待测 | safeParseJSON, safeParseJSONC, parseJSONL, addItemToJSONCArray |
| `src/utils/truncate.ts` | 待测 | truncatePathMiddle, truncateToWidth, truncateStartToWidth, truncateToWidthNoEllipsis, truncate, wrapText |
| `src/utils/diff.ts` | 待测 | adjustHunkLineNumbers, getPatchFromContents |
| `src/utils/frontmatterParser.ts` | 待测 | parseFrontmatter, splitPathInFrontmatter, parsePositiveIntFromFrontmatter, parseBooleanFrontmatter, parseShellFrontmatter |
| `src/utils/file.ts` | 待测(纯函数部分) | convertLeadingTabsToSpaces, addLineNumbers, stripLineNumberPrefix, pathsEqual, normalizePathForComparison |
| `src/utils/glob.ts` | 待测(纯函数部分) | extractGlobBaseDirectory |
| `src/utils/tokens.ts` | 待测 | getTokenCountFromUsage |
| `src/utils/path.ts` | 待测(纯函数部分) | containsPathTraversal, normalizePathForConfigKey |
---
## 测试用例
### src/utils/xml.ts — 测试文件: `src/utils/__tests__/xml.test.ts`
#### describe('escapeXml')
- test('escapes ampersand') — `&``&amp;`
- test('escapes less-than') — `<``&lt;`
- test('escapes greater-than') — `>``&gt;`
- test('does not escape quotes') — `"``'` 保持原样
- test('handles empty string') — `""``""`
- test('handles string with no special chars') — `"hello"` 原样返回
- test('escapes multiple special chars in one string') — `<a & b>``&lt;a &amp; b&gt;`
#### describe('escapeXmlAttr')
- test('escapes all xml chars plus quotes') — `"``&quot;`, `'``&apos;`
- test('escapes double quotes') — `he said "hi"` 正确转义
- test('escapes single quotes') — `it's` 正确转义
---
### src/utils/hash.ts — 测试文件: `src/utils/__tests__/hash.test.ts`
#### describe('djb2Hash')
- test('returns consistent hash for same input') — 相同输入返回相同结果
- test('returns different hashes for different inputs') — 不同输入大概率不同
- test('returns a 32-bit integer') — 结果在 int32 范围内
- test('handles empty string') — 空字符串有确定的哈希值
- test('handles unicode strings') — 中文/emoji 等正确处理
#### describe('hashContent')
- test('returns consistent hash for same content') — 确定性
- test('returns string result') — 返回值为字符串
#### describe('hashPair')
- test('returns consistent hash for same pair') — 确定性
- test('order matters') — hashPair(a, b) ≠ hashPair(b, a)
- test('handles empty strings')
---
### src/utils/stringUtils.ts — 测试文件: `src/utils/__tests__/stringUtils.test.ts`
#### describe('escapeRegExp')
- test('escapes dots') — `.``\\.`
- test('escapes asterisks') — `*``\\*`
- test('escapes brackets') — `[``\\[`
- test('escapes all special chars') — `.*+?^${}()|[]\` 全部转义
- test('leaves normal chars unchanged') — `hello` 原样
- test('escaped string works in RegExp') — `new RegExp(escapeRegExp('a.b'))` 精确匹配 `a.b`
#### describe('capitalize')
- test('uppercases first char') — `"foo"``"Foo"`
- test('does NOT lowercase rest') — `"fooBar"``"FooBar"`(区别于 lodash capitalize
- test('handles single char') — `"a"``"A"`
- test('handles empty string') — `""``""`
- test('handles already capitalized') — `"Foo"``"Foo"`
#### describe('plural')
- test('returns singular for n=1') — `plural(1, 'file')``'file'`
- test('returns plural for n=0') — `plural(0, 'file')``'files'`
- test('returns plural for n>1') — `plural(3, 'file')``'files'`
- test('uses custom plural form') — `plural(2, 'entry', 'entries')``'entries'`
#### describe('firstLineOf')
- test('returns first line of multi-line string') — `"a\nb\nc"``"a"`
- test('returns full string when no newline') — `"hello"``"hello"`
- test('handles empty string') — `""``""`
- test('handles string starting with newline') — `"\nhello"``""`
#### describe('countCharInString')
- test('counts occurrences') — `countCharInString("aabac", "a")``3`
- test('returns 0 when char not found') — `countCharInString("hello", "x")``0`
- test('handles empty string') — `countCharInString("", "a")``0`
- test('respects start position') — `countCharInString("aaba", "a", 2)``1`
#### describe('normalizeFullWidthDigits')
- test('converts full-width digits to half-width') — `""``"0123"`
- test('leaves half-width digits unchanged') — `"0123"``"0123"`
- test('mixed content') — `"port "``"port 8080"`
#### describe('normalizeFullWidthSpace')
- test('converts ideographic space to regular space') — `"\u3000"``" "`
- test('converts multiple spaces') — `"a\u3000b\u3000c"``"a b c"`
#### describe('safeJoinLines')
- test('joins lines with default delimiter') — `["a","b"]``"a,b"`
- test('truncates when exceeding maxSize') — 超限时截断并添加 `...[truncated]`
- test('handles empty array') — `[]``""`
- test('uses custom delimiter') — delimiter 为 `"\n"` 时按行连接
#### describe('truncateToLines')
- test('returns full text when within limit') — 行数不超限时原样返回
- test('truncates and adds ellipsis') — 超限时截断并加 `…`
- test('handles exact limit') — 刚好等于 maxLines 时不截断
- test('handles single line') — 单行文本不截断
#### describe('EndTruncatingAccumulator')
- test('accumulates strings normally within limit')
- test('truncates when exceeding maxSize')
- test('reports truncated status correctly')
- test('reports totalBytes including truncated content')
- test('toString includes truncation marker')
- test('clear resets all state')
- test('append with Buffer works') — 接受 Buffer 类型
---
### src/utils/semver.ts — 测试文件: `src/utils/__tests__/semver.test.ts`
#### describe('gt / gte / lt / lte')
- test('gt: 2.0.0 > 1.0.0') → true
- test('gt: 1.0.0 > 1.0.0') → false
- test('gte: 1.0.0 >= 1.0.0') → true
- test('lt: 1.0.0 < 2.0.0') → true
- test('lte: 1.0.0 <= 1.0.0') → true
- test('handles pre-release versions') — `1.0.0-beta < 1.0.0`
#### describe('satisfies')
- test('version satisfies caret range') — `satisfies('1.2.3', '^1.0.0')` → true
- test('version does not satisfy range') — `satisfies('2.0.0', '^1.0.0')` → false
- test('exact match') — `satisfies('1.0.0', '1.0.0')` → true
#### describe('order')
- test('returns -1 for lesser') — `order('1.0.0', '2.0.0')` → -1
- test('returns 0 for equal') — `order('1.0.0', '1.0.0')` → 0
- test('returns 1 for greater') — `order('2.0.0', '1.0.0')` → 1
---
### src/utils/uuid.ts — 测试文件: `src/utils/__tests__/uuid.test.ts`
#### describe('validateUuid')
- test('accepts valid v4 UUID') — `'550e8400-e29b-41d4-a716-446655440000'` → 返回 UUID
- test('returns null for invalid format') — `'not-a-uuid'` → null
- test('returns null for empty string') — `''` → null
- test('returns null for null/undefined input')
- test('accepts uppercase UUIDs') — 大写字母有效
#### describe('createAgentId')
- test('returns string starting with "a"') — 前缀为 `a`
- test('has correct length') — 前缀 + 16 hex 字符
- test('generates unique ids') — 连续两次调用结果不同
---
### src/utils/format.ts — 测试文件: `src/utils/__tests__/format.test.ts`
#### describe('formatFileSize')
- test('formats bytes') — `500``"500 bytes"`
- test('formats kilobytes') — `1536``"1.5KB"`
- test('formats megabytes') — `1572864``"1.5MB"`
- test('formats gigabytes') — `1610612736``"1.5GB"`
- test('removes trailing .0') — `1024``"1KB"` (不是 `"1.0KB"`)
#### describe('formatSecondsShort')
- test('formats milliseconds to seconds') — `1234``"1.2s"`
- test('formats zero') — `0``"0.0s"`
#### describe('formatDuration')
- test('formats seconds') — `5000``"5s"`
- test('formats minutes and seconds') — `65000``"1m 5s"`
- test('formats hours') — `3661000``"1h 1m 1s"`
- test('formats days') — `90061000``"1d 1h 1m"`
- test('returns "0s" for zero') — `0``"0s"`
- test('hideTrailingZeros omits zero components') — `3600000` + `hideTrailingZeros``"1h"`
- test('mostSignificantOnly returns largest unit') — `3661000` + `mostSignificantOnly``"1h"`
#### describe('formatNumber')
- test('formats thousands') — `1321``"1.3k"`
- test('formats small numbers as-is') — `900``"900"`
- test('lowercase output') — `1500``"1.5k"` (不是 `"1.5K"`)
#### describe('formatTokens')
- test('strips .0 suffix') — `1000``"1k"` (不是 `"1.0k"`)
- test('keeps non-zero decimal') — `1500``"1.5k"`
#### describe('formatRelativeTime')
- test('formats past time') — now - 3600s → `"1h ago"` (narrow style)
- test('formats future time') — now + 3600s → `"in 1h"` (narrow style)
- test('formats less than 1 second') — now → `"0s ago"`
- test('uses custom now parameter for deterministic output')
---
### src/utils/json.ts — 测试文件: `src/utils/__tests__/json.test.ts`
#### describe('safeParseJSON')
- test('parses valid JSON') — `'{"a":1}'``{ a: 1 }`
- test('returns null for invalid JSON') — `'not json'` → null
- test('returns null for null input') — `null` → null
- test('returns null for undefined input') — `undefined` → null
- test('returns null for empty string') — `""` → null
- test('handles JSON with BOM') — BOM 前缀不影响解析
- test('caches results for repeated calls') — 同一输入不重复解析
#### describe('safeParseJSONC')
- test('parses JSON with comments') — 含 `//` 注释的 JSON 正确解析
- test('parses JSON with trailing commas') — 宽松模式
- test('returns null for invalid input')
- test('returns null for null input')
#### describe('parseJSONL')
- test('parses multiple JSON lines') — `'{"a":1}\n{"b":2}'``[{a:1}, {b:2}]`
- test('skips malformed lines') — 含错误行时跳过该行
- test('handles empty input') — `""``[]`
- test('handles trailing newline') — 尾部换行不产生空元素
- test('accepts Buffer input') — Buffer 类型同样工作
- test('handles BOM prefix')
#### describe('addItemToJSONCArray')
- test('adds item to existing array') — `[1, 2]` + 3 → `[1, 2, 3]`
- test('creates new array for empty content') — `""` + item → `[item]`
- test('creates new array for non-array content') — `'"hello"'` + item → `[item]`
- test('preserves comments in JSONC') — 注释不被丢弃
- test('handles empty array') — `"[]"` + item → `[item]`
---
### src/utils/diff.ts — 测试文件: `src/utils/__tests__/diff.test.ts`
#### describe('adjustHunkLineNumbers')
- test('shifts line numbers by positive offset') — 所有 hunk 的 oldStart/newStart 增加 offset
- test('shifts by negative offset') — 负 offset 减少行号
- test('handles empty hunk array') — `[]``[]`
#### describe('getPatchFromContents')
- test('returns empty array for identical content') — 相同内容无差异
- test('detects added lines') — 新内容多出行
- test('detects removed lines') — 旧内容缺少行
- test('detects modified lines') — 行内容变化
- test('handles empty old content') — 从空文件到有内容
- test('handles empty new content') — 删除所有内容
---
### src/utils/frontmatterParser.ts — 测试文件: `src/utils/__tests__/frontmatterParser.test.ts`
#### describe('parseFrontmatter')
- test('extracts YAML frontmatter between --- delimiters') — 正确提取 frontmatter 并返回 body
- test('returns empty frontmatter for content without ---') — 无 frontmatter 时 data 为空
- test('handles empty content') — `""` 正确处理
- test('handles frontmatter-only content') — 只有 frontmatter 无 body
- test('falls back to quoting on YAML parse error') — 无效 YAML 不崩溃
#### describe('splitPathInFrontmatter')
- test('splits comma-separated paths') — `"a.ts, b.ts"``["a.ts", "b.ts"]`
- test('expands brace patterns') — `"*.{ts,tsx}"``["*.ts", "*.tsx"]`
- test('handles string array input') — `["a.ts", "b.ts"]``["a.ts", "b.ts"]`
- test('respects braces in comma splitting') — 大括号内的逗号不作为分隔符
#### describe('parsePositiveIntFromFrontmatter')
- test('returns number for valid positive int') — `5``5`
- test('returns undefined for negative') — `-1` → undefined
- test('returns undefined for non-number') — `"abc"` → undefined
- test('returns undefined for float') — `1.5` → undefined
#### describe('parseBooleanFrontmatter')
- test('returns true for true') — `true` → true
- test('returns true for "true"') — `"true"` → true
- test('returns false for false') — `false` → false
- test('returns false for other values') — `"yes"`, `1` → false
#### describe('parseShellFrontmatter')
- test('returns bash for "bash"') — 正确识别
- test('returns powershell for "powershell"')
- test('returns undefined for invalid value') — `"zsh"` → undefined
---
### src/utils/file.ts纯函数部分— 测试文件: `src/utils/__tests__/file.test.ts`
#### describe('convertLeadingTabsToSpaces')
- test('converts single tab to 2 spaces') — `"\thello"``" hello"`
- test('converts multiple leading tabs') — `"\t\thello"``" hello"`
- test('does not convert tabs within line') — `"a\tb"` 保持原样
- test('handles mixed content')
#### describe('addLineNumbers')
- test('adds line numbers starting from 1') — 每行添加 `N\t` 前缀
- test('respects startLine parameter') — 从指定行号开始
- test('handles empty content')
#### describe('stripLineNumberPrefix')
- test('strips tab-prefixed line number') — `"1\thello"``"hello"`
- test('strips padded line number') — `" 1\thello"``"hello"`
- test('returns line unchanged when no prefix')
#### describe('pathsEqual')
- test('returns true for identical paths')
- test('handles trailing slashes') — 带/不带尾部斜杠视为相同
- test('handles case sensitivity based on platform')
#### describe('normalizePathForComparison')
- test('normalizes forward slashes')
- test('resolves path for comparison')
---
### src/utils/glob.ts纯函数部分— 测试文件: `src/utils/__tests__/glob.test.ts`
#### describe('extractGlobBaseDirectory')
- test('extracts static prefix from glob') — `"src/**/*.ts"``{ baseDir: "src", relativePattern: "**/*.ts" }`
- test('handles root-level glob') — `"*.ts"``{ baseDir: ".", relativePattern: "*.ts" }`
- test('handles deep static path') — `"src/utils/model/*.ts"` → baseDir 为 `"src/utils/model"`
- test('handles Windows drive root') — `"C:\\Users\\**\\*.ts"` 正确分割
---
### src/utils/tokens.ts纯函数部分— 测试文件: `src/utils/__tests__/tokens.test.ts`
#### describe('getTokenCountFromUsage')
- test('sums input and output tokens') — `{ input_tokens: 100, output_tokens: 50 }` → 150
- test('includes cache tokens') — cache_creation + cache_read 加入总数
- test('handles zero values') — 全 0 时返回 0
---
### src/utils/path.ts纯函数部分— 测试文件: `src/utils/__tests__/path.test.ts`
#### describe('containsPathTraversal')
- test('detects ../ traversal') — `"../etc/passwd"` → true
- test('detects mid-path traversal') — `"foo/../../bar"` → true
- test('returns false for safe paths') — `"src/utils/file.ts"` → false
- test('returns false for paths containing .. in names') — `"foo..bar"` → false
#### describe('normalizePathForConfigKey')
- test('converts backslashes to forward slashes') — `"src\\utils"``"src/utils"`
- test('leaves forward slashes unchanged')
---
## Mock 需求
本计划中的函数大部分为纯函数,**不需要 mock**。少数例外:
| 函数 | 依赖 | 处理 |
|------|------|------|
| `hashContent` / `hashPair` | `Bun.hash` | Bun 运行时下自动可用 |
| `formatRelativeTime` | `Date` | 使用 `now` 参数注入确定性时间 |
| `safeParseJSON` | `logError` | 可通过 `shouldLogError: false` 跳过 |
| `safeParseJSONC` | `logError` | mock `logError` 避免测试输出噪音 |

View File

@@ -1,134 +0,0 @@
# Context 构建测试计划
## 概述
Context 构建系统负责组装发送给 Claude API 的系统提示和用户上下文。包括 git 状态获取、CLAUDE.md 文件发现与加载、系统提示拼装三部分。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/context.ts` | `getSystemContext`, `getUserContext`, `getGitStatus`, `setSystemPromptInjection` |
| `src/utils/claudemd.ts` | `stripHtmlComments`, `getClaudeMds`, `isMemoryFilePath`, `getLargeMemoryFiles`, `filterInjectedMemoryFiles`, `getExternalClaudeMdIncludes`, `hasExternalClaudeMdIncludes`, `processMemoryFile`, `getMemoryFiles` |
| `src/utils/systemPrompt.ts` | `buildEffectiveSystemPrompt` |
---
## 测试用例
### src/utils/claudemd.ts — 纯函数部分
#### describe('stripHtmlComments')
- test('strips block-level HTML comments') — `"text <!-- comment --> more"` → content 不含注释
- test('preserves inline content') — 行内文本保留
- test('preserves code block content') — ` ```html\n<!-- not stripped -->\n``` ` 内的注释不移除
- test('returns stripped: false when no comments') — 无注释时 stripped 为 false
- test('returns stripped: true when comments exist')
- test('handles empty string') — `""``{ content: "", stripped: false }`
- test('handles multiple comments') — 多个注释全部移除
#### describe('getClaudeMds')
- test('assembles memory files with type descriptions') — 不同 type 的文件有不同前缀描述
- test('includes instruction prompt prefix') — 输出包含指令前缀
- test('handles empty memory files array') — 空数组返回空字符串或最小前缀
- test('respects filter parameter') — filter 函数可过滤特定类型
- test('concatenates multiple files with separators')
#### describe('isMemoryFilePath')
- test('returns true for CLAUDE.md path') — `"/project/CLAUDE.md"` → true
- test('returns true for .claude/rules/ path') — `"/project/.claude/rules/foo.md"` → true
- test('returns true for memory file path') — `"~/.claude/memory/foo.md"` → true
- test('returns false for regular file') — `"/project/src/main.ts"` → false
- test('returns false for unrelated .md file') — `"/project/README.md"` → false
#### describe('getLargeMemoryFiles')
- test('returns files exceeding 40K chars') — 内容 > MAX_MEMORY_CHARACTER_COUNT 的文件被返回
- test('returns empty array when all files are small')
- test('correctly identifies threshold boundary')
#### describe('filterInjectedMemoryFiles')
- test('filters out AutoMem type files') — feature flag 开启时移除自动记忆
- test('filters out TeamMem type files')
- test('preserves other types') — 非 AutoMem/TeamMem 的文件保留
#### describe('getExternalClaudeMdIncludes')
- test('returns includes from outside CWD') — 外部 @include 路径被识别
- test('returns empty array when all includes are internal')
#### describe('hasExternalClaudeMdIncludes')
- test('returns true when external includes exist')
- test('returns false when no external includes')
---
### src/utils/systemPrompt.ts
#### describe('buildEffectiveSystemPrompt')
- test('returns default system prompt when no overrides') — 无任何覆盖时使用默认提示
- test('overrideSystemPrompt replaces everything') — override 模式替换全部内容
- test('customSystemPrompt replaces default') — `--system-prompt` 参数替换默认
- test('appendSystemPrompt is appended after main prompt') — append 在主提示之后
- test('agent definition replaces default prompt') — agent 模式使用 agent prompt
- test('agent definition with append combines both') — agent prompt + append
- test('override takes precedence over agent and custom') — 优先级最高
- test('returns array of strings') — 返回值为 SystemPrompt 类型(字符串数组)
---
### src/context.ts — 需 Mock 的部分
#### describe('getGitStatus')
- test('returns formatted git status string') — 包含 branch、status、log、user
- test('truncates status at 2000 chars') — 超长 status 被截断
- test('returns null in test environment') — `NODE_ENV=test` 时返回 null
- test('returns null in non-git directory') — 非 git 仓库返回 null
- test('runs git commands in parallel') — 多个 git 命令并行执行
#### describe('getSystemContext')
- test('includes gitStatus key') — 返回对象包含 gitStatus
- test('returns memoized result on subsequent calls') — 多次调用返回同一结果
- test('skips git when instructions disabled')
#### describe('getUserContext')
- test('includes currentDate key') — 返回对象包含当前日期
- test('includes claudeMd key when CLAUDE.md exists') — 加载 CLAUDE.md 内容
- test('respects CLAUDE_CODE_DISABLE_CLAUDE_MDS env') — 设置后不加载 CLAUDE.md
- test('returns memoized result')
#### describe('setSystemPromptInjection')
- test('clears memoized context caches') — 调用后下次 getSystemContext/getUserContext 重新计算
- test('injection value is accessible via getSystemPromptInjection')
---
## Mock 需求
| 依赖 | Mock 方式 | 用途 |
|------|-----------|------|
| `execFileNoThrow` | `mock.module` | `getGitStatus` 中的 git 命令 |
| `getMemoryFiles` | `mock.module` | `getUserContext` 中的 CLAUDE.md 加载 |
| `getCwd` | `mock.module` | 路径解析上下文 |
| `process.env.NODE_ENV` | 直接设置 | 测试环境检测 |
| `process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS` | 直接设置 | 禁用 CLAUDE.md |
## 集成测试场景
放在 `tests/integration/context-build.test.ts`
### describe('Context assembly pipeline')
- test('getUserContext produces claudeMd containing CLAUDE.md content') — 端到端验证 CLAUDE.md 被正确加载到 context
- test('buildEffectiveSystemPrompt + getUserContext produces complete prompt') — 系统提示 + 用户上下文完整性
- test('setSystemPromptInjection invalidates and rebuilds context') — 注入后重新构建上下文

View File

@@ -1,104 +0,0 @@
# 权限系统测试计划
## 概述
权限系统控制工具是否可以执行,包含规则解析器、权限检查管线和权限模式判断。测试重点是纯函数解析器和规则匹配逻辑。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/utils/permissions/permissionRuleParser.ts` | `permissionRuleValueFromString`, `permissionRuleValueToString`, `escapeRuleContent`, `unescapeRuleContent`, `normalizeLegacyToolName`, `getLegacyToolNames` |
| `src/utils/permissions/PermissionMode.ts` | 权限模式常量和辅助函数 |
| `src/utils/permissions/permissions.ts` | `hasPermissionsToUseTool`, `getDenyRuleForTool`, `checkRuleBasedPermissions` |
| `src/types/permissions.ts` | `PermissionMode`, `PermissionBehavior`, `PermissionRule` 类型定义 |
---
## 测试用例
### src/utils/permissions/permissionRuleParser.ts
#### describe('escapeRuleContent')
- test('escapes backslashes first') — `'test\\value'``'test\\\\value'`
- test('escapes opening parentheses') — `'print(1)'``'print\\(1\\)'`
- test('escapes closing parentheses') — `'func()'``'func\\(\\)'`
- test('handles combined escape') — `'echo "test\\nvalue"'` 中的 `\\` 先转义
- test('handles empty string') — `''``''`
- test('no-op for string without special chars') — `'npm install'` 原样返回
#### describe('unescapeRuleContent')
- test('unescapes parentheses') — `'print\\(1\\)'``'print(1)'`
- test('unescapes backslashes last') — `'test\\\\nvalue'``'test\\nvalue'`
- test('handles empty string')
- test('roundtrip: escape then unescape returns original') — `unescapeRuleContent(escapeRuleContent(x)) === x`
#### describe('permissionRuleValueFromString')
- test('parses tool name only') — `'Bash'``{ toolName: 'Bash' }`
- test('parses tool name with content') — `'Bash(npm install)'``{ toolName: 'Bash', ruleContent: 'npm install' }`
- test('parses content with escaped parentheses') — `'Bash(python -c "print\\(1\\)")'` → ruleContent 为 `'python -c "print(1)"'`
- test('treats empty parens as tool-wide rule') — `'Bash()'``{ toolName: 'Bash' }`(无 ruleContent
- test('treats wildcard content as tool-wide rule') — `'Bash(*)'``{ toolName: 'Bash' }`
- test('normalizes legacy tool names') — `'Task'``{ toolName: 'Agent' }`(或对应的 AGENT_TOOL_NAME
- test('handles malformed input: no closing paren') — `'Bash(npm'` → 整个字符串作为 toolName
- test('handles malformed input: content after closing paren') — `'Bash(npm)extra'` → 整个字符串作为 toolName
- test('handles missing tool name') — `'(foo)'` → 整个字符串作为 toolName
#### describe('permissionRuleValueToString')
- test('serializes tool name only') — `{ toolName: 'Bash' }``'Bash'`
- test('serializes with content') — `{ toolName: 'Bash', ruleContent: 'npm install' }``'Bash(npm install)'`
- test('escapes content with parentheses') — ruleContent 含 `()` 时正确转义
- test('roundtrip: fromString then toString preserves value') — 往返一致
#### describe('normalizeLegacyToolName')
- test('maps Task to Agent tool name') — `'Task'` → AGENT_TOOL_NAME
- test('maps KillShell to TaskStop tool name') — `'KillShell'` → TASK_STOP_TOOL_NAME
- test('maps AgentOutputTool to TaskOutput tool name')
- test('returns unknown names unchanged') — `'UnknownTool'``'UnknownTool'`
#### describe('getLegacyToolNames')
- test('returns legacy names for canonical name') — 给定 AGENT_TOOL_NAME 返回包含 `'Task'`
- test('returns empty array for name with no legacy aliases')
---
### src/utils/permissions/permissions.ts — 需 Mock
#### describe('getDenyRuleForTool')
- test('returns deny rule matching tool name') — 匹配到 blanket deny 规则时返回
- test('returns null when no deny rules match') — 无匹配时返回 null
- test('matches MCP tools by server prefix') — `mcp__server` 规则匹配该 server 下的 MCP 工具
- test('does not match content-specific deny rules') — 有 ruleContent 的 deny 规则不作为 blanket deny
#### describe('checkRuleBasedPermissions')(集成级别)
- test('deny rule takes precedence over allow') — 同时有 allow 和 deny 时 deny 优先
- test('ask rule prompts user') — 匹配 ask 规则返回 `{ behavior: 'ask' }`
- test('allow rule permits execution') — 匹配 allow 规则返回 `{ behavior: 'allow' }`
- test('passthrough when no rules match') — 无匹配规则返回 passthrough
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| `bun:bundle` (feature) | 已 polyfill | BRIEF_TOOL_NAME 条件加载 |
| Tool 常量导入 | 实际值 | AGENT_TOOL_NAME 等从常量文件导入 |
| `appState` | mock object | `hasPermissionsToUseTool` 中的状态依赖 |
| Tool 对象 | mock object | 模拟 tool 的 name, checkPermissions 等 |
## 集成测试场景
### describe('Permission pipeline end-to-end')
- test('deny rule blocks tool before it runs') — deny 规则在 call 前拦截
- test('bypassPermissions mode allows all') — bypass 模式下 ask → allow
- test('dontAsk mode converts ask to deny') — dontAsk 模式下 ask → deny

View File

@@ -1,113 +0,0 @@
# 模型路由测试计划
## 概述
模型路由系统负责 API provider 选择、模型别名解析、模型名规范化和运行时模型决策。测试重点是纯函数和环境变量驱动的逻辑。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/utils/model/aliases.ts` | `MODEL_ALIASES`, `MODEL_FAMILY_ALIASES`, `isModelAlias`, `isModelFamilyAlias` |
| `src/utils/model/providers.ts` | `APIProvider`, `getAPIProvider`, `isFirstPartyAnthropicBaseUrl` |
| `src/utils/model/model.ts` | `firstPartyNameToCanonical`, `getCanonicalName`, `parseUserSpecifiedModel`, `normalizeModelStringForAPI`, `getRuntimeMainLoopModel`, `getDefaultMainLoopModelSetting` |
---
## 测试用例
### src/utils/model/aliases.ts
#### describe('isModelAlias')
- test('returns true for "sonnet"') — 有效别名
- test('returns true for "opus"')
- test('returns true for "haiku"')
- test('returns true for "best"')
- test('returns true for "sonnet[1m]"')
- test('returns true for "opus[1m]"')
- test('returns true for "opusplan"')
- test('returns false for full model ID') — `'claude-sonnet-4-6-20250514'` → false
- test('returns false for unknown string') — `'gpt-4'` → false
- test('is case-sensitive') — `'Sonnet'` → false别名是小写
#### describe('isModelFamilyAlias')
- test('returns true for "sonnet"')
- test('returns true for "opus"')
- test('returns true for "haiku"')
- test('returns false for "best"') — best 不是 family alias
- test('returns false for "opusplan"')
- test('returns false for "sonnet[1m]"')
---
### src/utils/model/providers.ts
#### describe('getAPIProvider')
- test('returns "firstParty" by default') — 无相关 env 时返回 firstParty
- test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set') — env 为 truthy 值
- test('returns "vertex" when CLAUDE_CODE_USE_VERTEX is set')
- test('returns "foundry" when CLAUDE_CODE_USE_FOUNDRY is set')
- test('bedrock takes precedence over vertex') — 多个 env 同时设置时 bedrock 优先
#### describe('isFirstPartyAnthropicBaseUrl')
- test('returns true when ANTHROPIC_BASE_URL is not set') — 默认 API
- test('returns true for api.anthropic.com') — `'https://api.anthropic.com'` → true
- test('returns false for custom URL') — `'https://my-proxy.com'` → false
- test('returns false for invalid URL') — 非法 URL → false
- test('returns true for staging URL when USER_TYPE is ant') — `'https://api-staging.anthropic.com'` + ant → true
---
### src/utils/model/model.ts
#### describe('firstPartyNameToCanonical')
- test('maps opus-4-6 full name to canonical') — `'claude-opus-4-6-20250514'``'claude-opus-4-6'`
- test('maps sonnet-4-6 full name') — `'claude-sonnet-4-6-20250514'``'claude-sonnet-4-6'`
- test('maps haiku-4-5') — `'claude-haiku-4-5-20251001'``'claude-haiku-4-5'`
- test('maps 3P provider format') — `'us.anthropic.claude-opus-4-6-v1:0'``'claude-opus-4-6'`
- test('maps claude-3-7-sonnet') — `'claude-3-7-sonnet-20250219'``'claude-3-7-sonnet'`
- test('maps claude-3-5-sonnet') → `'claude-3-5-sonnet'`
- test('maps claude-3-5-haiku') → `'claude-3-5-haiku'`
- test('maps claude-3-opus') → `'claude-3-opus'`
- test('is case insensitive') — `'Claude-Opus-4-6'``'claude-opus-4-6'`
- test('falls back to input for unknown model') — `'unknown-model'``'unknown-model'`
- test('differentiates opus-4 vs opus-4-5 vs opus-4-6') — 更具体的版本优先匹配
#### describe('parseUserSpecifiedModel')
- test('resolves "sonnet" to default sonnet model')
- test('resolves "opus" to default opus model')
- test('resolves "haiku" to default haiku model')
- test('resolves "best" to best model')
- test('resolves "opusplan" to default sonnet model') — opusplan 默认用 sonnet
- test('appends [1m] suffix when alias has [1m]') — `'sonnet[1m]'` → 模型名 + `'[1m]'`
- test('preserves original case for custom model names') — `'my-Custom-Model'` 保留大小写
- test('handles [1m] suffix on non-alias models') — `'custom-model[1m]'``'custom-model[1m]'`
- test('trims whitespace') — `' sonnet '` → 正确解析
#### describe('getRuntimeMainLoopModel')
- test('returns mainLoopModel by default') — 无特殊条件时原样返回
- test('returns opus in plan mode when opusplan is set') — opusplan + plan mode → opus
- test('returns sonnet in plan mode when haiku is set') — haiku + plan mode → sonnet 升级
- test('returns mainLoopModel in non-plan mode') — 非 plan 模式不做替换
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| `process.env.CLAUDE_CODE_USE_BEDROCK/VERTEX/FOUNDRY` | 直接设置/恢复 | provider 选择 |
| `process.env.ANTHROPIC_BASE_URL` | 直接设置/恢复 | URL 检测 |
| `process.env.USER_TYPE` | 直接设置/恢复 | staging URL 和 ant 功能 |
| `getModelStrings()` | mock.module | 返回固定模型 ID |
| `getMainLoopModelOverride` | mock.module | 会话中模型覆盖 |
| `getSettings_DEPRECATED` | mock.module | 用户设置中的模型 |
| `getUserSpecifiedModelSetting` | mock.module | `getRuntimeMainLoopModel` 依赖 |
| `isModelAllowed` | mock.module | allowlist 检查 |

View File

@@ -1,165 +0,0 @@
# 消息处理测试计划
## 概述
消息处理系统负责消息的创建、查询、规范化和文本提取。覆盖消息类型定义、消息工厂函数、消息过滤/查询工具和 API 规范化管线。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/types/message.ts` | `MessageType`, `Message`, `AssistantMessage`, `UserMessage`, `SystemMessage` 等类型 |
| `src/utils/messages.ts` | 消息创建、查询、规范化、文本提取等函数(~3100 行) |
| `src/utils/messages/mappers.ts` | 消息映射工具 |
---
## 测试用例
### src/utils/messages.ts — 消息创建
#### describe('createAssistantMessage')
- test('creates message with type "assistant"') — type 字段正确
- test('creates message with role "assistant"') — role 正确
- test('creates message with empty content array') — 默认 content 为空
- test('generates unique uuid') — 每次调用 uuid 不同
- test('includes costUsd as 0')
#### describe('createUserMessage')
- test('creates message with type "user"') — type 字段正确
- test('creates message with provided content') — content 正确传入
- test('generates unique uuid')
#### describe('createSystemMessage')
- test('creates system message with correct type')
- test('includes message content')
#### describe('createProgressMessage')
- test('creates progress message with data')
- test('has correct type "progress"')
---
### src/utils/messages.ts — 消息查询
#### describe('getLastAssistantMessage')
- test('returns last assistant message from array') — 多条消息中返回最后一条 assistant
- test('returns undefined for empty array')
- test('returns undefined when no assistant messages exist')
#### describe('hasToolCallsInLastAssistantTurn')
- test('returns true when last assistant has tool_use content') — content 含 tool_use block
- test('returns false when last assistant has only text')
- test('returns false for empty messages')
#### describe('isSyntheticMessage')
- test('identifies interrupt message as synthetic') — INTERRUPT_MESSAGE 标记
- test('identifies cancel message as synthetic')
- test('returns false for normal user messages')
#### describe('isNotEmptyMessage')
- test('returns true for message with content')
- test('returns false for message with empty content array')
- test('returns false for message with empty text content')
---
### src/utils/messages.ts — 文本提取
#### describe('getAssistantMessageText')
- test('extracts text from text blocks') — content 含 `{ type: 'text', text: 'hello' }` 时提取
- test('returns empty string for non-text content') — 仅含 tool_use 时返回空
- test('concatenates multiple text blocks')
#### describe('getUserMessageText')
- test('extracts text from string content') — content 为纯字符串
- test('extracts text from content array') — content 为数组时提取 text 块
- test('handles empty content')
#### describe('extractTextContent')
- test('extracts text items from mixed content') — 过滤出 type: 'text' 的项
- test('returns empty array for all non-text content')
---
### src/utils/messages.ts — 规范化
#### describe('normalizeMessages')
- test('converts raw messages to normalized format') — 消息数组规范化
- test('handles empty array') — `[]``[]`
- test('preserves message order')
- test('handles mixed message types')
#### describe('normalizeMessagesForAPI')
- test('filters out system messages') — 系统消息不发送给 API
- test('filters out progress messages')
- test('filters out attachment messages')
- test('preserves user and assistant messages')
- test('reorders tool results to match API expectations')
- test('handles empty array')
---
### src/utils/messages.ts — 合并
#### describe('mergeUserMessages')
- test('merges consecutive user messages') — 相邻用户消息合并
- test('does not merge non-consecutive user messages')
- test('preserves assistant messages between user messages')
#### describe('mergeAssistantMessages')
- test('merges consecutive assistant messages')
- test('combines content arrays')
---
### src/utils/messages.ts — 辅助函数
#### describe('buildMessageLookups')
- test('builds index by message uuid') — 按 uuid 建立查找表
- test('returns empty lookups for empty messages')
- test('handles duplicate uuids gracefully')
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| `crypto.randomUUID` | `mock` 或 spy | 消息创建中的 uuid 生成 |
| Message 对象 | 手动构造 | 创建符合类型的 mock 消息对象 |
### Mock 消息工厂(放在 `tests/mocks/messages.ts`
```typescript
// 通用 mock 消息构造器
export function mockAssistantMessage(overrides?: Partial<AssistantMessage>): AssistantMessage
export function mockUserMessage(content: string, overrides?: Partial<UserMessage>): UserMessage
export function mockSystemMessage(overrides?: Partial<SystemMessage>): SystemMessage
export function mockToolUseBlock(name: string, input: unknown): ToolUseBlock
export function mockToolResultMessage(toolUseId: string, content: string): UserMessage
```
## 集成测试场景
### describe('Message pipeline')
- test('create → normalize → API format produces valid request') — 创建消息 → normalizeMessagesForAPI → 验证输出结构
- test('tool use and tool result pairing is preserved through normalization')
- test('merge + normalize handles conversation with interruptions')

View File

@@ -1,112 +0,0 @@
# Cron 调度测试计划
## 概述
Cron 模块提供 cron 表达式解析、下次运行时间计算和人类可读描述。全部为纯函数,无外部依赖,是最适合单元测试的模块之一。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/utils/cron.ts` | `CronFields`, `parseCronExpression`, `computeNextCronRun`, `cronToHuman` |
---
## 测试用例
### describe('parseCronExpression')
#### 有效表达式
- test('parses wildcard fields') — `'* * * * *'` → 每个字段为完整范围
- test('parses specific values') — `'30 14 1 6 3'` → minute=[30], hour=[14], dom=[1], month=[6], dow=[3]
- test('parses step syntax') — `'*/5 * * * *'` → minute=[0,5,10,...,55]
- test('parses range syntax') — `'1-5 * * * *'` → minute=[1,2,3,4,5]
- test('parses range with step') — `'1-10/3 * * * *'` → minute=[1,4,7,10]
- test('parses comma-separated list') — `'1,15,30 * * * *'` → minute=[1,15,30]
- test('parses day-of-week 7 as Sunday alias') — `'0 0 * * 7'` → dow=[0]
- test('parses range with day-of-week 7') — `'0 0 * * 5-7'` → dow=[0,5,6]
- test('parses complex combined expression') — `'0,30 9-17 * * 1-5'` → 工作日 9-17 每半小时
#### 无效表达式
- test('returns null for wrong field count') — `'* * *'` → null
- test('returns null for out-of-range values') — `'60 * * * *'` → nullminute max=59
- test('returns null for invalid step') — `'*/0 * * * *'` → nullstep=0
- test('returns null for reversed range') — `'10-5 * * * *'` → nulllo>hi
- test('returns null for empty string') — `''` → null
- test('returns null for non-numeric tokens') — `'abc * * * *'` → null
#### 字段范围验证
- test('minute: 0-59')
- test('hour: 0-23')
- test('dayOfMonth: 1-31')
- test('month: 1-12')
- test('dayOfWeek: 0-6 (plus 7 alias)')
---
### describe('computeNextCronRun')
#### 基本匹配
- test('finds next minute') — from 14:30:45, cron `'31 14 * * *'` → 14:31:00 同天
- test('finds next hour') — from 14:30, cron `'0 15 * * *'` → 15:00 同天
- test('rolls to next day') — from 14:30, cron `'0 10 * * *'` → 10:00 次日
- test('rolls to next month') — from 1月31日, cron `'0 0 1 * *'` → 2月1日
- test('is strictly after from date') — from 恰好匹配时应返回下一次而非当前时间
#### DOM/DOW 语义
- test('OR semantics when both dom and dow constrained') — dom=15, dow=3 → 匹配 15 号 OR 周三
- test('only dom constrained uses dom') — dom=15, dow=* → 只匹配 15 号
- test('only dow constrained uses dow') — dom=*, dow=3 → 只匹配周三
- test('both wildcarded matches every day') — dom=*, dow=* → 每天
#### 边界情况
- test('handles month boundary') — 从 2 月 28 日寻找 2 月 29 日或 3 月 1 日
- test('returns null after 366-day search') — 不可能匹配的表达式返回 null理论上不会发生
- test('handles step across midnight') — `'0 0 * * *'` 从 23:59 → 次日 0:00
#### 每 N 分钟
- test('every 5 minutes from arbitrary time') — `'*/5 * * * *'` from 14:32 → 14:35
- test('every minute') — `'* * * * *'` from 14:32:45 → 14:33:00
---
### describe('cronToHuman')
#### 常见模式
- test('every N minutes') — `'*/5 * * * *'``'Every 5 minutes'`
- test('every minute') — `'*/1 * * * *'``'Every minute'`
- test('every hour at :00') — `'0 * * * *'``'Every hour'`
- test('every hour at :30') — `'30 * * * *'``'Every hour at :30'`
- test('every N hours') — `'0 */2 * * *'``'Every 2 hours'`
- test('daily at specific time') — `'30 9 * * *'``'Every day at 9:30 AM'`
- test('specific day of week') — `'0 9 * * 3'``'Every Wednesday at 9:00 AM'`
- test('weekdays') — `'0 9 * * 1-5'``'Weekdays at 9:00 AM'`
#### Fallback
- test('returns raw cron for complex patterns') — 非常见模式返回原始 cron 字符串
- test('returns raw cron for wrong field count') — `'* * *'` → 原样返回
#### UTC 模式
- test('UTC option formats time in local timezone') — `{ utc: true }` 时 UTC 时间转本地显示
- test('UTC midnight crossing adjusts day name') — UTC 时间跨天时本地星期名正确
---
## Mock 需求
**无需 Mock**。所有函数为纯函数,唯一的外部依赖是 `Date` 构造器和 `toLocaleTimeString`,可通过传入确定性的 `from` 参数控制。
## 注意事项
- `cronToHuman` 的时间格式化依赖系统 locale测试中建议使用 `'en-US'` locale 或只验证部分输出
- `computeNextCronRun` 使用本地时区DST 相关测试需注意运行环境

View File

@@ -1,106 +0,0 @@
# Git 工具测试计划
## 概述
Git 工具模块提供 git 远程 URL 规范化、仓库根目录查找、裸仓库安全检测等功能。测试重点是纯函数的 URL 规范化和需要文件系统 mock 的仓库发现逻辑。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/utils/git.ts` | `normalizeGitRemoteUrl`, `findGitRoot`, `findCanonicalGitRoot`, `getIsGit`, `isAtGitRoot`, `getRepoRemoteHash`, `isCurrentDirectoryBareGitRepo`, `gitExe`, `getGitState`, `stashToCleanState`, `preserveGitStateForIssue` |
---
## 测试用例
### describe('normalizeGitRemoteUrl')(纯函数)
#### SSH 格式
- test('normalizes SSH URL') — `'git@github.com:owner/repo.git'``'github.com/owner/repo'`
- test('normalizes SSH URL without .git suffix') — `'git@github.com:owner/repo'``'github.com/owner/repo'`
- test('handles GitLab SSH') — `'git@gitlab.com:group/subgroup/repo.git'``'gitlab.com/group/subgroup/repo'`
#### HTTPS 格式
- test('normalizes HTTPS URL') — `'https://github.com/owner/repo.git'``'github.com/owner/repo'`
- test('normalizes HTTPS URL without .git suffix') — `'https://github.com/owner/repo'``'github.com/owner/repo'`
- test('normalizes HTTP URL') — `'http://github.com/owner/repo.git'``'github.com/owner/repo'`
#### SSH:// 协议格式
- test('normalizes ssh:// URL') — `'ssh://git@github.com/owner/repo'``'github.com/owner/repo'`
- test('handles user prefix in ssh://') — `'ssh://user@host/path'``'host/path'`
#### 代理 URLCCR git proxy
- test('normalizes legacy proxy URL') — `'http://local_proxy@127.0.0.1:16583/git/owner/repo'``'github.com/owner/repo'`
- test('normalizes GHE proxy URL') — `'http://user@127.0.0.1:8080/git/ghe.company.com/owner/repo'``'ghe.company.com/owner/repo'`
#### 边界情况
- test('returns null for empty string') — `''` → null
- test('returns null for whitespace') — `' '` → null
- test('returns null for unrecognized format') — `'not-a-url'` → null
- test('output is lowercase') — `'git@GitHub.com:Owner/Repo.git'``'github.com/owner/repo'`
- test('SSH and HTTPS for same repo produce same result') — 相同仓库不同协议 → 相同输出
---
### describe('findGitRoot')(需文件系统 Mock
- test('finds git root from nested directory') — `/project/src/utils/``/project/`(假设 `/project/.git` 存在)
- test('finds git root from root directory') — `/project/``/project/`
- test('returns null for non-git directory') — 无 `.git` → null
- test('handles worktree .git file') — `.git` 为文件时也识别
- test('memoizes results') — 同一路径不重复查找
### describe('findCanonicalGitRoot')
- test('returns same as findGitRoot for regular repo')
- test('resolves worktree to main repo root') — worktree 路径 → 主仓库根目录
- test('returns null for non-git directory')
### describe('gitExe')
- test('returns git path string') — 返回字符串
- test('memoizes the result') — 多次调用返回同一值
---
### describe('getRepoRemoteHash')(需 Mock
- test('returns 16-char hex hash') — 返回值为 16 位十六进制字符串
- test('returns null when no remote') — 无 remote URL 时返回 null
- test('same repo SSH/HTTPS produce same hash') — 不同协议同一仓库 hash 相同
---
### describe('isCurrentDirectoryBareGitRepo')(需文件系统 Mock
- test('detects bare git repo attack vector') — 目录含 HEAD + objects/ + refs/ 但无有效 .git/HEAD → true
- test('returns false for normal directory') — 普通目录 → false
- test('returns false for regular git repo') — 有效 .git 目录 → false
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| `statSync` | mock module | `findGitRoot` 中的 .git 检测 |
| `readFileSync` | mock module | worktree .git 文件读取 |
| `realpathSync` | mock module | 路径解析 |
| `execFileNoThrow` | mock module | git 命令执行 |
| `whichSync` | mock module | `gitExe` 中的 git 路径查找 |
| `getCwd` | mock module | 当前工作目录 |
| `getRemoteUrl` | mock module | `getRepoRemoteHash` 依赖 |
| 临时目录 | `mkdtemp` | 集成测试中创建临时 git 仓库 |
## 集成测试场景
### describe('Git repo discovery')(放在 tests/integration/
- test('findGitRoot works in actual git repo') — 在临时 git init 的目录中验证
- test('normalizeGitRemoteUrl + getRepoRemoteHash produces stable hash') — URL → hash 端到端验证

View File

@@ -1,161 +0,0 @@
# 配置系统测试计划
## 概述
配置系统包含全局配置GlobalConfig、项目配置ProjectConfig和设置Settings三层。测试重点是纯函数校验逻辑、Zod schema 验证和配置合并策略。
## 被测文件
| 文件 | 关键导出 |
|------|----------|
| `src/utils/config.ts` | `getGlobalConfig`, `saveGlobalConfig`, `getCurrentProjectConfig`, `checkHasTrustDialogAccepted`, `isPathTrusted`, `getOrCreateUserID`, `isAutoUpdaterDisabled` |
| `src/utils/settings/settings.ts` | `getSettingsForSource`, `parseSettingsFile`, `getSettingsFilePathForSource`, `getInitialSettings` |
| `src/utils/settings/types.ts` | `SettingsSchema`Zod schema |
| `src/utils/settings/validation.ts` | 设置验证函数 |
| `src/utils/settings/constants.ts` | 设置常量 |
---
## 测试用例
### src/utils/config.ts — 纯函数/常量
#### describe('DEFAULT_GLOBAL_CONFIG')
- test('has all required fields') — 默认配置对象包含所有必需字段
- test('has null auth fields by default') — oauthAccount 等为 null
#### describe('DEFAULT_PROJECT_CONFIG')
- test('has empty allowedTools') — 默认为空数组
- test('has empty mcpServers') — 默认为空对象
#### describe('isAutoUpdaterDisabled')
- test('returns true when CLAUDE_CODE_DISABLE_AUTOUPDATER is set') — env 设置时禁用
- test('returns true when disableAutoUpdater config is true')
- test('returns false by default')
---
### src/utils/config.ts — 需 Mock
#### describe('getGlobalConfig')
- test('returns cached config on subsequent calls') — 缓存机制
- test('returns TEST_GLOBAL_CONFIG_FOR_TESTING in test mode')
- test('reads config from ~/.claude.json')
- test('returns default config when file does not exist')
#### describe('saveGlobalConfig')
- test('applies updater function to current config') — updater 修改被保存
- test('creates backup before writing') — 写入前备份
- test('prevents auth state loss') — `wouldLoseAuthState` 检查
#### describe('getCurrentProjectConfig')
- test('returns project config for current directory')
- test('returns default config when no project config exists')
#### describe('checkHasTrustDialogAccepted')
- test('returns true when trust is accepted in current directory')
- test('returns true when parent directory is trusted') — 父目录信任传递
- test('returns false when no trust accepted')
- test('caches positive results')
#### describe('isPathTrusted')
- test('returns true for trusted path')
- test('returns false for untrusted path')
#### describe('getOrCreateUserID')
- test('returns existing user ID from config')
- test('creates and persists new ID when none exists')
- test('returns consistent ID across calls')
---
### src/utils/settings/settings.ts
#### describe('getSettingsFilePathForSource')
- test('returns ~/.claude/settings.json for userSettings') — 全局用户设置路径
- test('returns .claude/settings.json for projectSettings') — 项目设置路径
- test('returns .claude/settings.local.json for localSettings') — 本地设置路径
#### describe('parseSettingsFile')(需 Mock 文件读取)
- test('parses valid settings JSON') — 有效 JSON → `{ settings, errors: [] }`
- test('returns errors for invalid fields') — 无效字段 → errors 非空
- test('returns empty settings for non-existent file')
- test('handles JSON with comments') — JSONC 格式支持
#### describe('getInitialSettings')
- test('merges settings from all sources') — user + project + local 合并
- test('later sources override earlier ones') — 优先级policy > user > project > local
---
### src/utils/settings/types.ts — Zod Schema 验证
#### describe('SettingsSchema validation')
- test('accepts valid minimal settings') — `{}` → 有效
- test('accepts permissions block') — `{ permissions: { allow: ['Bash(*)'] } }` → 有效
- test('accepts model setting') — `{ model: 'sonnet' }` → 有效
- test('accepts hooks configuration') — 有效的 hooks 对象被接受
- test('accepts env variables') — `{ env: { FOO: 'bar' } }` → 有效
- test('rejects unknown top-level keys') — 未知字段被拒绝或忽略(取决于 schema 配置)
- test('rejects invalid permission mode') — `{ permissions: { defaultMode: 'invalid' } }` → 错误
- test('rejects non-string model') — `{ model: 123 }` → 错误
- test('accepts mcpServers configuration') — MCP server 配置有效
- test('accepts sandbox configuration')
---
### src/utils/settings/validation.ts
#### describe('settings validation')
- test('validates permission rules format') — `'Bash(npm install)'` 格式正确
- test('rejects malformed permission rules')
- test('validates hook configuration structure')
- test('provides helpful error messages') — 错误信息包含字段路径
---
## Mock 需求
| 依赖 | Mock 方式 | 说明 |
|------|-----------|------|
| 文件系统 | 临时目录 + mock | config 文件读写 |
| `lockfile` | mock module | 文件锁 |
| `getCwd` | mock module | 项目路径判断 |
| `findGitRoot` | mock module | 项目根目录 |
| `process.env` | 直接设置/恢复 | `CLAUDE_CODE_DISABLE_AUTOUPDATER` 等 |
### 测试用临时文件结构
```
/tmp/claude-test-xxx/
├── .claude/
│ ├── settings.json # projectSettings
│ └── settings.local.json # localSettings
├── home/
│ └── .claude/
│ └── settings.json # userSettingsmock HOME
└── project/
└── .git/
```
## 集成测试场景
### describe('Config + Settings merge pipeline')
- test('user settings + project settings merge correctly') — 验证合并优先级
- test('deny rules from settings are reflected in tool permission context')
- test('trust dialog state persists across config reads')

View File

@@ -1,361 +0,0 @@
# Plan 10 — 修复 WEAK 评分测试文件
> 优先级:高 | 8 个文件 | 预估新增/修改 ~60 个测试用例
本计划修复 testing-spec.md 中评定为 WEAK 的 8 个测试文件的断言缺陷和覆盖缺口。
---
## 10.1 `src/utils/__tests__/format.test.ts`
**问题**`formatNumber``formatTokens``formatRelativeTime` 使用 `toContain` 代替精确匹配,无法检测格式回归。
### 修改清单
#### formatNumber — toContain → toBe
```typescript
// 当前(弱)
expect(formatNumber(1321)).toContain("k");
expect(formatNumber(1500000)).toContain("m");
// 修复为
expect(formatNumber(1321)).toBe("1.3k");
expect(formatNumber(1500000)).toBe("1.5m");
```
> 注意:`Intl.NumberFormat` 输出可能因 locale 不同。若 CI locale 不一致,改用 `toMatch(/^\d+(\.\d)?[km]$/)` 正则匹配。
#### formatTokens — 补精确断言
```typescript
expect(formatTokens(1000)).toBe("1k");
expect(formatTokens(1500)).toBe("1.5k");
```
#### formatRelativeTime — toContain → toBe
```typescript
// 当前(弱)
expect(formatRelativeTime(diff, now)).toContain("30");
expect(formatRelativeTime(diff, now)).toContain("ago");
// 修复为
expect(formatRelativeTime(diff, now)).toBe("30s ago");
```
#### 新增formatDuration 进位边界
| 用例 | 输入 | 期望 |
|------|------|------|
| 59.5s 进位 | 59500ms | 至少含 `1m` |
| 59m59s 进位 | 3599000ms | 至少含 `1h` |
| sub-millisecond | 0.5ms | `"<1ms"``"0ms"` |
#### 新增:未测试函数
| 函数 | 最少用例 |
|------|---------|
| `formatRelativeTimeAgo` | 2过去 / 未来) |
| `formatLogMetadata` | 1基本调用不抛错 |
| `formatResetTime` | 2有值 / null |
| `formatResetText` | 1基本调用 |
---
## 10.2 `src/tools/shared/__tests__/gitOperationTracking.test.ts`
**问题**`detectGitOperation` 内部调用 `getCommitCounter()``getPrCounter()``logEvent()`,测试产生分析副作用。
### 修改清单
#### 添加 analytics mock
在文件顶部添加 `mock.module`
```typescript
import { mock, afterAll, afterEach, beforeEach } from "bun:test";
mock.module("src/services/analytics/index.ts", () => ({
logEvent: mock(() => {}),
}));
mock.module("src/bootstrap/state.ts", () => ({
getCommitCounter: mock(() => ({ increment: mock(() => {}) })),
getPrCounter: mock(() => ({ increment: mock(() => {}) })),
}));
```
> 需验证 `detectGitOperation` 的实际导入路径,按需调整 mock 目标。
#### 新增:缺失的 GH PR actions
| 用例 | 输入 | 期望 |
|------|------|------|
| gh pr edit | `'gh pr edit 123 --title "fix"'` | `result.pr.number === 123` |
| gh pr close | `'gh pr close 456'` | `result.pr.number === 456` |
| gh pr ready | `'gh pr ready 789'` | `result.pr.number === 789` |
| gh pr comment | `'gh pr comment 123 --body "done"'` | `result.pr.number === 123` |
#### 新增parseGitCommitId 边界
| 用例 | 输入 | 期望 |
|------|------|------|
| 完整 40 字符 SHA | `'[abcdef0123456789abcdef0123456789abcdef01] ...'` | 返回完整 40 字符 |
| 畸形括号输出 | `'create mode 100644 file.txt'` | 返回 `null` |
---
## 10.3 `src/utils/permissions/__tests__/PermissionMode.test.ts`
**问题**`isExternalPermissionMode` 在非 ant 环境永远返回 truefalse 路径从未执行mode 覆盖不完整。
### 修改清单
#### 补全 mode 覆盖
| 函数 | 缺失的 mode |
|------|-------------|
| `permissionModeTitle` | `bypassPermissions`, `dontAsk` |
| `permissionModeShortTitle` | `dontAsk`, `acceptEdits` |
| `getModeColor` | `dontAsk`, `acceptEdits`, `plan` |
| `permissionModeFromString` | `acceptEdits`, `bypassPermissions` |
| `toExternalPermissionMode` | `acceptEdits`, `bypassPermissions` |
#### 修复 isExternalPermissionMode
```typescript
// 当前:只测了非 ant 环境(永远 true
// 需要新增 ant 环境测试
describe("when USER_TYPE is 'ant'", () => {
beforeEach(() => {
process.env.USER_TYPE = "ant";
});
afterEach(() => {
delete process.env.USER_TYPE;
});
test("returns false for 'auto' in ant context", () => {
expect(isExternalPermissionMode("auto")).toBe(false);
});
test("returns false for 'bubble' in ant context", () => {
expect(isExternalPermissionMode("bubble")).toBe(false);
});
test("returns true for non-ant modes in ant context", () => {
expect(isExternalPermissionMode("plan")).toBe(true);
});
});
```
#### 新增permissionModeSchema
| 用例 | 输入 | 期望 |
|------|------|------|
| 有效 mode | `'plan'` | `success: true` |
| 无效 mode | `'invalid'` | `success: false` |
---
## 10.4 `src/utils/permissions/__tests__/dangerousPatterns.test.ts`
**问题**:纯数据 smoke test无行为验证。
### 修改清单
#### 新增:重复值检查
```typescript
test("CROSS_PLATFORM_CODE_EXEC has no duplicates", () => {
const set = new Set(CROSS_PLATFORM_CODE_EXEC);
expect(set.size).toBe(CROSS_PLATFORM_CODE_EXEC.length);
});
test("DANGEROUS_BASH_PATTERNS has no duplicates", () => {
const set = new Set(DANGEROUS_BASH_PATTERNS);
expect(set.size).toBe(DANGEROUS_BASH_PATTERNS.length);
});
```
#### 新增:全量成员断言(用 Set 确保精确)
```typescript
test("CROSS_PLATFORM_CODE_EXEC contains expected interpreters", () => {
const expected = ["node", "python", "python3", "ruby", "perl", "php",
"bun", "deno", "npx", "tsx"];
const set = new Set(CROSS_PLATFORM_CODE_EXEC);
for (const entry of expected) {
expect(set.has(entry)).toBe(true);
}
});
```
#### 新增:空字符串不匹配
```typescript
test("empty string does not match any pattern", () => {
for (const pattern of DANGEROUS_BASH_PATTERNS) {
expect("".startsWith(pattern)).toBe(false);
}
});
```
---
## 10.5 `src/utils/__tests__/zodToJsonSchema.test.ts`
**问题**object 属性仅 `toBeDefined` 未验证类型结构optional 字段未验证 absence。
### 修改清单
#### 修复 object schema 测试
```typescript
// 当前(弱)
expect(schema.properties!.name).toBeDefined();
expect(schema.properties!.age).toBeDefined();
// 修复为
expect(schema.properties!.name).toEqual({ type: "string" });
expect(schema.properties!.age).toEqual({ type: "number" });
```
#### 修复 optional 字段测试
```typescript
test("optional field is not in required array", () => {
const schema = zodToJsonSchema(z.object({
required: z.string(),
optional: z.string().optional(),
}));
expect(schema.required).toEqual(["required"]);
expect(schema.required).not.toContain("optional");
});
```
#### 新增:缺失的 schema 类型
| 用例 | 输入 | 期望 |
|------|------|------|
| `z.literal("foo")` | `z.literal("foo")` | `{ const: "foo" }` |
| `z.null()` | `z.null()` | `{ type: "null" }` |
| `z.union()` | `z.union([z.string(), z.number()])` | `{ anyOf: [...] }` |
| `z.record()` | `z.record(z.string(), z.number())` | `{ type: "object", additionalProperties: { type: "number" } }` |
| `z.tuple()` | `z.tuple([z.string(), z.number()])` | `{ type: "array", items: [...], additionalItems: false }` |
| 嵌套 object | `z.object({ a: z.object({ b: z.string() }) })` | 验证嵌套属性结构 |
---
## 10.6 `src/utils/__tests__/envValidation.test.ts`
**问题**`validateBoundedIntEnvVar` lower bound=100 时 value=1 返回 `status: "valid"`,疑似源码 bug。
### 修改清单
#### 验证 lower bound 行为
```typescript
// 当前测试
test("value of 1 with lower bound 100", () => {
const result = validateBoundedIntEnvVar("1", { defaultValue: 100, upperLimit: 1000, lowerLimit: 100 });
// 如果源码有 bug这里应该暴露
expect(result.effective).toBeGreaterThanOrEqual(100);
expect(result.status).toBe(result.effective !== 100 ? "capped" : "valid");
});
```
#### 新增边界用例
| 用例 | value | lowerLimit | 期望 |
|------|-------|------------|------|
| 低于 lower bound | `"50"` | 100 | `effective: 100, status: "capped"` |
| 等于 lower bound | `"100"` | 100 | `effective: 100, status: "valid"` |
| 浮点截断 | `"50.7"` | 100 | `effective: 100`parseInt 截断后 cap |
| 空白字符 | `" 500 "` | 1 | `effective: 500, status: "valid"` |
| defaultValue 为 0 | `"0"` | 0 | 需确认 `parsed <= 0` 逻辑 |
> **行动**:先确认 `validateBoundedIntEnvVar` 源码中 lower bound 的实际执行路径。如果确实不生效,需先修源码再补测试。
---
## 10.7 `src/utils/__tests__/file.test.ts`
**问题**`addLineNumbers``toContain`,未验证完整格式。
### 修改清单
#### 修复 addLineNumbers 断言
```typescript
// 当前(弱)
expect(result).toContain("1");
expect(result).toContain("hello");
// 修复为(需确定 isCompactLinePrefixEnabled 行为)
// 假设 compact=false格式为 " 1→hello"
test("formats single line with tab prefix", () => {
// 先确认环境,如果 compact 模式不确定,用正则
expect(result).toMatch(/^\s*\d+[→\t]hello$/m);
});
```
#### 新增stripLineNumberPrefix 边界
| 用例 | 输入 | 期望 |
|------|------|------|
| 纯数字行 | `"123"` | `""` |
| 无内容前缀 | `"→"` | `""` |
| compact 格式 `"1\thello"` | `"1\thello"` | `"hello"` |
#### 新增pathsEqual 边界
| 用例 | a | b | 期望 |
|------|---|---|------|
| 尾部斜杠差异 | `"/a/b"` | `"/a/b/"` | `false` |
| `..` 段 | `"/a/../b"` | `"/b"` | 视实现而定 |
---
## 10.8 `src/utils/__tests__/notebook.test.ts`
**问题**`mapNotebookCellsToToolResult` 内容检查用 `toContain`,未验证 XML 格式。
### 修改清单
#### 修复 content 断言
```typescript
// 当前(弱)
expect(result).toContain("cell-0");
expect(result).toContain("print('hello')");
// 修复为
expect(result).toContain('<cell id="cell-0">');
expect(result).toContain("</cell>");
```
#### 新增parseCellId 边界
| 用例 | 输入 | 期望 |
|------|------|------|
| 负数 | `"cell--1"` | `null` |
| 前导零 | `"cell-007"` | `7` |
| 极大数 | `"cell-999999999"` | `999999999` |
#### 新增mapNotebookCellsToToolResult 边界
| 用例 | 输入 | 期望 |
|------|------|------|
| 空 data 数组 | `{ cells: [] }` | 空字符串或空结果 |
| 无 cell_id | `{ cell_type: "code", source: "x" }` | fallback 到 `cell-${index}` |
| error output | `{ output_type: "error", ename: "Error", evalue: "msg" }` | 包含 error 信息 |
---
## 验收标准
- [ ] `bun test` 全部通过
- [ ] 8 个文件评分从 WEAK 提升至 ACCEPTABLE 或 GOOD
- [ ] `toContain` 仅用于警告文本等确实不确定精确值的场景
- [ ] envValidation bug 确认并修复(或确认非 bug 并更新测试)

View File

@@ -1,177 +0,0 @@
# Plan 11 — 加强 ACCEPTABLE 评分测试
> 优先级:中 | ~15 个文件 | 预估新增 ~80 个测试用例
本计划对 ACCEPTABLE 评分文件中的具体缺陷进行定向加强。每个条目只列出需要改动的部分,不做全量重写。
---
## 11.1 `src/utils/__tests__/diff.test.ts`
| 改动 | 当前 | 改为 |
|------|------|------|
| `getPatchFromContents` 断言 | `hunks.length > 0` | 验证具体 `+`/`-` 行内容 |
| `$` 字符转义 | 未测试 | 新增含 `$` 的内容测试 |
| `ignoreWhitespace` 选项 | 未测试 | 新增 `ignoreWhitespace: true` 用例 |
| 删除全部内容 | 未测试 | `newContent: ""` |
| 多 hunk 偏移 | `adjustHunkLineNumbers` 仅单 hunk | 新增多 hunk 同数组测试 |
---
## 11.2 `src/utils/__tests__/path.test.ts`
当前仅覆盖 2/5+ 导出函数。新增:
| 函数 | 最少用例 | 关键边界 |
|------|---------|---------|
| `expandPath` | 6 | `~/` 展开、绝对路径直通、相对路径、空串、含 null 字节、`~user` 格式 |
| `toRelativePath` | 3 | 同级文件、子目录、父目录 |
| `sanitizePath` | 3 | 正常路径、含 `..` 段、空串 |
`containsPathTraversal` 补充:
- URL 编码 `%2e%2e%2f`(确认不匹配,记录为非需求)
- 混合分隔符 `foo/..\bar`
`normalizePathForConfigKey` 补充:
- 混合分隔符 `foo/bar\baz`
- 冗余分隔符 `foo//bar`
- Windows 盘符 `C:\foo\bar`
---
## 11.3 `src/utils/__tests__/uuid.test.ts`
| 改动 | 说明 |
|------|------|
| 大写测试断言强化 | `not.toBeNull()` → 验证标准化输出(小写+连字符格式) |
| 新增 `createAgentId` | 3 用例:无 label / 有 label / 输出格式正则 `/^a[a-z]*-[a-f0-9]{16}$/` |
| 前后空白 | `" 550e8400-... "` 期望 `null` |
---
## 11.4 `src/utils/__tests__/semver.test.ts`
| 用例 | 输入 | 期望 |
|------|------|------|
| pre-release 比较 | `gt("1.0.0", "1.0.0-alpha")` | `true` |
| pre-release 间比较 | `order("1.0.0-alpha", "1.0.0-beta")` | `-1` |
| tilde range | `satisfies("1.2.5", "~1.2.3")` | `true` |
| `*` 通配符 | `satisfies("2.0.0", "*")` | `true` |
| 畸形版本 | `order("abc", "1.0.0")` | 确认不抛错 |
| `0.0.0` | `gt("0.0.0", "0.0.0")` | `false` |
---
## 11.5 `src/utils/__tests__/hash.test.ts`
| 改动 | 当前 | 改为 |
|------|------|------|
| djb2 32 位检查 | `hash \| 0`(恒 true | `Number.isSafeInteger(hash) && Math.abs(hash) <= 0x7FFFFFFF` |
| hashContent 空串 | 未测试 | 新增 |
| hashContent 格式 | 未验证输出为数字串 | `toMatch(/^\d+$/)` |
| hashPair 空串 | 未测试 | `hashPair("", "b")`, `hashPair("", "")` |
| 已知答案 | 无 | 断言 `djb2Hash("hello")` 为特定值(需先在控制台运行一次确定) |
---
## 11.6 `src/utils/__tests__/claudemd.test.ts`
当前仅覆盖 3 个辅助函数。新增:
| 用例 | 函数 | 说明 |
|------|------|------|
| 未闭合注释 | `stripHtmlComments` | `"<!-- no close some text"` → 原样返回 |
| 跨行注释 | `stripHtmlComments` | `"<!--\nmulti\nline\n-->text"``"text"` |
| 同行注释+内容 | `stripHtmlComments` | `"<!-- note -->some text"``"some text"` |
| 内联代码中的注释 | `stripHtmlComments` | `` `<!-- kept -->` `` → 保留 |
| 大小写不敏感 | `isMemoryFilePath` | `"claude.md"`, `"CLAUDE.MD"` |
| 非 .md 规则文件 | `isMemoryFilePath` | `.claude/rules/foo.txt` → `false` |
| 空数组 | `getLargeMemoryFiles` | `[]` → `[]` |
---
## 11.7 `src/tools/FileEditTool/__tests__/utils.test.ts`
| 函数 | 新增用例 |
|------|---------|
| `normalizeQuotes` | 混合引号 `"`she said 'hello'"` |
| `stripTrailingWhitespace` | CR-only `\r`、无尾部换行、全空白串 |
| `findActualString` | 空 content、Unicode content |
| `preserveQuoteStyle` | 单引号、缩写中的撇号(如 `it's`)、空串 |
| `applyEditToFile` | `replaceAll=true` 零匹配、`oldString` 无尾部 `\n`、多行内容 |
---
## 11.8 `src/utils/model/__tests__/providers.test.ts`
| 改动 | 说明 |
|------|------|
| 删除 `originalEnv` | 未使用,消除死代码 |
| env 恢复改为快照 | `beforeEach` 保存 `process.env``afterEach` 恢复 |
| 新增三变量同时设置 | bedrock + vertex + foundry 全部为 `"1"`,验证优先级 |
| 新增非 `"1"` 值 | `"true"`, `"0"`, `""` |
| `isFirstPartyAnthropicBaseUrl` | URL 含路径 `/v1`、含尾斜杠、非 HTTPS |
---
## 11.9 `src/utils/__tests__/hyperlink.test.ts`
| 用例 | 说明 |
|------|------|
| 空 URL | `createHyperlink("http://x.com", "", { supported: true })` 不抛错 |
| undefined supportsHyperlinks | 选项未传时走默认检测 |
| 非 ant staging URL | `USER_TYPE !== "ant"` 时 staging 返回 `false` |
---
## 11.10 `src/utils/__tests__/objectGroupBy.test.ts`
| 用例 | 说明 |
|------|------|
| key 返回 undefined | `(_, i) => undefined` → 全部归入 `undefined` 组 |
| key 为特殊字符 | `({ name }) => name` 含空格/中文 |
---
## 11.11 `src/utils/__tests__/CircularBuffer.test.ts`
| 用例 | 说明 |
|------|------|
| capacity=1 | 添加 2 个元素,仅保留最后一个 |
| 空 buffer 调用 getRecent | 返回空数组 |
| getRecent(0) | 返回空数组 |
---
## 11.12 `src/utils/__tests__/contentArray.test.ts`
| 用例 | 说明 |
|------|------|
| 混合交替 | `[tool_result, text, tool_result]` — 验证插入到正确位置 |
---
## 11.13 `src/utils/__tests__/argumentSubstitution.test.ts`
| 用例 | 说明 |
|------|------|
| 转义引号 | `"he said \"hello\""` |
| 越界索引 | `$ARGUMENTS[99]`(参数不够时) |
| 多占位符 | `"cmd $0 $1 $0"` |
---
## 11.14 `src/utils/__tests__/messages.test.ts`
| 改动 | 说明 |
|------|------|
| `normalizeMessages` 断言加强 | 验证拆分后的消息内容,不只是长度 |
| `isNotEmptyMessage` 空白 | `[{ type: "text", text: " " }]` |
---
## 验收标准
- [ ] `bun test` 全部通过
- [ ] 目标文件评分从 ACCEPTABLE 提升至 GOOD
- [ ]`toContain` 用于精确值检查的场景

View File

@@ -1,145 +0,0 @@
# Plan 12 — Mock 可靠性修复
> 优先级:高 | 影响 4 个测试文件 | 预估修改 ~15 处
本计划修复测试中 mock 相关的副作用、状态泄漏和虚假测试。
---
## 12.1 `gitOperationTracking.test.ts` — 消除分析副作用
**当前问题**`detectGitOperation` 内部调用 `logEvent()``getCommitCounter().increment()``getPrCounter().increment()`,每次测试运行都触发真实分析代码。
**修复步骤**
1. 读取 `src/tools/shared/gitOperationTracking.ts`,确认 analytics 导入路径
2. 在测试文件顶部添加 `mock.module`
```typescript
import { mock } from "bun:test";
mock.module("src/services/analytics/index.ts", () => ({
logEvent: mock(() => {}),
// 按需补充其他导出
}));
```
3. 如果 `getCommitCounter` / `getPrCounter` 来自 `src/bootstrap/state.ts`
```typescript
mock.module("src/bootstrap/state.ts", () => ({
getCommitCounter: mock(() => ({ increment: mock(() => {}) })),
getPrCounter: mock(() => ({ increment: mock(() => {}) })),
// 保留其他被测函数实际需要的导出
}));
```
4. 使用 `await import()` 模式加载被测模块
5. 运行测试验证无副作用
**风险**`mock.module` 会替换整个模块。如果 `detectGitOperation` 还需要其他来自这些模块的导出,需在 mock 工厂中提供。
---
## 12.2 `PermissionMode.test.ts` — 修复 `isExternalPermissionMode` 虚假测试
**当前问题**`isExternalPermissionMode` 依赖 `process.env.USER_TYPE`。非 ant 环境下所有 mode 都返回 true测试从未覆盖 false 分支。
**修复步骤**
1. 新增 ant 环境测试组(见 Plan 10.3 详细用例)
2. 使用 `beforeEach`/`afterEach` 管理 `process.env.USER_TYPE`
```typescript
describe("when USER_TYPE is 'ant'", () => {
const originalUserType = process.env.USER_TYPE;
beforeEach(() => { process.env.USER_TYPE = "ant"; });
afterEach(() => {
if (originalUserType !== undefined) {
process.env.USER_TYPE = originalUserType;
} else {
delete process.env.USER_TYPE;
}
});
test("returns false for 'auto'", () => {
expect(isExternalPermissionMode("auto")).toBe(false);
});
test("returns false for 'bubble'", () => {
expect(isExternalPermissionMode("bubble")).toBe(false);
});
test("returns true for 'plan'", () => {
expect(isExternalPermissionMode("plan")).toBe(true);
});
});
```
3. 验证新增测试确实执行 false 路径
---
## 12.3 `providers.test.ts` — 环境变量快照恢复
**当前问题**
- `originalEnv` 声明后未使用
- `afterEach` 仅删除已知 3 个 key如果源码新增 env var测试间状态泄漏
**修复步骤**
```typescript
let savedEnv: Record<string, string | undefined>;
beforeEach(() => {
savedEnv = {};
for (const key of Object.keys(process.env)) {
savedEnv[key] = process.env[key];
}
});
afterEach(() => {
// 删除所有当前 env恢复快照
for (const key of Object.keys(process.env)) {
delete process.env[key];
}
for (const [key, value] of Object.entries(savedEnv)) {
if (value !== undefined) {
process.env[key] = value;
}
}
});
```
> 简化方案:只保存/恢复相关 key 列表 `["CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY", "ANTHROPIC_BASE_URL", "USER_TYPE"]`,但需注释说明新增 env var 时需同步更新。
---
## 12.4 `envUtils.test.ts` — 验证环境变量恢复完整性
**当前状态**:已有 `afterEach` 恢复。需审查:
1. 确认所有 `describe` 块中的 `afterEach` 都完整恢复了修改的 env var
2. 确认 `process.argv` 修改也被恢复(`getClaudeConfigHomeDir` 测试修改了 argv
3. 新增:`afterEach` 中断言无意外 env 泄漏可选CI-only
---
## 12.5 `sleep.test.ts` / `memoize.test.ts` — 时间敏感测试加固
**当前状态**:已有合理 margin。可选加固
| 文件 | 用例 | 当前 | 加固 |
|------|------|------|------|
| `sleep.test.ts` | `resolves after timeout` | `sleep(50)`, check `>= 40ms` | 增大 margin`sleep(50)`, check `>= 30ms` |
| `memoize.test.ts` | stale serve & refresh | TTL=1ms, wait 10ms | 增大 marginTTL=5ms, wait 50ms |
> 仅在 CI 出现 flaky 时执行此加固。
---
## 验收标准
- [ ] `gitOperationTracking.test.ts` 无分析副作用(可通过在 mock 中增加 `expect(logEvent).toHaveBeenCalledTimes(N)` 验证)
- [ ] `PermissionMode.test.ts``isExternalPermissionMode` 覆盖 true + false 分支
- [ ] `providers.test.ts``originalEnv` 死代码已删除
- [ ] 所有修改 env 的测试文件恢复完整
- [ ] `bun test` 全部通过

View File

@@ -1,71 +0,0 @@
# Plan 13 — truncate CJK/Emoji 补充测试
> 优先级:中 | 1 个文件 | 预估新增 ~15 个测试用例
`truncate.ts` 使用 `stringWidth` 和 grapheme segmentation 实现宽度感知截断,但现有测试仅覆盖 ASCII。这是核心场景缺失。
---
## 被测函数
- `truncateToWidth(text, maxWidth)` — 尾部截断加 `…`
- `truncateStartToWidth(text, maxWidth)` — 头部截断加 `…`
- `truncateToWidthNoEllipsis(text, maxWidth)` — 尾部截断无省略号
- `truncatePathMiddle(path, maxLength)` — 路径中间截断
- `wrapText(text, maxWidth)` — 按宽度换行
---
## 新增用例
### CJK 全角字符
| 用例 | 函数 | 输入 | maxWidth | 期望行为 |
|------|------|------|----------|----------|
| 纯中文截断 | `truncateToWidth` | `"你好世界"` | 4 | `"你好…"` (每个中文字占 2 宽度) |
| 中英混合 | `truncateToWidth` | `"hello你好"` | 8 | `"hello你…"` |
| 全角不截断 | `truncateToWidth` | `"你好"` | 4 | `"你好"` (恰好 4) |
| emoji 单字符 | `truncateToWidth` | `"👋"` | 2 | `"👋"` (emoji 通常 2 宽度) |
| emoji 截断 | `truncateToWidth` | `"hello 👋 world"` | 8 | 确认宽度计算正确 |
| 头部中文 | `truncateStartToWidth` | `"你好世界"` | 4 | `"…界"` |
| 无省略中文 | `truncateToWidthNoEllipsis` | `"你好世界"` | 4 | `"你好"` |
> **注意**`stringWidth` 对 CJK/emoji 的宽度计算取决于具体实现。先在 REPL 中运行确认实际宽度再写断言:
> ```typescript
> import { stringWidth } from "src/utils/truncate.ts";
> console.log(stringWidth("你好")); // 确认是 4 还是 2
> console.log(stringWidth("👋")); // 确认 emoji 宽度
> ```
### 路径中间截断补充
| 用例 | 输入 | maxLength | 期望 |
|------|------|-----------|------|
| 文件名超长 | `"/very/long/path/to/MyComponent.tsx"` | 10 | 含 `…` 且以 `.tsx` 结尾 |
| 无斜杠短串 | `"abc"` | 1 | 确认行为不抛错 |
| maxLength 极小 | `"/a/b"` | 1 | 确认不抛错 |
| maxLength=4 | `"/a/b/c.ts"` | 4 | 确认行为 |
### wrapText 补充
| 用例 | 输入 | maxWidth | 期望 |
|------|------|----------|------|
| 含换行符 | `"hello\nworld"` | 10 | 保留原有换行 |
| 宽度=0 | `"hello"` | 0 | 空串或原串(确认不抛错) |
---
## 实施步骤
1. 在 REPL 中确认 `stringWidth` 对 CJK/emoji 的实际返回值
2. 按实际值编写精确断言
3. 如果 `stringWidth` 依赖 ICU 或平台特性,添加平台检查(`process.platform !== "win32"` 跳过条件)
4. 运行测试
---
## 验收标准
- [ ] 至少 5 个 CJK/emoji 相关测试通过
- [ ] 断言基于实际 `stringWidth` 返回值,非猜测
- [ ] `bun test` 全部通过

View File

@@ -1,191 +0,0 @@
# Plan 14 — 集成测试搭建
> 优先级:中 | 新建 ~3 个测试文件 | 预估 ~30 个测试用例
当前 `tests/integration/` 目录为空spec 设计的三个集成测试均未创建。本计划搭建 mock 基础设施并实现核心集成测试。
---
## 14.1 搭建 `tests/mocks/` 基础设施
### 文件结构
```
tests/
├── mocks/
│ ├── api-responses.ts # Claude API mock 响应
│ ├── file-system.ts # 临时文件系统工具
│ └── fixtures/
│ ├── sample-claudemd.md # CLAUDE.md 样本
│ └── sample-messages.json # 消息样本
├── integration/
│ ├── tool-chain.test.ts
│ ├── context-build.test.ts
│ └── message-pipeline.test.ts
└── helpers/
└── setup.ts # 共享 beforeAll/afterAll
```
### `tests/mocks/file-system.ts`
```typescript
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
export async function createTempDir(prefix = "claude-test-"): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), prefix));
return dir;
}
export async function cleanupTempDir(dir: string): Promise<void> {
await rm(dir, { recursive: true, force: true });
}
export async function writeTempFile(dir: string, name: string, content: string): Promise<string> {
const path = join(dir, name);
await writeFile(path, content, "utf-8");
return path;
}
```
### `tests/mocks/fixtures/sample-claudemd.md`
```markdown
# Project Instructions
This is a sample CLAUDE.md file for testing.
```
### `tests/mocks/api-responses.ts`
```typescript
export const mockStreamResponse = {
type: "message_start" as const,
message: {
id: "msg_mock_001",
type: "message" as const,
role: "assistant",
content: [],
model: "claude-sonnet-4-20250514",
stop_reason: null,
stop_sequence: null,
usage: { input_tokens: 100, output_tokens: 0 },
},
};
export const mockTextBlock = {
type: "content_block_start" as const,
index: 0,
content_block: { type: "text" as const, text: "Mock response" },
};
export const mockToolUseBlock = {
type: "content_block_start" as const,
index: 1,
content_block: {
type: "tool_use" as const,
id: "toolu_mock_001",
name: "Read",
input: { file_path: "/tmp/test.txt" },
},
};
export const mockEndEvent = {
type: "message_stop" as const,
};
```
---
## 14.2 `tests/integration/tool-chain.test.ts`
**目标**:验证 Tool 注册 → 发现 → 权限检查链路。
### 前置条件
`src/tools.ts``getAllBaseTools` / `getTools` 导入链过重。策略:
- 尝试直接 import 并 mock 最重依赖
- 若不可行,改为测试 `src/Tool.ts``findToolByName` + 手动构造 tool 列表
### 用例
| # | 用例 | 验证点 |
|---|------|--------|
| 1 | `findToolByName("Bash")` 在已注册列表中查找 | 返回正确的 tool 定义 |
| 2 | `findToolByName("NonExistent")` | 返回 `undefined` |
| 3 | `findToolByName` 大小写不敏感 | `"bash"` 也能找到 |
| 4 | `filterToolsByDenyRules` 拒绝特定工具 | 被拒绝工具不在结果中 |
| 5 | `parseToolPreset("default")` 返回已知列表 | 包含核心 tools |
| 6 | `buildTool` 构建的 tool 可被 `findToolByName` 发现 | 端到端验证 |
> 如果 `getAllBaseTools` 确实不可导入,改用 mock tool list 替代。
---
## 14.3 `tests/integration/context-build.test.ts`
**目标**验证系统提示组装流程CLAUDE.md 加载 + git status + 日期注入)。
### 前置条件
`src/context.ts` 依赖链极重。策略:
- Mock `src/bootstrap/state.ts`(提供 cwd、projectRoot
- Mock `src/utils/git.ts`(提供 git status
- 使用真实 `src/utils/claudemd.ts` + 临时文件
### 用例
| # | 用例 | 验证点 |
|---|------|--------|
| 1 | 基本 context 构建 | 返回值包含系统提示字符串 |
| 2 | CLAUDE.md 内容出现在 context 中 | `stripHtmlComments` 后的内容被包含 |
| 3 | 多层目录 CLAUDE.md 合并 | 父目录 + 子目录 CLAUDE.md 都被加载 |
| 4 | 无 CLAUDE.md 时不报错 | context 正常返回,无 crash |
| 5 | git status 为 null | context 正常构建(测试环境中 git 不可用时) |
> **风险评估**:如果 mock `context.ts` 的依赖链成本过高,退化为测试 `buildEffectiveSystemPrompt`(已在 systemPrompt.test.ts 中完成),记录为已知限制。
---
## 14.4 `tests/integration/message-pipeline.test.ts`
**目标**:验证用户输入 → 消息格式化 → API 请求构建。
### 前置条件
`src/services/api/claude.ts` 构建最终 API 请求。策略:
- Mock Anthropic SDK 的 streaming endpoint
- 验证请求参数结构
### 用例
| # | 用例 | 验证点 |
|---|------|--------|
| 1 | 文本消息格式化 | `createUserMessage` 生成正确 role+content |
| 2 | tool_result 消息格式化 | 包含 tool_use_id 和 content |
| 3 | 多轮消息序列化 | messages 数组保持顺序 |
| 4 | 系统提示注入到请求 | API 请求的 system 字段非空 |
| 5 | 消息 normalize 后格式一致 | `normalizeMessages` 输出结构正确 |
> **现实评估**:消息格式化的大部分已在 `messages.test.ts` 覆盖。API 请求构建需要 mock SDK复杂度高。如果投入产出比低仅实现用例 1-3 和 5用例 4 标记为 stretch goal。
---
## 实施步骤
1. 创建 `tests/mocks/` 目录和基础文件
2. 实现 `tool-chain.test.ts`(最低风险,最高价值)
3. 评估 `context-build.test.ts` 可行性,决定是否实施
4. 实现 `message-pipeline.test.ts`(可降级为单元测试)
5. 更新 `testing-spec.md` 状态
---
## 验收标准
- [ ] `tests/mocks/` 基础设施可用
- [ ] 至少 `tool-chain.test.ts` 实现并通过
- [ ] 集成测试独立于单元测试运行:`bun test tests/integration/`
- [ ] 所有集成测试使用 `createTempDir` + `cleanupTempDir`,不留文件系统残留
- [ ] `bun test` 全部通过

View File

@@ -1,67 +0,0 @@
# Plan 15 — CLI 参数测试 + 覆盖率基线
> 优先级:低 | 预估 ~15 个测试用例
---
## 15.1 `src/main.tsx` CLI 参数测试
**目标**:覆盖 Commander.js 配置的参数解析和模式切换。
### 前置条件
`src/main.tsx` 的 Commander 实例通常在模块顶层创建。测试策略:
- 直接构造 Commander 实例或 mock `main.tsx` 的 program 导出
- 使用 `parseArgs` 而非 `parse`(不触发 `process.exit`
### 用例
| # | 用例 | 输入 | 期望 |
|---|------|------|------|
| 1 | 默认模式 | `[]` | 模式为 REPL |
| 2 | pipe 模式 | `["-p"]` | 模式为 pipe |
| 3 | pipe 带输入 | `["-p", "say hello"]` | 输入为 `"say hello"` |
| 4 | print 模式 | `["--print", "hello"]` | 等效于 pipe |
| 5 | verbose | `["-v"]` | verbose 标志为 true |
| 6 | model 选择 | `["--model", "claude-opus-4-6"]` | model 值正确传递 |
| 7 | system prompt | `["--system-prompt", "custom"]` | system prompt 被设置 |
| 8 | help | `["--help"]` | 显示帮助信息,不报错 |
| 9 | version | `["--version"]` | 显示版本号 |
| 10 | unknown flag | `["--nonexistent"]` | 不报错Commander 允许未知参数时) |
> **风险**`main.tsx` 可能执行初始化逻辑auth、analytics需要在 mock 环境中运行。如果复杂度过高,降级为只测试参数解析部分。
---
## 15.2 覆盖率基线
### 运行命令
```bash
bun test --coverage 2>&1 | tail -50
```
### 记录内容
| 模块 | 当前覆盖率 | 目标 |
|------|-----------|------|
| `src/utils/` | 待测量 | >= 80% |
| `src/utils/permissions/` | 待测量 | >= 60% |
| `src/utils/model/` | 待测量 | >= 60% |
| `src/Tool.ts` + `src/tools.ts` | 待测量 | >= 80% |
| `src/utils/claudemd.ts` | 待测量 | >= 40%(核心逻辑难测) |
| 整体 | 待测量 | 不设强制指标 |
### 后续行动
- 将基线数据填入 `testing-spec.md` §4
- 识别覆盖率最低的 10 个文件,排入后续测试计划
-`bun test --coverage` 输出不可用Bun 版本限制),改用手动计算已测/总导出函数比
---
## 验收标准
- [ ] CLI 参数至少覆盖 5 个核心 flag
- [ ] 覆盖率基线数据记录到 testing-spec.md
- [ ] `bun test` 全部通过

View File

@@ -0,0 +1,88 @@
# OpenClaw Autonomy Baseline Test Spec
## Purpose
This test spec locks the current behavior of the existing trigger and context layers before any formal autonomy-subsystem implementation begins.
At this stage, production code is read-only. Only test files, fixtures, and planning documents may change.
## Goal
Establish a stable baseline around the parts of `Claude-code-bast` that later autonomy work is most likely to touch:
- proactive state handling
- cron task storage semantics
- cron scheduler helper semantics
- user-context cache and `CLAUDE.md` injection behavior
## Out of Scope for This Baseline Round
- New authority behavior (`AGENTS.md` / `HEARTBEAT.md`)
- New detached-run ledger behavior
- New flow behavior
- UI redesign
## Files Under Baseline Protection
- `src/proactive/index.ts`
- `src/utils/cronTasks.ts`
- `src/utils/cronScheduler.ts`
- `src/context.ts`
## Test Files Added In This Round
- `src/proactive/__tests__/state.baseline.test.ts`
- `src/commands/__tests__/proactive.baseline.test.ts`
- `src/utils/__tests__/cronTasks.baseline.test.ts`
- `src/utils/__tests__/cronScheduler.baseline.test.ts`
- `src/__tests__/context.baseline.test.ts`
## Baseline Assertions
### Proactive state
1. Activating proactive mode sets active state and activation source.
2. Pausing proactive mode suppresses `shouldTick()` and clears `nextTickAt`.
3. Blocking context suppresses `shouldTick()` and clears `nextTickAt`.
4. Subscribers are notified on state transitions.
5. The `/proactive` command enables proactive mode and emits the expected hidden reminder.
6. The `/proactive` command disables proactive mode on the second invocation.
### Cron task storage
1. Session-only cron tasks remain in memory only.
2. Durable cron tasks are persisted to `.claude/scheduled_tasks.json`.
3. Daemon-style `dir`-scoped reads exclude session-only cron tasks.
4. `removeCronTasks()` without `dir` can remove session-only tasks.
5. `removeCronTasks()` with `dir` does not mutate session-only task storage.
### Cron scheduler helpers
1. `isRecurringTaskAged()` preserves current aging semantics.
2. `buildMissedTaskNotification()` preserves the current AskUserQuestion safety wording.
3. `buildMissedTaskNotification()` preserves code-fence hardening for prompt bodies that contain backticks.
### User context caching
1. `getUserContext()` includes `currentDate`.
2. `getUserContext()` includes mocked `claudeMd` content when memory loading is enabled.
3. `CLAUDE_CODE_DISABLE_CLAUDE_MDS` suppresses `claudeMd`.
4. `setSystemPromptInjection()` clears the memoized user-context cache.
5. `getSystemContext()` reflects the injection after cache invalidation.
## Remaining Baseline Gaps
The following areas are intentionally deferred because they require higher-cost harnessing and should still avoid production-code changes:
1. `useScheduledTasks.ts` hook-level runtime behavior
2. `src/cli/print.ts` full headless scheduler loop behavior
3. `useProactive.ts` hook timer behavior
4. end-to-end queue interaction between proactive ticks and `SleepTool`
## Acceptance
This baseline round is complete when:
1. The four new test files pass.
2. No production source files are modified.
3. The tests are stable enough to serve as a pre-implementation guardrail.

View File

@@ -1,188 +0,0 @@
# Phase 16 — 零依赖纯函数测试
> 创建日期2026-04-02
> 预计:+120 tests / 8 files
> 目标:覆盖所有零外部依赖的纯函数/类模块
所有模块均为纯函数或零外部依赖类mock 成本为零ROI 最高。
---
## 16.1 `src/utils/__tests__/stream.test.ts`~15 tests
**目标模块**: `src/utils/stream.ts`76 行)
**导出**: `Stream<T>` class — 手动异步队列,实现 `AsyncIterator<T>`
| 测试用例 | 验证点 |
|---------|--------|
| enqueue then read | 单条消息正确传递 |
| enqueue multiple then drain | 多条消息顺序消费 |
| done resolves pending readers | `done()` 后迭代结束 |
| done with no pending readers | 无等待时安全关闭 |
| error rejects pending readers | `error(e)` 传播异常 |
| error after done | 后续操作安全处理 |
| single-iteration guard | `return()` 后不可再迭代 |
| empty stream done immediately | 无数据时 done 返回 `{ done: true }` |
| concurrent enqueue | 多次 enqueue 不丢失 |
| backpressure | reader 慢于 writer 时不丢数据 |
---
## 16.2 `src/utils/__tests__/abortController.test.ts`~12 tests
**目标模块**: `src/utils/abortController.ts`99 行)
**导出**: `createAbortController()`, `createChildAbortController()`
| 测试用例 | 验证点 |
|---------|--------|
| parent abort propagates to child | `parent.abort()` → child aborted |
| child abort does NOT propagate to parent | `child.abort()` → parent still active |
| already-aborted parent → child immediately aborted | 创建时即继承 abort 状态 |
| child listener cleanup after parent abort | WeakRef 回收后无泄漏 |
| multiple children of same parent | 独立 abort 传播 |
| child abort then parent abort | 顺序无关 |
| signal.maxListeners raised | MaxListenersExceededWarning 不触发 |
---
## 16.3 `src/utils/__tests__/bufferedWriter.test.ts`~14 tests
**目标模块**: `src/utils/bufferedWriter.ts`100 行)
**导出**: `createBufferedWriter()`
| 测试用例 | 验证点 |
|---------|--------|
| single write buffered | write → buffer 累积 |
| flush on size threshold | 超过 maxSize 时自动 flush |
| flush on timer | 定时器触发 flush |
| immediate mode | `{ immediate: true }` 跳过缓冲 |
| overflow coalescing | overflow 内容合并到下次 flush |
| empty buffer flush | 无数据时 flush 无副作用 |
| close flushes remaining | close 触发最终 flush |
| multiple writes before flush | 批量写入合并 |
| flush callback receives concatenated data | writeFn 参数正确 |
**Mock**: 注入 `writeFn` 回调,可选 fake timers
---
## 16.4 `src/utils/__tests__/gitDiff.test.ts`~20 tests
**目标模块**: `src/utils/gitDiff.ts`532 行)
**可测函数**: `parseGitNumstat()`, `parseGitDiff()`, `parseShortstat()`
| 测试用例 | 验证点 |
|---------|--------|
| parseGitNumstat — single file | `1\t2\tpath` → { added: 1, deleted: 2, file: "path" } |
| parseGitNumstat — binary file | `-\t-\timage.png` → binary flag |
| parseGitNumstat — rename | `{ old => new }` 格式解析 |
| parseGitNumstat — empty diff | 空字符串 → [] |
| parseGitNumstat — multiple files | 多行正确分割 |
| parseGitDiff — added lines | `+` 开头行计数 |
| parseGitDiff — deleted lines | `-` 开头行计数 |
| parseGitDiff — hunk header | `@@ -a,b +c,d @@` 解析 |
| parseGitDiff — new file mode | `new file mode 100644` 检测 |
| parseGitDiff — deleted file mode | `deleted file mode` 检测 |
| parseGitDiff — binary diff | Binary files differ 处理 |
| parseShortstat — all components | `1 file changed, 5 insertions(+), 3 deletions(-)` |
| parseShortstat — insertions only | 无 deletions |
| parseShortstat — deletions only | 无 insertions |
| parseShortstat — files only | 仅 file changed |
| parseShortstat — empty | 空字符串 → 默认值 |
| parseShortstat — rename | `1 file changed, ...` 重命名 |
**Mock**: 无需 mock — 全部是纯字符串解析
---
## 16.5 `src/__tests__/history.test.ts`~18 tests
**目标模块**: `src/history.ts`464 行)
**可测函数**: `parseReferences()`, `expandPastedTextRefs()`, `formatPastedTextRef()`, `formatImageRef()`, `getPastedTextRefNumLines()`
| 测试用例 | 验证点 |
|---------|--------|
| parseReferences — text ref | `#1` → [{ type: "text", ref: 1 }] |
| parseReferences — image ref | `@1` → [{ type: "image", ref: 1 }] |
| parseReferences — multiple refs | `#1 #2 @3` → 3 refs |
| parseReferences — no refs | `"hello"` → [] |
| parseReferences — duplicate refs | `#1 #1` → 去重或保留 |
| parseReferences — zero ref | `#0` → 边界 |
| parseReferences — large ref | `#999` → 正常 |
| formatPastedTextRef — basic | 输出格式验证 |
| formatPastedTextRef — multiline | 多行内容格式 |
| getPastedTextRefNumLines — 1 line | 返回 1 |
| getPastedTextRefNumLines — multiple lines | 换行计数 |
| expandPastedTextRefs — single ref | 替换单个引用 |
| expandPastedTextRefs — multiple refs | 替换多个引用 |
| expandPastedTextRefs — no refs | 原样返回 |
| expandPastedTextRefs — mixed content | 文本 + 引用混合 |
| formatImageRef — basic | 输出格式 |
**Mock**: `mock.module("src/bootstrap/state.ts", ...)` 解锁模块
---
## 16.6 `src/utils/__tests__/sliceAnsi.test.ts`~16 tests
**目标模块**: `src/utils/sliceAnsi.ts`91 行)
**导出**: `sliceAnsi()` — ANSI 感知的字符串切片
| 测试用例 | 验证点 |
|---------|--------|
| plain text slice | `"hello".slice(1,3)` 等价 |
| preserve ANSI codes | `\x1b[31mhello\x1b[0m` 切片后保留颜色 |
| close opened styles | 切片点在 ANSI 样式中间时正确关闭 |
| hyperlink handling | OSC 8 超链接不被切断 |
| combining marks (diacritics) | `é` = `e\u0301` 不被切开 |
| Devanagari matras | 零宽字符不被切断 |
| full-width characters | CJK 字符宽度 = 2 |
| empty slice | 返回空字符串 |
| full slice | 返回完整字符串 |
| boundary at ANSI code | 边界恰好在 escape 序列上 |
| nested ANSI styles | 多层嵌套时正确处理 |
| slice start > end | 空结果 |
**Mock**: `mock.module("@alcalzone/ansi-tokenize", ...)`, `mock.module("ink/stringWidth", ...)`
---
## 16.7 `src/utils/__tests__/treeify.test.ts`~15 tests
**目标模块**: `src/utils/treeify.ts`170 行)
**导出**: `treeify()` — 递归树渲染
| 测试用例 | 验证点 |
|---------|--------|
| simple flat tree | `{ a: {}, b: {} }` → 2 行 |
| nested tree | `{ a: { b: { c: {} } } }` → 3 行缩进 |
| array values | `[1, 2, 3]` 渲染为列表 |
| circular reference | 不无限递归 |
| empty object | `{}` 处理 |
| single key | 布局适配 |
| branch vs last-branch character | ├─ vs └─ |
| custom prefix | options 前缀传递 |
| deep nesting | 5+ 层缩进正确 |
| mixed object/array | 混合结构 |
**Mock**: `mock.module("figures", ...)`, color 模块 mock
---
## 16.8 `src/utils/__tests__/words.test.ts`~10 tests
**目标模块**: `src/utils/words.ts`800 行,大部分是词表数据)
**导出**: `generateWordSlug()`, `generateShortWordSlug()`
| 测试用例 | 验证点 |
|---------|--------|
| generateWordSlug format | `adjective-verb-noun` 三段式 |
| generateShortWordSlug format | `adjective-noun` 两段式 |
| all parts non-empty | 无空段 |
| hyphen separator | `-` 分隔 |
| all parts from word lists | 成分来自预定义词表 |
| multiple calls uniqueness | 连续调用不总是相同 |
| no consecutive hyphens | 无 `--` |
| lowercase only | 全小写 |
**Mock**: `mock.module("crypto", ...)` 控制 `randomBytes` 实现确定性测试

View File

@@ -1,203 +0,0 @@
# Phase 17 — Tool 子模块纯逻辑测试
> 创建日期2026-04-02
> 预计:+150 tests / 11 files
> 目标:覆盖 Tool 目录下有丰富纯逻辑但零测试的子模块
---
## 17.1 `src/tools/PowerShellTool/__tests__/powershellSecurity.test.ts`~25 tests
**目标模块**: `src/tools/PowerShellTool/powershellSecurity.ts`1091 行)
**安全关键** — 检测 ~20 种攻击向量。
| 测试分组 | 测试数 | 验证点 |
|---------|-------|--------|
| Invoke-Expression 检测 | 3 | `IEX`, `Invoke-Expression`, 变形 |
| Download cradle 检测 | 3 | `Net.WebClient`, `Invoke-WebRequest`, pipe |
| Privilege escalation | 3 | `Start-Process -Verb RunAs`, `runas.exe` |
| COM object | 2 | `New-Object -ComObject`, WScript.Shell |
| Scheduled tasks | 2 | `schtasks`, `Register-ScheduledTask` |
| WMI | 2 | `Invoke-WmiMethod`, `Get-WmiObject` |
| Module loading | 2 | `Import-Module` 从网络路径 |
| 安全命令通过 | 3 | `Get-Process`, `Get-ChildItem`, `Write-Host` |
| 混淆绕过尝试 | 3 | base64, 字符串拼接, 空格变形 |
| 组合命令 | 2 | `;` 分隔的多命令 |
**Mock**: 构造 `ParsedPowerShellCommand` 对象(不需要真实 AST
---
## 17.2 `src/tools/PowerShellTool/__tests__/commandSemantics.test.ts`~10 tests
**目标模块**: `src/tools/PowerShellTool/commandSemantics.ts`143 行)
| 测试用例 | 验证点 |
|---------|--------|
| grep exit 0/1/2 | 语义映射 |
| robocopy exit codes | Windows 特殊退出码 |
| findstr exit codes | Windows find 工具 |
| unknown command | 默认语义 |
| extractBaseCommand — basic | `grep "pattern" file``grep` |
| extractBaseCommand — path | `C:\tools\rg.exe``rg` |
| heuristicallyExtractBaseCommand | 模糊匹配 |
---
## 17.3 `src/tools/PowerShellTool/__tests__/destructiveCommandWarning.test.ts`~15 tests
**目标模块**: `src/tools/PowerShellTool/destructiveCommandWarning.ts`110 行)
| 测试用例 | 验证点 |
|---------|--------|
| Remove-Item -Recurse -Force | 危险 |
| Format-Volume | 危险 |
| git reset --hard | 危险 |
| DROP TABLE | 危险 |
| Remove-Item (no -Force) | 安全 |
| Get-ChildItem | 安全 |
| 管道组合 | `rm -rf` + pipe |
| 大小写混合 | `ReMoVe-ItEm` |
---
## 17.4 `src/tools/PowerShellTool/__tests__/gitSafety.test.ts`~12 tests
**目标模块**: `src/tools/PowerShellTool/gitSafety.ts`177 行)
| 测试用例 | 验证点 |
|---------|--------|
| normalizeGitPathArg — forward slash | 规范化 |
| normalizeGitPathArg — backslash | Windows 路径规范化 |
| normalizeGitPathArg — NTFS short name | `GITFI~1``.git` |
| isGitInternalPathPS — .git/config | true |
| isGitInternalPathPS — normal file | false |
| isDotGitPathPS — hidden git dir | true |
| isDotGitPathPS — .gitignore | false |
| bare repo attack | `.git` 路径遍历 |
---
## 17.5 `src/tools/LSPTool/__tests__/formatters.test.ts`~20 tests
**目标模块**: `src/tools/LSPTool/formatters.ts`593 行)
| 测试用例 | 验证点 |
|---------|--------|
| formatGoToDefinitionResult — single | 单个定义 |
| formatGoToDefinitionResult — multiple | 多个定义(分组) |
| formatFindReferencesResult | 引用列表 |
| formatHoverResult — markdown | markdown 内容 |
| formatHoverResult — plaintext | 纯文本 |
| formatDocumentSymbolResult — classes | 类符号 |
| formatDocumentSymbolResult — functions | 函数符号 |
| formatDocumentSymbolResult — nested | 嵌套符号 |
| formatWorkspaceSymbolResult | 工作区符号 |
| formatPrepareCallHierarchyResult | 调用层次 |
| formatIncomingCallsResult | 入调用 |
| formatOutgoingCallsResult | 出调用 |
| empty results | 各函数空结果 |
| groupByFile helper | 文件分组逻辑 |
---
## 17.6 `src/tools/GrepTool/__tests__/utils.test.ts`~10 tests
**目标模块**: `src/tools/GrepTool/GrepTool.ts`577 行)
| 测试用例 | 验证点 |
|---------|--------|
| applyHeadLimit — within limit | 不截断 |
| applyHeadLimit — exceeds limit | 正确截断 |
| applyHeadLimit — offset + limit | 分页逻辑 |
| applyHeadLimit — zero limit | 边界 |
| formatLimitInfo — basic | 格式化输出 |
**Mock**: `mock.module("src/utils/log.ts", ...)` 解锁导入
---
## 17.7 `src/tools/WebFetchTool/__tests__/utils.test.ts`~15 tests
**目标模块**: `src/tools/WebFetchTool/utils.ts`531 行)
| 测试用例 | 验证点 |
|---------|--------|
| validateURL — valid http | 通过 |
| validateURL — valid https | 通过 |
| validateURL — ftp | 拒绝 |
| validateURL — no protocol | 拒绝 |
| validateURL — localhost | 处理 |
| isPermittedRedirect — same host | 允许 |
| isPermittedRedirect — different host | 拒绝 |
| isPermittedRedirect — subdomain | 处理 |
| isRedirectInfo — valid object | true |
| isRedirectInfo — invalid | false |
---
## 17.8 `src/tools/WebFetchTool/__tests__/preapproved.test.ts`~10 tests
**目标模块**: `src/tools/WebFetchTool/preapproved.ts`167 行)
| 测试用例 | 验证点 |
|---------|--------|
| exact hostname match | 通过 |
| subdomain match | 处理 |
| path prefix match | `/docs/api` 匹配 |
| path non-match | `/internal` 不匹配 |
| unknown hostname | false |
| empty pathname | 边界 |
---
## 17.9 `src/tools/FileReadTool/__tests__/utils.test.ts`~15 tests
**目标模块**: `src/tools/FileReadTool/FileReadTool.ts`1184 行)
| 测试用例 | 验证点 |
|---------|--------|
| isBlockedDevicePath — /dev/sda | true |
| isBlockedDevicePath — /dev/null | 处理 |
| isBlockedDevicePath — normal file | false |
| detectSessionFileType — .jsonl | 会话文件类型 |
| detectSessionFileType — unknown | 未知类型 |
| formatFileLines — basic | 行号格式 |
| formatFileLines — empty | 空文件 |
---
## 17.10 `src/tools/AgentTool/__tests__/agentToolUtils.test.ts`~18 tests
**目标模块**: `src/tools/AgentTool/agentToolUtils.ts`688 行)
| 测试用例 | 验证点 |
|---------|--------|
| filterToolsForAgent — builtin only | 只返回内置工具 |
| filterToolsForAgent — exclude async | 排除异步工具 |
| filterToolsForAgent — permission mode | 权限过滤 |
| resolveAgentTools — wildcard | 通配符展开 |
| resolveAgentTools — explicit list | 显式列表 |
| countToolUses — multiple | 消息中工具调用计数 |
| countToolUses — zero | 无工具调用 |
| extractPartialResult — text only | 提取文本 |
| extractPartialResult — mixed | 混合内容 |
| getLastToolUseName — basic | 最后工具名 |
| getLastToolUseName — no tool use | 无工具调用 |
**Mock**: `mock.module("src/bootstrap/state.ts", ...)`, `mock.module("src/utils/log.ts", ...)`
---
## 17.11 `src/tools/LSPTool/__tests__/schemas.test.ts`~5 tests
**目标模块**: `src/tools/LSPTool/schemas.ts`216 行)
| 测试用例 | 验证点 |
|---------|--------|
| isValidLSPOperation — goToDefinition | true |
| isValidLSPOperation — findReferences | true |
| isValidLSPOperation — hover | true |
| isValidLSPOperation — invalid | false |
| isValidLSPOperation — empty string | false |

View File

@@ -1,110 +0,0 @@
# Phase 18 — WEAK 修复 + ACCEPTABLE 加固
> 创建日期2026-04-02
> 预计:+30 tests / 4 files (修改现有)
> 目标:修复所有 WEAK 评分测试文件,消除系统性问题
---
## 18.1 `src/utils/__tests__/format.test.ts` — 断言精确化(+5 tests
**问题**: `formatNumber`/`formatTokens`/`formatRelativeTime` 使用 `toContain`
**修复**: 改为 `toBe` 精确匹配
```diff
- expect(formatNumber(1500000)).toContain("1.5")
+ expect(formatNumber(1500000)).toBe("1.5m")
```
新增测试:
| 测试用例 | 验证点 |
|---------|--------|
| formatNumber — 0 | `"0"` |
| formatNumber — billions | `"1.5b"` |
| formatTokens — thousands | 精确匹配 |
| formatRelativeTime — hours ago | 精确匹配 |
| formatRelativeTime — days ago | 精确匹配 |
---
## 18.2 `src/utils/__tests__/envValidation.test.ts` — Bug 确认(+3 tests
**问题**: `value=1, lowerBound=100` 返回 `status: "valid"` — 函数名暗示有下界检查
**计划**: 先读取源码确认 `defaultValue``lowerBound` 的语义关系,然后:
- 如果是源码 bug → 在测试中注释标记,不修改源码
- 如果是设计意图 → 更新测试描述明确语义
新增测试:
| 测试用例 | 验证点 |
|---------|--------|
| parseFloat truncation | `"50.9"` → 50 |
| whitespace handling | `" 500 "` → 500 |
| very large number | overflow 处理 |
---
## 18.3 `src/utils/permissions/__tests__/PermissionMode.test.ts` — false 路径(+8 tests
**问题**: `isExternalPermissionMode` false 路径从未执行
**修复**: 覆盖所有 5 种 mode 的 true/false 期望
| 测试用例 | 验证点 |
|---------|--------|
| isExternalPermissionMode — plan | false |
| isExternalPermissionMode — auto | false |
| isExternalPermissionMode — default | false |
| permissionModeFromString — all modes | 5 种 mode 全覆盖 |
| permissionModeFromString — invalid | 默认值 |
| permissionModeFromString — case insensitive | 大小写 |
| isPermissionMode — valid strings | true |
| isPermissionMode — invalid strings | false |
---
## 18.4 `src/tools/shared/__tests__/gitOperationTracking.test.ts` — mock analytics+4 tests
**问题**: 未 mock analytics 依赖,测试产生副作用
**修复**: 添加 `mock.module("src/services/analytics/...", ...)`
新增测试:
| 测试用例 | 验证点 |
|---------|--------|
| parseGitCommitId — all GH PR actions | 补齐 6 个 action |
| detectGitOperation — no analytics call | mock 验证 |
| detectGitCommitId — various formats | SHA/短 SHA/HEAD |
| git operation tracking — edge cases | 空输入、畸形输入 |
---
## 排除清单
以下模块 **不纳入测试**,原因合理:
| 模块 | 行数 | 排除原因 |
|------|------|---------|
| `query.ts` | 1732 | 核心循环40+ 依赖,需完整集成环境 |
| `QueryEngine.ts` | 1320 | 编排器30+ 依赖 |
| `utils/hooks.ts` | 5121 | 51 exportsspawn 子进程 |
| `utils/config.ts` | 1817 | 文件系统 + lockfile + 全局状态 |
| `utils/auth.ts` | 2002 | 多 provider 认证,平台特定 |
| `utils/fileHistory.ts` | 1115 | 重 I/O 文件备份 |
| `utils/sessionRestore.ts` | 551 | 恢复状态涉及多个子系统 |
| `utils/ripgrep.ts` | 679 | spawn 子进程 |
| `utils/yaml.ts` | 15 | 两行 wrapper |
| `utils/lockfile.ts` | 43 | trivial wrapper |
| `screens/` / `components/` | — | Ink 渲染测试环境 |
| `bridge/` / `remote/` / `ssh/` | — | 网络层 |
| `daemon/` / `server/` | — | 进程管理 |
---
## 预期成果
| 指标 | Phase 16 后 | Phase 17 后 | Phase 18 后 |
|------|-----------|-----------|-----------|
| 测试数 | ~1417 | ~1567 | ~1597 |
| 文件数 | 76 | 87 | 91 |
| WEAK 文件 | 6 | 4 | **0** |

View File

@@ -1,435 +0,0 @@
# Phase 19 - Batch 1: 零依赖微型 utils
> 预计 ~154 tests / 13 文件 | 全部纯函数,无需 mock
---
## 1. `src/utils/__tests__/semanticBoolean.test.ts` (~8 tests)
**源文件**: `src/utils/semanticBoolean.ts` (30 行)
**依赖**: `zod/v4`
### 测试用例
```typescript
describe("semanticBoolean", () => {
// 基本 Zod 行为
test("parses boolean true to true")
test("parses boolean false to false")
test("parses string 'true' to true")
test("parses string 'false' to false")
// 边界
test("rejects string 'TRUE' (case-sensitive)")
test("rejects string 'FALSE' (case-sensitive)")
test("rejects number 1")
test("rejects null")
test("rejects undefined")
// 自定义 inner schema
test("works with custom inner schema (z.boolean().optional())")
})
```
### Mock 需求
---
## 2. `src/utils/__tests__/semanticNumber.test.ts` (~10 tests)
**源文件**: `src/utils/semanticNumber.ts` (37 行)
**依赖**: `zod/v4`
### 测试用例
```typescript
describe("semanticNumber", () => {
test("parses number 42")
test("parses number 0")
test("parses negative number -5")
test("parses float 3.14")
test("parses string '42' to 42")
test("parses string '-7.5' to -7.5")
test("rejects string 'abc'")
test("rejects empty string ''")
test("rejects null")
test("rejects boolean true")
test("works with custom inner schema (z.number().int().min(0))")
})
```
### Mock 需求
---
## 3. `src/utils/__tests__/lazySchema.test.ts` (~6 tests)
**源文件**: `src/utils/lazySchema.ts` (9 行)
**依赖**: 无
### 测试用例
```typescript
describe("lazySchema", () => {
test("returns a function")
test("calls factory on first invocation")
test("returns cached result on subsequent invocations")
test("factory is called only once (call count verification)")
test("works with different return types")
test("each call to lazySchema returns independent cache")
})
```
### Mock 需求
---
## 4. `src/utils/__tests__/withResolvers.test.ts` (~8 tests)
**源文件**: `src/utils/withResolvers.ts` (14 行)
**依赖**: 无
### 测试用例
```typescript
describe("withResolvers", () => {
test("returns object with promise, resolve, reject")
test("promise resolves when resolve is called")
test("promise rejects when reject is called")
test("resolve passes value through")
test("reject passes error through")
test("promise is instanceof Promise")
test("works with generic type parameter")
test("resolve/reject can be called asynchronously")
})
```
### Mock 需求
---
## 5. `src/utils/__tests__/userPromptKeywords.test.ts` (~12 tests)
**源文件**: `src/utils/userPromptKeywords.ts` (28 行)
**依赖**: 无
### 测试用例
```typescript
describe("matchesNegativeKeyword", () => {
test("matches 'wtf'")
test("matches 'shit'")
test("matches 'fucking broken'")
test("does not match normal input like 'fix the bug'")
test("is case-insensitive")
test("matches partial word in sentence")
})
describe("matchesKeepGoingKeyword", () => {
test("matches exact 'continue'")
test("matches 'keep going'")
test("matches 'go on'")
test("does not match 'cont'")
test("does not match empty string")
test("matches within larger sentence 'please continue'")
})
```
### Mock 需求
---
## 6. `src/utils/__tests__/xdg.test.ts` (~15 tests)
**源文件**: `src/utils/xdg.ts` (66 行)
**依赖**: 无(通过 options 参数注入)
### 测试用例
```typescript
describe("getXDGStateHome", () => {
test("returns ~/.local/state by default")
test("respects XDG_STATE_HOME env var")
test("uses custom homedir from options")
})
describe("getXDGCacheHome", () => {
test("returns ~/.cache by default")
test("respects XDG_CACHE_HOME env var")
})
describe("getXDGDataHome", () => {
test("returns ~/.local/share by default")
test("respects XDG_DATA_HOME env var")
})
describe("getUserBinDir", () => {
test("returns ~/.local/bin")
test("uses custom homedir from options")
})
describe("resolveOptions", () => {
test("defaults env to process.env")
test("defaults homedir to os.homedir()")
test("merges partial options")
})
describe("path construction", () => {
test("all paths end with correct subdirectory")
test("respects HOME env via homedir override")
})
```
### Mock 需求
无(通过 options.env 和 options.homedir 注入)
---
## 7. `src/utils/__tests__/horizontalScroll.test.ts` (~20 tests)
**源文件**: `src/utils/horizontalScroll.ts` (138 行)
**依赖**: 无
### 测试用例
```typescript
describe("calculateHorizontalScrollWindow", () => {
// 基本场景
test("all items fit within available width")
test("single item selected within view")
test("selected item at beginning")
test("selected item at end")
test("selected item beyond visible range scrolls right")
test("selected item before visible range scrolls left")
// 箭头指示器
test("showLeftArrow when items hidden on left")
test("showRightArrow when items hidden on right")
test("no arrows when all items visible")
test("both arrows when items hidden on both sides")
// 边界条件
test("empty itemWidths array")
test("single item")
test("available width is 0")
test("item wider than available width")
test("all items same width")
test("varying item widths")
test("firstItemHasSeparator adds separator width to first item")
test("selectedIdx in middle of overflow")
test("scroll snaps to show selected at left edge")
test("scroll snaps to show selected at right edge")
})
```
### Mock 需求
---
## 8. `src/utils/__tests__/generators.test.ts` (~18 tests)
**源文件**: `src/utils/generators.ts` (89 行)
**依赖**: 无
### 测试用例
```typescript
describe("lastX", () => {
test("returns last yielded value")
test("returns only value from single-yield generator")
test("throws on empty generator")
})
describe("returnValue", () => {
test("returns generator return value")
test("returns undefined for void return")
})
describe("toArray", () => {
test("collects all yielded values")
test("returns empty array for empty generator")
test("preserves order")
})
describe("fromArray", () => {
test("yields all array elements")
test("yields nothing for empty array")
})
describe("all", () => {
test("merges multiple generators preserving yield order")
test("respects concurrency cap")
test("handles empty generator array")
test("handles single generator")
test("handles generators of different lengths")
test("yields all values from all generators")
})
```
### Mock 需求
无(用 fromArray 构造测试数据)
---
## 9. `src/utils/__tests__/sequential.test.ts` (~12 tests)
**源文件**: `src/utils/sequential.ts` (57 行)
**依赖**: 无
### 测试用例
```typescript
describe("sequential", () => {
test("wraps async function, returns same result")
test("single call resolves normally")
test("concurrent calls execute sequentially (FIFO order)")
test("preserves arguments correctly")
test("error in first call does not block subsequent calls")
test("preserves rejection reason")
test("multiple args passed correctly")
test("returns different wrapper for each call to sequential")
test("handles rapid concurrent calls")
test("execution order matches call order")
test("works with functions returning different types")
test("wrapper has same arity expectations")
})
```
### Mock 需求
---
## 10. `src/utils/__tests__/fingerprint.test.ts` (~15 tests)
**源文件**: `src/utils/fingerprint.ts` (77 行)
**依赖**: `crypto` (内置)
### 测试用例
```typescript
describe("FINGERPRINT_SALT", () => {
test("has expected value '59cf53e54c78'")
})
describe("extractFirstMessageText", () => {
test("extracts text from first user message")
test("extracts text from single user message with array content")
test("returns empty string when no user messages")
test("skips assistant messages")
test("handles mixed content blocks (text + image)")
})
describe("computeFingerprint", () => {
test("returns deterministic 3-char hex string")
test("same input produces same fingerprint")
test("different message text produces different fingerprint")
test("different version produces different fingerprint")
test("handles short strings (length < 21)")
test("handles empty string")
test("fingerprint is valid hex")
})
describe("computeFingerprintFromMessages", () => {
test("end-to-end: messages -> fingerprint")
})
```
### Mock 需求
需要 `mock.module` 处理 `UserMessage`/`AssistantMessage` 类型依赖(查看实际 import 情况)
---
## 11. `src/utils/__tests__/configConstants.test.ts` (~8 tests)
**源文件**: `src/utils/configConstants.ts` (22 行)
**依赖**: 无
### 测试用例
```typescript
describe("NOTIFICATION_CHANNELS", () => {
test("contains expected channels")
test("is readonly array")
test("includes 'auto', 'iterm2', 'terminal_bell'")
})
describe("EDITOR_MODES", () => {
test("contains 'normal' and 'vim'")
test("has exactly 2 entries")
})
describe("TEAMMATE_MODES", () => {
test("contains 'auto', 'tmux', 'in-process'")
test("has exactly 3 entries")
})
```
### Mock 需求
---
## 12. `src/utils/__tests__/directMemberMessage.test.ts` (~12 tests)
**源文件**: `src/utils/directMemberMessage.ts` (70 行)
**依赖**: 仅类型(可 mock
### 测试用例
```typescript
describe("parseDirectMemberMessage", () => {
test("parses '@agent-name hello world'")
test("parses '@agent-name single-word'")
test("returns null for non-matching input")
test("returns null for empty string")
test("returns null for '@name' without message")
test("handles hyphenated agent names like '@my-agent msg'")
test("handles multiline message content")
test("extracts correct recipientName and message")
})
// sendDirectMemberMessage 需要 mock teamContext/writeToMailbox
describe("sendDirectMemberMessage", () => {
test("returns error when no team context")
test("returns error for unknown recipient")
test("calls writeToMailbox with correct args for valid recipient")
test("returns success for valid message")
})
```
### Mock 需求
`sendDirectMemberMessage` 需要 mock `AppState['teamContext']``WriteToMailboxFn`
---
## 13. `src/utils/__tests__/collapseHookSummaries.test.ts` (~12 tests)
**源文件**: `src/utils/collapseHookSummaries.ts` (60 行)
**依赖**: 仅类型
### 测试用例
```typescript
describe("collapseHookSummaries", () => {
test("returns same messages when no hook summaries")
test("collapses consecutive messages with same hookLabel")
test("does not collapse messages with different hookLabels")
test("aggregates hookCount across collapsed messages")
test("merges hookInfos arrays")
test("merges hookErrors arrays")
test("takes max totalDurationMs")
test("takes any truthy preventContinuation")
test("leaves single hook summary unchanged")
test("handles three consecutive same-label summaries")
test("preserves non-hook messages in between")
test("returns empty array for empty input")
})
```
### Mock 需求
需要构造 `RenderableMessage` mock 对象

View File

@@ -1,287 +0,0 @@
# Phase 19 - Batch 2: 更多 utils + state + commands
> 预计 ~120 tests / 8 文件 | 部分需轻量 mock
---
## 1. `src/utils/__tests__/collapseTeammateShutdowns.test.ts` (~10 tests)
**源文件**: `src/utils/collapseTeammateShutdowns.ts` (56 行)
**依赖**: 仅类型
### 测试用例
```typescript
describe("collapseTeammateShutdowns", () => {
test("returns same messages when no teammate shutdowns")
test("leaves single shutdown message unchanged")
test("collapses consecutive shutdown messages into batch")
test("batch attachment has correct count")
test("does not collapse non-consecutive shutdowns")
test("preserves non-shutdown messages between shutdowns")
test("handles empty array")
test("handles mixed message types")
test("collapses more than 2 consecutive shutdowns")
test("non-teammate task_status messages are not collapsed")
})
```
### Mock 需求
构造 `RenderableMessage` mock 对象(带 `task_status` attachment`status=completed``taskType=in_process_teammate`
---
## 2. `src/utils/__tests__/privacyLevel.test.ts` (~12 tests)
**源文件**: `src/utils/privacyLevel.ts` (56 行)
**依赖**: `process.env`
### 测试用例
```typescript
describe("getPrivacyLevel", () => {
test("returns 'default' when no env vars set")
test("returns 'essential-traffic' when CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is set")
test("returns 'no-telemetry' when DISABLE_TELEMETRY is set")
test("'essential-traffic' takes priority over 'no-telemetry'")
})
describe("isEssentialTrafficOnly", () => {
test("returns true for 'essential-traffic' level")
test("returns false for 'default' level")
test("returns false for 'no-telemetry' level")
})
describe("isTelemetryDisabled", () => {
test("returns true for 'no-telemetry' level")
test("returns true for 'essential-traffic' level")
test("returns false for 'default' level")
})
describe("getEssentialTrafficOnlyReason", () => {
test("returns env var name when restricted")
test("returns null when unrestricted")
})
```
### Mock 需求
`process.env` 保存/恢复模式(参考现有 `envUtils.test.ts`
---
## 3. `src/utils/__tests__/textHighlighting.test.ts` (~18 tests)
**源文件**: `src/utils/textHighlighting.ts` (167 行)
**依赖**: `@alcalzone/ansi-tokenize`
### 测试用例
```typescript
describe("segmentTextByHighlights", () => {
// 基本
test("returns single segment with no highlights")
test("returns highlighted segment for single highlight")
test("returns two segments for highlight covering middle portion")
test("returns three segments for highlight in the middle")
// 多高亮
test("handles non-overlapping highlights")
test("handles overlapping highlights (priority-based)")
test("handles adjacent highlights")
// 边界
test("highlight starting at 0")
test("highlight ending at text length")
test("highlight covering entire text")
test("empty text with highlights")
test("empty highlights array returns single segment")
// ANSI 处理
test("correctly segments text with ANSI escape codes")
test("handles text with mixed ANSI and highlights")
// 属性
test("preserves highlight color property")
test("preserves highlight priority property")
test("preserves dimColor and inverse flags")
test("highlights with start > end are handled gracefully")
})
```
### Mock 需求
可能需要 mock `@alcalzone/ansi-tokenize`,或直接使用(如果有安装)
---
## 4. `src/utils/__tests__/detectRepository.test.ts` (~15 tests)
**源文件**: `src/utils/detectRepository.ts` (179 行)
**依赖**: git 命令(`getRemoteUrl`
### 重点测试函数
**`parseGitRemote(input: string): ParsedRepository | null`** — 纯正则解析
**`parseGitHubRepository(input: string): string | null`** — 纯函数
### 测试用例
```typescript
describe("parseGitRemote", () => {
// HTTPS
test("parses HTTPS URL: https://github.com/owner/repo.git")
test("parses HTTPS URL without .git suffix")
test("parses HTTPS URL with subdirectory path (only takes first 2 segments)")
// SSH
test("parses SSH URL: git@github.com:owner/repo.git")
test("parses SSH URL without .git suffix")
// ssh://
test("parses ssh:// URL: ssh://git@github.com/owner/repo.git")
// git://
test("parses git:// URL")
// 边界
test("returns null for invalid URL")
test("returns null for empty string")
test("handles GHE hostname")
test("handles port number in URL")
})
describe("parseGitHubRepository", () => {
test("extracts 'owner/repo' from valid remote URL")
test("handles plain 'owner/repo' string input")
test("returns null for non-GitHub host (if restricted)")
test("returns null for invalid input")
test("is case-sensitive for owner/repo")
})
```
### Mock 需求
仅测试 `parseGitRemote``parseGitHubRepository`(纯函数),不需要 mock git
---
## 5. `src/utils/__tests__/markdown.test.ts` (~20 tests)
**源文件**: `src/utils/markdown.ts` (382 行)
**依赖**: `marked`, `cli-highlight`, theme types
### 重点测试函数
**`padAligned(content, displayWidth, targetWidth, align)`** — 纯函数
### 测试用例
```typescript
describe("padAligned", () => {
test("left-aligns: pads with spaces on right")
test("right-aligns: pads with spaces on left")
test("center-aligns: pads with spaces on both sides")
test("no padding when displayWidth equals targetWidth")
test("handles content wider than targetWidth")
test("null/undefined align defaults to left")
test("handles empty string content")
test("handles zero displayWidth")
test("handles zero targetWidth")
test("center alignment with odd padding distribution")
})
```
注意:`numberToLetter`/`numberToRoman`/`getListNumber` 是私有函数,除非从模块导出否则无法直接测试。如果确实私有,则通过 `applyMarkdown` 间接测试列表渲染:
```typescript
describe("list numbering (via applyMarkdown)", () => {
test("numbered list renders with digits")
test("nested ordered list uses letters (a, b, c)")
test("deep nested list uses roman numerals")
test("unordered list uses bullet markers")
})
```
### Mock 需求
`padAligned` 无需 mock。`applyMarkdown` 可能需要 mock theme 依赖。
---
## 6. `src/state/__tests__/store.test.ts` (~15 tests)
**源文件**: `src/state/store.ts` (35 行)
**依赖**: 无
### 测试用例
```typescript
describe("createStore", () => {
test("returns object with getState, setState, subscribe")
test("getState returns initial state")
test("setState updates state via updater function")
test("setState does not notify when state unchanged (Object.is)")
test("setState notifies subscribers on change")
test("subscribe returns unsubscribe function")
test("unsubscribe stops notifications")
test("multiple subscribers all get notified")
test("onChange callback is called on state change")
test("onChange is not called when state unchanged")
test("works with complex state objects")
test("works with primitive state")
test("updater receives previous state")
test("sequential setState calls produce final state")
test("subscriber called after all state changes in synchronous batch")
})
```
### Mock 需求
---
## 7. `src/commands/plugin/__tests__/parseArgs.test.ts` (~18 tests)
**源文件**: `src/commands/plugin/parseArgs.ts` (104 行)
**依赖**: 无
### 测试用例
```typescript
describe("parsePluginArgs", () => {
// 无参数
test("returns { type: 'menu' } for undefined")
test("returns { type: 'menu' } for empty string")
test("returns { type: 'menu' } for whitespace only")
// help
test("returns { type: 'help' } for 'help'")
// install
test("parses 'install my-plugin' -> { type: 'install', name: 'my-plugin' }")
test("parses 'install my-plugin@github' with marketplace")
test("parses 'install https://github.com/...' as URL marketplace")
// uninstall
test("returns { type: 'uninstall', name: '...' }")
// enable/disable
test("returns { type: 'enable', name: '...' }")
test("returns { type: 'disable', name: '...' }")
// validate
test("returns { type: 'validate', name: '...' }")
// manage
test("returns { type: 'manage' }")
// marketplace 子命令
test("parses 'marketplace add ...'")
test("parses 'marketplace remove ...'")
test("parses 'marketplace list'")
// 边界
test("handles extra whitespace")
test("handles unknown subcommand gracefully")
})
```
### Mock 需求

View File

@@ -1,258 +0,0 @@
# Phase 19 - Batch 3: Tool 子模块纯逻辑
> 预计 ~113 tests / 6 文件 | 采用 `mock.module()` + `await import()` 模式
---
## 1. `src/tools/GrepTool/__tests__/headLimit.test.ts` (~20 tests)
**源文件**: `src/tools/GrepTool/GrepTool.ts` (578 行)
**目标函数**: `applyHeadLimit<T>`, `formatLimitInfo` (非导出,需确认可测性)
### 测试策略
如果函数是文件内导出的,直接 `await import()` 获取。如果私有,则通过 GrepTool 的输出间接测试,或提取到独立文件。
### 测试用例
```typescript
describe("applyHeadLimit", () => {
test("returns full array when limit is undefined (default 250)")
test("applies limit correctly: limits to N items")
test("limit=0 means no limit (returns all)")
test("applies offset correctly")
test("offset + limit combined")
test("offset beyond array length returns empty")
test("returns appliedLimit when truncation occurred")
test("returns appliedLimit=undefined when no truncation")
test("limit larger than array returns all items with appliedLimit=undefined")
test("empty array returns empty with appliedLimit=undefined")
test("offset=0 is default")
test("negative limit behavior")
})
describe("formatLimitInfo", () => {
test("formats 'limit: N, offset: M' when both present")
test("formats 'limit: N' when only limit")
test("formats 'offset: M' when only offset")
test("returns empty string when both undefined")
test("handles limit=0 (no limit, should not appear)")
})
```
### Mock 需求
需 mock 重依赖链(`log`, `slowOperations` 等),通过 `mock.module()` + `await import()` 只取目标函数
---
## 2. `src/tools/MCPTool/__tests__/classifyForCollapse.test.ts` (~25 tests)
**源文件**: `src/tools/MCPTool/classifyForCollapse.ts` (605 行)
**目标函数**: `classifyMcpToolForCollapse`, `normalize`
### 测试用例
```typescript
describe("normalize", () => {
test("leaves snake_case unchanged: 'search_issues'")
test("converts camelCase to snake_case: 'searchIssues' -> 'search_issues'")
test("converts kebab-case to snake_case: 'search-issues' -> 'search_issues'")
test("handles mixed: 'searchIssuesByStatus' -> 'search_issues_by_status'")
test("handles already lowercase single word")
test("handles empty string")
test("handles PascalCase: 'SearchIssues' -> 'search_issues'")
})
describe("classifyMcpToolForCollapse", () => {
// 搜索工具
test("classifies Slack search_messages as search")
test("classifies GitHub search_code as search")
test("classifies Linear search_issues as search")
test("classifies Datadog search_logs as search")
test("classifies Notion search as search")
// 读取工具
test("classifies Slack get_message as read")
test("classifies GitHub get_file_contents as read")
test("classifies Linear get_issue as read")
test("classifies Filesystem read_file as read")
// 双重分类
test("some tools are both search and read")
test("some tools are neither search nor read")
// 未知工具
test("unknown tool returns { isSearch: false, isRead: false }")
test("tool name with camelCase variant still matches")
test("tool name with kebab-case variant still matches")
// server name 不影响分类
test("server name parameter is accepted but unused in current logic")
// 边界
test("empty tool name returns false/false")
test("case sensitivity check (should match after normalize)")
test("handles tool names with numbers")
})
```
### Mock 需求
文件自包含(仅内部 Set + normalize 函数),需确认 `normalize` 是否导出
---
## 3. `src/tools/FileReadTool/__tests__/blockedPaths.test.ts` (~18 tests)
**源文件**: `src/tools/FileReadTool/FileReadTool.ts` (1184 行)
**目标函数**: `isBlockedDevicePath`, `getAlternateScreenshotPath`
### 测试用例
```typescript
describe("isBlockedDevicePath", () => {
// 阻止的设备
test("blocks /dev/zero")
test("blocks /dev/random")
test("blocks /dev/urandom")
test("blocks /dev/full")
test("blocks /dev/stdin")
test("blocks /dev/tty")
test("blocks /dev/console")
test("blocks /dev/stdout")
test("blocks /dev/stderr")
test("blocks /dev/fd/0")
test("blocks /dev/fd/1")
test("blocks /dev/fd/2")
// 阻止 /proc
test("blocks /proc/self/fd/0")
test("blocks /proc/123/fd/2")
// 允许的路径
test("allows /dev/null")
test("allows regular file paths")
test("allows /home/user/file.txt")
})
describe("getAlternateScreenshotPath", () => {
test("returns undefined for path without AM/PM")
test("returns alternate path for macOS screenshot with regular space before AM")
test("returns alternate path for macOS screenshot with U+202F before PM")
test("handles path without time component")
test("handles multiple AM/PM occurrences")
test("returns undefined when no space variant difference")
})
```
### Mock 需求
需 mock 重依赖链,通过 `await import()` 获取函数
---
## 4. `src/tools/AgentTool/__tests__/agentDisplay.test.ts` (~15 tests)
**源文件**: `src/tools/AgentTool/agentDisplay.ts` (105 行)
**目标函数**: `resolveAgentOverrides`, `compareAgentsByName`
### 测试用例
```typescript
describe("resolveAgentOverrides", () => {
test("marks no overrides when all agents active")
test("marks inactive agent as overridden")
test("overriddenBy shows the overriding agent source")
test("deduplicates agents by (agentType, source)")
test("preserves agent definition properties")
test("handles empty arrays")
test("handles agent from git worktree (duplicate detection)")
})
describe("compareAgentsByName", () => {
test("sorts alphabetically ascending")
test("returns negative when a.name < b.name")
test("returns positive when a.name > b.name")
test("returns 0 for same name")
test("is case-sensitive")
})
describe("AGENT_SOURCE_GROUPS", () => {
test("contains expected source groups in order")
test("has unique labels")
})
```
### Mock 需求
需 mock `AgentDefinition`, `AgentSource` 类型依赖
---
## 5. `src/tools/AgentTool/__tests__/agentToolUtils.test.ts` (~20 tests)
**源文件**: `src/tools/AgentTool/agentToolUtils.ts` (688 行)
**目标函数**: `countToolUses`, `getLastToolUseName`, `extractPartialResult`
### 测试用例
```typescript
describe("countToolUses", () => {
test("counts tool_use blocks in messages")
test("returns 0 for messages without tool_use")
test("returns 0 for empty array")
test("counts multiple tool_use blocks across messages")
test("counts tool_use in single message with multiple blocks")
})
describe("getLastToolUseName", () => {
test("returns last tool name from assistant message")
test("returns undefined for message without tool_use")
test("returns the last tool when multiple tool_uses present")
test("handles message with non-array content")
})
describe("extractPartialResult", () => {
test("extracts text from last assistant message")
test("returns undefined for messages without assistant content")
test("handles interrupted agent with partial text")
test("returns undefined for empty messages")
test("concatenates multiple text blocks")
test("skips non-text content blocks")
})
```
### Mock 需求
需 mock 消息类型依赖
---
## 6. `src/tools/SkillTool/__tests__/skillSafety.test.ts` (~15 tests)
**源文件**: `src/tools/SkillTool/SkillTool.ts` (1110 行)
**目标函数**: `skillHasOnlySafeProperties`, `extractUrlScheme`
### 测试用例
```typescript
describe("skillHasOnlySafeProperties", () => {
test("returns true for command with only safe properties")
test("returns true for command with undefined extra properties")
test("returns false for command with unsafe meaningful property")
test("returns true for command with null extra properties")
test("returns true for command with empty array extra property")
test("returns true for command with empty object extra property")
test("returns false for command with non-empty unsafe array")
test("returns false for command with non-empty unsafe object")
test("returns true for empty command object")
})
describe("extractUrlScheme", () => {
test("extracts 'gs' from 'gs://bucket/path'")
test("extracts 'https' from 'https://example.com'")
test("extracts 'http' from 'http://example.com'")
test("extracts 's3' from 's3://bucket/path'")
test("defaults to 'gs' for unknown scheme")
test("defaults to 'gs' for path without scheme")
test("defaults to 'gs' for empty string")
})
```
### Mock 需求
需 mock 重依赖链,`await import()` 获取函数

View File

@@ -1,215 +0,0 @@
# Phase 19 - Batch 4: Services 纯逻辑
> 预计 ~84 tests / 5 文件 | 部分需轻量 mock
---
## 1. `src/services/compact/__tests__/grouping.test.ts` (~15 tests)
**源文件**: `src/services/compact/grouping.ts` (64 行)
**目标函数**: `groupMessagesByApiRound`
### 测试用例
```typescript
describe("groupMessagesByApiRound", () => {
test("returns single group for single API round")
test("splits at new assistant message ID")
test("keeps tool_result messages with their parent assistant message")
test("handles streaming chunks (same assistant ID stays grouped)")
test("returns empty array for empty input")
test("handles all user messages (no assistant)")
test("handles alternating assistant IDs")
test("three API rounds produce three groups")
test("user messages before first assistant go in first group")
test("consecutive user messages stay in same group")
test("does not produce empty groups")
test("handles single message")
test("preserves message order within groups")
test("handles system messages")
test("tool_result after assistant stays in same round")
})
```
### Mock 需求
需构造 `Message` mock 对象type: 'user'/'assistant', message: { id, content }
---
## 2. `src/services/compact/__tests__/stripMessages.test.ts` (~20 tests)
**源文件**: `src/services/compact/compact.ts` (1709 行)
**目标函数**: `stripImagesFromMessages`, `collectReadToolFilePaths` (私有)
### 测试用例
```typescript
describe("stripImagesFromMessages", () => {
// user 消息处理
test("replaces image block with [image] text")
test("replaces document block with [document] text")
test("preserves text blocks unchanged")
test("handles multiple image/document blocks in single message")
test("returns original message when no media blocks")
// tool_result 内嵌套
test("replaces image inside tool_result content")
test("replaces document inside tool_result content")
test("preserves non-media tool_result content")
// 非用户消息
test("passes through assistant messages unchanged")
test("passes through system messages unchanged")
// 边界
test("handles empty message array")
test("handles string content (non-array) in user message")
test("does not mutate original messages")
})
describe("collectReadToolFilePaths", () => {
// 注意:这是私有函数,可能需要通过 stripImagesFromMessages 或其他导出间接测试
// 如果不可直接测试,则跳过或通过集成测试覆盖
test("collects file_path from Read tool_use blocks")
test("skips tool_use with FILE_UNCHANGED_STUB result")
test("returns empty set for messages without Read tool_use")
test("handles multiple Read calls across messages")
test("normalizes paths via expandPath")
})
```
### Mock 需求
需 mock `expandPath`(如果 collectReadToolFilePaths 要测)
需 mock `log`, `slowOperations` 等重依赖
构造 `Message` mock 对象
---
## 3. `src/services/compact/__tests__/prompt.test.ts` (~12 tests)
**源文件**: `src/services/compact/prompt.ts` (375 行)
**目标函数**: `formatCompactSummary`
### 测试用例
```typescript
describe("formatCompactSummary", () => {
test("strips <analysis>...</analysis> block")
test("replaces <summary>...</summary> with 'Summary:\\n' prefix")
test("handles analysis + summary together")
test("handles summary without analysis")
test("handles analysis without summary")
test("collapses multiple newlines to double")
test("trims leading/trailing whitespace")
test("handles empty string")
test("handles plain text without tags")
test("handles multiline analysis content")
test("preserves content between analysis and summary")
test("handles nested-like tags gracefully")
})
```
### Mock 需求
需 mock 重依赖链(`log`, feature flags 等)
`formatCompactSummary` 是纯字符串处理,如果 import 链不太重则无需复杂 mock
---
## 4. `src/services/mcp/__tests__/channelPermissions.test.ts` (~25 tests)
**源文件**: `src/services/mcp/channelPermissions.ts` (241 行)
**目标函数**: `hashToId`, `shortRequestId`, `truncateForPreview`, `filterPermissionRelayClients`
### 测试用例
```typescript
describe("hashToId", () => {
test("returns 5-char string")
test("uses only letters a-z excluding 'l'")
test("is deterministic (same input = same output)")
test("different inputs produce different outputs (with high probability)")
test("handles empty string")
})
describe("shortRequestId", () => {
test("returns 5-char string from tool use ID")
test("is deterministic")
test("avoids profanity substrings (retries with salt)")
test("returns a valid ID even if all retries hit bad words (unlikely)")
})
describe("truncateForPreview", () => {
test("returns JSON string for object input")
test("truncates to <=200 chars when input is long")
test("adds ellipsis or truncation indicator")
test("returns short input unchanged")
test("handles string input")
test("handles null/undefined input")
})
describe("filterPermissionRelayClients", () => {
test("keeps connected clients in allowlist with correct capabilities")
test("filters out disconnected clients")
test("filters out clients not in allowlist")
test("filters out clients missing required capabilities")
test("returns empty array for empty input")
test("type predicate narrows correctly")
})
describe("PERMISSION_REPLY_RE", () => {
test("matches 'y abcde'")
test("matches 'yes abcde'")
test("matches 'n abcde'")
test("matches 'no abcde'")
test("is case-insensitive")
test("does not match without ID")
})
```
### Mock 需求
`hashToId` 可能需要确认导出状态
`filterPermissionRelayClients` 需要 mock 客户端类型
`truncateForPreview` 可能依赖 `jsonStringify`(需 mock `slowOperations`
---
## 5. `src/services/mcp/__tests__/officialRegistry.test.ts` (~12 tests)
**源文件**: `src/services/mcp/officialRegistry.ts` (73 行)
**目标函数**: `normalizeUrl` (私有), `isOfficialMcpUrl`, `resetOfficialMcpUrlsForTesting`
### 测试用例
```typescript
describe("normalizeUrl", () => {
// 注意:如果是私有的,通过 isOfficialMcpUrl 间接测试
test("removes trailing slash")
test("removes query parameters")
test("preserves path")
test("handles URL with port")
test("handles URL with hash fragment")
})
describe("isOfficialMcpUrl", () => {
test("returns false when registry not loaded (initial state)")
test("returns true for URL added to registry")
test("returns false for non-registered URL")
test("uses normalized URL for comparison")
})
describe("resetOfficialMcpUrlsForTesting", () => {
test("clears the cached URLs")
test("allows fresh start after reset")
})
describe("URL normalization + lookup integration", () => {
test("URL with trailing slash matches normalized version")
test("URL with query params matches normalized version")
test("different URLs do not match")
test("case sensitivity check")
})
```
### Mock 需求
需 mock `axios`(避免网络请求)
使用 `resetOfficialMcpUrlsForTesting` 做测试隔离

View File

@@ -1,200 +0,0 @@
# Phase 19 - Batch 5: MCP 配置 + modelCost
> 预计 ~80 tests / 4 文件 | 需中等 mock
---
## 1. `src/services/mcp/__tests__/configUtils.test.ts` (~30 tests)
**源文件**: `src/services/mcp/config.ts` (1580 行)
**目标函数**: `unwrapCcrProxyUrl`, `urlPatternToRegex` (私有), `commandArraysMatch` (私有), `toggleMembership` (私有), `addScopeToServers` (私有), `dedupPluginMcpServers`, `getMcpServerSignature` (如导出)
### 测试策略
私有函数如不可直接测试,通过公开的 `dedupPluginMcpServers` 间接覆盖。导出函数直接测。
### 测试用例
```typescript
describe("unwrapCcrProxyUrl", () => {
test("returns original URL when no CCR proxy markers")
test("extracts mcp_url from CCR proxy URL with /v2/session_ingress/shttp/mcp/")
test("extracts mcp_url from CCR proxy URL with /v2/ccr-sessions/")
test("returns original URL when mcp_url param is missing")
test("handles malformed URL gracefully")
test("handles URL with both proxy marker and mcp_url")
test("preserves non-CCR URLs unchanged")
})
describe("dedupPluginMcpServers", () => {
test("keeps unique plugin servers")
test("suppresses plugin server duplicated by manual config")
test("suppresses plugin server duplicated by earlier plugin")
test("keeps servers with null signature")
test("returns empty for empty inputs")
test("reports suppressed with correct duplicateOf name")
test("handles multiple plugins with same config")
})
describe("toggleMembership (via integration)", () => {
test("adds item when shouldContain=true and not present")
test("removes item when shouldContain=false and present")
test("returns same array when already in desired state")
})
describe("addScopeToServers (via integration)", () => {
test("adds scope to each server config")
test("returns empty object for undefined input")
test("returns empty object for empty input")
test("preserves all original config properties")
})
describe("urlPatternToRegex (via integration)", () => {
test("matches exact URL")
test("matches wildcard pattern *.example.com")
test("matches multiple wildcards")
test("does not match non-matching URL")
test("escapes regex special characters in pattern")
})
describe("commandArraysMatch (via integration)", () => {
test("returns true for identical arrays")
test("returns false for different lengths")
test("returns false for same length different elements")
test("returns true for empty arrays")
})
```
### Mock 需求
需 mock `feature()` (bun:bundle), `jsonStringify`, `safeParseJSON`, `log`
通过 `mock.module()` + `await import()` 解锁
---
## 2. `src/services/mcp/__tests__/filterUtils.test.ts` (~20 tests)
**源文件**: `src/services/mcp/utils.ts` (576 行)
**目标函数**: `filterToolsByServer`, `hashMcpConfig`, `isToolFromMcpServer`, `isMcpTool`, `parseHeaders`
### 测试用例
```typescript
describe("filterToolsByServer", () => {
test("filters tools matching server name prefix")
test("returns empty for no matching tools")
test("handles empty tools array")
test("normalizes server name for matching")
})
describe("hashMcpConfig", () => {
test("returns 16-char hex string")
test("is deterministic")
test("excludes scope from hash")
test("different configs produce different hashes")
test("key order does not affect hash (sorted)")
})
describe("isToolFromMcpServer", () => {
test("returns true when tool belongs to specified server")
test("returns false for different server")
test("returns false for non-MCP tool name")
test("handles empty tool name")
})
describe("isMcpTool", () => {
test("returns true for tool name starting with 'mcp__'")
test("returns true when tool.isMcp is true")
test("returns false for regular tool")
test("returns false when neither condition met")
})
describe("parseHeaders", () => {
test("parses 'Key: Value' format")
test("parses multiple headers")
test("trims whitespace around key and value")
test("throws on missing colon")
test("throws on empty key")
test("handles value with colons (like URLs)")
test("returns empty object for empty array")
test("handles duplicate keys (last wins)")
})
```
### Mock 需求
需 mock `normalizeNameForMCP`, `mcpInfoFromString`, `jsonStringify`, `createHash`
`parseHeaders` 是最独立的,可能不需要太多 mock
---
## 3. `src/services/mcp/__tests__/channelNotification.test.ts` (~15 tests)
**源文件**: `src/services/mcp/channelNotification.ts` (317 行)
**目标函数**: `wrapChannelMessage`, `findChannelEntry`
### 测试用例
```typescript
describe("wrapChannelMessage", () => {
test("wraps content in <channel> tag with source attribute")
test("escapes server name in attribute")
test("includes meta attributes when provided")
test("escapes meta values via escapeXmlAttr")
test("filters out meta keys not matching SAFE_META_KEY pattern")
test("handles empty meta")
test("handles content with special characters")
test("formats with newlines between tags and content")
})
describe("findChannelEntry", () => {
test("finds server entry by exact name match")
test("finds plugin entry by matching second segment")
test("returns undefined for no match")
test("handles empty channels array")
test("handles server name without colon")
test("handles 'plugin:name' format correctly")
test("prefers exact match over partial match")
})
```
### Mock 需求
需 mock `escapeXmlAttr`(来自 xml.ts已有测试或直接使用
`CHANNEL_TAG` 常量需确认导出
---
## 4. `src/utils/__tests__/modelCost.test.ts` (~15 tests)
**源文件**: `src/utils/modelCost.ts` (232 行)
**目标函数**: `formatModelPricing`, `COST_TIER_*` 常量
### 测试用例
```typescript
describe("COST_TIER constants", () => {
test("COST_TIER_3_15 has inputTokens=3, outputTokens=15")
test("COST_TIER_15_75 has inputTokens=15, outputTokens=75")
test("COST_TIER_5_25 has inputTokens=5, outputTokens=25")
test("COST_TIER_30_150 has inputTokens=30, outputTokens=150")
test("COST_HAIKU_35 has inputTokens=0.8, outputTokens=4")
test("COST_HAIKU_45 has inputTokens=1, outputTokens=5")
})
describe("formatModelPricing", () => {
test("formats integer prices without decimals: '$3/$15 per Mtok'")
test("formats float prices with 2 decimals: '$0.80/$4.00 per Mtok'")
test("formats mixed: '$5/$25 per Mtok'")
test("formats large prices: '$30/$150 per Mtok'")
test("formats $1/$5 correctly (integer but small)")
test("handles zero prices: '$0/$0 per Mtok'")
})
describe("MODEL_COSTS", () => {
test("maps known model names to cost tiers")
test("contains entries for claude-sonnet-4-6")
test("contains entries for claude-opus-4-6")
test("contains entries for claude-haiku-4-5")
})
```
### Mock 需求
需 mock `log`, `slowOperations` 等重依赖modelCost.ts 通常 import 链较重)
`formatModelPricing``COST_TIER_*` 是纯数据/纯函数mock 成功后直接测

View File

@@ -1,296 +0,0 @@
# Testing Specification
本文档定义 claude-code 项目的测试规范、当前覆盖状态和改进计划。
## 1. 技术栈
| 项 | 选型 |
|----|------|
| 测试框架 | `bun:test` |
| 断言/Mock | `bun:test` 内置 |
| 覆盖率 | `bun test --coverage` |
| CI | GitHub Actionspush/PR 到 main 自动运行 |
## 2. 测试层次
本项目采用 **单元测试 + 集成测试** 两层结构,不做 E2E 或快照测试。
- **单元测试** — 纯函数、工具类、解析器。文件就近放置于 `src/**/__tests__/`
- **集成测试** — 多模块协作流程。集中于 `tests/integration/`
## 3. 文件结构与命名
```
src/
├── utils/__tests__/ # 纯函数单元测试
├── tools/<Tool>/__tests__/ # Tool 单元测试
├── services/mcp/__tests__/ # MCP 单元测试
├── utils/permissions/__tests__/
├── utils/model/__tests__/
├── utils/settings/__tests__/
├── utils/shell/__tests__/
├── utils/git/__tests__/
└── __tests__/ # 顶层模块测试 (Tool.ts, tools.ts)
tests/
├── integration/ # 集成测试(尚未创建)
├── mocks/ # 共享 mock/fixture尚未创建
└── helpers/ # 测试辅助函数
```
- 测试文件:`<module>.test.ts`
- 命名风格:`describe("functionName")` + `test("行为描述")`,英文
- 编写原则Arrange-Act-Assert、单一职责、独立性、边界覆盖
## 4. 当前覆盖状态
> 更新日期2026-04-02 | **1623 tests, 84 files, 0 fail, 851ms**
### 4.1 可靠度评分
每个测试文件按断言深度、边界覆盖、mock 质量、测试独立性综合评定:
| 等级 | 含义 |
|------|------|
| **GOOD** | 断言精确exact match边界充分结构清晰 |
| **ACCEPTABLE** | 正常路径覆盖完整,部分边界或断言可加强 |
| **WEAK** | 存在明显缺陷:断言过弱、重要边界缺失、或有脆弱性风险 |
### 4.2 按模块分布
#### P0 — 核心模块
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|------|-------|------|----------|----------|
| `src/__tests__/Tool.test.ts` | 20 | GOOD | buildTool, toolMatchesName, findToolByName, filterToolProgressMessages | — |
| `src/__tests__/tools.test.ts` | 9 | ACCEPTABLE | parseToolPreset, filterToolsByDenyRules | 预设覆盖仅测 "default";有冗余用例 |
| `src/tools/FileEditTool/__tests__/utils.test.ts` | 22 | ACCEPTABLE | normalizeQuotes, applyEditToFile, preserveQuoteStyle | `findActualString` 断言过弱(`not.toBeNull``preserveQuoteStyle` 仅 2 用例 |
| `src/tools/shared/__tests__/gitOperationTracking.test.ts` | 20 | ACCEPTABLE | parseGitCommitId, detectGitOperation | 6 个 GH PR action 全覆盖;缺 `trackGitOperations` 测试(需 mock analytics |
| `src/tools/BashTool/__tests__/destructiveCommandWarning.test.ts` | 21 | ACCEPTABLE | git/rm/SQL/k8s/terraform 危险模式 | safe commands 4 断言合一;缺少 `rm -rf /``DROP DATABASE`、管道命令 |
| `src/tools/BashTool/__tests__/commandSemantics.test.ts` | 10 | ACCEPTABLE | grep/diff/test/rg/find 退出码语义 | mock `splitCommand_DEPRECATED` 与实现可能分歧;覆盖可更全面 |
**Utils 纯函数19 文件):**
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|------|-------|------|----------|----------|
| `utils/__tests__/array.test.ts` | 12 | GOOD | intersperse, count, uniq | — |
| `utils/__tests__/set.test.ts` | 11 | GOOD | difference, intersects, every, union | — |
| `utils/__tests__/xml.test.ts` | 9 | GOOD | escapeXml, escapeXmlAttr | 缺 null/undefined 输入测试 |
| `utils/__tests__/hash.test.ts` | 12 | ACCEPTABLE | djb2Hash, hashContent, hashPair | `hashContent`/`hashPair` 无已知答案断言(仅测确定性) |
| `utils/__tests__/stringUtils.test.ts` | 30 | GOOD | 10 个函数全覆盖,含 Unicode 边界 | — |
| `utils/__tests__/semver.test.ts` | 16 | ACCEPTABLE | gt/gte/lt/lte/satisfies/order | 缺 pre-release、tilde range、畸形版本串 |
| `utils/__tests__/uuid.test.ts` | 6 | ACCEPTABLE | validateUuid | 大写测试仅 `not.toBeNull`,未验证标准化输出 |
| `utils/__tests__/format.test.ts` | 27 | GOOD | formatFileSize, formatDuration, formatNumber, formatTokens, formatRelativeTime | 全部 `toBe` 精确匹配,含 billions/weeks/days 边界 |
| `utils/__tests__/frontmatterParser.test.ts` | 22 | GOOD | parseFrontmatter, splitPathInFrontmatter, parsePositiveIntFromFrontmatter | — |
| `utils/__tests__/file.test.ts` | 13 | ACCEPTABLE | convertLeadingTabsToSpaces, addLineNumbers, stripLineNumberPrefix | `addLineNumbers``toContain`;缺 Windows 路径分隔符测试 |
| `utils/__tests__/glob.test.ts` | 6 | ACCEPTABLE | extractGlobBaseDirectory | 缺绝对路径、根 `/`、Windows 路径 |
| `utils/__tests__/diff.test.ts` | 8 | ACCEPTABLE | adjustHunkLineNumbers, getPatchFromContents | `getPatchFromContents` 仅检查结构,未验证 diff 内容正确性 |
| `utils/__tests__/json.test.ts` | 15 | GOOD | safeParseJSON, parseJSONL, addItemToJSONCArray | — |
| `utils/__tests__/truncate.test.ts` | 18 | ACCEPTABLE | truncateToWidth, wrapText, truncatePathMiddle | **缺 CJK/emoji/wide-char 测试**(这是宽度感知实现的核心场景) |
| `utils/__tests__/path.test.ts` | 15 | ACCEPTABLE | containsPathTraversal, normalizePathForConfigKey | 仅覆盖 2/5+ 导出函数 |
| `utils/__tests__/tokens.test.ts` | 18 | GOOD | getTokenCountFromUsage, doesMostRecentAssistantMessageExceed200k 等 | — |
| `utils/__tests__/stream.test.ts` | 15 | GOOD | Stream\<T\> enqueue/read/drain/next/done/error/for-await | — |
| `utils/__tests__/abortController.test.ts` | 13 | GOOD | createAbortController/createChildAbortController 父子传播 | — |
| `utils/__tests__/bufferedWriter.test.ts` | 10 | GOOD | createBufferedWriter 立即/缓冲/flush/overflow | — |
| `utils/__tests__/gitDiff.test.ts` | 25 | GOOD | parseGitNumstat/parseGitDiff/parseShortstat 纯解析 | — |
| `utils/__tests__/sliceAnsi.test.ts` | 13 | GOOD | sliceAnsi ANSI 感知切片 + undoAnsiCodes | — |
| `utils/__tests__/treeify.test.ts` | 13 | ACCEPTABLE | treeify 扁平/嵌套/循环引用 | 缺深度嵌套性能测试 |
| `utils/__tests__/words.test.ts` | 11 | GOOD | slug 格式 (adjective-verb-noun)、唯一性 | — |
**Context 构建3 文件):**
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|------|-------|------|----------|----------|
| `utils/__tests__/claudemd.test.ts` | 14 | ACCEPTABLE | stripHtmlComments, isMemoryFilePath, getLargeMemoryFiles | **仅测 3 个辅助函数**,核心发现/加载/`@include` 指令/memoization 未覆盖 |
| `utils/__tests__/systemPrompt.test.ts` | 8 | GOOD | buildEffectiveSystemPrompt | — |
| `__tests__/history.test.ts` | 26 | GOOD | parseReferences/expandPastedTextRefs/formatPastedTextRef 等 5 个函数 | — |
#### P1 — 重要模块
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|------|-------|------|----------|----------|
| `permissions/__tests__/permissionRuleParser.test.ts` | 16 | GOOD | escape/unescape 规则roundtrip 完整性 | — |
| `permissions/__tests__/permissions.test.ts` | 12 | ACCEPTABLE | getDenyRuleForTool, getAskRuleForTool, filterDeniedAgents | `as any` cast缺 MCP tool deny 测试 |
| `permissions/__tests__/shellRuleMatching.test.ts` | 19 | GOOD | 通配符、转义、正则特殊字符 | — |
| `permissions/__tests__/PermissionMode.test.ts` | 22 | ACCEPTABLE | permissionModeFromString, isExternalPermissionMode 等 | isExternalPermissionMode ant false 路径已覆盖;缺 `bubble` 模式独立测试 |
| `permissions/__tests__/dangerousPatterns.test.ts` | 7 | WEAK | CROSS_PLATFORM_CODE_EXEC, DANGEROUS_BASH_PATTERNS | 纯数据 smoke test无行为测试不验证数组无重复 |
| `model/__tests__/aliases.test.ts` | 15 | ACCEPTABLE | isModelAlias, isModelFamilyAlias | 缺 null/undefined/空串输入 |
| `model/__tests__/model.test.ts` | 13 | ACCEPTABLE | firstPartyNameToCanonical | 缺空串、非标准日期后缀 |
| `model/__tests__/providers.test.ts` | 9 | ACCEPTABLE | getAPIProvider, isFirstPartyAnthropicBaseUrl | `originalEnv` 声明未使用env 恢复不完整 |
| `utils/__tests__/messages.test.ts` | 36 | GOOD | createAssistantMessage, createUserMessage, extractTag 等 16 个 describe | `normalizeMessages` 仅检查长度未验证内容 |
**Tool 子模块8 文件):**
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|------|-------|------|----------|----------|
| `tools/PowerShellTool/__tests__/powershellSecurity.test.ts` | 24 | GOOD | AST 安全检测Invoke-Expression/iex/encoded/dynamic/download/COM | — |
| `tools/PowerShellTool/__tests__/commandSemantics.test.ts` | 21 | GOOD | grep/rg/findstr/robocopy 退出码、pipeline last-segment | — |
| `tools/PowerShellTool/__tests__/destructiveCommandWarning.test.ts` | 38 | GOOD | Remove-Item/Format-Volume/Clear-Disk/git/SQL/COMPUTER/alias 全覆盖 | — |
| `tools/PowerShellTool/__tests__/gitSafety.test.ts` | 29 | GOOD | .git 路径检测/NTFS 短名/反斜杠/引号/反引号转义 | — |
| `tools/LSPTool/__tests__/formatters.test.ts` | 18 | GOOD | 全部 8 个 format 函数 null/empty/valid 输入 | — |
| `tools/LSPTool/__tests__/schemas.test.ts` | 13 | GOOD | isValidLSPOperation 类型守卫 9 种操作 + 无效/空/大小写 | — |
| `tools/WebFetchTool/__tests__/preapproved.test.ts` | 18 | GOOD | isPreapprovedHost 精确/路径作用域/子路径/大小写/子域名 | — |
| `tools/WebFetchTool/__tests__/urlValidation.test.ts` | 18 | GOOD | validateURL/isPermittedRedirect 本地重实现(避免重依赖链) | — |
#### P2 — 补充模块
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|------|-------|------|----------|----------|
| `utils/__tests__/cron.test.ts` | 31 | GOOD | parseCronExpression, computeNextCronRun, cronToHuman | 缺月边界、闰年 |
| `utils/__tests__/git.test.ts` | 15 | ACCEPTABLE | normalizeGitRemoteUrl (SSH/HTTPS/ssh://) | 缺 git://、file://、端口号 |
| `settings/__tests__/config.test.ts` | 38 | GOOD | SettingsSchema, type guards, validateSettingsFileContent, formatZodError | 缺 DeniedMcpServerEntrySchema |
#### P3-P6 — 扩展覆盖27 文件)
| 文件 | Tests | 评分 | 备注 |
|------|-------|------|------|
| `utils/__tests__/errors.test.ts` | 33 | GOOD | — |
| `utils/__tests__/envUtils.test.ts` | 33 | GOOD | env 保存/恢复规范 |
| `utils/__tests__/effort.test.ts` | 30 | GOOD | 5 个 mock 模块,边界完整 |
| `utils/__tests__/argumentSubstitution.test.ts` | 22 | ACCEPTABLE | 缺转义引号、越界索引 |
| `utils/__tests__/sanitization.test.ts` | 14 | ACCEPTABLE | — |
| `utils/__tests__/sleep.test.ts` | 14 | GOOD | 时间相关测试margin 充足 |
| `utils/__tests__/CircularBuffer.test.ts` | 11 | ACCEPTABLE | 缺 capacity=1、空 buffer getRecent |
| `utils/__tests__/memoize.test.ts` | 18 | GOOD | 缓存 hit/stale/LRU 全覆盖 |
| `utils/__tests__/tokenBudget.test.ts` | 21 | GOOD | — |
| `utils/__tests__/displayTags.test.ts` | 17 | GOOD | — |
| `utils/__tests__/taggedId.test.ts` | 10 | GOOD | — |
| `utils/__tests__/controlMessageCompat.test.ts` | 15 | GOOD | — |
| `utils/__tests__/gitConfigParser.test.ts` | 21 | GOOD | — |
| `utils/__tests__/windowsPaths.test.ts` | 19 | GOOD | 双向 round-trip 测试 |
| `utils/__tests__/envExpansion.test.ts` | 15 | GOOD | — |
| `utils/__tests__/formatBriefTimestamp.test.ts` | 10 | GOOD | 固定 now 时间戳,确定性 |
| `utils/__tests__/notebook.test.ts` | 9 | ACCEPTABLE | 合并断言偏弱 |
| `utils/__tests__/hyperlink.test.ts` | 10 | ACCEPTABLE | 空串测试行为注释混乱 |
| `utils/__tests__/zodToJsonSchema.test.ts` | 9 | WEAK | **object 属性仅 `toBeDefined` 未验证类型**optional 字段未验证 absence |
| `utils/__tests__/objectGroupBy.test.ts` | 5 | ACCEPTABLE | 极简,缺 undefined key 测试 |
| `utils/__tests__/contentArray.test.ts` | 6 | ACCEPTABLE | 缺混合 tool_result+text 交替 |
| `utils/__tests__/slashCommandParsing.test.ts` | 8 | GOOD | — |
| `utils/__tests__/groupToolUses.test.ts` | 10 | GOOD | — |
| `utils/__tests__/shell/__tests__/outputLimits.test.ts` | 7 | ACCEPTABLE | — |
| `utils/__tests__/envValidation.test.ts` | 12 | GOOD | validateBoundedIntEnvVar | value=1 无下界确认为设计意图(函数仅校验 >0 和 <=upperLimit |
| `utils/git/__tests__/gitConfigParser.test.ts` | 20 | GOOD | — |
| `services/mcp/__tests__/mcpStringUtils.test.ts` | 16 | GOOD | — |
| `services/mcp/__tests__/normalization.test.ts` | 10 | GOOD | — |
### 4.3 评分汇总
| 等级 | 文件数 | 占比 |
|------|--------|------|
| **GOOD** | 46 | 55% |
| **ACCEPTABLE** | 32 | 38% |
| **WEAK** | 6 | 7% |
## 5. 系统性问题
### 5.1 断言过弱Smell: `toContain` 代替精确匹配)
以下文件的部分测试使用 `toContain``not.toBeNull` 检查结果,当实现返回包含目标子串的任何字符串时测试仍通过,无法检测格式错误:
| 文件 | 受影响函数 | 建议 |
|------|-----------|------|
| `file.test.ts` | addLineNumbers | 断言完整输出格式 |
| `diff.test.ts` | getPatchFromContents | 验证 hunk 内容正确性 |
| `notebook.test.ts` | mapNotebookCellsToToolResult | 验证合并后内容 |
| `uuid.test.ts` | validateUuid (uppercase) | 断言标准化后的精确值 |
### 5.2 集成测试空白
Spec 定义的三个集成测试均未创建:
| 计划 | 状态 | 依赖 |
|------|------|------|
| `tests/integration/tool-chain.test.ts` | 未创建 | 需 mock tools.ts 完整注册链 |
| `tests/integration/context-build.test.ts` | 未创建 | 需 mock context.ts 重依赖链 |
| `tests/integration/message-pipeline.test.ts` | 未创建 | 需 mock API 层 |
`tests/mocks/` 目录也不存在,无共享 mock/fixture 基础设施。
### 5.3 Mock 相关
| 问题 | 影响文件 | 说明 |
|------|----------|------|
| 未 mock 重依赖 | `gitOperationTracking.test.ts` | `trackGitOperations` 调用 analytics/bootstrap测试仅覆盖 `detectGitOperation`(无副作用) |
| env 恢复不完整 | `providers.test.ts` | 仅删除已知 key新增 env var 会导致测试泄漏 |
### 5.4 潜在 Bug
| 文件 | 函数 | 问题 |
|------|------|------|
| ~~`envValidation.test.ts`~~ | ~~validateBoundedIntEnvVar~~ | ~~value=1 无下界检查~~**已确认**:函数仅校验 `parsed > 0``parsed <= upperLimit`,不强制 `parsed >= defaultValue`,为设计意图 |
### 5.5 已知限制
| 模块 | 问题 |
|------|------|
| `Bun.JSONL.parseChunk` | 畸形行时无限挂起Bun 1.3.10 bug |
| `context.ts` 核心逻辑 | 依赖 bootstrap/state + git + 50+ 模块mock 不可行 |
| `tools.ts` (getAllBaseTools) | 导入链过重 |
| `spawnMultiAgent.ts` | 50+ 依赖 |
| `messages.ts` 部分函数 | 依赖 `getFeatureValue_CACHED_MAY_BE_STALE` |
| UI 组件 (`screens/`, `components/`) | 需 Ink 渲染测试环境 |
### 5.6 Mock 模式
通过 `mock.module()` + `await import()` 解锁重依赖模块:
| 被 Mock 模块 | 解锁的测试 |
|-------------|-----------|
| `src/utils/log.ts` | json, tokens, FileEditTool/utils, permissions, memoize, PermissionMode |
| `src/services/tokenEstimation.ts` | tokens |
| `src/utils/slowOperations.ts` | tokens, permissions, memoize, PermissionMode |
| `src/utils/debug.ts` | envValidation, outputLimits |
| `src/utils/bash/commands.ts` | commandSemantics |
| `src/utils/thinking.js` | effort |
| `src/utils/settings/settings.js` | effort |
| `src/utils/auth.js` | effort |
| `src/services/analytics/growthbook.js` | effort, tokenBudget |
| `src/utils/powershell/dangerousCmdlets.js` | powershellSecurity |
| `src/utils/cwd.js` | gitSafety |
| `src/utils/powershell/parser.js` | gitSafety |
| `src/utils/stringUtils.js` | LSP formatters |
| `figures` | treeify |
**约束**`mock.module()` 必须在每个测试文件内联调用,不能从共享 helper 导入。
## 6. 完成状态
> 更新日期2026-04-02 | **1623 tests, 84 files, 0 fail, 851ms**
### 已完成
| 计划 | 状态 | 新增测试 | 说明 |
|------|------|---------|------|
| Plan 12 — Mock 可靠性 | **已完成** | +9 | PermissionMode ant false 路径、providers env 快照恢复 |
| Plan 10 — WEAK 修复 | **已完成** | +15 | format 断言精确化、envValidation 修正、zodToJsonSchema/destructors/notebook 加固 |
| Plan 13 — CJK/Emoji | **已完成** | +17 | truncate CJK/emoji 宽度感知测试 |
| Plan 11 — ACCEPTABLE 加强 | **已完成** | +62 | diff/uuid/hash/semver/path/claudemd/fileEdit/providers/messages 等 15 文件 |
| Plan 14 — 集成测试 | **已完成** | +43 | 搭建 tests/mocks/ + tool-chain/context-build/message-pipeline/cli-arguments |
| Plan 15 — CLI + 覆盖率 | **已完成** | +11 | Commander.js 参数解析、覆盖率基线 |
| Phase 16 — 零依赖纯函数 | **已完成** | +126 | stream/abortController/bufferedWriter/gitDiff/history/sliceAnsi/treeify/words 8 文件 |
| Phase 17 — 工具子模块 | **已完成** | +179 | PowerShell 安全/语义/破坏性/gitSafety + LSP 格式化/schema + WebFetch 预批准/URL 8 文件 |
| Phase 18 — WEAK 修复 | **已完成** | +20 | format 精确匹配、envValidation 边界、PermissionMode 补强、gitOperationTracking PR actions |
### 覆盖率基线
| 指标 | 数值 |
|------|------|
| 总测试数 | 1623 |
| 测试文件数 | 84 |
| 失败数 | 0 |
| 断言数 | 2516 |
| 运行耗时 | ~851ms |
| Tool.ts 行覆盖率 | 100% |
| 整体行覆盖率 | ~33%Bun coverage 限制:`mock.module` 模式下的模块不报告) |
> **注意**Bun `--coverage` 仅报告测试 import 链中直接加载的文件。使用 `mock.module()` + `await import()` 模式的源文件(大多数 `src/utils/` 纯函数)不显示在覆盖率报告中。实际测试覆盖率高于报告值。
### 不纳入计划
| 模块 | 原因 |
|------|------|
| `query.ts` / `QueryEngine.ts` | 核心循环,需完整集成环境 |
| `services/api/claude.ts` | 需 mock SDK 流式响应 |
| `spawnMultiAgent.ts` | 50+ 依赖 |
| `modelCost.ts` | 依赖 bootstrap/state + analytics |
| `mcp/dateTimeParser.ts` | 调用 Haiku API |
| `screens/` / `components/` | 需 Ink 渲染测试 |

View File

@@ -146,14 +146,15 @@ AI 的信息获取不局限于本地代码:
### WebSearch 实现机制
WebSearch 通过适配器模式支持种搜索后端,由 `src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
WebSearch 通过适配器模式支持种搜索后端,由 `src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
```
适配器架构:
WebSearchTool.call()
→ createAdapter() 选择后端
├─ ApiSearchAdapter — Anthropic API 服务端搜索(需官方 API 密钥)
─ BingSearchAdapter — 直接抓取 Bing 搜索页面解析(无需 API 密钥)
─ BingSearchAdapter — 直接抓取 Bing 搜索页面解析(无需 API 密钥)
└─ BraveSearchAdapter — 调用 Brave LLM Context API 解析(需 Brave API 密钥)
→ adapter.search(query, options)
→ 转换为统一 SearchResult[] 格式返回
```
@@ -166,8 +167,9 @@ WebSearch 通过适配器模式支持两种搜索后端,由 `src/tools/WebSear
|--------|------|--------|
| 1 | 环境变量 `WEB_SEARCH_ADAPTER=api` | `ApiSearchAdapter` |
| 2 | 环境变量 `WEB_SEARCH_ADAPTER=bing` | `BingSearchAdapter` |
| 3 | API Base URL 指向 Anthropic 官方 | `ApiSearchAdapter` |
| 4 | 第三方代理 / 非官方端点 | `BingSearchAdapter` |
| 3 | 环境变量 `WEB_SEARCH_ADAPTER=brave` | `BraveSearchAdapter` |
| 4 | API Base URL 指向 Anthropic 官方 | `ApiSearchAdapter` |
| 5 | 第三方代理 / 非官方端点 | `BingSearchAdapter` |
适配器是无状态的,同一会话内缓存复用。

View File

@@ -1,444 +0,0 @@
# ULTRAPLAN增强规划实现分析
> 生成日期2026-04-02
> Feature Flag`FEATURE_ULTRAPLAN=1`
> 引用数10跨 8 个文件)
---
## 一、功能概述
ULTRAPLAN 是一个**远程增强规划**功能,将用户的规划请求发送到 Claude Code on the WebCCR云端容器执行。使用 Opus 模型在云端生成高级计划,用户可以在浏览器中编辑和审批,然后选择在云端继续执行或将计划"传送"回本地终端执行。
**核心卖点**
- 终端不被阻塞 — 远程在云端规划,本地可继续工作
- 使用最强大的模型Opus
- 用户可在浏览器中实时查看和编辑计划
- 支持多轮迭代(云端可追问,用户在浏览器回复)
---
## 二、架构总览
```
用户输入 "ultraplan xxx"
┌─────────────────────────────────┐
│ 关键字检测层 (keyword.ts) │ 识别 "ultraplan" 关键字
│ + 输入处理层 (processUserInput) │ 重写为 /ultraplan 命令
└───────────┬─────────────────────┘
┌─────────────────────────────────┐
│ 命令处理层 (ultraplan.tsx) │ launchUltraplan()
│ - 前置校验(资格、防重入) │ → launchDetached()
│ - 构建提示词 │ buildUltraplanPrompt()
└───────────┬─────────────────────┘
┌─────────────────────────────────┐
│ 远程会话层 │ teleportToRemote()
│ - 创建 CCR 云端会话 │ permissionMode: 'plan'
│ - 设置 plan 权限模式 │ model: Opus
└───────────┬─────────────────────┘
┌─────────────────────────────────┐
│ 轮询层 (ccrSession.ts) │ pollForApprovedExitPlanMode()
│ - ExitPlanModeScanner │ 每 3 秒轮询事件流
│ - 状态机: running → needs_input │ 超时: 30 分钟
│ → plan_ready │
└───────────┬─────────────────────┘
┌─────┴─────┐
▼ ▼
approved teleport
(云端执行) (传送回本地)
│ │
│ ▼
│ UltraplanChoiceDialog
│ 用户选择执行方式
▼ ▼
完成通知 本地执行计划
```
---
## 三、模块详解
### 3.1 关键字检测 — `src/utils/ultraplan/keyword.ts`
负责检测用户输入中的 "ultraplan" 关键字。检测逻辑相当精细,避免误触发:
**触发条件**:输入中包含独立的 `ultraplan` 单词(大小写不敏感)。
**不触发的场景**
- 在引号/括号内:`` `ultraplan` ``、`"ultraplan"`、`[ultraplan]`、`{ultraplan}`
- 路径/标识符上下文:`src/ultraplan/foo.ts``ultraplan.tsx``--ultraplan-mode`
- 问句:`ultraplan?`
- 斜杠命令内:`/rename ultraplan foo`
- 已有 ultraplan 会话运行中或正在启动时
**关键字替换**:触发后将 `ultraplan` 替换为 `plan`,保持语法通顺(如 "please ultraplan this" → "please plan this")。
```typescript
// 核心导出函数
findUltraplanTriggerPositions(text) // 返回触发位置数组
hasUltraplanKeyword(text) // 布尔判断
replaceUltraplanKeyword(text) // 替换第一个触发词为 "plan"
```
### 3.2 命令注册 — `src/commands.ts`
```typescript
const ultraplan = feature('ULTRAPLAN')
? require('./commands/ultraplan.js').default
: null
```
命令仅在 `FEATURE_ULTRAPLAN=1` 时加载。命令定义:
```typescript
{
type: 'local-jsx',
name: 'ultraplan',
description: '~1030 min · Claude Code on the web drafts an advanced plan...',
argumentHint: '<prompt>',
isEnabled: () => process.env.USER_TYPE === 'ant', // 仅 ant 用户可用
}
```
> 注意:`isEnabled` 检查 `USER_TYPE === 'ant'`Anthropic 内部用户),这是命令级限制。关键字触发路径没有此限制,只要 feature flag 开启即可。
### 3.3 核心命令实现 — `src/commands/ultraplan.tsx`
#### 3.3.1 入口函数 `call()`
处理 `/ultraplan <prompt>` 斜杠命令:
1. **无参数调用**:显示使用帮助文本
2. **已有活跃会话**:返回 "already polling" 提示
3. **正常调用**:设置 `ultraplanLaunchPending` 状态,触发 `UltraplanLaunchDialog` 对话框
#### 3.3.2 `launchUltraplan()`
公共启动入口,被三个路径共享:
- 斜杠命令 (`/ultraplan`)
- 关键字触发 (`processUserInput.ts`)
- Plan 审批对话框的 "Ultraplan" 按钮 (`ExitPlanModePermissionRequest`)
关键逻辑:
1. 防重入检查(`ultraplanSessionUrl` / `ultraplanLaunching`
2. 同步设置 `ultraplanLaunching = true` 防止竞态
3. 异步调用 `launchDetached()`
4. 立即返回启动消息(不等远程会话创建)
#### 3.3.3 `launchDetached()`
异步后台流程:
1. **获取模型**:从 GrowthBook 读取 `tengu_ultraplan_model`,默认 `opus46` 的 firstParty ID
2. **资格检查**`checkRemoteAgentEligibility()` — 验证用户是否有权限使用远程 agent
3. **构建提示词**`buildUltraplanPrompt(blurb, seedPlan)`
- 如有 `seedPlan`(来自 plan 审批对话框),作为草稿前缀
- 加载 `prompt.txt` 中的指令模板
- 附加用户 blurb
4. **创建远程会话**`teleportToRemote()`
- `permissionMode: 'plan'` — 远程以 plan 模式运行
- `ultraplan: true` — 标记为 ultraplan 会话
- `useDefaultEnvironment: true` — 使用默认云端环境
5. **注册任务**`registerRemoteAgentTask()` 创建 `RemoteAgentTask` 追踪条目
6. **启动轮询**`startDetachedPoll()` 后台轮询审批状态
#### 3.3.4 提示词构建
```
buildUltraplanPrompt(blurb, seedPlan?)
```
- `prompt.txt`:当前为空文件(反编译丢失),原始内容应包含指导远程 agent 生成计划的系统指令
- 开发者可通过 `ULTRAPLAN_PROMPT_FILE` 环境变量覆盖提示词文件(仅 `USER_TYPE=ant` 时生效)
#### 3.3.5 `startDetachedPoll()`
后台轮询管理:
1. 调用 `pollForApprovedExitPlanMode()` 等待计划审批
2. 阶段变化时更新 `RemoteAgentTask.ultraplanPhase`UI 展示)
3. 审批完成后的两种路径:
- **`executionTarget: 'remote'`**:用户选择在云端执行
- 标记任务完成
- 清除 `ultraplanSessionUrl`
- 发送通知:结果将以 PR 形式提交
- **`executionTarget: 'local'`**用户选择传送回本地teleport
- 设置 `ultraplanPendingChoice`
- 触发 `UltraplanChoiceDialog` 对话框
4. 失败时:归档远程会话、清除状态、发送错误通知
#### 3.3.6 `stopUltraplan()`
用户主动停止:
1. `RemoteAgentTask.kill()` 归档远程会话
2. 清除所有 ultraplan 状态(`ultraplanSessionUrl``ultraplanPendingChoice``ultraplanLaunching`
3. 发送停止通知
### 3.4 CCR 会话轮询 — `src/utils/ultraplan/ccrSession.ts`
#### 3.4.1 `ExitPlanModeScanner`
纯状态机,无 I/O。摄入 `SDKMessage[]` 事件批次,分类 `ExitPlanMode` 工具调用的结果。
**状态类型**
```typescript
type ScanResult =
| { kind: 'approved' } // 用户批准了计划
| { kind: 'teleport' } // 用户点击"传送回本地"
| { kind: 'rejected' } // 用户拒绝(可继续迭代)
| { kind: 'pending' } // 等待用户审批中
| { kind: 'terminated' } // 远程会话意外终止
| { kind: 'unchanged' } // 无新事件,状态不变
```
**优先级**approved > terminated > rejected > pending > unchanged
**关键设计**
- 同一批事件可能包含审批和后续崩溃 — 不丢弃已审批的计划
- 拒绝后重新扫描(`rescanAfterRejection`),因为新事件可能包含修改后的计划
- 使用 `is_error: true` 判断拒绝,`content` 中查找标记提取计划文本
#### 3.4.2 `pollForApprovedExitPlanMode()`
轮询主循环:
- **轮询间隔**3 秒
- **超时**30 分钟
- **容错**:连续 5 次网络错误才放弃
- **阶段推断**
- `hasPendingPlan`(有 ExitPlanMode 无结果)→ `plan_ready`
- `quietIdle`(空闲且无新事件)→ `needs_input`(远程在等用户输入)
- 其他 → `running`
#### 3.4.3 计划文本提取
两种提取路径:
1. **Approved**:从 `tool_result` 中查找 `## Approved Plan:\n``## Approved Plan (edited by user):\n` 标记
2. **Teleport**:从 `tool_result` 中查找 `__ULTRAPLAN_TELEPORT_SENTINEL__` 标记(浏览器端嵌入)
### 3.5 输入处理集成 — `src/utils/processUserInput/processUserInput.ts`
关键字触发路径(在斜杠命令处理之前):
```typescript
if (feature('ULTRAPLAN') &&
mode === 'prompt' && // 非非交互模式
!isNonInteractiveSession && // 非后台会话
inputString !== null &&
!inputString.startsWith('/') && // 非斜杠命令
!ultraplanSessionUrl && // 无活跃会话
!ultraplanLaunching && // 非正在启动
hasUltraplanKeyword(inputString)) {
// 重写为 /ultraplan 命令
const rewritten = replaceUltraplanKeyword(inputString).trim()
await processSlashCommand(`/ultraplan ${rewritten}`, ...)
}
```
### 3.6 UI 层
#### 3.6.1 彩虹高亮 — `src/components/PromptInput/PromptInput.tsx`
当输入中检测到 `ultraplan` 关键字时:
- 对每个字符施加**彩虹渐变色**高亮(`getRainbowColor()`
- 显示通知:"This prompt will launch an ultraplan session in Claude Code on the web"
#### 3.6.2 预启动对话框 — `UltraplanLaunchDialog`
在 REPL 的 `focusedInputDialog === 'ultraplan-launch'` 时渲染。
用户选择:
- **确认**:调用 `launchUltraplan()`,先添加命令回显,异步启动远程会话
- **取消**:清除 `ultraplanLaunchPending` 状态
#### 3.6.3 计划选择对话框 — `UltraplanChoiceDialog`
`focusedInputDialog === 'ultraplan-choice'` 时渲染。
当 teleport 路径返回已审批计划时,用户可选择执行方式。
#### 3.6.4 Plan 审批按钮 — `ExitPlanModePermissionRequest`
本地 Plan Mode 的审批对话框中,如果 `feature('ULTRAPLAN')` 开启,会显示额外的 "Ultraplan" 按钮:
- 将当前本地计划作为 `seedPlan` 发送给远程
- 按钮仅在无活跃 ultraplan 会话时显示
### 3.7 应用状态 — `src/state/AppStateStore.ts`
```typescript
interface AppState {
ultraplanLaunching?: boolean // 防重入锁5 秒窗口)
ultraplanSessionUrl?: string // 活跃远程会话 URL
ultraplanPendingChoice?: { // 已审批计划等待选择
plan: string
sessionId: string
taskId: string
}
ultraplanLaunchPending?: { // 预启动对话框
blurb: string
}
isUltraplanMode?: boolean // 远程端CCR 侧的 ultraplan 标记
}
```
### 3.8 远程任务追踪 — `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx`
Ultraplan 使用 `RemoteAgentTask` 基础设施追踪远程会话:
```typescript
registerRemoteAgentTask({
remoteTaskType: 'ultraplan',
session: { id, title },
command: blurb,
isUltraplan: true // 特殊标记,跳过通用轮询逻辑
})
```
`extractPlanFromLog()``<ultraplan>...</ultraplan>` XML 标签中提取计划内容。
---
## 四、数据流时序
```
时间线 →
用户 本地 CLI CCR 云端
│ │ │
│ "ultraplan xxx" │ │
│──────────────────────>│ │
│ │ keyword 检测 + 重写 │
│ │ /ultraplan "plan xxx" │
│ │ │
│ [UltraplanLaunch │ │
│ Dialog] │ │
│──── confirm ─────────>│ │
│ │ launchDetached() │
│ │─────────────────────────────>│
│ │ teleportToRemote() │
│ │ (permissionMode: 'plan') │
│ │ │
│ "Starting..." │ │
│<──────────────────────│ │
│ │ │
│ (终端空闲,可继续) │ startDetachedPoll() │
│ │ ═══ 3s 轮询循环 ═══ │
│ │ │
│ │ [浏览器打开]│
│ │ [云端生成计划]
│ │ │
│ │ ← needs_input ─────────────│
│ │ (云端追问用户) │
│ │ │
│ │ [用户在浏览器回复]
│ │ │
│ │ ← plan_ready ──────────────│
│ │ (ExitPlanMode 等待审批) │
│ │ │
│ │ [用户审批/编辑]
│ │ │
│ ┌───────┤ ← approved ────────────────│
│ │ │ │
│ [远程执行] │ │ │
│ 通知完成 │ │ │
│ │ │ │
│ └── OR ─┤ ← teleport ───────────────│
│ │ │
│ [UltraplanChoice │ │
│ Dialog] │ │
│── 选择执行方式 ───────>│ │
│ │ 本地执行计划 │
```
---
## 五、关键文件清单
| 文件 | 职责 |
|------|------|
| `src/utils/ultraplan/keyword.ts` | 关键字检测、高亮位置计算、关键字替换 |
| `src/utils/ultraplan/ccrSession.ts` | CCR 会话轮询、ExitPlanMode 状态机、计划文本提取 |
| `src/utils/ultraplan/prompt.txt` | 远程指令模板(当前为空,需重建) |
| `src/commands/ultraplan.tsx` | `/ultraplan` 命令、启动/停止逻辑、提示词构建 |
| `src/utils/processUserInput/processUserInput.ts` | 关键字触发 → `/ultraplan` 命令路由 |
| `src/components/PromptInput/PromptInput.tsx` | 彩虹高亮 + 通知提示 |
| `src/screens/REPL.tsx` | 对话框渲染UltraplanLaunchDialog / UltraplanChoiceDialog |
| `src/components/permissions/ExitPlanModePermissionRequest/` | Plan 审批中的 "Ultraplan" 按钮 |
| `src/state/AppStateStore.ts` | ultraplan 相关状态字段定义 |
| `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx` | 远程任务追踪 + `<ultraplan>` 标签提取 |
| `src/constants/xml.ts` | `ULTRAPLAN_TAG = 'ultraplan'` |
---
## 六、依赖关系
### 外部依赖
| 依赖 | 用途 | 必要性 |
|------|------|--------|
| `teleportToRemote()` | 创建 CCR 云端会话 | 必须 — 核心功能 |
| `checkRemoteAgentEligibility()` | 验证用户远程 agent 使用资格 | 必须 — 前置检查 |
| `archiveRemoteSession()` | 归档/终止远程会话 | 必须 — 清理 |
| GrowthBook `tengu_ultraplan_model` | 获取使用的模型 ID | 可选 — 默认 opus46 |
| `@anthropic-ai/sdk` | SDKMessage 类型 | 必须 — 类型定义 |
| `pollRemoteSessionEvents()` | 事件流分页轮询 | 必须 — 轮询基础设施 |
### 内部依赖
- **ExitPlanModeV2Tool**:远程端调用的工具,触发 plan 审批流程
- **RemoteAgentTask**:任务追踪和状态管理基础设施
- **AppState Store**ultraplan 状态管理
---
## 七、当前状态与补全要点
| 组件 | 状态 | 说明 |
|------|------|------|
| 关键字检测 | ✅ 完整 | `keyword.ts` 逻辑完善 |
| 命令框架 | ✅ 完整 | 注册、路由、防重入完整 |
| 启动流程 | ✅ 完整 | `launchUltraplan` / `launchDetached` 完整 |
| CCR 轮询 | ✅ 完整 | `ccrSession.ts` 状态机完整 |
| UI 高亮/通知 | ✅ 完整 | 彩虹高亮 + 提示通知完整 |
| 状态管理 | ✅ 完整 | AppState 字段完整 |
| `prompt.txt` | ❌ 空文件 | 需要重建远程指令模板 |
| `UltraplanLaunchDialog` | ⚠️ 全局声明 | 组件实现未找到(可能在内置包中) |
| `UltraplanChoiceDialog` | ⚠️ 全局声明 | 组件实现未找到(可能在内置包中) |
| `isEnabled` 限制 | ⚠️ `USER_TYPE === 'ant'` | 命令级限制,仅 Anthropic 内部用户 |
### 补全建议
1. **重建 `prompt.txt`**:这是远程 agent 的核心指令,定义如何进行多 agent 探索式规划。需要设计:
- 规划方法论(多角度分析、风险评估、分阶段执行)
- ExitPlanMode 工具的使用引导
- 输出格式要求
2. **对话框组件**`UltraplanLaunchDialog``UltraplanChoiceDialog``global.d.ts` 中声明但实现缺失,需要新建:
- Launch Dialog确认对话框含 CCR 使用条款链接)
- Choice Dialog展示已审批计划 + 执行方式选择
3. **放宽 `isEnabled`**:如果要让非 ant 用户使用斜杠命令,需移除 `USER_TYPE === 'ant'` 检查
---
## 八、与相关 Feature 的关系
| Feature | 关系 |
|---------|------|
| `ULTRATHINK` | 类似的高能力模式,但 `ULTRATHINK` 只调高 effort不启动远程会话 |
| `FORK_SUBAGENT` | Ultraplan 不使用 fork subagent使用的是 CCR 远程 agent |
| `COORDINATOR_MODE` | 不同范式的多 agentCoordinator 在本地编排Ultraplan 在云端 |
| `BRIDGE_MODE` | 底层依赖相同的 `teleportToRemote()` 基础设施 |
| `ExitPlanModeTool` | 远程端的审批机制Ultraplan 的核心交互模型 |

View File

@@ -86,6 +86,7 @@
"group": "可扩展性",
"pages": [
"docs/extensibility/mcp-protocol",
"docs/extensibility/mcp-configuration",
"docs/extensibility/hooks",
"docs/extensibility/skills",
"docs/extensibility/custom-agents"
@@ -124,7 +125,10 @@
"docs/features/coordinator-mode",
"docs/features/fork-subagent",
"docs/features/daemon",
"docs/features/teammem"
"docs/features/teammem",
"docs/features/pipes-and-lan",
"docs/features/lan-pipes",
"docs/features/uds-inbox"
]
},
{
@@ -133,6 +137,7 @@
"docs/features/kairos",
"docs/features/voice-mode",
"docs/features/bridge-mode",
"docs/features/remote-control-self-hosting",
"docs/features/proactive",
"docs/features/ultraplan"
]
@@ -144,7 +149,11 @@
"docs/features/tree-sitter-bash",
"docs/features/bash-classifier",
"docs/features/web-browser-tool",
"docs/features/experimental-skill-search"
"docs/features/web-search-tool",
"docs/features/experimental-skill-search",
"docs/features/langfuse-monitoring",
"docs/features/computer-use",
"docs/features/claude-in-chrome-mcp"
]
},
{
@@ -169,13 +178,7 @@
]
}
],
"excludes": [
"docs/test-plans/**",
"docs/testing-spec.md",
"docs/REVISION-PLAN.md",
"docs/feature-exploration-plan.md",
"docs/ultraplan-implementation.md"
],
"excludes": [],
"footerSocials": {
"github": "https://github.com/anthropics/claude-code"
}

View File

@@ -1,6 +1,6 @@
{
"name": "claude-code-best",
"version": "1.1.0",
"version": "1.4.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>",
@@ -25,20 +25,25 @@
"bun": ">=1.2.0"
},
"bin": {
"ccb": "dist/cli.js",
"claude-code-best": "dist/cli.js"
"ccb": "dist/cli-node.js",
"ccb-bun": "dist/cli-bun.js",
"claude-code-best": "dist/cli-node.js"
},
"workspaces": [
"packages/*",
"packages/@ant/*"
"packages/@ant/*",
"packages/@anthropic-ai/*"
],
"files": [
"dist",
"scripts/download-ripgrep.ts",
"scripts/postinstall.cjs"
"scripts/postinstall.cjs",
"scripts/setup-chrome-mcp.mjs"
],
"scripts": {
"build": "bun run build.ts",
"build:vite": "vite build && bun run scripts/post-build.ts",
"build:vite:only": "vite build",
"build:bun": "bun run build.ts",
"dev": "bun run scripts/dev.ts",
"dev:inspect": "bun run scripts/dev-debug.ts",
"prepublishOnly": "bun run build",
@@ -49,14 +54,19 @@
"test": "bun test",
"check:unused": "knip-bun",
"health": "bun run scripts/health-check.ts",
"postinstall": "node scripts/postinstall.cjs",
"postinstall": "node scripts/run-parallel.mjs scripts/postinstall.cjs scripts/setup-chrome-mcp.mjs",
"docs:dev": "npx mintlify dev",
"typecheck": "tsc --noEmit",
"rcs": "bun run scripts/rcs.ts"
},
"dependencies": {},
"dependencies": {
"@agentclientprotocol/sdk": "^0.19.0",
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
"ws": "^8.20.0"
},
"devDependencies": {
"openai": "^6.33.0",
"@alcalzone/ansi-tokenize": "^0.3.0",
"@ant/model-provider": "workspace:*",
"@ant/claude-for-chrome-mcp": "workspace:*",
"@ant/computer-use-input": "workspace:*",
"@ant/computer-use-mcp": "workspace:*",
@@ -68,6 +78,7 @@
"@anthropic-ai/sandbox-runtime": "^0.0.44",
"@anthropic-ai/sdk": "^0.80.0",
"@anthropic-ai/vertex-sdk": "^0.14.4",
"@anthropic/ink": "workspace:*",
"@aws-sdk/client-bedrock": "^3.1020.0",
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
"@aws-sdk/client-sts": "^3.1020.0",
@@ -75,8 +86,13 @@
"@aws-sdk/credential-providers": "^3.1020.0",
"@azure/identity": "^4.13.1",
"@biomejs/biome": "^2.4.10",
"@claude-code-best/agent-tools": "workspace:*",
"@claude-code-best/builtin-tools": "workspace:*",
"@claude-code-best/mcp-client": "workspace:*",
"@commander-js/extra-typings": "^14.0.0",
"@growthbook/growthbook": "^1.6.5",
"@langfuse/otel": "^5.1.0",
"@langfuse/tracing": "^5.1.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api-logs": "^0.214.0",
@@ -99,13 +115,23 @@
"@sentry/node": "^10.47.0",
"@smithy/core": "^3.23.13",
"@smithy/node-http-handler": "^4.5.1",
"@types/bun": "^1.3.11",
"@types/bun": "^1.3.12",
"@types/cacache": "^20.0.1",
"@types/he": "^1.2.3",
"@types/lodash-es": "^4.17.12",
"@types/node": "^25.6.0",
"@types/picomatch": "^4.0.3",
"@types/plist": "^3.0.5",
"@types/proper-lockfile": "^4.1.4",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.14",
"@types/react-reconciler": "^0.33.0",
"@types/semver": "^7.7.1",
"@types/sharp": "^0.32.0",
"@types/shell-quote": "^1.7.5",
"@types/stack-utils": "^2.0.3",
"@types/turndown": "^5.0.6",
"@types/ws": "^8.18.1",
"ajv": "^8.18.0",
"asciichart": "^1.5.25",
"audio-capture-napi": "workspace:*",
@@ -132,7 +158,6 @@
"highlight.js": "^11.11.1",
"https-proxy-agent": "^8.0.0",
"ignore": "^7.0.5",
"@anthropic/ink": "workspace:*",
"image-processor-napi": "workspace:*",
"indent-string": "^5.0.0",
"jsonc-parser": "^3.3.1",
@@ -141,6 +166,7 @@
"lru-cache": "^11.2.7",
"marked": "^17.0.5",
"modifiers-napi": "workspace:*",
"openai": "^6.33.0",
"p-map": "^7.0.4",
"picomatch": "^4.0.4",
"plist": "^3.1.0",
@@ -149,6 +175,7 @@
"react": "^19.2.4",
"react-compiler-runtime": "^1.0.0",
"react-reconciler": "^0.33.0",
"rollup": "^4.60.1",
"semver": "^7.7.4",
"sharp": "^0.34.5",
"shell-quote": "^1.8.3",
@@ -163,11 +190,11 @@
"undici": "^7.24.6",
"url-handler-napi": "workspace:*",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.8",
"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"

View File

@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -5,9 +5,12 @@
* mouse and keyboard via CoreGraphics events and System Events.
*/
import { $ } from 'bun'
import { execFile, execFileSync } from 'child_process'
import { promisify } from 'util'
import type { FrontmostAppInfo, InputBackend } from '../types.js'
const execFileAsync = promisify(execFile)
const KEY_MAP: Record<string, number> = {
return: 36, enter: 36, tab: 48, space: 49, delete: 51, backspace: 51,
escape: 53, esc: 53,
@@ -25,13 +28,17 @@ const MODIFIER_MAP: Record<string, string> = {
}
async function osascript(script: string): Promise<string> {
const result = await $`osascript -e ${script}`.quiet().nothrow().text()
return result.trim()
const { stdout } = await execFileAsync('osascript', ['-e', script], {
encoding: 'utf-8',
})
return stdout.trim()
}
async function jxa(script: string): Promise<string> {
const result = await $`osascript -l JavaScript -e ${script}`.quiet().nothrow().text()
return result.trim()
const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', '-e', script], {
encoding: 'utf-8',
})
return stdout.trim()
}
function buildMouseJxa(eventType: string, x: number, y: number, btn: number, clickState?: number): string {
@@ -115,19 +122,14 @@ export const typeText: InputBackend['typeText'] = async (text) => {
export const getFrontmostAppInfo: InputBackend['getFrontmostAppInfo'] = () => {
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()
const output = execFileSync('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
`], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
if (!output || !output.includes('|')) return null
const [bundleId, appName] = output.split('|', 2)
return { bundleId: bundleId!, appName: appName! }

View File

@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -5,6 +5,8 @@ export interface DisplayGeometry {
scaleFactor: number
originX: number
originY: number
label?: string
isPrimary?: boolean
}
export interface ScreenshotResult {
@@ -42,6 +44,7 @@ export interface ResolvePrepareCaptureResult extends ScreenshotResult {
hidden: string[]
activated?: string
displayId: number
captureError?: string
}
export interface ComputerExecutorCapabilities {

View File

@@ -37,6 +37,24 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { randomUUID } from "node:crypto";
/** Detect actual image MIME type from base64 data by decoding the magic bytes. */
function detectMimeFromBase64(b64: string): string {
// Decode first 12 raw bytes (16 base64 chars is enough) and check standard magic bytes.
// PNG: 89 50 4E 47
// JPEG: FF D8 FF
// RIFF+WEBP: "RIFF" at 0..3 + "WEBP" at 8..11
// GIF: "GIF" at 0..2
const raw = Buffer.from(b64.slice(0, 16), "base64");
if (raw[0] === 0x89 && raw[1] === 0x50 && raw[2] === 0x4e && raw[3] === 0x47) return "image/png";
if (raw[0] === 0xff && raw[1] === 0xd8 && raw[2] === 0xff) return "image/jpeg";
if (
raw[0] === 0x52 && raw[1] === 0x49 && raw[2] === 0x46 && raw[3] === 0x46 && // RIFF
raw[8] === 0x57 && raw[9] === 0x45 && raw[10] === 0x42 && raw[11] === 0x50 // WEBP
) return "image/webp";
if (raw[0] === 0x47 && raw[1] === 0x49 && raw[2] === 0x46) return "image/gif";
return "image/png";
}
import { getDefaultTierForApp, getDeniedCategoryForApp, isPolicyDenied } from "./deniedApps.js";
import type {
ComputerExecutor,
@@ -88,6 +106,8 @@ export type CuErrorKind =
| "state_conflict" // wrong state for action (call sequence, mouse already held)
| "grant_flag_required" // action needs a grant flag (systemKeyCombos, clipboard*) from request_access
| "display_error" // display enumeration failed (platform)
| "launch_failed" // failed to launch an external process (e.g. terminal)
| "element_not_found" // UI element not found (e.g. window, automation element)
| "other";
/**
@@ -906,9 +926,10 @@ async function handleRequestAccess(
);
}
const perms = recheck as { granted: false; accessibility: boolean; screenRecording: boolean };
const missing: string[] = [];
if (!recheck.accessibility) missing.push("Accessibility");
if (!recheck.screenRecording) missing.push("Screen Recording");
if (!perms.accessibility) missing.push("Accessibility");
if (!perms.screenRecording) missing.push("Screen Recording");
return errorResult(
`macOS ${missing.join(" and ")} permission(s) not yet granted. ` +
`The permission panel has been shown. Once the user grants the ` +
@@ -1423,9 +1444,10 @@ async function handleRequestTeachAccess(
);
}
const perms = recheck as { granted: false; accessibility: boolean; screenRecording: boolean };
const missing: string[] = [];
if (!recheck.accessibility) missing.push("Accessibility");
if (!recheck.screenRecording) missing.push("Screen Recording");
if (!perms.accessibility) missing.push("Accessibility");
if (!perms.screenRecording) missing.push("Screen Recording");
return errorResult(
`macOS ${missing.join(" and ")} permission(s) not yet granted. ` +
`The permission panel has been shown. Once the user grants the ` +
@@ -2144,7 +2166,7 @@ async function handleScreenshot(
const monitorNote = await buildMonitorNote(
adapter,
shot.displayId,
shot.displayId ?? 0,
overrides.lastScreenshot?.displayId,
overrides.onDisplayPinned !== undefined,
);
@@ -2158,7 +2180,7 @@ async function handleScreenshot(
{
type: "image",
data: shot.base64,
mimeType: "image/jpeg",
mimeType: detectMimeFromBase64(shot.base64),
},
],
screenshot: shot,
@@ -2213,7 +2235,7 @@ async function handleScreenshot(
const monitorNote = await buildMonitorNote(
adapter,
shot.displayId,
shot.displayId ?? 0,
overrides.lastScreenshot?.displayId,
overrides.onDisplayPinned !== undefined,
);
@@ -2227,7 +2249,7 @@ async function handleScreenshot(
{
type: "image",
data: shot.base64,
mimeType: "image/jpeg",
mimeType: detectMimeFromBase64(shot.base64),
},
],
// Piggybacked for serverDef.ts to stash on InternalServerContext.
@@ -2306,7 +2328,7 @@ async function handleZoom(
// Return the image. NO `.screenshot` piggyback — this is the invariant.
return {
content: [{ type: "image", data: zoomed.base64, mimeType: "image/jpeg" }],
content: [{ type: "image", data: zoomed.base64, mimeType: detectMimeFromBase64(zoomed.base64) }],
};
}
@@ -4082,8 +4104,8 @@ export async function handleToolCall(
);
}
tccState = {
accessibility: osPerms.accessibility,
screenRecording: osPerms.screenRecording,
accessibility: (osPerms as { granted: false; accessibility: boolean; screenRecording: boolean }).accessibility,
screenRecording: (osPerms as { granted: false; accessibility: boolean; screenRecording: boolean }).screenRecording,
};
}

View File

@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -14,6 +14,17 @@ import type {
SwiftBackend, WindowDisplayInfo,
} from '../types.js'
export type {
DisplayGeometry,
PrepareDisplayResult,
AppInfo,
InstalledApp,
RunningApp,
ScreenshotResult,
ResolvePrepareCaptureResult,
WindowDisplayInfo,
} from '../types.js'
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -263,4 +274,9 @@ export const screenshot: ScreenshotAPI = {
if (displayId !== undefined) args.push('-D', String(displayId))
return captureScreenToBase64(args)
},
captureWindowTarget(_titleOrHwnd: string | number): ScreenshotResult | null {
// Window capture not supported on macOS via this backend
return null
},
}

View File

@@ -275,4 +275,9 @@ export const screenshot: ScreenshotAPI = {
return { base64: '', width: 0, height: 0 }
}
},
captureWindowTarget(_titleOrHwnd: string | number): ScreenshotResult | null {
// Window capture not supported on Linux via this backend
return null
},
}

View File

@@ -3,6 +3,8 @@ export interface DisplayGeometry {
height: number
scaleFactor: number
displayId: number
label?: string
isPrimary?: boolean
}
export interface PrepareDisplayResult {
@@ -37,6 +39,9 @@ export interface ResolvePrepareCaptureResult {
base64: string
width: number
height: number
captureError?: string
displayId?: number
hidden?: string[]
}
export interface WindowDisplayInfo {
@@ -71,6 +76,7 @@ export interface ScreenshotAPI {
x: number, y: number, w: number, h: number,
outW: number, outH: number, quality: number, displayId?: number,
): Promise<ScreenshotResult>
captureWindowTarget(titleOrHwnd: string | number): ScreenshotResult | null
}
export interface SwiftBackend {

View File

@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,176 @@
# Chapter 1: Getting Started
## Installation
`@anthropic/ink` is a workspace package. It is consumed internally and not published to npm.
```json
{
"dependencies": {
"@anthropic/ink": "workspace:*"
}
}
```
### Peer Dependencies
- `react` ^19.2.4
- `react-reconciler` ^0.33.0
### Key Dependencies
| Package | Purpose |
|---------|---------|
| `chalk` | ANSI color generation |
| `cli-boxes` | Border style definitions |
| `get-east-asian-width` | CJK character width measurement |
| `wrap-ansi` | ANSI-aware word wrapping |
| `bidi-js` | Bidirectional text support |
| `lodash-es` | Utility functions (throttle, noop) |
| `signal-exit` | Process exit handler cleanup |
| `emoji-regex` | Emoji width handling |
## Basic Rendering
### `render(node, options?)`
The primary entry point. Renders a React element tree to the terminal.
```tsx
import { render } from '@anthropic/ink'
import { Box, Text } from '@anthropic/ink'
const { unmount, rerender, waitUntilExit } = await render(
<Box>
<Text>Hello, World!</Text>
</Box>
)
```
**Parameters:**
- `node` -- `ReactNode` to render
- `options` -- `RenderOptions | NodeJS.WriteStream` (optional)
**Returns:** `Promise<Instance>` with:
- `rerender(node)` -- Replace the root node
- `unmount()` -- Unmount and clean up
- `waitUntilExit()` -- `Promise<void>` that resolves on unmount
- `cleanup()` -- Remove from instance registry
### `renderSync(node, options?)`
Synchronous version of render. Same API, returns `Instance` directly (no Promise).
```tsx
import { renderSync } from '@anthropic/ink'
const instance = renderSync(<App />)
// instance.rerender, instance.unmount, etc.
```
### `createRoot(options?)`
Creates a managed Ink root without immediately rendering. Similar to `react-dom`'s `createRoot`.
```tsx
import { createRoot } from '@anthropic/ink'
const root = await createRoot({ exitOnCtrlC: false })
// Later, render into it
root.render(<App />)
// You can re-render into the same root
root.render(<DifferentApp />)
// Clean up
root.unmount()
```
**Returns:** `Promise<Root>` with:
- `render(node)` -- Mount or update the tree
- `unmount()` -- Unmount
- `waitUntilExit()` -- `Promise<void>`
## RenderOptions
```ts
type RenderOptions = {
/** Output stream. Default: process.stdout */
stdout?: NodeJS.WriteStream
/** Input stream. Default: process.stdin */
stdin?: NodeJS.ReadStream
/** Error stream. Default: process.stderr */
stderr?: NodeJS.WriteStream
/** Handle Ctrl+C to exit. Default: true */
exitOnCtrlC?: boolean
/** Patch console methods to prevent Ink output mixing. Default: true */
patchConsole?: boolean
/** Called after each frame render with timing info. */
onFrame?: (event: FrameEvent) => void
}
```
## Basic Concepts
### Component Tree
Ink renders React components to a terminal using a custom reconciler. The tree structure maps to terminal output:
```tsx
<Box flexDirection="column">
<Text bold color="green">Header</Text>
<Box flexDirection="row" gap={1}>
<Text>Left</Text>
<Text>Right</Text>
</Box>
</Box>
```
This produces terminal output with Flexbox layout (via Yoga).
### Rendering Pipeline
1. **React Reconciler** -- Standard React reconciliation; diffs virtual tree
2. **Yoga Layout** -- Computes Flexbox positions/ sizes for every node
3. **Render to Output** -- Walks the DOM tree, emits styled text into an `Output` buffer
4. **Screen Diff** -- Compares new frame against previous frame in a screen buffer
5. **Terminal Write** -- Emits minimal ANSI escape sequences to update only changed cells
### Module System
Import everything from the package root:
```tsx
// Core rendering
import { render, createRoot, renderSync } from '@anthropic/ink'
// Components (base, no theme)
import { BaseBox, BaseText, ScrollBox, Button, Link, Newline, Spacer } from '@anthropic/ink'
// Theme-aware components (recommended)
import { Box, Text } from '@anthropic/ink'
// Hooks
import { useApp, useInput, useTerminalSize, useInterval } from '@anthropic/ink'
// Theme
import { ThemeProvider, useTheme, color } from '@anthropic/ink'
// Keybindings
import { useKeybinding, KeybindingProvider } from '@anthropic/ink'
```
### Naming Convention: Base vs Theme-aware
The package exports both raw and theme-aware versions of core components:
- **`BaseBox`** / **`BaseText`** -- Raw components that only accept raw color values (`rgb(...)`, `#hex`, `ansi:...`, `ansi256(...)`)
- **`Box`** / **`Text`** -- Theme-aware wrappers that accept both theme keys (`'claude'`, `'success'`, `'error'`) and raw color values
Always prefer the theme-aware versions unless you have a specific reason to use raw components.

View File

@@ -0,0 +1,348 @@
# Chapter 2: Layout System
Ink uses [Yoga](https://yogalayout.com/) (Facebook's cross-platform layout engine) to implement CSS Flexbox in the terminal. Every layout is flexbox-based -- there is no CSS Grid or flow layout.
## Box Component
`Box` is the fundamental layout primitive. It is the terminal equivalent of `<div style="display: flex">`.
```tsx
import { Box, Text } from '@anthropic/ink'
<Box flexDirection="row" gap={1}>
<Text>Left</Text>
<Text>Right</Text>
</Box>
```
### Box Props (Styles)
All layout props are passed directly as JSX props (no `style={}` wrapper needed):
#### Flex Direction
Controls the main axis direction.
```tsx
<Box flexDirection="row">...</Box> // Left to right (default)
<Box flexDirection="column">...</Box> // Top to bottom
<Box flexDirection="row-reverse">...</Box> // Right to left
<Box flexDirection="column-reverse">...</Box> // Bottom to top
```
#### Flex Grow / Shrink / Basis
```tsx
<Box flexGrow={1}>...</Box> // Grow to fill available space
<Box flexShrink={0}>...</Box> // Don't shrink below intrinsic size
<Box flexBasis={20}>...</Box> // Initial size before flex distribution
<Box flexBasis="50%">...</Box> // Percentage basis
```
Default values: `flexGrow={0}`, `flexShrink={1}`, `flexBasis=auto`.
#### Flex Wrap
```tsx
<Box flexWrap="nowrap">...</Box> // Single line (default)
<Box flexWrap="wrap">...</Box> // Multiple lines
<Box flexWrap="wrap-reverse">...</Box> // Reverse cross-axis stacking
```
#### Alignment
```tsx
<Box alignItems="flex-start">...</Box> // Cross-axis start
<Box alignItems="center">...</Box> // Cross-axis center
<Box alignItems="flex-end">...</Box> // Cross-axis end
<Box alignItems="stretch">...</Box> // Stretch to fill (default)
<Box alignSelf="flex-start">...</Box> // Override parent's alignItems
<Box alignSelf="center">...</Box>
<Box alignSelf="flex-end">...</Box>
<Box alignSelf="auto">...</Box> // Inherit from parent
```
#### Justify Content
```tsx
<Box justifyContent="flex-start">...</Box> // Main-axis start (default)
<Box justifyContent="flex-end">...</Box> // Main-axis end
<Box justifyContent="center">...</Box> // Center
<Box justifyContent="space-between">...</Box> // Equal gaps, no edges
<Box justifyContent="space-around">...</Box> // Equal gaps with edges
<Box justifyContent="space-evenly">...</Box> // Evenly distributed
```
#### Gap
Spacing between children (only accepts integers):
```tsx
<Box gap={1}>...</Box> // Both row and column gap
<Box columnGap={2}>...</Box> // Gap between columns only
<Box rowGap={1}>...</Box> // Gap between rows only
```
#### Padding
Inner spacing (only accepts integers):
```tsx
<Box padding={1}>...</Box> // All sides
<Box paddingX={2}>...</Box> // Left and right
<Box paddingY={1}>...</Box> // Top and bottom
<Box paddingLeft={2}>...</Box> // Left only
<Box paddingRight={2}>...</Box> // Right only
<Box paddingTop={1}>...</Box> // Top only
<Box paddingBottom={1}>...</Box> // Bottom only
```
#### Margin
Outer spacing (only accepts integers):
```tsx
<Box margin={1}>...</Box> // All sides
<Box marginX={2}>...</Box> // Left and right
<Box marginY={1}>...</Box> // Top and bottom
<Box marginLeft={2}>...</Box> // Left only
<Box marginRight={2}>...</Box> // Right only
<Box marginTop={1}>...</Box> // Top only
<Box marginBottom={1}>...</Box> // Bottom only
```
> **Note:** Fractional values for padding, margin, and gap are not supported. Ink will emit warnings if non-integer values are used.
#### Width & Height
```tsx
<Box width={40}>...</Box> // Fixed 40 characters wide
<Box height={10}>...</Box> // Fixed 10 rows tall
<Box width="50%">...</Box> // 50% of parent's width
<Box width="100%">...</Box> // Full parent width
```
#### Min/Max Dimensions
```tsx
<Box minWidth={20}>...</Box>
<Box maxWidth={80}>...</Box>
<Box minHeight={5}>...</Box>
<Box maxHeight={20}>...</Box>
```
Percentage values are supported: `minWidth="30%"`.
#### Position
```tsx
<Box position="absolute" top={0} right={0}>...</Box>
<Box position="absolute" top="10%" left="20%">...</Box>
<Box position="relative">...</Box> // Default
```
Position `absolute` removes the element from normal flow and positions it relative to its nearest positioned ancestor. Useful for overlays.
#### Display
```tsx
<Box display="flex">...</Box> // Visible (default)
<Box display="none">...</Box> // Hidden (removed from layout)
```
#### Border
```tsx
<Box borderStyle="single">...</Box> // Thin border
<Box borderStyle="double">...</Box> // Double-line border
<Box borderStyle="round">...</Box> // Rounded corners
<Box borderStyle="bold">...</Box> // Bold border
<Box borderStyle="singleDouble">...</Box> // Mixed
<Box borderStyle="doubleSingle">...</Box> // Mixed
<Box borderStyle="classic">...</Box> // ASCII art border
```
Control individual sides and colors:
```tsx
<Box
borderStyle="single"
borderTop={false} // Hide top border
borderBottom={true} // Show bottom border
borderColor="rgb(255,0,0)" // Red border
borderDimColor={true} // Dim the border
>
...
</Box>
```
Per-side colors:
```tsx
<Box
borderStyle="single"
borderTopColor="rgb(255,0,0)"
borderBottomColor="ansi:green"
borderLeftColor="#0000FF"
borderRightColor="ansi256(200)"
/>
```
Border text (labels in the border):
```tsx
<Box
borderStyle="round"
borderText={{ title: "My Panel", align: "left" }}
/>
```
#### Background
```tsx
<Box backgroundColor="rgb(40,40,40)">...</Box>
```
#### Overflow
```tsx
<Box overflow="visible">...</Box> // Content expands container (default)
<Box overflow="hidden">...</Box> // Clip without scrolling
<Box overflow="scroll">...</Box> // Enable scrolling (use ScrollBox)
```
`overflowX` and `overflowY` control each axis independently.
#### Opaque
```tsx
<Box opaque={true}>...</Box>
```
Fills the box interior with spaces (using terminal's default background) before rendering children. Useful for absolute-positioned overlays where gaps would otherwise be transparent.
#### NoSelect
```tsx
<Box noSelect={true}>...</Box> // Exclude from text selection
<Box noSelect="from-left-edge">...</Box> // Exclude from column 0 to box edge
```
Only affects alt-screen text selection. Useful for gutters (line numbers, diff markers).
## Spacer
`Spacer` fills all available space along the main axis (equivalent to `flexGrow: 1`).
```tsx
<Box flexDirection="row">
<Text>Left</Text>
<Spacer />
<Text>Right</Text>
</Box>
```
## Newline
Inserts line breaks.
```tsx
<Text>
Line 1
<Newline />
Line 2
<Newline count={2} />
Line 4 (after double break)
</Text>
```
## Layout Examples
### Two-column layout
```tsx
<Box flexDirection="row" width={80}>
<Box width="50%" padding={1}>
<Text>Left column</Text>
</Box>
<Box width="50%" padding={1}>
<Text>Right column</Text>
</Box>
</Box>
```
### Centered content
```tsx
<Box justifyContent="center" alignItems="center" height={20}>
<Text>Centered!</Text>
</Box>
```
### Sticky footer
```tsx
<Box flexDirection="column" height={24}>
<Box flexGrow={1}>
<Text>Scrollable content area</Text>
</Box>
<Box>
<Text>Status bar at bottom</Text>
</Box>
</Box>
```
### Bordered panel with title
```tsx
<Box
flexDirection="column"
borderStyle="round"
borderColor="rgb(87,105,247)"
padding={1}
width={60}
>
<Text bold>Panel Title</Text>
<Text>Panel content goes here.</Text>
</Box>
```
## NoSelect
Wraps a region to exclude it from text selection in alt-screen mode. A convenience wrapper around `Box` with `noSelect` set.
```tsx
import { NoSelect } from '@anthropic/ink'
<Box flexDirection="row">
<NoSelect>
<Text dimColor>1 </Text>
</NoSelect>
<Text>selectable code here</Text>
</Box>
```
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | - | Content |
| `fromLeftEdge` | `boolean` | `false` | Extend exclusion from column 0 to box's right edge |
Accepts all `BoxProps` except `noSelect`.
## BaseBox vs ThemedBox
Two versions of Box are exported:
- **`BaseBox`** (imported as `BaseBox`) -- Raw box, color props accept only raw `Color` values
- **`Box`** (themed, imported as `Box`) -- Theme-aware, color props accept `keyof Theme | Color`
```tsx
// Raw
<BaseBox borderStyle="single" borderColor="rgb(255,0,0)" />
// Theme-aware (resolves 'permission' to the current theme's blue)
<Box borderStyle="single" borderColor="permission" />
```

View File

@@ -0,0 +1,238 @@
# Chapter 3: Text & Styling
## Text Component
`Text` renders styled text content. It supports colors, emphasis, and text wrapping.
```tsx
import { Text } from '@anthropic/ink'
<Text bold color="success">Operation complete</Text>
```
### Text Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `color` | `keyof Theme \| Color` | - | Foreground color |
| `backgroundColor` | `keyof Theme` | - | Background color (theme-aware) |
| `bold` | `boolean` | `false` | Bold text |
| `dimColor` | `boolean` | `false` | Dim text (uses theme's `inactive` color) |
| `italic` | `boolean` | `false` | Italic text |
| `underline` | `boolean` | `false` | Underlined text |
| `strikethrough` | `boolean` | `false` | Strikethrough text |
| `inverse` | `boolean` | `false` | Swap foreground/background |
| `wrap` | `TextWrap` | `'wrap'` | Wrapping/truncation mode |
| `children` | `ReactNode` | - | Text content |
> **Note:** `bold` and `dimColor` are mutually exclusive (ANSI terminals cannot render both simultaneously).
### BaseText vs ThemedText
- **`BaseText`** -- Accepts raw `Color` values only
- **`Text`** (default export) -- Theme-aware, accepts `keyof Theme | Color` for `color`, and `keyof Theme` for `backgroundColor`
```tsx
// Raw color
<BaseText color="rgb(255,0,0)">Red text</BaseText>
// Theme key (resolved to current theme palette)
<Text color="error">Error message</Text>
// Mixed
<Text color="#FF0000">Custom red</Text>
```
### Text Wrap Modes
```tsx
<Text wrap="wrap">...</Text> // Word-wrap at container width (default)
<Text wrap="wrap-trim">...</Text> // Wrap + trim trailing whitespace
<Text wrap="end">...</Text> // Truncate with "..." at end
<Text wrap="truncate-end">...</Text> // Same as "end"
<Text wrap="truncate">...</Text> // Truncate (no ellipsis)
<Text wrap="middle">...</Text> // "start...end"
<Text wrap="truncate-middle">...</Text> // Same as "middle"
<Text wrap="truncate-start">...</Text> // "...text"
```
### TextHoverColorContext
Uncolored `Text` children inherit a hover color from context:
```tsx
import { TextHoverColorContext } from '@anthropic/ink'
<TextHoverColorContext.Provider value="suggestion">
<Text>Uncolored text gets the suggestion color</Text>
<Text color="error">This stays red</Text>
</TextHoverColorContext.Provider>
```
Precedence: explicit `color` > `TextHoverColorContext` > `dimColor`.
## Color System
### Raw Color Formats
Four formats are supported for raw color values:
```tsx
// RGB
<Text color="rgb(255,107,128)">Bright red</Text>
// Hex
<Text color="#FF6B80">Bright red</Text>
// ANSI 256-color
<Text color="ansi256(196)">Red from 256-color palette</Text>
// Named ANSI 16-color
<Text color="ansi:red">Red</Text>
<Text color="ansi:greenBright">Bright green</Text>
```
### ANSI Named Colors
Full list of `ansi:` prefixed names:
| Name | Color |
|------|-------|
| `ansi:black` | Black |
| `ansi:red` | Red |
| `ansi:green` | Green |
| `ansi:yellow` | Yellow |
| `ansi:blue` | Blue |
| `ansi:magenta` | Magenta |
| `ansi:cyan` | Cyan |
| `ansi:white` | White |
| `ansi:blackBright` | Dark gray |
| `ansi:redBright` | Bright red |
| `ansi:greenBright` | Bright green |
| `ansi:yellowBright` | Bright yellow |
| `ansi:blueBright` | Bright blue |
| `ansi:magentaBright` | Bright magenta |
| `ansi:cyanBright` | Bright cyan |
| `ansi:whiteBright` | Bright white |
## Utility Functions
### `color(colorValue, themeName, type?)`
Curried theme-aware color function. Resolves theme keys to raw color values.
```tsx
import { color } from '@anthropic/ink'
const paint = color('error', 'dark') // Returns (text: string) => string
console.log(paint('failed')) // 'failed' wrapped in ANSI red codes
const paintFg = color('rgb(255,0,0)', 'dark', 'foreground')
const paintBg = color('success', 'dark', 'background')
```
Parameters:
- `c` -- `keyof Theme | Color | undefined` -- Theme key or raw color
- `theme` -- `ThemeName` -- Current theme
- `type` -- `'foreground' | 'background'` (default `'foreground'`)
### `stringWidth(text)`
Measures the visual width of a string in terminal columns, accounting for:
- CJK characters (2 columns each)
- Emoji (2 columns each)
- ANSI escape sequences (0 columns)
```tsx
import { stringWidth } from '@anthropic/ink'
stringWidth('hello') // 5
stringWidth('你好') // 4
stringWidth('\x1b[31mhi') // 2 (ANSI codes ignored)
```
### `wrapText(text, width, textWrap)`
Wraps text to a given width with the specified wrapping mode.
```tsx
import { wrapText } from '@anthropic/ink'
wrapText('Hello World', 5, 'wrap') // 'Hello\nWorld'
wrapText('Hello World', 8, 'end') // 'Hello...'
```
### `wrapAnsi(text, width)`
Wraps text containing ANSI escape codes while preserving styling.
```tsx
import { wrapAnsi } from '@anthropic/ink'
wrapAnsi('\x1b[31mHello World\x1b[0m', 5)
// Wraps at word boundaries, keeps color codes intact
```
### `measureElement(node)`
Measures a rendered DOM element's dimensions.
```tsx
import { measureElement } from '@anthropic/ink'
const { width, height } = measureElement(domElement)
```
## Link Component
Renders an OSC 8 terminal hyperlink (clickable URL in supported terminals).
```tsx
import { Link } from '@anthropic/ink'
<Link url="https://example.com">
<Text underline color="suggestion">example.com</Text>
</Link>
```
Props:
- `url` -- `string` (required) -- Target URL
- `children` -- `ReactNode` -- Display content
- `fallback` -- `ReactNode` -- Shown when hyperlinks are unsupported
## RawAnsi Component
Renders pre-formatted ANSI strings directly into the layout.
```tsx
import { RawAnsi } from '@anthropic/ink'
<RawAnsi
lines={['\x1b[31mRed line 1\x1b[0m', '\x1b[32mGreen line 2\x1b[0m']}
width={40}
/>
```
Props:
- `lines` -- `string[]` -- Pre-rendered ANSI lines (one terminal row each)
- `width` -- `number` -- Column width the producer wrapped to
## Border Rendering
### `renderBorder(box, output, options?)`
Low-level border rendering function used internally by Box.
```tsx
import { renderBorder } from '@anthropic/ink'
import type { BorderTextOptions } from '@anthropic/ink'
```
Border styles available (from `cli-boxes`):
- `single` -- Thin lines `─│┌┐└┘`
- `double` -- Double lines `═║╔╗╚╝`
- `round` -- Rounded corners `─│╭╮╰╯`
- `bold` -- Bold lines `━┃┏┓┗┛`
- `singleDouble` -- Single horizontal, double vertical
- `doubleSingle` -- Double horizontal, single vertical
- `classic` -- ASCII `─|++++`

View File

@@ -0,0 +1,213 @@
# Chapter 4: Theme System
The theme system provides consistent, accessible color palettes across the application. It supports dark mode, light mode, ANSI-only terminals, and colorblind-accessible variants.
## ThemeProvider
Wraps the application to provide theme context.
```tsx
import { ThemeProvider } from '@anthropic/ink'
function App() {
return (
<ThemeProvider initialState="dark" onThemeSave={(setting) => saveConfig(setting)}>
<MyComponent />
</ThemeProvider>
)
}
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | Child components |
| `initialState` | `ThemeSetting` | Initial theme (default: loads from config) |
| `onThemeSave` | `(setting: ThemeSetting) => void` | Called when theme is saved |
### Theme Configuration Injection
Before mounting, inject config persistence callbacks:
```tsx
import { setThemeConfigCallbacks } from '@anthropic/ink'
setThemeConfigCallbacks({
loadTheme: () => configStore.get('theme', 'dark'),
saveTheme: (setting) => configStore.set('theme', setting),
})
```
## Theme Settings
```ts
type ThemeSetting = 'auto' | 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized' | 'light-ansi' | 'dark-ansi'
type ThemeName = 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized' | 'light-ansi' | 'dark-ansi'
```
| Theme | Description |
|-------|-------------|
| `dark` | Dark theme with RGB colors (default) |
| `light` | Light theme with RGB colors |
| `dark-daltonized` | Colorblind-accessible dark theme |
| `light-daltonized` | Colorblind-accessible light theme |
| `dark-ansi` | Dark theme using only 16 ANSI colors |
| `light-ansi` | Light theme using only 16 ANSI colors |
| `auto` | Follows terminal's dark/light mode (resolved at runtime) |
## Theme Hooks
### `useTheme()`
Returns the resolved theme name and setter.
```tsx
const [currentTheme, setTheme] = useTheme()
// currentTheme: ThemeName (never 'auto')
// setTheme: (setting: ThemeSetting) => void
```
### `useThemeSetting()`
Returns the raw setting (may be `'auto'`).
```tsx
const setting = useThemeSetting() // 'auto' | 'dark' | ...
```
### `usePreviewTheme()`
Returns preview controls for a theme picker UI.
```tsx
const { setPreviewTheme, savePreview, cancelPreview } = usePreviewTheme()
// Show preview
setPreviewTheme('light')
// User confirms
savePreview()
// User cancels
cancelPreview()
```
## Theme Color Palette
Every theme defines these semantic color keys:
### Brand & Identity
| Key | Purpose |
|-----|---------|
| `claude` | Brand orange |
| `claudeShimmer` | Lighter brand orange (animated) |
| `permission` | Permission/blue |
| `permissionShimmer` | Lighter permission blue |
| `autoAccept` | Electric violet |
| `planMode` | Teal/sage |
| `ide` | Muted blue |
### Semantic Colors
| Key | Purpose |
|-----|---------|
| `text` | Primary text color |
| `inverseText` | Text on inverse backgrounds |
| `inactive` | Dimmed/disabled elements |
| `inactiveShimmer` | Lighter inactive |
| `subtle` | Very subtle text |
| `suggestion` | Interactive/accent |
| `background` | General background accent |
| `success` | Positive/success |
| `error` | Negative/error |
| `warning` | Caution/warning |
| `warningShimmer` | Lighter warning |
| `merged` | Merged state |
### Diff Colors
| Key | Purpose |
|-----|---------|
| `diffAdded` | Added lines background |
| `diffRemoved` | Removed lines background |
| `diffAddedDimmed` | Dimmed added |
| `diffRemovedDimmed` | Dimmed removed |
| `diffAddedWord` | Word-level added |
| `diffRemovedWord` | Word-level removed |
### UI Colors
| Key | Purpose |
|-----|---------|
| `promptBorder` | Input prompt border |
| `promptBorderShimmer` | Lighter prompt border |
| `bashBorder` | Shell block border |
| `selectionBg` | Text selection highlight background |
| `userMessageBackground` | User message background |
| `userMessageBackgroundHover` | User message hover |
| `messageActionsBackground` | Action buttons background |
### Agent Colors
| Key | Purpose |
|-----|---------|
| `red_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `blue_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `green_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `yellow_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `purple_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `orange_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `pink_FOR_SUBAGENTS_ONLY` | Agent color assignment |
| `cyan_FOR_SUBAGENTS_ONLY` | Agent color assignment |
## Using Theme Colors in Components
### ThemedText
```tsx
<Text color="success">Operation complete</Text>
<Text color="error" bold>Failed!</Text>
<Text color="claude">Claude says...</Text>
<Text dimColor>Secondary info</Text>
<Text backgroundColor="userMessageBackground">Highlighted</Text>
```
### ThemedBox
```tsx
<Box borderStyle="single" borderColor="permission" backgroundColor="userMessageBackground">
<Text>Themed content</Text>
</Box>
```
### color() Utility
```tsx
import { color, useTheme } from '@anthropic/ink'
function MyComponent() {
const [themeName] = useTheme()
const paint = color('success', themeName)
// paint('text') returns ANSI-colored string
}
```
## Daltonized Themes
The daltonized themes (`light-daltonized`, `dark-daltonized`) are designed for users with protanopia/deuteranopia:
- Green/red diffs replaced with blue/red
- Status colors use blue instead of green
- Warning colors adjusted for better distinction
- All color pairs verified for sufficient contrast
## System Theme Detection
When `ThemeSetting` is `'auto'`:
1. Seeds from `$COLORFGBG` environment variable
2. Queries terminal via OSC 11 for live background color
3. Watches for changes (terminal theme switch) in real-time
4. Resolves to `'dark'` or `'light'` based on detected brightness

View File

@@ -0,0 +1,390 @@
# Chapter 5: Design System Components
Pre-built theme-aware UI components for common terminal interface patterns.
## Dialog
Modal dialog with border, title, and keyboard navigation.
```tsx
import { Dialog } from '@anthropic/ink'
<Dialog
title="Confirm Action"
subtitle="This cannot be undone"
onCancel={() => setShowDialog(false)}
color="warning"
>
<Text>Are you sure you want to proceed?</Text>
</Dialog>
```
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `title` | `ReactNode` | - | Dialog title (required) |
| `subtitle` | `ReactNode` | - | Optional subtitle |
| `children` | `ReactNode` | - | Dialog body content |
| `onCancel` | `() => void` | - | Called on Esc/n (required) |
| `color` | `keyof Theme` | `'permission'` | Title and border color |
| `hideInputGuide` | `boolean` | `false` | Hide the keyboard hint footer |
| `hideBorder` | `boolean` | `false` | Render without Pane border |
| `inputGuide` | `(exitState) => ReactNode` | - | Custom input guide footer |
| `isCancelActive` | `boolean` | `true` | Enable/disable cancel keybindings |
### Keyboard Shortcuts
- **Enter** -- Confirm (consumer handles this)
- **Esc / n** -- Cancel (calls `onCancel`)
- **Ctrl+C / Ctrl+D** -- Double-press to exit
### Custom Input Guide
```tsx
<Dialog
title="Save file?"
onCancel={handleCancel}
inputGuide={(exitState) => (
exitState.pending
? <Text>Press {exitState.keyName} again to exit</Text>
: <Text>Press Enter to save, Esc to cancel</Text>
)}
>
...
</Dialog>
```
## Pane
Bordered container with themed top border.
```tsx
import { Pane } from '@anthropic/ink'
<Pane color="permission">
<Text>Content inside a bordered pane</Text>
</Pane>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | - | Content |
| `color` | `keyof Theme` | `'permission'` | Top border color |
## ProgressBar
Visual progress indicator.
```tsx
import { ProgressBar } from '@anthropic/ink'
<ProgressBar
ratio={0.65}
width={40}
fillColor="rate_limit_fill"
emptyColor="rate_limit_empty"
/>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `ratio` | `number` | - | Progress 0..1 (required) |
| `width` | `number` | - | Character width (required) |
| `fillColor` | `keyof Theme` | - | Filled portion color |
| `emptyColor` | `keyof Theme` | - | Empty portion color |
## Spinner
Animated loading spinner. No props.
```tsx
import { Spinner } from '@anthropic/ink'
<Box gap={1}>
<Spinner />
<Text>Loading...</Text>
</Box>
```
## LoadingState
Loading message with spinner and optional subtitle.
```tsx
import { LoadingState } from '@anthropic/ink'
<LoadingState
message="Installing dependencies"
subtitle="This may take a moment"
bold={true}
/>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `message` | `string` | - | Loading message (required) |
| `bold` | `boolean` | `false` | Bold message |
| `dimColor` | `boolean` | `false` | Dimmed message |
| `subtitle` | `string` | - | Secondary text below |
## StatusIcon
Semantic status indicator with icon and color.
```tsx
import { StatusIcon } from '@anthropic/ink'
<StatusIcon status="success" withSpace />
<Text>Build complete</Text>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `status` | `'success' \| 'error' \| 'warning' \| 'info' \| 'pending' \| 'loading'` | - | Status type (required) |
| `withSpace` | `boolean` | `false` | Add trailing space |
Status icons:
- `success` -- Green checkmark
- `error` -- Red cross
- `warning` -- Yellow warning
- `info` -- Blue info
- `pending` -- Dimmed circle
- `loading` -- Dimmed ellipsis
## FuzzyPicker
Full-featured fuzzy search selector with preview support.
```tsx
import { FuzzyPicker } from '@anthropic/ink'
<FuzzyPicker
title="Select a file"
items={files}
getKey={(f) => f.path}
renderItem={(f, focused) => <Text>{f.name}</Text>}
onQueryChange={(q) => setFilteredFiles(filterFiles(q))}
onSelect={(f) => openFile(f)}
onCancel={() => setShowPicker(false)}
/>
```
### Props
| Prop | Type | Description |
|------|------|-------------|
| `title` | `string` | Picker title (required) |
| `items` | `readonly T[]` | Items to display (required) |
| `getKey` | `(item: T) => string` | Unique key extractor (required) |
| `renderItem` | `(item: T, isFocused: boolean) => ReactNode` | Item renderer (required) |
| `onQueryChange` | `(query: string) => void` | Filter callback (required) |
| `onSelect` | `(item: T) => void` | Enter key handler (required) |
| `onCancel` | `() => void` | Esc handler (required) |
| `renderPreview` | `(item: T) => ReactNode` | Preview panel renderer |
| `previewPosition` | `'bottom' \| 'right'` | Preview placement |
| `visibleCount` | `number` | Max visible items |
| `direction` | `'down' \| 'up'` | Item ordering |
| `onTab` | `PickerAction<T>` | Tab key handler |
| `onShiftTab` | `PickerAction<T>` | Shift+Tab handler |
| `onFocus` | `(item: T \| undefined) => void` | Focus change callback |
| `emptyMessage` | `string \| ((query: string) => string)` | Empty state message |
| `matchLabel` | `string` | Status line below list |
| `placeholder` | `string` | Input placeholder |
| `initialQuery` | `string` | Initial search query |
| `selectAction` | `string` | Action label for byline |
| `extraHints` | `ReactNode` | Additional keyboard hints |
## Tabs / Tab
Tabbed interface with keyboard navigation.
```tsx
import { Tabs, Tab } from '@anthropic/ink'
<Tabs title="Settings" color="claude">
<Tab title="General" id="general">
<GeneralSettings />
</Tab>
<Tab title="Advanced" id="advanced">
<AdvancedSettings />
</Tab>
</Tabs>
```
### Tabs Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactElement<TabProps>[]` | - | Tab elements |
| `title` | `string` | - | Header title |
| `color` | `keyof Theme` | - | Active tab indicator color |
| `defaultTab` | `string` | - | Initial tab id |
| `selectedTab` | `string` | - | Controlled selected tab |
| `onTabChange` | `(tabId: string) => void` | - | Tab change callback |
| `hidden` | `boolean` | `false` | Hide tab headers |
| `useFullWidth` | `boolean` | `false` | Use full terminal width |
| `banner` | `ReactNode` | - | Banner below tab headers |
| `disableNavigation` | `boolean` | `false` | Disable keyboard nav |
| `initialHeaderFocused` | `boolean` | `true` | Start with header focused |
| `contentHeight` | `number` | - | Fixed content height |
| `navFromContent` | `boolean` | `false` | Allow Tab/Arrow from content |
### Tab Props
| Prop | Type | Description |
|------|------|-------------|
| `title` | `string` | Tab label (required) |
| `id` | `string` | Tab identifier |
| `children` | `ReactNode` | Tab content |
### Tab Hooks
```tsx
import { useTabsWidth, useTabHeaderFocus } from '@anthropic/ink'
const width = useTabsWidth() // Available content width
const focused = useTabHeaderFocus() // Whether tab header is focused
```
## ListItem
Selectable list item with focus/selection indicators.
```tsx
import { ListItem } from '@anthropic/ink'
<ListItem isFocused={index === focusedIndex} isSelected={item.checked}>
<Text>{item.label}</Text>
</ListItem>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `isFocused` | `boolean` | - | Keyboard focus (required) |
| `isSelected` | `boolean` | `false` | Checked/active state |
| `children` | `ReactNode` | - | Content |
| `description` | `string` | - | Secondary text below |
| `styled` | `boolean` | `true` | Auto-style based on state |
| `disabled` | `boolean` | `false` | Dimmed, non-interactive |
| `showScrollDown` | `boolean` | `false` | Scroll-down hint arrow |
| `showScrollUp` | `boolean` | `false` | Scroll-up hint arrow |
| `declareCursor` | `boolean` | `true` | Declare terminal cursor |
## SearchBox
Search input with theme-aware styling.
```tsx
import { SearchBox } from '@anthropic/ink'
<SearchBox
query={searchQuery}
placeholder="Search..."
isFocused={true}
isTerminalFocused={true}
width="100%"
/>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `query` | `string` | - | Current search text |
| `placeholder` | `string` | - | Placeholder text |
| `isFocused` | `boolean` | - | Focus state |
| `isTerminalFocused` | `boolean` | - | Terminal focus state |
| `prefix` | `string` | - | Input prefix label |
| `width` | `number \| string` | - | Input width |
| `cursorOffset` | `number` | - | Cursor position offset |
| `borderless` | `boolean` | `false` | Remove border |
## Divider
Horizontal/vertical divider line.
```tsx
import { Divider } from '@anthropic/ink'
<Divider width={60} color="subtle" />
<Divider title="Section Title" />
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `width` | `number` | Terminal width | Divider width |
| `color` | `keyof Theme` | Dimmed | Line color |
| `char` | `string` | `'─'` | Line character |
| `padding` | `number` | `0` | Width reduction |
| `title` | `string` | - | Centered title text |
## Byline
Footer with middot-separated items.
```tsx
import { Byline } from '@anthropic/ink'
<Byline>
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
<KeyboardShortcutHint shortcut="Esc" action="cancel" />
</Byline>
```
## KeyboardShortcutHint
Display a keyboard shortcut with its action.
```tsx
import { KeyboardShortcutHint } from '@anthropic/ink'
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
<KeyboardShortcutHint shortcut="↑/↓" action="navigate" parens />
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `shortcut` | `string` | - | Key or chord to display |
| `action` | `string` | - | Action description |
| `parens` | `boolean` | `false` | Wrap in parentheses |
| `bold` | `boolean` | `false` | Bold shortcut text |
## ConfigurableShortcutHint
Displays a shortcut hint that reads the actual keybinding from config.
```tsx
import { ConfigurableShortcutHint } from '@anthropic/ink'
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="cancel"
/>
```
| Prop | Type | Description |
|------|------|-------------|
| `action` | `string` | Keybinding action name |
| `context` | `string` | Keybinding context |
| `fallback` | `string` | Default shortcut if unbound |
| `description` | `string` | Action description |
| `parens` | `boolean` | Wrap in parentheses |
| `bold` | `boolean` | Bold shortcut text |
## Ratchet
Animated counter component that prevents layout jumps.
```tsx
import { Ratchet } from '@anthropic/ink'
<Ratchet lock="always">
<Text>{count}</Text>
</Ratchet>
```
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | - | Content |
| `lock` | `'always' \| 'offscreen'` | `'always'` | Width locking strategy. `'always'` locks always; `'offscreen'` only locks when the element is scrolled off-screen |

View File

@@ -0,0 +1,189 @@
# Chapter 6: Scrolling
## ScrollBox
A scrollable container with imperative scroll API, viewport culling, and sticky scroll support.
```tsx
import { ScrollBox } from '@anthropic/ink'
import type { ScrollBoxHandle } from '@anthropic/ink'
function MessageList({ messages }) {
const scrollRef = useRef<ScrollBoxHandle>(null)
// Auto-scroll to bottom on new messages
useEffect(() => {
scrollRef.current?.scrollToBottom()
}, [messages.length])
return (
<ScrollBox ref={scrollRef} stickyScroll flexDirection="column" height={20}>
{messages.map(msg => (
<Text key={msg.id}>{msg.text}</Text>
))}
</ScrollBox>
)
}
```
### Props
ScrollBox accepts all Box layout props except `textWrap`, `overflow`, `overflowX`, `overflowY` (these are managed internally):
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `ref` | `Ref<ScrollBoxHandle>` | - | Imperative handle |
| `stickyScroll` | `boolean` | `false` | Auto-follow new content |
| *(layout props)* | `Styles` | - | Width, height, padding, etc. |
### ScrollBoxHandle (Imperative API)
```ts
interface ScrollBoxHandle {
// Absolute positioning
scrollTo(y: number): void
scrollToElement(el: DOMElement, offset?: number): void
scrollToBottom(): void
// Relative positioning
scrollBy(dy: number): void
// Query state
getScrollTop(): number
getPendingDelta(): number
getScrollHeight(): number
getFreshScrollHeight(): number
getViewportHeight(): number
getViewportTop(): number
isSticky(): boolean
// Events
subscribe(listener: () => void): () => void
// Virtual scroll support
setClampBounds(min?: number, max?: number): void
}
```
### Method Details
#### `scrollTo(y)`
Jump to an absolute position. Breaks sticky scroll.
```tsx
scrollRef.current?.scrollTo(0) // Scroll to top
```
#### `scrollBy(dy)`
Scroll by a relative amount. Accumulates deltas for smooth scrolling.
```tsx
scrollRef.current?.scrollBy(3) // Scroll down 3 rows
scrollRef.current?.scrollBy(-5) // Scroll up 5 rows
```
#### `scrollToElement(el, offset?)`
Scroll so a specific DOM element is at the viewport top. More reliable than `scrollTo` because it reads the element's position at render time (avoids stale layout values).
```tsx
const elementRef = useRef<DOMElement>(null)
scrollRef.current?.scrollToElement(elementRef.current!, 2)
```
#### `scrollToBottom()`
Pin scroll to bottom. Enables sticky mode.
```tsx
scrollRef.current?.scrollToBottom()
```
#### `isSticky()`
Returns `true` when scroll is pinned to the bottom.
```tsx
if (scrollRef.current?.isSticky()) {
// User hasn't scrolled up
}
```
#### `subscribe(listener)`
Subscribe to imperative scroll changes. Returns unsubscribe function.
```tsx
useEffect(() => {
return scrollRef.current?.subscribe(() => {
console.log('Scroll position changed')
})
}, [])
```
### Sticky Scroll
When `stickyScroll` is enabled:
1. Scroll automatically follows new content at the bottom
2. User scroll (via `scrollBy`/`scrollTo`) breaks stickiness
3. `scrollToBottom()` re-enables stickiness
4. Content growth at the bottom is detected and followed automatically
```tsx
<ScrollBox stickyScroll height={20}>
{/* New items auto-scroll to bottom */}
{items.map(renderItem)}
</ScrollBox>
```
### Viewport Culling
ScrollBox only renders children that intersect the visible viewport. Children outside the viewport are still mounted in React but skipped during terminal rendering. This makes large lists performant.
### Virtual Scrolling
For very large lists, use `setClampBounds` in combination with a virtual scrolling hook:
```tsx
const scrollRef = useRef<ScrollBoxHandle>(null)
// After computing visible range
scrollRef.current?.setClampBounds(firstVisibleRow, lastVisibleRow)
```
This prevents burst `scrollTo` calls from showing blank space beyond mounted content.
### Scroll Events
ScrollBox bypasses React state for scroll operations. Instead:
1. `scrollTo`/`scrollBy` mutate `scrollTop` directly on the DOM node
2. The node is marked dirty
3. A microtask-deferred render fires to coalesce multiple scroll events
4. The Ink renderer reads `scrollTop` during layout
This avoids React reconciler overhead per wheel event.
### Integration with Mouse Wheel
In alt-screen mode, mouse wheel events are captured by the `App` component and forwarded to the focused ScrollBox:
```
Wheel event → App.handleMouseEvent → ScrollBox.scrollBy(delta)
```
### Layout Structure
ScrollBox creates a two-level DOM structure:
```
ink-box (overflow: scroll, constrained height)
└── Box (flexGrow: 1, flexShrink: 0, width: 100%)
├── Child 1
├── Child 2
└── ...
```
The outer `ink-box` is the viewport with constrained size. The inner `Box` grows to fit all content. The renderer computes `scrollHeight` from the inner box and translates content by `-scrollTop`.

View File

@@ -0,0 +1,267 @@
# Chapter 7: User Input
## useInput
The primary hook for handling keyboard input.
```tsx
import { useInput } from '@anthropic/ink'
function MyComponent() {
useInput((input, key, event) => {
if (input === 'q') {
// 'q' key pressed
}
if (key.leftArrow) {
// Left arrow
}
if (key.ctrl && input === 'c') {
// Ctrl+C (only if exitOnCtrlC is false)
}
if (key.meta && input === 'b') {
// Alt+B (Option+B on Mac)
}
if (key.shift && input === 'Tab') {
// Shift+Tab
}
})
return <Text>Press keys...</Text>
}
```
### Signature
```ts
function useInput(
handler: (input: string, key: Key, event: InputEvent) => void,
options?: { isActive?: boolean }
): void
```
### Parameters
- **`input`** (`string`) -- The character entered. Empty string for non-printable keys (arrows, function keys). For paste events, the entire pasted text.
- **`key`** (`Key`) -- Parsed key metadata (see below)
- **`event`** (`InputEvent`) -- Raw event with `stopImmediatePropagation()`
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `isActive` | `boolean` | `true` | Enable/disable input handling |
### Key Object
```ts
type Key = {
upArrow: boolean
downArrow: boolean
leftArrow: boolean
rightArrow: boolean
pageDown: boolean
pageUp: boolean
wheelUp: boolean // Mouse wheel in alt-screen
wheelDown: boolean // Mouse wheel in alt-screen
home: boolean
end: boolean
return: boolean
escape: boolean
ctrl: boolean
shift: boolean
fn: boolean
tab: boolean
backspace: boolean
delete: boolean
meta: boolean // Alt / Option
super: boolean // Cmd (macOS) / Win key
}
```
### Event Propagation
Multiple `useInput` handlers form a chain. Call `event.stopImmediatePropagation()` to prevent downstream handlers from receiving the event:
```tsx
useInput((input, key, event) => {
if (input === 'j') {
// Consumed by this handler
event.stopImmediatePropagation()
}
// Other handlers won't see 'j'
})
useInput((input, key) => {
// This won't fire for 'j'
})
```
### Raw Mode
`useInput` automatically enables raw mode on stdin when active. Raw mode is reference-counted -- it stays enabled as long as any hook has `isActive: true`.
In raw mode:
- Keystrokes don't echo
- Ctrl+C is not sent as signal (app must handle it)
- Line buffering is disabled
## InputEvent
```ts
class InputEvent extends Event {
readonly input: string
readonly key: Key
readonly keypress: ParsedKey // Raw parsed keypress data
}
```
## KeyboardEvent
DOM-like keyboard event dispatched to focused elements:
```ts
class KeyboardEvent extends Event {
readonly key: Key
}
```
Used with `Box`'s `onKeyDown` and `onKeyDownCapture` props:
```tsx
<Box
tabIndex={0}
autoFocus
onKeyDown={(event) => {
if (event.key.return) {
handleSubmit()
}
}}
>
<Text>Press Enter to submit</Text>
</Box>
```
## Key Parsing
Ink supports multiple keyboard protocols:
### Standard Escape Sequences
- Arrow keys, function keys, Home/End, Page Up/Down
- Ctrl+letter combinations
- Shift, Alt, Meta modifiers
### Kitty Keyboard Protocol (CSI u)
Extended key reporting with full modifier support:
- Distinguishes Ctrl+Shift+A from Ctrl+A
- Reports Super (Cmd/Win) key
- Sends key release events
### xterm modifyOtherKeys
Alternative extended key reporting for xterm-compatible terminals.
### Application Keypad Mode
Numpad keys mapped to their digit characters.
## Paste Detection
When `Bracketed Paste` mode is enabled (DECSET 2004), pasted text is delivered as a single `InputEvent` with the full text in `input`. This distinguishes paste from rapid typing:
```tsx
useInput((input, key, event) => {
if (event.keypress.paste) {
// User pasted text -- handle as a batch
handlePaste(input)
} else {
// Regular keypress
handleKey(input, key)
}
})
```
## Mouse Events (Alt-Screen Only)
In alternate screen mode, mouse events are parsed and dispatched:
### Click Events
```tsx
<Box
onClick={(event) => {
console.log(`Clicked at (${event.x}, ${event.y})`)
event.stopImmediatePropagation()
}}
>
<Text>Click me</Text>
</Box>
```
### Hover Events
```tsx
<Box
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Text>{hovered ? 'Hovered!' : 'Hover me'}</Text>
</Box>
```
Hover events use `mouseenter`/`mouseleave` semantics (no bubbling between children).
### Wheel Events
Mouse wheel events arrive as `Key.wheelUp`/`Key.wheelDown`:
```tsx
useInput((input, key) => {
if (key.wheelUp) scrollUp()
if (key.wheelDown) scrollDown()
})
```
## useStdin
Lower-level access to the stdin stream.
```tsx
import { useStdin } from '@anthropic/ink'
const {
stdin, // Raw stdin stream
setRawMode, // (enabled: boolean) => void
isRawModeSupported, // boolean
internal_exitOnCtrlC, // boolean
internal_eventEmitter, // EventEmitter | undefined
internal_querier, // Terminal querier
} = useStdin()
```
> **Prefer `useInput` for keyboard handling.** `useStdin` is for advanced use cases like terminal querying or custom event handling.
## Button Component
Interactive button that responds to keyboard and mouse:
```tsx
import { Button } from '@anthropic/ink'
<Button onAction={() => handleClick()} tabIndex={0} autoFocus>
{(state) => (
<Text bold={state.focused} color={state.focused ? 'claude' : 'text'}>
{state.focused ? '> Click Me' : ' Click Me'}
</Text>
)}
</Button>
```
Button receives a render prop with state:
```ts
type ButtonState = {
focused: boolean // Has keyboard focus
hovered: boolean // Mouse is over it (alt-screen)
active: boolean // True for 100ms after activation (flash effect)
}
```
Activation triggers: Enter key, Space key, or mouse click.

Some files were not shown because too many files have changed in this diff Show More