mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
Compare commits
2 Commits
rcs-v3.3
...
refactor/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d60e0eaccb | ||
|
|
a3fa374bb3 |
22
.githooks/pre-commit
Normal file
22
.githooks/pre-commit
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/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
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -13,6 +13,7 @@ src/utils/vendor/
|
|||||||
# AI tool runtime directories
|
# AI tool runtime directories
|
||||||
.agents/
|
.agents/
|
||||||
.claude/
|
.claude/
|
||||||
|
.codex/
|
||||||
.omx/
|
.omx/
|
||||||
.docs/task/
|
.docs/task/
|
||||||
# Binary / screenshot files (root only)
|
# Binary / screenshot files (root only)
|
||||||
@@ -29,12 +30,3 @@ __pycache__/
|
|||||||
logs
|
logs
|
||||||
|
|
||||||
data
|
data
|
||||||
.omc
|
|
||||||
.codex/*
|
|
||||||
!.codex/agents/
|
|
||||||
!.codex/agents/**
|
|
||||||
!.codex/skills/
|
|
||||||
!.codex/skills/**
|
|
||||||
.codex/skills/.system/**
|
|
||||||
!.codex/prompts/
|
|
||||||
!.codex/prompts/**
|
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
# Impeccable Design Context
|
|
||||||
|
|
||||||
## Users
|
|
||||||
|
|
||||||
**Primary**: Technical teams and enterprises using AI-assisted coding in production workflows.
|
|
||||||
- DevOps engineers managing remote agents via RCS dashboard
|
|
||||||
- Development teams collaborating through shared sessions
|
|
||||||
- Individual developers using terminal CLI daily
|
|
||||||
|
|
||||||
**Context**: Used during focused work sessions — debugging, code review, agent orchestration. Users are in "get things done" mode, not browsing. They value efficiency but also appreciate warmth and personality.
|
|
||||||
|
|
||||||
**Job to be done**: Make advanced AI coding tools accessible and controllable, especially features that normally require enterprise accounts or Anthropic OAuth.
|
|
||||||
|
|
||||||
## Brand Personality
|
|
||||||
|
|
||||||
**3 words**: Warm, Considered, Human
|
|
||||||
|
|
||||||
**Voice**: Like a knowledgeable colleague who's genuinely enthusiastic about the craft — not a corporate product manager. Community-first, open, slightly playful. Chinese developer community culture (贴吧/discord 温暖氛围).
|
|
||||||
|
|
||||||
**Emotional goals**: Confidence (this tool is solid), Warmth (this community is welcoming), Delight (small moments of personality make the difference).
|
|
||||||
|
|
||||||
**References**:
|
|
||||||
- **Anthropic's own design language** — their clean, considered aesthetic with warm undertones. The terra cotta/burnt orange as a human accent. Lots of breathing room. Typography-forward.
|
|
||||||
- **NOT**: Generic AI product (no ChatGPT blue, no gradient text, no "AI slop"). NOT corporate SaaS (no Salesforce-blue dashboards, no enterprise sterility).
|
|
||||||
|
|
||||||
**Anti-references**: Corporate enterprise dashboards, generic AI product pages, anything that looks like it was "designed by committee."
|
|
||||||
|
|
||||||
## Aesthetic Direction
|
|
||||||
|
|
||||||
**Theme**: Light + Dark dual mode (user/system preference switch)
|
|
||||||
|
|
||||||
**Tone**: Anthropomorphic warmth meets terminal precision. The brand orange (Claude's terra cotta) is the thread that ties everything together — it's the human element in a technical world.
|
|
||||||
|
|
||||||
**Typography**: Clean, considered, with good hierarchy. Terminal-native for CLI; modern web fonts for Web UI (RCS dashboard, docs). Favor readability and personality.
|
|
||||||
|
|
||||||
**Color**:
|
|
||||||
- Primary: Claude orange family (`#D77757` / terra cotta)
|
|
||||||
- Accent: Warm neutrals tinted toward orange
|
|
||||||
- Semantic: Success/Error/Warning following Anthropic's established palette
|
|
||||||
- Dark mode: Warm dark surfaces (not cold blue-black)
|
|
||||||
|
|
||||||
**Differentiation**: The CCB brand sits at the intersection of "serious tool" and "community project." It should feel like Anthropic's design principles applied to an open-source context — less corporate polish, more human craft. The mascot "Clawd" and the playful "踩踩背" naming hint at personality that the design should honor.
|
|
||||||
|
|
||||||
**Scope**: All Web UI — RCS control panel, documentation site, landing pages.
|
|
||||||
|
|
||||||
## Design Principles
|
|
||||||
|
|
||||||
1. **Considered over clever** — Every design choice should feel intentional, not trendy. If it doesn't serve the user, it doesn't ship.
|
|
||||||
2. **Warmth through subtlety** — Orange tints on neutrals, breathing room in layouts, personality in copy. Not giant emoji or aggressive color.
|
|
||||||
3. **Density with clarity** — Technical users need information density, but not chaos. Every pixel earns its place.
|
|
||||||
4. **Community voice** — The design should feel like it was made by people who use it, not by a distant design team. Slightly rough edges are fine if they're honest.
|
|
||||||
5. **Anthropic's shadow** — When in doubt, follow Anthropic's design instincts — the clean layouts, the generous spacing, the warm color temperature. Then add the community touch.
|
|
||||||
|
|
||||||
## Existing Design Assets
|
|
||||||
|
|
||||||
### Brand Colors (from theme system)
|
|
||||||
- Claude Orange: `rgb(215,119,87)` / `#D77757`
|
|
||||||
- Claude Blue: `rgb(87,105,247)` / `#5769F7`
|
|
||||||
- Permission Blue: `rgb(87,105,247)`
|
|
||||||
- Auto Accept Violet: `rgb(135,0,255)`
|
|
||||||
- Plan Mode Teal: `rgb(0,102,102)`
|
|
||||||
- Success: `rgb(78,186,101)`
|
|
||||||
- Error: `rgb(255,107,128)`
|
|
||||||
- Warning: `rgb(255,193,7)`
|
|
||||||
|
|
||||||
### Logo
|
|
||||||
- CCB text + orange play button icon
|
|
||||||
- Dark/Light SVG variants in `docs/logo/`
|
|
||||||
- Favicon: Orange circle `#D97706` with white play triangle
|
|
||||||
|
|
||||||
### Mascot
|
|
||||||
- "Clawd" — terminal-art character with multiple poses
|
|
||||||
- Theme-aware coloring
|
|
||||||
|
|
||||||
### Theme System
|
|
||||||
- 7 variants: dark, light, dark-ansi, light-ansi, dark-daltonized, light-daltonized, auto
|
|
||||||
- 89+ semantic color tokens
|
|
||||||
- Full documentation in `packages/@ant/ink/docs/04-theme-system.md`
|
|
||||||
102
CLAUDE.md
102
CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is a **reverse-engineered / decompiled** version of Anthropic's official Claude Code CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. TypeScript strict mode is enforced(见 Working with This Codebase 段的 tsc 要求)。
|
This is a **reverse-engineered / decompiled** version of Anthropic's official Claude Code CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. TypeScript strict mode is enforced — **`bunx tsc --noEmit` must pass with zero errors**.
|
||||||
|
|
||||||
## Git Commit Message Convention
|
## Git Commit Message Convention
|
||||||
|
|
||||||
@@ -39,11 +39,8 @@ echo "say hello" | bun run src/entrypoints/cli.tsx -p
|
|||||||
# Build (code splitting, outputs dist/cli.js + chunk files)
|
# Build (code splitting, outputs dist/cli.js + chunk files)
|
||||||
bun run build
|
bun run build
|
||||||
|
|
||||||
# Build with Vite (alternative build pipeline)
|
|
||||||
bun run build:vite
|
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
bun test # run all tests (3175 tests / 207 files / 0 fail)
|
bun test # run all tests (2453 tests / 137 files / 0 fail)
|
||||||
bun test src/utils/__tests__/hash.test.ts # run single file
|
bun test src/utils/__tests__/hash.test.ts # run single file
|
||||||
bun test --coverage # with coverage report
|
bun test --coverage # with coverage report
|
||||||
|
|
||||||
@@ -77,14 +74,14 @@ bun run docs:dev
|
|||||||
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。Build 默认启用 19 个 feature(见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。
|
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。Build 默认启用 19 个 feature(见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。
|
||||||
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines,运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。
|
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines,运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。
|
||||||
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
|
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
|
||||||
- **Monorepo**: Bun workspaces — 15 个 workspace packages + 若干辅助目录 in `packages/` resolved via `workspace:*`。
|
- **Monorepo**: Bun workspaces — 14 个 internal packages in `packages/` resolved via `workspace:*`。
|
||||||
- **Lint/Format**: Biome (`biome.json`)。`bun run lint` / `bun run lint:fix` / `bun run format`。
|
- **Lint/Format**: Biome (`biome.json`)。`bun run lint` / `bun run lint:fix` / `bun run format`。
|
||||||
- **Defines**: 集中管理在 `scripts/defines.ts`。当前版本 `2.1.888`。
|
- **Defines**: 集中管理在 `scripts/defines.ts`。当前版本 `2.1.888`。
|
||||||
- **CI**: GitHub Actions — `ci.yml`(构建+测试)、`release-rcs.yml`(RCS 发布)、`update-contributors.yml`(自动更新贡献者)。
|
- **CI**: GitHub Actions — `ci.yml`(构建+测试)、`release-rcs.yml`(RCS 发布)、`update-contributors.yml`(自动更新贡献者)。
|
||||||
|
|
||||||
### Entry & Bootstrap
|
### Entry & Bootstrap
|
||||||
|
|
||||||
1. **`src/entrypoints/cli.tsx`** (373 行) — True entrypoint。`main()` 函数按优先级处理多条快速路径:
|
1. **`src/entrypoints/cli.tsx`** (323 行) — True entrypoint。`main()` 函数按优先级处理多条快速路径:
|
||||||
- `--version` / `-v` — 零模块加载
|
- `--version` / `-v` — 零模块加载
|
||||||
- `--dump-system-prompt` — feature-gated (DUMP_SYSTEM_PROMPT)
|
- `--dump-system-prompt` — feature-gated (DUMP_SYSTEM_PROMPT)
|
||||||
- `--claude-in-chrome-mcp` / `--chrome-native-host`
|
- `--claude-in-chrome-mcp` / `--chrome-native-host`
|
||||||
@@ -97,7 +94,7 @@ bun run docs:dev
|
|||||||
- `environment-runner` / `self-hosted-runner` — BYOC runner
|
- `environment-runner` / `self-hosted-runner` — BYOC runner
|
||||||
- `--tmux` + `--worktree` 组合
|
- `--tmux` + `--worktree` 组合
|
||||||
- 默认路径:加载 `main.tsx` 启动完整 CLI
|
- 默认路径:加载 `main.tsx` 启动完整 CLI
|
||||||
2. **`src/main.tsx`** (~6981 行) — Commander.js CLI definition。注册大量 subcommands:`mcp` (serve/add/remove/list...)、`server`、`ssh`、`open`、`auth`、`plugin`、`agents`、`auto-mode`、`doctor`、`update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。
|
2. **`src/main.tsx`** (~6970 行) — Commander.js CLI definition。注册大量 subcommands:`mcp` (serve/add/remove/list...)、`server`、`ssh`、`open`、`auth`、`plugin`、`agents`、`auto-mode`、`doctor`、`update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。
|
||||||
3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog)。
|
3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog)。
|
||||||
|
|
||||||
### Core Loop
|
### Core Loop
|
||||||
@@ -115,8 +112,8 @@ bun run docs:dev
|
|||||||
### Tool System
|
### Tool System
|
||||||
|
|
||||||
- **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`).
|
- **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`).
|
||||||
- **`src/tools.ts`** (392 行) — Tool registry. Assembles the tool list; tools are imported from `@claude-code-best/builtin-tools` package. Some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
|
- **`src/tools.ts`** (387 行) — Tool registry. Assembles the tool list; some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
|
||||||
- **`packages/builtin-tools/src/tools/`** — 59 个子目录(含 shared/testing 等工具目录),通过 `@claude-code-best/builtin-tools` 包导出。主要分类:
|
- **`src/tools/<ToolName>/`** — 55 个 tool 目录。主要分类:
|
||||||
- **文件操作**: FileEditTool, FileReadTool, FileWriteTool, GlobTool, GrepTool
|
- **文件操作**: FileEditTool, FileReadTool, FileWriteTool, GlobTool, GrepTool
|
||||||
- **Shell/执行**: BashTool, PowerShellTool, REPLTool
|
- **Shell/执行**: BashTool, PowerShellTool, REPLTool
|
||||||
- **Agent 系统**: AgentTool, TaskCreateTool, TaskUpdateTool, TaskListTool, TaskGetTool
|
- **Agent 系统**: AgentTool, TaskCreateTool, TaskUpdateTool, TaskListTool, TaskGetTool
|
||||||
@@ -124,6 +121,7 @@ bun run docs:dev
|
|||||||
- **Web/MCP**: WebFetchTool, WebSearchTool, MCPTool, McpAuthTool
|
- **Web/MCP**: WebFetchTool, WebSearchTool, MCPTool, McpAuthTool
|
||||||
- **调度**: CronCreateTool, CronDeleteTool, CronListTool
|
- **调度**: CronCreateTool, CronDeleteTool, CronListTool
|
||||||
- **其他**: LSPTool, ConfigTool, SkillTool, EnterWorktreeTool, ExitWorktreeTool 等
|
- **其他**: LSPTool, ConfigTool, SkillTool, EnterWorktreeTool, ExitWorktreeTool 等
|
||||||
|
- **`src/tools/shared/`** — Tool 共享工具函数。
|
||||||
|
|
||||||
### UI Layer (Ink)
|
### UI Layer (Ink)
|
||||||
|
|
||||||
@@ -154,17 +152,9 @@ bun run docs:dev
|
|||||||
| `packages/@ant/computer-use-input/` | 键鼠模拟(dispatcher + darwin/win32/linux backend) |
|
| `packages/@ant/computer-use-input/` | 键鼠模拟(dispatcher + darwin/win32/linux backend) |
|
||||||
| `packages/@ant/computer-use-swift/` | 截图 + 应用管理(dispatcher + per-platform backend) |
|
| `packages/@ant/computer-use-swift/` | 截图 + 应用管理(dispatcher + per-platform backend) |
|
||||||
| `packages/@ant/claude-for-chrome-mcp/` | Chrome 浏览器控制(通过 `--chrome` 启用) |
|
| `packages/@ant/claude-for-chrome-mcp/` | Chrome 浏览器控制(通过 `--chrome` 启用) |
|
||||||
| `packages/@ant/model-provider/` | Model provider 抽象层 |
|
| `packages/remote-control-server/` | 自托管 Remote Control Server(Docker 部署,含 Web UI) |
|
||||||
| `packages/builtin-tools/` | 内置工具集(60 个 tool 实现,通过 `@claude-code-best/builtin-tools` 导出) |
|
| `packages/swarm/` | Swarm 解耦模块 |
|
||||||
| `packages/agent-tools/` | Agent 工具集 |
|
| `packages/shell/` | Shell 抽象 |
|
||||||
| `packages/acp-link/` | ACP 代理服务器(WebSocket → ACP agent 桥接) |
|
|
||||||
| `packages/cc-knowledge/` | Claude Code 知识库(非 workspace 包) |
|
|
||||||
| `packages/langfuse-dashboard/` | Langfuse 可观测性面板(非 workspace 包) |
|
|
||||||
| `packages/mcp-client/` | MCP 客户端库 |
|
|
||||||
| `packages/mcp-server/` | MCP 服务端库(非 workspace 包) |
|
|
||||||
| `packages/remote-control-server/` | 自托管 Remote Control Server(Docker 部署,含 Web UI)— Web UI 已重构为 React + Vite + Radix UI,支持 ACP agent 接入 |
|
|
||||||
| `packages/swarm/` | Swarm 解耦模块(非 workspace 包) |
|
|
||||||
| `packages/shell/` | Shell 抽象(非 workspace 包) |
|
|
||||||
| `packages/audio-capture-napi/` | 原生音频捕获(已恢复) |
|
| `packages/audio-capture-napi/` | 原生音频捕获(已恢复) |
|
||||||
| `packages/color-diff-napi/` | 颜色差异计算(完整实现,11 tests) |
|
| `packages/color-diff-napi/` | 颜色差异计算(完整实现,11 tests) |
|
||||||
| `packages/image-processor-napi/` | 图像处理(已恢复) |
|
| `packages/image-processor-napi/` | 图像处理(已恢复) |
|
||||||
@@ -173,18 +163,11 @@ bun run docs:dev
|
|||||||
|
|
||||||
### Bridge / Remote Control
|
### Bridge / Remote Control
|
||||||
|
|
||||||
- **`src/bridge/`** (~38 files) — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`。
|
- **`src/bridge/`** (~37 files) — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`。
|
||||||
- **`packages/remote-control-server/`** — 自托管 RCS,支持 Docker 部署,含 Web UI 控制面板(React 19 + Vite + Radix UI)。支持 ACP agent 通过 acp-link 接入(ACP WebSocket handler、relay handler、SSE event stream)。通过 `bun run rcs` 启动。
|
- **`packages/remote-control-server/`** — 自托管 RCS,支持 Docker 部署,含 Web UI 控制面板。通过 `bun run rcs` 启动。
|
||||||
- CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`。
|
- CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`。
|
||||||
- 详见 `docs/features/remote-control-self-hosting.md`。
|
- 详见 `docs/features/remote-control-self-hosting.md`。
|
||||||
|
|
||||||
### ACP Protocol (Agent Client Protocol)
|
|
||||||
|
|
||||||
- **`src/services/acp/`** — ACP agent 实现,包含 `agent.ts`(AcpAgent 类)、`bridge.ts`(Claude Code ↔ ACP 桥接)、`permissions.ts`(权限处理)、`entry.ts`(入口)。
|
|
||||||
- **`packages/acp-link/`** — ACP 代理服务器,将 WebSocket 客户端桥接到 ACP agent。提供 `acp-link` CLI 命令,支持自定义端口/HTTPS/认证/会话管理、RCS 集成(REST 注册 + WS identify 两步流程)、权限模式透传(fallback: 客户端传值 > config > `ACP_PERMISSION_MODE` 环境变量)。
|
|
||||||
- ACP 权限管道改进:`createAcpCanUseTool` 统一权限流水线,`applySessionMode` 模式同步,`bypassPermissions` 可用性检测(非 root/sandbox 环境)。
|
|
||||||
- ACP Plan 可视化已支持 `session/update plan` 类型的消息展示(PlanView 组件,含进度条/状态图标/优先级标签)。
|
|
||||||
|
|
||||||
### Daemon Mode
|
### Daemon Mode
|
||||||
|
|
||||||
- **`src/daemon/`** — Daemon 模式(长驻 supervisor)。feature-gated by `DAEMON`。包含 `main.ts`(entry)和 `workerRegistry.ts`(worker 管理)。
|
- **`src/daemon/`** — Daemon 模式(长驻 supervisor)。feature-gated by `DAEMON`。包含 `main.ts`(entry)和 `workerRegistry.ts`(worker 管理)。
|
||||||
@@ -215,13 +198,30 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
|
|||||||
|
|
||||||
### Multi-API 兼容层
|
### Multi-API 兼容层
|
||||||
|
|
||||||
支持 OpenAI、Gemini、Grok 三种第三方 API,通过 `/login` 命令配置,均采用流适配器模式转为 Anthropic 内部格式。详见各兼容层的 docs 文档。
|
所有兼容层均采用流适配器模式:将第三方 API 格式转为 Anthropic 内部格式,下游代码完全不改。
|
||||||
|
|
||||||
### 穷鬼模式(Budget Mode)
|
#### OpenAI 兼容层
|
||||||
|
|
||||||
- 通过 `/poor` 命令切换,持久化到 `settings.json`。
|
通过 `CLAUDE_CODE_USE_OPENAI=1` 启用,支持 Ollama/DeepSeek/vLLM 等任意 OpenAI Chat Completions 协议端点。含 DeepSeek thinking mode 支持。
|
||||||
- 启用后跳过 `extract_memories`、`prompt_suggestion` 和 `verification_agent`,显著减少 token 消耗。
|
|
||||||
- 实现在 `src/commands/poor/poorMode.ts`。
|
- **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射
|
||||||
|
- 关键环境变量:`CLAUDE_CODE_USE_OPENAI`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`
|
||||||
|
|
||||||
|
#### Gemini 兼容层
|
||||||
|
|
||||||
|
通过 `CLAUDE_CODE_USE_GEMINI=1` 启用。独立环境变量体系。
|
||||||
|
|
||||||
|
- **`src/services/api/gemini/`** — client、模型映射、类型定义
|
||||||
|
- 关键环境变量:`GEMINI_API_KEY`(必填)、`GEMINI_MODEL`(直接指定)、`GEMINI_DEFAULT_SONNET_MODEL`/`GEMINI_DEFAULT_OPUS_MODEL`(按能力映射)
|
||||||
|
- 模型映射优先级:`GEMINI_MODEL` > `GEMINI_DEFAULT_*_MODEL` > `ANTHROPIC_DEFAULT_*_MODEL`(已废弃) > 原样返回
|
||||||
|
|
||||||
|
#### Grok 兼容层
|
||||||
|
|
||||||
|
通过 `CLAUDE_CODE_USE_GROK=1` 启用。自定义模型映射支持 xAI Grok API。
|
||||||
|
|
||||||
|
- **`src/services/api/grok/`** — client、模型映射
|
||||||
|
|
||||||
|
详见各兼容层的 docs 文档。
|
||||||
|
|
||||||
### Stubbed/Deleted Modules
|
### Stubbed/Deleted Modules
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- **框架**: `bun:test`(内置断言 + mock)
|
- **框架**: `bun:test`(内置断言 + mock)
|
||||||
- **当前状态**: 3175 tests / 207 files / 0 fail
|
- **当前状态**: 2992 tests / 188 files / 0 fail
|
||||||
- **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `<module>.test.ts`
|
- **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `<module>.test.ts`
|
||||||
- **集成测试**: `tests/integration/` — 4 个文件(cli-arguments, context-build, message-pipeline, tool-chain)
|
- **集成测试**: `tests/integration/` — 4 个文件(cli-arguments, context-build, message-pipeline, tool-chain)
|
||||||
- **共享 mock/fixture**: `tests/mocks/`(api-responses, file-system, fixtures/)
|
- **共享 mock/fixture**: `tests/mocks/`(api-responses, file-system, fixtures/)
|
||||||
@@ -269,7 +269,7 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
|
|||||||
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:
|
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run typecheck # equivalent to bun run typecheck
|
bunx tsc --noEmit
|
||||||
```
|
```
|
||||||
|
|
||||||
**类型规范**:
|
**类型规范**:
|
||||||
@@ -282,7 +282,7 @@ bun run typecheck # equivalent to bun run typecheck
|
|||||||
|
|
||||||
## Working with This Codebase
|
## Working with This Codebase
|
||||||
|
|
||||||
- **tsc must pass** — `bun run typecheck` 必须零错误,任何修改都不能引入新的类型错误。
|
- **tsc must pass** — `bunx tsc --noEmit` 必须零错误,任何修改都不能引入新的类型错误。
|
||||||
- **Feature flags** — 默认全部关闭(`feature()` 返回 `false`)。Dev/build 各有自己的默认启用列表。不要在 `cli.tsx` 中重定义 `feature` 函数。
|
- **Feature flags** — 默认全部关闭(`feature()` 返回 `false`)。Dev/build 各有自己的默认启用列表。不要在 `cli.tsx` 中重定义 `feature` 函数。
|
||||||
- **React Compiler output** — Components have decompiled memoization boilerplate (`const $ = _c(N)`). This is normal.
|
- **React Compiler output** — Components have decompiled memoization boilerplate (`const $ = _c(N)`). This is normal.
|
||||||
- **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。**`feature()` 只能直接用在 `if` 语句或三元表达式的条件位置**(Bun 编译器限制),不能赋值给变量、不能放在箭头函数体里、不能作为 `&&` 链的一部分。正确:`if (feature('X')) {}` 或 `feature('X') ? a : b`。
|
- **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。**`feature()` 只能直接用在 `if` 语句或三元表达式的条件位置**(Bun 编译器限制),不能赋值给变量、不能放在箭头函数体里、不能作为 `&&` 链的一部分。正确:`if (feature('X')) {}` 或 `feature('X') ? a : b`。
|
||||||
@@ -292,29 +292,3 @@ bun run typecheck # equivalent to bun run typecheck
|
|||||||
- **Biome 配置** — 大量 lint 规则被关闭(decompiled 代码不适合严格 lint)。`.tsx` 文件用 120 行宽 + 强制分号;其他文件 80 行宽 + 按需分号。
|
- **Biome 配置** — 大量 lint 规则被关闭(decompiled 代码不适合严格 lint)。`.tsx` 文件用 120 行宽 + 强制分号;其他文件 80 行宽 + 按需分号。
|
||||||
- **Ink 框架在 `packages/@ant/ink/`** — 不是 `src/ink/`(该目录不存在)。Ink 相关的组件、hooks、keybindings 都在 packages 中。
|
- **Ink 框架在 `packages/@ant/ink/`** — 不是 `src/ink/`(该目录不存在)。Ink 相关的组件、hooks、keybindings 都在 packages 中。
|
||||||
- **Provider 优先级** — `modelType` 参数 > 环境变量 > 默认 `firstParty`。新增 provider 需在 `src/utils/model/providers.ts` 注册。
|
- **Provider 优先级** — `modelType` 参数 > 环境变量 > 默认 `firstParty`。新增 provider 需在 `src/utils/model/providers.ts` 注册。
|
||||||
|
|
||||||
## Design Context
|
|
||||||
|
|
||||||
Impeccable 设计上下文保存在 `.impeccable.md` 中。设计 Web UI(RCS 控制面板、文档站、着陆页)时必须参考该文件。
|
|
||||||
|
|
||||||
### 核心设计原则
|
|
||||||
|
|
||||||
1. **Considered over clever** — 每个设计选择都应感觉有意为之,而非追逐潮流
|
|
||||||
2. **Warmth through subtlety** — 通过橙色色调的中性色、留白布局、有温度的文案来传达温暖
|
|
||||||
3. **Density with clarity** — 技术用户需要信息密度,但不能混乱
|
|
||||||
4. **Community voice** — 设计应感觉是由使用者创造的,而非遥远的设计团队
|
|
||||||
5. **Anthropic's shadow** — 遵循 Anthropic 的设计直觉:干净的布局、充足的间距、温暖的色温
|
|
||||||
|
|
||||||
### 品牌色
|
|
||||||
|
|
||||||
- 主色:Claude Orange `#D77757`(terra cotta)
|
|
||||||
- 辅色:Claude Blue `#5769F7`
|
|
||||||
- 暗色模式使用温暖的深色表面(非冷蓝黑色)
|
|
||||||
|
|
||||||
### 目标用户
|
|
||||||
|
|
||||||
技术团队/企业,在专业工作流中使用 AI 辅助编程。友好的开源社区氛围,非企业 SaaS 风格。
|
|
||||||
|
|
||||||
### 视觉参考
|
|
||||||
|
|
||||||
Anthropic 公司的设计风格 — 干净、考究、温暖的底色。大量留白,以排版为核心。避免 AI 产品常见的设计套路(渐变文字、玻璃态、霓虹色)。
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
| **Langfuse 监控** | 企业级 Agent 监控, 可以清晰看到每次 agent loop 细节, 可以一键转化为数据集 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
|
| **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) |
|
| **Web Search** | 内置网页搜索工具, 支持 bing 和 brave 搜索 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
|
||||||
| **Poor Mode** | 穷鬼模式,关闭记忆提取和键入建议,大幅度减少并发请求 | /poor 可以开关 |
|
| **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) |
|
| **自定义模型供应商** | 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) |
|
| 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) |
|
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
|
||||||
|
|||||||
19
build.ts
19
build.ts
@@ -11,7 +11,6 @@ rmSync(outdir, { recursive: true, force: true })
|
|||||||
// Default features that match the official CLI build.
|
// Default features that match the official CLI build.
|
||||||
// Additional features can be enabled via FEATURE_<NAME>=1 env vars.
|
// Additional features can be enabled via FEATURE_<NAME>=1 env vars.
|
||||||
const DEFAULT_BUILD_FEATURES = [
|
const DEFAULT_BUILD_FEATURES = [
|
||||||
'BUDDY', 'TRANSCRIPT_CLASSIFIER', 'BRIDGE_MODE',
|
|
||||||
'AGENT_TRIGGERS_REMOTE',
|
'AGENT_TRIGGERS_REMOTE',
|
||||||
'CHICAGO_MCP',
|
'CHICAGO_MCP',
|
||||||
'VOICE_MODE',
|
'VOICE_MODE',
|
||||||
@@ -121,7 +120,23 @@ const vendorDir = join(outdir, 'vendor', 'audio-capture')
|
|||||||
await cp('vendor/audio-capture', vendorDir, { recursive: true })
|
await cp('vendor/audio-capture', vendorDir, { recursive: true })
|
||||||
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`)
|
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`)
|
||||||
|
|
||||||
// Step 5: Generate cli-bun and cli-node executable entry points
|
// 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
|
||||||
const cliBun = join(outdir, 'cli-bun.js')
|
const cliBun = join(outdir, 'cli-bun.js')
|
||||||
const cliNode = join(outdir, 'cli-node.js')
|
const cliNode = join(outdir, 'cli-node.js')
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
@@ -13,14 +13,14 @@ keywords: ["子 Agent", "AgentTool", "任务委派", "forkSubagent", "子进程
|
|||||||
```
|
```
|
||||||
AI 生成 tool_use: { prompt: "修复 bug", subagent_type: "Explore" }
|
AI 生成 tool_use: { prompt: "修复 bug", subagent_type: "Explore" }
|
||||||
↓
|
↓
|
||||||
AgentTool.call() ← 入口(AgentTool.tsx:387)
|
AgentTool.call() ← 入口(AgentTool.tsx:239)
|
||||||
├── 解析 effectiveType(fork vs 命名 agent vs GP 回退)
|
├── 解析 effectiveType(fork vs 命名 agent vs GP 回退)
|
||||||
├── filterDeniedAgents() ← 仅命名 Agent 路径执行:权限过滤
|
├── filterDeniedAgents() ← 仅命名 Agent 路径执行:权限过滤
|
||||||
├── 检查 requiredMcpServers ← MCP 依赖验证(最长等 30s)
|
├── 检查 requiredMcpServers ← MCP 依赖验证(最长等 30s)
|
||||||
├── assembleToolPool(workerPermissionContext) ← 独立组装工具池
|
├── assembleToolPool(workerPermissionContext) ← 独立组装工具池
|
||||||
├── createAgentWorktree() ← 可选 worktree 隔离
|
├── createAgentWorktree() ← 可选 worktree 隔离
|
||||||
↓
|
↓
|
||||||
runAgent() ← 核心执行(runAgent.ts)
|
runAgent() ← 核心执行(runAgent.ts:248)
|
||||||
├── getAgentSystemPrompt() ← 构建 agent 专属 system prompt
|
├── getAgentSystemPrompt() ← 构建 agent 专属 system prompt
|
||||||
├── initializeAgentMcpServers() ← agent 级 MCP 服务器
|
├── initializeAgentMcpServers() ← agent 级 MCP 服务器
|
||||||
├── executeSubagentStartHooks() ← Hook 注入
|
├── executeSubagentStartHooks() ← Hook 注入
|
||||||
@@ -54,7 +54,7 @@ Fork 实验的门控函数 `isForkSubagentEnabled()` 需要同时满足三个前
|
|||||||
Fork 路径的设计核心是 **Prompt Cache 共享**:所有 fork 子进程共享父 Agent 的完整 `assistant` 消息(所有 `tool_use` 块),用相同的占位符 `tool_result` 填充,只有最后一个 `text` 块包含各自的指令。这使得 API 请求前缀字节完全一致,最大化缓存命中。
|
Fork 路径的设计核心是 **Prompt Cache 共享**:所有 fork 子进程共享父 Agent 的完整 `assistant` 消息(所有 `tool_use` 块),用相同的占位符 `tool_result` 填充,只有最后一个 `text` 块包含各自的指令。这使得 API 请求前缀字节完全一致,最大化缓存命中。
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// forkSubagent.ts:93 — 所有 fork 子进程的占位结果
|
// forkSubagent.ts:142 — 所有 fork 子进程的占位结果
|
||||||
const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
|
const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
|
||||||
|
|
||||||
// buildForkedMessages() 构建:
|
// buildForkedMessages() 构建:
|
||||||
@@ -63,7 +63,7 @@ const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
|
|||||||
|
|
||||||
### Fork 递归防护
|
### Fork 递归防护
|
||||||
|
|
||||||
Fork 子进程保留 Agent 工具(为了 cache-identical tool defs),但通过两道防线防止递归 fork:
|
Fork 子进程保留 Agent 工具(为了 cache-identical tool defs),但通过两道防线防止递归 fork(`AgentTool.tsx:332`):
|
||||||
|
|
||||||
1. **`querySource` 检查**(压缩安全):`context.options.querySource === 'agent:builtin:fork'`
|
1. **`querySource` 检查**(压缩安全):`context.options.querySource === 'agent:builtin:fork'`
|
||||||
2. **消息扫描**(降级兜底):检测 `<fork-boilerplate>` 标签
|
2. **消息扫描**(降级兜底):检测 `<fork-boilerplate>` 标签
|
||||||
@@ -88,7 +88,7 @@ Fork 子进程保留 Agent 工具(为了 cache-identical tool defs),但通
|
|||||||
|
|
||||||
### 内置 Agent
|
### 内置 Agent
|
||||||
|
|
||||||
系统预定义了几个内置 Agent(`packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts`),各有明确的职责和模型配置:
|
系统预定义了几个内置 Agent(`src/tools/AgentTool/builtinAgents.ts`),各有明确的职责和模型配置:
|
||||||
|
|
||||||
| Agent | 模型 | 权限 | 用途 |
|
| Agent | 模型 | 权限 | 用途 |
|
||||||
|-------|------|------|------|
|
|-------|------|------|------|
|
||||||
@@ -119,7 +119,7 @@ const workerTools = assembleToolPool(workerPermissionContext, appState.mcp.tools
|
|||||||
|
|
||||||
### 工具过滤的 resolveAgentTools
|
### 工具过滤的 resolveAgentTools
|
||||||
|
|
||||||
`runAgent.ts:508` 在工具组装后进一步过滤:
|
`runAgent.ts:500-502` 在工具组装后进一步过滤:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const resolvedTools = useExactTools
|
const resolvedTools = useExactTools
|
||||||
@@ -142,7 +142,7 @@ const resolvedTools = useExactTools
|
|||||||
|
|
||||||
## Worktree 隔离机制
|
## Worktree 隔离机制
|
||||||
|
|
||||||
`isolation: "worktree"` 参数让子 Agent 在独立的 git worktree 中工作(`AgentTool.tsx:863`):
|
`isolation: "worktree"` 参数让子 Agent 在独立的 git worktree 中工作(`AgentTool.tsx:590-593`):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const slug = `agent-${earlyAgentId.slice(0, 8)}`
|
const slug = `agent-${earlyAgentId.slice(0, 8)}`
|
||||||
@@ -183,7 +183,7 @@ runAsyncAgentLifecycle() ← 后台执行(agentToolUtils.ts)
|
|||||||
|
|
||||||
### 同步 Agent(前台运行)
|
### 同步 Agent(前台运行)
|
||||||
|
|
||||||
同步 Agent 的关键特性是 **可后台化**(`AgentTool.tsx:1107`):
|
同步 Agent 的关键特性是 **可后台化**(`AgentTool.tsx:818-833`):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const registration = registerAgentForeground({
|
const registration = registerAgentForeground({
|
||||||
@@ -218,7 +218,7 @@ const raceResult = await Promise.race([
|
|||||||
|
|
||||||
## MCP 依赖的等待机制
|
## MCP 依赖的等待机制
|
||||||
|
|
||||||
如果 Agent 声明了 `requiredMcpServers`,`call()` 会等待这些服务器连接完成(`AgentTool.tsx:576`):
|
如果 Agent 声明了 `requiredMcpServers`,`call()` 会等待这些服务器连接完成(`AgentTool.tsx:371-410`):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const MAX_WAIT_MS = 30_000 // 最长等 30 秒
|
const MAX_WAIT_MS = 30_000 // 最长等 30 秒
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Worktree 文件统一存放在仓库根目录下的 `.claude/worktrees/`:
|
|||||||
|
|
||||||
## 创建流程:EnterWorktreeTool
|
## 创建流程:EnterWorktreeTool
|
||||||
|
|
||||||
`EnterWorktreeTool`(`packages/builtin-tools/src/tools/EnterWorktreeTool/EnterWorktreeTool.ts`)的执行链路:
|
`EnterWorktreeTool`(`src/tools/EnterWorktreeTool/EnterWorktreeTool.ts`)的执行链路:
|
||||||
|
|
||||||
```
|
```
|
||||||
EnterWorktreeTool.call({ name? })
|
EnterWorktreeTool.call({ name? })
|
||||||
@@ -83,7 +83,7 @@ EnterWorktreeTool.call({ name? })
|
|||||||
|
|
||||||
## 退出流程:ExitWorktreeTool
|
## 退出流程:ExitWorktreeTool
|
||||||
|
|
||||||
`ExitWorktreeTool`(`packages/builtin-tools/src/tools/ExitWorktreeTool/ExitWorktreeTool.ts`)支持两种退出策略:
|
`ExitWorktreeTool`(`src/tools/ExitWorktreeTool/ExitWorktreeTool.ts`)支持两种退出策略:
|
||||||
|
|
||||||
### keep:保留 worktree
|
### keep:保留 worktree
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ useInterval(checkForUpdates, 30 * 60 * 1000); // 每 30 分钟
|
|||||||
|
|
||||||
任何更新尝试之前,系统会依次检查:
|
任何更新尝试之前,系统会依次检查:
|
||||||
|
|
||||||
1. **自动更新是否被禁用?** — `getAutoUpdaterDisabledReason()`(`src/utils/config.ts:1737`)
|
1. **自动更新是否被禁用?** — `getAutoUpdaterDisabledReason()`(`src/utils/config.ts:1735`)
|
||||||
- `NODE_ENV === 'development'`
|
- `NODE_ENV === 'development'`
|
||||||
- 设置了 `DISABLE_AUTOUPDATER` 环境变量
|
- 设置了 `DISABLE_AUTOUPDATER` 环境变量
|
||||||
- 仅限必要流量模式
|
- 仅限必要流量模式
|
||||||
@@ -81,7 +81,7 @@ useInterval(checkForUpdates, 30 * 60 * 1000); // 每 30 分钟
|
|||||||
|
|
||||||
`src/utils/autoUpdater.ts:70` — `assertMinVersion()`
|
`src/utils/autoUpdater.ts:70` — `assertMinVersion()`
|
||||||
|
|
||||||
定义于 `src/utils/autoUpdater.ts:70`,设计上在启动时调用(当前未接入启动流程):
|
从 `src/main.tsx:1775` 在启动时调用:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
void assertMinVersion();
|
void assertMinVersion();
|
||||||
@@ -200,7 +200,7 @@ Windows 系统使用文件复制而非符号链接。
|
|||||||
|
|
||||||
**文件**: `src/migrations/migrateAutoUpdatesToSettings.ts`
|
**文件**: `src/migrations/migrateAutoUpdatesToSettings.ts`
|
||||||
|
|
||||||
一次性将旧版 `globalConfig.autoUpdates = false` 迁移为 settings 中的 `DISABLE_AUTOUPDATER=1` 环境变量。定义于 `src/migrations/migrateAutoUpdatesToSettings.ts`(当前未接入启动流程)。
|
一次性将旧版 `globalConfig.autoUpdates = false` 迁移为 settings 中的 `DISABLE_AUTOUPDATER=1` 环境变量。从 `src/main.tsx:325` 在启动时调用。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@ React hook `useUpdateNotification(updatedVersion)` — 确保每次 semver 变
|
|||||||
| `src/utils/releaseNotes.ts` | Changelog 获取、缓存与展示 |
|
| `src/utils/releaseNotes.ts` | Changelog 获取、缓存与展示 |
|
||||||
| `src/utils/semver.ts` | Semver 版本比较(Bun 原生 + npm 回退) |
|
| `src/utils/semver.ts` | Semver 版本比较(Bun 原生 + npm 回退) |
|
||||||
| `src/utils/doctorDiagnostic.ts` | 安装类型检测与健康诊断 |
|
| `src/utils/doctorDiagnostic.ts` | 安装类型检测与健康诊断 |
|
||||||
| `src/utils/config.ts:1737` | `getAutoUpdaterDisabledReason()` — 禁用检查逻辑 |
|
| `src/utils/config.ts:1735` | `getAutoUpdaterDisabledReason()` — 禁用检查逻辑 |
|
||||||
| `src/migrations/migrateAutoUpdatesToSettings.ts` | 旧版配置迁移 |
|
| `src/migrations/migrateAutoUpdatesToSettings.ts` | 旧版配置迁移 |
|
||||||
| `src/screens/Doctor.tsx` | Doctor 命令 UI,展示自动更新状态 |
|
| `src/screens/Doctor.tsx` | Doctor 命令 UI,展示自动更新状态 |
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const messagesForCompact = microcompactResult.messages
|
|||||||
MicroCompact 不压缩整个对话,而是**清除旧工具输出的内容**。它维护一个白名单:
|
MicroCompact 不压缩整个对话,而是**清除旧工具输出的内容**。它维护一个白名单:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/services/compact/microCompact.ts:41-50
|
// src/services/compact/microCompact.ts:41-48
|
||||||
const COMPACTABLE_TOOLS = new Set([
|
const COMPACTABLE_TOOLS = new Set([
|
||||||
FILE_READ_TOOL_NAME, // 'Read' - 文件读取
|
FILE_READ_TOOL_NAME, // 'Read' - 文件读取
|
||||||
...SHELL_TOOL_NAMES, // 'Bash' - 命令输出
|
...SHELL_TOOL_NAMES, // 'Bash' - 命令输出
|
||||||
@@ -143,7 +143,7 @@ const stripped2 = stripReinjectedAttachments(stripped) // 移除会被重新注
|
|||||||
压缩后,系统会从摘要中**重新注入关键上下文**:
|
压缩后,系统会从摘要中**重新注入关键上下文**:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// compact.ts:126-134
|
// compact.ts:124-132
|
||||||
export const POST_COMPACT_TOKEN_BUDGET = 50_000 // 总预算
|
export const POST_COMPACT_TOKEN_BUDGET = 50_000 // 总预算
|
||||||
export const POST_COMPACT_MAX_FILES_TO_RESTORE = 5 // 最多恢复 5 个文件
|
export const POST_COMPACT_MAX_FILES_TO_RESTORE = 5 // 最多恢复 5 个文件
|
||||||
export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000 // 每文件 5K token
|
export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000 // 每文件 5K token
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ Claude Code 的记忆系统是**纯文件**的——没有数据库、没有向
|
|||||||
`MEMORY.md` 是记忆的入口索引,每次对话都完整加载到上下文中:
|
`MEMORY.md` 是记忆的入口索引,每次对话都完整加载到上下文中:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// memdir.ts:34-38
|
// memdir.ts:35-38
|
||||||
export const ENTRYPOINT_NAME = 'MEMORY.md'
|
export const ENTRYPOINT_NAME = 'MEMORY.md'
|
||||||
export const MAX_ENTRYPOINT_LINES = 200
|
export const MAX_ENTRYPOINT_LINES = 200
|
||||||
export const MAX_ENTRYPOINT_BYTES = 25_000
|
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` 分界标记
|
1. **`getSystemPrompt()`**(`src/constants/prompts.ts:444`)—— 收集静态段 + 动态段,插入 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 分界标记
|
||||||
2. **`buildEffectiveSystemPrompt()`**(`src/utils/systemPrompt.ts:41`)—— 按 Override > Coordinator > Agent > Custom > Default 优先级选择
|
2. **`buildEffectiveSystemPrompt()`**(`src/utils/systemPrompt.ts:41`)—— 按 Override > Coordinator > Agent > Custom > Default 优先级选择
|
||||||
3. **`buildSystemPromptBlocks()`**(`src/services/api/claude.ts:3279`)—— 调用 `splitSysPromptPrefix()` 分块,为每个块附加 `cache_control`
|
3. **`buildSystemPromptBlocks()`**(`src/services/api/claude.ts:3214`)—— 调用 `splitSysPromptPrefix()` 分块,为每个块附加 `cache_control`
|
||||||
|
|
||||||
## SystemPrompt 品牌类型
|
## SystemPrompt 品牌类型
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// packages/@ant/model-provider/src/types/systemPrompt.ts:4
|
// src/utils/systemPromptType.ts:8
|
||||||
export type SystemPrompt = readonly string[] & {
|
export type SystemPrompt = readonly string[] & {
|
||||||
readonly __brand: 'SystemPrompt'
|
readonly __brand: 'SystemPrompt'
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ export function shouldUseGlobalCacheScope(): boolean {
|
|||||||
|
|
||||||
### `getCacheControl()`:TTL 决策
|
### `getCacheControl()`:TTL 决策
|
||||||
|
|
||||||
`src/services/api/claude.ts:348` 生成的 `cache_control` 对象:
|
`src/services/api/claude.ts:359` 生成的 `cache_control` 对象:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
@@ -195,14 +195,14 @@ export function shouldUseGlobalCacheScope(): boolean {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
1 小时 TTL 的判定逻辑(`should1hCacheTTL()`,第 383 行):
|
1 小时 TTL 的判定逻辑(`should1hCacheTTL()`,第 394 行):
|
||||||
- **Bedrock 用户**:通过环境变量 `ENABLE_PROMPT_CACHING_1H_BEDROCK` 启用
|
- **Bedrock 用户**:通过环境变量 `ENABLE_PROMPT_CACHING_1H_BEDROCK` 启用
|
||||||
- **1P 用户**:通过 GrowthBook 配置的 `allowlist` 数组匹配 `querySource`,支持前缀通配符(如 `"repl_main_thread*"`)
|
- **1P 用户**:通过 GrowthBook 配置的 `allowlist` 数组匹配 `querySource`,支持前缀通配符(如 `"repl_main_thread*"`)
|
||||||
- **会话级锁定**:资格判定结果在 bootstrap state 中缓存,防止 GrowthBook 配置中途变化导致同一会话内 TTL 不一致
|
- **会话级锁定**:资格判定结果在 bootstrap state 中缓存,防止 GrowthBook 配置中途变化导致同一会话内 TTL 不一致
|
||||||
|
|
||||||
### 缓存破坏:Session-Specific Guidance 的放置
|
### 缓存破坏:Session-Specific Guidance 的放置
|
||||||
|
|
||||||
`getSessionSpecificGuidanceSection()`(`src/constants/prompts.ts:354`)的内容必须放在 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` **之后**。因为它包含:
|
`getSessionSpecificGuidanceSection()`(`src/constants/prompts.ts:352`)的内容必须放在 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` **之后**。因为它包含:
|
||||||
- 当前会话的 enabledTools 集合
|
- 当前会话的 enabledTools 集合
|
||||||
- `isForkSubagentEnabled()` 的运行时判定
|
- `isForkSubagentEnabled()` 的运行时判定
|
||||||
- `getIsNonInteractiveSession()` 的结果
|
- `getIsNonInteractiveSession()` 的结果
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ message_stop ← 消息结束
|
|||||||
|
|
||||||
### 事件处理状态机
|
### 事件处理状态机
|
||||||
|
|
||||||
`src/services/api/claude.ts` 中 `queryModelWithStreaming()` 函数的事件处理循环实现了一个基于 `switch(part.type)` 的状态机:
|
`src/services/api/claude.ts` 中 `queryStreamRaw()` 函数的事件处理循环实现了一个基于 `switch(part.type)` 的状态机:
|
||||||
|
|
||||||
| 事件类型 | 处理逻辑 | 状态变更 |
|
| 事件类型 | 处理逻辑 | 状态变更 |
|
||||||
|----------|----------|----------|
|
|----------|----------|----------|
|
||||||
@@ -167,13 +167,10 @@ UI 层通过 `useToolCallProgress` hook 实时展示命令输出,而不是等
|
|||||||
|
|
||||||
| Provider | 流式协议 | 特殊处理 |
|
| Provider | 流式协议 | 特殊处理 |
|
||||||
|----------|----------|----------|
|
|----------|----------|----------|
|
||||||
| **firstParty** (Anthropic Direct) | 原生 SSE | 延迟最低,TTFT 最快 |
|
| **Anthropic Direct** | 原生 SSE | 延迟最低,TTFT 最快 |
|
||||||
| **AWS Bedrock** | AWS SDK 流式接口 | 需要额外的 beta header 和认证 |
|
| **AWS Bedrock** | AWS SDK 流式接口 | 需要额外的 beta header 和认证 |
|
||||||
| **Google Vertex** | gRPC → 事件流 | 通过 `getMergedBetas()` 适配 |
|
| **Google Vertex** | gRPC → 事件流 | 通过 `getMergedBetas()` 适配 |
|
||||||
| **foundry** | Anthropic 兼容 API | 内部部署 |
|
| **Azure** | Anthropic 兼容 API | 自定义 base URL |
|
||||||
| **openai** | OpenAI 流式适配器 | 转换为 Anthropic 内部格式 |
|
|
||||||
| **gemini** | Gemini 流式适配器 | 转换为 Anthropic 内部格式 |
|
|
||||||
| **grok** (xAI) | Grok 流式适配器 | 转换为 Anthropic 内部格式 |
|
|
||||||
|
|
||||||
所有 Provider 通过统一的 `Stream<BetaRawMessageStreamEvent>` 抽象层屏蔽差异。上层代码(QueryEngine、REPL)不需要关心底层用的是哪个 Provider。
|
所有 Provider 通过统一的 `Stream<BetaRawMessageStreamEvent>` 抽象层屏蔽差异。上层代码(QueryEngine、REPL)不需要关心底层用的是哪个 Provider。
|
||||||
|
|
||||||
|
|||||||
@@ -74,17 +74,17 @@ const toolUpdates = streamingToolExecutor
|
|||||||
|
|
||||||
| 终止原因 | 触发位置 | 机制 |
|
| 终止原因 | 触发位置 | 机制 |
|
||||||
|----------|---------|------|
|
|----------|---------|------|
|
||||||
| **blocking_limit** | 第 686 行 | Token 计数超过硬限制(非 autocompact 模式)→ 生成 PTL 错误消息 → 返回 |
|
| **blocking_limit** | 第 646 行 | Token 计数超过硬限制(非 autocompact 模式)→ 生成 PTL 错误消息 → 返回 |
|
||||||
| **image_error** | 第 1021 行 | `ImageSizeError` / `ImageResizeError` 异常 → 直接返回 |
|
| **image_error** | 第 980 行 | `ImageSizeError` / `ImageResizeError` 异常 → 直接返回 |
|
||||||
| **model_error** | 第 1040 行 | `callModel()` 抛出不可恢复异常 → 生成错误消息 → 返回 |
|
| **model_error** | 第 999 行 | `callModel()` 抛出不可恢复异常 → 生成错误消息 → 返回 |
|
||||||
| **aborted_streaming** | 第 1095 行 | `abortController.signal.aborted`(流式阶段)→ 为未完成的 tool_use 生成合成 tool_result → 返回 |
|
| **aborted_streaming** | 第 1054 行 | `abortController.signal.aborted`(流式阶段)→ 为未完成的 tool_use 生成合成 tool_result → 返回 |
|
||||||
| **prompt_too_long** | 第 1219/1226 行 | 413 错误且 reactive compact 无法恢复 → 暂扣的错误消息被释放 → 返回 |
|
| **prompt_too_long** | 第 1178/1185 行 | 413 错误且 reactive compact 无法恢复 → 暂扣的错误消息被释放 → 返回 |
|
||||||
| **completed** | 第 1308 行 | API 错误(限流、认证失败等)导致无法继续 → 返回 |
|
| **completed** | 第 1267 行 | API 错误(限流、认证失败等)导致无法继续 → 返回 |
|
||||||
| **stop_hook_prevented** | 第 1323 行 | Stop hook 返回 `preventContinuation: true` → 返回 |
|
| **stop_hook_prevented** | 第 1282 行 | Stop hook 返回 `preventContinuation: true` → 返回 |
|
||||||
| **completed** | 第 1401 行 | 正常完成:AI 未发出 tool_use → `needsFollowUp = false` → 经过 stop hooks → 返回 |
|
| **completed** | 第 1360 行 | 正常完成:AI 未发出 tool_use → `needsFollowUp = false` → 经过 stop hooks → 返回 |
|
||||||
| **aborted_tools** | 第 1559 行 | `abortController.signal.aborted`(工具执行阶段)→ 返回 |
|
| **aborted_tools** | 第 1518 行 | `abortController.signal.aborted`(工具执行阶段)→ 返回 |
|
||||||
| **hook_stopped** | 第 1564 行 | 工具执行期间 hook 返回 `shouldPreventContinuation` → 返回 |
|
| **hook_stopped** | 第 1523 行 | 工具执行期间 hook 返回 `shouldPreventContinuation` → 返回 |
|
||||||
| **max_turns** | 第 1755 行 | 轮次计数超过 `maxTurns` 限制 → 返回 |
|
| **max_turns** | 第 1714 行 | 轮次计数超过 `maxTurns` 限制 → 返回 |
|
||||||
|
|
||||||
## 继续条件(恢复路径)
|
## 继续条件(恢复路径)
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ type State = {
|
|||||||
- **每一步都产生真实信息**:`runTools()` 返回的 `toolResults` 是 API 不可能预知的——命令输出、文件内容、错误信息
|
- **每一步都产生真实信息**:`runTools()` 返回的 `toolResults` 是 API 不可能预知的——命令输出、文件内容、错误信息
|
||||||
- **动态上下文管理**:每轮迭代前都重新评估压缩需求(autocompact → microcompact → snip),基于最新的 token 计数
|
- **动态上下文管理**:每轮迭代前都重新评估压缩需求(autocompact → microcompact → snip),基于最新的 token 计数
|
||||||
- **错误即时恢复**:工具失败不需要推倒重来——stop hook 可以注入阻塞错误让 AI 修正策略
|
- **错误即时恢复**:工具失败不需要推倒重来——stop hook 可以注入阻塞错误让 AI 修正策略
|
||||||
- **用户可控**:`abortController.signal` 在循环的多个检查点被检测(第 1059、1095、1529 行),用户按 ESC 可以优雅中断
|
- **用户可控**:`abortController.signal` 在循环的多个检查点被检测(第 1018、1048、1488 行),用户按 ESC 可以优雅中断
|
||||||
- **成本控制**:Token Budget 在每轮终止前检查,防止 AI 无效循环
|
- **成本控制**:Token Budget 在每轮终止前检查,防止 AI 无效循环
|
||||||
|
|
||||||
## 一个完整的迭代示例
|
## 一个完整的迭代示例
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Claude Code 的 Agent 不仅仅来自用户自定义——系统有三类来源
|
|||||||
|
|
||||||
| 来源 | 位置 | 优先级 |
|
| 来源 | 位置 | 优先级 |
|
||||||
|------|------|--------|
|
|------|------|--------|
|
||||||
| **Built-in** | `packages/builtin-tools/src/tools/AgentTool/built-in/` 硬编码 | 最低(可被覆盖) |
|
| **Built-in** | `src/tools/AgentTool/built-in/` 硬编码 | 最低(可被覆盖) |
|
||||||
| **Plugin** | 通过插件系统注册 | 中 |
|
| **Plugin** | 通过插件系统注册 | 中 |
|
||||||
| **User/Project/Policy** | `.claude/agents/*.md` 或 settings.json | 最高 |
|
| **User/Project/Policy** | `.claude/agents/*.md` 或 settings.json | 最高 |
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ color: "blue" # 终端中的 Agent 颜色标识
|
|||||||
以内置 Explore Agent 为例:
|
以内置 Explore Agent 为例:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// packages/builtin-tools/src/tools/AgentTool/built-in/exploreAgent.ts
|
// src/tools/AgentTool/built-in/exploreAgent.ts
|
||||||
disallowedTools: [
|
disallowedTools: [
|
||||||
'Agent', // 不能嵌套调用 Agent
|
'Agent', // 不能嵌套调用 Agent
|
||||||
'ExitPlanMode', // 不需要 plan mode
|
'ExitPlanMode', // 不需要 plan mode
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
|
|||||||
|
|
||||||
## Session Hook 的生命周期
|
## Session Hook 的生命周期
|
||||||
|
|
||||||
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(调用位置:`packages/builtin-tools/src/tools/AgentTool/runAgent.ts`;定义位置:`src/utils/hooks/registerFrontmatterHooks.ts`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()`(定义位置:`src/utils/hooks/sessionHooks.ts`)清理。
|
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(调用位置:`src/tools/AgentTool/runAgent.ts`;定义位置:`src/utils/hooks/registerFrontmatterHooks.ts`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()`(定义位置:`src/utils/hooks/sessionHooks.ts`)清理。
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// runAgent.ts — 注册 agent 的前置 Hook
|
// runAgent.ts — 注册 agent 的前置 Hook
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ timer.unref?.() // 不阻止进程退出
|
|||||||
|
|
||||||
## 工具发现:从 MCP 到 Tool 接口
|
## 工具发现:从 MCP 到 Tool 接口
|
||||||
|
|
||||||
`fetchToolsForClient()`(`client.ts:1744-2000`)使用 `memoizeWithLRU` 缓存(上限 100),将 MCP 工具转换为 Claude Code 的统一 Tool 接口:
|
`fetchToolsForClient()`(`client.ts:1745-2000`)使用 `memoizeWithLRU` 缓存(上限 20),将 MCP 工具转换为 Claude Code 的统一 Tool 接口:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
|
const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Promp
|
|||||||
|
|
||||||
### 1. 内置命令(Built-in Commands)
|
### 1. 内置命令(Built-in Commands)
|
||||||
|
|
||||||
硬编码在 `src/commands.ts:299` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown,但实现了相同的 `Command` 接口(`src/types/command.ts`)。
|
硬编码在 `src/commands.ts:258` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown,但实现了相同的 `Command` 接口(`src/types/command.ts`)。
|
||||||
|
|
||||||
### 2. Bundled Skills(编译时打包)
|
### 2. Bundled Skills(编译时打包)
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ shell: ["bash"] # Shell 执行环境
|
|||||||
|
|
||||||
## 两条执行路径:Inline vs Fork
|
## 两条执行路径:Inline vs Fork
|
||||||
|
|
||||||
SkillTool(`packages/builtin-tools/src/tools/SkillTool/SkillTool.ts:332`)在 `call()` 中根据 `command.context` 分流:
|
SkillTool(`src/tools/SkillTool/SkillTool.ts:332`)在 `call()` 中根据 `command.context` 分流:
|
||||||
|
|
||||||
### Inline 模式(默认)
|
### Inline 模式(默认)
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
- **端点**: `{region}-aiplatform.googleapis.com`
|
- **端点**: `{region}-aiplatform.googleapis.com`
|
||||||
- **认证**: `GoogleAuth` + `cloud-platform` scope
|
- **认证**: `GoogleAuth` + `cloud-platform` scope
|
||||||
- **文件**: `src/services/api/client.ts:221-298`
|
- **文件**: `src/services/api/client.ts:228-298`
|
||||||
|
|
||||||
### 4. Azure Foundry
|
### 4. Azure Foundry
|
||||||
|
|
||||||
@@ -129,12 +129,12 @@ WebSearch 工具支持直接抓取 Bing 搜索结果页面,也支持通过 Bra
|
|||||||
- **Bing 端点**: `https://www.bing.com/search?q={query}&setmkt=en-US`
|
- **Bing 端点**: `https://www.bing.com/search?q={query}&setmkt=en-US`
|
||||||
- **Brave 端点**: `https://api.search.brave.com/res/v1/llm/context?q={query}`
|
- **Brave 端点**: `https://api.search.brave.com/res/v1/llm/context?q={query}`
|
||||||
- **文件**:
|
- **文件**:
|
||||||
- `packages/builtin-tools/src/tools/WebSearchTool/adapters/bingAdapter.ts`
|
- `src/tools/WebSearchTool/adapters/bingAdapter.ts`
|
||||||
- `packages/builtin-tools/src/tools/WebSearchTool/adapters/braveAdapter.ts`
|
- `src/tools/WebSearchTool/adapters/braveAdapter.ts`
|
||||||
|
|
||||||
另外还有 Domain Blocklist 查询:
|
另外还有 Domain Blocklist 查询:
|
||||||
- **端点**: `https://api.anthropic.com/api/web/domain_info?domain={domain}`
|
- **端点**: `https://api.anthropic.com/api/web/domain_info?domain={domain}`
|
||||||
- **文件**: `packages/builtin-tools/src/tools/WebFetchTool/utils.ts`
|
- **文件**: `src/tools/WebFetchTool/utils.ts`
|
||||||
|
|
||||||
### 15. Google Cloud Storage (自动更新)
|
### 15. Google Cloud Storage (自动更新)
|
||||||
|
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
# acp-link — ACP 代理服务器
|
|
||||||
|
|
||||||
> 源码目录:`packages/acp-link/`
|
|
||||||
> PR: #292
|
|
||||||
> 新增时间:2026-04-18
|
|
||||||
|
|
||||||
## 一、功能概述
|
|
||||||
|
|
||||||
`acp-link` 是一个 ACP (Agent Client Protocol) 代理服务器,将 WebSocket 客户端桥接到 ACP agent 的 stdio 接口。它让 ACP agent(如 Claude Code)可以通过 WebSocket 远程访问,而不仅限于本地 stdio。
|
|
||||||
|
|
||||||
### 核心特性
|
|
||||||
|
|
||||||
- **WebSocket → stdio 桥接**:将浏览器/远程客户端的 WebSocket 连接转换为 ACP agent 的 stdin/stdout NDJSON 流
|
|
||||||
- **会话管理**:创建、加载、恢复、列出、关闭会话
|
|
||||||
- **权限审批流程**:客户端可远程审批 agent 的工具权限请求
|
|
||||||
- **RCS 集成**:可与 Remote Control Server (RCS) 连接,将 ACP agent 注册到 RCS 并通过 Web UI 交互
|
|
||||||
- **HTTPS 支持**:内置自签名证书生成,支持安全连接
|
|
||||||
- **Token 认证**:自动生成或通过环境变量配置认证 token
|
|
||||||
|
|
||||||
## 二、架构
|
|
||||||
|
|
||||||
### 独立模式
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────┐ WebSocket ┌──────────────────┐ stdio/NDJSON ┌──────────────┐
|
|
||||||
│ 浏览器/客户端 │ ◄──────────────►│ acp-link │ ◄────────────────►│ ACP Agent │
|
|
||||||
│ (WS Client) │ ws://host:port │ (Proxy Server) │ spawn subprocess │ (Claude等) │
|
|
||||||
└──────────────────┘ └──────────────────┘ └──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### RCS 集成模式
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────┐ WebSocket ┌──────────────────┐ stdio/NDJSON ┌──────────────┐
|
|
||||||
│ RCS Web UI │ ◄──────────────►│ Remote Control │ ◄─────────────────►│ acp-link │
|
|
||||||
│ (/code/*) │ ACP Relay WS │ Server (RCS) │ ACP events │ + Agent │
|
|
||||||
└──────────────┘ └──────────────────┘ └──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
packages/acp-link/
|
|
||||||
├── src/
|
|
||||||
│ ├── server.ts # 主服务器:WS 连接管理、会话管理、权限处理、消息桥接
|
|
||||||
│ ├── rcs-upstream.ts # RCS 上游客户端:REST 注册 + WS identify 两步流程
|
|
||||||
│ ├── cert.ts # TLS 证书生成(自签名)
|
|
||||||
│ ├── logger.ts # 日志模块
|
|
||||||
│ ├── types.ts # JSON-RPC 和 ACP 协议类型定义
|
|
||||||
│ ├── cli/
|
|
||||||
│ │ ├── bin.ts # CLI 入口
|
|
||||||
│ │ ├── command.ts # 命令行参数解析
|
|
||||||
│ │ ├── app.ts # 应用启动
|
|
||||||
│ │ └── context.ts # 上下文配置
|
|
||||||
│ └── __tests__/ # 测试(cert, server, types)
|
|
||||||
├── package.json
|
|
||||||
└── tsconfig.json
|
|
||||||
```
|
|
||||||
|
|
||||||
## 三、安装与使用
|
|
||||||
|
|
||||||
### 基本用法
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 直接运行(在 monorepo 中)
|
|
||||||
# 注意:claude 本身不支持 ACP,需要用 ccb-bun --acp 启动 ACP agent
|
|
||||||
bun packages/acp-link/src/cli/bin.ts ccb-bun -- --acp
|
|
||||||
|
|
||||||
# 指定端口和主机
|
|
||||||
acp-link --port 9000 --host 0.0.0.0 ccb-bun -- --acp
|
|
||||||
|
|
||||||
# 启用 HTTPS(自签名证书)
|
|
||||||
acp-link --https ccb-bun -- --acp
|
|
||||||
|
|
||||||
# 调试模式
|
|
||||||
acp-link --debug ccb-bun -- --acp
|
|
||||||
```
|
|
||||||
|
|
||||||
### CLI 参考
|
|
||||||
|
|
||||||
```
|
|
||||||
USAGE
|
|
||||||
acp-link [--port value] [--host value] [--debug] [--no-auth] [--https] <command>...
|
|
||||||
acp-link --help
|
|
||||||
acp-link --version
|
|
||||||
|
|
||||||
FLAGS
|
|
||||||
[--port] Port to listen on [default = 9315]
|
|
||||||
[--host] Host to bind to [default = localhost]
|
|
||||||
[--debug] Enable debug logging to file
|
|
||||||
[--no-auth] Disable authentication (dangerous)
|
|
||||||
[--https] Enable HTTPS with self-signed cert
|
|
||||||
-h --help Print help information and exit
|
|
||||||
-v --version Print version information and exit
|
|
||||||
|
|
||||||
ARGUMENTS
|
|
||||||
command... Agent command followed by its arguments (e.g. "ccb-bun -- --acp")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 四、认证
|
|
||||||
|
|
||||||
默认启动时自动生成随机 token。客户端连接时需通过 query 参数传递:
|
|
||||||
|
|
||||||
```
|
|
||||||
ws://localhost:9315/ws?token=<your-token>
|
|
||||||
```
|
|
||||||
|
|
||||||
配置固定 token:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ACP_AUTH_TOKEN=my-fixed-token acp-link ccb-bun -- --acp
|
|
||||||
```
|
|
||||||
|
|
||||||
禁用认证(不推荐,仅用于开发):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
acp-link --no-auth ccb-bun -- --acp
|
|
||||||
```
|
|
||||||
|
|
||||||
## 五、RCS 集成
|
|
||||||
|
|
||||||
acp-link 支持将 ACP agent 注册到 Remote Control Server,通过 Web UI 远程操控。
|
|
||||||
|
|
||||||
### 连接方式
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 通过环境变量配置 RCS 连接
|
|
||||||
ACP_RCS_URL=http://localhost:3000 \
|
|
||||||
ACP_RCS_TOKEN=sk-rcs-your-key \
|
|
||||||
acp-link ccb-bun -- --acp
|
|
||||||
```
|
|
||||||
|
|
||||||
### 注册流程(两步)
|
|
||||||
|
|
||||||
1. **REST 注册**:通过 `POST /v1/environments/bridge` 向 RCS 注册环境
|
|
||||||
2. **WS identify**:建立 WebSocket 连接后发送 `identify` 消息(携带 agentId),替代完整 `register`
|
|
||||||
|
|
||||||
```
|
|
||||||
acp-link RCS
|
|
||||||
│ │
|
|
||||||
│── POST /v1/environments/bridge ──►│ (REST 注册)
|
|
||||||
│◄── { agentId, sessionId } ───────│
|
|
||||||
│ │
|
|
||||||
│── WS connect ─────────────────►│ (WebSocket)
|
|
||||||
│── identify { agentId } ────────►│ (WS 标识)
|
|
||||||
│◄── identified ─────────────────│
|
|
||||||
│ │
|
|
||||||
│── ACP events ─────────────────►│ (双向消息转发)
|
|
||||||
│◄── user prompts/permissions ───│
|
|
||||||
```
|
|
||||||
|
|
||||||
## 六、权限模式
|
|
||||||
|
|
||||||
### permissionMode 传递链
|
|
||||||
|
|
||||||
权限模式通过整条链路传递:Web UI → RCS → acp-link → ACP agent。
|
|
||||||
|
|
||||||
支持的权限模式:
|
|
||||||
- `default` — 每次请求权限确认
|
|
||||||
- `auto` — 自动判断
|
|
||||||
- `acceptEdits` — 自动接受编辑
|
|
||||||
- `plan` — 规划模式
|
|
||||||
- `dontAsk` — 不询问
|
|
||||||
- `bypassPermissions` — 绕过权限(需 sandbox 环境)
|
|
||||||
|
|
||||||
### fallback 链
|
|
||||||
|
|
||||||
当客户端未显式传递 permissionMode 时,使用以下 fallback 链:
|
|
||||||
|
|
||||||
```
|
|
||||||
客户端传值 > config.permissionMode > ACP_PERMISSION_MODE 环境变量
|
|
||||||
```
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ACP_PERMISSION_MODE=auto acp-link ccb-bun -- --acp
|
|
||||||
```
|
|
||||||
|
|
||||||
## 七、权限管道(2026-04-18 改进)
|
|
||||||
|
|
||||||
### 模式同步
|
|
||||||
|
|
||||||
`applySessionMode` 在 agent 切换权限模式时同步 `appState.toolPermissionContext.mode`,确保内部权限上下文与 ACP 客户端状态一致。
|
|
||||||
|
|
||||||
### 统一权限流水线
|
|
||||||
|
|
||||||
`createAcpCanUseTool` 接入 `hasPermissionsToUseTool` 统一权限流水线,替代原来分散的处理逻辑。支持 `onModeChange` 回调,模式变更时实时同步。
|
|
||||||
|
|
||||||
### bypass 检测
|
|
||||||
|
|
||||||
`bypassPermissions` 模式增加可用性检测 — 仅在非 root 或 sandbox 环境中允许启用,防止权限绕过的安全风险。
|
|
||||||
|
|
||||||
## 八、环境变量
|
|
||||||
|
|
||||||
| 变量 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `ACP_AUTH_TOKEN` | 固定认证 token(默认自动生成) |
|
|
||||||
| `ACP_PERMISSION_MODE` | 默认权限模式 fallback |
|
|
||||||
| `ACP_RCS_URL` | RCS 服务器地址(启用 RCS 集成) |
|
|
||||||
| `ACP_RCS_TOKEN` | RCS API token |
|
|
||||||
@@ -516,37 +516,25 @@ AI 也可通过 `SnipTool` 自动截断过长的对话:
|
|||||||
|
|
||||||
| Flag | 默认 | 说明 |
|
| Flag | 默认 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `BUDDY` | ✅ dev only | 伴侣系统 |
|
| `BUDDY` | ✅ dev/build | 伴侣系统 |
|
||||||
| `BRIDGE_MODE` | ✅ dev only | 远程控制 |
|
| `BRIDGE_MODE` | ✅ dev/build | 远程控制 |
|
||||||
| `VOICE_MODE` | ✅ dev+build | 语音模式 |
|
| `VOICE_MODE` | ✅ dev/build | 语音模式 |
|
||||||
| `CHICAGO_MCP` | ✅ dev+build | Computer Use + Chrome |
|
| `CHICAGO_MCP` | ✅ dev/build | Computer Use + Chrome |
|
||||||
| `AGENT_TRIGGERS_REMOTE` | ✅ dev+build | 定时任务 |
|
| `AGENT_TRIGGERS_REMOTE` | ✅ dev/build | 定时任务 |
|
||||||
| `SHOT_STATS` | ✅ dev+build | API 统计 |
|
| `SHOT_STATS` | ✅ dev/build | API 统计 |
|
||||||
| `TOKEN_BUDGET` | ✅ dev+build | Token 预算 |
|
| `TOKEN_BUDGET` | ✅ dev/build | Token 预算 |
|
||||||
| `PROMPT_CACHE_BREAK_DETECTION` | ✅ dev+build | 缓存检测 |
|
| `PROMPT_CACHE_BREAK_DETECTION` | ✅ dev/build | 缓存检测 |
|
||||||
| `ULTRAPLAN` | ✅ dev+build | 高级规划 |
|
| `ULTRAPLAN` | ✅ dev/build | 高级规划 |
|
||||||
| `DAEMON` | ✅ dev+build | 后台守护 |
|
| `DAEMON` | ✅ dev/build | 后台守护 |
|
||||||
| `UDS_INBOX` | ✅ dev only | Pipe IPC |
|
| `UDS_INBOX` | ✅ dev/build | Pipe IPC |
|
||||||
| `LAN_PIPES` | ✅ dev only | LAN 群控 |
|
| `LAN_PIPES` | ✅ dev/build | LAN 群控 |
|
||||||
| `MONITOR_TOOL` | ✅ dev+build | 后台监控 |
|
| `MONITOR_TOOL` | ✅ dev/build | 后台监控 |
|
||||||
| `WORKFLOW_SCRIPTS` | ✅ dev+build | 工作流脚本 |
|
| `WORKFLOW_SCRIPTS` | ✅ dev/build | 工作流脚本 |
|
||||||
| `FORK_SUBAGENT` | ✅ dev+build | 子 Agent |
|
| `FORK_SUBAGENT` | ✅ dev/build | 子 Agent |
|
||||||
| `KAIROS` | ✅ dev+build | Kairos 调度 |
|
| `KAIROS` | ✅ dev/build | Kairos 调度 |
|
||||||
| `COORDINATOR_MODE` | ✅ dev+build | 多 Worker |
|
| `COORDINATOR_MODE` | ✅ dev/build | 多 Worker |
|
||||||
| `HISTORY_SNIP` | ✅ dev+build | 历史管理 |
|
| `HISTORY_SNIP` | ✅ dev/build | 历史管理 |
|
||||||
| `CONTEXT_COLLAPSE` | ✅ 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:
|
手动启用任意 flag:
|
||||||
```bash
|
```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/bashClassifier.ts` | — | Bash 分类器(stub,ANT-ONLY) |
|
||||||
| `src/utils/permissions/yoloClassifier.ts` | 1496 | YOLO 分类器(完整参考实现) |
|
| `src/utils/permissions/yoloClassifier.ts` | 1496 | YOLO 分类器(完整参考实现) |
|
||||||
| `src/utils/classifierApprovals.ts` | — | 分类器审批信号管理 |
|
| `src/utils/classifierApprovals.ts` | — | 分类器审批信号管理 |
|
||||||
| `src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx` | — | 分类器 UI |
|
| `src/components/permissions/BashPermissionRequest.tsx:261-469` | — | 分类器 UI |
|
||||||
| `src/hooks/toolPermission/handlers/interactiveHandler.ts` | — | 交互式权限处理 |
|
| `src/hooks/toolPermission/handlers/interactiveHandler.ts` | — | 交互式权限处理 |
|
||||||
| `src/services/api/withRetry.ts` | — | API beta 标头 |
|
| `src/services/api/withRetry.ts:81` | — | API beta 标头 |
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ BRIDGE_MODE 将本地 CLI 注册为"bridge 环境",可从 claude.ai 或其他
|
|||||||
|
|
||||||
文件:`src/bridge/bridgeApi.ts`
|
文件:`src/bridge/bridgeApi.ts`
|
||||||
|
|
||||||
Bridge API Client 提供 9 个核心操作:
|
Bridge API Client 提供 7 个核心操作:
|
||||||
|
|
||||||
| 操作 | HTTP | 说明 |
|
| 操作 | HTTP | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
@@ -137,7 +137,7 @@ FEATURE_BRIDGE_MODE=1 FEATURE_DAEMON=1 bun run dev
|
|||||||
|
|
||||||
| 文件 | 行数 | 职责 |
|
| 文件 | 行数 | 职责 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `src/bridge/bridgeApi.ts` | 541 | API Client(核心) |
|
| `src/bridge/bridgeApi.ts` | 540 | API Client(核心) |
|
||||||
| `src/bridge/sessionRunner.ts` | — | 会话运行器 |
|
| `src/bridge/sessionRunner.ts` | — | 会话运行器 |
|
||||||
| `src/bridge/bridgeConfig.ts` | — | 配置管理 |
|
| `src/bridge/bridgeConfig.ts` | — | 配置管理 |
|
||||||
| `src/bridge/replBridgeTransport.ts` | — | 传输层 |
|
| `src/bridge/replBridgeTransport.ts` | — | 传输层 |
|
||||||
|
|||||||
@@ -78,13 +78,10 @@ FEATURE_BUDDY=1 bun run dev
|
|||||||
|
|
||||||
| 文件 | 说明 |
|
| 文件 | 说明 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/commands/buddy/index.ts` | `/buddy` 命令注册 |
|
|
||||||
| `src/commands/buddy/buddy.ts` | `/buddy` 命令处理 |
|
| `src/commands/buddy/buddy.ts` | `/buddy` 命令处理 |
|
||||||
| `src/buddy/companion.ts` | 宠物生成与加载 |
|
| `src/buddy/companion.ts` | 宠物生成与加载 |
|
||||||
| `src/buddy/companionReact.ts` | 宠物反应系统(REPL 每轮查询后触发) |
|
|
||||||
| `src/buddy/types.ts` | 类型定义(物种、稀有度、属性) |
|
| `src/buddy/types.ts` | 类型定义(物种、稀有度、属性) |
|
||||||
| `src/buddy/sprites.ts` | 终端像素画渲染 |
|
| `src/buddy/sprites.ts` | 终端像素画渲染 |
|
||||||
| `src/buddy/CompanionSprite.tsx` | React 组件(输入框旁显示) |
|
| `src/buddy/CompanionSprite.tsx` | React 组件(输入框旁显示) |
|
||||||
| `src/buddy/CompanionCard.tsx` | 宠物信息卡片(`/buddy` 无参数时展示) |
|
|
||||||
| `src/buddy/useBuddyNotification.tsx` | 启动提示通知 |
|
| `src/buddy/useBuddyNotification.tsx` | 启动提示通知 |
|
||||||
| `src/buddy/prompt.ts` | 宠物相关 prompt 模板 |
|
| `src/buddy/prompt.ts` | 宠物相关 prompt 模板 |
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
# 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 插件,支持双向消息、配对认证、群组聊天、文件附件
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 启用频道监听(plugin 格式)
|
|
||||||
ccb --channels plugin:feishu@claude-code-feishu-channel
|
|
||||||
|
|
||||||
# 启用频道监听(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` |
|
|
||||||
|
|
||||||
## 相关文件
|
|
||||||
|
|
||||||
| 文件 | 职责 |
|
|
||||||
|------|------|
|
|
||||||
| `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 提供 38 个工具,分为三类:
|
Computer Use 提供 37 个工具,分为三类:
|
||||||
|
|
||||||
| 分类 | 平台 | 工具数 | 说明 |
|
| 分类 | 平台 | 工具数 | 说明 |
|
||||||
|------|------|--------|------|
|
|------|------|--------|------|
|
||||||
| 通用工具 | 全平台 | 24 | 官方 Computer Use 标准能力 |
|
| 通用工具 | 全平台 | 24 | 官方 Computer Use 标准能力 |
|
||||||
| Windows 专属工具 | Win32 | 11 | 绑定窗口模式下的增强能力 |
|
| Windows 专属工具 | Win32 | 10 | 绑定窗口模式下的增强能力 |
|
||||||
| 教学工具 | 全平台 | 3 | 分步引导模式(需 teachMode 开启) |
|
| 教学工具 | 全平台 | 3 | 分步引导模式(需 teachMode 开启) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -82,7 +82,7 @@ Computer Use 提供 38 个工具,分为三类:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 二、Windows 专属工具(12 个)
|
## 二、Windows 专属工具(10 个)
|
||||||
|
|
||||||
仅 Windows 平台可见。核心能力:**绑定窗口后的独立操作——不抢占用户鼠标键盘**。
|
仅 Windows 平台可见。核心能力:**绑定窗口后的独立操作——不抢占用户鼠标键盘**。
|
||||||
|
|
||||||
@@ -235,19 +235,8 @@ Computer Use 提供 38 个工具,分为三类:
|
|||||||
|
|
||||||
| 工具 | 参数 | 说明 |
|
| 工具 | 参数 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `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/选择提示 |
|
| `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 详情:**
|
||||||
|
|
||||||
| response_type | 操作 | 场景 |
|
| response_type | 操作 | 场景 |
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
- ✅ `@ant/computer-use-input` 拆为 dispatcher + backends(darwin + win32)
|
- ✅ `@ant/computer-use-input` 拆为 dispatcher + backends(darwin + win32)
|
||||||
- ✅ `@ant/computer-use-swift` 拆为 dispatcher + backends(darwin + win32)
|
- ✅ `@ant/computer-use-swift` 拆为 dispatcher + backends(darwin + win32)
|
||||||
- ✅ `CHICAGO_MCP` 编译开关已开
|
- ✅ `CHICAGO_MCP` 编译开关已开
|
||||||
- ✅ `src/` 层 macOS 硬编码已移除(Phase 2 已完成)
|
- ❌ `src/` 层有 6 处 macOS 硬编码阻塞
|
||||||
|
|
||||||
## 2. 阻塞点全景
|
## 2. 阻塞点全景
|
||||||
|
|
||||||
@@ -19,25 +19,25 @@
|
|||||||
|
|
||||||
| # | 文件:行号 | 阻塞代码 | 影响 |
|
| # | 文件:行号 | 阻塞代码 | 影响 |
|
||||||
|---|----------|---------|------|
|
|---|----------|---------|------|
|
||||||
| 1 | `src/main.tsx:2366` | `feature("CHICAGO_MCP")` 门控 | CU 初始化入口 |
|
| 1 | `src/main.tsx:1605` | `getPlatform() === 'macos'` | 整个 CU 初始化被跳过 |
|
||||||
|
|
||||||
### 2.2 加载层
|
### 2.2 加载层
|
||||||
|
|
||||||
| # | 文件:行号 | 阻塞代码 | 影响 |
|
| # | 文件:行号 | 阻塞代码 | 影响 |
|
||||||
|---|----------|---------|------|
|
|---|----------|---------|------|
|
||||||
| 2 | `src/utils/computerUse/swiftLoader.ts` | macOS-only loader(已改为仅 darwin 加载) | 非 darwin 使用 platforms/ 替代 |
|
| 2 | `src/utils/computerUse/swiftLoader.ts:16` | `process.platform !== 'darwin'` → throw | 截图、应用管理全部不可用 |
|
||||||
| 3 | `src/utils/computerUse/executor.ts:302` | `process.platform !== 'darwin'` → cross-platform executor | 非 darwin 走跨平台路径 |
|
| 3 | `src/utils/computerUse/executor.ts:263` | `process.platform !== 'darwin'` → throw | 整个 executor 工厂函数不可用 |
|
||||||
|
|
||||||
### 2.3 macOS 特有依赖
|
### 2.3 macOS 特有依赖
|
||||||
|
|
||||||
| # | 文件:行号 | 依赖 | macOS 实现 | 需要替代方案 |
|
| # | 文件:行号 | 依赖 | macOS 实现 | 需要替代方案 |
|
||||||
|---|----------|------|-----------|------------|
|
|---|----------|------|-----------|------------|
|
||||||
| 4 | `executor.ts:72-96` | 剪贴板 | `pbcopy`/`pbpaste` / PowerShell / xclip | Win: PowerShell `Get/Set-Clipboard`;Linux: `xclip`/`wl-copy` |
|
| 4 | `executor.ts:70-88` | 剪贴板 | `pbcopy`/`pbpaste` | Win: PowerShell `Get/Set-Clipboard`;Linux: `xclip`/`wl-copy` |
|
||||||
| 5 | `drainRunLoop.ts` | CFRunLoop pump | `cu._drainMainRunLoop()` | 非 darwin:直接执行 fn(),不需要 pump |
|
| 5 | `drainRunLoop.ts:21` | CFRunLoop pump | `cu._drainMainRunLoop()` | 非 darwin:直接执行 fn(),不需要 pump |
|
||||||
| 6 | `escHotkey.ts` | ESC 热键 | CGEventTap | 非 darwin:返回 false(已有 Ctrl+C fallback) |
|
| 6 | `escHotkey.ts:28` | ESC 热键 | CGEventTap | 非 darwin:返回 false(已有 Ctrl+C fallback) |
|
||||||
| 7 | `hostAdapter.ts` | 系统权限 | TCC accessibility + screenRecording | Win:直接 granted;Linux:检查 xdotool |
|
| 7 | `hostAdapter.ts:48-54` | 系统权限 | TCC accessibility + screenRecording | Win:直接 granted;Linux:检查 xdotool |
|
||||||
| 8 | `common.ts:55-58` | 平台标识 | 动态获取 | 已改为 `process.platform` 分发 |
|
| 8 | `common.ts:56` | 平台标识 | `platform: 'darwin'` 硬编码 | 动态获取 |
|
||||||
| 9 | `executor.ts:232` | 粘贴快捷键 | `command`/`ctrl` 分发 | 已按平台分发粘贴快捷键 |
|
| 9 | `executor.ts:180` | 粘贴快捷键 | `command+v` | Win/Linux:`ctrl+v` |
|
||||||
|
|
||||||
### 2.4 缺失的 Linux 后端
|
### 2.4 缺失的 Linux 后端
|
||||||
|
|
||||||
@@ -100,19 +100,19 @@
|
|||||||
|
|
||||||
| 步骤 | 文件 | 改动 |
|
| 步骤 | 文件 | 改动 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 2.1 | `src/main.tsx:2366` | `feature("CHICAGO_MCP")` → 已为跨平台入口 |
|
| 2.1 | `src/main.tsx:1605` | `getPlatform() === 'macos'` → 去掉平台限制,或改为 `!== 'unknown'` |
|
||||||
| 2.2 | `src/utils/computerUse/swiftLoader.ts` | 已改为仅 darwin 加载,非 darwin 使用 platforms/ |
|
| 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:302-309` | 已改为 cross-platform dispatch(非 darwin → createCrossPlatformExecutor) |
|
| 2.3 | `src/utils/computerUse/executor.ts:263-267` | 移除 `process.platform !== 'darwin'` throw。改为检查 input/swift isSupported |
|
||||||
| 2.4 | `src/utils/computerUse/executor.ts:72-96` | 剪贴板已按平台分发:darwin→pbcopy/pbpaste,win32→PowerShell,linux→xclip |
|
| 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:232` | 粘贴快捷键已按平台分发:darwin→command,其他→ctrl |
|
| 2.5 | `src/utils/computerUse/executor.ts:180` | `typeViaClipboard` 中 `command+v` → 非 darwin 时用 `ctrl+v` |
|
||||||
| 2.6 | `src/utils/computerUse/executor.ts:302-309` | 非 darwin 已改为 `createCrossPlatformExecutor()` |
|
| 2.6 | `src/utils/computerUse/executor.ts:273` | `const cu = requireComputerUseSwift()` → 改为 `new ComputerUseAPI()`(从 package 直接实例化,不走 swiftLoader throw) |
|
||||||
| 2.7 | `src/utils/computerUse/drainRunLoop.ts` | 非 darwin 无需 pump(直接执行 fn) |
|
| 2.7 | `src/utils/computerUse/drainRunLoop.ts` | 开头加 `if (process.platform !== 'darwin') return fn()` |
|
||||||
| 2.8 | `src/utils/computerUse/escHotkey.ts` | 非 darwin 返回 false(已有 Ctrl+C fallback) |
|
| 2.8 | `src/utils/computerUse/escHotkey.ts` | `registerEscHotkey` 非 darwin 返回 false(已有 Ctrl+C fallback) |
|
||||||
| 2.9 | `src/utils/computerUse/hostAdapter.ts` | 非 darwin 权限检查逻辑已实现 |
|
| 2.9 | `src/utils/computerUse/hostAdapter.ts:48-54` | `ensureOsPermissions` 非 darwin 返回 `{ granted: true }` |
|
||||||
| 2.10 | `src/utils/computerUse/common.ts:58` | 已改为动态 `process.platform` 分发 |
|
| 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` | 已改为 darwin→'native',其他→'none' |
|
| 2.11 | `src/utils/computerUse/common.ts:55` | `screenshotFiltering: 'native'` → 非 darwin 时 `'none'`(Windows/Linux 截图不支持 per-app 过滤) |
|
||||||
| 2.12 | `src/utils/computerUse/gates.ts:55` | 已更新(需验证 enabled 默认值) |
|
| 2.12 | `src/utils/computerUse/gates.ts:13` | `enabled: false` → `enabled: true`(无 GrowthBook 时默认可用) |
|
||||||
| 2.13 | `src/utils/computerUse/gates.ts:39` | `hasRequiredSubscription()` 已更新 |
|
| 2.13 | `src/utils/computerUse/gates.ts:39-43` | `hasRequiredSubscription()` → 直接返回 `true` |
|
||||||
|
|
||||||
### Phase 3:新增 Linux 后端
|
### Phase 3:新增 Linux 后端
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ CONTEXT_COLLAPSE 让模型内省上下文窗口使用情况,并智能压缩旧
|
|||||||
| 折叠核心 | `src/services/contextCollapse/index.ts` | **Stub** — 接口完整(`ContextCollapseStats`、`CollapseResult`、`DrainResult`),函数全部空操作 |
|
| 折叠核心 | `src/services/contextCollapse/index.ts` | **Stub** — 接口完整(`ContextCollapseStats`、`CollapseResult`、`DrainResult`),函数全部空操作 |
|
||||||
| 折叠操作 | `src/services/contextCollapse/operations.ts` | **Stub** — `projectView` 为恒等函数 |
|
| 折叠操作 | `src/services/contextCollapse/operations.ts` | **Stub** — `projectView` 为恒等函数 |
|
||||||
| 折叠持久化 | `src/services/contextCollapse/persist.ts` | **Stub** — `restoreFromEntries` 为空操作 |
|
| 折叠持久化 | `src/services/contextCollapse/persist.ts` | **Stub** — `restoreFromEntries` 为空操作 |
|
||||||
| CtxInspectTool | `packages/builtin-tools/src/tools/CtxInspectTool/CtxInspectTool.ts` | **实现** — 上下文内省工具 |
|
| CtxInspectTool | `src/tools/CtxInspectTool/` | **缺失** — 目录不存在 |
|
||||||
| SnipTool 提示 | `src/tools/SnipTool/prompt.ts` | **Stub** — 空工具名 |
|
| SnipTool 提示 | `src/tools/SnipTool/prompt.ts` | **Stub** — 空工具名 |
|
||||||
| SnipTool 实现 | `src/tools/SnipTool/SnipTool.ts` | **缺失** |
|
| SnipTool 实现 | `src/tools/SnipTool/SnipTool.ts` | **缺失** |
|
||||||
| force-snip 命令 | `src/commands/force-snip.js` | **缺失** |
|
| force-snip 命令 | `src/commands/force-snip.js` | **缺失** |
|
||||||
@@ -106,7 +106,7 @@ SnipTool 提供手动折叠能力:
|
|||||||
| 1 | `services/contextCollapse/index.ts` | 大 | 折叠状态机、LLM 调用、消息压缩 |
|
| 1 | `services/contextCollapse/index.ts` | 大 | 折叠状态机、LLM 调用、消息压缩 |
|
||||||
| 2 | `services/contextCollapse/operations.ts` | 中 | `projectView()` 消息过滤 |
|
| 2 | `services/contextCollapse/operations.ts` | 中 | `projectView()` 消息过滤 |
|
||||||
| 3 | `services/contextCollapse/persist.ts` | 小 | `restoreFromEntries()` 磁盘持久化 |
|
| 3 | `services/contextCollapse/persist.ts` | 小 | `restoreFromEntries()` 磁盘持久化 |
|
||||||
| 4 | `tools/CtxInspectTool/` | 已完成 | 上下文内省工具已实现(`packages/builtin-tools/src/tools/CtxInspectTool/`) |
|
| 4 | `tools/CtxInspectTool/` | 中 | 上下文内省工具(token 计数、已折叠范围) |
|
||||||
| 5 | `tools/SnipTool/SnipTool.ts` | 中 | Snip 工具实现 |
|
| 5 | `tools/SnipTool/SnipTool.ts` | 中 | Snip 工具实现 |
|
||||||
| 6 | `commands/force-snip.js` | 小 | `/force-snip` 命令 |
|
| 6 | `commands/force-snip.js` | 小 | `/force-snip` 命令 |
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# DAEMON — 后台守护进程
|
# DAEMON — 后台守护进程
|
||||||
|
|
||||||
> Feature Flag: `FEATURE_DAEMON=1`
|
> Feature Flag: `FEATURE_DAEMON=1`
|
||||||
> 实现状态:Supervisor 和 remoteControl Worker 已实现
|
> 实现状态:主进程和 worker 注册为 Stub,CLI 路由完整
|
||||||
> 引用数:3
|
> 引用数:3
|
||||||
|
|
||||||
## 一、功能概述
|
## 一、功能概述
|
||||||
|
|
||||||
DAEMON 将 Claude Code 变为后台守护进程。主进程(supervisor)管理多个 worker 子进程的生命周期,通过文件系统状态文件进行通信。适用于持续运行的后台服务场景(如配合 BRIDGE_MODE 提供远程控制服务)。
|
DAEMON 将 Claude Code 变为后台守护进程。主进程(supervisor)管理多个 worker 进程的生命周期,通过 Unix 域套接字进行 IPC。适用于持续运行的后台服务场景(如配合 BRIDGE_MODE 提供远程控制服务)。
|
||||||
|
|
||||||
## 二、实现架构
|
## 二、实现架构
|
||||||
|
|
||||||
@@ -14,9 +14,8 @@ DAEMON 将 Claude Code 变为后台守护进程。主进程(supervisor)管
|
|||||||
|
|
||||||
| 模块 | 文件 | 状态 |
|
| 模块 | 文件 | 状态 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 守护主进程 | `src/daemon/main.ts` | **已实现** — Supervisor 含子命令、Worker 生命周期管理、指数退避重启 |
|
| 守护主进程 | `src/daemon/main.ts` | **Stub** — `daemonMain: () => Promise.resolve()` |
|
||||||
| Worker 注册 | `src/daemon/workerRegistry.ts` | **已实现** — remoteControl Worker(headless bridge) |
|
| Worker 注册 | `src/daemon/workerRegistry.ts` | **Stub** — `runDaemonWorker: () => Promise.resolve()` |
|
||||||
| Daemon 状态 | `src/daemon/state.ts` | **已实现** — PID/状态文件的读写与查询 |
|
|
||||||
| CLI 路由 | `src/entrypoints/cli.tsx` | **布线** — `--daemon-worker` 和 `daemon` 子命令 |
|
| CLI 路由 | `src/entrypoints/cli.tsx` | **布线** — `--daemon-worker` 和 `daemon` 子命令 |
|
||||||
| 命令注册 | `src/commands.ts` | **布线** — DAEMON + BRIDGE_MODE 门控 |
|
| 命令注册 | `src/commands.ts` | **布线** — DAEMON + BRIDGE_MODE 门控 |
|
||||||
|
|
||||||
@@ -24,49 +23,34 @@ DAEMON 将 Claude Code 变为后台守护进程。主进程(supervisor)管
|
|||||||
|
|
||||||
```
|
```
|
||||||
# 启动守护进程
|
# 启动守护进程
|
||||||
claude daemon start
|
claude daemon
|
||||||
|
|
||||||
# 查看状态(默认子命令)
|
# 以 worker 身份启动
|
||||||
claude daemon status
|
claude --daemon-worker=<kind>
|
||||||
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)
|
Supervisor (daemonMain)
|
||||||
│
|
│
|
||||||
├── Worker: remoteControl
|
├── Worker 1: assistant-mode
|
||||||
│ └── runBridgeHeadless() — 远程控制 headless 模式
|
│ └── 接收和处理 assistant 会话
|
||||||
│ 接收远程会话、处理消息、权限审批
|
│
|
||||||
|
├── Worker 2: bridge-sync
|
||||||
|
│ └── bridge 消息同步
|
||||||
|
│
|
||||||
|
└── Worker 3: proactive
|
||||||
|
└── 主动任务执行
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
文件系统状态文件 (daemon-state.json)
|
IPC via Unix Domain Sockets
|
||||||
- PID、CWD、启动时间、Worker 类型
|
- 生命周期管理(启动、停止、重启)
|
||||||
- queryDaemonStatus() / stopDaemonByPid()
|
- 工作分发
|
||||||
|
- 状态报告
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.4 Worker 生命周期管理
|
### 2.4 与 BRIDGE_MODE 的关系
|
||||||
|
|
||||||
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 常组合使用:
|
DAEMON 和 BRIDGE_MODE 常组合使用:
|
||||||
|
|
||||||
@@ -79,39 +63,40 @@ if (feature('DAEMON') && feature('BRIDGE_MODE')) {
|
|||||||
|
|
||||||
双重门控:两个 feature 都需要开启才能使用远程控制服务器。
|
双重门控:两个 feature 都需要开启才能使用远程控制服务器。
|
||||||
|
|
||||||
## 三、关键设计决策
|
## 三、需要补全的内容
|
||||||
|
|
||||||
|
| 模块 | 工作量 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `daemon/main.ts` | 大 | Supervisor 主进程:启动 worker、生命周期管理、IPC |
|
||||||
|
| `daemon/workerRegistry.ts` | 中 | Worker 类型分发(assistant/bridge-sync/proactive) |
|
||||||
|
| Worker 实现 | 大 | 各类型 worker 的具体实现 |
|
||||||
|
| IPC 协议 | 中 | Supervisor-Worker 通信层 |
|
||||||
|
|
||||||
|
## 四、关键设计决策
|
||||||
|
|
||||||
1. **多进程架构**:一个 supervisor + 多个 worker,进程隔离
|
1. **多进程架构**:一个 supervisor + 多个 worker,进程隔离
|
||||||
2. **文件系统状态通信**:通过 `daemon-state.json` 文件进行状态共享(非 Unix 域套接字)
|
2. **Unix 域套接字 IPC**:本地进程间通信,低延迟
|
||||||
3. **与 BRIDGE_MODE 强绑定**:守护进程最常见的用途是提供远程控制服务
|
3. **与 BRIDGE_MODE 强绑定**:守护进程最常见的用途是提供远程控制服务
|
||||||
4. **CLI 子命令路由**:`daemon` 子命令和 `--daemon-worker` 参数在 `cli.tsx` 中路由
|
4. **CLI 子命令路由**:`daemon` 子命令和 `--daemon-worker` 参数在 `cli.tsx` 中路由
|
||||||
5. **Worker 环境变量**:supervisor 通过环境变量(`DAEMON_WORKER_*`)向 worker 传递配置
|
|
||||||
|
|
||||||
## 四、使用方式
|
## 五、使用方式
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 启用守护进程模式
|
# 启用守护进程模式
|
||||||
FEATURE_DAEMON=1 FEATURE_BRIDGE_MODE=1 bun run dev
|
FEATURE_DAEMON=1 FEATURE_BRIDGE_MODE=1 bun run dev
|
||||||
|
|
||||||
# 启动守护进程
|
# 启动守护进程
|
||||||
claude daemon start
|
claude daemon
|
||||||
|
|
||||||
# 查看状态
|
# 以特定 worker 启动
|
||||||
claude daemon status
|
claude --daemon-worker=assistant
|
||||||
|
|
||||||
# 停止守护进程
|
|
||||||
claude daemon stop
|
|
||||||
|
|
||||||
# 以特定 worker 启动(通常由 supervisor 自动调用)
|
|
||||||
claude --daemon-worker=remoteControl
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 五、文件索引
|
## 六、文件索引
|
||||||
|
|
||||||
| 文件 | 职责 |
|
| 文件 | 职责 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `src/daemon/main.ts` | Supervisor 主进程:子命令分发、Worker 生命周期管理、退避重启 |
|
| `src/daemon/main.ts` | Supervisor 主进程(stub) |
|
||||||
| `src/daemon/workerRegistry.ts` | Worker 入口:remoteControl worker 实现 |
|
| `src/daemon/workerRegistry.ts` | Worker 注册(stub) |
|
||||||
| `src/daemon/state.ts` | Daemon 状态管理:PID 文件读写、状态查询 |
|
| `src/entrypoints/cli.tsx:95,149` | CLI 路由 |
|
||||||
| `src/entrypoints/cli.tsx` | CLI 路由 |
|
| `src/commands.ts:77` | 命令注册(双重门控) |
|
||||||
| `src/commands.ts` | 命令注册(双重门控) |
|
|
||||||
|
|||||||
@@ -27,15 +27,13 @@ bun run dev:inspect
|
|||||||
|
|
||||||
## 原理
|
## 原理
|
||||||
|
|
||||||
`dev:inspect` 脚本实际执行的是 `scripts/dev-debug.ts`:
|
`dev:inspect` 脚本实际执行的是:
|
||||||
|
|
||||||
```typescript
|
```bash
|
||||||
// scripts/dev-debug.ts
|
bun --inspect-wait=localhost:8888/<token> run scripts/dev.ts
|
||||||
process.env.BUN_INSPECT = "localhost:8888/<token>"
|
|
||||||
await import("./dev")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
通过设置 `BUN_INSPECT` 环境变量启动一个 Chrome DevTools Protocol 兼容的 inspect 服务,然后导入 dev 模式入口。VS Code 的 `bun` 扩展通过 WebSocket 连接到输出的地址实现 attach。
|
Bun 的 `--inspect-wait` 参数启动一个 Chrome DevTools Protocol 兼容的 inspect 服务,等待调试器连接后才开始执行。VS Code 的 `bun` 扩展通过 WebSocket 连接到这个地址实现 attach。
|
||||||
|
|
||||||
## JetBrains IDE
|
## 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 |
|
| 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 |
|
| 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 | 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 |
|
| 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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -51,31 +51,14 @@ Claude Code 使用三层门控系统:
|
|||||||
| SHOT_STATS | **ON** | **ON** | compile-only, 已验证 | 纯本地统计 |
|
| SHOT_STATS | **ON** | **ON** | compile-only, 已验证 | 纯本地统计 |
|
||||||
| PROMPT_CACHE_BREAK_DETECTION | **ON** | **ON** | compile-only, 已验证 | 内部诊断 |
|
| PROMPT_CACHE_BREAK_DETECTION | **ON** | **ON** | compile-only, 已验证 | 内部诊断 |
|
||||||
| TOKEN_BUDGET | **ON** | **ON** | compile-only, 已验证 | 支持 `+500k` 语法 |
|
| TOKEN_BUDGET | **ON** | **ON** | compile-only, 已验证 | 支持 `+500k` 语法 |
|
||||||
| AGENT_TRIGGERS | **ON** | **ON** | compile+GB gate, 已验证 | 本地定时任务系统 |
|
| AGENT_TRIGGERS | **ON** | **ON** | compile+GB gate, 已验证 | 本轮新增,定时任务系统 |
|
||||||
| ULTRATHINK | **ON** | **ON** | compile-only | 扩展思考模式 |
|
| EXTRACT_MEMORIES | **ON** | **ON** | compile+GB gate, 已验证 | 本轮新增,自动记忆提取 |
|
||||||
| BUILTIN_EXPLORE_PLAN_AGENTS | **ON** | **ON** | compile-only | 内置 Explore/Plan agent |
|
| VERIFICATION_AGENT | **ON** | **ON** | compile+GB gate, 已验证 | 本轮新增,对抗性验证代理 |
|
||||||
| LODESTONE | **ON** | **ON** | compile-only | 深度链接 URL 协议 |
|
| KAIROS_BRIEF | **ON** | **ON** | compile+GB gate, 已验证 | 本轮新增,Brief 精简模式 |
|
||||||
| EXTRACT_MEMORIES | **ON** | **ON** | compile+GB gate, 已验证 | 自动记忆提取 |
|
| AWAY_SUMMARY | **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 模式 |
|
| BUDDY | off | **ON** | compile+GrowthBook | 仅 dev 模式 |
|
||||||
| TRANSCRIPT_CLASSIFIER | off | **ON** | compile+GrowthBook | 仅 dev 模式 |
|
| TRANSCRIPT_CLASSIFIER | off | **ON** | compile+GrowthBook | 仅 dev 模式 |
|
||||||
| BRIDGE_MODE | off | **ON** | compile+remote | 仅 dev 模式,需 claude.ai 订阅 |
|
| 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 模式 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -141,9 +124,9 @@ Claude Code 使用三层门控系统:
|
|||||||
8. src/hooks/useReplBridge.tsx — REPL 桥接 Hook
|
8. src/hooks/useReplBridge.tsx — REPL 桥接 Hook
|
||||||
9. src/main.tsx — 主入口中的桥接模式启动
|
9. src/main.tsx — 主入口中的桥接模式启动
|
||||||
10. src/screens/REPL.tsx — REPL 屏幕中的桥接集成
|
10. src/screens/REPL.tsx — REPL 屏幕中的桥接集成
|
||||||
11. packages/builtin-tools/src/tools/BriefTool/attachments.ts — Brief 工具附件处理
|
11. src/tools/BriefTool/attachments.ts — Brief 工具附件处理
|
||||||
12. packages/builtin-tools/src/tools/BriefTool/upload.ts — Brief 工具上传
|
12. src/tools/BriefTool/upload.ts — Brief 工具上传
|
||||||
13. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts — 配置工具中的桥接设置
|
13. src/tools/ConfigTool/supportedSettings.ts — 配置工具中的桥接设置
|
||||||
|
|
||||||
**启用所需操作**: 仅需将编译标志 `BRIDGE_MODE` 设为 `true`。所有代码完整,命令入口 `src/commands/bridge/index.ts`(604 行)和 `src/commands/bridge/bridge.tsx`(46,907 行)均存在。
|
**启用所需操作**: 仅需将编译标志 `BRIDGE_MODE` 设为 `true`。所有代码完整,命令入口 `src/commands/bridge/index.ts`(604 行)和 `src/commands/bridge/bridge.tsx`(46,907 行)均存在。
|
||||||
|
|
||||||
@@ -202,8 +185,8 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
7. src/screens/REPL.tsx — REPL 屏幕中的协调器集成
|
7. src/screens/REPL.tsx — REPL 屏幕中的协调器集成
|
||||||
8. src/screens/ResumeConversation.tsx — 恢复对话时的协调器处理
|
8. src/screens/ResumeConversation.tsx — 恢复对话时的协调器处理
|
||||||
9. src/tools.ts — 工具注册中的协调器工具
|
9. src/tools.ts — 工具注册中的协调器工具
|
||||||
10. packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx — Agent 工具中的协调器模式分支
|
10. src/tools/AgentTool/AgentTool.tsx — Agent 工具中的协调器模式分支
|
||||||
11. packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts — 内置代理定义
|
11. src/tools/AgentTool/builtInAgents.ts — 内置代理定义
|
||||||
12. src/utils/processUserInput/processSlashCommand.tsx — 斜杠命令处理中的协调器
|
12. src/utils/processUserInput/processSlashCommand.tsx — 斜杠命令处理中的协调器
|
||||||
13. src/utils/sessionRestore.ts — 会话恢复中的协调器状态
|
13. src/utils/sessionRestore.ts — 会话恢复中的协调器状态
|
||||||
14. src/utils/systemPrompt.ts — 系统提示中的协调器指令
|
14. src/utils/systemPrompt.ts — 系统提示中的协调器指令
|
||||||
@@ -276,9 +259,9 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
9. src/screens/REPL.tsx — REPL 中的语音模式集成
|
9. src/screens/REPL.tsx — REPL 中的语音模式集成
|
||||||
10. src/services/voiceStreamSTT.ts — STT 服务
|
10. src/services/voiceStreamSTT.ts — STT 服务
|
||||||
11. src/state/AppState.tsx — 应用状态中的语音状态
|
11. src/state/AppState.tsx — 应用状态中的语音状态
|
||||||
12. packages/builtin-tools/src/tools/ConfigTool/ConfigTool.ts — 配置工具中的语音设置
|
12. src/tools/ConfigTool/ConfigTool.ts — 配置工具中的语音设置
|
||||||
13. packages/builtin-tools/src/tools/ConfigTool/prompt.ts — 配置工具提示
|
13. src/tools/ConfigTool/prompt.ts — 配置工具提示
|
||||||
14. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts — 支持的设置项
|
14. src/tools/ConfigTool/supportedSettings.ts — 支持的设置项
|
||||||
15. src/utils/settings/types.ts — 设置类型定义
|
15. src/utils/settings/types.ts — 设置类型定义
|
||||||
16. src/voice/voiceModeEnabled.ts — 语音模式启用逻辑
|
16. src/voice/voiceModeEnabled.ts — 语音模式启用逻辑
|
||||||
|
|
||||||
@@ -402,8 +385,8 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
11. src/hooks/toolPermission/permissionLogging.ts — 权限日志
|
11. src/hooks/toolPermission/permissionLogging.ts — 权限日志
|
||||||
12. src/hooks/useCanUseTool.tsx — 工具可用性检查
|
12. src/hooks/useCanUseTool.tsx — 工具可用性检查
|
||||||
13. src/services/api/withRetry.ts — API 重试中的分类器
|
13. src/services/api/withRetry.ts — API 重试中的分类器
|
||||||
14. packages/builtin-tools/src/tools/BashTool/bashPermissions.ts — Bash 权限逻辑
|
14. src/tools/BashTool/bashPermissions.ts — Bash 权限逻辑
|
||||||
15. packages/builtin-tools/src/tools/BashTool/pathValidation.ts — 路径验证
|
15. src/tools/BashTool/pathValidation.ts — 路径验证
|
||||||
16. src/utils/classifierApprovals.ts — 分类器审批记录
|
16. src/utils/classifierApprovals.ts — 分类器审批记录
|
||||||
17. src/utils/messages.ts — 消息处理
|
17. src/utils/messages.ts — 消息处理
|
||||||
18. src/utils/permissions/permissions.ts — 权限核心
|
18. src/utils/permissions/permissions.ts — 权限核心
|
||||||
@@ -448,11 +431,11 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
22. src/screens/REPL.tsx — REPL 屏幕
|
22. src/screens/REPL.tsx — REPL 屏幕
|
||||||
23. src/services/api/claude.ts — Claude API 服务
|
23. src/services/api/claude.ts — Claude API 服务
|
||||||
24. src/services/tools/toolExecution.ts — 工具执行
|
24. src/services/tools/toolExecution.ts — 工具执行
|
||||||
25. packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx — Agent 工具
|
25. src/tools/AgentTool/AgentTool.tsx — Agent 工具
|
||||||
26. packages/builtin-tools/src/tools/AgentTool/agentToolUtils.ts — Agent 工具工具函数
|
26. src/tools/AgentTool/agentToolUtils.ts — Agent 工具工具函数
|
||||||
27. packages/builtin-tools/src/tools/AgentTool/runAgent.ts — 运行 Agent
|
27. src/tools/AgentTool/runAgent.ts — 运行 Agent
|
||||||
28. packages/builtin-tools/src/tools/BashTool/bashPermissions.ts — Bash 权限
|
28. src/tools/BashTool/bashPermissions.ts — Bash 权限
|
||||||
29. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts — 支持的设置
|
29. src/tools/ConfigTool/supportedSettings.ts — 支持的设置
|
||||||
30. src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts — 退出计划模式工具
|
30. src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts — 退出计划模式工具
|
||||||
31. src/tools/NotebookEditTool/NotebookEditTool.ts — Notebook 编辑工具
|
31. src/tools/NotebookEditTool/NotebookEditTool.ts — Notebook 编辑工具
|
||||||
32. src/types/permissions.ts — 权限类型
|
32. src/types/permissions.ts — 权限类型
|
||||||
@@ -560,10 +543,11 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
|
|
||||||
| 文件路径 | 行数 | 功能说明 |
|
| 文件路径 | 行数 | 功能说明 |
|
||||||
|----------|------|----------|
|
|----------|------|----------|
|
||||||
| packages/builtin-tools/src/tools/ScheduleCronTool/CronCreateTool.ts | 157 行 | Cron 创建工具 |
|
| src/tools/ScheduleCronTool/CronCreateTool.ts | 157 行 | Cron 创建工具 |
|
||||||
| packages/builtin-tools/src/tools/ScheduleCronTool/prompt.ts | 135 行 | Cron 工具提示词 |
|
| src/tools/ScheduleCronTool/prompt.ts | 135 行 | Cron 工具提示词 |
|
||||||
| packages/builtin-tools/src/tools/ScheduleCronTool/CronListTool.ts | 97 行 | Cron 列表工具 |
|
| src/tools/ScheduleCronTool/CronListTool.ts | 97 行 | Cron 列表工具 |
|
||||||
| packages/builtin-tools/src/tools/ScheduleCronTool/CronDeleteTool.ts | 95 行 | Cron 删除工具 |
|
| src/tools/ScheduleCronTool/CronDeleteTool.ts | 95 行 | Cron 删除工具 |
|
||||||
|
| src/tools/ScheduleCronTool/UI.tsx | 59 行 | Cron UI 组件 |
|
||||||
|
|
||||||
**引用该标志的文件(6 个)**:
|
**引用该标志的文件(6 个)**:
|
||||||
1. src/cli/print.ts — CLI 输出
|
1. src/cli/print.ts — CLI 输出
|
||||||
@@ -614,7 +598,7 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
|
|
||||||
| 文件路径 | 行数 | 功能说明 |
|
| 文件路径 | 行数 | 功能说明 |
|
||||||
|----------|------|----------|
|
|----------|------|----------|
|
||||||
| packages/builtin-tools/src/tools/BriefTool/BriefTool.ts | 204 行 | Brief 工具核心 |
|
| src/tools/BriefTool/BriefTool.ts | 204 行 | Brief 工具核心 |
|
||||||
| src/commands/brief.ts | 130 行 | Brief 命令实现 |
|
| src/commands/brief.ts | 130 行 | Brief 命令实现 |
|
||||||
|
|
||||||
**引用该标志的文件(20 个)**:
|
**引用该标志的文件(20 个)**:
|
||||||
@@ -632,7 +616,7 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
12. src/hooks/useGlobalKeybindings.tsx — 全局键绑定
|
12. src/hooks/useGlobalKeybindings.tsx — 全局键绑定
|
||||||
13. src/keybindings/defaultBindings.ts — 默认键绑定
|
13. src/keybindings/defaultBindings.ts — 默认键绑定
|
||||||
14. src/main.tsx — 主入口
|
14. src/main.tsx — 主入口
|
||||||
15. packages/builtin-tools/src/tools/BriefTool/BriefTool.ts — Brief 工具
|
15. src/tools/BriefTool/BriefTool.ts — Brief 工具
|
||||||
16. src/tools/ToolSearchTool/prompt.ts — 工具搜索提示
|
16. src/tools/ToolSearchTool/prompt.ts — 工具搜索提示
|
||||||
17. src/utils/attachments.ts — 附件
|
17. src/utils/attachments.ts — 附件
|
||||||
18. src/utils/conversationRecovery.ts — 对话恢复
|
18. src/utils/conversationRecovery.ts — 对话恢复
|
||||||
@@ -734,7 +718,7 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
9. src/screens/REPL.tsx — REPL(多处引用,通过 require 加载 proactive 模块)
|
9. src/screens/REPL.tsx — REPL(多处引用,通过 require 加载 proactive 模块)
|
||||||
10. src/services/compact/prompt.ts — 压缩提示
|
10. src/services/compact/prompt.ts — 压缩提示
|
||||||
11. src/tools.ts — 工具注册
|
11. src/tools.ts — 工具注册
|
||||||
12. packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx — Agent 工具
|
12. src/tools/AgentTool/AgentTool.tsx — Agent 工具
|
||||||
13. src/utils/sessionStorage.ts — 会话存储
|
13. src/utils/sessionStorage.ts — 会话存储
|
||||||
14. src/utils/settings/types.ts — 设置类型
|
14. src/utils/settings/types.ts — 设置类型
|
||||||
15. src/utils/systemPrompt.ts — 系统提示
|
15. src/utils/systemPrompt.ts — 系统提示
|
||||||
@@ -787,11 +771,11 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
| 文件路径 | 行数 | 功能说明 |
|
| 文件路径 | 行数 | 功能说明 |
|
||||||
|----------|------|----------|
|
|----------|------|----------|
|
||||||
| src/tools/TaskUpdateTool/TaskUpdateTool.ts | 406 行 | 任务更新工具 |
|
| src/tools/TaskUpdateTool/TaskUpdateTool.ts | 406 行 | 任务更新工具 |
|
||||||
| packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts | 72 行 | 内置代理定义 |
|
| src/tools/AgentTool/builtInAgents.ts | 72 行 | 内置代理定义 |
|
||||||
|
|
||||||
**引用该标志的文件(4 个)**:
|
**引用该标志的文件(4 个)**:
|
||||||
1. src/constants/prompts.ts — 提示词
|
1. src/constants/prompts.ts — 提示词
|
||||||
2. packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts — 内置代理
|
2. src/tools/AgentTool/builtInAgents.ts — 内置代理
|
||||||
3. src/tools/TaskUpdateTool/TaskUpdateTool.ts — 任务更新工具
|
3. src/tools/TaskUpdateTool/TaskUpdateTool.ts — 任务更新工具
|
||||||
4. src/tools/TodoWriteTool/TodoWriteTool.ts — TodoWrite 工具
|
4. src/tools/TodoWriteTool/TodoWriteTool.ts — TodoWrite 工具
|
||||||
|
|
||||||
@@ -812,7 +796,7 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
3. src/services/compact/autoCompact.ts — 自动压缩
|
3. src/services/compact/autoCompact.ts — 自动压缩
|
||||||
4. src/services/compact/compact.ts — 压缩核心
|
4. src/services/compact/compact.ts — 压缩核心
|
||||||
5. src/services/compact/microCompact.ts — 微压缩
|
5. src/services/compact/microCompact.ts — 微压缩
|
||||||
6. packages/builtin-tools/src/tools/AgentTool/runAgent.ts — 运行 Agent
|
6. src/tools/AgentTool/runAgent.ts — 运行 Agent
|
||||||
|
|
||||||
**启用所需操作**: 仅需将编译标志 `PROMPT_CACHE_BREAK_DETECTION` 设为 `true`。
|
**启用所需操作**: 仅需将编译标志 `PROMPT_CACHE_BREAK_DETECTION` 设为 `true`。
|
||||||
|
|
||||||
@@ -872,11 +856,11 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
38. src/services/mcp/useManageMCPConnections.ts
|
38. src/services/mcp/useManageMCPConnections.ts
|
||||||
39. src/skills/bundled/index.ts
|
39. src/skills/bundled/index.ts
|
||||||
40. src/tools.ts
|
40. src/tools.ts
|
||||||
41. packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx
|
41. src/tools/AgentTool/AgentTool.tsx
|
||||||
42. src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx
|
42. src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx
|
||||||
43. packages/builtin-tools/src/tools/BashTool/BashTool.tsx
|
43. src/tools/BashTool/BashTool.tsx
|
||||||
44. packages/builtin-tools/src/tools/BriefTool/BriefTool.ts
|
44. src/tools/BriefTool/BriefTool.ts
|
||||||
45. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts
|
45. src/tools/ConfigTool/supportedSettings.ts
|
||||||
46. src/tools/EnterPlanModeTool/EnterPlanModeTool.ts
|
46. src/tools/EnterPlanModeTool/EnterPlanModeTool.ts
|
||||||
47. src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts
|
47. src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts
|
||||||
48. src/tools/PowerShellTool/PowerShellTool.tsx
|
48. src/tools/PowerShellTool/PowerShellTool.tsx
|
||||||
@@ -893,8 +877,8 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
59. src/utils/systemPrompt.ts
|
59. src/utils/systemPrompt.ts
|
||||||
|
|
||||||
**缺失文件**:
|
**缺失文件**:
|
||||||
- ~~src/commands/assistant/index.ts~~ — 已补全
|
- src/commands/assistant/index.ts — 完全缺失(src/commands.ts 第 69 行引用了 `commands/assistant/index.js`)
|
||||||
- ~~src/commands/assistant/gate.ts~~ — 已补全
|
- src/commands/assistant/gate.ts — 完全缺失
|
||||||
|
|
||||||
**启用所需修复**: 需要创建 `src/commands/assistant/` 目录及其 `index.ts` 和 `gate.ts` 文件。
|
**启用所需修复**: 需要创建 `src/commands/assistant/` 目录及其 `index.ts` 和 `gate.ts` 文件。
|
||||||
|
|
||||||
@@ -946,7 +930,7 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
| 文件路径 | 行数 | 功能说明 |
|
| 文件路径 | 行数 | 功能说明 |
|
||||||
|----------|------|----------|
|
|----------|------|----------|
|
||||||
| src/tasks/LocalShellTask/LocalShellTask.tsx | 522 行 | 本地 Shell 任务完整实现 |
|
| src/tasks/LocalShellTask/LocalShellTask.tsx | 522 行 | 本地 Shell 任务完整实现 |
|
||||||
| packages/builtin-tools/src/tools/MonitorTool/MonitorTool.ts | 1 行 | 监控工具(桩) |
|
| src/tools/MonitorTool/MonitorTool.ts | 1 行 | 监控工具(桩) |
|
||||||
| src/tasks/MonitorMcpTask/MonitorMcpTask.ts | 5 行 | MCP 监控任务(桩) |
|
| src/tasks/MonitorMcpTask/MonitorMcpTask.ts | 5 行 | MCP 监控任务(桩) |
|
||||||
| src/components/tasks/MonitorMcpDetailDialog.tsx | 3 行 | MCP 详情对话框(桩) |
|
| src/components/tasks/MonitorMcpDetailDialog.tsx | 3 行 | MCP 详情对话框(桩) |
|
||||||
| src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.tsx | 3 行 | 监控权限请求(桩) |
|
| src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.tsx | 3 行 | 监控权限请求(桩) |
|
||||||
@@ -957,12 +941,12 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
3. src/tasks.ts — 任务注册
|
3. src/tasks.ts — 任务注册
|
||||||
4. src/tasks/LocalShellTask/LocalShellTask.tsx — Shell 任务
|
4. src/tasks/LocalShellTask/LocalShellTask.tsx — Shell 任务
|
||||||
5. src/tools.ts — 工具注册
|
5. src/tools.ts — 工具注册
|
||||||
6. packages/builtin-tools/src/tools/AgentTool/runAgent.ts — Agent 运行
|
6. src/tools/AgentTool/runAgent.ts — Agent 运行
|
||||||
7. packages/builtin-tools/src/tools/BashTool/BashTool.tsx — Bash 工具
|
7. src/tools/BashTool/BashTool.tsx — Bash 工具
|
||||||
8. packages/builtin-tools/src/tools/BashTool/prompt.ts — Bash 提示
|
8. src/tools/BashTool/prompt.ts — Bash 提示
|
||||||
9. src/tools/PowerShellTool/PowerShellTool.tsx — PowerShell 工具
|
9. src/tools/PowerShellTool/PowerShellTool.tsx — PowerShell 工具
|
||||||
|
|
||||||
**启用所需修复**: 需要实现 `packages/builtin-tools/src/tools/MonitorTool/MonitorTool.ts`、`src/tasks/MonitorMcpTask/MonitorMcpTask.ts`、`src/components/tasks/MonitorMcpDetailDialog.tsx` 和 `src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.tsx`。
|
**启用所需修复**: 需要实现 `src/tools/MonitorTool/MonitorTool.ts`、`src/tasks/MonitorMcpTask/MonitorMcpTask.ts`、`src/components/tasks/MonitorMcpDetailDialog.tsx` 和 `src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.tsx`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1004,11 +988,10 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
| src/components/WorkflowMultiselectDialog.tsx | 127 行 | 工作流多选对话框(有内容) |
|
| src/components/WorkflowMultiselectDialog.tsx | 127 行 | 工作流多选对话框(有内容) |
|
||||||
| src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts | 5 行 | 本地工作流任务(桩) |
|
| src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts | 5 行 | 本地工作流任务(桩) |
|
||||||
| src/components/tasks/WorkflowDetailDialog.tsx | 3 行 | 工作流详情对话框(桩) |
|
| src/components/tasks/WorkflowDetailDialog.tsx | 3 行 | 工作流详情对话框(桩) |
|
||||||
| packages/builtin-tools/src/tools/WorkflowTool/WorkflowPermissionRequest.tsx | ~80 行 | 工作流权限请求组件 |
|
| src/tools/WorkflowTool/WorkflowPermissionRequest.tsx | 3 行 | 工作流权限请求(桩) |
|
||||||
| packages/builtin-tools/src/tools/WorkflowTool/createWorkflowCommand.ts | 41 行 | 创建工作流命令(已实现) |
|
| src/tools/WorkflowTool/createWorkflowCommand.ts | 3 行 | 创建工作流命令(桩) |
|
||||||
| packages/builtin-tools/src/tools/WorkflowTool/WorkflowTool.ts | 74 行 | 工作流工具(部分实现,call 需运行时) |
|
| src/tools/WorkflowTool/WorkflowTool.ts | 1 行 | 工作流工具(桩) |
|
||||||
| packages/builtin-tools/src/tools/WorkflowTool/constants.ts | ~10 行 | 常量定义 |
|
| src/tools/WorkflowTool/constants.ts | 1 行 | 常量(桩) |
|
||||||
| packages/builtin-tools/src/tools/WorkflowTool/bundled/index.ts | ~20 行 | 内置工作流初始化 |
|
|
||||||
|
|
||||||
**引用该标志的文件(7 个)**:
|
**引用该标志的文件(7 个)**:
|
||||||
1. src/commands.ts — 命令注册(引用 `commands/workflows/index.js`)
|
1. src/commands.ts — 命令注册(引用 `commands/workflows/index.js`)
|
||||||
@@ -1103,13 +1086,13 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
|
|
||||||
| 文件路径 | 行数 | 功能说明 |
|
| 文件路径 | 行数 | 功能说明 |
|
||||||
|----------|------|----------|
|
|----------|------|----------|
|
||||||
| packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts | 210 行 | 分叉子代理核心逻辑 |
|
| src/tools/AgentTool/forkSubagent.ts | 210 行 | 分叉子代理核心逻辑 |
|
||||||
|
|
||||||
**引用该标志的文件(5 个)**:
|
**引用该标志的文件(5 个)**:
|
||||||
1. src/commands.ts — 命令注册
|
1. src/commands.ts — 命令注册
|
||||||
2. src/commands/branch/index.ts — 分支命令入口
|
2. src/commands/branch/index.ts — 分支命令入口
|
||||||
3. src/components/messages/UserTextMessage.tsx — 用户消息
|
3. src/components/messages/UserTextMessage.tsx — 用户消息
|
||||||
4. packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts — 分叉逻辑
|
4. src/tools/AgentTool/forkSubagent.ts — 分叉逻辑
|
||||||
5. src/tools/ToolSearchTool/prompt.ts — 工具搜索提示
|
5. src/tools/ToolSearchTool/prompt.ts — 工具搜索提示
|
||||||
|
|
||||||
**缺失文件**:
|
**缺失文件**:
|
||||||
@@ -1133,7 +1116,7 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
4. src/query.ts — 查询
|
4. src/query.ts — 查询
|
||||||
5. src/services/compact/compact.ts — 压缩
|
5. src/services/compact/compact.ts — 压缩
|
||||||
6. src/services/mcp/useManageMCPConnections.ts — MCP 连接管理
|
6. src/services/mcp/useManageMCPConnections.ts — MCP 连接管理
|
||||||
7. packages/builtin-tools/src/tools/SkillTool/SkillTool.ts — 技能工具(1,108 行)
|
7. src/tools/SkillTool/SkillTool.ts — 技能工具(1,108 行)
|
||||||
8. src/utils/attachments.ts — 附件
|
8. src/utils/attachments.ts — 附件
|
||||||
9. src/utils/messages.ts — 消息
|
9. src/utils/messages.ts — 消息
|
||||||
|
|
||||||
@@ -1353,24 +1336,21 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
**引用文件**:
|
**引用文件**:
|
||||||
1. src/components/Settings/Config.tsx — 设置
|
1. src/components/Settings/Config.tsx — 设置
|
||||||
2. src/tools.ts — 工具注册
|
2. src/tools.ts — 工具注册
|
||||||
3. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts — 支持的设置
|
3. src/tools/ConfigTool/supportedSettings.ts — 支持的设置
|
||||||
**代码量**: 0 行专属代码,仅在设置中预留了开关位
|
**代码量**: 0 行专属代码,仅在设置中预留了开关位
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 45. DAEMON `[build: ON] [dev: ON]`
|
## 45. DAEMON
|
||||||
|
|
||||||
**编译时引用次数**: 3
|
**编译时引用次数**: 3
|
||||||
**功能描述**: 守护进程模式。允许 Claude Code 作为后台长驻 supervisor 进程运行,管理多个 worker。
|
**功能描述**: 守护进程模式。
|
||||||
**分类**: COMPLETE(已恢复)
|
**分类**: STUB
|
||||||
**核心实现文件**:
|
|
||||||
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 — 条件注册命令
|
1. src/commands.ts — 条件注册命令(与 BRIDGE_MODE 组合)
|
||||||
2. src/entrypoints/cli.tsx — CLI 入口中的 `--daemon-worker` 路径
|
2. src/entrypoints/cli.tsx — CLI 入口
|
||||||
**说明**: 已从 stub 恢复为完整实现,支持 `daemon start/status/stop` 子命令、exponential backoff、state file 持久化。
|
**代码量**: 0 行专属代码
|
||||||
|
**说明**: 在 commands.ts 中,`DAEMON` 与 `BRIDGE_MODE` 一起用于条件加载 `commands/remoteControlServer/index.js`,该文件不存在。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1441,7 +1421,7 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
**分类**: STUB
|
**分类**: STUB
|
||||||
**引用文件**:
|
**引用文件**:
|
||||||
1. src/main.tsx — 主入口
|
1. src/main.tsx — 主入口
|
||||||
2. packages/builtin-tools/src/tools/AgentTool/loadAgentsDir.ts — 加载代理目录
|
2. src/tools/AgentTool/loadAgentsDir.ts — 加载代理目录
|
||||||
**代码量**: 0 行专属代码
|
**代码量**: 0 行专属代码
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1476,7 +1456,7 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
**引用文件**:
|
**引用文件**:
|
||||||
1. src/components/ThemePicker.tsx — 主题选择器
|
1. src/components/ThemePicker.tsx — 主题选择器
|
||||||
2. src/components/design-system/ThemeProvider.tsx — 主题提供者
|
2. src/components/design-system/ThemeProvider.tsx — 主题提供者
|
||||||
3. packages/builtin-tools/src/tools/ConfigTool/supportedSettings.ts — 支持的设置
|
3. src/tools/ConfigTool/supportedSettings.ts — 支持的设置
|
||||||
**代码量**: 0 行专属代码
|
**代码量**: 0 行专属代码
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1518,7 +1498,7 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
**编译时引用次数**: 1
|
**编译时引用次数**: 1
|
||||||
**功能描述**: 内置探索和计划代理。
|
**功能描述**: 内置探索和计划代理。
|
||||||
**分类**: STUB
|
**分类**: STUB
|
||||||
**引用文件**: packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts — 内置代理定义
|
**引用文件**: src/tools/AgentTool/builtInAgents.ts — 内置代理定义
|
||||||
**代码量**: 0 行专属代码
|
**代码量**: 0 行专属代码
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1809,7 +1789,7 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
**功能描述**: Tree-sitter Bash 影子模式(并行运行 tree-sitter 和传统解析器进行对比)。
|
**功能描述**: Tree-sitter Bash 影子模式(并行运行 tree-sitter 和传统解析器进行对比)。
|
||||||
**分类**: STUB
|
**分类**: STUB
|
||||||
**引用文件**:
|
**引用文件**:
|
||||||
1. packages/builtin-tools/src/tools/BashTool/bashPermissions.ts — Bash 权限
|
1. src/tools/BashTool/bashPermissions.ts — Bash 权限
|
||||||
2. src/utils/bash/parser.ts — Bash 解析器
|
2. src/utils/bash/parser.ts — Bash 解析器
|
||||||
**代码量**: 0 行专属代码
|
**代码量**: 0 行专属代码
|
||||||
|
|
||||||
@@ -1883,16 +1863,16 @@ src/utils/swarm/ 目录(22 个文件):
|
|||||||
|
|
||||||
| 文件路径 | 行数 | 所属标志 |
|
| 文件路径 | 行数 | 所属标志 |
|
||||||
|----------|------|----------|
|
|----------|------|----------|
|
||||||
| packages/builtin-tools/src/tools/MonitorTool/MonitorTool.ts | 1 行 | MONITOR_TOOL |
|
| src/tools/MonitorTool/MonitorTool.ts | 1 行 | MONITOR_TOOL |
|
||||||
| packages/builtin-tools/src/tools/WorkflowTool/WorkflowTool.ts | 1 行 | WORKFLOW_SCRIPTS |
|
| src/tools/WorkflowTool/WorkflowTool.ts | 1 行 | WORKFLOW_SCRIPTS |
|
||||||
| packages/builtin-tools/src/tools/WorkflowTool/constants.ts | 1 行 | WORKFLOW_SCRIPTS |
|
| src/tools/WorkflowTool/constants.ts | 1 行 | WORKFLOW_SCRIPTS |
|
||||||
| src/tools/ReviewArtifactTool/ReviewArtifactTool.ts | 1 行 | REVIEW_ARTIFACT |
|
| src/tools/ReviewArtifactTool/ReviewArtifactTool.ts | 1 行 | REVIEW_ARTIFACT |
|
||||||
| src/utils/udsMessaging.ts | 已实现 | UDS_INBOX |
|
| src/utils/udsMessaging.ts | 1 行 | UDS_INBOX |
|
||||||
| src/utils/udsClient.ts | 已实现 | UDS_INBOX |
|
| src/utils/udsClient.ts | 3 行 | UDS_INBOX |
|
||||||
| src/skills/mcpSkills.ts | 3 行 | MCP_SKILLS |
|
| src/skills/mcpSkills.ts | 3 行 | MCP_SKILLS |
|
||||||
| src/tools/WebBrowserTool/WebBrowserPanel.tsx | 3 行 | WEB_BROWSER_TOOL |
|
| src/tools/WebBrowserTool/WebBrowserPanel.tsx | 3 行 | WEB_BROWSER_TOOL |
|
||||||
| packages/builtin-tools/src/tools/WorkflowTool/createWorkflowCommand.ts | 3 行 | WORKFLOW_SCRIPTS |
|
| src/tools/WorkflowTool/createWorkflowCommand.ts | 3 行 | WORKFLOW_SCRIPTS |
|
||||||
| packages/builtin-tools/src/tools/WorkflowTool/WorkflowPermissionRequest.tsx | 3 行 | WORKFLOW_SCRIPTS |
|
| src/tools/WorkflowTool/WorkflowPermissionRequest.tsx | 3 行 | WORKFLOW_SCRIPTS |
|
||||||
| src/components/tasks/WorkflowDetailDialog.tsx | 3 行 | WORKFLOW_SCRIPTS |
|
| src/components/tasks/WorkflowDetailDialog.tsx | 3 行 | WORKFLOW_SCRIPTS |
|
||||||
| src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.tsx | 3 行 | MONITOR_TOOL |
|
| src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.tsx | 3 行 | MONITOR_TOOL |
|
||||||
| src/components/tasks/MonitorMcpDetailDialog.tsx | 3 行 | MONITOR_TOOL |
|
| src/components/tasks/MonitorMcpDetailDialog.tsx | 3 行 | MONITOR_TOOL |
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Agent({ subagent_type: "general-purpose", prompt: "..." })
|
|||||||
|
|
||||||
### 3.1 门控与互斥
|
### 3.1 门控与互斥
|
||||||
|
|
||||||
文件:`packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts:32-39`
|
文件:`src/tools/AgentTool/forkSubagent.ts:32-39`
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export function isForkSubagentEnabled(): boolean {
|
export function isForkSubagentEnabled(): boolean {
|
||||||
@@ -105,7 +105,7 @@ isForkSubagentEnabled() && !subagent_type?
|
|||||||
|
|
||||||
### 3.4 消息构建:buildForkedMessages
|
### 3.4 消息构建:buildForkedMessages
|
||||||
|
|
||||||
文件:`packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts:107-169`
|
文件:`src/tools/AgentTool/forkSubagent.ts:107-169`
|
||||||
|
|
||||||
构建的消息结构:
|
构建的消息结构:
|
||||||
|
|
||||||
@@ -185,11 +185,11 @@ FEATURE_FORK_SUBAGENT=1 bun run dev
|
|||||||
|
|
||||||
| 文件 | 行数 | 职责 |
|
| 文件 | 行数 | 职责 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts` | ~210 | 核心定义 + 消息构建 + 递归防护 |
|
| `src/tools/AgentTool/forkSubagent.ts` | ~210 | 核心定义 + 消息构建 + 递归防护 |
|
||||||
| `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` | — | Fork 路由 + 强制异步 |
|
| `src/tools/AgentTool/AgentTool.tsx` | — | Fork 路由 + 强制异步 |
|
||||||
| `packages/builtin-tools/src/tools/AgentTool/prompt.ts` | — | "When to Fork" 提示词段落 |
|
| `src/tools/AgentTool/prompt.ts` | — | "When to Fork" 提示词段落 |
|
||||||
| `packages/builtin-tools/src/tools/AgentTool/runAgent.ts` | — | useExactTools 路径 |
|
| `src/tools/AgentTool/runAgent.ts` | — | useExactTools 路径 |
|
||||||
| `packages/builtin-tools/src/tools/AgentTool/resumeAgent.ts` | — | Fork agent 恢复 |
|
| `src/tools/AgentTool/resumeAgent.ts` | — | Fork agent 恢复 |
|
||||||
| `src/constants/xml.ts` | — | XML 标签常量 |
|
| `src/constants/xml.ts` | — | XML 标签常量 |
|
||||||
| `src/utils/forkedAgent.ts` | — | CacheSafeParams + ContentReplacementState 克隆 |
|
| `src/utils/forkedAgent.ts` | — | CacheSafeParams + ContentReplacementState 克隆 |
|
||||||
| `src/commands/fork/index.ts` | — | /fork 命令(stub) |
|
| `src/commands/fork/index.ts` | — | /fork 命令(stub) |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# KAIROS — 常驻助手模式
|
# KAIROS — 常驻助手模式
|
||||||
|
|
||||||
> Feature Flag: `FEATURE_KAIROS=1`(及子 Feature)
|
> Feature Flag: `FEATURE_KAIROS=1`(及子 Feature)
|
||||||
> 实现状态:核心框架完整,部分子模块为 stub;proactive/sleep 节奏控制已可用
|
> 实现状态:核心框架完整,部分子模块为 stub
|
||||||
> 引用数:154(全库最大)
|
> 引用数:154(全库最大)
|
||||||
|
|
||||||
## 一、功能概述
|
## 一、功能概述
|
||||||
@@ -34,13 +34,13 @@ KAIROS 在系统提示中注入两大段落:
|
|||||||
|
|
||||||
### 2.1 Brief 段落 (`getBriefSection`)
|
### 2.1 Brief 段落 (`getBriefSection`)
|
||||||
|
|
||||||
文件:`src/constants/prompts.ts:847-858`
|
文件:`src/constants/prompts.ts:843-858`
|
||||||
|
|
||||||
当 `feature('KAIROS') || feature('KAIROS_BRIEF')` 时注入。Brief 工具(`SendUserMessage`)的结构化消息输出指令。`/brief` toggle 和 `--brief` flag 只控制显示过滤,不影响模型行为。
|
当 `feature('KAIROS') || feature('KAIROS_BRIEF')` 时注入。Brief 工具(`SendUserMessage`)的结构化消息输出指令。`/brief` toggle 和 `--brief` flag 只控制显示过滤,不影响模型行为。
|
||||||
|
|
||||||
### 2.2 Proactive/Autonomous Work 段落 (`getProactiveSection`)
|
### 2.2 Proactive/Autonomous Work 段落 (`getProactiveSection`)
|
||||||
|
|
||||||
文件:`src/constants/prompts.ts:864-918`
|
文件:`src/constants/prompts.ts:860-914`
|
||||||
|
|
||||||
当 `feature('PROACTIVE') || feature('KAIROS')` 且 `isProactiveActive()` 时注入。核心行为指令:
|
当 `feature('PROACTIVE') || feature('KAIROS')` 且 `isProactiveActive()` 时注入。核心行为指令:
|
||||||
|
|
||||||
@@ -74,9 +74,8 @@ KAIROS 在系统提示中注入两大段落:
|
|||||||
|
|
||||||
SleepTool 是 KAIROS/Proactive 的节奏控制核心。工具描述让模型理解"休眠"概念:
|
SleepTool 是 KAIROS/Proactive 的节奏控制核心。工具描述让模型理解"休眠"概念:
|
||||||
- 工具名:`Sleep`
|
- 工具名:`Sleep`
|
||||||
- 功能:等待指定时间后响应 tick prompt;若队列出现新工作或 proactive 被关闭,会提前唤醒
|
- 功能:等待指定时间后响应 tick prompt
|
||||||
- 与 `<tick_tag>` 配合实现心跳式自主工作
|
- 与 `<tick_tag>` 配合实现心跳式自主工作
|
||||||
- 远程控制 surfaces 可通过 `automation_state` 看到 `standby` / `sleeping` 两种状态
|
|
||||||
|
|
||||||
### 3.3 Bridge 集成
|
### 3.3 Bridge 集成
|
||||||
|
|
||||||
@@ -173,10 +172,8 @@ FEATURE_KAIROS=1 FEATURE_TOKEN_BUDGET=1 bun run dev
|
|||||||
| `src/assistant/AssistantSessionChooser.ts` | — | Session 选择 UI(stub) |
|
| `src/assistant/AssistantSessionChooser.ts` | — | Session 选择 UI(stub) |
|
||||||
| `src/tools/BriefTool/` | — | BriefTool 实现(stub) |
|
| `src/tools/BriefTool/` | — | BriefTool 实现(stub) |
|
||||||
| `src/tools/SleepTool/prompt.ts` | ~30 | SleepTool 工具提示 |
|
| `src/tools/SleepTool/prompt.ts` | ~30 | SleepTool 工具提示 |
|
||||||
| `src/tools/SleepTool/SleepTool.ts` | ~200 | 休眠/唤醒与 automation metadata |
|
|
||||||
| `src/services/mcp/channelNotification.ts` | 5 | 频道消息接入(stub) |
|
| `src/services/mcp/channelNotification.ts` | 5 | 频道消息接入(stub) |
|
||||||
| `src/memdir/memdir.ts` | — | 记忆目录管理(stub) |
|
| `src/memdir/memdir.ts` | — | 记忆目录管理(stub) |
|
||||||
| `src/constants/prompts.ts:557,847-918` | 72 | 系统提示注入 |
|
| `src/constants/prompts.ts:552-554,843-914` | 72 | 系统提示注入 |
|
||||||
| `src/components/tasks/src/tasks/DreamTask/` | 3 | Dream 任务(stub) |
|
| `src/components/tasks/src/tasks/DreamTask/` | 3 | Dream 任务(stub) |
|
||||||
| `src/proactive/index.ts` | — | Proactive 核心(KAIROS 共享) |
|
| `src/proactive/index.ts` | — | Proactive 核心(stub,KAIROS 共享) |
|
||||||
| `src/utils/sessionState.ts` | — | 向 bridge/CCR 暴露 automation 状态 |
|
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ CLI-B (192.168.50.27) 心跳循环
|
|||||||
|
|
||||||
## SendMessageTool TCP 支持
|
## SendMessageTool TCP 支持
|
||||||
|
|
||||||
`packages/builtin-tools/src/tools/SendMessageTool/SendMessageTool.ts`
|
`src/tools/SendMessageTool/SendMessageTool.ts`
|
||||||
|
|
||||||
- `to` 字段支持 `tcp:host:port` 格式
|
- `to` 字段支持 `tcp:host:port` 格式
|
||||||
- `checkPermissions`:`tcp:` scheme 返回 `behavior: 'ask'`,`classifierApprovable: false`
|
- `checkPermissions`:`tcp:` scheme 返回 `behavior: 'ask'`,`classifierApprovable: false`
|
||||||
|
|||||||
@@ -202,4 +202,4 @@ docker run -d \
|
|||||||
| `src/services/langfuse/__tests__/langfuse.test.ts` | 测试(568 行) |
|
| `src/services/langfuse/__tests__/langfuse.test.ts` | 测试(568 行) |
|
||||||
| `src/query.ts` | 主查询流程中的 Trace 集成 |
|
| `src/query.ts` | 主查询流程中的 Trace 集成 |
|
||||||
| `src/services/tools/toolExecution.ts` | 工具执行中的观察记录 |
|
| `src/services/tools/toolExecution.ts` | 工具执行中的观察记录 |
|
||||||
| `packages/builtin-tools/src/tools/AgentTool/runAgent.ts` | 子 Agent Trace 创建 |
|
| `src/tools/AgentTool/runAgent.ts` | 子 Agent Trace 创建 |
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ getMcpSkillCommands() 过滤 → SkillTool 调用
|
|||||||
|
|
||||||
### 2.2 技能筛选
|
### 2.2 技能筛选
|
||||||
|
|
||||||
文件:`src/commands.ts:604-616`
|
文件:`src/commands.ts:547-558`
|
||||||
|
|
||||||
`getMcpSkillCommands(mcpCommands)` 过滤条件:
|
`getMcpSkillCommands(mcpCommands)` 过滤条件:
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ feature('MCP_SKILLS') // feature flag 必须开启
|
|||||||
|
|
||||||
### 2.3 条件加载
|
### 2.3 条件加载
|
||||||
|
|
||||||
文件:`src/services/mcp/client.ts:129-133`
|
文件:`src/services/mcp/client.ts:117-121`
|
||||||
|
|
||||||
`fetchMcpSkillsForClient` 通过 `require()` 条件加载,feature flag 关闭时不加载任何模块:
|
`fetchMcpSkillsForClient` 通过 `require()` 条件加载,feature flag 关闭时不加载任何模块:
|
||||||
|
|
||||||
@@ -79,8 +79,8 @@ const fetchMcpSkillsForClient = feature('MCP_SKILLS')
|
|||||||
|
|
||||||
| 文件 | 行 | 说明 |
|
| 文件 | 行 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `src/commands.ts` | 604-616, 620-633 | 命令过滤和 SkillTool 命令收集 |
|
| `src/commands.ts` | 547-558, 561-608 | 命令过滤和 SkillTool 命令收集 |
|
||||||
| `src/services/mcp/client.ts` | 129-133, 1394, 1672, 2176 | 技能获取、缓存清除、连接时获取 |
|
| `src/services/mcp/client.ts` | 117-121, 1394, 1672, 2173-2181, 2346-2358 | 技能获取、缓存清除、连接时获取 |
|
||||||
| `src/services/mcp/useManageMCPConnections.ts` | 22-26, 682-740 | 实时刷新(prompts/resources 变化) |
|
| `src/services/mcp/useManageMCPConnections.ts` | 22-26, 682-740 | 实时刷新(prompts/resources 变化) |
|
||||||
|
|
||||||
## 三、关键设计决策
|
## 三、关键设计决策
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ sub 角色:
|
|||||||
| `src/commands/pipes/pipes.ts` | /pipes 命令 |
|
| `src/commands/pipes/pipes.ts` | /pipes 命令 |
|
||||||
| `src/commands/attach/attach.ts` | /attach 命令 |
|
| `src/commands/attach/attach.ts` | /attach 命令 |
|
||||||
| `src/commands/send/send.ts` | /send 命令 |
|
| `src/commands/send/send.ts` | /send 命令 |
|
||||||
| `packages/builtin-tools/src/tools/SendMessageTool/SendMessageTool.ts` | AI 发消息工具(含 tcp: 支持) |
|
| `src/tools/SendMessageTool/SendMessageTool.ts` | AI 发消息工具(含 tcp: 支持) |
|
||||||
|
|
||||||
## 后续优化方向
|
## 后续优化方向
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# PROACTIVE — 主动模式
|
# PROACTIVE — 主动模式
|
||||||
|
|
||||||
> Feature Flag: `FEATURE_PROACTIVE=1`(与 `FEATURE_KAIROS=1` 共享功能)
|
> Feature Flag: `FEATURE_PROACTIVE=1`(与 `FEATURE_KAIROS=1` 共享功能)
|
||||||
> 实现状态:核心循环与 SleepTool 已落地,部分外围文档仍在补齐
|
> 实现状态:核心模块全部 Stub,布线完整
|
||||||
> 引用数:37
|
> 引用数:37
|
||||||
|
|
||||||
## 一、功能概述
|
## 一、功能概述
|
||||||
@@ -21,13 +21,13 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
|
|||||||
|
|
||||||
| 模块 | 文件 | 状态 | 说明 |
|
| 模块 | 文件 | 状态 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| 核心逻辑 | `src/proactive/index.ts` | **已实现** | `activateProactive()`、`deactivateProactive()`、`pause/resume`、`nextTickAt` 调度状态 |
|
| 核心逻辑 | `src/proactive/index.ts` | **Stub** | `activateProactive()`、`deactivateProactive()`、`isProactiveActive() => false` |
|
||||||
| SleepTool 提示 | `src/tools/SleepTool/prompt.ts` | **完整** | 工具提示定义(工具名:`Sleep`) |
|
| SleepTool 提示 | `src/tools/SleepTool/prompt.ts` | **完整** | 工具提示定义(工具名:`Sleep`) |
|
||||||
| 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` |
|
| 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` |
|
||||||
| 工具注册 | `src/tools.ts:26-28` | **布线** | SleepTool 动态加载 |
|
| 工具注册 | `src/tools.ts:26-28` | **布线** | SleepTool 动态加载 |
|
||||||
| REPL 集成 | `src/screens/REPL.tsx` | **已实现** | tick 驱动、standby/sleeping 状态、页脚与 bridge automation metadata 上报 |
|
| REPL 集成 | `src/screens/REPL.tsx` | **布线** | tick 驱动逻辑、占位符、页脚 UI |
|
||||||
| 系统提示 | `src/constants/prompts.ts:864-918` | **完整** | 自主工作行为指令(~55 行详细 prompt) |
|
| 系统提示 | `src/constants/prompts.ts:860-914` | **完整** | 自主工作行为指令(~55 行详细 prompt) |
|
||||||
| 远控状态镜像 | `src/utils/sessionState.ts` | **已实现** | 向 remote-control/CCR 暴露 `automation_state` 元数据 |
|
| 会话存储 | `src/utils/sessionStorage.ts:4892-4912` | **布线** | tick 消息注入对话流 |
|
||||||
|
|
||||||
### 2.2 系统提示内容
|
### 2.2 系统提示内容
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
|
|||||||
### 2.3 数据流
|
### 2.3 数据流
|
||||||
|
|
||||||
```
|
```
|
||||||
activateProactive()
|
activateProactive() [需要实现]
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Tick 调度器启动
|
Tick 调度器启动
|
||||||
@@ -62,22 +62,20 @@ Tick 调度器启动
|
|||||||
└── 无事可做 → 必须调用 SleepTool
|
└── 无事可做 → 必须调用 SleepTool
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
SleepTool 等待
|
SleepTool 等待 [需要实现]
|
||||||
│
|
|
||||||
├── 用户插入新工作 / 队列中有命令 → 立即唤醒
|
|
||||||
├── proactive 被关闭 → 立即中断
|
|
||||||
└── 进入休眠时向远端 surfaces 上报 `automation_state = sleeping`
|
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
下一个 tick 到达
|
下一个 tick 到达
|
||||||
```
|
```
|
||||||
|
|
||||||
## 三、当前行为补充
|
## 三、需要补全的内容
|
||||||
|
|
||||||
- `standby`:proactive 已开启,当前没有执行中的 turn,且已调度下一个 tick。
|
| 优先级 | 模块 | 工作量 | 说明 |
|
||||||
- `sleeping`:模型显式调用 `SleepTool` 进入等待窗口。
|
|--------|------|--------|------|
|
||||||
- remote-control/CCR 通过 `external_metadata.automation_state` 接收这两个状态,用于 Web UI 的 Autopilot 状态显示。
|
| 1 | `src/proactive/index.ts` | 中 | Tick 调度器、activate/deactivate 状态机、pause/resume |
|
||||||
- `SleepTool` 现在不是纯定时器;它会在共享命令队列出现新工作时提前醒来。
|
| 2 | `src/tools/SleepTool/SleepTool.ts` | 小 | 工具执行(等待指定时间后触发 tick) |
|
||||||
|
| 3 | `src/commands/proactive.js` | 小 | `/proactive` 斜杠命令处理器 |
|
||||||
|
| 4 | `src/hooks/useProactive.ts` | 中 | React hook(REPL 引用但不存在) |
|
||||||
|
|
||||||
## 四、关键设计决策
|
## 四、关键设计决策
|
||||||
|
|
||||||
@@ -103,11 +101,9 @@ FEATURE_PROACTIVE=1 FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 bun run dev
|
|||||||
|
|
||||||
| 文件 | 职责 |
|
| 文件 | 职责 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `src/proactive/index.ts` | 核心逻辑与 next-tick 状态 |
|
| `src/proactive/index.ts` | 核心逻辑(stub) |
|
||||||
| `src/tools/SleepTool/prompt.ts` | SleepTool 工具提示 |
|
| `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 集成 |
|
||||||
| `src/screens/REPL.tsx` | REPL tick 集成与 automation 状态上报 |
|
|
||||||
| `src/utils/sessionStorage.ts:4892-4912` | Tick 消息注入 |
|
| `src/utils/sessionStorage.ts:4892-4912` | Tick 消息注入 |
|
||||||
| `src/utils/sessionState.ts` | bridge/CCR metadata 镜像 |
|
|
||||||
| `src/components/PromptInput/PromptInputFooterLeftSide.tsx` | 页脚 UI 状态 |
|
| `src/components/PromptInput/PromptInputFooterLeftSide.tsx` | 页脚 UI 状态 |
|
||||||
|
|||||||
@@ -13,22 +13,17 @@
|
|||||||
┌──────────────────┐ HTTP/SSE │ │ In-Memory │ │
|
┌──────────────────┐ HTTP/SSE │ │ In-Memory │ │
|
||||||
│ Web UI 控制面板 │ ◄─────────────── │ │ Store │ │
|
│ Web UI 控制面板 │ ◄─────────────── │ │ Store │ │
|
||||||
│ (/code/*) │ │ └──────────────┘ │
|
│ (/code/*) │ │ └──────────────┘ │
|
||||||
│ (React + Vite) │ │ ┌──────────────┐ │
|
└──────────────────┘ │ ┌──────────────┐ │
|
||||||
└──────────────────┘ │ │ JWT Auth │ │
|
│ │ JWT Auth │ │
|
||||||
│ └──────────────┘ │
|
│ └──────────────┘ │
|
||||||
┌──────────────────┐ │ ┌──────────────┐ │
|
└──────────────────────┘
|
||||||
│ acp-link │ ◄── ACP Relay ─── │ │ ACP Handler │ │
|
|
||||||
│ + ACP Agent │ WebSocket │ └──────────────┘ │
|
|
||||||
└──────────────────┘ └──────────────────────┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**RCS 是一个纯内存的中间服务**,它的职责是:
|
**RCS 是一个纯内存的中间服务**,它的职责是:
|
||||||
- 接收 Claude Code CLI 的环境注册和工作轮询
|
- 接收 Claude Code CLI 的环境注册和工作轮询
|
||||||
- 接收 acp-link 的 ACP agent 注册,支持 WebSocket relay 桥接
|
|
||||||
- 提供 Web UI 供操作者远程监控和审批
|
- 提供 Web UI 供操作者远程监控和审批
|
||||||
- 通过 WebSocket/SSE 双向传输消息
|
- 通过 WebSocket/SSE 双向传输消息
|
||||||
- 管理会话、环境、权限请求
|
- 管理会话、环境、权限请求
|
||||||
- 提供 ACP SSE event stream 供外部消费者订阅 channel group 事件
|
|
||||||
|
|
||||||
## 前置条件
|
## 前置条件
|
||||||
|
|
||||||
@@ -104,8 +99,6 @@ docker compose up -d
|
|||||||
| `RCS_HEARTBEAT_INTERVAL` | 否 | `20` | 心跳间隔(秒) |
|
| `RCS_HEARTBEAT_INTERVAL` | 否 | `20` | 心跳间隔(秒) |
|
||||||
| `RCS_JWT_EXPIRES_IN` | 否 | `3600` | JWT 令牌有效期(秒) |
|
| `RCS_JWT_EXPIRES_IN` | 否 | `3600` | JWT 令牌有效期(秒) |
|
||||||
| `RCS_DISCONNECT_TIMEOUT` | 否 | `300` | 断线判定超时(秒) |
|
| `RCS_DISCONNECT_TIMEOUT` | 否 | `300` | 断线判定超时(秒) |
|
||||||
| `RCS_WS_IDLE_TIMEOUT` | 否 | `30` | WebSocket 空闲超时(秒),Bun 发送协议级 ping |
|
|
||||||
| `RCS_WS_KEEPALIVE_INTERVAL` | 否 | `20` | 服务端→客户端 keep_alive 帧间隔(秒),防止反向代理关闭空闲连接 |
|
|
||||||
|
|
||||||
### 客户端(Claude Code CLI)
|
### 客户端(Claude Code CLI)
|
||||||
|
|
||||||
@@ -176,69 +169,15 @@ claude bridge
|
|||||||
|
|
||||||
## Web UI 控制面板
|
## Web UI 控制面板
|
||||||
|
|
||||||
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。
|
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。功能:
|
||||||
|
|
||||||
### 技术栈(v2,2026-04-18 重构)
|
- 查看已注册的运行环境(environment 模式)
|
||||||
|
|
||||||
Web UI 已从原生 JS 重构为 **React + Vite + Radix UI**:
|
|
||||||
|
|
||||||
- **框架**: React 19 + Vite 构建,TypeScript
|
|
||||||
- **UI 组件**: Radix UI primitives(Dialog、Tabs、Select、Popover 等)
|
|
||||||
- **聊天组件**: 完整的 ACP 聊天界面,支持 Plan 可视化、工具调用展示、权限审批
|
|
||||||
- **AI Elements**: 独立的 AI 交互组件库(message、reasoning、tool、code-block、prompt-input 等)
|
|
||||||
- **ACP 直连**: 支持 QR 码扫描自动跳转 ACP 直连视图(`ACPDirectView`)
|
|
||||||
- **主题系统**: 暗色/亮色主题切换,遵循 Impeccable 设计系统
|
|
||||||
|
|
||||||
### 功能
|
|
||||||
|
|
||||||
- 查看已注册的运行环境(environment 模式),区分 ACP Agent 和 Claude Code 类型
|
|
||||||
- 创建和管理会话
|
- 创建和管理会话
|
||||||
- 实时查看对话消息和工具调用
|
- 实时查看对话消息和工具调用
|
||||||
- 查看 Autopilot 状态(`standby` / `sleeping`)和自动运行指示
|
|
||||||
- 查看 authoritative task snapshots 驱动的 Tasks 面板
|
|
||||||
- 审批 Claude Code 的工具权限请求
|
- 审批 Claude Code 的工具权限请求
|
||||||
- 权限模式选择器(6 种模式:默认/自动接受编辑/跳过权限/规划/不询问/自动判断)
|
|
||||||
- 模型选择器(可选可用模型)
|
|
||||||
- Plan 可视化(进度条、状态图标、优先级标签)
|
|
||||||
- ACP QR 扫描自动跳转到 ACP 聊天界面
|
|
||||||
|
|
||||||
Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境。
|
Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境。
|
||||||
|
|
||||||
## ACP 支持
|
|
||||||
|
|
||||||
RCS 支持 ACP (Agent Client Protocol) agent 通过 `acp-link` 包接入。
|
|
||||||
|
|
||||||
### 架构
|
|
||||||
|
|
||||||
```
|
|
||||||
acp-link ──REST注册──► RCS POST /v1/environments/bridge
|
|
||||||
acp-link ──WS identify──► RCS WebSocket (携带 agentId)
|
|
||||||
acp-link ◄──ACP relay──► RCS ◄──Web UI WS──► 浏览器
|
|
||||||
```
|
|
||||||
|
|
||||||
### 后端组件
|
|
||||||
|
|
||||||
| 文件 | 职责 |
|
|
||||||
|------|------|
|
|
||||||
| `src/routes/acp/index.ts` | ACP REST 路由:agents 列表、channel groups、relay |
|
|
||||||
| `src/transport/acp-ws-handler.ts` | ACP WebSocket 处理:agent 注册、心跳、消息转发 |
|
|
||||||
| `src/transport/acp-relay-handler.ts` | 前端 WS → acp-link 透传 + EventBus inbound 转发 |
|
|
||||||
| `src/transport/acp-sse-writer.ts` | SSE event stream 供外部消费者订阅 |
|
|
||||||
|
|
||||||
### acp-link 连接
|
|
||||||
|
|
||||||
详见 [acp-link 文档](./acp-link.md)。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 在 RCS 环境中启动 acp-link
|
|
||||||
# 注意:claude 本身不支持 ACP,需要用 ccb-bun --acp
|
|
||||||
ACP_RCS_URL=http://localhost:3000 \
|
|
||||||
ACP_RCS_TOKEN=sk-rcs-your-key \
|
|
||||||
acp-link ccb-bun -- --acp
|
|
||||||
```
|
|
||||||
|
|
||||||
ACP session 在 Web UI 中显示品牌色标签,与普通 Claude Code session 区分。
|
|
||||||
|
|
||||||
## 工作流程详解
|
## 工作流程详解
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -276,7 +215,6 @@ ACP session 在 Web UI 中显示品牌色标签,与普通 Claude Code session
|
|||||||
9. 双向通信
|
9. 双向通信
|
||||||
CLI ──消息/工具调用结果──► RCS ──► Browser
|
CLI ──消息/工具调用结果──► RCS ──► Browser
|
||||||
CLI ◄──权限审批/指令───── RCS ◄──── Browser
|
CLI ◄──权限审批/指令───── RCS ◄──── Browser
|
||||||
CLI ──automation_state / task_state──► RCS ──► Browser
|
|
||||||
|
|
||||||
10. 心跳保活(每 20 秒)
|
10. 心跳保活(每 20 秒)
|
||||||
CLI ──POST /v1/environments/:id/work/:workId/heartbeat──► RCS
|
CLI ──POST /v1/environments/:id/work/:workId/heartbeat──► RCS
|
||||||
@@ -286,13 +224,6 @@ ACP session 在 Web UI 中显示品牌色标签,与普通 Claude Code session
|
|||||||
|
|
||||||
## 故障排查
|
## 故障排查
|
||||||
|
|
||||||
### Web UI 看不到当前 Autopilot 状态
|
|
||||||
|
|
||||||
- `standby`:proactive 已开启,正在等待下一个 tick
|
|
||||||
- `sleeping`:模型正在 `SleepTool` 等待窗口中
|
|
||||||
|
|
||||||
这两个状态通过 worker `external_metadata.automation_state` 上报。如果页面只显示普通 working spinner,优先检查 CLI 和 RCS 之间的 worker metadata PUT 是否成功。
|
|
||||||
|
|
||||||
### CLI 无法连接
|
### CLI 无法连接
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -16,12 +16,12 @@
|
|||||||
### 现状
|
### 现状
|
||||||
|
|
||||||
- `start` 路径已有完整 supervisor + worker 生命周期:
|
- `start` 路径已有完整 supervisor + worker 生命周期:
|
||||||
`src/daemon/main.ts`
|
[src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:1>)
|
||||||
`src/daemon/workerRegistry.ts`
|
[src/daemon/workerRegistry.ts](</e:/Source_code/Claude-code-bast/src/daemon/workerRegistry.ts:1>)
|
||||||
- `status` / `stop` 目前只是占位输出:
|
- `status` / `stop` 目前只是占位输出:
|
||||||
`src/daemon/main.ts`
|
[src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:49>)
|
||||||
- `/remote-control-server` 有自己的命令内 UI 状态,但只维护当前进程内的 `daemonProcess`,并不适合作为跨进程 CLI 管理基础:
|
- `/remote-control-server` 有自己的命令内 UI 状态,但只维护当前进程内的 `daemonProcess`,并不适合作为跨进程 CLI 管理基础:
|
||||||
`src/commands/remoteControlServer/remoteControlServer.tsx`
|
[src/commands/remoteControlServer/remoteControlServer.tsx](</e:/Source_code/Claude-code-bast/src/commands/remoteControlServer/remoteControlServer.tsx:32>)
|
||||||
|
|
||||||
### 目标
|
### 目标
|
||||||
|
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
### 代码范围
|
### 代码范围
|
||||||
|
|
||||||
- 新增 `src/daemon/state.ts`
|
- 新增 `src/daemon/state.ts`
|
||||||
- 修改 `src/daemon/main.ts`
|
- 修改 [src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:1>)
|
||||||
- 轻量修改 `src/commands/remoteControlServer/remoteControlServer.tsx`,让 UI 尽量读取同一份状态文件
|
- 轻量修改 [src/commands/remoteControlServer/remoteControlServer.tsx](</e:/Source_code/Claude-code-bast/src/commands/remoteControlServer/remoteControlServer.tsx:32>),让 UI 尽量读取同一份状态文件
|
||||||
|
|
||||||
### 验证
|
### 验证
|
||||||
|
|
||||||
@@ -78,15 +78,15 @@
|
|||||||
### 现状
|
### 现状
|
||||||
|
|
||||||
- fast-path 已接好:
|
- fast-path 已接好:
|
||||||
`src/entrypoints/cli.tsx`
|
[src/entrypoints/cli.tsx](</e:/Source_code/Claude-code-bast/src/entrypoints/cli.tsx:218>)
|
||||||
- session registry 已有真实实现:
|
- session registry 已有真实实现:
|
||||||
`src/utils/concurrentSessions.ts`
|
[src/utils/concurrentSessions.ts](</e:/Source_code/Claude-code-bast/src/utils/concurrentSessions.ts:1>)
|
||||||
- `exit` 在 bg session 内已会 `tmux detach-client`:
|
- `exit` 在 bg session 内已会 `tmux detach-client`:
|
||||||
`src/commands/exit/exit.tsx`
|
[src/commands/exit/exit.tsx](</e:/Source_code/Claude-code-bast/src/commands/exit/exit.tsx:20>)
|
||||||
- 但 CLI handler 仍全空:
|
- 但 CLI handler 仍全空:
|
||||||
`src/cli/bg.ts`
|
[src/cli/bg.ts](</e:/Source_code/Claude-code-bast/src/cli/bg.ts:1>)
|
||||||
- task summary 仍然是 stub:
|
- task summary 仍然是 stub:
|
||||||
`src/utils/taskSummary.ts`
|
[src/utils/taskSummary.ts](</e:/Source_code/Claude-code-bast/src/utils/taskSummary.ts:1>)
|
||||||
|
|
||||||
### 目标
|
### 目标
|
||||||
|
|
||||||
@@ -122,12 +122,12 @@
|
|||||||
|
|
||||||
### 代码范围
|
### 代码范围
|
||||||
|
|
||||||
- 修改 `src/cli/bg.ts`
|
- 修改 [src/cli/bg.ts](</e:/Source_code/Claude-code-bast/src/cli/bg.ts:1>)
|
||||||
- 修改 `src/utils/concurrentSessions.ts` 以便后续 attach/--bg 扩展
|
- 修改 [src/utils/concurrentSessions.ts](</e:/Source_code/Claude-code-bast/src/utils/concurrentSessions.ts:1>) 以便后续 attach/--bg 扩展
|
||||||
- 修改 `src/utils/taskSummary.ts`
|
- 修改 [src/utils/taskSummary.ts](</e:/Source_code/Claude-code-bast/src/utils/taskSummary.ts:1>)
|
||||||
- 复用:
|
- 复用:
|
||||||
`src/utils/sessionStorage.ts`
|
[src/utils/sessionStorage.ts](</e:/Source_code/Claude-code-bast/src/utils/sessionStorage.ts:3870>)
|
||||||
`src/utils/udsClient.ts`
|
[src/utils/udsClient.ts](</e:/Source_code/Claude-code-bast/src/utils/udsClient.ts:1>)
|
||||||
|
|
||||||
### 验证
|
### 验证
|
||||||
|
|
||||||
@@ -150,15 +150,15 @@
|
|||||||
### 现状
|
### 现状
|
||||||
|
|
||||||
- 命令入口只有 fast-path:
|
- 命令入口只有 fast-path:
|
||||||
`src/entrypoints/cli.tsx`
|
[src/entrypoints/cli.tsx](</e:/Source_code/Claude-code-bast/src/entrypoints/cli.tsx:249>)
|
||||||
- handler 是空的:
|
- handler 是空的:
|
||||||
`src/cli/handlers/templateJobs.ts`
|
[src/cli/handlers/templateJobs.ts](</e:/Source_code/Claude-code-bast/src/cli/handlers/templateJobs.ts:1>)
|
||||||
- `markdownConfigLoader` 已把 `templates` 纳入配置目录:
|
- `markdownConfigLoader` 已把 `templates` 纳入配置目录:
|
||||||
`src/utils/markdownConfigLoader.ts`
|
[src/utils/markdownConfigLoader.ts](</e:/Source_code/Claude-code-bast/src/utils/markdownConfigLoader.ts:29>)
|
||||||
- `query / stopHooks` 已预留 job classifier 链路:
|
- `query / stopHooks` 已预留 job classifier 链路:
|
||||||
`src/query/stopHooks.ts`
|
[src/query/stopHooks.ts](</e:/Source_code/Claude-code-bast/src/query/stopHooks.ts:103>)
|
||||||
- `jobs/classifier.ts` 仍是 stub:
|
- `jobs/classifier.ts` 仍是 stub:
|
||||||
`src/jobs/classifier.ts`
|
[src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
|
||||||
|
|
||||||
### 目标
|
### 目标
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
|
|
||||||
### Phase 2
|
### Phase 2
|
||||||
|
|
||||||
- 恢复 `src/jobs/classifier.ts`
|
- 恢复 [src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
|
||||||
- 让带 `CLAUDE_JOB_DIR` 的 job session 在 turn 完成后自动更新 `state.json`
|
- 让带 `CLAUDE_JOB_DIR` 的 job session 在 turn 完成后自动更新 `state.json`
|
||||||
- 再决定是否补自动 job runner
|
- 再决定是否补自动 job runner
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,50 @@
|
|||||||
|
|
||||||
| Feature | 引用 | 状态 | 类别 | 简要说明 |
|
| Feature | 引用 | 状态 | 类别 | 简要说明 |
|
||||||
|---------|------|------|------|---------|
|
|---------|------|------|------|---------|
|
||||||
| CHICAGO_MCP | 16 | 已实现 | 工具 | Computer Use + Chrome MCP 控制(build 默认启用) |
|
| CHICAGO_MCP | 16 | N/A | 内部基础设施 | Anthropic 内部 MCP 基础设施,非外部可用 |
|
||||||
| MONITOR_TOOL | 13 | 已实现 | 工具 | 后台监控工具,持续监视 shell 输出(build 默认启用) |
|
| MONITOR_TOOL | 13 | Stub | 工具 | 文件/进程监控工具,检测变更并通知 |
|
||||||
| BG_SESSIONS | 11 | 部分实现 | 会话管理 | 后台会话注册/清理已实现,任务摘要是 stub(dev 默认启用) |
|
| BG_SESSIONS | 11 | Stub | 会话管理 | 后台会话管理,支持多会话并行 |
|
||||||
| SHOT_STATS | 10 | 已实现 | 统计 | API 调用统计面板(build 默认启用) |
|
| SHOT_STATS | 10 | 无实现 | 统计 | 逐 prompt 统计信息收集 |
|
||||||
| EXTRACT_MEMORIES | 7 | 已实现 | 记忆 | 自动记忆提取(build 默认启用,受 GrowthBook 门控) |
|
| EXTRACT_MEMORIES | 7 | 无实现 | 记忆 | 自动从对话中提取重要信息作为记忆 |
|
||||||
| TEMPLATES | 6 | 部分实现 | 项目管理 | 项目/提示模板系统(dev 默认启用) |
|
| TEMPLATES | 6 | Stub | 项目管理 | 项目/提示模板系统 |
|
||||||
| LODESTONE | 6 | 已实现 | 深度链接 | URL 协议处理器(build 默认启用) |
|
| 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 状态 |
|
||||||
|
|
||||||
## 单引用 Feature(40+ 个)
|
## 单引用 Feature(40+ 个)
|
||||||
|
|
||||||
@@ -29,9 +66,10 @@ BUILDING_CLAUDE_APPS, ANTI_DISTILLATION_CC, AGENT_TRIGGERS, ABLATION_BASELINE
|
|||||||
|
|
||||||
这些 feature 被列为 Tier 3 的原因:
|
这些 feature 被列为 Tier 3 的原因:
|
||||||
|
|
||||||
1. **已实现但影响范围小**(CHICAGO_MCP, LODESTONE, SHOT_STATS, EXTRACT_MEMORIES, MONITOR_TOOL):已在 build/dev 默认启用,主要作为其他功能的基础设施
|
1. **内部基础设施**(CHICAGO_MCP, LODESTONE):Anthropic 内部使用,外部无法运行
|
||||||
2. **部分实现**(BG_SESSIONS, TEMPLATES):核心注册已实现,但部分功能如任务摘要仍是 stub
|
2. **纯 Stub 且引用低**(MONITOR_TOOL, BG_SESSIONS):需要大量工作才能实现
|
||||||
3. **辅助功能**(STREAMLINED_OUTPUT, HOOK_PROMPTS):影响范围小
|
3. **实验性功能**(SHOT_STATS, EXTRACT_MEMORIES):尚在概念阶段
|
||||||
4. **CCR 系列**:依赖远程控制基础设施,需要 BRIDGE_MODE 先完善
|
4. **辅助功能**(STREAMLINED_OUTPUT, HOOK_PROMPTS):影响范围小
|
||||||
|
5. **CCR 系列**:依赖远程控制基础设施,需要 BRIDGE_MODE 先完善
|
||||||
|
|
||||||
如需深入了解某个 Tier 3 feature,可以在代码库中搜索 `feature('FEATURE_NAME')` 查看具体使用场景。
|
如需深入了解某个 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/query/tokenBudget.ts` | 93 | 预算追踪器 + continue/stop 决策 |
|
||||||
| `src/bootstrap/state.ts:724-743` | 20 | turn 级 token 快照状态 |
|
| `src/bootstrap/state.ts:724-743` | 20 | turn 级 token 快照状态 |
|
||||||
| `src/constants/prompts.ts:538-551` | 14 | 系统提示注入 |
|
| `src/constants/prompts.ts:538-551` | 14 | 系统提示注入 |
|
||||||
| `src/utils/attachments.ts:3830-3844` | 17 | API attachment 附加 |
|
| `src/utils/attachments.ts:3829-3845` | 17 | API attachment 附加 |
|
||||||
| `src/query.ts:280,1311-1358` | 48 | 主循环集成 |
|
| `src/query.ts:280,1311-1358` | 48 | 主循环集成 |
|
||||||
| `src/screens/REPL.tsx:2897,2963,2138` | 20 | REPL 提交/完成/取消处理 |
|
| `src/screens/REPL.tsx:2897,2963,2138` | 20 | REPL 提交/完成/取消处理 |
|
||||||
| `src/components/Spinner.tsx:319-338` | 20 | 进度条 UI |
|
| `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/bashParser.ts` | 4437 | 纯 TS bash 解析器 |
|
||||||
| `src/utils/bash/ast.ts` | 2680 | 安全分析器(核心) |
|
| `src/utils/bash/ast.ts` | 2680 | 安全分析器(核心) |
|
||||||
| `src/utils/bash/treeSitterAnalysis.ts` | 507 | AST 分析辅助 |
|
| `src/utils/bash/treeSitterAnalysis.ts` | 507 | AST 分析辅助 |
|
||||||
| `packages/builtin-tools/src/tools/BashTool/bashPermissions.ts` | ~140 | 权限集成 + Shadow 遥测 |
|
| `src/tools/BashTool/bashPermissions.ts:1670-1810` | ~140 | 权限集成 + Shadow 遥测 |
|
||||||
|
|||||||
@@ -22,16 +22,16 @@ ULTRAPLAN 在用户输入中检测 "ultraplan" 关键字时,自动进入增强
|
|||||||
|
|
||||||
| 模块 | 文件 | 行数 | 状态 |
|
| 模块 | 文件 | 行数 | 状态 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| 命令处理器 | `src/commands/ultraplan.tsx` | 525 | **完整** |
|
| 命令处理器 | `src/commands/ultraplan.tsx` | 472 | **完整** |
|
||||||
| CCR 会话 | `src/utils/ultraplan/ccrSession.ts` | 349 | **完整** |
|
| CCR 会话 | `src/utils/ultraplan/ccrSession.ts` | 350 | **完整** |
|
||||||
| 关键字检测 | `src/utils/ultraplan/keyword.ts` | 127 | **完整** |
|
| 关键字检测 | `src/utils/ultraplan/keyword.ts` | 128 | **完整** |
|
||||||
| 嵌入式提示 | `src/utils/ultraplan/prompt.txt` | 1 | **完整** |
|
| 嵌入式提示 | `src/utils/ultraplan/prompt.txt` | 1 | **完整** |
|
||||||
| REPL 对话框 | `src/screens/REPL.tsx` | — | **布线** |
|
| REPL 对话框 | `src/screens/REPL.tsx` | — | **布线** |
|
||||||
| 关键字高亮 | `src/components/PromptInput/PromptInput.tsx` | — | **布线** |
|
| 关键字高亮 | `src/components/PromptInput/PromptInput.tsx` | — | **布线** |
|
||||||
|
|
||||||
### 2.2 关键字检测
|
### 2.2 关键字检测
|
||||||
|
|
||||||
文件:`src/utils/ultraplan/keyword.ts`(127 行)
|
文件:`src/utils/ultraplan/keyword.ts`(128 行)
|
||||||
|
|
||||||
`findUltraplanTriggerPositions(text)` 智能过滤:
|
`findUltraplanTriggerPositions(text)` 智能过滤:
|
||||||
- 排除引号内的 "ultraplan"
|
- 排除引号内的 "ultraplan"
|
||||||
@@ -41,7 +41,7 @@ ULTRAPLAN 在用户输入中检测 "ultraplan" 关键字时,自动进入增强
|
|||||||
|
|
||||||
### 2.3 CCR 远程会话
|
### 2.3 CCR 远程会话
|
||||||
|
|
||||||
文件:`src/utils/ultraplan/ccrSession.ts`(349 行)
|
文件:`src/utils/ultraplan/ccrSession.ts`(350 行)
|
||||||
|
|
||||||
`ExitPlanModeScanner` 类实现完整的事件状态机:
|
`ExitPlanModeScanner` 类实现完整的事件状态机:
|
||||||
- `pollForApprovedExitPlanMode()` — 3 秒轮询间隔
|
- `pollForApprovedExitPlanMode()` — 3 秒轮询间隔
|
||||||
@@ -99,9 +99,9 @@ FEATURE_ULTRAPLAN=1 bun run dev
|
|||||||
|
|
||||||
| 文件 | 行数 | 职责 |
|
| 文件 | 行数 | 职责 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `src/commands/ultraplan.tsx` | 525 | 斜杠命令处理器 |
|
| `src/commands/ultraplan.tsx` | 472 | 斜杠命令处理器 |
|
||||||
| `src/utils/ultraplan/ccrSession.ts` | 349 | CCR 远程会话管理 |
|
| `src/utils/ultraplan/ccrSession.ts` | 350 | CCR 远程会话管理 |
|
||||||
| `src/utils/ultraplan/keyword.ts` | 127 | 关键字检测和替换 |
|
| `src/utils/ultraplan/keyword.ts` | 128 | 关键字检测和替换 |
|
||||||
| `src/utils/ultraplan/prompt.txt` | 1 | 嵌入式提示 |
|
| `src/utils/ultraplan/prompt.txt` | 1 | 嵌入式提示 |
|
||||||
| `src/utils/processUserInput/processUserInput.ts:468` | — | 关键字重定向 |
|
| `src/utils/processUserInput/processUserInput.ts:468` | — | 关键字重定向 |
|
||||||
| `src/components/PromptInput/PromptInput.tsx` | — | 彩虹高亮 |
|
| `src/components/PromptInput/PromptInput.tsx` | — | 彩虹高亮 |
|
||||||
|
|||||||
@@ -120,6 +120,6 @@ FEATURE_VOICE_MODE=1 bun run dev
|
|||||||
|
|
||||||
| 文件 | 行数 | 职责 |
|
| 文件 | 行数 | 职责 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `src/voice/voiceModeEnabled.ts` | 54 | 三层门控逻辑 |
|
| `src/voice/voiceModeEnabled.ts` | 55 | 三层门控逻辑 |
|
||||||
| `src/hooks/useVoice.ts` | — | React hook(录音状态 + WebSocket) |
|
| `src/hooks/useVoice.ts` | — | React hook(录音状态 + WebSocket) |
|
||||||
| `src/services/voiceStreamSTT.ts` | — | STT WebSocket 流式传输 |
|
| `src/services/voiceStreamSTT.ts` | — | STT WebSocket 流式传输 |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# WEB_BROWSER_TOOL — 浏览器工具
|
# WEB_BROWSER_TOOL — 浏览器工具
|
||||||
|
|
||||||
> Feature Flag: `FEATURE_WEB_BROWSER_TOOL=1`
|
> Feature Flag: `FEATURE_WEB_BROWSER_TOOL=1`
|
||||||
> 实现状态:核心工具已实现,面板为 Stub,布线完整
|
> 实现状态:核心实现缺失,面板为 Stub,布线完整
|
||||||
> 引用数:4
|
> 引用数:4
|
||||||
|
|
||||||
## 一、功能概述
|
## 一、功能概述
|
||||||
@@ -14,8 +14,8 @@ WEB_BROWSER_TOOL 让模型可以启动浏览器实例、导航网页、与页面
|
|||||||
|
|
||||||
| 模块 | 文件 | 状态 |
|
| 模块 | 文件 | 状态 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 浏览器面板 | `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserPanel.ts` | **Stub** — 返回 null |
|
| 浏览器面板 | `src/tools/WebBrowserTool/WebBrowserPanel.ts` | **Stub** — 返回 null |
|
||||||
| 浏览器工具 | `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserTool.ts` | **已实现** |
|
| 浏览器工具 | `src/tools/WebBrowserTool/WebBrowserTool.ts` | **缺失** |
|
||||||
| REPL 集成 | `src/screens/REPL.tsx` | **布线** — 渲染 WebBrowserPanel |
|
| REPL 集成 | `src/screens/REPL.tsx` | **布线** — 渲染 WebBrowserPanel |
|
||||||
| 工具注册 | `src/tools.ts` | **布线** — 动态加载 |
|
| 工具注册 | `src/tools.ts` | **布线** — 动态加载 |
|
||||||
| WebView 检测 | `src/main.tsx` | **布线** — `'WebView' in Bun` 检测 |
|
| WebView 检测 | `src/main.tsx` | **布线** — `'WebView' in Bun` 检测 |
|
||||||
@@ -44,8 +44,8 @@ WebBrowserPanel 在 REPL 侧边显示浏览器状态
|
|||||||
|
|
||||||
| 模块 | 工作量 | 说明 |
|
| 模块 | 工作量 | 说明 |
|
||||||
|------|--------|------|
|
|------|--------|------|
|
||||||
| `WebBrowserTool.ts` | ✅ 已实现 | 工具 schema + Bun WebView API 执行 |
|
| `WebBrowserTool.ts` | 大 | 工具 schema + Bun WebView API 执行 |
|
||||||
| `WebBrowserPanel.tsx` | 中 | REPL 侧边栏浏览器状态面板(仍为 Stub) |
|
| `WebBrowserPanel.tsx` | 中 | REPL 侧边栏浏览器状态面板 |
|
||||||
|
|
||||||
## 四、关键设计决策
|
## 四、关键设计决策
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ FEATURE_WEB_BROWSER_TOOL=1 bun run dev
|
|||||||
|
|
||||||
| 文件 | 职责 |
|
| 文件 | 职责 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserPanel.ts` | 面板组件(stub) |
|
| `src/tools/WebBrowserTool/WebBrowserPanel.ts` | 面板组件(stub) |
|
||||||
| `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserTool.ts` | 工具实现(已实现) |
|
| `src/tools/WebBrowserTool/WebBrowserTool.ts` | 工具实现(缺失) |
|
||||||
| `src/screens/REPL.tsx:471,5676` | 面板渲染 |
|
| `src/screens/REPL.tsx:273,4582` | 面板渲染 |
|
||||||
| `src/tools.ts:115-116` | 工具注册 |
|
| `src/tools.ts:115-116` | 工具注册 |
|
||||||
|
|||||||
@@ -34,16 +34,16 @@ WebSearchTool.call()
|
|||||||
|
|
||||||
| 模块 | 文件 | 说明 |
|
| 模块 | 文件 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 工具入口 | `packages/builtin-tools/src/tools/WebSearchTool/WebSearchTool.ts` | `buildTool()` 定义:schema、权限、执行、输出格式化 |
|
| 工具入口 | `src/tools/WebSearchTool/WebSearchTool.ts` | `buildTool()` 定义:schema、权限、执行、输出格式化 |
|
||||||
| 工具 prompt | `packages/builtin-tools/src/tools/WebSearchTool/prompt.ts` | 搜索工具的系统提示词 |
|
| 工具 prompt | `src/tools/WebSearchTool/prompt.ts` | 搜索工具的系统提示词 |
|
||||||
| UI 渲染 | `packages/builtin-tools/src/tools/WebSearchTool/UI.tsx` | 搜索结果的终端渲染组件 |
|
| UI 渲染 | `src/tools/WebSearchTool/UI.tsx` | 搜索结果的终端渲染组件 |
|
||||||
| 适配器接口 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/types.ts` | `WebSearchAdapter` 接口、`SearchResult`/`SearchOptions`/`SearchProgress` 类型 |
|
| 适配器接口 | `src/tools/WebSearchTool/adapters/types.ts` | `WebSearchAdapter` 接口、`SearchResult`/`SearchOptions`/`SearchProgress` 类型 |
|
||||||
| 适配器工厂 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/index.ts` | `createAdapter()` 工厂函数,选择后端 |
|
| 适配器工厂 | `src/tools/WebSearchTool/adapters/index.ts` | `createAdapter()` 工厂函数,选择后端 |
|
||||||
| API 适配器 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/apiAdapter.ts` | 封装原有 `queryModelWithStreaming` 逻辑,使用 server tool |
|
| API 适配器 | `src/tools/WebSearchTool/adapters/apiAdapter.ts` | 封装原有 `queryModelWithStreaming` 逻辑,使用 server tool |
|
||||||
| Bing 适配器 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 抓取 + 正则解析 |
|
| Bing 适配器 | `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 抓取 + 正则解析 |
|
||||||
| Brave 适配器 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/braveAdapter.ts` | Brave LLM Context API 适配与结果映射 |
|
| Brave 适配器 | `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 解析与工厂逻辑测试 |
|
| 单元测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts`, `src/tools/WebSearchTool/__tests__/braveAdapter*.test.ts`, `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` | 真实网络请求验证 |
|
| 集成测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts`, `src/tools/WebSearchTool/__tests__/braveAdapter.integration.ts` | 真实网络请求验证 |
|
||||||
|
|
||||||
### 2.3 数据流
|
### 2.3 数据流
|
||||||
|
|
||||||
@@ -176,13 +176,13 @@ interface SearchProgress {
|
|||||||
|
|
||||||
| 文件 | 职责 |
|
| 文件 | 职责 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `packages/builtin-tools/src/tools/WebSearchTool/WebSearchTool.ts` | 工具定义入口 |
|
| `src/tools/WebSearchTool/WebSearchTool.ts` | 工具定义入口 |
|
||||||
| `packages/builtin-tools/src/tools/WebSearchTool/prompt.ts` | 搜索工具 prompt |
|
| `src/tools/WebSearchTool/prompt.ts` | 搜索工具 prompt |
|
||||||
| `packages/builtin-tools/src/tools/WebSearchTool/UI.tsx` | 终端 UI 渲染 |
|
| `src/tools/WebSearchTool/UI.tsx` | 终端 UI 渲染 |
|
||||||
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/types.ts` | 适配器接口 |
|
| `src/tools/WebSearchTool/adapters/types.ts` | 适配器接口 |
|
||||||
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/index.ts` | 适配器工厂 |
|
| `src/tools/WebSearchTool/adapters/index.ts` | 适配器工厂 |
|
||||||
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/apiAdapter.ts` | API 服务端搜索适配器 |
|
| `src/tools/WebSearchTool/adapters/apiAdapter.ts` | API 服务端搜索适配器 |
|
||||||
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 解析适配器 |
|
| `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 解析适配器 |
|
||||||
| `packages/builtin-tools/src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 单元测试 (32 cases) |
|
| `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 单元测试 (32 cases) |
|
||||||
| `packages/builtin-tools/src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 集成测试 |
|
| `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 集成测试 |
|
||||||
| `src/tools.ts` | 工具注册 |
|
| `src/tools.ts` | 工具注册 |
|
||||||
|
|||||||
@@ -14,17 +14,17 @@ WORKFLOW_SCRIPTS 实现基于文件的多步自动化工作流。用户可以定
|
|||||||
|
|
||||||
| 模块 | 文件 | 状态 |
|
| 模块 | 文件 | 状态 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| WorkflowTool | `packages/builtin-tools/src/tools/WorkflowTool/WorkflowTool.ts` | **部分实现** — tool schema + 渲染完整,call 返回运行时缺失提示 |
|
| WorkflowTool | `src/tools/WorkflowTool/WorkflowTool.ts` | **Stub** — 空对象 |
|
||||||
| Workflow 权限 | `packages/builtin-tools/src/tools/WorkflowTool/WorkflowPermissionRequest.tsx` | **部分实现** — 权限请求组件 |
|
| Workflow 权限 | `src/tools/WorkflowTool/WorkflowPermissionRequest.ts` | **Stub** — 返回 null |
|
||||||
| 常量 | `packages/builtin-tools/src/tools/WorkflowTool/constants.ts` | **实现** — 工具名 + 目录名 + 文件扩展名常量 |
|
| 常量 | `src/tools/WorkflowTool/constants.ts` | **Stub** — 空工具名 |
|
||||||
| 命令创建 | `packages/builtin-tools/src/tools/WorkflowTool/createWorkflowCommand.ts` | **实现** — 扫描 .claude/workflows/ 目录创建 Command 对象 |
|
| 命令创建 | `src/tools/WorkflowTool/createWorkflowCommand.ts` | **Stub** — 空操作 |
|
||||||
| 捆绑工作流 | `packages/builtin-tools/src/tools/WorkflowTool/bundled/index.ts` | **实现** — 内置工作流初始化 |
|
| 捆绑工作流 | `src/tools/WorkflowTool/bundled/` | **缺失** — 目录不存在 |
|
||||||
| 本地工作流任务 | `src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts` | **Stub** — 类型 + 空操作 |
|
| 本地工作流任务 | `src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts` | **Stub** — 类型 + 空操作 |
|
||||||
| UI 任务组件 | `src/components/tasks/src/tasks/LocalWorkflowTask/` | **Stub** — 空导出 |
|
| UI 任务组件 | `src/components/tasks/src/tasks/LocalWorkflowTask/` | **Stub** — 空导出 |
|
||||||
| 详情对话框 | `src/components/tasks/WorkflowDetailDialog.ts` | **Stub** — 返回 null |
|
| 详情对话框 | `src/components/tasks/WorkflowDetailDialog.ts` | **Stub** — 返回 null |
|
||||||
| 任务注册 | `src/tasks.ts` | **布线** — 动态加载 |
|
| 任务注册 | `src/tasks.ts` | **布线** — 动态加载 |
|
||||||
| 工具注册 | `src/tools.ts` | **布线** — 动态加载 + bundled 工作流初始化 (行 131-134,235) |
|
| 工具注册 | `src/tools.ts` | **布线** — 包含 bundled 工作流初始化 |
|
||||||
| 命令注册 | `src/commands.ts` | **布线** — `/workflows` 命令 (行 93-95,395,460) |
|
| 命令注册 | `src/commands.ts` | **布线** — `/workflows` 命令 |
|
||||||
|
|
||||||
### 2.2 预期数据流
|
### 2.2 预期数据流
|
||||||
|
|
||||||
@@ -69,9 +69,13 @@ steps:
|
|||||||
|
|
||||||
| 优先级 | 模块 | 工作量 | 说明 |
|
| 优先级 | 模块 | 工作量 | 说明 |
|
||||||
|--------|------|--------|------|
|
|--------|------|--------|------|
|
||||||
| 1 | `WorkflowTool.ts` call 方法 | 中 | 实际工作流执行逻辑(当前返回运行时缺失提示) |
|
| 1 | `WorkflowTool.ts` | 大 | Schema 定义 + 多步执行引擎 |
|
||||||
| 2 | `LocalWorkflowTask.ts` | 大 | 步骤协调、kill/skip/retry |
|
| 2 | `bundled/index.js` | 中 | 内置工作流定义(initBundledWorkflows) |
|
||||||
| 3 | `WorkflowDetailDialog.ts` | 中 | 进度详情 UI |
|
| 3 | `createWorkflowCommand.ts` | 中 | 从文件解析创建命令对象 |
|
||||||
|
| 4 | `LocalWorkflowTask.ts` | 大 | 步骤协调、kill/skip/retry |
|
||||||
|
| 5 | `WorkflowDetailDialog.ts` | 中 | 进度详情 UI |
|
||||||
|
| 6 | `WorkflowPermissionRequest.ts` | 小 | 权限对话框 |
|
||||||
|
| 7 | `constants.ts` | 小 | 工具名常量 |
|
||||||
|
|
||||||
## 四、关键设计决策
|
## 四、关键设计决策
|
||||||
|
|
||||||
@@ -91,12 +95,11 @@ FEATURE_WORKFLOW_SCRIPTS=1 bun run dev
|
|||||||
|
|
||||||
| 文件 | 职责 |
|
| 文件 | 职责 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `packages/builtin-tools/src/tools/WorkflowTool/WorkflowTool.ts` | 工具定义(部分实现) |
|
| `src/tools/WorkflowTool/WorkflowTool.ts` | 工具定义(stub) |
|
||||||
| `packages/builtin-tools/src/tools/WorkflowTool/WorkflowPermissionRequest.tsx` | 权限请求组件 |
|
| `src/tools/WorkflowTool/WorkflowPermissionRequest.ts` | 权限对话框(stub) |
|
||||||
| `packages/builtin-tools/src/tools/WorkflowTool/constants.ts` | 常量定义 |
|
| `src/tools/WorkflowTool/constants.ts` | 常量(stub) |
|
||||||
| `packages/builtin-tools/src/tools/WorkflowTool/createWorkflowCommand.ts` | 命令创建(已实现) |
|
| `src/tools/WorkflowTool/createWorkflowCommand.ts` | 命令创建(stub) |
|
||||||
| `packages/builtin-tools/src/tools/WorkflowTool/bundled/index.ts` | 内置工作流初始化 |
|
|
||||||
| `src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts` | 任务协调(stub) |
|
| `src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts` | 任务协调(stub) |
|
||||||
| `src/components/tasks/WorkflowDetailDialog.ts` | 详情对话框(stub) |
|
| `src/components/tasks/WorkflowDetailDialog.ts` | 详情对话框(stub) |
|
||||||
| `src/tools.ts:131-134,235` | 工具注册 |
|
| `src/tools.ts:127-132` | 工具注册 |
|
||||||
| `src/commands.ts:93-95,395,460` | 命令注册 |
|
| `src/commands.ts:86-89` | 命令注册 |
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic
|
|||||||
|
|
||||||
`BUILD_TARGET` 等构建时常量在反编译版本中已被移除。`USER_TYPE` 通过 Bun 的 `--define` 或环境变量注入,Bun 会进行**常量折叠**——所有 `process.env.USER_TYPE === 'ant'` 在外部构建中直接变为 `false`,后续代码被 DCE 移除。但在反编译版本中,这些代码保留完整。
|
`BUILD_TARGET` 等构建时常量在反编译版本中已被移除。`USER_TYPE` 通过 Bun 的 `--define` 或环境变量注入,Bun 会进行**常量折叠**——所有 `process.env.USER_TYPE === 'ant'` 在外部构建中直接变为 `false`,后续代码被 DCE 移除。但在反编译版本中,这些代码保留完整。
|
||||||
|
|
||||||
`USER_TYPE === 'ant'` 在代码库中出现 **351+ 次**(跨 163 个文件),另有 `!== 'ant'` 59 次(跨 38 个文件),总计 **410+ 处引用**,控制着工具、命令、API、UI 等方方面面。
|
`USER_TYPE === 'ant'` 在代码库中出现 **377+ 次**(含 `=== 'ant'` 291 次、`(process.env.USER_TYPE) === 'ant'` 86 次),另有 `!== 'ant'` 53 次、其他引用约 35 次,总计 **465 处引用**,控制着工具、命令、API、UI 等方方面面。
|
||||||
|
|
||||||
## Ant-Only 工具
|
## Ant-Only 工具
|
||||||
|
|
||||||
@@ -25,10 +25,10 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic
|
|||||||
|
|
||||||
| 工具 | 代码位置 | 用途 |
|
| 工具 | 代码位置 | 用途 |
|
||||||
|------|---------|------|
|
|------|---------|------|
|
||||||
| **REPLTool** | `packages/builtin-tools/src/tools/REPLTool/` | 高级 REPL 模式——在 VM 中包装 Bash/Read/Edit/Glob/Grep/Agent 等工具 |
|
| **REPLTool** | `src/tools/REPLTool/` | 高级 REPL 模式——在 VM 中包装 Bash/Read/Edit/Glob/Grep/Agent 等工具 |
|
||||||
| **SuggestBackgroundPRTool** | `packages/builtin-tools/src/tools/SuggestBackgroundPRTool/` | 建议在后台创建 PR |
|
| **SuggestBackgroundPRTool** | `src/tools/SuggestBackgroundPRTool/` | 建议在后台创建 PR |
|
||||||
| **ConfigTool** | `packages/builtin-tools/src/tools/ConfigTool/` | 交互式配置编辑器,包含 Gates 标签页用于覆盖 GrowthBook flags |
|
| **ConfigTool** | `src/tools/ConfigTool/` | 交互式配置编辑器,包含 Gates 标签页用于覆盖 GrowthBook flags |
|
||||||
| **TungstenTool** | `packages/builtin-tools/src/tools/TungstenTool/` | 基于 tmux 的终端面板工具(反编译版中已 stub) |
|
| **TungstenTool** | `src/tools/TungstenTool/` | 基于 tmux 的终端面板工具(反编译版中已 stub) |
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/tools.ts 第 14-24 行——条件导入 + Dead Code Elimination 标记
|
// 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 */
|
/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
|
||||||
const REPLTool =
|
const REPLTool =
|
||||||
process.env.USER_TYPE === 'ant'
|
process.env.USER_TYPE === 'ant'
|
||||||
? require('@claude-code-best/builtin-tools/tools/REPLTool/REPLTool.js').REPLTool
|
? require('./tools/REPLTool/REPLTool.js').REPLTool
|
||||||
: null
|
: null
|
||||||
const SuggestBackgroundPRTool =
|
const SuggestBackgroundPRTool =
|
||||||
process.env.USER_TYPE === 'ant'
|
process.env.USER_TYPE === 'ant'
|
||||||
? require('@claude-code-best/builtin-tools/tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js')
|
? require('./tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js')
|
||||||
.SuggestBackgroundPRTool
|
.SuggestBackgroundPRTool
|
||||||
: null
|
: null
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ant-Only 命令
|
## Ant-Only 命令
|
||||||
|
|
||||||
`src/commands.ts` 注册了 **24+** 个仅在内部构建中可用的斜杠命令(`INTERNAL_ONLY_COMMANDS`,lines 267-295),在 `USER_TYPE === 'ant' && !IS_DEMO` 时才加载(line 400-401):
|
`src/commands.ts` 注册了 **28** 个仅在内部构建中可用的斜杠命令(`INTERNAL_ONLY_COMMANDS`,lines 225-254),在 `USER_TYPE === 'ant' && !IS_DEMO` 时才加载(line 343-345):
|
||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
<Accordion title="调试类">
|
<Accordion title="调试类">
|
||||||
@@ -74,7 +74,7 @@ const SuggestBackgroundPRTool =
|
|||||||
- `summary` — 生成摘要
|
- `summary` — 生成摘要
|
||||||
- `subscribePr` — 订阅 PR(需要 `KAIROS_GITHUB_WEBHOOKS` feature flag)
|
- `subscribePr` — 订阅 PR(需要 `KAIROS_GITHUB_WEBHOOKS` feature flag)
|
||||||
- `forceSnip` — 强制截断历史(需要 `HISTORY_SNIP` feature flag)
|
- `forceSnip` — 强制截断历史(需要 `HISTORY_SNIP` feature flag)
|
||||||
- `ultraplan` — 超级规划(需要 `ULTRAPLAN` feature flag,单独注册于 `commands.ts:396`)
|
- `ultraplan` — 超级规划(需要 `ULTRAPLAN` feature flag)
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="基础设施类">
|
<Accordion title="基础设施类">
|
||||||
- `backfillSessions` — 回填会话数据
|
- `backfillSessions` — 回填会话数据
|
||||||
|
|||||||
@@ -15,19 +15,17 @@ Claude Code 使用 Bun 打包器的 `bun:bundle` 模块提供编译时特性门
|
|||||||
import { feature } from 'bun:bundle'
|
import { feature } from 'bun:bundle'
|
||||||
|
|
||||||
const SleepTool = feature('PROACTIVE') || feature('KAIROS')
|
const SleepTool = feature('PROACTIVE') || feature('KAIROS')
|
||||||
? require('@claude-code-best/builtin-tools/tools/SleepTool/SleepTool.js').SleepTool
|
? require('./tools/SleepTool/SleepTool.js').SleepTool
|
||||||
: null
|
: null
|
||||||
```
|
```
|
||||||
|
|
||||||
在 Anthropic 的内部构建中,`feature()` 在打包时被求值——返回 `true` 的代码会被保留,返回 `false` 的代码会被 **Dead Code Elimination (DCE)** 彻底移除。
|
在 Anthropic 的内部构建中,`feature()` 在打包时被求值——返回 `true` 的代码会被保留,返回 `false` 的代码会被 **Dead Code Elimination (DCE)** 彻底移除。
|
||||||
|
|
||||||
在我们的反编译版本中,`feature` 从 `bun:bundle` 导入(声明在 `src/types/internal-modules.d.ts`),在运行时始终返回 `false`:
|
在我们的反编译版本中,这个函数被兜底为:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/types/internal-modules.d.ts
|
// src/entrypoints/cli.tsx 第 3 行
|
||||||
declare module 'bun:bundle' {
|
const feature = (_name: string) => false;
|
||||||
export function feature(name: string): boolean;
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
这意味着所有 88+ 个 feature flag 后的代码**在运行时永远不会执行**,但代码本身完整保留,可以阅读和分析。
|
这意味着所有 88+ 个 feature flag 后的代码**在运行时永远不会执行**,但代码本身完整保留,可以阅读和分析。
|
||||||
@@ -81,7 +79,7 @@ Feature flags 在代码中主要有三种使用模式:
|
|||||||
```typescript
|
```typescript
|
||||||
// src/tools.ts — 最常见的模式
|
// src/tools.ts — 最常见的模式
|
||||||
const MonitorTool = feature('MONITOR_TOOL')
|
const MonitorTool = feature('MONITOR_TOOL')
|
||||||
? require('@claude-code-best/builtin-tools/tools/MonitorTool/MonitorTool.js').MonitorTool
|
? require('./tools/MonitorTool/MonitorTool.js').MonitorTool
|
||||||
: null
|
: null
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ keywords: ["GrowthBook", "A/B 测试", "运行时门控", "tengu", "渐进式发
|
|||||||
|
|
||||||
## 集成架构
|
## 集成架构
|
||||||
|
|
||||||
GrowthBook 的完整实现位于 `src/services/analytics/growthbook.ts`(1258 行),工作流程如下:
|
GrowthBook 的完整实现位于 `src/services/analytics/growthbook.ts`(1156 行),工作流程如下:
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="启动时获取远程配置">
|
<Step title="启动时获取远程配置">
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ keywords: ["隐藏功能", "未公开功能", "秘密功能", "Claude Code 彩
|
|||||||
<Accordion title="VOICE_MODE:语音交互">
|
<Accordion title="VOICE_MODE:语音交互">
|
||||||
**门控**: `feature('VOICE_MODE')`
|
**门控**: `feature('VOICE_MODE')`
|
||||||
|
|
||||||
代码中存在语音输入模式的注册点,核心实现依赖 `audio-capture-napi` 包(已恢复):
|
代码中存在语音输入模式的注册点,但核心实现依赖于 `audio-napi` 包(在反编译版本中已 stub):
|
||||||
|
|
||||||
- 通过 `/voice` 命令激活
|
- 通过 `/voice` 命令激活
|
||||||
- "按住说话"(hold-to-talk)交互模式
|
- "按住说话"(hold-to-talk)交互模式
|
||||||
|
|||||||
@@ -64,27 +64,24 @@ Claude Code 从上到下分为五个层次,每一层职责清晰、边界分
|
|||||||
needsFollowUp ? continue : return { reason }
|
needsFollowUp ? continue : return { reason }
|
||||||
```
|
```
|
||||||
|
|
||||||
完整的状态机通过 `State` 类型(`src/query.ts:207`)在迭代间传递,包含 10 个字段(messages、autoCompactTracking、maxOutputTokensRecoveryCount 等)。
|
完整的状态机通过 `State` 类型(`src/query.ts:204`)在迭代间传递,包含 10 个字段(messages、autoCompactTracking、maxOutputTokensRecoveryCount 等)。
|
||||||
|
|
||||||
### 4. 工具层(`src/tools.ts` → `src/Tool.ts`)
|
### 4. 工具层(`src/tools.ts` → `src/Tool.ts`)
|
||||||
|
|
||||||
`getAllBaseTools()`(`src/tools.ts:195`)组装 50+ 工具列表,经过 `filterToolsByDenyRules()` 权限过滤后传给 API。
|
`getAllBaseTools()`(`src/tools.ts:191`)组装 50+ 工具列表,经过 `filterToolsByDenyRules()` 权限过滤后传给 API。
|
||||||
|
|
||||||
每个工具实现 `Tool<Input, Output, Progress>` 接口(`src/Tool.ts:368`),核心方法链:
|
每个工具实现 `Tool<Input, Output, Progress>` 接口(`src/Tool.ts:362`),核心方法链:
|
||||||
```
|
```
|
||||||
validateInput() → canUseTool()(UI 层)→ checkPermissions() → call() → ToolResult
|
validateInput() → canUseTool()(UI 层)→ checkPermissions() → call() → ToolResult
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. 通信层(`src/services/api/claude.ts`)
|
### 5. 通信层(`src/services/api/claude.ts`)
|
||||||
|
|
||||||
API 客户端支持 7 种 Provider:
|
API 客户端支持 4 种 Provider:
|
||||||
- **Anthropic Direct (firstParty)**:默认
|
- **Anthropic Direct**:默认
|
||||||
- **AWS Bedrock**:`ANTHROPIC_BEDROCK_BASE_URL`
|
- **AWS Bedrock**:`ANTHROPIC_BEDROCK_BASE_URL`
|
||||||
- **Google Vertex**:`ANTHROPIC_VERTEX_PROJECT_ID`
|
- **Google Vertex**:`ANTHROPIC_VERTEX_PROJECT_ID`
|
||||||
- **Foundry**:`ANTHROPIC_CODE_USE_FOUNDRY`
|
- **Azure**:通过自定义 base URL
|
||||||
- **OpenAI**:兼容层
|
|
||||||
- **Gemini**:兼容层
|
|
||||||
- **Grok (xAI)**:兼容层
|
|
||||||
|
|
||||||
`deps.callModel()` 发起流式请求,返回 `BetaRawMessageStreamEvent` 事件流。支持 Prompt Cache(`cache_control`)、thinking blocks、multi-turn tool use。
|
`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) │
|
│ 6. 通信层 (claude.ts → Anthropic API) │
|
||||||
│ 流式 HTTP, 支持 Bedrock/Vertex/Foundry 等 7 种 provider │
|
│ 流式 HTTP, 支持 Bedrock/Vertex/Azure 多 provider │
|
||||||
└─────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ AI 没有真正的"记忆",Claude Code 通过精心分层营造了这个幻觉
|
|||||||
|
|
||||||
### 3. 工具系统的权限双轨制
|
### 3. 工具系统的权限双轨制
|
||||||
|
|
||||||
`packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.ts` 展示了一个精巧的双重安全模型:
|
`src/tools/BashTool/shouldUseSandbox.ts` 展示了一个精巧的双重安全模型:
|
||||||
|
|
||||||
- **应用层**:权限规则决定"能不能执行"(白名单/黑名单/用户确认)
|
- **应用层**:权限规则决定"能不能执行"(白名单/黑名单/用户确认)
|
||||||
- **OS 层**:沙箱决定"执行时能做什么"(文件系统/网络/进程隔离)
|
- **OS 层**:沙箱决定"执行时能做什么"(文件系统/网络/进程隔离)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ ENABLE_LSP_TOOL=1 bun run dev
|
|||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────┐
|
||||||
│ LSP Tool │
|
│ LSP Tool │
|
||||||
│ packages/builtin-tools/src/tools/LSPTool/LSPTool.ts│
|
│ src/tools/LSPTool/LSPTool.ts │
|
||||||
│ (Claude 可调用的工具,9 种操作) │
|
│ (Claude 可调用的工具,9 种操作) │
|
||||||
└──────────────────────┬──────────────────────────────┘
|
└──────────────────────┬──────────────────────────────┘
|
||||||
│
|
│
|
||||||
@@ -128,10 +128,10 @@ LSP 服务器会异步推送 `textDocument/publishDiagnostics` 通知,经去
|
|||||||
| `src/services/lsp/config.ts` | 从插件加载 LSP 服务器配置 |
|
| `src/services/lsp/config.ts` | 从插件加载 LSP 服务器配置 |
|
||||||
| `src/services/lsp/LSPDiagnosticRegistry.ts` | 诊断信息注册、去重、容量限制 |
|
| `src/services/lsp/LSPDiagnosticRegistry.ts` | 诊断信息注册、去重、容量限制 |
|
||||||
| `src/services/lsp/passiveFeedback.ts` | 注册 `publishDiagnostics` 通知处理器 |
|
| `src/services/lsp/passiveFeedback.ts` | 注册 `publishDiagnostics` 通知处理器 |
|
||||||
| `packages/builtin-tools/src/tools/LSPTool/LSPTool.ts` | LSP Tool 实现(暴露给 Claude) |
|
| `src/tools/LSPTool/LSPTool.ts` | LSP Tool 实现(暴露给 Claude) |
|
||||||
| `packages/builtin-tools/src/tools/LSPTool/schemas.ts` | 输入 schema(9 种操作的 discriminated union) |
|
| `src/tools/LSPTool/schemas.ts` | 输入 schema(9 种操作的 discriminated union) |
|
||||||
| `packages/builtin-tools/src/tools/LSPTool/formatters.ts` | 各操作结果的格式化 |
|
| `src/tools/LSPTool/formatters.ts` | 各操作结果的格式化 |
|
||||||
| `packages/builtin-tools/src/tools/LSPTool/prompt.ts` | Tool 描述文本 |
|
| `src/tools/LSPTool/prompt.ts` | Tool 描述文本 |
|
||||||
| `src/utils/plugins/lspPluginIntegration.ts` | 从插件加载、验证、环境变量解析、作用域管理 |
|
| `src/utils/plugins/lspPluginIntegration.ts` | 从插件加载、验证、环境变量解析、作用域管理 |
|
||||||
|
|
||||||
## LSP Tool 支持的操作
|
## LSP Tool 支持的操作
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ Auto mode 可通过以下方式激活:
|
|||||||
|
|
||||||
### 进入时(Full Instructions)
|
### 进入时(Full Instructions)
|
||||||
|
|
||||||
注入到对话中的指令(`messages.ts:3481`):
|
注入到对话中的指令(`messages.ts:3464`):
|
||||||
|
|
||||||
> Auto mode is active. The user chose continuous, autonomous execution. You should:
|
> Auto mode is active. The user chose continuous, autonomous execution. You should:
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,19 +18,17 @@ keywords: ["权限模型", "Allow Ask Deny", "PermissionRule", "checkPermissions
|
|||||||
|
|
||||||
这些行为由 `PermissionResult` 类型定义(`src/utils/permissions/PermissionResult.ts`)。
|
这些行为由 `PermissionResult` 类型定义(`src/utils/permissions/PermissionResult.ts`)。
|
||||||
|
|
||||||
## 权限规则的来源
|
## 权限规则的五层来源
|
||||||
|
|
||||||
规则从 8 个来源汇聚(`PERMISSION_RULE_SOURCES`,`permissions.ts:109`),优先级从低到高(后者覆盖前者):
|
规则从 5 个来源汇聚(`PERMISSION_RULE_SOURCES`,`permissions.ts:109`),优先级从高到低:
|
||||||
|
|
||||||
```
|
```
|
||||||
1. userSettings — ~/.claude/settings.json(跨项目)
|
1. session — 用户在当前对话中手动授权("Always allow")
|
||||||
2. projectSettings — .claude/settings.json(团队共享)
|
2. cliArg — 命令行 --allow/--deny 参数
|
||||||
3. localSettings — .claude/settings.local.json(gitignored,个人覆盖)
|
3. command — Skill 工具的 allowedTools 白名单
|
||||||
4. flagSettings — --settings 命令行参数
|
4. projectSettings — .claude/settings.json(团队共享)
|
||||||
5. policySettings — 企业管理员下发的策略(用户不可覆盖)
|
5. userSettings — ~/.claude/settings.json(跨项目)
|
||||||
6. cliArg — 命令行 --allow/--deny 参数
|
6. policySettings — 企业管理员下发的策略(用户不可覆盖)
|
||||||
7. command — Skill 工具的 allowedTools 白名单
|
|
||||||
8. session — 用户在当前对话中手动授权("Always allow")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
每个来源维护三个数组:`alwaysAllowRules[source]`、`alwaysAskRules[source]`、`alwaysDenyRules[source]`。
|
每个来源维护三个数组:`alwaysAllowRules[source]`、`alwaysAskRules[source]`、`alwaysDenyRules[source]`。
|
||||||
@@ -67,7 +65,7 @@ MCP 工具使用 `getToolNameForPermissionCheck()` 获取匹配名称,支持
|
|||||||
|
|
||||||
**2. 命令模式匹配**(BashTool 的 `checkPermissions()`)
|
**2. 命令模式匹配**(BashTool 的 `checkPermissions()`)
|
||||||
|
|
||||||
BashTool 通过 `preparePermissionMatcher()`(`Tool.ts:520`)解析命令模式:
|
BashTool 通过 `preparePermissionMatcher()`(`Tool.ts:514`)解析命令模式:
|
||||||
```json
|
```json
|
||||||
{"tool": "Bash", "ruleContent": "git *"} → 匹配 "git commit -m 'fix'"
|
{"tool": "Bash", "ruleContent": "git *"} → 匹配 "git commit -m 'fix'"
|
||||||
```
|
```
|
||||||
@@ -122,9 +120,7 @@ Read/Edit/Write 工具通过 `getPath()` 提取文件路径,与 `ruleContent`
|
|||||||
|------|---------------------|---------|------|
|
|------|---------------------|---------|------|
|
||||||
| **Default** | `'default'` | 日常使用 | 敏感操作逐一确认 |
|
| **Default** | `'default'` | 日常使用 | 敏感操作逐一确认 |
|
||||||
| **Plan Mode** | `'plan'` | 探索阶段 | 只能读不能写(`isReadOnly()` 检查) |
|
| **Plan Mode** | `'plan'` | 探索阶段 | 只能读不能写(`isReadOnly()` 检查) |
|
||||||
| **Accept Edits** | `'acceptEdits'` | 快速迭代 | 工作区内文件编辑自动放行,其他操作仍需确认 |
|
| **Auto** | `'auto'` | 信任 AI | 通过 transcript classifier 自动决策 |
|
||||||
| **Don't Ask** | `'dontAsk'` | 减少打断 | 尽量自动决策,减少确认弹窗 |
|
|
||||||
| **Auto** | `'auto'` | 信任 AI | 通过 transcript classifier 自动决策(需 `TRANSCRIPT_CLASSIFIER` feature flag) |
|
|
||||||
| **Bypass** | `'bypassPermissions'` | 完全信任 | 所有操作自动放行(需显式 `--dangerously-skip-permissions`) |
|
| **Bypass** | `'bypassPermissions'` | 完全信任 | 所有操作自动放行(需显式 `--dangerously-skip-permissions`) |
|
||||||
|
|
||||||
Plan Mode 切换由 `EnterPlanModeTool.call()` 触发:
|
Plan Mode 切换由 `EnterPlanModeTool.call()` 触发:
|
||||||
@@ -147,8 +143,8 @@ context.setAppState(prev => ({
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const DENIAL_LIMITS = {
|
const DENIAL_LIMITS = {
|
||||||
maxConsecutive: 3, // 同一工具连续拒绝上限
|
maxDenialsPerTool: 3, // 同一工具连续拒绝上限
|
||||||
maxTotal: 20, // 总拒绝上限
|
cooldownPeriodMs: 30_000, // 冷却期 30 秒
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -166,12 +162,9 @@ const DENIAL_LIMITS = {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type PermissionUpdate =
|
type PermissionUpdate =
|
||||||
| { type: 'addRules', destination, rules, behavior }
|
| { type: 'addRule', behavior, rule, destination }
|
||||||
| { type: 'replaceRules', destination, rules, behavior }
|
| { type: 'removeRule', behavior, rule, destination }
|
||||||
| { type: 'removeRules', destination, rules, behavior }
|
| { type: 'setMode', mode, destination }
|
||||||
| { type: 'setMode', destination, mode }
|
|
||||||
| { type: 'addDirectories', destination, directories }
|
|
||||||
| { type: 'removeDirectories', destination, directories }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
当用户在 Ask 对话框中选择 "Always allow",系统调用 `persistPermissionUpdates()` 将规则写入对应层级的 settings 文件(project/user/managed),同时更新内存中的 `toolPermissionContext`。
|
当用户在 Ask 对话框中选择 "Always allow",系统调用 `persistPermissionUpdates()` 将规则写入对应层级的 settings 文件(project/user/managed),同时更新内存中的 `toolPermissionContext`。
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ keywords: ["Plan Mode", "计划模式", "EnterPlanMode", "ExitPlanMode", "prepar
|
|||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="EnterPlanMode — 进入计划模式">
|
<Step title="EnterPlanMode — 进入计划模式">
|
||||||
AI 自主判断(或用户触发)任务需要规划,调用 `EnterPlanModeTool`(`packages/builtin-tools/src/tools/EnterPlanModeTool/EnterPlanModeTool.ts:36`)。该工具需要**用户审批**(`checkPermissions` 返回 `ask`)。
|
AI 自主判断(或用户触发)任务需要规划,调用 `EnterPlanModeTool`(`src/tools/EnterPlanModeTool/EnterPlanModeTool.ts:36`)。该工具需要**用户审批**(`checkPermissions` 返回 `ask`)。
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="探索阶段 — 只读工具集">
|
<Step title="探索阶段 — 只读工具集">
|
||||||
权限模式切换为 `'plan'`,AI 只能使用 `isReadOnly()` 为 true 的工具(Read、Grep、Glob、Agent 等)。写操作被自动拒绝。
|
权限模式切换为 `'plan'`,AI 只能使用 `isReadOnly()` 为 true 的工具(Read、Grep、Glob、Agent 等)。写操作被自动拒绝。
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="ExitPlanMode — 提交方案审批">
|
<Step title="ExitPlanMode — 提交方案审批">
|
||||||
AI 完成探索后,调用 `ExitPlanModeV2Tool`(`packages/builtin-tools/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts:147`),将计划文件提交给用户审阅。这是第二个**需要用户审批**的节点。
|
AI 完成探索后,调用 `ExitPlanModeV2Tool`(`src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts:147`),将计划文件提交给用户审阅。这是第二个**需要用户审批**的节点。
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="恢复执行 — 全部工具权限">
|
<Step title="恢复执行 — 全部工具权限">
|
||||||
用户批准后,权限模式恢复为进入前的状态,AI 按计划执行。
|
用户批准后,权限模式恢复为进入前的状态,AI 按计划执行。
|
||||||
@@ -107,7 +107,7 @@ if (isTeammate()) {
|
|||||||
|
|
||||||
## 什么时候该用计划模式
|
## 什么时候该用计划模式
|
||||||
|
|
||||||
`EnterPlanModeTool` 的 Prompt(`packages/builtin-tools/src/tools/EnterPlanModeTool/prompt.ts`)定义了两套触发标准——外部版本更积极(鼓励规划),内部版本更克制(仅在真正模糊时使用):
|
`EnterPlanModeTool` 的 Prompt(`src/tools/EnterPlanModeTool/prompt.ts`)定义了两套触发标准——外部版本更积极(鼓励规划),内部版本更克制(仅在真正模糊时使用):
|
||||||
|
|
||||||
| 场景 | 外部版本 | 内部版本 |
|
| 场景 | 外部版本 | 内部版本 |
|
||||||
|------|---------|---------|
|
|------|---------|---------|
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ keywords: ["沙箱", "sandbox", "权限", "Bash", "PowerShell", "bubblewrap", "s
|
|||||||
5. 这条命令没有被显式排除
|
5. 这条命令没有被显式排除
|
||||||
6. 这次调用没有被允许以 `dangerouslyDisableSandbox` 绕过
|
6. 这次调用没有被允许以 `dangerouslyDisableSandbox` 绕过
|
||||||
|
|
||||||
对应入口在 `packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.ts` 和 `src/utils/sandbox/sandbox-adapter.ts`。
|
对应入口在 `src/tools/BashTool/shouldUseSandbox.ts` 和 `src/utils/sandbox/sandbox-adapter.ts`。
|
||||||
|
|
||||||
### 3. PowerShell 只在支持平台上走
|
### 3. PowerShell 只在支持平台上走
|
||||||
|
|
||||||
@@ -518,11 +518,11 @@ REPL / CLI 启动
|
|||||||
|
|
||||||
如果你想继续顺着源码深入,推荐按下面顺序看:
|
如果你想继续顺着源码深入,推荐按下面顺序看:
|
||||||
|
|
||||||
1. `packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.ts`
|
1. `src/tools/BashTool/shouldUseSandbox.ts`
|
||||||
2. `src/utils/Shell.ts`
|
2. `src/utils/Shell.ts`
|
||||||
3. `src/utils/sandbox/sandbox-adapter.ts`
|
3. `src/utils/sandbox/sandbox-adapter.ts`
|
||||||
4. `src/utils/permissions/permissions.ts`
|
4. `src/utils/permissions/permissions.ts`
|
||||||
5. `packages/builtin-tools/src/tools/BashTool/bashPermissions.ts`
|
5. `src/tools/BashTool/bashPermissions.ts`
|
||||||
6. `src/utils/permissions/pathValidation.ts`
|
6. `src/utils/permissions/pathValidation.ts`
|
||||||
7. `src/utils/permissions/filesystem.ts`
|
7. `src/utils/permissions/filesystem.ts`
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ Claude 的 System Prompt 中包含安全指令——这是"软性"约束,依
|
|||||||
| `deny` | 直接拒绝 | 匹配 deny 规则 |
|
| `deny` | 直接拒绝 | 匹配 deny 规则 |
|
||||||
| `ask` | 弹窗确认 | 未匹配任何规则 或 匹配 ask 规则 |
|
| `ask` | 弹窗确认 | 未匹配任何规则 或 匹配 ask 规则 |
|
||||||
|
|
||||||
以 BashTool 为例(`packages/builtin-tools/src/tools/BashTool/bashPermissions.ts`),`bashToolHasPermission()` 执行了极其细致的检查链:
|
以 BashTool 为例(`src/tools/BashTool/bashPermissions.ts`),`bashToolHasPermission()` 执行了极其细致的检查链:
|
||||||
|
|
||||||
1. **AST 安全解析**:用 tree-sitter 解析 bash AST,检测命令注入(`$()`、反引号等)
|
1. **AST 安全解析**:用 tree-sitter 解析 bash AST,检测命令注入(`$()`、反引号等)
|
||||||
2. **语义检查**:识别危险命令(`eval`、`exec`、`source` 等)
|
2. **语义检查**:识别危险命令(`eval`、`exec`、`source` 等)
|
||||||
@@ -169,7 +169,7 @@ Bash("rm -rf node_modules") → ⚠️ 需确认(不可逆)
|
|||||||
攻击:cd /malicious/dir && git status
|
攻击:cd /malicious/dir && git status
|
||||||
/malicious/dir 包含 bare repo + 恶意钩子
|
/malicious/dir 包含 bare repo + 恶意钩子
|
||||||
防御:bashToolHasPermission() 检测 cd + git 组合
|
防御:bashToolHasPermission() 检测 cd + git 组合
|
||||||
强制 require approval(packages/builtin-tools/src/tools/BashTool/bashPermissions.ts:2209)
|
强制 require approval(src/tools/BashTool/bashPermissions.ts:2209)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 场景3:管道注入
|
### 场景3:管道注入
|
||||||
@@ -178,5 +178,5 @@ Bash("rm -rf node_modules") → ⚠️ 需确认(不可逆)
|
|||||||
攻击:echo 'x' | xargs printf '%s' >> /etc/passwd
|
攻击:echo 'x' | xargs printf '%s' >> /etc/passwd
|
||||||
splitCommand 会剥离重定向,导致路径检查遗漏
|
splitCommand 会剥离重定向,导致路径检查遗漏
|
||||||
防御:即使管道段独立检查通过,仍对原始命令重新验证路径约束
|
防御:即使管道段独立检查通过,仍对原始命令重新验证路径约束
|
||||||
检查重定向目标中的危险模式(反引号、$())(packages/builtin-tools/src/tools/BashTool/bashPermissions.ts:1992-2056)
|
检查重定向目标中的危险模式(反引号、$())(bashPermissions.ts:1992-2056)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
|
|
||||||
## 背景
|
## 背景
|
||||||
|
|
||||||
- 命令入口只有 fast-path (`src/entrypoints/cli.tsx:272`)
|
- 命令入口只有 fast-path (`src/entrypoints/cli.tsx:249`)
|
||||||
- handler 是空的 (`src/cli/handlers/templateJobs.ts`)
|
- handler 是空的 (`src/cli/handlers/templateJobs.ts`)
|
||||||
- `markdownConfigLoader` 已把 `templates` 纳入配置目录 (`src/utils/markdownConfigLoader.ts:35`)
|
- `markdownConfigLoader` 已把 `templates` 纳入配置目录 (`src/utils/markdownConfigLoader.ts:29`)
|
||||||
- `query/stopHooks` 已预留 job classifier 链路 (`src/query/stopHooks.ts:103`)
|
- `query/stopHooks` 已预留 job classifier 链路 (`src/query/stopHooks.ts:103`)
|
||||||
- `jobs/classifier.ts` 仍是 stub (`src/jobs/classifier.ts`)
|
- `jobs/classifier.ts` 仍是 stub (`src/jobs/classifier.ts`)
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
- `src/assistant/sessionHistory.ts`
|
- `src/assistant/sessionHistory.ts`
|
||||||
- 真正 stub 的主要是:
|
- 真正 stub 的主要是:
|
||||||
- `src/assistant/sessionDiscovery.ts`
|
- `src/assistant/sessionDiscovery.ts`
|
||||||
- `src/assistant/AssistantSessionChooser.tsx`
|
- `src/assistant/AssistantSessionChooser.ts`
|
||||||
- `src/commands/assistant/assistant.tsx:7`
|
- `src/commands/assistant/assistant.ts:7`
|
||||||
- `src/assistant/index.ts`
|
- `src/assistant/index.ts`
|
||||||
|
|
||||||
## 分阶段实现
|
## 分阶段实现
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ Read 的 `maxResultSizeChars` 是 `Infinity`,但这并不意味着无限制输
|
|||||||
|
|
||||||
## FileRead:多模态文件读取引擎
|
## FileRead:多模态文件读取引擎
|
||||||
|
|
||||||
源码路径:`packages/builtin-tools/src/tools/FileReadTool/FileReadTool.ts`
|
源码路径:`src/tools/FileReadTool/FileReadTool.ts`
|
||||||
|
|
||||||
### 读取去重机制
|
### 读取去重机制
|
||||||
|
|
||||||
Read 工具有一个常被忽视但至关重要的**去重层**。当 AI 重复读取同一个文件的同一范围时,系统不会浪费 token 发送两份完整内容:
|
Read 工具有一个常被忽视但至关重要的**去重层**。当 AI 重复读取同一个文件的同一范围时,系统不会浪费 token 发送两份完整内容:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// FileReadTool.ts — 去重逻辑
|
// FileReadTool.ts:530-573 — 去重逻辑
|
||||||
const existingState = readFileState.get(fullFilePath)
|
const existingState = readFileState.get(fullFilePath)
|
||||||
if (existingState && !existingState.isPartialView && existingState.offset !== undefined) {
|
if (existingState && !existingState.isPartialView && existingState.offset !== undefined) {
|
||||||
const rangeMatch = existingState.offset === offset && existingState.limit === limit
|
const rangeMatch = existingState.offset === offset && existingState.limit === limit
|
||||||
@@ -83,7 +83,7 @@ Read 工具在 `validateInput()` 中设置了多层安全门:
|
|||||||
当文件不存在时,Read 不会只报一个 "file not found":
|
当文件不存在时,Read 不会只报一个 "file not found":
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// FileReadTool.ts
|
// FileReadTool.ts:639-647
|
||||||
const similarFilename = findSimilarFile(fullFilePath) // 相似扩展名
|
const similarFilename = findSimilarFile(fullFilePath) // 相似扩展名
|
||||||
const cwdSuggestion = await suggestPathUnderCwd(fullFilePath) // cwd 相对路径建议
|
const cwdSuggestion = await suggestPathUnderCwd(fullFilePath) // cwd 相对路径建议
|
||||||
// macOS 截图特殊处理:薄空格(U+202F) vs 普通空格
|
// macOS 截图特殊处理:薄空格(U+202F) vs 普通空格
|
||||||
@@ -94,7 +94,7 @@ const altPath = getAlternateScreenshotPath(fullFilePath)
|
|||||||
|
|
||||||
## FileEdit:精确字符串替换引擎
|
## FileEdit:精确字符串替换引擎
|
||||||
|
|
||||||
源码路径:`packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts` + `utils.ts`
|
源码路径:`src/tools/FileEditTool/FileEditTool.ts` + `utils.ts`
|
||||||
|
|
||||||
### 引号标准化:AI 无法输出的字符怎么办
|
### 引号标准化:AI 无法输出的字符怎么办
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ Edit 工具在 `validateInput()` 中检查两个条件:
|
|||||||
2. **文件未被外部修改**(`mtime` 未变,或全量读取时内容完全一致)
|
2. **文件未被外部修改**(`mtime` 未变,或全量读取时内容完全一致)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// FileEditTool.ts — Windows 特殊处理
|
// FileEditTool.ts:290-311 — Windows 特殊处理
|
||||||
const isFullRead = readTimestamp.offset === undefined && readTimestamp.limit === undefined
|
const isFullRead = readTimestamp.offset === undefined && readTimestamp.limit === undefined
|
||||||
if (isFullRead && fileContent === readTimestamp.content) {
|
if (isFullRead && fileContent === readTimestamp.content) {
|
||||||
// 内容不变,安全继续(Windows 云同步/杀毒可能改 mtime)
|
// 内容不变,安全继续(Windows 云同步/杀毒可能改 mtime)
|
||||||
@@ -157,7 +157,7 @@ const MAX_EDIT_FILE_SIZE = 1024 * 1024 * 1024 // 1 GiB
|
|||||||
|
|
||||||
## FileWrite:全量写入与创建
|
## FileWrite:全量写入与创建
|
||||||
|
|
||||||
源码路径:`packages/builtin-tools/src/tools/FileWriteTool/FileWriteTool.ts`
|
源码路径:`src/tools/FileWriteTool/FileWriteTool.ts`
|
||||||
|
|
||||||
Write 工具与 Edit 共享大部分基础设施(权限检查、mtime 校验、fileHistory 备份),但有两个关键差异:
|
Write 工具与 Edit 共享大部分基础设施(权限检查、mtime 校验、fileHistory 备份),但有两个关键差异:
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ Glob 默认把**最近修改的文件排在前面**。这不是默认的文件
|
|||||||
实际效果:AI 优先看到"活"的代码,而不是沉寂的历史文件
|
实际效果:AI 优先看到"活"的代码,而不是沉寂的历史文件
|
||||||
```
|
```
|
||||||
|
|
||||||
在 `packages/builtin-tools/src/tools/GlobTool/` 中,ripgrep 的输出在返回给 AI 前按 mtime 排序。
|
在 `src/tools/GlobTool/` 中,ripgrep 的输出在返回给 AI 前按 mtime 排序。
|
||||||
|
|
||||||
### ripgrep 的错误处理
|
### ripgrep 的错误处理
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ ripgrep 执行有专门的错误恢复链(`src/utils/ripgrep.ts`):
|
|||||||
|
|
||||||
## ToolSearch:在 50+ 工具中发现目标
|
## ToolSearch:在 50+ 工具中发现目标
|
||||||
|
|
||||||
当可用工具超过 50 个时(含 MCP 提供的外部工具),AI 可能不知道该用哪个。**ToolSearch**(`packages/builtin-tools/src/tools/ToolSearchTool/`)提供了工具发现机制。
|
当可用工具超过 50 个时(含 MCP 提供的外部工具),AI 可能不知道该用哪个。**ToolSearch**(`src/tools/ToolSearchTool/`)提供了工具发现机制。
|
||||||
|
|
||||||
### 搜索算法
|
### 搜索算法
|
||||||
|
|
||||||
@@ -139,14 +139,14 @@ function getDeferredToolsCacheKey(deferredTools: Tools): string {
|
|||||||
|
|
||||||
AI 的信息获取不局限于本地代码:
|
AI 的信息获取不局限于本地代码:
|
||||||
|
|
||||||
- **WebSearch**(`packages/builtin-tools/src/tools/WebSearchTool/`):调用 Anthropic API 的 `web_search_20250305` server tool 搜索互联网
|
- **WebSearch**(`src/tools/WebSearchTool/`):调用 Anthropic API 的 `web_search_20250305` server tool 搜索互联网
|
||||||
- **WebFetch**(`packages/builtin-tools/src/tools/WebFetchTool/`):抓取特定 URL 内容,转换为 Markdown 供 AI 阅读
|
- **WebFetch**(`src/tools/WebFetchTool/`):抓取特定 URL 内容,转换为 Markdown 供 AI 阅读
|
||||||
|
|
||||||
这让 AI 可以查阅文档、搜索 Stack Overflow、阅读 GitHub issue——和人类开发者的工作方式一致。
|
这让 AI 可以查阅文档、搜索 Stack Overflow、阅读 GitHub issue——和人类开发者的工作方式一致。
|
||||||
|
|
||||||
### WebSearch 实现机制
|
### WebSearch 实现机制
|
||||||
|
|
||||||
WebSearch 通过适配器模式支持三种搜索后端,由 `packages/builtin-tools/src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
|
WebSearch 通过适配器模式支持三种搜索后端,由 `src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
|
||||||
|
|
||||||
```
|
```
|
||||||
适配器架构:
|
适配器架构:
|
||||||
@@ -229,7 +229,7 @@ WebSearch 通过适配器模式支持三种搜索后端,由 `packages/builtin-
|
|||||||
|
|
||||||
### WebSearchTool 统一接口
|
### WebSearchTool 统一接口
|
||||||
|
|
||||||
`WebSearchTool`(`packages/builtin-tools/src/tools/WebSearchTool/WebSearchTool.ts`)是面向主循环的工具定义,所有 provider 均可使用(`isEnabled()` 始终返回 true)。它将适配器返回的 `SearchResult[]` 转换为内部 `Output` 格式,`mapToolResultToToolResultBlockParam` 将搜索结果格式化为带 markdown 超链接的文本,并附加 "REMINDER" 要求主模型在回复中包含 Sources。
|
`WebSearchTool`(`src/tools/WebSearchTool/WebSearchTool.ts`)是面向主循环的工具定义,所有 provider 均可使用(`isEnabled()` 始终返回 true)。它将适配器返回的 `SearchResult[]` 转换为内部 `Output` 格式,`mapToolResultToToolResultBlockParam` 将搜索结果格式化为带 markdown 超链接的文本,并附加 "REMINDER" 要求主模型在回复中包含 Sources。
|
||||||
|
|
||||||
### WebFetch 实现机制
|
### WebFetch 实现机制
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ WebFetch 是一个完整的 HTTP 客户端 + 内容处理管线:
|
|||||||
| **URL 验证** | `validateURL()` | 长度、协议、用户名密码、公网域名检查 |
|
| **URL 验证** | `validateURL()` | 长度、协议、用户名密码、公网域名检查 |
|
||||||
| **egress 检测** | `X-Proxy-Error: blocked-by-allowlist` | 检测企业代理拦截 |
|
| **egress 检测** | `X-Proxy-Error: blocked-by-allowlist` | 检测企业代理拦截 |
|
||||||
|
|
||||||
预批准域名(`packages/builtin-tools/src/tools/WebFetchTool/preapproved.ts`):
|
预批准域名(`src/tools/WebFetchTool/preapproved.ts`):
|
||||||
|
|
||||||
用户无需手动授权即可抓取的域名列表,包含 ~90 个主流技术文档站点(MDN、Python docs、React docs、AWS docs 等)。列表分为 hostname-only 和 path-prefix 两类,查找复杂度 O(1)。
|
用户无需手动授权即可抓取的域名列表,包含 ~90 个主流技术文档站点(MDN、Python docs、React docs、AWS docs 等)。列表分为 hostname-only 和 path-prefix 两类,查找复杂度 O(1)。
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ spawn(wrapped_command) ← 实际进程创建
|
|||||||
|
|
||||||
## 只读命令的判定:为什么 Read 免审批而 Bash 不一定
|
## 只读命令的判定:为什么 Read 免审批而 Bash 不一定
|
||||||
|
|
||||||
BashTool 的 `isReadOnly()` 方法(`packages/builtin-tools/src/tools/BashTool/BashTool.tsx:655`)决定一条命令是否被视为"只读":
|
BashTool 的 `isReadOnly()` 方法(`BashTool.tsx:437`)决定一条命令是否被视为"只读":
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
isReadOnly(input) {
|
isReadOnly(input) {
|
||||||
@@ -41,7 +41,7 @@ isReadOnly(input) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
判定逻辑基于 4 个命令集合(`BashTool.tsx:120-166`):
|
判定逻辑基于 4 个命令集合(`BashTool.tsx:60-78`):
|
||||||
|
|
||||||
| 集合 | 命令 | 性质 |
|
| 集合 | 命令 | 性质 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
@@ -53,7 +53,7 @@ isReadOnly(input) {
|
|||||||
对于复合命令(`ls dir && echo "---" && ls dir2`),系统拆分后逐段检查——**所有非中性段都必须属于上述集合**,整条命令才被视为只读。
|
对于复合命令(`ls dir && echo "---" && ls dir2`),系统拆分后逐段检查——**所有非中性段都必须属于上述集合**,整条命令才被视为只读。
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// BashTool.tsx — 简化的判定逻辑
|
// BashTool.tsx:95 — 简化的判定逻辑
|
||||||
for (const part of partsWithOperators) {
|
for (const part of partsWithOperators) {
|
||||||
if (BASH_SEMANTIC_NEUTRAL_COMMANDS.has(baseCommand)) continue // 跳过中性段
|
if (BASH_SEMANTIC_NEUTRAL_COMMANDS.has(baseCommand)) continue // 跳过中性段
|
||||||
if (!isPartSearch && !isPartRead && !isPartList) {
|
if (!isPartSearch && !isPartRead && !isPartList) {
|
||||||
@@ -64,7 +64,7 @@ for (const part of partsWithOperators) {
|
|||||||
|
|
||||||
## AST 安全解析:tree-sitter bash 解析
|
## AST 安全解析:tree-sitter bash 解析
|
||||||
|
|
||||||
`preparePermissionMatcher()`(`BashTool.tsx:663`)在权限检查前用 `parseForSecurity()` 解析命令结构:
|
`preparePermissionMatcher()`(`BashTool.tsx:445`)在权限检查前用 `parseForSecurity()` 解析命令结构:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async preparePermissionMatcher({ command }) {
|
async preparePermissionMatcher({ command }) {
|
||||||
@@ -92,14 +92,14 @@ getDefaultTimeoutMs()
|
|||||||
└── 最大上限:600,000ms(10 分钟,用户显式设置时)
|
└── 最大上限:600,000ms(10 分钟,用户显式设置时)
|
||||||
```
|
```
|
||||||
|
|
||||||
超时后系统不会直接杀进程——`ShellCommand`(`src/utils/ShellCommand.ts:144`)通过 `onTimeout` 回调通知调用方,由调用方决定是终止还是后台化。
|
超时后系统不会直接杀进程——`ShellCommand`(`src/utils/ShellCommand.ts:129`)通过 `onTimeout` 回调通知调用方,由调用方决定是终止还是后台化。
|
||||||
|
|
||||||
## 自动后台化
|
## 自动后台化
|
||||||
|
|
||||||
长时间运行的命令可以自动转为后台任务,不阻塞 AI 的 agentic loop:
|
长时间运行的命令可以自动转为后台任务,不阻塞 AI 的 agentic loop:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// BashTool.tsx:1158
|
// BashTool.tsx:880
|
||||||
const shouldAutoBackground = !isBackgroundTasksDisabled
|
const shouldAutoBackground = !isBackgroundTasksDisabled
|
||||||
&& isAutobackgroundingAllowed(command)
|
&& isAutobackgroundingAllowed(command)
|
||||||
```
|
```
|
||||||
@@ -148,7 +148,7 @@ Claude Code 为文件读写、代码搜索等操作提供了专用工具(Read
|
|||||||
| **并发安全** | `isConcurrencySafe()` 返回 `true` → 可并行执行 | Bash 命令可能有副作用,串行执行 |
|
| **并发安全** | `isConcurrencySafe()` 返回 `true` → 可并行执行 | Bash 命令可能有副作用,串行执行 |
|
||||||
| **安全审计** | 工具名精确匹配权限规则 | 需 AST 解析命令结构后匹配 |
|
| **安全审计** | 工具名精确匹配权限规则 | 需 AST 解析命令结构后匹配 |
|
||||||
|
|
||||||
`isConcurrencySafe()`(`BashTool.tsx:652`)是一个常被忽视但重要的设计——只有只读命令可以在 agentic loop 中并行执行,有副作用的命令必须串行,防止竞态条件。
|
`isConcurrencySafe()`(`BashTool.tsx:434`)是一个常被忽视但重要的设计——只有只读命令可以在 agentic loop 中并行执行,有副作用的命令必须串行,防止竞态条件。
|
||||||
|
|
||||||
## 进度反馈的流式设计
|
## 进度反馈的流式设计
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Claude Code 的任务管理并非单一系统,而是两个并存、按运行
|
|||||||
TodoWrite 本质是一个**全量替换**操作——每次调用传入完整的 `todos[]` 数组,完全覆盖之前的状态:
|
TodoWrite 本质是一个**全量替换**操作——每次调用传入完整的 `todos[]` 数组,完全覆盖之前的状态:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// packages/builtin-tools/src/tools/TodoWriteTool/TodoWriteTool.ts — call() 核心逻辑
|
// src/tools/TodoWriteTool/TodoWriteTool.ts — call() 核心逻辑
|
||||||
async call({ todos }, context) {
|
async call({ todos }, context) {
|
||||||
const todoKey = context.agentId ?? getSessionId()
|
const todoKey = context.agentId ?? getSessionId()
|
||||||
const oldTodos = appState.todos[todoKey] ?? []
|
const oldTodos = appState.todos[todoKey] ?? []
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ keywords: ["工具系统", "Tool 抽象", "AI 工具", "function calling", "buil
|
|||||||
|
|
||||||
## Tool 类型:35 个字段的统一接口
|
## Tool 类型:35 个字段的统一接口
|
||||||
|
|
||||||
所有工具都实现 `src/Tool.ts:368` 的 `Tool<Input, Output, Progress>` 类型。这不是一个 class,而是一个包含 35+ 字段的**结构化类型**(structural typing),任何满足该接口的对象就是一个工具:
|
所有工具都实现 `src/Tool.ts:362` 的 `Tool<Input, Output, Progress>` 类型。这不是一个 class,而是一个包含 35+ 字段的**结构化类型**(structural typing),任何满足该接口的对象就是一个工具:
|
||||||
|
|
||||||
### 核心四要素
|
### 核心四要素
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ keywords: ["工具系统", "Tool 抽象", "AI 工具", "function calling", "buil
|
|||||||
|
|
||||||
## 工具注册:`getTools()` 的分层组装
|
## 工具注册:`getTools()` 的分层组装
|
||||||
|
|
||||||
`src/tools.ts` 的 `getAllBaseTools()`(第 195 行)是工具注册的核心:
|
`src/tools.ts` 的 `getAllBaseTools()`(第 191 行)是工具注册的核心:
|
||||||
|
|
||||||
```
|
```
|
||||||
固定工具(始终可用):
|
固定工具(始终可用):
|
||||||
@@ -96,7 +96,7 @@ Ant-only 工具:
|
|||||||
← process.env.USER_TYPE === 'ant' ? [REPLTool, ConfigTool, TungstenTool]
|
← process.env.USER_TYPE === 'ant' ? [REPLTool, ConfigTool, TungstenTool]
|
||||||
```
|
```
|
||||||
|
|
||||||
`getTools()`(第 274 行)在 `getAllBaseTools()` 基础上应用权限过滤:
|
`getTools()`(第 269 行)在 `getAllBaseTools()` 基础上应用权限过滤:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const getTools = (permissionContext): Tools => {
|
export const getTools = (permissionContext): Tools => {
|
||||||
@@ -110,7 +110,7 @@ export const getTools = (permissionContext): Tools => {
|
|||||||
|
|
||||||
## `buildTool()` 工厂函数
|
## `buildTool()` 工厂函数
|
||||||
|
|
||||||
大多数工具通过 `buildTool()` 创建(`src/Tool.ts:789`),它是一个类型安全的构造器:
|
大多数工具通过 `buildTool()` 创建(`src/Tool.ts:721`),它是一个类型安全的构造器:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const BashTool: Tool<...> = buildTool({
|
export const BashTool: Tool<...> = buildTool({
|
||||||
|
|||||||
@@ -135,11 +135,9 @@
|
|||||||
"group": "运行模式",
|
"group": "运行模式",
|
||||||
"pages": [
|
"pages": [
|
||||||
"docs/features/kairos",
|
"docs/features/kairos",
|
||||||
"docs/features/channels",
|
|
||||||
"docs/features/voice-mode",
|
"docs/features/voice-mode",
|
||||||
"docs/features/bridge-mode",
|
"docs/features/bridge-mode",
|
||||||
"docs/features/remote-control-self-hosting",
|
"docs/features/remote-control-self-hosting",
|
||||||
"docs/features/acp-link",
|
|
||||||
"docs/features/proactive",
|
"docs/features/proactive",
|
||||||
"docs/features/ultraplan"
|
"docs/features/ultraplan"
|
||||||
]
|
]
|
||||||
|
|||||||
57
package.json
57
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"version": "1.5.0",
|
"version": "1.4.1",
|
||||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||||
@@ -37,7 +37,6 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"scripts/postinstall.cjs",
|
"scripts/postinstall.cjs",
|
||||||
"scripts/run-parallel.mjs",
|
|
||||||
"scripts/setup-chrome-mcp.mjs"
|
"scripts/setup-chrome-mcp.mjs"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -73,20 +72,20 @@
|
|||||||
"@ant/computer-use-mcp": "workspace:*",
|
"@ant/computer-use-mcp": "workspace:*",
|
||||||
"@ant/computer-use-swift": "workspace:*",
|
"@ant/computer-use-swift": "workspace:*",
|
||||||
"@anthropic-ai/bedrock-sdk": "^0.26.4",
|
"@anthropic-ai/bedrock-sdk": "^0.26.4",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.114",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
|
||||||
"@anthropic-ai/foundry-sdk": "^0.2.3",
|
"@anthropic-ai/foundry-sdk": "^0.2.3",
|
||||||
"@anthropic-ai/mcpb": "^2.1.2",
|
"@anthropic-ai/mcpb": "^2.1.2",
|
||||||
"@anthropic-ai/sandbox-runtime": "^0.0.44",
|
"@anthropic-ai/sandbox-runtime": "^0.0.44",
|
||||||
"@anthropic-ai/sdk": "^0.80.0",
|
"@anthropic-ai/sdk": "^0.80.0",
|
||||||
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
||||||
"@anthropic/ink": "workspace:*",
|
"@anthropic/ink": "workspace:*",
|
||||||
"@aws-sdk/client-bedrock": "^3.1032.0",
|
"@aws-sdk/client-bedrock": "^3.1020.0",
|
||||||
"@aws-sdk/client-bedrock-runtime": "^3.1032.0",
|
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
|
||||||
"@aws-sdk/client-sts": "^3.1032.0",
|
"@aws-sdk/client-sts": "^3.1020.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.972.32",
|
"@aws-sdk/credential-provider-node": "^3.972.28",
|
||||||
"@aws-sdk/credential-providers": "^3.1032.0",
|
"@aws-sdk/credential-providers": "^3.1020.0",
|
||||||
"@azure/identity": "^4.13.1",
|
"@azure/identity": "^4.13.1",
|
||||||
"@biomejs/biome": "^2.4.12",
|
"@biomejs/biome": "^2.4.10",
|
||||||
"@claude-code-best/agent-tools": "workspace:*",
|
"@claude-code-best/agent-tools": "workspace:*",
|
||||||
"@claude-code-best/builtin-tools": "workspace:*",
|
"@claude-code-best/builtin-tools": "workspace:*",
|
||||||
"@claude-code-best/mcp-client": "workspace:*",
|
"@claude-code-best/mcp-client": "workspace:*",
|
||||||
@@ -97,7 +96,7 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"@opentelemetry/api": "^1.9.1",
|
"@opentelemetry/api": "^1.9.1",
|
||||||
"@opentelemetry/api-logs": "^0.214.0",
|
"@opentelemetry/api-logs": "^0.214.0",
|
||||||
"@opentelemetry/core": "^2.7.0",
|
"@opentelemetry/core": "^2.6.1",
|
||||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0",
|
"@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0",
|
||||||
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
|
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
|
||||||
"@opentelemetry/exporter-logs-otlp-proto": "^0.214.0",
|
"@opentelemetry/exporter-logs-otlp-proto": "^0.214.0",
|
||||||
@@ -108,14 +107,14 @@
|
|||||||
"@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0",
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.214.0",
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.214.0",
|
||||||
"@opentelemetry/resources": "^2.7.0",
|
"@opentelemetry/resources": "^2.6.1",
|
||||||
"@opentelemetry/sdk-logs": "^0.214.0",
|
"@opentelemetry/sdk-logs": "^0.214.0",
|
||||||
"@opentelemetry/sdk-metrics": "^2.7.0",
|
"@opentelemetry/sdk-metrics": "^2.6.1",
|
||||||
"@opentelemetry/sdk-trace-base": "^2.7.0",
|
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
||||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||||
"@sentry/node": "^10.49.0",
|
"@sentry/node": "^10.47.0",
|
||||||
"@smithy/core": "^3.23.15",
|
"@smithy/core": "^3.23.13",
|
||||||
"@smithy/node-http-handler": "^4.5.3",
|
"@smithy/node-http-handler": "^4.5.1",
|
||||||
"@types/bun": "^1.3.12",
|
"@types/bun": "^1.3.12",
|
||||||
"@types/cacache": "^20.0.1",
|
"@types/cacache": "^20.0.1",
|
||||||
"@types/he": "^1.2.3",
|
"@types/he": "^1.2.3",
|
||||||
@@ -137,7 +136,7 @@
|
|||||||
"asciichart": "^1.5.25",
|
"asciichart": "^1.5.25",
|
||||||
"audio-capture-napi": "workspace:*",
|
"audio-capture-napi": "workspace:*",
|
||||||
"auto-bind": "^5.0.1",
|
"auto-bind": "^5.0.1",
|
||||||
"axios": "^1.15.0",
|
"axios": "^1.14.0",
|
||||||
"bidi-js": "^1.0.3",
|
"bidi-js": "^1.0.3",
|
||||||
"cacache": "^20.0.4",
|
"cacache": "^20.0.4",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
@@ -152,7 +151,7 @@
|
|||||||
"execa": "^9.6.1",
|
"execa": "^9.6.1",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"figures": "^6.1.0",
|
"figures": "^6.1.0",
|
||||||
"fuse.js": "^7.3.0",
|
"fuse.js": "^7.1.0",
|
||||||
"get-east-asian-width": "^1.5.0",
|
"get-east-asian-width": "^1.5.0",
|
||||||
"google-auth-library": "^10.6.2",
|
"google-auth-library": "^10.6.2",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
@@ -162,21 +161,21 @@
|
|||||||
"image-processor-napi": "workspace:*",
|
"image-processor-napi": "workspace:*",
|
||||||
"indent-string": "^5.0.0",
|
"indent-string": "^5.0.0",
|
||||||
"jsonc-parser": "^3.3.1",
|
"jsonc-parser": "^3.3.1",
|
||||||
"knip": "^6.4.1",
|
"knip": "^6.1.1",
|
||||||
"lodash-es": "^4.18.1",
|
"lodash-es": "^4.17.23",
|
||||||
"lru-cache": "^11.3.5",
|
"lru-cache": "^11.2.7",
|
||||||
"marked": "^17.0.6",
|
"marked": "^17.0.5",
|
||||||
"modifiers-napi": "workspace:*",
|
"modifiers-napi": "workspace:*",
|
||||||
"openai": "^6.34.0",
|
"openai": "^6.33.0",
|
||||||
"p-map": "^7.0.4",
|
"p-map": "^7.0.4",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"plist": "^3.1.0",
|
"plist": "^3.1.0",
|
||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.4",
|
||||||
"react-compiler-runtime": "^1.0.0",
|
"react-compiler-runtime": "^1.0.0",
|
||||||
"react-reconciler": "^0.33.0",
|
"react-reconciler": "^0.33.0",
|
||||||
"rollup": "^4.60.2",
|
"rollup": "^4.60.1",
|
||||||
"semver": "^7.7.4",
|
"semver": "^7.7.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"shell-quote": "^1.8.3",
|
"shell-quote": "^1.8.3",
|
||||||
@@ -185,10 +184,10 @@
|
|||||||
"strip-ansi": "^7.2.0",
|
"strip-ansi": "^7.2.0",
|
||||||
"supports-hyperlinks": "^4.4.0",
|
"supports-hyperlinks": "^4.4.0",
|
||||||
"tree-kill": "^1.2.2",
|
"tree-kill": "^1.2.2",
|
||||||
"turndown": "^7.2.4",
|
"turndown": "^7.2.2",
|
||||||
"type-fest": "^5.6.0",
|
"type-fest": "^5.5.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.2",
|
||||||
"undici": "^7.25.0",
|
"undici": "^7.24.6",
|
||||||
"url-handler-napi": "workspace:*",
|
"url-handler-napi": "workspace:*",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.1",
|
||||||
"vite": "^8.0.8",
|
"vite": "^8.0.8",
|
||||||
|
|||||||
34
packages/acp-link/.gitignore
vendored
34
packages/acp-link/.gitignore
vendored
@@ -1,34 +0,0 @@
|
|||||||
# dependencies (bun install)
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
# output
|
|
||||||
out
|
|
||||||
dist
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# code coverage
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# logs
|
|
||||||
logs
|
|
||||||
_.log
|
|
||||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# caches
|
|
||||||
.eslintcache
|
|
||||||
.cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# IntelliJ based IDEs
|
|
||||||
.idea
|
|
||||||
|
|
||||||
# Finder (MacOS) folder config
|
|
||||||
.DS_Store
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
# acp-link
|
|
||||||
|
|
||||||
ACP proxy server that bridges WebSocket clients to ACP (Agent Client Protocol) agents.
|
|
||||||
|
|
||||||
> Source code adapted from [chrome-acp](https://github.com/Areo-Joe/chrome-acp).
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### From source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From monorepo root
|
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Via global install
|
|
||||||
acp-link /path/to/agent
|
|
||||||
|
|
||||||
# Via source
|
|
||||||
bun src/cli/bin.ts /path/to/agent
|
|
||||||
```
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Basic usage
|
|
||||||
acp-link /path/to/agent
|
|
||||||
|
|
||||||
# With custom port and host
|
|
||||||
acp-link --port 9000 --host 0.0.0.0 /path/to/agent
|
|
||||||
|
|
||||||
# With debug logging
|
|
||||||
acp-link --debug /path/to/agent
|
|
||||||
|
|
||||||
# Enable HTTPS with self-signed certificate
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## CLI Reference
|
|
||||||
|
|
||||||
```
|
|
||||||
USAGE
|
|
||||||
acp-link [--port value] [--host value] [--debug] [--no-auth] [--https] [--group value] <command>...
|
|
||||||
acp-link --help
|
|
||||||
acp-link --version
|
|
||||||
|
|
||||||
FLAGS
|
|
||||||
[--port] Port to listen on [default = 9315]
|
|
||||||
[--host] Host to bind to [default = localhost]
|
|
||||||
[--debug] Enable debug logging to file
|
|
||||||
[--no-auth] Disable authentication (dangerous)
|
|
||||||
[--https] Enable HTTPS with self-signed cert
|
|
||||||
[--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
|
|
||||||
|
|
||||||
ARGUMENTS
|
|
||||||
command... Agent command followed by its arguments
|
|
||||||
```
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
1. Listens for WebSocket connections from clients
|
|
||||||
2. When a "connect" message is received, spawns the configured ACP agent as a subprocess
|
|
||||||
3. Bridges messages between the WebSocket (client) and stdin/stdout (agent via ACP protocol)
|
|
||||||
4. Supports session management: create, load, resume, list sessions
|
|
||||||
5. Handles permission approval flow and heartbeat keepalive
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
By default, a random token is auto-generated on startup. Pass it as a query parameter:
|
|
||||||
|
|
||||||
```
|
|
||||||
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.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "acp-link",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"description": "ACP proxy server that bridges WebSocket clients to ACP agents",
|
|
||||||
"author": "claude-code-best",
|
|
||||||
"type": "module",
|
|
||||||
"main": "./dist/server.js",
|
|
||||||
"types": "./dist/server.d.ts",
|
|
||||||
"bin": {
|
|
||||||
"acp-link": "dist/cli/bin.js"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"dev": "ACP_RCS_URL=http://localhost:3000 ACP_RCS_TOKEN=test-my-key bun run src/cli/bin.ts ccb-bun -- --acp",
|
|
||||||
"prepublishOnly": "bun run build"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/selfsigned": "^2.0.4",
|
|
||||||
"@types/ws": "^8.18.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@agentclientprotocol/sdk": "^0.19.0",
|
|
||||||
"@hono/node-server": "^1.13.8",
|
|
||||||
"@hono/node-ws": "^1.0.5",
|
|
||||||
"@stricli/auto-complete": "^1.2.4",
|
|
||||||
"@stricli/core": "^1.2.4",
|
|
||||||
"hono": "^4.7.0",
|
|
||||||
"pino": "^10.3.0",
|
|
||||||
"pino-pretty": "^13.1.3",
|
|
||||||
"selfsigned": "^5.5.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
|
||||||
import { getLanIPs } from "../cert.js";
|
|
||||||
|
|
||||||
describe("getLanIPs", () => {
|
|
||||||
test("returns an array", () => {
|
|
||||||
const ips = getLanIPs();
|
|
||||||
expect(Array.isArray(ips)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns only IPv4 addresses", () => {
|
|
||||||
const ips = getLanIPs();
|
|
||||||
for (const ip of ips) {
|
|
||||||
// IPv4 format: x.x.x.x
|
|
||||||
expect(ip).toMatch(/^\d+\.\d+\.\d+\.\d+$/);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not include loopback addresses", () => {
|
|
||||||
const ips = getLanIPs();
|
|
||||||
expect(ips).not.toContain("127.0.0.1");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("may be empty in isolated environments", () => {
|
|
||||||
// This test just ensures it doesn't throw
|
|
||||||
const ips = getLanIPs();
|
|
||||||
expect(ips.length).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
|
||||||
import type { ServerConfig } from "../server.js";
|
|
||||||
|
|
||||||
describe("Server HTTP endpoints", () => {
|
|
||||||
test("package.json has correct bin and main entries", async () => {
|
|
||||||
const pkg = await import("../../package.json", { with: { type: "json" } });
|
|
||||||
expect(pkg.default.name).toBe("acp-link");
|
|
||||||
expect(pkg.default.main).toBe("./dist/server.js");
|
|
||||||
expect(pkg.default.bin).toBeDefined();
|
|
||||||
expect(pkg.default.bin["acp-link"]).toBe("dist/cli/bin.js");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ServerConfig interface accepts all expected fields", () => {
|
|
||||||
const config: ServerConfig = {
|
|
||||||
port: 9315,
|
|
||||||
host: "localhost",
|
|
||||||
command: "echo",
|
|
||||||
args: [],
|
|
||||||
cwd: "/tmp",
|
|
||||||
debug: false,
|
|
||||||
token: "test-token",
|
|
||||||
https: false,
|
|
||||||
};
|
|
||||||
expect(config.port).toBe(9315);
|
|
||||||
expect(config.token).toBe("test-token");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ServerConfig allows optional fields to be omitted", () => {
|
|
||||||
const config: ServerConfig = {
|
|
||||||
port: 9315,
|
|
||||||
host: "localhost",
|
|
||||||
command: "echo",
|
|
||||||
args: [],
|
|
||||||
cwd: "/tmp",
|
|
||||||
};
|
|
||||||
expect(config.debug).toBeUndefined();
|
|
||||||
expect(config.token).toBeUndefined();
|
|
||||||
expect(config.https).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("WebSocket message types", () => {
|
|
||||||
const clientMessageTypes = [
|
|
||||||
"connect",
|
|
||||||
"disconnect",
|
|
||||||
"new_session",
|
|
||||||
"prompt",
|
|
||||||
"permission_response",
|
|
||||||
"cancel",
|
|
||||||
"set_session_model",
|
|
||||||
"list_sessions",
|
|
||||||
"load_session",
|
|
||||||
"resume_session",
|
|
||||||
"ping",
|
|
||||||
];
|
|
||||||
|
|
||||||
test("all client message types are recognized", () => {
|
|
||||||
expect(clientMessageTypes.length).toBe(11);
|
|
||||||
expect(clientMessageTypes).toContain("ping");
|
|
||||||
expect(clientMessageTypes).toContain("connect");
|
|
||||||
expect(clientMessageTypes).toContain("cancel");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Heartbeat constants", () => {
|
|
||||||
test("PERMISSION_TIMEOUT_MS is 5 minutes", () => {
|
|
||||||
const PERMISSION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
||||||
expect(PERMISSION_TIMEOUT_MS).toBe(300_000);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("HEARTBEAT_INTERVAL_MS is 30 seconds", () => {
|
|
||||||
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
||||||
expect(HEARTBEAT_INTERVAL_MS).toBe(30_000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
|
||||||
import { isRequest, isResponse, isNotification } from "../types.js";
|
|
||||||
import type { JsonRpcRequest, JsonRpcResponse, JsonRpcNotification } from "../types.js";
|
|
||||||
|
|
||||||
describe("isRequest", () => {
|
|
||||||
test("returns true for a valid JSON-RPC request", () => {
|
|
||||||
const msg: JsonRpcRequest = { jsonrpc: "2.0", id: 1, method: "test" };
|
|
||||||
expect(isRequest(msg)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns true for request with params", () => {
|
|
||||||
const msg = { jsonrpc: "2.0" as const, id: "abc", method: "test", params: { x: 1 } };
|
|
||||||
expect(isRequest(msg)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false for response (no method)", () => {
|
|
||||||
const msg: JsonRpcResponse = { jsonrpc: "2.0", id: 1, result: {} };
|
|
||||||
expect(isRequest(msg)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false for notification (no id)", () => {
|
|
||||||
const msg: JsonRpcNotification = { jsonrpc: "2.0", method: "notify" };
|
|
||||||
expect(isRequest(msg)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isResponse", () => {
|
|
||||||
test("returns true for a valid JSON-RPC response with result", () => {
|
|
||||||
const msg: JsonRpcResponse = { jsonrpc: "2.0", id: 1, result: "ok" };
|
|
||||||
expect(isResponse(msg)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns true for a valid JSON-RPC error response", () => {
|
|
||||||
const msg: JsonRpcResponse = { jsonrpc: "2.0", id: 2, error: { code: -32600, message: "bad" } };
|
|
||||||
expect(isResponse(msg)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false for request (has method)", () => {
|
|
||||||
const msg: JsonRpcRequest = { jsonrpc: "2.0", id: 1, method: "test" };
|
|
||||||
expect(isResponse(msg)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false for notification", () => {
|
|
||||||
const msg: JsonRpcNotification = { jsonrpc: "2.0", method: "notify" };
|
|
||||||
expect(isResponse(msg)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isNotification", () => {
|
|
||||||
test("returns true for a valid JSON-RPC notification", () => {
|
|
||||||
const msg: JsonRpcNotification = { jsonrpc: "2.0", method: "update" };
|
|
||||||
expect(isNotification(msg)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns true for notification with params", () => {
|
|
||||||
const msg = { jsonrpc: "2.0" as const, method: "progress", params: { pct: 50 } };
|
|
||||||
expect(isNotification(msg)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false for request (has id)", () => {
|
|
||||||
const msg: JsonRpcRequest = { jsonrpc: "2.0", id: 1, method: "test" };
|
|
||||||
expect(isNotification(msg)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false for response (no method)", () => {
|
|
||||||
const msg: JsonRpcResponse = { jsonrpc: "2.0", id: 1, result: null };
|
|
||||||
expect(isNotification(msg)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
/**
|
|
||||||
* Self-signed certificate generation for HTTPS support
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { X509Certificate } from "node:crypto";
|
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
||||||
import { homedir, networkInterfaces } from "node:os";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { generate } from "selfsigned";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all LAN IPv4 addresses
|
|
||||||
*/
|
|
||||||
export function getLanIPs(): string[] {
|
|
||||||
const ips: string[] = [];
|
|
||||||
const nets = networkInterfaces();
|
|
||||||
for (const name of Object.keys(nets)) {
|
|
||||||
for (const net of nets[name] || []) {
|
|
||||||
// Skip internal (loopback) and non-IPv4 addresses
|
|
||||||
if (!net.internal && net.family === "IPv4") {
|
|
||||||
ips.push(net.address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ips;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract IP addresses from certificate's Subject Alternative Name (SAN)
|
|
||||||
* SAN format: "IP Address:192.168.1.100, IP Address:127.0.0.1, DNS:localhost"
|
|
||||||
*/
|
|
||||||
function extractSanIPs(x509: X509Certificate): string[] {
|
|
||||||
const san = x509.subjectAltName;
|
|
||||||
if (!san) return [];
|
|
||||||
|
|
||||||
const ips: string[] = [];
|
|
||||||
// Parse "IP Address:x.x.x.x" entries from SAN string
|
|
||||||
const parts = san.split(", ");
|
|
||||||
for (const part of parts) {
|
|
||||||
const match = part.match(/^IP Address:(.+)$/);
|
|
||||||
if (match && match[1]) {
|
|
||||||
ips.push(match[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ips;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CERT_DIR = join(homedir(), ".acp-proxy");
|
|
||||||
const KEY_PATH = join(CERT_DIR, "key.pem");
|
|
||||||
const CERT_PATH = join(CERT_DIR, "cert.pem");
|
|
||||||
|
|
||||||
// Certificate validity in days
|
|
||||||
const CERT_VALIDITY_DAYS = 365;
|
|
||||||
|
|
||||||
export interface TlsOptions {
|
|
||||||
key: string;
|
|
||||||
cert: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or generate self-signed certificate
|
|
||||||
* Certificates are cached in ~/.acp-proxy/
|
|
||||||
*/
|
|
||||||
export async function getOrCreateCertificate(): Promise<TlsOptions> {
|
|
||||||
// Ensure directory exists
|
|
||||||
if (!existsSync(CERT_DIR)) {
|
|
||||||
mkdirSync(CERT_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if certificates already exist and are still valid
|
|
||||||
if (existsSync(KEY_PATH) && existsSync(CERT_PATH)) {
|
|
||||||
const certPem = readFileSync(CERT_PATH, "utf-8");
|
|
||||||
const keyPem = readFileSync(KEY_PATH, "utf-8");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const x509 = new X509Certificate(certPem);
|
|
||||||
const validTo = new Date(x509.validTo);
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
// Check if cert is expired or will expire within 7 days
|
|
||||||
const daysUntilExpiry = Math.floor((validTo.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
||||||
|
|
||||||
if (daysUntilExpiry <= 7) {
|
|
||||||
// Certificate expired or expiring soon
|
|
||||||
console.log(`⚠️ Certificate ${daysUntilExpiry <= 0 ? "expired" : `expires in ${daysUntilExpiry} days`}, regenerating...`);
|
|
||||||
} else {
|
|
||||||
// Check if current LAN IPs are in the certificate's SAN
|
|
||||||
const currentLanIPs = getLanIPs();
|
|
||||||
const certSanIPs = extractSanIPs(x509);
|
|
||||||
|
|
||||||
// Check if all current LAN IPs are covered by the certificate
|
|
||||||
const missingIPs = currentLanIPs.filter(ip => !certSanIPs.includes(ip));
|
|
||||||
|
|
||||||
if (missingIPs.length === 0) {
|
|
||||||
console.log(`🔐 Using existing certificate from ${CERT_DIR}`);
|
|
||||||
console.log(` Valid for ${daysUntilExpiry} more days`);
|
|
||||||
return { key: keyPem, cert: certPem };
|
|
||||||
}
|
|
||||||
|
|
||||||
// LAN IP changed, regenerate
|
|
||||||
console.log(`⚠️ LAN IP changed (missing: ${missingIPs.join(", ")}), regenerating certificate...`);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Failed to parse certificate, regenerate
|
|
||||||
console.log(`⚠️ Invalid certificate, regenerating...`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new self-signed certificate
|
|
||||||
console.log(`🔐 Generating self-signed certificate...`);
|
|
||||||
|
|
||||||
const attrs = [{ name: "commonName", value: "ACP Proxy Server" }];
|
|
||||||
|
|
||||||
// Calculate expiry date
|
|
||||||
const notAfterDate = new Date();
|
|
||||||
notAfterDate.setDate(notAfterDate.getDate() + CERT_VALIDITY_DAYS);
|
|
||||||
|
|
||||||
// Build altNames: localhost + loopback + all LAN IPs
|
|
||||||
const altNames: Array<{ type: 1 | 2 | 6 | 7; value?: string; ip?: string }> = [
|
|
||||||
{ type: 2, value: "localhost" },
|
|
||||||
{ type: 7, ip: "127.0.0.1" },
|
|
||||||
{ type: 7, ip: "::1" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Add all current LAN IPs
|
|
||||||
const lanIPs = getLanIPs();
|
|
||||||
for (const ip of lanIPs) {
|
|
||||||
altNames.push({ type: 7, ip });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lanIPs.length > 0) {
|
|
||||||
console.log(` Including LAN IPs: ${lanIPs.join(", ")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pems = await generate(attrs, {
|
|
||||||
keySize: 2048,
|
|
||||||
notAfterDate,
|
|
||||||
algorithm: "sha256",
|
|
||||||
extensions: [
|
|
||||||
{
|
|
||||||
name: "basicConstraints",
|
|
||||||
cA: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "keyUsage",
|
|
||||||
keyCertSign: true,
|
|
||||||
digitalSignature: true,
|
|
||||||
keyEncipherment: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "extKeyUsage",
|
|
||||||
serverAuth: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "subjectAltName",
|
|
||||||
altNames,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save certificates
|
|
||||||
writeFileSync(KEY_PATH, pems.private);
|
|
||||||
writeFileSync(CERT_PATH, pems.cert);
|
|
||||||
|
|
||||||
console.log(`✅ Certificate saved to ${CERT_DIR}`);
|
|
||||||
console.log(` Valid for ${CERT_VALIDITY_DAYS} days`);
|
|
||||||
console.log(` ⚠️ First access will show a security warning - click "Advanced" → "Proceed"`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: pems.private,
|
|
||||||
cert: pems.cert,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { buildApplication } from "@stricli/core";
|
|
||||||
import { createRequire } from "node:module";
|
|
||||||
import { command } from "./command.js";
|
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
const pkg = require("../../package.json") as { version: string };
|
|
||||||
|
|
||||||
export const app = buildApplication(command, {
|
|
||||||
name: "acp-link",
|
|
||||||
versionInfo: {
|
|
||||||
currentVersion: pkg.version,
|
|
||||||
},
|
|
||||||
scanner: {
|
|
||||||
caseStyle: "allow-kebab-for-camel",
|
|
||||||
allowArgumentEscapeSequence: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
import { run } from "@stricli/core";
|
|
||||||
import { app } from "./app.js";
|
|
||||||
import { buildContext } from "./context.js";
|
|
||||||
|
|
||||||
await run(app, process.argv.slice(2), buildContext());
|
|
||||||
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { buildCommand, numberParser } from "@stricli/core";
|
|
||||||
import type { LocalContext } from "./context.js";
|
|
||||||
|
|
||||||
export const command = buildCommand({
|
|
||||||
docs: {
|
|
||||||
brief: "Start the ACP proxy server",
|
|
||||||
fullDescription:
|
|
||||||
"Starts a WebSocket proxy server that bridges clients to ACP agents. " +
|
|
||||||
"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" +
|
|
||||||
"For remote access, set ACP_AUTH_TOKEN environment variable or let it auto-generate.",
|
|
||||||
},
|
|
||||||
parameters: {
|
|
||||||
flags: {
|
|
||||||
port: {
|
|
||||||
kind: "parsed",
|
|
||||||
parse: numberParser,
|
|
||||||
brief: "Port to listen on",
|
|
||||||
default: "9315",
|
|
||||||
},
|
|
||||||
host: {
|
|
||||||
kind: "parsed",
|
|
||||||
parse: String,
|
|
||||||
brief: "Host to bind to (use 0.0.0.0 for remote access)",
|
|
||||||
default: "localhost",
|
|
||||||
},
|
|
||||||
debug: {
|
|
||||||
kind: "boolean",
|
|
||||||
brief: "Enable debug logging to file",
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
"no-auth": {
|
|
||||||
kind: "boolean",
|
|
||||||
brief: "DANGEROUS: Disable authentication (not recommended)",
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
https: {
|
|
||||||
kind: "boolean",
|
|
||||||
brief: "Enable HTTPS with auto-generated self-signed certificate",
|
|
||||||
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",
|
|
||||||
parameter: {
|
|
||||||
brief: "Agent command and arguments (use -- before agent flags)",
|
|
||||||
parse: String,
|
|
||||||
placeholder: "command",
|
|
||||||
},
|
|
||||||
minimum: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
func: async function (
|
|
||||||
this: LocalContext,
|
|
||||||
flags: { port: number; host: string; debug: boolean; "no-auth": boolean; https: boolean; group: string | undefined },
|
|
||||||
...args: readonly string[]
|
|
||||||
) {
|
|
||||||
const port = flags.port;
|
|
||||||
const host = flags.host;
|
|
||||||
const debug = flags.debug;
|
|
||||||
const noAuth = flags["no-auth"];
|
|
||||||
const https = flags.https;
|
|
||||||
const group = flags.group;
|
|
||||||
const [command, ...agentArgs] = args;
|
|
||||||
const cwd = process.cwd();
|
|
||||||
|
|
||||||
// Determine auth token
|
|
||||||
// Priority: ACP_AUTH_TOKEN env var > auto-generate (unless --no-auth)
|
|
||||||
let token: string | undefined;
|
|
||||||
if (noAuth) {
|
|
||||||
console.warn("⚠️ WARNING: Authentication disabled. This is dangerous for remote access!");
|
|
||||||
token = undefined;
|
|
||||||
} else {
|
|
||||||
token = process.env.ACP_AUTH_TOKEN;
|
|
||||||
if (!token) {
|
|
||||||
// Auto-generate random token
|
|
||||||
const { randomBytes } = await import("node:crypto");
|
|
||||||
token = randomBytes(32).toString("hex");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize logger
|
|
||||||
const { initLogger } = await import("../logger.js");
|
|
||||||
initLogger({ debug });
|
|
||||||
|
|
||||||
// Import and run the server
|
|
||||||
const { startServer } = await import("../server.js");
|
|
||||||
await startServer({ port, host, command: command!, args: [...agentArgs], cwd, debug, token, https, group });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { CommandContext } from "@stricli/core";
|
|
||||||
|
|
||||||
export interface LocalContext extends CommandContext {}
|
|
||||||
|
|
||||||
export function buildContext(): LocalContext {
|
|
||||||
return {
|
|
||||||
process,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import pino from "pino";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { mkdirSync, existsSync } from "node:fs";
|
|
||||||
|
|
||||||
let rootLogger: pino.Logger;
|
|
||||||
|
|
||||||
export interface LoggerConfig {
|
|
||||||
debug: boolean;
|
|
||||||
logDir?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Pretty-print config for console output */
|
|
||||||
const PRETTY_CONFIG = {
|
|
||||||
colorize: true,
|
|
||||||
translateTime: "SYS:HH:MM:ss.l",
|
|
||||||
ignore: "pid,hostname",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function initLogger(config: LoggerConfig): pino.Logger {
|
|
||||||
const { debug, logDir } = config;
|
|
||||||
|
|
||||||
if (debug) {
|
|
||||||
const dir = logDir || join(process.cwd(), ".acp-proxy");
|
|
||||||
if (!existsSync(dir)) {
|
|
||||||
mkdirSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const timestamp = now.toISOString()
|
|
||||||
.replace(/T/, "_")
|
|
||||||
.replace(/:/g, "-")
|
|
||||||
.replace(/\..+/, "");
|
|
||||||
const logFile = join(dir, `acp-proxy-${timestamp}.log`);
|
|
||||||
|
|
||||||
// Debug mode: JSON to file + pretty to console (multistream)
|
|
||||||
rootLogger = pino(
|
|
||||||
{
|
|
||||||
level: "trace",
|
|
||||||
timestamp: pino.stdTimeFunctions.isoTime,
|
|
||||||
},
|
|
||||||
pino.transport({
|
|
||||||
targets: [
|
|
||||||
{ target: "pino/file", options: { destination: logFile } },
|
|
||||||
{ target: "pino-pretty", options: { ...PRETTY_CONFIG, destination: 1 } },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`📝 Debug logging enabled: ${logFile}`);
|
|
||||||
} else {
|
|
||||||
rootLogger = pino(
|
|
||||||
{ level: "info", timestamp: pino.stdTimeFunctions.isoTime },
|
|
||||||
pino.transport({
|
|
||||||
target: "pino-pretty",
|
|
||||||
options: { ...PRETTY_CONFIG, destination: 1 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rootLogger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the root logger (auto-creates a default one if not initialized). */
|
|
||||||
export function getLogger(): pino.Logger {
|
|
||||||
if (!rootLogger) {
|
|
||||||
rootLogger = pino(
|
|
||||||
{ level: "info" },
|
|
||||||
pino.transport({
|
|
||||||
target: "pino-pretty",
|
|
||||||
options: { ...PRETTY_CONFIG, destination: 1 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return rootLogger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a child logger scoped to a module.
|
|
||||||
* Usage: `const log = createLogger("agent"); log.info({ pid }, "spawned")`
|
|
||||||
*/
|
|
||||||
export function createLogger(module: string): pino.Logger {
|
|
||||||
return getLogger().child({ module });
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
import { createLogger } from "./logger.js";
|
|
||||||
|
|
||||||
export interface RcsUpstreamConfig {
|
|
||||||
rcsUrl: string; // e.g. "http://localhost:3000"
|
|
||||||
apiToken: string;
|
|
||||||
agentName: string;
|
|
||||||
channelGroupId?: string;
|
|
||||||
capabilities?: Record<string, unknown>;
|
|
||||||
maxSessions?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RCS upstream client — connects acp-link to a Remote Control Server.
|
|
||||||
*
|
|
||||||
* Lifecycle:
|
|
||||||
* 1. connect() — opens WS to RCS
|
|
||||||
* 2. Sends register message
|
|
||||||
* 3. Waits for registered response
|
|
||||||
* 4. Forwards all ACP events via send()
|
|
||||||
* 5. Reconnects with exponential backoff on failure
|
|
||||||
*/
|
|
||||||
export class RcsUpstreamClient {
|
|
||||||
private static log = createLogger("rcs-upstream");
|
|
||||||
private ws: WebSocket | null = null;
|
|
||||||
private registered = false;
|
|
||||||
private reconnectAttempts = 0;
|
|
||||||
private closed = false;
|
|
||||||
private readonly maxReconnectDelay = 30_000;
|
|
||||||
private readonly baseReconnectDelay = 1_000;
|
|
||||||
/** Agent ID obtained from REST registration */
|
|
||||||
private agentId: string | null = null;
|
|
||||||
/** Session ID from REST registration (ACP agents auto-create a session) */
|
|
||||||
private sessionId: string | undefined;
|
|
||||||
|
|
||||||
/** Handler for incoming ACP messages from RCS relay */
|
|
||||||
private messageHandler: ((message: Record<string, unknown>) => void) | null = null;
|
|
||||||
|
|
||||||
constructor(private config: RcsUpstreamConfig) {}
|
|
||||||
|
|
||||||
/** Get the agent ID from REST registration */
|
|
||||||
getAgentId(): string | null {
|
|
||||||
return this.agentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set handler for incoming ACP messages from RCS relay */
|
|
||||||
setMessageHandler(handler: (message: Record<string, unknown>) => void): void {
|
|
||||||
this.messageHandler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Register via REST API before establishing WS connection */
|
|
||||||
private async registerViaRest(): Promise<string> {
|
|
||||||
const baseUrl = this.config.rcsUrl
|
|
||||||
.replace(/^ws:\/\//, "http://")
|
|
||||||
.replace(/^wss:\/\//, "https://")
|
|
||||||
.replace(/\/acp\/ws.*$/, "")
|
|
||||||
.replace(/\/$/, "");
|
|
||||||
|
|
||||||
const url = `${baseUrl}/v1/environments/bridge`;
|
|
||||||
RcsUpstreamClient.log.info({ url }, "REST register");
|
|
||||||
|
|
||||||
const resp = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${this.config.apiToken}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
machine_name: this.config.agentName,
|
|
||||||
worker_type: "acp",
|
|
||||||
bridge_id: this.config.channelGroupId || undefined,
|
|
||||||
max_sessions: this.config.maxSessions,
|
|
||||||
capabilities: this.config.capabilities,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!resp.ok) {
|
|
||||||
const text = await resp.text();
|
|
||||||
throw new Error(`REST register failed (${resp.status}): ${text}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await resp.json() as { environment_id: string; environment_secret: string; status: string; session_id?: string };
|
|
||||||
this.agentId = data.environment_id;
|
|
||||||
this.sessionId = data.session_id;
|
|
||||||
RcsUpstreamClient.log.info({ agentId: this.agentId, sessionId: this.sessionId }, "REST register success");
|
|
||||||
return data.environment_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Normalize RCS URL: accept http(s) base URL and convert to ws(s) + /acp/ws path */
|
|
||||||
private buildWsUrl(): string {
|
|
||||||
let raw = this.config.rcsUrl;
|
|
||||||
raw = raw.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
|
|
||||||
const url = new URL(raw);
|
|
||||||
const path = url.pathname.replace(/\/+$/, "");
|
|
||||||
if (!path || path === "/") {
|
|
||||||
url.pathname = "/acp/ws";
|
|
||||||
}
|
|
||||||
if (this.config.apiToken) {
|
|
||||||
url.searchParams.set("token", this.config.apiToken);
|
|
||||||
}
|
|
||||||
return url.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Open connection to RCS: REST register → WS identify */
|
|
||||||
async connect(): Promise<void> {
|
|
||||||
if (this.closed) return;
|
|
||||||
|
|
||||||
// Step 1: REST registration
|
|
||||||
try {
|
|
||||||
await this.registerViaRest();
|
|
||||||
} catch (err) {
|
|
||||||
RcsUpstreamClient.log.error({ err }, "REST registration failed");
|
|
||||||
if (!this.closed) {
|
|
||||||
this.scheduleReconnect();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: WebSocket connection with identify
|
|
||||||
const wsUrl = this.buildWsUrl();
|
|
||||||
RcsUpstreamClient.log.info({ url: wsUrl }, "connecting WS");
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
this.ws = new WebSocket(wsUrl);
|
|
||||||
|
|
||||||
this.ws.onopen = () => {
|
|
||||||
RcsUpstreamClient.log.debug("ws open — sending identify");
|
|
||||||
this.ws!.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "identify",
|
|
||||||
agent_id: this.agentId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ws.onmessage = (event) => {
|
|
||||||
let data: Record<string, unknown>;
|
|
||||||
try {
|
|
||||||
data = JSON.parse(event.data as string);
|
|
||||||
} catch {
|
|
||||||
RcsUpstreamClient.log.warn({ raw: String(event.data).slice(0, 200) }, "invalid JSON from server");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.type === "identified") {
|
|
||||||
RcsUpstreamClient.log.info({ agent_id: data.agent_id, channel_group_id: data.channel_group_id }, "identified");
|
|
||||||
this.registered = true;
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
const webBase = this.config.rcsUrl
|
|
||||||
.replace(/^ws:\/\//, "http://")
|
|
||||||
.replace(/^wss:\/\//, "https://")
|
|
||||||
.replace(/\/acp\/ws.*$/, "")
|
|
||||||
.replace(/\/$/, "");
|
|
||||||
console.log();
|
|
||||||
if (this.sessionId) {
|
|
||||||
console.log(` 🔗 Dashboard: ${webBase}/code/?sid=${this.sessionId}`);
|
|
||||||
} else {
|
|
||||||
console.log(` 🔗 Dashboard: ${webBase}/code/`);
|
|
||||||
}
|
|
||||||
if (this.agentId) {
|
|
||||||
console.log(` Agent ID: ${this.agentId}`);
|
|
||||||
}
|
|
||||||
console.log();
|
|
||||||
resolve();
|
|
||||||
} else if (data.type === "registered") {
|
|
||||||
// Legacy fallback: server still uses old register flow
|
|
||||||
RcsUpstreamClient.log.info({ agent_id: data.agent_id }, "registered (legacy)");
|
|
||||||
this.agentId = (data.agent_id as string) || this.agentId;
|
|
||||||
this.registered = true;
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
resolve();
|
|
||||||
} else if (data.type === "error") {
|
|
||||||
RcsUpstreamClient.log.error({ message: data.message }, "server error");
|
|
||||||
if (!this.registered) {
|
|
||||||
reject(new Error(data.message as string));
|
|
||||||
}
|
|
||||||
} else if (data.type === "keep_alive") {
|
|
||||||
// ignore keepalive
|
|
||||||
} else {
|
|
||||||
// Forward ACP protocol messages to handler (for RCS relay support)
|
|
||||||
RcsUpstreamClient.log.debug({ type: data.type }, "forwarding to relay handler");
|
|
||||||
this.messageHandler?.(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ws.onerror = () => {
|
|
||||||
// onclose fires after onerror with the actual close code, so we log there
|
|
||||||
if (!this.registered) {
|
|
||||||
reject(new Error("WebSocket connection failed"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ws.onclose = (event) => {
|
|
||||||
RcsUpstreamClient.log.info({ code: event.code, reason: event.reason || undefined }, "ws closed");
|
|
||||||
this.registered = false;
|
|
||||||
this.ws = null;
|
|
||||||
if (!this.closed) {
|
|
||||||
this.scheduleReconnect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
RcsUpstreamClient.log.error({ err }, "connect threw");
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Send an ACP message to RCS for broadcast */
|
|
||||||
send(message: object): void {
|
|
||||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.registered) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.ws.send(JSON.stringify(message));
|
|
||||||
} catch (err) {
|
|
||||||
RcsUpstreamClient.log.error({ err }, "send failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check if registered with RCS */
|
|
||||||
isRegistered(): boolean {
|
|
||||||
return this.registered && this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Close the RCS connection permanently */
|
|
||||||
async close(): Promise<void> {
|
|
||||||
this.closed = true;
|
|
||||||
this.registered = false;
|
|
||||||
if (this.ws) {
|
|
||||||
this.ws.close(1000, "client shutdown");
|
|
||||||
this.ws = null;
|
|
||||||
}
|
|
||||||
RcsUpstreamClient.log.info("closed");
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleReconnect(): void {
|
|
||||||
if (this.closed) return;
|
|
||||||
|
|
||||||
const delay = Math.min(
|
|
||||||
this.baseReconnectDelay * 2 ** this.reconnectAttempts,
|
|
||||||
this.maxReconnectDelay,
|
|
||||||
);
|
|
||||||
const jitter = delay * Math.random() * 0.2;
|
|
||||||
const actualDelay = delay + jitter;
|
|
||||||
this.reconnectAttempts++;
|
|
||||||
|
|
||||||
RcsUpstreamClient.log.warn({ attempt: this.reconnectAttempts, delayMs: Math.round(actualDelay) }, "reconnecting");
|
|
||||||
|
|
||||||
setTimeout(async () => {
|
|
||||||
if (this.closed) return;
|
|
||||||
try {
|
|
||||||
await this.connect();
|
|
||||||
} catch {
|
|
||||||
// connect() itself logs the error; nothing to add here
|
|
||||||
}
|
|
||||||
}, actualDelay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,902 +0,0 @@
|
|||||||
import { spawn, type ChildProcess } from "node:child_process";
|
|
||||||
import { createServer as createHttpsServer } from "node:https";
|
|
||||||
import { Writable, Readable } from "node:stream";
|
|
||||||
import * as acp from "@agentclientprotocol/sdk";
|
|
||||||
import { Hono } from "hono";
|
|
||||||
import { serve } from "@hono/node-server";
|
|
||||||
import { createNodeWebSocket } from "@hono/node-ws";
|
|
||||||
import type { WSContext } from "hono/ws";
|
|
||||||
import type { WebSocket as RawWebSocket } from "ws";
|
|
||||||
import { createLogger } from "./logger.js";
|
|
||||||
import { getOrCreateCertificate, getLanIPs } from "./cert.js";
|
|
||||||
import { RcsUpstreamClient, type RcsUpstreamConfig } from "./rcs-upstream.js";
|
|
||||||
|
|
||||||
export interface ServerConfig {
|
|
||||||
port: number;
|
|
||||||
host: string;
|
|
||||||
command: string;
|
|
||||||
args: string[];
|
|
||||||
cwd: string;
|
|
||||||
debug?: boolean;
|
|
||||||
token?: string;
|
|
||||||
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
|
|
||||||
interface PendingPermission {
|
|
||||||
resolve: (outcome: { outcome: "cancelled" } | { outcome: "selected"; optionId: string }) => void;
|
|
||||||
timeout: ReturnType<typeof setTimeout>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PromptCapabilities from ACP protocol
|
|
||||||
// Reference: Zed's prompt_capabilities to check image support
|
|
||||||
interface PromptCapabilities {
|
|
||||||
audio?: boolean;
|
|
||||||
embeddedContext?: boolean;
|
|
||||||
image?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SessionModelState from ACP protocol
|
|
||||||
// Reference: Zed's AgentModelSelector reads from state.available_models
|
|
||||||
interface SessionModelState {
|
|
||||||
availableModels: Array<{
|
|
||||||
modelId: string;
|
|
||||||
name: string;
|
|
||||||
description?: string | null;
|
|
||||||
}>;
|
|
||||||
currentModelId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// AgentCapabilities from ACP protocol
|
|
||||||
// Reference: Zed's AcpConnection.agent_capabilities
|
|
||||||
// Matches SDK's AgentCapabilities exactly
|
|
||||||
interface AgentCapabilities {
|
|
||||||
_meta?: Record<string, unknown> | null;
|
|
||||||
loadSession?: boolean;
|
|
||||||
mcpCapabilities?: {
|
|
||||||
_meta?: Record<string, unknown> | null;
|
|
||||||
clientServers?: boolean;
|
|
||||||
};
|
|
||||||
promptCapabilities?: PromptCapabilities;
|
|
||||||
sessionCapabilities?: {
|
|
||||||
_meta?: Record<string, unknown> | null;
|
|
||||||
fork?: Record<string, unknown> | null;
|
|
||||||
list?: Record<string, unknown> | null;
|
|
||||||
resume?: Record<string, unknown> | null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track connected clients and their agent connections
|
|
||||||
interface ClientState {
|
|
||||||
process: ChildProcess | null;
|
|
||||||
connection: acp.ClientSideConnection | null;
|
|
||||||
sessionId: string | null;
|
|
||||||
pendingPermissions: Map<string, PendingPermission>;
|
|
||||||
agentCapabilities: AgentCapabilities | null;
|
|
||||||
promptCapabilities: PromptCapabilities | null;
|
|
||||||
modelState: SessionModelState | null;
|
|
||||||
isAlive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module-level state (set when server starts)
|
|
||||||
let AGENT_COMMAND: string;
|
|
||||||
let AGENT_ARGS: string[];
|
|
||||||
let AGENT_CWD: string;
|
|
||||||
let SERVER_PORT: number;
|
|
||||||
let SERVER_HOST: string;
|
|
||||||
let AUTH_TOKEN: string | undefined;
|
|
||||||
let DEFAULT_PERMISSION_MODE: string | undefined;
|
|
||||||
|
|
||||||
const clients = new Map<WSContext, ClientState>();
|
|
||||||
|
|
||||||
// Module-scoped child loggers
|
|
||||||
const logWs = createLogger("ws");
|
|
||||||
const logAgent = createLogger("agent");
|
|
||||||
const logSession = createLogger("session");
|
|
||||||
const logPrompt = createLogger("prompt");
|
|
||||||
const logPerm = createLogger("perm");
|
|
||||||
const logRelay = createLogger("relay");
|
|
||||||
const logServer = createLogger("server");
|
|
||||||
|
|
||||||
// RCS upstream client (optional — enabled via ACP_RCS_URL env var)
|
|
||||||
let rcsUpstream: RcsUpstreamClient | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a virtual WSContext for RCS relay messages.
|
|
||||||
* Responses via send() go to RCS upstream (not a local WS).
|
|
||||||
*/
|
|
||||||
function createRelayWs(): WSContext {
|
|
||||||
return {
|
|
||||||
get readyState() { return 1; }, // always OPEN
|
|
||||||
send: () => {}, // no-op — responses go through rcsUpstream.send()
|
|
||||||
close: () => {},
|
|
||||||
raw: null,
|
|
||||||
isInner: false,
|
|
||||||
url: "",
|
|
||||||
origin: "",
|
|
||||||
protocol: "",
|
|
||||||
} as unknown as WSContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permission request timeout (5 minutes)
|
|
||||||
const PERMISSION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
||||||
|
|
||||||
// Heartbeat interval for WebSocket ping/pong (30 seconds)
|
|
||||||
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
||||||
|
|
||||||
// Generate unique request ID
|
|
||||||
function generateRequestId(): string {
|
|
||||||
return `perm_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send a message to the WebSocket client (and optionally forward to RCS upstream)
|
|
||||||
function send(ws: WSContext, type: string, payload?: unknown): void {
|
|
||||||
if (ws.readyState === 1) {
|
|
||||||
// WebSocket.OPEN
|
|
||||||
ws.send(JSON.stringify({ type, payload }));
|
|
||||||
}
|
|
||||||
// Forward to RCS upstream if connected
|
|
||||||
if (rcsUpstream?.isRegistered()) {
|
|
||||||
rcsUpstream.send({ type, payload });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a Client implementation that forwards events to WebSocket
|
|
||||||
function createClient(ws: WSContext, clientState: ClientState): acp.Client {
|
|
||||||
return {
|
|
||||||
async requestPermission(params) {
|
|
||||||
const requestId = generateRequestId();
|
|
||||||
logPerm.debug({ requestId, title: params.toolCall.title }, "requested");
|
|
||||||
|
|
||||||
const outcomePromise = new Promise<{ outcome: "cancelled" } | { outcome: "selected"; optionId: string }>((resolve) => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
logPerm.warn({ requestId }, "timed out");
|
|
||||||
clientState.pendingPermissions.delete(requestId);
|
|
||||||
resolve({ outcome: "cancelled" });
|
|
||||||
}, PERMISSION_TIMEOUT_MS);
|
|
||||||
|
|
||||||
clientState.pendingPermissions.set(requestId, { resolve, timeout });
|
|
||||||
});
|
|
||||||
|
|
||||||
send(ws, "permission_request", {
|
|
||||||
requestId,
|
|
||||||
sessionId: params.sessionId,
|
|
||||||
options: params.options,
|
|
||||||
toolCall: params.toolCall,
|
|
||||||
});
|
|
||||||
|
|
||||||
const outcome = await outcomePromise;
|
|
||||||
logPerm.debug({ requestId, outcome: outcome.outcome }, "resolved");
|
|
||||||
|
|
||||||
return { outcome };
|
|
||||||
},
|
|
||||||
|
|
||||||
async sessionUpdate(params) {
|
|
||||||
send(ws, "session_update", params);
|
|
||||||
},
|
|
||||||
|
|
||||||
async readTextFile(params) {
|
|
||||||
logWs.debug({ path: params.path }, "readTextFile");
|
|
||||||
return { content: "" };
|
|
||||||
},
|
|
||||||
|
|
||||||
async writeTextFile(params) {
|
|
||||||
logWs.debug({ path: params.path }, "writeTextFile");
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle permission response from client
|
|
||||||
function handlePermissionResponse(ws: WSContext, payload: { requestId: string; outcome: { outcome: "cancelled" } | { outcome: "selected"; optionId: string } }): void {
|
|
||||||
const state = clients.get(ws);
|
|
||||||
if (!state) {
|
|
||||||
logPerm.warn("response from unknown client");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pending = state.pendingPermissions.get(payload.requestId);
|
|
||||||
if (!pending) {
|
|
||||||
logPerm.warn({ requestId: payload.requestId }, "response for unknown request");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(pending.timeout);
|
|
||||||
state.pendingPermissions.delete(payload.requestId);
|
|
||||||
pending.resolve(payload.outcome);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel all pending permissions for a client (called on disconnect)
|
|
||||||
function cancelPendingPermissions(clientState: ClientState): void {
|
|
||||||
for (const [requestId, pending] of clientState.pendingPermissions) {
|
|
||||||
logPerm.debug({ requestId }, "cancelled on disconnect");
|
|
||||||
clearTimeout(pending.timeout);
|
|
||||||
pending.resolve({ outcome: "cancelled" });
|
|
||||||
}
|
|
||||||
clientState.pendingPermissions.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleConnect(ws: WSContext): Promise<void> {
|
|
||||||
const state = clients.get(ws);
|
|
||||||
if (!state) return;
|
|
||||||
|
|
||||||
// If already connected to a running agent, just resend status
|
|
||||||
// This handles frontend reconnections without restarting the agent process
|
|
||||||
// Check both .killed and .exitCode to detect crashed processes
|
|
||||||
if (state.connection && state.process && !state.process.killed && state.process.exitCode === null) {
|
|
||||||
logAgent.info("already connected, resending status");
|
|
||||||
send(ws, "status", {
|
|
||||||
connected: true,
|
|
||||||
agentInfo: { name: AGENT_COMMAND },
|
|
||||||
capabilities: state.agentCapabilities,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kill existing process if any (only if not healthy)
|
|
||||||
if (state.process) {
|
|
||||||
cancelPendingPermissions(state);
|
|
||||||
state.process.kill();
|
|
||||||
state.process = null;
|
|
||||||
state.connection = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
logAgent.info({ command: AGENT_COMMAND, args: AGENT_ARGS }, "spawning");
|
|
||||||
|
|
||||||
const agentProcess = spawn(AGENT_COMMAND, AGENT_ARGS, {
|
|
||||||
cwd: AGENT_CWD,
|
|
||||||
stdio: ["pipe", "pipe", "inherit"],
|
|
||||||
});
|
|
||||||
|
|
||||||
state.process = agentProcess;
|
|
||||||
|
|
||||||
// Clean up state when agent process exits unexpectedly
|
|
||||||
agentProcess.on("exit", (code) => {
|
|
||||||
logAgent.info({ exitCode: code }, "agent process exited");
|
|
||||||
// Only clear if this is still the current process
|
|
||||||
if (state.process === agentProcess) {
|
|
||||||
state.process = null;
|
|
||||||
state.connection = null;
|
|
||||||
state.sessionId = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const input = Writable.toWeb(agentProcess.stdin!) as unknown as WritableStream<Uint8Array>;
|
|
||||||
const output = Readable.toWeb(agentProcess.stdout!) as unknown as ReadableStream<Uint8Array>;
|
|
||||||
|
|
||||||
const stream = acp.ndJsonStream(input, output);
|
|
||||||
const connection = new acp.ClientSideConnection(
|
|
||||||
(_agent) => createClient(ws, state),
|
|
||||||
stream,
|
|
||||||
);
|
|
||||||
|
|
||||||
state.connection = connection;
|
|
||||||
|
|
||||||
const initResult = await connection.initialize({
|
|
||||||
protocolVersion: acp.PROTOCOL_VERSION,
|
|
||||||
clientInfo: { name: "zed", version: "1.0.0" },
|
|
||||||
clientCapabilities: {
|
|
||||||
fs: { readTextFile: true, writeTextFile: true },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const agentCaps = initResult.agentCapabilities;
|
|
||||||
state.agentCapabilities = agentCaps ? {
|
|
||||||
_meta: agentCaps._meta,
|
|
||||||
loadSession: agentCaps.loadSession,
|
|
||||||
mcpCapabilities: agentCaps.mcpCapabilities,
|
|
||||||
promptCapabilities: agentCaps.promptCapabilities,
|
|
||||||
sessionCapabilities: agentCaps.sessionCapabilities,
|
|
||||||
} : null;
|
|
||||||
state.promptCapabilities = agentCaps?.promptCapabilities ?? null;
|
|
||||||
|
|
||||||
logAgent.info({
|
|
||||||
protocolVersion: initResult.protocolVersion,
|
|
||||||
loadSession: !!state.agentCapabilities?.loadSession,
|
|
||||||
sessionList: !!state.agentCapabilities?.sessionCapabilities?.list,
|
|
||||||
sessionResume: !!state.agentCapabilities?.sessionCapabilities?.resume,
|
|
||||||
hasMcp: !!state.agentCapabilities?.mcpCapabilities,
|
|
||||||
}, "initialized");
|
|
||||||
|
|
||||||
send(ws, "status", {
|
|
||||||
connected: true,
|
|
||||||
agentInfo: initResult.agentInfo,
|
|
||||||
capabilities: state.agentCapabilities,
|
|
||||||
});
|
|
||||||
|
|
||||||
connection.closed.then(() => {
|
|
||||||
logAgent.info("connection closed");
|
|
||||||
state.connection = null;
|
|
||||||
state.sessionId = null;
|
|
||||||
send(ws, "status", { connected: false });
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logAgent.error({ error: (error as Error).message }, "connect failed");
|
|
||||||
send(ws, "error", { message: `Failed to connect: ${(error as Error).message}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleNewSession(
|
|
||||||
ws: WSContext,
|
|
||||||
params: { cwd?: string; permissionMode?: string },
|
|
||||||
): Promise<void> {
|
|
||||||
const state = clients.get(ws);
|
|
||||||
if (!state?.connection) {
|
|
||||||
logAgent.warn({ hasState: !!state, hasProcess: !!state?.process, processKilled: state?.process?.killed, exitCode: state?.process?.exitCode }, "handleNewSession: not connected to agent");
|
|
||||||
send(ws, "error", { message: "Not connected to agent" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sessionCwd = params.cwd || AGENT_CWD;
|
|
||||||
const permissionMode = params.permissionMode || DEFAULT_PERMISSION_MODE;
|
|
||||||
const result = await state.connection.newSession({
|
|
||||||
cwd: sessionCwd,
|
|
||||||
mcpServers: [],
|
|
||||||
...(permissionMode ? { _meta: { permissionMode } } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
state.sessionId = result.sessionId;
|
|
||||||
state.modelState = result.models ?? null;
|
|
||||||
logSession.info({ sessionId: result.sessionId, cwd: sessionCwd, hasModels: !!result.models }, "created");
|
|
||||||
|
|
||||||
send(ws, "session_created", {
|
|
||||||
...result,
|
|
||||||
promptCapabilities: state.promptCapabilities,
|
|
||||||
models: state.modelState,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logSession.error({ error: (error as Error).message }, "create failed");
|
|
||||||
send(ws, "error", { message: `Failed to create session: ${(error as Error).message}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Session History Operations
|
|
||||||
// Reference: Zed's AgentConnection trait - list_sessions, load_session, resume_session
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
async function handleListSessions(
|
|
||||||
ws: WSContext,
|
|
||||||
params: { cwd?: string; cursor?: string },
|
|
||||||
): Promise<void> {
|
|
||||||
const state = clients.get(ws);
|
|
||||||
if (!state?.connection) {
|
|
||||||
logAgent.warn({ hasState: !!state, hasProcess: !!state?.process, processKilled: state?.process?.killed, exitCode: state?.process?.exitCode }, "handleListSessions: not connected to agent");
|
|
||||||
send(ws, "error", { message: "Not connected to agent" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.agentCapabilities?.sessionCapabilities?.list) {
|
|
||||||
send(ws, "error", { message: "Listing sessions is not supported by this agent" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await state.connection.listSessions({
|
|
||||||
cwd: params.cwd,
|
|
||||||
cursor: params.cursor,
|
|
||||||
});
|
|
||||||
|
|
||||||
const MAX_SESSIONS = 20;
|
|
||||||
const sessions = result.sessions.slice(0, MAX_SESSIONS);
|
|
||||||
logSession.info({ total: result.sessions.length, returned: sessions.length, hasMore: !!result.nextCursor }, "listed");
|
|
||||||
|
|
||||||
send(ws, "session_list", {
|
|
||||||
sessions: sessions.map((s: acp.SessionInfo) => ({
|
|
||||||
_meta: s._meta,
|
|
||||||
cwd: s.cwd,
|
|
||||||
sessionId: s.sessionId,
|
|
||||||
title: s.title,
|
|
||||||
updatedAt: s.updatedAt,
|
|
||||||
})),
|
|
||||||
nextCursor: result.nextCursor,
|
|
||||||
_meta: result._meta,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logSession.error({ error: (error as Error).message }, "list failed");
|
|
||||||
send(ws, "error", { message: `Failed to list sessions: ${(error as Error).message}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLoadSession(
|
|
||||||
ws: WSContext,
|
|
||||||
params: { sessionId: string; cwd?: string },
|
|
||||||
): Promise<void> {
|
|
||||||
const state = clients.get(ws);
|
|
||||||
if (!state?.connection) {
|
|
||||||
logAgent.warn({ hasState: !!state, hasProcess: !!state?.process, processKilled: state?.process?.killed, exitCode: state?.process?.exitCode }, "handleLoadSession: not connected to agent");
|
|
||||||
send(ws, "error", { message: "Not connected to agent" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.agentCapabilities?.loadSession) {
|
|
||||||
send(ws, "error", { message: "Loading sessions is not supported by this agent" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sessionCwd = params.cwd || AGENT_CWD;
|
|
||||||
const sessionId = params.sessionId;
|
|
||||||
const result = await state.connection.loadSession({
|
|
||||||
sessionId,
|
|
||||||
cwd: sessionCwd,
|
|
||||||
mcpServers: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
state.sessionId = sessionId;
|
|
||||||
state.modelState = result.models ?? null;
|
|
||||||
logSession.info({ sessionId, cwd: sessionCwd }, "loaded");
|
|
||||||
|
|
||||||
send(ws, "session_loaded", {
|
|
||||||
sessionId,
|
|
||||||
promptCapabilities: state.promptCapabilities,
|
|
||||||
models: state.modelState,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logSession.error({ error: (error as Error).message }, "load failed");
|
|
||||||
send(ws, "error", { message: `Failed to load session: ${(error as Error).message}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleResumeSession(
|
|
||||||
ws: WSContext,
|
|
||||||
params: { sessionId: string; cwd?: string },
|
|
||||||
): Promise<void> {
|
|
||||||
const state = clients.get(ws);
|
|
||||||
if (!state?.connection) {
|
|
||||||
logAgent.warn({ hasState: !!state, hasProcess: !!state?.process, processKilled: state?.process?.killed, exitCode: state?.process?.exitCode }, "handleResumeSession: not connected to agent");
|
|
||||||
send(ws, "error", { message: "Not connected to agent" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.agentCapabilities?.sessionCapabilities?.resume) {
|
|
||||||
send(ws, "error", { message: "Resuming sessions is not supported by this agent" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sessionCwd = params.cwd || AGENT_CWD;
|
|
||||||
const sessionId = params.sessionId;
|
|
||||||
const result = await state.connection.unstable_resumeSession({
|
|
||||||
sessionId,
|
|
||||||
cwd: sessionCwd,
|
|
||||||
});
|
|
||||||
|
|
||||||
state.sessionId = sessionId;
|
|
||||||
state.modelState = result.models ?? null;
|
|
||||||
logSession.info({ sessionId, cwd: sessionCwd }, "resumed");
|
|
||||||
|
|
||||||
send(ws, "session_resumed", {
|
|
||||||
sessionId,
|
|
||||||
promptCapabilities: state.promptCapabilities,
|
|
||||||
models: state.modelState,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logSession.error({ error: (error as Error).message }, "resume failed");
|
|
||||||
send(ws, "error", { message: `Failed to resume session: ${(error as Error).message}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reference: Zed's AcpThread.send() forwards Vec<acp::ContentBlock> to agent
|
|
||||||
async function handlePrompt(
|
|
||||||
ws: WSContext,
|
|
||||||
params: { content: ContentBlock[] },
|
|
||||||
): Promise<void> {
|
|
||||||
const state = clients.get(ws);
|
|
||||||
if (!state?.connection || !state.sessionId) {
|
|
||||||
send(ws, "error", { message: "No active session" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const firstText = params.content.find(b => b.type === "text")?.text;
|
|
||||||
const images = params.content.filter(b => b.type === "image");
|
|
||||||
logPrompt.debug({
|
|
||||||
text: firstText?.slice(0, 100),
|
|
||||||
imageCount: images.length,
|
|
||||||
blockCount: params.content.length,
|
|
||||||
}, "sending");
|
|
||||||
|
|
||||||
const result = await state.connection.prompt({
|
|
||||||
sessionId: state.sessionId,
|
|
||||||
prompt: params.content as acp.ContentBlock[],
|
|
||||||
});
|
|
||||||
|
|
||||||
logPrompt.info({ stopReason: result.stopReason }, "completed");
|
|
||||||
send(ws, "prompt_complete", result);
|
|
||||||
} catch (error) {
|
|
||||||
logPrompt.error({ error: (error as Error).message }, "failed");
|
|
||||||
send(ws, "error", { message: `Prompt failed: ${(error as Error).message}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDisconnect(ws: WSContext): void {
|
|
||||||
const state = clients.get(ws);
|
|
||||||
if (!state) return;
|
|
||||||
|
|
||||||
if (state.process) {
|
|
||||||
state.process.kill();
|
|
||||||
state.process = null;
|
|
||||||
}
|
|
||||||
state.connection = null;
|
|
||||||
state.sessionId = null;
|
|
||||||
|
|
||||||
send(ws, "status", { connected: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle cancel request from client
|
|
||||||
async function handleCancel(ws: WSContext): Promise<void> {
|
|
||||||
const state = clients.get(ws);
|
|
||||||
if (!state?.connection || !state.sessionId) {
|
|
||||||
logWs.warn("cancel requested but no active session");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logSession.info({ sessionId: state.sessionId }, "cancel requested");
|
|
||||||
cancelPendingPermissions(state);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await state.connection.cancel({ sessionId: state.sessionId });
|
|
||||||
logSession.info({ sessionId: state.sessionId }, "cancel sent");
|
|
||||||
} catch (error) {
|
|
||||||
logSession.error({ error: (error as Error).message }, "cancel failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reference: Zed's AgentModelSelector.select_model() calls connection.set_session_model()
|
|
||||||
async function handleSetSessionModel(
|
|
||||||
ws: WSContext,
|
|
||||||
params: { modelId: string },
|
|
||||||
): Promise<void> {
|
|
||||||
const state = clients.get(ws);
|
|
||||||
if (!state?.connection || !state.sessionId) {
|
|
||||||
send(ws, "error", { message: "No active session" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.modelState) {
|
|
||||||
send(ws, "error", { message: "Model selection not supported by this agent" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
logSession.info({ sessionId: state.sessionId, modelId: params.modelId }, "setting model");
|
|
||||||
await state.connection.unstable_setSessionModel({
|
|
||||||
sessionId: state.sessionId,
|
|
||||||
modelId: params.modelId,
|
|
||||||
});
|
|
||||||
state.modelState = { ...state.modelState, currentModelId: params.modelId };
|
|
||||||
send(ws, "model_changed", { modelId: params.modelId });
|
|
||||||
logSession.info({ modelId: params.modelId }, "model changed");
|
|
||||||
} catch (error) {
|
|
||||||
logSession.error({ error: (error as Error).message }, "set model failed");
|
|
||||||
send(ws, "error", { message: `Failed to set model: ${(error as Error).message}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContentBlock type matching @agentclientprotocol/sdk
|
|
||||||
interface ContentBlock {
|
|
||||||
type: string;
|
|
||||||
text?: string;
|
|
||||||
data?: string;
|
|
||||||
mimeType?: string;
|
|
||||||
uri?: string;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProxyMessage {
|
|
||||||
type: "connect" | "disconnect" | "new_session" | "prompt" | "cancel" | "set_session_model";
|
|
||||||
payload?: { cwd?: string } | { content: ContentBlock[] } | { modelId: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function startServer(config: ServerConfig): Promise<void> {
|
|
||||||
const { port, host, command, args, cwd, token, https } = config;
|
|
||||||
|
|
||||||
// Set module-level config
|
|
||||||
AGENT_COMMAND = command;
|
|
||||||
AGENT_ARGS = args;
|
|
||||||
AGENT_CWD = cwd;
|
|
||||||
SERVER_PORT = port;
|
|
||||||
SERVER_HOST = host;
|
|
||||||
AUTH_TOKEN = token;
|
|
||||||
DEFAULT_PERMISSION_MODE = config.permissionMode || process.env.ACP_PERMISSION_MODE;
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
|
|
||||||
const relayWs = createRelayWs();
|
|
||||||
const relayState: ClientState = {
|
|
||||||
process: null,
|
|
||||||
connection: null,
|
|
||||||
sessionId: null,
|
|
||||||
pendingPermissions: new Map(),
|
|
||||||
agentCapabilities: null,
|
|
||||||
promptCapabilities: null,
|
|
||||||
modelState: null,
|
|
||||||
isAlive: true,
|
|
||||||
};
|
|
||||||
clients.set(relayWs, relayState);
|
|
||||||
|
|
||||||
rcsUpstream.setMessageHandler(async (msg) => {
|
|
||||||
try {
|
|
||||||
logRelay.debug({ type: msg.type }, "processing");
|
|
||||||
switch (msg.type) {
|
|
||||||
case "connect":
|
|
||||||
await handleConnect(relayWs);
|
|
||||||
break;
|
|
||||||
case "disconnect":
|
|
||||||
handleDisconnect(relayWs);
|
|
||||||
break;
|
|
||||||
case "new_session":
|
|
||||||
await handleNewSession(relayWs, (msg.payload as { cwd?: string; permissionMode?: string }) || {});
|
|
||||||
break;
|
|
||||||
case "prompt":
|
|
||||||
await handlePrompt(relayWs, msg.payload as { content: ContentBlock[] });
|
|
||||||
break;
|
|
||||||
case "permission_response":
|
|
||||||
handlePermissionResponse(relayWs, msg.payload as { requestId: string; outcome: { outcome: "cancelled" } | { outcome: "selected"; optionId: string } });
|
|
||||||
break;
|
|
||||||
case "cancel":
|
|
||||||
await handleCancel(relayWs);
|
|
||||||
break;
|
|
||||||
case "set_session_model":
|
|
||||||
await handleSetSessionModel(relayWs, msg.payload as { modelId: string });
|
|
||||||
break;
|
|
||||||
case "list_sessions":
|
|
||||||
await handleListSessions(relayWs, (msg.payload as { cwd?: string; cursor?: string }) || {});
|
|
||||||
break;
|
|
||||||
case "load_session":
|
|
||||||
await handleLoadSession(relayWs, msg.payload as { sessionId: string; cwd?: string });
|
|
||||||
break;
|
|
||||||
case "resume_session":
|
|
||||||
await handleResumeSession(relayWs, msg.payload as { sessionId: string; cwd?: string });
|
|
||||||
break;
|
|
||||||
case "ping":
|
|
||||||
send(relayWs, "pong");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
logRelay.warn({ type: msg.type }, "unknown message type");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logRelay.error({ error: (error as Error).message }, "handler error");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
rcsUpstream.connect().catch((err) => {
|
|
||||||
logRelay.warn({ error: (err as Error).message }, "initial connection failed");
|
|
||||||
});
|
|
||||||
logRelay.info({ url: rcsUrl }, "upstream enabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = new Hono();
|
|
||||||
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
|
|
||||||
|
|
||||||
// Health check endpoint
|
|
||||||
app.get("/health", (c) => {
|
|
||||||
return c.json({ status: "ok" });
|
|
||||||
});
|
|
||||||
|
|
||||||
// WebSocket endpoint with token validation
|
|
||||||
app.get(
|
|
||||||
"/ws",
|
|
||||||
upgradeWebSocket((c) => {
|
|
||||||
if (AUTH_TOKEN) {
|
|
||||||
const url = new URL(c.req.url);
|
|
||||||
const providedToken = url.searchParams.get("token");
|
|
||||||
if (providedToken !== AUTH_TOKEN) {
|
|
||||||
logWs.warn("connection rejected: invalid token");
|
|
||||||
return {
|
|
||||||
onOpen(_event, ws) {
|
|
||||||
ws.close(4001, "Unauthorized: Invalid token");
|
|
||||||
},
|
|
||||||
onMessage() {},
|
|
||||||
onClose() {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
onOpen(_event, ws) {
|
|
||||||
logWs.info("client connected");
|
|
||||||
const state: ClientState = {
|
|
||||||
process: null,
|
|
||||||
connection: null,
|
|
||||||
sessionId: null,
|
|
||||||
pendingPermissions: new Map(),
|
|
||||||
agentCapabilities: null,
|
|
||||||
promptCapabilities: null,
|
|
||||||
modelState: null,
|
|
||||||
isAlive: true,
|
|
||||||
};
|
|
||||||
clients.set(ws, state);
|
|
||||||
|
|
||||||
const rawWs = ws.raw as RawWebSocket;
|
|
||||||
rawWs.on("pong", () => {
|
|
||||||
state.isAlive = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async onMessage(event, ws) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data.toString());
|
|
||||||
logWs.debug({ type: data.type }, "received");
|
|
||||||
|
|
||||||
switch (data.type) {
|
|
||||||
case "connect":
|
|
||||||
await handleConnect(ws);
|
|
||||||
break;
|
|
||||||
case "disconnect":
|
|
||||||
handleDisconnect(ws);
|
|
||||||
break;
|
|
||||||
case "new_session":
|
|
||||||
await handleNewSession(ws, (data.payload as { cwd?: string; permissionMode?: string }) || {});
|
|
||||||
break;
|
|
||||||
case "prompt":
|
|
||||||
await handlePrompt(ws, data.payload as { content: ContentBlock[] });
|
|
||||||
break;
|
|
||||||
case "permission_response":
|
|
||||||
handlePermissionResponse(ws, data.payload);
|
|
||||||
break;
|
|
||||||
case "cancel":
|
|
||||||
await handleCancel(ws);
|
|
||||||
break;
|
|
||||||
case "set_session_model":
|
|
||||||
await handleSetSessionModel(ws, data.payload as { modelId: string });
|
|
||||||
break;
|
|
||||||
case "list_sessions":
|
|
||||||
await handleListSessions(ws, (data.payload as { cwd?: string; cursor?: string }) || {});
|
|
||||||
break;
|
|
||||||
case "load_session":
|
|
||||||
await handleLoadSession(ws, data.payload as { sessionId: string; cwd?: string });
|
|
||||||
break;
|
|
||||||
case "resume_session":
|
|
||||||
await handleResumeSession(ws, data.payload as { sessionId: string; cwd?: string });
|
|
||||||
break;
|
|
||||||
case "ping":
|
|
||||||
send(ws, "pong");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
send(ws, "error", { message: `Unknown message type: ${data.type}` });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logWs.error({ error: (error as Error).message }, "message error");
|
|
||||||
send(ws, "error", { message: `Error: ${(error as Error).message}` });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClose(_event, ws) {
|
|
||||||
logWs.info("client disconnected");
|
|
||||||
const state = clients.get(ws);
|
|
||||||
if (state) {
|
|
||||||
cancelPendingPermissions(state);
|
|
||||||
}
|
|
||||||
handleDisconnect(ws);
|
|
||||||
clients.delete(ws);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create server with optional HTTPS
|
|
||||||
let server;
|
|
||||||
if (https) {
|
|
||||||
const tlsOptions = await getOrCreateCertificate();
|
|
||||||
server = serve({
|
|
||||||
fetch: app.fetch,
|
|
||||||
port,
|
|
||||||
hostname: host,
|
|
||||||
createServer: createHttpsServer,
|
|
||||||
serverOptions: tlsOptions,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
server = serve({ fetch: app.fetch, port, hostname: host });
|
|
||||||
}
|
|
||||||
injectWebSocket(server);
|
|
||||||
|
|
||||||
// Heartbeat: periodically ping all connected clients
|
|
||||||
setInterval(() => {
|
|
||||||
for (const [ws, state] of clients) {
|
|
||||||
// Skip virtual relay connections (no raw socket, always alive)
|
|
||||||
if (!ws.raw && state.isAlive) continue;
|
|
||||||
if (!ws.raw) {
|
|
||||||
// Connection already closed, clean up
|
|
||||||
clients.delete(ws);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!state.isAlive) {
|
|
||||||
logWs.info("heartbeat timeout, terminating");
|
|
||||||
(ws.raw as RawWebSocket).terminate();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
state.isAlive = false;
|
|
||||||
(ws.raw as RawWebSocket).ping();
|
|
||||||
}
|
|
||||||
}, HEARTBEAT_INTERVAL_MS);
|
|
||||||
|
|
||||||
// Protocol strings based on HTTPS mode
|
|
||||||
const wsProtocol = https ? "wss" : "ws";
|
|
||||||
|
|
||||||
// Get actual LAN IP when binding to 0.0.0.0
|
|
||||||
let displayHost = host;
|
|
||||||
if (host === "0.0.0.0") {
|
|
||||||
const lanIPs = getLanIPs();
|
|
||||||
displayHost = lanIPs[0] || "localhost";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build URLs
|
|
||||||
const localWsUrl = `${wsProtocol}://localhost:${port}/ws`;
|
|
||||||
const networkWsUrl = `${wsProtocol}://${displayHost}:${port}/ws`;
|
|
||||||
|
|
||||||
// Print startup banner
|
|
||||||
console.log();
|
|
||||||
console.log(` 🚀 ACP Proxy Server${https ? " (HTTPS)" : ""}`);
|
|
||||||
console.log();
|
|
||||||
console.log(` Connection:`);
|
|
||||||
if (host === "0.0.0.0") {
|
|
||||||
console.log(` URL: ${networkWsUrl}`);
|
|
||||||
} else {
|
|
||||||
console.log(` URL: ${localWsUrl}`);
|
|
||||||
}
|
|
||||||
if (AUTH_TOKEN) {
|
|
||||||
console.log(` Token: ${AUTH_TOKEN}`);
|
|
||||||
}
|
|
||||||
console.log();
|
|
||||||
if (!AUTH_TOKEN) {
|
|
||||||
console.log(` ⚠️ Authentication disabled (--no-auth)`);
|
|
||||||
console.log();
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentDisplay = AGENT_ARGS.length > 0
|
|
||||||
? `${AGENT_COMMAND} ${AGENT_ARGS.join(" ")}`
|
|
||||||
: AGENT_COMMAND;
|
|
||||||
console.log(` 📦 Agent: ${agentDisplay}`);
|
|
||||||
console.log(` CWD: ${AGENT_CWD}`);
|
|
||||||
console.log();
|
|
||||||
console.log(` Press Ctrl+C to stop`);
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
logServer.info({
|
|
||||||
port,
|
|
||||||
host,
|
|
||||||
https,
|
|
||||||
wsEndpoint: `${wsProtocol}://${displayHost}:${port}/ws`,
|
|
||||||
agent: AGENT_COMMAND,
|
|
||||||
agentArgs: AGENT_ARGS,
|
|
||||||
cwd: AGENT_CWD,
|
|
||||||
authEnabled: !!AUTH_TOKEN,
|
|
||||||
}, "started");
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
// JSON-RPC 2.0 Types
|
|
||||||
export interface JsonRpcRequest {
|
|
||||||
jsonrpc: "2.0";
|
|
||||||
id: string | number;
|
|
||||||
method: string;
|
|
||||||
params?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface JsonRpcResponse {
|
|
||||||
jsonrpc: "2.0";
|
|
||||||
id: string | number;
|
|
||||||
result?: unknown;
|
|
||||||
error?: JsonRpcError;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface JsonRpcNotification {
|
|
||||||
jsonrpc: "2.0";
|
|
||||||
method: string;
|
|
||||||
params?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface JsonRpcError {
|
|
||||||
code: number;
|
|
||||||
message: string;
|
|
||||||
data?: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JsonRpcMessage =
|
|
||||||
| JsonRpcRequest
|
|
||||||
| JsonRpcResponse
|
|
||||||
| JsonRpcNotification;
|
|
||||||
|
|
||||||
// Helper to check message types
|
|
||||||
export function isRequest(msg: JsonRpcMessage): msg is JsonRpcRequest {
|
|
||||||
return "method" in msg && "id" in msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isResponse(msg: JsonRpcMessage): msg is JsonRpcResponse {
|
|
||||||
return "id" in msg && !("method" in msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isNotification(
|
|
||||||
msg: JsonRpcMessage,
|
|
||||||
): msg is JsonRpcNotification {
|
|
||||||
return "method" in msg && !("id" in msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACP Protocol Types
|
|
||||||
|
|
||||||
// Client -> Server messages (from extension to proxy)
|
|
||||||
export interface ProxyConnectParams {
|
|
||||||
command: string; // Command to launch the agent (e.g., "claude-agent")
|
|
||||||
args?: string[]; // Optional arguments
|
|
||||||
cwd?: string; // Working directory for the agent
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProxyMessage {
|
|
||||||
type: "connect" | "disconnect" | "message";
|
|
||||||
payload?: ProxyConnectParams | JsonRpcMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server -> Client messages (from proxy to extension)
|
|
||||||
export interface ProxyStatus {
|
|
||||||
type: "status";
|
|
||||||
connected: boolean;
|
|
||||||
agentInfo?: {
|
|
||||||
name?: string;
|
|
||||||
version?: string;
|
|
||||||
};
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProxyAgentMessage {
|
|
||||||
type: "agent_message";
|
|
||||||
payload: JsonRpcMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProxyError {
|
|
||||||
type: "error";
|
|
||||||
message: string;
|
|
||||||
code?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProxyResponse = ProxyStatus | ProxyAgentMessage | ProxyError;
|
|
||||||
|
|
||||||
// ACP Initialization
|
|
||||||
export interface InitializeParams {
|
|
||||||
protocolVersion: string;
|
|
||||||
clientInfo: {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
};
|
|
||||||
capabilities?: ClientCapabilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientCapabilities {
|
|
||||||
streaming?: boolean;
|
|
||||||
toolApproval?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InitializeResult {
|
|
||||||
protocolVersion: string;
|
|
||||||
serverInfo: {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
};
|
|
||||||
capabilities?: ServerCapabilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerCapabilities {
|
|
||||||
streaming?: boolean;
|
|
||||||
tools?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACP Session
|
|
||||||
export interface SessionSetupParams {
|
|
||||||
sessionId?: string;
|
|
||||||
context?: SessionContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SessionContext {
|
|
||||||
workingDirectory?: string;
|
|
||||||
files?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACP Prompt
|
|
||||||
export interface PromptParams {
|
|
||||||
sessionId: string;
|
|
||||||
messages: PromptMessage[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PromptMessage {
|
|
||||||
role: "user" | "assistant";
|
|
||||||
content: string | ContentPart[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContentPart {
|
|
||||||
type: "text" | "image" | "file";
|
|
||||||
text?: string;
|
|
||||||
data?: string;
|
|
||||||
mimeType?: string;
|
|
||||||
path?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content streaming notification
|
|
||||||
export interface ContentNotification {
|
|
||||||
sessionId: string;
|
|
||||||
content: string;
|
|
||||||
done?: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
// Environment setup & latest features
|
|
||||||
"lib": ["ESNext"],
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"allowJs": true,
|
|
||||||
|
|
||||||
// Node.js module resolution
|
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
|
|
||||||
// Output
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
|
|
||||||
// Best practices
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"noPropertyAccessFromIndexSignature": false
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist", "src/__tests__"]
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,8 @@ import { z } from 'zod/v4'
|
|||||||
import type { ToolResultBlockParam } from 'src/Tool.js'
|
import type { ToolResultBlockParam } from 'src/Tool.js'
|
||||||
import { buildTool } from 'src/Tool.js'
|
import { buildTool } from 'src/Tool.js'
|
||||||
import { lazySchema } from 'src/utils/lazySchema.js'
|
import { lazySchema } from 'src/utils/lazySchema.js'
|
||||||
import { notifyAutomationStateChanged } from 'src/utils/sessionState.js'
|
|
||||||
import { SLEEP_TOOL_NAME, DESCRIPTION, SLEEP_TOOL_PROMPT } from './prompt.js'
|
import { SLEEP_TOOL_NAME, DESCRIPTION, SLEEP_TOOL_PROMPT } from './prompt.js'
|
||||||
|
|
||||||
const SLEEP_WAKE_CHECK_INTERVAL_MS = 500
|
|
||||||
|
|
||||||
const inputSchema = lazySchema(() =>
|
const inputSchema = lazySchema(() =>
|
||||||
z.strictObject({
|
z.strictObject({
|
||||||
duration_seconds: z
|
duration_seconds: z
|
||||||
@@ -22,36 +19,6 @@ type SleepInput = z.infer<InputSchema>
|
|||||||
|
|
||||||
type SleepOutput = { slept_seconds: number; interrupted: boolean }
|
type SleepOutput = { slept_seconds: number; interrupted: boolean }
|
||||||
|
|
||||||
function isProactiveAutomationEnabled(): boolean {
|
|
||||||
if (!(feature('PROACTIVE') || feature('KAIROS'))) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const mod =
|
|
||||||
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
|
|
||||||
return mod.isProactiveActive()
|
|
||||||
}
|
|
||||||
|
|
||||||
function isProactiveSleepAllowed(): boolean {
|
|
||||||
if (!(feature('PROACTIVE') || feature('KAIROS'))) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const mod =
|
|
||||||
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
|
|
||||||
return mod.isProactiveActive()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasQueuedWakeSignal(): boolean {
|
|
||||||
const queue =
|
|
||||||
require('src/utils/messageQueueManager.js') as typeof import('src/utils/messageQueueManager.js')
|
|
||||||
return queue.hasCommandsInQueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldInterruptSleep(): boolean {
|
|
||||||
return !isProactiveSleepAllowed() || hasQueuedWakeSignal()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SleepTool = buildTool({
|
export const SleepTool = buildTool({
|
||||||
name: SLEEP_TOOL_NAME,
|
name: SLEEP_TOOL_NAME,
|
||||||
searchHint: 'wait pause sleep rest idle duration timer',
|
searchHint: 'wait pause sleep rest idle duration timer',
|
||||||
@@ -75,9 +42,6 @@ export const SleepTool = buildTool({
|
|||||||
isReadOnly() {
|
isReadOnly() {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
interruptBehavior() {
|
|
||||||
return 'cancel'
|
|
||||||
},
|
|
||||||
|
|
||||||
userFacingName() {
|
userFacingName() {
|
||||||
return SLEEP_TOOL_NAME
|
return SLEEP_TOOL_NAME
|
||||||
@@ -103,84 +67,53 @@ export const SleepTool = buildTool({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async call(input: SleepInput, context) {
|
async call(input: SleepInput, context) {
|
||||||
// Don't enter sleep if proactive was disabled or new work arrived while
|
// Refuse to sleep when proactive mode is off — prevents the model from
|
||||||
// the model was deciding to wait.
|
// re-issuing Sleep after an interruption caused by /proactive disable.
|
||||||
if (shouldInterruptSleep()) {
|
if (feature('PROACTIVE') || feature('KAIROS')) {
|
||||||
return {
|
const mod =
|
||||||
data: {
|
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
|
||||||
slept_seconds: 0,
|
if (!mod.isProactiveActive()) {
|
||||||
interrupted: true,
|
return {
|
||||||
},
|
data: {
|
||||||
|
slept_seconds: 0,
|
||||||
|
interrupted: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { duration_seconds } = input
|
const { duration_seconds } = input
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
const sleepUntil = startTime + duration_seconds * 1000
|
|
||||||
|
|
||||||
if (isProactiveAutomationEnabled()) {
|
|
||||||
notifyAutomationStateChanged({
|
|
||||||
enabled: true,
|
|
||||||
phase: 'sleeping',
|
|
||||||
next_tick_at: null,
|
|
||||||
sleep_until: sleepUntil,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null
|
const timer = setTimeout(resolve, duration_seconds * 1000)
|
||||||
let wakeCheck: ReturnType<typeof setInterval> | null = null
|
|
||||||
let settled = false
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
if (timer !== null) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
timer = null
|
|
||||||
}
|
|
||||||
if (wakeCheck !== null) {
|
|
||||||
clearInterval(wakeCheck)
|
|
||||||
wakeCheck = null
|
|
||||||
}
|
|
||||||
context.abortController.signal.removeEventListener('abort', onAbort)
|
|
||||||
}
|
|
||||||
|
|
||||||
const finish = () => {
|
|
||||||
if (settled) return
|
|
||||||
settled = true
|
|
||||||
cleanup()
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
const interrupt = () => {
|
|
||||||
if (settled) return
|
|
||||||
settled = true
|
|
||||||
cleanup()
|
|
||||||
reject(new Error('interrupted'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const onAbort = () => {
|
|
||||||
interrupt()
|
|
||||||
}
|
|
||||||
|
|
||||||
timer = setTimeout(finish, duration_seconds * 1000)
|
|
||||||
|
|
||||||
// Abort via user interrupt
|
// Abort via user interrupt
|
||||||
if (context.abortController.signal.aborted) {
|
context.abortController.signal.addEventListener(
|
||||||
interrupt()
|
'abort',
|
||||||
return
|
() => {
|
||||||
}
|
clearTimeout(timer)
|
||||||
context.abortController.signal.addEventListener('abort', onAbort, {
|
clearInterval(proactiveCheck)
|
||||||
once: true,
|
reject(new Error('interrupted'))
|
||||||
})
|
},
|
||||||
|
{ once: true },
|
||||||
|
)
|
||||||
|
|
||||||
// Poll proactive state and the shared command queue so new work can
|
// Poll proactive state — if deactivated mid-sleep, interrupt early
|
||||||
// wake Sleep without waiting for the full duration.
|
// so the user doesn't have to wait for the full duration.
|
||||||
wakeCheck = setInterval(() => {
|
const proactiveCheck =
|
||||||
if (shouldInterruptSleep()) {
|
feature('PROACTIVE') || feature('KAIROS')
|
||||||
interrupt()
|
? setInterval(() => {
|
||||||
}
|
const mod =
|
||||||
}, SLEEP_WAKE_CHECK_INTERVAL_MS)
|
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
|
||||||
|
if (!mod.isProactiveActive()) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
clearInterval(proactiveCheck)
|
||||||
|
reject(new Error('interrupted'))
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
: (null as unknown as ReturnType<typeof setInterval>)
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
@@ -196,17 +129,6 @@ export const SleepTool = buildTool({
|
|||||||
interrupted: true,
|
interrupted: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
notifyAutomationStateChanged(
|
|
||||||
isProactiveAutomationEnabled()
|
|
||||||
? {
|
|
||||||
enabled: true,
|
|
||||||
phase: null,
|
|
||||||
next_tick_at: null,
|
|
||||||
sleep_until: null,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
||||||
import { SleepTool } from '../SleepTool'
|
|
||||||
import {
|
|
||||||
enqueue,
|
|
||||||
getCommandQueue,
|
|
||||||
resetCommandQueue,
|
|
||||||
} from 'src/utils/messageQueueManager.js'
|
|
||||||
|
|
||||||
describe('SleepTool', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
resetCommandQueue()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('declares cancel interrupt behavior', () => {
|
|
||||||
expect(SleepTool.interruptBehavior()).toBe('cancel')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('wakes early when queued work arrives', async () => {
|
|
||||||
const sleepPromise = SleepTool.call(
|
|
||||||
{ duration_seconds: 10 },
|
|
||||||
{ abortController: new AbortController() } as any,
|
|
||||||
)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
enqueue({
|
|
||||||
value: 'wake up',
|
|
||||||
mode: 'prompt',
|
|
||||||
})
|
|
||||||
}, 20)
|
|
||||||
|
|
||||||
const result = await sleepPromise
|
|
||||||
|
|
||||||
expect(result.data.interrupted).toBe(true)
|
|
||||||
expect(result.data.slept_seconds).toBeLessThan(10)
|
|
||||||
expect(getCommandQueue()).toHaveLength(1)
|
|
||||||
expect(getCommandQueue()[0]).toMatchObject({
|
|
||||||
value: 'wake up',
|
|
||||||
mode: 'prompt',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -4,21 +4,10 @@ WORKDIR /app
|
|||||||
|
|
||||||
ARG VERSION=0.1.0
|
ARG VERSION=0.1.0
|
||||||
|
|
||||||
# Copy package files for install
|
|
||||||
COPY packages/remote-control-server/package.json ./package.json
|
COPY packages/remote-control-server/package.json ./package.json
|
||||||
|
|
||||||
# Install all dependencies (including devDeps for vite build)
|
|
||||||
RUN bun install
|
RUN bun install
|
||||||
|
|
||||||
# Copy source code
|
|
||||||
COPY packages/remote-control-server/src ./src
|
COPY packages/remote-control-server/src ./src
|
||||||
COPY packages/remote-control-server/tsconfig.json ./tsconfig.json
|
|
||||||
|
|
||||||
# Copy web frontend source and build it
|
|
||||||
COPY packages/remote-control-server/web ./web
|
|
||||||
RUN bun run build:web
|
|
||||||
|
|
||||||
# Build backend
|
|
||||||
RUN bun build src/index.ts --outfile=dist/server.js --target=bun \
|
RUN bun build src/index.ts --outfile=dist/server.js --target=bun \
|
||||||
--define "process.env.RCS_VERSION=\"${VERSION}\""
|
--define "process.env.RCS_VERSION=\"${VERSION}\""
|
||||||
|
|
||||||
@@ -30,9 +19,8 @@ ENV RCS_VERSION=${VERSION}
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy built artifacts
|
|
||||||
COPY --from=builder /app/dist/server.js ./dist/server.js
|
COPY --from=builder /app/dist/server.js ./dist/server.js
|
||||||
COPY --from=builder /app/web/dist ./web/dist
|
COPY packages/remote-control-server/web ./web
|
||||||
|
|
||||||
VOLUME /app/data
|
VOLUME /app/data
|
||||||
|
|
||||||
|
|||||||
@@ -99,13 +99,6 @@ volumes:
|
|||||||
rcs-data:
|
rcs-data:
|
||||||
```
|
```
|
||||||
|
|
||||||
## ACP 兼容的 remote-control
|
|
||||||
|
|
||||||
|
|
||||||
```sh
|
|
||||||
ACP_RCS_URL=http://localhost:3000 ACP_RCS_TOKEN=test-my-key acp-link ccb-bun -- --acp
|
|
||||||
```
|
|
||||||
|
|
||||||
## 反向代理配置
|
## 反向代理配置
|
||||||
|
|
||||||
使用 Nginx 或 Caddy 反向代理时,需要支持 WebSocket 升级:
|
使用 Nginx 或 Caddy 反向代理时,需要支持 WebSocket 升级:
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
|
||||||
"style": "new-york",
|
|
||||||
"rsc": false,
|
|
||||||
"tsx": true,
|
|
||||||
"tailwind": {
|
|
||||||
"config": "",
|
|
||||||
"css": "src/styles/globals.css",
|
|
||||||
"baseColor": "neutral",
|
|
||||||
"cssVariables": true,
|
|
||||||
"prefix": ""
|
|
||||||
},
|
|
||||||
"iconLibrary": "lucide",
|
|
||||||
"aliases": {
|
|
||||||
"components": "@/components",
|
|
||||||
"utils": "@/lib/utils",
|
|
||||||
"ui": "@/components/ui",
|
|
||||||
"lib": "@/lib",
|
|
||||||
"hooks": "@/hooks"
|
|
||||||
},
|
|
||||||
"registries": {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -4,60 +4,24 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --watch src/index.ts",
|
"dev": "bun run --watch src/index.ts",
|
||||||
"dev:web": "cd web && bunx vite",
|
|
||||||
"start": "bun run src/index.ts",
|
"start": "bun run src/index.ts",
|
||||||
"build:web": "cd web && bunx vite build",
|
"build:web": "cd web && bun run build",
|
||||||
"preview:web": "cd web && bunx vite preview",
|
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/react": "^3.0.170",
|
|
||||||
"ai": "^6.0.168",
|
|
||||||
"hono": "^4.7.0",
|
"hono": "^4.7.0",
|
||||||
"jsqr": "^1.4.0",
|
"uuid": "^11.0.0"
|
||||||
"qrcode": "^1.5.4",
|
|
||||||
"uuid": "^11.0.0",
|
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
|
||||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
|
||||||
"browser-image-compression": "^2.0.2",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"cmdk": "^1.1.1",
|
|
||||||
"lucide-react": "^0.555.0",
|
|
||||||
"motion": "^12.29.2",
|
|
||||||
"nanoid": "^5.1.6",
|
|
||||||
"qr-scanner": "^1.4.2",
|
|
||||||
"radix-ui": "^1.4.3",
|
|
||||||
"react": "^19",
|
|
||||||
"react-dom": "^19",
|
|
||||||
"react-resizable-panels": "^4",
|
|
||||||
"shiki": "^3.17.0",
|
|
||||||
"streamdown": "^1.6.8",
|
|
||||||
"tailwind-merge": "^3.3.1",
|
|
||||||
"use-stick-to-bottom": "^1.1.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
|
||||||
"@types/qrcode": "^1.5.6",
|
|
||||||
"@types/react": "^19.0.0",
|
|
||||||
"@types/react-dom": "^19.0.0",
|
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"vite": "^6.0.0",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.7.0",
|
"@tailwindcss/vite": "^4.0.0"
|
||||||
"vite": "^6.0.0",
|
|
||||||
"tw-animate-css": "^1.4.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
|
||||||
import {
|
|
||||||
getAutomationStateSnapshot,
|
|
||||||
getAutomationStateEventPayload,
|
|
||||||
automationStatesEqual,
|
|
||||||
} from "../services/automationState";
|
|
||||||
import type { AutomationStateResponse } from "../types/api";
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// normalizeAutomationState (via getAutomationStateSnapshot)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("normalizeAutomationState", () => {
|
|
||||||
test("returns undefined when metadata has no automation_state key", () => {
|
|
||||||
expect(getAutomationStateSnapshot({})).toBeUndefined();
|
|
||||||
expect(getAutomationStateSnapshot({ other: true })).toBeUndefined();
|
|
||||||
expect(getAutomationStateSnapshot(null)).toBeUndefined();
|
|
||||||
expect(getAutomationStateSnapshot(undefined)).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns disabled state for null automation_state", () => {
|
|
||||||
const result = getAutomationStateSnapshot({ automation_state: null });
|
|
||||||
expect(result).toEqual({
|
|
||||||
enabled: false,
|
|
||||||
phase: null,
|
|
||||||
next_tick_at: null,
|
|
||||||
sleep_until: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns disabled state for non-object automation_state", () => {
|
|
||||||
for (const val of ["string", 123, true, []]) {
|
|
||||||
const result = getAutomationStateSnapshot({ automation_state: val });
|
|
||||||
expect(result?.enabled).toBe(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("normalizes enabled: true correctly", () => {
|
|
||||||
const result = getAutomationStateSnapshot({ automation_state: { enabled: true } });
|
|
||||||
expect(result?.enabled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("normalizes enabled to false for non-true values", () => {
|
|
||||||
const result = getAutomationStateSnapshot({ automation_state: { enabled: "yes" } });
|
|
||||||
expect(result?.enabled).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("accepts phase: standby", () => {
|
|
||||||
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, phase: "standby" } });
|
|
||||||
expect(result?.phase).toBe("standby");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("accepts phase: sleeping", () => {
|
|
||||||
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, phase: "sleeping" } });
|
|
||||||
expect(result?.phase).toBe("sleeping");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("rejects invalid phase values", () => {
|
|
||||||
for (const phase of ["running", "idle", "active", "", null]) {
|
|
||||||
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, phase } });
|
|
||||||
expect(result?.phase).toBeNull();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("normalizes next_tick_at as number", () => {
|
|
||||||
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, next_tick_at: 12345 } });
|
|
||||||
expect(result?.next_tick_at).toBe(12345);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("normalizes next_tick_at as null for non-number", () => {
|
|
||||||
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, next_tick_at: "soon" } });
|
|
||||||
expect(result?.next_tick_at).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("normalizes sleep_until as number", () => {
|
|
||||||
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, sleep_until: 99999 } });
|
|
||||||
expect(result?.sleep_until).toBe(99999);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("normalizes sleep_until as null for non-number", () => {
|
|
||||||
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, sleep_until: false } });
|
|
||||||
expect(result?.sleep_until).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("fully normalizes a complete valid state", () => {
|
|
||||||
const result = getAutomationStateSnapshot({
|
|
||||||
automation_state: { enabled: true, phase: "sleeping", next_tick_at: 100, sleep_until: 200 },
|
|
||||||
});
|
|
||||||
expect(result).toEqual({
|
|
||||||
enabled: true,
|
|
||||||
phase: "sleeping",
|
|
||||||
next_tick_at: 100,
|
|
||||||
sleep_until: 200,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// getAutomationStateEventPayload
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("getAutomationStateEventPayload", () => {
|
|
||||||
test("returns disabled default when no automation_state in metadata", () => {
|
|
||||||
const result = getAutomationStateEventPayload({});
|
|
||||||
expect(result).toEqual({
|
|
||||||
enabled: false,
|
|
||||||
phase: null,
|
|
||||||
next_tick_at: null,
|
|
||||||
sleep_until: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns disabled default for null metadata", () => {
|
|
||||||
const result = getAutomationStateEventPayload(null);
|
|
||||||
expect(result).toEqual({
|
|
||||||
enabled: false,
|
|
||||||
phase: null,
|
|
||||||
next_tick_at: null,
|
|
||||||
sleep_until: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns normalized state when automation_state present", () => {
|
|
||||||
const result = getAutomationStateEventPayload({
|
|
||||||
automation_state: { enabled: true, phase: "standby", next_tick_at: 50, sleep_until: 60 },
|
|
||||||
});
|
|
||||||
expect(result).toEqual({
|
|
||||||
enabled: true,
|
|
||||||
phase: "standby",
|
|
||||||
next_tick_at: 50,
|
|
||||||
sleep_until: 60,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns a new object each call (not frozen reference)", () => {
|
|
||||||
const a = getAutomationStateEventPayload({});
|
|
||||||
const b = getAutomationStateEventPayload({});
|
|
||||||
expect(a).toEqual(b);
|
|
||||||
expect(a).not.toBe(b);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// automationStatesEqual
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("automationStatesEqual", () => {
|
|
||||||
const base: AutomationStateResponse = {
|
|
||||||
enabled: true,
|
|
||||||
phase: "standby",
|
|
||||||
next_tick_at: 100,
|
|
||||||
sleep_until: 200,
|
|
||||||
};
|
|
||||||
|
|
||||||
test("returns true for identical states", () => {
|
|
||||||
expect(automationStatesEqual(base, { ...base })).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false when enabled differs", () => {
|
|
||||||
expect(automationStatesEqual(base, { ...base, enabled: false })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false when phase differs", () => {
|
|
||||||
expect(automationStatesEqual(base, { ...base, phase: "sleeping" })).toBe(false);
|
|
||||||
expect(automationStatesEqual(base, { ...base, phase: null })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false when next_tick_at differs", () => {
|
|
||||||
expect(automationStatesEqual(base, { ...base, next_tick_at: 999 })).toBe(false);
|
|
||||||
expect(automationStatesEqual(base, { ...base, next_tick_at: null })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false when sleep_until differs", () => {
|
|
||||||
expect(automationStatesEqual(base, { ...base, sleep_until: 999 })).toBe(false);
|
|
||||||
expect(automationStatesEqual(base, { ...base, sleep_until: null })).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns true when both are disabled defaults", () => {
|
|
||||||
const disabled: AutomationStateResponse = { enabled: false, phase: null, next_tick_at: null, sleep_until: null };
|
|
||||||
expect(automationStatesEqual(disabled, { ...disabled })).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
|
||||||
import { toClientPayload } from "../transport/client-payload";
|
|
||||||
import type { SessionEvent } from "../transport/event-bus";
|
|
||||||
|
|
||||||
function makeEvent(overrides: Partial<SessionEvent> & Pick<SessionEvent, "type" | "sessionId">): SessionEvent {
|
|
||||||
return {
|
|
||||||
id: "evt-1",
|
|
||||||
payload: null,
|
|
||||||
direction: "inbound",
|
|
||||||
seqNum: 1,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// user / user_message
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("toClientPayload — user message", () => {
|
|
||||||
test("maps user type with content", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "user",
|
|
||||||
sessionId: "sess-1",
|
|
||||||
payload: { content: "hello" },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect(result.type).toBe("user");
|
|
||||||
expect(result.session_id).toBe("sess-1");
|
|
||||||
expect((result as any).message.role).toBe("user");
|
|
||||||
expect((result as any).message.content).toBe("hello");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("maps user_message type same as user", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "user_message",
|
|
||||||
sessionId: "sess-2",
|
|
||||||
payload: { content: "world" },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect(result.type).toBe("user");
|
|
||||||
expect(result.session_id).toBe("sess-2");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("falls back to message field when content is missing", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "user",
|
|
||||||
sessionId: "sess-3",
|
|
||||||
payload: { message: "fallback msg" },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect((result as any).message.content).toBe("fallback msg");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("falls back to empty string when both content and message missing", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "user",
|
|
||||||
sessionId: "sess-4",
|
|
||||||
payload: {},
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect((result as any).message.content).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("includes isSynthetic when true", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "user",
|
|
||||||
sessionId: "sess-5",
|
|
||||||
payload: { content: "auto", isSynthetic: true },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect((result as any).isSynthetic).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not include isSynthetic when false", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "user",
|
|
||||||
sessionId: "sess-6",
|
|
||||||
payload: { content: "manual", isSynthetic: false },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect((result as any).isSynthetic).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses payload.uuid when present", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "user",
|
|
||||||
sessionId: "sess-7",
|
|
||||||
payload: { content: "hi", uuid: "custom-uuid" },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect(result.uuid).toBe("custom-uuid");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("falls back to event.id when payload.uuid is missing", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "user",
|
|
||||||
sessionId: "sess-8",
|
|
||||||
payload: { content: "hi" },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect(result.uuid).toBe("evt-1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// permission_response / control_response
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("toClientPayload — permission response", () => {
|
|
||||||
test("approved=true maps to allow behavior", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "permission_response",
|
|
||||||
sessionId: "sess-1",
|
|
||||||
payload: { approved: true, request_id: "req-1" },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect(result.type).toBe("control_response");
|
|
||||||
const resp = (result as any).response;
|
|
||||||
expect(resp.subtype).toBe("success");
|
|
||||||
expect(resp.request_id).toBe("req-1");
|
|
||||||
expect(resp.response.behavior).toBe("allow");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("approved=false maps to deny behavior with error", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "permission_response",
|
|
||||||
sessionId: "sess-2",
|
|
||||||
payload: { approved: false, request_id: "req-2" },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect(result.type).toBe("control_response");
|
|
||||||
const resp = (result as any).response;
|
|
||||||
expect(resp.subtype).toBe("error");
|
|
||||||
expect(resp.error).toBe("Permission denied by user");
|
|
||||||
expect(resp.response.behavior).toBe("deny");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("approved=false includes feedback message when provided", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "permission_response",
|
|
||||||
sessionId: "sess-3",
|
|
||||||
payload: { approved: false, request_id: "req-3", message: "please revise" },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect((result as any).response.message).toBe("please revise");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("passes through existingResponse directly", () => {
|
|
||||||
const existingResponse = { subtype: "success", custom: true };
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "control_response",
|
|
||||||
sessionId: "sess-4",
|
|
||||||
payload: { approved: true, response: existingResponse },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect(result.type).toBe("control_response");
|
|
||||||
expect((result as any).response).toBe(existingResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("includes updatedInput when approved with updated_input", () => {
|
|
||||||
const updatedInput = { file_path: "/new/path" };
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "permission_response",
|
|
||||||
sessionId: "sess-5",
|
|
||||||
payload: { approved: true, request_id: "req-5", updated_input: updatedInput },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect((result as any).response.response.updatedInput).toEqual(updatedInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("includes updatedPermissions when approved with updated_permissions", () => {
|
|
||||||
const perms = [{ type: "allow", tool: "bash" }];
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "permission_response",
|
|
||||||
sessionId: "sess-6",
|
|
||||||
payload: { approved: true, request_id: "req-6", updated_permissions: perms },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect((result as any).response.response.updatedPermissions).toEqual(perms);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// interrupt
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("toClientPayload — interrupt", () => {
|
|
||||||
test("maps interrupt to control_request with subtype interrupt", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "interrupt",
|
|
||||||
sessionId: "sess-1",
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect(result.type).toBe("control_request");
|
|
||||||
expect((result as any).request_id).toBe("evt-1");
|
|
||||||
expect((result as any).request.subtype).toBe("interrupt");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// control_request
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("toClientPayload — control_request", () => {
|
|
||||||
test("passes through request_id and request from payload", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "control_request",
|
|
||||||
sessionId: "sess-1",
|
|
||||||
payload: { request_id: "req-99", request: { subtype: "permission", tool: "bash" } },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect(result.type).toBe("control_request");
|
|
||||||
expect((result as any).request_id).toBe("req-99");
|
|
||||||
expect((result as any).request.subtype).toBe("permission");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("falls back request to payload when no request field", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "control_request",
|
|
||||||
sessionId: "sess-2",
|
|
||||||
payload: { request_id: "req-10", custom: "data" },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect((result as any).request).toEqual({ request_id: "req-10", custom: "data" });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("falls back request_id to event.id when missing", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "control_request",
|
|
||||||
sessionId: "sess-3",
|
|
||||||
payload: { request: { subtype: "test" } },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect((result as any).request_id).toBe("evt-1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// default fallback
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("toClientPayload — default types", () => {
|
|
||||||
test("passes through unknown type with type/uuid/session_id/message", () => {
|
|
||||||
const event = makeEvent({
|
|
||||||
type: "assistant",
|
|
||||||
sessionId: "sess-1",
|
|
||||||
payload: { uuid: "u-1", content: "response text" },
|
|
||||||
});
|
|
||||||
const result = toClientPayload(event);
|
|
||||||
expect(result.type).toBe("assistant");
|
|
||||||
expect(result.uuid).toBe("u-1");
|
|
||||||
expect(result.session_id).toBe("sess-1");
|
|
||||||
expect(result.message).toEqual({ uuid: "u-1", content: "response text" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -678,44 +678,6 @@ describe("Web Session Routes", () => {
|
|||||||
expect(getRes.status).toBe(200);
|
expect(getRes.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("GET /web/sessions/:id — includes automation_state snapshot when worker metadata has it", async () => {
|
|
||||||
const createRes = await app.request("/v1/code/sessions", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
});
|
|
||||||
const {
|
|
||||||
session: { id },
|
|
||||||
} = await createRes.json();
|
|
||||||
storeBindSession(id, "user-1");
|
|
||||||
|
|
||||||
await app.request(`/v1/code/sessions/${id}/worker`, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
worker_epoch: 1,
|
|
||||||
external_metadata: {
|
|
||||||
automation_state: {
|
|
||||||
enabled: true,
|
|
||||||
phase: "standby",
|
|
||||||
next_tick_at: 123456,
|
|
||||||
sleep_until: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const getRes = await app.request(`/web/sessions/${toWebSessionId(id)}?uuid=user-1`);
|
|
||||||
expect(getRes.status).toBe(200);
|
|
||||||
const body = await getRes.json();
|
|
||||||
expect(body.automation_state).toEqual({
|
|
||||||
enabled: true,
|
|
||||||
phase: "standby",
|
|
||||||
next_tick_at: 123456,
|
|
||||||
sleep_until: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("GET /web/sessions/:id — 403 for non-owner", async () => {
|
test("GET /web/sessions/:id — 403 for non-owner", async () => {
|
||||||
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -742,35 +704,6 @@ describe("Web Session Routes", () => {
|
|||||||
expect(body.events).toEqual([]);
|
expect(body.events).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("GET /web/sessions/:id/history — returns task_state snapshots", async () => {
|
|
||||||
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
});
|
|
||||||
const { id } = await createRes.json();
|
|
||||||
|
|
||||||
publishSessionEvent(
|
|
||||||
id,
|
|
||||||
"task_state",
|
|
||||||
{
|
|
||||||
task_list_id: "team-alpha",
|
|
||||||
tasks: [{ id: "1", subject: "Investigate", status: "pending" }],
|
|
||||||
},
|
|
||||||
"inbound",
|
|
||||||
);
|
|
||||||
|
|
||||||
const histRes = await app.request(`/web/sessions/${id}/history?uuid=user-1`);
|
|
||||||
expect(histRes.status).toBe(200);
|
|
||||||
const body = await histRes.json();
|
|
||||||
expect(body.events).toHaveLength(1);
|
|
||||||
expect(body.events[0]?.type).toBe("task_state");
|
|
||||||
expect(body.events[0]?.payload.task_list_id).toBe("team-alpha");
|
|
||||||
expect(body.events[0]?.payload.tasks).toEqual([
|
|
||||||
{ id: "1", subject: "Investigate", status: "pending" },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("GET /web/sessions/:id and history — supports compat code session IDs", async () => {
|
test("GET /web/sessions/:id and history — supports compat code session IDs", async () => {
|
||||||
const codeSession = storeCreateSession({ idPrefix: "cse_" });
|
const codeSession = storeCreateSession({ idPrefix: "cse_" });
|
||||||
storeBindSession(codeSession.id, "user-1");
|
storeBindSession(codeSession.id, "user-1");
|
||||||
@@ -1285,15 +1218,7 @@ describe("V2 Worker Events Routes", () => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
worker_epoch: 1,
|
worker_epoch: 1,
|
||||||
worker_status: "running",
|
worker_status: "running",
|
||||||
external_metadata: {
|
external_metadata: { permission_mode: "default" },
|
||||||
permission_mode: "default",
|
|
||||||
automation_state: {
|
|
||||||
enabled: true,
|
|
||||||
phase: "sleeping",
|
|
||||||
next_tick_at: null,
|
|
||||||
sleep_until: 123456,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
expect(putRes.status).toBe(200);
|
expect(putRes.status).toBe(200);
|
||||||
@@ -1305,21 +1230,6 @@ describe("V2 Worker Events Routes", () => {
|
|||||||
const body = await getRes.json();
|
const body = await getRes.json();
|
||||||
expect(body.worker.worker_status).toBe("running");
|
expect(body.worker.worker_status).toBe("running");
|
||||||
expect(body.worker.external_metadata.permission_mode).toBe("default");
|
expect(body.worker.external_metadata.permission_mode).toBe("default");
|
||||||
expect(body.worker.external_metadata.automation_state).toEqual({
|
|
||||||
enabled: true,
|
|
||||||
phase: "sleeping",
|
|
||||||
next_tick_at: null,
|
|
||||||
sleep_until: 123456,
|
|
||||||
});
|
|
||||||
|
|
||||||
const events = getEventBus(id).getEventsSince(0);
|
|
||||||
expect(events.some((event) => event.type === "automation_state")).toBe(true);
|
|
||||||
expect(events.at(-1)?.payload).toEqual({
|
|
||||||
enabled: true,
|
|
||||||
phase: "sleeping",
|
|
||||||
next_tick_at: null,
|
|
||||||
sleep_until: 123456,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("POST /v1/code/sessions/:id/worker/heartbeat — updates heartbeat", async () => {
|
test("POST /v1/code/sessions/:id/worker/heartbeat — updates heartbeat", async () => {
|
||||||
@@ -1374,123 +1284,6 @@ describe("V2 Worker Events Routes", () => {
|
|||||||
reader.cancel();
|
reader.cancel();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("GET /v1/code/sessions/:id/worker/events/stream — normalizes web permission approvals to control_response", async () => {
|
|
||||||
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
});
|
|
||||||
const { id } = await createRes.json();
|
|
||||||
|
|
||||||
const streamRes = await app.request(`/v1/code/sessions/${id}/worker/events/stream`, {
|
|
||||||
headers: AUTH_HEADERS,
|
|
||||||
});
|
|
||||||
expect(streamRes.status).toBe(200);
|
|
||||||
|
|
||||||
const reader = streamRes.body?.getReader();
|
|
||||||
expect(reader).toBeTruthy();
|
|
||||||
if (!reader) return;
|
|
||||||
|
|
||||||
await reader.read(); // initial keepalive
|
|
||||||
|
|
||||||
const controlRes = await app.request(`/web/sessions/${id}/control?uuid=user-1`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
type: "permission_response",
|
|
||||||
approved: true,
|
|
||||||
request_id: "req-1",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(controlRes.status).toBe(200);
|
|
||||||
|
|
||||||
const chunk = await reader.read();
|
|
||||||
const frame = new TextDecoder().decode(chunk.value!);
|
|
||||||
expect(frame).toContain("event: client_event");
|
|
||||||
expect(frame).toContain("\"event_type\":\"permission_response\"");
|
|
||||||
expect(frame).toContain("\"payload\":{\"type\":\"control_response\"");
|
|
||||||
expect(frame).toContain("\"request_id\":\"req-1\"");
|
|
||||||
expect(frame).toContain("\"behavior\":\"allow\"");
|
|
||||||
reader.cancel();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("GET /v1/code/sessions/:id/worker/events/stream — normalizes web plan rejection feedback to deny control_response", async () => {
|
|
||||||
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
});
|
|
||||||
const { id } = await createRes.json();
|
|
||||||
|
|
||||||
const streamRes = await app.request(`/v1/code/sessions/${id}/worker/events/stream`, {
|
|
||||||
headers: AUTH_HEADERS,
|
|
||||||
});
|
|
||||||
expect(streamRes.status).toBe(200);
|
|
||||||
|
|
||||||
const reader = streamRes.body?.getReader();
|
|
||||||
expect(reader).toBeTruthy();
|
|
||||||
if (!reader) return;
|
|
||||||
|
|
||||||
await reader.read(); // initial keepalive
|
|
||||||
|
|
||||||
const controlRes = await app.request(`/web/sessions/${id}/control?uuid=user-1`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
type: "permission_response",
|
|
||||||
approved: false,
|
|
||||||
request_id: "req-2",
|
|
||||||
message: "Need more detail",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
expect(controlRes.status).toBe(200);
|
|
||||||
|
|
||||||
const chunk = await reader.read();
|
|
||||||
const frame = new TextDecoder().decode(chunk.value!);
|
|
||||||
expect(frame).toContain("event: client_event");
|
|
||||||
expect(frame).toContain("\"event_type\":\"permission_response\"");
|
|
||||||
expect(frame).toContain("\"payload\":{\"type\":\"control_response\"");
|
|
||||||
expect(frame).toContain("\"request_id\":\"req-2\"");
|
|
||||||
expect(frame).toContain("\"subtype\":\"error\"");
|
|
||||||
expect(frame).toContain("\"behavior\":\"deny\"");
|
|
||||||
expect(frame).toContain("\"message\":\"Need more detail\"");
|
|
||||||
reader.cancel();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("GET /v1/code/sessions/:id/worker/events/stream — normalizes web interrupts to control_request", async () => {
|
|
||||||
const createRes = await app.request("/web/sessions?uuid=user-1", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
});
|
|
||||||
const { id } = await createRes.json();
|
|
||||||
|
|
||||||
const streamRes = await app.request(`/v1/code/sessions/${id}/worker/events/stream`, {
|
|
||||||
headers: AUTH_HEADERS,
|
|
||||||
});
|
|
||||||
expect(streamRes.status).toBe(200);
|
|
||||||
|
|
||||||
const reader = streamRes.body?.getReader();
|
|
||||||
expect(reader).toBeTruthy();
|
|
||||||
if (!reader) return;
|
|
||||||
|
|
||||||
await reader.read(); // initial keepalive
|
|
||||||
|
|
||||||
const interruptRes = await app.request(`/web/sessions/${id}/interrupt?uuid=user-1`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
expect(interruptRes.status).toBe(200);
|
|
||||||
|
|
||||||
const chunk = await reader.read();
|
|
||||||
const frame = new TextDecoder().decode(chunk.value!);
|
|
||||||
expect(frame).toContain("event: client_event");
|
|
||||||
expect(frame).toContain("\"event_type\":\"interrupt\"");
|
|
||||||
expect(frame).toContain("\"payload\":{\"type\":\"control_request\"");
|
|
||||||
expect(frame).toContain("\"subtype\":\"interrupt\"");
|
|
||||||
reader.cancel();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("PUT /v1/code/sessions/:id/worker/state — updates session status", async () => {
|
test("PUT /v1/code/sessions/:id/worker/state — updates session status", async () => {
|
||||||
const sessRes = await app.request("/v1/sessions", {
|
const sessRes = await app.request("/v1/sessions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -353,14 +353,6 @@ describe("Transport Service", () => {
|
|||||||
expect(result.uuid).toBe("msg_123");
|
expect(result.uuid).toBe("msg_123");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("preserves isSynthetic field", () => {
|
|
||||||
const result = normalizePayload("user", {
|
|
||||||
content: "scheduled job: refresh analytics cache",
|
|
||||||
isSynthetic: true,
|
|
||||||
});
|
|
||||||
expect(result.isSynthetic).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses name as tool_name fallback", () => {
|
test("uses name as tool_name fallback", () => {
|
||||||
const result = normalizePayload("tool", { name: "Read" });
|
const result = normalizePayload("tool", { name: "Read" });
|
||||||
expect(result.tool_name).toBe("Read");
|
expect(result.tool_name).toBe("Read");
|
||||||
@@ -378,28 +370,6 @@ describe("Transport Service", () => {
|
|||||||
expect(result.content).toBe("");
|
expect(result.content).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("preserves task_state fields", () => {
|
|
||||||
const result = normalizePayload("task_state", {
|
|
||||||
task_list_id: "team-alpha",
|
|
||||||
tasks: [{ id: "1", subject: "Task 1", status: "pending" }],
|
|
||||||
});
|
|
||||||
expect(result.task_list_id).toBe("team-alpha");
|
|
||||||
expect(result.tasks).toEqual([
|
|
||||||
{ id: "1", subject: "Task 1", status: "pending" },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves status metadata for conversation reset events", () => {
|
|
||||||
const result = normalizePayload("status", {
|
|
||||||
status: "conversation_cleared",
|
|
||||||
subtype: "status",
|
|
||||||
message: "conversation_cleared",
|
|
||||||
});
|
|
||||||
expect(result.status).toBe("conversation_cleared");
|
|
||||||
expect(result.subtype).toBe("status");
|
|
||||||
expect(result.message).toBe("conversation_cleared");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("handles undefined payload", () => {
|
test("handles undefined payload", () => {
|
||||||
const result = normalizePayload("user", undefined);
|
const result = normalizePayload("user", undefined);
|
||||||
expect(result.content).toBe("");
|
expect(result.content).toBe("");
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
import { describe, test, expect } from "bun:test";
|
|
||||||
|
|
||||||
const { normalizePayload } = await import("../services/transport");
|
|
||||||
|
|
||||||
// extractContent is not exported; we test it via normalizePayload's content field
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// extractContent (via normalizePayload content field)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("extractContent", () => {
|
|
||||||
test("returns empty string for null payload", () => {
|
|
||||||
const result = normalizePayload("assistant", null);
|
|
||||||
expect(result.content).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns empty string for undefined payload", () => {
|
|
||||||
const result = normalizePayload("assistant", undefined);
|
|
||||||
expect(result.content).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns the string for string payload", () => {
|
|
||||||
const result = normalizePayload("assistant", "hello world");
|
|
||||||
expect(result.content).toBe("hello world");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("extracts content field from object payload", () => {
|
|
||||||
const result = normalizePayload("assistant", { content: "direct content" });
|
|
||||||
expect(result.content).toBe("direct content");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("extracts message.content string from object payload", () => {
|
|
||||||
const result = normalizePayload("assistant", { message: { content: "msg content" } });
|
|
||||||
expect(result.content).toBe("msg content");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("extracts text blocks from message.content array", () => {
|
|
||||||
const payload = {
|
|
||||||
message: {
|
|
||||||
content: [
|
|
||||||
{ type: "text", text: "Hello " },
|
|
||||||
{ type: "text", text: "World" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const result = normalizePayload("assistant", payload);
|
|
||||||
expect(result.content).toBe("Hello World");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("ignores non-text blocks in message.content array", () => {
|
|
||||||
const payload = {
|
|
||||||
message: {
|
|
||||||
content: [
|
|
||||||
{ type: "image", url: "http://example.com/img.png" },
|
|
||||||
{ type: "text", text: "only this" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const result = normalizePayload("assistant", payload);
|
|
||||||
expect(result.content).toBe("only this");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns empty string when no extractable content", () => {
|
|
||||||
const result = normalizePayload("assistant", { foo: "bar" });
|
|
||||||
expect(result.content).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("prefers direct content over message.content", () => {
|
|
||||||
const result = normalizePayload("assistant", { content: "direct", message: { content: "nested" } });
|
|
||||||
expect(result.content).toBe("direct");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// normalizePayload — field preservation
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("normalizePayload — field preservation", () => {
|
|
||||||
test("preserves raw payload", () => {
|
|
||||||
const payload = { content: "test", extra: true };
|
|
||||||
const result = normalizePayload("assistant", payload);
|
|
||||||
expect(result.raw).toBe(payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves uuid field", () => {
|
|
||||||
const result = normalizePayload("assistant", { uuid: "u-123" });
|
|
||||||
expect(result.uuid).toBe("u-123");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not preserve uuid when empty string", () => {
|
|
||||||
const result = normalizePayload("assistant", { uuid: "" });
|
|
||||||
expect(result.uuid).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves isSynthetic boolean", () => {
|
|
||||||
const result = normalizePayload("assistant", { isSynthetic: true });
|
|
||||||
expect(result.isSynthetic).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves status string", () => {
|
|
||||||
const result = normalizePayload("assistant", { status: "running" });
|
|
||||||
expect(result.status).toBe("running");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves subtype string", () => {
|
|
||||||
const result = normalizePayload("assistant", { subtype: "progress" });
|
|
||||||
expect(result.subtype).toBe("progress");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves tool_name from tool_name field", () => {
|
|
||||||
const result = normalizePayload("tool", { tool_name: "bash" });
|
|
||||||
expect(result.tool_name).toBe("bash");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves tool_name from name field", () => {
|
|
||||||
const result = normalizePayload("tool", { name: "read" });
|
|
||||||
expect(result.tool_name).toBe("read");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves tool_input from tool_input field", () => {
|
|
||||||
const input = { command: "ls" };
|
|
||||||
const result = normalizePayload("tool", { tool_input: input });
|
|
||||||
expect(result.tool_input).toEqual(input);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves tool_input from input field", () => {
|
|
||||||
const input = { path: "/tmp" };
|
|
||||||
const result = normalizePayload("tool", { input });
|
|
||||||
expect(result.tool_input).toEqual(input);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves request_id", () => {
|
|
||||||
const result = normalizePayload("permission", { request_id: "req-1" });
|
|
||||||
expect(result.request_id).toBe("req-1");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves request object", () => {
|
|
||||||
const req = { subtype: "permission" };
|
|
||||||
const result = normalizePayload("permission", { request: req });
|
|
||||||
expect(result.request).toEqual(req);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves approved field", () => {
|
|
||||||
const result = normalizePayload("permission", { approved: true });
|
|
||||||
expect(result.approved).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves updated_input", () => {
|
|
||||||
const input = { command: "rm -rf" };
|
|
||||||
const result = normalizePayload("permission", { updated_input: input });
|
|
||||||
expect(result.updated_input).toEqual(input);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves message field for backward compat", () => {
|
|
||||||
const msg = { role: "user", content: "hi" };
|
|
||||||
const result = normalizePayload("assistant", { message: msg });
|
|
||||||
expect(result.message).toEqual(msg);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// normalizePayload — task_state special handling
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
describe("normalizePayload — task_state type", () => {
|
|
||||||
test("preserves task_list_id (snake_case)", () => {
|
|
||||||
const result = normalizePayload("task_state", { task_list_id: "tl-1" });
|
|
||||||
expect(result.task_list_id).toBe("tl-1");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves taskListId (camelCase)", () => {
|
|
||||||
const result = normalizePayload("task_state", { taskListId: "tl-2" });
|
|
||||||
expect(result.taskListId).toBe("tl-2");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves tasks array", () => {
|
|
||||||
const tasks = [{ id: "t1", title: "Task 1" }];
|
|
||||||
const result = normalizePayload("task_state", { tasks });
|
|
||||||
expect(result.tasks).toEqual(tasks);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not preserve task fields for non-task_state type", () => {
|
|
||||||
const result = normalizePayload("assistant", { task_list_id: "tl-1", taskListId: "tl-2", tasks: [] });
|
|
||||||
expect(result.task_list_id).toBeUndefined();
|
|
||||||
expect(result.taskListId).toBeUndefined();
|
|
||||||
expect(result.tasks).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user