Compare commits

..

19 Commits

Author SHA1 Message Date
claude-code-best
5ed0e63095 refactor: 将 weixin 模块从 src/ 迁移至 packages/weixin 工作区包
将 src/services/weixin/ 中的纯业务逻辑迁入 @claude-code-best/weixin
workspace 包,降低 src/ 耦合度。仅保留 server.ts 作为薄适配层。

- 迁移 7 个无修改的纯模块 (types/api/accounts/login/pairing/media/send)
- monitor.ts 内联 PERMISSION_REPLY_RE 正则,解除对 src/ 的依赖
- permissions.ts 本地定义 ChannelPermissionRequestParams 接口
- cli.ts 拆分:serve 子命令通过回调注入,login/access 保留在包内
- server.ts 重写为从 @claude-code-best/weixin 导入
- 新增 cli-serve.ts 作为 serve 入口薄壳

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 20:55:13 +08:00
claude-code-best
0201db55ac chore: 删除临时规划文档 wx_channel.md 并还原 package.json 排序
wx_channel.md 内容已整合到 docs/features/channels.md,不再需要。
package.json 中 @ant/model-provider 位置从原始位置被无意移动,还原。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 20:32:20 +08:00
1111
7a9f53b63f fix: 修正 vite 构建的 Windows 路径解析
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-19 18:50:20 +08:00
1111
dbc8a85cd7 fix: 改用 qrcode 生成 weixin 登录二维码
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-19 18:49:57 +08:00
1111
3b3e4fb1ea fix: 延迟加载 weixin 登录二维码依赖
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-19 18:38:40 +08:00
1111
6607b13364 docs: 更新微信 channel 接入计划状态
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-19 14:40:30 +08:00
1111
cc09c304ec docs: 补充内建 weixin channel 使用说明
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-19 14:39:29 +08:00
1111
3c2e046bf9 fix: 修复 builtin channel 的 ChannelsNotice 误报
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-19 14:37:45 +08:00
1111
bd6417c715 fix: 修正 channel permission relay 路由与能力判定
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-19 14:36:35 +08:00
1111
4bf9f04a4d feat: 注册内建 weixin channel 插件
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-19 14:35:23 +08:00
1111
c0f7735110 feat: 接入 weixin 服务层与命令入口
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-19 14:34:33 +08:00
claude-code-best
f9d011164a fix: 替换 web 端 crypto.randomUUID 为 uuid 库以支持 HTTP 环境
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 10:33:15 +08:00
claude-code-best
481e2a58a9 feat: 恢复 --channels 能力 (#297)
* feat: 恢复  --channels 能力

* docs: 添加 channels 注释
2026-04-19 10:24:34 +08:00
claude-code-best
c5edee431f docs: 文档检查/check 20260419 (#296)
* docs: 修复文档巡检发现的 4 处错误

- daemon.md: 反映实际实现状态(supervisor/worker 已实现而非 stub)
- bridge-mode.md: API 操作数量从 7 修正为 9
- web-search-tool.md: 文件路径从 src/tools/ 修正为 packages/builtin-tools/src/tools/
- remote-control-self-hosting.md: 补充缺失的 RCS_WS_IDLE_TIMEOUT 和 RCS_WS_KEEPALIVE_INTERVAL 配置项

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

* docs: 修正 Safety 和 Context 文档中的代码引用和类型错误

- permission-model: 修正规则来源从"五层"到八层,优先级顺序对齐代码
- permission-model: PermissionUpdate 类型改为实际的 addRules/replaceRules 等
- permission-model: 补充 acceptEdits 和 dontAsk 两种权限模式
- permission-model: DENIAL_LIMITS 字段名对齐实际代码
- plan-mode: 工具路径从 src/tools/ 改为 packages/builtin-tools/src/tools/
- compaction: 修正 COMPACTABLE_TOOLS 和 POST_COMPACT_* 的行号
- project-memory: 修正 ENTRYPOINT_NAME 常量的行号
- system-prompt: 修正 SystemPrompt 类型定义文件路径和多个行号引用

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

* docs: 修复 introduction 文档中的错误路径和行号引用

- why-this-whitepaper.mdx: BashTool 路径从 src/tools/ 修正为 packages/builtin-tools/src/tools/
- what-is-claude-code.mdx: 移除不存在的 Azure provider,改为实际的 7 种 provider
- architecture-overview.mdx: State 类型行号从 204 修正为 207

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

* docs: 修复 conversation/features 文档中的错误

- streaming.mdx: queryStreamRaw → queryModelWithStreaming 函数名修正
- streaming.mdx: Azure 提供商不存在,替换为实际 7 个提供商
- debug-mode.mdx: --inspect-wait 描述错误,实际使用 BUN_INSPECT 环境变量
- buddy.mdx: 补充缺失的 companionReact.ts、CompanionCard.tsx、index.ts

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

* docs: 修复文档巡检中的源码引用错误

- feature-flags.mdx: 修正 feature() 兜底描述,实际从 bun:bundle 导入而非 cli.tsx:3 内联
- feature-flags.mdx: 修正工具 require 路径为 @claude-code-best/builtin-tools 包路径
- ant-only-world.mdx: 修正 tools.ts 中 require 路径为包路径
- ant-only-world.mdx: 修正 INTERNAL_ONLY_COMMANDS 行号 (267-295) 和数量 (24+)
- skills.mdx: 修正 COMMANDS memoize 行号 258 → 299
- mcp-protocol.mdx: 修正 fetchToolsForClient LRU 缓存上限 20 → 100
- streaming.mdx: 修正流式事件引用
- file-operations.mdx: 修正工具路径引用
- search-and-navigation.mdx: 修正搜索工具引用
- shell-execution.mdx: 修正 shell 工具引用
- buddy.mdx: 补充缺失的 frontmatter 字段
- debug-mode.mdx: 修正调试模式描述

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

* docs: 修正 tools/agent 文档中的文件路径和行号引用

- 修正 TodoWriteTool、AgentTool、ToolSearchTool 等工具路径
  src/tools/ → packages/builtin-tools/src/tools/
- 更新 Tool.ts、tools.ts、BashTool.tsx 中过时的行号引用
- 修正 WebSearchTool/WebFetchTool/EnterWorktreeTool/ExitWorktreeTool 路径
- 修正 AgentTool.tsx 中多行行号引用

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

* docs: 修正 feature 文档中的文件路径和行号引用

- ultraplan.md: 更新文件行数(525/349/127)
- fork-subagent.md: 路径迁移 src/tools/ → packages/builtin-tools/
- mcp-skills.md: 修正 getMcpSkillCommands 行号 547→604,client.ts 行号 117→129
- kairos.md: 修正 getBriefSection/getProactiveSection 行号
- proactive.md: 修正 getProactiveSection 行号 860→864

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

* docs: 修正顶层文档中的路径迁移和行号引用

- auto-updater.md: config.ts 行号 1735→1737,标注未接入启动流程的函数
- external-dependencies.md: WebSearchTool/WebFetchTool 路径迁移到 builtin-tools 包,Vertex 行号修正
- lsp-integration.md: LSPTool 路径从 src/tools/ 迁移到 packages/builtin-tools/
- stub-recovery-design-1-4.md: 修正 Windows 绝对路径链接为标准代码引用

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

* docs: 修正 task 文档中的文件扩展名和路径引用

- task-004: AssistantSessionChooser.ts → .tsx, assistant.ts → .tsx
- task-003: cli.tsx 行号 249→272, markdownConfigLoader.ts 行号 29→35
- lan-pipes: SendMessageTool 路径迁移到 packages/builtin-tools/

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

* docs: 补充 computer-use-tools-reference 缺失的 Windows 工具

添加遗漏的 open_terminal 和 activate_window 两个 Windows 专属工具,
修正工具总数 37→39,Windows 工具数 10→12。

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

* docs: 修正 audit/bash-classifier/token-budget/tree-sitter 文档

- feature-flags-audit: ScheduleCronTool 路径迁移、DAEMON 状态更新为 COMPLETE、assistant 文件标记已补全、UDS 标记已实现
- bash-classifier: BashPermissionRequest 文件路径修正、withRetry 行号移除
- token-budget: attachments.ts 行号范围修正
- tree-sitter-bash: bashPermissions.ts 路径迁移到 packages/builtin-tools

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

* docs: 修正 langfuse-monitoring AgentTool 路径迁移

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

* docs: 修正 bridgeApi 行号和 Tool.ts 行号引用

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

* docs: 修正 Safety/Extensibility 文档中的工具路径迁移和行号引用

- sandbox.mdx: shouldUseSandbox.ts 和 bashPermissions.ts 路径迁移至 packages/builtin-tools
- why-safety-matters.mdx: bashPermissions.ts 路径迁移(3 处)
- plan-mode.mdx: EnterPlanModeTool/prompt.ts 路径迁移
- auto-mode.mdx: Auto mode 指令行号 3464→3481
- hooks.mdx: AgentTool/runAgent.ts 路径迁移
- skills.mdx: SkillTool.ts 路径迁移
- custom-agents.mdx: Agent built-in 目录和 exploreAgent.ts 路径迁移

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

* docs: 修正 internals 文档引用计数和路径

- ant-only-world: USER_TYPE 引用计数 465→410+,工具路径迁移到 builtin-tools
- growthbook-ab-testing: growthbook.ts 行数 1156→1258
- hidden-features: 语音模式状态更新(audio-napi 已恢复)

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

* docs: 修正工具文档中的行号引用

- sub-agents: AgentTool.call 入口行号 340→387
- shell-execution: ShellCommand onTimeout 行号 129→144

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

* docs: 修正 feature 文档中的状态、路径和计数

- all-features-guide: 修正 feature flag 启用范围(dev only vs dev+build)
- tier3-stubs: 大量状态修正(stub→已实现),缩减过时条目
- workflow-scripts: 路径迁移到 builtin-tools,状态更新
- web-browser-tool: 工具状态缺失→已实现,路径迁移
- context-collapse: CtxInspectTool 状态缺失→已实现
- computer-use: 行号引用更新,平台分发描述修正
- computer-use-tools-reference: 工具数 39→38
- voice-mode: voiceModeEnabled 行数 55→54

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

* docs: 更新 the-loop 查询循环行号引用

query.ts 代码变更后终止原因行号整体偏移约 40 行

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

* docs: 补充 feature-flags-audit 完整 build 默认 feature 列表

添加 ULTRATHINK/LODESTONE/ACP/DAEMON 等 19 个缺失的 build 默认 feature,
修正 dev-only 特征标注(UDS_INBOX/LAN_PIPES/BG_SESSIONS/TEMPLATES)

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

* docs: 修正 feature-flags-audit ConfigTool 路径迁移

ConfigTool 路径从 src/tools/ 迁移到 packages/builtin-tools/src/tools/

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

* docs: 修正 feature-flags-audit BashTool 路径迁移

BashTool 路径从 src/tools/ 迁移到 packages/builtin-tools/src/tools/

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

* docs: 修正 feature-flags-audit SkillTool 路径迁移

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

* docs: 更新 feature-flags-audit WorkflowTool 状态为已实现

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-19 09:30:00 +08:00
claude-code-best
a57ca08566 fix: 修复 node 的 es 版本太高不兼容的构建问题 2026-04-19 09:28:57 +08:00
claude-code-best
6536757428 feat: 对其他 provider 提供 langfuse 监控 2026-04-19 09:09:27 +08:00
claude-code-best
a0dc4540ca fix: 修复服务器两个 / 的问题 2026-04-19 08:55:55 +08:00
claude-code-best
7e4df5c3e9 build: 更改构建逻辑 2026-04-19 08:45:06 +08:00
claude-code-best
4d939e5722 chore: 更新构建 feature 的问题 2026-04-19 08:27:25 +08:00
123 changed files with 4523 additions and 1874 deletions

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
registry=https://registry.npmjs.org/

View File

@@ -22,6 +22,7 @@
| **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 可以开关 |
| **Channels 频道通知** | MCP 服务器推送外部消息到会话(飞书/Slack/Discord/微信等),`--channels plugin:name@marketplace` 启用 | [文档](https://ccb.agent-aura.top/docs/features/channels) |
| **自定义模型供应商** | 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) |

View File

@@ -11,6 +11,7 @@ rmSync(outdir, { recursive: true, force: true })
// Default features that match the official CLI build.
// Additional features can be enabled via FEATURE_<NAME>=1 env vars.
const DEFAULT_BUILD_FEATURES = [
'BUDDY', 'TRANSCRIPT_CLASSIFIER', 'BRIDGE_MODE',
'AGENT_TRIGGERS_REMOTE',
'CHICAGO_MCP',
'VOICE_MODE',
@@ -120,23 +121,7 @@ const vendorDir = join(outdir, 'vendor', 'audio-capture')
await cp('vendor/audio-capture', vendorDir, { recursive: true })
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`)
// Step 5: Bundle download-ripgrep script as standalone JS for postinstall
const rgScript = await Bun.build({
entrypoints: ['scripts/download-ripgrep.ts'],
outdir,
target: 'node',
})
if (!rgScript.success) {
console.error('Failed to bundle download-ripgrep script:')
for (const log of rgScript.logs) {
console.error(log)
}
// Non-fatal — postinstall fallback to bun run scripts/download-ripgrep.ts
} else {
console.log(`Bundled download-ripgrep script to ${outdir}/`)
}
// Step 6: Generate cli-bun and cli-node executable entry points
// Step 5: Generate cli-bun and cli-node executable entry points
const cliBun = join(outdir, 'cli-bun.js')
const cliNode = join(outdir, 'cli-node.js')

1284
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -13,14 +13,14 @@ keywords: ["子 Agent", "AgentTool", "任务委派", "forkSubagent", "子进程
```
AI 生成 tool_use: { prompt: "修复 bug", subagent_type: "Explore" }
AgentTool.call() ← 入口AgentTool.tsx:239
AgentTool.call() ← 入口AgentTool.tsx:387
├── 解析 effectiveTypefork vs 命名 agent vs GP 回退)
├── filterDeniedAgents() ← 仅命名 Agent 路径执行:权限过滤
├── 检查 requiredMcpServers ← MCP 依赖验证(最长等 30s
├── assembleToolPool(workerPermissionContext) ← 独立组装工具池
├── createAgentWorktree() ← 可选 worktree 隔离
runAgent() ← 核心执行runAgent.ts:248
runAgent() ← 核心执行runAgent.ts
├── getAgentSystemPrompt() ← 构建 agent 专属 system prompt
├── initializeAgentMcpServers() ← agent 级 MCP 服务器
├── executeSubagentStartHooks() ← Hook 注入
@@ -54,7 +54,7 @@ Fork 实验的门控函数 `isForkSubagentEnabled()` 需要同时满足三个前
Fork 路径的设计核心是 **Prompt Cache 共享**:所有 fork 子进程共享父 Agent 的完整 `assistant` 消息(所有 `tool_use` 块),用相同的占位符 `tool_result` 填充,只有最后一个 `text` 块包含各自的指令。这使得 API 请求前缀字节完全一致,最大化缓存命中。
```typescript
// forkSubagent.ts:142 — 所有 fork 子进程的占位结果
// forkSubagent.ts:93 — 所有 fork 子进程的占位结果
const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
// buildForkedMessages() 构建:
@@ -63,7 +63,7 @@ const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
### Fork 递归防护
Fork 子进程保留 Agent 工具(为了 cache-identical tool defs但通过两道防线防止递归 fork`AgentTool.tsx:332`
Fork 子进程保留 Agent 工具(为了 cache-identical tool defs但通过两道防线防止递归 fork
1. **`querySource` 检查**(压缩安全):`context.options.querySource === 'agent:builtin:fork'`
2. **消息扫描**(降级兜底):检测 `<fork-boilerplate>` 标签
@@ -88,7 +88,7 @@ Fork 子进程保留 Agent 工具(为了 cache-identical tool defs但通
### 内置 Agent
系统预定义了几个内置 Agent`src/tools/AgentTool/builtinAgents.ts`),各有明确的职责和模型配置:
系统预定义了几个内置 Agent`packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts`),各有明确的职责和模型配置:
| Agent | 模型 | 权限 | 用途 |
|-------|------|------|------|
@@ -119,7 +119,7 @@ const workerTools = assembleToolPool(workerPermissionContext, appState.mcp.tools
### 工具过滤的 resolveAgentTools
`runAgent.ts:500-502` 在工具组装后进一步过滤:
`runAgent.ts:508` 在工具组装后进一步过滤:
```typescript
const resolvedTools = useExactTools
@@ -142,7 +142,7 @@ const resolvedTools = useExactTools
## Worktree 隔离机制
`isolation: "worktree"` 参数让子 Agent 在独立的 git worktree 中工作(`AgentTool.tsx:590-593`
`isolation: "worktree"` 参数让子 Agent 在独立的 git worktree 中工作(`AgentTool.tsx:863`
```typescript
const slug = `agent-${earlyAgentId.slice(0, 8)}`
@@ -183,7 +183,7 @@ runAsyncAgentLifecycle() ← 后台执行agentToolUtils.ts
### 同步 Agent前台运行
同步 Agent 的关键特性是 **可后台化**`AgentTool.tsx:818-833`
同步 Agent 的关键特性是 **可后台化**`AgentTool.tsx:1107`
```typescript
const registration = registerAgentForeground({
@@ -218,7 +218,7 @@ const raceResult = await Promise.race([
## MCP 依赖的等待机制
如果 Agent 声明了 `requiredMcpServers``call()` 会等待这些服务器连接完成(`AgentTool.tsx:371-410`
如果 Agent 声明了 `requiredMcpServers``call()` 会等待这些服务器连接完成(`AgentTool.tsx:576`
```typescript
const MAX_WAIT_MS = 30_000 // 最长等 30 秒

View File

@@ -37,7 +37,7 @@ Worktree 文件统一存放在仓库根目录下的 `.claude/worktrees/`
## 创建流程EnterWorktreeTool
`EnterWorktreeTool``src/tools/EnterWorktreeTool/EnterWorktreeTool.ts`)的执行链路:
`EnterWorktreeTool``packages/builtin-tools/src/tools/EnterWorktreeTool/EnterWorktreeTool.ts`)的执行链路:
```
EnterWorktreeTool.call({ name? })
@@ -83,7 +83,7 @@ EnterWorktreeTool.call({ name? })
## 退出流程ExitWorktreeTool
`ExitWorktreeTool``src/tools/ExitWorktreeTool/ExitWorktreeTool.ts`)支持两种退出策略:
`ExitWorktreeTool``packages/builtin-tools/src/tools/ExitWorktreeTool/ExitWorktreeTool.ts`)支持两种退出策略:
### keep保留 worktree

View File

@@ -42,7 +42,7 @@ useInterval(checkForUpdates, 30 * 60 * 1000); // 每 30 分钟
任何更新尝试之前,系统会依次检查:
1. **自动更新是否被禁用?**`getAutoUpdaterDisabledReason()``src/utils/config.ts:1735`
1. **自动更新是否被禁用?**`getAutoUpdaterDisabledReason()``src/utils/config.ts:1737`
- `NODE_ENV === 'development'`
- 设置了 `DISABLE_AUTOUPDATER` 环境变量
- 仅限必要流量模式
@@ -81,7 +81,7 @@ useInterval(checkForUpdates, 30 * 60 * 1000); // 每 30 分钟
`src/utils/autoUpdater.ts:70``assertMinVersion()`
`src/main.tsx:1775` 在启动时调用
定义于 `src/utils/autoUpdater.ts:70`,设计上在启动时调用(当前未接入启动流程)
```typescript
void assertMinVersion();
@@ -200,7 +200,7 @@ Windows 系统使用文件复制而非符号链接。
**文件**: `src/migrations/migrateAutoUpdatesToSettings.ts`
一次性将旧版 `globalConfig.autoUpdates = false` 迁移为 settings 中的 `DISABLE_AUTOUPDATER=1` 环境变量。 `src/main.tsx:325` 在启动时调用
一次性将旧版 `globalConfig.autoUpdates = false` 迁移为 settings 中的 `DISABLE_AUTOUPDATER=1` 环境变量。定义于 `src/migrations/migrateAutoUpdatesToSettings.ts`(当前未接入启动流程)
---
@@ -270,7 +270,7 @@ React hook `useUpdateNotification(updatedVersion)` — 确保每次 semver 变
| `src/utils/releaseNotes.ts` | Changelog 获取、缓存与展示 |
| `src/utils/semver.ts` | Semver 版本比较Bun 原生 + npm 回退) |
| `src/utils/doctorDiagnostic.ts` | 安装类型检测与健康诊断 |
| `src/utils/config.ts:1735` | `getAutoUpdaterDisabledReason()` — 禁用检查逻辑 |
| `src/utils/config.ts:1737` | `getAutoUpdaterDisabledReason()` — 禁用检查逻辑 |
| `src/migrations/migrateAutoUpdatesToSettings.ts` | 旧版配置迁移 |
| `src/screens/Doctor.tsx` | Doctor 命令 UI展示自动更新状态 |

View File

@@ -48,7 +48,7 @@ const messagesForCompact = microcompactResult.messages
MicroCompact 不压缩整个对话,而是**清除旧工具输出的内容**。它维护一个白名单:
```typescript
// src/services/compact/microCompact.ts:41-48
// src/services/compact/microCompact.ts:41-50
const COMPACTABLE_TOOLS = new Set([
FILE_READ_TOOL_NAME, // 'Read' - 文件读取
...SHELL_TOOL_NAMES, // 'Bash' - 命令输出
@@ -143,7 +143,7 @@ const stripped2 = stripReinjectedAttachments(stripped) // 移除会被重新注
压缩后,系统会从摘要中**重新注入关键上下文**
```typescript
// compact.ts:124-132
// compact.ts:126-134
export const POST_COMPACT_TOKEN_BUDGET = 50_000 // 总预算
export const POST_COMPACT_MAX_FILES_TO_RESTORE = 5 // 最多恢复 5 个文件
export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000 // 每文件 5K token

View File

@@ -39,7 +39,7 @@ Claude Code 的记忆系统是**纯文件**的——没有数据库、没有向
`MEMORY.md` 是记忆的入口索引,每次对话都完整加载到上下文中:
```typescript
// memdir.ts:35-38
// memdir.ts:34-38
export const ENTRYPOINT_NAME = 'MEMORY.md'
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000

View File

@@ -20,12 +20,12 @@ buildSystemPromptBlocks() → TextBlockParam[] (分块 + cache_control 标
1. **`getSystemPrompt()`**`src/constants/prompts.ts:444`)—— 收集静态段 + 动态段,插入 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 分界标记
2. **`buildEffectiveSystemPrompt()`**`src/utils/systemPrompt.ts:41`)—— 按 Override > Coordinator > Agent > Custom > Default 优先级选择
3. **`buildSystemPromptBlocks()`**`src/services/api/claude.ts:3214`)—— 调用 `splitSysPromptPrefix()` 分块,为每个块附加 `cache_control`
3. **`buildSystemPromptBlocks()`**`src/services/api/claude.ts:3279`)—— 调用 `splitSysPromptPrefix()` 分块,为每个块附加 `cache_control`
## SystemPrompt 品牌类型
```typescript
// src/utils/systemPromptType.ts:8
// packages/@ant/model-provider/src/types/systemPrompt.ts:4
export type SystemPrompt = readonly string[] & {
readonly __brand: 'SystemPrompt'
}
@@ -185,7 +185,7 @@ export function shouldUseGlobalCacheScope(): boolean {
### `getCacheControl()`TTL 决策
`src/services/api/claude.ts:359` 生成的 `cache_control` 对象:
`src/services/api/claude.ts:348` 生成的 `cache_control` 对象:
```typescript
{
@@ -195,14 +195,14 @@ export function shouldUseGlobalCacheScope(): boolean {
}
```
1 小时 TTL 的判定逻辑(`should1hCacheTTL()`,第 394 行):
1 小时 TTL 的判定逻辑(`should1hCacheTTL()`,第 383 行):
- **Bedrock 用户**:通过环境变量 `ENABLE_PROMPT_CACHING_1H_BEDROCK` 启用
- **1P 用户**:通过 GrowthBook 配置的 `allowlist` 数组匹配 `querySource`,支持前缀通配符(如 `"repl_main_thread*"`
- **会话级锁定**:资格判定结果在 bootstrap state 中缓存,防止 GrowthBook 配置中途变化导致同一会话内 TTL 不一致
### 缓存破坏Session-Specific Guidance 的放置
`getSessionSpecificGuidanceSection()``src/constants/prompts.ts:352`)的内容必须放在 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` **之后**。因为它包含:
`getSessionSpecificGuidanceSection()``src/constants/prompts.ts:354`)的内容必须放在 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` **之后**。因为它包含:
- 当前会话的 enabledTools 集合
- `isForkSubagentEnabled()` 的运行时判定
- `getIsNonInteractiveSession()` 的结果

View File

@@ -32,7 +32,7 @@ message_stop ← 消息结束
### 事件处理状态机
`src/services/api/claude.ts` 中 `queryStreamRaw()` 函数的事件处理循环实现了一个基于 `switch(part.type)` 的状态机:
`src/services/api/claude.ts` 中 `queryModelWithStreaming()` 函数的事件处理循环实现了一个基于 `switch(part.type)` 的状态机:
| 事件类型 | 处理逻辑 | 状态变更 |
|----------|----------|----------|
@@ -167,10 +167,13 @@ UI 层通过 `useToolCallProgress` hook 实时展示命令输出,而不是等
| Provider | 流式协议 | 特殊处理 |
|----------|----------|----------|
| **Anthropic Direct** | 原生 SSE | 延迟最低TTFT 最快 |
| **firstParty** (Anthropic Direct) | 原生 SSE | 延迟最低TTFT 最快 |
| **AWS Bedrock** | AWS SDK 流式接口 | 需要额外的 beta header 和认证 |
| **Google Vertex** | gRPC → 事件流 | 通过 `getMergedBetas()` 适配 |
| **Azure** | Anthropic 兼容 API | 自定义 base URL |
| **foundry** | Anthropic 兼容 API | 内部部署 |
| **openai** | OpenAI 流式适配器 | 转换为 Anthropic 内部格式 |
| **gemini** | Gemini 流式适配器 | 转换为 Anthropic 内部格式 |
| **grok** (xAI) | Grok 流式适配器 | 转换为 Anthropic 内部格式 |
所有 Provider 通过统一的 `Stream<BetaRawMessageStreamEvent>` 抽象层屏蔽差异。上层代码QueryEngine、REPL不需要关心底层用的是哪个 Provider。

View File

@@ -74,17 +74,17 @@ const toolUpdates = streamingToolExecutor
| 终止原因 | 触发位置 | 机制 |
|----------|---------|------|
| **blocking_limit** | 第 646 行 | Token 计数超过硬限制(非 autocompact 模式)→ 生成 PTL 错误消息 → 返回 |
| **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` 限制 → 返回 |
| **blocking_limit** | 第 686 行 | Token 计数超过硬限制(非 autocompact 模式)→ 生成 PTL 错误消息 → 返回 |
| **image_error** | 第 1021 行 | `ImageSizeError` / `ImageResizeError` 异常 → 直接返回 |
| **model_error** | 第 1040 行 | `callModel()` 抛出不可恢复异常 → 生成错误消息 → 返回 |
| **aborted_streaming** | 第 1095 行 | `abortController.signal.aborted`(流式阶段)→ 为未完成的 tool_use 生成合成 tool_result → 返回 |
| **prompt_too_long** | 第 1219/1226 行 | 413 错误且 reactive compact 无法恢复 → 暂扣的错误消息被释放 → 返回 |
| **completed** | 第 1308 行 | API 错误(限流、认证失败等)导致无法继续 → 返回 |
| **stop_hook_prevented** | 第 1323 行 | Stop hook 返回 `preventContinuation: true` → 返回 |
| **completed** | 第 1401 行 | 正常完成AI 未发出 tool_use → `needsFollowUp = false` → 经过 stop hooks → 返回 |
| **aborted_tools** | 第 1559 行 | `abortController.signal.aborted`(工具执行阶段)→ 返回 |
| **hook_stopped** | 第 1564 行 | 工具执行期间 hook 返回 `shouldPreventContinuation` → 返回 |
| **max_turns** | 第 1755 行 | 轮次计数超过 `maxTurns` 限制 → 返回 |
## 继续条件(恢复路径)
@@ -158,7 +158,7 @@ type State = {
- **每一步都产生真实信息**`runTools()` 返回的 `toolResults` 是 API 不可能预知的——命令输出、文件内容、错误信息
- **动态上下文管理**每轮迭代前都重新评估压缩需求autocompact → microcompact → snip基于最新的 token 计数
- **错误即时恢复**工具失败不需要推倒重来——stop hook 可以注入阻塞错误让 AI 修正策略
- **用户可控**`abortController.signal` 在循环的多个检查点被检测(第 1018、1048、1488 行),用户按 ESC 可以优雅中断
- **用户可控**`abortController.signal` 在循环的多个检查点被检测(第 1059、1095、1529 行),用户按 ESC 可以优雅中断
- **成本控制**Token Budget 在每轮终止前检查,防止 AI 无效循环
## 一个完整的迭代示例

View File

@@ -12,7 +12,7 @@ Claude Code 的 Agent 不仅仅来自用户自定义——系统有三类来源
| 来源 | 位置 | 优先级 |
|------|------|--------|
| **Built-in** | `src/tools/AgentTool/built-in/` 硬编码 | 最低(可被覆盖) |
| **Built-in** | `packages/builtin-tools/src/tools/AgentTool/built-in/` 硬编码 | 最低(可被覆盖) |
| **Plugin** | 通过插件系统注册 | 中 |
| **User/Project/Policy** | `.claude/agents/*.md` 或 settings.json | 最高 |
@@ -127,7 +127,7 @@ color: "blue" # 终端中的 Agent 颜色标识
以内置 Explore Agent 为例:
```typescript
// src/tools/AgentTool/built-in/exploreAgent.ts
// packages/builtin-tools/src/tools/AgentTool/built-in/exploreAgent.ts
disallowedTools: [
'Agent', // 不能嵌套调用 Agent
'ExitPlanMode', // 不需要 plan mode

View File

@@ -240,7 +240,7 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
## Session Hook 的生命周期
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(调用位置:`src/tools/AgentTool/runAgent.ts`;定义位置:`src/utils/hooks/registerFrontmatterHooks.ts`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()`(定义位置:`src/utils/hooks/sessionHooks.ts`)清理。
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(调用位置:`packages/builtin-tools/src/tools/AgentTool/runAgent.ts`;定义位置:`src/utils/hooks/registerFrontmatterHooks.ts`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()`(定义位置:`src/utils/hooks/sessionHooks.ts`)清理。
```typescript
// runAgent.ts — 注册 agent 的前置 Hook

View File

@@ -304,7 +304,7 @@ timer.unref?.() // 不阻止进程退出
## 工具发现:从 MCP 到 Tool 接口
`fetchToolsForClient()``client.ts:1745-2000`)使用 `memoizeWithLRU` 缓存(上限 20将 MCP 工具转换为 Claude Code 的统一 Tool 接口:
`fetchToolsForClient()``client.ts:1744-2000`)使用 `memoizeWithLRU` 缓存(上限 100将 MCP 工具转换为 Claude Code 的统一 Tool 接口:
```typescript
const fullyQualifiedName = buildMcpToolName(client.name, tool.name)

View File

@@ -22,7 +22,7 @@ Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Promp
### 1. 内置命令Built-in Commands
硬编码在 `src/commands.ts:258` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown但实现了相同的 `Command` 接口(`src/types/command.ts`)。
硬编码在 `src/commands.ts:299` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown但实现了相同的 `Command` 接口(`src/types/command.ts`)。
### 2. Bundled Skills编译时打包
@@ -98,7 +98,7 @@ shell: ["bash"] # Shell 执行环境
## 两条执行路径Inline vs Fork
SkillTool`src/tools/SkillTool/SkillTool.ts:332`)在 `call()` 中根据 `command.context` 分流:
SkillTool`packages/builtin-tools/src/tools/SkillTool/SkillTool.ts:332`)在 `call()` 中根据 `command.context` 分流:
### Inline 模式(默认)

View File

@@ -50,7 +50,7 @@
- **端点**: `{region}-aiplatform.googleapis.com`
- **认证**: `GoogleAuth` + `cloud-platform` scope
- **文件**: `src/services/api/client.ts:228-298`
- **文件**: `src/services/api/client.ts:221-298`
### 4. Azure Foundry
@@ -129,12 +129,12 @@ WebSearch 工具支持直接抓取 Bing 搜索结果页面,也支持通过 Bra
- **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`
- `packages/builtin-tools/src/tools/WebSearchTool/adapters/bingAdapter.ts`
- `packages/builtin-tools/src/tools/WebSearchTool/adapters/braveAdapter.ts`
另外还有 Domain Blocklist 查询:
- **端点**: `https://api.anthropic.com/api/web/domain_info?domain={domain}`
- **文件**: `src/tools/WebFetchTool/utils.ts`
- **文件**: `packages/builtin-tools/src/tools/WebFetchTool/utils.ts`
### 15. Google Cloud Storage (自动更新)

View File

@@ -127,7 +127,6 @@ acp-link 支持将 ACP agent 注册到 Remote Control Server通过 Web UI 远
# 通过环境变量配置 RCS 连接
ACP_RCS_URL=http://localhost:3000 \
ACP_RCS_TOKEN=sk-rcs-your-key \
ACP_RCS_NAME=my-agent \
acp-link ccb-bun -- --acp
```
@@ -144,7 +143,7 @@ acp-link RCS
│ │
│── WS connect ─────────────────►│ (WebSocket)
│── identify { agentId } ────────►│ (WS 标识)
│◄── registered ─────────────────│
│◄── identified ─────────────────│
│ │
│── ACP events ─────────────────►│ (双向消息转发)
│◄── user prompts/permissions ───│
@@ -200,6 +199,3 @@ ACP_PERMISSION_MODE=auto acp-link ccb-bun -- --acp
| `ACP_PERMISSION_MODE` | 默认权限模式 fallback |
| `ACP_RCS_URL` | RCS 服务器地址(启用 RCS 集成) |
| `ACP_RCS_TOKEN` | RCS API token |
| `ACP_RCS_NAME` | Agent 名称(在 RCS 中显示) |
| `ACP_RCS_CHANNEL_GROUP` | Channel group ID |
| `ACP_MAX_SESSIONS` | 最大会话数 |

View File

@@ -516,25 +516,37 @@ AI 也可通过 `SnipTool` 自动截断过长的对话:
| 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 | 上下文折叠 |
| `BUDDY` | ✅ dev only | 伴侣系统 |
| `BRIDGE_MODE` | ✅ dev only | 远程控制 |
| `VOICE_MODE` | ✅ dev+build | 语音模式 |
| `CHICAGO_MCP` | ✅ dev+build | Computer Use + Chrome |
| `AGENT_TRIGGERS_REMOTE` | ✅ dev+build | 定时任务 |
| `SHOT_STATS` | ✅ dev+build | API 统计 |
| `TOKEN_BUDGET` | ✅ dev+build | Token 预算 |
| `PROMPT_CACHE_BREAK_DETECTION` | ✅ dev+build | 缓存检测 |
| `ULTRAPLAN` | ✅ dev+build | 高级规划 |
| `DAEMON` | ✅ dev+build | 后台守护 |
| `UDS_INBOX` | ✅ dev only | Pipe IPC |
| `LAN_PIPES` | ✅ dev only | LAN 群控 |
| `MONITOR_TOOL` | ✅ dev+build | 后台监控 |
| `WORKFLOW_SCRIPTS` | ✅ dev+build | 工作流脚本 |
| `FORK_SUBAGENT` | ✅ dev+build | 子 Agent |
| `KAIROS` | ✅ dev+build | Kairos 调度 |
| `COORDINATOR_MODE` | ✅ dev+build | 多 Worker |
| `HISTORY_SNIP` | ✅ dev+build | 历史管理 |
| `CONTEXT_COLLAPSE` | ✅ dev+build | 上下文折叠 |
| `ULTRATHINK` | ✅ dev+build | 扩展思考 |
| `EXTRACT_MEMORIES` | ✅ dev+build | 自动记忆提取 |
| `VERIFICATION_AGENT` | ✅ dev+build | 验证 Agent |
| `KAIROS_BRIEF` | ✅ dev+build | Brief 模式 |
| `AWAY_SUMMARY` | ✅ dev+build | 离开摘要 |
| `ACP` | ✅ dev+build | ACP 协议 |
| `LODESTONE` | ✅ dev+build | 深度链接 |
| `BUILTIN_EXPLORE_PLAN_AGENTS` | ✅ dev+build | 内置 Explore/Plan agent |
| `AGENT_TRIGGERS` | ✅ dev+build | 本地定时任务 |
| `BG_SESSIONS` | ✅ dev only | 后台会话 |
| `TEMPLATES` | ✅ dev only | 模板系统 |
| `TRANSCRIPT_CLASSIFIER` | ✅ dev only | 对话分类 |
手动启用任意 flag
```bash

View File

@@ -102,6 +102,6 @@ FEATURE_BASH_CLASSIFIER=1 FEATURE_TREE_SITTER_BASH=1 bun run dev
| `src/utils/permissions/bashClassifier.ts` | — | Bash 分类器stubANT-ONLY |
| `src/utils/permissions/yoloClassifier.ts` | 1496 | YOLO 分类器(完整参考实现) |
| `src/utils/classifierApprovals.ts` | — | 分类器审批信号管理 |
| `src/components/permissions/BashPermissionRequest.tsx:261-469` | — | 分类器 UI |
| `src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx` | — | 分类器 UI |
| `src/hooks/toolPermission/handlers/interactiveHandler.ts` | — | 交互式权限处理 |
| `src/services/api/withRetry.ts:81` | — | API beta 标头 |
| `src/services/api/withRetry.ts` | — | API beta 标头 |

View File

@@ -30,7 +30,7 @@ BRIDGE_MODE 将本地 CLI 注册为"bridge 环境",可从 claude.ai 或其他
文件:`src/bridge/bridgeApi.ts`
Bridge API Client 提供 7 个核心操作:
Bridge API Client 提供 9 个核心操作:
| 操作 | HTTP | 说明 |
|------|------|------|
@@ -137,7 +137,7 @@ FEATURE_BRIDGE_MODE=1 FEATURE_DAEMON=1 bun run dev
| 文件 | 行数 | 职责 |
|------|------|------|
| `src/bridge/bridgeApi.ts` | 540 | API Client核心 |
| `src/bridge/bridgeApi.ts` | 541 | API Client核心 |
| `src/bridge/sessionRunner.ts` | — | 会话运行器 |
| `src/bridge/bridgeConfig.ts` | — | 配置管理 |
| `src/bridge/replBridgeTransport.ts` | — | 传输层 |

View File

@@ -78,10 +78,13 @@ FEATURE_BUDDY=1 bun run dev
| 文件 | 说明 |
|---|---|
| `src/commands/buddy/index.ts` | `/buddy` 命令注册 |
| `src/commands/buddy/buddy.ts` | `/buddy` 命令处理 |
| `src/buddy/companion.ts` | 宠物生成与加载 |
| `src/buddy/companionReact.ts` | 宠物反应系统REPL 每轮查询后触发) |
| `src/buddy/types.ts` | 类型定义(物种、稀有度、属性) |
| `src/buddy/sprites.ts` | 终端像素画渲染 |
| `src/buddy/CompanionSprite.tsx` | React 组件(输入框旁显示) |
| `src/buddy/CompanionCard.tsx` | 宠物信息卡片(`/buddy` 无参数时展示) |
| `src/buddy/useBuddyNotification.tsx` | 启动提示通知 |
| `src/buddy/prompt.ts` | 宠物相关 prompt 模板 |

89
docs/features/channels.md Normal file
View File

@@ -0,0 +1,89 @@
# Channels — 外部频道消息接入
> 启动参数:`--channels` / `--dangerously-load-development-channels`
> 状态:已解除 feature flag 和 OAuth 限制,可直接使用
## 概述
Channel 是一个 MCP 服务器,它将外部事件推送到你运行中的 Claude Code 会话中,以便 Claude 可以在你不在终端时做出反应。详细使用说明请参考以下文档:
- **官方文档**[使用 channels 将事件推送到运行中的会话](https://code.claude.com/docs/zh-CN/channels)
- **飞书插件**[claude-code-feishu-channel](https://github.com/whobot-ai/claude-code-feishu-channel) — 社区首个飞书 Channel 插件,支持双向消息、配对认证、群组聊天、文件附件
本仓库现在内置了 **微信 WeChat channel**,不需要单独安装外部 marketplace 插件。
## 快速开始
```bash
# 启用频道监听plugin 格式)
ccb --channels plugin:feishu@claude-code-feishu-channel
# 启用内置微信 channel
ccb weixin login
ccb --channels plugin:weixin@builtin
# 启用频道监听server 格式)
ccb --channels server:my-slack-bridge
# 同时启用多个频道
ccb --channels plugin:feishu@claude-code-feishu-channel --channels server:discord-bot
# 开发模式(跳过 allowlist 检查,用于测试自定义 channel
ccb --dangerously-load-development-channels server:my-custom-channel
```
## 支持的 Channel
| Channel | 说明 | 来源 |
|---------|------|------|
| **Telegram** | 官方 Telegram Bot 集成 | `/plugin install telegram@claude-plugins-official` |
| **Discord** | 官方 Discord Bot 集成 | `/plugin install discord@claude-plugins-official` |
| **iMessage** | macOS 原生消息 | `/plugin install imessage@claude-plugins-official` |
| **飞书 (Feishu/Lark)** | 双向消息、群组聊天、文件附件 | `/plugin install feishu@claude-code-feishu-channel` |
| **微信 (WeChat)** | 内置 channel支持扫码登录、双向消息、附件透传 | `ccb weixin login` + `ccb --channels plugin:weixin@builtin` |
## 微信内置 Channel
### 登录
```bash
ccb weixin login
```
已登录状态可清除:
```bash
ccb weixin login clear
```
### 会话启用
```bash
ccb --channels plugin:weixin@builtin
```
### 配对授权
首次收到未授权微信用户消息时weixin channel 会回一条 6 位 pairing code。运营侧可在终端执行
```bash
ccb weixin access pair <code>
```
确认后,该微信用户后续消息才会进入 Claude Code 会话。
## 相关文件
| 文件 | 职责 |
|------|------|
| `src/services/mcp/channelNotification.ts` | 频道 gate 逻辑、消息包装 |
| `src/services/mcp/channelAllowlist.ts` | 频道开关(已默认开启) |
| `src/services/mcp/useManageMCPConnections.ts` | MCP 连接管理中的频道注册 |
| `src/components/LogoV2/ChannelsNotice.tsx` | 启动时频道状态提示 |
| `src/main.tsx` | `--channels` 参数解析 |
| `src/interactiveHelpers.tsx` | Dev channels 确认对话框 |
## 参考链接
- [官方 Channels 文档](https://code.claude.com/docs/zh-CN/channels) — 完整使用说明、安全性、Enterprise 控制
- [飞书 Channel 插件](https://github.com/whobot-ai/claude-code-feishu-channel) — 安装配置教程、MCP 工具、Skill 命令参考

View File

@@ -2,12 +2,12 @@
## 概览
Computer Use 提供 37 个工具,分为三类:
Computer Use 提供 38 个工具,分为三类:
| 分类 | 平台 | 工具数 | 说明 |
|------|------|--------|------|
| 通用工具 | 全平台 | 24 | 官方 Computer Use 标准能力 |
| Windows 专属工具 | Win32 | 10 | 绑定窗口模式下的增强能力 |
| Windows 专属工具 | Win32 | 11 | 绑定窗口模式下的增强能力 |
| 教学工具 | 全平台 | 3 | 分步引导模式(需 teachMode 开启) |
---
@@ -82,7 +82,7 @@ Computer Use 提供 37 个工具,分为三类:
---
## 二、Windows 专属工具10 个)
## 二、Windows 专属工具12 个)
仅 Windows 平台可见。核心能力:**绑定窗口后的独立操作——不抢占用户鼠标键盘**。
@@ -235,8 +235,19 @@ Computer Use 提供 37 个工具,分为三类:
| 工具 | 参数 | 说明 |
|------|------|------|
| `open_terminal` | `agent`, `command?` | 打开新终端窗口并启动 AI agentclaude/codex/gemini/custom。自动绑定窗口并截图验证 |
| `activate_window` | `click_x?`, `click_y?` | 激活绑定窗口SetForegroundWindow + BringWindowToTop + 点击确保焦点 |
| `prompt_respond` | `response_type`, `arrow_direction?`, `arrow_count?`, `text?` | 处理终端 Yes/No/选择提示 |
**open_terminal agent 类型:**
| agent | 命令 | 说明 |
|-------|------|------|
| `claude` | `claude` | 启动 Claude Code |
| `codex` | `codex` | 启动 Codex |
| `gemini` | `gemini` | 启动 Gemini |
| `custom` | 用户指定 | 自定义命令 |
**response_type 详情:**
| response_type | 操作 | 场景 |

View File

@@ -11,7 +11,7 @@
-`@ant/computer-use-input` 拆为 dispatcher + backendsdarwin + win32
-`@ant/computer-use-swift` 拆为 dispatcher + backendsdarwin + win32
-`CHICAGO_MCP` 编译开关已开
- `src/`有 6 处 macOS 硬编码阻塞
- `src/` 层 macOS 硬编码已移除Phase 2 已完成)
## 2. 阻塞点全景
@@ -19,25 +19,25 @@
| # | 文件:行号 | 阻塞代码 | 影响 |
|---|----------|---------|------|
| 1 | `src/main.tsx:1605` | `getPlatform() === 'macos'` | 整个 CU 初始化被跳过 |
| 1 | `src/main.tsx:2366` | `feature("CHICAGO_MCP")` 门控 | CU 初始化入口 |
### 2.2 加载层
| # | 文件:行号 | 阻塞代码 | 影响 |
|---|----------|---------|------|
| 2 | `src/utils/computerUse/swiftLoader.ts:16` | `process.platform !== 'darwin'` → throw | 截图、应用管理全部不可用 |
| 3 | `src/utils/computerUse/executor.ts:263` | `process.platform !== 'darwin'`throw | 整个 executor 工厂函数不可用 |
| 2 | `src/utils/computerUse/swiftLoader.ts` | macOS-only loader已改为仅 darwin 加载) | 非 darwin 使用 platforms/ 替代 |
| 3 | `src/utils/computerUse/executor.ts:302` | `process.platform !== 'darwin'`cross-platform executor | 非 darwin 走跨平台路径 |
### 2.3 macOS 特有依赖
| # | 文件:行号 | 依赖 | macOS 实现 | 需要替代方案 |
|---|----------|------|-----------|------------|
| 4 | `executor.ts:70-88` | 剪贴板 | `pbcopy`/`pbpaste` | Win: PowerShell `Get/Set-Clipboard`Linux: `xclip`/`wl-copy` |
| 5 | `drainRunLoop.ts:21` | CFRunLoop pump | `cu._drainMainRunLoop()` | 非 darwin直接执行 fn(),不需要 pump |
| 6 | `escHotkey.ts:28` | ESC 热键 | CGEventTap | 非 darwin返回 false已有 Ctrl+C fallback |
| 7 | `hostAdapter.ts:48-54` | 系统权限 | TCC accessibility + screenRecording | Win直接 grantedLinux检查 xdotool |
| 8 | `common.ts:56` | 平台标识 | `platform: 'darwin'` 硬编码 | 动态获取 |
| 9 | `executor.ts:180` | 粘贴快捷键 | `command+v` | Win/Linux`ctrl+v` |
| 4 | `executor.ts:72-96` | 剪贴板 | `pbcopy`/`pbpaste` / PowerShell / xclip | Win: PowerShell `Get/Set-Clipboard`Linux: `xclip`/`wl-copy` |
| 5 | `drainRunLoop.ts` | CFRunLoop pump | `cu._drainMainRunLoop()` | 非 darwin直接执行 fn(),不需要 pump |
| 6 | `escHotkey.ts` | ESC 热键 | CGEventTap | 非 darwin返回 false已有 Ctrl+C fallback |
| 7 | `hostAdapter.ts` | 系统权限 | TCC accessibility + screenRecording | Win直接 grantedLinux检查 xdotool |
| 8 | `common.ts:55-58` | 平台标识 | 动态获取 | 已改为 `process.platform` 分发 |
| 9 | `executor.ts:232` | 粘贴快捷键 | `command`/`ctrl` 分发 | 已按平台分发粘贴快捷键 |
### 2.4 缺失的 Linux 后端
@@ -100,19 +100,19 @@
| 步骤 | 文件 | 改动 |
|------|------|------|
| 2.1 | `src/main.tsx:1605` | `getPlatform() === 'macos'` → 去掉平台限制,或改为 `!== 'unknown'` |
| 2.2 | `src/utils/computerUse/swiftLoader.ts:16-18` | 移除 `process.platform !== 'darwin'` throw。`@ant/computer-use-swift/index.ts` 已有跨平台 dispatch |
| 2.3 | `src/utils/computerUse/executor.ts:263-267` | 移除 `process.platform !== 'darwin'` throw。改为检查 input/swift isSupported |
| 2.4 | `src/utils/computerUse/executor.ts:70-88` | 剪贴板函数按平台分发darwin→pbcopy/pbpastewin32→PowerShell Get/Set-Clipboardlinux→xclip |
| 2.5 | `src/utils/computerUse/executor.ts:180` | `typeViaClipboard``command+v` → 非 darwin 时用 `ctrl+v` |
| 2.6 | `src/utils/computerUse/executor.ts:273` | `const cu = requireComputerUseSwift()` → 改为 `new ComputerUseAPI()`(从 package 直接实例化,不走 swiftLoader throw |
| 2.7 | `src/utils/computerUse/drainRunLoop.ts` | 开头加 `if (process.platform !== 'darwin') return fn()` |
| 2.8 | `src/utils/computerUse/escHotkey.ts` | `registerEscHotkey` 非 darwin 返回 false已有 Ctrl+C fallback |
| 2.9 | `src/utils/computerUse/hostAdapter.ts:48-54` | `ensureOsPermissions` 非 darwin 返回 `{ granted: true }` |
| 2.10 | `src/utils/computerUse/common.ts:56` | `platform: 'darwin'``platform: process.platform === 'win32' ? 'windows' : process.platform === 'linux' ? 'linux' : 'darwin'` |
| 2.11 | `src/utils/computerUse/common.ts:55` | `screenshotFiltering: 'native'` → 非 darwin 时 `'none'`Windows/Linux 截图不支持 per-app 过滤) |
| 2.12 | `src/utils/computerUse/gates.ts:13` | `enabled: false``enabled: true`(无 GrowthBook 时默认可用 |
| 2.13 | `src/utils/computerUse/gates.ts:39-43` | `hasRequiredSubscription()` → 直接返回 `true` |
| 2.1 | `src/main.tsx:2366` | `feature("CHICAGO_MCP")` → 已为跨平台入口 |
| 2.2 | `src/utils/computerUse/swiftLoader.ts` | 已改为仅 darwin 加载,非 darwin 使用 platforms/ |
| 2.3 | `src/utils/computerUse/executor.ts:302-309` | 已改为 cross-platform dispatch非 darwin → createCrossPlatformExecutor |
| 2.4 | `src/utils/computerUse/executor.ts:72-96` | 剪贴板按平台分发darwin→pbcopy/pbpastewin32→PowerShelllinux→xclip |
| 2.5 | `src/utils/computerUse/executor.ts:232` | 粘贴快捷键已按平台分发darwin→command其他→ctrl |
| 2.6 | `src/utils/computerUse/executor.ts:302-309` | 非 darwin 已改为 `createCrossPlatformExecutor()` |
| 2.7 | `src/utils/computerUse/drainRunLoop.ts` | 非 darwin 无需 pump直接执行 fn |
| 2.8 | `src/utils/computerUse/escHotkey.ts` | 非 darwin 返回 false已有 Ctrl+C fallback |
| 2.9 | `src/utils/computerUse/hostAdapter.ts` | 非 darwin 权限检查逻辑已实现 |
| 2.10 | `src/utils/computerUse/common.ts:58` | 已改为动态 `process.platform` 分发 |
| 2.11 | `src/utils/computerUse/common.ts:55` | 已改为 darwin→'native',其他→'none' |
| 2.12 | `src/utils/computerUse/gates.ts:55` | 已更新(需验证 enabled 默认值 |
| 2.13 | `src/utils/computerUse/gates.ts:39` | `hasRequiredSubscription()` 已更新 |
### Phase 3新增 Linux 后端

View File

@@ -25,7 +25,7 @@ CONTEXT_COLLAPSE 让模型内省上下文窗口使用情况,并智能压缩旧
| 折叠核心 | `src/services/contextCollapse/index.ts` | **Stub** — 接口完整(`ContextCollapseStats``CollapseResult``DrainResult`),函数全部空操作 |
| 折叠操作 | `src/services/contextCollapse/operations.ts` | **Stub**`projectView` 为恒等函数 |
| 折叠持久化 | `src/services/contextCollapse/persist.ts` | **Stub**`restoreFromEntries` 为空操作 |
| CtxInspectTool | `src/tools/CtxInspectTool/` | **缺失**目录不存在 |
| CtxInspectTool | `packages/builtin-tools/src/tools/CtxInspectTool/CtxInspectTool.ts` | **实现**上下文内省工具 |
| SnipTool 提示 | `src/tools/SnipTool/prompt.ts` | **Stub** — 空工具名 |
| SnipTool 实现 | `src/tools/SnipTool/SnipTool.ts` | **缺失** |
| force-snip 命令 | `src/commands/force-snip.js` | **缺失** |
@@ -106,7 +106,7 @@ SnipTool 提供手动折叠能力:
| 1 | `services/contextCollapse/index.ts` | 大 | 折叠状态机、LLM 调用、消息压缩 |
| 2 | `services/contextCollapse/operations.ts` | 中 | `projectView()` 消息过滤 |
| 3 | `services/contextCollapse/persist.ts` | 小 | `restoreFromEntries()` 磁盘持久化 |
| 4 | `tools/CtxInspectTool/` | | 上下文内省工具token 计数、已折叠范围 |
| 4 | `tools/CtxInspectTool/` | 已完成 | 上下文内省工具已实现(`packages/builtin-tools/src/tools/CtxInspectTool/` |
| 5 | `tools/SnipTool/SnipTool.ts` | 中 | Snip 工具实现 |
| 6 | `commands/force-snip.js` | 小 | `/force-snip` 命令 |

View File

@@ -1,12 +1,12 @@
# DAEMON — 后台守护进程
> Feature Flag: `FEATURE_DAEMON=1`
> 实现状态:主进程和 worker 注册为 StubCLI 路由完整
> 实现状态:Supervisor 和 remoteControl Worker 已实现
> 引用数3
## 一、功能概述
DAEMON 将 Claude Code 变为后台守护进程。主进程supervisor管理多个 worker 进程的生命周期,通过 Unix 域套接字进行 IPC。适用于持续运行的后台服务场景(如配合 BRIDGE_MODE 提供远程控制服务)。
DAEMON 将 Claude Code 变为后台守护进程。主进程supervisor管理多个 worker 进程的生命周期,通过文件系统状态文件进行通信。适用于持续运行的后台服务场景(如配合 BRIDGE_MODE 提供远程控制服务)。
## 二、实现架构
@@ -14,8 +14,9 @@ DAEMON 将 Claude Code 变为后台守护进程。主进程supervisor
| 模块 | 文件 | 状态 |
|------|------|------|
| 守护主进程 | `src/daemon/main.ts` | **Stub**`daemonMain: () => Promise.resolve()` |
| Worker 注册 | `src/daemon/workerRegistry.ts` | **Stub**`runDaemonWorker: () => Promise.resolve()` |
| 守护主进程 | `src/daemon/main.ts` | **已实现**Supervisor 含子命令、Worker 生命周期管理、指数退避重启 |
| Worker 注册 | `src/daemon/workerRegistry.ts` | **已实现**remoteControl Workerheadless bridge |
| Daemon 状态 | `src/daemon/state.ts` | **已实现** — PID/状态文件的读写与查询 |
| CLI 路由 | `src/entrypoints/cli.tsx` | **布线**`--daemon-worker``daemon` 子命令 |
| 命令注册 | `src/commands.ts` | **布线** — DAEMON + BRIDGE_MODE 门控 |
@@ -23,34 +24,49 @@ DAEMON 将 Claude Code 变为后台守护进程。主进程supervisor
```
# 启动守护进程
claude daemon
claude daemon start
# 以 worker 身份启动
claude --daemon-worker=<kind>
# 查看状态(默认子命令)
claude daemon status
claude daemon ps
# 停止守护进程
claude daemon stop
# 以 worker 身份启动(由 supervisor 自动调用)
claude --daemon-worker=remoteControl
# 后台会话管理
claude daemon bg
claude daemon attach <session>
claude daemon logs <session>
claude daemon kill <session>
```
### 2.3 预期架构
### 2.3 架构
```
Supervisor (daemonMain)
├── Worker 1: assistant-mode
│ └── 接收和处理 assistant 会话
├── Worker 2: bridge-sync
│ └── bridge 消息同步
└── Worker 3: proactive
└── 主动任务执行
├── Worker: remoteControl
│ └── runBridgeHeadless() — 远程控制 headless 模式
接收远程会话、处理消息、权限审批
IPC via Unix Domain Sockets
- 生命周期管理(启动、停止、重启)
- 工作分发
- 状态报告
文件系统状态文件 (daemon-state.json)
- PID、CWD、启动时间、Worker 类型
- queryDaemonStatus() / stopDaemonByPid()
```
### 2.4 与 BRIDGE_MODE 的关系
### 2.4 Worker 生命周期管理
Supervisor 为每个 worker 实现:
- **指数退避重启**:初始 2s上限 120s倍数 ×2
- **快速失败检测**10s 内连续崩溃 5 次则 parking不再重启
- **永久错误退出码**78 (EXIT_CODE_PERMANENT) 导致直接 parking
- **优雅关闭**SIGTERM/SIGINT → abort signal → 30s 强制 SIGKILL
### 2.5 与 BRIDGE_MODE 的关系
DAEMON 和 BRIDGE_MODE 常组合使用:
@@ -63,40 +79,39 @@ if (feature('DAEMON') && feature('BRIDGE_MODE')) {
双重门控:两个 feature 都需要开启才能使用远程控制服务器。
## 三、需要补全的内容
| 模块 | 工作量 | 说明 |
|------|--------|------|
| `daemon/main.ts` | 大 | Supervisor 主进程:启动 worker、生命周期管理、IPC |
| `daemon/workerRegistry.ts` | 中 | Worker 类型分发assistant/bridge-sync/proactive |
| Worker 实现 | 大 | 各类型 worker 的具体实现 |
| IPC 协议 | 中 | Supervisor-Worker 通信层 |
## 四、关键设计决策
## 三、关键设计决策
1. **多进程架构**:一个 supervisor + 多个 worker进程隔离
2. **Unix 域套接字 IPC**:本地进程间通信,低延迟
2. **文件系统状态通信**:通过 `daemon-state.json` 文件进行状态共享(非 Unix 域套接字)
3. **与 BRIDGE_MODE 强绑定**:守护进程最常见的用途是提供远程控制服务
4. **CLI 子命令路由**`daemon` 子命令和 `--daemon-worker` 参数在 `cli.tsx` 中路由
5. **Worker 环境变量**supervisor 通过环境变量(`DAEMON_WORKER_*`)向 worker 传递配置
## 、使用方式
## 、使用方式
```bash
# 启用守护进程模式
FEATURE_DAEMON=1 FEATURE_BRIDGE_MODE=1 bun run dev
# 启动守护进程
claude daemon
claude daemon start
# 以特定 worker 启动
claude --daemon-worker=assistant
# 查看状态
claude daemon status
# 停止守护进程
claude daemon stop
# 以特定 worker 启动(通常由 supervisor 自动调用)
claude --daemon-worker=remoteControl
```
## 、文件索引
## 、文件索引
| 文件 | 职责 |
|------|------|
| `src/daemon/main.ts` | Supervisor 主进程stub |
| `src/daemon/workerRegistry.ts` | Worker 注册stub |
| `src/entrypoints/cli.tsx:95,149` | CLI 路由 |
| `src/commands.ts:77` | 命令注册(双重门控) |
| `src/daemon/main.ts` | Supervisor 主进程子命令分发、Worker 生命周期管理、退避重启 |
| `src/daemon/workerRegistry.ts` | Worker 入口remoteControl worker 实现 |
| `src/daemon/state.ts` | Daemon 状态管理PID 文件读写、状态查询 |
| `src/entrypoints/cli.tsx` | CLI 路由 |
| `src/commands.ts` | 命令注册(双重门控) |

View File

@@ -27,13 +27,15 @@ bun run dev:inspect
## 原理
`dev:inspect` 脚本实际执行的是:
`dev:inspect` 脚本实际执行的是 `scripts/dev-debug.ts`
```bash
bun --inspect-wait=localhost:8888/<token> run scripts/dev.ts
```typescript
// scripts/dev-debug.ts
process.env.BUN_INSPECT = "localhost:8888/<token>"
await import("./dev")
```
Bun 的 `--inspect-wait` 参数启动一个 Chrome DevTools Protocol 兼容的 inspect 服务,等待调试器连接后才开始执行。VS Code 的 `bun` 扩展通过 WebSocket 连接到这个地址实现 attach。
通过设置 `BUN_INSPECT` 环境变量启动一个 Chrome DevTools Protocol 兼容的 inspect 服务,然后导入 dev 模式入口。VS Code 的 `bun` 扩展通过 WebSocket 连接到输出的地址实现 attach。
## JetBrains IDE

View File

@@ -34,7 +34,7 @@ Claude Code 使用三层门控系统:
|------|------|----------|
| COMPLETE | 22 | BRIDGE_MODE, COORDINATOR_MODE, CONTEXT_COLLAPSE, VOICE_MODE, TEAMMEM, COMMIT_ATTRIBUTION, ULTRAPLAN, BASH_CLASSIFIER, TRANSCRIPT_CLASSIFIER, EXTRACT_MEMORIES, CACHED_MICROCOMPACT, TOKEN_BUDGET, AGENT_TRIGGERS, REACTIVE_COMPACT, KAIROS_BRIEF, CCR_REMOTE_SETUP, SHOT_STATS, BG_SESSIONS, PROACTIVE, CHICAGO_MCP, VERIFICATION_AGENT, PROMPT_CACHE_BREAK_DETECTION |
| PARTIAL | 19 | KAIROS, BUDDY, MONITOR_TOOL, HISTORY_SNIP, WORKFLOW_SCRIPTS, UDS_INBOX, KAIROS_CHANNELS, FORK_SUBAGENT, EXPERIMENTAL_SKILL_SEARCH, WEB_BROWSER_TOOL, MCP_SKILLS, REVIEW_ARTIFACT, KAIROS_GITHUB_WEBHOOKS, CONNECTOR_TEXT, TEMPLATES, LODESTONE, HISTORY_PICKER, MESSAGE_ACTIONS, TERMINAL_PANEL |
| STUB | 51 | TORCH, KAIROS_DREAM, KAIROS_PUSH_NOTIFICATION, DAEMON, DIRECT_CONNECT, SSH_REMOTE, STREAMLINED_OUTPUT, ANTI_DISTILLATION_CC, NATIVE_CLIENT_ATTESTATION, ABLATION_BASELINE, AGENT_MEMORY_SNAPSHOT, AGENT_TRIGGERS_REMOTE, ALLOW_TEST_VERSIONS, AUTO_THEME, AWAY_SUMMARY, BREAK_CACHE_COMMAND, BUILDING_CLAUDE_APPS, BUILTIN_EXPLORE_PLAN_AGENTS, BYOC_ENVIRONMENT_RUNNER, CCR_AUTO_CONNECT, CCR_MIRROR, COMPACTION_REMINDERS, COWORKER_TYPE_TELEMETRY, DOWNLOAD_USER_SETTINGS, DUMP_SYSTEM_PROMPT, ENHANCED_TELEMETRY_BETA, FILE_PERSISTENCE, HARD_FAIL, HOOK_PROMPTS, IS_LIBC_GLIBC, IS_LIBC_MUSL, MCP_RICH_OUTPUT, MEMORY_SHAPE_TELEMETRY, NATIVE_CLIPBOARD_IMAGE, NEW_INIT, OVERFLOW_TEST_TOOL, PERFETTO_TRACING, POWERSHELL_AUTO_MODE, QUICK_SEARCH, RUN_SKILL_GENERATOR, SELF_HOSTED_RUNNER, SKILL_IMPROVEMENT, SLOW_OPERATION_LOGGING, TREE_SITTER_BASH, TREE_SITTER_BASH_SHADOW, ULTRATHINK, UNATTENDED_RETRY, UPLOAD_USER_SETTINGS, SKIP_DETECTION_WHEN_AUTOUPDATES_DISABLED |
| STUB | 38 | TORCH, KAIROS_DREAM, KAIROS_PUSH_NOTIFICATION, DIRECT_CONNECT, SSH_REMOTE, STREAMLINED_OUTPUT, ANTI_DISTILLATION_CC, NATIVE_CLIENT_ATTESTATION, ABLATION_BASELINE, AGENT_MEMORY_SNAPSHOT, ALLOW_TEST_VERSIONS, AUTO_THEME, BREAK_CACHE_COMMAND, BUILDING_CLAUDE_APPS, BYOC_ENVIRONMENT_RUNNER, CCR_AUTO_CONNECT, CCR_MIRROR, COMPACTION_REMINDERS, COWORKER_TYPE_TELEMETRY, DOWNLOAD_USER_SETTINGS, DUMP_SYSTEM_PROMPT, ENHANCED_TELEMETRY_BETA, FILE_PERSISTENCE, HARD_FAIL, HOOK_PROMPTS, IS_LIBC_GLIBC, IS_LIBC_MUSL, MCP_RICH_OUTPUT, MEMORY_SHAPE_TELEMETRY, NATIVE_CLIPBOARD_IMAGE, NEW_INIT, OVERFLOW_TEST_TOOL, PERFETTO_TRACING, POWERSHELL_AUTO_MODE, QUICK_SEARCH, RUN_SKILL_GENERATOR, SELF_HOSTED_RUNNER, SKILL_IMPROVEMENT, SLOW_OPERATION_LOGGING, TREE_SITTER_BASH, TREE_SITTER_BASH_SHADOW, UNATTENDED_RETRY, UPLOAD_USER_SETTINGS, SKIP_DETECTION_WHEN_AUTOUPDATES_DISABLED |
---
@@ -51,14 +51,31 @@ Claude Code 使用三层门控系统:
| SHOT_STATS | **ON** | **ON** | compile-only, 已验证 | 纯本地统计 |
| PROMPT_CACHE_BREAK_DETECTION | **ON** | **ON** | compile-only, 已验证 | 内部诊断 |
| TOKEN_BUDGET | **ON** | **ON** | compile-only, 已验证 | 支持 `+500k` 语法 |
| AGENT_TRIGGERS | **ON** | **ON** | compile+GB gate, 已验证 | 本轮新增,定时任务系统 |
| EXTRACT_MEMORIES | **ON** | **ON** | compile+GB gate, 已验证 | 本轮新增,自动记忆提取 |
| VERIFICATION_AGENT | **ON** | **ON** | compile+GB gate, 已验证 | 本轮新增,对抗性验证代理 |
| KAIROS_BRIEF | **ON** | **ON** | compile+GB gate, 已验证 | 本轮新增Brief 精简模式 |
| AWAY_SUMMARY | **ON** | **ON** | compile+GB gate, 已验证 | 本轮新增,离开摘要 |
| AGENT_TRIGGERS | **ON** | **ON** | compile+GB gate, 已验证 | 本定时任务系统 |
| ULTRATHINK | **ON** | **ON** | compile-only | 扩展思考模式 |
| BUILTIN_EXPLORE_PLAN_AGENTS | **ON** | **ON** | compile-only | 内置 Explore/Plan agent |
| LODESTONE | **ON** | **ON** | compile-only | 深度链接 URL 协议 |
| EXTRACT_MEMORIES | **ON** | **ON** | compile+GB gate, 已验证 | 自动记忆提取 |
| VERIFICATION_AGENT | **ON** | **ON** | compile+GB gate, 已验证 | 对抗性验证代理 |
| KAIROS_BRIEF | **ON** | **ON** | compile+GB gate, 已验证 | Brief 精简模式 |
| AWAY_SUMMARY | **ON** | **ON** | compile+GB gate, 已验证 | 离开摘要 |
| ULTRAPLAN | **ON** | **ON** | compile+remote | 高级规划,需 CCR 基础设施 |
| DAEMON | **ON** | **ON** | compile-only | 后台守护进程 |
| ACP | **ON** | **ON** | compile-only | ACP 协议支持 |
| WORKFLOW_SCRIPTS | **ON** | **ON** | compile-only | 工作流脚本 |
| HISTORY_SNIP | **ON** | **ON** | compile-only | 历史管理 |
| CONTEXT_COLLAPSE | **ON** | **ON** | compile-only | 上下文折叠(核心 stub |
| MONITOR_TOOL | **ON** | **ON** | compile-only | 后台监控 |
| FORK_SUBAGENT | **ON** | **ON** | compile-only | 子 Agent |
| KAIROS | **ON** | **ON** | compile-only | Kairos 调度 |
| COORDINATOR_MODE | **ON** | **ON** | compile-only | 多 Worker 协调 |
| BUDDY | off | **ON** | compile+GrowthBook | 仅 dev 模式 |
| TRANSCRIPT_CLASSIFIER | off | **ON** | compile+GrowthBook | 仅 dev 模式 |
| BRIDGE_MODE | off | **ON** | compile+remote | 仅 dev 模式,需 claude.ai 订阅 |
| UDS_INBOX | off | **ON** | compile-only | 仅 dev 模式 |
| LAN_PIPES | off | **ON** | compile-only | 仅 dev 模式 |
| BG_SESSIONS | off | **ON** | compile+GB gate | 仅 dev 模式 |
| TEMPLATES | off | **ON** | compile-only | 仅 dev 模式 |
---
@@ -124,9 +141,9 @@ Claude Code 使用三层门控系统:
8. src/hooks/useReplBridge.tsx — REPL 桥接 Hook
9. src/main.tsx — 主入口中的桥接模式启动
10. src/screens/REPL.tsx — REPL 屏幕中的桥接集成
11. src/tools/BriefTool/attachments.ts — Brief 工具附件处理
12. src/tools/BriefTool/upload.ts — Brief 工具上传
13. src/tools/ConfigTool/supportedSettings.ts — 配置工具中的桥接设置
11. packages/builtin-tools/src/tools/BriefTool/attachments.ts — Brief 工具附件处理
12. packages/builtin-tools/src/tools/BriefTool/upload.ts — Brief 工具上传
13. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts — 配置工具中的桥接设置
**启用所需操作**: 仅需将编译标志 `BRIDGE_MODE` 设为 `true`。所有代码完整,命令入口 `src/commands/bridge/index.ts`604 行)和 `src/commands/bridge/bridge.tsx`46,907 行)均存在。
@@ -185,8 +202,8 @@ src/utils/swarm/ 目录22 个文件):
7. src/screens/REPL.tsx — REPL 屏幕中的协调器集成
8. src/screens/ResumeConversation.tsx — 恢复对话时的协调器处理
9. src/tools.ts — 工具注册中的协调器工具
10. src/tools/AgentTool/AgentTool.tsx — Agent 工具中的协调器模式分支
11. src/tools/AgentTool/builtInAgents.ts — 内置代理定义
10. packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx — Agent 工具中的协调器模式分支
11. packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts — 内置代理定义
12. src/utils/processUserInput/processSlashCommand.tsx — 斜杠命令处理中的协调器
13. src/utils/sessionRestore.ts — 会话恢复中的协调器状态
14. src/utils/systemPrompt.ts — 系统提示中的协调器指令
@@ -259,9 +276,9 @@ src/utils/swarm/ 目录22 个文件):
9. src/screens/REPL.tsx — REPL 中的语音模式集成
10. src/services/voiceStreamSTT.ts — STT 服务
11. src/state/AppState.tsx — 应用状态中的语音状态
12. src/tools/ConfigTool/ConfigTool.ts — 配置工具中的语音设置
13. src/tools/ConfigTool/prompt.ts — 配置工具提示
14. src/tools/ConfigTool/supportedSettings.ts — 支持的设置项
12. packages/builtin-tools/src/tools/ConfigTool/ConfigTool.ts — 配置工具中的语音设置
13. packages/builtin-tools/src/tools/ConfigTool/prompt.ts — 配置工具提示
14. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts — 支持的设置项
15. src/utils/settings/types.ts — 设置类型定义
16. src/voice/voiceModeEnabled.ts — 语音模式启用逻辑
@@ -385,8 +402,8 @@ src/utils/swarm/ 目录22 个文件):
11. src/hooks/toolPermission/permissionLogging.ts — 权限日志
12. src/hooks/useCanUseTool.tsx — 工具可用性检查
13. src/services/api/withRetry.ts — API 重试中的分类器
14. src/tools/BashTool/bashPermissions.ts — Bash 权限逻辑
15. src/tools/BashTool/pathValidation.ts — 路径验证
14. packages/builtin-tools/src/tools/BashTool/bashPermissions.ts — Bash 权限逻辑
15. packages/builtin-tools/src/tools/BashTool/pathValidation.ts — 路径验证
16. src/utils/classifierApprovals.ts — 分类器审批记录
17. src/utils/messages.ts — 消息处理
18. src/utils/permissions/permissions.ts — 权限核心
@@ -431,11 +448,11 @@ src/utils/swarm/ 目录22 个文件):
22. src/screens/REPL.tsx — REPL 屏幕
23. src/services/api/claude.ts — Claude API 服务
24. src/services/tools/toolExecution.ts — 工具执行
25. src/tools/AgentTool/AgentTool.tsx — Agent 工具
26. src/tools/AgentTool/agentToolUtils.ts — Agent 工具工具函数
27. src/tools/AgentTool/runAgent.ts — 运行 Agent
28. src/tools/BashTool/bashPermissions.ts — Bash 权限
29. src/tools/ConfigTool/supportedSettings.ts — 支持的设置
25. packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx — Agent 工具
26. packages/builtin-tools/src/tools/AgentTool/agentToolUtils.ts — Agent 工具工具函数
27. packages/builtin-tools/src/tools/AgentTool/runAgent.ts — 运行 Agent
28. packages/builtin-tools/src/tools/BashTool/bashPermissions.ts — Bash 权限
29. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts — 支持的设置
30. src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts — 退出计划模式工具
31. src/tools/NotebookEditTool/NotebookEditTool.ts — Notebook 编辑工具
32. src/types/permissions.ts — 权限类型
@@ -543,11 +560,10 @@ src/utils/swarm/ 目录22 个文件):
| 文件路径 | 行数 | 功能说明 |
|----------|------|----------|
| src/tools/ScheduleCronTool/CronCreateTool.ts | 157 行 | Cron 创建工具 |
| src/tools/ScheduleCronTool/prompt.ts | 135 行 | Cron 工具提示词 |
| src/tools/ScheduleCronTool/CronListTool.ts | 97 行 | Cron 列表工具 |
| src/tools/ScheduleCronTool/CronDeleteTool.ts | 95 行 | Cron 删除工具 |
| src/tools/ScheduleCronTool/UI.tsx | 59 行 | Cron UI 组件 |
| packages/builtin-tools/src/tools/ScheduleCronTool/CronCreateTool.ts | 157 行 | Cron 创建工具 |
| packages/builtin-tools/src/tools/ScheduleCronTool/prompt.ts | 135 行 | Cron 工具提示词 |
| packages/builtin-tools/src/tools/ScheduleCronTool/CronListTool.ts | 97 行 | Cron 列表工具 |
| packages/builtin-tools/src/tools/ScheduleCronTool/CronDeleteTool.ts | 95 行 | Cron 删除工具 |
**引用该标志的文件6 个)**:
1. src/cli/print.ts — CLI 输出
@@ -598,7 +614,7 @@ src/utils/swarm/ 目录22 个文件):
| 文件路径 | 行数 | 功能说明 |
|----------|------|----------|
| src/tools/BriefTool/BriefTool.ts | 204 行 | Brief 工具核心 |
| packages/builtin-tools/src/tools/BriefTool/BriefTool.ts | 204 行 | Brief 工具核心 |
| src/commands/brief.ts | 130 行 | Brief 命令实现 |
**引用该标志的文件20 个)**:
@@ -616,7 +632,7 @@ src/utils/swarm/ 目录22 个文件):
12. src/hooks/useGlobalKeybindings.tsx — 全局键绑定
13. src/keybindings/defaultBindings.ts — 默认键绑定
14. src/main.tsx — 主入口
15. src/tools/BriefTool/BriefTool.ts — Brief 工具
15. packages/builtin-tools/src/tools/BriefTool/BriefTool.ts — Brief 工具
16. src/tools/ToolSearchTool/prompt.ts — 工具搜索提示
17. src/utils/attachments.ts — 附件
18. src/utils/conversationRecovery.ts — 对话恢复
@@ -718,7 +734,7 @@ src/utils/swarm/ 目录22 个文件):
9. src/screens/REPL.tsx — REPL多处引用通过 require 加载 proactive 模块)
10. src/services/compact/prompt.ts — 压缩提示
11. src/tools.ts — 工具注册
12. src/tools/AgentTool/AgentTool.tsx — Agent 工具
12. packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx — Agent 工具
13. src/utils/sessionStorage.ts — 会话存储
14. src/utils/settings/types.ts — 设置类型
15. src/utils/systemPrompt.ts — 系统提示
@@ -771,11 +787,11 @@ src/utils/swarm/ 目录22 个文件):
| 文件路径 | 行数 | 功能说明 |
|----------|------|----------|
| src/tools/TaskUpdateTool/TaskUpdateTool.ts | 406 行 | 任务更新工具 |
| src/tools/AgentTool/builtInAgents.ts | 72 行 | 内置代理定义 |
| packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts | 72 行 | 内置代理定义 |
**引用该标志的文件4 个)**:
1. src/constants/prompts.ts — 提示词
2. src/tools/AgentTool/builtInAgents.ts — 内置代理
2. packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts — 内置代理
3. src/tools/TaskUpdateTool/TaskUpdateTool.ts — 任务更新工具
4. src/tools/TodoWriteTool/TodoWriteTool.ts — TodoWrite 工具
@@ -796,7 +812,7 @@ src/utils/swarm/ 目录22 个文件):
3. src/services/compact/autoCompact.ts — 自动压缩
4. src/services/compact/compact.ts — 压缩核心
5. src/services/compact/microCompact.ts — 微压缩
6. src/tools/AgentTool/runAgent.ts — 运行 Agent
6. packages/builtin-tools/src/tools/AgentTool/runAgent.ts — 运行 Agent
**启用所需操作**: 仅需将编译标志 `PROMPT_CACHE_BREAK_DETECTION` 设为 `true`
@@ -856,11 +872,11 @@ src/utils/swarm/ 目录22 个文件):
38. src/services/mcp/useManageMCPConnections.ts
39. src/skills/bundled/index.ts
40. src/tools.ts
41. src/tools/AgentTool/AgentTool.tsx
41. packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx
42. src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx
43. src/tools/BashTool/BashTool.tsx
44. src/tools/BriefTool/BriefTool.ts
45. src/tools/ConfigTool/supportedSettings.ts
43. packages/builtin-tools/src/tools/BashTool/BashTool.tsx
44. packages/builtin-tools/src/tools/BriefTool/BriefTool.ts
45. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts
46. src/tools/EnterPlanModeTool/EnterPlanModeTool.ts
47. src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts
48. src/tools/PowerShellTool/PowerShellTool.tsx
@@ -877,8 +893,8 @@ src/utils/swarm/ 目录22 个文件):
59. src/utils/systemPrompt.ts
**缺失文件**:
- src/commands/assistant/index.ts — 完全缺失src/commands.ts 第 69 行引用了 `commands/assistant/index.js`
- src/commands/assistant/gate.ts — 完全缺失
- ~~src/commands/assistant/index.ts~~已补全
- ~~src/commands/assistant/gate.ts~~已补全
**启用所需修复**: 需要创建 `src/commands/assistant/` 目录及其 `index.ts``gate.ts` 文件。
@@ -930,7 +946,7 @@ src/utils/swarm/ 目录22 个文件):
| 文件路径 | 行数 | 功能说明 |
|----------|------|----------|
| src/tasks/LocalShellTask/LocalShellTask.tsx | 522 行 | 本地 Shell 任务完整实现 |
| src/tools/MonitorTool/MonitorTool.ts | 1 行 | 监控工具(桩) |
| packages/builtin-tools/src/tools/MonitorTool/MonitorTool.ts | 1 行 | 监控工具(桩) |
| src/tasks/MonitorMcpTask/MonitorMcpTask.ts | 5 行 | MCP 监控任务(桩) |
| src/components/tasks/MonitorMcpDetailDialog.tsx | 3 行 | MCP 详情对话框(桩) |
| src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.tsx | 3 行 | 监控权限请求(桩) |
@@ -941,12 +957,12 @@ src/utils/swarm/ 目录22 个文件):
3. src/tasks.ts — 任务注册
4. src/tasks/LocalShellTask/LocalShellTask.tsx — Shell 任务
5. src/tools.ts — 工具注册
6. src/tools/AgentTool/runAgent.ts — Agent 运行
7. src/tools/BashTool/BashTool.tsx — Bash 工具
8. src/tools/BashTool/prompt.ts — Bash 提示
6. packages/builtin-tools/src/tools/AgentTool/runAgent.ts — Agent 运行
7. packages/builtin-tools/src/tools/BashTool/BashTool.tsx — Bash 工具
8. packages/builtin-tools/src/tools/BashTool/prompt.ts — Bash 提示
9. src/tools/PowerShellTool/PowerShellTool.tsx — PowerShell 工具
**启用所需修复**: 需要实现 `src/tools/MonitorTool/MonitorTool.ts``src/tasks/MonitorMcpTask/MonitorMcpTask.ts``src/components/tasks/MonitorMcpDetailDialog.tsx``src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.tsx`
**启用所需修复**: 需要实现 `packages/builtin-tools/src/tools/MonitorTool/MonitorTool.ts``src/tasks/MonitorMcpTask/MonitorMcpTask.ts``src/components/tasks/MonitorMcpDetailDialog.tsx``src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.tsx`
---
@@ -988,10 +1004,11 @@ src/utils/swarm/ 目录22 个文件):
| src/components/WorkflowMultiselectDialog.tsx | 127 行 | 工作流多选对话框(有内容) |
| src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts | 5 行 | 本地工作流任务(桩) |
| src/components/tasks/WorkflowDetailDialog.tsx | 3 行 | 工作流详情对话框(桩) |
| src/tools/WorkflowTool/WorkflowPermissionRequest.tsx | 3 行 | 工作流权限请求(桩) |
| src/tools/WorkflowTool/createWorkflowCommand.ts | 3 行 | 创建工作流命令( |
| src/tools/WorkflowTool/WorkflowTool.ts | 1 行 | 工作流工具( |
| src/tools/WorkflowTool/constants.ts | 1 行 | 常量(桩) |
| packages/builtin-tools/src/tools/WorkflowTool/WorkflowPermissionRequest.tsx | ~80 行 | 工作流权限请求组件 |
| packages/builtin-tools/src/tools/WorkflowTool/createWorkflowCommand.ts | 41 行 | 创建工作流命令(已实现 |
| packages/builtin-tools/src/tools/WorkflowTool/WorkflowTool.ts | 74 行 | 工作流工具(部分实现call 需运行时 |
| packages/builtin-tools/src/tools/WorkflowTool/constants.ts | ~10 行 | 常量定义 |
| packages/builtin-tools/src/tools/WorkflowTool/bundled/index.ts | ~20 行 | 内置工作流初始化 |
**引用该标志的文件7 个)**:
1. src/commands.ts — 命令注册(引用 `commands/workflows/index.js`
@@ -1086,13 +1103,13 @@ src/utils/swarm/ 目录22 个文件):
| 文件路径 | 行数 | 功能说明 |
|----------|------|----------|
| src/tools/AgentTool/forkSubagent.ts | 210 行 | 分叉子代理核心逻辑 |
| packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts | 210 行 | 分叉子代理核心逻辑 |
**引用该标志的文件5 个)**:
1. src/commands.ts — 命令注册
2. src/commands/branch/index.ts — 分支命令入口
3. src/components/messages/UserTextMessage.tsx — 用户消息
4. src/tools/AgentTool/forkSubagent.ts — 分叉逻辑
4. packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts — 分叉逻辑
5. src/tools/ToolSearchTool/prompt.ts — 工具搜索提示
**缺失文件**:
@@ -1116,7 +1133,7 @@ src/utils/swarm/ 目录22 个文件):
4. src/query.ts — 查询
5. src/services/compact/compact.ts — 压缩
6. src/services/mcp/useManageMCPConnections.ts — MCP 连接管理
7. src/tools/SkillTool/SkillTool.ts — 技能工具1,108 行)
7. packages/builtin-tools/src/tools/SkillTool/SkillTool.ts — 技能工具1,108 行)
8. src/utils/attachments.ts — 附件
9. src/utils/messages.ts — 消息
@@ -1336,21 +1353,24 @@ src/utils/swarm/ 目录22 个文件):
**引用文件**:
1. src/components/Settings/Config.tsx — 设置
2. src/tools.ts — 工具注册
3. src/tools/ConfigTool/supportedSettings.ts — 支持的设置
3. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts — 支持的设置
**代码量**: 0 行专属代码,仅在设置中预留了开关位
---
## 45. DAEMON
## 45. DAEMON `[build: ON] [dev: ON]`
**编译时引用次数**: 3
**功能描述**: 守护进程模式。
**分类**: STUB
**功能描述**: 守护进程模式。允许 Claude Code 作为后台长驻 supervisor 进程运行,管理多个 worker。
**分类**: COMPLETE已恢复
**核心实现文件**:
1. src/daemon/main.ts — 413 行daemon 主入口,管理生命周期
2. src/daemon/workerRegistry.ts — 112 行worker 注册和管理
3. src/commands/daemon/index.ts — daemon 子命令入口
**引用文件**:
1. src/commands.ts — 条件注册命令(与 BRIDGE_MODE 组合)
2. src/entrypoints/cli.tsx — CLI 入口
**代码量**: 0 行专属代码
**说明**: 在 commands.ts 中,`DAEMON``BRIDGE_MODE` 一起用于条件加载 `commands/remoteControlServer/index.js`,该文件不存在。
1. src/commands.ts — 条件注册命令
2. src/entrypoints/cli.tsx — CLI 入口中的 `--daemon-worker` 路径
**说明**: 已从 stub 恢复为完整实现,支持 `daemon start/status/stop` 子命令、exponential backoff、state file 持久化。
---
@@ -1421,7 +1441,7 @@ src/utils/swarm/ 目录22 个文件):
**分类**: STUB
**引用文件**:
1. src/main.tsx — 主入口
2. src/tools/AgentTool/loadAgentsDir.ts — 加载代理目录
2. packages/builtin-tools/src/tools/AgentTool/loadAgentsDir.ts — 加载代理目录
**代码量**: 0 行专属代码
---
@@ -1456,7 +1476,7 @@ src/utils/swarm/ 目录22 个文件):
**引用文件**:
1. src/components/ThemePicker.tsx — 主题选择器
2. src/components/design-system/ThemeProvider.tsx — 主题提供者
3. src/tools/ConfigTool/supportedSettings.ts — 支持的设置
3. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts — 支持的设置
**代码量**: 0 行专属代码
---
@@ -1498,7 +1518,7 @@ src/utils/swarm/ 目录22 个文件):
**编译时引用次数**: 1
**功能描述**: 内置探索和计划代理。
**分类**: STUB
**引用文件**: src/tools/AgentTool/builtInAgents.ts — 内置代理定义
**引用文件**: packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts — 内置代理定义
**代码量**: 0 行专属代码
---
@@ -1789,7 +1809,7 @@ src/utils/swarm/ 目录22 个文件):
**功能描述**: Tree-sitter Bash 影子模式(并行运行 tree-sitter 和传统解析器进行对比)。
**分类**: STUB
**引用文件**:
1. src/tools/BashTool/bashPermissions.ts — Bash 权限
1. packages/builtin-tools/src/tools/BashTool/bashPermissions.ts — Bash 权限
2. src/utils/bash/parser.ts — Bash 解析器
**代码量**: 0 行专属代码
@@ -1863,16 +1883,16 @@ src/utils/swarm/ 目录22 个文件):
| 文件路径 | 行数 | 所属标志 |
|----------|------|----------|
| src/tools/MonitorTool/MonitorTool.ts | 1 行 | MONITOR_TOOL |
| src/tools/WorkflowTool/WorkflowTool.ts | 1 行 | WORKFLOW_SCRIPTS |
| src/tools/WorkflowTool/constants.ts | 1 行 | WORKFLOW_SCRIPTS |
| packages/builtin-tools/src/tools/MonitorTool/MonitorTool.ts | 1 行 | MONITOR_TOOL |
| packages/builtin-tools/src/tools/WorkflowTool/WorkflowTool.ts | 1 行 | WORKFLOW_SCRIPTS |
| packages/builtin-tools/src/tools/WorkflowTool/constants.ts | 1 行 | WORKFLOW_SCRIPTS |
| src/tools/ReviewArtifactTool/ReviewArtifactTool.ts | 1 行 | REVIEW_ARTIFACT |
| src/utils/udsMessaging.ts | 1 行 | UDS_INBOX |
| src/utils/udsClient.ts | 3 行 | UDS_INBOX |
| src/utils/udsMessaging.ts | 已实现 | UDS_INBOX |
| src/utils/udsClient.ts | 已实现 | UDS_INBOX |
| src/skills/mcpSkills.ts | 3 行 | MCP_SKILLS |
| src/tools/WebBrowserTool/WebBrowserPanel.tsx | 3 行 | WEB_BROWSER_TOOL |
| src/tools/WorkflowTool/createWorkflowCommand.ts | 3 行 | WORKFLOW_SCRIPTS |
| src/tools/WorkflowTool/WorkflowPermissionRequest.tsx | 3 行 | WORKFLOW_SCRIPTS |
| packages/builtin-tools/src/tools/WorkflowTool/createWorkflowCommand.ts | 3 行 | WORKFLOW_SCRIPTS |
| packages/builtin-tools/src/tools/WorkflowTool/WorkflowPermissionRequest.tsx | 3 行 | WORKFLOW_SCRIPTS |
| src/components/tasks/WorkflowDetailDialog.tsx | 3 行 | WORKFLOW_SCRIPTS |
| src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.tsx | 3 行 | MONITOR_TOOL |
| src/components/tasks/MonitorMcpDetailDialog.tsx | 3 行 | MONITOR_TOOL |

View File

@@ -37,7 +37,7 @@ Agent({ subagent_type: "general-purpose", prompt: "..." })
### 3.1 门控与互斥
文件:`src/tools/AgentTool/forkSubagent.ts:32-39`
文件:`packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts:32-39`
```ts
export function isForkSubagentEnabled(): boolean {
@@ -105,7 +105,7 @@ isForkSubagentEnabled() && !subagent_type?
### 3.4 消息构建buildForkedMessages
文件:`src/tools/AgentTool/forkSubagent.ts:107-169`
文件:`packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts:107-169`
构建的消息结构:
@@ -185,11 +185,11 @@ FEATURE_FORK_SUBAGENT=1 bun run dev
| 文件 | 行数 | 职责 |
|------|------|------|
| `src/tools/AgentTool/forkSubagent.ts` | ~210 | 核心定义 + 消息构建 + 递归防护 |
| `src/tools/AgentTool/AgentTool.tsx` | — | Fork 路由 + 强制异步 |
| `src/tools/AgentTool/prompt.ts` | — | "When to Fork" 提示词段落 |
| `src/tools/AgentTool/runAgent.ts` | — | useExactTools 路径 |
| `src/tools/AgentTool/resumeAgent.ts` | — | Fork agent 恢复 |
| `packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts` | ~210 | 核心定义 + 消息构建 + 递归防护 |
| `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` | — | Fork 路由 + 强制异步 |
| `packages/builtin-tools/src/tools/AgentTool/prompt.ts` | — | "When to Fork" 提示词段落 |
| `packages/builtin-tools/src/tools/AgentTool/runAgent.ts` | — | useExactTools 路径 |
| `packages/builtin-tools/src/tools/AgentTool/resumeAgent.ts` | — | Fork agent 恢复 |
| `src/constants/xml.ts` | — | XML 标签常量 |
| `src/utils/forkedAgent.ts` | — | CacheSafeParams + ContentReplacementState 克隆 |
| `src/commands/fork/index.ts` | — | /fork 命令stub |

View File

@@ -34,13 +34,13 @@ KAIROS 在系统提示中注入两大段落:
### 2.1 Brief 段落 (`getBriefSection`)
文件:`src/constants/prompts.ts:843-858`
文件:`src/constants/prompts.ts:847-858`
`feature('KAIROS') || feature('KAIROS_BRIEF')` 时注入。Brief 工具(`SendUserMessage`)的结构化消息输出指令。`/brief` toggle 和 `--brief` flag 只控制显示过滤,不影响模型行为。
### 2.2 Proactive/Autonomous Work 段落 (`getProactiveSection`)
文件:`src/constants/prompts.ts:860-914`
文件:`src/constants/prompts.ts:864-918`
`feature('PROACTIVE') || feature('KAIROS')``isProactiveActive()` 时注入。核心行为指令:
@@ -176,7 +176,7 @@ FEATURE_KAIROS=1 FEATURE_TOKEN_BUDGET=1 bun run dev
| `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/constants/prompts.ts:557,847-918` | 72 | 系统提示注入 |
| `src/components/tasks/src/tasks/DreamTask/` | 3 | Dream 任务stub |
| `src/proactive/index.ts` | — | Proactive 核心KAIROS 共享) |
| `src/utils/sessionState.ts` | — | 向 bridge/CCR 暴露 automation 状态 |

View File

@@ -281,7 +281,7 @@ CLI-B (192.168.50.27) 心跳循环
## SendMessageTool TCP 支持
`src/tools/SendMessageTool/SendMessageTool.ts`
`packages/builtin-tools/src/tools/SendMessageTool/SendMessageTool.ts`
- `to` 字段支持 `tcp:host:port` 格式
- `checkPermissions``tcp:` scheme 返回 `behavior: 'ask'``classifierApprovable: false`

View File

@@ -202,4 +202,4 @@ docker run -d \
| `src/services/langfuse/__tests__/langfuse.test.ts` | 测试568 行) |
| `src/query.ts` | 主查询流程中的 Trace 集成 |
| `src/services/tools/toolExecution.ts` | 工具执行中的观察记录 |
| `src/tools/AgentTool/runAgent.ts` | 子 Agent Trace 创建 |
| `packages/builtin-tools/src/tools/AgentTool/runAgent.ts` | 子 Agent Trace 创建 |

View File

@@ -41,7 +41,7 @@ getMcpSkillCommands() 过滤 → SkillTool 调用
### 2.2 技能筛选
文件:`src/commands.ts:547-558`
文件:`src/commands.ts:604-616`
`getMcpSkillCommands(mcpCommands)` 过滤条件:
@@ -54,7 +54,7 @@ feature('MCP_SKILLS') // feature flag 必须开启
### 2.3 条件加载
文件:`src/services/mcp/client.ts:117-121`
文件:`src/services/mcp/client.ts:129-133`
`fetchMcpSkillsForClient` 通过 `require()` 条件加载feature flag 关闭时不加载任何模块:
@@ -79,8 +79,8 @@ const fetchMcpSkillsForClient = feature('MCP_SKILLS')
| 文件 | 行 | 说明 |
|------|------|------|
| `src/commands.ts` | 547-558, 561-608 | 命令过滤和 SkillTool 命令收集 |
| `src/services/mcp/client.ts` | 117-121, 1394, 1672, 2173-2181, 2346-2358 | 技能获取、缓存清除、连接时获取 |
| `src/commands.ts` | 604-616, 620-633 | 命令过滤和 SkillTool 命令收集 |
| `src/services/mcp/client.ts` | 129-133, 1394, 1672, 2176 | 技能获取、缓存清除、连接时获取 |
| `src/services/mcp/useManageMCPConnections.ts` | 22-26, 682-740 | 实时刷新prompts/resources 变化) |
## 三、关键设计决策

View File

@@ -318,7 +318,7 @@ sub 角色:
| `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: 支持) |
| `packages/builtin-tools/src/tools/SendMessageTool/SendMessageTool.ts` | AI 发消息工具(含 tcp: 支持) |
## 后续优化方向

View File

@@ -26,7 +26,7 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
| 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` |
| 工具注册 | `src/tools.ts:26-28` | **布线** | SleepTool 动态加载 |
| REPL 集成 | `src/screens/REPL.tsx` | **已实现** | tick 驱动、standby/sleeping 状态、页脚与 bridge automation metadata 上报 |
| 系统提示 | `src/constants/prompts.ts:860-914` | **完整** | 自主工作行为指令(~55 行详细 prompt |
| 系统提示 | `src/constants/prompts.ts:864-918` | **完整** | 自主工作行为指令(~55 行详细 prompt |
| 远控状态镜像 | `src/utils/sessionState.ts` | **已实现** | 向 remote-control/CCR 暴露 `automation_state` 元数据 |
### 2.2 系统提示内容
@@ -106,7 +106,7 @@ FEATURE_PROACTIVE=1 FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 bun run dev
| `src/proactive/index.ts` | 核心逻辑与 next-tick 状态 |
| `src/tools/SleepTool/prompt.ts` | SleepTool 工具提示 |
| `src/tools/SleepTool/SleepTool.ts` | 休眠/唤醒执行逻辑 |
| `src/constants/prompts.ts:860-914` | 自主工作系统提示 |
| `src/constants/prompts.ts:864-918` | 自主工作系统提示 |
| `src/screens/REPL.tsx` | REPL tick 集成与 automation 状态上报 |
| `src/utils/sessionStorage.ts:4892-4912` | Tick 消息注入 |
| `src/utils/sessionState.ts` | bridge/CCR metadata 镜像 |

View File

@@ -104,6 +104,8 @@ docker compose up -d
| `RCS_HEARTBEAT_INTERVAL` | 否 | `20` | 心跳间隔(秒) |
| `RCS_JWT_EXPIRES_IN` | 否 | `3600` | JWT 令牌有效期(秒) |
| `RCS_DISCONNECT_TIMEOUT` | 否 | `300` | 断线判定超时(秒) |
| `RCS_WS_IDLE_TIMEOUT` | 否 | `30` | WebSocket 空闲超时Bun 发送协议级 ping |
| `RCS_WS_KEEPALIVE_INTERVAL` | 否 | `20` | 服务端→客户端 keep_alive 帧间隔(秒),防止反向代理关闭空闲连接 |
### 客户端Claude Code CLI
@@ -232,11 +234,10 @@ acp-link ◄──ACP relay──► RCS ◄──Web UI WS──► 浏览器
# 注意claude 本身不支持 ACP需要用 ccb-bun --acp
ACP_RCS_URL=http://localhost:3000 \
ACP_RCS_TOKEN=sk-rcs-your-key \
ACP_RCS_NAME=my-agent \
acp-link ccb-bun -- --acp
```
ACP session 在 Web UI 中显示色标签,与普通 Claude Code session 区分。
ACP session 在 Web UI 中显示品牌色标签,与普通 Claude Code session 区分。
## 工作流程详解

View File

@@ -16,12 +16,12 @@
### 现状
- `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>)
`src/daemon/main.ts`
`src/daemon/workerRegistry.ts`
- `status` / `stop` 目前只是占位输出:
[src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:49>)
`src/daemon/main.ts`
- `/remote-control-server` 有自己的命令内 UI 状态,但只维护当前进程内的 `daemonProcess`,并不适合作为跨进程 CLI 管理基础:
[src/commands/remoteControlServer/remoteControlServer.tsx](</e:/Source_code/Claude-code-bast/src/commands/remoteControlServer/remoteControlServer.tsx:32>)
`src/commands/remoteControlServer/remoteControlServer.tsx`
### 目标
@@ -53,8 +53,8 @@
### 代码范围
- 新增 `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 尽量读取同一份状态文件
- 修改 `src/daemon/main.ts`
- 轻量修改 `src/commands/remoteControlServer/remoteControlServer.tsx`,让 UI 尽量读取同一份状态文件
### 验证
@@ -78,15 +78,15 @@
### 现状
- fast-path 已接好:
[src/entrypoints/cli.tsx](</e:/Source_code/Claude-code-bast/src/entrypoints/cli.tsx:218>)
`src/entrypoints/cli.tsx`
- session registry 已有真实实现:
[src/utils/concurrentSessions.ts](</e:/Source_code/Claude-code-bast/src/utils/concurrentSessions.ts:1>)
`src/utils/concurrentSessions.ts`
- `exit` 在 bg session 内已会 `tmux detach-client`
[src/commands/exit/exit.tsx](</e:/Source_code/Claude-code-bast/src/commands/exit/exit.tsx:20>)
`src/commands/exit/exit.tsx`
- 但 CLI handler 仍全空:
[src/cli/bg.ts](</e:/Source_code/Claude-code-bast/src/cli/bg.ts:1>)
`src/cli/bg.ts`
- task summary 仍然是 stub
[src/utils/taskSummary.ts](</e:/Source_code/Claude-code-bast/src/utils/taskSummary.ts:1>)
`src/utils/taskSummary.ts`
### 目标
@@ -122,12 +122,12 @@
### 代码范围
- 修改 [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/cli/bg.ts`
- 修改 `src/utils/concurrentSessions.ts` 以便后续 attach/--bg 扩展
- 修改 `src/utils/taskSummary.ts`
- 复用:
[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>)
`src/utils/sessionStorage.ts`
`src/utils/udsClient.ts`
### 验证
@@ -150,15 +150,15 @@
### 现状
- 命令入口只有 fast-path
[src/entrypoints/cli.tsx](</e:/Source_code/Claude-code-bast/src/entrypoints/cli.tsx:249>)
`src/entrypoints/cli.tsx`
- handler 是空的:
[src/cli/handlers/templateJobs.ts](</e:/Source_code/Claude-code-bast/src/cli/handlers/templateJobs.ts:1>)
`src/cli/handlers/templateJobs.ts`
- `markdownConfigLoader` 已把 `templates` 纳入配置目录:
[src/utils/markdownConfigLoader.ts](</e:/Source_code/Claude-code-bast/src/utils/markdownConfigLoader.ts:29>)
`src/utils/markdownConfigLoader.ts`
- `query / stopHooks` 已预留 job classifier 链路:
[src/query/stopHooks.ts](</e:/Source_code/Claude-code-bast/src/query/stopHooks.ts:103>)
`src/query/stopHooks.ts`
- `jobs/classifier.ts` 仍是 stub
[src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
`src/jobs/classifier.ts`
### 目标
@@ -185,7 +185,7 @@
### Phase 2
- 恢复 [src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
- 恢复 `src/jobs/classifier.ts`
- 让带 `CLAUDE_JOB_DIR` 的 job session 在 turn 完成后自动更新 `state.json`
- 再决定是否补自动 job runner

View File

@@ -7,50 +7,13 @@
| Feature | 引用 | 状态 | 类别 | 简要说明 |
|---------|------|------|------|---------|
| CHICAGO_MCP | 16 | N/A | 内部基础设施 | Anthropic 内部 MCP 基础设施,非外部可用 |
| 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 | — | 连接器 | 连接器文本,外部系统文本适配 |
| COMMIT_ATTRIBUTION | 12 | — | Git | Commit 归因,标记 commit 来源 |
| CACHED_MICROCOMPACT | 12 | — | 压缩 | 缓存微压缩,优化 compaction 性能 |
| PROMPT_CACHE_BREAK_DETECTION | 9 | — | 性能 | Prompt cache 中断检测,监控 cache miss |
| MEMORY_SHAPE_TELEMETRY | 3 | — | 遥测 | 记忆形态遥测,记忆使用模式追踪 |
| MCP_RICH_OUTPUT | 3 | — | MCP | MCP 富输出,增强 MCP 工具输出格式 |
| FILE_PERSISTENCE | 3 | — | 持久化 | 文件持久化,跨会话保持状态 |
| TREE_SITTER_BASH_SHADOW | 5 | Shadow | 安全 | Bash AST Shadow 模式(见 tree-sitter-bash.md |
| QUICK_SEARCH | 5 | — | 搜索 | 快速搜索,优化的文件/内容搜索 |
| MESSAGE_ACTIONS | 5 | — | UI | 消息操作,对消息执行后处理动作 |
| DOWNLOAD_USER_SETTINGS | 5 | — | 配置 | 下载用户设置,从服务端同步配置 |
| DIRECT_CONNECT | 5 | — | 网络 | 直连模式,绕过代理直接连接 API |
| VERIFICATION_AGENT | 4 | — | Agent | 验证 Agent专门用于验证代码变更 |
| TERMINAL_PANEL | 4 | — | UI | 终端面板,嵌入式终端输出显示 |
| SSH_REMOTE | 4 | — | 远程 | SSH 远程,通过 SSH 连接远程 Claude |
| REVIEW_ARTIFACT | 4 | — | 审查 | Review Artifact代码审查产出物 |
| REACTIVE_COMPACT | 4 | — | 压缩 | 响应式压缩,基于上下文变化触发 compaction |
| HISTORY_PICKER | 4 | — | UI | 历史选择器,浏览和选择历史对话 |
| UPLOAD_USER_SETTINGS | 2 | — | 配置 | 上传用户设置,同步配置到服务端 |
| POWERSHELL_AUTO_MODE | 2 | — | 平台 | PowerShell 自动模式Windows 权限自动化 |
| OVERFLOW_TEST_TOOL | 2 | — | 测试 | 溢出测试工具,测试上下文溢出处理 |
| NEW_INIT | 2 | — | 初始化 | 新版初始化流程 |
| HARD_FAIL | 2 | — | 错误处理 | 硬失败模式,不可恢复错误直接终止 |
| ENHANCED_TELEMETRY_BETA | 2 | — | 遥测 | 增强遥测 Beta详细的性能指标收集 |
| COWORKER_TYPE_TELEMETRY | 2 | — | 遥测 | 协作者类型遥测,追踪协作模式 |
| BREAK_CACHE_COMMAND | 2 | — | 缓存 | 中断缓存命令,强制刷新 prompt cache |
| AWAY_SUMMARY | 2 | — | 摘要 | 离开摘要,用户返回时总结期间工作 |
| AUTO_THEME | 2 | — | UI | 自动主题,根据终端设置切换主题 |
| ALLOW_TEST_VERSIONS | 2 | — | 版本 | 允许测试版本,跳过版本检查 |
| AGENT_TRIGGERS_REMOTE | 2 | — | Agent | Agent 远程触发,从远程触发 Agent 任务 |
| AGENT_MEMORY_SNAPSHOT | 2 | — | Agent | Agent 记忆快照,保存/恢复 Agent 状态 |
| CHICAGO_MCP | 16 | 已实现 | 工具 | Computer Use + Chrome MCP 控制build 默认启用) |
| MONITOR_TOOL | 13 | 已实现 | 工具 | 后台监控工具,持续监视 shell 输出build 默认启用) |
| BG_SESSIONS | 11 | 部分实现 | 会话管理 | 后台会话注册/清理已实现,任务摘要是 stubdev 默认启用) |
| SHOT_STATS | 10 | 实现 | 统计 | API 调用统计面板build 默认启用) |
| EXTRACT_MEMORIES | 7 | 实现 | 记忆 | 自动记忆提取build 默认启用,受 GrowthBook 门控) |
| TEMPLATES | 6 | 部分实现 | 项目管理 | 项目/提示模板系统dev 默认启用) |
| LODESTONE | 6 | 已实现 | 深度链接 | URL 协议处理器build 默认启用) |
## 单引用 Feature40+ 个)
@@ -66,10 +29,9 @@ BUILDING_CLAUDE_APPS, ANTI_DISTILLATION_CC, AGENT_TRIGGERS, ABLATION_BASELINE
这些 feature 被列为 Tier 3 的原因:
1. **内部基础设施**CHICAGO_MCP, LODESTONEAnthropic 内部使用,外部无法运行
2. **纯 Stub 且引用低**MONITOR_TOOL, BG_SESSIONS需要大量工作才能实现
3. **实验性功能**SHOT_STATS, EXTRACT_MEMORIES尚在概念阶段
4. **辅助功能**STREAMLINED_OUTPUT, HOOK_PROMPTS影响范围小
5. **CCR 系列**:依赖远程控制基础设施,需要 BRIDGE_MODE 先完善
1. **已实现但影响范围小**CHICAGO_MCP, LODESTONE, SHOT_STATS, EXTRACT_MEMORIES, MONITOR_TOOL已在 build/dev 默认启用,主要作为其他功能的基础设施
2. **部分实现**BG_SESSIONS, TEMPLATES核心注册已实现但部分功能如任务摘要仍是 stub
3. **辅助功能**STREAMLINED_OUTPUT, HOOK_PROMPTS影响范围小
4. **CCR 系列**:依赖远程控制基础设施,需要 BRIDGE_MODE 先完善
如需深入了解某个 Tier 3 feature可以在代码库中搜索 `feature('FEATURE_NAME')` 查看具体使用场景。

View File

@@ -191,7 +191,7 @@ FEATURE_TOKEN_BUDGET=1 bun run dev
| `src/query/tokenBudget.ts` | 93 | 预算追踪器 + continue/stop 决策 |
| `src/bootstrap/state.ts:724-743` | 20 | turn 级 token 快照状态 |
| `src/constants/prompts.ts:538-551` | 14 | 系统提示注入 |
| `src/utils/attachments.ts:3829-3845` | 17 | API attachment 附加 |
| `src/utils/attachments.ts:3830-3844` | 17 | API attachment 附加 |
| `src/query.ts:280,1311-1358` | 48 | 主循环集成 |
| `src/screens/REPL.tsx:2897,2963,2138` | 20 | REPL 提交/完成/取消处理 |
| `src/components/Spinner.tsx:319-338` | 20 | 进度条 UI |

View File

@@ -158,4 +158,4 @@ FEATURE_TREE_SITTER_BASH_SHADOW=1 bun run dev
| `src/utils/bash/bashParser.ts` | 4437 | 纯 TS bash 解析器 |
| `src/utils/bash/ast.ts` | 2680 | 安全分析器(核心) |
| `src/utils/bash/treeSitterAnalysis.ts` | 507 | AST 分析辅助 |
| `src/tools/BashTool/bashPermissions.ts:1670-1810` | ~140 | 权限集成 + Shadow 遥测 |
| `packages/builtin-tools/src/tools/BashTool/bashPermissions.ts` | ~140 | 权限集成 + Shadow 遥测 |

View File

@@ -22,16 +22,16 @@ ULTRAPLAN 在用户输入中检测 "ultraplan" 关键字时,自动进入增强
| 模块 | 文件 | 行数 | 状态 |
|------|------|------|------|
| 命令处理器 | `src/commands/ultraplan.tsx` | 472 | **完整** |
| CCR 会话 | `src/utils/ultraplan/ccrSession.ts` | 350 | **完整** |
| 关键字检测 | `src/utils/ultraplan/keyword.ts` | 128 | **完整** |
| 命令处理器 | `src/commands/ultraplan.tsx` | 525 | **完整** |
| CCR 会话 | `src/utils/ultraplan/ccrSession.ts` | 349 | **完整** |
| 关键字检测 | `src/utils/ultraplan/keyword.ts` | 127 | **完整** |
| 嵌入式提示 | `src/utils/ultraplan/prompt.txt` | 1 | **完整** |
| REPL 对话框 | `src/screens/REPL.tsx` | — | **布线** |
| 关键字高亮 | `src/components/PromptInput/PromptInput.tsx` | — | **布线** |
### 2.2 关键字检测
文件:`src/utils/ultraplan/keyword.ts`128 行)
文件:`src/utils/ultraplan/keyword.ts`127 行)
`findUltraplanTriggerPositions(text)` 智能过滤:
- 排除引号内的 "ultraplan"
@@ -41,7 +41,7 @@ ULTRAPLAN 在用户输入中检测 "ultraplan" 关键字时,自动进入增强
### 2.3 CCR 远程会话
文件:`src/utils/ultraplan/ccrSession.ts`350 行)
文件:`src/utils/ultraplan/ccrSession.ts`349 行)
`ExitPlanModeScanner` 类实现完整的事件状态机:
- `pollForApprovedExitPlanMode()` — 3 秒轮询间隔
@@ -99,9 +99,9 @@ FEATURE_ULTRAPLAN=1 bun run dev
| 文件 | 行数 | 职责 |
|------|------|------|
| `src/commands/ultraplan.tsx` | 472 | 斜杠命令处理器 |
| `src/utils/ultraplan/ccrSession.ts` | 350 | CCR 远程会话管理 |
| `src/utils/ultraplan/keyword.ts` | 128 | 关键字检测和替换 |
| `src/commands/ultraplan.tsx` | 525 | 斜杠命令处理器 |
| `src/utils/ultraplan/ccrSession.ts` | 349 | CCR 远程会话管理 |
| `src/utils/ultraplan/keyword.ts` | 127 | 关键字检测和替换 |
| `src/utils/ultraplan/prompt.txt` | 1 | 嵌入式提示 |
| `src/utils/processUserInput/processUserInput.ts:468` | — | 关键字重定向 |
| `src/components/PromptInput/PromptInput.tsx` | — | 彩虹高亮 |

View File

@@ -120,6 +120,6 @@ FEATURE_VOICE_MODE=1 bun run dev
| 文件 | 行数 | 职责 |
|------|------|------|
| `src/voice/voiceModeEnabled.ts` | 55 | 三层门控逻辑 |
| `src/voice/voiceModeEnabled.ts` | 54 | 三层门控逻辑 |
| `src/hooks/useVoice.ts` | — | React hook录音状态 + WebSocket |
| `src/services/voiceStreamSTT.ts` | — | STT WebSocket 流式传输 |

View File

@@ -1,7 +1,7 @@
# WEB_BROWSER_TOOL — 浏览器工具
> Feature Flag: `FEATURE_WEB_BROWSER_TOOL=1`
> 实现状态:核心实现缺失,面板为 Stub布线完整
> 实现状态:核心工具已实现,面板为 Stub布线完整
> 引用数4
## 一、功能概述
@@ -14,8 +14,8 @@ WEB_BROWSER_TOOL 让模型可以启动浏览器实例、导航网页、与页面
| 模块 | 文件 | 状态 |
|------|------|------|
| 浏览器面板 | `src/tools/WebBrowserTool/WebBrowserPanel.ts` | **Stub** — 返回 null |
| 浏览器工具 | `src/tools/WebBrowserTool/WebBrowserTool.ts` | **缺失** |
| 浏览器面板 | `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserPanel.ts` | **Stub** — 返回 null |
| 浏览器工具 | `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserTool.ts` | **已实现** |
| REPL 集成 | `src/screens/REPL.tsx` | **布线** — 渲染 WebBrowserPanel |
| 工具注册 | `src/tools.ts` | **布线** — 动态加载 |
| WebView 检测 | `src/main.tsx` | **布线**`'WebView' in Bun` 检测 |
@@ -44,8 +44,8 @@ WebBrowserPanel 在 REPL 侧边显示浏览器状态
| 模块 | 工作量 | 说明 |
|------|--------|------|
| `WebBrowserTool.ts` | | 工具 schema + Bun WebView API 执行 |
| `WebBrowserPanel.tsx` | 中 | REPL 侧边栏浏览器状态面板 |
| `WebBrowserTool.ts` | ✅ 已实现 | 工具 schema + Bun WebView API 执行 |
| `WebBrowserPanel.tsx` | 中 | REPL 侧边栏浏览器状态面板(仍为 Stub |
## 四、关键设计决策
@@ -63,7 +63,7 @@ FEATURE_WEB_BROWSER_TOOL=1 bun run dev
| 文件 | 职责 |
|------|------|
| `src/tools/WebBrowserTool/WebBrowserPanel.ts` | 面板组件stub |
| `src/tools/WebBrowserTool/WebBrowserTool.ts` | 工具实现(缺失 |
| `src/screens/REPL.tsx:273,4582` | 面板渲染 |
| `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserPanel.ts` | 面板组件stub |
| `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserTool.ts` | 工具实现(已实现 |
| `src/screens/REPL.tsx:471,5676` | 面板渲染 |
| `src/tools.ts:115-116` | 工具注册 |

View File

@@ -34,16 +34,16 @@ WebSearchTool.call()
| 模块 | 文件 | 说明 |
|------|------|------|
| 工具入口 | `src/tools/WebSearchTool/WebSearchTool.ts` | `buildTool()` 定义schema、权限、执行、输出格式化 |
| 工具 prompt | `src/tools/WebSearchTool/prompt.ts` | 搜索工具的系统提示词 |
| UI 渲染 | `src/tools/WebSearchTool/UI.tsx` | 搜索结果的终端渲染组件 |
| 适配器接口 | `src/tools/WebSearchTool/adapters/types.ts` | `WebSearchAdapter` 接口、`SearchResult`/`SearchOptions`/`SearchProgress` 类型 |
| 适配器工厂 | `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 抓取 + 正则解析 |
| 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` | 真实网络请求验证 |
| 工具入口 | `packages/builtin-tools/src/tools/WebSearchTool/WebSearchTool.ts` | `buildTool()` 定义schema、权限、执行、输出格式化 |
| 工具 prompt | `packages/builtin-tools/src/tools/WebSearchTool/prompt.ts` | 搜索工具的系统提示词 |
| UI 渲染 | `packages/builtin-tools/src/tools/WebSearchTool/UI.tsx` | 搜索结果的终端渲染组件 |
| 适配器接口 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/types.ts` | `WebSearchAdapter` 接口、`SearchResult`/`SearchOptions`/`SearchProgress` 类型 |
| 适配器工厂 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/index.ts` | `createAdapter()` 工厂函数,选择后端 |
| API 适配器 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/apiAdapter.ts` | 封装原有 `queryModelWithStreaming` 逻辑,使用 server tool |
| Bing 适配器 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 抓取 + 正则解析 |
| Brave 适配器 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/braveAdapter.ts` | Brave LLM Context API 适配与结果映射 |
| 单元测试 | `packages/builtin-tools/src/tools/WebSearchTool/__tests__/bingAdapter.test.ts`, `packages/builtin-tools/src/tools/WebSearchTool/__tests__/braveAdapter*.test.ts`, `packages/builtin-tools/src/tools/WebSearchTool/__tests__/adapterFactory.test.ts` | Bing / Brave 解析与工厂逻辑测试 |
| 集成测试 | `packages/builtin-tools/src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts`, `packages/builtin-tools/src/tools/WebSearchTool/__tests__/braveAdapter.integration.ts` | 真实网络请求验证 |
### 2.3 数据流
@@ -176,13 +176,13 @@ interface SearchProgress {
| 文件 | 职责 |
|------|------|
| `src/tools/WebSearchTool/WebSearchTool.ts` | 工具定义入口 |
| `src/tools/WebSearchTool/prompt.ts` | 搜索工具 prompt |
| `src/tools/WebSearchTool/UI.tsx` | 终端 UI 渲染 |
| `src/tools/WebSearchTool/adapters/types.ts` | 适配器接口 |
| `src/tools/WebSearchTool/adapters/index.ts` | 适配器工厂 |
| `src/tools/WebSearchTool/adapters/apiAdapter.ts` | API 服务端搜索适配器 |
| `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 解析适配器 |
| `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 单元测试 (32 cases) |
| `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 集成测试 |
| `packages/builtin-tools/src/tools/WebSearchTool/WebSearchTool.ts` | 工具定义入口 |
| `packages/builtin-tools/src/tools/WebSearchTool/prompt.ts` | 搜索工具 prompt |
| `packages/builtin-tools/src/tools/WebSearchTool/UI.tsx` | 终端 UI 渲染 |
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/types.ts` | 适配器接口 |
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/index.ts` | 适配器工厂 |
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/apiAdapter.ts` | API 服务端搜索适配器 |
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 解析适配器 |
| `packages/builtin-tools/src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 单元测试 (32 cases) |
| `packages/builtin-tools/src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 集成测试 |
| `src/tools.ts` | 工具注册 |

View File

@@ -14,17 +14,17 @@ WORKFLOW_SCRIPTS 实现基于文件的多步自动化工作流。用户可以定
| 模块 | 文件 | 状态 |
|------|------|------|
| WorkflowTool | `src/tools/WorkflowTool/WorkflowTool.ts` | **Stub**空对象 |
| Workflow 权限 | `src/tools/WorkflowTool/WorkflowPermissionRequest.ts` | **Stub**返回 null |
| 常量 | `src/tools/WorkflowTool/constants.ts` | **Stub**工具名 |
| 命令创建 | `src/tools/WorkflowTool/createWorkflowCommand.ts` | **Stub**空操作 |
| 捆绑工作流 | `src/tools/WorkflowTool/bundled/` | **缺失**目录不存在 |
| WorkflowTool | `packages/builtin-tools/src/tools/WorkflowTool/WorkflowTool.ts` | **部分实现**tool schema + 渲染完整call 返回运行时缺失提示 |
| Workflow 权限 | `packages/builtin-tools/src/tools/WorkflowTool/WorkflowPermissionRequest.tsx` | **部分实现**权限请求组件 |
| 常量 | `packages/builtin-tools/src/tools/WorkflowTool/constants.ts` | **实现** — 工具名 + 目录名 + 文件扩展名常量 |
| 命令创建 | `packages/builtin-tools/src/tools/WorkflowTool/createWorkflowCommand.ts` | **实现**扫描 .claude/workflows/ 目录创建 Command 对象 |
| 捆绑工作流 | `packages/builtin-tools/src/tools/WorkflowTool/bundled/index.ts` | **实现**内置工作流初始化 |
| 本地工作流任务 | `src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts` | **Stub** — 类型 + 空操作 |
| UI 任务组件 | `src/components/tasks/src/tasks/LocalWorkflowTask/` | **Stub** — 空导出 |
| 详情对话框 | `src/components/tasks/WorkflowDetailDialog.ts` | **Stub** — 返回 null |
| 任务注册 | `src/tasks.ts` | **布线** — 动态加载 |
| 工具注册 | `src/tools.ts` | **布线**包含 bundled 工作流初始化 |
| 命令注册 | `src/commands.ts` | **布线**`/workflows` 命令 |
| 工具注册 | `src/tools.ts` | **布线**动态加载 + bundled 工作流初始化 (行 131-134,235) |
| 命令注册 | `src/commands.ts` | **布线**`/workflows` 命令 (行 93-95,395,460) |
### 2.2 预期数据流
@@ -69,13 +69,9 @@ steps:
| 优先级 | 模块 | 工作量 | 说明 |
|--------|------|--------|------|
| 1 | `WorkflowTool.ts` | 大 | Schema 定义 + 多步执行引擎 |
| 2 | `bundled/index.js` | | 内置工作流定义initBundledWorkflows |
| 3 | `createWorkflowCommand.ts` | 中 | 从文件解析创建命令对象 |
| 4 | `LocalWorkflowTask.ts` | 大 | 步骤协调、kill/skip/retry |
| 5 | `WorkflowDetailDialog.ts` | 中 | 进度详情 UI |
| 6 | `WorkflowPermissionRequest.ts` | 小 | 权限对话框 |
| 7 | `constants.ts` | 小 | 工具名常量 |
| 1 | `WorkflowTool.ts` call 方法 | 中 | 实际工作流执行逻辑(当前返回运行时缺失提示) |
| 2 | `LocalWorkflowTask.ts` | | 步骤协调、kill/skip/retry |
| 3 | `WorkflowDetailDialog.ts` | 中 | 进度详情 UI |
## 四、关键设计决策
@@ -95,11 +91,12 @@ FEATURE_WORKFLOW_SCRIPTS=1 bun run dev
| 文件 | 职责 |
|------|------|
| `src/tools/WorkflowTool/WorkflowTool.ts` | 工具定义(stub |
| `src/tools/WorkflowTool/WorkflowPermissionRequest.ts` | 权限对话框stub |
| `src/tools/WorkflowTool/constants.ts` | 常量stub |
| `src/tools/WorkflowTool/createWorkflowCommand.ts` | 命令创建(stub |
| `packages/builtin-tools/src/tools/WorkflowTool/WorkflowTool.ts` | 工具定义(部分实现 |
| `packages/builtin-tools/src/tools/WorkflowTool/WorkflowPermissionRequest.tsx` | 权限请求组件 |
| `packages/builtin-tools/src/tools/WorkflowTool/constants.ts` | 常量定义 |
| `packages/builtin-tools/src/tools/WorkflowTool/createWorkflowCommand.ts` | 命令创建(已实现 |
| `packages/builtin-tools/src/tools/WorkflowTool/bundled/index.ts` | 内置工作流初始化 |
| `src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts` | 任务协调stub |
| `src/components/tasks/WorkflowDetailDialog.ts` | 详情对话框stub |
| `src/tools.ts:127-132` | 工具注册 |
| `src/commands.ts:86-89` | 命令注册 |
| `src/tools.ts:131-134,235` | 工具注册 |
| `src/commands.ts:93-95,395,460` | 命令注册 |

View File

@@ -17,7 +17,7 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic
`BUILD_TARGET` 等构建时常量在反编译版本中已被移除。`USER_TYPE` 通过 Bun 的 `--define` 或环境变量注入Bun 会进行**常量折叠**——所有 `process.env.USER_TYPE === 'ant'` 在外部构建中直接变为 `false`,后续代码被 DCE 移除。但在反编译版本中,这些代码保留完整。
`USER_TYPE === 'ant'` 在代码库中出现 **377+ 次**含 `=== 'ant'` 291 次、`(process.env.USER_TYPE) === 'ant'` 86 次),另有 `!== 'ant'` 53、其他引用约 35 次,总计 **465 处引用**控制着工具、命令、API、UI 等方方面面。
`USER_TYPE === 'ant'` 在代码库中出现 **351+ 次**跨 163 个文件),另有 `!== 'ant'` 59(跨 38 个文件),总计 **410+ 处引用**控制着工具、命令、API、UI 等方方面面。
## Ant-Only 工具
@@ -25,10 +25,10 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic
| 工具 | 代码位置 | 用途 |
|------|---------|------|
| **REPLTool** | `src/tools/REPLTool/` | 高级 REPL 模式——在 VM 中包装 Bash/Read/Edit/Glob/Grep/Agent 等工具 |
| **SuggestBackgroundPRTool** | `src/tools/SuggestBackgroundPRTool/` | 建议在后台创建 PR |
| **ConfigTool** | `src/tools/ConfigTool/` | 交互式配置编辑器,包含 Gates 标签页用于覆盖 GrowthBook flags |
| **TungstenTool** | `src/tools/TungstenTool/` | 基于 tmux 的终端面板工具(反编译版中已 stub |
| **REPLTool** | `packages/builtin-tools/src/tools/REPLTool/` | 高级 REPL 模式——在 VM 中包装 Bash/Read/Edit/Glob/Grep/Agent 等工具 |
| **SuggestBackgroundPRTool** | `packages/builtin-tools/src/tools/SuggestBackgroundPRTool/` | 建议在后台创建 PR |
| **ConfigTool** | `packages/builtin-tools/src/tools/ConfigTool/` | 交互式配置编辑器,包含 Gates 标签页用于覆盖 GrowthBook flags |
| **TungstenTool** | `packages/builtin-tools/src/tools/TungstenTool/` | 基于 tmux 的终端面板工具(反编译版中已 stub |
```typescript
// src/tools.ts 第 14-24 行——条件导入 + Dead Code Elimination 标记
@@ -36,18 +36,18 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic
/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
const REPLTool =
process.env.USER_TYPE === 'ant'
? require('./tools/REPLTool/REPLTool.js').REPLTool
? require('@claude-code-best/builtin-tools/tools/REPLTool/REPLTool.js').REPLTool
: null
const SuggestBackgroundPRTool =
process.env.USER_TYPE === 'ant'
? require('./tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js')
? require('@claude-code-best/builtin-tools/tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js')
.SuggestBackgroundPRTool
: null
```
## Ant-Only 命令
`src/commands.ts` 注册了 **28** 个仅在内部构建中可用的斜杠命令(`INTERNAL_ONLY_COMMANDS`lines 225-254),在 `USER_TYPE === 'ant' && !IS_DEMO` 时才加载line 343-345
`src/commands.ts` 注册了 **24+** 个仅在内部构建中可用的斜杠命令(`INTERNAL_ONLY_COMMANDS`lines 267-295),在 `USER_TYPE === 'ant' && !IS_DEMO` 时才加载line 400-401
<AccordionGroup>
<Accordion title="调试类">
@@ -74,7 +74,7 @@ const SuggestBackgroundPRTool =
- `summary` — 生成摘要
- `subscribePr` — 订阅 PR需要 `KAIROS_GITHUB_WEBHOOKS` feature flag
- `forceSnip` — 强制截断历史(需要 `HISTORY_SNIP` feature flag
- `ultraplan` — 超级规划(需要 `ULTRAPLAN` feature flag
- `ultraplan` — 超级规划(需要 `ULTRAPLAN` feature flag,单独注册于 `commands.ts:396`
</Accordion>
<Accordion title="基础设施类">
- `backfillSessions` — 回填会话数据

View File

@@ -15,17 +15,19 @@ Claude Code 使用 Bun 打包器的 `bun:bundle` 模块提供编译时特性门
import { feature } from 'bun:bundle'
const SleepTool = feature('PROACTIVE') || feature('KAIROS')
? require('./tools/SleepTool/SleepTool.js').SleepTool
? require('@claude-code-best/builtin-tools/tools/SleepTool/SleepTool.js').SleepTool
: null
```
在 Anthropic 的内部构建中,`feature()` 在打包时被求值——返回 `true` 的代码会被保留,返回 `false` 的代码会被 **Dead Code Elimination (DCE)** 彻底移除。
在我们的反编译版本中,这个函数被兜底为
在我们的反编译版本中,`feature` 从 `bun:bundle` 导入(声明在 `src/types/internal-modules.d.ts`),在运行时始终返回 `false`
```typescript
// src/entrypoints/cli.tsx 第 3 行
const feature = (_name: string) => false;
// src/types/internal-modules.d.ts
declare module 'bun:bundle' {
export function feature(name: string): boolean;
}
```
这意味着所有 88+ 个 feature flag 后的代码**在运行时永远不会执行**,但代码本身完整保留,可以阅读和分析。
@@ -79,7 +81,7 @@ Feature flags 在代码中主要有三种使用模式:
```typescript
// src/tools.ts — 最常见的模式
const MonitorTool = feature('MONITOR_TOOL')
? require('./tools/MonitorTool/MonitorTool.js').MonitorTool
? require('@claude-code-best/builtin-tools/tools/MonitorTool/MonitorTool.js').MonitorTool
: null
```

View File

@@ -19,7 +19,7 @@ keywords: ["GrowthBook", "A/B 测试", "运行时门控", "tengu", "渐进式发
## 集成架构
GrowthBook 的完整实现位于 `src/services/analytics/growthbook.ts`1156 行),工作流程如下:
GrowthBook 的完整实现位于 `src/services/analytics/growthbook.ts`1258 行),工作流程如下:
<Steps>
<Step title="启动时获取远程配置">

View File

@@ -84,7 +84,7 @@ keywords: ["隐藏功能", "未公开功能", "秘密功能", "Claude Code 彩
<Accordion title="VOICE_MODE语音交互">
**门控**: `feature('VOICE_MODE')`
代码中存在语音输入模式的注册点,核心实现依赖 `audio-napi` 包(在反编译版本中已 stub
代码中存在语音输入模式的注册点,核心实现依赖 `audio-capture-napi` 包(已恢复
- 通过 `/voice` 命令激活
- "按住说话"hold-to-talk交互模式

View File

@@ -64,24 +64,27 @@ Claude Code 从上到下分为五个层次,每一层职责清晰、边界分
needsFollowUp ? continue : return { reason }
```
完整的状态机通过 `State` 类型(`src/query.ts:204`)在迭代间传递,包含 10 个字段messages、autoCompactTracking、maxOutputTokensRecoveryCount 等)。
完整的状态机通过 `State` 类型(`src/query.ts:207`)在迭代间传递,包含 10 个字段messages、autoCompactTracking、maxOutputTokensRecoveryCount 等)。
### 4. 工具层(`src/tools.ts` → `src/Tool.ts`
`getAllBaseTools()``src/tools.ts:191`)组装 50+ 工具列表,经过 `filterToolsByDenyRules()` 权限过滤后传给 API。
`getAllBaseTools()``src/tools.ts:195`)组装 50+ 工具列表,经过 `filterToolsByDenyRules()` 权限过滤后传给 API。
每个工具实现 `Tool<Input, Output, Progress>` 接口(`src/Tool.ts:362`),核心方法链:
每个工具实现 `Tool<Input, Output, Progress>` 接口(`src/Tool.ts:368`),核心方法链:
```
validateInput() → canUseTool()UI 层)→ checkPermissions() → call() → ToolResult
```
### 5. 通信层(`src/services/api/claude.ts`
API 客户端支持 4 种 Provider
- **Anthropic Direct**:默认
API 客户端支持 7 种 Provider
- **Anthropic Direct (firstParty)**:默认
- **AWS Bedrock**`ANTHROPIC_BEDROCK_BASE_URL`
- **Google Vertex**`ANTHROPIC_VERTEX_PROJECT_ID`
- **Azure**:通过自定义 base URL
- **Foundry**`ANTHROPIC_CODE_USE_FOUNDRY`
- **OpenAI**:兼容层
- **Gemini**:兼容层
- **Grok (xAI)**:兼容层
`deps.callModel()` 发起流式请求,返回 `BetaRawMessageStreamEvent` 事件流。支持 Prompt Cache`cache_control`、thinking blocks、multi-turn tool use。

View File

@@ -53,7 +53,7 @@ Claude Code 是一个**运行在本地终端中的 agentic coding system**。它
│ 实际执行: 读文件、运行命令、搜索代码... │
├─────────────────────────────────────────────────────────┤
│ 6. 通信层 (claude.ts → Anthropic API) │
│ 流式 HTTP, 支持 Bedrock/Vertex/Azure 多 provider │
│ 流式 HTTP, 支持 Bedrock/Vertex/Foundry 等 7 种 provider │
└─────────────────────────────────────────────────────────┘
```

View File

@@ -49,7 +49,7 @@ AI 没有真正的"记忆"Claude Code 通过精心分层营造了这个幻觉
### 3. 工具系统的权限双轨制
`src/tools/BashTool/shouldUseSandbox.ts` 展示了一个精巧的双重安全模型:
`packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.ts` 展示了一个精巧的双重安全模型:
- **应用层**:权限规则决定"能不能执行"(白名单/黑名单/用户确认)
- **OS 层**:沙箱决定"执行时能做什么"(文件系统/网络/进程隔离)

View File

@@ -65,7 +65,7 @@ ENABLE_LSP_TOOL=1 bun run dev
```
┌─────────────────────────────────────────────────────┐
│ LSP Tool │
│ src/tools/LSPTool/LSPTool.ts
packages/builtin-tools/src/tools/LSPTool/LSPTool.ts│
│ (Claude 可调用的工具9 种操作) │
└──────────────────────┬──────────────────────────────┘
@@ -128,10 +128,10 @@ LSP 服务器会异步推送 `textDocument/publishDiagnostics` 通知,经去
| `src/services/lsp/config.ts` | 从插件加载 LSP 服务器配置 |
| `src/services/lsp/LSPDiagnosticRegistry.ts` | 诊断信息注册、去重、容量限制 |
| `src/services/lsp/passiveFeedback.ts` | 注册 `publishDiagnostics` 通知处理器 |
| `src/tools/LSPTool/LSPTool.ts` | LSP Tool 实现(暴露给 Claude |
| `src/tools/LSPTool/schemas.ts` | 输入 schema9 种操作的 discriminated union |
| `src/tools/LSPTool/formatters.ts` | 各操作结果的格式化 |
| `src/tools/LSPTool/prompt.ts` | Tool 描述文本 |
| `packages/builtin-tools/src/tools/LSPTool/LSPTool.ts` | LSP Tool 实现(暴露给 Claude |
| `packages/builtin-tools/src/tools/LSPTool/schemas.ts` | 输入 schema9 种操作的 discriminated union |
| `packages/builtin-tools/src/tools/LSPTool/formatters.ts` | 各操作结果的格式化 |
| `packages/builtin-tools/src/tools/LSPTool/prompt.ts` | Tool 描述文本 |
| `src/utils/plugins/lspPluginIntegration.ts` | 从插件加载、验证、环境变量解析、作用域管理 |
## LSP Tool 支持的操作

View File

@@ -137,7 +137,7 @@ Auto mode 可通过以下方式激活:
### 进入时Full Instructions
注入到对话中的指令(`messages.ts:3464`
注入到对话中的指令(`messages.ts:3481`
> Auto mode is active. The user chose continuous, autonomous execution. You should:
>

View File

@@ -18,17 +18,19 @@ keywords: ["权限模型", "Allow Ask Deny", "PermissionRule", "checkPermissions
这些行为由 `PermissionResult` 类型定义(`src/utils/permissions/PermissionResult.ts`)。
## 权限规则的五层来源
## 权限规则的来源
规则从 5 个来源汇聚(`PERMISSION_RULE_SOURCES``permissions.ts:109`),优先级从高到低:
规则从 8 个来源汇聚(`PERMISSION_RULE_SOURCES``permissions.ts:109`),优先级从低到高(后者覆盖前者)
```
1. session — 用户在当前对话中手动授权("Always allow"
2. cliArg — 命令行 --allow/--deny 参数
3. command — Skill 工具的 allowedTools 白名单
4. projectSettings — .claude/settings.json团队共享
5. userSettings — ~/.claude/settings.json跨项目
6. policySettings — 企业管理员下发的策略(用户不可覆盖)
1. userSettings — ~/.claude/settings.json跨项目
2. projectSettings — .claude/settings.json团队共享
3. localSettings — .claude/settings.local.jsongitignored个人覆盖
4. flagSettings — --settings 命令行参数
5. policySettings — 企业管理员下发的策略(用户不可覆盖
6. cliArg — 命令行 --allow/--deny 参数
7. command — Skill 工具的 allowedTools 白名单
8. session — 用户在当前对话中手动授权("Always allow"
```
每个来源维护三个数组:`alwaysAllowRules[source]`、`alwaysAskRules[source]`、`alwaysDenyRules[source]`。
@@ -65,7 +67,7 @@ MCP 工具使用 `getToolNameForPermissionCheck()` 获取匹配名称,支持
**2. 命令模式匹配**BashTool 的 `checkPermissions()`
BashTool 通过 `preparePermissionMatcher()``Tool.ts:514`)解析命令模式:
BashTool 通过 `preparePermissionMatcher()``Tool.ts:520`)解析命令模式:
```json
{"tool": "Bash", "ruleContent": "git *"} → 匹配 "git commit -m 'fix'"
```
@@ -120,7 +122,9 @@ Read/Edit/Write 工具通过 `getPath()` 提取文件路径,与 `ruleContent`
|------|---------------------|---------|------|
| **Default** | `'default'` | 日常使用 | 敏感操作逐一确认 |
| **Plan Mode** | `'plan'` | 探索阶段 | 只能读不能写(`isReadOnly()` 检查) |
| **Auto** | `'auto'` | 信任 AI | 通过 transcript classifier 自动决策 |
| **Accept Edits** | `'acceptEdits'` | 快速迭代 | 工作区内文件编辑自动放行,其他操作仍需确认 |
| **Don't Ask** | `'dontAsk'` | 减少打断 | 尽量自动决策,减少确认弹窗 |
| **Auto** | `'auto'` | 信任 AI | 通过 transcript classifier 自动决策(需 `TRANSCRIPT_CLASSIFIER` feature flag |
| **Bypass** | `'bypassPermissions'` | 完全信任 | 所有操作自动放行(需显式 `--dangerously-skip-permissions` |
Plan Mode 切换由 `EnterPlanModeTool.call()` 触发:
@@ -143,8 +147,8 @@ context.setAppState(prev => ({
```typescript
const DENIAL_LIMITS = {
maxDenialsPerTool: 3, // 同一工具连续拒绝上限
cooldownPeriodMs: 30_000, // 冷却期 30 秒
maxConsecutive: 3, // 同一工具连续拒绝上限
maxTotal: 20, // 总拒绝上限
}
```
@@ -162,9 +166,12 @@ const DENIAL_LIMITS = {
```typescript
type PermissionUpdate =
| { type: 'addRule', behavior, rule, destination }
| { type: 'removeRule', behavior, rule, destination }
| { type: 'setMode', mode, destination }
| { type: 'addRules', destination, rules, behavior }
| { type: 'replaceRules', destination, rules, behavior }
| { type: 'removeRules', destination, rules, behavior }
| { type: 'setMode', destination, mode }
| { type: 'addDirectories', destination, directories }
| { type: 'removeDirectories', destination, directories }
```
当用户在 Ask 对话框中选择 "Always allow",系统调用 `persistPermissionUpdates()` 将规则写入对应层级的 settings 文件project/user/managed同时更新内存中的 `toolPermissionContext`。

View File

@@ -16,13 +16,13 @@ keywords: ["Plan Mode", "计划模式", "EnterPlanMode", "ExitPlanMode", "prepar
<Steps>
<Step title="EnterPlanMode — 进入计划模式">
AI 自主判断(或用户触发)任务需要规划,调用 `EnterPlanModeTool``src/tools/EnterPlanModeTool/EnterPlanModeTool.ts:36`)。该工具需要**用户审批**`checkPermissions` 返回 `ask`)。
AI 自主判断(或用户触发)任务需要规划,调用 `EnterPlanModeTool``packages/builtin-tools/src/tools/EnterPlanModeTool/EnterPlanModeTool.ts:36`)。该工具需要**用户审批**`checkPermissions` 返回 `ask`)。
</Step>
<Step title="探索阶段 — 只读工具集">
权限模式切换为 `'plan'`AI 只能使用 `isReadOnly()` 为 true 的工具Read、Grep、Glob、Agent 等)。写操作被自动拒绝。
</Step>
<Step title="ExitPlanMode — 提交方案审批">
AI 完成探索后,调用 `ExitPlanModeV2Tool``src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts:147`),将计划文件提交给用户审阅。这是第二个**需要用户审批**的节点。
AI 完成探索后,调用 `ExitPlanModeV2Tool``packages/builtin-tools/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts:147`),将计划文件提交给用户审阅。这是第二个**需要用户审批**的节点。
</Step>
<Step title="恢复执行 — 全部工具权限">
用户批准后权限模式恢复为进入前的状态AI 按计划执行。
@@ -107,7 +107,7 @@ if (isTeammate()) {
## 什么时候该用计划模式
`EnterPlanModeTool` 的 Prompt`src/tools/EnterPlanModeTool/prompt.ts`)定义了两套触发标准——外部版本更积极(鼓励规划),内部版本更克制(仅在真正模糊时使用):
`EnterPlanModeTool` 的 Prompt`packages/builtin-tools/src/tools/EnterPlanModeTool/prompt.ts`)定义了两套触发标准——外部版本更积极(鼓励规划),内部版本更克制(仅在真正模糊时使用):
| 场景 | 外部版本 | 内部版本 |
|------|---------|---------|

View File

@@ -166,7 +166,7 @@ keywords: ["沙箱", "sandbox", "权限", "Bash", "PowerShell", "bubblewrap", "s
5. 这条命令没有被显式排除
6. 这次调用没有被允许以 `dangerouslyDisableSandbox` 绕过
对应入口在 `src/tools/BashTool/shouldUseSandbox.ts` 和 `src/utils/sandbox/sandbox-adapter.ts`。
对应入口在 `packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.ts` 和 `src/utils/sandbox/sandbox-adapter.ts`。
### 3. PowerShell 只在支持平台上走
@@ -518,11 +518,11 @@ REPL / CLI 启动
如果你想继续顺着源码深入,推荐按下面顺序看:
1. `src/tools/BashTool/shouldUseSandbox.ts`
1. `packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.ts`
2. `src/utils/Shell.ts`
3. `src/utils/sandbox/sandbox-adapter.ts`
4. `src/utils/permissions/permissions.ts`
5. `src/tools/BashTool/bashPermissions.ts`
5. `packages/builtin-tools/src/tools/BashTool/bashPermissions.ts`
6. `src/utils/permissions/pathValidation.ts`
7. `src/utils/permissions/filesystem.ts`

View File

@@ -61,7 +61,7 @@ Claude 的 System Prompt 中包含安全指令——这是"软性"约束,依
| `deny` | 直接拒绝 | 匹配 deny 规则 |
| `ask` | 弹窗确认 | 未匹配任何规则 或 匹配 ask 规则 |
以 BashTool 为例(`src/tools/BashTool/bashPermissions.ts``bashToolHasPermission()` 执行了极其细致的检查链:
以 BashTool 为例(`packages/builtin-tools/src/tools/BashTool/bashPermissions.ts``bashToolHasPermission()` 执行了极其细致的检查链:
1. **AST 安全解析**:用 tree-sitter 解析 bash AST检测命令注入`$()`、反引号等)
2. **语义检查**:识别危险命令(`eval`、`exec`、`source` 等)
@@ -169,7 +169,7 @@ Bash("rm -rf node_modules") → ⚠️ 需确认(不可逆)
攻击cd /malicious/dir && git status
/malicious/dir 包含 bare repo + 恶意钩子
防御bashToolHasPermission() 检测 cd + git 组合
强制 require approvalsrc/tools/BashTool/bashPermissions.ts:2209
强制 require approvalpackages/builtin-tools/src/tools/BashTool/bashPermissions.ts:2209
```
### 场景3管道注入
@@ -178,5 +178,5 @@ Bash("rm -rf node_modules") → ⚠️ 需确认(不可逆)
攻击echo 'x' | xargs printf '%s' >> /etc/passwd
splitCommand 会剥离重定向,导致路径检查遗漏
防御:即使管道段独立检查通过,仍对原始命令重新验证路径约束
检查重定向目标中的危险模式(反引号、$()bashPermissions.ts:1992-2056
检查重定向目标中的危险模式(反引号、$()packages/builtin-tools/src/tools/BashTool/bashPermissions.ts:1992-2056
```

View File

@@ -12,9 +12,9 @@
## 背景
- 命令入口只有 fast-path (`src/entrypoints/cli.tsx:249`)
- 命令入口只有 fast-path (`src/entrypoints/cli.tsx:272`)
- handler 是空的 (`src/cli/handlers/templateJobs.ts`)
- `markdownConfigLoader` 已把 `templates` 纳入配置目录 (`src/utils/markdownConfigLoader.ts:29`)
- `markdownConfigLoader` 已把 `templates` 纳入配置目录 (`src/utils/markdownConfigLoader.ts:35`)
- `query/stopHooks` 已预留 job classifier 链路 (`src/query/stopHooks.ts:103`)
- `jobs/classifier.ts` 仍是 stub (`src/jobs/classifier.ts`)

View File

@@ -18,8 +18,8 @@
- `src/assistant/sessionHistory.ts`
- 真正 stub 的主要是:
- `src/assistant/sessionDiscovery.ts`
- `src/assistant/AssistantSessionChooser.ts`
- `src/commands/assistant/assistant.ts:7`
- `src/assistant/AssistantSessionChooser.tsx`
- `src/commands/assistant/assistant.tsx:7`
- `src/assistant/index.ts`
## 分阶段实现

View File

@@ -22,14 +22,14 @@ Read 的 `maxResultSizeChars` 是 `Infinity`,但这并不意味着无限制输
## FileRead多模态文件读取引擎
源码路径:`src/tools/FileReadTool/FileReadTool.ts`
源码路径:`packages/builtin-tools/src/tools/FileReadTool/FileReadTool.ts`
### 读取去重机制
Read 工具有一个常被忽视但至关重要的**去重层**。当 AI 重复读取同一个文件的同一范围时,系统不会浪费 token 发送两份完整内容:
```typescript
// FileReadTool.ts:530-573 — 去重逻辑
// FileReadTool.ts — 去重逻辑
const existingState = readFileState.get(fullFilePath)
if (existingState && !existingState.isPartialView && existingState.offset !== undefined) {
const rangeMatch = existingState.offset === offset && existingState.limit === limit
@@ -83,7 +83,7 @@ Read 工具在 `validateInput()` 中设置了多层安全门:
当文件不存在时Read 不会只报一个 "file not found"
```typescript
// FileReadTool.ts:639-647
// FileReadTool.ts
const similarFilename = findSimilarFile(fullFilePath) // 相似扩展名
const cwdSuggestion = await suggestPathUnderCwd(fullFilePath) // cwd 相对路径建议
// macOS 截图特殊处理:薄空格(U+202F) vs 普通空格
@@ -94,7 +94,7 @@ const altPath = getAlternateScreenshotPath(fullFilePath)
## FileEdit精确字符串替换引擎
源码路径:`src/tools/FileEditTool/FileEditTool.ts` + `utils.ts`
源码路径:`packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts` + `utils.ts`
### 引号标准化AI 无法输出的字符怎么办
@@ -138,7 +138,7 @@ Edit 工具在 `validateInput()` 中检查两个条件:
2. **文件未被外部修改**`mtime` 未变,或全量读取时内容完全一致)
```typescript
// FileEditTool.ts:290-311 — Windows 特殊处理
// FileEditTool.ts — Windows 特殊处理
const isFullRead = readTimestamp.offset === undefined && readTimestamp.limit === undefined
if (isFullRead && fileContent === readTimestamp.content) {
// 内容不变安全继续Windows 云同步/杀毒可能改 mtime
@@ -157,7 +157,7 @@ const MAX_EDIT_FILE_SIZE = 1024 * 1024 * 1024 // 1 GiB
## FileWrite全量写入与创建
源码路径:`src/tools/FileWriteTool/FileWriteTool.ts`
源码路径:`packages/builtin-tools/src/tools/FileWriteTool/FileWriteTool.ts`
Write 工具与 Edit 共享大部分基础设施权限检查、mtime 校验、fileHistory 备份),但有两个关键差异:

View File

@@ -77,7 +77,7 @@ Glob 默认把**最近修改的文件排在前面**。这不是默认的文件
实际效果AI 优先看到"活"的代码,而不是沉寂的历史文件
```
在 `src/tools/GlobTool/` 中ripgrep 的输出在返回给 AI 前按 mtime 排序。
在 `packages/builtin-tools/src/tools/GlobTool/` 中ripgrep 的输出在返回给 AI 前按 mtime 排序。
### ripgrep 的错误处理
@@ -92,7 +92,7 @@ ripgrep 执行有专门的错误恢复链(`src/utils/ripgrep.ts`
## ToolSearch在 50+ 工具中发现目标
当可用工具超过 50 个时(含 MCP 提供的外部工具AI 可能不知道该用哪个。**ToolSearch**`src/tools/ToolSearchTool/`)提供了工具发现机制。
当可用工具超过 50 个时(含 MCP 提供的外部工具AI 可能不知道该用哪个。**ToolSearch**`packages/builtin-tools/src/tools/ToolSearchTool/`)提供了工具发现机制。
### 搜索算法
@@ -139,14 +139,14 @@ function getDeferredToolsCacheKey(deferredTools: Tools): string {
AI 的信息获取不局限于本地代码:
- **WebSearch**`src/tools/WebSearchTool/`):调用 Anthropic API 的 `web_search_20250305` server tool 搜索互联网
- **WebFetch**`src/tools/WebFetchTool/`):抓取特定 URL 内容,转换为 Markdown 供 AI 阅读
- **WebSearch**`packages/builtin-tools/src/tools/WebSearchTool/`):调用 Anthropic API 的 `web_search_20250305` server tool 搜索互联网
- **WebFetch**`packages/builtin-tools/src/tools/WebFetchTool/`):抓取特定 URL 内容,转换为 Markdown 供 AI 阅读
这让 AI 可以查阅文档、搜索 Stack Overflow、阅读 GitHub issue——和人类开发者的工作方式一致。
### WebSearch 实现机制
WebSearch 通过适配器模式支持三种搜索后端,由 `src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
WebSearch 通过适配器模式支持三种搜索后端,由 `packages/builtin-tools/src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
```
适配器架构:
@@ -229,7 +229,7 @@ WebSearch 通过适配器模式支持三种搜索后端,由 `src/tools/WebSear
### WebSearchTool 统一接口
`WebSearchTool``src/tools/WebSearchTool/WebSearchTool.ts`)是面向主循环的工具定义,所有 provider 均可使用(`isEnabled()` 始终返回 true。它将适配器返回的 `SearchResult[]` 转换为内部 `Output` 格式,`mapToolResultToToolResultBlockParam` 将搜索结果格式化为带 markdown 超链接的文本,并附加 "REMINDER" 要求主模型在回复中包含 Sources。
`WebSearchTool``packages/builtin-tools/src/tools/WebSearchTool/WebSearchTool.ts`)是面向主循环的工具定义,所有 provider 均可使用(`isEnabled()` 始终返回 true。它将适配器返回的 `SearchResult[]` 转换为内部 `Output` 格式,`mapToolResultToToolResultBlockParam` 将搜索结果格式化为带 markdown 超链接的文本,并附加 "REMINDER" 要求主模型在回复中包含 Sources。
### WebFetch 实现机制
@@ -264,7 +264,7 @@ WebFetch 是一个完整的 HTTP 客户端 + 内容处理管线:
| **URL 验证** | `validateURL()` | 长度、协议、用户名密码、公网域名检查 |
| **egress 检测** | `X-Proxy-Error: blocked-by-allowlist` | 检测企业代理拦截 |
预批准域名(`src/tools/WebFetchTool/preapproved.ts`
预批准域名(`packages/builtin-tools/src/tools/WebFetchTool/preapproved.ts`
用户无需手动授权即可抓取的域名列表,包含 ~90 个主流技术文档站点MDN、Python docs、React docs、AWS docs 等)。列表分为 hostname-only 和 path-prefix 两类,查找复杂度 O(1)。

View File

@@ -31,7 +31,7 @@ spawn(wrapped_command) ← 实际进程创建
## 只读命令的判定:为什么 Read 免审批而 Bash 不一定
BashTool 的 `isReadOnly()` 方法(`BashTool.tsx:437`)决定一条命令是否被视为"只读"
BashTool 的 `isReadOnly()` 方法(`packages/builtin-tools/src/tools/BashTool/BashTool.tsx:655`)决定一条命令是否被视为"只读"
```typescript
isReadOnly(input) {
@@ -41,7 +41,7 @@ isReadOnly(input) {
}
```
判定逻辑基于 4 个命令集合(`BashTool.tsx:60-78`
判定逻辑基于 4 个命令集合(`BashTool.tsx:120-166`
| 集合 | 命令 | 性质 |
|------|------|------|
@@ -53,7 +53,7 @@ isReadOnly(input) {
对于复合命令(`ls dir && echo "---" && ls dir2`),系统拆分后逐段检查——**所有非中性段都必须属于上述集合**,整条命令才被视为只读。
```typescript
// BashTool.tsx:95 — 简化的判定逻辑
// BashTool.tsx — 简化的判定逻辑
for (const part of partsWithOperators) {
if (BASH_SEMANTIC_NEUTRAL_COMMANDS.has(baseCommand)) continue // 跳过中性段
if (!isPartSearch && !isPartRead && !isPartList) {
@@ -64,7 +64,7 @@ for (const part of partsWithOperators) {
## AST 安全解析tree-sitter bash 解析
`preparePermissionMatcher()``BashTool.tsx:445`)在权限检查前用 `parseForSecurity()` 解析命令结构:
`preparePermissionMatcher()``BashTool.tsx:663`)在权限检查前用 `parseForSecurity()` 解析命令结构:
```typescript
async preparePermissionMatcher({ command }) {
@@ -92,14 +92,14 @@ getDefaultTimeoutMs()
└── 最大上限600,000ms10 分钟,用户显式设置时)
```
超时后系统不会直接杀进程——`ShellCommand``src/utils/ShellCommand.ts:129`)通过 `onTimeout` 回调通知调用方,由调用方决定是终止还是后台化。
超时后系统不会直接杀进程——`ShellCommand``src/utils/ShellCommand.ts:144`)通过 `onTimeout` 回调通知调用方,由调用方决定是终止还是后台化。
## 自动后台化
长时间运行的命令可以自动转为后台任务,不阻塞 AI 的 agentic loop
```typescript
// BashTool.tsx:880
// BashTool.tsx:1158
const shouldAutoBackground = !isBackgroundTasksDisabled
&& isAutobackgroundingAllowed(command)
```
@@ -148,7 +148,7 @@ Claude Code 为文件读写、代码搜索等操作提供了专用工具Read
| **并发安全** | `isConcurrencySafe()` 返回 `true` → 可并行执行 | Bash 命令可能有副作用,串行执行 |
| **安全审计** | 工具名精确匹配权限规则 | 需 AST 解析命令结构后匹配 |
`isConcurrencySafe()``BashTool.tsx:434`)是一个常被忽视但重要的设计——只有只读命令可以在 agentic loop 中并行执行,有副作用的命令必须串行,防止竞态条件。
`isConcurrencySafe()``BashTool.tsx:652`)是一个常被忽视但重要的设计——只有只读命令可以在 agentic loop 中并行执行,有副作用的命令必须串行,防止竞态条件。
## 进度反馈的流式设计

View File

@@ -25,7 +25,7 @@ Claude Code 的任务管理并非单一系统,而是两个并存、按运行
TodoWrite 本质是一个**全量替换**操作——每次调用传入完整的 `todos[]` 数组,完全覆盖之前的状态:
```typescript
// src/tools/TodoWriteTool/TodoWriteTool.ts — call() 核心逻辑
// packages/builtin-tools/src/tools/TodoWriteTool/TodoWriteTool.ts — call() 核心逻辑
async call({ todos }, context) {
const todoKey = context.agentId ?? getSessionId()
const oldTodos = appState.todos[todoKey] ?? []

View File

@@ -16,7 +16,7 @@ keywords: ["工具系统", "Tool 抽象", "AI 工具", "function calling", "buil
## Tool 类型35 个字段的统一接口
所有工具都实现 `src/Tool.ts:362` 的 `Tool<Input, Output, Progress>` 类型。这不是一个 class而是一个包含 35+ 字段的**结构化类型**structural typing任何满足该接口的对象就是一个工具
所有工具都实现 `src/Tool.ts:368` 的 `Tool<Input, Output, Progress>` 类型。这不是一个 class而是一个包含 35+ 字段的**结构化类型**structural typing任何满足该接口的对象就是一个工具
### 核心四要素
@@ -69,7 +69,7 @@ keywords: ["工具系统", "Tool 抽象", "AI 工具", "function calling", "buil
## 工具注册:`getTools()` 的分层组装
`src/tools.ts` 的 `getAllBaseTools()`(第 191 行)是工具注册的核心:
`src/tools.ts` 的 `getAllBaseTools()`(第 195 行)是工具注册的核心:
```
固定工具(始终可用):
@@ -96,7 +96,7 @@ Ant-only 工具:
← process.env.USER_TYPE === 'ant' ? [REPLTool, ConfigTool, TungstenTool]
```
`getTools()`(第 269 行)在 `getAllBaseTools()` 基础上应用权限过滤:
`getTools()`(第 274 行)在 `getAllBaseTools()` 基础上应用权限过滤:
```typescript
export const getTools = (permissionContext): Tools => {
@@ -110,7 +110,7 @@ export const getTools = (permissionContext): Tools => {
## `buildTool()` 工厂函数
大多数工具通过 `buildTool()` 创建(`src/Tool.ts:721`),它是一个类型安全的构造器:
大多数工具通过 `buildTool()` 创建(`src/Tool.ts:789`),它是一个类型安全的构造器:
```typescript
export const BashTool: Tool<...> = buildTool({

View File

@@ -135,6 +135,7 @@
"group": "运行模式",
"pages": [
"docs/features/kairos",
"docs/features/channels",
"docs/features/voice-mode",
"docs/features/bridge-mode",
"docs/features/remote-control-self-hosting",

View File

@@ -1,6 +1,6 @@
{
"name": "claude-code-best",
"version": "1.4.2",
"version": "1.4.4",
"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>",
@@ -37,6 +37,7 @@
"files": [
"dist",
"scripts/postinstall.cjs",
"scripts/run-parallel.mjs",
"scripts/setup-chrome-mcp.mjs"
],
"scripts": {
@@ -72,23 +73,24 @@
"@ant/computer-use-mcp": "workspace:*",
"@ant/computer-use-swift": "workspace:*",
"@anthropic-ai/bedrock-sdk": "^0.26.4",
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
"@anthropic-ai/claude-agent-sdk": "^0.2.114",
"@anthropic-ai/foundry-sdk": "^0.2.3",
"@anthropic-ai/mcpb": "^2.1.2",
"@anthropic-ai/sandbox-runtime": "^0.0.44",
"@anthropic-ai/sdk": "^0.80.0",
"@anthropic-ai/vertex-sdk": "^0.14.4",
"@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",
"@aws-sdk/credential-provider-node": "^3.972.28",
"@aws-sdk/credential-providers": "^3.1020.0",
"@aws-sdk/client-bedrock": "^3.1032.0",
"@aws-sdk/client-bedrock-runtime": "^3.1032.0",
"@aws-sdk/client-sts": "^3.1032.0",
"@aws-sdk/credential-provider-node": "^3.972.32",
"@aws-sdk/credential-providers": "^3.1032.0",
"@azure/identity": "^4.13.1",
"@biomejs/biome": "^2.4.10",
"@biomejs/biome": "^2.4.12",
"@claude-code-best/agent-tools": "workspace:*",
"@claude-code-best/builtin-tools": "workspace:*",
"@claude-code-best/mcp-client": "workspace:*",
"@claude-code-best/weixin": "workspace:*",
"@commander-js/extra-typings": "^14.0.0",
"@growthbook/growthbook": "^1.6.5",
"@langfuse/otel": "^5.1.0",
@@ -96,7 +98,7 @@
"@modelcontextprotocol/sdk": "^1.29.0",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api-logs": "^0.214.0",
"@opentelemetry/core": "^2.6.1",
"@opentelemetry/core": "^2.7.0",
"@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
"@opentelemetry/exporter-logs-otlp-proto": "^0.214.0",
@@ -107,14 +109,14 @@
"@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.214.0",
"@opentelemetry/resources": "^2.6.1",
"@opentelemetry/resources": "^2.7.0",
"@opentelemetry/sdk-logs": "^0.214.0",
"@opentelemetry/sdk-metrics": "^2.6.1",
"@opentelemetry/sdk-trace-base": "^2.6.1",
"@opentelemetry/sdk-metrics": "^2.7.0",
"@opentelemetry/sdk-trace-base": "^2.7.0",
"@opentelemetry/semantic-conventions": "^1.40.0",
"@sentry/node": "^10.47.0",
"@smithy/core": "^3.23.13",
"@smithy/node-http-handler": "^4.5.1",
"@sentry/node": "^10.49.0",
"@smithy/core": "^3.23.15",
"@smithy/node-http-handler": "^4.5.3",
"@types/bun": "^1.3.12",
"@types/cacache": "^20.0.1",
"@types/he": "^1.2.3",
@@ -136,7 +138,7 @@
"asciichart": "^1.5.25",
"audio-capture-napi": "workspace:*",
"auto-bind": "^5.0.1",
"axios": "^1.14.0",
"axios": "^1.15.0",
"bidi-js": "^1.0.3",
"cacache": "^20.0.4",
"chalk": "^5.6.2",
@@ -151,7 +153,7 @@
"execa": "^9.6.1",
"fflate": "^0.8.2",
"figures": "^6.1.0",
"fuse.js": "^7.1.0",
"fuse.js": "^7.3.0",
"get-east-asian-width": "^1.5.0",
"google-auth-library": "^10.6.2",
"he": "^1.2.0",
@@ -161,21 +163,21 @@
"image-processor-napi": "workspace:*",
"indent-string": "^5.0.0",
"jsonc-parser": "^3.3.1",
"knip": "^6.1.1",
"lodash-es": "^4.17.23",
"lru-cache": "^11.2.7",
"marked": "^17.0.5",
"knip": "^6.4.1",
"lodash-es": "^4.18.1",
"lru-cache": "^11.3.5",
"marked": "^17.0.6",
"modifiers-napi": "workspace:*",
"openai": "^6.33.0",
"openai": "^6.34.0",
"p-map": "^7.0.4",
"picomatch": "^4.0.4",
"plist": "^3.1.0",
"proper-lockfile": "^4.1.2",
"qrcode": "^1.5.4",
"react": "^19.2.4",
"react": "^19.2.5",
"react-compiler-runtime": "^1.0.0",
"react-reconciler": "^0.33.0",
"rollup": "^4.60.1",
"rollup": "^4.60.2",
"semver": "^7.7.4",
"sharp": "^0.34.5",
"shell-quote": "^1.8.3",
@@ -184,10 +186,10 @@
"strip-ansi": "^7.2.0",
"supports-hyperlinks": "^4.4.0",
"tree-kill": "^1.2.2",
"turndown": "^7.2.2",
"type-fest": "^5.5.0",
"typescript": "^6.0.2",
"undici": "^7.24.6",
"turndown": "^7.2.4",
"type-fest": "^5.6.0",
"typescript": "^6.0.3",
"undici": "^7.25.0",
"url-handler-napi": "workspace:*",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.8",

View File

@@ -18,6 +18,6 @@ export const config = {
} as const;
export function getBaseUrl(): string {
if (config.baseUrl) return config.baseUrl;
return `http://localhost:${config.port}`;
const url = config.baseUrl || `http://localhost:${config.port}`;
return url.replace(/\/+$/, "");
}

View File

@@ -33,6 +33,17 @@ const app = new Hono();
// Middleware
app.use("*", logger());
app.use("*", async (c, next) => {
// Normalize double slashes in path (e.g. //v1/environments/bridge → /v1/environments/bridge)
const path = new URL(c.req.url).pathname;
if (path.includes("//")) {
const normalized = path.replace(/\/+/g, "/");
const url = new URL(c.req.url);
url.pathname = normalized;
return app.fetch(new Request(url.toString(), c.req.raw));
}
await next();
});
app.use("/web/*", cors());
// Health check

View File

@@ -444,7 +444,7 @@ function AskUserPanel({
const handleSubmit = () => {
const mapped: Record<string, unknown> = {};
for (const [qIdx, val] of Object.entries(answers)) {
const q = questions[parseInt(qIdx)];
const q = questions[parseInt(qIdx, 10)];
if (!q) continue;
if (typeof val === "number") {
mapped[qIdx] = q.options?.[val]?.label || String(val);

View File

@@ -97,7 +97,7 @@ export function AskUserPanelView({
const handleSubmit = () => {
const mapped: Record<string, unknown> = {};
for (const [qIdx, val] of Object.entries(answers)) {
const q = questions[parseInt(qIdx)];
const q = questions[parseInt(qIdx, 10)];
if (!q) continue;
if (typeof val === "number") mapped[qIdx] = q.options?.[val]?.label || String(val);
else if (Array.isArray(val)) mapped[qIdx] = val.map((i) => q.options?.[i]?.label || String(i));

View File

@@ -1,4 +1,5 @@
import type { SetStateAction } from "react";
import { v4 as uuidv4 } from "uuid";
import {
apiFetchSession,
apiFetchSessionHistory,
@@ -421,7 +422,7 @@ export class RCSChatAdapter {
// Send to backend
await apiSendEvent(this.sessionId, {
type: "user",
uuid: crypto.randomUUID(),
uuid: uuidv4(),
content: text,
message: { content: text },
});

View File

@@ -1,4 +1,5 @@
import type { ChatTransport, UIMessage, UIMessageChunk } from "ai";
import { v4 as uuidv4 } from "uuid";
import { getUuid } from "../api/client";
import type { SessionEvent, EventPayload } from "../types";
@@ -112,7 +113,7 @@ export class RCSTransport implements ChatTransport<UIMessage> {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "user",
uuid: crypto.randomUUID(),
uuid: uuidv4(),
content: text,
message: { content: text },
}),

View File

@@ -1,5 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { v4 as uuidv4 } from "uuid";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@@ -42,10 +43,7 @@ export function truncate(str: string | null | undefined, max: number): string {
}
export function generateMessageUuid(): string {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
return `msg_${Date.now()}_${Math.random().toString(16).slice(2)}`;
return uuidv4();
}
export function extractEventText(payload: Record<string, unknown> | null | undefined): string {

View File

@@ -0,0 +1,11 @@
{
"name": "@claude-code-best/weixin",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"qrcode": "^1.5.4"
}
}

View File

@@ -0,0 +1,54 @@
import { afterEach, describe, expect, test } from 'bun:test'
import { mkdtempSync, rmSync, statSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
const testDir = mkdtempSync(join(tmpdir(), 'weixin-test-accounts-'))
process.env.WEIXIN_STATE_DIR = testDir
import { clearAccount, loadAccount, saveAccount } from '../accounts.js'
afterEach(() => {
rmSync(testDir, { recursive: true, force: true })
})
describe('account storage', () => {
test('loadAccount returns null when no account exists', () => {
expect(loadAccount()).toBeNull()
})
test('saveAccount and loadAccount round-trip', () => {
const data = {
token: 'test-token',
baseUrl: 'https://example.com',
userId: 'user1',
savedAt: '2025-01-01T00:00:00.000Z',
}
saveAccount(data)
expect(loadAccount()).toEqual(data)
})
test('saveAccount sets file permissions to 0600', () => {
saveAccount({
token: 'test',
baseUrl: 'https://example.com',
savedAt: new Date().toISOString(),
})
const stats = statSync(join(testDir, 'account.json'))
if (process.platform === 'win32') {
expect(stats.isFile()).toBe(true)
return
}
expect(stats.mode & 0o777).toBe(0o600)
})
test('clearAccount removes the file', () => {
saveAccount({
token: 'test',
baseUrl: 'https://example.com',
savedAt: new Date().toISOString(),
})
clearAccount()
expect(loadAccount()).toBeNull()
})
})

View File

@@ -0,0 +1,90 @@
import { describe, expect, test } from 'bun:test'
import { randomBytes } from 'node:crypto'
import {
aesEcbPaddedSize,
buildCdnDownloadUrl,
buildCdnUploadUrl,
decryptAesEcb,
encryptAesEcb,
guessMediaType,
parseAesKey,
} from '../media.js'
import { UploadMediaType } from '../types.js'
describe('AES-128-ECB', () => {
test('encrypt then decrypt returns original data', () => {
const key = randomBytes(16)
const plaintext = Buffer.from('hello world test data!!')
const ciphertext = encryptAesEcb(plaintext, key)
expect(decryptAesEcb(ciphertext, key)).toEqual(plaintext)
})
test('different keys produce different ciphertext', () => {
const plaintext = Buffer.from('test data')
expect(
encryptAesEcb(plaintext, randomBytes(16)),
).not.toEqual(encryptAesEcb(plaintext, randomBytes(16)))
})
})
describe('aesEcbPaddedSize', () => {
test('pads to next 16-byte boundary', () => {
expect(aesEcbPaddedSize(1)).toBe(16)
expect(aesEcbPaddedSize(16)).toBe(32)
expect(aesEcbPaddedSize(17)).toBe(32)
expect(aesEcbPaddedSize(32)).toBe(48)
})
})
describe('parseAesKey', () => {
test('parses 16 raw bytes from base64', () => {
const raw = randomBytes(16)
expect(parseAesKey(raw.toString('base64'))).toEqual(raw)
})
test('parses hex-encoded key from base64', () => {
const raw = randomBytes(16)
const b64 = Buffer.from(raw.toString('hex'), 'ascii').toString('base64')
expect(parseAesKey(b64)).toEqual(raw)
})
test('throws on invalid key length', () => {
expect(() => parseAesKey(Buffer.from('short').toString('base64'))).toThrow(
'Invalid aes_key',
)
})
})
describe('CDN URL builders', () => {
test('buildCdnDownloadUrl encodes param', () => {
expect(buildCdnDownloadUrl('abc=123', 'https://cdn.example.com')).toBe(
'https://cdn.example.com/download?encrypted_query_param=abc%3D123',
)
})
test('buildCdnUploadUrl encodes params', () => {
expect(
buildCdnUploadUrl('https://cdn.example.com', 'param1', 'key1'),
).toBe(
'https://cdn.example.com/upload?encrypted_query_param=param1&filekey=key1',
)
})
})
describe('guessMediaType', () => {
test('detects image extensions', () => {
expect(guessMediaType('photo.jpg')).toBe(UploadMediaType.IMAGE)
expect(guessMediaType('photo.png')).toBe(UploadMediaType.IMAGE)
expect(guessMediaType('photo.webp')).toBe(UploadMediaType.IMAGE)
})
test('detects video extensions', () => {
expect(guessMediaType('video.mp4')).toBe(UploadMediaType.VIDEO)
expect(guessMediaType('video.mov')).toBe(UploadMediaType.VIDEO)
})
test('defaults to FILE for unknown extensions', () => {
expect(guessMediaType('doc.pdf')).toBe(UploadMediaType.FILE)
expect(guessMediaType('archive.zip')).toBe(UploadMediaType.FILE)
})
})

View File

@@ -0,0 +1,22 @@
import { describe, expect, test } from 'bun:test'
import { extractPermissionReply } from '../monitor.js'
describe('extractPermissionReply', () => {
test('parses allow replies', () => {
expect(extractPermissionReply('yes abcde')).toEqual({
requestId: 'abcde',
behavior: 'allow',
})
})
test('parses deny replies', () => {
expect(extractPermissionReply('No abcde')).toEqual({
requestId: 'abcde',
behavior: 'deny',
})
})
test('ignores unrelated text', () => {
expect(extractPermissionReply('yes please do it')).toBeNull()
})
})

View File

@@ -0,0 +1,78 @@
import { afterEach, describe, expect, test } from 'bun:test'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
const testDir = mkdtempSync(join(tmpdir(), 'weixin-test-pairing-'))
process.env.WEIXIN_STATE_DIR = testDir
import {
addPendingPairing,
confirmPairing,
isAllowed,
loadAccessConfig,
saveAccessConfig,
} from '../pairing.js'
afterEach(() => {
rmSync(testDir, { recursive: true, force: true })
})
describe('loadAccessConfig', () => {
test('returns default config when no file exists', () => {
const config = loadAccessConfig()
expect(config.policy).toBe('pairing')
expect(config.allowFrom).toEqual([])
})
test('round-trips saved config', () => {
saveAccessConfig({ policy: 'allowlist', allowFrom: ['user1'] })
const config = loadAccessConfig()
expect(config.policy).toBe('allowlist')
expect(config.allowFrom).toEqual(['user1'])
})
})
describe('isAllowed', () => {
test('returns false for unknown user under pairing policy', () => {
expect(isAllowed('unknown')).toBe(false)
})
test('returns true for allowed user', () => {
saveAccessConfig({ policy: 'pairing', allowFrom: ['user1'] })
expect(isAllowed('user1')).toBe(true)
})
test('returns true for any user under disabled policy', () => {
saveAccessConfig({ policy: 'disabled', allowFrom: [] })
expect(isAllowed('anyone')).toBe(true)
})
})
describe('pairing flow', () => {
test('generates 6-digit code', () => {
expect(addPendingPairing('user1')).toMatch(/^\d{6}$/)
})
test('returns same code for same user', () => {
const code1 = addPendingPairing('user1')
const code2 = addPendingPairing('user1')
expect(code1).toBe(code2)
})
test('confirm adds user to allowlist', () => {
const code = addPendingPairing('user1')
expect(confirmPairing(code)).toBe('user1')
expect(isAllowed('user1')).toBe(true)
})
test('confirm returns null for invalid code', () => {
expect(confirmPairing('000000')).toBeNull()
})
test('code cannot be reused after confirmation', () => {
const code = addPendingPairing('user1')
confirmPairing(code)
expect(confirmPairing(code)).toBeNull()
})
})

View File

@@ -0,0 +1,43 @@
import { afterEach, describe, expect, test } from 'bun:test'
import {
clearPermissionStateForTests,
consumePendingPermission,
getActivePermissionChat,
savePendingPermission,
setActivePermissionChat,
} from '../permissions.js'
afterEach(() => {
clearPermissionStateForTests()
})
describe('permission state', () => {
test('tracks active permission chat', () => {
setActivePermissionChat('user-1', 'ctx-1')
expect(getActivePermissionChat()).toEqual({
chatId: 'user-1',
contextToken: 'ctx-1',
updatedAt: expect.any(Number),
})
})
test('consumes pending permission only for matching user', () => {
savePendingPermission(
{
request_id: 'abcde',
tool_name: 'Bash',
description: 'Run a command',
input_preview: '{"command":"pwd"}',
},
'user-1',
'ctx-1',
)
expect(consumePendingPermission('abcde', 'user-2')).toBeNull()
expect(consumePendingPermission('ABCDE', 'user-1')).toMatchObject({
request_id: 'abcde',
chatId: 'user-1',
})
expect(consumePendingPermission('abcde', 'user-1')).toBeNull()
})
})

View File

@@ -0,0 +1,32 @@
import { describe, expect, test } from 'bun:test'
import { markdownToPlainText } from '../send.js'
describe('markdownToPlainText', () => {
test('removes bold markers', () => {
expect(markdownToPlainText('**bold**')).toBe('bold')
})
test('removes italic markers', () => {
expect(markdownToPlainText('*italic*')).toBe('italic')
})
test('removes inline code backticks', () => {
expect(markdownToPlainText('`code`')).toBe('code')
})
test('removes code block fences', () => {
expect(markdownToPlainText("```js\nconsole.log('hi');\n```"))
.toBe("console.log('hi');")
})
test('converts links to text with URL', () => {
expect(markdownToPlainText('[click](https://example.com)')).toBe(
'click (https://example.com)',
)
})
test('handles mixed markdown', () => {
expect(markdownToPlainText('# Hello\n\n**bold** and *italic* with `code`'))
.toBe('Hello\n\nbold and italic with code')
})
})

View File

@@ -0,0 +1,57 @@
import {
chmodSync,
existsSync,
mkdirSync,
readFileSync,
unlinkSync,
writeFileSync,
} from 'node:fs'
import { homedir } from 'node:os'
import { join } from 'node:path'
export const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com'
export const CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c'
export interface AccountData {
token: string
baseUrl: string
userId?: string
savedAt: string
}
export function getStateDir(): string {
const dir =
process.env.WEIXIN_STATE_DIR ||
join(homedir(), '.claude', 'channels', 'weixin')
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true })
}
return dir
}
function accountPath(): string {
return join(getStateDir(), 'account.json')
}
export function loadAccount(): AccountData | null {
const path = accountPath()
if (!existsSync(path)) return null
try {
return JSON.parse(readFileSync(path, 'utf-8')) as AccountData
} catch {
return null
}
}
export function saveAccount(data: AccountData): void {
const path = accountPath()
writeFileSync(path, JSON.stringify(data, null, 2), 'utf-8')
chmodSync(path, 0o600)
}
export function clearAccount(): void {
const path = accountPath()
if (existsSync(path)) {
unlinkSync(path)
}
}

148
packages/weixin/src/api.ts Normal file
View File

@@ -0,0 +1,148 @@
import { randomBytes } from 'node:crypto'
import type {
BaseInfo,
GetConfigResp,
GetUpdatesReq,
GetUpdatesResp,
GetUploadUrlReq,
GetUploadUrlResp,
SendMessageReq,
SendTypingReq,
SendTypingResp,
} from './types.js'
const CHANNEL_VERSION = '0.1.0'
function baseInfo(): BaseInfo {
return { channel_version: CHANNEL_VERSION }
}
function randomUin(): string {
return randomBytes(4).toString('base64')
}
function buildHeaders(token?: string): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-WECHAT-UIN': randomUin(),
}
if (token) {
headers.AuthorizationType = 'ilink_bot_token'
headers.Authorization = `Bearer ${token}`
}
return headers
}
async function post<T>(
baseUrl: string,
path: string,
body: unknown,
token?: string,
timeoutMs = 40_000,
signal?: AbortSignal,
): Promise<T> {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), timeoutMs)
if (signal) {
signal.addEventListener('abort', () => controller.abort(), { once: true })
}
try {
const response = await fetch(`${baseUrl}${path}`, {
method: 'POST',
headers: buildHeaders(token),
body: JSON.stringify(body),
signal: controller.signal,
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return (await response.json()) as T
} finally {
clearTimeout(timeout)
}
}
export async function getUpdates(
baseUrl: string,
token: string,
getUpdatesBuf: string,
signal?: AbortSignal,
): Promise<GetUpdatesResp> {
const body: GetUpdatesReq = {
get_updates_buf: getUpdatesBuf,
base_info: baseInfo(),
}
try {
return await post<GetUpdatesResp>(
baseUrl,
'/ilink/bot/getupdates',
body,
token,
40_000,
signal,
)
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return { ret: 0, msgs: [], get_updates_buf: getUpdatesBuf }
}
throw error
}
}
export async function sendMessage(
baseUrl: string,
token: string,
msg: SendMessageReq['msg'],
): Promise<void> {
const body: SendMessageReq = { msg, base_info: baseInfo() }
await post(baseUrl, '/ilink/bot/sendmessage', body, token)
}
export async function getUploadUrl(
baseUrl: string,
token: string,
params: Omit<GetUploadUrlReq, 'base_info'>,
): Promise<GetUploadUrlResp> {
return post<GetUploadUrlResp>(
baseUrl,
'/ilink/bot/getuploadurl',
{ ...params, base_info: baseInfo() },
token,
)
}
export async function getConfig(
baseUrl: string,
token: string,
userId: string,
contextToken?: string,
): Promise<GetConfigResp> {
return post<GetConfigResp>(
baseUrl,
'/ilink/bot/getconfig',
{
ilink_user_id: userId,
context_token: contextToken,
base_info: baseInfo(),
},
token,
)
}
export async function sendTyping(
baseUrl: string,
token: string,
req: Omit<SendTypingReq, 'base_info'>,
): Promise<SendTypingResp> {
return post<SendTypingResp>(
baseUrl,
'/ilink/bot/sendtyping',
{ ...req, base_info: baseInfo() },
token,
)
}

117
packages/weixin/src/cli.ts Normal file
View File

@@ -0,0 +1,117 @@
import { clearAccount, DEFAULT_BASE_URL, loadAccount, saveAccount } from './accounts.js'
import { startLogin, waitForLogin } from './login.js'
import { confirmPairing } from './pairing.js'
function printUsage(): void {
process.stdout.write(
[
'Usage:',
' ccb weixin serve',
' ccb weixin login',
' ccb weixin login clear',
' ccb weixin access pair <code>',
'',
'Session enablement:',
' ccb --channels plugin:weixin@builtin',
].join('\n') + '\n',
)
}
async function runLogin(clear = false): Promise<void> {
if (clear) {
clearAccount()
process.stdout.write('WeChat account cleared.\n')
return
}
const existing = loadAccount()
if (existing) {
process.stdout.write(
[
'Already connected:',
` User ID: ${existing.userId || 'unknown'}`,
` Connected since: ${existing.savedAt}`,
'',
'Run `ccb weixin login clear` to disconnect.',
'Restart Claude Code with:',
' ccb --channels plugin:weixin@builtin',
].join('\n') + '\n',
)
return
}
process.stdout.write('Starting WeChat QR login...\n\n')
const qr = await startLogin(DEFAULT_BASE_URL)
process.stdout.write(
`\nScan the QR code above with WeChat, or open this URL:\n${qr.qrcodeUrl || ''}\n\n`,
)
const result = await waitForLogin({
qrcodeId: qr.qrcodeId,
apiBaseUrl: DEFAULT_BASE_URL,
})
if (!result.connected || !result.token) {
process.stderr.write(`Login failed: ${result.message}\n`)
process.exit(1)
}
saveAccount({
token: result.token,
baseUrl: result.baseUrl || DEFAULT_BASE_URL,
userId: result.userId,
savedAt: new Date().toISOString(),
})
process.stdout.write(
[
'Connected successfully!',
` User ID: ${result.userId || 'unknown'}`,
` Base URL: ${result.baseUrl || DEFAULT_BASE_URL}`,
'',
'Restart Claude Code with:',
' ccb --channels plugin:weixin@builtin',
].join('\n') + '\n',
)
}
function runAccess(args: string[]): void {
if (args[0] !== 'pair' || !args[1]) {
printUsage()
process.exit(1)
}
const userId = confirmPairing(args[1])
if (!userId) {
process.stderr.write('Invalid or expired pairing code.\n')
process.exit(1)
}
process.stdout.write(`Paired successfully: ${userId}\n`)
}
export async function handleWeixinCli(
args: string[],
serveHandler?: () => Promise<void>,
): Promise<void> {
const [subcommand, ...rest] = args
switch (subcommand) {
case 'serve':
if (serveHandler) {
await serveHandler()
} else {
process.stderr.write('[weixin] serve handler not available in this context.\n')
process.exit(1)
}
return
case 'login':
await runLogin(rest[0] === 'clear')
return
case 'access':
runAccess(rest)
return
default:
printUsage()
}
}

View File

@@ -0,0 +1,111 @@
// @claude-code-best/weixin — WeChat channel integration
// Types
export {
MessageType,
MessageItemType,
MessageState,
UploadMediaType,
TypingStatus,
} from './types.js'
export type {
BaseInfo,
CDNMedia,
TextItem,
ImageItem,
VoiceItem,
FileItem,
VideoItem,
RefMessage,
MessageItem,
WeixinMessage,
GetUpdatesReq,
GetUpdatesResp,
SendMessageReq,
GetUploadUrlReq,
GetUploadUrlResp,
GetConfigResp,
SendTypingReq,
SendTypingResp,
} from './types.js'
// API client
export {
getUpdates,
sendMessage,
getUploadUrl,
getConfig,
sendTyping,
} from './api.js'
// Account management
export {
DEFAULT_BASE_URL,
CDN_BASE_URL,
getStateDir,
loadAccount,
saveAccount,
clearAccount,
} from './accounts.js'
export type { AccountData } from './accounts.js'
// Login
export { startLogin, waitForLogin } from './login.js'
export type { QRCodeResult, LoginResult } from './login.js'
// Pairing / access control
export {
loadAccessConfig,
saveAccessConfig,
isAllowed,
addPendingPairing,
confirmPairing,
} from './pairing.js'
export type { AccessConfig } from './pairing.js'
// Media encryption / upload
export {
encryptAesEcb,
decryptAesEcb,
aesEcbPaddedSize,
buildCdnDownloadUrl,
buildCdnUploadUrl,
parseAesKey,
downloadAndDecrypt,
uploadFile,
guessMediaType,
downloadRemoteToTemp,
} from './media.js'
export type { UploadedFileInfo } from './media.js'
// Message sending
export { markdownToPlainText, sendText, sendMediaFile } from './send.js'
// Monitor (message polling)
export {
getContextToken,
extractPermissionReply,
startPollLoop,
} from './monitor.js'
export type {
ParsedMessage,
OnMessageCallback,
PermissionResponse,
OnPermissionResponseCallback,
} from './monitor.js'
// Permission state
export {
ChannelPermissionRequestParams,
setActivePermissionChat,
getActivePermissionChat,
savePendingPermission,
consumePendingPermission,
} from './permissions.js'
export type {
PendingPermissionRequest,
ActivePermissionChat,
} from './permissions.js'
// CLI
export { handleWeixinCli } from './cli.js'

View File

@@ -0,0 +1,134 @@
import { toString as qrToString } from 'qrcode'
export interface QRCodeResult {
qrcodeUrl?: string
qrcodeId: string
message: string
}
export interface LoginResult {
connected: boolean
token?: string
accountId?: string
baseUrl?: string
userId?: string
message: string
}
async function renderQrCodeToTerminal(qrcodeUrl: string): Promise<void> {
const output = await qrToString(qrcodeUrl, {
type: 'terminal',
errorCorrectionLevel: 'L',
small: true,
})
process.stderr.write(`${output}\n`)
}
export async function startLogin(apiBaseUrl: string): Promise<QRCodeResult> {
const response = await fetch(`${apiBaseUrl}/ilink/bot/get_bot_qrcode?bot_type=3`)
if (!response.ok) {
throw new Error(`Failed to get QR code: HTTP ${response.status}`)
}
const data = (await response.json()) as {
qrcode?: string
qrcode_img_content?: string
}
if (!data.qrcode) {
throw new Error('No qrcode in response')
}
const qrcodeUrl = data.qrcode_img_content || ''
if (qrcodeUrl) {
await renderQrCodeToTerminal(qrcodeUrl)
}
return {
qrcodeUrl,
qrcodeId: data.qrcode,
message: 'Scan the QR code with WeChat to connect.',
}
}
export async function waitForLogin(params: {
qrcodeId: string
apiBaseUrl: string
timeoutMs?: number
maxRetries?: number
}): Promise<LoginResult> {
const { qrcodeId, apiBaseUrl, timeoutMs = 480_000, maxRetries = 3 } = params
const deadline = Date.now() + timeoutMs
let currentQrcodeId = qrcodeId
let retryCount = 0
while (Date.now() < deadline) {
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 60_000)
const response = await fetch(
`${apiBaseUrl}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(currentQrcodeId)}`,
{
headers: { 'iLink-App-ClientVersion': '1' },
signal: controller.signal,
},
)
clearTimeout(timeout)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const data = (await response.json()) as {
status?: string
bot_token?: string
ilink_bot_id?: string
baseurl?: string
ilink_user_id?: string
}
switch (data.status) {
case 'confirmed':
return {
connected: true,
token: data.bot_token,
accountId: data.ilink_bot_id,
baseUrl: data.baseurl,
userId: data.ilink_user_id,
message: 'Connected to WeChat successfully!',
}
case 'scaned':
process.stderr.write(
'QR code scanned, waiting for confirmation...\n',
)
break
case 'expired': {
retryCount += 1
if (retryCount >= maxRetries) {
return {
connected: false,
message: 'QR code expired after maximum retries.',
}
}
process.stderr.write('QR code expired, refreshing...\n')
const refreshed = await startLogin(apiBaseUrl)
currentQrcodeId = refreshed.qrcodeId
break
}
case 'wait':
default:
break
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
continue
}
throw error
}
await new Promise(resolve => setTimeout(resolve, 1000))
}
return { connected: false, message: 'Login timed out.' }
}

View File

@@ -0,0 +1,163 @@
import {
createCipheriv,
createDecipheriv,
createHash,
randomBytes,
} from 'node:crypto'
import {
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { basename, extname, join } from 'node:path'
import { getUploadUrl } from './api.js'
import { UploadMediaType } from './types.js'
export function encryptAesEcb(plaintext: Buffer, key: Buffer): Buffer {
const cipher = createCipheriv('aes-128-ecb', key, null)
return Buffer.concat([cipher.update(plaintext), cipher.final()])
}
export function decryptAesEcb(ciphertext: Buffer, key: Buffer): Buffer {
const decipher = createDecipheriv('aes-128-ecb', key, null)
return Buffer.concat([decipher.update(ciphertext), decipher.final()])
}
export function aesEcbPaddedSize(size: number): number {
return size + (16 - (size % 16))
}
export function buildCdnDownloadUrl(
encryptedQueryParam: string,
cdnBaseUrl: string,
): string {
return `${cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(encryptedQueryParam)}`
}
export function buildCdnUploadUrl(
cdnBaseUrl: string,
uploadParam: string,
filekey: string,
): string {
return `${cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(filekey)}`
}
export function parseAesKey(aesKeyBase64: string): Buffer {
const decoded = Buffer.from(aesKeyBase64, 'base64')
if (decoded.length === 16) {
return decoded
}
if (decoded.length === 32 && /^[0-9a-fA-F]{32}$/.test(decoded.toString('ascii'))) {
return Buffer.from(decoded.toString('ascii'), 'hex')
}
throw new Error(
`Invalid aes_key: expected 16 raw bytes or 32 hex chars, got ${decoded.length} bytes`,
)
}
export async function downloadAndDecrypt(params: {
encryptQueryParam: string
aesKey: string
cdnBaseUrl: string
}): Promise<Buffer> {
const url = buildCdnDownloadUrl(params.encryptQueryParam, params.cdnBaseUrl)
const response = await fetch(url)
if (!response.ok) {
throw new Error(`CDN download failed: HTTP ${response.status}`)
}
const ciphertext = Buffer.from(await response.arrayBuffer())
return decryptAesEcb(ciphertext, parseAesKey(params.aesKey))
}
export interface UploadedFileInfo {
encryptQueryParam: string
aesKey: string
fileSize: number
rawSize: number
fileName: string
}
export async function uploadFile(params: {
filePath: string
toUserId: string
mediaType: number
apiBaseUrl: string
token: string
cdnBaseUrl: string
}): Promise<UploadedFileInfo> {
const plaintext = readFileSync(params.filePath)
const rawSize = plaintext.length
const rawMd5 = createHash('md5').update(plaintext).digest('hex')
const aesKey = randomBytes(16)
const filekey = randomBytes(16).toString('hex')
const ciphertext = encryptAesEcb(plaintext, aesKey)
const fileSize = ciphertext.length
const uploadResp = await getUploadUrl(params.apiBaseUrl, params.token, {
filekey,
media_type: params.mediaType,
to_user_id: params.toUserId,
rawsize: rawSize,
rawfilemd5: rawMd5,
filesize: fileSize,
no_need_thumb: true,
aeskey: aesKey.toString('hex'),
})
if (!uploadResp.upload_param) {
throw new Error('No upload_param in response')
}
const uploadUrl = buildCdnUploadUrl(
params.cdnBaseUrl,
uploadResp.upload_param,
filekey,
)
const uploadResult = await fetch(uploadUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: new Uint8Array(ciphertext),
})
if (!uploadResult.ok) {
throw new Error(`CDN upload failed: HTTP ${uploadResult.status}`)
}
return {
encryptQueryParam: uploadResult.headers.get('x-encrypted-param') || '',
aesKey: Buffer.from(aesKey.toString('hex')).toString('base64'),
fileSize,
rawSize,
fileName: basename(params.filePath),
}
}
export function guessMediaType(filePath: string): number {
const ext = extname(filePath).toLowerCase()
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.heic']
const videoExts = ['.mp4', '.mov', '.avi', '.mkv', '.webm']
if (imageExts.includes(ext)) return UploadMediaType.IMAGE
if (videoExts.includes(ext)) return UploadMediaType.VIDEO
return UploadMediaType.FILE
}
export async function downloadRemoteToTemp(
url: string,
destDir?: string,
): Promise<string> {
const dir = destDir || join(tmpdir(), 'weixin-downloads')
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
const response = await fetch(url)
if (!response.ok) throw new Error(`Download failed: HTTP ${response.status}`)
const buffer = Buffer.from(await response.arrayBuffer())
const urlPath = new URL(url).pathname
const name = basename(urlPath) || `file_${Date.now()}`
const dest = join(dir, name)
writeFileSync(dest, buffer)
return dest
}

View File

@@ -0,0 +1,303 @@
import {
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
// Matches the canonical definition in src/services/mcp/channelPermissions.ts
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
import { getUpdates } from './api.js'
import { getStateDir } from './accounts.js'
import { downloadAndDecrypt } from './media.js'
import { addPendingPairing, isAllowed } from './pairing.js'
import { consumePendingPermission, setActivePermissionChat } from './permissions.js'
import { sendText } from './send.js'
import { MessageItemType, MessageType, type MessageItem, type WeixinMessage } from './types.js'
const contextTokens = new Map<string, string>()
export function getContextToken(userId: string): string | undefined {
return contextTokens.get(userId)
}
function cursorPath(): string {
return join(getStateDir(), 'cursor.txt')
}
function loadCursor(): string {
const path = cursorPath()
if (existsSync(path)) return readFileSync(path, 'utf-8').trim()
return ''
}
function saveCursor(cursor: string): void {
writeFileSync(cursorPath(), cursor, 'utf-8')
}
async function downloadMedia(
item: MessageItem,
cdnBaseUrl: string,
): Promise<{ path: string; type: string } | null> {
let encryptQueryParam: string | undefined
let aesKey: string | undefined
let ext = ''
let mediaType = ''
switch (item.type) {
case MessageItemType.IMAGE:
encryptQueryParam = item.image_item?.media?.encrypt_query_param
aesKey = item.image_item?.aeskey
? Buffer.from(item.image_item.aeskey, 'hex').toString('base64')
: item.image_item?.media?.aes_key
ext = '.jpg'
mediaType = 'image'
break
case MessageItemType.VOICE:
encryptQueryParam = item.voice_item?.media?.encrypt_query_param
aesKey = item.voice_item?.media?.aes_key
ext = '.silk'
mediaType = 'voice'
break
case MessageItemType.FILE:
encryptQueryParam = item.file_item?.media?.encrypt_query_param
aesKey = item.file_item?.media?.aes_key
ext = item.file_item?.file_name
? `.${item.file_item.file_name.split('.').pop()}`
: ''
mediaType = 'file'
break
case MessageItemType.VIDEO:
encryptQueryParam = item.video_item?.media?.encrypt_query_param
aesKey = item.video_item?.media?.aes_key
ext = '.mp4'
mediaType = 'video'
break
default:
return null
}
if (!encryptQueryParam || !aesKey) return null
try {
const data = await downloadAndDecrypt({
encryptQueryParam,
aesKey,
cdnBaseUrl,
})
const dir = join(tmpdir(), 'weixin-media')
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
const rawFileName = item.file_item?.file_name || `${Date.now()}${ext}`
const fileName = basename(rawFileName)
const filePath = join(dir, fileName)
writeFileSync(filePath, data)
return { path: filePath, type: mediaType }
} catch (error) {
process.stderr.write(`[weixin] Failed to download media: ${error}\n`)
return null
}
}
export interface ParsedMessage {
fromUserId: string
messageId: string
text: string
attachmentPath?: string
attachmentType?: string
}
export type OnMessageCallback = (msg: ParsedMessage) => Promise<void>
export type PermissionResponse = {
requestId: string
behavior: 'allow' | 'deny'
fromUserId: string
}
export type OnPermissionResponseCallback = (
response: PermissionResponse,
) => Promise<void>
export function extractPermissionReply(
text: string,
): { requestId: string; behavior: 'allow' | 'deny' } | null {
const match = text.match(PERMISSION_REPLY_RE)
if (!match) return null
const behavior =
match[1]?.toLowerCase().startsWith('y') ? 'allow' : 'deny'
const requestId = match[2]?.toLowerCase()
if (!requestId) return null
return { requestId, behavior }
}
export async function startPollLoop(params: {
baseUrl: string
cdnBaseUrl: string
token: string
onMessage: OnMessageCallback
onPermissionResponse?: OnPermissionResponseCallback
abortSignal: AbortSignal
}): Promise<void> {
const {
baseUrl,
cdnBaseUrl,
token,
onMessage,
onPermissionResponse,
abortSignal,
} = params
let cursor = loadCursor()
let consecutiveErrors = 0
process.stderr.write('[weixin] Starting message poll loop...\n')
while (!abortSignal.aborted) {
try {
const response = await getUpdates(baseUrl, token, cursor, abortSignal)
if (response.errcode === -14) {
process.stderr.write(
'[weixin] Session expired (errcode -14). Pausing for 30s...\n',
)
await new Promise(resolve => setTimeout(resolve, 30_000))
continue
}
if (response.ret !== 0 && response.ret !== undefined) {
throw new Error(
`getUpdates error: ret=${response.ret} errcode=${response.errcode} ${response.errmsg}`,
)
}
consecutiveErrors = 0
if (response.get_updates_buf) {
cursor = response.get_updates_buf
saveCursor(cursor)
}
if (response.msgs && response.msgs.length > 0) {
for (const msg of response.msgs) {
await processMessage(msg, {
baseUrl,
cdnBaseUrl,
token,
onMessage,
onPermissionResponse,
})
}
}
} catch (error) {
if (abortSignal.aborted) break
consecutiveErrors += 1
process.stderr.write(
`[weixin] Poll error (${consecutiveErrors}): ${error instanceof Error ? error.message : String(error)}\n`,
)
if (consecutiveErrors >= 3) {
process.stderr.write(
'[weixin] Too many consecutive errors, backing off 30s...\n',
)
await new Promise(resolve => setTimeout(resolve, 30_000))
consecutiveErrors = 0
} else {
await new Promise(resolve => setTimeout(resolve, 2000))
}
}
}
process.stderr.write('[weixin] Poll loop stopped.\n')
}
async function processMessage(
msg: WeixinMessage,
ctx: {
baseUrl: string
cdnBaseUrl: string
token: string
onMessage: OnMessageCallback
onPermissionResponse?: OnPermissionResponseCallback
},
): Promise<void> {
if (msg.message_type !== MessageType.USER) return
const fromUserId = msg.from_user_id
if (!fromUserId) return
if (msg.context_token) {
contextTokens.set(fromUserId, msg.context_token)
}
if (!isAllowed(fromUserId)) {
const code = addPendingPairing(fromUserId)
try {
await sendText({
to: fromUserId,
text: `Your pairing code is: ${code}\n\nAsk the operator to confirm:\nccb weixin access pair ${code}`,
baseUrl: ctx.baseUrl,
token: ctx.token,
contextToken: msg.context_token || '',
})
} catch (error) {
process.stderr.write(`[weixin] Failed to send pairing code: ${error}\n`)
}
return
}
setActivePermissionChat(fromUserId, msg.context_token)
let textContent = ''
let mediaPath: string | undefined
let mediaType: string | undefined
if (msg.item_list) {
for (const item of msg.item_list) {
if (item.type === MessageItemType.TEXT && item.text_item?.text) {
textContent += `${textContent ? '\n' : ''}${item.text_item.text}`
} else if (
item.type === MessageItemType.IMAGE ||
item.type === MessageItemType.VOICE ||
item.type === MessageItemType.FILE ||
item.type === MessageItemType.VIDEO
) {
const downloaded = await downloadMedia(item, ctx.cdnBaseUrl)
if (downloaded) {
mediaPath = downloaded.path
mediaType = downloaded.type
}
if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
textContent += `${textContent ? '\n' : ''}[Voice transcription]: ${item.voice_item.text}`
}
}
}
}
if (!textContent && !mediaPath) return
if (textContent && ctx.onPermissionResponse) {
const permissionReply = extractPermissionReply(textContent)
if (permissionReply) {
const pending = consumePendingPermission(
permissionReply.requestId,
fromUserId,
)
if (pending) {
await ctx.onPermissionResponse({
requestId: pending.request_id,
behavior: permissionReply.behavior,
fromUserId,
})
return
}
}
}
await ctx.onMessage({
fromUserId,
messageId: String(msg.message_id || ''),
text: textContent || '(media attachment)',
attachmentPath: mediaPath,
attachmentType: mediaType,
})
}

View File

@@ -0,0 +1,101 @@
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { getStateDir } from './accounts.js'
export interface AccessConfig {
policy: 'pairing' | 'allowlist' | 'disabled'
allowFrom: string[]
}
interface PendingEntry {
userId: string
expiresAt: number
}
function configPath(): string {
return join(getStateDir(), 'access.json')
}
function pendingPath(): string {
return join(getStateDir(), 'pending-pairings.json')
}
function loadPending(): Record<string, PendingEntry> {
const path = pendingPath()
if (!existsSync(path)) return {}
try {
return JSON.parse(readFileSync(path, 'utf-8')) as Record<string, PendingEntry>
} catch {
return {}
}
}
function savePending(data: Record<string, PendingEntry>): void {
writeFileSync(pendingPath(), JSON.stringify(data, null, 2), 'utf-8')
}
export function loadAccessConfig(): AccessConfig {
const path = configPath()
if (!existsSync(path)) {
return { policy: 'pairing', allowFrom: [] }
}
try {
return JSON.parse(readFileSync(path, 'utf-8')) as AccessConfig
} catch {
return { policy: 'pairing', allowFrom: [] }
}
}
export function saveAccessConfig(config: AccessConfig): void {
writeFileSync(configPath(), JSON.stringify(config, null, 2), 'utf-8')
}
export function isAllowed(userId: string): boolean {
const config = loadAccessConfig()
if (config.policy === 'disabled') return true
return config.allowFrom.includes(userId)
}
export function addPendingPairing(userId: string): string {
const pending = loadPending()
const now = Date.now()
for (const code of Object.keys(pending)) {
if (pending[code]!.expiresAt < now) {
delete pending[code]
}
}
for (const [code, entry] of Object.entries(pending)) {
if (entry.userId === userId) {
savePending(pending)
return code
}
}
const code = String(Math.floor(100000 + Math.random() * 900000))
pending[code] = { userId, expiresAt: now + 10 * 60 * 1000 }
savePending(pending)
return code
}
export function confirmPairing(code: string): string | null {
const pending = loadPending()
const entry = pending[code]
if (!entry || entry.expiresAt < Date.now()) {
delete pending[code]
savePending(pending)
return null
}
delete pending[code]
savePending(pending)
const config = loadAccessConfig()
if (!config.allowFrom.includes(entry.userId)) {
config.allowFrom.push(entry.userId)
saveAccessConfig(config)
}
return entry.userId
}

View File

@@ -0,0 +1,83 @@
/** Mirrors ChannelPermissionRequestParams from src/services/mcp/channelNotification.ts */
export interface ChannelPermissionRequestParams {
request_id: string
tool_name: string
description: string
input_preview: string
channel_context?: {
source_server?: string
chat_id?: string
}
}
export type PendingPermissionRequest = ChannelPermissionRequestParams & {
chatId: string
contextToken?: string
createdAt: number
expiresAt: number
}
export type ActivePermissionChat = {
chatId: string
contextToken?: string
updatedAt: number
}
const PENDING_PERMISSION_TTL_MS = 15 * 60 * 1000
const pendingPermissions = new Map<string, PendingPermissionRequest>()
let activePermissionChat: ActivePermissionChat | null = null
function pruneExpiredPendingPermissions(now = Date.now()): void {
for (const [requestId, entry] of pendingPermissions.entries()) {
if (entry.expiresAt <= now) {
pendingPermissions.delete(requestId)
}
}
}
export function setActivePermissionChat(
chatId: string,
contextToken?: string,
): void {
activePermissionChat = { chatId, contextToken, updatedAt: Date.now() }
}
export function getActivePermissionChat(): ActivePermissionChat | null {
return activePermissionChat
}
export function savePendingPermission(
request: ChannelPermissionRequestParams,
chatId: string,
contextToken?: string,
): PendingPermissionRequest {
pruneExpiredPendingPermissions()
const entry: PendingPermissionRequest = {
...request,
chatId,
contextToken,
createdAt: Date.now(),
expiresAt: Date.now() + PENDING_PERMISSION_TTL_MS,
}
pendingPermissions.set(request.request_id.toLowerCase(), entry)
return entry
}
export function consumePendingPermission(
requestId: string,
fromUserId: string,
): PendingPermissionRequest | null {
pruneExpiredPendingPermissions()
const key = requestId.toLowerCase()
const entry = pendingPermissions.get(key)
if (!entry) return null
if (entry.chatId !== fromUserId) return null
pendingPermissions.delete(key)
return entry
}
export function clearPermissionStateForTests(): void {
pendingPermissions.clear()
activePermissionChat = null
}

144
packages/weixin/src/send.ts Normal file
View File

@@ -0,0 +1,144 @@
import { randomUUID } from 'node:crypto'
import type { CDNMedia, MessageItem } from './types.js'
import { sendMessage } from './api.js'
import { guessMediaType, uploadFile } from './media.js'
import { MessageItemType, MessageState, MessageType } from './types.js'
export function markdownToPlainText(text: string): string {
return text
.replace(/```[\s\S]*?\n([\s\S]*?)```/g, '$1')
.replace(/`([^`]+)`/g, '$1')
.replace(/\*\*\*(.+?)\*\*\*/g, '$1')
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/___(.+?)___/g, '$1')
.replace(/__(.+?)__/g, '$1')
.replace(/_(.+?)_/g, '$1')
.replace(/~~(.+?)~~/g, '$1')
.replace(/^#{1,6}\s+/gm, '')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '[$1]')
.replace(/^>\s+/gm, '')
.replace(/^[-*_]{3,}$/gm, '---')
.replace(/^[\s]*[-*+]\s+/gm, '- ')
.replace(/^[\s]*(\d+)\.\s+/gm, '$1. ')
.replace(/\n{3,}/g, '\n\n')
.trim()
}
export async function sendText(params: {
to: string
text: string
baseUrl: string
token: string
contextToken: string
}): Promise<{ messageId: string }> {
const clientId = randomUUID()
await sendMessage(params.baseUrl, params.token, {
to_user_id: params.to,
from_user_id: '',
client_id: clientId,
message_type: MessageType.BOT,
message_state: MessageState.FINISH,
context_token: params.contextToken,
item_list: [
{
type: MessageItemType.TEXT,
text_item: { text: markdownToPlainText(params.text) },
},
],
})
return { messageId: clientId }
}
async function sendItems(params: {
items: MessageItem[]
to: string
baseUrl: string
token: string
contextToken: string
}): Promise<string> {
let lastClientId = ''
for (const item of params.items) {
lastClientId = randomUUID()
await sendMessage(params.baseUrl, params.token, {
to_user_id: params.to,
from_user_id: '',
client_id: lastClientId,
message_type: MessageType.BOT,
message_state: MessageState.FINISH,
context_token: params.contextToken,
item_list: [item],
})
}
return lastClientId
}
export async function sendMediaFile(params: {
filePath: string
to: string
text: string
baseUrl: string
token: string
contextToken: string
cdnBaseUrl: string
}): Promise<{ messageId: string }> {
const mediaType = guessMediaType(params.filePath)
const uploaded = await uploadFile({
filePath: params.filePath,
toUserId: params.to,
mediaType,
apiBaseUrl: params.baseUrl,
token: params.token,
cdnBaseUrl: params.cdnBaseUrl,
})
const cdnMedia: CDNMedia = {
encrypt_query_param: uploaded.encryptQueryParam,
aes_key: uploaded.aesKey,
encrypt_type: 1,
}
const items: MessageItem[] = []
if (params.text) {
items.push({
type: MessageItemType.TEXT,
text_item: { text: markdownToPlainText(params.text) },
})
}
switch (mediaType) {
case 1:
items.push({
type: MessageItemType.IMAGE,
image_item: { media: cdnMedia, mid_size: uploaded.fileSize },
})
break
case 2:
items.push({
type: MessageItemType.VIDEO,
video_item: { media: cdnMedia, video_size: uploaded.fileSize },
})
break
default:
items.push({
type: MessageItemType.FILE,
file_item: {
media: cdnMedia,
file_name: uploaded.fileName,
len: String(uploaded.rawSize),
},
})
break
}
const messageId = await sendItems({
items,
to: params.to,
baseUrl: params.baseUrl,
token: params.token,
contextToken: params.contextToken,
})
return { messageId }
}

View File

@@ -0,0 +1,178 @@
export const MessageType = {
NONE: 0,
USER: 1,
BOT: 2,
} as const
export const MessageItemType = {
NONE: 0,
TEXT: 1,
IMAGE: 2,
VOICE: 3,
FILE: 4,
VIDEO: 5,
} as const
export const MessageState = {
NEW: 0,
GENERATING: 1,
FINISH: 2,
} as const
export const UploadMediaType = {
IMAGE: 1,
VIDEO: 2,
FILE: 3,
VOICE: 4,
} as const
export const TypingStatus = {
TYPING: 1,
CANCEL: 2,
} as const
export interface BaseInfo {
channel_version?: string
}
export interface CDNMedia {
encrypt_query_param?: string
aes_key?: string
encrypt_type?: number
}
export interface TextItem {
text?: string
}
export interface ImageItem {
media?: CDNMedia
thumb_media?: CDNMedia
aeskey?: string
url?: string
mid_size?: number
thumb_size?: number
thumb_height?: number
thumb_width?: number
hd_size?: number
}
export interface VoiceItem {
media?: CDNMedia
encode_type?: number
bits_per_sample?: number
sample_rate?: number
playtime?: number
text?: string
}
export interface FileItem {
media?: CDNMedia
file_name?: string
md5?: string
len?: string
}
export interface VideoItem {
media?: CDNMedia
video_size?: number
play_length?: number
video_md5?: string
thumb_media?: CDNMedia
thumb_size?: number
thumb_height?: number
thumb_width?: number
}
export interface RefMessage {
message_item?: MessageItem
title?: string
}
export interface MessageItem {
type?: number
create_time_ms?: number
update_time_ms?: number
is_completed?: boolean
msg_id?: string
ref_msg?: RefMessage
text_item?: TextItem
image_item?: ImageItem
voice_item?: VoiceItem
file_item?: FileItem
video_item?: VideoItem
}
export interface WeixinMessage {
seq?: number
message_id?: number
from_user_id?: string
to_user_id?: string
client_id?: string
create_time_ms?: number
update_time_ms?: number
delete_time_ms?: number
session_id?: string
group_id?: string
message_type?: number
message_state?: number
item_list?: MessageItem[]
context_token?: string
}
export interface GetUpdatesReq {
get_updates_buf?: string
base_info?: BaseInfo
}
export interface GetUpdatesResp {
ret?: number
errcode?: number
errmsg?: string
msgs?: WeixinMessage[]
get_updates_buf?: string
longpolling_timeout_ms?: number
}
export interface SendMessageReq {
msg?: WeixinMessage
base_info?: BaseInfo
}
export interface GetUploadUrlReq {
filekey?: string
media_type?: number
to_user_id?: string
rawsize?: number
rawfilemd5?: string
filesize?: number
thumb_rawsize?: number
thumb_rawfilemd5?: string
thumb_filesize?: number
no_need_thumb?: boolean
aeskey?: string
base_info?: BaseInfo
}
export interface GetUploadUrlResp {
upload_param?: string
thumb_upload_param?: string
}
export interface GetConfigResp {
ret?: number
errmsg?: string
typing_ticket?: string
}
export interface SendTypingReq {
ilink_user_id?: string
typing_ticket?: string
status?: number
base_info?: BaseInfo
}
export interface SendTypingResp {
ret?: number
errmsg?: string
}

View File

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

View File

@@ -1,335 +0,0 @@
/**
* Download ripgrep binary from GitHub releases.
*
* Run automatically via `bun install` (postinstall hook),
* or manually: `bun run scripts/download-ripgrep.ts [--force]`
*
* Idempotent — skips download if the binary already exists.
* Use --force to re-download.
*
* Environment:
* - HTTPS_PROXY / HTTP_PROXY — when set, download uses `undici` + EnvHttpProxyAgent.
* - RIPGREP_DOWNLOAD_BASE — override release URL prefix, e.g. mirror:
* `https://ghproxy.net/https://github.com/microsoft/ripgrep-prebuilt/releases/download/v15.0.1`
*/
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from 'fs'
import { setDefaultResultOrder } from 'node:dns'
import { tmpdir } from 'os'
import { chmodSync } from 'fs'
import { spawnSync } from 'child_process'
import * as path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Prefer IPv4 first — Bun on Windows sometimes fails GitHub over broken IPv6 paths.
try {
setDefaultResultOrder('ipv4first')
} catch {
/* ignore */
}
const RG_VERSION = '15.0.1'
const DEFAULT_RELEASE_BASE = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`
const RELEASE_BASE = (process.env.RIPGREP_DOWNLOAD_BASE ?? DEFAULT_RELEASE_BASE).replace(/\/$/, '')
// --- Platform mapping ---
type PlatformMapping = {
target: string
ext: 'tar.gz' | 'zip'
}
function getPlatformMapping(): PlatformMapping {
const arch = process.arch
const platform = process.platform
if (platform === 'darwin') {
if (arch === 'arm64') return { target: 'aarch64-apple-darwin', ext: 'tar.gz' }
if (arch === 'x64') return { target: 'x86_64-apple-darwin', ext: 'tar.gz' }
throw new Error(`Unsupported macOS arch: ${arch}`)
}
if (platform === 'win32') {
if (arch === 'x64') return { target: 'x86_64-pc-windows-msvc', ext: 'zip' }
if (arch === 'arm64') return { target: 'aarch64-pc-windows-msvc', ext: 'zip' }
throw new Error(`Unsupported Windows arch: ${arch}`)
}
if (platform === 'linux') {
const isMusl = detectMusl()
if (arch === 'x64') {
// x64 Linux always uses musl (statically linked, most portable)
return { target: 'x86_64-unknown-linux-musl', ext: 'tar.gz' }
}
if (arch === 'arm64') {
return isMusl
? { target: 'aarch64-unknown-linux-musl', ext: 'tar.gz' }
: { target: 'aarch64-unknown-linux-gnu', ext: 'tar.gz' }
}
throw new Error(`Unsupported Linux arch: ${arch}`)
}
throw new Error(`Unsupported platform: ${platform}`)
}
function detectMusl(): boolean {
const muslArch = process.arch === 'x64' ? 'x86_64' : 'aarch64'
try {
statSync(`/lib/libc.musl-${muslArch}.so.1`)
return true
} catch {
return false
}
}
// --- Target vendor path (must match ripgrep.ts logic) ---
function getVendorDir(): string {
const packageRoot = path.resolve(__dirname, '..')
// Dev mode: package root has src/ directory
// ripgrep.ts at src/utils/ripgrep.ts: __dirname = src/utils/
// vendor path = src/utils/vendor/ripgrep/
if (existsSync(path.join(packageRoot, 'src'))) {
return path.resolve(packageRoot, 'src', 'utils', 'vendor', 'ripgrep')
}
// Published mode: compiled chunks are flat in dist/
// ripgrep chunk at dist/xxxx.js: __dirname = dist/
// vendor path = dist/vendor/ripgrep/
return path.resolve(packageRoot, 'dist', 'vendor', 'ripgrep')
}
function getBinaryPath(): string {
const dir = getVendorDir()
const subdir = `${process.arch}-${process.platform}`
const binary = process.platform === 'win32' ? 'rg.exe' : 'rg'
return path.resolve(dir, subdir, binary)
}
// --- Download & extract ---
function proxyEnvSet(): boolean {
const v = (s: string | undefined) => (s ?? '').trim()
return !!(
v(process.env.HTTPS_PROXY) ||
v(process.env.HTTP_PROXY) ||
v(process.env.ALL_PROXY) ||
v(process.env.https_proxy) ||
v(process.env.http_proxy)
)
}
async function fetchRelease(url: string): Promise<Response> {
if (proxyEnvSet()) {
const { EnvHttpProxyAgent, fetch: undiciFetch } = await import('undici')
return (await undiciFetch(url, {
redirect: 'follow',
dispatcher: new EnvHttpProxyAgent(),
})) as unknown as Response
}
return await fetch(url, { redirect: 'follow' })
}
function tryPowerShellDownload(url: string, dest: string): boolean {
const u = url.replace(/'/g, "''")
const d = dest.replace(/'/g, "''")
const cmd = `Invoke-WebRequest -Uri '${u}' -OutFile '${d}' -UseBasicParsing`
const result = spawnSync(
'powershell.exe',
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', cmd],
{ stdio: 'pipe', windowsHide: true },
)
return result.status === 0 && existsSync(dest) && statSync(dest).size > 0
}
function tryCurlDownload(url: string, dest: string): boolean {
const curl = process.platform === 'win32' ? 'curl.exe' : 'curl'
const result = spawnSync(curl, ['-fsSL', '-L', '--fail', '-o', dest, url], {
stdio: 'pipe',
windowsHide: true,
})
return result.status === 0 && existsSync(dest) && statSync(dest).size > 0
}
/** Bun `fetch` on Windows can fail while browser / WinINET still works — use subprocess fallbacks. */
async function downloadUrlToBuffer(url: string): Promise<Buffer> {
const response = await fetchRelease(url)
if (!response.ok) {
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
}
return Buffer.from(await response.arrayBuffer())
}
async function downloadUrlToBufferWithFallback(url: string): Promise<Buffer> {
let firstError: unknown
try {
return await downloadUrlToBuffer(url)
} catch (e) {
firstError = e
}
const tmpRoot = path.join(tmpdir(), `ripgrep-dl-${process.pid}-${Date.now()}`)
const tmpFile = path.join(tmpRoot, 'archive')
mkdirSync(tmpRoot, { recursive: true })
try {
if (process.platform === 'win32' && tryPowerShellDownload(url, tmpFile)) {
return readFileSync(tmpFile)
}
if (tryCurlDownload(url, tmpFile)) {
return readFileSync(tmpFile)
}
} finally {
rmSync(tmpRoot, { recursive: true, force: true })
}
throw firstError
}
function findZipEntryKey(files: Record<string, Uint8Array>, want: string): string | undefined {
return Object.keys(files).find(k => {
const norm = k.replace(/\\/g, '/')
return norm === want || norm.endsWith(`/${want}`)
})
}
async function downloadAndExtract(): Promise<void> {
const { target, ext } = getPlatformMapping()
const assetName = `ripgrep-v${RG_VERSION}-${target}.${ext}`
const downloadUrl = `${RELEASE_BASE}/${assetName}`
const binaryPath = getBinaryPath()
const binaryDir = path.dirname(binaryPath)
// Idempotent: skip if binary exists and has content
const force = process.argv.includes('--force')
if (!force && existsSync(binaryPath)) {
const stat = statSync(binaryPath)
if (stat.size > 0) {
console.log(`[ripgrep] Binary already exists at ${binaryPath}, skipping.`)
return
}
}
console.log(`[ripgrep] Downloading v${RG_VERSION} for ${target}...`)
console.log(`[ripgrep] URL: ${downloadUrl}`)
const extractedBinary = process.platform === 'win32' ? 'rg.exe' : 'rg'
const { writeFileSync } = await import('fs')
try {
const buffer = await downloadUrlToBufferWithFallback(downloadUrl)
console.log(`[ripgrep] Downloaded ${Math.round(buffer.length / 1024)} KB`)
mkdirSync(binaryDir, { recursive: true })
if (ext === 'tar.gz') {
const tmpDir = path.join(binaryDir, '.tmp-download')
rmSync(tmpDir, { recursive: true, force: true })
mkdirSync(tmpDir, { recursive: true })
try {
const archivePath = path.join(tmpDir, assetName)
writeFileSync(archivePath, buffer)
const result = spawnSync('tar', ['xzf', archivePath, '-C', tmpDir], {
stdio: 'pipe',
})
if (result.status !== 0) {
throw new Error(`tar extract failed: ${result.stderr?.toString()}`)
}
const srcBinary = path.join(tmpDir, extractedBinary)
if (!existsSync(srcBinary)) {
throw new Error(`Binary not found at expected path: ${srcBinary}`)
}
renameSync(srcBinary, binaryPath)
} finally {
rmSync(tmpDir, { recursive: true, force: true })
}
} else {
let fflateError: unknown
try {
const { unzipSync } = await import('fflate')
const unzipped = unzipSync(new Uint8Array(buffer))
const key = findZipEntryKey(unzipped, extractedBinary)
if (!key) {
throw new Error(`Binary ${extractedBinary} not found in zip`)
}
writeFileSync(binaryPath, Buffer.from(unzipped[key]))
fflateError = undefined
} catch (e) {
fflateError = e
}
if (fflateError) {
// fflate failed — try PowerShell Expand-Archive on Windows, then unzip CLI
const tmpDir = path.join(binaryDir, '.tmp-download')
rmSync(tmpDir, { recursive: true, force: true })
mkdirSync(tmpDir, { recursive: true })
try {
const archivePath = path.join(tmpDir, assetName)
writeFileSync(archivePath, buffer)
let extracted = false
// On Windows, prefer PowerShell Expand-Archive
if (process.platform === 'win32') {
const psCmd = `Expand-Archive -Path '${archivePath.replace(/'/g, "''")}' -DestinationPath '${tmpDir.replace(/'/g, "''")}' -Force`
const psResult = spawnSync(
'powershell.exe',
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', psCmd],
{ stdio: 'pipe', windowsHide: true },
)
if (psResult.status === 0) {
extracted = true
} else {
const psErr = psResult.stderr?.toString().trim() || 'unknown error'
console.log(`[ripgrep] PowerShell Expand-Archive failed: ${psErr}`)
}
}
// Fallback: unzip CLI (Git for Windows, MSYS2, or Unix)
if (!extracted) {
const result = spawnSync('unzip', ['-o', archivePath, '-d', tmpDir], {
stdio: 'pipe',
})
if (result.status !== 0) {
const unzipErr = result.stderr?.toString().trim() || 'command not found'
const fflateMsg = fflateError instanceof Error ? fflateError.message : String(fflateError)
throw new Error(`zip extraction failed (fflate: ${fflateMsg}; unzip: ${unzipErr})`)
}
}
const srcBinary = path.join(tmpDir, extractedBinary)
if (!existsSync(srcBinary)) {
throw new Error(`Binary not found at expected path: ${srcBinary}`)
}
renameSync(srcBinary, binaryPath)
} finally {
rmSync(tmpDir, { recursive: true, force: true })
}
}
}
if (process.platform !== 'win32') {
chmodSync(binaryPath, 0o755)
}
console.log(`[ripgrep] Installed to ${binaryPath}`)
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
const hint =
'Check network or set HTTPS_PROXY. If GitHub is blocked, set RIPGREP_DOWNLOAD_BASE to a mirror (see script header).'
throw new Error(`${msg} ${hint}`)
}
}
// --- Main ---
downloadAndExtract().catch(error => {
console.error(`[ripgrep] Download failed: ${error.message}`)
console.error(`[ripgrep] You can install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`)
// Don't exit with error code — postinstall should not break bun install
process.exit(0)
})

View File

@@ -1,163 +0,0 @@
#!/usr/bin/env bun
/**
* 代码健康度检查脚本
*
* 汇总项目各维度指标,输出健康度报告:
* - 代码规模(文件数、代码行数)
* - Lint 问题数Biome
* - 测试结果Bun test
* - 冗余代码Knip
* - 构建状态
*/
import { $ } from "bun";
const DIVIDER = "─".repeat(60);
interface Metric {
label: string;
value: string | number;
status: "ok" | "warn" | "error" | "info";
}
const metrics: Metric[] = [];
function add(label: string, value: string | number, status: Metric["status"] = "info") {
metrics.push({ label, value, status });
}
function icon(status: Metric["status"]): string {
switch (status) {
case "ok":
return "[OK]";
case "warn":
return "[!!]";
case "error":
return "[XX]";
case "info":
return "[--]";
}
}
// ---------------------------------------------------------------------------
// 1. 代码规模
// ---------------------------------------------------------------------------
async function checkCodeSize() {
const tsFiles = await $`find src -name '*.ts' -o -name '*.tsx' | grep -v node_modules`.text();
const fileCount = tsFiles.trim().split("\n").filter(Boolean).length;
add("TypeScript 文件数", fileCount, "info");
const loc = await $`find src -name '*.ts' -o -name '*.tsx' | grep -v node_modules | xargs wc -l | tail -1`.text();
const totalLines = loc.trim().split(/\s+/)[0] ?? "?";
add("总代码行数 (src/)", totalLines, "info");
}
// ---------------------------------------------------------------------------
// 2. Lint 检查
// ---------------------------------------------------------------------------
async function checkLint() {
try {
const result = await $`bunx biome check src/ 2>&1`.quiet().nothrow().text();
const errorMatch = result.match(/Found (\d+) errors?/);
const warnMatch = result.match(/Found (\d+) warnings?/);
const errors = errorMatch ? Number.parseInt(errorMatch[1]) : 0;
const warnings = warnMatch ? Number.parseInt(warnMatch[1]) : 0;
add("Lint 错误", errors, errors === 0 ? "ok" : errors < 100 ? "warn" : "info");
add("Lint 警告", warnings, warnings === 0 ? "ok" : "info");
} catch {
add("Lint 检查", "执行失败", "error");
}
}
// ---------------------------------------------------------------------------
// 3. 测试
// ---------------------------------------------------------------------------
async function checkTests() {
try {
const result = await $`bun test 2>&1`.quiet().nothrow().text();
const passMatch = result.match(/(\d+) pass/);
const failMatch = result.match(/(\d+) fail/);
const pass = passMatch ? Number.parseInt(passMatch[1]) : 0;
const fail = failMatch ? Number.parseInt(failMatch[1]) : 0;
add("测试通过", pass, pass > 0 ? "ok" : "warn");
add("测试失败", fail, fail === 0 ? "ok" : "error");
} catch {
add("测试", "执行失败", "error");
}
}
// ---------------------------------------------------------------------------
// 4. 冗余代码
// ---------------------------------------------------------------------------
async function checkUnused() {
try {
const result = await $`bunx knip-bun 2>&1`.quiet().nothrow().text();
const unusedFiles = result.match(/Unused files \((\d+)\)/);
const unusedExports = result.match(/Unused exports \((\d+)\)/);
const unusedDeps = result.match(/Unused dependencies \((\d+)\)/);
add("未使用文件", unusedFiles?.[1] ?? "0", "info");
add("未使用导出", unusedExports?.[1] ?? "0", "info");
add("未使用依赖", unusedDeps?.[1] ?? "0", unusedDeps && Number(unusedDeps[1]) > 0 ? "warn" : "ok");
} catch {
add("冗余代码检查", "执行失败", "error");
}
}
// ---------------------------------------------------------------------------
// 5. 构建
// ---------------------------------------------------------------------------
async function checkBuild() {
try {
const result = await $`bun run build 2>&1`.quiet().nothrow();
if (result.exitCode === 0) {
// 获取产物大小
const stat = Bun.file("dist/cli.js");
const mb = (stat.size / 1024 / 1024).toFixed(1);
const size = `${mb} MB`;
add("构建状态", "成功", "ok");
add("产物大小 (dist/cli.js)", size, "info");
} else {
add("构建状态", "失败", "error");
}
} catch {
add("构建", "执行失败", "error");
}
}
// ---------------------------------------------------------------------------
// Run
// ---------------------------------------------------------------------------
console.log("");
console.log(DIVIDER);
console.log(" 代码健康度检查报告");
console.log(` ${new Date().toLocaleString("zh-CN")}`);
console.log(DIVIDER);
await checkCodeSize();
await checkLint();
await checkTests();
await checkUnused();
await checkBuild();
console.log("");
for (const m of metrics) {
const tag = icon(m.status);
console.log(` ${tag} ${m.label.padEnd(20)} ${m.value}`);
}
const errorCount = metrics.filter((m) => m.status === "error").length;
const warnCount = metrics.filter((m) => m.status === "warn").length;
console.log("");
console.log(DIVIDER);
if (errorCount > 0) {
console.log(` 结果: ${errorCount} 个错误, ${warnCount} 个警告`);
} else if (warnCount > 0) {
console.log(` 结果: 无错误, ${warnCount} 个警告`);
} else {
console.log(" 结果: 全部通过");
}
console.log(DIVIDER);
console.log("");
process.exit(errorCount > 0 ? 1 : 0);

View File

@@ -4,8 +4,7 @@
*
* 1. Patch globalThis.Bun destructuring in third-party deps for Node.js compat
* 2. Copy native addon files
* 3. Bundle standalone scripts (download-ripgrep)
* 4. Generate dual entry points (cli-bun.js, cli-node.js)
* 3. Generate dual entry points (cli-bun.js, cli-node.js)
*/
import { readdir, readFile, writeFile, cp } from "node:fs/promises";
import { chmodSync } from "node:fs";
@@ -41,35 +40,7 @@ async function postBuild() {
await cp("vendor/audio-capture", vendorDir, { recursive: true } as never);
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`);
// Step 3: Bundle standalone scripts via Bun.build (kept for simplicity)
try {
const { default: Bun } = await import("bun");
const rgScript = await Bun.build({
entrypoints: ["scripts/download-ripgrep.ts"],
outdir,
target: "node",
});
if (rgScript.success) {
console.log(`Bundled download-ripgrep script to ${outdir}/`);
} else {
console.warn("Failed to bundle download-ripgrep script (non-fatal)");
}
} catch {
// Bun not available — try esbuild fallback
try {
execSync(
`npx esbuild scripts/download-ripgrep.ts --bundle --platform=node --outfile=${outdir}/download-ripgrep.js --format=esm`,
{ stdio: "inherit" },
);
console.log(`Bundled download-ripgrep script via esbuild to ${outdir}/`);
} catch {
console.warn(
"Failed to bundle download-ripgrep script — skipping (non-fatal)",
);
}
}
// Step 4: Generate dual entry points
// Step 3: Generate dual entry points
const cliBun = join(outdir, "cli-bun.js");
const cliNode = join(outdir, "cli-node.js");

View File

@@ -1 +1,2 @@
ACP_RCS_URL=http://localhost:3000 ACP_RCS_TOKEN=test-my-key acp-link ccb-bun -- --acp
# ACP_RCS_URL=http://localhost:3000 ACP_RCS_TOKEN=test-my-key acp-link ccb-bun -- --acp
ACP_RCS_URL=https://remote-control.claude-code-best.win/ ACP_RCS_TOKEN=test-my-key acp-link ccb-bun -- --acp

View File

@@ -10,8 +10,6 @@ import { config } from "../packages/remote-control-server/src/config";
console.log(`[RCS] Starting Remote Control Server...`);
console.log(`[RCS] Port: ${config.port}`);
console.log(`[RCS] API Key configuration loaded`);
console.log(`[RCS] JWT Secret: ${config.jwtSecret === "change-me-in-production" ? "default (set RCS_JWT_SECRET)" : "custom"}`);
console.log(`[RCS] DB: ${config.dbPath}`);
const server = await import("../packages/remote-control-server/src/index.ts");

View File

@@ -1,107 +0,0 @@
#!/usr/bin/env bun
/**
* Verify GrowthBook gate defaults and compile-time feature flags.
*
* Usage:
* bun run scripts/verify-gates.ts
*
* This script checks that LOCAL_GATE_DEFAULTS are being returned correctly
* when GrowthBook is not connected, and that compile-time feature flags
* are properly enabled.
*/
// We can't import feature() from bun:bundle in a standalone script,
// so we test the GrowthBook layer directly.
import {
getFeatureValue_CACHED_MAY_BE_STALE,
checkStatsigFeatureGate_CACHED_MAY_BE_STALE,
} from '../src/services/analytics/growthbook.js'
interface GateCheck {
name: string
gate: string
expected: unknown
category: string
/** If set, this compile flag must also be enabled at build time */
compileFlag?: string
}
const gates: GateCheck[] = [
// P0: Pure local
{ name: 'Custom keybindings', gate: 'tengu_keybinding_customization_release', expected: true, category: 'P0' },
{ name: 'Streaming tool exec', gate: 'tengu_streaming_tool_execution2', expected: true, category: 'P0' },
{ name: 'Cron tasks', gate: 'tengu_kairos_cron', expected: true, category: 'P0' },
{ name: 'JSON tools format', gate: 'tengu_amber_json_tools', expected: true, category: 'P0' },
{ name: 'Immediate model cmd', gate: 'tengu_immediate_model_command', expected: true, category: 'P0' },
{ name: 'MCP delta', gate: 'tengu_basalt_3kr', expected: true, category: 'P0' },
{ name: 'Leaf pruning', gate: 'tengu_pebble_leaf_prune', expected: true, category: 'P0' },
{ name: 'Message smooshing', gate: 'tengu_chair_sermon', expected: true, category: 'P0' },
{ name: 'Deep link', gate: 'tengu_lodestone_enabled', expected: true, category: 'P0', compileFlag: 'LODESTONE' },
{ name: 'Auto background', gate: 'tengu_auto_background_agents', expected: true, category: 'P0' },
{ name: 'Fine-grained tools', gate: 'tengu_fgts', expected: true, category: 'P0' },
// P1: API-dependent
{ name: 'Session memory', gate: 'tengu_session_memory', expected: true, category: 'P1' },
{ name: 'Auto memory extract', gate: 'tengu_passport_quail', expected: true, category: 'P1', compileFlag: 'EXTRACT_MEMORIES' },
{ name: 'Memory skip index', gate: 'tengu_moth_copse', expected: true, category: 'P1' },
{ name: 'Memory search section', gate: 'tengu_coral_fern', expected: true, category: 'P1' },
{ name: 'Prompt suggestions', gate: 'tengu_chomp_inflection', expected: true, category: 'P1' },
{ name: 'Verification agent', gate: 'tengu_hive_evidence', expected: true, category: 'P1', compileFlag: 'VERIFICATION_AGENT' },
{ name: 'Brief mode', gate: 'tengu_kairos_brief', expected: true, category: 'P1', compileFlag: 'KAIROS_BRIEF' },
{ name: 'Away summary', gate: 'tengu_sedge_lantern', expected: true, category: 'P1', compileFlag: 'AWAY_SUMMARY' },
{ name: 'Idle return prompt', gate: 'tengu_willow_mode', expected: 'dialog', category: 'P1' },
// Kill switches
{ name: 'Ultrathink', gate: 'tengu_turtle_carbon', expected: true, category: 'KS', compileFlag: 'ULTRATHINK' },
{ name: 'Explore/Plan agents', gate: 'tengu_amber_stoat', expected: true, category: 'KS', compileFlag: 'BUILTIN_EXPLORE_PLAN_AGENTS' },
{ name: 'Agent teams', gate: 'tengu_amber_flint', expected: true, category: 'KS' },
{ name: 'Slim subagent CLAUDE.md', gate: 'tengu_slim_subagent_claudemd', expected: true, category: 'KS' },
{ name: 'Bash security', gate: 'tengu_birch_trellis', expected: true, category: 'KS' },
{ name: 'macOS clipboard', gate: 'tengu_collage_kaleidoscope', expected: true, category: 'KS' },
{ name: 'Compact cache prefix', gate: 'tengu_compact_cache_prefix', expected: true, category: 'KS' },
{ name: 'Durable cron', gate: 'tengu_kairos_cron_durable', expected: true, category: 'KS' },
{ name: 'Attribution header', gate: 'tengu_attribution_header', expected: true, category: 'KS' },
{ name: 'Agent progress', gate: 'tengu_slate_prism', expected: true, category: 'KS' },
]
console.log('=== GrowthBook Local Gate Verification ===\n')
let pass = 0
let fail = 0
for (const category of ['P0', 'P1', 'KS']) {
const label = category === 'KS' ? 'Kill Switches' : category
console.log(`--- ${label} ---`)
for (const check of gates.filter(g => g.category === category)) {
const actual = typeof check.expected === 'boolean'
? checkStatsigFeatureGate_CACHED_MAY_BE_STALE(check.gate)
: getFeatureValue_CACHED_MAY_BE_STALE(check.gate, null)
const matches = typeof check.expected === 'boolean'
? actual === check.expected
: actual === check.expected || JSON.stringify(actual) === JSON.stringify(check.expected)
const status = matches ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m'
const flagNote = check.compileFlag ? ` [needs feature('${check.compileFlag}')]` : ''
console.log(` ${status} ${check.name}: ${check.gate} = ${JSON.stringify(actual)}${flagNote}`)
if (matches) pass++
else fail++
}
console.log()
}
console.log(`\nResult: ${pass} passed, ${fail} failed out of ${pass + fail} gates`)
if (fail > 0) {
console.log('\n\x1b[31mSome gates are not returning expected values!\x1b[0m')
console.log('If CLAUDE_CODE_DISABLE_LOCAL_GATES=1 is set, all gates will return defaults.')
process.exit(1)
}
console.log('\n\x1b[32mAll GrowthBook gates returning expected local defaults.\x1b[0m')
console.log('\nNote: Compile-time feature() flags cannot be verified in this script.')
console.log('Use "bun run dev" and test manually for features with [needs feature()] markers.')

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