mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-16 13:25:51 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4ce08fe39 | ||
|
|
92f8a92fbb | ||
|
|
a67e2d0e97 | ||
|
|
8c629858ab | ||
|
|
494eab7204 | ||
|
|
b83c3008d0 | ||
|
|
66d2671c98 | ||
|
|
c7bc8c8636 | ||
|
|
673ccd1800 | ||
|
|
d1ab38c089 | ||
|
|
f9d011164a | ||
|
|
481e2a58a9 | ||
|
|
c5edee431f | ||
|
|
a57ca08566 | ||
|
|
6536757428 | ||
|
|
a0dc4540ca | ||
|
|
7e4df5c3e9 | ||
|
|
4d939e5722 |
@@ -1,22 +0,0 @@
|
||||
#!/bin/sh
|
||||
# pre-commit hook: 对暂存的文件运行 Biome 检查
|
||||
# 仅检查 src/ 下的 .ts/.tsx/.js/.jsx 文件
|
||||
|
||||
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^src/.*\.(ts|tsx|js|jsx)$')
|
||||
|
||||
if [ -z "$STAGED_FILES" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Running Biome lint on staged files..."
|
||||
|
||||
# 使用 biome lint 对暂存文件进行检查(仅 lint,不格式化,不自动修复)
|
||||
echo "$STAGED_FILES" | xargs bunx biome lint --no-errors-on-unmatched
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Biome lint failed. Fix errors or use --no-verify to bypass."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -58,6 +58,9 @@ bun run health
|
||||
# Check unused exports
|
||||
bun run check:unused
|
||||
|
||||
# Full check (typecheck + lint + test) — run after completing any task
|
||||
bun run test:all
|
||||
|
||||
bun run typecheck
|
||||
|
||||
# Remote Control Server
|
||||
|
||||
13
README.md
13
README.md
@@ -6,13 +6,13 @@
|
||||
[](https://github.com/claude-code-best/claude-code/blob/main/LICENSE)
|
||||
[](https://github.com/claude-code-best/claude-code/commits/main)
|
||||
[](https://bun.sh/)
|
||||
[](https://discord.gg/qZU6zS7Q)
|
||||
[](https://discord.gg/uApuzJWGKX)
|
||||
|
||||
> Which Claude do you like? The open source one is the best.
|
||||
|
||||
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)... 而且, 我们实现了企业版或者需要登陆 Claude 账号才能使用的特性, 实现技术普惠
|
||||
|
||||
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/qZU6zS7Q)
|
||||
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/uApuzJWGKX)
|
||||
|
||||
| 特性 | 说明 | 文档 |
|
||||
|------|------|------|
|
||||
@@ -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) |
|
||||
@@ -40,8 +41,12 @@
|
||||
不用克隆仓库, 从 NPM 下载后, 直接使用
|
||||
|
||||
```sh
|
||||
bun i -g claude-code-best
|
||||
bun pm -g trust claude-code-best
|
||||
npm i -g claude-code-best
|
||||
|
||||
# bun 安装比较多问题, 推荐 npm 装
|
||||
# bun i -g claude-code-best
|
||||
# bun pm -g trust claude-code-best
|
||||
|
||||
ccb # 以 nodejs 打开 claude code
|
||||
ccb-bun # 以 bun 形态打开
|
||||
CLAUDE_BRIDGE_BASE_URL=https://remote-control.claude-code-best.win/ CLAUDE_BRIDGE_OAUTH_TOKEN=test-my-key ccb --remote-control # 我们有自部署的远程控制
|
||||
|
||||
60
build.ts
60
build.ts
@@ -1,6 +1,7 @@
|
||||
import { readdir, readFile, writeFile, cp } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { getMacroDefines } from './scripts/defines.ts'
|
||||
import { DEFAULT_BUILD_FEATURES } from './scripts/defines.ts'
|
||||
|
||||
const outdir = 'dist'
|
||||
|
||||
@@ -8,47 +9,6 @@ const outdir = 'dist'
|
||||
const { rmSync } = await import('fs')
|
||||
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 = [
|
||||
'AGENT_TRIGGERS_REMOTE',
|
||||
'CHICAGO_MCP',
|
||||
'VOICE_MODE',
|
||||
'SHOT_STATS',
|
||||
'PROMPT_CACHE_BREAK_DETECTION',
|
||||
'TOKEN_BUDGET',
|
||||
// P0: local features
|
||||
'AGENT_TRIGGERS',
|
||||
'ULTRATHINK',
|
||||
'BUILTIN_EXPLORE_PLAN_AGENTS',
|
||||
'LODESTONE',
|
||||
// P1: API-dependent features
|
||||
'EXTRACT_MEMORIES',
|
||||
'VERIFICATION_AGENT',
|
||||
'KAIROS_BRIEF',
|
||||
'AWAY_SUMMARY',
|
||||
'ULTRAPLAN',
|
||||
// P2: daemon + remote control server
|
||||
'DAEMON',
|
||||
// ACP (Agent Client Protocol) agent mode
|
||||
'ACP',
|
||||
// PR-package restored features
|
||||
'WORKFLOW_SCRIPTS',
|
||||
'HISTORY_SNIP',
|
||||
'CONTEXT_COLLAPSE',
|
||||
'MONITOR_TOOL',
|
||||
'FORK_SUBAGENT',
|
||||
// 'UDS_INBOX',
|
||||
'KAIROS',
|
||||
'COORDINATOR_MODE',
|
||||
'LAN_PIPES',
|
||||
'BG_SESSIONS',
|
||||
'TEMPLATES',
|
||||
// 'REVIEW_ARTIFACT', // API 请求无响应,需进一步排查 schema 兼容性
|
||||
// P3: poor mode (disable extract_memories + prompt_suggestion)
|
||||
'POOR',
|
||||
]
|
||||
|
||||
// Collect FEATURE_* env vars → Bun.build features
|
||||
const envFeatures = Object.keys(process.env)
|
||||
.filter(k => k.startsWith('FEATURE_'))
|
||||
@@ -120,23 +80,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')
|
||||
|
||||
|
||||
@@ -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)
|
||||
├── 解析 effectiveType(fork 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 秒
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,展示自动更新状态 |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()` 的结果
|
||||
|
||||
@@ -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。
|
||||
|
||||
|
||||
@@ -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 无效循环
|
||||
|
||||
## 一个完整的迭代示例
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 模式(默认)
|
||||
|
||||
|
||||
@@ -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 (自动更新)
|
||||
|
||||
|
||||
@@ -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` | 最大会话数 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -102,6 +102,6 @@ FEATURE_BASH_CLASSIFIER=1 FEATURE_TREE_SITTER_BASH=1 bun run dev
|
||||
| `src/utils/permissions/bashClassifier.ts` | — | Bash 分类器(stub,ANT-ONLY) |
|
||||
| `src/utils/permissions/yoloClassifier.ts` | 1496 | YOLO 分类器(完整参考实现) |
|
||||
| `src/utils/classifierApprovals.ts` | — | 分类器审批信号管理 |
|
||||
| `src/components/permissions/BashPermissionRequest.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 标头 |
|
||||
|
||||
@@ -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` | — | 传输层 |
|
||||
|
||||
@@ -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
89
docs/features/channels.md
Normal 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 命令参考
|
||||
@@ -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 agent(claude/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 | 操作 | 场景 |
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
- ✅ `@ant/computer-use-input` 拆为 dispatcher + backends(darwin + win32)
|
||||
- ✅ `@ant/computer-use-swift` 拆为 dispatcher + backends(darwin + 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:直接 granted;Linux:检查 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:直接 granted;Linux:检查 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/pbpaste,win32→PowerShell Get/Set-Clipboard,linux→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/pbpaste,win32→PowerShell,linux→xclip |
|
||||
| 2.5 | `src/utils/computerUse/executor.ts:232` | 粘贴快捷键已按平台分发:darwin→command,其他→ctrl |
|
||||
| 2.6 | `src/utils/computerUse/executor.ts:302-309` | 非 darwin 已改为 `createCrossPlatformExecutor()` |
|
||||
| 2.7 | `src/utils/computerUse/drainRunLoop.ts` | 非 darwin 无需 pump(直接执行 fn) |
|
||||
| 2.8 | `src/utils/computerUse/escHotkey.ts` | 非 darwin 返回 false(已有 Ctrl+C fallback) |
|
||||
| 2.9 | `src/utils/computerUse/hostAdapter.ts` | 非 darwin 权限检查逻辑已实现 |
|
||||
| 2.10 | `src/utils/computerUse/common.ts:58` | 已改为动态 `process.platform` 分发 |
|
||||
| 2.11 | `src/utils/computerUse/common.ts:55` | 已改为 darwin→'native',其他→'none' |
|
||||
| 2.12 | `src/utils/computerUse/gates.ts:55` | 已更新(需验证 enabled 默认值) |
|
||||
| 2.13 | `src/utils/computerUse/gates.ts:39` | `hasRequiredSubscription()` 已更新 |
|
||||
|
||||
### Phase 3:新增 Linux 后端
|
||||
|
||||
|
||||
@@ -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` 命令 |
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# DAEMON — 后台守护进程
|
||||
|
||||
> Feature Flag: `FEATURE_DAEMON=1`
|
||||
> 实现状态:主进程和 worker 注册为 Stub,CLI 路由完整
|
||||
> 实现状态: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 Worker(headless 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` | 命令注册(双重门控) |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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 状态 |
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 创建 |
|
||||
|
||||
@@ -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 变化) |
|
||||
|
||||
## 三、关键设计决策
|
||||
|
||||
@@ -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: 支持) |
|
||||
|
||||
## 后续优化方向
|
||||
|
||||
|
||||
@@ -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 镜像 |
|
||||
|
||||
@@ -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 区分。
|
||||
|
||||
## 工作流程详解
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 | 部分实现 | 会话管理 | 后台会话注册/清理已实现,任务摘要是 stub(dev 默认启用) |
|
||||
| SHOT_STATS | 10 | 已实现 | 统计 | API 调用统计面板(build 默认启用) |
|
||||
| EXTRACT_MEMORIES | 7 | 已实现 | 记忆 | 自动记忆提取(build 默认启用,受 GrowthBook 门控) |
|
||||
| TEMPLATES | 6 | 部分实现 | 项目管理 | 项目/提示模板系统(dev 默认启用) |
|
||||
| LODESTONE | 6 | 已实现 | 深度链接 | URL 协议处理器(build 默认启用) |
|
||||
|
||||
## 单引用 Feature(40+ 个)
|
||||
|
||||
@@ -66,10 +29,9 @@ BUILDING_CLAUDE_APPS, ANTI_DISTILLATION_CC, AGENT_TRIGGERS, ABLATION_BASELINE
|
||||
|
||||
这些 feature 被列为 Tier 3 的原因:
|
||||
|
||||
1. **内部基础设施**(CHICAGO_MCP, LODESTONE):Anthropic 内部使用,外部无法运行
|
||||
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')` 查看具体使用场景。
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 遥测 |
|
||||
|
||||
@@ -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` | — | 彩虹高亮 |
|
||||
|
||||
@@ -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 流式传输 |
|
||||
|
||||
@@ -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` | 工具注册 |
|
||||
|
||||
@@ -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` | 工具注册 |
|
||||
|
||||
@@ -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` | 命令注册 |
|
||||
|
||||
@@ -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` — 回填会话数据
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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="启动时获取远程配置">
|
||||
|
||||
@@ -84,7 +84,7 @@ keywords: ["隐藏功能", "未公开功能", "秘密功能", "Claude Code 彩
|
||||
<Accordion title="VOICE_MODE:语音交互">
|
||||
**门控**: `feature('VOICE_MODE')`
|
||||
|
||||
代码中存在语音输入模式的注册点,但核心实现依赖于 `audio-napi` 包(在反编译版本中已 stub):
|
||||
代码中存在语音输入模式的注册点,核心实现依赖 `audio-capture-napi` 包(已恢复):
|
||||
|
||||
- 通过 `/voice` 命令激活
|
||||
- "按住说话"(hold-to-talk)交互模式
|
||||
|
||||
@@ -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。
|
||||
|
||||
|
||||
@@ -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 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ AI 没有真正的"记忆",Claude Code 通过精心分层营造了这个幻觉
|
||||
|
||||
### 3. 工具系统的权限双轨制
|
||||
|
||||
`src/tools/BashTool/shouldUseSandbox.ts` 展示了一个精巧的双重安全模型:
|
||||
`packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.ts` 展示了一个精巧的双重安全模型:
|
||||
|
||||
- **应用层**:权限规则决定"能不能执行"(白名单/黑名单/用户确认)
|
||||
- **OS 层**:沙箱决定"执行时能做什么"(文件系统/网络/进程隔离)
|
||||
|
||||
@@ -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` | 输入 schema(9 种操作的 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` | 输入 schema(9 种操作的 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 支持的操作
|
||||
|
||||
@@ -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:
|
||||
>
|
||||
|
||||
@@ -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.json(gitignored,个人覆盖)
|
||||
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`。
|
||||
|
||||
@@ -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`)定义了两套触发标准——外部版本更积极(鼓励规划),内部版本更克制(仅在真正模糊时使用):
|
||||
|
||||
| 场景 | 外部版本 | 内部版本 |
|
||||
|------|---------|---------|
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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 approval(src/tools/BashTool/bashPermissions.ts:2209)
|
||||
强制 require approval(packages/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)
|
||||
```
|
||||
|
||||
@@ -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`)
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
## 分阶段实现
|
||||
|
||||
@@ -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 备份),但有两个关键差异:
|
||||
|
||||
|
||||
@@ -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)。
|
||||
|
||||
|
||||
@@ -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,000ms(10 分钟,用户显式设置时)
|
||||
```
|
||||
|
||||
超时后系统不会直接杀进程——`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 中并行执行,有副作用的命令必须串行,防止竞态条件。
|
||||
|
||||
## 进度反馈的流式设计
|
||||
|
||||
|
||||
@@ -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] ?? []
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
59
package.json
59
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-code-best",
|
||||
"version": "1.4.2",
|
||||
"version": "1.6.0",
|
||||
"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": {
|
||||
@@ -57,6 +58,7 @@
|
||||
"postinstall": "node scripts/run-parallel.mjs scripts/postinstall.cjs scripts/setup-chrome-mcp.mjs",
|
||||
"docs:dev": "npx mintlify dev",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:all": "bun run typecheck && bun test",
|
||||
"rcs": "bun run scripts/rcs.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -72,23 +74,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 +99,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 +110,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 +139,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 +154,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 +164,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 +187,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",
|
||||
|
||||
@@ -41,6 +41,9 @@ acp-link --https /path/to/agent
|
||||
# Disable authentication (dangerous)
|
||||
acp-link --no-auth /path/to/agent
|
||||
|
||||
# Register to RCS with a specific channel group
|
||||
acp-link --group my-team /path/to/agent
|
||||
|
||||
# Pass arguments to the agent (use -- to separate)
|
||||
acp-link /path/to/agent -- --verbose --model gpt-4
|
||||
```
|
||||
@@ -49,7 +52,7 @@ acp-link /path/to/agent -- --verbose --model gpt-4
|
||||
|
||||
```
|
||||
USAGE
|
||||
acp-link [--port value] [--host value] [--debug] [--no-auth] [--https] <command>...
|
||||
acp-link [--port value] [--host value] [--debug] [--no-auth] [--https] [--group value] <command>...
|
||||
acp-link --help
|
||||
acp-link --version
|
||||
|
||||
@@ -59,6 +62,7 @@ FLAGS
|
||||
[--debug] Enable debug logging to file
|
||||
[--no-auth] Disable authentication (dangerous)
|
||||
[--https] Enable HTTPS with self-signed cert
|
||||
[--group] Channel group ID for RCS registration (letters, digits, hyphens, underscores only)
|
||||
-h --help Print help information and exit
|
||||
-v --version Print version information and exit
|
||||
|
||||
@@ -84,6 +88,34 @@ ws://localhost:9315/ws?token=<your-token>
|
||||
|
||||
Set `ACP_AUTH_TOKEN` env var to use a fixed token, or use `--no-auth` to disable (not recommended).
|
||||
|
||||
## RCS Upstream
|
||||
|
||||
acp-link can register to a Remote Control Server (RCS) for remote access. Set the following environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `ACP_RCS_URL` | RCS server URL (e.g. `http://rcs.example.com:3000`) |
|
||||
| `ACP_RCS_TOKEN` | API token for RCS authentication |
|
||||
| `ACP_RCS_GROUP` | Channel group ID to lock the agent into (letters, digits, `-`, `_` only) |
|
||||
|
||||
You can also use `--group <id>` on the CLI. The CLI flag takes priority over the env var.
|
||||
|
||||
## Manager UI
|
||||
|
||||
通过 `--manager` flag 启动独立的管理服务(不启动代理):
|
||||
|
||||
```bash
|
||||
# 启动 Manager(默认端口 9315)
|
||||
acp-link --manager
|
||||
|
||||
# 指定端口
|
||||
acp-link --manager --port 3210
|
||||
```
|
||||
|
||||
在浏览器打开 `http://localhost:<port>` 即可访问管理界面,创建、停止、删除多个 acp-link 子进程实例并实时查看日志。
|
||||
|
||||
通过 Manager UI 创建的子进程会自动跳过 Manager UI。
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "acp-link",
|
||||
"version": "1.0.1",
|
||||
"version": "2.0.0",
|
||||
"description": "ACP proxy server that bridges WebSocket clients to ACP agents",
|
||||
"author": "claude-code-best",
|
||||
"type": "module",
|
||||
@@ -14,12 +14,15 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "bun run src/cli/bin.ts",
|
||||
"dev": "ACP_RCS_URL=http://localhost:3000 ACP_RCS_TOKEN=test-my-key bun run src/cli/bin.ts ccb-bun -- --acp",
|
||||
"dev:remote": "ACP_RCS_URL=https://remote-control.claude-code-best.win/ ACP_RCS_TOKEN=test-my-key bun run src/cli/bin.ts ccb-bun -- --acp",
|
||||
"dev:manager": "ACP_RCS_URL=http://localhost:3000 ACP_RCS_TOKEN=test-my-key bun run src/cli/bin.ts --manager",
|
||||
"prepublishOnly": "bun run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/selfsigned": "^2.0.4",
|
||||
"@types/ws": "^8.18.1"
|
||||
"@types/ws": "^8.18.1",
|
||||
"@types/bun": "^1.3.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.19.0",
|
||||
|
||||
@@ -9,6 +9,8 @@ export const command = buildCommand({
|
||||
"The agent command is spawned as a subprocess and communicates via stdin/stdout.\n\n" +
|
||||
"Use -- to pass arguments to the agent:\n" +
|
||||
" acp-link /path/to/agent -- --verbose --model gpt-4\n\n" +
|
||||
"Use --manager to start the Manager Web UI instead:\n" +
|
||||
" acp-link --manager\n\n" +
|
||||
"For remote access, set ACP_AUTH_TOKEN environment variable or let it auto-generate.",
|
||||
},
|
||||
parameters: {
|
||||
@@ -40,6 +42,22 @@ export const command = buildCommand({
|
||||
brief: "Enable HTTPS with auto-generated self-signed certificate",
|
||||
default: false,
|
||||
},
|
||||
manager: {
|
||||
kind: "boolean",
|
||||
brief: "Start Manager Web UI (no proxy)",
|
||||
default: false,
|
||||
},
|
||||
group: {
|
||||
kind: "parsed",
|
||||
parse: (value: string) => {
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
|
||||
throw new Error(`Invalid group "${value}": only letters, digits, hyphens, and underscores are allowed`);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
brief: "Channel group ID for RCS registration (env: ACP_RCS_GROUP)",
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
positional: {
|
||||
kind: "array",
|
||||
@@ -48,12 +66,12 @@ export const command = buildCommand({
|
||||
parse: String,
|
||||
placeholder: "command",
|
||||
},
|
||||
minimum: 1,
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
func: async function (
|
||||
this: LocalContext,
|
||||
flags: { port: number; host: string; debug: boolean; "no-auth": boolean; https: boolean },
|
||||
flags: { port: number; host: string; debug: boolean; "no-auth": boolean; https: boolean; manager: boolean; group: string | undefined },
|
||||
...args: readonly string[]
|
||||
) {
|
||||
const port = flags.port;
|
||||
@@ -61,6 +79,21 @@ export const command = buildCommand({
|
||||
const debug = flags.debug;
|
||||
const noAuth = flags["no-auth"];
|
||||
const https = flags.https;
|
||||
const manager = flags.manager;
|
||||
const group = flags.group;
|
||||
|
||||
// Manager mode: start web UI only, no proxy
|
||||
if (manager) {
|
||||
const { startManager } = await import("../manager/index.js");
|
||||
await startManager(port);
|
||||
return;
|
||||
}
|
||||
|
||||
// Proxy mode: agent command is required
|
||||
if (args.length === 0) {
|
||||
console.error("Error: agent command is required (or use --manager)");
|
||||
process.exit(1);
|
||||
}
|
||||
const [command, ...agentArgs] = args;
|
||||
const cwd = process.cwd();
|
||||
|
||||
@@ -85,6 +118,6 @@ export const command = buildCommand({
|
||||
|
||||
// Import and run the server
|
||||
const { startServer } = await import("../server.js");
|
||||
await startServer({ port, host, command: command!, args: [...agentArgs], cwd, debug, token, https });
|
||||
await startServer({ port, host, command: command!, args: [...agentArgs], cwd, debug, token, https, group });
|
||||
},
|
||||
});
|
||||
|
||||
345
packages/acp-link/src/manager/html.ts
Normal file
345
packages/acp-link/src/manager/html.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
export const MANAGER_HTML = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ACP Manager</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f8f7f5;
|
||||
color: #1a1a1a;
|
||||
padding: 24px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h1 { font-size: 20px; font-weight: 600; margin-bottom: 20px; color: #1a1a1a; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
||||
.create-form {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e2de;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.form-group { display: flex; flex-direction: column; gap: 4px; }
|
||||
.form-group label { font-size: 12px; color: #888; }
|
||||
.form-group input {
|
||||
background: #fff;
|
||||
border: 1px solid #d5d2ce;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
color: #1a1a1a;
|
||||
font-size: 14px;
|
||||
width: 200px;
|
||||
}
|
||||
.form-group input.wide { width: 400px; }
|
||||
button {
|
||||
background: #d77757;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
button:hover { background: #c4694b; }
|
||||
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
button.danger { background: #a63d3d; }
|
||||
button.danger:hover { background: #c44a4a; }
|
||||
button.small { padding: 4px 10px; font-size: 12px; }
|
||||
.instances { display: flex; flex-direction: column; gap: 8px; }
|
||||
.instance-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e2de;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.instance-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.instance-header:hover { background: #f5f3f0; }
|
||||
.status-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.status-dot.running { background: #4ade80; box-shadow: 0 0 6px #4ade8066; }
|
||||
.status-dot.stopped { background: #aaa; }
|
||||
.status-dot.failed { background: #f87171; box-shadow: 0 0 6px #f8717166; }
|
||||
.instance-info { flex: 1; display: flex; gap: 16px; align-items: center; font-size: 13px; }
|
||||
.instance-info .group { font-weight: 600; color: #d77757; }
|
||||
.instance-info .cmd { color: #888; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.instance-info .pid { color: #999; font-size: 12px; }
|
||||
.instance-info .uptime { color: #999; font-size: 12px; }
|
||||
.instance-actions { display: flex; gap: 6px; }
|
||||
.expand-icon { color: #999; font-size: 12px; transition: transform 0.2s; }
|
||||
.expand-icon.open { transform: rotate(90deg); }
|
||||
.log-panel {
|
||||
display: none;
|
||||
border-top: 1px solid #e5e2de;
|
||||
background: #faf9f7;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.log-panel.visible { display: block; }
|
||||
.log-line { white-space: pre-wrap; word-break: break-all; }
|
||||
.log-line.stdout { color: #333; }
|
||||
.log-line.stderr { color: #d94040; }
|
||||
.empty { color: #999; text-align: center; padding: 40px; font-size: 14px; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
body { padding: 12px; }
|
||||
.create-form { flex-wrap: wrap; }
|
||||
.form-group input, .form-group input.wide { width: 100%; }
|
||||
.form-group { flex: 1 1 120px; min-width: 0; }
|
||||
.instance-header { flex-wrap: wrap; padding: 10px 12px; gap: 8px; }
|
||||
.instance-info { flex-wrap: wrap; gap: 6px; font-size: 12px; }
|
||||
.instance-info .cmd { max-width: 100%; }
|
||||
button.small { padding: 8px 14px; min-height: 44px; font-size: 13px; }
|
||||
.log-panel { max-height: 50vh; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>ACP Manager</h1>
|
||||
</div>
|
||||
|
||||
<div class="create-form">
|
||||
<div class="form-group">
|
||||
<label>Group</label>
|
||||
<input type="text" id="inp-group" placeholder="my-group" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>ACP Command</label>
|
||||
<input type="text" id="inp-command" class="wide" placeholder="/path/to/agent --verbose" />
|
||||
</div>
|
||||
<button id="btn-create">Create</button>
|
||||
</div>
|
||||
|
||||
<div class="instances" id="instance-list"></div>
|
||||
|
||||
<script>
|
||||
var listEl = document.getElementById('instance-list');
|
||||
var esMap = {};
|
||||
var instances = [];
|
||||
var inpGroup = document.getElementById('inp-group');
|
||||
var inpCommand = document.getElementById('inp-command');
|
||||
var btnCreate = document.getElementById('btn-create');
|
||||
|
||||
// localStorage persistence
|
||||
function loadForm() {
|
||||
try {
|
||||
inpGroup.value = localStorage.getItem('acp-mgr-group') || '';
|
||||
inpCommand.value = localStorage.getItem('acp-mgr-command') || '';
|
||||
} catch(e) {}
|
||||
}
|
||||
function saveForm() {
|
||||
try {
|
||||
localStorage.setItem('acp-mgr-group', inpGroup.value);
|
||||
localStorage.setItem('acp-mgr-command', inpCommand.value);
|
||||
} catch(e) {}
|
||||
}
|
||||
inpGroup.addEventListener('input', saveForm);
|
||||
inpCommand.addEventListener('input', saveForm);
|
||||
loadForm();
|
||||
|
||||
btnCreate.addEventListener('click', function() {
|
||||
var group = inpGroup.value.trim();
|
||||
var command = inpCommand.value.trim();
|
||||
if (!group || !command) return alert('Both fields required');
|
||||
btnCreate.disabled = true;
|
||||
fetch('/api/instances', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ group: group, command: command }),
|
||||
}).then(function() { fetchInstances(); })
|
||||
.finally(function() { btnCreate.disabled = false; });
|
||||
});
|
||||
|
||||
// event delegation for instance actions
|
||||
listEl.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('[data-action]');
|
||||
if (btn) {
|
||||
e.stopPropagation();
|
||||
var id = btn.getAttribute('data-id');
|
||||
var action = btn.getAttribute('data-action');
|
||||
if (action === 'stop') stopInstance(id);
|
||||
else if (action === 'delete') deleteInstance(id);
|
||||
return;
|
||||
}
|
||||
var header = e.target.closest('.instance-header');
|
||||
if (header) {
|
||||
var cardId = header.closest('.instance-card').getAttribute('data-id');
|
||||
toggleLog(cardId);
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchInstances() {
|
||||
var res = await fetch('/api/instances');
|
||||
instances = await res.json();
|
||||
render();
|
||||
}
|
||||
|
||||
function uptime(start) {
|
||||
var s = Math.floor((Date.now() - start) / 1000);
|
||||
if (s < 60) return s + 's';
|
||||
if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's';
|
||||
return Math.floor(s/3600) + 'h ' + Math.floor((s%3600)/60) + 'm';
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (instances.length === 0) {
|
||||
listEl.innerHTML = '<div class="empty">No instances. Create one above.</div>';
|
||||
return;
|
||||
}
|
||||
// Diff-based update: only rebuild cards whose status changed
|
||||
var existingCards = {};
|
||||
listEl.querySelectorAll('.instance-card').forEach(function(card) {
|
||||
existingCards[card.getAttribute('data-id')] = card;
|
||||
});
|
||||
|
||||
var newIds = new Set(instances.map(function(i) { return i.id; }));
|
||||
|
||||
// Remove cards that no longer exist
|
||||
for (var eid in existingCards) {
|
||||
if (!newIds.has(eid)) {
|
||||
closeLog(eid);
|
||||
existingCards[eid].remove();
|
||||
delete existingCards[eid];
|
||||
}
|
||||
}
|
||||
|
||||
// Update or create cards in order
|
||||
instances.forEach(function(inst) {
|
||||
var card = existingCards[inst.id];
|
||||
if (!card) {
|
||||
// New instance — create card
|
||||
card = document.createElement('div');
|
||||
card.className = 'instance-card';
|
||||
card.setAttribute('data-id', inst.id);
|
||||
card.innerHTML =
|
||||
'<div class="instance-header">' +
|
||||
'<span class="expand-icon">▶</span>' +
|
||||
'<span class="status-dot"></span>' +
|
||||
'<div class="instance-info">' +
|
||||
'<span class="group"></span>' +
|
||||
'<span class="cmd"></span>' +
|
||||
'<span class="pid"></span>' +
|
||||
'<span class="uptime"></span>' +
|
||||
'</div>' +
|
||||
'<div class="instance-actions"></div>' +
|
||||
'</div>' +
|
||||
'<div class="log-panel" id="log-' + inst.id + '"></div>';
|
||||
listEl.appendChild(card);
|
||||
}
|
||||
// Update card content
|
||||
card.querySelector('.status-dot').className = 'status-dot ' + inst.status;
|
||||
card.querySelector('.group').textContent = inst.group;
|
||||
card.querySelector('.cmd').textContent = inst.command;
|
||||
card.querySelector('.pid').textContent = inst.pid ? 'PID ' + inst.pid : '';
|
||||
card.querySelector('.uptime').textContent = inst.status === 'running' ? uptime(inst.startTime) : '';
|
||||
|
||||
// Update action buttons
|
||||
var actions = card.querySelector('.instance-actions');
|
||||
var prevStatus = card.getAttribute('data-status');
|
||||
if (prevStatus !== inst.status) {
|
||||
card.setAttribute('data-status', inst.status);
|
||||
actions.innerHTML = inst.status === 'running'
|
||||
? '<button class="small danger" data-action="stop" data-id="' + inst.id + '">Stop</button>'
|
||||
: '<button class="small danger" data-action="delete" data-id="' + inst.id + '">Delete</button>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function stopInstance(id) {
|
||||
var btn = listEl.querySelector('[data-action="stop"][data-id="' + id + '"]');
|
||||
if (btn) btn.disabled = true;
|
||||
await fetch('/api/instances/' + id + '/stop', { method: 'POST' });
|
||||
await fetchInstances();
|
||||
}
|
||||
|
||||
async function deleteInstance(id) {
|
||||
var btn = listEl.querySelector('[data-action="delete"][data-id="' + id + '"]');
|
||||
if (btn) btn.disabled = true;
|
||||
await fetch('/api/instances/' + id, { method: 'DELETE' });
|
||||
closeLog(id);
|
||||
await fetchInstances();
|
||||
}
|
||||
|
||||
function toggleLog(id) {
|
||||
var panel = document.getElementById('log-' + id);
|
||||
if (!panel) return;
|
||||
if (panel.classList.contains('visible')) {
|
||||
closeLog(id);
|
||||
} else {
|
||||
openLog(id);
|
||||
}
|
||||
var icon = listEl.querySelector('[data-id="' + id + '"] .expand-icon');
|
||||
if (icon) icon.classList.toggle('open', panel.classList.contains('visible'));
|
||||
}
|
||||
|
||||
function openLog(id) {
|
||||
var panel = document.getElementById('log-' + id);
|
||||
if (!panel) return;
|
||||
panel.classList.add('visible');
|
||||
panel.innerHTML = '';
|
||||
var es = new EventSource('/api/instances/' + id + '/logs');
|
||||
esMap[id] = es;
|
||||
var scrollPending = false;
|
||||
es.onmessage = function(e) {
|
||||
try {
|
||||
var entry = JSON.parse(e.data);
|
||||
var line = document.createElement('div');
|
||||
line.className = 'log-line ' + entry.stream;
|
||||
var time = new Date(entry.timestamp).toLocaleTimeString();
|
||||
line.textContent = '[' + time + '] ' + entry.text;
|
||||
panel.appendChild(line);
|
||||
if (panel.children.length > 500) panel.removeChild(panel.firstChild);
|
||||
if (!scrollPending) {
|
||||
scrollPending = true;
|
||||
requestAnimationFrame(function() {
|
||||
panel.scrollTop = panel.scrollHeight;
|
||||
scrollPending = false;
|
||||
});
|
||||
}
|
||||
} catch(err) {}
|
||||
};
|
||||
es.onerror = function() {
|
||||
es.close();
|
||||
delete esMap[id];
|
||||
};
|
||||
}
|
||||
|
||||
function closeLog(id) {
|
||||
if (esMap[id]) {
|
||||
esMap[id].close();
|
||||
delete esMap[id];
|
||||
}
|
||||
var panel = document.getElementById('log-' + id);
|
||||
if (panel) panel.classList.remove('visible');
|
||||
}
|
||||
|
||||
fetchInstances();
|
||||
setInterval(fetchInstances, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
44
packages/acp-link/src/manager/index.ts
Normal file
44
packages/acp-link/src/manager/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Hono } from "hono";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { ProcessManager } from "./manager.js";
|
||||
import { createApp } from "./routes.js";
|
||||
|
||||
export async function startManager(port: number): Promise<void> {
|
||||
const manager = new ProcessManager();
|
||||
const app = createApp(manager);
|
||||
|
||||
// Health check
|
||||
app.get("/health", (c) => c.json({ status: "ok" }));
|
||||
|
||||
let shuttingDown = false;
|
||||
const shutdown = async () => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
console.log("Shutting down...");
|
||||
await manager.shutdownAll();
|
||||
process.exit(0);
|
||||
};
|
||||
process.on("SIGTERM", shutdown);
|
||||
process.on("SIGINT", shutdown);
|
||||
|
||||
const server = serve({ fetch: app.fetch, port });
|
||||
server.on("error", (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
console.error(`\n Error: port ${port} is already in use. Use --port to specify a different port.\n`);
|
||||
} else {
|
||||
console.error(`\n Error: ${err.message}\n`);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
console.log();
|
||||
console.log(` 🖥️ ACP Manager`);
|
||||
console.log();
|
||||
console.log(` URL: http://localhost:${port}`);
|
||||
console.log();
|
||||
console.log(` Press Ctrl+C to stop`);
|
||||
console.log();
|
||||
|
||||
// Keep running
|
||||
await new Promise(() => {});
|
||||
}
|
||||
233
packages/acp-link/src/manager/manager.ts
Normal file
233
packages/acp-link/src/manager/manager.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import type { AcpInstance, InstanceSummary, LogEntry } from "./types.js";
|
||||
|
||||
function log(tag: string, msg: string) {
|
||||
const ts = new Date().toISOString();
|
||||
console.log(`[${ts}] [${tag}] ${msg}`);
|
||||
}
|
||||
|
||||
const MAX_LOG_LINES = 2000;
|
||||
const SHUTDOWN_TIMEOUT_MS = 5000;
|
||||
|
||||
export class ProcessManager {
|
||||
private instances = new Map<string, AcpInstance>();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private processes = new Map<string, any>();
|
||||
|
||||
create(group: string, command: string): AcpInstance {
|
||||
const id = crypto.randomUUID();
|
||||
const instance: AcpInstance = {
|
||||
id,
|
||||
group,
|
||||
command,
|
||||
status: "running",
|
||||
pid: undefined,
|
||||
startTime: Date.now(),
|
||||
exitCode: null,
|
||||
logs: [],
|
||||
subscribers: new Set(),
|
||||
};
|
||||
|
||||
const args = this.parseCommand(command);
|
||||
const fullArgs = ["--group", group, ...args];
|
||||
|
||||
const proc = Bun.spawn(["acp-link", ...fullArgs], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: { ...Bun.env, ACP_CHILD: "1" },
|
||||
});
|
||||
|
||||
instance.pid = proc.pid;
|
||||
this.instances.set(id, instance);
|
||||
this.processes.set(id, proc);
|
||||
log("manager", `created instance ${id.slice(0, 8)} group=${group} pid=${proc.pid} cmd="acp-link ${fullArgs.join(" ")}"`);
|
||||
|
||||
this.pipeStream(proc.stdout, id, "stdout");
|
||||
this.pipeStream(proc.stderr, id, "stderr");
|
||||
|
||||
proc.exited.then((code) => {
|
||||
instance.status = code === 0 ? "stopped" : "failed";
|
||||
instance.exitCode = code;
|
||||
instance.pid = undefined;
|
||||
this.processes.delete(id);
|
||||
log("manager", `instance ${id.slice(0, 8)} ${instance.status} exit=${code}`);
|
||||
this.notifyStatus(instance);
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
stop(id: string): boolean {
|
||||
const proc = this.processes.get(id);
|
||||
if (!proc) return false;
|
||||
const inst = this.instances.get(id);
|
||||
log("manager", `stopping instance ${id.slice(0, 8)} pid=${proc.pid}`);
|
||||
proc.kill("SIGTERM");
|
||||
// Immediately mark as stopped to prevent stale state
|
||||
if (inst) {
|
||||
inst.status = "stopped";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
remove(id: string): boolean {
|
||||
const instance = this.instances.get(id);
|
||||
if (!instance) return false;
|
||||
if (instance.status === "running") return false;
|
||||
instance.subscribers.clear();
|
||||
this.instances.delete(id);
|
||||
log("manager", `removed instance ${id.slice(0, 8)} group=${instance.group}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
list(): InstanceSummary[] {
|
||||
return Array.from(this.instances.values()).map(this.toSummary);
|
||||
}
|
||||
|
||||
get(id: string): AcpInstance | undefined {
|
||||
return this.instances.get(id);
|
||||
}
|
||||
|
||||
subscribe(id: string, callback: (entry: LogEntry) => void): () => void {
|
||||
const instance = this.instances.get(id);
|
||||
if (!instance) return () => {};
|
||||
instance.subscribers.add(callback);
|
||||
return () => instance.subscribers.delete(callback);
|
||||
}
|
||||
|
||||
async shutdownAll(): Promise<void> {
|
||||
const running = Array.from(this.processes.entries());
|
||||
if (running.length === 0) return;
|
||||
|
||||
log("manager", `shutting down ${running.length} running instance(s)...`);
|
||||
for (const [id, proc] of running) {
|
||||
try {
|
||||
proc.kill("SIGTERM");
|
||||
log("manager", `sent SIGTERM to ${id.slice(0, 8)} pid=${proc.pid}`);
|
||||
} catch {
|
||||
// already dead
|
||||
}
|
||||
}
|
||||
|
||||
const timeout = new Promise<void>((resolve) => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS));
|
||||
await Promise.race([
|
||||
Promise.all(running.map(([, proc]) => proc.exited.catch(() => {}))),
|
||||
timeout,
|
||||
]);
|
||||
|
||||
for (const [id, proc] of running) {
|
||||
try {
|
||||
proc.kill("SIGKILL");
|
||||
log("manager", `sent SIGKILL to ${id.slice(0, 8)}`);
|
||||
} catch {
|
||||
// already dead
|
||||
}
|
||||
}
|
||||
log("manager", "all instances shut down");
|
||||
}
|
||||
|
||||
private parseCommand(command: string): string[] {
|
||||
const args: string[] = [];
|
||||
let current = "";
|
||||
let inQuote: string | null = null;
|
||||
|
||||
for (const ch of command) {
|
||||
if (inQuote) {
|
||||
if (ch === inQuote) {
|
||||
inQuote = null;
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
} else if (ch === '"' || ch === "'") {
|
||||
inQuote = ch;
|
||||
} else if (ch === " " || ch === "\t") {
|
||||
if (current) {
|
||||
args.push(current);
|
||||
current = "";
|
||||
}
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
if (current) args.push(current);
|
||||
return args;
|
||||
}
|
||||
|
||||
private pipeStream(
|
||||
readable: ReadableStream<Uint8Array>,
|
||||
instanceId: string,
|
||||
stream: "stdout" | "stderr",
|
||||
) {
|
||||
const reader = readable.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
const processChunk = () => {
|
||||
reader
|
||||
.read()
|
||||
.then(({ done, value }) => {
|
||||
if (done) {
|
||||
if (buffer) this.appendLog(instanceId, buffer, stream);
|
||||
return;
|
||||
}
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
if (line) this.appendLog(instanceId, line, stream);
|
||||
}
|
||||
processChunk();
|
||||
})
|
||||
.catch(() => {
|
||||
// stream ended or error
|
||||
});
|
||||
};
|
||||
processChunk();
|
||||
}
|
||||
|
||||
private appendLog(instanceId: string, text: string, stream: "stdout" | "stderr") {
|
||||
const instance = this.instances.get(instanceId);
|
||||
if (!instance) return;
|
||||
|
||||
const entry: LogEntry = { timestamp: Date.now(), stream, text };
|
||||
instance.logs.push(entry);
|
||||
if (instance.logs.length > MAX_LOG_LINES) {
|
||||
instance.logs.splice(0, instance.logs.length - MAX_LOG_LINES);
|
||||
}
|
||||
|
||||
for (const sub of instance.subscribers) {
|
||||
try {
|
||||
sub(entry);
|
||||
} catch {
|
||||
// subscriber error, remove it
|
||||
instance.subscribers.delete(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private notifyStatus(instance: AcpInstance) {
|
||||
const statusEntry: LogEntry = {
|
||||
timestamp: Date.now(),
|
||||
stream: "stderr",
|
||||
text: `[${instance.status}] exit code: ${instance.exitCode}`,
|
||||
};
|
||||
for (const sub of instance.subscribers) {
|
||||
try {
|
||||
sub(statusEntry);
|
||||
} catch {
|
||||
instance.subscribers.delete(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private toSummary(inst: AcpInstance): InstanceSummary {
|
||||
return {
|
||||
id: inst.id,
|
||||
group: inst.group,
|
||||
command: inst.command,
|
||||
status: inst.status,
|
||||
pid: inst.pid,
|
||||
startTime: inst.startTime,
|
||||
exitCode: inst.exitCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
153
packages/acp-link/src/manager/routes.ts
Normal file
153
packages/acp-link/src/manager/routes.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Hono } from "hono";
|
||||
import type { ProcessManager } from "./manager.js";
|
||||
import { MANAGER_HTML } from "./html.js";
|
||||
|
||||
function logReq(method: string, path: string, status?: number) {
|
||||
const ts = new Date().toISOString();
|
||||
const suffix = status != null ? ` -> ${status}` : "";
|
||||
console.log(`[${ts}] [http] ${method} ${path}${suffix}`);
|
||||
}
|
||||
|
||||
export function createApp(manager: ProcessManager): Hono {
|
||||
const app = new Hono();
|
||||
|
||||
app.get("/", (c) => {
|
||||
logReq("GET", "/", 200);
|
||||
return c.html(MANAGER_HTML);
|
||||
});
|
||||
|
||||
app.get("/api/instances", (c) => {
|
||||
const list = manager.list();
|
||||
logReq("GET", "/api/instances", 200);
|
||||
return c.json(list);
|
||||
});
|
||||
|
||||
app.post("/api/instances", async (c) => {
|
||||
let body: { group?: string; command?: string };
|
||||
try {
|
||||
body = await c.req.json<{ group?: string; command?: string }>();
|
||||
} catch {
|
||||
logReq("POST", "/api/instances", 400);
|
||||
return c.json({ error: "invalid JSON body" }, 400);
|
||||
}
|
||||
if (!body.group?.trim() || !body.command?.trim()) {
|
||||
logReq("POST", "/api/instances", 400);
|
||||
return c.json({ error: "group and command are required" }, 400);
|
||||
}
|
||||
const instance = manager.create(body.group.trim(), body.command.trim());
|
||||
logReq("POST", `/api/instances group=${body.group}`, 201);
|
||||
return c.json(
|
||||
{
|
||||
id: instance.id,
|
||||
group: instance.group,
|
||||
command: instance.command,
|
||||
status: instance.status,
|
||||
pid: instance.pid,
|
||||
startTime: instance.startTime,
|
||||
exitCode: instance.exitCode,
|
||||
},
|
||||
201,
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/api/instances/:id/stop", (c) => {
|
||||
const id = c.req.param("id");
|
||||
const inst = manager.get(id);
|
||||
if (!inst) {
|
||||
logReq("POST", `/api/instances/${id.slice(0, 8)}/stop`, 404);
|
||||
return c.json({ error: "not found" }, 404);
|
||||
}
|
||||
if (inst.status !== "running") {
|
||||
logReq("POST", `/api/instances/${id.slice(0, 8)}/stop`, 400);
|
||||
return c.json({ error: "not running" }, 400);
|
||||
}
|
||||
manager.stop(inst.id);
|
||||
logReq("POST", `/api/instances/${id.slice(0, 8)}/stop`, 200);
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
app.delete("/api/instances/:id", (c) => {
|
||||
const id = c.req.param("id");
|
||||
const inst = manager.get(id);
|
||||
if (!inst) {
|
||||
logReq("DELETE", `/api/instances/${id.slice(0, 8)}`, 404);
|
||||
return c.json({ error: "not found" }, 404);
|
||||
}
|
||||
if (inst.status === "running") {
|
||||
logReq("DELETE", `/api/instances/${id.slice(0, 8)}`, 400);
|
||||
return c.json({ error: "still running" }, 400);
|
||||
}
|
||||
manager.remove(inst.id);
|
||||
logReq("DELETE", `/api/instances/${id.slice(0, 8)}`, 200);
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
app.get("/api/instances/:id/logs", (c) => {
|
||||
const id = c.req.param("id");
|
||||
const inst = manager.get(id);
|
||||
if (!inst) {
|
||||
logReq("GET", `/api/instances/${id.slice(0, 8)}/logs`, 404);
|
||||
return c.json({ error: "not found" }, 404);
|
||||
}
|
||||
logReq("GET", `/api/instances/${id.slice(0, 8)}/logs SSE`);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const send = (data: string) => {
|
||||
try {
|
||||
controller.enqueue(encoder.encode(data));
|
||||
} catch {
|
||||
// stream closed
|
||||
}
|
||||
};
|
||||
|
||||
// send historical logs
|
||||
for (const log of inst.logs) {
|
||||
send(`data: ${JSON.stringify(log)}\n\n`);
|
||||
}
|
||||
|
||||
// subscribe to new logs
|
||||
const unsub = manager.subscribe(inst.id, (entry) => {
|
||||
send(`data: ${JSON.stringify(entry)}\n\n`);
|
||||
});
|
||||
|
||||
// keepalive every 15s
|
||||
const keepalive = setInterval(() => {
|
||||
send(": keepalive\n\n");
|
||||
}, 15000);
|
||||
|
||||
const cleanup = () => {
|
||||
unsub();
|
||||
clearInterval(keepalive);
|
||||
logReq("SSE", `/api/instances/${id.slice(0, 8)}/logs closed`);
|
||||
try {
|
||||
controller.close();
|
||||
} catch {
|
||||
// already closed
|
||||
}
|
||||
};
|
||||
|
||||
c.req.raw.signal.addEventListener("abort", cleanup, { once: true });
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Catch-all: log unmatched routes for debugging
|
||||
app.all("*", (c) => {
|
||||
logReq(c.req.method, c.req.path, 404);
|
||||
return c.json({ error: "not found", path: c.req.path }, 404);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
34
packages/acp-link/src/manager/types.ts
Normal file
34
packages/acp-link/src/manager/types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export type InstanceStatus = "running" | "stopped" | "failed";
|
||||
|
||||
export interface AcpInstance {
|
||||
id: string;
|
||||
group: string;
|
||||
command: string;
|
||||
status: InstanceStatus;
|
||||
pid: number | undefined;
|
||||
startTime: number;
|
||||
exitCode: number | null;
|
||||
logs: LogEntry[];
|
||||
subscribers: Set<(entry: LogEntry) => void>;
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number;
|
||||
stream: "stdout" | "stderr";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface CreateInstanceRequest {
|
||||
group: string;
|
||||
command: string;
|
||||
}
|
||||
|
||||
export interface InstanceSummary {
|
||||
id: string;
|
||||
group: string;
|
||||
command: string;
|
||||
status: InstanceStatus;
|
||||
pid: number | undefined;
|
||||
startTime: number;
|
||||
exitCode: number | null;
|
||||
}
|
||||
@@ -22,6 +22,8 @@ export interface ServerConfig {
|
||||
https?: boolean;
|
||||
/** Default permission mode for new sessions (e.g. "auto", "default", "bypassPermissions") */
|
||||
permissionMode?: string;
|
||||
/** Channel group ID for RCS registration */
|
||||
group?: string;
|
||||
}
|
||||
|
||||
// Pending permission request
|
||||
@@ -608,11 +610,16 @@ export async function startServer(config: ServerConfig): Promise<void> {
|
||||
// Initialize RCS upstream client if configured
|
||||
const rcsUrl = process.env.ACP_RCS_URL;
|
||||
const rcsToken = process.env.ACP_RCS_TOKEN;
|
||||
const rcsGroup = config.group || process.env.ACP_RCS_GROUP;
|
||||
if (rcsGroup && !/^[a-zA-Z0-9_-]+$/.test(rcsGroup)) {
|
||||
throw new Error(`Invalid ACP_RCS_GROUP "${rcsGroup}": only letters, digits, hyphens, and underscores are allowed`);
|
||||
}
|
||||
if (rcsUrl) {
|
||||
rcsUpstream = new RcsUpstreamClient({
|
||||
rcsUrl,
|
||||
apiToken: rcsToken || "",
|
||||
agentName: command,
|
||||
channelGroupId: rcsGroup || undefined,
|
||||
maxSessions: 1,
|
||||
});
|
||||
|
||||
@@ -876,20 +883,16 @@ export async function startServer(config: ServerConfig): Promise<void> {
|
||||
authEnabled: !!AUTH_TOKEN,
|
||||
}, "started");
|
||||
|
||||
// Graceful shutdown — close RCS upstream
|
||||
const shutdown = async () => {
|
||||
if (rcsUpstream) {
|
||||
await rcsUpstream.close();
|
||||
}
|
||||
process.exit(0);
|
||||
};
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
// Keep the server running
|
||||
await new Promise(() => {});
|
||||
}
|
||||
|
||||
// Graceful shutdown — close RCS upstream on process exit
|
||||
process.on("SIGINT", async () => {
|
||||
if (rcsUpstream) {
|
||||
await rcsUpstream.close();
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
process.on("SIGTERM", async () => {
|
||||
if (rcsUpstream) {
|
||||
await rcsUpstream.close();
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"module": "esnext",
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
|
||||
// Node.js module resolution
|
||||
"moduleResolution": "NodeNext",
|
||||
"moduleResolution": "bundler",
|
||||
"verbatimModuleSyntax": true,
|
||||
|
||||
// Output
|
||||
@@ -30,7 +30,8 @@
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"types": ["bun"],
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "src/__tests__"]
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
// createRequire works in both Bun and Node.js ESM contexts.
|
||||
// Needed because this package is "type": "module" but uses require() for
|
||||
// loading native .node addons — bare require is not available in Node.js ESM.
|
||||
const nodeRequire = createRequire(import.meta.url)
|
||||
|
||||
type AudioCaptureNapi = {
|
||||
startRecording(
|
||||
@@ -41,7 +47,7 @@ function loadModule(): AudioCaptureNapi | null {
|
||||
if (process.env.AUDIO_CAPTURE_NODE_PATH) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
cachedModule = require(
|
||||
cachedModule = nodeRequire(
|
||||
process.env.AUDIO_CAPTURE_NODE_PATH,
|
||||
) as AudioCaptureNapi
|
||||
return cachedModule
|
||||
@@ -63,7 +69,7 @@ function loadModule(): AudioCaptureNapi | null {
|
||||
for (const p of fallbacks) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
cachedModule = require(p) as AudioCaptureNapi
|
||||
cachedModule = nodeRequire(p) as AudioCaptureNapi
|
||||
return cachedModule
|
||||
} catch {
|
||||
// try next
|
||||
|
||||
@@ -17,10 +17,16 @@
|
||||
* getSyntaxTheme always returns the default for the given Claude theme.
|
||||
*/
|
||||
|
||||
import { createRequire } from 'node:module'
|
||||
import { diffArrays } from 'diff'
|
||||
import type * as hljsNamespace from 'highlight.js'
|
||||
import { basename, extname } from 'path'
|
||||
|
||||
// createRequire works in both Bun and Node.js ESM contexts.
|
||||
// Needed because this package is "type": "module" but uses require() for
|
||||
// lazy loading — bare require is not available in Node.js ESM.
|
||||
const nodeRequire = createRequire(import.meta.url)
|
||||
|
||||
// Lazy: defers loading highlight.js until first render. The full bundle
|
||||
// registers 190+ language grammars at require time (~50MB, 100-200ms on
|
||||
// macOS, several× that on Windows). With a top-level import, any caller
|
||||
@@ -34,8 +40,7 @@ type HLJSApi = typeof hljsNamespace.default
|
||||
let cachedHljs: HLJSApi | null = null
|
||||
function hljs(): HLJSApi {
|
||||
if (cachedHljs) return cachedHljs
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const mod = require('highlight.js')
|
||||
const mod = nodeRequire('highlight.js')
|
||||
// highlight.js uses `export =` (CJS). Under bun/ESM the interop wraps it
|
||||
// in .default; under node CJS the module IS the API. Check at runtime.
|
||||
cachedHljs = 'default' in mod && mod.default ? mod.default : mod
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readFileSync, unlinkSync } from 'node:fs'
|
||||
import sharpModule from 'sharp'
|
||||
|
||||
export const sharp = sharpModule
|
||||
@@ -62,13 +63,11 @@ return "${tmpPath}"
|
||||
}
|
||||
|
||||
const file = Bun.file(tmpPath)
|
||||
// Use synchronous read via Node compat
|
||||
const fs = require('fs')
|
||||
const buffer: Buffer = fs.readFileSync(tmpPath)
|
||||
const buffer: Buffer = readFileSync(tmpPath)
|
||||
|
||||
// Clean up temp file
|
||||
try {
|
||||
fs.unlinkSync(tmpPath)
|
||||
unlinkSync(tmpPath)
|
||||
} catch {
|
||||
// ignore cleanup errors
|
||||
}
|
||||
|
||||
@@ -90,9 +90,20 @@ export function getUuidFromRequest(c: Context): string | undefined {
|
||||
|
||||
/**
|
||||
* UUID-based auth for Web UI routes (no-login mode).
|
||||
* Requires a UUID in query param or header, injects it into context as c.set("uuid").
|
||||
* Accepts UUID in query param/header, OR a valid API key via Authorization header.
|
||||
*/
|
||||
export async function uuidAuth(c: Context, next: Next) {
|
||||
// Try API key auth via Authorization header
|
||||
const bearer = extractBearerToken(c);
|
||||
if (bearer && validateApiKey(bearer)) {
|
||||
// Valid API key — generate a stable UUID from the key for downstream use
|
||||
const uuid = getUuidFromRequest(c);
|
||||
c.set("uuid", uuid || bearer);
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to UUID auth
|
||||
const uuid = getUuidFromRequest(c);
|
||||
if (!uuid) {
|
||||
return c.json({ error: { type: "unauthorized", message: "Missing UUID" } }, 401);
|
||||
|
||||
@@ -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(/\/+$/, "");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import { registerEnvironment, deregisterEnvironment, reconnectEnvironment } from "../../services/environment";
|
||||
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
|
||||
import { storeBindSession } from "../../store";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
@@ -9,6 +10,13 @@ app.post("/bridge", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||
const body = await c.req.json();
|
||||
const username = c.get("username");
|
||||
const result = registerEnvironment({ ...body, username });
|
||||
// Bind ACP session to the group ID so the web UI can find it by group
|
||||
if (result.session_id) {
|
||||
const groupId = body.bridge_id as string | undefined;
|
||||
if (groupId) {
|
||||
storeBindSession(result.session_id, groupId);
|
||||
}
|
||||
}
|
||||
return c.json(result, 200);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
storeUpdateEnvironment,
|
||||
storeListActiveEnvironments,
|
||||
storeListActiveEnvironmentsByUsername,
|
||||
storeListSessionsByEnvironment,
|
||||
} from "../store";
|
||||
import type { RegisterEnvironmentRequest, EnvironmentResponse } from "../types/api";
|
||||
import type { EnvironmentRecord } from "../store";
|
||||
@@ -20,6 +21,7 @@ function toResponse(row: EnvironmentRecord): EnvironmentResponse {
|
||||
username: row.username,
|
||||
last_poll_at: row.lastPollAt ? row.lastPollAt.getTime() / 1000 : null,
|
||||
worker_type: row.workerType,
|
||||
channel_group_id: row.bridgeId,
|
||||
capabilities: row.capabilities,
|
||||
};
|
||||
}
|
||||
@@ -41,14 +43,19 @@ export function registerEnvironment(req: RegisterEnvironmentRequest & { metadata
|
||||
});
|
||||
|
||||
let sessionId: string | undefined;
|
||||
// ACP agents: auto-create a session so they appear in the dashboard sessions list
|
||||
// ACP agents: reuse existing session or create one
|
||||
if (workerType === "acp") {
|
||||
const session = storeCreateSession({
|
||||
environmentId: record.id,
|
||||
title: req.machine_name || "ACP Agent",
|
||||
source: "acp",
|
||||
});
|
||||
sessionId = session.id;
|
||||
const existing = storeListSessionsByEnvironment(record.id);
|
||||
if (existing.length > 0) {
|
||||
sessionId = existing[0].id;
|
||||
} else {
|
||||
const session = storeCreateSession({
|
||||
environmentId: record.id,
|
||||
title: req.machine_name || "ACP Agent",
|
||||
source: "acp",
|
||||
});
|
||||
sessionId = session.id;
|
||||
}
|
||||
}
|
||||
|
||||
return { environment_id: record.id, environment_secret: record.secret, status: record.status as "active", session_id: sessionId };
|
||||
|
||||
@@ -98,13 +98,14 @@ export function storeDeleteToken(token: string): boolean {
|
||||
|
||||
// ---------- Environment ----------
|
||||
|
||||
/** Find an active environment by machineName (optionally filtered by workerType) */
|
||||
/** Find an active or offline environment by machineName (optionally filtered by workerType).
|
||||
* Includes "offline" so ACP agents can be reused on reconnect. */
|
||||
export function storeFindEnvironmentByMachineName(
|
||||
machineName: string,
|
||||
workerType?: string,
|
||||
): EnvironmentRecord | undefined {
|
||||
for (const rec of environments.values()) {
|
||||
if (rec.machineName === machineName && rec.status === "active") {
|
||||
if (rec.machineName === machineName && (rec.status === "active" || rec.status === "offline")) {
|
||||
if (!workerType || rec.workerType === workerType) {
|
||||
return rec;
|
||||
}
|
||||
@@ -313,12 +314,32 @@ export function storeGetSessionOwners(sessionId: string): Set<string> | undefine
|
||||
|
||||
export function storeListSessionsByOwnerUuid(uuid: string): SessionRecord[] {
|
||||
const result: SessionRecord[] = [];
|
||||
const resultIds = new Set<string>();
|
||||
|
||||
// Collect sessions already owned by this UUID
|
||||
for (const [sessionId, owners] of sessionOwners) {
|
||||
if (owners.has(uuid)) {
|
||||
const session = sessions.get(sessionId);
|
||||
if (session) result.push(session);
|
||||
if (session) {
|
||||
result.push(session);
|
||||
resultIds.add(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-bind orphaned sessions (no owner — typically ACP agent sessions created via REST registration)
|
||||
for (const [sessionId, session] of sessions) {
|
||||
if (resultIds.has(sessionId)) continue;
|
||||
const owners = sessionOwners.get(sessionId);
|
||||
// No owners map entry at all, or empty owners set
|
||||
const isOrphaned = !owners || owners.size === 0;
|
||||
if (isOrphaned) {
|
||||
storeBindSession(sessionId, uuid);
|
||||
result.push(session);
|
||||
resultIds.add(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ export interface EnvironmentResponse {
|
||||
username: string | null;
|
||||
last_poll_at: number | null;
|
||||
worker_type?: string;
|
||||
channel_group_id?: string | null;
|
||||
capabilities?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useState, useEffect, useCallback, lazy, Suspense } from "react";
|
||||
import { Navbar } from "./components/Navbar";
|
||||
import { IdentityPanel } from "./components/IdentityPanel";
|
||||
import { TokenManagerDialog } from "./components/TokenManagerDialog";
|
||||
import { ThemeProvider } from "./lib/theme";
|
||||
import { getUuid, setUuid, apiBind } from "./api/client";
|
||||
import { getUuid, setUuid, apiBind, setActiveApiToken } from "./api/client";
|
||||
import { ACPDirectView } from "./components/ACPDirectView";
|
||||
import { useTokens } from "./hooks/useTokens";
|
||||
|
||||
const Dashboard = lazy(() => import("./pages/Dashboard").then((m) => ({ default: m.Dashboard })));
|
||||
const SessionDetail = lazy(() => import("./pages/SessionDetail").then((m) => ({ default: m.SessionDetail })));
|
||||
@@ -11,7 +13,18 @@ const SessionDetail = lazy(() => import("./pages/SessionDetail").then((m) => ({
|
||||
export default function App() {
|
||||
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
||||
const [identityOpen, setIdentityOpen] = useState(false);
|
||||
const [tokenDialogOpen, setTokenDialogOpen] = useState(false);
|
||||
const [acpDirect, setAcpDirect] = useState<{ url: string; token: string } | null>(null);
|
||||
const { tokens, activeTokenId, activeLabel, activeTokenValue, setActiveTokenId, addToken, removeToken, updateToken } = useTokens();
|
||||
|
||||
// Sync active token to API client
|
||||
useEffect(() => {
|
||||
setActiveApiToken(activeTokenValue);
|
||||
}, [activeTokenValue]);
|
||||
|
||||
const handleSetActiveToken = useCallback((id: string) => {
|
||||
setActiveTokenId(id);
|
||||
}, [setActiveTokenId]);
|
||||
|
||||
// Simple hash-based router
|
||||
const parseRoute = useCallback(() => {
|
||||
@@ -97,6 +110,8 @@ export default function App() {
|
||||
<div className="flex h-screen flex-col bg-surface-0 text-text-primary">
|
||||
<Navbar
|
||||
onIdentityClick={() => setIdentityOpen(true)}
|
||||
onTokenClick={() => setTokenDialogOpen(true)}
|
||||
activeTokenLabel={currentSessionId ? undefined : activeLabel}
|
||||
sessionTitle={currentSessionId || (acpDirect ? "ACP" : undefined)}
|
||||
onBack={(currentSessionId || acpDirect) ? navigateToDashboard : undefined}
|
||||
/>
|
||||
@@ -114,6 +129,17 @@ export default function App() {
|
||||
</Suspense>
|
||||
|
||||
<IdentityPanel open={identityOpen} onClose={() => setIdentityOpen(false)} />
|
||||
|
||||
<TokenManagerDialog
|
||||
open={tokenDialogOpen}
|
||||
onClose={() => setTokenDialogOpen(false)}
|
||||
tokens={tokens}
|
||||
activeTokenId={activeTokenId}
|
||||
onSetActive={handleSetActiveToken}
|
||||
onAdd={addToken}
|
||||
onRemove={removeToken}
|
||||
onUpdate={updateToken}
|
||||
/>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -24,11 +24,35 @@ export function setUuid(uuid: string): void {
|
||||
localStorage.setItem("rcs_uuid", uuid);
|
||||
}
|
||||
|
||||
/** Active API token for Authorization header (set by useTokens) */
|
||||
let _activeToken: string | null = null;
|
||||
|
||||
export function setActiveApiToken(token: string | null): void {
|
||||
_activeToken = token;
|
||||
}
|
||||
|
||||
export function getActiveApiToken(): string | null {
|
||||
return _activeToken;
|
||||
}
|
||||
|
||||
async function api<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
const uuid = getUuid();
|
||||
const sep = path.includes("?") ? "&" : "?";
|
||||
const url = `${BASE}${path}${sep}uuid=${encodeURIComponent(uuid)}`;
|
||||
|
||||
if (_activeToken) {
|
||||
headers["Authorization"] = `Bearer ${_activeToken}`;
|
||||
}
|
||||
|
||||
// When using Bearer token auth, backend derives UUID from the token — no need to send query param.
|
||||
// Otherwise fall back to UUID auth via query param.
|
||||
let url: string;
|
||||
if (_activeToken) {
|
||||
const sep = path.includes("?") ? "&" : "?";
|
||||
url = `${BASE}${path}${sep}uuid=${encodeURIComponent(_activeToken)}`;
|
||||
} else {
|
||||
const uuid = getUuid();
|
||||
const sep = path.includes("?") ? "&" : "?";
|
||||
url = `${BASE}${path}${sep}uuid=${encodeURIComponent(uuid)}`;
|
||||
}
|
||||
const opts: RequestInit = { method, headers };
|
||||
if (body !== undefined) opts.body = JSON.stringify(body);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { cn } from "../lib/utils";
|
||||
import { ThemeToggle } from "../../components/ui/theme-toggle";
|
||||
import { ChevronLeft, LayoutGrid, UserPlus } from "lucide-react";
|
||||
import { ChevronLeft, LayoutGrid, UserPlus, KeyRound } from "lucide-react";
|
||||
|
||||
interface NavbarProps {
|
||||
onIdentityClick: () => void;
|
||||
onTokenClick: () => void;
|
||||
activeTokenLabel?: string | null;
|
||||
sessionTitle?: string;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
export function Navbar({ onIdentityClick, sessionTitle, onBack }: NavbarProps) {
|
||||
export function Navbar({ onIdentityClick, onTokenClick, activeTokenLabel, sessionTitle, onBack }: NavbarProps) {
|
||||
return (
|
||||
<nav className="sticky top-0 z-40 border-b border-border bg-surface-1/80 backdrop-blur-md">
|
||||
<div className="mx-auto flex h-11 sm:h-12 max-w-5xl items-center justify-between px-3 sm:px-4">
|
||||
@@ -51,6 +53,19 @@ export function Navbar({ onIdentityClick, sessionTitle, onBack }: NavbarProps) {
|
||||
</a>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
<button
|
||||
onClick={onTokenClick}
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded-md px-2 sm:px-3 py-1.5 text-sm transition-colors",
|
||||
activeTokenLabel
|
||||
? "bg-brand/10 text-brand hover:bg-brand/20"
|
||||
: "text-text-secondary hover:bg-surface-2 hover:text-text-primary"
|
||||
)}
|
||||
title="Token Manager"
|
||||
>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
<span className="hidden sm:inline max-w-24 truncate">{activeTokenLabel || "No Token"}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onIdentityClick}
|
||||
className="flex items-center gap-1 rounded-md px-2 sm:px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-2 hover:text-text-primary transition-colors"
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import { useState } from "react";
|
||||
import type { TokenEntry } from "../hooks/useTokens";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "../../components/ui/dialog";
|
||||
import { Check, Copy, Eye, EyeOff, Pencil, Plus, Trash2, X } from "lucide-react";
|
||||
|
||||
interface TokenManagerDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
tokens: TokenEntry[];
|
||||
activeTokenId: string | null;
|
||||
onSetActive: (id: string) => void;
|
||||
onAdd: (token: string, label: string) => string | null;
|
||||
onRemove: (id: string) => void;
|
||||
onUpdate: (id: string, label: string) => void;
|
||||
}
|
||||
|
||||
export function TokenManagerDialog({
|
||||
open,
|
||||
onClose,
|
||||
tokens,
|
||||
activeTokenId,
|
||||
onSetActive,
|
||||
onAdd,
|
||||
onRemove,
|
||||
onUpdate,
|
||||
}: TokenManagerDialogProps) {
|
||||
const [newToken, setNewToken] = useState("");
|
||||
const [newLabel, setNewLabel] = useState("");
|
||||
const [addError, setAddError] = useState("");
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editLabel, setEditLabel] = useState("");
|
||||
const [visibleTokenId, setVisibleTokenId] = useState<string | null>(null);
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
|
||||
const handleCopy = (id: string, token: string) => {
|
||||
navigator.clipboard.writeText(token).then(() => {
|
||||
setCopiedId(id);
|
||||
setTimeout(() => setCopiedId(null), 1500);
|
||||
});
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
const error = onAdd(newToken, newLabel);
|
||||
if (error) {
|
||||
setAddError(error);
|
||||
return;
|
||||
}
|
||||
setNewToken("");
|
||||
setNewLabel("");
|
||||
setAddError("");
|
||||
};
|
||||
|
||||
const handleStartEdit = (entry: TokenEntry) => {
|
||||
setEditingId(entry.id);
|
||||
setEditLabel(entry.label);
|
||||
};
|
||||
|
||||
const handleSaveEdit = (id: string) => {
|
||||
onUpdate(id, editLabel.trim() || "Unnamed");
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const handleSwitch = (id: string) => {
|
||||
onSetActive(id);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
|
||||
<DialogContent className="max-w-md rounded-2xl border-border bg-surface-1 p-6 shadow-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-display text-lg font-semibold text-text-primary">
|
||||
Token Manager
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm text-text-muted">
|
||||
Manage API tokens for RCS authentication.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Token list */}
|
||||
<div className="space-y-1 max-h-64 overflow-y-auto">
|
||||
{tokens.map((entry) => (
|
||||
<div key={entry.id} className="group flex items-center gap-1">
|
||||
{editingId === entry.id ? (
|
||||
<div className="flex flex-1 items-center gap-2 rounded-lg bg-surface-2 px-3 py-1.5">
|
||||
<input
|
||||
value={editLabel}
|
||||
onChange={(e) => setEditLabel(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleSaveEdit(entry.id);
|
||||
if (e.key === "Escape") setEditingId(null);
|
||||
}}
|
||||
className="flex-1 rounded border border-border bg-surface-1 px-2 py-1 text-sm text-text-primary focus:border-brand focus:outline-none"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleSaveEdit(entry.id)}
|
||||
className="text-brand hover:text-brand-light transition-colors"
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingId(null)}
|
||||
className="text-text-muted hover:text-text-primary transition-colors"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleSwitch(entry.id)}
|
||||
className={`flex flex-1 items-center justify-between rounded-lg px-3 py-2 text-sm transition-colors ${
|
||||
activeTokenId === entry.id
|
||||
? "bg-brand/10 text-brand"
|
||||
: "text-text-secondary hover:bg-surface-2"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col items-start min-w-0">
|
||||
<span className="font-medium truncate w-full">{entry.label}</span>
|
||||
<span className="text-xs text-text-muted font-mono">
|
||||
{visibleTokenId === entry.id
|
||||
? entry.token
|
||||
: `${entry.token.slice(0, 6)}${"\u2022".repeat(6)}`}
|
||||
</span>
|
||||
</div>
|
||||
{activeTokenId === entry.id && <Check className="h-4 w-4 flex-shrink-0" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setVisibleTokenId(visibleTokenId === entry.id ? null : entry.id)}
|
||||
className="rounded p-1 text-text-muted opacity-0 group-hover:opacity-100 hover:text-text-primary transition-all"
|
||||
title="Toggle token visibility"
|
||||
>
|
||||
{visibleTokenId === entry.id ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleCopy(entry.id, entry.token)}
|
||||
className="rounded p-1 text-text-muted opacity-0 group-hover:opacity-100 hover:text-text-primary transition-all"
|
||||
title="Copy token"
|
||||
>
|
||||
{copiedId === entry.id ? <Check className="h-3.5 w-3.5 text-status-active" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStartEdit(entry)}
|
||||
className="rounded p-1 text-text-muted opacity-0 group-hover:opacity-100 hover:text-text-primary transition-all"
|
||||
title="Edit label"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onRemove(entry.id)}
|
||||
className="rounded p-1 text-text-muted opacity-0 group-hover:opacity-100 hover:text-status-error transition-all"
|
||||
title="Delete token"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{tokens.length === 0 && (
|
||||
<div className="py-4 text-center text-sm text-text-muted">
|
||||
No tokens saved yet. Add one below.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add form */}
|
||||
<div className="border-t border-border pt-4 space-y-3">
|
||||
<div className="text-sm font-medium text-text-secondary">Add Token</div>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newToken}
|
||||
onChange={(e) => {
|
||||
setNewToken(e.target.value);
|
||||
setAddError("");
|
||||
}}
|
||||
placeholder="API Token"
|
||||
className="w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-brand focus:outline-none font-mono"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleAdd();
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newLabel}
|
||||
onChange={(e) => setNewLabel(e.target.value)}
|
||||
placeholder="Label (optional)"
|
||||
className="flex-1 rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-brand focus:outline-none"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleAdd();
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
disabled={!newToken.trim()}
|
||||
className="rounded-lg bg-brand px-3 py-2 text-white hover:bg-brand-light disabled:opacity-50 transition-colors"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{addError && <div className="text-xs text-status-error">{addError}</div>}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
120
packages/remote-control-server/web/src/hooks/useTokens.ts
Normal file
120
packages/remote-control-server/web/src/hooks/useTokens.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
export interface TokenEntry {
|
||||
id: string;
|
||||
token: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const TOKENS_KEY = "rcs_tokens";
|
||||
const ACTIVE_TOKEN_KEY = "rcs_uuid";
|
||||
const DEFAULT_ID = "__default__";
|
||||
|
||||
function generateId(): string {
|
||||
return `tk_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
/** Ensure the existing rcs_uuid is present as the default token entry */
|
||||
function ensureDefault(tokens: TokenEntry[]): TokenEntry[] {
|
||||
if (tokens.some((t) => t.id === DEFAULT_ID)) return tokens;
|
||||
let uuid: string | null = null;
|
||||
try {
|
||||
uuid = localStorage.getItem("rcs_uuid");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (!uuid) return tokens;
|
||||
return [{ id: DEFAULT_ID, token: uuid, label: "Default" }, ...tokens];
|
||||
}
|
||||
|
||||
function loadTokens(): TokenEntry[] {
|
||||
let tokens: TokenEntry[] = [];
|
||||
try {
|
||||
const raw = localStorage.getItem(TOKENS_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) tokens = parsed;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return ensureDefault(tokens);
|
||||
}
|
||||
|
||||
function loadActiveTokenId(tokens: TokenEntry[]): string {
|
||||
// Try saved active token
|
||||
try {
|
||||
const saved = localStorage.getItem(ACTIVE_TOKEN_KEY);
|
||||
if (saved && tokens.some((t) => t.id === saved)) return saved;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
// Fall back to default (rcs_uuid) entry
|
||||
const defaultEntry = tokens.find((t) => t.id === DEFAULT_ID);
|
||||
if (defaultEntry) return defaultEntry.id;
|
||||
// Fall back to first entry
|
||||
return tokens[0]?.id ?? DEFAULT_ID;
|
||||
}
|
||||
|
||||
export function useTokens() {
|
||||
const [tokens, setTokens] = useState<TokenEntry[]>(loadTokens);
|
||||
const [activeTokenId, setActiveTokenIdState] = useState<string>(() => loadActiveTokenId(loadTokens()));
|
||||
|
||||
const persistTokens = useCallback((next: TokenEntry[]) => {
|
||||
setTokens(next);
|
||||
try {
|
||||
localStorage.setItem(TOKENS_KEY, JSON.stringify(next));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setActiveTokenId = useCallback((id: string) => {
|
||||
setActiveTokenIdState(id);
|
||||
try {
|
||||
localStorage.setItem(ACTIVE_TOKEN_KEY, id);
|
||||
location.reload(); // Reload to ensure api client picks up new token from localStorage
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
const addToken = useCallback((token: string, label: string): string | null => {
|
||||
const trimmed = token.trim();
|
||||
if (!trimmed) return "Token is required";
|
||||
const entry: TokenEntry = { id: generateId(), token: trimmed, label: label.trim() || trimmed.slice(0, 8) };
|
||||
const next = [...tokens, entry];
|
||||
persistTokens(next);
|
||||
return null;
|
||||
}, [tokens, persistTokens]);
|
||||
|
||||
const removeToken = useCallback((id: string) => {
|
||||
if (id === DEFAULT_ID) return; // Cannot remove default
|
||||
const next = tokens.filter((t) => t.id !== id);
|
||||
persistTokens(next);
|
||||
if (activeTokenId === id) {
|
||||
setActiveTokenId(DEFAULT_ID);
|
||||
}
|
||||
}, [tokens, persistTokens, activeTokenId, setActiveTokenId]);
|
||||
|
||||
const updateToken = useCallback((id: string, label: string) => {
|
||||
const next = tokens.map((t) => t.id === id ? { ...t, label } : t);
|
||||
persistTokens(next);
|
||||
}, [tokens, persistTokens]);
|
||||
|
||||
const activeToken = tokens.find((t) => t.id === activeTokenId) ?? tokens[0] ?? null;
|
||||
const activeLabel = activeToken?.label ?? "Default";
|
||||
const activeTokenValue = activeToken?.token ?? null;
|
||||
|
||||
return {
|
||||
tokens,
|
||||
activeTokenId,
|
||||
activeToken,
|
||||
activeLabel,
|
||||
activeTokenValue,
|
||||
setActiveTokenId,
|
||||
addToken,
|
||||
removeToken,
|
||||
updateToken,
|
||||
};
|
||||
}
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
}),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user