mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 05:45:51 +00:00
Compare commits
2 Commits
v1.0.3
...
version/i1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f6fe77eee | ||
|
|
a68e9637c0 |
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: hello-agent
|
||||
description: A friendly greeting agent that introduces the project
|
||||
---
|
||||
|
||||
You are a friendly greeting agent. Your job is to greet the user and provide helpful information about the current project.
|
||||
|
||||
Instructions:
|
||||
1. Read the project's CLAUDE.md to understand the project context.
|
||||
2. Greet the user warmly.
|
||||
3. Provide a brief summary of the project based on what you learned from CLAUDE.md.
|
||||
4. Offer to help with any questions about the project.
|
||||
|
||||
Style:
|
||||
- Be concise and friendly.
|
||||
- Respond in 简体中文.
|
||||
- Keep responses short — no more than a few sentences.
|
||||
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -20,6 +20,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: bun run lint
|
||||
|
||||
- name: Test
|
||||
run: bun test
|
||||
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -8,4 +8,9 @@ coverage
|
||||
.vscode
|
||||
*.suo
|
||||
*.lock
|
||||
src/utils/vendor/
|
||||
.gitignore
|
||||
*.code-workspace
|
||||
doc-prompt-eng.md
|
||||
claude.bat
|
||||
.claude
|
||||
chn_prompt_plan.md
|
||||
13
.vscode/launch.json
vendored
13
.vscode/launch.json
vendored
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "Attach to Bun (TUI debug)",
|
||||
"url": "ws://localhost:8888/2dc3gzl5xot",
|
||||
"stopOnEntry": false,
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
}
|
||||
110
CLAUDE.md
110
CLAUDE.md
@@ -12,66 +12,36 @@ This is a **reverse-engineered / decompiled** version of Anthropic's official Cl
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Dev mode (runs cli.tsx with MACRO defines injected via -d flags)
|
||||
# Dev mode (direct execution via Bun)
|
||||
bun run dev
|
||||
|
||||
# Dev mode with debugger (set BUN_INSPECT=9229 to pick port)
|
||||
bun run dev:inspect
|
||||
# equivalent to: bun run src/entrypoints/cli.tsx
|
||||
|
||||
# Pipe mode
|
||||
echo "say hello" | bun run src/entrypoints/cli.tsx -p
|
||||
|
||||
# Build (code splitting, outputs dist/cli.js + ~450 chunk files)
|
||||
# Build (outputs dist/cli.js, ~25MB)
|
||||
bun run build
|
||||
|
||||
# Test
|
||||
bun test # run all tests
|
||||
bun test src/utils/__tests__/hash.test.ts # run single file
|
||||
bun test --coverage # with coverage report
|
||||
|
||||
# Lint & Format (Biome)
|
||||
bun run lint # check only
|
||||
bun run lint:fix # auto-fix
|
||||
bun run format # format all src/
|
||||
|
||||
# Health check
|
||||
bun run health
|
||||
|
||||
# Check unused exports
|
||||
bun run check:unused
|
||||
|
||||
# Docs dev server (Mintlify)
|
||||
bun run docs:dev
|
||||
```
|
||||
|
||||
详细的测试规范、覆盖状态和改进计划见 `docs/testing-spec.md`。
|
||||
No test runner is configured. No linter is configured.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Runtime & Build
|
||||
|
||||
- **Runtime**: Bun (not Node.js). All imports, builds, and execution use Bun APIs.
|
||||
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。默认启用 `AGENT_TRIGGERS_REMOTE` feature。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。
|
||||
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines,运行 `src/entrypoints/cli.tsx`。默认启用 `BUDDY`、`TRANSCRIPT_CLASSIFIER`、`BRIDGE_MODE`、`AGENT_TRIGGERS_REMOTE` 四个 feature。
|
||||
- **Build**: `bun build src/entrypoints/cli.tsx --outdir dist --target bun` — single-file bundle.
|
||||
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
|
||||
- **Monorepo**: Bun workspaces — internal packages live in `packages/` resolved via `workspace:*`.
|
||||
- **Lint/Format**: Biome (`biome.json`)。`bun run lint` / `bun run lint:fix` / `bun run format`。
|
||||
- **Defines**: 集中管理在 `scripts/defines.ts`。当前版本 `2.1.888`。
|
||||
|
||||
### Entry & Bootstrap
|
||||
|
||||
1. **`src/entrypoints/cli.tsx`** — True entrypoint。`main()` 函数按优先级处理多条快速路径:
|
||||
- `--version` / `-v` — 零模块加载
|
||||
- `--dump-system-prompt` — feature-gated (DUMP_SYSTEM_PROMPT)
|
||||
- `--claude-in-chrome-mcp` / `--chrome-native-host`
|
||||
- `--daemon-worker=<kind>` — feature-gated (DAEMON)
|
||||
- `remote-control` / `rc` / `bridge` — feature-gated (BRIDGE_MODE)
|
||||
- `daemon` — feature-gated (DAEMON)
|
||||
- `ps` / `logs` / `attach` / `kill` / `--bg` — feature-gated (BG_SESSIONS)
|
||||
- `--tmux` + `--worktree` 组合
|
||||
- 默认路径:加载 `main.tsx` 启动完整 CLI
|
||||
2. **`src/main.tsx`** (~4680 行) — Commander.js CLI definition。注册大量 subcommands:`mcp` (serve/add/remove/list...)、`server`、`ssh`、`open`、`auth`、`plugin`、`agents`、`auto-mode`、`doctor`、`update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。
|
||||
3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog)。
|
||||
1. **`src/entrypoints/cli.tsx`** — True entrypoint. Injects runtime polyfills at the top:
|
||||
- `feature()` always returns `false` (all feature flags disabled, skipping unimplemented branches).
|
||||
- `globalThis.MACRO` — simulates build-time macro injection (VERSION, BUILD_TIME, etc.).
|
||||
- `BUILD_TARGET`, `BUILD_ENV`, `INTERFACE_TYPE` globals.
|
||||
2. **`src/main.tsx`** — Commander.js CLI definition. Parses args, initializes services (auth, analytics, policy), then launches the REPL or runs in pipe mode.
|
||||
3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog).
|
||||
|
||||
### Core Loop
|
||||
|
||||
@@ -89,37 +59,25 @@ bun run docs:dev
|
||||
|
||||
- **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`).
|
||||
- **`src/tools.ts`** — Tool registry. Assembles the tool list; some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
|
||||
- **`src/tools/<ToolName>/`** — 61 个 tool 目录(如 BashTool, FileEditTool, GrepTool, AgentTool, WebFetchTool, LSPTool, MCPTool 等)。每个 tool 包含 `name`、`description`、`inputSchema`、`call()` 及可选的 React 渲染组件。
|
||||
- **`src/tools/shared/`** — Tool 共享工具函数。
|
||||
- **`src/tools/<ToolName>/`** — Each tool in its own directory (e.g., `BashTool`, `FileEditTool`, `GrepTool`, `AgentTool`).
|
||||
- Tools define: `name`, `description`, `inputSchema` (JSON Schema), `call()` (execution), and optionally a React component for rendering results.
|
||||
|
||||
### UI Layer (Ink)
|
||||
|
||||
- **`src/ink.ts`** — Ink render wrapper with ThemeProvider injection.
|
||||
- **`src/ink/`** — Custom Ink framework (forked/internal): custom reconciler, hooks (`useInput`, `useTerminalSize`, `useSearchHighlight`), virtual list rendering.
|
||||
- **`src/components/`** — 大量 React 组件(170+ 项),渲染于终端 Ink 环境中。关键组件:
|
||||
- `App.tsx` — Root provider (AppState, Stats, FpsMetrics)
|
||||
- `Messages.tsx` / `MessageRow.tsx` — Conversation message rendering
|
||||
- `PromptInput/` — User input handling
|
||||
- `permissions/` — Tool permission approval UI
|
||||
- `design-system/` — 复用 UI 组件(Dialog, FuzzyPicker, ProgressBar, ThemeProvider 等)
|
||||
- **`src/components/`** — React components rendered in terminal via Ink. Key ones:
|
||||
- `App.tsx` — Root provider (AppState, Stats, FpsMetrics).
|
||||
- `Messages.tsx` / `MessageRow.tsx` — Conversation message rendering.
|
||||
- `PromptInput/` — User input handling.
|
||||
- `permissions/` — Tool permission approval UI.
|
||||
- Components use React Compiler runtime (`react/compiler-runtime`) — decompiled output has `_c()` memoization calls throughout.
|
||||
|
||||
### State Management
|
||||
|
||||
- **`src/state/AppState.tsx`** — Central app state type and context provider. Contains messages, tools, permissions, MCP connections, etc.
|
||||
- **`src/state/AppStateStore.ts`** — Default state and store factory.
|
||||
- **`src/state/store.ts`** — Zustand-style store for AppState (`createStore`).
|
||||
- **`src/state/selectors.ts`** — State selectors.
|
||||
- **`src/bootstrap/state.ts`** — Module-level singletons for session-global state (session ID, CWD, project root, token counts, model overrides, client type, permission mode).
|
||||
|
||||
### Bridge / Remote Control
|
||||
|
||||
- **`src/bridge/`** (~35 files) — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`。
|
||||
- CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`。
|
||||
|
||||
### Daemon Mode
|
||||
|
||||
- **`src/daemon/`** — Daemon 模式(长驻 supervisor)。feature-gated by `DAEMON`。包含 `main.ts`(entry)和 `workerRegistry.ts`(worker 管理)。
|
||||
- **`src/state/store.ts`** — Zustand-style store for AppState.
|
||||
- **`src/bootstrap/state.ts`** — Module-level singletons for session-global state (session ID, CWD, project root, token counts).
|
||||
|
||||
### Context & System Prompt
|
||||
|
||||
@@ -128,16 +86,7 @@ bun run docs:dev
|
||||
|
||||
### Feature Flag System
|
||||
|
||||
Feature flags control which functionality is enabled at runtime:
|
||||
|
||||
- **在代码中使用**: 统一通过 `import { feature } from 'bun:bundle'` 导入,调用 `feature('FLAG_NAME')` 返回 `boolean`。**不要**在 `cli.tsx` 或其他文件里自己定义 `feature` 函数或覆盖这个 import。
|
||||
- **启用方式**: 通过环境变量 `FEATURE_<FLAG_NAME>=1`。例如 `FEATURE_BUDDY=1 bun run dev` 启用 BUDDY 功能。
|
||||
- **Dev 默认 features**: `BUDDY`、`TRANSCRIPT_CLASSIFIER`、`BRIDGE_MODE`、`AGENT_TRIGGERS_REMOTE`(见 `scripts/dev.ts`)。
|
||||
- **Build 默认 features**: `AGENT_TRIGGERS_REMOTE`(见 `build.ts`)。
|
||||
- **常见 flag**: `BUDDY`, `DAEMON`, `BRIDGE_MODE`, `BG_SESSIONS`, `PROACTIVE`, `KAIROS`, `VOICE_MODE`, `FORK_SUBAGENT`, `SSH_REMOTE`, `DIRECT_CONNECT`, `TEMPLATES`, `CHICAGO_MCP`, `BYOC_ENVIRONMENT_RUNNER`, `SELF_HOSTED_RUNNER`, `COORDINATOR_MODE`, `UDS_INBOX`, `LODESTONE`, `ABLATION_BASELINE` 等。
|
||||
- **类型声明**: `src/types/internal-modules.d.ts` 中声明了 `bun:bundle` 模块的 `feature` 函数签名。
|
||||
|
||||
**新增功能的正确做法**: 保留 `import { feature } from 'bun:bundle'` + `feature('FLAG_NAME')` 的标准模式,在运行时通过环境变量或配置控制,不要绕过 feature flag 直接 import。
|
||||
All `feature('FLAG_NAME')` calls come from `bun:bundle` (a build-time API). In this decompiled version, `feature()` is polyfilled to always return `false` in `cli.tsx`. This means all Anthropic-internal features (COORDINATOR_MODE, KAIROS, PROACTIVE, etc.) are disabled.
|
||||
|
||||
### Stubbed/Deleted Modules
|
||||
|
||||
@@ -157,23 +106,10 @@ Feature flags control which functionality is enabled at runtime:
|
||||
- **`src/types/message.ts`** — Message type hierarchy (UserMessage, AssistantMessage, SystemMessage, etc.).
|
||||
- **`src/types/permissions.ts`** — Permission mode and result types.
|
||||
|
||||
## Testing
|
||||
|
||||
- **框架**: `bun:test`(内置断言 + mock)
|
||||
- **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `<module>.test.ts`
|
||||
- **集成测试**: `tests/integration/` — 4 个文件(cli-arguments, context-build, message-pipeline, tool-chain)
|
||||
- **共享 mock/fixture**: `tests/mocks/`(api-responses, file-system, fixtures/)
|
||||
- **命名**: `describe("functionName")` + `test("behavior description")`,英文
|
||||
- **Mock 模式**: 对重依赖模块使用 `mock.module()` + `await import()` 解锁(必须内联在测试文件中,不能从共享 helper 导入)
|
||||
- **当前状态**: ~1623 tests / 114 files (110 unit + 4 integration) / 0 fail(详见 `docs/testing-spec.md`)
|
||||
|
||||
## Working with This Codebase
|
||||
|
||||
- **Don't try to fix all tsc errors** — they're from decompilation and don't affect runtime.
|
||||
- **Feature flags** — 默认全部关闭(`feature()` 返回 `false`)。Dev/build 各有自己的默认启用列表。不要在 `cli.tsx` 中重定义 `feature` 函数。
|
||||
- **`feature()` is always `false`** — any code behind a feature flag is dead code in this build.
|
||||
- **React Compiler output** — Components have decompiled memoization boilerplate (`const $ = _c(N)`). This is normal.
|
||||
- **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。
|
||||
- **`bun:bundle` import** — In `src/main.tsx` and other files, `import { feature } from 'bun:bundle'` works at build time. At dev-time, the polyfill in `cli.tsx` provides it.
|
||||
- **`src/` path alias** — tsconfig maps `src/*` to `./src/*`. Imports like `import { ... } from 'src/utils/...'` are valid.
|
||||
- **MACRO defines** — 集中管理在 `scripts/defines.ts`。Dev mode 通过 `bun -d` 注入,build 通过 `Bun.build({ define })` 注入。修改版本号等常量只改这个文件。
|
||||
- **构建产物兼容 Node.js** — `build.ts` 会自动后处理 `import.meta.require`,产物可直接用 `node dist/cli.js` 运行。
|
||||
- **Biome 配置** — 大量 lint 规则被关闭(decompiled 代码不适合严格 lint)。`.tsx` 文件用 120 行宽 + 强制分号;其他文件 80 行宽 + 按需分号。
|
||||
|
||||
287
DEV-LOG.md
287
DEV-LOG.md
@@ -1,287 +0,0 @@
|
||||
# DEV-LOG
|
||||
|
||||
## OpenAI 接口兼容 (2026-04-03)
|
||||
|
||||
**分支**: `feature/openai`
|
||||
|
||||
在 `/login` 流程中新增 "OpenAI Compatible" 选项,支持 Ollama、DeepSeek、vLLM、One API 等兼容 OpenAI Chat Completions API 的第三方服务。用户通过 `/login` 配置后,所有 API 请求自动走 OpenAI 路径。
|
||||
|
||||
**改动文件(10 个,+384 / -134):**
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `.github/workflows/ci.yml` | CI runner 从 `ubuntu-latest` 改为 `macos-latest` |
|
||||
| `README.md` | TODO 列表新增 "OpenAI 接口兼容" 条目 |
|
||||
| `src/components/ConsoleOAuthFlow.tsx` | 新增 `openai_chat_api` OAuth state(含 Base URL / API Key / 3 个模型映射字段);idle 选择列表新增 "OpenAI Compatible" 选项;完整表单 UI(Tab 切换、Enter 保存);保存时写入 `modelType: 'openai'` + env 到 settings.json;OAuth 登录时重置 `modelType` 为 `anthropic` |
|
||||
| `src/services/api/openai/index.ts` | 从直接 `yield* adaptOpenAIStreamToAnthropic()` 改为完整流处理循环:累积 content blocks(text/tool_use/thinking)、按 `content_block_stop` yield `AssistantMessage`、同时 yield `StreamEvent` 用于实时显示;错误处理改用新签名 `createAssistantAPIErrorMessage({ content, apiError, error })` |
|
||||
| `src/services/api/openai/convertMessages.ts` | 输入类型从 Anthropic SDK `BetaMessageParam[]` 改为内部 `(UserMessage \| AssistantMessage)[]`;通过 `msg.type` 而非 `msg.role` 判断角色;从 `msg.message.content` 读取内容;跳过 `cache_edits` / `server_tool_use` 等内部 block 类型 |
|
||||
| `src/services/api/openai/modelMapping.ts` | 移除 `OPENAI_MODEL_MAP` JSON 环境变量 + 缓存机制;新增 `getModelFamily()` 按 haiku/sonnet/opus 分类;解析优先级改为:`OPENAI_MODEL` → `ANTHROPIC_DEFAULT_{FAMILY}_MODEL` → `DEFAULT_MODEL_MAP` → 原名透传 |
|
||||
| `src/services/api/openai/__tests__/convertMessages.test.ts` | 测试输入从裸 `{ role, content }` 改为 `makeUserMsg()` / `makeAssistantMsg()` 包装的内部格式 |
|
||||
| `src/services/api/openai/__tests__/modelMapping.test.ts` | 测试从 `OPENAI_MODEL_MAP` 改为 `ANTHROPIC_DEFAULT_{HAIKU,SONNET,OPUS}_MODEL`;新增 3 个 env var override 测试 |
|
||||
| `src/utils/model/providers.ts` | `getAPIProvider()` 新增最高优先级:从 settings.json `modelType` 字段判断;环境变量 `CLAUDE_CODE_USE_OPENAI` 降为次优先 |
|
||||
| `src/utils/settings/types.ts` | `SettingsSchema` 新增 `modelType` 字段:`z.enum(['anthropic', 'openai']).optional()` |
|
||||
|
||||
**关键设计决策:**
|
||||
|
||||
1. **`modelType` 存入 settings.json** — 而非纯环境变量,使 `/login` 配置持久化,重启后仍然生效
|
||||
2. **复用 `ANTHROPIC_DEFAULT_*_MODEL` 环境变量** — 而非新增 `OPENAI_MODEL_MAP`,与 Custom Platform 共用同一套模型映射配置,减少用户认知负担
|
||||
3. **流处理双 yield** — 同时 yield `AssistantMessage`(给消费方处理工具调用)和 `StreamEvent`(给 REPL 实时渲染),与 Anthropic 路径行为对齐
|
||||
4. **OAuth 登录重置 modelType** — 用户切换回官方 Anthropic 登录时自动重置为 `anthropic`,避免残留配置导致请求走错误路径
|
||||
|
||||
**配置方式:**
|
||||
|
||||
```
|
||||
/login → 选择 "OpenAI Compatible" → 填写 Base URL / API Key / 模型名称
|
||||
```
|
||||
|
||||
或手动编辑 `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"modelType": "openai",
|
||||
"env": {
|
||||
"OPENAI_BASE_URL": "http://localhost:11434/v1",
|
||||
"OPENAI_API_KEY": "ollama",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "qwen3:32b"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Enable Remote Control / BRIDGE_MODE (2026-04-03)
|
||||
|
||||
**PR**: [claude-code-best/claude-code#60](https://github.com/claude-code-best/claude-code/pull/60)
|
||||
|
||||
Remote Control 功能将本地 CLI 注册为 bridge 环境,生成可分享的 URL(`https://claude.ai/code/session_xxx`),允许从浏览器、手机或其他设备远程查看输出、发送消息、审批工具调用。
|
||||
|
||||
**改动文件:**
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `scripts/dev.ts` | `DEFAULT_FEATURES` 加入 `"BRIDGE_MODE"`,dev 模式默认启用 |
|
||||
| `src/bridge/peerSessions.ts` | stub → 完整实现:通过 bridge API 发送跨会话消息,含三层安全防护(trim + validateBridgeId 白名单 + encodeURIComponent) |
|
||||
| `src/bridge/webhookSanitizer.ts` | stub → 完整实现:正则 redact 8 类 secret(GitHub/Anthropic/AWS/npm/Slack token),先 redact 再截断,失败返回安全占位符 |
|
||||
| `src/entrypoints/sdk/controlTypes.ts` | 12 个 `any` stub → `z.infer<ReturnType<typeof XxxSchema>>` 从现有 Zod schema 推导类型 |
|
||||
| `src/hooks/useReplBridge.tsx` | `tengu_bridge_system_init` 默认值 `false` → `true`,使 app 端显示 "active" 而非卡在 "connecting" |
|
||||
|
||||
**关键设计决策:**
|
||||
|
||||
1. **不改现有代码逻辑** — 只补全 stub、修正默认值、开启编译开关
|
||||
2. **`tengu_bridge_system_init`** — Anthropic 通过 GrowthBook 给订阅用户推送 `true`,但我们的 build 收不到推送;改默认值是唯一不侵入其他代码的方案
|
||||
3. **`peerSessions.ts` 认证** — 使用 `getBridgeAccessToken()` 获取 OAuth Bearer token,与 `bridgeApi.ts`/`codeSessionApi.ts` 认证模式一致
|
||||
4. **`webhookSanitizer.ts` 安全** — fail-closed(出错返回 `[webhook content redacted due to sanitization error]`),不泄露原始内容
|
||||
|
||||
**验证结果:**
|
||||
|
||||
- `/remote-control` 命令可见且可用
|
||||
- CLI 连接 Anthropic CCR,生成可分享 URL
|
||||
- App 端(claude.ai/code)显示 "Remote Control active"
|
||||
- 手机端(Claude iOS app)通过 URL 连接,双向消息正常
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## GrowthBook 自定义服务器适配器 (2026-04-03)
|
||||
|
||||
GrowthBook 功能开关系统原为 Anthropic 内部构建设计,硬编码 SDK key 和 API 地址,外部构建因 `is1PEventLoggingEnabled()` 门控始终禁用。新增适配器模式,通过环境变量连接自定义 GrowthBook 服务器,无配置时所有 feature 读取返回代码默认值。
|
||||
|
||||
**修改文件:**
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/constants/keys.ts` | `getGrowthBookClientKey()` 优先读取 `CLAUDE_GB_ADAPTER_KEY` 环境变量 |
|
||||
| `src/services/analytics/growthbook.ts` | `isGrowthBookEnabled()` 适配器模式下直接返回 `true`,绕过 1P event logging 门控 |
|
||||
| `src/services/analytics/growthbook.ts` | `getGrowthBookClient()` base URL 优先使用 `CLAUDE_GB_ADAPTER_URL` |
|
||||
| `docs/internals/growthbook-adapter.mdx` | 新增适配器配置文档,含全部 ~58 个 feature key 列表 |
|
||||
|
||||
**用法:** `CLAUDE_GB_ADAPTER_URL=https://gb.example.com/ CLAUDE_GB_ADAPTER_KEY=sdk-xxx bun run dev`
|
||||
|
||||
---
|
||||
|
||||
## Datadog 日志端点可配置化 (2026-04-03)
|
||||
|
||||
将 Datadog 硬编码的 Anthropic 内部端点改为环境变量驱动,默认禁用。
|
||||
|
||||
**修改文件:**
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/services/analytics/datadog.ts` | `DATADOG_LOGS_ENDPOINT` 和 `DATADOG_CLIENT_TOKEN` 从硬编码常量改为读取 `process.env.DATADOG_LOGS_ENDPOINT` / `process.env.DATADOG_API_KEY`,默认空字符串;`initializeDatadog()` 增加守卫:端点或 Token 未配置时直接返回 `false` |
|
||||
| `docs/telemetry-remote-config-audit.md` | 更新第 1 节,反映新的环境变量配置方式 |
|
||||
|
||||
**效果:** 默认不向任何外部发送数据;设置两个环境变量即可接入自己的 Datadog 实例。原有 `DISABLE_TELEMETRY`、privacy level、sink killswitch 等防线保留。
|
||||
|
||||
**用法:** `DATADOG_LOGS_ENDPOINT=https://http-intake.logs.datadoghq.com/api/v2/logs DATADOG_API_KEY=xxx bun run dev`
|
||||
|
||||
---
|
||||
|
||||
## Sentry 错误上报集成 (2026-04-03)
|
||||
|
||||
恢复反编译过程中被移除的 Sentry 集成。通过 `SENTRY_DSN` 环境变量控制,未设置时所有函数为 no-op,不影响正常运行。
|
||||
|
||||
**新增文件:**
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `src/utils/sentry.ts` | 核心模块:`initSentry()`、`captureException()`、`setTag()`、`setUser()`、`closeSentry()`;`beforeSend` 过滤 auth headers 等敏感信息;忽略 ECONNREFUSED/AbortError 等非 actionable 错误 |
|
||||
|
||||
**修改文件:**
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/utils/errorLogSink.ts` | `logErrorImpl` 末尾调用 `captureException()`,所有经 `logError()` 的错误自动上报 |
|
||||
| `src/components/SentryErrorBoundary.ts` | 添加 `componentDidCatch`,React 组件渲染错误上报到 Sentry(含 componentStack) |
|
||||
| `src/entrypoints/init.ts` | 网络配置后调用 `initSentry()` |
|
||||
| `src/utils/gracefulShutdown.ts` | 优雅关闭时 flush Sentry 事件 |
|
||||
| `src/screens/REPL.tsx:2809` | `fireCompanionObserver` 调用增加 `typeof` 防护,BUDDY feature 启用时不报错(TODO: 待实现) |
|
||||
| `package.json` | devDependencies 新增 `@sentry/node` |
|
||||
|
||||
**用法:** `SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx bun run dev`
|
||||
|
||||
---
|
||||
|
||||
## 默认关闭自动更新 (2026-04-03)
|
||||
|
||||
修改 `src/utils/config.ts` — `getAutoUpdaterDisabledReason()`,在原有检查逻辑前插入默认关闭逻辑。未设置 `ENABLE_AUTOUPDATER=1` 时,自动更新始终返回 `{ type: 'config' }` 被禁用。
|
||||
|
||||
**启用方式:** `ENABLE_AUTOUPDATER=1 bun run dev`
|
||||
|
||||
**原因:** 本项目为逆向工程/反编译版本,自动更新会覆盖本地修改的代码。
|
||||
|
||||
**同时新增文档:** `docs/auto-updater.md` — 自动更新机制完整审计,涵盖三种安装类型的更新策略、后台轮询、版本门控、原生安装器架构、文件锁、配置项等。
|
||||
|
||||
---
|
||||
|
||||
## WebSearch Bing 适配器补全 (2026-04-03)
|
||||
|
||||
原始 `WebSearchTool` 仅支持 Anthropic API 服务端搜索(`web_search_20250305` server tool),在非官方 API 端点(第三方代理)下搜索功能不可用。本次改动引入适配器架构,新增 Bing 搜索页面解析作为 fallback。
|
||||
|
||||
**新增文件:**
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `src/tools/WebSearchTool/adapters/types.ts` | 适配器接口定义:`WebSearchAdapter`、`SearchResult`、`SearchOptions`、`SearchProgress` |
|
||||
| `src/tools/WebSearchTool/adapters/apiAdapter.ts` | API 适配器 — 将原有 `queryModelWithStreaming` 逻辑封装为 `ApiSearchAdapter` |
|
||||
| `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing 适配器 — 直接抓取 Bing HTML,正则提取搜索结果 |
|
||||
| `src/tools/WebSearchTool/adapters/index.ts` | 适配器工厂 — 根据环境变量 / API Base URL 选择后端 |
|
||||
| `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | Bing 适配器单元测试(32 cases:decodeHtmlEntities、extractBingResults、search mock) |
|
||||
| `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | Bing 适配器集成测试 — 真实网络请求验证 |
|
||||
|
||||
**重构文件:**
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/tools/WebSearchTool/WebSearchTool.ts` | 从直接调用 API 改为 `createAdapter()` 工厂模式;`isEnabled()` 始终返回 true;删除 ~200 行内联 API 调用逻辑 |
|
||||
| `src/tools/WebFetchTool/utils.ts` | `skipWebFetchPreflight` 默认值从 `!undefined`(即 true)改为显式 `=== false`,使域名预检默认启用 |
|
||||
|
||||
**Bing 适配器关键技术细节:**
|
||||
|
||||
1. **反爬绕过**:使用完整 Edge 浏览器请求头(含 `Sec-Ch-Ua`、`Sec-Fetch-*` 等 13 个标头),避免 Bing 返回 JS 渲染的空页面;`setmkt=en-US` 参数强制美式英语市场,避免 IP 地理定位导致的区域化结果(德语论坛、新加坡金价等不相关内容)
|
||||
2. **URL 解码**(`resolveBingUrl()`):Bing 返回的重定向 URL(`bing.com/ck/a?...&u=a1aHR0cHM6Ly9...`)中 `u` 参数为 base64 编码的真实 URL,需解码后使用
|
||||
3. **摘要提取**(`extractSnippet()`):三级降级策略 — `b_lineclamp` → `b_caption <p>` → `b_caption` 直接文本
|
||||
4. **HTML 实体解码**(`decodeHtmlEntities()`):处理 7 种常见 HTML 实体
|
||||
5. **域过滤**:客户端侧 `allowedDomains` / `blockedDomains` 过滤,支持子域名匹配
|
||||
|
||||
**当前状态**:`adapters/index.ts` 中 `createAdapter()` 硬编码返回 `BingSearchAdapter`,跳过了 API/Bing 自动选择逻辑(原逻辑被注释保留)。未来可通过取消注释恢复自动选择。
|
||||
|
||||
---
|
||||
|
||||
## 移除反蒸馏机制 (2026-04-02)
|
||||
|
||||
项目中发现三处 anti-distillation 相关代码,全部移除。
|
||||
|
||||
**移除内容:**
|
||||
- `src/services/api/claude.ts` — 删除 fake_tools 注入逻辑(原第 302-314 行),该代码通过 `ANTI_DISTILLATION_CC` feature flag 在 API 请求中注入 `anti_distillation: ['fake_tools']`,使服务端在响应中混入虚假工具调用以污染蒸馏数据
|
||||
- `src/utils/betas.ts` — 删除 connector-text summarization beta 注入块及 `SUMMARIZE_CONNECTOR_TEXT_BETA_HEADER` 导入,该机制让服务端缓冲工具调用间的 assistant 文本并摘要化返回
|
||||
- `src/constants/betas.ts` — 删除 `SUMMARIZE_CONNECTOR_TEXT_BETA_HEADER` 常量定义(原第 23-25 行)
|
||||
- `src/utils/streamlinedTransform.ts` — 注释从 "distillation-resistant" 改为 "compact",streamlined 模式本身是有效的输出压缩功能,仅修正描述
|
||||
|
||||
---
|
||||
|
||||
## Buddy 命令合入 + Feature Flag 规范修正 (2026-04-02)
|
||||
|
||||
合入 `pr/smallflyingpig/36` 分支(支持 buddy 命令 + 修复 rehatch),并修正 feature flag 使用方式。
|
||||
|
||||
**合入内容(来自 PR):**
|
||||
- `src/commands/buddy/buddy.ts` — 新增 `/buddy` 命令,支持 hatch / rehatch / pet / mute / unmute 子命令
|
||||
- `src/commands/buddy/index.ts` — 从 stub 改为正确的 `Command` 类型导出
|
||||
- `src/buddy/companion.ts` — 新增 `generateSeed()`,`getCompanion()` 支持 seed 驱动的可复现 rolling
|
||||
- `src/buddy/types.ts` — `CompanionSoul` 增加 `seed?` 字段
|
||||
|
||||
**合并后修正:**
|
||||
- `src/entrypoints/cli.tsx` — PR 硬编码了 `const feature = (name) => name === "BUDDY"`,违反 feature flag 规范,恢复为标准 `import { feature } from 'bun:bundle'`
|
||||
- `src/commands.ts` — PR 用静态 `import buddy` 绕过了 feature gate,恢复为 `feature('BUDDY') ? require(...) : null` + 条件展开
|
||||
- `src/commands/buddy/buddy.ts` — 删除未使用的 `companionInfoText` 函数和多余的 `Roll`/`SPECIES` import
|
||||
- `CLAUDE.md` — 重写 Feature Flag System 章节,明确规范:代码中统一用 `import { feature } from 'bun:bundle'`,启用走环境变量 `FEATURE_<NAME>=1`
|
||||
|
||||
**用法:** `FEATURE_BUDDY=1 bun run dev`
|
||||
|
||||
---
|
||||
|
||||
## Auto Mode 补全 (2026-04-02)
|
||||
|
||||
反编译丢失了 auto mode 分类器的三个 prompt 模板文件,代码逻辑完整但无法运行。
|
||||
|
||||
**新增:**
|
||||
- `yolo-classifier-prompts/auto_mode_system_prompt.txt` — 主系统提示词
|
||||
- `yolo-classifier-prompts/permissions_external.txt` — 外部权限模板(用户规则替换默认值)
|
||||
- `yolo-classifier-prompts/permissions_anthropic.txt` — 内部权限模板(用户规则追加)
|
||||
|
||||
**改动:**
|
||||
- `scripts/dev.ts` + `build.ts` — 扫描 `FEATURE_*` 环境变量注入 Bun `--feature`
|
||||
- `cli.tsx` — 启动时打印已启用的 feature
|
||||
- `permissionSetup.ts` — `AUTO_MODE_ENABLED_DEFAULT` 由 `feature('TRANSCRIPT_CLASSIFIER')` 决定,开 feature 即开 auto mode
|
||||
- `docs/safety/auto-mode.mdx` — 补充 prompt 模板章节
|
||||
|
||||
**用法:** `FEATURE_TRANSCRIPT_CLASSIFIER=1 bun run dev`
|
||||
|
||||
**注意:** prompt 模板为重建产物。
|
||||
|
||||
---
|
||||
|
||||
## USER_TYPE=ant TUI 修复 (2026-04-02)
|
||||
|
||||
`global.d.ts` 声明的全局函数在反编译版本运行时未定义,导致 `USER_TYPE=ant` 时 TUI 崩溃。
|
||||
|
||||
修复方式:显式 import / 本地 stub / 全局 stub / 新建 stub 文件。涉及文件:
|
||||
`cli.tsx`, `model.ts`, `context.ts`, `effort.ts`, `thinking.ts`, `undercover.ts`, `Spinner.tsx`, `AntModelSwitchCallout.tsx`(新建), `UndercoverAutoCallout.tsx`(新建)
|
||||
|
||||
注意:
|
||||
- `USER_TYPE=ant` 启用 alt-screen 全屏模式,中心区域满屏是预期行为
|
||||
- `global.d.ts` 中剩余未 stub 的全局函数(`getAntModels` 等)遇到 `X is not defined` 时按同样模式处理
|
||||
|
||||
---
|
||||
|
||||
## /login 添加 Custom Platform 选项 (2026-04-03)
|
||||
|
||||
在 `/login` 命令的登录方式选择列表中新增 "Custom Platform" 选项(位于第一位),允许用户直接在终端配置第三方 API 兼容服务的 Base URL、API Key 和三种模型映射,保存到 `~/.claude/settings.json`。
|
||||
|
||||
**修改文件:**
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/components/ConsoleOAuthFlow.tsx` | `OAuthStatus` 类型新增 `custom_platform` state(含 `baseUrl`、`apiKey`、`haikuModel`、`sonnetModel`、`opusModel`、`activeField`);`idle` case Select 选项新增 Custom Platform 并排第一位;新增 `custom_platform` case 渲染 5 字段表单(Tab/Shift+Tab 切换、focus 高亮、Enter 跳转/保存);Select onChange 处理 `custom_platform` 初始状态(从 `process.env` 预填当前值);`OAuthStatusMessageProps` 类型及调用处新增 `onDone` prop |
|
||||
| `src/components/ConsoleOAuthFlow.tsx` | 新增 `updateSettingsForSource` import |
|
||||
|
||||
**UI 交互:**
|
||||
- 5 个字段同屏:Base URL、API Key、Haiku Model、Sonnet Model、Opus Model
|
||||
- 当前活动字段的标签用 `suggestion` 背景色 + `inverseText` 反色高亮
|
||||
- Tab / Shift+Tab 在字段间切换,各自保留输入值
|
||||
- 每个字段按 Enter 跳到下一个,最后一个字段 (Opus) 按 Enter 保存
|
||||
- 模型字段自动从 `process.env` 读取当前配置作为预填值,无值则空
|
||||
- 保存时调用 `updateSettingsForSource('userSettings', { env })` 写入 settings.json,同时更新 `process.env`
|
||||
|
||||
**保存的 settings.json env 字段:**
|
||||
```json
|
||||
{
|
||||
"ANTHROPIC_BASE_URL": "...",
|
||||
"ANTHROPIC_AUTH_TOKEN": "...",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "...",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "...",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "..."
|
||||
}
|
||||
```
|
||||
|
||||
非空字段才写入,保存后立即生效(`onDone()` 触发 `onChangeAPIKey()` 刷新 API 客户端)。
|
||||
475
README.md
475
README.md
@@ -1,21 +1,9 @@
|
||||
# Claude Code Best V5 (CCB)
|
||||
# Claude Code Best V3 (CCB)
|
||||
|
||||
[](https://github.com/claude-code-best/claude-code/stargazers)
|
||||
[](https://github.com/claude-code-best/claude-code/graphs/contributors)
|
||||
[](https://github.com/claude-code-best/claude-code/issues)
|
||||
[](https://github.com/claude-code-best/claude-code/blob/main/LICENSE)
|
||||
[](https://github.com/claude-code-best/claude-code/commits/main)
|
||||
[](https://bun.sh/)
|
||||
[](https://discord.gg/qZU6zS7Q)
|
||||
|
||||
> Which Claude do you like? The open source one is the best.
|
||||
|
||||
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)...
|
||||
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现。虽然很难绷, 但是它叫做 CCB(踩踩背)...
|
||||
|
||||
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/)
|
||||
|
||||
[Discord 群组](https://discord.gg/qZU6zS7Q)
|
||||
|
||||
赞助商占位符
|
||||
|
||||
- [x] v1 会完成跑通及基本的类型检查通过;
|
||||
@@ -23,29 +11,22 @@
|
||||
- [ ] Biome 格式化可能不会先实施, 避免代码冲突
|
||||
- [x] 构建流水线完成, 产物 Node/Bun 都可以运行
|
||||
- [x] V3 会写大量文档, 完善文档站点
|
||||
- [x] V4 会完成大量的测试文件, 以提高稳定性
|
||||
- [x] Buddy 小宠物回来啦 [文档](https://ccb.agent-aura.top/docs/features/buddy)
|
||||
- [x] Auto Mode 回归 [文档](https://ccb.agent-aura.top/docs/safety/auto-mode)
|
||||
- [x] 所有 Feature 现在可以通过环境变量配置, 而不是垃圾的 bun --feature
|
||||
- [x] V5 支持企业级的监控上报功能, 补全缺失的工具, 解除限制
|
||||
- [x] 移除牢 A 的反蒸馏代码!!!
|
||||
- [x] 补全 web search 能力(用的 Bing 搜索)!!! [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool)
|
||||
- [x] 支持 Debug [文档](https://ccb.agent-aura.top/docs/features/debug-mode)
|
||||
- [x] 关闭自动更新;
|
||||
- [x] 添加自定义 sentry 错误上报支持 [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup)
|
||||
- [x] 添加自定义 GrowthBook 支持 (GB 也是开源的, 现在你可以配置一个自定义的遥控平台) [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter)
|
||||
- [x] 自定义 login 模式, 大家可以用这个配置 Claude 的模型!
|
||||
- [x] 修复搜索工具的 rg 缺失问题(需要重新 bun i)
|
||||
- [ ] OpenAI 接口兼容! /login 然后配置 OpenAI 平台即可!
|
||||
- [ ] V6 大规模重构石山代码, 全面模块分包
|
||||
- [ ] V6 将会为全新分支, 届时 main 分支将会封存为历史版本
|
||||
- [ ] V4 会完成大量的测试文件, 以提高稳定性
|
||||
|
||||
> 我不知道这个项目还会存在多久, Star + Fork + git clone + .zip 包最稳健; 说白了就是扛旗项目, 看看能走多远
|
||||
> 我不知道这个项目还会存在多久, Star + Fork + git clone + .zip 包最稳健;
|
||||
>
|
||||
> 这个项目更新很快, 后台有 Opus 持续优化, 几乎几个小时就有新变化;
|
||||
> 这个项目更新很快, 后台有 Opus 持续优化, 所以你可以提 issues, 但是 PR 暂时不会接受;
|
||||
>
|
||||
> Claude 已经烧了 1000$ 以上, 没钱了, 换成 GLM 继续玩; @zai-org GLM 5.1 非常可以;
|
||||
> Claude 已经烧了 800$ 以上, 如果你个人想赞助, 请随便找个机构捐款, 然后截图在 issues, 大家的力量是温暖的;
|
||||
>
|
||||
> 某些模型提供商想要赞助, 那么请私发一个 1w 额度以上的账号到 <claude-code-best@proton.me>; 我们会在赞助商栏直接给你最亮的位置
|
||||
|
||||
存活记录:
|
||||
|
||||
1. 开源后 24 小时: 突破 6k Star, 感谢各位支持. 完成 docs 文档的站点构建, 达到 v3 版本, 后续开始进行测试用例维护, 完成之后可以接受 PR; 看来牢 A 是不想理我们了;
|
||||
2. 开源后 15 小时: 完成了构建产物的 node 支持, 现在是完全体了; star 快到 3k 了; 等待牢 A 的邮件
|
||||
3. 开源后 12 小时: 愚人节, star 破 1k, 并且牢 A 没有发邮件搞这个项目
|
||||
4. 如果你想要私人咨询服务, 那么可以发送邮件到 <claude-code-best@proton.me>, 备注咨询与联系方式即可; 由于后续工作非常多, 可能会忽略邮件, 半天没回复, 可以多发;
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -78,77 +59,9 @@ bun run build
|
||||
|
||||
如果遇到 bug 请直接提一个 issues, 我们优先解决
|
||||
|
||||
### 新人配置 /login
|
||||
|
||||
首次运行后,在 REPL 中输入 `/login` 命令进入登录配置界面,选择 **Custom Platform** 即可对接第三方 API 兼容服务(无需 Anthropic 官方账号)。
|
||||
|
||||
需要填写的字段:
|
||||
|
||||
| 字段 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| Base URL | API 服务地址 | `https://api.example.com/v1` |
|
||||
| API Key | 认证密钥 | `sk-xxx` |
|
||||
| Haiku Model | 快速模型 ID | `claude-haiku-4-5-20251001` |
|
||||
| Sonnet Model | 均衡模型 ID | `claude-sonnet-4-6` |
|
||||
| Opus Model | 高性能模型 ID | `claude-opus-4-6` |
|
||||
|
||||
- **Tab / Shift+Tab** 切换字段,**Enter** 确认并跳到下一个,最后一个字段按 Enter 保存
|
||||
- 模型字段会自动读取当前环境变量预填
|
||||
- 配置保存到 `~/.claude/settings.json` 的 `env` 字段,保存后立即生效
|
||||
|
||||
也可以直接编辑 `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com/v1",
|
||||
"ANTHROPIC_AUTH_TOKEN": "sk-xxx",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 支持所有 Anthropic API 兼容服务(如 OpenRouter、AWS Bedrock 代理等),只要接口兼容 Messages API 即可。
|
||||
|
||||
## Feature Flags
|
||||
|
||||
所有功能开关通过 `FEATURE_<FLAG_NAME>=1` 环境变量启用,例如:
|
||||
|
||||
```bash
|
||||
FEATURE_BUDDY=1 FEATURE_FORK_SUBAGENT=1 bun run dev
|
||||
```
|
||||
|
||||
各 Feature 的详细说明见 [`docs/features/`](docs/features/) 目录,欢迎投稿补充。
|
||||
|
||||
## VS Code 调试
|
||||
|
||||
TUI (REPL) 模式需要真实终端,无法直接通过 VS Code launch 启动调试。使用 **attach 模式**:
|
||||
|
||||
### 步骤
|
||||
|
||||
1. **终端启动 inspect 服务**:
|
||||
```bash
|
||||
bun run dev:inspect
|
||||
```
|
||||
会输出类似 `ws://localhost:8888/xxxxxxxx` 的地址。
|
||||
|
||||
2. **VS Code 附着调试器**:
|
||||
- 在 `src/` 文件中打断点
|
||||
- F5 → 选择 **"Attach to Bun (TUI debug)"**
|
||||
|
||||
|
||||
## 相关文档及网站
|
||||
|
||||
- **在线文档(Mintlify)**: [ccb.agent-aura.top](https://ccb.agent-aura.top/) — 文档源码位于 [`docs/`](docs/) 目录,欢迎投稿 PR
|
||||
- **DeepWiki**: <https://deepwiki.com/claude-code-best/claude-code>
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/claude-code-best/claude-code/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=claude-code-best/claude-code" />
|
||||
</a>
|
||||
<https://deepwiki.com/claude-code-best/claude-code>
|
||||
|
||||
## Star History
|
||||
|
||||
@@ -160,6 +73,364 @@ TUI (REPL) 模式需要真实终端,无法直接通过 VS Code launch 启动
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 能力清单
|
||||
|
||||
> ✅ = 已实现 ⚠️ = 部分实现 / 条件启用 ❌ = stub / 移除 / feature flag 关闭
|
||||
|
||||
### 核心系统
|
||||
|
||||
| 能力 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| REPL 交互界面(Ink 终端渲染) | ✅ | 主屏幕 5000+ 行,完整交互 |
|
||||
| API 通信 — Anthropic Direct | ✅ | 支持 API Key + OAuth |
|
||||
| API 通信 — AWS Bedrock | ✅ | 支持凭据刷新、Bearer Token |
|
||||
| API 通信 — Google Vertex | ✅ | 支持 GCP 凭据刷新 |
|
||||
| API 通信 — Azure Foundry | ✅ | 支持 API Key + Azure AD |
|
||||
| 流式对话与工具调用循环 (`query.ts`) | ✅ | 1700+ 行,含自动压缩、token 追踪 |
|
||||
| 会话引擎 (`QueryEngine.ts`) | ✅ | 1300+ 行,管理对话状态与归因 |
|
||||
| 上下文构建(git status / CLAUDE.md / memory) | ✅ | `context.ts` 完整实现 |
|
||||
| 权限系统(plan/auto/manual 模式) | ✅ | 6300+ 行,含 YOLO 分类器、路径验证、规则匹配 |
|
||||
| Hook 系统(pre/post tool use) | ✅ | 支持 settings.json 配置 |
|
||||
| 会话恢复 (`/resume`) | ✅ | 独立 ResumeConversation 屏幕 |
|
||||
| Doctor 诊断 (`/doctor`) | ✅ | 版本、API、插件、沙箱检查 |
|
||||
| 自动压缩 (compaction) | ✅ | auto-compact / micro-compact / API compact |
|
||||
|
||||
### 工具 — 始终可用
|
||||
|
||||
| 工具 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| BashTool | ✅ | Shell 执行,沙箱,权限检查 |
|
||||
| FileReadTool | ✅ | 文件 / PDF / 图片 / Notebook 读取 |
|
||||
| FileEditTool | ✅ | 字符串替换式编辑 + diff 追踪 |
|
||||
| FileWriteTool | ✅ | 文件创建 / 覆写 + diff 生成 |
|
||||
| NotebookEditTool | ✅ | Jupyter Notebook 单元格编辑 |
|
||||
| AgentTool | ✅ | 子代理派生(fork / async / background / remote) |
|
||||
| WebFetchTool | ✅ | URL 抓取 → Markdown → AI 摘要 |
|
||||
| WebSearchTool | ✅ | 网页搜索 + 域名过滤 |
|
||||
| AskUserQuestionTool | ✅ | 多问题交互提示 + 预览 |
|
||||
| SendMessageTool | ✅ | 消息发送(peers / teammates / mailbox) |
|
||||
| SkillTool | ✅ | 斜杠命令 / Skill 调用 |
|
||||
| EnterPlanModeTool | ✅ | 进入计划模式 |
|
||||
| ExitPlanModeTool (V2) | ✅ | 退出计划模式 |
|
||||
| TodoWriteTool | ✅ | Todo 列表 v1 |
|
||||
| BriefTool | ✅ | 简短消息 + 附件发送 |
|
||||
| TaskOutputTool | ✅ | 后台任务输出读取 |
|
||||
| TaskStopTool | ✅ | 后台任务停止 |
|
||||
| ListMcpResourcesTool | ⚠️ | MCP 资源列表(被 specialTools 过滤,特定条件下加入) |
|
||||
| ReadMcpResourceTool | ⚠️ | MCP 资源读取(同上) |
|
||||
| SyntheticOutputTool | ⚠️ | 仅在非交互会话(SDK/pipe 模式)下创建 |
|
||||
| CronCreateTool | ✅ | 定时任务创建(已移除 AGENT_TRIGGERS gate) |
|
||||
| CronDeleteTool | ✅ | 定时任务删除 |
|
||||
| CronListTool | ✅ | 定时任务列表 |
|
||||
| EnterWorktreeTool | ✅ | 进入 Git Worktree(`isWorktreeModeEnabled()` 已硬编码为 true) |
|
||||
| ExitWorktreeTool | ✅ | 退出 Git Worktree |
|
||||
|
||||
### 工具 — 条件启用
|
||||
|
||||
| 工具 | 状态 | 启用条件 |
|
||||
|------|------|----------|
|
||||
| GlobTool | ✅ | 未嵌入 bfs/ugrep 时启用(默认启用) |
|
||||
| GrepTool | ✅ | 同上 |
|
||||
| TaskCreateTool | ⚠️ | `isTodoV2Enabled()` 为 true 时 |
|
||||
| TaskGetTool | ⚠️ | 同上 |
|
||||
| TaskUpdateTool | ⚠️ | 同上 |
|
||||
| TaskListTool | ⚠️ | 同上 |
|
||||
| TeamCreateTool | ⚠️ | `isAgentSwarmsEnabled()` |
|
||||
| TeamDeleteTool | ⚠️ | 同上 |
|
||||
| ToolSearchTool | ⚠️ | `isToolSearchEnabledOptimistic()` |
|
||||
| PowerShellTool | ⚠️ | Windows 平台检测 |
|
||||
| LSPTool | ⚠️ | `ENABLE_LSP_TOOL` 环境变量 |
|
||||
| ConfigTool | ❌ | `USER_TYPE === 'ant'`(永远为 false) |
|
||||
|
||||
### 工具 — Feature Flag 关闭(全部不可用)
|
||||
|
||||
| 工具 | Feature Flag |
|
||||
|------|-------------|
|
||||
| SleepTool | `PROACTIVE` / `KAIROS` |
|
||||
| RemoteTriggerTool | `AGENT_TRIGGERS_REMOTE` |
|
||||
| MonitorTool | `MONITOR_TOOL` |
|
||||
| SendUserFileTool | `KAIROS` |
|
||||
| OverflowTestTool | `OVERFLOW_TEST_TOOL` |
|
||||
| TerminalCaptureTool | `TERMINAL_PANEL` |
|
||||
| WebBrowserTool | `WEB_BROWSER_TOOL` |
|
||||
| SnipTool | `HISTORY_SNIP` |
|
||||
| WorkflowTool | `WORKFLOW_SCRIPTS` |
|
||||
| PushNotificationTool | `KAIROS` / `KAIROS_PUSH_NOTIFICATION` |
|
||||
| SubscribePRTool | `KAIROS_GITHUB_WEBHOOKS` |
|
||||
| ListPeersTool | `UDS_INBOX` |
|
||||
| CtxInspectTool | `CONTEXT_COLLAPSE` |
|
||||
|
||||
### 工具 — Stub / 不可用
|
||||
|
||||
| 工具 | 说明 |
|
||||
|------|------|
|
||||
| TungstenTool | ANT-ONLY stub |
|
||||
| REPLTool | ANT-ONLY,`isEnabled: () => false` |
|
||||
| SuggestBackgroundPRTool | ANT-ONLY,`isEnabled: () => false` |
|
||||
| VerifyPlanExecutionTool | 需 `CLAUDE_CODE_VERIFY_PLAN=true` 环境变量,且为 stub |
|
||||
| ReviewArtifactTool | stub,未注册到 tools.ts |
|
||||
| DiscoverSkillsTool | stub,未注册到 tools.ts |
|
||||
|
||||
### 斜杠命令 — 可用
|
||||
|
||||
| 命令 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| `/add-dir` | ✅ | 添加目录 |
|
||||
| `/advisor` | ✅ | Advisor 配置 |
|
||||
| `/agents` | ✅ | 代理列表/管理 |
|
||||
| `/branch` | ✅ | 分支管理 |
|
||||
| `/btw` | ✅ | 快速备注 |
|
||||
| `/chrome` | ✅ | Chrome 集成 |
|
||||
| `/clear` | ✅ | 清屏 |
|
||||
| `/color` | ✅ | Agent 颜色 |
|
||||
| `/compact` | ✅ | 压缩对话 |
|
||||
| `/config` (`/settings`) | ✅ | 配置管理 |
|
||||
| `/context` | ✅ | 上下文信息 |
|
||||
| `/copy` | ✅ | 复制最后消息 |
|
||||
| `/cost` | ✅ | 会话费用 |
|
||||
| `/desktop` | ✅ | Claude Desktop 集成 |
|
||||
| `/diff` | ✅ | 显示 diff |
|
||||
| `/doctor` | ✅ | 健康检查 |
|
||||
| `/effort` | ✅ | 设置 effort 等级 |
|
||||
| `/exit` | ✅ | 退出 |
|
||||
| `/export` | ✅ | 导出对话 |
|
||||
| `/extra-usage` | ✅ | 额外用量信息 |
|
||||
| `/fast` | ✅ | 切换 fast 模式 |
|
||||
| `/feedback` | ✅ | 反馈 |
|
||||
| `/loop` | ✅ | 定时循环执行(bundled skill,可通过 `CLAUDE_CODE_DISABLE_CRON` 关闭) |
|
||||
| `/heapdump` | ✅ | Heap dump(调试) |
|
||||
| `/help` | ✅ | 帮助 |
|
||||
| `/hooks` | ✅ | Hook 管理 |
|
||||
| `/ide` | ✅ | IDE 连接 |
|
||||
| `/init` | ✅ | 初始化项目 |
|
||||
| `/install-github-app` | ✅ | 安装 GitHub App |
|
||||
| `/install-slack-app` | ✅ | 安装 Slack App |
|
||||
| `/keybindings` | ✅ | 快捷键管理 |
|
||||
| `/login` / `/logout` | ✅ | 登录 / 登出 |
|
||||
| `/mcp` | ✅ | MCP 服务管理 |
|
||||
| `/memory` | ✅ | Memory / CLAUDE.md 管理 |
|
||||
| `/mobile` | ✅ | 移动端 QR 码 |
|
||||
| `/model` | ✅ | 模型选择 |
|
||||
| `/output-style` | ✅ | 输出风格 |
|
||||
| `/passes` | ✅ | 推荐码 |
|
||||
| `/permissions` | ✅ | 权限管理 |
|
||||
| `/plan` | ✅ | 计划模式 |
|
||||
| `/plugin` | ✅ | 插件管理 |
|
||||
| `/pr-comments` | ✅ | PR 评论 |
|
||||
| `/privacy-settings` | ✅ | 隐私设置 |
|
||||
| `/rate-limit-options` | ✅ | 限速选项 |
|
||||
| `/release-notes` | ✅ | 更新日志 |
|
||||
| `/reload-plugins` | ✅ | 重载插件 |
|
||||
| `/remote-env` | ✅ | 远程环境配置 |
|
||||
| `/rename` | ✅ | 重命名会话 |
|
||||
| `/resume` | ✅ | 恢复会话 |
|
||||
| `/review` | ✅ | 代码审查(本地) |
|
||||
| `/ultrareview` | ✅ | 云端审查 |
|
||||
| `/rewind` | ✅ | 回退对话 |
|
||||
| `/sandbox-toggle` | ✅ | 切换沙箱 |
|
||||
| `/security-review` | ✅ | 安全审查 |
|
||||
| `/session` | ✅ | 会话信息 |
|
||||
| `/skills` | ✅ | Skill 管理 |
|
||||
| `/stats` | ✅ | 会话统计 |
|
||||
| `/status` | ✅ | 状态信息 |
|
||||
| `/statusline` | ✅ | 状态栏 UI |
|
||||
| `/stickers` | ✅ | 贴纸 |
|
||||
| `/tasks` | ✅ | 任务管理 |
|
||||
| `/theme` | ✅ | 终端主题 |
|
||||
| `/think-back` | ✅ | 年度回顾 |
|
||||
| `/upgrade` | ✅ | 升级 CLI |
|
||||
| `/usage` | ✅ | 用量信息 |
|
||||
| `/insights` | ✅ | 使用分析报告 |
|
||||
| `/vim` | ✅ | Vim 模式 |
|
||||
|
||||
### 斜杠命令 — Feature Flag 关闭
|
||||
|
||||
| 命令 | Feature Flag |
|
||||
|------|-------------|
|
||||
| `/voice` | `VOICE_MODE` |
|
||||
| `/proactive` | `PROACTIVE` / `KAIROS` |
|
||||
| `/brief` | `KAIROS` / `KAIROS_BRIEF` |
|
||||
| `/assistant` | `KAIROS` |
|
||||
| `/remote-control` (alias `rc`) | `BRIDGE_MODE` |
|
||||
| `/remote-control-server` | `DAEMON` + `BRIDGE_MODE` |
|
||||
| `/force-snip` | `HISTORY_SNIP` |
|
||||
| `/workflows` | `WORKFLOW_SCRIPTS` |
|
||||
| `/web-setup` | `CCR_REMOTE_SETUP` |
|
||||
| `/subscribe-pr` | `KAIROS_GITHUB_WEBHOOKS` |
|
||||
| `/ultraplan` | `ULTRAPLAN` |
|
||||
| `/torch` | `TORCH` |
|
||||
| `/peers` | `UDS_INBOX` |
|
||||
| `/fork` | `FORK_SUBAGENT` |
|
||||
| `/buddy` | `BUDDY` |
|
||||
|
||||
### 斜杠命令 — ANT-ONLY(不可用)
|
||||
|
||||
`/files` `/tag` `/backfill-sessions` `/break-cache` `/bughunter` `/commit` `/commit-push-pr` `/ctx_viz` `/good-claude` `/issue` `/init-verifiers` `/mock-limits` `/bridge-kick` `/version` `/reset-limits` `/onboarding` `/share` `/summary` `/teleport` `/ant-trace` `/perf-issue` `/env` `/oauth-refresh` `/debug-tool-call` `/agents-platform` `/autofix-pr`
|
||||
|
||||
### CLI 子命令
|
||||
|
||||
| 子命令 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| `claude`(默认) | ✅ | 主 REPL / 交互 / print 模式 |
|
||||
| `claude mcp serve/add/remove/list/get/...` | ✅ | MCP 服务管理(7 个子命令) |
|
||||
| `claude auth login/status/logout` | ✅ | 认证管理 |
|
||||
| `claude plugin validate/list/install/...` | ✅ | 插件管理(7 个子命令) |
|
||||
| `claude setup-token` | ✅ | 长效 Token 配置 |
|
||||
| `claude agents` | ✅ | 代理列表 |
|
||||
| `claude doctor` | ✅ | 健康检查 |
|
||||
| `claude update` / `upgrade` | ✅ | 自动更新 |
|
||||
| `claude install [target]` | ✅ | Native 安装 |
|
||||
| `claude server` | ❌ | `DIRECT_CONNECT` flag |
|
||||
| `claude ssh <host>` | ❌ | `SSH_REMOTE` flag |
|
||||
| `claude open <cc-url>` | ❌ | `DIRECT_CONNECT` flag |
|
||||
| `claude auto-mode` | ❌ | `TRANSCRIPT_CLASSIFIER` flag |
|
||||
| `claude remote-control` | ❌ | `BRIDGE_MODE` + `DAEMON` flag |
|
||||
| `claude assistant` | ❌ | `KAIROS` flag |
|
||||
| `claude up/rollback/log/error/export/task/completion` | ❌ | ANT-ONLY |
|
||||
|
||||
### 服务层
|
||||
|
||||
| 服务 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| API 客户端 (`services/api/`) | ✅ | 3400+ 行,4 个 provider |
|
||||
| MCP (`services/mcp/`) | ✅ | 34 个文件,12000+ 行 |
|
||||
| OAuth (`services/oauth/`) | ✅ | 完整 OAuth 流程 |
|
||||
| 插件 (`services/plugins/`) | ✅ | 基础设施完整,无内置插件 |
|
||||
| LSP (`services/lsp/`) | ⚠️ | 实现存在,默认关闭 |
|
||||
| 压缩 (`services/compact/`) | ✅ | auto / micro / API 压缩 |
|
||||
| Hook 系统 (`services/tools/toolHooks.ts`) | ✅ | pre/post tool use hooks |
|
||||
| 会话记忆 (`services/SessionMemory/`) | ✅ | 会话记忆管理 |
|
||||
| 记忆提取 (`services/extractMemories/`) | ✅ | 自动记忆提取 |
|
||||
| Skill 搜索 (`services/skillSearch/`) | ✅ | 本地/远程 skill 搜索 |
|
||||
| 策略限制 (`services/policyLimits/`) | ✅ | 策略限制执行 |
|
||||
| 分析 / GrowthBook / Sentry | ⚠️ | 框架存在,实际 sink 为空 |
|
||||
| Voice (`services/voice.ts`) | ❌ | `VOICE_MODE` flag 关闭 |
|
||||
|
||||
### 内部包 (`packages/`)
|
||||
|
||||
| 包 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| `color-diff-napi` | ✅ | 1006 行完整 TypeScript 实现(语法高亮 diff) |
|
||||
| `audio-capture-napi` | ✅ | 151 行完整实现(跨平台音频录制,使用 SoX/arecord) |
|
||||
| `image-processor-napi` | ✅ | 125 行完整实现(macOS 剪贴板图片读取,使用 osascript + sharp) |
|
||||
| `modifiers-napi` | ✅ | 67 行完整实现(macOS 修饰键检测,bun:ffi + CoreGraphics) |
|
||||
| `url-handler-napi` | ❌ | stub,`waitForUrlEvent()` 返回 null |
|
||||
| `@ant/claude-for-chrome-mcp` | ❌ | stub,`createServer()` 返回 null |
|
||||
| `@ant/computer-use-mcp` | ⚠️ | 类型安全 stub(265 行,完整类型定义但函数返回空值) |
|
||||
| `@ant/computer-use-input` | ✅ | 183 行完整实现(macOS 键鼠模拟,AppleScript/JXA/CGEvent) |
|
||||
| `@ant/computer-use-swift` | ✅ | 388 行完整实现(macOS 显示器/应用管理/截图,JXA/screencapture) |
|
||||
|
||||
### Feature Flags(31 个,全部返回 `false`)
|
||||
|
||||
`ABLATION_BASELINE` `AGENT_MEMORY_SNAPSHOT` `BG_SESSIONS` `BRIDGE_MODE` `BUDDY` `CCR_MIRROR` `CCR_REMOTE_SETUP` `CHICAGO_MCP` `COORDINATOR_MODE` `DAEMON` `DIRECT_CONNECT` `EXPERIMENTAL_SKILL_SEARCH` `FORK_SUBAGENT` `HARD_FAIL` `HISTORY_SNIP` `KAIROS` `KAIROS_BRIEF` `KAIROS_CHANNELS` `KAIROS_GITHUB_WEBHOOKS` `LODESTONE` `MCP_SKILLS` `PROACTIVE` `SSH_REMOTE` `TORCH` `TRANSCRIPT_CLASSIFIER` `UDS_INBOX` `ULTRAPLAN` `UPLOAD_USER_SETTINGS` `VOICE_MODE` `WEB_BROWSER_TOOL` `WORKFLOW_SCRIPTS`
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
claude-code/
|
||||
├── src/
|
||||
│ ├── entrypoints/
|
||||
│ │ ├── cli.tsx # 入口文件(含 MACRO/feature polyfill)
|
||||
│ │ └── sdk/ # SDK 子模块 stub
|
||||
│ ├── main.tsx # 主 CLI 逻辑(Commander 定义)
|
||||
│ └── types/
|
||||
│ ├── global.d.ts # 全局变量/宏声明
|
||||
│ └── internal-modules.d.ts # 内部 npm 包类型声明
|
||||
├── packages/ # Monorepo workspace 包
|
||||
│ ├── color-diff-napi/ # 完整实现(终端 color diff)
|
||||
│ ├── modifiers-napi/ # stub(macOS 修饰键检测)
|
||||
│ ├── audio-capture-napi/ # stub
|
||||
│ ├── image-processor-napi/# stub
|
||||
│ ├── url-handler-napi/ # stub
|
||||
│ └── @ant/ # Anthropic 内部包 stub
|
||||
│ ├── claude-for-chrome-mcp/
|
||||
│ ├── computer-use-mcp/
|
||||
│ ├── computer-use-input/
|
||||
│ └── computer-use-swift/
|
||||
├── scripts/ # 自动化 stub 生成脚本
|
||||
├── build.ts # 构建脚本(Bun.build + code splitting + Node.js 兼容后处理)
|
||||
├── dist/ # 构建输出(入口 cli.js + ~450 chunk 文件)
|
||||
└── package.json # Bun workspaces monorepo 配置
|
||||
```
|
||||
|
||||
## 技术说明
|
||||
|
||||
### 运行时 Polyfill
|
||||
|
||||
入口文件 `src/entrypoints/cli.tsx` 顶部注入了必要的 polyfill:
|
||||
|
||||
- `feature()` — 所有 feature flag 返回 `false`,跳过未实现分支
|
||||
- `globalThis.MACRO` — 模拟构建时宏注入(VERSION 等)
|
||||
|
||||
### Monorepo
|
||||
|
||||
项目采用 Bun workspaces 管理内部包。原先手工放在 `node_modules/` 下的 stub 已统一迁入 `packages/`,通过 `workspace:*` 解析。
|
||||
|
||||
## Feature Flags 详解
|
||||
|
||||
原版 Claude Code 通过 `bun:bundle` 的 `feature()` 在构建时注入 feature flag,由 GrowthBook 等 A/B 实验平台控制灰度发布。本项目中 `feature()` 被 polyfill 为始终返回 `false`,因此以下 30 个 flag 全部关闭。
|
||||
|
||||
### 自主 Agent
|
||||
|
||||
| Flag | 用途 |
|
||||
|------|------|
|
||||
| `KAIROS` | Assistant 模式 — 长期运行的自主 Agent(含 brief、push 通知、文件发送) |
|
||||
| `KAIROS_BRIEF` | Kairos Brief — 向用户发送简报摘要 |
|
||||
| `KAIROS_CHANNELS` | Kairos 频道 — 多频道通信 |
|
||||
| `KAIROS_GITHUB_WEBHOOKS` | GitHub Webhook 订阅 — PR 事件实时推送给 Agent |
|
||||
| `PROACTIVE` | 主动模式 — Agent 主动执行任务,含 SleepTool 定时唤醒 |
|
||||
| `COORDINATOR_MODE` | 协调器模式 — 多 Agent 编排调度 |
|
||||
| `BUDDY` | Buddy 配对编程功能 |
|
||||
| `FORK_SUBAGENT` | Fork 子代理 — 从当前会话分叉出独立子代理 |
|
||||
|
||||
### 远程 / 分布式
|
||||
|
||||
| Flag | 用途 |
|
||||
|------|------|
|
||||
| `BRIDGE_MODE` | 远程控制桥接 — 允许外部客户端远程操控 Claude Code |
|
||||
| `DAEMON` | 守护进程 — 后台常驻服务,支持 worker 和 supervisor |
|
||||
| `BG_SESSIONS` | 后台会话 — `ps`/`logs`/`attach`/`kill`/`--bg` 等后台进程管理 |
|
||||
| `SSH_REMOTE` | SSH 远程 — `claude ssh <host>` 连接远程主机 |
|
||||
| `DIRECT_CONNECT` | 直连模式 — `cc://` URL 协议、server 命令、`open` 命令 |
|
||||
| `CCR_REMOTE_SETUP` | 网页端远程配置 — 通过浏览器配置 Claude Code |
|
||||
| `CCR_MIRROR` | Claude Code Runtime 镜像 — 会话状态同步/复制 |
|
||||
|
||||
### 通信
|
||||
|
||||
| Flag | 用途 |
|
||||
|------|------|
|
||||
| `UDS_INBOX` | Unix Domain Socket 收件箱 — Agent 间本地通信(`/peers`) |
|
||||
|
||||
### 增强工具
|
||||
|
||||
| Flag | 用途 |
|
||||
|------|------|
|
||||
| `CHICAGO_MCP` | Computer Use MCP — 计算机操作(屏幕截图、鼠标键盘控制) |
|
||||
| `WEB_BROWSER_TOOL` | 网页浏览器工具 — 在终端内嵌浏览器交互 |
|
||||
| `VOICE_MODE` | 语音模式 — 语音输入输出,麦克风 push-to-talk |
|
||||
| `WORKFLOW_SCRIPTS` | 工作流脚本 — 用户自定义自动化工作流 |
|
||||
| `MCP_SKILLS` | 基于 MCP 的 Skill 加载机制 |
|
||||
|
||||
### 对话管理
|
||||
|
||||
| Flag | 用途 |
|
||||
|------|------|
|
||||
| `HISTORY_SNIP` | 历史裁剪 — 手动裁剪对话历史中的片段(`/force-snip`) |
|
||||
| `ULTRAPLAN` | 超级计划 — 远程 Agent 协作的大规模规划功能 |
|
||||
| `AGENT_MEMORY_SNAPSHOT` | Agent 运行时的记忆快照功能 |
|
||||
|
||||
### 基础设施 / 实验
|
||||
|
||||
| Flag | 用途 |
|
||||
|------|------|
|
||||
| `ABLATION_BASELINE` | 科学实验 — 基线消融测试,用于 A/B 实验对照组 |
|
||||
| `HARD_FAIL` | 硬失败模式 — 遇错直接中断而非降级 |
|
||||
| `TRANSCRIPT_CLASSIFIER` | 对话分类器 — `auto-mode` 命令,自动分析和分类对话记录 |
|
||||
| `UPLOAD_USER_SETTINGS` | 设置同步上传 — 将本地配置同步到云端 |
|
||||
| `LODESTONE` | 深度链接协议处理器 — 从外部应用跳转到 Claude Code 指定位置 |
|
||||
| `EXPERIMENTAL_SKILL_SEARCH` | 实验性 Skill 搜索索引 |
|
||||
| `TORCH` | Torch 功能(具体用途未知,可能是某种高亮/追踪机制) |
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目仅供学习研究用途。Claude Code 的所有权利归 [Anthropic](https://www.anthropic.com/) 所有。
|
||||
|
||||
158
README_EN.md
158
README_EN.md
@@ -1,158 +0,0 @@
|
||||
# Claude Code Best V5 (CCB)
|
||||
|
||||
[](https://github.com/claude-code-best/claude-code/stargazers)
|
||||
[](https://github.com/claude-code-best/claude-code/graphs/contributors)
|
||||
[](https://github.com/claude-code-best/claude-code/issues)
|
||||
[](https://github.com/claude-code-best/claude-code/blob/main/LICENSE)
|
||||
[](https://github.com/claude-code-best/claude-code/commits/main)
|
||||
[](https://bun.sh/)
|
||||
|
||||
> Which Claude do you like? The open source one is the best.
|
||||
|
||||
A reverse-engineered / decompiled source restoration of Anthropic's official [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI tool. The goal is to reproduce most of Claude Code's functionality and engineering capabilities. It's abbreviated as CCB.
|
||||
|
||||
[Documentation (Chinese)](https://ccb.agent-aura.top/) — PR contributions welcome.
|
||||
|
||||
Sponsor placeholder.
|
||||
|
||||
- [x] v1: Basic runability and type checking pass
|
||||
- [x] V2: Complete engineering infrastructure
|
||||
- [ ] Biome formatting may not be implemented first to avoid code conflicts
|
||||
- [x] Build pipeline complete, output runnable on both Node.js and Bun
|
||||
- [x] V3: Extensive documentation and documentation site improvements
|
||||
- [x] V4: Large-scale test suite for improved stability
|
||||
- [x] Buddy pet feature restored [Docs](https://ccb.agent-aura.top/docs/features/buddy)
|
||||
- [x] Auto Mode restored [Docs](https://ccb.agent-aura.top/docs/safety/auto-mode)
|
||||
- [x] All features now configurable via environment variables instead of `bun --feature`
|
||||
- [x] V5: Enterprise-grade monitoring/reporting, missing tools补全, restrictions removed
|
||||
- [x] Removed anti-distillation code
|
||||
- [x] Web search capability (using Bing) [Docs](https://ccb.agent-aura.top/docs/features/web-browser-tool)
|
||||
- [x] Debug mode support [Docs](https://ccb.agent-aura.top/docs/features/debug-mode)
|
||||
- [x] Disabled auto-updates
|
||||
- [x] Custom Sentry error reporting support [Docs](https://ccb.agent-aura.top/docs/internals/sentry-setup)
|
||||
- [x] Custom GrowthBook support (GB is open source — configure your own feature flag platform) [Docs](https://ccb.agent-aura.top/docs/internals/growthbook-adapter)
|
||||
- [x] Custom login mode — configure Claude models your way
|
||||
- [ ] V6: Large-scale refactoring, full modular packaging
|
||||
- [ ] V6 will be a new branch; main branch will be archived as a historical version
|
||||
|
||||
> I don't know how long this project will survive. Star + Fork + git clone + .zip is the safest bet.
|
||||
>
|
||||
> This project updates rapidly — Opus continuously optimizes in the background, with new changes almost every few hours.
|
||||
>
|
||||
> Claude has burned over $1000, out of budget, switching to GLM to continue; @zai-org GLM 5.1 is quite capable.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Make sure you're on the latest version of Bun, otherwise you'll run into all sorts of weird bugs. Run `bun upgrade`!
|
||||
|
||||
- [Bun](https://bun.sh/) >= 1.3.11
|
||||
- Standard Claude Code configuration — each provider has its own setup method
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
# Dev mode — if you see version 888, it's working
|
||||
bun run dev
|
||||
|
||||
# Build
|
||||
bun run build
|
||||
```
|
||||
|
||||
The build uses code splitting (`build.ts`), outputting to `dist/` (entry `dist/cli.js` + ~450 chunk files).
|
||||
|
||||
The build output runs on both Bun and Node.js — you can publish to a private registry and run directly.
|
||||
|
||||
If you encounter a bug, please open an issue — we'll prioritize it.
|
||||
|
||||
### First-time Setup /login
|
||||
|
||||
After the first run, enter `/login` in the REPL to access the login configuration screen. Select **Custom Platform** to connect to third-party API-compatible services (no Anthropic account required).
|
||||
|
||||
Fields to fill in:
|
||||
|
||||
| Field | Description | Example |
|
||||
|-------|-------------|---------|
|
||||
| Base URL | API service URL | `https://api.example.com/v1` |
|
||||
| API Key | Authentication key | `sk-xxx` |
|
||||
| Haiku Model | Fast model ID | `claude-haiku-4-5-20251001` |
|
||||
| Sonnet Model | Balanced model ID | `claude-sonnet-4-6` |
|
||||
| Opus Model | High-performance model ID | `claude-opus-4-6` |
|
||||
|
||||
- **Tab / Shift+Tab** to switch fields, **Enter** to confirm and move to the next, press Enter on the last field to save
|
||||
- Model fields auto-fill from current environment variables
|
||||
- Configuration saves to `~/.claude/settings.json` under the `env` key, effective immediately
|
||||
|
||||
You can also edit `~/.claude/settings.json` directly:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com/v1",
|
||||
"ANTHROPIC_AUTH_TOKEN": "sk-xxx",
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001",
|
||||
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6",
|
||||
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Supports all Anthropic API-compatible services (e.g., OpenRouter, AWS Bedrock proxies, etc.) as long as the interface is compatible with the Messages API.
|
||||
|
||||
## Feature Flags
|
||||
|
||||
All feature toggles are enabled via `FEATURE_<FLAG_NAME>=1` environment variables, for example:
|
||||
|
||||
```bash
|
||||
FEATURE_BUDDY=1 FEATURE_FORK_SUBAGENT=1 bun run dev
|
||||
```
|
||||
|
||||
See [`docs/features/`](docs/features/) for detailed descriptions of each feature. Contributions welcome.
|
||||
|
||||
## VS Code Debugging
|
||||
|
||||
The TUI (REPL) mode requires a real terminal and cannot be launched directly via VS Code's launch config. Use **attach mode**:
|
||||
|
||||
### Steps
|
||||
|
||||
1. **Start inspect server in terminal**:
|
||||
```bash
|
||||
bun run dev:inspect
|
||||
```
|
||||
This outputs an address like `ws://localhost:8888/xxxxxxxx`.
|
||||
|
||||
2. **Attach debugger from VS Code**:
|
||||
- Set breakpoints in `src/` files
|
||||
- Press F5 → select **"Attach to Bun (TUI debug)"**
|
||||
|
||||
## Documentation & Links
|
||||
|
||||
- **Online docs (Mintlify)**: [ccb.agent-aura.top](https://ccb.agent-aura.top/) — source in [`docs/`](docs/), PR contributions welcome
|
||||
- **DeepWiki**: <https://deepwiki.com/claude-code-best/claude-code>
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/claude-code-best/claude-code/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=claude-code-best/claude-code" />
|
||||
</a>
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/?repos=claude-code-best%2Fclaude-code&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=claude-code-best%2Fclaude-code&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=claude-code-best%2Fclaude-code&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=claude-code-best%2Fclaude-code&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
This project is for educational and research purposes only. All rights to Claude Code belong to [Anthropic](https://www.anthropic.com/).
|
||||
29
build.ts
29
build.ts
@@ -1,6 +1,5 @@
|
||||
import { readdir, readFile, writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { getMacroDefines } from "./scripts/defines.ts";
|
||||
|
||||
const outdir = "dist";
|
||||
|
||||
@@ -8,24 +7,12 @@ const outdir = "dist";
|
||||
const { rmSync } = await import("fs");
|
||||
rmSync(outdir, { recursive: true, force: true });
|
||||
|
||||
// Default features that match the official CLI build.
|
||||
// Additional features can be enabled via FEATURE_<NAME>=1 env vars.
|
||||
const DEFAULT_BUILD_FEATURES = ["AGENT_TRIGGERS_REMOTE"];
|
||||
|
||||
// Collect FEATURE_* env vars → Bun.build features
|
||||
const envFeatures = Object.keys(process.env)
|
||||
.filter(k => k.startsWith("FEATURE_"))
|
||||
.map(k => k.replace("FEATURE_", ""));
|
||||
const features = [...new Set([...DEFAULT_BUILD_FEATURES, ...envFeatures])];
|
||||
|
||||
// Step 2: Bundle with splitting
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["src/entrypoints/cli.tsx"],
|
||||
outdir,
|
||||
target: "bun",
|
||||
splitting: true,
|
||||
define: getMacroDefines(),
|
||||
features,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
@@ -58,19 +45,3 @@ for (const file of files) {
|
||||
console.log(
|
||||
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for Node.js compat)`,
|
||||
);
|
||||
|
||||
// Step 4: 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}/`);
|
||||
}
|
||||
|
||||
153
bun.lock
153
bun.lock
@@ -45,7 +45,6 @@
|
||||
"@opentelemetry/sdk-metrics": "^2.6.1",
|
||||
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||
"@sentry/node": "^10.47.0",
|
||||
"@smithy/core": "^3.23.13",
|
||||
"@smithy/node-http-handler": "^4.5.1",
|
||||
"@types/bun": "^1.3.11",
|
||||
@@ -77,7 +76,6 @@
|
||||
"fuse.js": "^7.1.0",
|
||||
"get-east-asian-width": "^1.5.0",
|
||||
"google-auth-library": "^10.6.2",
|
||||
"he": "^1.2.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"https-proxy-agent": "^8.0.0",
|
||||
"ignore": "^7.0.5",
|
||||
@@ -89,7 +87,6 @@
|
||||
"lru-cache": "^11.2.7",
|
||||
"marked": "^17.0.5",
|
||||
"modifiers-napi": "workspace:*",
|
||||
"openai": "^4.73.0",
|
||||
"p-map": "^7.0.4",
|
||||
"picomatch": "^4.0.4",
|
||||
"plist": "^3.1.0",
|
||||
@@ -318,8 +315,6 @@
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||
|
||||
"@fastify/otel": ["@fastify/otel@0.18.0", "https://registry.npmmirror.com/@fastify/otel/-/otel-0.18.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.212.0", "@opentelemetry/semantic-conventions": "^1.28.0", "minimatch": "^10.2.4" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA=="],
|
||||
|
||||
"@growthbook/growthbook": ["@growthbook/growthbook@1.6.5", "https://registry.npmmirror.com/@growthbook/growthbook/-/growthbook-1.6.5.tgz", { "dependencies": { "dom-mutator": "^0.6.0" } }, "sha512-mUaMsgeUTpRIUOTn33EUXHRK6j7pxBjwqH4WpQyq+pukjd1AIzWlEa6w7i6bInJUcweGgP2beXZmaP6b6UPn7A=="],
|
||||
|
||||
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "https://registry.npmmirror.com/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="],
|
||||
@@ -426,8 +421,6 @@
|
||||
|
||||
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "https://registry.npmmirror.com/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="],
|
||||
|
||||
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "https://registry.npmmirror.com/@opentelemetry/context-async-hooks/-/context-async-hooks-2.6.1.tgz", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="],
|
||||
|
||||
"@opentelemetry/core": ["@opentelemetry/core@2.6.1", "https://registry.npmmirror.com/@opentelemetry/core/-/core-2.6.1.tgz", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.214.0", "https://registry.npmmirror.com/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.214.0.tgz", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-grpc-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/sdk-logs": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-SwmFRwO8mi6nndzbsjPgSFg7qy1WeNHRFD+s6uCsdiUDUt3+yzI2qiHE3/ub2f37+/CbeGcG+Ugc8Gwr6nu2Aw=="],
|
||||
@@ -450,60 +443,12 @@
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.214.0", "https://registry.npmmirror.com/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.214.0.tgz", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ON0spYWb2yAdQ9b+ItNyK0c6qdtcs+0eVR4YFJkhJL7agfT8sHFg0e5EesauSRiTHPZHiDobI92k77q0lwAmqg=="],
|
||||
|
||||
"@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.214.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w=="],
|
||||
|
||||
"@opentelemetry/instrumentation-amqplib": ["@opentelemetry/instrumentation-amqplib@0.61.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.61.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-mCKoyTGfRNisge4br0NpOFSy2Z1NnEW8hbCJdUDdJFHrPqVzc4IIBPA/vX0U+LUcQqrQvJX+HMIU0dbDRe0i0Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-connect": ["@opentelemetry/instrumentation-connect@0.57.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.57.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.38" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FMEBChnI4FLN5TE9DHwfH7QpNir1JzXno1uz/TAucVdLCyrG0jTrKIcNHt/i30A0M2AunNBCkcd8Ei26dIPKdg=="],
|
||||
|
||||
"@opentelemetry/instrumentation-dataloader": ["@opentelemetry/instrumentation-dataloader@0.31.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.31.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-f654tZFQXS5YeLDNb9KySrwtg7SnqZN119FauD7acBoTzuLduaiGTNz88ixcVSOOMGZ+EjJu/RFtx5klObC95g=="],
|
||||
|
||||
"@opentelemetry/instrumentation-express": ["@opentelemetry/instrumentation-express@0.62.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.62.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Tvx+vgAZKEQxU3Rx+xWLiR0mLxHwmk69/8ya04+VsV9WYh8w6Lhx5hm5yAMvo1wy0KqWgFKBLwSeo3sHCwdOww=="],
|
||||
|
||||
"@opentelemetry/instrumentation-fs": ["@opentelemetry/instrumentation-fs@0.33.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.33.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-sCZWXGalQ01wr3tAhSR9ucqFJ0phidpAle6/17HVjD6gN8FLmZMK/8sKxdXYHy3PbnlV1P4zeiSVFNKpbFMNLA=="],
|
||||
|
||||
"@opentelemetry/instrumentation-generic-pool": ["@opentelemetry/instrumentation-generic-pool@0.57.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.57.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-orhmlaK+ZIW9hKU+nHTbXrCSXZcH83AescTqmpamHRobRmYSQwRbD0a1odc0yAzuzOtxYiHiXAnpnIpaSSY7Ow=="],
|
||||
|
||||
"@opentelemetry/instrumentation-graphql": ["@opentelemetry/instrumentation-graphql@0.62.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.62.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3YNuLVPUxafXkH1jBAbGsKNsP3XVzcFDhCDCE3OqBwCwShlqQbLMRMFh1T/d5jaVZiGVmSsfof+ICKD2iOV8xg=="],
|
||||
|
||||
"@opentelemetry/instrumentation-hapi": ["@opentelemetry/instrumentation-hapi@0.60.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.60.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aNljZKYrEa7obLAxd1bCEDxF7kzCLGXTuTJZ8lMR9rIVEjmuKBXN1gfqpm/OB//Zc2zP4iIve1jBp7sr3mQV6w=="],
|
||||
|
||||
"@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.214.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.214.0.tgz", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/instrumentation": "0.214.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FlkDhZDRjDJDcO2LcSCtjRpkal1NJ8y0fBqBhTvfAR3JSYY2jAIj1kSS5IjmEBt4c3aWv+u/lqLuoCDrrKCSKg=="],
|
||||
|
||||
"@opentelemetry/instrumentation-ioredis": ["@opentelemetry/instrumentation-ioredis@0.62.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.62.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ZYt//zcPve8qklaZX+5Z4MkU7UpEkFRrxsf2cnaKYBitqDnsCN69CPAuuMOX6NYdW2rG9sFy7V/QWtBlP5XiNQ=="],
|
||||
|
||||
"@opentelemetry/instrumentation-kafkajs": ["@opentelemetry/instrumentation-kafkajs@0.23.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.23.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4K+nVo+zI+aDz0Z85SObwbdixIbzS9moIuKJaYsdlzcHYnKOPtB7ya8r8Ezivy/GVIBHiKJVq4tv+BEkgOMLaQ=="],
|
||||
|
||||
"@opentelemetry/instrumentation-knex": ["@opentelemetry/instrumentation-knex@0.58.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.58.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Hc/o8fSsaWxZ8r1Yw4rNDLwTpUopTf4X32y4W6UhlHmW8Wizz8wfhgOKIelSeqFVTKBBPIDUOsQWuIMxBmu8Bw=="],
|
||||
|
||||
"@opentelemetry/instrumentation-koa": ["@opentelemetry/instrumentation-koa@0.62.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.62.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.36.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-uVip0VuGUQXZ+vFxkKxAUNq8qNl+VFlyHDh/U6IQ8COOEDfbEchdaHnpFrMYF3psZRUuoSIgb7xOeXj00RdwDA=="],
|
||||
|
||||
"@opentelemetry/instrumentation-lru-memoizer": ["@opentelemetry/instrumentation-lru-memoizer@0.58.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.58.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6grM3TdMyHzlGY1cUA+mwoPueB1F3dYKgKtZIH6jOFXqfHAByyLTc+6PFjGM9tKh52CFBJaDwodNlL/Td39z7Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-mongodb": ["@opentelemetry/instrumentation-mongodb@0.67.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.67.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-1WJp5N1lYfHq2IhECOTewFs5Tf2NfUOwQRqs/rZdXKTezArMlucxgzAaqcgp3A3YREXopXTpXHsxZTGHjNhMdQ=="],
|
||||
|
||||
"@opentelemetry/instrumentation-mongoose": ["@opentelemetry/instrumentation-mongoose@0.60.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.60.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-8BahAZpKsOoc+lrZGb7Ofn4g3z8qtp5IxDfvAVpKXsEheQN7ONMH5djT5ihy6yf8yyeQJGS0gXFfpEAEeEHqQg=="],
|
||||
|
||||
"@opentelemetry/instrumentation-mysql": ["@opentelemetry/instrumentation-mysql@0.60.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.60.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/mysql": "2.15.27" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-08pO8GFPEIz2zquKDGteBZDNmwketdgH8hTe9rVYgW9kCJXq1Psj3wPQGx+VaX4ZJKCfPeoLMYup9+cxHvZyVQ=="],
|
||||
|
||||
"@opentelemetry/instrumentation-mysql2": ["@opentelemetry/instrumentation-mysql2@0.60.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.60.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@opentelemetry/sql-common": "^0.41.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-m/5d3bxQALllCzezYDk/6vajh0tj5OijMMvOZGr+qN1NMXm1dzMNwyJ0gNZW7Fo3YFRyj/jJMxIw+W7d525dlw=="],
|
||||
|
||||
"@opentelemetry/instrumentation-pg": ["@opentelemetry/instrumentation-pg@0.66.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.66.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@opentelemetry/sql-common": "^0.41.2", "@types/pg": "8.15.6", "@types/pg-pool": "2.0.7" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-KxfLGXBb7k2ueaPJfq2GXBDXBly8P+SpR/4Mj410hhNgmQF3sCqwXvUBQxZQkDAmsdBAoenM+yV1LhtsMRamcA=="],
|
||||
|
||||
"@opentelemetry/instrumentation-redis": ["@opentelemetry/instrumentation-redis@0.62.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.62.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/redis-common": "^0.38.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y3pPpot7WzR/8JtHcYlTYsyY8g+pbFhAqbwAuG5bLPnR6v6pt1rQc0DpH0OlGP/9CZbWBP+Zhwp9yFoygf/ZXQ=="],
|
||||
|
||||
"@opentelemetry/instrumentation-tedious": ["@opentelemetry/instrumentation-tedious@0.33.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.33.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.33.0", "@types/tedious": "^4.0.14" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Q6WQwAD01MMTub31GlejoiFACYNw26J426wyjvU7by7fDIr2nZXNW4vhTGs7i7F0TnXBO3xN688g1tdUgYwJ5w=="],
|
||||
|
||||
"@opentelemetry/instrumentation-undici": ["@opentelemetry/instrumentation-undici@0.24.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.24.0.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/semantic-conventions": "^1.24.0" }, "peerDependencies": { "@opentelemetry/api": "^1.7.0" } }, "sha512-oKzZ3uvqP17sV0EsoQcJgjEfIp0kiZRbYu/eD8p13Cbahumf8lb/xpYeNr/hfAJ4owzEtIDcGIjprfLcYbIKBQ=="],
|
||||
|
||||
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "https://registry.npmmirror.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.214.0.tgz", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="],
|
||||
|
||||
"@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.214.0", "https://registry.npmmirror.com/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.214.0.tgz", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IDP6zcyA24RhNZ289MP6eToIZcinlmirHjX8v3zKCQ2ZhPpt5cGwkN91tCth337lqHIgWcTy90uKRiX/SzALDw=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "https://registry.npmmirror.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.214.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],
|
||||
|
||||
"@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.38.2", "https://registry.npmmirror.com/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz", {}, "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA=="],
|
||||
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "https://registry.npmmirror.com/@opentelemetry/resources/-/resources-2.6.1.tgz", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "https://registry.npmmirror.com/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="],
|
||||
@@ -514,8 +459,6 @@
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "https://registry.npmmirror.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
|
||||
|
||||
"@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "https://registry.npmmirror.com/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="],
|
||||
|
||||
"@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.121.0", "https://registry.npmmirror.com/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.121.0.tgz", { "os": "android", "cpu": "arm" }, "sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A=="],
|
||||
|
||||
"@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.121.0", "https://registry.npmmirror.com/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.121.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-/Dd1xIXboYAicw+twT2utxPD7bL8qh7d3ej0qvaYIMj3/EgIrGR+tSnjCUkiCT6g6uTC0neSS4JY8LxhdSU/sA=="],
|
||||
@@ -600,8 +543,6 @@
|
||||
|
||||
"@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "https://registry.npmmirror.com/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="],
|
||||
|
||||
"@prisma/instrumentation": ["@prisma/instrumentation@7.6.0", "https://registry.npmmirror.com/@prisma/instrumentation/-/instrumentation-7.6.0.tgz", { "dependencies": { "@opentelemetry/instrumentation": "^0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
@@ -624,14 +565,6 @@
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@sentry/core": ["@sentry/core@10.47.0", "https://registry.npmmirror.com/@sentry/core/-/core-10.47.0.tgz", {}, "sha512-nsYRAx3EWezDut+Zl+UwwP07thh9uY7CfSAi2whTdcJl5hu1nSp2z8bba7Vq/MGbNLnazkd3A+GITBEML924JA=="],
|
||||
|
||||
"@sentry/node": ["@sentry/node@10.47.0", "https://registry.npmmirror.com/@sentry/node/-/node-10.47.0.tgz", { "dependencies": { "@fastify/otel": "0.18.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/context-async-hooks": "^2.6.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-amqplib": "0.61.0", "@opentelemetry/instrumentation-connect": "0.57.0", "@opentelemetry/instrumentation-dataloader": "0.31.0", "@opentelemetry/instrumentation-express": "0.62.0", "@opentelemetry/instrumentation-fs": "0.33.0", "@opentelemetry/instrumentation-generic-pool": "0.57.0", "@opentelemetry/instrumentation-graphql": "0.62.0", "@opentelemetry/instrumentation-hapi": "0.60.0", "@opentelemetry/instrumentation-http": "0.214.0", "@opentelemetry/instrumentation-ioredis": "0.62.0", "@opentelemetry/instrumentation-kafkajs": "0.23.0", "@opentelemetry/instrumentation-knex": "0.58.0", "@opentelemetry/instrumentation-koa": "0.62.0", "@opentelemetry/instrumentation-lru-memoizer": "0.58.0", "@opentelemetry/instrumentation-mongodb": "0.67.0", "@opentelemetry/instrumentation-mongoose": "0.60.0", "@opentelemetry/instrumentation-mysql": "0.60.0", "@opentelemetry/instrumentation-mysql2": "0.60.0", "@opentelemetry/instrumentation-pg": "0.66.0", "@opentelemetry/instrumentation-redis": "0.62.0", "@opentelemetry/instrumentation-tedious": "0.33.0", "@opentelemetry/instrumentation-undici": "0.24.0", "@opentelemetry/resources": "^2.6.1", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "@prisma/instrumentation": "7.6.0", "@sentry/core": "10.47.0", "@sentry/node-core": "10.47.0", "@sentry/opentelemetry": "10.47.0", "import-in-the-middle": "^3.0.0" } }, "sha512-R+btqPepv88o635G6HtVewLjqCLUedBg5HBs7Nq1qbbKvyti01uArUF2f+3DsLenk5B9LUNiRlE+frZA44Ahmw=="],
|
||||
|
||||
"@sentry/node-core": ["@sentry/node-core@10.47.0", "https://registry.npmmirror.com/@sentry/node-core/-/node-core-10.47.0.tgz", { "dependencies": { "@sentry/core": "10.47.0", "@sentry/opentelemetry": "10.47.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/context-async-hooks", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/resources", "@opentelemetry/sdk-trace-base", "@opentelemetry/semantic-conventions"] }, "sha512-qv6LsqHbkQmd0aQEUox/svRSz26J+l4gGjFOUNEay2armZu9XLD+Ct89jpFgZD5oIPNAj2jraodTRqydXiwS5w=="],
|
||||
|
||||
"@sentry/opentelemetry": ["@sentry/opentelemetry@10.47.0", "https://registry.npmmirror.com/@sentry/opentelemetry/-/opentelemetry-10.47.0.tgz", { "dependencies": { "@sentry/core": "10.47.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" } }, "sha512-f6Hw2lrpCjlOksiosP0Z2jK/+l+21SIdoNglVeG/sttMyx8C8ywONKh0Ha50sFsvB1VaB8n94RKzzf3hkh9V3g=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
"@smithy/abort-controller": ["@smithy/abort-controller@2.2.0", "https://registry.npmmirror.com/@smithy/abort-controller/-/abort-controller-2.2.0.tgz", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw=="],
|
||||
@@ -730,24 +663,14 @@
|
||||
|
||||
"@types/cacache": ["@types/cacache@20.0.1", "https://registry.npmmirror.com/@types/cacache/-/cacache-20.0.1.tgz", { "dependencies": { "@types/node": "*", "minipass": "*" } }, "sha512-QlKW3AFoFr/hvPHwFHMIVUH/ZCYeetBNou3PCmxu5LaNDvrtBlPJtIA6uhmU9JRt9oxj7IYoqoLcpxtzpPiTcw=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/lodash": ["@types/lodash@4.17.24", "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
|
||||
|
||||
"@types/lodash-es": ["@types/lodash-es@4.17.12", "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
|
||||
|
||||
"@types/mute-stream": ["@types/mute-stream@0.0.4", "https://registry.npmmirror.com/@types/mute-stream/-/mute-stream-0.0.4.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow=="],
|
||||
|
||||
"@types/mysql": ["@types/mysql@2.15.27", "https://registry.npmmirror.com/@types/mysql/-/mysql-2.15.27.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.0", "https://registry.npmmirror.com/@types/node/-/node-25.5.0.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.13.tgz", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.15.6", "https://registry.npmmirror.com/@types/pg/-/pg-8.15.6.tgz", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
|
||||
|
||||
"@types/pg-pool": ["@types/pg-pool@2.0.7", "https://registry.npmmirror.com/@types/pg-pool/-/pg-pool-2.0.7.tgz", { "dependencies": { "@types/pg": "*" } }, "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng=="],
|
||||
|
||||
"@types/plist": ["@types/plist@3.0.5", "https://registry.npmmirror.com/@types/plist/-/plist-3.0.5.tgz", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.14", "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
@@ -756,8 +679,6 @@
|
||||
|
||||
"@types/sharp": ["@types/sharp@0.32.0", "https://registry.npmmirror.com/@types/sharp/-/sharp-0.32.0.tgz", { "dependencies": { "sharp": "*" } }, "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw=="],
|
||||
|
||||
"@types/tedious": ["@types/tedious@4.0.14", "https://registry.npmmirror.com/@types/tedious/-/tedious-4.0.14.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="],
|
||||
|
||||
"@types/turndown": ["@types/turndown@5.0.6", "https://registry.npmmirror.com/@types/turndown/-/turndown-5.0.6.tgz", {}, "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg=="],
|
||||
|
||||
"@types/wrap-ansi": ["@types/wrap-ansi@3.0.0", "https://registry.npmmirror.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", {}, "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g=="],
|
||||
@@ -766,18 +687,10 @@
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.12.tgz", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "https://registry.npmmirror.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
|
||||
|
||||
"agent-base": ["agent-base@8.0.0", "https://registry.npmmirror.com/agent-base/-/agent-base-8.0.0.tgz", {}, "sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg=="],
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
@@ -838,8 +751,6 @@
|
||||
|
||||
"chokidar": ["chokidar@5.0.0", "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"cjs-module-lexer": ["cjs-module-lexer@2.2.0", "https://registry.npmmirror.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@4.0.1", "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-4.0.1.tgz", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="],
|
||||
|
||||
"cli-highlight": ["cli-highlight@2.1.11", "https://registry.npmmirror.com/cli-highlight/-/cli-highlight-2.1.11.tgz", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="],
|
||||
@@ -934,8 +845,6 @@
|
||||
|
||||
"etag": ["etag@1.8.1", "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"event-target-shim": ["event-target-shim@5.0.1", "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||
|
||||
"eventsource": ["eventsource@3.0.7", "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.7.tgz", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.6", "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
@@ -982,18 +891,12 @@
|
||||
|
||||
"form-data": ["form-data@4.0.5", "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"form-data-encoder": ["form-data-encoder@1.7.2", "https://registry.npmmirror.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
||||
|
||||
"formatly": ["formatly@0.3.0", "https://registry.npmmirror.com/formatly/-/formatly-0.3.0.tgz", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="],
|
||||
|
||||
"formdata-node": ["formdata-node@4.4.1", "https://registry.npmmirror.com/formdata-node/-/formdata-node-4.4.1.tgz", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
|
||||
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"forwarded-parse": ["forwarded-parse@2.1.2", "https://registry.npmmirror.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"fs-extra": ["fs-extra@10.1.0", "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
|
||||
@@ -1044,8 +947,6 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"he": ["he@1.2.0", "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
|
||||
|
||||
"highlight.js": ["highlight.js@11.11.1", "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
||||
|
||||
"hono": ["hono@4.12.9", "https://registry.npmmirror.com/hono/-/hono-4.12.9.tgz", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
|
||||
@@ -1058,16 +959,12 @@
|
||||
|
||||
"human-signals": ["human-signals@8.0.1", "https://registry.npmmirror.com/human-signals/-/human-signals-8.0.1.tgz", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||
|
||||
"humanize-ms": ["humanize-ms@1.2.1", "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"image-processor-napi": ["image-processor-napi@workspace:packages/image-processor-napi"],
|
||||
|
||||
"import-in-the-middle": ["import-in-the-middle@3.0.0", "https://registry.npmmirror.com/import-in-the-middle/-/import-in-the-middle-3.0.0.tgz", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-OnGy+eYT7wVejH2XWgLRgbmzujhhVIATQH0ztIeRilwHBjTeG3pD+XnH3PKX0r9gJ0BuJmJ68q/oh9qgXnNDQg=="],
|
||||
|
||||
"indent-string": ["indent-string@5.0.0", "https://registry.npmmirror.com/indent-string/-/indent-string-5.0.0.tgz", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
@@ -1184,8 +1081,6 @@
|
||||
|
||||
"modifiers-napi": ["modifiers-napi@workspace:packages/modifiers-napi"],
|
||||
|
||||
"module-details-from-path": ["module-details-from-path@1.0.4", "https://registry.npmmirror.com/module-details-from-path/-/module-details-from-path-1.0.4.tgz", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mute-stream": ["mute-stream@1.0.0", "https://registry.npmmirror.com/mute-stream/-/mute-stream-1.0.0.tgz", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="],
|
||||
@@ -1196,7 +1091,7 @@
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
"node-fetch": ["node-fetch@3.3.2", "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"node-forge": ["node-forge@1.4.0", "https://registry.npmmirror.com/node-forge/-/node-forge-1.4.0.tgz", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="],
|
||||
|
||||
@@ -1212,8 +1107,6 @@
|
||||
|
||||
"open": ["open@10.2.0", "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
|
||||
"openai": ["openai@4.104.0", "https://registry.npmmirror.com/openai/-/openai-4.104.0.tgz", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
|
||||
|
||||
"os-tmpdir": ["os-tmpdir@1.0.2", "https://registry.npmmirror.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="],
|
||||
|
||||
"oxc-parser": ["oxc-parser@0.121.0", "https://registry.npmmirror.com/oxc-parser/-/oxc-parser-0.121.0.tgz", { "dependencies": { "@oxc-project/types": "^0.121.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.121.0", "@oxc-parser/binding-android-arm64": "0.121.0", "@oxc-parser/binding-darwin-arm64": "0.121.0", "@oxc-parser/binding-darwin-x64": "0.121.0", "@oxc-parser/binding-freebsd-x64": "0.121.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.121.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.121.0", "@oxc-parser/binding-linux-arm64-gnu": "0.121.0", "@oxc-parser/binding-linux-arm64-musl": "0.121.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.121.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.121.0", "@oxc-parser/binding-linux-riscv64-musl": "0.121.0", "@oxc-parser/binding-linux-s390x-gnu": "0.121.0", "@oxc-parser/binding-linux-x64-gnu": "0.121.0", "@oxc-parser/binding-linux-x64-musl": "0.121.0", "@oxc-parser/binding-openharmony-arm64": "0.121.0", "@oxc-parser/binding-wasm32-wasi": "0.121.0", "@oxc-parser/binding-win32-arm64-msvc": "0.121.0", "@oxc-parser/binding-win32-ia32-msvc": "0.121.0", "@oxc-parser/binding-win32-x64-msvc": "0.121.0" } }, "sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg=="],
|
||||
@@ -1246,12 +1139,6 @@
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.4.1", "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.4.1.tgz", {}, "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "https://registry.npmmirror.com/pg-int8/-/pg-int8-1.0.1.tgz", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.13.0", "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.13.0.tgz", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="],
|
||||
|
||||
"pg-types": ["pg-types@2.2.0", "https://registry.npmmirror.com/pg-types/-/pg-types-2.2.0.tgz", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
@@ -1262,14 +1149,6 @@
|
||||
|
||||
"pngjs": ["pngjs@5.0.0", "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
|
||||
|
||||
"postgres-array": ["postgres-array@2.0.0", "https://registry.npmmirror.com/postgres-array/-/postgres-array-2.0.0.tgz", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"postgres-bytea": ["postgres-bytea@1.0.1", "https://registry.npmmirror.com/postgres-bytea/-/postgres-bytea-1.0.1.tgz", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||
|
||||
"postgres-date": ["postgres-date@1.0.7", "https://registry.npmmirror.com/postgres-date/-/postgres-date-1.0.7.tgz", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "https://registry.npmmirror.com/postgres-interval/-/postgres-interval-1.2.0.tgz", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"pretty-bytes": ["pretty-bytes@5.6.0", "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="],
|
||||
|
||||
"pretty-ms": ["pretty-ms@9.3.0", "https://registry.npmmirror.com/pretty-ms/-/pretty-ms-9.3.0.tgz", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
|
||||
@@ -1304,8 +1183,6 @@
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"require-in-the-middle": ["require-in-the-middle@8.0.1", "https://registry.npmmirror.com/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="],
|
||||
|
||||
"require-main-filename": ["require-main-filename@2.0.0", "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
@@ -1434,7 +1311,7 @@
|
||||
|
||||
"walk-up-path": ["walk-up-path@4.0.0", "https://registry.npmmirror.com/walk-up-path/-/walk-up-path-4.0.0.tgz", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
@@ -1456,8 +1333,6 @@
|
||||
|
||||
"xss": ["xss@1.0.15", "https://registry.npmmirror.com/xss/-/xss-1.0.15.tgz", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": { "xss": "bin/xss" } }, "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@4.0.0", "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||
@@ -1632,8 +1507,6 @@
|
||||
|
||||
"@aws-sdk/xml-builder/@smithy/types": ["@smithy/types@4.13.1", "https://registry.npmmirror.com/@smithy/types/-/types-4.13.1.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="],
|
||||
|
||||
"@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.212.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="],
|
||||
|
||||
"@grpc/proto-loader/yargs": ["yargs@17.7.2", "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"@inquirer/core/@types/node": ["@types/node@22.19.15", "https://registry.npmmirror.com/@types/node/-/node-22.19.15.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
|
||||
@@ -1642,8 +1515,6 @@
|
||||
|
||||
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
||||
"@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.207.0", "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.207.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="],
|
||||
|
||||
"@smithy/config-resolver/@smithy/types": ["@smithy/types@4.13.1", "https://registry.npmmirror.com/@smithy/types/-/types-4.13.1.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="],
|
||||
|
||||
"@smithy/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.12", "https://registry.npmmirror.com/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="],
|
||||
@@ -1772,14 +1643,10 @@
|
||||
|
||||
"external-editor/iconv-lite": ["iconv-lite@0.4.24", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"gaxios/node-fetch": ["node-fetch@3.3.2", "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"gtoken/gaxios": ["gaxios@6.7.1", "https://registry.npmmirror.com/gaxios/-/gaxios-6.7.1.tgz", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
|
||||
|
||||
"http-proxy-agent/agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
@@ -1794,8 +1661,6 @@
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"openai/@types/node": ["@types/node@18.19.130", "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
"parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "https://registry.npmmirror.com/parse5/-/parse5-6.0.1.tgz", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="],
|
||||
|
||||
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
@@ -1850,10 +1715,6 @@
|
||||
|
||||
"@aws-sdk/nested-clients/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "https://registry.npmmirror.com/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="],
|
||||
|
||||
"@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "https://registry.npmmirror.com/@opentelemetry/api-logs/-/api-logs-0.212.0.tgz", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="],
|
||||
|
||||
"@fastify/otel/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "https://registry.npmmirror.com/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="],
|
||||
|
||||
"@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
@@ -1868,10 +1729,6 @@
|
||||
|
||||
"@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "https://registry.npmmirror.com/@opentelemetry/api-logs/-/api-logs-0.207.0.tgz", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="],
|
||||
|
||||
"@prisma/instrumentation/@opentelemetry/instrumentation/import-in-the-middle": ["import-in-the-middle@2.0.6", "https://registry.npmmirror.com/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="],
|
||||
|
||||
"@smithy/core/@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "https://registry.npmmirror.com/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="],
|
||||
|
||||
"@smithy/eventstream-serde-universal/@smithy/eventstream-codec/@aws-crypto/crc32": ["@aws-crypto/crc32@3.0.0", "https://registry.npmmirror.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz", { "dependencies": { "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", "tslib": "^1.11.1" } }, "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA=="],
|
||||
@@ -1932,6 +1789,8 @@
|
||||
|
||||
"gtoken/gaxios/is-stream": ["is-stream@2.0.1", "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"gtoken/gaxios/node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"gtoken/gaxios/uuid": ["uuid@9.0.1", "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"image-processor-napi/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||
@@ -1972,8 +1831,6 @@
|
||||
|
||||
"image-processor-napi/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
||||
|
||||
"openai/@types/node/undici-types": ["undici-types@5.26.5", "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"qrcode/yargs/cliui": ["cliui@6.0.0", "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
|
||||
|
||||
"qrcode/yargs/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
@@ -1992,6 +1849,8 @@
|
||||
|
||||
"@anthropic-ai/vertex-sdk/google-auth-library/gaxios/is-stream": ["is-stream@2.0.1", "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"@anthropic-ai/vertex-sdk/google-auth-library/gaxios/node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"@anthropic-ai/vertex-sdk/google-auth-library/gaxios/uuid": ["uuid@9.0.1", "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"@anthropic-ai/vertex-sdk/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "https://registry.npmmirror.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
# 自动更新机制
|
||||
|
||||
## 概述
|
||||
|
||||
Claude Code 拥有一套复杂的多策略自动更新系统,支持三种安装方式、后台静默更新、手动 CLI 命令、服务端版本门控以及更新日志展示。系统设计目标是在最小用户干预下保持 CLI 最新,同时提供回滚和手动控制的兜底手段。
|
||||
|
||||
---
|
||||
|
||||
## 安装类型与更新策略
|
||||
|
||||
更新策略由安装方式决定,通过 `src/utils/doctorDiagnostic.ts` 检测:
|
||||
|
||||
| 安装类型 | 更新策略 | 自动安装? |
|
||||
|---|---|---|
|
||||
| `native` | 从 GCS/Artifactory 下载二进制文件,通过符号链接激活 | 是(静默) |
|
||||
| `npm-global` | `npm install -g` / `bun install -g` | 是(静默) |
|
||||
| `npm-local` | `npm install` 到 `~/.claude/local/` | 是(静默) |
|
||||
| `package-manager` | 显示通知,附带对应操作系统的升级命令 | 否(仅通知) |
|
||||
| `development` | 不适用 — 执行 `claude update` 时报错 | 不适用 |
|
||||
|
||||
### 策略路由
|
||||
|
||||
`src/components/AutoUpdaterWrapper.tsx` — 挂载在 React/Ink UI 树中 — 检测安装类型并渲染对应的更新组件:
|
||||
|
||||
- `native` → `NativeAutoUpdater`(二进制下载 + 符号链接)
|
||||
- `package-manager` → `PackageManagerAutoUpdater`(仅通知)
|
||||
- 其他 → `AutoUpdater`(基于 JS/npm)
|
||||
|
||||
---
|
||||
|
||||
## 后台自动更新循环
|
||||
|
||||
三个更新组件共享相同的轮询模式:
|
||||
|
||||
```typescript
|
||||
useInterval(checkForUpdates, 30 * 60 * 1000); // 每 30 分钟
|
||||
```
|
||||
|
||||
组件挂载时(即启动时)也会执行一次检查。
|
||||
|
||||
### 前置检查门控
|
||||
|
||||
任何更新尝试之前,系统会依次检查:
|
||||
|
||||
1. **自动更新是否被禁用?** — `getAutoUpdaterDisabledReason()`(`src/utils/config.ts:1735`)
|
||||
- `NODE_ENV === 'development'`
|
||||
- 设置了 `DISABLE_AUTOUPDATER` 环境变量
|
||||
- 仅限必要流量模式
|
||||
- `config.autoUpdates === false`(native 安装的保护模式除外)
|
||||
2. **最大版本上限?** — `getMaxVersion()`(`src/utils/autoUpdater.ts:108`)— 服务端熔断开关,防止更新到已知有问题的版本
|
||||
3. **是否跳过该版本?** — `shouldSkipVersion()`(`src/utils/autoUpdater.ts:145`)— 尊重用户的 `minimumVersion` 设置,防止切换到 stable 频道时发生意外的版本降级
|
||||
|
||||
### Native 自动更新器(`src/components/NativeAutoUpdater.tsx`)
|
||||
|
||||
1. 调用 `src/utils/nativeInstaller/installer.ts` 中的 `installLatest()`
|
||||
2. 通过 `src/utils/nativeInstaller/download.ts` 下载二进制文件(GCS 或 Artifactory)
|
||||
3. 验证 SHA256 校验和(3 次重试,60 秒卡顿检测)
|
||||
4. 将版本化二进制文件存储到 XDG 目录
|
||||
5. 更新符号链接(`~/.local/bin/claude` → 新版本二进制文件)
|
||||
6. 保留最近 2 个版本,清理旧版本
|
||||
7. 将错误分类上报分析(超时、校验和、权限、磁盘空间不足、npm、网络)
|
||||
|
||||
### JS/npm 自动更新器(`src/components/AutoUpdater.tsx`)
|
||||
|
||||
1. 调用 `getLatestVersion()` 获取当前 npm dist-tag
|
||||
2. 通过 semver `gte()` 比较版本
|
||||
3. 根据安装类型路由到本地或全局安装
|
||||
4. 使用文件锁(`acquireLock()` / `releaseLock()`)防止并发更新
|
||||
|
||||
### 包管理器通知器(`src/components/PackageManagerAutoUpdater.tsx`)
|
||||
|
||||
每 30 分钟通过 GCS 存储桶(非 npm)检查更新。**不会自动安装** — 仅显示对应操作系统的升级命令:
|
||||
|
||||
- macOS: `brew upgrade claude-code`
|
||||
- Windows: `winget upgrade Anthropic.ClaudeCode`
|
||||
- Alpine: `apk upgrade claude-code`
|
||||
|
||||
---
|
||||
|
||||
## 启动版本门控
|
||||
|
||||
`src/utils/autoUpdater.ts:70` — `assertMinVersion()`
|
||||
|
||||
从 `src/main.tsx:1775` 在启动时调用:
|
||||
|
||||
```typescript
|
||||
void assertMinVersion();
|
||||
```
|
||||
|
||||
1. 从 GrowthBook 动态配置获取 `tengu_version_config`
|
||||
2. 如果 `MACRO.VERSION < minVersion`,打印错误信息并调用 `gracefulShutdownSync(1)` — 强制用户更新
|
||||
3. 这是一个**硬性门控** — 低于最低版本的 CLI 将无法启动
|
||||
|
||||
---
|
||||
|
||||
## 手动 CLI 命令
|
||||
|
||||
### `claude update` / `claude upgrade`
|
||||
|
||||
**文件**: `src/cli/update.ts`
|
||||
|
||||
完整流程:
|
||||
1. 运行 `getDoctorDiagnostic()` 检查系统健康状态
|
||||
2. 检查是否存在多个安装及配置不一致
|
||||
3. 根据安装类型路由:
|
||||
- `development` → 报错("开发版本不支持自动更新")
|
||||
- `package-manager` → 打印对应操作系统的更新命令
|
||||
- `native` → 使用原生安装器的 `updateLatest()`
|
||||
- `npm-local` → 在 `~/.claude/local/` 执行 `npm install`
|
||||
- `npm-global` → 执行 `npm install -g`(含权限检查)
|
||||
4. 报告当前版本、最新版本、成功/失败状态
|
||||
|
||||
### `claude rollback [target]`(仅限内部)
|
||||
|
||||
回滚到之前的版本。支持 `--list`、`--dry-run`、`--safe` 标志。
|
||||
|
||||
### `claude install [target]`
|
||||
|
||||
安装或重新安装原生构建版本。接受可选的版本目标参数。
|
||||
|
||||
### `claude doctor`
|
||||
|
||||
检查自动更新器的健康状态,报告状态、权限和配置信息。
|
||||
|
||||
---
|
||||
|
||||
## 原生安装器架构
|
||||
|
||||
**文件**: `src/utils/nativeInstaller/installer.ts`
|
||||
|
||||
### 二进制文件存储布局
|
||||
|
||||
```
|
||||
~/.local/share/claude-code/
|
||||
├── versions/ # 版本化二进制文件 (claude-1.0.3, claude-1.0.4, ...)
|
||||
├── staging/ # 临时下载暂存区
|
||||
└── locks/ # 基于 PID 和 mtime 的锁文件
|
||||
|
||||
~/.local/bin/claude # 指向当前版本二进制文件的符号链接
|
||||
```
|
||||
|
||||
Windows 系统使用文件复制而非符号链接。
|
||||
|
||||
### 核心操作
|
||||
|
||||
| 函数 | 说明 |
|
||||
|---|---|
|
||||
| `updateLatest()` | 核心更新流程:最大版本上限 → 跳过检查 → 加锁 → 下载 → 安装 → 更新符号链接 |
|
||||
| `installLatest()` | Singleflight 包装版本,防止重复的进行中安装 |
|
||||
| `cleanupOldVersions()` | 保留最近 2 个版本,清理过期的暂存区和临时文件 |
|
||||
| `lockCurrentVersion()` | 进程生命周期锁,防止正在运行的版本被删除 |
|
||||
| `cleanupNpmInstallations()` | 迁移到原生安装时清理旧的 npm 安装 |
|
||||
|
||||
### 下载与校验
|
||||
|
||||
**文件**: `src/utils/nativeInstaller/download.ts`
|
||||
|
||||
1. 路由到 Artifactory(内部用户)或 GCS 存储桶(外部用户)
|
||||
2. 下载二进制文件并跟踪进度
|
||||
3. SHA256 校验和验证
|
||||
4. 60 秒卡顿检测(中止停滞的下载)
|
||||
5. 失败时自动重试 3 次
|
||||
|
||||
---
|
||||
|
||||
## 文件锁机制
|
||||
|
||||
**文件**: `src/utils/autoUpdater.ts:176-268`
|
||||
|
||||
防止并发更新进程破坏安装:
|
||||
|
||||
- 锁文件:`~/.claude/update.lock`(或等效路径)
|
||||
- 5 分钟超时 — 超过 5 分钟的锁被视为过期,强制获取
|
||||
- 进程将其 PID 写入锁文件
|
||||
- `acquireLock()` 和 `releaseLock()` 同时被 JS/npm 和原生安装器使用
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
### 设置项
|
||||
|
||||
**文件**: `src/utils/settings/types.ts`
|
||||
|
||||
| 设置项 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `autoUpdatesChannel` | `'latest' \| 'stable'` | 自动更新的发布频道 |
|
||||
| `minimumVersion` | string | 最低版本要求,防止意外的版本降级 |
|
||||
|
||||
### 全局配置
|
||||
|
||||
**文件**: `src/utils/config.ts:191-193`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `autoUpdates` | boolean | 启用/禁用自动更新(旧版) |
|
||||
| `autoUpdatesProtectedForNative` | boolean | 原生安装始终自动更新 |
|
||||
|
||||
### 配置迁移
|
||||
|
||||
**文件**: `src/migrations/migrateAutoUpdatesToSettings.ts`
|
||||
|
||||
一次性将旧版 `globalConfig.autoUpdates = false` 迁移为 settings 中的 `DISABLE_AUTOUPDATER=1` 环境变量。从 `src/main.tsx:325` 在启动时调用。
|
||||
|
||||
---
|
||||
|
||||
## 更新通知去重
|
||||
|
||||
**文件**: `src/hooks/useUpdateNotification.ts`
|
||||
|
||||
React hook `useUpdateNotification(updatedVersion)` — 确保每次 semver 变更(major.minor.patch)只显示一次"重启以更新"消息,避免同一版本的重复通知。
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
**文件**: `src/utils/releaseNotes.ts`
|
||||
|
||||
1. 从 `src/setup.ts:387` 在每次启动时调用
|
||||
2. 从 GitHub 获取 changelog
|
||||
3. 缓存到 `~/.claude/cache/changelog.md`
|
||||
4. 展示比 `lastReleaseNotesSeen` 更新的版本的更新日志
|
||||
5. 使用 semver 比较确定需要展示哪些日志
|
||||
|
||||
---
|
||||
|
||||
## 版本比较
|
||||
|
||||
**文件**: `src/utils/semver.ts`
|
||||
|
||||
- 提供 `gt()`、`gte()`、`lt()`、`lte()`、`satisfies()`、`order()`
|
||||
- 在 Bun 环境下使用 `Bun.semver.order()`(快 20 倍)
|
||||
- 在 Node.js 环境下回退到 npm `semver` 包
|
||||
|
||||
---
|
||||
|
||||
## 分析事件
|
||||
|
||||
所有更新相关的遥测数据使用 `tengu_` 前缀的事件:
|
||||
|
||||
| 类别 | 事件 |
|
||||
|---|---|
|
||||
| 版本检查 | `tengu_version_check_success`、`tengu_version_check_failure` |
|
||||
| JS 自动更新器 | `tengu_auto_updater_start/success/fail/up_to_date/lock_contention` |
|
||||
| 原生自动更新器 | `tengu_native_auto_updater_start/success/fail` |
|
||||
| 原生更新 | `tengu_native_update_complete/skipped_max_version/skipped_minimum_version` |
|
||||
| 锁机制 | `tengu_version_lock_acquired/failed`、`tengu_native_update_lock_failed` |
|
||||
| 二进制下载 | `tengu_binary_download_attempt/success/failure`、`tengu_binary_manifest_fetch_failure` |
|
||||
| 清理 | `tengu_native_version_cleanup`、`tengu_native_staging_cleanup`、`tengu_native_stale_locks_cleanup` |
|
||||
| 安装 | `tengu_native_install_package_success/failure`、`tengu_native_install_binary_success/failure` |
|
||||
| 手动更新 | `tengu_update_check` |
|
||||
| 迁移 | `tengu_migrate_autoupdates_to_settings`、`tengu_migrate_autoupdates_error` |
|
||||
|
||||
---
|
||||
|
||||
## 关键文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| `src/utils/autoUpdater.ts` | 核心逻辑:版本检查、npm 安装、文件锁、最低/最高版本门控 |
|
||||
| `src/cli/update.ts` | `claude update` 命令处理 |
|
||||
| `src/utils/nativeInstaller/installer.ts` | 原生二进制安装器:下载、版本管理、符号链接、清理 |
|
||||
| `src/utils/nativeInstaller/download.ts` | 从 GCS/Artifactory 下载二进制文件并校验 |
|
||||
| `src/utils/localInstaller.ts` | 本地安装器(`~/.claude/local/`)基于 npm |
|
||||
| `src/components/AutoUpdaterWrapper.tsx` | 基于安装类型的策略路由 |
|
||||
| `src/components/AutoUpdater.tsx` | JS/npm 后台自动更新器(30 分钟间隔) |
|
||||
| `src/components/NativeAutoUpdater.tsx` | 原生二进制后台自动更新器(30 分钟间隔) |
|
||||
| `src/components/PackageManagerAutoUpdater.tsx` | 包管理器通知(30 分钟,仅展示) |
|
||||
| `src/hooks/useUpdateNotification.ts` | 按 semver 去重更新通知 |
|
||||
| `src/utils/releaseNotes.ts` | Changelog 获取、缓存与展示 |
|
||||
| `src/utils/semver.ts` | Semver 版本比较(Bun 原生 + npm 回退) |
|
||||
| `src/utils/doctorDiagnostic.ts` | 安装类型检测与健康诊断 |
|
||||
| `src/utils/config.ts:1735` | `getAutoUpdaterDisabledReason()` — 禁用检查逻辑 |
|
||||
| `src/migrations/migrateAutoUpdatesToSettings.ts` | 旧版配置迁移 |
|
||||
| `src/screens/Doctor.tsx` | Doctor 命令 UI,展示自动更新状态 |
|
||||
|
||||
---
|
||||
|
||||
## 流程图
|
||||
|
||||
```
|
||||
启动阶段
|
||||
├── assertMinVersion() → 版本过低时硬性拦截,拒绝启动
|
||||
├── migrateAutoUpdatesToSettings() → 一次性配置迁移
|
||||
└── checkForReleaseNotes() → 展示新版本的更新日志
|
||||
|
||||
REPL 运行中(每 30 分钟)
|
||||
├── AutoUpdaterWrapper 检测安装类型
|
||||
│
|
||||
├── native → NativeAutoUpdater
|
||||
│ ├── 从 GCS/Artifactory 获取版本
|
||||
│ ├── 检查最大版本上限(服务端控制)
|
||||
│ ├── 检查 minimumVersion 设置(跳过)
|
||||
│ ├── acquireLock()
|
||||
│ ├── downloadAndVerifyBinary()(SHA256 校验,3 次重试)
|
||||
│ ├── 安装到 versions/ 目录
|
||||
│ ├── 更新符号链接
|
||||
│ └── cleanupOldVersions()(保留 2 个版本)
|
||||
│
|
||||
├── npm-global/local → AutoUpdater
|
||||
│ ├── 从 npm registry 获取最新版本
|
||||
│ ├── semver 版本比较
|
||||
│ ├── acquireLock()
|
||||
│ └── npm install -g / 本地安装
|
||||
│
|
||||
└── package-manager → PackageManagerAutoUpdater
|
||||
├── 从 GCS 获取版本
|
||||
└── 显示 "Run: brew upgrade ..."(不自动安装)
|
||||
|
||||
手动操作
|
||||
└── claude update → 完整诊断 + 安装编排
|
||||
```
|
||||
@@ -1,209 +0,0 @@
|
||||
# Claude Code 远程服务器依赖
|
||||
|
||||
> 只列出代码中实际发起网络请求的远程服务。本地服务、npm 包依赖、展示用 URL 不包含在内。
|
||||
|
||||
## 总览表
|
||||
|
||||
| # | 服务 | 远程端点 | 协议 | 状态 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Anthropic API | `api.anthropic.com` | HTTPS | 默认启用 |
|
||||
| 2 | AWS Bedrock | `bedrock-runtime.*.amazonaws.com` | HTTPS | 需 `CLAUDE_CODE_USE_BEDROCK=1` |
|
||||
| 3 | Google Vertex AI | `{region}-aiplatform.googleapis.com` | HTTPS | 需 `CLAUDE_CODE_USE_VERTEX=1` |
|
||||
| 4 | Azure Foundry | `{resource}.services.ai.azure.com` | HTTPS | 需 `CLAUDE_CODE_USE_FOUNDRY=1` |
|
||||
| 5 | OAuth (Anthropic) | `platform.claude.com`, `claude.com`, `claude.ai` | HTTPS | 用户登录时 |
|
||||
| 6 | GrowthBook | `api.anthropic.com` (remoteEval) | HTTPS | 默认启用 |
|
||||
| 7 | Sentry | 可配置 (`SENTRY_DSN`) | HTTPS | 需设环境变量 |
|
||||
| 8 | Datadog | 可配置 (`DATADOG_LOGS_ENDPOINT`) | HTTPS | 需设环境变量 |
|
||||
| 9 | OpenTelemetry Collector | 可配置 (`OTEL_EXPORTER_OTLP_ENDPOINT`) | gRPC/HTTP | 需设环境变量 |
|
||||
| 10 | 1P Event Logging | `api.anthropic.com/api/event_logging/batch` | HTTPS | 默认启用 |
|
||||
| 11 | BigQuery Metrics | `api.anthropic.com/api/claude_code/metrics` | HTTPS | 默认启用 |
|
||||
| 12 | MCP Proxy | `mcp-proxy.anthropic.com` | HTTPS+WS | 使用 MCP 工具时 |
|
||||
| 13 | MCP Registry | `api.anthropic.com/mcp-registry` | HTTPS | 查询 MCP 服务器时 |
|
||||
| 14 | Bing Search | `www.bing.com` | HTTPS | WebSearch 工具 |
|
||||
| 15 | Google Cloud Storage (更新) | `storage.googleapis.com` | HTTPS | 版本检查 |
|
||||
| 16 | GitHub Raw (Changelog/Stats) | `raw.githubusercontent.com` | HTTPS | 更新提示 |
|
||||
| 17 | Claude in Chrome Bridge | `bridge.claudeusercontent.com` | WSS | Chrome 集成 |
|
||||
| 18 | CCR Upstream Proxy | `api.anthropic.com` | WS | CCR 远程会话 |
|
||||
| 19 | Voice STT | `api.anthropic.com/api/ws/...` | WSS | Voice Mode |
|
||||
| 20 | Desktop App Download | `claude.ai/api/desktop/...` | HTTPS | 下载引导 |
|
||||
|
||||
---
|
||||
|
||||
## 详细说明
|
||||
|
||||
### 1. Anthropic Messages API
|
||||
|
||||
核心 LLM 推理服务,发送对话消息、接收流式响应。
|
||||
|
||||
- **端点**: `https://api.anthropic.com` (生产) / `https://api-staging.anthropic.com` (staging)
|
||||
- **覆盖**: `ANTHROPIC_BASE_URL` 环境变量
|
||||
- **认证**: API Key / OAuth Token
|
||||
- **文件**: `src/services/api/client.ts`, `src/services/api/claude.ts`
|
||||
|
||||
### 2. AWS Bedrock
|
||||
|
||||
- **端点**: `bedrock-runtime.{region}.amazonaws.com`
|
||||
- **认证**: AWS 凭证链 / `AWS_BEARER_TOKEN_BEDROCK`
|
||||
- **文件**: `src/services/api/client.ts:153-190`, `src/utils/aws.ts`
|
||||
|
||||
### 3. Google Vertex AI
|
||||
|
||||
- **端点**: `{region}-aiplatform.googleapis.com`
|
||||
- **认证**: `GoogleAuth` + `cloud-platform` scope
|
||||
- **文件**: `src/services/api/client.ts:228-298`
|
||||
|
||||
### 4. Azure Foundry
|
||||
|
||||
- **端点**: `https://{resource}.services.ai.azure.com/anthropic/v1/messages`
|
||||
- **认证**: API Key 或 Azure AD `DefaultAzureCredential`
|
||||
- **文件**: `src/services/api/client.ts:191-220`
|
||||
|
||||
### 5. OAuth
|
||||
|
||||
OAuth 2.0 + PKCE 授权码流程。
|
||||
|
||||
- **端点**:
|
||||
- `https://platform.claude.com/oauth/authorize` — 授权页
|
||||
- `https://claude.com/cai/oauth/authorize` — Claude.ai 授权
|
||||
- `https://platform.claude.com/v1/oauth/token` — Token 交换
|
||||
- `https://api.anthropic.com/api/oauth/claude_cli/create_api_key` — 创建 API Key
|
||||
- `https://api.anthropic.com/api/oauth/claude_cli/roles` — 获取角色
|
||||
- `https://claude.ai/oauth/claude-code-client-metadata` — MCP 客户端元数据
|
||||
- `https://claude.fedstart.com` — FedStart 政府部署
|
||||
- **文件**: `src/constants/oauth.ts`, `src/services/oauth/`
|
||||
|
||||
### 6. GrowthBook (功能开关)
|
||||
|
||||
- **端点**: `https://api.anthropic.com/` (remoteEval 模式) 或 `CLAUDE_GB_ADAPTER_URL`
|
||||
- **SDK Keys**: `sdk-zAZezfDKGoZuXXKe` (外部), `sdk-xRVcrliHIlrg4og4` (ant prod), `sdk-yZQvlplybuXjYh6L` (ant dev)
|
||||
- **文件**: `src/services/analytics/growthbook.ts`, `src/constants/keys.ts`
|
||||
|
||||
### 7. Sentry (错误追踪)
|
||||
|
||||
- **激活**: 设置 `SENTRY_DSN` (默认未配置)
|
||||
- **行为**: 仅错误上报,自动过滤敏感 header
|
||||
- **文件**: `src/utils/sentry.ts`
|
||||
|
||||
### 8. Datadog (日志)
|
||||
|
||||
- **激活**: 同时设 `DATADOG_LOGS_ENDPOINT` + `DATADOG_API_KEY` (默认未配置)
|
||||
- **文件**: `src/services/analytics/datadog.ts`
|
||||
|
||||
### 9. OpenTelemetry Collector
|
||||
|
||||
- **激活**: `CLAUDE_CODE_ENABLE_TELEMETRY=1` 或 `OTEL_*` 环境变量
|
||||
- **协议**: gRPC / HTTP / Protobuf,支持 OTLP 和 Prometheus 导出
|
||||
- **文件**: `src/utils/telemetry/instrumentation.ts`
|
||||
|
||||
### 10. 1P Event Logging (内部事件)
|
||||
|
||||
- **端点**: `https://api.anthropic.com/api/event_logging/batch`
|
||||
- **协议**: 批量导出 (10s 间隔, 每批 200 事件)
|
||||
- **文件**: `src/services/analytics/firstPartyEventLoggingExporter.ts`
|
||||
|
||||
### 11. BigQuery Metrics
|
||||
|
||||
- **端点**: `https://api.anthropic.com/api/claude_code/metrics`
|
||||
- **文件**: `src/utils/telemetry/bigqueryExporter.ts`
|
||||
|
||||
### 12. MCP Proxy
|
||||
|
||||
Anthropic 托管的 MCP 服务器代理。
|
||||
|
||||
- **端点**: `https://mcp-proxy.anthropic.com/v1/mcp/{server_id}`
|
||||
- **认证**: Claude.ai OAuth tokens
|
||||
- **文件**: `src/services/mcp/client.ts`, `src/constants/oauth.ts`
|
||||
|
||||
### 13. MCP Registry
|
||||
|
||||
获取官方 MCP 服务器列表。
|
||||
|
||||
- **端点**: `https://api.anthropic.com/mcp-registry/v0/servers?version=latest&visibility=commercial`
|
||||
- **文件**: `src/services/mcp/officialRegistry.ts`
|
||||
|
||||
### 14. Bing Search
|
||||
|
||||
WebSearch 工具的默认适配器,抓取 Bing 搜索结果。
|
||||
|
||||
- **端点**: `https://www.bing.com/search?q={query}&setmkt=en-US`
|
||||
- **文件**: `src/tools/WebSearchTool/adapters/bingAdapter.ts`
|
||||
|
||||
另外还有 Domain Blocklist 查询:
|
||||
- **端点**: `https://api.anthropic.com/api/web/domain_info?domain={domain}`
|
||||
- **文件**: `src/tools/WebFetchTool/utils.ts`
|
||||
|
||||
### 15. Google Cloud Storage (自动更新)
|
||||
|
||||
- **端点**: `https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases`
|
||||
- **文件**: `src/utils/autoUpdater.ts`
|
||||
|
||||
### 16. GitHub Raw Content
|
||||
|
||||
- **端点**: `https://raw.githubusercontent.com/anthropics/claude-code/refs/heads/main/CHANGELOG.md`
|
||||
- **端点**: `https://raw.githubusercontent.com/anthropics/claude-plugins-official/refs/heads/stats/stats/plugin-installs.json`
|
||||
- **文件**: `src/utils/releaseNotes.ts`, `src/utils/plugins/installCounts.ts`
|
||||
|
||||
### 17. Claude in Chrome Bridge
|
||||
|
||||
- **端点**: `wss://bridge.claudeusercontent.com` (生产) / `wss://bridge-staging.claudeusercontent.com` (staging)
|
||||
- **文件**: `src/utils/claudeInChrome/mcpServer.ts`
|
||||
|
||||
### 18. CCR Upstream Proxy
|
||||
|
||||
- **端点**: `ws://api.anthropic.com/v1/code/upstreamproxy/ws`
|
||||
- **激活**: `CLAUDE_CODE_REMOTE=1` + `CCR_UPSTREAM_PROXY_ENABLED=1`
|
||||
- **文件**: `src/upstreamproxy/upstreamproxy.ts`
|
||||
|
||||
### 19. Voice STT
|
||||
|
||||
- **端点**: `wss://api.anthropic.com/api/ws/...`
|
||||
- **文件**: `src/services/voiceStreamSTT.ts`
|
||||
|
||||
### 20. Desktop App Download
|
||||
|
||||
- **端点**: `https://claude.ai/api/desktop/win32/x64/exe/latest/redirect` (Windows)
|
||||
- **端点**: `https://claude.ai/api/desktop/darwin/universal/dmg/latest/redirect` (macOS)
|
||||
- **文件**: `src/components/DesktopHandoff.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Anthropic API 辅助端点汇总
|
||||
|
||||
以下端点都挂在 `api.anthropic.com` 上,按功能分类:
|
||||
|
||||
| 端点路径 | 用途 | 文件 |
|
||||
|---|---|---|
|
||||
| `/api/event_logging/batch` | 事件批量上报 | `src/services/analytics/firstPartyEventLoggingExporter.ts` |
|
||||
| `/api/claude_code/metrics` | BigQuery 指标导出 | `src/utils/telemetry/bigqueryExporter.ts` |
|
||||
| `/api/oauth/claude_cli/create_api_key` | 创建 API Key | `src/constants/oauth.ts` |
|
||||
| `/api/oauth/claude_cli/roles` | 获取用户角色 | `src/constants/oauth.ts` |
|
||||
| `/api/oauth/accounts/grove` | 通知设置 | `src/services/api/grove.ts` |
|
||||
| `/api/oauth/organizations/{id}/referral/*` | 推荐活动 | `src/services/api/referral.ts` |
|
||||
| `/api/oauth/organizations/{id}/overage_credit_grant` | 超额信用 | `src/services/api/overageCreditGrant.ts` |
|
||||
| `/api/oauth/organizations/{id}/admin_requests` | 管理请求 | `src/services/api/adminRequests.ts` |
|
||||
| `/api/web/domain_info?domain={}` | 域名安全检查 | `src/tools/WebFetchTool/utils.ts` |
|
||||
| `/api/claude_code/settings` | 设置同步 | `src/services/settingsSync/index.ts` |
|
||||
| `/api/claude_code/managed_settings` | 企业托管设置 (1h 轮询) | `src/services/remoteManagedSettings/index.ts` |
|
||||
| `/api/claude_code/team_memory?repo={}` | 团队记忆同步 | `src/services/teamMemorySync/index.ts` |
|
||||
| `/api/auth/trusted_devices` | 可信设备注册 | `src/bridge/trustedDevice.ts` |
|
||||
| `/api/organizations/{id}/claude_code/buddy_react` | Companion 反应 | `src/buddy/companionReact.ts` |
|
||||
| `/mcp-registry/v0/servers` | MCP 服务器注册表 | `src/services/mcp/officialRegistry.ts` |
|
||||
| `/v1/files` | 文件上传/下载 | `src/services/api/filesApi.ts` |
|
||||
| `/v1/sessions/{id}/events` | 会话历史 | `src/assistant/sessionHistory.ts` |
|
||||
| `/v1/code/triggers` | 远程触发器 | `src/tools/RemoteTriggerTool/RemoteTriggerTool.ts` |
|
||||
| `/v1/organizations/{id}/mcp_servers` | 组织 MCP 配置 | `src/services/mcp/claudeai.ts` |
|
||||
|
||||
## 非 Anthropic 远程域名汇总
|
||||
|
||||
| 域名 | 服务 | 协议 |
|
||||
|---|---|---|
|
||||
| `bedrock-runtime.*.amazonaws.com` | AWS Bedrock | HTTPS |
|
||||
| `{region}-aiplatform.googleapis.com` | Google Vertex AI | HTTPS |
|
||||
| `{resource}.services.ai.azure.com` | Azure Foundry | HTTPS |
|
||||
| `www.bing.com` | Bing 搜索 | HTTPS |
|
||||
| `storage.googleapis.com` | 自动更新 | HTTPS |
|
||||
| `raw.githubusercontent.com` | Changelog / 插件统计 | HTTPS |
|
||||
| `bridge.claudeusercontent.com` | Chrome Bridge | WSS |
|
||||
| `platform.claude.com` | OAuth 授权页 | HTTPS |
|
||||
| `claude.com` / `claude.ai` | OAuth / 下载 | HTTPS |
|
||||
| `claude.fedstart.com` | FedStart OAuth | HTTPS |
|
||||
@@ -1,457 +0,0 @@
|
||||
# Feature 探索计划书
|
||||
|
||||
> 生成日期:2026-04-02
|
||||
> 代码库中已识别 89 个 feature flag,本文档按实现完整度和探索价值分级,制定探索优先级和路线图。
|
||||
>
|
||||
> **已完成**:BUDDY(✅ 2026-04-02)、TRANSCRIPT_CLASSIFIER / Auto Mode(✅ 2026-04-02)
|
||||
|
||||
---
|
||||
|
||||
## 一、总览
|
||||
|
||||
### 按实现状态分类
|
||||
|
||||
| 状态 | 数量 | 说明 |
|
||||
|------|------|------|
|
||||
| 已实现/可用 | 11 | 代码完整,开启 feature 后可运行(可能需要 OAuth 等外部依赖) |
|
||||
| 部分实现 | 8 | 核心逻辑存在但关键模块为 stub,需要补全 |
|
||||
| 纯 Stub | 15 | 所有函数/工具返回空值,需要从零实现 |
|
||||
| N/A | 55+ | 内部基础设施、低引用量辅助功能,或反编译丢失过多 |
|
||||
|
||||
### 启用方式
|
||||
|
||||
所有 feature 通过环境变量启用:
|
||||
|
||||
```bash
|
||||
# 单个 feature
|
||||
FEATURE_BUDDY=1 bun run dev
|
||||
|
||||
# 多个 feature 组合
|
||||
FEATURE_KAIROS=1 FEATURE_PROACTIVE=1 FEATURE_FORK_SUBAGENT=1 bun run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、Tier 1 — 已实现/可用(优先探索)
|
||||
|
||||
### 2.1 KAIROS(常驻助手模式)⭐ 最高优先级
|
||||
|
||||
- **引用数**:154(全库最大)
|
||||
- **功能**:将 CLI 变为常驻后台助手,支持:
|
||||
- 持久化 bridge 会话(跨重启复用 session)
|
||||
- 后台执行任务(用户离开终端时继续工作)
|
||||
- 推送通知到移动端(任务完成/需要输入时)
|
||||
- 每日记忆日志 + `/dream` 知识蒸馏
|
||||
- 外部频道消息接入(Slack/Discord/Telegram)
|
||||
- **子 Feature**:
|
||||
|
||||
| 子 Feature | 引用 | 功能 |
|
||||
|-----------|------|------|
|
||||
| `KAIROS_BRIEF` | 39 | Brief 工具(`SendUserMessage`),结构化消息输出 |
|
||||
| `KAIROS_CHANNELS` | 19 | 外部频道消息接入 |
|
||||
| `KAIROS_PUSH_NOTIFICATION` | 4 | 移动端推送通知 |
|
||||
| `KAIROS_GITHUB_WEBHOOKS` | 3 | GitHub PR webhook 订阅 |
|
||||
| `KAIROS_DREAM` | 1 | 夜间记忆蒸馏 |
|
||||
|
||||
- **关键文件**:`src/assistant/`、`src/tools/BriefTool/`、`src/services/mcp/channelNotification.ts`、`src/memdir/memdir.ts`
|
||||
- **外部依赖**:Anthropic OAuth(claude.ai 订阅)、GrowthBook 特性门控
|
||||
- **探索命令**:`FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 FEATURE_PROACTIVE=1 bun run dev`
|
||||
|
||||
**探索步骤**:
|
||||
1. 开启 feature,观察启动行为变化
|
||||
2. 测试 `/assistant`、`/brief` 命令
|
||||
3. 验证 BriefTool 输出模式
|
||||
4. 尝试频道消息接入
|
||||
5. 测试 `/dream` 记忆蒸馏
|
||||
|
||||
---
|
||||
|
||||
### ~~2.2 TRANSCRIPT_CLASSIFIER(Auto Mode 分类器)~~ ✅ 已完成
|
||||
|
||||
- **引用数**:108
|
||||
- **功能**:使用 LLM 对用户意图进行分类,实现 auto mode(自动决定工具权限)
|
||||
- **状态**:✅ prompt 模板已重建,功能完整可用(2026-04-02 完成)
|
||||
|
||||
---
|
||||
|
||||
### 2.3 VOICE_MODE(语音输入)
|
||||
|
||||
- **引用数**:46
|
||||
- **功能**:按键说话(Push-to-Talk),音频流式传输到 Anthropic STT 端点(Nova 3),实时转录显示
|
||||
- **当前状态**:**完整实现**,包括录音、WebSocket 流、转录插入
|
||||
- **关键文件**:`src/voice/voiceModeEnabled.ts`、`src/hooks/useVoice.ts`、`src/services/voiceStreamSTT.ts`
|
||||
- **外部依赖**:Anthropic OAuth(非 API key)、macOS 原生音频或 SoX
|
||||
- **探索命令**:`FEATURE_VOICE_MODE=1 bun run dev`
|
||||
- **默认快捷键**:长按空格键录音
|
||||
|
||||
**探索步骤**:
|
||||
1. 确认 OAuth token 可用
|
||||
2. 测试按住空格录音 → 释放后转录
|
||||
3. 验证实时中间转录显示
|
||||
4. 测试 `/voice` 命令切换
|
||||
|
||||
---
|
||||
|
||||
### 2.4 TEAMMEM(团队共享记忆)
|
||||
|
||||
- **引用数**:51
|
||||
- **功能**:基于 GitHub 仓库的团队共享记忆系统,`memory/team/` 目录双向同步到 Anthropic 服务器
|
||||
- **当前状态**:**完整实现**,包括增量同步、冲突解决、密钥扫描、路径穿越防护
|
||||
- **关键文件**:`src/services/teamMemorySync/`(index、watcher、secretScanner)、`src/memdir/teamMemPaths.ts`
|
||||
- **外部依赖**:Anthropic OAuth + GitHub remote(`getGithubRepo()`)
|
||||
- **探索命令**:`FEATURE_TEAMMEM=1 bun run dev`
|
||||
|
||||
**探索步骤**:
|
||||
1. 确认项目有 GitHub remote
|
||||
2. 开启后观察 `memory/team/` 目录创建
|
||||
3. 测试团队记忆写入和同步
|
||||
4. 验证密钥扫描防护
|
||||
|
||||
---
|
||||
|
||||
### 2.5 COORDINATOR_MODE(多 Agent 编排)
|
||||
|
||||
- **引用数**:32
|
||||
- **功能**:CLI 变为编排者,通过 AgentTool 派发任务给多个 worker 并行执行
|
||||
- **当前状态**:核心逻辑实现,worker agent 模块为 stub
|
||||
- **关键文件**:`src/coordinator/coordinatorMode.ts`(系统 prompt 完整)、`src/coordinator/workerAgent.ts`(stub)
|
||||
- **限制**:编排者只能使用 AgentTool/TaskStop/SendMessage,不能直接操作文件
|
||||
- **探索命令**:`FEATURE_COORDINATOR_MODE=1 CLAUDE_CODE_COORDINATOR_MODE=1 bun run dev`
|
||||
|
||||
**探索步骤**:
|
||||
1. 补全 `workerAgent.ts` stub
|
||||
2. 测试多 worker 并行任务派发
|
||||
3. 验证 worker 结果汇总
|
||||
|
||||
---
|
||||
|
||||
### 2.6 BRIDGE_MODE(远程控制)
|
||||
|
||||
- **引用数**:28
|
||||
- **功能**:本地 CLI 注册为 bridge 环境,可从 claude.ai 或其他控制面远程驱动
|
||||
- **当前状态**:v1(env-based)和 v2(env-less)实现均存在
|
||||
- **关键文件**:`src/bridge/bridgeEnabled.ts`、`src/bridge/replBridge.ts`(v1)、`src/bridge/remoteBridgeCore.ts`(v2)
|
||||
- **外部依赖**:claude.ai OAuth、GrowthBook 门控 `tengu_ccr_bridge`
|
||||
- **探索命令**:`FEATURE_BRIDGE_MODE=1 bun run dev`
|
||||
|
||||
---
|
||||
|
||||
### 2.7 FORK_SUBAGENT(上下文继承子 Agent)
|
||||
|
||||
- **引用数**:4
|
||||
- **功能**:AgentTool 生成 fork 子 agent,继承父级完整对话上下文,优化 prompt cache
|
||||
- **当前状态**:**完整实现**(`forkSubagent.ts`),支持 worktree 隔离通知、递归防护
|
||||
- **关键文件**:`src/tools/AgentTool/forkSubagent.ts`
|
||||
- **探索命令**:`FEATURE_FORK_SUBAGENT=1 bun run dev`
|
||||
|
||||
---
|
||||
|
||||
### 2.8 TOKEN_BUDGET(Token 预算控制)
|
||||
|
||||
- **引用数**:9
|
||||
- **功能**:解析用户指定的 token 预算(如 "spend 2M tokens"),自动持续工作直到达到目标
|
||||
- **当前状态**:解析器**完整实现**,支持简写和详细语法;QueryEngine 中的周转逻辑已连接
|
||||
- **关键文件**:`src/utils/tokenBudget.ts`、`src/QueryEngine.ts`
|
||||
- **探索命令**:`FEATURE_TOKEN_BUDGET=1 bun run dev`
|
||||
|
||||
---
|
||||
|
||||
### 2.9 MCP_SKILLS(MCP 技能发现)
|
||||
|
||||
- **引用数**:9
|
||||
- **功能**:将 MCP 服务器提供的 prompt 类型命令筛选为可调用技能
|
||||
- **当前状态**:**功能性实现**(config 门控筛选器)
|
||||
- **关键文件**:`src/commands.ts`(`getMcpSkillCommands()`)
|
||||
- **探索命令**:`FEATURE_MCP_SKILLS=1 bun run dev`
|
||||
|
||||
---
|
||||
|
||||
### 2.10 TREE_SITTER_BASH(Bash AST 解析)
|
||||
|
||||
- **引用数**:3
|
||||
- **功能**:纯 TypeScript bash 命令 AST 解析器,用于 fail-closed 权限匹配
|
||||
- **当前状态**:**完整实现**(`bashParser.ts` ~2000行 + `ast.ts` ~400行)
|
||||
- **关键文件**:`src/utils/vendor/tree-sitter-bash/`
|
||||
- **探索命令**:`FEATURE_TREE_SITTER_BASH=1 bun run dev`
|
||||
|
||||
---
|
||||
|
||||
### ~~2.11 BUDDY(虚拟伙伴)~~ ✅ 已完成
|
||||
|
||||
- **引用数**:16
|
||||
- **功能**:`/buddy` 命令,支持 hatch/rehatch/pet/mute/unmute
|
||||
- **状态**:✅ 已合入,功能完整可用(2026-04-02 完成)
|
||||
|
||||
---
|
||||
|
||||
## 三、Tier 2 — 部分实现(需要补全)
|
||||
|
||||
### 3.1 PROACTIVE(主动模式)
|
||||
|
||||
- **引用数**:37
|
||||
- **功能**:Tick 驱动的自主代理,定时唤醒执行工作,配合 SleepTool 控制节奏
|
||||
- **当前状态**:核心模块 `src/proactive/index.ts` **全部 stub**(activate/deactivate/pause 返回 false 或空操作)
|
||||
- **依赖**:与 KAIROS 强绑定(所有检查都是 `feature('PROACTIVE') || feature('KAIROS')`)
|
||||
- **补全工作量**:中等 — 需要实现 tick 生成、SleepTool 集成、暂停/恢复逻辑
|
||||
|
||||
### 3.2 BASH_CLASSIFIER(Bash 命令分类器)
|
||||
|
||||
- **引用数**:45
|
||||
- **功能**:LLM 驱动的 bash 命令意图分类(允许/拒绝/询问)
|
||||
- **当前状态**:`bashClassifier.ts` **全部 stub**(`matches: false`)
|
||||
- **补全工作量**:大 — 需要 LLM 调用实现、prompt 设计
|
||||
|
||||
### 3.3 ULTRAPLAN(增强规划)
|
||||
|
||||
- **引用数**:10
|
||||
- **功能**:关键字触发增强计划模式,输入 "ultraplan" 自动转为 plan
|
||||
- **当前状态**:关键字检测**完整实现**,`/ultraplan` 命令**为 stub**
|
||||
- **补全工作量**:小 — 只需实现命令处理逻辑
|
||||
|
||||
### 3.4 EXPERIMENTAL_SKILL_SEARCH(技能语义搜索)
|
||||
|
||||
- **引用数**:21
|
||||
- **功能**:DiscoverSkills 工具,根据当前任务语义搜索可用技能
|
||||
- **当前状态**:布线完整,核心搜索逻辑 stub
|
||||
- **补全工作量**:中等 — 需要实现搜索引擎和索引
|
||||
|
||||
### 3.5 CONTEXT_COLLAPSE(上下文折叠)
|
||||
|
||||
- **引用数**:20
|
||||
- **功能**:CtxInspectTool 让模型内省上下文窗口大小,优化压缩决策
|
||||
- **当前状态**:工具 stub,HISTORY_SNIP 子功能也 stub
|
||||
- **补全工作量**:中等
|
||||
|
||||
### 3.6 WORKFLOW_SCRIPTS(工作流自动化)
|
||||
|
||||
- **引用数**:10
|
||||
- **功能**:基于文件的自动化工作流 + `/workflows` 命令
|
||||
- **当前状态**:WorkflowTool、命令、加载器全部 stub
|
||||
- **补全工作量**:大 — 需要从零设计工作流 DSL
|
||||
|
||||
### 3.7 WEB_BROWSER_TOOL(浏览器工具)
|
||||
|
||||
- **引用数**:4
|
||||
- **功能**:模型可调用浏览器工具导航和交互网页
|
||||
- **当前状态**:工具注册存在,实现 stub
|
||||
- **补全工作量**:大
|
||||
|
||||
### 3.8 DAEMON(后台守护进程)
|
||||
|
||||
- **引用数**:3
|
||||
- **功能**:后台守护进程 + 远程控制服务器
|
||||
- **当前状态**:只有条件导入布线,无实现
|
||||
- **补全工作量**:极大
|
||||
|
||||
---
|
||||
|
||||
## 四、Tier 3 — 纯 Stub / N/A(低优先级)
|
||||
|
||||
| Feature | 引用 | 状态 | 说明 |
|
||||
|---------|------|------|------|
|
||||
| CHICAGO_MCP | 16 | N/A | Anthropic 内部 MCP 基础设施 |
|
||||
| UDS_INBOX | 17 | Stub | Unix 域套接字对等消息 |
|
||||
| MONITOR_TOOL | 13 | Stub | 文件/进程监控工具 |
|
||||
| BG_SESSIONS | 11 | Stub | 后台会话管理 |
|
||||
| SHOT_STATS | 10 | 无实现 | 逐 prompt 统计 |
|
||||
| EXTRACT_MEMORIES | 7 | 无实现 | 自动记忆提取 |
|
||||
| TEMPLATES | 6 | Stub | 项目/提示模板 |
|
||||
| LODESTONE | 6 | N/A | 内部基础设施 |
|
||||
| STREAMLINED_OUTPUT | 1 | — | 精简输出模式 |
|
||||
| HOOK_PROMPTS | 1 | — | Hook 提示词 |
|
||||
| CCR_AUTO_CONNECT | 3 | — | CCR 自动连接 |
|
||||
| CCR_MIRROR | 4 | — | CCR 镜像模式 |
|
||||
| CCR_REMOTE_SETUP | 1 | — | CCR 远程设置 |
|
||||
| NATIVE_CLIPBOARD_IMAGE | 2 | — | 原生剪贴板图片 |
|
||||
| CONNECTOR_TEXT | 7 | — | 连接器文本 |
|
||||
|
||||
以及其余 40+ 个低引用量 feature。
|
||||
|
||||
---
|
||||
|
||||
## 五、探索路线图
|
||||
|
||||
### Phase 1:快速验证(无外部依赖)
|
||||
|
||||
> 目标:确认代码可以正常运行,体验基本功能
|
||||
|
||||
| 优先级 | Feature | 命令 | 预期效果 |
|
||||
|--------|---------|------|----------|
|
||||
| 1 | BUDDY | `FEATURE_BUDDY=1 bun run dev` | `/buddy hatch` 生成伙伴 |
|
||||
| 2 | FORK_SUBAGENT | `FEATURE_FORK_SUBAGENT=1 bun run dev` | Agent 可生成上下文继承的子任务 |
|
||||
| 3 | TOKEN_BUDGET | `FEATURE_TOKEN_BUDGET=1 bun run dev` | 输入 "spend 500k tokens" 测试自动持续 |
|
||||
| 4 | TREE_SITTER_BASH | `FEATURE_TREE_SITTER_BASH=1 bun run dev` | 更精确的 bash 权限匹配 |
|
||||
| 5 | MCP_SKILLS | `FEATURE_MCP_SKILLS=1 bun run dev` | MCP 服务器 prompt 提升为技能 |
|
||||
|
||||
### Phase 2:核心功能探索(需要 OAuth)
|
||||
|
||||
> 目标:体验 KAIROS 全套能力
|
||||
|
||||
| 优先级 | Feature | 命令 | 预期效果 |
|
||||
|--------|---------|------|----------|
|
||||
| 1 | TRANSCRIPT_CLASSIFIER | `FEATURE_TRANSCRIPT_CLASSIFIER=1 bun run dev` | Auto mode 自动激活 |
|
||||
| 2 | KAIROS 全套 | `FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 FEATURE_KAIROS_CHANNELS=1 FEATURE_PROACTIVE=1 bun run dev` | 常驻助手 + Brief 输出 + 频道消息 |
|
||||
| 3 | VOICE_MODE | `FEATURE_VOICE_MODE=1 bun run dev` | 按空格说话 |
|
||||
| 4 | TEAMMEM | `FEATURE_TEAMMEM=1 bun run dev` | 团队记忆同步 |
|
||||
| 5 | COORDINATOR_MODE | `FEATURE_COORDINATOR_MODE=1 CLAUDE_CODE_COORDINATOR_MODE=1 bun run dev` | 多 agent 编排 |
|
||||
|
||||
### Phase 3:Stub 补全开发
|
||||
|
||||
> 目标:将高价值 stub 实现为可用功能
|
||||
|
||||
| 优先级 | Feature | 补全难度 | 价值 |
|
||||
|--------|---------|----------|------|
|
||||
| 1 | PROACTIVE | 中 | 自主工作能力 |
|
||||
| 2 | ULTRAPLAN | 小 | 增强规划 |
|
||||
| 3 | CONTEXT_COLLAPSE | 中 | 长对话优化 |
|
||||
| 4 | EXPERIMENTAL_SKILL_SEARCH | 中 | 技能发现 |
|
||||
| 5 | BASH_CLASSIFIER | 大 | 安全增强 |
|
||||
|
||||
---
|
||||
|
||||
## 六、推荐组合方案
|
||||
|
||||
### "全功能助手"组合
|
||||
|
||||
```bash
|
||||
FEATURE_KAIROS=1 \
|
||||
FEATURE_KAIROS_BRIEF=1 \
|
||||
FEATURE_KAIROS_CHANNELS=1 \
|
||||
FEATURE_KAIROS_PUSH_NOTIFICATION=1 \
|
||||
FEATURE_PROACTIVE=1 \
|
||||
FEATURE_FORK_SUBAGENT=1 \
|
||||
FEATURE_TOKEN_BUDGET=1 \
|
||||
FEATURE_TRANSCRIPT_CLASSIFIER=1 \
|
||||
FEATURE_BUDDY=1 \
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### "多 Agent 协作"组合
|
||||
|
||||
```bash
|
||||
FEATURE_COORDINATOR_MODE=1 \
|
||||
FEATURE_FORK_SUBAGENT=1 \
|
||||
FEATURE_BRIDGE_MODE=1 \
|
||||
FEATURE_BG_SESSIONS=1 \
|
||||
CLAUDE_CODE_COORDINATOR_MODE=1 \
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### "开发者增强"组合
|
||||
|
||||
```bash
|
||||
FEATURE_TRANSCRIPT_CLASSIFIER=1 \
|
||||
FEATURE_TREE_SITTER_BASH=1 \
|
||||
FEATURE_TOKEN_BUDGET=1 \
|
||||
FEATURE_MCP_SKILLS=1 \
|
||||
FEATURE_CONTEXT_COLLAPSE=1 \
|
||||
bun run dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、风险与注意事项
|
||||
|
||||
1. **OAuth 依赖**:KAIROS、VOICE_MODE、TEAMMEM、BRIDGE_MODE 需要 Anthropic OAuth 认证(claude.ai 订阅),API key 用户无法使用
|
||||
2. **GrowthBook 门控**:部分功能(VOICE_MODE 的 `tengu_cobalt_frost`、TEAMMEM 的 `tengu_herring_clock`)即使 feature flag 开启,还需要服务端 GrowthBook 开关
|
||||
3. **反编译不完整**:所有"已实现"功能均为反编译产物,可能存在运行时错误,需要逐个验证
|
||||
4. **Proactive stub**:KAIROS 的自主工作能力依赖 PROACTIVE,但 PROACTIVE 核心是 stub,需先补全
|
||||
5. **tsc 错误**:代码库有 ~1341 个 TypeScript 编译错误(来自反编译),不影响 Bun 运行时但在 IDE 中会有大量红线
|
||||
|
||||
---
|
||||
|
||||
## 附录:Feature Flag 完整列表
|
||||
|
||||
共 89 个 feature flag(按引用数降序):
|
||||
|
||||
| Feature | 引用 | Tier |
|
||||
|---------|------|------|
|
||||
| KAIROS | 154 | 1 |
|
||||
| TRANSCRIPT_CLASSIFIER | 108 | 1 |
|
||||
| TEAMMEM | 51 | 1 |
|
||||
| VOICE_MODE | 46 | 1 |
|
||||
| BASH_CLASSIFIER | 45 | 2 |
|
||||
| KAIROS_BRIEF | 39 | 1 |
|
||||
| PROACTIVE | 37 | 2 |
|
||||
| COORDINATOR_MODE | 32 | 1 |
|
||||
| BRIDGE_MODE | 28 | 1 |
|
||||
| EXPERIMENTAL_SKILL_SEARCH | 21 | 2 |
|
||||
| CONTEXT_COLLAPSE | 20 | 2 |
|
||||
| KAIROS_CHANNELS | 19 | 1 |
|
||||
| UDS_INBOX | 17 | 3 |
|
||||
| CHICAGO_MCP | 16 | 3 |
|
||||
| BUDDY | 16 | 1 |
|
||||
| HISTORY_SNIP | 15 | 2 |
|
||||
| MONITOR_TOOL | 13 | 3 |
|
||||
| COMMIT_ATTRIBUTION | 12 | — |
|
||||
| CACHED_MICROCOMPACT | 12 | — |
|
||||
| BG_SESSIONS | 11 | 3 |
|
||||
| WORKFLOW_SCRIPTS | 10 | 2 |
|
||||
| ULTRAPLAN | 10 | 2 |
|
||||
| SHOT_STATS | 10 | 3 |
|
||||
| TOKEN_BUDGET | 9 | 1 |
|
||||
| PROMPT_CACHE_BREAK_DETECTION | 9 | — |
|
||||
| MCP_SKILLS | 9 | 1 |
|
||||
| EXTRACT_MEMORIES | 7 | 3 |
|
||||
| CONNECTOR_TEXT | 7 | — |
|
||||
| TEMPLATES | 6 | 3 |
|
||||
| LODESTONE | 6 | 3 |
|
||||
| TREE_SITTER_BASH_SHADOW | 5 | — |
|
||||
| QUICK_SEARCH | 5 | — |
|
||||
| MESSAGE_ACTIONS | 5 | — |
|
||||
| DOWNLOAD_USER_SETTINGS | 5 | — |
|
||||
| DIRECT_CONNECT | 5 | — |
|
||||
| WEB_BROWSER_TOOL | 4 | 2 |
|
||||
| VERIFICATION_AGENT | 4 | — |
|
||||
| TERMINAL_PANEL | 4 | — |
|
||||
| SSH_REMOTE | 4 | — |
|
||||
| REVIEW_ARTIFACT | 4 | — |
|
||||
| REACTIVE_COMPACT | 4 | — |
|
||||
| KAIROS_PUSH_NOTIFICATION | 4 | 1 |
|
||||
| HISTORY_PICKER | 4 | — |
|
||||
| FORK_SUBAGENT | 4 | 1 |
|
||||
| CCR_MIRROR | 4 | — |
|
||||
| TREE_SITTER_BASH | 3 | 1 |
|
||||
| MEMORY_SHAPE_TELEMETRY | 3 | — |
|
||||
| MCP_RICH_OUTPUT | 3 | — |
|
||||
| KAIROS_GITHUB_WEBHOOKS | 3 | 1 |
|
||||
| FILE_PERSISTENCE | 3 | — |
|
||||
| DAEMON | 3 | 2 |
|
||||
| CCR_AUTO_CONNECT | 3 | — |
|
||||
| UPLOAD_USER_SETTINGS | 2 | — |
|
||||
| POWERSHELL_AUTO_MODE | 2 | — |
|
||||
| OVERFLOW_TEST_TOOL | 2 | — |
|
||||
| NEW_INIT | 2 | — |
|
||||
| NATIVE_CLIPBOARD_IMAGE | 2 | — |
|
||||
| HARD_FAIL | 2 | — |
|
||||
| ENHANCED_TELEMETRY_BETA | 2 | — |
|
||||
| COWORKER_TYPE_TELEMETRY | 2 | — |
|
||||
| BREAK_CACHE_COMMAND | 2 | — |
|
||||
| AWAY_SUMMARY | 2 | — |
|
||||
| AUTO_THEME | 2 | — |
|
||||
| ALLOW_TEST_VERSIONS | 2 | — |
|
||||
| AGENT_TRIGGERS_REMOTE | 2 | — |
|
||||
| AGENT_MEMORY_SNAPSHOT | 2 | — |
|
||||
| UNATTENDED_RETRY | 1 | — |
|
||||
| ULTRATHINK | 1 | — |
|
||||
| TORCH | 1 | — |
|
||||
| STREAMLINED_OUTPUT | 1 | — |
|
||||
| SLOW_OPERATION_LOGGING | 1 | — |
|
||||
| SKILL_IMPROVEMENT | 1 | — |
|
||||
| SELF_HOSTED_RUNNER | 1 | — |
|
||||
| RUN_SKILL_GENERATOR | 1 | — |
|
||||
| PERFETTO_TRACING | 1 | — |
|
||||
| NATIVE_CLIENT_ATTESTATION | 1 | — |
|
||||
| KAIROS_DREAM | 1 | 1 |
|
||||
| IS_LIBC_MUSL | 1 | — |
|
||||
| IS_LIBC_GLIBC | 1 | — |
|
||||
| HOOK_PROMPTS | 1 | — |
|
||||
| DUMP_SYSTEM_PROMPT | 1 | — |
|
||||
| COMPACTION_REMINDERS | 1 | — |
|
||||
| CCR_REMOTE_SETUP | 1 | — |
|
||||
| BYOC_ENVIRONMENT_RUNNER | 1 | — |
|
||||
| BUILTIN_EXPLORE_PLAN_AGENTS | 1 | — |
|
||||
| BUILDING_CLAUDE_APPS | 1 | — |
|
||||
| ANTI_DISTILLATION_CC | 1 | — |
|
||||
| AGENT_TRIGGERS | 1 | — |
|
||||
| ABLATION_BASELINE | 1 | — |
|
||||
@@ -1,107 +0,0 @@
|
||||
# BASH_CLASSIFIER — Bash 命令分类器
|
||||
|
||||
> Feature Flag: `FEATURE_BASH_CLASSIFIER=1`
|
||||
> 实现状态:bashClassifier.ts 全部 Stub,yoloClassifier.ts 完整实现可参考
|
||||
> 引用数:45
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
BASH_CLASSIFIER 使用 LLM 对 bash 命令进行意图分类(允许/拒绝/询问),实现自动权限决策。用户不需要逐个审批 bash 命令,分类器根据命令内容和上下文自动判断安全性。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **LLM 驱动分类**:使用 Opus 模型评估命令安全性
|
||||
- **两阶段分类**:快速阻止/允许 → 深度思考链
|
||||
- **自动审批**:分类器判定安全的命令自动通过
|
||||
- **UI 集成**:权限对话框显示分类器状态和审核选项
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
### 2.1 模块状态
|
||||
|
||||
| 模块 | 文件 | 状态 | 说明 |
|
||||
|------|------|------|------|
|
||||
| Bash 分类器 | `src/utils/permissions/bashClassifier.ts` | **Stub** | 所有函数返回空操作。注释:"ANT-ONLY" |
|
||||
| YOLO 分类器 | `src/utils/permissions/yoloClassifier.ts` | **完整** | 1496 行,两阶段 XML 分类器 |
|
||||
| 审批信号 | `src/utils/classifierApprovals.ts` | **完整** | Map + 信号管理分类器决策 |
|
||||
| 权限 UI | `src/components/permissions/BashPermissionRequest.tsx` | **布线** | 分类器状态显示、审核选项 |
|
||||
| 权限管道 | `src/hooks/toolPermission/handlers/*.ts` | **布线** | 分类器结果路由到决策 |
|
||||
| API beta 标头 | `src/services/api/withRetry.ts` | **布线** | 启用时发送 `bash_classifier` beta |
|
||||
|
||||
### 2.2 参考实现:yoloClassifier.ts
|
||||
|
||||
文件:`src/utils/permissions/yoloClassifier.ts`(1496 行)
|
||||
|
||||
这是已实现的完整分类器,可作为 bashClassifier.ts 的参考:
|
||||
|
||||
```
|
||||
两阶段分类:
|
||||
1. 快速阶段:构建对话记录 → 调用 sideQuery(Opus)→ 快速阻止/允许
|
||||
2. 深度阶段:思考链分析 → 最终决策
|
||||
```
|
||||
|
||||
特性:
|
||||
- 构建完整对话记录上下文
|
||||
- 调用安全系统提示的 sideQuery
|
||||
- GrowthBook 配置和指标
|
||||
- 错误处理和降级
|
||||
|
||||
### 2.3 分类器在权限管道中的位置
|
||||
|
||||
```
|
||||
bash 命令到达
|
||||
│
|
||||
▼
|
||||
bashPermissions.ts 权限检查
|
||||
│
|
||||
├── 传统规则匹配(字符串级别)
|
||||
│
|
||||
└── [BASH_CLASSIFIER] LLM 分类
|
||||
│
|
||||
├── allow → 自动通过
|
||||
├── deny → 自动拒绝
|
||||
└── ask → 显示权限对话框
|
||||
│
|
||||
├── 分类器自动审批标记
|
||||
└── 审核选项(用户可覆盖)
|
||||
```
|
||||
|
||||
## 三、需要补全的内容
|
||||
|
||||
| 函数 | 需要实现 | 说明 |
|
||||
|------|---------|------|
|
||||
| `classifyBashCommand()` | LLM 调用评估安全性 | 参考 yoloClassifier.ts 的两阶段模式 |
|
||||
| `isClassifierPermissionsEnabled()` | GrowthBook/配置检查 | 控制分类器是否激活 |
|
||||
| `getBashPromptDenyDescriptions()` | 返回基于提示的拒绝规则 | 权限设置描述 |
|
||||
| `getBashPromptAskDescriptions()` | 返回询问规则 | 需要用户确认的命令 |
|
||||
| `getBashPromptAllowDescriptions()` | 返回允许规则 | 自动通过的命令 |
|
||||
| `generateGenericDescription()` | LLM 生成命令描述 | 为权限对话框提供说明 |
|
||||
| `extractPromptDescription()` | 解析规则内容 | 从规则中提取描述 |
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **ANT-ONLY 标记**:bashClassifier.ts 标注为 "ANT-ONLY",可能是 Anthropic 内部服务端分类器的客户端适配
|
||||
2. **两阶段分类**:快速阶段处理明确情况(减少延迟),深度阶段处理模糊情况
|
||||
3. **分类器结果可审核**:权限 UI 显示分类器决策,用户可覆盖
|
||||
4. **YOLO 分类器参考**:yoloClassifier.ts 提供完整的分类器实现模式,可直接参考
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 启用 feature
|
||||
FEATURE_BASH_CLASSIFIER=1 bun run dev
|
||||
|
||||
# 配合 TREE_SITTER_BASH 使用(AST + LLM 双重安全)
|
||||
FEATURE_BASH_CLASSIFIER=1 FEATURE_TREE_SITTER_BASH=1 bun run dev
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/utils/permissions/bashClassifier.ts` | — | Bash 分类器(stub,ANT-ONLY) |
|
||||
| `src/utils/permissions/yoloClassifier.ts` | 1496 | YOLO 分类器(完整参考实现) |
|
||||
| `src/utils/classifierApprovals.ts` | — | 分类器审批信号管理 |
|
||||
| `src/components/permissions/BashPermissionRequest.tsx:261-469` | — | 分类器 UI |
|
||||
| `src/hooks/toolPermission/handlers/interactiveHandler.ts` | — | 交互式权限处理 |
|
||||
| `src/services/api/withRetry.ts:81` | — | API beta 标头 |
|
||||
@@ -1,158 +0,0 @@
|
||||
# BRIDGE_MODE — 远程控制
|
||||
|
||||
> Feature Flag: `FEATURE_BRIDGE_MODE=1`
|
||||
> 实现状态:完整可用(v1 + v2 实现)
|
||||
> 引用数:28
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
BRIDGE_MODE 将本地 CLI 注册为"bridge 环境",可从 claude.ai 或其他控制面远程驱动。本地终端变为一个"执行者",接受远程指令并执行。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **环境注册**:本地 CLI 向 Anthropic 服务器注册为可用的 bridge 环境
|
||||
- **工作轮询**:长轮询(long-poll)等待远程任务分配
|
||||
- **会话管理**:创建、恢复、归档远程会话
|
||||
- **权限透传**:远程权限请求发送到控制面,用户在 claude.ai 上批准/拒绝
|
||||
- **心跳保活**:定期发送 heartbeat 延长任务租约
|
||||
- **可信设备**:v2 支持可信设备令牌增强安全性
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
### 2.1 版本演进
|
||||
|
||||
| 版本 | 实现 | 特点 |
|
||||
|------|------|------|
|
||||
| v1(env-based) | `src/bridge/replBridge.ts` | 基于环境变量的传统 bridge |
|
||||
| v2(env-less) | `src/bridge/remoteBridgeCore.ts` | 无需环境变量,更安全的 bridge |
|
||||
|
||||
### 2.2 API 协议
|
||||
|
||||
文件:`src/bridge/bridgeApi.ts`
|
||||
|
||||
Bridge API Client 提供 7 个核心操作:
|
||||
|
||||
| 操作 | HTTP | 说明 |
|
||||
|------|------|------|
|
||||
| `registerBridgeEnvironment` | POST `/v1/environments/bridge` | 注册本地环境,获取 `environment_id` + `environment_secret` |
|
||||
| `pollForWork` | GET `/v1/environments/{id}/work/poll` | 长轮询等待任务(10s 超时) |
|
||||
| `acknowledgeWork` | POST `/v1/environments/{id}/work/{workId}/ack` | 确认接收任务 |
|
||||
| `stopWork` | POST `/v1/environments/{id}/work/{workId}/stop` | 停止任务 |
|
||||
| `heartbeatWork` | POST `/v1/environments/{id}/work/{workId}/heartbeat` | 续约任务租约 |
|
||||
| `deregisterEnvironment` | DELETE `/v1/environments/bridge/{id}` | 注销环境 |
|
||||
| `archiveSession` | POST `/v1/sessions/{id}/archive` | 归档会话(409 = 已归档,幂等) |
|
||||
| `sendPermissionResponseEvent` | POST `/v1/sessions/{id}/events` | 发送权限审批结果 |
|
||||
| `reconnectSession` | POST `/v1/environments/{id}/bridge/reconnect` | 重连已存在的会话 |
|
||||
|
||||
### 2.3 认证流程
|
||||
|
||||
```
|
||||
注册: OAuth Bearer Token → 获取 environment_secret
|
||||
轮询: environment_secret 作为 Authorization
|
||||
├── 401 → 尝试 OAuth token 刷新(onAuth401)
|
||||
└── 刷新成功 → 重试一次
|
||||
```
|
||||
|
||||
**OAuth 刷新**:API client 内置 `withOAuthRetry` 机制。401 时调用 `handleOAuth401Error`(同 withRetry.ts 的 v1/messages 模式),刷新后重试一次。
|
||||
|
||||
### 2.4 安全设计
|
||||
|
||||
- **路径穿越防护**:`validateBridgeId()` 使用 `/^[a-zA-Z0-9_-]+$/` 白名单验证所有服务端 ID
|
||||
- **BridgeFatalError**:不可重试的错误(401/403/404/410)直接抛出,阻止重试循环
|
||||
- **可信设备令牌**:v2 通过 `X-Trusted-Device-Token` header 增强安全层级
|
||||
- **幂关注册**:支持 `reuseEnvironmentId` 实现会话恢复,避免重复创建环境
|
||||
|
||||
### 2.5 数据流
|
||||
|
||||
```
|
||||
claude.ai 用户选择远程环境
|
||||
│
|
||||
▼
|
||||
POST /v1/environments/bridge (注册)
|
||||
│
|
||||
◀── environment_id + environment_secret
|
||||
│
|
||||
▼
|
||||
GET .../work/poll (长轮询)
|
||||
│
|
||||
◀── WorkResponse { id, data: { type, sessionId } }
|
||||
│
|
||||
▼
|
||||
POST .../work/{id}/ack (确认)
|
||||
│
|
||||
▼
|
||||
sessionRunner 创建 REPL session
|
||||
│
|
||||
├── 权限请求 → sendPermissionResponseEvent
|
||||
├── 心跳 → heartbeatWork (续约)
|
||||
└── 任务完成 → 自动归档
|
||||
```
|
||||
|
||||
### 2.6 模块结构
|
||||
|
||||
| 模块 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| API Client | `bridgeApi.ts` | HTTP 通信(注册/轮询/确认/心跳/注销) |
|
||||
| Session Runner | `sessionRunner.ts` | 创建/恢复 REPL 会话 |
|
||||
| Bridge Config | `bridgeConfig.ts` | 配置管理(machine name、max sessions 等) |
|
||||
| Transport | `replBridgeTransport.ts` | Bridge 传输层 |
|
||||
| Permission Callbacks | `bridgePermissionCallbacks.ts` | 权限请求处理 |
|
||||
| Pointer | `bridgePointer.ts` | 当前活跃 bridge 状态指针 |
|
||||
| Flush Gate | `flushGate.ts` | 刷新控制 |
|
||||
| JWT Utils | `jwtUtils.ts` | JWT 令牌工具 |
|
||||
| Trusted Device | `trustedDevice.ts` | 可信设备管理 |
|
||||
| Debug Utils | `debugUtils.ts` | 调试日志 |
|
||||
| Types | `types.ts` | 类型定义 |
|
||||
|
||||
## 三、关键设计决策
|
||||
|
||||
1. **长轮询而非 WebSocket**:`pollForWork` 使用 HTTP GET + 10s 超时。简单可靠,无需维护 WebSocket 连接
|
||||
2. **OAuth 刷新内嵌**:API client 自带 `withOAuthRetry`,无需外层重试逻辑
|
||||
3. **ETag 条件请求**:注册时支持 `reuseEnvironmentId` 实现幂等会话恢复
|
||||
4. **v1/v2 共存**:代码中同时存在两套实现,v2 是更安全的升级版
|
||||
5. **权限双向流动**:本地权限请求发送到 claude.ai,用户在 web 上审批
|
||||
|
||||
## 四、使用方式
|
||||
|
||||
```bash
|
||||
# 启用 bridge mode
|
||||
FEATURE_BRIDGE_MODE=1 bun run dev
|
||||
|
||||
# 从 claude.ai/code 远程连接
|
||||
# 在 web 界面选择已注册的环境
|
||||
|
||||
# 配合 DAEMON 使用(后台守护)
|
||||
FEATURE_BRIDGE_MODE=1 FEATURE_DAEMON=1 bun run dev
|
||||
```
|
||||
|
||||
## 五、外部依赖
|
||||
|
||||
| 依赖 | 说明 |
|
||||
|------|------|
|
||||
| Anthropic OAuth | claude.ai 订阅登录 |
|
||||
| GrowthBook | `tengu_ccr_bridge` 门控 |
|
||||
| Bridge API | `/v1/environments/bridge` 系列端点 |
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/bridge/bridgeApi.ts` | 540 | API Client(核心) |
|
||||
| `src/bridge/sessionRunner.ts` | — | 会话运行器 |
|
||||
| `src/bridge/bridgeConfig.ts` | — | 配置管理 |
|
||||
| `src/bridge/replBridgeTransport.ts` | — | 传输层 |
|
||||
| `src/bridge/bridgePermissionCallbacks.ts` | — | 权限回调 |
|
||||
| `src/bridge/bridgePointer.ts` | — | 状态指针 |
|
||||
| `src/bridge/flushGate.ts` | — | 刷新控制 |
|
||||
| `src/bridge/jwtUtils.ts` | — | JWT 工具 |
|
||||
| `src/bridge/trustedDevice.ts` | — | 可信设备 |
|
||||
| `src/bridge/remoteBridgeCore.ts` | — | v2 核心实现 |
|
||||
| `src/bridge/types.ts` | — | 类型定义 |
|
||||
| `src/bridge/debugUtils.ts` | — | 调试工具 |
|
||||
| `src/bridge/pollConfigDefaults.ts` | — | 轮询配置默认值 |
|
||||
| `src/bridge/bridgeUI.ts` | — | UI 组件 |
|
||||
| `src/bridge/codeSessionApi.ts` | — | 代码会话 API |
|
||||
| `src/bridge/peerSessions.ts` | — | 对等会话管理 |
|
||||
| `src/bridge/sessionIdCompat.ts` | — | Session ID 兼容层 |
|
||||
| `src/bridge/createSession.ts` | — | 会话创建 |
|
||||
| `src/bridge/replBridgeHandle.ts` | — | Bridge 句柄 |
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
title: "Buddy 宠物系统"
|
||||
description: "Buddy 是 CLI 中的虚拟宠物伴侣,通过 /buddy 命令孵化、互动,会出现在输入框旁边陪伴你写代码。"
|
||||
keywords: ["buddy", "宠物", "companion", "伴侣", "虚拟宠物"]
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
Buddy 是 Claude Code 内置的虚拟宠物系统。在 REPL 中通过 `/buddy` 命令可以孵化一只随机生成的宠物伴侣,它会出现在输入框旁边,陪伴你的编码过程。
|
||||
|
||||
> Feature Flag: `FEATURE_BUDDY=1`
|
||||
|
||||
## 启用方式
|
||||
|
||||
```bash
|
||||
FEATURE_BUDDY=1 bun run dev
|
||||
```
|
||||
|
||||
孵化窗口:2026 年 4 月 1-7 日期间启动时,会在 REPL 顶部显示彩虹色的 `/buddy` 提示。4 月 7 日之后命令仍然可用,但不再自动提示。
|
||||
|
||||
## 命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|---|---|
|
||||
| `/buddy` | 查看当前宠物信息和属性 |
|
||||
| `/buddy hatch` | 孵化一只新宠物(首次使用) |
|
||||
| `/buddy rehatch` | 重新随机生成宠物(替换现有) |
|
||||
| `/buddy pet` | 撸宠物,触发爱心动画 |
|
||||
| `/buddy mute` | 静音宠物(隐藏) |
|
||||
| `/buddy unmute` | 取消静音 |
|
||||
|
||||
## 宠物属性
|
||||
|
||||
### 物种(18 种)
|
||||
|
||||
| | | | |
|
||||
|---|---|---|---|
|
||||
| Duck | Goose | Blob | Cat |
|
||||
| Dragon | Octopus | Owl | Penguin |
|
||||
| Turtle | Snail | Ghost | Axolotl |
|
||||
| Capybara | Cactus | Robot | Rabbit |
|
||||
| Mushroom | Chonk | | |
|
||||
|
||||
### 稀有度
|
||||
|
||||
| 稀有度 | 星级 | 权重 |
|
||||
|---|---|---|
|
||||
| Common | ★ | 60% |
|
||||
| Uncommon | ★★ | 25% |
|
||||
| Rare | ★★★ | 10% |
|
||||
| Epic | ★★★★ | 4% |
|
||||
| Legendary | ★★★★★ | 1% |
|
||||
|
||||
孵化时基于种子随机决定,存在极低概率出现 Shiny(闪光)变体。
|
||||
|
||||
### 属性值
|
||||
|
||||
每只宠物拥有 5 项属性(0-100):
|
||||
|
||||
- **DEBUGGING** — 调试能力
|
||||
- **PATIENCE** — 耐心程度
|
||||
- **CHAOS** — 混乱指数
|
||||
- **WISDOM** — 智慧值
|
||||
- **SNARK** — 毒舌度
|
||||
|
||||
### 外观
|
||||
|
||||
每只宠物还有随机的外观配件:
|
||||
|
||||
- **眼睛**: `·` `✦` `×` `◉` `@` `°`
|
||||
- **帽子**: none, crown, tophat, propeller, halo, wizard, beanie, tinyduck
|
||||
|
||||
## 数据存储
|
||||
|
||||
宠物信息存储在 `~/.claude.json` 的 `companion` 字段中。宠物的外观属性(物种、稀有度、属性值等)基于用户 ID 的哈希确定性生成,不可通过编辑配置文件来篡改稀有度。
|
||||
|
||||
## 相关源码
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `src/commands/buddy/buddy.ts` | `/buddy` 命令处理 |
|
||||
| `src/buddy/companion.ts` | 宠物生成与加载 |
|
||||
| `src/buddy/types.ts` | 类型定义(物种、稀有度、属性) |
|
||||
| `src/buddy/sprites.ts` | 终端像素画渲染 |
|
||||
| `src/buddy/CompanionSprite.tsx` | React 组件(输入框旁显示) |
|
||||
| `src/buddy/useBuddyNotification.tsx` | 启动提示通知 |
|
||||
| `src/buddy/prompt.ts` | 宠物相关 prompt 模板 |
|
||||
@@ -1,140 +0,0 @@
|
||||
# CONTEXT_COLLAPSE — 上下文折叠
|
||||
|
||||
> Feature Flag: `FEATURE_CONTEXT_COLLAPSE=1`
|
||||
> 子 Feature: `FEATURE_HISTORY_SNIP=1`
|
||||
> 实现状态:核心逻辑全部 Stub,布线完整
|
||||
> 引用数:CONTEXT_COLLAPSE 20 + HISTORY_SNIP 16 = 36
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
CONTEXT_COLLAPSE 让模型内省上下文窗口使用情况,并智能压缩旧消息。当对话接近上下文限制时,自动将旧消息折叠为压缩摘要,保留关键信息的同时释放 token 空间。
|
||||
|
||||
### 子 Feature
|
||||
|
||||
| Feature | 功能 |
|
||||
|---------|------|
|
||||
| `CONTEXT_COLLAPSE` | 上下文折叠引擎(后台 LLM 调用压缩旧消息) |
|
||||
| `HISTORY_SNIP` | SnipTool — 标记消息进行折叠/修剪 |
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
### 2.1 模块状态
|
||||
|
||||
| 模块 | 文件 | 状态 |
|
||||
|------|------|------|
|
||||
| 折叠核心 | `src/services/contextCollapse/index.ts` | **Stub** — 接口完整(`ContextCollapseStats`、`CollapseResult`、`DrainResult`),函数全部空操作 |
|
||||
| 折叠操作 | `src/services/contextCollapse/operations.ts` | **Stub** — `projectView` 为恒等函数 |
|
||||
| 折叠持久化 | `src/services/contextCollapse/persist.ts` | **Stub** — `restoreFromEntries` 为空操作 |
|
||||
| CtxInspectTool | `src/tools/CtxInspectTool/` | **缺失** — 目录不存在 |
|
||||
| SnipTool 提示 | `src/tools/SnipTool/prompt.ts` | **Stub** — 空工具名 |
|
||||
| SnipTool 实现 | `src/tools/SnipTool/SnipTool.ts` | **缺失** |
|
||||
| force-snip 命令 | `src/commands/force-snip.js` | **缺失** |
|
||||
| 折叠读取搜索 | `src/utils/collapseReadSearch.ts` | **完整** — Snip 作为静默吸收操作 |
|
||||
| QueryEngine 集成 | `src/QueryEngine.ts` | **布线** — 导入并使用 snip 投影 |
|
||||
| Token 警告 UI | `src/components/TokenWarning.tsx` | **布线** — 折叠进度标签 |
|
||||
|
||||
### 2.2 核心接口(已定义,待实现)
|
||||
|
||||
```ts
|
||||
// contextCollapse/index.ts
|
||||
interface ContextCollapseStats {
|
||||
// 上下文使用统计
|
||||
}
|
||||
interface CollapseResult {
|
||||
// 折叠操作结果
|
||||
}
|
||||
interface DrainResult {
|
||||
// 紧急释放结果
|
||||
}
|
||||
|
||||
// 关键函数(全部 stub):
|
||||
isContextCollapseEnabled() // → false
|
||||
applyCollapsesIfNeeded(messages) // 透传
|
||||
recoverFromOverflow(messages) // 透传(413 恢复)
|
||||
initContextCollapse() // 空操作
|
||||
```
|
||||
|
||||
### 2.3 预期数据流
|
||||
|
||||
```
|
||||
对话持续增长
|
||||
│
|
||||
▼
|
||||
上下文接近限制(由 query.ts 检测)
|
||||
│
|
||||
├── 溢出检测 (query.ts:440,616,802)
|
||||
│
|
||||
▼
|
||||
applyCollapsesIfNeeded(messages) [需要实现]
|
||||
│
|
||||
├── 后台 LLM 调用压缩旧消息
|
||||
├── 保留关键信息(决策、文件路径、错误)
|
||||
└── 替换旧消息为压缩摘要
|
||||
│
|
||||
├── 413 恢复 (query.ts:1093,1179)
|
||||
│ └── recoverFromOverflow() 紧急折叠
|
||||
│
|
||||
▼
|
||||
projectView() 过滤折叠后的消息视图
|
||||
│
|
||||
▼
|
||||
模型继续工作(在压缩后的上下文中)
|
||||
```
|
||||
|
||||
### 2.4 HISTORY_SNIP 子功能
|
||||
|
||||
SnipTool 提供手动折叠能力:
|
||||
|
||||
- `/force-snip` 命令 — 强制执行折叠
|
||||
- SnipTool — 标记特定消息进行折叠/修剪
|
||||
- `collapseReadSearch.ts` 已完整实现,将 Snip 作为静默吸收操作处理
|
||||
|
||||
### 2.5 集成点
|
||||
|
||||
| 文件 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| `src/query.ts` | 18,440,616,802,1093,1179 | 溢出检测、413 恢复、折叠应用 |
|
||||
| `src/QueryEngine.ts` | 124,127,1301 | Snip 投影使用 |
|
||||
| `src/utils/analyzeContext.ts` | 1122 | 跳过保留缓冲区显示 |
|
||||
| `src/utils/sessionRestore.ts` | 127,494 | 恢复折叠状态 |
|
||||
| `src/services/compact/autoCompact.ts` | 179,215 | 自动压缩时考虑折叠 |
|
||||
|
||||
## 三、需要补全的内容
|
||||
|
||||
| 优先级 | 模块 | 工作量 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| 1 | `services/contextCollapse/index.ts` | 大 | 折叠状态机、LLM 调用、消息压缩 |
|
||||
| 2 | `services/contextCollapse/operations.ts` | 中 | `projectView()` 消息过滤 |
|
||||
| 3 | `services/contextCollapse/persist.ts` | 小 | `restoreFromEntries()` 磁盘持久化 |
|
||||
| 4 | `tools/CtxInspectTool/` | 中 | 上下文内省工具(token 计数、已折叠范围) |
|
||||
| 5 | `tools/SnipTool/SnipTool.ts` | 中 | Snip 工具实现 |
|
||||
| 6 | `commands/force-snip.js` | 小 | `/force-snip` 命令 |
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **后台 LLM 压缩**:折叠不是简单截断,而是用 LLM 生成压缩摘要保留关键信息
|
||||
2. **413 恢复**:当 API 返回 413(请求过大)时,紧急折叠是最重要的恢复手段
|
||||
3. **与 autoCompact 协作**:折叠和自动压缩(compact)是不同的机制,折叠在消息级别,压缩在对话级别
|
||||
4. **持久化**:折叠状态持久化到磁盘,会话恢复时重载
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 启用 context collapse
|
||||
FEATURE_CONTEXT_COLLAPSE=1 bun run dev
|
||||
|
||||
# 启用 snip 子功能
|
||||
FEATURE_CONTEXT_COLLAPSE=1 FEATURE_HISTORY_SNIP=1 bun run dev
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/services/contextCollapse/index.ts` | 折叠核心(stub,接口已定义) |
|
||||
| `src/services/contextCollapse/operations.ts` | 投影操作(stub) |
|
||||
| `src/services/contextCollapse/persist.ts` | 持久化(stub) |
|
||||
| `src/utils/collapseReadSearch.ts` | Snip 吸收操作(完整) |
|
||||
| `src/query.ts` | 溢出检测和 413 恢复集成 |
|
||||
| `src/QueryEngine.ts` | Snip 投影使用 |
|
||||
| `src/components/TokenWarning.tsx` | 折叠进度 UI |
|
||||
@@ -1,151 +0,0 @@
|
||||
# COORDINATOR_MODE — 多 Agent 编排
|
||||
|
||||
> Feature Flag: `FEATURE_COORDINATOR_MODE=1` + 环境变量 `CLAUDE_CODE_COORDINATOR_MODE=1`
|
||||
> 实现状态:编排者完整可用,worker agent 为通用 AgentTool worker
|
||||
> 引用数:32
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
COORDINATOR_MODE 将 CLI 变为"编排者"角色。编排者不直接操作文件,而是通过 AgentTool 派发任务给多个 worker 并行执行。适用于大型任务拆分、并行研究、实现+验证分离等场景。
|
||||
|
||||
### 核心约束
|
||||
|
||||
- 编排者只能使用:`Agent`(派发 worker)、`SendMessage`(继续 worker)、`TaskStop`(停止 worker)
|
||||
- Worker 可以使用所有标准工具(Bash、Read、Edit 等)+ MCP 工具 + Skill 工具
|
||||
- 编排者的每条消息都是给用户看的;worker 结果以 `<task-notification>` XML 形式到达
|
||||
|
||||
## 二、用户交互
|
||||
|
||||
### 启用方式
|
||||
|
||||
```bash
|
||||
FEATURE_COORDINATOR_MODE=1 CLAUDE_CODE_COORDINATOR_MODE=1 bun run dev
|
||||
```
|
||||
|
||||
需要同时设置 feature flag 和环境变量。`CLAUDE_CODE_COORDINATOR_MODE` 可在会话恢复时自动切换(`matchSessionMode`)。
|
||||
|
||||
### 典型工作流
|
||||
|
||||
```
|
||||
用户: "修复 auth 模块的 null pointer"
|
||||
|
||||
编排者:
|
||||
1. 并行派发两个 worker:
|
||||
- Agent({ description: "调查 auth bug", prompt: "..." })
|
||||
- Agent({ description: "研究 auth 测试", prompt: "..." })
|
||||
|
||||
2. 收到 <task-notification>:
|
||||
- Worker A: "在 validate.ts:42 发现 null pointer"
|
||||
- Worker B: "测试覆盖情况..."
|
||||
|
||||
3. 综合发现,继续 Worker A:
|
||||
- SendMessage({ to: "agent-a1b", message: "修复 validate.ts:42..." })
|
||||
|
||||
4. 收到修复结果,派发验证:
|
||||
- Agent({ description: "验证修复", prompt: "..." })
|
||||
```
|
||||
|
||||
## 三、实现架构
|
||||
|
||||
### 3.1 模式检测
|
||||
|
||||
文件:`src/coordinator/coordinatorMode.ts:36-41`
|
||||
|
||||
```ts
|
||||
export function isCoordinatorMode(): boolean {
|
||||
return feature('COORDINATOR_MODE') &&
|
||||
isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 会话模式恢复
|
||||
|
||||
`matchSessionMode(sessionMode)` 在恢复旧会话时检查存储的模式,如果当前环境变量与存储不一致,自动翻转环境变量。防止在普通模式下恢复编排会话(或反之)。
|
||||
|
||||
### 3.3 Worker 工具集
|
||||
|
||||
`getCoordinatorUserContext()` 告知编排者 worker 可用的工具列表:
|
||||
|
||||
- **标准模式**:`ASYNC_AGENT_ALLOWED_TOOLS` 排除内部工具(TeamCreate、TeamDelete、SendMessage、SyntheticOutput)
|
||||
- **Simple 模式**(`CLAUDE_CODE_SIMPLE=1`):仅 Bash、Read、Edit
|
||||
- **MCP 工具**:列出已连接的 MCP 服务器名称
|
||||
- **Scratchpad**:如果 GrowthBook `tengu_scratch` 启用,提供跨 worker 共享的 scratchpad 目录
|
||||
|
||||
### 3.4 系统提示
|
||||
|
||||
文件:`src/coordinator/coordinatorMode.ts:111-369`
|
||||
|
||||
编排者系统提示(`getCoordinatorSystemPrompt()`)约 370 行,包含:
|
||||
|
||||
| 章节 | 内容 |
|
||||
|------|------|
|
||||
| 1. Your Role | 编排者职责定义 |
|
||||
| 2. Your Tools | Agent/SendMessage/TaskStop 使用说明 |
|
||||
| 3. Workers | Worker 能力和限制 |
|
||||
| 4. Task Workflow | Research → Synthesis → Implementation → Verification 流程 |
|
||||
| 5. Writing Worker Prompts | 自包含 prompt 编写指南 + 好坏示例对比 |
|
||||
| 6. Example Session | 完整示例对话 |
|
||||
|
||||
### 3.5 Worker Agent
|
||||
|
||||
文件:`src/coordinator/workerAgent.ts`
|
||||
|
||||
当前为 stub。Worker 实际使用通用 AgentTool 的 `worker` subagent_type。
|
||||
|
||||
### 3.6 数据流
|
||||
|
||||
```
|
||||
用户消息
|
||||
│
|
||||
▼
|
||||
编排者 REPL(受限工具集)
|
||||
│
|
||||
├──→ Agent({ subagent_type: "worker", prompt: "..." })
|
||||
│ │
|
||||
│ ▼
|
||||
│ Worker Agent(完整工具集)
|
||||
│ ├── 执行任务(Bash/Read/Edit/...)
|
||||
│ └── 返回 <task-notification>
|
||||
│
|
||||
├──→ SendMessage({ to: "agent-id", message: "..." })
|
||||
│ │
|
||||
│ ▼
|
||||
│ 继续已存在的 Worker
|
||||
│
|
||||
└──→ TaskStop({ task_id: "agent-id" })
|
||||
│
|
||||
▼
|
||||
停止运行中的 Worker
|
||||
```
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **双开关设计**:feature flag 控制代码可用性,环境变量控制实际激活。允许编译时包含但不默认启用
|
||||
2. **编排者受限**:只能用 Agent/SendMessage/TaskStop,确保编排者专注于派发而非执行
|
||||
3. **Worker 不可见编排者对话**:每个 worker 的 prompt 必须自包含(所有必要上下文)
|
||||
4. **并行优先**:系统提示强调"Parallelism is your superpower",鼓励并行派发独立任务
|
||||
5. **综合而非转发**:编排者必须理解 worker 发现,再写出具体的实现指令。禁止 "based on your findings" 类懒惰委托
|
||||
6. **Scratchpad 可选共享**:通过 GrowthBook 门控的共享目录,让 worker 之间持久化共享知识
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 基本启用
|
||||
FEATURE_COORDINATOR_MODE=1 CLAUDE_CODE_COORDINATOR_MODE=1 bun run dev
|
||||
|
||||
# 配合 Fork Subagent
|
||||
FEATURE_COORDINATOR_MODE=1 FEATURE_FORK_SUBAGENT=1 \
|
||||
CLAUDE_CODE_COORDINATOR_MODE=1 bun run dev
|
||||
|
||||
# Simple 模式(worker 只有 Bash/Read/Edit)
|
||||
FEATURE_COORDINATOR_MODE=1 CLAUDE_CODE_COORDINATOR_MODE=1 \
|
||||
CLAUDE_CODE_SIMPLE=1 bun run dev
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/coordinator/coordinatorMode.ts` | 370 | 模式检测 + 系统提示 + 用户上下文 |
|
||||
| `src/coordinator/workerAgent.ts` | — | Worker agent 定义(stub) |
|
||||
| `src/constants/tools.ts` | — | `ASYNC_AGENT_ALLOWED_TOOLS` 工具白名单 |
|
||||
@@ -1,102 +0,0 @@
|
||||
# DAEMON — 后台守护进程
|
||||
|
||||
> Feature Flag: `FEATURE_DAEMON=1`
|
||||
> 实现状态:主进程和 worker 注册为 Stub,CLI 路由完整
|
||||
> 引用数:3
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
DAEMON 将 Claude Code 变为后台守护进程。主进程(supervisor)管理多个 worker 进程的生命周期,通过 Unix 域套接字进行 IPC。适用于持续运行的后台服务场景(如配合 BRIDGE_MODE 提供远程控制服务)。
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
### 2.1 模块状态
|
||||
|
||||
| 模块 | 文件 | 状态 |
|
||||
|------|------|------|
|
||||
| 守护主进程 | `src/daemon/main.ts` | **Stub** — `daemonMain: () => Promise.resolve()` |
|
||||
| Worker 注册 | `src/daemon/workerRegistry.ts` | **Stub** — `runDaemonWorker: () => Promise.resolve()` |
|
||||
| CLI 路由 | `src/entrypoints/cli.tsx` | **布线** — `--daemon-worker` 和 `daemon` 子命令 |
|
||||
| 命令注册 | `src/commands.ts` | **布线** — DAEMON + BRIDGE_MODE 门控 |
|
||||
|
||||
### 2.2 CLI 入口
|
||||
|
||||
```
|
||||
# 启动守护进程
|
||||
claude daemon
|
||||
|
||||
# 以 worker 身份启动
|
||||
claude --daemon-worker=<kind>
|
||||
```
|
||||
|
||||
### 2.3 预期架构
|
||||
|
||||
```
|
||||
Supervisor (daemonMain)
|
||||
│
|
||||
├── Worker 1: assistant-mode
|
||||
│ └── 接收和处理 assistant 会话
|
||||
│
|
||||
├── Worker 2: bridge-sync
|
||||
│ └── bridge 消息同步
|
||||
│
|
||||
└── Worker 3: proactive
|
||||
└── 主动任务执行
|
||||
│
|
||||
▼
|
||||
IPC via Unix Domain Sockets
|
||||
- 生命周期管理(启动、停止、重启)
|
||||
- 工作分发
|
||||
- 状态报告
|
||||
```
|
||||
|
||||
### 2.4 与 BRIDGE_MODE 的关系
|
||||
|
||||
DAEMON 和 BRIDGE_MODE 常组合使用:
|
||||
|
||||
```ts
|
||||
// src/commands.ts
|
||||
if (feature('DAEMON') && feature('BRIDGE_MODE')) {
|
||||
// 加载 remoteControlServer 命令
|
||||
}
|
||||
```
|
||||
|
||||
双重门控:两个 feature 都需要开启才能使用远程控制服务器。
|
||||
|
||||
## 三、需要补全的内容
|
||||
|
||||
| 模块 | 工作量 | 说明 |
|
||||
|------|--------|------|
|
||||
| `daemon/main.ts` | 大 | Supervisor 主进程:启动 worker、生命周期管理、IPC |
|
||||
| `daemon/workerRegistry.ts` | 中 | Worker 类型分发(assistant/bridge-sync/proactive) |
|
||||
| Worker 实现 | 大 | 各类型 worker 的具体实现 |
|
||||
| IPC 协议 | 中 | Supervisor-Worker 通信层 |
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **多进程架构**:一个 supervisor + 多个 worker,进程隔离
|
||||
2. **Unix 域套接字 IPC**:本地进程间通信,低延迟
|
||||
3. **与 BRIDGE_MODE 强绑定**:守护进程最常见的用途是提供远程控制服务
|
||||
4. **CLI 子命令路由**:`daemon` 子命令和 `--daemon-worker` 参数在 `cli.tsx` 中路由
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 启用守护进程模式
|
||||
FEATURE_DAEMON=1 FEATURE_BRIDGE_MODE=1 bun run dev
|
||||
|
||||
# 启动守护进程
|
||||
claude daemon
|
||||
|
||||
# 以特定 worker 启动
|
||||
claude --daemon-worker=assistant
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/daemon/main.ts` | Supervisor 主进程(stub) |
|
||||
| `src/daemon/workerRegistry.ts` | Worker 注册(stub) |
|
||||
| `src/entrypoints/cli.tsx:95,149` | CLI 路由 |
|
||||
| `src/commands.ts:77` | 命令注册(双重门控) |
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
title: "Debug 模式"
|
||||
description: "通过 VS Code attach 模式调试 CLI 运行时,支持断点、单步执行和变量查看。"
|
||||
keywords: ["debug", "调试", "VS Code", "inspect", "断点"]
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
TUI (REPL) 模式需要真实终端,无法直接通过 VS Code launch 启动调试。使用 **attach 模式**连接到正在运行的 Bun 进程。
|
||||
|
||||
## 步骤
|
||||
|
||||
### 1. 终端启动 inspect 服务
|
||||
|
||||
```bash
|
||||
bun run dev:inspect
|
||||
```
|
||||
|
||||
会输出类似 `ws://localhost:8888/xxxxxxxx` 的地址。
|
||||
|
||||
### 2. VS Code 附着调试器
|
||||
|
||||
1. 在 `src/` 文件中打断点
|
||||
2. F5 → 选择 **"Attach to Bun (TUI debug)"**
|
||||
|
||||
> **注意**:`dev:inspect` 和 `launch.json` 中的 WebSocket 地址会在每次启动时变化,需要同步更新两处。
|
||||
|
||||
## 原理
|
||||
|
||||
`dev:inspect` 脚本实际执行的是:
|
||||
|
||||
```bash
|
||||
bun --inspect-wait=localhost:8888/<token> run scripts/dev.ts
|
||||
```
|
||||
|
||||
Bun 的 `--inspect-wait` 参数启动一个 Chrome DevTools Protocol 兼容的 inspect 服务,等待调试器连接后才开始执行。VS Code 的 `bun` 扩展通过 WebSocket 连接到这个地址实现 attach。
|
||||
|
||||
## JetBrains IDE
|
||||
|
||||
理论上 JetBrains 系列(WebStorm / IntelliJ 等)也支持 attach 到 Bun inspect 服务(Run → Attach to Process),但尚未实际验证过。如果你验证成功,欢迎补充文档。
|
||||
|
||||
## 相关文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `package.json` → `dev:inspect` | 启动 inspect 服务的 npm script |
|
||||
| `.vscode/launch.json` | VS Code attach 调试配置 |
|
||||
| `scripts/dev.ts` | dev 模式入口,注入 MACRO defines |
|
||||
@@ -1,99 +0,0 @@
|
||||
# EXPERIMENTAL_SKILL_SEARCH — 技能语义搜索
|
||||
|
||||
> Feature Flag: `FEATURE_EXPERIMENTAL_SKILL_SEARCH=1`
|
||||
> 实现状态:全部 Stub(8 个文件),布线完整
|
||||
> 引用数:21
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
EXPERIMENTAL_SKILL_SEARCH 提供 DiscoverSkills 工具,根据当前任务语义搜索可用技能。目标是让模型在执行任务时自动发现和推荐相关的技能(包括本地和远程),无需用户手动查找。
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
### 2.1 模块状态
|
||||
|
||||
| 模块 | 文件 | 状态 | 说明 |
|
||||
|------|------|------|------|
|
||||
| DiscoverSkillsTool | `src/tools/DiscoverSkillsTool/prompt.ts` | **Stub** | 空工具名 |
|
||||
| 预取 | `src/services/skillSearch/prefetch.ts` | **Stub** | 3 个函数全部空操作 |
|
||||
| 远程加载 | `src/services/skillSearch/remoteSkillLoader.ts` | **Stub** | 返回空结果 |
|
||||
| 远程状态 | `src/services/skillSearch/remoteSkillState.ts` | **Stub** | 返回 null/undefined |
|
||||
| 信号 | `src/services/skillSearch/signals.ts` | **Stub** | `DiscoverySignal = any` |
|
||||
| 遥测 | `src/services/skillSearch/telemetry.ts` | **Stub** | 空操作日志 |
|
||||
| 本地搜索 | `src/services/skillSearch/localSearch.ts` | **Stub** | 空操作缓存 |
|
||||
| 功能检查 | `src/services/skillSearch/featureCheck.ts` | **Stub** | `isSkillSearchEnabled => false` |
|
||||
| SkillTool 集成 | `src/tools/SkillTool/SkillTool.ts` | **布线** | 动态加载所有远程技能模块 |
|
||||
| 提示集成 | `src/constants/prompts.ts` | **布线** | DiscoverSkills schema 注入 |
|
||||
|
||||
### 2.2 预期数据流
|
||||
|
||||
```
|
||||
模型处理用户任务
|
||||
│
|
||||
▼
|
||||
DiscoverSkills 工具触发 [需要实现]
|
||||
│
|
||||
├── 本地搜索:索引已安装技能元数据
|
||||
│ └── localSearch.ts → 技能名称/描述/关键字匹配
|
||||
│
|
||||
└── 远程搜索:查询技能市场/注册表
|
||||
└── remoteSkillLoader.ts → fetch + 解析
|
||||
│
|
||||
▼
|
||||
结果排序和过滤
|
||||
│
|
||||
▼
|
||||
返回推荐技能列表
|
||||
│
|
||||
▼
|
||||
模型使用 SkillTool 调用推荐技能
|
||||
```
|
||||
|
||||
### 2.3 预取机制
|
||||
|
||||
`prefetch.ts` 预期在用户提交输入前分析消息内容,提前搜索相关技能:
|
||||
|
||||
- `startSkillDiscoveryPrefetch()` — 开始预取
|
||||
- `collectSkillDiscoveryPrefetch()` — 收集预取结果
|
||||
- `getTurnZeroSkillDiscovery()` — 获取 turn 0 的技能发现结果
|
||||
|
||||
## 三、需要补全的内容
|
||||
|
||||
| 优先级 | 模块 | 工作量 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| 1 | `DiscoverSkillsTool` | 大 | 语义搜索工具 schema + 执行 |
|
||||
| 2 | `skillSearch/prefetch.ts` | 中 | 用户输入分析和预取逻辑 |
|
||||
| 3 | `skillSearch/remoteSkillLoader.ts` | 大 | 远程市场/注册表获取 |
|
||||
| 4 | `skillSearch/remoteSkillState.ts` | 小 | 已发现技能状态管理 |
|
||||
| 5 | `skillSearch/localSearch.ts` | 中 | 本地索引构建/查询 |
|
||||
| 6 | `skillSearch/featureCheck.ts` | 小 | GrowthBook/配置门控 |
|
||||
| 7 | `skillSearch/signals.ts` | 小 | `DiscoverySignal` 类型定义 |
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **预取优化**:在用户提交前就开始搜索,减少首次响应延迟
|
||||
2. **本地+远程双搜索**:本地索引快速匹配 + 远程市场深度搜索
|
||||
3. **SkillTool 集成**:发现的技能通过 SkillTool 调用,不需要新的调用机制
|
||||
4. **独立于 MCP_SKILLS**:MCP_SKILLS 从 MCP 服务器发现,EXPERIMENTAL_SKILL_SEARCH 从技能市场发现
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 启用 feature(需要补全后才能真正使用)
|
||||
FEATURE_EXPERIMENTAL_SKILL_SEARCH=1 bun run dev
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/tools/DiscoverSkillsTool/prompt.ts` | 工具 schema(stub) |
|
||||
| `src/services/skillSearch/prefetch.ts` | 预取逻辑(stub) |
|
||||
| `src/services/skillSearch/remoteSkillLoader.ts` | 远程加载(stub) |
|
||||
| `src/services/skillSearch/remoteSkillState.ts` | 远程状态(stub) |
|
||||
| `src/services/skillSearch/signals.ts` | 信号类型(stub) |
|
||||
| `src/services/skillSearch/telemetry.ts` | 遥测(stub) |
|
||||
| `src/services/skillSearch/localSearch.ts` | 本地搜索(stub) |
|
||||
| `src/services/skillSearch/featureCheck.ts` | 功能检查(stub) |
|
||||
| `src/tools/SkillTool/SkillTool.ts` | SkillTool 集成点 |
|
||||
| `src/constants/prompts.ts:95,335,778` | 提示增强 |
|
||||
@@ -1,195 +0,0 @@
|
||||
# FORK_SUBAGENT — 上下文继承子 Agent
|
||||
|
||||
> Feature Flag: `FEATURE_FORK_SUBAGENT=1`
|
||||
> 实现状态:完整可用
|
||||
> 引用数:4
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
FORK_SUBAGENT 让 AgentTool 生成"fork 子 agent",继承父级完整对话上下文。子 agent 看到父级的所有历史消息、工具集和系统提示,并且与父级共享 API 请求前缀以最大化 prompt cache 命中率。
|
||||
|
||||
### 核心优势
|
||||
|
||||
- **Prompt Cache 最大化**:多个并行 fork 共享相同的 API 请求前缀,只有最后的 directive 文本块不同
|
||||
- **上下文完整性**:子 agent 继承父级的完整对话历史(包括 thinking config)
|
||||
- **权限冒泡**:子 agent 的权限提示上浮到父级终端显示
|
||||
- **Worktree 隔离**:支持 git worktree 隔离,子 agent 在独立分支工作
|
||||
|
||||
## 二、用户交互
|
||||
|
||||
### 触发方式
|
||||
|
||||
当 `FORK_SUBAGENT` 启用时,AgentTool 调用不指定 `subagent_type` 时自动走 fork 路径:
|
||||
|
||||
```
|
||||
// Fork 路径(继承上下文)
|
||||
Agent({ prompt: "修复这个 bug" }) // 无 subagent_type
|
||||
|
||||
// 普通 agent 路径(全新上下文)
|
||||
Agent({ subagent_type: "general-purpose", prompt: "..." })
|
||||
```
|
||||
|
||||
### /fork 命令
|
||||
|
||||
注册了 `/fork` 斜杠命令(当前为 stub)。当 FORK_SUBAGENT 开启时,`/branch` 命令失去 `fork` 别名,避免冲突。
|
||||
|
||||
## 三、实现架构
|
||||
|
||||
### 3.1 门控与互斥
|
||||
|
||||
文件:`src/tools/AgentTool/forkSubagent.ts:32-39`
|
||||
|
||||
```ts
|
||||
export function isForkSubagentEnabled(): boolean {
|
||||
if (feature('FORK_SUBAGENT')) {
|
||||
if (isCoordinatorMode()) return false // Coordinator 有自己的委派模型
|
||||
if (getIsNonInteractiveSession()) return false // pipe/SDK 模式禁用
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 FORK_AGENT 定义
|
||||
|
||||
```ts
|
||||
export const FORK_AGENT = {
|
||||
agentType: 'fork',
|
||||
tools: ['*'], // 通配符:使用父级完整工具集
|
||||
maxTurns: 200,
|
||||
model: 'inherit', // 继承父级模型
|
||||
permissionMode: 'bubble', // 权限冒泡到父级终端
|
||||
getSystemPrompt: () => '', // 不使用:直接传递父级已渲染 prompt
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 核心调用流程
|
||||
|
||||
```
|
||||
AgentTool.call({ prompt, name })
|
||||
│
|
||||
▼
|
||||
isForkSubagentEnabled() && !subagent_type?
|
||||
│
|
||||
├── No → 普通 agent 路径
|
||||
│
|
||||
└── Yes → Fork 路径
|
||||
│
|
||||
▼
|
||||
递归防护检查
|
||||
├── querySource === 'agent:builtin:fork' → 拒绝
|
||||
└── isInForkChild(messages) → 拒绝
|
||||
│
|
||||
▼
|
||||
获取父级 system prompt
|
||||
├── toolUseContext.renderedSystemPrompt(首选)
|
||||
└── buildEffectiveSystemPrompt(回退)
|
||||
│
|
||||
▼
|
||||
buildForkedMessages(prompt, assistantMessage)
|
||||
├── 克隆父级 assistant 消息
|
||||
├── 生成占位符 tool_result
|
||||
└── 附加 directive 文本块
|
||||
│
|
||||
▼
|
||||
[可选] buildWorktreeNotice()
|
||||
│
|
||||
▼
|
||||
runAgent({
|
||||
useExactTools: true,
|
||||
override.systemPrompt: 父级,
|
||||
forkContextMessages: 父级消息,
|
||||
availableTools: 父级工具,
|
||||
})
|
||||
```
|
||||
|
||||
### 3.4 消息构建:buildForkedMessages
|
||||
|
||||
文件:`src/tools/AgentTool/forkSubagent.ts:107-169`
|
||||
|
||||
构建的消息结构:
|
||||
|
||||
```
|
||||
[
|
||||
...history (filterIncompleteToolCalls), // 父级完整历史
|
||||
assistant(所有 tool_use 块), // 父级当前 turn 的 assistant 消息
|
||||
user(
|
||||
占位符 tool_result × N + // 相同占位符文本
|
||||
<fork-boilerplate> directive // 每个 fork 不同
|
||||
)
|
||||
]
|
||||
```
|
||||
|
||||
**所有 fork 使用相同的占位符文本**:`"Fork started — processing in background"`。这确保多个并行 fork 的 API 请求前缀完全一致,最大化 prompt cache 命中。
|
||||
|
||||
### 3.5 递归防护
|
||||
|
||||
两层检查防止 fork 嵌套:
|
||||
|
||||
1. **querySource 检查**:`toolUseContext.options.querySource === 'agent:builtin:fork'`。在 `context.options` 上设置,抗自动压缩(autocompact 只重写消息不改 options)
|
||||
2. **消息扫描**:`isInForkChild()` 扫描消息历史中的 `<fork-boilerplate>` 标签
|
||||
|
||||
### 3.6 Worktree 隔离通知
|
||||
|
||||
当 fork + worktree 组合时,追加通知告知子 agent:
|
||||
|
||||
> "你继承了父 agent 在 `{parentCwd}` 的对话上下文,但你在独立的 git worktree `{worktreeCwd}` 中操作。路径需要转换,编辑前重新读取。"
|
||||
|
||||
### 3.7 强制异步
|
||||
|
||||
当 `isForkSubagentEnabled()` 为 true 时,所有 agent 启动都强制异步。`run_in_background` 参数从 schema 中移除。统一通过 `<task-notification>` XML 消息交互。
|
||||
|
||||
## 四、Prompt Cache 优化
|
||||
|
||||
这是整个 fork 设计的核心优化目标:
|
||||
|
||||
| 优化点 | 实现 |
|
||||
|--------|------|
|
||||
| **相同 system prompt** | 直传 `renderedSystemPrompt`,避免重新渲染(GrowthBook 状态可能不一致) |
|
||||
| **相同工具集** | `useExactTools: true` 直接使用父级工具,不经过 `resolveAgentTools` 过滤 |
|
||||
| **相同 thinking config** | 继承父级 thinking 配置(非 fork agent 默认禁用 thinking) |
|
||||
| **相同占位符结果** | 所有 fork 使用 `FORK_PLACEHOLDER_RESULT` 相同文本 |
|
||||
| **ContentReplacementState 克隆** | 默认克隆父级替换状态,保持 wire prefix 一致 |
|
||||
|
||||
## 五、子 Agent 指令
|
||||
|
||||
`buildChildMessage()` 生成 `<fork-boilerplate>` 包裹的指令:
|
||||
|
||||
- 你是 fork worker,不是主 agent
|
||||
- 禁止再次 spawn sub-agent(直接执行)
|
||||
- 不要闲聊、不要元评论
|
||||
- 直接使用工具
|
||||
- 修改文件后要 commit,报告 commit hash
|
||||
- 报告格式:`Scope:` / `Result:` / `Key files:` / `Files changed:` / `Issues:`
|
||||
|
||||
## 六、关键设计决策
|
||||
|
||||
1. **Fork ≠ 普通 agent**:fork 继承完整上下文,普通 agent 从零开始。选择依据是 `subagent_type` 是否存在
|
||||
2. **renderedSystemPrompt 直传**:避免 fork 时重新调用 `getSystemPrompt()`。父级在 turn 开始时冻结 prompt 字节
|
||||
3. **占位符结果共享**:多个并行 fork 使用完全相同的占位符,只有 directive 不同
|
||||
4. **Coordinator 互斥**:Coordinator 模式下禁用 fork,两者有不兼容的委派模型
|
||||
5. **非交互式禁用**:pipe 模式和 SDK 模式下禁用,避免不可见的 fork 嵌套
|
||||
|
||||
## 七、使用方式
|
||||
|
||||
```bash
|
||||
# 启用 feature
|
||||
FEATURE_FORK_SUBAGENT=1 bun run dev
|
||||
|
||||
# 在 REPL 中使用(不指定 subagent_type 即走 fork)
|
||||
# Agent({ prompt: "研究这个模块的结构" })
|
||||
# Agent({ prompt: "实现这个功能" })
|
||||
```
|
||||
|
||||
## 八、文件索引
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/tools/AgentTool/forkSubagent.ts` | ~210 | 核心定义 + 消息构建 + 递归防护 |
|
||||
| `src/tools/AgentTool/AgentTool.tsx` | — | Fork 路由 + 强制异步 |
|
||||
| `src/tools/AgentTool/prompt.ts` | — | "When to Fork" 提示词段落 |
|
||||
| `src/tools/AgentTool/runAgent.ts` | — | useExactTools 路径 |
|
||||
| `src/tools/AgentTool/resumeAgent.ts` | — | Fork agent 恢复 |
|
||||
| `src/constants/xml.ts` | — | XML 标签常量 |
|
||||
| `src/utils/forkedAgent.ts` | — | CacheSafeParams + ContentReplacementState 克隆 |
|
||||
| `src/commands/fork/index.ts` | — | /fork 命令(stub) |
|
||||
@@ -1,179 +0,0 @@
|
||||
# KAIROS — 常驻助手模式
|
||||
|
||||
> Feature Flag: `FEATURE_KAIROS=1`(及子 Feature)
|
||||
> 实现状态:核心框架完整,部分子模块为 stub
|
||||
> 引用数:154(全库最大)
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
KAIROS 将 Claude Code CLI 从"问答工具"转变为"常驻助手"。开启后,CLI 持续运行在后台,支持:
|
||||
|
||||
- **持久化 bridge 会话**:跨终端重启复用 session,通过 Anthropic OAuth 连接 claude.ai
|
||||
- **后台执行任务**:用户离开终端时继续工作(配合 PROACTIVE feature)
|
||||
- **推送通知到移动端**:任务完成或需要输入时推送(配合 `KAIROS_PUSH_NOTIFICATION`)
|
||||
- **每日记忆日志**:自动记录和回顾工作内容(配合 `KAIROS_DREAM`)
|
||||
- **外部频道消息接入**:Slack/Discord/Telegram 消息转发到 CLI(配合 `KAIROS_CHANNELS`)
|
||||
- **结构化 Brief 输出**:通过 BriefTool 输出结构化消息(配合 `KAIROS_BRIEF`)
|
||||
|
||||
### 子 Feature 依赖关系
|
||||
|
||||
```
|
||||
KAIROS (主开关)
|
||||
├── KAIROS_BRIEF (BriefTool, 结构化输出)
|
||||
├── KAIROS_CHANNELS (外部频道消息)
|
||||
├── KAIROS_PUSH_NOTIFICATION (移动端推送)
|
||||
├── KAIROS_GITHUB_WEBHOOKS (GitHub PR webhook)
|
||||
└── KAIROS_DREAM (记忆蒸馏)
|
||||
```
|
||||
|
||||
**注意**:PROACTIVE 与 KAIROS 强绑定。所有代码检查都是 `feature('PROACTIVE') || feature('KAIROS')`,即 KAIROS 开启时自动获得 proactive 能力。
|
||||
|
||||
## 二、系统提示
|
||||
|
||||
KAIROS 在系统提示中注入两大段落:
|
||||
|
||||
### 2.1 Brief 段落 (`getBriefSection`)
|
||||
|
||||
文件:`src/constants/prompts.ts:843-858`
|
||||
|
||||
当 `feature('KAIROS') || feature('KAIROS_BRIEF')` 时注入。Brief 工具(`SendUserMessage`)的结构化消息输出指令。`/brief` toggle 和 `--brief` flag 只控制显示过滤,不影响模型行为。
|
||||
|
||||
### 2.2 Proactive/Autonomous Work 段落 (`getProactiveSection`)
|
||||
|
||||
文件:`src/constants/prompts.ts:860-914`
|
||||
|
||||
当 `feature('PROACTIVE') || feature('KAIROS')` 且 `isProactiveActive()` 时注入。核心行为指令:
|
||||
|
||||
- **Tick 驱动**:通过 `<tick_tag>` prompt 保持存活,每个 tick 包含用户当前本地时间
|
||||
- **节奏控制**:使用 `SleepTool` 控制等待间隔(prompt cache 5 分钟过期)
|
||||
- **空操作时必须 Sleep**:禁止输出 "still waiting" 类文本(浪费 turn 和 token)
|
||||
- **偏向行动**:读文件、搜索代码、修改文件、commit — 都不需询问
|
||||
- **终端焦点感知**:`terminalFocus` 字段指示用户是否在看终端
|
||||
- Unfocused → 高度自主行动
|
||||
- Focused → 更协作,展示选择
|
||||
|
||||
## 三、实现架构
|
||||
|
||||
### 3.1 核心模块
|
||||
|
||||
| 模块 | 文件 | 状态 | 职责 |
|
||||
|------|------|------|------|
|
||||
| Assistant 入口 | `src/assistant/index.ts` | Stub | `isAssistantMode()`、`initializeAssistantTeam()` |
|
||||
| Session 发现 | `src/assistant/sessionDiscovery.ts` | Stub | 发现可用 bridge session |
|
||||
| Session 历史 | `src/assistant/sessionHistory.ts` | Stub | 持久化 session 历史 |
|
||||
| Gate 控制 | `src/assistant/gate.ts` | Stub | GrowthBook 门控检查 |
|
||||
| Session 选择器 | `src/assistant/AssistantSessionChooser.ts` | Stub | UI 选择 session |
|
||||
| BriefTool | `src/tools/BriefTool/` | Stub | 结构化消息输出工具 |
|
||||
| Channel Notification | `src/services/mcp/channelNotification.ts` | Stub | 外部频道消息接入 |
|
||||
| Dream Task | `src/components/tasks/src/tasks/DreamTask/` | Stub | 记忆蒸馏任务 |
|
||||
| Memory Directory | `src/memdir/memdir.ts` | Stub | 记忆目录管理 |
|
||||
|
||||
### 3.2 SleepTool(与 Proactive 共享)
|
||||
|
||||
文件:`src/tools/SleepTool/prompt.ts`
|
||||
|
||||
SleepTool 是 KAIROS/Proactive 的节奏控制核心。工具描述让模型理解"休眠"概念:
|
||||
- 工具名:`Sleep`
|
||||
- 功能:等待指定时间后响应 tick prompt
|
||||
- 与 `<tick_tag>` 配合实现心跳式自主工作
|
||||
|
||||
### 3.3 Bridge 集成
|
||||
|
||||
KAIROS 通过 Bridge Mode(`src/bridge/`)连接到 claude.ai 服务器:
|
||||
|
||||
```
|
||||
claude.ai web/app
|
||||
│
|
||||
▼ (HTTPS long-poll)
|
||||
┌──────────────────────┐
|
||||
│ Bridge API Client │ src/bridge/bridgeApi.ts
|
||||
│ (register/poll/ │
|
||||
│ acknowledge) │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Session Runner │ src/bridge/sessionRunner.ts
|
||||
│ (创建/恢复 REPL) │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ REPL + Proactive │ Tick 驱动自主工作
|
||||
│ Tick Loop │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### 3.4 数据流
|
||||
|
||||
```
|
||||
用户从 claude.ai 发送消息
|
||||
│
|
||||
▼
|
||||
Bridge pollForWork() 收到 WorkResponse
|
||||
│
|
||||
▼
|
||||
acknowledgeWork() 确认接收
|
||||
│
|
||||
▼
|
||||
sessionRunner 创建/恢复 REPL session
|
||||
│
|
||||
▼
|
||||
用户消息注入到 REPL 对话
|
||||
│
|
||||
▼
|
||||
模型处理 → 工具调用 → BriefTool 结构化输出
|
||||
│
|
||||
▼
|
||||
结果通过 Bridge API 回传到 claude.ai
|
||||
```
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **Tick 驱动而非事件驱动**:模型通过 SleepTool 自行控制唤醒频率,而非外部事件推送。简化架构但增加 API 调用开销
|
||||
2. **KAIROS ⊃ PROACTIVE**:所有 proactive 检查都包含 KAIROS,无需同时开启两个 flag
|
||||
3. **Brief 显示/行为分离**:`/brief` toggle 只控制 UI 过滤,模型始终可以使用 BriefTool
|
||||
4. **Terminal Focus 感知**:模型根据用户是否在看终端自动调节自主程度
|
||||
5. **GrowthBook 门控**:部分功能(如推送通知)即使 feature flag 开启还需要服务端 GrowthBook 开关
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 最小启用(常驻助手 + Brief)
|
||||
FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 bun run dev
|
||||
|
||||
# 全功能启用
|
||||
FEATURE_KAIROS=1 \
|
||||
FEATURE_KAIROS_BRIEF=1 \
|
||||
FEATURE_KAIROS_CHANNELS=1 \
|
||||
FEATURE_KAIROS_PUSH_NOTIFICATION=1 \
|
||||
FEATURE_KAIROS_GITHUB_WEBHOOKS=1 \
|
||||
FEATURE_PROACTIVE=1 \
|
||||
bun run dev
|
||||
|
||||
# 配合 Token Budget 使用
|
||||
FEATURE_KAIROS=1 FEATURE_TOKEN_BUDGET=1 bun run dev
|
||||
```
|
||||
|
||||
## 六、外部依赖
|
||||
|
||||
- **Anthropic OAuth**:必须使用 claude.ai 订阅登录(非 API key)
|
||||
- **GrowthBook**:服务端特性门控(`tengu_ccr_bridge` 等)
|
||||
- **Bridge API**:`/v1/environments/bridge` 系列端点
|
||||
|
||||
## 七、文件索引
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/assistant/index.ts` | 9 | Assistant 模块入口(stub) |
|
||||
| `src/assistant/gate.ts` | — | GrowthBook 门控(stub) |
|
||||
| `src/assistant/sessionDiscovery.ts` | — | Session 发现(stub) |
|
||||
| `src/assistant/sessionHistory.ts` | — | Session 历史(stub) |
|
||||
| `src/assistant/AssistantSessionChooser.ts` | — | Session 选择 UI(stub) |
|
||||
| `src/tools/BriefTool/` | — | BriefTool 实现(stub) |
|
||||
| `src/tools/SleepTool/prompt.ts` | ~30 | SleepTool 工具提示 |
|
||||
| `src/services/mcp/channelNotification.ts` | 5 | 频道消息接入(stub) |
|
||||
| `src/memdir/memdir.ts` | — | 记忆目录管理(stub) |
|
||||
| `src/constants/prompts.ts:552-554,843-914` | 72 | 系统提示注入 |
|
||||
| `src/components/tasks/src/tasks/DreamTask/` | 3 | Dream 任务(stub) |
|
||||
| `src/proactive/index.ts` | — | Proactive 核心(stub,KAIROS 共享) |
|
||||
@@ -1,118 +0,0 @@
|
||||
# MCP_SKILLS — MCP 技能发现
|
||||
|
||||
> Feature Flag: `FEATURE_MCP_SKILLS=1`
|
||||
> 实现状态:功能性实现(config 门控筛选器完整,核心 fetcher 为 stub)
|
||||
> 引用数:9
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
MCP_SKILLS 将 MCP 服务器暴露的资源(`skill://` URI 方案)发现并转换为可调用的技能命令。MCP 服务器可以同时提供 tools、prompts 和 resources;启用此 feature 后,带有 `skill://` URI 的资源被识别为技能。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **自动发现**:MCP 服务器连接时自动获取 `skill://` 资源
|
||||
- **命令转换**:将 MCP 资源转换为 `prompt` 类型的 Command 对象
|
||||
- **实时刷新**:prompts/resources 列表变化时重新获取技能
|
||||
- **缓存一致性**:连接关闭时清除技能缓存
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
### 2.1 数据流
|
||||
|
||||
```
|
||||
MCP Server 连接
|
||||
│
|
||||
▼
|
||||
client.ts: connectToServer / setupMcpClientConnections
|
||||
├── fetchToolsForClient (MCP tools)
|
||||
├── fetchCommandsForClient (MCP prompts → Command 对象)
|
||||
├── fetchMcpSkillsForClient (MCP skill:// 资源 → Command 对象) [MCP_SKILLS]
|
||||
└── fetchResourcesForClient (MCP resources)
|
||||
│
|
||||
▼
|
||||
commands = [...mcpPrompts, ...mcpSkills]
|
||||
│
|
||||
▼
|
||||
AppState.mcp.commands 更新
|
||||
│
|
||||
▼
|
||||
getMcpSkillCommands() 过滤 → SkillTool 调用
|
||||
```
|
||||
|
||||
### 2.2 技能筛选
|
||||
|
||||
文件:`src/commands.ts:547-558`
|
||||
|
||||
`getMcpSkillCommands(mcpCommands)` 过滤条件:
|
||||
|
||||
```ts
|
||||
cmd.type === 'prompt' // 必须是 prompt 类型
|
||||
cmd.loadedFrom === 'mcp' // 必须来自 MCP 服务器
|
||||
!cmd.disableModelInvocation // 必须可由模型调用
|
||||
feature('MCP_SKILLS') // feature flag 必须开启
|
||||
```
|
||||
|
||||
### 2.3 条件加载
|
||||
|
||||
文件:`src/services/mcp/client.ts:117-121`
|
||||
|
||||
`fetchMcpSkillsForClient` 通过 `require()` 条件加载,feature flag 关闭时不加载任何模块:
|
||||
|
||||
```ts
|
||||
const fetchMcpSkillsForClient = feature('MCP_SKILLS')
|
||||
? require('../../skills/mcpSkills.js').fetchMcpSkillsForClient
|
||||
: null
|
||||
```
|
||||
|
||||
### 2.4 缓存管理
|
||||
|
||||
技能获取函数维护 `.cache`(Map),在以下时机清除:
|
||||
|
||||
| 事件 | 行为 |
|
||||
|------|------|
|
||||
| 连接关闭 | 清除该 client 的技能缓存 |
|
||||
| `disconnectMcpServer()` | 清除技能缓存 |
|
||||
| `prompts/list_changed` 通知 | 刷新 prompts + 并行获取技能 |
|
||||
| `resources/list_changed` 通知 | 刷新 resources + prompts + 技能 |
|
||||
|
||||
### 2.5 集成点
|
||||
|
||||
| 文件 | 行 | 说明 |
|
||||
|------|------|------|
|
||||
| `src/commands.ts` | 547-558, 561-608 | 命令过滤和 SkillTool 命令收集 |
|
||||
| `src/services/mcp/client.ts` | 117-121, 1394, 1672, 2173-2181, 2346-2358 | 技能获取、缓存清除、连接时获取 |
|
||||
| `src/services/mcp/useManageMCPConnections.ts` | 22-26, 682-740 | 实时刷新(prompts/resources 变化) |
|
||||
|
||||
## 三、关键设计决策
|
||||
|
||||
1. **Feature gate 隔离**:`feature('MCP_SKILLS')` 守护条件 `require()` 和所有调用点。关闭时无模块加载、无获取操作
|
||||
2. **资源到技能映射**:技能从 MCP 服务器的 `skill://` URI 资源中发现。`fetchMcpSkillsForClient` 负责转换(当前为 stub)
|
||||
3. **循环依赖避免**:`mcpSkillBuilders.ts` 作为依赖图叶节点,避免 `client.ts ↔ mcpSkills.ts ↔ loadSkillsDir.ts` 循环
|
||||
4. **服务器能力检查**:技能获取还需要 MCP 服务器支持 resources (`!!client.capabilities?.resources`)
|
||||
|
||||
## 四、使用方式
|
||||
|
||||
```bash
|
||||
# 启用 feature
|
||||
FEATURE_MCP_SKILLS=1 bun run dev
|
||||
|
||||
# 前提条件:
|
||||
# 1. 配置了支持 skill:// 资源的 MCP 服务器
|
||||
# 2. MCP 服务器声明了 resources 能力
|
||||
```
|
||||
|
||||
## 五、需要补全的内容
|
||||
|
||||
| 文件 | 状态 | 需要实现 |
|
||||
|------|------|---------|
|
||||
| `src/skills/mcpSkills.ts` | Stub | `fetchMcpSkillsForClient()` — 从 MCP 资源列表中筛选 `skill://` URI 并转换为 Command 对象 |
|
||||
| `src/skills/mcpSkillBuilders.ts` | Stub | 技能构建器注册(避免循环依赖) |
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/commands.ts:547-608` | 技能命令过滤 |
|
||||
| `src/services/mcp/client.ts:117-2358` | 技能获取 + 缓存管理 |
|
||||
| `src/services/mcp/useManageMCPConnections.ts` | 实时刷新 |
|
||||
| `src/skills/mcpSkills.ts` | 核心转换逻辑(stub) |
|
||||
@@ -1,109 +0,0 @@
|
||||
# PROACTIVE — 主动模式
|
||||
|
||||
> Feature Flag: `FEATURE_PROACTIVE=1`(与 `FEATURE_KAIROS=1` 共享功能)
|
||||
> 实现状态:核心模块全部 Stub,布线完整
|
||||
> 引用数:37
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持续工作:定时唤醒执行任务,配合 SleepTool 控制节奏。适用于长时间运行的后台任务(等待 CI、监控文件变化、定时检查等)。
|
||||
|
||||
### 与 KAIROS 的关系
|
||||
|
||||
所有代码检查都是 `feature('PROACTIVE') || feature('KAIROS')`,即:
|
||||
- 单独开 `FEATURE_PROACTIVE=1` → 获得 proactive 能力
|
||||
- 单独开 `FEATURE_KAIROS=1` → 自动获得 proactive 能力
|
||||
- 两者都开 → 相同效果(不重复)
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
### 2.1 模块状态
|
||||
|
||||
| 模块 | 文件 | 状态 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 核心逻辑 | `src/proactive/index.ts` | **Stub** | `activateProactive()`、`deactivateProactive()`、`isProactiveActive() => false` |
|
||||
| SleepTool 提示 | `src/tools/SleepTool/prompt.ts` | **完整** | 工具提示定义(工具名:`Sleep`) |
|
||||
| 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` |
|
||||
| 工具注册 | `src/tools.ts:26-28` | **布线** | SleepTool 动态加载 |
|
||||
| REPL 集成 | `src/screens/REPL.tsx` | **布线** | tick 驱动逻辑、占位符、页脚 UI |
|
||||
| 系统提示 | `src/constants/prompts.ts:860-914` | **完整** | 自主工作行为指令(~55 行详细 prompt) |
|
||||
| 会话存储 | `src/utils/sessionStorage.ts:4892-4912` | **布线** | tick 消息注入对话流 |
|
||||
|
||||
### 2.2 系统提示内容
|
||||
|
||||
`getProactiveSection()` 注入的自主工作指令包含:
|
||||
|
||||
| 章节 | 内容 |
|
||||
|------|------|
|
||||
| Tick 驱动 | `<tick_tag>` prompt 保持存活,包含用户本地时间 |
|
||||
| 节奏控制 | SleepTool 控制等待间隔,prompt cache 5 分钟过期 |
|
||||
| 空操作规则 | 无事可做时**必须**调用 Sleep,禁止输出 "still waiting" |
|
||||
| 首次唤醒 | 简短问候,等待方向(不主动探索) |
|
||||
| 后续唤醒 | 寻找有用工作:调查、验证、检查(不 spam 用户) |
|
||||
| 偏向行动 | 读文件、搜索代码、commit — 不需询问 |
|
||||
| 终端焦点 | `terminalFocus` 字段调节自主程度 |
|
||||
|
||||
### 2.3 数据流
|
||||
|
||||
```
|
||||
activateProactive() [需要实现]
|
||||
│
|
||||
▼
|
||||
Tick 调度器启动
|
||||
│
|
||||
├── 定时生成 <tick_tag> 消息
|
||||
│ ├── 包含用户当前本地时间
|
||||
│ └── 注入到对话流(sessionStorage)
|
||||
│
|
||||
▼
|
||||
模型处理 tick
|
||||
│
|
||||
├── 有事可做 → 使用工具执行 → 可能再次 Sleep
|
||||
└── 无事可做 → 必须调用 SleepTool
|
||||
│
|
||||
▼
|
||||
SleepTool 等待 [需要实现]
|
||||
│
|
||||
▼
|
||||
下一个 tick 到达
|
||||
```
|
||||
|
||||
## 三、需要补全的内容
|
||||
|
||||
| 优先级 | 模块 | 工作量 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| 1 | `src/proactive/index.ts` | 中 | Tick 调度器、activate/deactivate 状态机、pause/resume |
|
||||
| 2 | `src/tools/SleepTool/SleepTool.ts` | 小 | 工具执行(等待指定时间后触发 tick) |
|
||||
| 3 | `src/commands/proactive.js` | 小 | `/proactive` 斜杠命令处理器 |
|
||||
| 4 | `src/hooks/useProactive.ts` | 中 | React hook(REPL 引用但不存在) |
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **Tick 驱动**:模型通过 SleepTool 自行控制唤醒频率,不是外部事件推送
|
||||
2. **空操作必须 Sleep**:防止 "still waiting" 类空消息浪费 turn 和 token
|
||||
3. **Prompt cache 考量**:SleepTool 提示中提到 cache 5 分钟过期,建议平衡等待时间
|
||||
4. **Terminal Focus 感知**:模型根据用户是否在看终端调整自主程度
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 单独启用 proactive
|
||||
FEATURE_PROACTIVE=1 bun run dev
|
||||
|
||||
# 通过 KAIROS 间接启用
|
||||
FEATURE_KAIROS=1 bun run dev
|
||||
|
||||
# 组合使用
|
||||
FEATURE_PROACTIVE=1 FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 bun run dev
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/proactive/index.ts` | 核心逻辑(stub) |
|
||||
| `src/tools/SleepTool/prompt.ts` | SleepTool 工具提示 |
|
||||
| `src/constants/prompts.ts:860-914` | 自主工作系统提示 |
|
||||
| `src/screens/REPL.tsx` | REPL tick 集成 |
|
||||
| `src/utils/sessionStorage.ts:4892-4912` | Tick 消息注入 |
|
||||
| `src/components/PromptInput/PromptInputFooterLeftSide.tsx` | 页脚 UI 状态 |
|
||||
@@ -1,167 +0,0 @@
|
||||
# TEAMMEM — 团队共享记忆
|
||||
|
||||
> Feature Flag: `FEATURE_TEAMMEM=1`
|
||||
> 实现状态:完整可用(需要 Anthropic OAuth + GitHub remote)
|
||||
> 引用数:51
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
TEAMMEM 实现基于 GitHub 仓库的团队共享记忆系统。`memory/team/` 目录中的文件双向同步到 Anthropic 服务器,团队所有认证成员可共享项目知识。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **增量同步**:只上传内容哈希变化的文件(delta upload)
|
||||
- **冲突解决**:基于 ETag 的乐观锁 + 412 冲突重试
|
||||
- **密钥扫描**:上传前检测并跳过包含密钥的文件(PSR M22174)
|
||||
- **路径穿越防护**:所有写入路径验证在 `memory/team/` 边界内
|
||||
- **分批上传**:自动拆分超过 200KB 的 PUT 请求避免网关拒绝
|
||||
|
||||
## 二、用户交互
|
||||
|
||||
### 同步行为
|
||||
|
||||
| 事件 | 行为 |
|
||||
|------|------|
|
||||
| 项目启动 | 自动 pull 团队记忆到 `memory/team/` |
|
||||
| 本地文件编辑 | watcher 检测变更,自动 push |
|
||||
| 服务端更新 | 下次 pull 时覆盖本地(server-wins) |
|
||||
| 密钥检测 | 跳过该文件,记录警告,不阻止其他文件同步 |
|
||||
|
||||
### API 端点
|
||||
|
||||
```
|
||||
GET /api/claude_code/team_memory?repo={owner/repo} → 完整数据 + entryChecksums
|
||||
GET /api/claude_code/team_memory?repo={owner/repo}&view=hashes → 仅 checksums(冲突解决用)
|
||||
PUT /api/claude_code/team_memory?repo={owner/repo} → 上传 entries(upsert 语义)
|
||||
```
|
||||
|
||||
## 三、实现架构
|
||||
|
||||
### 3.1 同步状态
|
||||
|
||||
```ts
|
||||
type SyncState = {
|
||||
lastKnownChecksum: string | null // ETag 条件请求
|
||||
serverChecksums: Map<string, string> // sha256:<hex> 逐文件哈希
|
||||
serverMaxEntries: number | null // 从 413 学习的服务端容量
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Pull 流程(Server → Local)
|
||||
|
||||
文件:`src/services/teamMemorySync/index.ts:770-867`
|
||||
|
||||
```
|
||||
pullTeamMemory(state)
|
||||
│
|
||||
▼
|
||||
检查 OAuth + GitHub remote
|
||||
│
|
||||
▼
|
||||
fetchTeamMemory(state, repo, etag)
|
||||
├── 304 Not Modified → 返回(无变化)
|
||||
├── 404 → 返回(服务端无数据)
|
||||
└── 200 → 解析 TeamMemoryData
|
||||
│
|
||||
▼
|
||||
刷新 serverChecksums(per-key hashes)
|
||||
│
|
||||
▼
|
||||
writeRemoteEntriesToLocal(entries)
|
||||
├── 路径穿越验证(validateTeamMemKey)
|
||||
├── 文件大小检查(> 250KB 跳过)
|
||||
├── 内容比较(相同则跳过写入)
|
||||
└── 并行写入(Promise.all)
|
||||
```
|
||||
|
||||
### 3.3 Push 流程(Local → Server)
|
||||
|
||||
文件:`src/services/teamMemorySync/index.ts:889-1146`
|
||||
|
||||
```
|
||||
pushTeamMemory(state)
|
||||
│
|
||||
▼
|
||||
readLocalTeamMemory(maxEntries)
|
||||
├── 递归扫描 memory/team/ 目录
|
||||
├── 跳过超大文件(> 250KB)
|
||||
├── 密钥扫描(scanForSecrets,gitleaks 规则)
|
||||
└── 按 serverMaxEntries 截断(如果已知)
|
||||
│
|
||||
▼
|
||||
计算 delta = 本地文件 - serverChecksums
|
||||
(只包含哈希不同的文件)
|
||||
│
|
||||
▼
|
||||
batchDeltaByBytes(delta)
|
||||
(拆分为 ≤200KB 的批次)
|
||||
│
|
||||
▼
|
||||
逐批 uploadTeamMemory(state, repo, batch, etag)
|
||||
├── 200 成功 → 更新 serverChecksums
|
||||
├── 412 冲突 → fetchTeamMemoryHashes() 刷新 checksums
|
||||
│ → 重试 delta 计算(最多 2 次)
|
||||
└── 413 超容量 → 学习 serverMaxEntries
|
||||
```
|
||||
|
||||
### 3.4 密钥扫描
|
||||
|
||||
文件:`src/services/teamMemorySync/secretScanner.ts`
|
||||
|
||||
使用 gitleaks 规则模式扫描文件内容。检测到密钥时:
|
||||
- 跳过该文件(不上传)
|
||||
- 记录 `tengu_team_mem_secret_skipped` 事件(仅记录规则 ID,不记录值)
|
||||
- 不阻止其他文件同步
|
||||
|
||||
### 3.5 文件监视
|
||||
|
||||
文件:`src/services/teamMemorySync/watcher.ts`
|
||||
|
||||
监视 `memory/team/` 目录变更,触发自动 push。抑制由 pull 写入引起的假变更。
|
||||
|
||||
### 3.6 路径安全
|
||||
|
||||
文件:`src/memdir/teamMemPaths.ts`
|
||||
|
||||
- `validateTeamMemKey(relPath)` — 验证相对路径不超出 `memory/team/` 边界
|
||||
- `getTeamMemPath()` — 返回 team memory 根目录路径
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **Server-wins on pull, Local-wins on push**:pull 时服务端内容覆盖本地;push 时本地编辑覆盖服务端。本地用户正在编辑,不应被静默丢弃
|
||||
2. **Delta upload**:只上传哈希变化的条目,节省带宽。首次 push 为全量,后续增量
|
||||
3. **分批 PUT**:单次 PUT ≤200KB,避免 API 网关(~256-512KB)拒绝。每批独立 upsert,部分失败不影响已提交批次
|
||||
4. **密钥扫描在上传前**:PSR M22174 要求密钥永不离开本机。扫描在 `readLocalTeamMemory` 中执行,密钥文件不进入上传集
|
||||
5. **ETag 乐观锁**:push 使用 `If-Match` header。412 时 probe `?view=hashes`(只获取 checksums,不下载内容),刷新后重试
|
||||
6. **服务端容量动态学习**:不假设客户端容量上限,从 413 的 `extra_details.max_entries` 学习
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 启用 feature
|
||||
FEATURE_TEAMMEM=1 bun run dev
|
||||
|
||||
# 前提条件:
|
||||
# 1. 已通过 Anthropic OAuth 登录
|
||||
# 2. 项目有 GitHub remote(git remote -v 显示 origin)
|
||||
# 3. memory/team/ 目录自动创建
|
||||
```
|
||||
|
||||
## 六、外部依赖
|
||||
|
||||
| 依赖 | 说明 |
|
||||
|------|------|
|
||||
| Anthropic OAuth | first-party 认证 |
|
||||
| GitHub Remote | `getGithubRepo()` 获取 `owner/repo` 作为同步 scope |
|
||||
| Team Memory API | `/api/claude_code/team_memory` 端点 |
|
||||
|
||||
## 七、文件索引
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/services/teamMemorySync/index.ts` | 1257 | 核心同步逻辑(pull/push/sync) |
|
||||
| `src/services/teamMemorySync/watcher.ts` | — | 文件监视 + 自动同步触发 |
|
||||
| `src/services/teamMemorySync/secretScanner.ts` | — | gitleaks 密钥扫描 |
|
||||
| `src/services/teamMemorySync/types.ts` | — | Zod schema + 类型定义 |
|
||||
| `src/services/teamMemorySync/teamMemSecretGuard.ts` | — | 密钥防护辅助 |
|
||||
| `src/memdir/teamMemPaths.ts` | — | 路径验证 + 目录管理 |
|
||||
@@ -1,76 +0,0 @@
|
||||
# Tier 3 — 纯 Stub / N/A 低优先级 Feature 概览
|
||||
|
||||
> 本文档汇总所有 Tier 3 feature。这些功能要么是纯 Stub(所有函数返回空值),
|
||||
> 要么是 Anthropic 内部基础设施(N/A),要么是引用量极低的辅助功能。
|
||||
|
||||
## 概览
|
||||
|
||||
| Feature | 引用 | 状态 | 类别 | 简要说明 |
|
||||
|---------|------|------|------|---------|
|
||||
| CHICAGO_MCP | 16 | N/A | 内部基础设施 | Anthropic 内部 MCP 基础设施,非外部可用 |
|
||||
| UDS_INBOX | 17 | Stub | 消息通信 | Unix 域套接字对等消息,进程间消息传递 |
|
||||
| MONITOR_TOOL | 13 | Stub | 工具 | 文件/进程监控工具,检测变更并通知 |
|
||||
| BG_SESSIONS | 11 | Stub | 会话管理 | 后台会话管理,支持多会话并行 |
|
||||
| SHOT_STATS | 10 | 无实现 | 统计 | 逐 prompt 统计信息收集 |
|
||||
| EXTRACT_MEMORIES | 7 | 无实现 | 记忆 | 自动从对话中提取重要信息作为记忆 |
|
||||
| TEMPLATES | 6 | Stub | 项目管理 | 项目/提示模板系统 |
|
||||
| LODESTONE | 6 | N/A | 内部基础设施 | 内部基础设施模块 |
|
||||
| STREAMLINED_OUTPUT | 1 | — | 输出 | 精简输出模式,减少终端输出量 |
|
||||
| HOOK_PROMPTS | 1 | — | 钩子 | Hook 提示词,自定义钩子的提示注入 |
|
||||
| CCR_AUTO_CONNECT | 3 | — | 远程控制 | CCR 自动连接,自动建立远程控制会话 |
|
||||
| CCR_MIRROR | 4 | — | 远程控制 | CCR 镜像模式,会话状态同步 |
|
||||
| CCR_REMOTE_SETUP | 1 | — | 远程控制 | CCR 远程设置,初始化远程控制配置 |
|
||||
| NATIVE_CLIPBOARD_IMAGE | 2 | — | 系统集成 | 原生剪贴板图片,从剪贴板读取图片 |
|
||||
| CONNECTOR_TEXT | 7 | — | 连接器 | 连接器文本,外部系统文本适配 |
|
||||
| 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 各只有 1 处引用,多为内部标记或实验性功能:
|
||||
|
||||
UNATTENDED_RETRY, ULTRATHINK, TORCH, SLOW_OPERATION_LOGGING, SKILL_IMPROVEMENT,
|
||||
SELF_HOSTED_RUNNER, RUN_SKILL_GENERATOR, PERFETTO_TRACING, NATIVE_CLIENT_ATTESTATION,
|
||||
KAIROS_DREAM(见 kairos.md), IS_LIBC_MUSL, IS_LIBC_GLIBC, DUMP_SYSTEM_PROMPT,
|
||||
COMPACTION_REMINDERS, CCR_REMOTE_SETUP, BYOC_ENVIRONMENT_RUNNER, BUILTIN_EXPLORE_PLAN_AGENTS,
|
||||
BUILDING_CLAUDE_APPS, ANTI_DISTILLATION_CC, AGENT_TRIGGERS, ABLATION_BASELINE
|
||||
|
||||
## 优先级说明
|
||||
|
||||
这些 feature 被列为 Tier 3 的原因:
|
||||
|
||||
1. **内部基础设施**(CHICAGO_MCP, LODESTONE):Anthropic 内部使用,外部无法运行
|
||||
2. **纯 Stub 且引用低**(UDS_INBOX, MONITOR_TOOL, BG_SESSIONS):需要大量工作才能实现
|
||||
3. **实验性功能**(SHOT_STATS, EXTRACT_MEMORIES):尚在概念阶段
|
||||
4. **辅助功能**(STREAMLINED_OUTPUT, HOOK_PROMPTS):影响范围小
|
||||
5. **CCR 系列**:依赖远程控制基础设施,需要 BRIDGE_MODE 先完善
|
||||
|
||||
如需深入了解某个 Tier 3 feature,可以在代码库中搜索 `feature('FEATURE_NAME')` 查看具体使用场景。
|
||||
@@ -1,198 +0,0 @@
|
||||
# TOKEN_BUDGET — Token 预算自动持续模式
|
||||
|
||||
> Feature Flag: `FEATURE_TOKEN_BUDGET=1`
|
||||
> 实现状态:完整可用
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
TOKEN_BUDGET 让用户在 prompt 中指定一个 output token 预算目标(如 `+500k`、`spend 2M tokens`),Claude 会**自动持续工作**直到达到目标,无需用户反复按回车催促继续。
|
||||
|
||||
适用于大型重构、批量修改、大规模代码生成等需要多轮工具调用的长任务。
|
||||
|
||||
## 二、用户交互
|
||||
|
||||
### 语法
|
||||
|
||||
| 格式 | 示例 | 说明 |
|
||||
|------|------|------|
|
||||
| 简写(开头) | `+500k` | 输入开头直接写 |
|
||||
| 简写(结尾) | `帮我重构这个模块 +2m` | 输入末尾追加 |
|
||||
| 完整语法 | `spend 2M tokens` 或 `use 1B tokens` | 自然语言嵌入 |
|
||||
|
||||
单位支持:`k`(千)、`m`(百万)、`b`(十亿),大小写不敏感。
|
||||
|
||||
### UI 反馈
|
||||
|
||||
- **输入框高亮**:输入包含预算语法时,对应文字会被高亮标记(`PromptInput.tsx` 通过 `findTokenBudgetPositions` 计算)
|
||||
- **Spinner 进度**:底部 spinner 显示实时进度,格式如:
|
||||
- 未完成:`Target: 125,000 / 500,000 (25%) · ~2m 30s`
|
||||
- 已完成:`Target: 510,000 used (500,000 min ✓)`
|
||||
- 包含 ETA(基于当前 token 产出速率计算)
|
||||
|
||||
## 三、实现架构
|
||||
|
||||
### 数据流
|
||||
|
||||
```
|
||||
用户输入 "+500k"
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ parseTokenBudget() │ src/utils/tokenBudget.ts
|
||||
│ 正则解析 → 500,000 │
|
||||
└────────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ REPL.tsx │ 提交时调用
|
||||
│ snapshotOutputTokens │ snapshotOutputTokensForTurn(500000)
|
||||
│ ForTurn(500000) │ 记录 turn 起始 token 数 + 预算
|
||||
└────────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ query.ts 主循环 │ 每轮结束后检查
|
||||
│ checkTokenBudget() │ 当前 output tokens vs 预算
|
||||
└────────┬────────────────┘
|
||||
│
|
||||
┌────┴─────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
continue stop
|
||||
(未达 90%) (已达 90% 或收益递减)
|
||||
│ │
|
||||
▼ ▼
|
||||
注入 nudge 正常结束
|
||||
消息继续 发送完成事件
|
||||
```
|
||||
|
||||
### 核心模块
|
||||
|
||||
#### 1. 解析层 — `src/utils/tokenBudget.ts`
|
||||
|
||||
三个正则表达式解析用户输入:
|
||||
|
||||
```
|
||||
SHORTHAND_START_RE = /^\s*\+(\d+(?:\.\d+)?)\s*(k|m|b)\b/i // "+500k" 在开头
|
||||
SHORTHAND_END_RE = /\s\+(\d+(?:\.\d+)?)\s*(k|m|b)\s*[.!?]?\s*$/i // "+2m" 在结尾
|
||||
VERBOSE_RE = /\b(?:use|spend)\s+(\d+(?:\.\d+)?)\s*(k|m|b)\s*tokens?\b/i // "spend 2M tokens"
|
||||
```
|
||||
|
||||
- `parseTokenBudget(text)` — 提取预算数值,返回 `number | null`
|
||||
- `findTokenBudgetPositions(text)` — 返回匹配位置数组,用于输入框高亮
|
||||
- `getBudgetContinuationMessage(pct, turnTokens, budget)` — 生成继续消息
|
||||
|
||||
#### 2. 状态层 — `src/bootstrap/state.ts`
|
||||
|
||||
模块级单例变量追踪当前 turn 的预算状态:
|
||||
|
||||
```
|
||||
outputTokensAtTurnStart — 本 turn 开始时的累计 output token 数
|
||||
currentTurnTokenBudget — 本 turn 的预算目标(null 表示无预算)
|
||||
budgetContinuationCount — 本 turn 已自动续接的次数
|
||||
```
|
||||
|
||||
关键函数:
|
||||
- `getTotalOutputTokens()` — 从 `STATE.modelUsage` 汇总所有模型的 output tokens
|
||||
- `getTurnOutputTokens()` — `getTotalOutputTokens() - outputTokensAtTurnStart`
|
||||
- `snapshotOutputTokensForTurn(budget)` — 重置 turn 起点,设置新预算
|
||||
- `getCurrentTurnTokenBudget()` — 返回当前预算
|
||||
|
||||
#### 3. 决策层 — `src/query/tokenBudget.ts`
|
||||
|
||||
`checkTokenBudget(tracker, agentId, budget, globalTurnTokens)` 做出 continue/stop 决策:
|
||||
|
||||
**继续条件**:
|
||||
- 不在子 agent 中(`agentId` 为空)
|
||||
- 预算存在且 > 0
|
||||
- 当前 token 未达预算的 **90%**
|
||||
- 非收益递减(连续 3 轮 nudge 后,每轮新增 < 500 tokens)
|
||||
|
||||
**停止条件**:
|
||||
- 达到预算 90%
|
||||
- 收益递减(模型已经"做不动了")
|
||||
- 子 agent 模式下直接跳过
|
||||
|
||||
**收益递减检测**:`continuationCount >= 3` 且最近两次 nudge 的 delta 都 < 500 tokens。
|
||||
|
||||
#### 4. 主循环集成 — `src/query.ts`
|
||||
|
||||
```
|
||||
query() 函数内:
|
||||
1. 创建 budgetTracker = createBudgetTracker()
|
||||
2. 进入 while 循环
|
||||
3. 每轮结束后调用 checkTokenBudget()
|
||||
4. decision.action === 'continue' 时:
|
||||
- 注入 meta user message(nudge)
|
||||
- continue 回到循环顶部
|
||||
5. decision.action === 'stop' 时:
|
||||
- 记录完成事件(含 diminishingReturns 标记)
|
||||
- 正常返回
|
||||
```
|
||||
|
||||
#### 5. UI 层
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `components/PromptInput/PromptInput.tsx:534` | 输入框中高亮预算语法 |
|
||||
| `components/Spinner.tsx:319-338` | spinner 显示进度百分比 + ETA |
|
||||
| `screens/REPL.tsx:2897` | 提交时解析预算并快照 |
|
||||
| `screens/REPL.tsx:2138` | 用户取消时清除预算 |
|
||||
| `screens/REPL.tsx:2963` | turn 结束时捕获预算信息用于显示 |
|
||||
|
||||
#### 6. 系统提示 — `src/constants/prompts.ts:538-551`
|
||||
|
||||
注入 `token_budget` section:
|
||||
|
||||
> "When the user specifies a token target (e.g., '+500k', 'spend 2M tokens', 'use 1B tokens'), your output token count will be shown each turn. Keep working until you approach the target — plan your work to fill it productively. The target is a hard minimum, not a suggestion. If you stop early, the system will automatically continue you."
|
||||
|
||||
注意:这段 prompt **无条件缓存**(不随预算开关变化),因为 "When the user specifies..." 的措辞在没有预算时是空操作。
|
||||
|
||||
#### 7. API 附件 — `src/utils/attachments.ts:3830-3845`
|
||||
|
||||
每轮 API 调用附带 `output_token_usage` attachment:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "output_token_usage",
|
||||
"turn": 125000, // 本 turn 产出
|
||||
"session": 350000, // 会话总产出
|
||||
"budget": 500000 // 预算目标
|
||||
}
|
||||
```
|
||||
|
||||
让模型能看到自己的进度。
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **90% 阈值而非 100%**:在 `COMPLETION_THRESHOLD = 0.9` 处停止,避免最后一轮 nudge 产生远超预算的 token
|
||||
2. **收益递减保护**:连续 3 轮 nudge 后如果每轮产出 < 500 tokens,判定模型已无实质进展,提前终止
|
||||
3. **子 agent 豁免**:AgentTool 内部的子任务不做预算检查,避免子任务重复触发续接
|
||||
4. **无条件缓存系统提示**:预算 prompt 始终注入(不随预算变化 toggle),避免每次切换预算导致 ~20K token 的 cache miss
|
||||
5. **用户取消清预算**:按 Escape 取消时调用 `snapshotOutputTokensForTurn(null)`,防止残留预算触发续接
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 启用 feature
|
||||
FEATURE_TOKEN_BUDGET=1 bun run dev
|
||||
|
||||
# 在 prompt 中使用
|
||||
> +500k 重构所有测试文件
|
||||
> spend 2M tokens 把这个项目从 JS 迁移到 TS
|
||||
> 帮我写完整的 CRUD 模块 +1m
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/utils/tokenBudget.ts` | 73 | 正则解析 + 位置查找 + 续接消息生成 |
|
||||
| `src/query/tokenBudget.ts` | 93 | 预算追踪器 + continue/stop 决策 |
|
||||
| `src/bootstrap/state.ts:724-743` | 20 | turn 级 token 快照状态 |
|
||||
| `src/constants/prompts.ts:538-551` | 14 | 系统提示注入 |
|
||||
| `src/utils/attachments.ts:3829-3845` | 17 | API attachment 附加 |
|
||||
| `src/query.ts:280,1311-1358` | 48 | 主循环集成 |
|
||||
| `src/screens/REPL.tsx:2897,2963,2138` | 20 | REPL 提交/完成/取消处理 |
|
||||
| `src/components/Spinner.tsx:319-338` | 20 | 进度条 UI |
|
||||
| `src/components/PromptInput/PromptInput.tsx:534` | 1 | 输入高亮 |
|
||||
@@ -1,161 +0,0 @@
|
||||
# TREE_SITTER_BASH — Bash AST 解析
|
||||
|
||||
> Feature Flag: `FEATURE_TREE_SITTER_BASH=1`
|
||||
> 实现状态:完整可用(纯 TypeScript 实现,~7000+ 行)
|
||||
> 引用数:3
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
TREE_SITTER_BASH 启用一个完整的 Bash AST 解析器,用于安全验证 Bash 命令。它用完整的树遍历安全分析器取代了旧的基于正则表达式的 shell-quote 解析器。关键属性是 **fail-closed**:任何无法识别的内容都被归类为 `too-complex` 并需要用户批准。
|
||||
|
||||
### 关联 Feature
|
||||
|
||||
| Feature | 说明 |
|
||||
|---------|------|
|
||||
| `TREE_SITTER_BASH` | 激活用于权限检查的 AST 解析器 |
|
||||
| `TREE_SITTER_BASH_SHADOW` | Shadow/观测模式:运行解析器但丢弃结果,仅记录遥测 |
|
||||
|
||||
## 二、安全架构
|
||||
|
||||
### 2.1 Fail-Closed 设计
|
||||
|
||||
核心设计使用 **allowlist** 遍历模式:
|
||||
|
||||
- `walkArgument()` 只处理已知安全的节点类型(`word`、`number`、`raw_string`、`string`、`concatenation`、`arithmetic_expansion`、`simple_expansion`)
|
||||
- 任何未知节点类型 → `tooComplex()` → 需要用户批准
|
||||
- 解析器加载但失败(超时/节点预算/panic)→ 返回 `PARSE_ABORTED` 符号(区别于"模块未加载")
|
||||
|
||||
### 2.2 解析结果
|
||||
|
||||
```ts
|
||||
parseForSecurity(cmd) 返回:
|
||||
{ kind: 'simple', commands: SimpleCommand[] } // 可静态分析
|
||||
{ kind: 'too-complex', reason, nodeType } // 需要用户批准
|
||||
{ kind: 'parse-unavailable' } // 解析器未加载
|
||||
```
|
||||
|
||||
### 2.3 安全检查层次
|
||||
|
||||
```
|
||||
parseForSecurity(cmd)
|
||||
│
|
||||
▼
|
||||
parseCommandRaw(cmd) → AST root node
|
||||
│
|
||||
▼
|
||||
预检查:控制字符、Unicode 空白、反斜杠+空白、
|
||||
zsh ~[ ] 语法、zsh =cmd 展开、大括号+引号混淆
|
||||
│
|
||||
▼
|
||||
walkProgram(root) → collectCommands(root, commands, varScope)
|
||||
│
|
||||
├── 'command' → walkCommand()
|
||||
├── 'pipeline'/'list' → 结构性,递归子节点
|
||||
├── 'for_statement' → 跟踪循环变量为 VAR_PLACEHOLDER
|
||||
├── 'if/while' → 作用域隔离的分支
|
||||
├── 'subshell' → 作用域复制
|
||||
├── 'variable_assignment' → walkVariableAssignment()
|
||||
├── 'declaration_command' → 验证 declare/export flags
|
||||
├── 'test_command' → walk test expressions
|
||||
└── 其他 → tooComplex()
|
||||
│
|
||||
▼
|
||||
checkSemantics(commands)
|
||||
├── EVAL_LIKE_BUILTINS(eval, source, exec, trap...)
|
||||
├── ZSH_DANGEROUS_BUILTINS(zmodload, emulate...)
|
||||
├── SUBSCRIPT_EVAL_FLAGS(test -v, printf -v, read -a)
|
||||
├── Shell keywords as argv[0](误解析检测)
|
||||
├── /proc/*/environ 访问
|
||||
├── jq system() 和危险 flags
|
||||
└── 包装器剥离(time, nohup, timeout, nice, env, stdbuf)
|
||||
```
|
||||
|
||||
## 三、实现架构
|
||||
|
||||
### 3.1 核心模块
|
||||
|
||||
| 模块 | 文件 | 行数 | 职责 |
|
||||
|------|------|------|------|
|
||||
| 门控入口 | `src/utils/bash/parser.ts` | ~110 | `parseCommand()`、`parseCommandRaw()`、`ensureInitialized()` |
|
||||
| Bash 解析器 | `src/utils/bash/bashParser.ts` | 4437 | 纯 TS 词法分析 + 递归下降解析器 |
|
||||
| 安全分析器 | `src/utils/bash/ast.ts` | 2680 | 树遍历安全分析 + `parseForSecurity()` |
|
||||
| AST 分析辅助 | `src/utils/bash/treeSitterAnalysis.ts` | 507 | 引号上下文、复合结构、危险模式提取 |
|
||||
| 权限检查入口 | `src/tools/BashTool/bashPermissions.ts` | — | 集成 AST 结果到权限决策 |
|
||||
|
||||
### 3.2 Bash 解析器
|
||||
|
||||
文件:`src/utils/bash/bashParser.ts`(4437 行)
|
||||
|
||||
- 纯 TypeScript 实现(无原生依赖)
|
||||
- 生成与 tree-sitter-bash 兼容的 AST
|
||||
- 关键类型:`TsNode`(type、text、startIndex、endIndex、children)
|
||||
- 安全限制:`PARSE_TIMEOUT_MS = 50`、`MAX_NODES = 50_000` — 防止对抗性输入导致 OOM
|
||||
|
||||
### 3.3 安全分析器
|
||||
|
||||
文件:`src/utils/bash/ast.ts`(2680 行)
|
||||
|
||||
核心函数:
|
||||
|
||||
| 函数 | 职责 |
|
||||
|------|------|
|
||||
| `parseForSecurity(cmd)` | 顶层入口,返回 `simple/too-complex/parse-unavailable` |
|
||||
| `parseForSecurityFromAst(cmd, root)` | 接受预解析 AST |
|
||||
| `checkSemantics(commands)` | 后解析语义检查 |
|
||||
| `walkCommand()` | 提取 argv、envVars、redirects |
|
||||
| `walkArgument()` | Allowlist 参数遍历 |
|
||||
| `collectCommands()` | 递归收集所有命令 |
|
||||
|
||||
### 3.4 AST 分析辅助
|
||||
|
||||
文件:`src/utils/bash/treeSitterAnalysis.ts`(507 行)
|
||||
|
||||
| 函数 | 职责 |
|
||||
|------|------|
|
||||
| `extractQuoteContext()` | 识别单引号、双引号、ANSI-C 字符串、heredoc |
|
||||
| `extractCompoundStructure()` | 检测管道、子 shell、命令组 |
|
||||
| `hasActualOperatorNodes()` | 区分真实 `;`/`&&`/`||` 与转义形式 |
|
||||
| `extractDangerousPatterns()` | 检测命令替换、参数展开、heredocs |
|
||||
| `analyzeCommand()` | 单次遍历提取 |
|
||||
|
||||
### 3.5 Shadow 模式
|
||||
|
||||
`TREE_SITTER_BASH_SHADOW` 运行解析器但**从不影响权限决策**:
|
||||
|
||||
```ts
|
||||
// Shadow 模式:记录遥测,然后强制使用旧版路径
|
||||
astResult = { kind: 'parse-unavailable' }
|
||||
astRoot = null
|
||||
// 记录: available, astTooComplex, astSemanticFail, subsDiffer, ...
|
||||
```
|
||||
|
||||
记录 `tengu_tree_sitter_shadow` 事件,包含与旧版 `splitCommand()` 的对比数据。用于在不影响行为的情况下收集遥测。
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **Allowlist 遍历**:只处理已知安全的节点类型,未知类型直接 `tooComplex()`
|
||||
2. **PARSE_ABORTED 符号**:区分"解析器未加载"和"解析器加载但失败"。后者阻止回退旧版(旧版缺少 `EVAL_LIKE_BUILTINS` 检查)
|
||||
3. **变量作用域跟踪**:`VAR=value && cmd $VAR` 模式。静态值解析为真实字符串,`$()` 输出使用 `VAR_PLACEHOLDER`
|
||||
4. **PS4/IFS Allowlist**:PS4 赋值使用严格字符白名单 `[A-Za-z0-9 _+:.\/=\[\]-]`,只允许 `${VAR}` 引用
|
||||
5. **包装器剥离**:从 argv 前面剥离 `time/nohup/timeout/nice/env/stdbuf`,未知标志 → fail-closed
|
||||
6. **Shadow 安全性**:Shadow 模式**总是**强制 `astResult = { kind: 'parse-unavailable' }`,绝不影响权限
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 激活 AST 解析用于权限检查
|
||||
FEATURE_TREE_SITTER_BASH=1 bun run dev
|
||||
|
||||
# Shadow 模式(仅遥测,不影响行为)
|
||||
FEATURE_TREE_SITTER_BASH_SHADOW=1 bun run dev
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/utils/bash/parser.ts` | ~110 | 门控入口点 |
|
||||
| `src/utils/bash/bashParser.ts` | 4437 | 纯 TS bash 解析器 |
|
||||
| `src/utils/bash/ast.ts` | 2680 | 安全分析器(核心) |
|
||||
| `src/utils/bash/treeSitterAnalysis.ts` | 507 | AST 分析辅助 |
|
||||
| `src/tools/BashTool/bashPermissions.ts:1670-1810` | ~140 | 权限集成 + Shadow 遥测 |
|
||||
@@ -1,107 +0,0 @@
|
||||
# ULTRAPLAN — 增强规划
|
||||
|
||||
> Feature Flag: `FEATURE_ULTRAPLAN=1`
|
||||
> 实现状态:关键字检测完整,命令处理完整,CCR 远程会话完整
|
||||
> 引用数:10
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
ULTRAPLAN 在用户输入中检测 "ultraplan" 关键字时,自动进入增强计划模式。相比普通 plan mode,ultraplan 提供更深入的规划能力,支持本地和远程(CCR)执行。
|
||||
|
||||
### 触发方式
|
||||
|
||||
| 方式 | 行为 |
|
||||
|------|------|
|
||||
| 输入含 "ultraplan" 的文本 | 自动重定向到 `/ultraplan` 命令 |
|
||||
| `/ultraplan` 斜杠命令 | 直接执行 |
|
||||
| 彩虹高亮 | 输入框中 "ultraplan" 关键字彩虹动画 |
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
### 2.1 模块状态
|
||||
|
||||
| 模块 | 文件 | 行数 | 状态 |
|
||||
|------|------|------|------|
|
||||
| 命令处理器 | `src/commands/ultraplan.tsx` | 472 | **完整** |
|
||||
| CCR 会话 | `src/utils/ultraplan/ccrSession.ts` | 350 | **完整** |
|
||||
| 关键字检测 | `src/utils/ultraplan/keyword.ts` | 128 | **完整** |
|
||||
| 嵌入式提示 | `src/utils/ultraplan/prompt.txt` | 1 | **完整** |
|
||||
| REPL 对话框 | `src/screens/REPL.tsx` | — | **布线** |
|
||||
| 关键字高亮 | `src/components/PromptInput/PromptInput.tsx` | — | **布线** |
|
||||
|
||||
### 2.2 关键字检测
|
||||
|
||||
文件:`src/utils/ultraplan/keyword.ts`(128 行)
|
||||
|
||||
`findUltraplanTriggerPositions(text)` 智能过滤:
|
||||
- 排除引号内的 "ultraplan"
|
||||
- 排除路径中的 "ultraplan"(如 `/path/to/ultraplan/`)
|
||||
- 排除斜杠命令以外的上下文
|
||||
- `replaceUltraplanKeyword(text)` 清理关键字
|
||||
|
||||
### 2.3 CCR 远程会话
|
||||
|
||||
文件:`src/utils/ultraplan/ccrSession.ts`(350 行)
|
||||
|
||||
`ExitPlanModeScanner` 类实现完整的事件状态机:
|
||||
- `pollForApprovedExitPlanMode()` — 3 秒轮询间隔
|
||||
- 超时处理和重试
|
||||
- 支持远程(teleport)和本地执行
|
||||
|
||||
### 2.4 数据流
|
||||
|
||||
```
|
||||
用户输入 "帮我 ultraplan 重构这个模块"
|
||||
│
|
||||
▼
|
||||
processUserInput 检测 "ultraplan"
|
||||
│
|
||||
▼
|
||||
重定向到 /ultraplan 命令
|
||||
│
|
||||
├── 本地执行 → EnterPlanMode
|
||||
│
|
||||
└── 远程执行 → teleportToRemote → CCR 会话
|
||||
│
|
||||
▼
|
||||
ExitPlanModeScanner 轮询
|
||||
│
|
||||
▼
|
||||
用户在远程审批 → 本地收到结果
|
||||
```
|
||||
|
||||
## 三、需要补全的内容
|
||||
|
||||
| 模块 | 说明 |
|
||||
|------|------|
|
||||
| `src/screens/REPL.tsx` 中的 UltraplanChoiceDialog / UltraplanLaunchDialog | 用户选择本地/远程执行的对话框组件 |
|
||||
| `src/commands/ultraplan/` | 空目录,可能是未合并的子命令结构 |
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **智能关键字过滤**:排除引号和路径中的 "ultraplan",避免误触发
|
||||
2. **本地/远程双模式**:支持本地 plan mode 和 CCR 远程会话
|
||||
3. **彩虹高亮反馈**:输入框中 "ultraplan" 关键字使用彩虹动画,暗示这是特殊功能
|
||||
4. **processUserInput 集成**:在用户输入处理管道中拦截,无缝重定向
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 启用 feature
|
||||
FEATURE_ULTRAPLAN=1 bun run dev
|
||||
|
||||
# 在 REPL 中使用
|
||||
# > ultraplan 重构认证模块
|
||||
# > /ultraplan
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/commands/ultraplan.tsx` | 472 | 斜杠命令处理器 |
|
||||
| `src/utils/ultraplan/ccrSession.ts` | 350 | CCR 远程会话管理 |
|
||||
| `src/utils/ultraplan/keyword.ts` | 128 | 关键字检测和替换 |
|
||||
| `src/utils/ultraplan/prompt.txt` | 1 | 嵌入式提示 |
|
||||
| `src/utils/processUserInput/processUserInput.ts:468` | — | 关键字重定向 |
|
||||
| `src/components/PromptInput/PromptInput.tsx` | — | 彩虹高亮 |
|
||||
@@ -1,125 +0,0 @@
|
||||
# VOICE_MODE — 语音输入
|
||||
|
||||
> Feature Flag: `FEATURE_VOICE_MODE=1`
|
||||
> 实现状态:完整可用(需要 Anthropic OAuth)
|
||||
> 引用数:46
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
VOICE_MODE 实现"按键说话"(Push-to-Talk)语音输入。用户按住空格键录音,音频通过 WebSocket 流式传输到 Anthropic STT 端点(Nova 3),实时转录显示在终端中。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **Push-to-Talk**:长按空格键录音,释放后自动发送
|
||||
- **流式转录**:录音过程中实时显示中间转录结果
|
||||
- **无缝集成**:转录文本直接作为用户消息提交到对话
|
||||
|
||||
## 二、用户交互
|
||||
|
||||
| 操作 | 行为 |
|
||||
|------|------|
|
||||
| 长按空格 | 开始录音,显示录音状态 |
|
||||
| 释放空格 | 停止录音,等待最终转录 |
|
||||
| 转录完成 | 自动插入到输入框并提交 |
|
||||
| `/voice` 命令 | 切换语音模式开关 |
|
||||
|
||||
### UI 反馈
|
||||
|
||||
- **录音指示器**:录音时显示红色/脉冲动画
|
||||
- **中间转录**:录音过程中显示 STT 实时识别文本
|
||||
- **最终转录**:完成后替换中间结果
|
||||
|
||||
## 三、实现架构
|
||||
|
||||
### 3.1 门控逻辑
|
||||
|
||||
文件:`src/voice/voiceModeEnabled.ts`
|
||||
|
||||
三层检查:
|
||||
|
||||
```ts
|
||||
isVoiceModeEnabled() = hasVoiceAuth() && isVoiceGrowthBookEnabled()
|
||||
```
|
||||
|
||||
1. **Feature Flag**:`feature('VOICE_MODE')` — 编译时/运行时开关
|
||||
2. **GrowthBook Kill-Switch**:`!getFeatureValue_CACHED_MAY_BE_STALE('tengu_amber_quartz_disabled', false)` — 紧急关闭开关(默认 false = 未禁用)
|
||||
3. **Auth 检查**:`hasVoiceAuth()` — 需要 Anthropic OAuth token(非 API key)
|
||||
|
||||
### 3.2 核心模块
|
||||
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| `src/voice/voiceModeEnabled.ts` | Feature flag + GrowthBook + Auth 三层门控 |
|
||||
| `src/hooks/useVoice.ts` | React hook 管理录音状态和 WebSocket 连接 |
|
||||
| `src/services/voiceStreamSTT.ts` | WebSocket 流式传输到 Anthropic STT |
|
||||
|
||||
### 3.3 数据流
|
||||
|
||||
```
|
||||
用户按下空格键
|
||||
│
|
||||
▼
|
||||
useVoice hook 激活
|
||||
│
|
||||
▼
|
||||
macOS 原生音频 / SoX 开始录音
|
||||
│
|
||||
▼
|
||||
WebSocket 连接到 Anthropic STT 端点
|
||||
│
|
||||
├──→ 中间转录结果 → 实时显示
|
||||
│
|
||||
▼
|
||||
用户释放空格键
|
||||
│
|
||||
▼
|
||||
停止录音,等待最终转录
|
||||
│
|
||||
▼
|
||||
转录文本 → 插入输入框 → 自动提交
|
||||
```
|
||||
|
||||
### 3.4 音频录制
|
||||
|
||||
支持两种音频后端:
|
||||
- **macOS 原生音频**:优先使用,低延迟
|
||||
- **SoX(Sound eXchange)**:回退方案,跨平台
|
||||
|
||||
音频流通过 WebSocket 发送到 Anthropic 的 Nova 3 STT 模型。
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **OAuth 独占**:语音模式使用 `voice_stream` 端点(claude.ai),仅 Anthropic OAuth 用户可用。API key、Bedrock、Vertex 用户无法使用
|
||||
2. **GrowthBook 负向门控**:`tengu_amber_quartz_disabled` 默认 `false`,新安装自动可用(无需等 GrowthBook 初始化)
|
||||
3. **Keychain 缓存**:`getClaudeAIOAuthTokens()` 首次调用访问 macOS keychain(~20-50ms),后续缓存命中
|
||||
4. **独立于主 feature flag**:`isVoiceGrowthBookEnabled()` 在 feature flag 关闭时短路返回 `false`,不触发任何模块加载
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 启用 feature
|
||||
FEATURE_VOICE_MODE=1 bun run dev
|
||||
|
||||
# 在 REPL 中使用
|
||||
# 1. 确保已通过 OAuth 登录(claude.ai 订阅)
|
||||
# 2. 按住空格键说话
|
||||
# 3. 释放空格键等待转录
|
||||
# 4. 或使用 /voice 命令切换开关
|
||||
```
|
||||
|
||||
## 六、外部依赖
|
||||
|
||||
| 依赖 | 说明 |
|
||||
|------|------|
|
||||
| Anthropic OAuth | claude.ai 订阅登录,非 API key |
|
||||
| GrowthBook | `tengu_amber_quartz_disabled` 紧急关闭 |
|
||||
| macOS 原生音频 或 SoX | 音频录制 |
|
||||
| Nova 3 STT | 语音转文本模型 |
|
||||
|
||||
## 七、文件索引
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/voice/voiceModeEnabled.ts` | 55 | 三层门控逻辑 |
|
||||
| `src/hooks/useVoice.ts` | — | React hook(录音状态 + WebSocket) |
|
||||
| `src/services/voiceStreamSTT.ts` | — | STT WebSocket 流式传输 |
|
||||
@@ -1,69 +0,0 @@
|
||||
# WEB_BROWSER_TOOL — 浏览器工具
|
||||
|
||||
> Feature Flag: `FEATURE_WEB_BROWSER_TOOL=1`
|
||||
> 实现状态:核心实现缺失,面板为 Stub,布线完整
|
||||
> 引用数:4
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
WEB_BROWSER_TOOL 让模型可以启动浏览器实例、导航网页、与页面元素交互。使用 Bun 的内置 WebView API 提供无头/有头浏览器能力。
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
### 2.1 模块状态
|
||||
|
||||
| 模块 | 文件 | 状态 |
|
||||
|------|------|------|
|
||||
| 浏览器面板 | `src/tools/WebBrowserTool/WebBrowserPanel.ts` | **Stub** — 返回 null |
|
||||
| 浏览器工具 | `src/tools/WebBrowserTool/WebBrowserTool.ts` | **缺失** |
|
||||
| REPL 集成 | `src/screens/REPL.tsx` | **布线** — 渲染 WebBrowserPanel |
|
||||
| 工具注册 | `src/tools.ts` | **布线** — 动态加载 |
|
||||
| WebView 检测 | `src/main.tsx` | **布线** — `'WebView' in Bun` 检测 |
|
||||
|
||||
### 2.2 预期数据流
|
||||
|
||||
```
|
||||
模型调用 WebBrowserTool
|
||||
│
|
||||
▼
|
||||
Bun WebView 创建浏览器实例
|
||||
│
|
||||
├── navigate(url) — 导航到 URL
|
||||
├── click(selector) — 点击元素
|
||||
├── screenshot() — 截取页面截图
|
||||
└── extract(selector) — 提取页面内容
|
||||
│
|
||||
▼
|
||||
结果返回给模型
|
||||
│
|
||||
▼
|
||||
WebBrowserPanel 在 REPL 侧边显示浏览器状态
|
||||
```
|
||||
|
||||
## 三、需要补全的内容
|
||||
|
||||
| 模块 | 工作量 | 说明 |
|
||||
|------|--------|------|
|
||||
| `WebBrowserTool.ts` | 大 | 工具 schema + Bun WebView API 执行 |
|
||||
| `WebBrowserPanel.tsx` | 中 | REPL 侧边栏浏览器状态面板 |
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **Bun WebView API**:使用 Bun 内置的 WebView 而非外部浏览器驱动(Puppeteer/Playwright)
|
||||
2. **REPL 侧边面板**:浏览器状态在 REPL 布局中独立渲染
|
||||
3. **Bun 特性检测**:`'WebView' in Bun` 检查运行时是否支持
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
FEATURE_WEB_BROWSER_TOOL=1 bun run dev
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/tools/WebBrowserTool/WebBrowserPanel.ts` | 面板组件(stub) |
|
||||
| `src/tools/WebBrowserTool/WebBrowserTool.ts` | 工具实现(缺失) |
|
||||
| `src/screens/REPL.tsx:273,4582` | 面板渲染 |
|
||||
| `src/tools.ts:115-116` | 工具注册 |
|
||||
@@ -1,186 +0,0 @@
|
||||
# WEB_SEARCH_TOOL — 网页搜索工具
|
||||
|
||||
> 实现状态:适配器架构完成,Bing 适配器为当前默认后端
|
||||
> 引用数:核心工具,无 feature flag 门控(始终启用)
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
WebSearchTool 让模型可以搜索互联网获取最新信息。原始实现仅支持 Anthropic API 服务端搜索(`web_search_20250305` server tool),在第三方代理端点下不可用。现已重构为适配器架构,新增 Bing 搜索页面解析作为 fallback,确保任何 API 端点都能使用搜索功能。
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
### 2.1 适配器模式
|
||||
|
||||
```
|
||||
WebSearchTool.call()
|
||||
│
|
||||
▼
|
||||
createAdapter() ← 适配器工厂
|
||||
│
|
||||
├── ApiSearchAdapter — Anthropic 官方 API 服务端搜索
|
||||
│ └── 使用 web_search_20250305 server tool
|
||||
│ 通过 queryModelWithStreaming 二次调用 API
|
||||
│
|
||||
└── BingSearchAdapter — Bing HTML 抓取 + 正则提取(当前默认)
|
||||
└── 直接抓取 Bing 搜索页 HTML
|
||||
正则提取 b_algo 块中的标题/URL/摘要
|
||||
```
|
||||
|
||||
### 2.2 模块结构
|
||||
|
||||
| 模块 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| 工具入口 | `src/tools/WebSearchTool/WebSearchTool.ts` | `buildTool()` 定义:schema、权限、执行、输出格式化 |
|
||||
| 工具 prompt | `src/tools/WebSearchTool/prompt.ts` | 搜索工具的系统提示词 |
|
||||
| UI 渲染 | `src/tools/WebSearchTool/UI.tsx` | 搜索结果的终端渲染组件 |
|
||||
| 适配器接口 | `src/tools/WebSearchTool/adapters/types.ts` | `WebSearchAdapter` 接口、`SearchResult`/`SearchOptions`/`SearchProgress` 类型 |
|
||||
| 适配器工厂 | `src/tools/WebSearchTool/adapters/index.ts` | `createAdapter()` 工厂函数,选择后端 |
|
||||
| API 适配器 | `src/tools/WebSearchTool/adapters/apiAdapter.ts` | 封装原有 `queryModelWithStreaming` 逻辑,使用 server tool |
|
||||
| Bing 适配器 | `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 抓取 + 正则解析 |
|
||||
| 单元测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 32 个测试用例 |
|
||||
| 集成测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 真实网络请求验证 |
|
||||
|
||||
### 2.3 数据流
|
||||
|
||||
```
|
||||
模型调用 WebSearchTool(query, allowed_domains, blocked_domains)
|
||||
│
|
||||
▼
|
||||
validateInput() — 校验 query 非空、allowed/block 不共存
|
||||
│
|
||||
▼
|
||||
createAdapter() → BingSearchAdapter(当前硬编码)
|
||||
│
|
||||
▼
|
||||
adapter.search(query, { allowedDomains, blockedDomains, signal, onProgress })
|
||||
│
|
||||
├── onProgress({ type: 'query_update', query })
|
||||
│
|
||||
├── axios.get(bing.com/search?q=...&setmkt=en-US)
|
||||
│ └── 13 个 Edge 浏览器请求头
|
||||
│
|
||||
├── extractBingResults(html) — 正则提取 <li class="b_algo"> 块
|
||||
│ ├── resolveBingUrl() — 解码 base64 重定向 URL
|
||||
│ ├── extractSnippet() — 三级降级摘要提取
|
||||
│ └── decodeHtmlEntities() — he.decode
|
||||
│
|
||||
├── 客户端域名过滤 (allowedDomains / blockedDomains)
|
||||
│
|
||||
├── onProgress({ type: 'search_results_received', resultCount })
|
||||
│
|
||||
▼
|
||||
格式化为 markdown 链接列表返回给模型
|
||||
```
|
||||
|
||||
## 三、Bing 适配器技术细节
|
||||
|
||||
### 3.1 反爬绕过
|
||||
|
||||
使用 13 个 Edge 浏览器请求头(含 `Sec-Ch-Ua`、`Sec-Fetch-*` 等),避免 Bing 返回 JS 渲染的空页面:
|
||||
|
||||
```typescript
|
||||
const BROWSER_HEADERS = {
|
||||
'User-Agent': '...Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0',
|
||||
'Sec-Ch-Ua': '"Microsoft Edge";v="131", "Chromium";v="131", ...',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
// ... 共 13 个标头
|
||||
}
|
||||
```
|
||||
|
||||
`setmkt=en-US` 参数强制美式英语市场,避免 IP 地理定位导致区域化结果。
|
||||
|
||||
### 3.2 URL 解码(`resolveBingUrl()`)
|
||||
|
||||
Bing 返回的重定向 URL 格式:`bing.com/ck/a?...&u=a1aHR0cHM6Ly9...`
|
||||
|
||||
- `u` 参数前 2 字符为协议前缀:`a1` = https,`a0` = http
|
||||
- 剩余部分为 base64url 编码的真实 URL
|
||||
- Bing 内部链接和相对路径被过滤返回 `undefined`
|
||||
|
||||
### 3.3 摘要提取(`extractSnippet()`)
|
||||
|
||||
三级降级策略:
|
||||
|
||||
1. `<p class="b_lineclamp...">` — Bing 的搜索摘要段落
|
||||
2. `<div class="b_caption">` 内的 `<p>` — 备选摘要位置
|
||||
3. `<div class="b_caption">` 直接文本 — 最终 fallback
|
||||
|
||||
### 3.4 域名过滤
|
||||
|
||||
客户端侧实现,支持子域名匹配:
|
||||
- `allowedDomains`:白名单,结果域名必须匹配列表中的某项(含子域名)
|
||||
- `blockedDomains`:黑名单,匹配的结果被过滤
|
||||
- 两者不可同时使用(`validateInput` 校验)
|
||||
|
||||
## 四、适配器选择逻辑
|
||||
|
||||
当前 `createAdapter()` 硬编码返回 `BingSearchAdapter`,原逻辑已注释保留:
|
||||
|
||||
```typescript
|
||||
export function createAdapter(): WebSearchAdapter {
|
||||
return new BingSearchAdapter()
|
||||
// 注释保留的选择逻辑:
|
||||
// 1. WEB_SEARCH_ADAPTER 环境变量强制指定 api|bing
|
||||
// 2. isFirstPartyAnthropicBaseUrl() → API 适配器
|
||||
// 3. 第三方端点 → Bing 适配器
|
||||
}
|
||||
```
|
||||
|
||||
恢复自动选择:取消 `index.ts` 中的注释即可。
|
||||
|
||||
## 五、接口定义
|
||||
|
||||
### WebSearchAdapter
|
||||
|
||||
```typescript
|
||||
interface WebSearchAdapter {
|
||||
search(query: string, options: SearchOptions): Promise<SearchResult[]>
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
title: string
|
||||
url: string
|
||||
snippet?: string
|
||||
}
|
||||
|
||||
interface SearchOptions {
|
||||
allowedDomains?: string[]
|
||||
blockedDomains?: string[]
|
||||
signal?: AbortSignal
|
||||
onProgress?: (progress: SearchProgress) => void
|
||||
}
|
||||
|
||||
interface SearchProgress {
|
||||
type: 'query_update' | 'search_results_received'
|
||||
query?: string
|
||||
resultCount?: number
|
||||
}
|
||||
```
|
||||
|
||||
### 工具 Input Schema
|
||||
|
||||
```typescript
|
||||
{
|
||||
query: string // 搜索关键词,最少 2 字符
|
||||
allowed_domains?: string[] // 域名白名单
|
||||
blocked_domains?: string[] // 域名黑名单
|
||||
}
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/tools/WebSearchTool/WebSearchTool.ts` | 工具定义入口 |
|
||||
| `src/tools/WebSearchTool/prompt.ts` | 搜索工具 prompt |
|
||||
| `src/tools/WebSearchTool/UI.tsx` | 终端 UI 渲染 |
|
||||
| `src/tools/WebSearchTool/adapters/types.ts` | 适配器接口 |
|
||||
| `src/tools/WebSearchTool/adapters/index.ts` | 适配器工厂 |
|
||||
| `src/tools/WebSearchTool/adapters/apiAdapter.ts` | API 服务端搜索适配器 |
|
||||
| `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 解析适配器 |
|
||||
| `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 单元测试 (32 cases) |
|
||||
| `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 集成测试 |
|
||||
| `src/tools.ts` | 工具注册 |
|
||||
@@ -1,105 +0,0 @@
|
||||
# WORKFLOW_SCRIPTS — 工作流自动化
|
||||
|
||||
> Feature Flag: `FEATURE_WORKFLOW_SCRIPTS=1`
|
||||
> 实现状态:全部 Stub(7 个文件),布线完整
|
||||
> 引用数:10
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
WORKFLOW_SCRIPTS 实现基于文件的多步自动化工作流。用户可以定义 YAML/JSON 格式的工作流描述文件,系统将其解析为可执行的多 agent 步骤序列。提供 `/workflows` 命令管理和触发工作流。
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
### 2.1 模块状态
|
||||
|
||||
| 模块 | 文件 | 状态 |
|
||||
|------|------|------|
|
||||
| WorkflowTool | `src/tools/WorkflowTool/WorkflowTool.ts` | **Stub** — 空对象 |
|
||||
| Workflow 权限 | `src/tools/WorkflowTool/WorkflowPermissionRequest.ts` | **Stub** — 返回 null |
|
||||
| 常量 | `src/tools/WorkflowTool/constants.ts` | **Stub** — 空工具名 |
|
||||
| 命令创建 | `src/tools/WorkflowTool/createWorkflowCommand.ts` | **Stub** — 空操作 |
|
||||
| 捆绑工作流 | `src/tools/WorkflowTool/bundled/` | **缺失** — 目录不存在 |
|
||||
| 本地工作流任务 | `src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts` | **Stub** — 类型 + 空操作 |
|
||||
| UI 任务组件 | `src/components/tasks/src/tasks/LocalWorkflowTask/` | **Stub** — 空导出 |
|
||||
| 详情对话框 | `src/components/tasks/WorkflowDetailDialog.ts` | **Stub** — 返回 null |
|
||||
| 任务注册 | `src/tasks.ts` | **布线** — 动态加载 |
|
||||
| 工具注册 | `src/tools.ts` | **布线** — 包含 bundled 工作流初始化 |
|
||||
| 命令注册 | `src/commands.ts` | **布线** — `/workflows` 命令 |
|
||||
|
||||
### 2.2 预期数据流
|
||||
|
||||
```
|
||||
用户定义工作流(YAML/JSON 文件)
|
||||
│
|
||||
▼
|
||||
/workflows 命令发现工作流文件
|
||||
│
|
||||
▼
|
||||
createWorkflowCommand() 解析为 Command 对象 [需要实现]
|
||||
│
|
||||
▼
|
||||
WorkflowTool 执行工作流 [需要实现]
|
||||
│
|
||||
├── 步骤 1: Agent({ task: "..." })
|
||||
├── 步骤 2: Agent({ task: "..." })
|
||||
└── 步骤 N: Agent({ task: "..." })
|
||||
│
|
||||
▼
|
||||
LocalWorkflowTask 协调步骤执行 [需要实现]
|
||||
│
|
||||
▼
|
||||
WorkflowDetailDialog 显示进度 [需要实现]
|
||||
```
|
||||
|
||||
### 2.3 预期工作流 DSL
|
||||
|
||||
```
|
||||
# workflow.yaml(预期格式,需要设计)
|
||||
name: "代码审查工作流"
|
||||
steps:
|
||||
- name: "静态分析"
|
||||
agent: { type: "general-purpose", prompt: "运行 lint 和类型检查" }
|
||||
- name: "测试"
|
||||
agent: { type: "general-purpose", prompt: "运行测试套件" }
|
||||
- name: "综合报告"
|
||||
agent: { type: "general-purpose", prompt: "综合分析结果写报告" }
|
||||
```
|
||||
|
||||
## 三、需要补全的内容
|
||||
|
||||
| 优先级 | 模块 | 工作量 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| 1 | `WorkflowTool.ts` | 大 | Schema 定义 + 多步执行引擎 |
|
||||
| 2 | `bundled/index.js` | 中 | 内置工作流定义(initBundledWorkflows) |
|
||||
| 3 | `createWorkflowCommand.ts` | 中 | 从文件解析创建命令对象 |
|
||||
| 4 | `LocalWorkflowTask.ts` | 大 | 步骤协调、kill/skip/retry |
|
||||
| 5 | `WorkflowDetailDialog.ts` | 中 | 进度详情 UI |
|
||||
| 6 | `WorkflowPermissionRequest.ts` | 小 | 权限对话框 |
|
||||
| 7 | `constants.ts` | 小 | 工具名常量 |
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
1. **基于文件的 DSL**:工作流定义为文件(YAML/JSON),版本控制友好
|
||||
2. **多 Agent 步骤**:每个步骤是独立的 agent 任务,支持并行/串行
|
||||
3. **内置工作流**:`bundled/` 目录提供开箱即用的常用工作流
|
||||
4. **/workflows 命令**:统一的发现和触发入口
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
```bash
|
||||
# 启用 feature(需要补全后才能真正使用)
|
||||
FEATURE_WORKFLOW_SCRIPTS=1 bun run dev
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/tools/WorkflowTool/WorkflowTool.ts` | 工具定义(stub) |
|
||||
| `src/tools/WorkflowTool/WorkflowPermissionRequest.ts` | 权限对话框(stub) |
|
||||
| `src/tools/WorkflowTool/constants.ts` | 常量(stub) |
|
||||
| `src/tools/WorkflowTool/createWorkflowCommand.ts` | 命令创建(stub) |
|
||||
| `src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts` | 任务协调(stub) |
|
||||
| `src/components/tasks/WorkflowDetailDialog.ts` | 详情对话框(stub) |
|
||||
| `src/tools.ts:127-132` | 工具注册 |
|
||||
| `src/commands.ts:86-89` | 命令注册 |
|
||||
@@ -11,13 +11,13 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic
|
||||
`USER_TYPE` 是一个构建时常量,通过 Bun 打包器的 `--define` 注入。在 Anthropic 的内部构建中它被设为 `'ant'`,在公开发布的版本中是 `'external'`:
|
||||
|
||||
```typescript
|
||||
// 反编译版本(src/types/global.d.ts 第 63 行)
|
||||
// Build-time constants BUILD_TARGET/BUILD_ENV/INTERFACE_TYPE — removed (zero runtime usage)
|
||||
// 反编译版本(src/entrypoints/cli.tsx 第 16 行)
|
||||
(globalThis as any).BUILD_TARGET = "external";
|
||||
```
|
||||
|
||||
`BUILD_TARGET` 等构建时常量在反编译版本中已被移除。`USER_TYPE` 通过 Bun 的 `--define` 或环境变量注入,Bun 会进行**常量折叠**——所有 `process.env.USER_TYPE === 'ant'` 在外部构建中直接变为 `false`,后续代码被 DCE 移除。但在反编译版本中,这些代码保留完整。
|
||||
由于这是编译时常量,Bun 会进行**常量折叠**——所有 `process.env.USER_TYPE === 'ant'` 在外部构建中直接变为 `false`,后续代码被 DCE 移除。但在反编译版本中,这些代码保留完整。
|
||||
|
||||
`USER_TYPE === 'ant'` 在代码库中出现 **377+ 次**(含 `=== 'ant'` 291 次、`(process.env.USER_TYPE) === 'ant'` 86 次),另有 `!== 'ant'` 53 次、其他引用约 35 次,总计 **465 处引用**,控制着工具、命令、API、UI 等方方面面。
|
||||
`USER_TYPE === 'ant'` 出现在代码库的 **60+ 个位置**,控制着工具、命令、API、UI 等方方面面。
|
||||
|
||||
## Ant-Only 工具
|
||||
|
||||
@@ -31,9 +31,7 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic
|
||||
| **TungstenTool** | `src/tools/TungstenTool/` | 基于 tmux 的终端面板工具(反编译版中已 stub) |
|
||||
|
||||
```typescript
|
||||
// src/tools.ts 第 14-24 行——条件导入 + Dead Code Elimination 标记
|
||||
// Dead code elimination: conditional import for ant-only tools
|
||||
/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
|
||||
// src/tools.ts 第 16-24 行
|
||||
const REPLTool =
|
||||
process.env.USER_TYPE === 'ant'
|
||||
? require('./tools/REPLTool/REPLTool.js').REPLTool
|
||||
@@ -47,7 +45,7 @@ const SuggestBackgroundPRTool =
|
||||
|
||||
## Ant-Only 命令
|
||||
|
||||
`src/commands.ts` 注册了 **28** 个仅在内部构建中可用的斜杠命令(`INTERNAL_ONLY_COMMANDS`,lines 225-254),在 `USER_TYPE === 'ant' && !IS_DEMO` 时才加载(line 343-345):
|
||||
`src/commands.ts` 注册了 25+ 个仅在内部构建中可用的斜杠命令:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="调试类">
|
||||
@@ -57,7 +55,6 @@ const SuggestBackgroundPRTool =
|
||||
- `env` — 显示环境变量
|
||||
- `mockLimits` — 模拟速率限制
|
||||
- `resetLimits` — 重置速率限制
|
||||
- `resetLimitsNonInteractive` — 重置速率限制(非交互式)
|
||||
</Accordion>
|
||||
<Accordion title="实验类">
|
||||
- `bughunter` — Bug 猎人模式
|
||||
@@ -72,9 +69,6 @@ const SuggestBackgroundPRTool =
|
||||
- `autofixPr` — 自动修复 PR 中的问题
|
||||
- `share` — 分享会话
|
||||
- `summary` — 生成摘要
|
||||
- `subscribePr` — 订阅 PR(需要 `KAIROS_GITHUB_WEBHOOKS` feature flag)
|
||||
- `forceSnip` — 强制截断历史(需要 `HISTORY_SNIP` feature flag)
|
||||
- `ultraplan` — 超级规划(需要 `ULTRAPLAN` feature flag)
|
||||
</Accordion>
|
||||
<Accordion title="基础设施类">
|
||||
- `backfillSessions` — 回填会话数据
|
||||
@@ -94,72 +88,30 @@ const SuggestBackgroundPRTool =
|
||||
|
||||
## Beta API Headers
|
||||
|
||||
Claude Code 向 API 发送的 beta headers 分布在 `src/constants/betas.ts`(主注册表)和其他文件中,按可见性分为以下几类:
|
||||
Claude Code 向 API 发送的 beta headers 也分为公开和内部两类:
|
||||
|
||||
### 公开 Headers(所有构建均发送)
|
||||
|
||||
| Header | 功能 | 额外条件 |
|
||||
|--------|------|----------|
|
||||
| `claude-code-20250219` | Claude Code 标识 | 非 Haiku 时始终发送;Haiku 在 agentic 模式下也发送 |
|
||||
| `effort-2025-11-24` | 推理强度控制 | 动态注入 |
|
||||
| `task-budgets-2026-03-13` | 任务预算 | 始终通过 `addAgenticBetas()` 注入 |
|
||||
| `fast-mode-2026-02-01` | 快速模式 | 通过 sticky-on latch 动态注入 |
|
||||
| `advisor-tool-2026-03-01` | 顾问工具 | 启用 advisor 时动态注入 |
|
||||
| `advanced-tool-use-2025-11-20` | 工具搜索(1P) | Claude API / Foundry |
|
||||
| `tool-search-tool-2025-10-19` | 工具搜索(3P) | Vertex / Bedrock |
|
||||
|
||||
### 模型能力相关(有条件发送)
|
||||
|
||||
| Header | 功能 | 条件 |
|
||||
|--------|------|------|
|
||||
| `interleaved-thinking-2025-05-14` | 交错思考模式 | 模型支持 ISP 且未禁用 |
|
||||
| `context-1m-2025-08-07` | 1M 上下文窗口 | 模型支持 1M context |
|
||||
| `context-management-2025-06-27` | 上下文管理 | Claude 4+ 或 ant 手动启用 |
|
||||
| `structured-outputs-2025-12-15` | 结构化输出 | Claude 4.5/4.6 + GrowthBook `tengu_tool_pear` |
|
||||
| `web-search-2025-03-05` | 网页搜索 | Vertex (Claude 4+) / Foundry |
|
||||
| `redact-thinking-2026-02-12` | 思维摘要/脱敏 | ISP 模型 + 非交互 + 未强制显示思维 |
|
||||
| `prompt-caching-scope-2026-01-05` | 提示缓存作用域 | firstParty/foundry + 全局缓存 |
|
||||
|
||||
### Ant-Only Headers
|
||||
|
||||
| Header | 功能 | 条件 |
|
||||
|--------|------|------|
|
||||
| **`cli-internal-2026-02-09`** | 内部 CLI 功能 | `USER_TYPE === 'ant'` + CLI 入口 |
|
||||
| **`token-efficient-tools-2026-03-28`** | Token 高效工具 | `USER_TYPE === 'ant'` + GrowthBook `tengu_amber_json_tools` |
|
||||
|
||||
### Feature Flag Gated
|
||||
|
||||
| Header | 功能 | 条件 |
|
||||
|--------|------|------|
|
||||
| **`afk-mode-2026-01-31`** | AFK 模式(离开键盘自动审批) | `feature('TRANSCRIPT_CLASSIFIER')` |
|
||||
|
||||
### 其他特殊 Headers
|
||||
|
||||
| Header | 功能 | 来源 |
|
||||
|--------|------|------|
|
||||
| `oauth-2025-04-20` | OAuth 订阅者标识 | `src/constants/oauth.ts`,Pro/Max/Team/Enterprise |
|
||||
| `environments-2025-11-01` | Bridge 环境 API | `src/bridge/bridgeApi.ts`,仅 Bridge 模式 |
|
||||
| Header | 功能 | 可见性 |
|
||||
|--------|------|--------|
|
||||
| `claude-code-20250219` | Claude Code 标识 | 公开 |
|
||||
| `interleaved-thinking-2025-05-14` | 交错思考模式 | 公开 |
|
||||
| `context-1m-2025-08-07` | 1M 上下文窗口 | 公开 |
|
||||
| `context-management-2025-06-27` | 上下文管理 | 公开 |
|
||||
| `web-search-2025-03-05` | 网页搜索 | 公开 |
|
||||
| `effort-2025-11-24` | 推理强度控制 | 公开 |
|
||||
| `fast-mode-2026-02-01` | 快速模式 | 公开 |
|
||||
| `token-efficient-tools-2026-03-28` | Token 高效工具 | 公开 |
|
||||
| `advisor-tool-2026-03-01` | 顾问工具 | 公开 |
|
||||
| **`cli-internal-2026-02-09`** | 内部 CLI 功能 | **Ant-Only** |
|
||||
| **`afk-mode-2026-01-31`** | AFK 模式(离开键盘自动审批) | **Feature Flag** |
|
||||
| **`summarize-connector-text-2026-03-13`** | 连接器文本摘要 | **Feature Flag** |
|
||||
|
||||
```typescript
|
||||
// src/constants/betas.ts — 常量定义
|
||||
export const TOKEN_EFFICIENT_TOOLS_BETA_HEADER =
|
||||
'token-efficient-tools-2026-03-28'
|
||||
// src/constants/betas.ts 第 29-30 行
|
||||
export const CLI_INTERNAL_BETA_HEADER =
|
||||
process.env.USER_TYPE === 'ant' ? 'cli-internal-2026-02-09' : ''
|
||||
```
|
||||
|
||||
```typescript
|
||||
// src/utils/betas.ts 第 315-321 行——TOKEN_EFFICIENT_TOOLS 的实际门控逻辑
|
||||
if (
|
||||
process.env.USER_TYPE === 'ant' &&
|
||||
includeFirstPartyOnlyBetas &&
|
||||
tokenEfficientToolsEnabled // GrowthBook 'tengu_amber_json_tools' flag
|
||||
) {
|
||||
betaHeaders.push(TOKEN_EFFICIENT_TOOLS_BETA_HEADER)
|
||||
}
|
||||
```
|
||||
|
||||
`cli-internal` header 意味着 Anthropic 的 API 服务端也维护着一套 ant-only 的服务端行为——这不仅仅是客户端的门控。`token-efficient-tools` 进一步需要 GrowthBook flag 开启,说明 Ant 员工内部也有分层灰度。
|
||||
`cli-internal` header 意味着 Anthropic 的 API 服务端也维护着一套 ant-only 的服务端行为——这不仅仅是客户端的门控。
|
||||
|
||||
## 内部代号体系
|
||||
|
||||
@@ -186,8 +138,6 @@ Anthropic 有浓厚的"动物命名"文化:
|
||||
- `DISABLE_AUTO_COMPACT` — 禁用自动压缩
|
||||
- `CLAUDE_CODE_DISABLE_AUTO_MEMORY` — 禁用自动记忆
|
||||
- `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS` — 禁用后台任务
|
||||
- `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` — 禁用实验性 beta headers
|
||||
- `USE_API_CONTEXT_MANAGEMENT` — 上下文管理工具清除(需 ant)
|
||||
</Accordion>
|
||||
<Accordion title="功能启用开关">
|
||||
- `CLAUDE_CODE_VERIFY_PLAN` — 启用 VerifyPlanExecutionTool
|
||||
@@ -201,7 +151,6 @@ Anthropic 有浓厚的"动物命名"文化:
|
||||
- `CLAUDE_CODE_COORDINATOR_MODE` — 启用 Coordinator 模式
|
||||
- `CLAUDE_INTERNAL_FC_OVERRIDES` — GrowthBook flag 覆盖(ant-only)
|
||||
- `IS_DEMO` — 演示模式(隐藏内部命令和敏感信息)
|
||||
- `CLAUDE_CODE_ENTRYPOINT` — 入口类型标识(`cli` | 其他)
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
---
|
||||
title: "GrowthBook 适配器 - 自定义 Feature Flag 服务器接入"
|
||||
description: "通过环境变量连接自定义 GrowthBook 服务器,实现远程 feature flag 控制。无配置时自动回退到代码默认值。"
|
||||
keywords: ["growthbook", "feature flags", "远程配置", "适配器", "环境变量"]
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
Claude Code 的 GrowthBook 系统支持通过环境变量连接自定义 GrowthBook 服务器,实现远程 feature flag 控制。
|
||||
|
||||
- **有配置时**:连接你的 GrowthBook 实例,拉取并缓存 feature 值
|
||||
- **无配置时**:所有 feature 读取直接返回代码中的默认值,零网络请求
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| `CLAUDE_GB_ADAPTER_URL` | 是 | GrowthBook API 地址,如 `https://gb.example.com/` |
|
||||
| `CLAUDE_GB_ADAPTER_KEY` | 是 | GrowthBook SDK Client Key,如 `sdk-xxxxx` |
|
||||
|
||||
两个变量都设置时启用适配器模式,否则完全跳过 GrowthBook。
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 基本用法
|
||||
|
||||
```bash
|
||||
CLAUDE_GB_ADAPTER_URL=https://gb.example.com/ \
|
||||
CLAUDE_GB_ADAPTER_KEY=sdk-abc123 \
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### 不使用 GrowthBook(默认行为)
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
# 所有 getFeatureValue_CACHED_MAY_BE_STALE("xxx", defaultValue) 直接返回 defaultValue
|
||||
```
|
||||
|
||||
## GrowthBook 服务端配置
|
||||
|
||||
### 步骤
|
||||
|
||||
1. **部署 GrowthBook 服务端**(Docker 自托管或 Cloud 版)
|
||||
2. **创建 Environment**(如 `production`)
|
||||
3. **创建 SDK Connection**,获得 SDK Key(即 `CLAUDE_GB_ADAPTER_KEY`)
|
||||
4. **按需添加 Feature**,key 和类型见下方列表
|
||||
|
||||
### 核心原则
|
||||
|
||||
- **不配置任何 feature 也能正常运行**——代码中每个调用都提供了默认值
|
||||
- 只创建你想远程控制的 feature,其余走代码默认
|
||||
- GrowthBook 上配了某个 feature 后,其值会覆盖代码中的默认值
|
||||
|
||||
## Feature Key 列表
|
||||
|
||||
### 高频使用
|
||||
|
||||
| Feature Key | 类型 | 代码默认值 | 用途 |
|
||||
|---|---|---|---|
|
||||
| `tengu_hive_evidence` | boolean | `false` | 任务证据系统 |
|
||||
| `tengu_quartz_lantern` | boolean | `false` | 文件写入/编辑保护 |
|
||||
| `tengu_auto_background_agents` | boolean | `false` | 自动后台 Agent |
|
||||
| `tengu_agent_list_attach` | boolean | `false` | Agent 列表附件 |
|
||||
| `tengu_amber_stoat` | boolean | `true` | 内置 Agents |
|
||||
| `tengu_slim_subagent_claudemd` | boolean | `true` | 子 Agent CLAUDE.md |
|
||||
| `tengu_attribution_header` | boolean | `true` | API 归因 Header |
|
||||
| `tengu_cobalt_harbor` | boolean | `false` | Bridge 模式 |
|
||||
| `tengu_ccr_bridge` | boolean | `false` | CCR Bridge |
|
||||
| `tengu_cicada_nap_ms` | number | `0` | 后台刷新节流(毫秒) |
|
||||
| `tengu_miraculo_the_bard` | boolean | `false` | 启动欢迎信息 |
|
||||
|
||||
### Agent / 工具控制
|
||||
|
||||
| Feature Key | 类型 | 代码默认值 | 用途 |
|
||||
|---|---|---|---|
|
||||
| `tengu_surreal_dali` | boolean | `false` | 远程触发工具 |
|
||||
| `tengu_glacier_2xr` | boolean | `false` | 工具搜索增强 |
|
||||
| `tengu_plum_vx3` | boolean | `false` | Web Search 使用 Haiku |
|
||||
| `tengu_destructive_command_warning` | boolean | `false` | 危险命令警告 |
|
||||
| `tengu_birch_trellis` | boolean | `true` | Bash 权限控制 |
|
||||
| `tengu_harbor_permissions` | boolean | `false` | Harbor 权限模式 |
|
||||
|
||||
### Bridge / 远程连接
|
||||
|
||||
| Feature Key | 类型 | 代码默认值 | 用途 |
|
||||
|---|---|---|---|
|
||||
| `tengu_bridge_repl_v2` | boolean | `false` | Bridge REPL v2 |
|
||||
| `tengu_copper_bridge` | boolean | `false` | Copper Bridge |
|
||||
| `tengu_ccr_mirror` | boolean | `false` | CCR Mirror |
|
||||
|
||||
### 内存 / 上下文
|
||||
|
||||
| Feature Key | 类型 | 代码默认值 | 用途 |
|
||||
|---|---|---|---|
|
||||
| `tengu_coral_fern` | boolean | `false` | 内存目录功能 |
|
||||
| `tengu_passport_quail` | boolean | `false` | 内存路径配置 |
|
||||
| `tengu_slate_thimble` | boolean | `false` | Slate Thimble |
|
||||
| `tengu_herring_clock` | boolean | `false` | 跳过索引 |
|
||||
| `tengu_session_memory` | boolean | `false` | 会话内存 |
|
||||
| `tengu_pebble_leaf_prune` | boolean | `false` | 内存修剪 |
|
||||
|
||||
### UI / 体验
|
||||
|
||||
| Feature Key | 类型 | 代码默认值 | 用途 |
|
||||
|---|---|---|---|
|
||||
| `tengu_terminal_sidebar` | boolean | `false` | 终端侧边栏 |
|
||||
| `tengu_terminal_panel` | boolean | `false` | 终端面板 |
|
||||
| `tengu_willow_mode` | boolean | `false` | Willow 模式 |
|
||||
| `tengu_collage_kaleidoscope` | boolean | `false` | UI 效果 |
|
||||
| `tengu_chrome_auto_enable` | boolean | `false` | Chrome 自动启用 |
|
||||
| `tengu_immediate_model_command` | boolean | `false` | 即时模型切换 |
|
||||
| `tengu_remote_backend` | boolean | `false` | 远程后端 |
|
||||
|
||||
### 配置对象(动态配置)
|
||||
|
||||
| Feature Key | 类型 | 代码默认值 | 用途 |
|
||||
|---|---|---|---|
|
||||
| `tengu_file_read_limits` | object | `null` | 文件读取限制配置 |
|
||||
| `tengu_cobalt_raccoon` | object | `null` | Cobalt 配置 |
|
||||
| `tengu_cobalt_lantern` | object | `null` | Lantern 配置 |
|
||||
| `tengu_desktop_upsell` | object | `null` | 桌面版引导 |
|
||||
| `tengu_marble_sandcastle` | object | `null` | Marble 配置 |
|
||||
| `tengu_marble_fox` | object | `null` | Marble Fox 配置 |
|
||||
| `tengu_ultraplan_model` | string | `null` | Ultraplan 模型名 |
|
||||
|
||||
### Gate(布尔门控)
|
||||
|
||||
| Gate Key | 代码默认值 | 用途 |
|
||||
|---|---|---|
|
||||
| `tengu_chair_sermon` | `false` | 功能门控 |
|
||||
| `tengu_scratch` | `false` | Scratch 功能 |
|
||||
| `tengu_thinkback` | `false` | Thinkback 功能 |
|
||||
| `tengu_tool_pear` | `false` | Tool Pear 功能 |
|
||||
|
||||
## 读取优先级链
|
||||
|
||||
每个 feature 的值按以下顺序解析,第一个命中即返回:
|
||||
|
||||
```
|
||||
1. CLAUDE_INTERNAL_FC_OVERRIDES 环境变量(JSON 对象覆盖)
|
||||
↓ 未命中
|
||||
2. growthBookOverrides 配置(~/.claude.json,仅 ant 构建)
|
||||
↓ 未命中
|
||||
3. 内存缓存(remoteEvalFeatureValues,本次进程从服务器拉取)
|
||||
↓ 未命中
|
||||
4. 磁盘缓存(~/.claude.json 的 cachedGrowthBookFeatures)
|
||||
↓ 未命中
|
||||
5. 代码中的 defaultValue 参数
|
||||
```
|
||||
|
||||
## 缓存与刷新机制
|
||||
|
||||
| 机制 | 说明 |
|
||||
|---|---|
|
||||
| **磁盘缓存** | `~/.claude.json` 的 `cachedGrowthBookFeatures` 字段,跨进程持久化 |
|
||||
| **周期刷新** | 每 6 小时自动从服务器拉取最新值(`setInterval` + `unref`) |
|
||||
| **初始化超时** | 首次连接超时 5 秒,超时后使用磁盘缓存或默认值 |
|
||||
| **Auth 变更** | 登录/登出时自动销毁并重建客户端 |
|
||||
|
||||
## 实现细节
|
||||
|
||||
修改了 2 个文件共 3 处:
|
||||
|
||||
1. **`src/constants/keys.ts`** — `getGrowthBookClientKey()` 优先读取 `CLAUDE_GB_ADAPTER_KEY`
|
||||
2. **`src/services/analytics/growthbook.ts`** — `isGrowthBookEnabled()` 适配器模式下直接启用
|
||||
3. **`src/services/analytics/growthbook.ts`** — base URL 优先使用 `CLAUDE_GB_ADAPTER_URL`
|
||||
|
||||
所有 130+ 个调用方文件无需修改。
|
||||
@@ -1,106 +0,0 @@
|
||||
---
|
||||
title: "自定义 Sentry 错误上报配置"
|
||||
description: "通过环境变量连接自托管或 Cloud Sentry,实现 CLI 运行时的错误捕获与上报。不配置则完全静默。"
|
||||
keywords: ["sentry", "错误上报", "监控", "DSN", "自托管"]
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
Claude Code 支持通过 Sentry 捕获运行时异常并上报到你自己指定的 Sentry 实例。
|
||||
|
||||
- **配置了 `SENTRY_DSN`**:自动初始化 Sentry SDK,捕获未处理异常和关键错误
|
||||
- **未配置**:所有 Sentry 调用均为 no-op,零开销
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| `SENTRY_DSN` | 是 | Sentry 项目 DSN,如 `https://xxx@o123456.ingest.sentry.io/789` |
|
||||
|
||||
只需要这一个变量,设置后即启用。
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 自托管 Sentry
|
||||
|
||||
```bash
|
||||
SENTRY_DSN=https://public_key@your-sentry.example.com/123 \
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### Sentry Cloud (SaaS)
|
||||
|
||||
```bash
|
||||
SENTRY_DSN=https://public_key@o123456.ingest.sentry.io/789 \
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### 不使用 Sentry(默认行为)
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
# SENTRY_DSN 未设置,所有 sentry 函数为 no-op
|
||||
```
|
||||
|
||||
## Sentry 服务端配置
|
||||
|
||||
### 步骤
|
||||
|
||||
1. **部署 Sentry 实例**(Docker 自托管 或 使用 [sentry.io](https://sentry.io) Cloud)
|
||||
2. **创建 Project**,选择 **Node.js** 平台
|
||||
3. 获取项目的 **DSN**(Settings → Projects → Client Keys → DSN)
|
||||
4. 将 DSN 设置为 `SENTRY_DSN` 环境变量
|
||||
|
||||
## 功能详情
|
||||
|
||||
### 错误捕获
|
||||
|
||||
- **自动捕获**:`SentryErrorBoundary` 包裹关键 React 组件,捕获渲染错误
|
||||
- **手动上报**:`errorLogSink` 在写入错误日志时同步上报到 Sentry
|
||||
- **优雅关闭**:进程退出时 `closeSentry()` 确保事件发送完毕(2s 超时)
|
||||
|
||||
### 安全过滤
|
||||
|
||||
`beforeSend` 钩子会自动剥离以下敏感 header:
|
||||
|
||||
- `authorization`
|
||||
- `x-api-key`
|
||||
- `cookie`
|
||||
- `set-cookie`
|
||||
|
||||
### 忽略的错误类型
|
||||
|
||||
以下错误模式会被忽略,不会上报:
|
||||
|
||||
| 错误 | 原因 |
|
||||
|---|---|
|
||||
| `ECONNREFUSED` / `ECONNRESET` / `ENOTFOUND` / `ETIMEDOUT` | 网络不可达,不可操作 |
|
||||
| `AbortError` / `The user aborted a request` | 用户主动取消 |
|
||||
| `CancelError` | 交互式取消信号 |
|
||||
|
||||
### 其他配置
|
||||
|
||||
- **采样率**:`sampleRate: 1.0`(捕获全部错误事件)
|
||||
- **面包屑上限**:`maxBreadcrumbs: 20`(控制 payload 体积)
|
||||
- **性能事务**:已关闭(`beforeSendTransaction` 返回 `null`),仅上报错误
|
||||
|
||||
## API
|
||||
|
||||
| 函数 | 说明 |
|
||||
|---|---|
|
||||
| `initSentry()` | 初始化 SDK,在 `src/entrypoints/init.ts` 中自动调用 |
|
||||
| `captureException(error, context?)` | 手动上报异常,可附加额外上下文 |
|
||||
| `setTag(key, value)` | 设置标签,用于 Sentry 面板分组过滤 |
|
||||
| `setUser({ id, email, username })` | 设置用户上下文,用于错误归因 |
|
||||
| `closeSentry(timeoutMs?)` | 刷出队列并关闭客户端,进程退出时调用 |
|
||||
| `isSentryInitialized()` | 检查是否已初始化 |
|
||||
|
||||
## 实现文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `src/utils/sentry.ts` | 核心 SDK 初始化与封装 |
|
||||
| `src/components/SentryErrorBoundary.ts` | React Error Boundary 组件 |
|
||||
| `src/utils/errorLogSink.ts` | 错误日志 sink,集成 `captureException` |
|
||||
| `src/utils/gracefulShutdown.ts` | 优雅退出,调用 `closeSentry()` |
|
||||
| `src/entrypoints/init.ts` | 启动时调用 `initSentry()` |
|
||||
@@ -1,264 +0,0 @@
|
||||
# LSP Integration
|
||||
|
||||
Claude Code 内置了 Language Server Protocol (LSP) 集成,提供代码智能功能(跳转定义、查找引用、悬停信息、文档符号等)和被动的诊断反馈。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装 LSP 插件
|
||||
|
||||
在 Claude Code REPL 中使用 `/plugin` 命令搜索并安装 LSP 插件:
|
||||
|
||||
```
|
||||
/plugin
|
||||
```
|
||||
|
||||
搜索 `lsp`,找到对应语言的插件(如 `typescript-lsp`),选择安装。
|
||||
|
||||
安装后运行 `/reload-plugins` 使插件生效。
|
||||
|
||||
LSP 插件安装后,后台的 LSP Server Manager 会自动加载并启动对应的语言服务器,无需手动配置。
|
||||
|
||||
### 2. 启用 LSP Tool
|
||||
|
||||
LSP Tool 需要通过环境变量显式启用,Claude 才能主动发起代码智能查询:
|
||||
|
||||
```bash
|
||||
ENABLE_LSP_TOOL=1 bun run dev
|
||||
```
|
||||
|
||||
不启用时,LSP 服务器仍然在后台运行并推送被动的诊断反馈(类型错误等)。
|
||||
|
||||
## 自动推荐
|
||||
|
||||
除了手动 `/plugin` 搜索安装外,Claude Code 会在编辑文件时自动检测:
|
||||
|
||||
1. 监听 `fileHistory.trackedFiles`,发现有新文件被编辑
|
||||
2. 扫描已安装的 marketplace,找到声明支持该文件扩展名的 LSP 插件
|
||||
3. 检查系统上是否已安装对应的 LSP 二进制(如 `typescript-language-server`)
|
||||
4. 满足条件时弹出推荐对话框,可选择安装
|
||||
|
||||
```
|
||||
┌───── LSP Plugin Recommendation ─────────────┐
|
||||
│ │
|
||||
│ LSP provides code intelligence like │
|
||||
│ go-to-definition and error checking │
|
||||
│ │
|
||||
│ Plugin: typescript-lsp │
|
||||
│ Triggered by: .ts files │
|
||||
│ │
|
||||
│ Would you like to install this LSP plugin? │
|
||||
│ │
|
||||
│ > Yes, install typescript-lsp │
|
||||
│ No, not now │
|
||||
│ Never for typescript-lsp │
|
||||
│ Disable all LSP recommendations │
|
||||
└───────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 30 秒不操作自动关闭(算作 "No")
|
||||
- 选 "Never" 不再推荐该插件
|
||||
- 选 "Disable" 关闭所有 LSP 推荐
|
||||
- 连续忽略 5 次后自动禁用推荐
|
||||
|
||||
## 架构概览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ LSP Tool │
|
||||
│ src/tools/LSPTool/LSPTool.ts │
|
||||
│ (Claude 可调用的工具,9 种操作) │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────▼──────────────────────────────┐
|
||||
│ LSP Server Manager (Singleton) │
|
||||
│ src/services/lsp/manager.ts │
|
||||
│ - initializeLspServerManager() │
|
||||
│ - reinitializeLspServerManager() │
|
||||
│ - shutdownLspServerManager() │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌──────────────────────▼──────────────────────────────┐
|
||||
│ LSP Server Manager (实例) │
|
||||
│ src/services/lsp/LSPServerManager.ts │
|
||||
│ - 管理多个 LSPServerInstance │
|
||||
│ - 按文件扩展名路由请求 │
|
||||
│ - 文件同步 (didOpen/didChange/didSave/didClose) │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ LSPServer │ │ LSPServer │ │ LSPServer │
|
||||
│ Instance │ │ Instance │ │ Instance │
|
||||
│ (typescript) │ │ (python) │ │ (rust...) │
|
||||
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
||||
│ │ │
|
||||
┌──────▼───────┐ ┌──────▼───────┐ ┌──────▼───────┐
|
||||
│ LSPClient │ │ LSPClient │ │ LSPClient │
|
||||
│ (JSON-RPC) │ │ (JSON-RPC) │ │ (JSON-RPC) │
|
||||
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
||||
│ │ │
|
||||
子进程 (stdio) 子进程 (stdio) 子进程 (stdio)
|
||||
```
|
||||
|
||||
### 被动诊断反馈
|
||||
|
||||
```
|
||||
LSP Server ──publishDiagnostics──▶ passiveFeedback.ts
|
||||
│
|
||||
▼
|
||||
LSPDiagnosticRegistry
|
||||
(去重、容量限制)
|
||||
│
|
||||
▼
|
||||
Attachment System
|
||||
(异步注入到对话)
|
||||
```
|
||||
|
||||
LSP 服务器会异步推送 `textDocument/publishDiagnostics` 通知,经去重和容量限制后作为 attachment 注入到 Claude 的对话上下文中。
|
||||
|
||||
## 核心模块
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/services/lsp/manager.ts` | 全局单例,初始化/重初始化/关闭生命周期管理 |
|
||||
| `src/services/lsp/LSPServerManager.ts` | 多服务器管理,按文件扩展名路由,文件同步 |
|
||||
| `src/services/lsp/LSPServerInstance.ts` | 单个 LSP 服务器实例生命周期(启动/停止/重启/健康检查) |
|
||||
| `src/services/lsp/LSPClient.ts` | JSON-RPC 通信层(基于 `vscode-jsonrpc`),子进程管理 |
|
||||
| `src/services/lsp/config.ts` | 从插件加载 LSP 服务器配置 |
|
||||
| `src/services/lsp/LSPDiagnosticRegistry.ts` | 诊断信息注册、去重、容量限制 |
|
||||
| `src/services/lsp/passiveFeedback.ts` | 注册 `publishDiagnostics` 通知处理器 |
|
||||
| `src/tools/LSPTool/LSPTool.ts` | LSP Tool 实现(暴露给 Claude) |
|
||||
| `src/tools/LSPTool/schemas.ts` | 输入 schema(9 种操作的 discriminated union) |
|
||||
| `src/tools/LSPTool/formatters.ts` | 各操作结果的格式化 |
|
||||
| `src/tools/LSPTool/prompt.ts` | Tool 描述文本 |
|
||||
| `src/utils/plugins/lspPluginIntegration.ts` | 从插件加载、验证、环境变量解析、作用域管理 |
|
||||
|
||||
## LSP Tool 支持的操作
|
||||
|
||||
| 操作 | LSP Method | 说明 |
|
||||
|------|-----------|------|
|
||||
| `goToDefinition` | `textDocument/definition` | 跳转到符号定义 |
|
||||
| `findReferences` | `textDocument/references` | 查找所有引用 |
|
||||
| `hover` | `textDocument/hover` | 获取悬停信息(文档、类型) |
|
||||
| `documentSymbol` | `textDocument/documentSymbol` | 获取文档内所有符号 |
|
||||
| `workspaceSymbol` | `workspace/symbol` | 全工作区符号搜索 |
|
||||
| `goToImplementation` | `textDocument/implementation` | 查找接口/抽象方法的实现 |
|
||||
| `prepareCallHierarchy` | `textDocument/prepareCallHierarchy` | 获取位置处的调用层级项 |
|
||||
| `incomingCalls` | `callHierarchy/incomingCalls` | 查找调用此函数的所有函数 |
|
||||
| `outgoingCalls` | `callHierarchy/outgoingCalls` | 查找此函数调用的所有函数 |
|
||||
|
||||
所有操作需要 `filePath`、`line`(1-based)和 `character`(1-based)参数。
|
||||
|
||||
## 插件开发:LSP 服务器配置
|
||||
|
||||
LSP 服务器通过插件提供。插件的 `manifest.json` 中可以声明 LSP 服务器,支持三种格式:
|
||||
|
||||
**1. 内联配置(在 manifest 中直接定义)**
|
||||
|
||||
```json
|
||||
{
|
||||
"lspServers": {
|
||||
"typescript": {
|
||||
"command": "typescript-language-server",
|
||||
"args": ["--stdio"],
|
||||
"extensionToLanguage": {
|
||||
".ts": "typescript",
|
||||
".tsx": "typescriptreact"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2. 引用外部 .lsp.json 文件**
|
||||
|
||||
```json
|
||||
{
|
||||
"lspServers": "path/to/.lsp.json"
|
||||
}
|
||||
```
|
||||
|
||||
**3. 数组混合格式**
|
||||
|
||||
```json
|
||||
{
|
||||
"lspServers": [
|
||||
"path/to/.lsp.json",
|
||||
{
|
||||
"another-server": { "command": "...", "extensionToLanguage": { "...": "..." } }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
也可以在插件目录下直接放置 `.lsp.json` 文件,无需在 manifest 中声明。
|
||||
|
||||
### LSP 服务器配置 Schema
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `command` | string | 是 | LSP 服务器可执行命令(不含空格) |
|
||||
| `args` | string[] | 否 | 命令行参数 |
|
||||
| `extensionToLanguage` | Record<string, string> | 是 | 文件扩展名到语言 ID 的映射(至少一个) |
|
||||
| `transport` | `"stdio"` \| `"socket"` | 否 | 通信方式,默认 `stdio` |
|
||||
| `env` | Record<string, string> | 否 | 启动服务器时设置的环境变量 |
|
||||
| `initializationOptions` | unknown | 否 | 传给服务器的初始化选项 |
|
||||
| `settings` | unknown | 否 | 通过 `workspace/didChangeConfiguration` 传递的设置 |
|
||||
| `workspaceFolder` | string | 否 | 工作区目录路径 |
|
||||
| `startupTimeout` | number | 否 | 启动超时时间(毫秒) |
|
||||
| `maxRestarts` | number | 否 | 最大重启次数(默认 3) |
|
||||
|
||||
### 环境变量替换
|
||||
|
||||
配置中的 `command`、`args`、`env`、`workspaceFolder` 支持:
|
||||
|
||||
- `${CLAUDE_PLUGIN_ROOT}` — 插件根目录
|
||||
- `${CLAUDE_PLUGIN_DATA}` — 插件数据目录
|
||||
- `${user_config.KEY}` — 用户在插件启用时配置的值
|
||||
- `${VAR}` — 系统环境变量
|
||||
|
||||
## 生命周期管理
|
||||
|
||||
### 服务器状态机
|
||||
|
||||
```
|
||||
stopped → starting → running
|
||||
running → stopping → stopped
|
||||
any → error (失败时)
|
||||
error → starting (重试时)
|
||||
```
|
||||
|
||||
### 崩溃恢复
|
||||
|
||||
- LSP 服务器崩溃时状态设为 `error`
|
||||
- 下次请求时自动尝试重启(通过 `ensureServerStarted`)
|
||||
- 超过 `maxRestarts`(默认 3)次后放弃
|
||||
|
||||
### 瞬态错误重试
|
||||
|
||||
- `ContentModified` 错误(LSP 错误码 -32801)会自动重试,最多 3 次
|
||||
- 使用指数退避:500ms → 1000ms → 2000ms
|
||||
- 常见于 rust-analyzer 等仍在索引项目的服务器
|
||||
|
||||
### 诊断信息容量限制
|
||||
|
||||
- 每个文件最多 10 条诊断
|
||||
- 总计最多 30 条诊断
|
||||
- 超出部分按严重性排序后截断(Error > Warning > Info > Hint)
|
||||
- 跨 turn 去重:已发送过的相同诊断不会重复发送
|
||||
- 文件编辑后清除该文件的已发送记录,允许新诊断通过
|
||||
|
||||
### 插件刷新
|
||||
|
||||
安装/卸载插件后使用 `/reload-plugins`,会调用 `reinitializeLspServerManager()`:
|
||||
1. 异步关闭旧服务器实例
|
||||
2. 重置状态为 `not-started`
|
||||
3. 调用 `initializeLspServerManager()` 重新加载插件配置
|
||||
|
||||
## 依赖
|
||||
|
||||
- `vscode-jsonrpc` — JSON-RPC 通信(懒加载,仅在实际创建服务器实例时才 require)
|
||||
- `vscode-languageserver-protocol` — LSP 协议类型
|
||||
- `vscode-languageserver-types` — LSP 类型定义
|
||||
- `lru-cache` — 诊断去重缓存
|
||||
@@ -1,421 +0,0 @@
|
||||
# OpenAI 协议兼容层
|
||||
|
||||
## 概述
|
||||
|
||||
claude-code 支持通过 OpenAI Chat Completions API(`/v1/chat/completions`)兼容任意 OpenAI 协议端点,包括 Ollama、DeepSeek、vLLM、One API、LiteLLM 等。
|
||||
|
||||
核心策略为**流适配器模式**:在 `queryModel()` 中插入提前返回分支,将 Anthropic 格式请求转为 OpenAI 格式,调用 OpenAI SDK,再将 SSE 流转换回 `BetaRawMessageStreamEvent` 格式。下游代码(流处理循环、query.ts、QueryEngine.ts、REPL)**完全不改**。
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 必需 | 说明 |
|
||||
|---|---|---|
|
||||
| `CLAUDE_CODE_USE_OPENAI` | 是 | 设为 `1` 启用 OpenAI 后端 |
|
||||
| `OPENAI_API_KEY` | 是 | API key(Ollama 等可设为任意值) |
|
||||
| `OPENAI_BASE_URL` | 推荐 | 端点 URL(如 `http://localhost:11434/v1`) |
|
||||
| `OPENAI_MODEL` | 可选 | 覆盖所有请求的模型名(跳过映射) |
|
||||
| `OPENAI_MODEL_MAP` | 可选 | JSON 映射,如 `{"claude-sonnet-4-6":"gpt-4o"}` |
|
||||
| `OPENAI_ORG_ID` | 可选 | Organization ID |
|
||||
| `OPENAI_PROJECT_ID` | 可选 | Project ID |
|
||||
|
||||
### 使用示例
|
||||
|
||||
```bash
|
||||
# Ollama
|
||||
CLAUDE_CODE_USE_OPENAI=1 \
|
||||
OPENAI_API_KEY=ollama \
|
||||
OPENAI_BASE_URL=http://localhost:11434/v1 \
|
||||
OPENAI_MODEL=qwen2.5-coder-32b \
|
||||
bun run dev
|
||||
|
||||
# DeepSeek(自动支持 Thinking)
|
||||
CLAUDE_CODE_USE_OPENAI=1 \
|
||||
OPENAI_API_KEY=sk-xxx \
|
||||
OPENAI_BASE_URL=https://api.deepseek.com/v1 \
|
||||
OPENAI_MODEL=deepseek-chat \
|
||||
bun run dev
|
||||
|
||||
# vLLM
|
||||
CLAUDE_CODE_USE_OPENAI=1 \
|
||||
OPENAI_API_KEY=token-abc123 \
|
||||
OPENAI_BASE_URL=http://localhost:8000/v1 \
|
||||
OPENAI_MODEL=Qwen/Qwen2.5-Coder-32B-Instruct \
|
||||
bun run dev
|
||||
|
||||
# One API / LiteLLM
|
||||
CLAUDE_CODE_USE_OPENAI=1 \
|
||||
OPENAI_API_KEY=sk-your-key \
|
||||
OPENAI_BASE_URL=https://your-one-api.example.com/v1 \
|
||||
OPENAI_MODEL=gpt-4o \
|
||||
bun run dev
|
||||
|
||||
# 自定义模型映射
|
||||
CLAUDE_CODE_USE_OPENAI=1 \
|
||||
OPENAI_API_KEY=sk-xxx \
|
||||
OPENAI_BASE_URL=https://my-gateway.example.com/v1 \
|
||||
OPENAI_MODEL_MAP='{"claude-sonnet-4-6":"gpt-4o-2024-11-20","claude-haiku-4-5":"gpt-4o-mini"}' \
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## 架构
|
||||
|
||||
### 请求流程
|
||||
|
||||
```
|
||||
queryModel() [claude.ts]
|
||||
├── 共享预处理(消息归一化、工具过滤、媒体裁剪)
|
||||
└── if (getAPIProvider() === 'openai')
|
||||
└── queryModelOpenAI() [openai/index.ts]
|
||||
├── resolveOpenAIModel() → 解析模型名
|
||||
├── normalizeMessagesForAPI() → 共享消息预处理
|
||||
├── toolToAPISchema() → 构建工具 schema
|
||||
├── anthropicMessagesToOpenAI() → 消息格式转换
|
||||
├── anthropicToolsToOpenAI() → 工具格式转换
|
||||
├── openai.chat.completions.create({ stream: true })
|
||||
└── adaptOpenAIStreamToAnthropic() → 流格式转换
|
||||
├── delta.reasoning_content → thinking 块
|
||||
├── delta.content → text 块
|
||||
├── delta.tool_calls → tool_use 块
|
||||
├── usage.cached_tokens → cache_read_input_tokens
|
||||
└── yield BetaRawMessageStreamEvent
|
||||
```
|
||||
|
||||
### 模型名解析优先级
|
||||
|
||||
`resolveOpenAIModel()` 的解析顺序:
|
||||
|
||||
1. `OPENAI_MODEL` 环境变量 → 直接使用,覆盖所有
|
||||
2. `OPENAI_MODEL_MAP` JSON 查表 → 自定义映射
|
||||
3. 内置默认映射(见下表)
|
||||
4. 以上都不匹配 → 原名透传
|
||||
|
||||
### 内置模型映射
|
||||
|
||||
| Anthropic 模型 | OpenAI 映射 |
|
||||
|---|---|
|
||||
| `claude-sonnet-4-6` | `gpt-4o` |
|
||||
| `claude-sonnet-4-5-20250929` | `gpt-4o` |
|
||||
| `claude-sonnet-4-20250514` | `gpt-4o` |
|
||||
| `claude-3-7-sonnet-20250219` | `gpt-4o` |
|
||||
| `claude-3-5-sonnet-20241022` | `gpt-4o` |
|
||||
| `claude-opus-4-6` | `o3` |
|
||||
| `claude-opus-4-5-20251101` | `o3` |
|
||||
| `claude-opus-4-1-20250805` | `o3` |
|
||||
| `claude-opus-4-20250514` | `o3` |
|
||||
| `claude-haiku-4-5-20251001` | `gpt-4o-mini` |
|
||||
| `claude-3-5-haiku-20241022` | `gpt-4o-mini` |
|
||||
|
||||
同时会自动剥离 `[1m]` 后缀(Claude 特有的 modifier)。
|
||||
|
||||
## 文件结构
|
||||
|
||||
### 新增文件
|
||||
|
||||
```
|
||||
src/services/api/openai/
|
||||
├── client.ts # OpenAI SDK 客户端工厂(~50 行)
|
||||
├── convertMessages.ts # Anthropic → OpenAI 消息格式转换(~190 行)
|
||||
├── convertTools.ts # Anthropic → OpenAI 工具格式转换(~70 行)
|
||||
├── streamAdapter.ts # SSE 流转换核心,含 thinking + caching(~270 行)
|
||||
├── modelMapping.ts # 模型名解析(~60 行)
|
||||
├── index.ts # 公共入口 queryModelOpenAI()(~110 行)
|
||||
└── __tests__/
|
||||
├── convertMessages.test.ts # 10 个测试
|
||||
├── convertTools.test.ts # 7 个测试
|
||||
├── modelMapping.test.ts # 6 个测试
|
||||
└── streamAdapter.test.ts # 14 个测试(含 thinking + caching)
|
||||
```
|
||||
|
||||
### 修改文件
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `src/utils/model/providers.ts` | 添加 `'openai'` provider 类型 + `CLAUDE_CODE_USE_OPENAI` 检查(最高优先级) |
|
||||
| `src/utils/model/configs.ts` | 每个 ModelConfig 添加 `openai` 键 |
|
||||
| `src/services/api/claude.ts` | 在 `stripExcessMediaItems()` 后插入 OpenAI 提前返回分支(~8 行) |
|
||||
| `package.json` | 添加 `"openai": "^4.73.0"` 依赖 |
|
||||
|
||||
## 消息转换规则
|
||||
|
||||
### Anthropic → OpenAI
|
||||
|
||||
| Anthropic | OpenAI |
|
||||
|---|---|
|
||||
| `system` prompt(`string[]`) | `role: "system"` 消息(`\n\n` 拼接) |
|
||||
| `user` + `text` 块 | `role: "user"` 消息 |
|
||||
| `assistant` + `text` 块 | `role: "assistant"` + `content` |
|
||||
| `assistant` + `tool_use` 块 | `role: "assistant"` + `tool_calls[]` |
|
||||
| `user` + `tool_result` 块 | `role: "tool"` + `tool_call_id` |
|
||||
| `thinking` 块 | 静默丢弃(请求侧) |
|
||||
|
||||
### 工具转换
|
||||
|
||||
| Anthropic | OpenAI |
|
||||
|---|---|
|
||||
| `{ name, description, input_schema }` | `{ type: "function", function: { name, description, parameters } }` |
|
||||
| `cache_control`, `defer_loading` 等字段 | 剥离 |
|
||||
| `tool_choice: { type: "auto" }` | `"auto"` |
|
||||
| `tool_choice: { type: "any" }` | `"required"` |
|
||||
| `tool_choice: { type: "tool", name }` | `{ type: "function", function: { name } }` |
|
||||
|
||||
### 消息转换示例
|
||||
|
||||
```
|
||||
Anthropic: OpenAI:
|
||||
[
|
||||
system: ["You are helpful."], [
|
||||
{ role: "system",
|
||||
{ role: "user", content: "You are helpful." },
|
||||
content: [ { role: "user",
|
||||
{ type: "text", text: "Run ls" } content: "Run ls"
|
||||
] },
|
||||
}, { role: "assistant",
|
||||
{ role: "assistant", content: "I'll check.",
|
||||
content: [ tool_calls: [{
|
||||
{ type: "text", text: "I'll check."}, id: "tu_123",
|
||||
{ type: "tool_use", type: "function",
|
||||
id: "tu_123", name: "bash", function: {
|
||||
input: { command: "ls" } } name: "bash",
|
||||
] arguments: '{"command":"ls"}'
|
||||
}, }] }
|
||||
{ role: "user", { role: "tool",
|
||||
content: [ tool_call_id: "tu_123",
|
||||
{ type: "tool_result", content: "file1\nfile2"
|
||||
tool_use_id: "tu_123", }
|
||||
content: "file1\nfile2" ]
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 流转换规则
|
||||
|
||||
### SSE Chunk → Anthropic Event 映射
|
||||
|
||||
| OpenAI Chunk | Anthropic Event |
|
||||
|---|---|
|
||||
| 首个 chunk | `message_start`(含 usage) |
|
||||
| `delta.reasoning_content` | `content_block_start(thinking)` + `thinking_delta` |
|
||||
| `delta.content` | `content_block_start(text)` + `text_delta` |
|
||||
| `delta.tool_calls` | `content_block_start(tool_use)` + `input_json_delta` |
|
||||
| `finish_reason: "stop"` | `message_delta(stop_reason: "end_turn")` |
|
||||
| `finish_reason: "tool_calls"` | `message_delta(stop_reason: "tool_use")` |
|
||||
| `finish_reason: "length"` | `message_delta(stop_reason: "max_tokens")` |
|
||||
|
||||
### 块顺序
|
||||
|
||||
当模型返回 `reasoning_content` 时(如 DeepSeek),块顺序与 Anthropic 一致:
|
||||
|
||||
```
|
||||
thinking block (index 0) ← delta.reasoning_content
|
||||
text block (index 1) ← delta.content
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```
|
||||
thinking block (index 0) ← delta.reasoning_content
|
||||
tool_use block (index 1) ← delta.tool_calls
|
||||
```
|
||||
|
||||
无 `reasoning_content` 时:
|
||||
|
||||
```
|
||||
text block (index 0) ← delta.content
|
||||
tool_use block (index 1) ← delta.tool_calls(如果有)
|
||||
```
|
||||
|
||||
### finish_reason 映射
|
||||
|
||||
| OpenAI | Anthropic |
|
||||
|---|---|
|
||||
| `stop` | `end_turn` |
|
||||
| `tool_calls` | `tool_use` |
|
||||
| `length` | `max_tokens` |
|
||||
| `content_filter` | `end_turn` |
|
||||
|
||||
### 事件序列示例
|
||||
|
||||
**纯文本响应**:
|
||||
```
|
||||
OpenAI chunks:
|
||||
delta.content = "Hello"
|
||||
delta.content = " world"
|
||||
finish_reason = "stop"
|
||||
|
||||
→ Anthropic events:
|
||||
message_start { message: { id, role: 'assistant', usage: {...} } }
|
||||
content_block_start { index: 0, content_block: { type: 'text' } }
|
||||
content_block_delta { index: 0, delta: { type: 'text_delta', text: 'Hello' } }
|
||||
content_block_delta { index: 0, delta: { type: 'text_delta', text: ' world' } }
|
||||
content_block_stop { index: 0 }
|
||||
message_delta { delta: { stop_reason: 'end_turn' } }
|
||||
message_stop
|
||||
```
|
||||
|
||||
**Thinking + 文本(DeepSeek 风格)**:
|
||||
```
|
||||
OpenAI chunks:
|
||||
delta.reasoning_content = "Let me think..."
|
||||
delta.reasoning_content = " step by step."
|
||||
delta.content = "The answer is 42."
|
||||
finish_reason = "stop"
|
||||
|
||||
→ Anthropic events:
|
||||
message_start { ... }
|
||||
content_block_start { index: 0, content_block: { type: 'thinking', signature: '' } }
|
||||
content_block_delta { index: 0, delta: { type: 'thinking_delta', thinking: 'Let me think...' } }
|
||||
content_block_delta { index: 0, delta: { type: 'thinking_delta', thinking: ' step by step.' } }
|
||||
content_block_stop { index: 0 }
|
||||
content_block_start { index: 1, content_block: { type: 'text' } }
|
||||
content_block_delta { index: 1, delta: { type: 'text_delta', text: 'The answer is 42.' } }
|
||||
content_block_stop { index: 1 }
|
||||
message_delta { delta: { stop_reason: 'end_turn' } }
|
||||
message_stop
|
||||
```
|
||||
|
||||
**工具调用**:
|
||||
```
|
||||
OpenAI chunks:
|
||||
delta.tool_calls[0] = { id: 'call_xxx', function: { name: 'bash', arguments: '' } }
|
||||
delta.tool_calls[0].function.arguments = '{"comm'
|
||||
delta.tool_calls[0].function.arguments = 'and":"ls"}'
|
||||
finish_reason = "tool_calls"
|
||||
|
||||
→ Anthropic events:
|
||||
message_start { ... }
|
||||
content_block_start { index: 0, content_block: { type: 'tool_use', id: 'call_xxx', name: 'bash' } }
|
||||
content_block_delta { index: 0, delta: { type: 'input_json_delta', partial_json: '{"comm' } }
|
||||
content_block_delta { index: 0, delta: { type: 'input_json_delta', partial_json: 'and":"ls"}' } }
|
||||
content_block_stop { index: 0 }
|
||||
message_delta { delta: { stop_reason: 'tool_use' } }
|
||||
message_stop
|
||||
```
|
||||
|
||||
## 功能支持
|
||||
|
||||
### Thinking(思维链)
|
||||
|
||||
**请求侧**:不需要显式配置。支持思维链的模型(DeepSeek 等)会自动返回 `delta.reasoning_content`。
|
||||
|
||||
**响应侧**:`delta.reasoning_content` 被转换为 Anthropic `thinking` content block:
|
||||
|
||||
```ts
|
||||
// content_block_start
|
||||
{ type: 'content_block_start', index: 0,
|
||||
content_block: { type: 'thinking', thinking: '', signature: '' } }
|
||||
|
||||
// content_block_delta
|
||||
{ type: 'content_block_delta', index: 0,
|
||||
delta: { type: 'thinking_delta', thinking: 'Let me analyze...' } }
|
||||
```
|
||||
|
||||
thinking block 在 text/tool_use block 之前自动关闭,保持 Anthropic 的块顺序。
|
||||
|
||||
### Prompt Caching
|
||||
|
||||
**请求侧**:OpenAI 端点使用自动缓存,无需显式设置 `cache_control`。
|
||||
|
||||
**响应侧**:OpenAI 的 `usage.prompt_tokens_details.cached_tokens` 被映射到 Anthropic 的 `cache_read_input_tokens`:
|
||||
|
||||
```
|
||||
OpenAI: usage.prompt_tokens_details.cached_tokens = 800
|
||||
↓
|
||||
Anthropic: message_start.message.usage.cache_read_input_tokens = 800
|
||||
```
|
||||
|
||||
在 `message_start` 的 usage 中报告缓存命中量。
|
||||
|
||||
### 工具调用(Tool Use)
|
||||
|
||||
完整支持 OpenAI function calling 格式。所有本地工具(Bash、FileEdit、Grep、Glob、Agent 等)透明工作——它们通过 JSON 输入输出通信,格式无关。
|
||||
|
||||
工具参数以 `input_json_delta` 形式流式传输,由下游代码拼接解析。
|
||||
|
||||
### 不支持的功能
|
||||
|
||||
| 功能 | 策略 |
|
||||
|---|---|
|
||||
| Beta Headers | 不发送 |
|
||||
| Server Tools (advisor) | 不发送 |
|
||||
| Structured Output | 不发送 |
|
||||
| Fast Mode / Effort | 不发送 |
|
||||
| Tool Search / defer_loading | 不启用,所有工具直接发送 |
|
||||
| Anthropic Signature | thinking block 的 `signature` 字段为空字符串 |
|
||||
| cache_creation_input_tokens | 始终为 0(OpenAI 不区分创建/读取) |
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
# 运行所有 OpenAI 适配层测试
|
||||
bun test src/services/api/openai/__tests__/
|
||||
|
||||
# 单独运行
|
||||
bun test src/services/api/openai/__tests__/streamAdapter.test.ts # 14 tests(含 thinking + caching)
|
||||
bun test src/services/api/openai/__tests__/convertMessages.test.ts # 10 tests
|
||||
bun test src/services/api/openai/__tests__/convertTools.test.ts # 7 tests
|
||||
bun test src/services/api/openai/__tests__/modelMapping.test.ts # 6 tests
|
||||
```
|
||||
|
||||
当前测试覆盖:**39 tests / 73 assertions / 0 fail**。
|
||||
|
||||
### 测试覆盖矩阵
|
||||
|
||||
| 功能 | convertMessages | convertTools | streamAdapter | modelMapping |
|
||||
|---|---|---|---|---|
|
||||
| 文本消息转换 | ✅ | | | |
|
||||
| tool_use 转换 | ✅ | | | |
|
||||
| tool_result 转换 | ✅ | | | |
|
||||
| thinking 剥离 | ✅ | | | |
|
||||
| 完整对话流程 | ✅ | | | |
|
||||
| 工具 schema 转换 | | ✅ | | |
|
||||
| tool_choice 映射 | | ✅ | | |
|
||||
| 纯文本流 | | | ✅ | |
|
||||
| 工具调用流 | | | ✅ | |
|
||||
| 混合文本+工具 | | | ✅ | |
|
||||
| finish_reason 映射 | | | ✅ | |
|
||||
| thinking 流 | | | ✅ | |
|
||||
| thinking+text 切换 | | | ✅ | |
|
||||
| thinking+tool_use 切换 | | | ✅ | |
|
||||
| 块索引正确性 | | | ✅ | |
|
||||
| cached_tokens 映射 | | | ✅ | |
|
||||
| OPENAI_MODEL 覆盖 | | | | ✅ |
|
||||
| 默认模型映射 | | | | ✅ |
|
||||
| 未知模型透传 | | | | ✅ |
|
||||
| [1m] 后缀剥离 | | | | ✅ |
|
||||
|
||||
## 端到端验证
|
||||
|
||||
```bash
|
||||
# 1. 安装依赖
|
||||
bun install
|
||||
|
||||
# 2. 运行单元测试
|
||||
bun test src/services/api/openai/__tests__/
|
||||
|
||||
# 3. 连接实际端点(以 Ollama 为例)
|
||||
CLAUDE_CODE_USE_OPENAI=1 \
|
||||
OPENAI_API_KEY=ollama \
|
||||
OPENAI_BASE_URL=http://localhost:11434/v1 \
|
||||
OPENAI_MODEL=qwen2.5-coder-32b \
|
||||
bun run dev
|
||||
|
||||
# 4. 连接 DeepSeek(测试 thinking 支持)
|
||||
CLAUDE_CODE_USE_OPENAI=1 \
|
||||
OPENAI_API_KEY=sk-xxx \
|
||||
OPENAI_BASE_URL=https://api.deepseek.com/v1 \
|
||||
OPENAI_MODEL=deepseek-reasoner \
|
||||
bun run dev
|
||||
|
||||
# 5. 确认现有测试不受影响
|
||||
bun test # 无 CLAUDE_CODE_USE_OPENAI 时走原有路径
|
||||
```
|
||||
|
||||
## 代码统计
|
||||
|
||||
| 类别 | 行数 |
|
||||
|---|---|
|
||||
| 新增源码 | ~620 行 |
|
||||
| 新增测试 | ~450 行 |
|
||||
| 改动现有代码 | ~25 行 |
|
||||
| **总计** | **~1100 行** |
|
||||
@@ -1,35 +0,0 @@
|
||||
# 社区项目 & Blog 合集
|
||||
|
||||
> 每日更新,欢迎自荐!
|
||||
|
||||
## 工具 & 应用
|
||||
|
||||
| 项目 | 描述 | 作者 |
|
||||
|------|------|------|
|
||||
| [4qtask.vercel.app](https://4qtask.vercel.app/) | 免费四象限时间管理工具 | @kevinhuky |
|
||||
| [kaying.studio](https://kaying.studio/) | 个人 AI 工具箱 | @kayingai |
|
||||
| [supsub.ai](https://supsub.ai/) | 高效阅读工具 | @hidumou |
|
||||
| [x-video-download.net](https://x-video-download.net/) | 视频下载工具 | @syakadou |
|
||||
| [1openapi.com](https://1openapi.com/) | API 中转站 | @thinker007 |
|
||||
| [claw-z.com](https://claw-z.com/) | 一键部署 OpenClaw AI Agent(场景驱动、全面管理) | @uhhc |
|
||||
| [gemini-watermark-remover.net](https://gemini-watermark-remover.net/) | Gemini 水印移除工具 | @syakadou |
|
||||
|
||||
## GitHub 开源项目
|
||||
|
||||
| 项目 | 描述 | 作者 |
|
||||
|------|------|------|
|
||||
| [VersperClaw](https://github.com/versperai/VersperClaw) | 全自动科研流 | @versperai |
|
||||
| [claude-reviews-claude](https://github.com/openedclaude/claude-reviews-claude) | 原汤化原食——Claude 如何看待眼中的老己 | @openedclaude |
|
||||
| [agentica](https://github.com/shibing624/agentica) | 自研 Agent 框架,借鉴 claude-code 多 Agent 处理 | @shibing624 |
|
||||
| [macman](https://github.com/tonngw/macman) | Mac 从 0 到 1 保姆级配置教程 | @tonngw |
|
||||
| [SuperSpec](https://github.com/asasugar/SuperSpec) | SDD / Spec-Driven Development | @asasugar |
|
||||
| [adnify](https://github.com/adnaan-worker/adnify) | 高颜值高定制化 AI 编辑器 | @adnaan-worker |
|
||||
| [another-rule-engine](https://github.com/eatmoreduck/another-rule-engine) | 基于 Groovy 的开源多功能决策引擎 | @eatmoreduck |
|
||||
| [creative_master](https://github.com/chatabc/creative_master) | AI 驱动的创意灵感管理工具 | @chatabc |
|
||||
| [RapidDoc](https://github.com/RapidAI/RapidDoc) | Office 文件解析工具转 Markdown(支持 PDF/Image/Word/PPT/Excel) | @hzkitt |
|
||||
|
||||
## Blog
|
||||
|
||||
| 链接 | 作者 |
|
||||
|------|------|
|
||||
| [blog.xiaohuangyu.space](https://blog.xiaohuangyu.space/) | @eatmoreduck |
|
||||
@@ -1,263 +0,0 @@
|
||||
---
|
||||
title: "Auto Mode - AI 分类器驱动的自主执行模式"
|
||||
description: "详解 Claude Code 的 auto mode:基于 transcript classifier 的自动权限决策、两阶段分类流水线、危险权限剥离机制、模式切换状态管理、以及与 plan mode 的协作方式。"
|
||||
keywords: ["auto mode", "yoloClassifier", "transcript classifier", "权限分类", "自动执行", "两阶段分类"]
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
Auto mode 是 Claude Code 的一种权限模式,让 AI 进入**连续自主执行**状态。与传统模式(每个敏感操作都弹出权限对话框等待用户审批)不同,auto mode 使用 AI 分类器(transcript classifier)自动判断每个工具调用是否安全,从而实现无中断的执行体验。
|
||||
|
||||
```
|
||||
权限模式层级:
|
||||
|
||||
default → auto → bypassPermissions
|
||||
(逐项确认) (AI 分类器审批) (全部放行)
|
||||
```
|
||||
|
||||
## 核心架构
|
||||
|
||||
### 1. AI 分类器(yoloClassifier)
|
||||
|
||||
分类器是 auto mode 的核心,位于 `src/utils/permissions/yoloClassifier.ts`。
|
||||
|
||||
每个工具调用经过分类器评估,返回三种裁决之一:
|
||||
|
||||
| 裁决 | 含义 | 处理方式 |
|
||||
|------|------|---------|
|
||||
| **allow** | 操作安全 | 直接执行,用户无感知 |
|
||||
| **deny** | 操作危险 | 拒绝执行,向 AI 返回拒绝消息 |
|
||||
| **ask** | 无法确定 | 回退到传统权限对话框 |
|
||||
|
||||
分类器的判断基于完整的对话上下文(transcript),而不仅仅看单条命令——它能理解操作的意图和上下文。
|
||||
|
||||
### 2. 两阶段分类流水线
|
||||
|
||||
分类器支持两阶段 XML 分类(`classifyYoloActionXml`):
|
||||
|
||||
```
|
||||
Stage 1 — "fast"(快速判断)
|
||||
├── max_tokens=64,stop_sequences 触发即时 yes/no
|
||||
├── 如果 allow → 直接放行(最快路径)
|
||||
└── 如果 block → 进入 Stage 2
|
||||
|
||||
Stage 2 — "thinking"(深度思考)
|
||||
├── chain-of-thought 推理
|
||||
├── 减少误报(false positives)
|
||||
└── 最终决定 allow / deny / ask
|
||||
```
|
||||
|
||||
两个阶段共享相同的 system prompt 和 user content,利用 API 的 prompt caching(1 小时 TTL)优化性能。
|
||||
|
||||
可通过配置选择模式:
|
||||
- `'both'`(默认)— 两阶段都跑
|
||||
- `'fast'` — 只跑 Stage 1
|
||||
- `'thinking'` — 只跑 Stage 2
|
||||
|
||||
### 3. 分类器结果类型
|
||||
|
||||
```typescript
|
||||
// src/types/permissions.ts
|
||||
type YoloClassifierResult = {
|
||||
thinking?: string // 分类器的推理过程
|
||||
shouldBlock: boolean // 是否阻止
|
||||
reason: string // 决策原因
|
||||
unavailable?: boolean // 分类器是否不可用
|
||||
transcriptTooLong?: boolean // 对话是否超出上下文窗口
|
||||
model: string // 使用的分类器模型
|
||||
stage?: 'fast' | 'thinking' // 哪个阶段做出的决定
|
||||
// ... token 使用量、耗时等监控字段
|
||||
}
|
||||
```
|
||||
|
||||
## 安全机制
|
||||
|
||||
### 危险权限剥离
|
||||
|
||||
进入 auto mode 时,系统调用 `stripDangerousPermissionsForAutoMode()`(`permissionSetup.ts:510`),移除所有可能绕过分类器的 allow 规则。
|
||||
|
||||
被剥离的规则类型(`dangerousPatterns.ts`):
|
||||
|
||||
| 规则类型 | 示例 | 剥离原因 |
|
||||
|---------|------|---------|
|
||||
| **Bash 代码执行** | `Bash(python:*)`, `Bash(node:*)` | 解释器可执行任意代码,绕过分类器审查 |
|
||||
| **Shell 入口** | `Bash(bash:*)`, `Bash(sh:*)` | 直接 shell 访问等同无限制 |
|
||||
| **Agent 规则** | `Agent(*)` | 任何 Agent allow 规则会绕过分类器审批子代理 |
|
||||
| **PowerShell 代码执行** | `PowerShell(node:*)` | 同 Bash 逻辑 |
|
||||
| **权限提升** | `Bash(sudo:*)`, `Bash(eval:*)` | 可执行任意命令 |
|
||||
|
||||
剥离的规则被暂存在 `strippedDangerousRules` 中,退出 auto mode 时通过 `restoreDangerousPermissions()` 恢复。
|
||||
|
||||
### 模型支持检测
|
||||
|
||||
不是所有模型都支持 auto mode。`modelSupportsAutoMode()`(`src/utils/betas.ts`)检查当前模型是否具备安全分类能力。不支持的模型无法进入 auto mode。
|
||||
|
||||
### Circuit Breaker 机制
|
||||
|
||||
`autoModeState.ts` 维护一个 circuit breaker 标志:
|
||||
|
||||
```typescript
|
||||
let autoModeCircuitBroken = false // 由远程配置控制
|
||||
```
|
||||
|
||||
当远程配置(GrowthBook `tengu_auto_mode_config.enabled`)设为 `'disabled'` 时,circuit breaker 触发,阻止 auto mode 的进入和继续使用。这为 Anthropic 提供了远程紧急关停能力。
|
||||
|
||||
## 模式切换状态管理
|
||||
|
||||
### 进入 Auto Mode
|
||||
|
||||
`transitionPermissionMode()`(`permissionSetup.ts:597`)处理所有模式切换:
|
||||
|
||||
```
|
||||
1. 检查 auto mode gate 是否开启(isAutoModeGateEnabled)
|
||||
2. 设置 autoModeActive = true
|
||||
3. 调用 stripDangerousPermissionsForAutoMode() 剥离危险规则
|
||||
4. 向对话注入 Auto Mode 系统提示
|
||||
```
|
||||
|
||||
### 退出 Auto Mode
|
||||
|
||||
```
|
||||
1. 设置 autoModeActive = false
|
||||
2. 设置 needsAutoModeExitAttachment = true(触发退出通知)
|
||||
3. 调用 restoreDangerousPermissions() 恢复被剥离的规则
|
||||
4. 向对话注入 "Exited Auto Mode" 提示
|
||||
```
|
||||
|
||||
### 触发路径
|
||||
|
||||
Auto mode 可通过以下方式激活:
|
||||
- CLI 参数 `--enable-auto-mode`
|
||||
- settings.json 中的 `autoMode` 配置
|
||||
- Plan mode 默认使用 auto mode 语义(`useAutoModeDuringPlan`,默认 true)
|
||||
- SDK 控制消息
|
||||
- REPL 中 Shift+Tab 切换
|
||||
|
||||
## 系统提示词
|
||||
|
||||
### 进入时(Full Instructions)
|
||||
|
||||
注入到对话中的指令(`messages.ts:3464`):
|
||||
|
||||
> Auto mode is active. The user chose continuous, autonomous execution. You should:
|
||||
>
|
||||
> 1. **Execute immediately** — 直接实现,做合理假设
|
||||
> 2. **Minimize interruptions** — 常规决策自行判断,减少提问
|
||||
> 3. **Prefer action over planning** — 默认直接编码,不进 plan mode
|
||||
> 4. **Expect course corrections** — 用户可随时纠正
|
||||
> 5. **Do not take overly destructive actions** — 删除数据/修改生产系统仍需确认
|
||||
> 6. **Avoid data exfiltration** — 不主动分享密钥/内部文档
|
||||
|
||||
### 持续运行时(Sparse Instructions)
|
||||
|
||||
后续轮次注入简短提醒:
|
||||
|
||||
> Auto mode still active. Execute autonomously, minimize interruptions, prefer action over planning.
|
||||
|
||||
### 退出时(Exit Instructions)
|
||||
|
||||
> You have exited auto mode. Ask clarifying questions when the approach is ambiguous rather than making assumptions.
|
||||
|
||||
## 与 Plan Mode 的协作
|
||||
|
||||
Plan mode 默认使用 auto mode 语义(`getUseAutoModeDuringPlan()`,默认 true)。这意味着:
|
||||
|
||||
- Plan mode 进入时,如果 auto mode 可用,也会激活分类器
|
||||
- `isAutoModeActive()` 是权威信号(`prePlanMode`/`strippedDangerousRules` 不可靠)
|
||||
- 退出 plan mode 时会同时退出 auto mode
|
||||
|
||||
## 分类器不可用的降级策略
|
||||
|
||||
当分类器 API 不可用时(`unavailable: true` 或 `transcriptTooLong: true`):
|
||||
|
||||
- 不会直接 allow — 回退到传统的权限对话框(ask)
|
||||
- 向 AI 发送消息:"{model} is temporarily unavailable, so auto mode cannot determine the safety of {toolName} right now."
|
||||
- 确定性错误(如对话过长)不重试,直接降级
|
||||
|
||||
## 分类器 Prompt 模板
|
||||
|
||||
分类器的行为由三个 prompt 文件控制,位于 `src/utils/permissions/yolo-classifier-prompts/`。这些文件在构建时通过 `require()` 内联为字符串常量,运行时不可修改。
|
||||
|
||||
### auto_mode_system_prompt.txt
|
||||
|
||||
主系统提示词,定义分类器的角色、分类流程和决策类别。包含:
|
||||
|
||||
- **分类流程**:理解操作 → 检查用户意图 → 评估风险
|
||||
- **BLOCK 始终阻止**:外部代码执行、不可逆删除、未授权持久化、安全削弱、提权、网络服务
|
||||
- **BLOCK 除非明确意图**:CWD 外写入、系统包管理、git push、大规模变更
|
||||
- **ALLOW 安全操作**:读文件、搜索、git 只读命令、测试/lint/构建、CWD 内编辑
|
||||
- `<permissions_template>` 占位符 — 运行时替换为具体权限模板(external 或 anthropic)
|
||||
- 末尾 `Use the classify_result tool to report your classification.` — XML 模式下被替换为 `<block>` 标签格式说明
|
||||
|
||||
### permissions_external.txt
|
||||
|
||||
外部用户版本的权限模板。三个 `<user_*_to_replace>` 标签内包裹默认规则(bullet 格式),用户自定义规则**整体替换**默认值:
|
||||
|
||||
```
|
||||
<user_allow_rules_to_replace>
|
||||
- 默认 allow 规则 1
|
||||
- 默认 allow 规则 2
|
||||
</user_allow_rules_to_replace>
|
||||
```
|
||||
|
||||
- **allow**:9 条默认规则(只读命令、版本检查、git 只读、测试/lint/构建、包安装、CWD 内文件操作等)
|
||||
- **soft_deny**:10 条默认规则(外部代码执行、递归删除、shell 配置修改、提权、网络服务等)
|
||||
- **environment**:4 条环境描述(终端环境、auto mode 上下文、开发工具可用、语言/框架不限)
|
||||
|
||||
`getDefaultExternalAutoModeRules()` 从此文件提取 bullet 项,供 `claude auto-mode defaults` 命令输出。
|
||||
|
||||
### permissions_anthropic.txt
|
||||
|
||||
Anthropic 内部版本的权限模板。默认规则在标签**外部**,标签内为空,用户自定义规则以**追加**方式叠加:
|
||||
|
||||
```
|
||||
- 默认规则(在标签外,始终生效)
|
||||
<user_allow_rules_to_replace>
|
||||
</user_allow_rules_to_replace>
|
||||
```
|
||||
|
||||
相比 external 版本,额外包含:
|
||||
- 云 CLI 只读命令(aws describe, gcloud describe, kubectl get 等)
|
||||
- 基础设施即代码 plan 命令(terraform plan, pulumi preview 等)
|
||||
- 对应的 deny 规则(云资源创建/修改/删除、IaC apply、生产环境访问等)
|
||||
|
||||
### 模板替换流程
|
||||
|
||||
```
|
||||
buildYoloSystemPrompt()
|
||||
├── BASE_PROMPT.replace('<permissions_template>', EXTERNAL/ANTHROPIC_TEMPLATE)
|
||||
├── .replace(<user_allow_rules_to_replace>, userAllow ?? defaults)
|
||||
├── .replace(<user_deny_rules_to_replace>, userDeny ?? defaults)
|
||||
└── .replace(<user_environment_to_replace>, userEnvironment ?? defaults)
|
||||
```
|
||||
|
||||
- 外部模板:用户设置非空时**替换**对应标签内容,否则保留默认值
|
||||
- 内部模板:用户设置**追加**到默认值之后(标签在末尾为空)
|
||||
|
||||
## 当前状态说明
|
||||
|
||||
> **注意**:auto mode 的完整代码逻辑已存在于代码库中,但依赖 `feature('TRANSCRIPT_CLASSIFIER')` feature flag。
|
||||
> 在当前反编译版本中,`feature()` 始终返回 `false`,因此 auto mode 不可用。
|
||||
> 要启用需将 `feature('TRANSCRIPT_CLASSIFIER')` 改为 `true`,并确保 GrowthBook 配置源有合理的 fallback 默认值。
|
||||
|
||||
Prompt 模板文件为**重建产物**——原始文件在反编译过程中丢失,已根据代码逻辑和 `yoloClassifier.ts` 中的替换模式重新编写。
|
||||
|
||||
## 相关源码索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/utils/permissions/yoloClassifier.ts` | 分类器核心实现 |
|
||||
| `src/utils/permissions/autoModeState.ts` | Auto mode 状态管理 |
|
||||
| `src/utils/permissions/permissionSetup.ts` | 模式切换、危险权限剥离 |
|
||||
| `src/utils/permissions/dangerousPatterns.ts` | 危险命令模式列表 |
|
||||
| `src/utils/permissions/classifierDecision.ts` | 分类器决策处理 |
|
||||
| `src/utils/permissions/classifierShared.ts` | 分类器共享逻辑 |
|
||||
| `src/utils/permissions/bashClassifier.ts` | Bash 命令分类规则 |
|
||||
| `src/utils/permissions/bypassPermissionsKillswitch.ts` | bypass 权限熔断器 |
|
||||
| `src/utils/permissions/yolo-classifier-prompts/auto_mode_system_prompt.txt` | 分类器主系统提示词 |
|
||||
| `src/utils/permissions/yolo-classifier-prompts/permissions_external.txt` | 外部权限模板 |
|
||||
| `src/utils/permissions/yolo-classifier-prompts/permissions_anthropic.txt` | 内部权限模板 |
|
||||
| `src/cli/handlers/autoMode.ts` | CLI `auto-mode` 子命令处理 |
|
||||
| `src/utils/messages.ts` | Auto mode 系统提示词注入 |
|
||||
| `src/types/permissions.ts` | 权限类型定义 |
|
||||
| `src/utils/betas.ts` | 模型 auto mode 支持检测 |
|
||||
@@ -1,155 +0,0 @@
|
||||
# 遥测与远程配置下发系统审计(除 Sentry 外)
|
||||
|
||||
## 1. Datadog 日志
|
||||
|
||||
**文件**: `src/services/analytics/datadog.ts`
|
||||
|
||||
- **端点**: 通过环境变量 `DATADOG_LOGS_ENDPOINT` 配置(默认为空,即禁用)
|
||||
- **客户端 token**: 通过环境变量 `DATADOG_API_KEY` 配置(默认为空,即禁用)
|
||||
- **行为**: 批量发送日志(15s flush 间隔,100 条上限),仅限 1P(直连 Anthropic API)用户
|
||||
- **事件白名单**: `tengu_*` 系列事件(启动、错误、OAuth、工具调用等 ~35 种)
|
||||
- **基线数据**: 收集 model、platform、arch、version、userBucket(用户 hash 到 30 个桶)等
|
||||
- **仅限**: `NODE_ENV === 'production'`
|
||||
- **配置示例**: `DATADOG_LOGS_ENDPOINT=https://http-intake.logs.datadoghq.com/api/v2/logs DATADOG_API_KEY=xxx bun run dev`
|
||||
|
||||
## 2. 1P 事件日志(BigQuery)
|
||||
|
||||
**文件**: `src/services/analytics/firstPartyEventLogger.ts` + `firstPartyEventLoggingExporter.ts`
|
||||
|
||||
- **端点**: `https://api.anthropic.com/api/event_logging/batch`(staging 可切换)
|
||||
- **行为**: 使用 OpenTelemetry SDK 的 `BatchLogRecordProcessor`,批量导出到 Anthropic 自有的 BQ 管道
|
||||
- **数据**: 完整事件 metadata(session、model、env context、用户数据、subscription type 等)
|
||||
- **弹性**: 本地磁盘持久化失败事件(JSONL),二次退避重试,最多 8 次尝试
|
||||
- **Proto schema**: 事件序列化为 `ClaudeCodeInternalEvent` / `GrowthbookExperimentEvent` protobuf 格式
|
||||
- **Auth fallback**: 401 时自动去掉 auth header 重试
|
||||
|
||||
## 3. GrowthBook 远程 Feature Flags / 动态配置
|
||||
|
||||
**文件**: `src/services/analytics/growthbook.ts`
|
||||
|
||||
- **服务端**: `https://api.anthropic.com/`(remote eval 模式)
|
||||
- **行为**: 启动时拉取全量 feature flags,每 6h(外部用户)/ 20min(ant)定时刷新
|
||||
- **磁盘缓存**: feature values 写入 `~/.claude.json` 的 `cachedGrowthBookFeatures`
|
||||
- **用途**:
|
||||
- 控制 Datadog 开关(`tengu_log_datadog_events`)
|
||||
- 控制事件采样率(`tengu_event_sampling_config`)
|
||||
- 控制 sink killswitch(`tengu_frond_boric`)
|
||||
- 控制 BQ batch 配置(`tengu_1p_event_batch_config`)
|
||||
- 控制版本上限/自动更新 kill switch
|
||||
- 控制远程管理设置的安全检查 gate
|
||||
- **用户属性**: 发送 deviceId, sessionId, organizationUUID, accountUUID, email, subscriptionType 等
|
||||
|
||||
## 4. Remote Managed Settings(企业远程配置下发)
|
||||
|
||||
**文件**: `src/services/remoteManagedSettings/index.ts`
|
||||
|
||||
- **端点**: `{BASE_API_URL}/api/claude_code/settings`
|
||||
- **行为**: 企业用户配置下发,支持 ETag/304 缓存,每小时后台轮询
|
||||
- **安全**: 变更包含"危险设置"时弹窗让用户确认
|
||||
- **适用**: API key 用户全部可拉取;OAuth 用户仅 Enterprise/C4E/Team
|
||||
- **Fail-open**: 请求失败时使用本地缓存,无缓存则跳过
|
||||
|
||||
## 5. Settings Sync(设置同步)
|
||||
|
||||
**文件**: `src/services/settingsSync/index.ts`
|
||||
|
||||
- **端点**: `{BASE_API_URL}/api/claude_code/user_settings`
|
||||
- **行为**: CLI 上传本地设置/memory 到远程;CCR 模式从远程下载
|
||||
- **同步内容**: userSettings、userMemory、projectSettings、projectMemory
|
||||
- **Feature gate**: `UPLOAD_USER_SETTINGS` / `DOWNLOAD_USER_SETTINGS`
|
||||
- **文件大小限制**: 500KB/文件
|
||||
|
||||
## 6. OpenTelemetry 三方遥测
|
||||
|
||||
**文件**: `src/utils/telemetry/instrumentation.ts`
|
||||
|
||||
- **行为**: 完整的 OTEL SDK 初始化,支持 metrics / logs / traces 三种信号
|
||||
- **协议**: gRPC / http-json / http-protobuf(通过 `OTEL_EXPORTER_OTLP_PROTOCOL` 选择)
|
||||
- **exporter**: console / otlp / prometheus
|
||||
- **触发**: `CLAUDE_CODE_ENABLE_TELEMETRY=1` 环境变量
|
||||
- **增强 trace**: `feature('ENHANCED_TELEMETRY_BETA')` + GrowthBook gate `enhanced_telemetry_beta`
|
||||
|
||||
## 7. BigQuery Metrics Exporter(内部指标)
|
||||
|
||||
**文件**: `src/utils/telemetry/bigqueryExporter.ts`
|
||||
|
||||
- **端点**: `https://api.anthropic.com/api/claude_code/metrics`
|
||||
- **行为**: 定期(5min 间隔)导出 OTel metrics 到内部 BQ
|
||||
- **适用**: API 客户、C4E/Team 订阅者
|
||||
- **组织级 opt-out**: 通过 `checkMetricsEnabled()` API 查询(见下方第 8 项)
|
||||
|
||||
## 8. 组织级 Metrics Opt-out 查询
|
||||
|
||||
**文件**: `src/services/api/metricsOptOut.ts`
|
||||
|
||||
- **端点**: `https://api.anthropic.com/api/claude_code/organizations/metrics_enabled`
|
||||
- **行为**: 查询组织是否启用了 metrics,二级缓存(内存 1h + 磁盘 24h)
|
||||
- **作用**: 控制 BigQuery metrics exporter 是否导出
|
||||
|
||||
## 9. Startup Profiling
|
||||
|
||||
**文件**: `src/utils/startupProfiler.ts`
|
||||
|
||||
- **行为**: 采样启动性能数据(100% ant / 0.5% 外部),通过 `logEvent('tengu_startup_perf')` 上报
|
||||
- **详细模式**: `CLAUDE_CODE_PROFILE_STARTUP=1` 输出完整性能报告到文件
|
||||
|
||||
## 10. Beta Session Tracing
|
||||
|
||||
**文件**: `src/utils/telemetry/betaSessionTracing.ts`
|
||||
|
||||
- **行为**: 详细调试 trace,发送 system prompt、model output、tool schema 等
|
||||
- **触发**: `ENABLE_BETA_TRACING_DETAILED=1` + `BETA_TRACING_ENDPOINT`
|
||||
- **外部用户**: SDK/headless 模式自动启用,交互模式需要 GrowthBook gate `tengu_trace_lantern`
|
||||
|
||||
## 11. Bridge Poll Config(远程轮询间隔配置)
|
||||
|
||||
**文件**: `src/bridge/pollConfig.ts`
|
||||
|
||||
- **行为**: 从 GrowthBook 拉取 bridge 轮询间隔配置(`tengu_bridge_poll_interval_config`)
|
||||
- **控制**: 单会话和多会话的各种 poll interval
|
||||
|
||||
## 12. Plugin/MCP 遥测
|
||||
|
||||
**文件**: `src/utils/plugins/fetchTelemetry.ts`
|
||||
|
||||
- **行为**: 记录 plugin/marketplace 的网络请求(安装计数、marketplace clone/pull 等)
|
||||
- **事件**: `tengu_plugin_remote_fetch`,包含 host(已脱敏)、outcome、duration
|
||||
|
||||
---
|
||||
|
||||
## 全局禁用方式
|
||||
|
||||
```bash
|
||||
# 禁用所有遥测(Datadog + 1P + 调查问卷)
|
||||
DISABLE_TELEMETRY=1
|
||||
|
||||
# 更激进:禁用所有非必要网络(包括自动更新、grove、release notes 等)
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
|
||||
|
||||
# 3P 提供商自动禁用
|
||||
CLAUDE_CODE_USE_BEDROCK=1 # 或 VERTEX/FOUNDRY
|
||||
```
|
||||
|
||||
`src/utils/privacyLevel.ts` 是集中控制点,三个级别:`default < no-telemetry < essential-traffic`。
|
||||
|
||||
---
|
||||
|
||||
## 数据流架构
|
||||
|
||||
```
|
||||
用户操作 → logEvent()
|
||||
↓
|
||||
sink.ts (路由层)
|
||||
↙ ↘
|
||||
trackDatadogEvent() logEventTo1P()
|
||||
↓ ↓
|
||||
Datadog HTTP API OTel BatchLogRecordProcessor
|
||||
(us5.datadoghq.com) ↓
|
||||
FirstPartyEventLoggingExporter
|
||||
↓
|
||||
api.anthropic.com/api/event_logging/batch
|
||||
↓
|
||||
BigQuery (ClaudeCodeInternalEvent proto)
|
||||
```
|
||||
|
||||
GrowthBook 作为独立通道,同时驱动上述两个 sink 的开关和配置。
|
||||
@@ -1,147 +0,0 @@
|
||||
# Tool 系统测试计划
|
||||
|
||||
## 概述
|
||||
|
||||
Tool 系统是 Claude Code 的核心,负责工具的定义、注册、发现和过滤。本计划覆盖 `src/Tool.ts` 中的工具接口与工具函数、`src/tools.ts` 中的注册/过滤逻辑,以及各工具目录下可独立测试的纯函数。
|
||||
|
||||
## 被测文件
|
||||
|
||||
| 文件 | 关键导出 |
|
||||
|------|----------|
|
||||
| `src/Tool.ts` | `buildTool`, `toolMatchesName`, `findToolByName`, `getEmptyToolPermissionContext`, `filterToolProgressMessages` |
|
||||
| `src/tools.ts` | `parseToolPreset`, `filterToolsByDenyRules`, `getAllBaseTools`, `getTools`, `assembleToolPool` |
|
||||
| `src/tools/shared/gitOperationTracking.ts` | `parseGitCommitId`, `detectGitOperation` |
|
||||
| `src/tools/shared/spawnMultiAgent.ts` | `resolveTeammateModel`, `generateUniqueTeammateName` |
|
||||
| `src/tools/GrepTool/GrepTool.ts` | `applyHeadLimit`, `formatLimitInfo`(内部辅助函数) |
|
||||
| `src/tools/FileEditTool/utils.ts` | 字符串匹配/补丁相关纯函数 |
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### src/Tool.ts
|
||||
|
||||
#### describe('buildTool')
|
||||
|
||||
- test('fills in default isEnabled as true') — 不传 isEnabled 时,构建的 tool.isEnabled() 应返回 true
|
||||
- test('fills in default isConcurrencySafe as false') — 默认值应为 false(fail-closed)
|
||||
- test('fills in default isReadOnly as false') — 默认假设有写操作
|
||||
- test('fills in default isDestructive as false') — 默认非破坏性
|
||||
- test('fills in default checkPermissions as allow') — 默认 checkPermissions 应返回 `{ behavior: 'allow', updatedInput }`
|
||||
- test('fills in default userFacingName from tool name') — userFacingName 默认应返回 tool.name
|
||||
- test('preserves explicitly provided methods') — 传入自定义 isEnabled 等方法时应覆盖默认值
|
||||
- test('preserves all non-defaultable properties') — name, inputSchema, call, description 等属性原样保留
|
||||
|
||||
#### describe('toolMatchesName')
|
||||
|
||||
- test('returns true for exact name match') — `{ name: 'Bash' }` 匹配 'Bash'
|
||||
- test('returns false for non-matching name') — `{ name: 'Bash' }` 不匹配 'Read'
|
||||
- test('returns true when name matches an alias') — `{ name: 'Bash', aliases: ['BashTool'] }` 匹配 'BashTool'
|
||||
- test('returns false when aliases is undefined') — `{ name: 'Bash' }` 不匹配 'BashTool'
|
||||
- test('returns false when aliases is empty') — `{ name: 'Bash', aliases: [] }` 不匹配 'BashTool'
|
||||
|
||||
#### describe('findToolByName')
|
||||
|
||||
- test('finds tool by primary name') — 从 tools 列表中按 name 找到工具
|
||||
- test('finds tool by alias') — 从 tools 列表中按 alias 找到工具
|
||||
- test('returns undefined when no match') — 找不到时返回 undefined
|
||||
- test('returns first match when duplicates exist') — 多个同名工具时返回第一个
|
||||
|
||||
#### describe('getEmptyToolPermissionContext')
|
||||
|
||||
- test('returns default permission mode') — mode 应为 'default'
|
||||
- test('returns empty maps and arrays') — additionalWorkingDirectories 为空 Map,rules 为空对象
|
||||
- test('returns isBypassPermissionsModeAvailable as false')
|
||||
|
||||
#### describe('filterToolProgressMessages')
|
||||
|
||||
- test('filters out hook_progress messages') — 移除 type 为 hook_progress 的消息
|
||||
- test('keeps tool progress messages') — 保留非 hook_progress 的消息
|
||||
- test('returns empty array for empty input')
|
||||
- test('handles messages without type field') — data 不含 type 时应保留
|
||||
|
||||
---
|
||||
|
||||
### src/tools.ts
|
||||
|
||||
#### describe('parseToolPreset')
|
||||
|
||||
- test('returns "default" for "default" input') — 精确匹配
|
||||
- test('returns "default" for "Default" input') — 大小写不敏感
|
||||
- test('returns null for unknown preset') — 未知字符串返回 null
|
||||
- test('returns null for empty string')
|
||||
|
||||
#### describe('filterToolsByDenyRules')
|
||||
|
||||
- test('returns all tools when no deny rules') — 空 deny 规则不过滤任何工具
|
||||
- test('filters out tools matching blanket deny rule') — deny rule `{ toolName: 'Bash' }` 应移除 Bash
|
||||
- test('does not filter tools with content-specific deny rules') — deny rule `{ toolName: 'Bash', ruleContent: 'rm -rf' }` 不移除 Bash(只在运行时阻止特定命令)
|
||||
- test('filters MCP tools by server name prefix') — deny rule `mcp__server` 应移除该 server 下所有工具
|
||||
- test('preserves tools not matching any deny rule')
|
||||
|
||||
#### describe('getAllBaseTools')
|
||||
|
||||
- test('returns a non-empty array of tools') — 至少包含核心工具
|
||||
- test('each tool has required properties') — 每个工具应有 name, inputSchema, call 等属性
|
||||
- test('includes BashTool, FileReadTool, FileEditTool') — 核心工具始终存在
|
||||
- test('includes TestingPermissionTool when NODE_ENV is test') — 需设置 env
|
||||
|
||||
#### describe('getTools')
|
||||
|
||||
- test('returns filtered tools based on permission context') — 根据 deny rules 过滤
|
||||
- test('returns simple tools in CLAUDE_CODE_SIMPLE mode') — 仅返回 Bash/Read/Edit
|
||||
- test('filters disabled tools via isEnabled') — isEnabled 返回 false 的工具被排除
|
||||
|
||||
---
|
||||
|
||||
### src/tools/shared/gitOperationTracking.ts
|
||||
|
||||
#### describe('parseGitCommitId')
|
||||
|
||||
- test('extracts commit hash from git commit output') — 从 `[main abc1234] message` 中提取 `abc1234`
|
||||
- test('returns null for non-commit output') — 无法解析时返回 null
|
||||
- test('handles various branch name formats') — `[feature/foo abc1234]` 等
|
||||
|
||||
#### describe('detectGitOperation')
|
||||
|
||||
- test('detects git commit operation') — 命令含 `git commit` 时识别为 commit
|
||||
- test('detects git push operation') — 命令含 `git push` 时识别
|
||||
- test('returns null for non-git commands') — 非 git 命令返回 null
|
||||
- test('detects git merge operation')
|
||||
- test('detects git rebase operation')
|
||||
|
||||
---
|
||||
|
||||
### src/tools/shared/spawnMultiAgent.ts
|
||||
|
||||
#### describe('resolveTeammateModel')
|
||||
|
||||
- test('returns specified model when provided')
|
||||
- test('falls back to default model when not specified')
|
||||
|
||||
#### describe('generateUniqueTeammateName')
|
||||
|
||||
- test('generates a name when no existing names') — 无冲突时返回基础名
|
||||
- test('appends suffix when name conflicts') — 与已有名称冲突时添加后缀
|
||||
- test('handles multiple conflicts') — 多次冲突时递增后缀
|
||||
|
||||
---
|
||||
|
||||
## Mock 需求
|
||||
|
||||
| 依赖 | Mock 方式 | 说明 |
|
||||
|------|-----------|------|
|
||||
| `bun:bundle` (feature) | 已 polyfill 为 `() => false` | 不需额外 mock |
|
||||
| `process.env` | `bun:test` mock | 测试 `USER_TYPE`、`NODE_ENV`、`CLAUDE_CODE_SIMPLE` |
|
||||
| `getDenyRuleForTool` | mock module | `filterToolsByDenyRules` 测试中需控制返回值 |
|
||||
| `isToolSearchEnabledOptimistic` | mock module | `getAllBaseTools` 中条件加载 |
|
||||
|
||||
## 集成测试场景
|
||||
|
||||
放在 `tests/integration/tool-chain.test.ts`:
|
||||
|
||||
### describe('Tool registration and discovery')
|
||||
|
||||
- test('getAllBaseTools returns tools that can be found by findToolByName') — 注册 → 查找完整链路
|
||||
- test('filterToolsByDenyRules + getTools produces consistent results') — 过滤管线一致性
|
||||
- test('assembleToolPool deduplicates built-in and MCP tools') — 合并去重逻辑
|
||||
@@ -1,416 +0,0 @@
|
||||
# 工具函数(纯函数)测试计划
|
||||
|
||||
## 概述
|
||||
|
||||
覆盖 `src/utils/` 下所有可独立单元测试的纯函数。这些函数无外部依赖,输入输出确定性强,是测试金字塔的底层基石。
|
||||
|
||||
## 被测文件
|
||||
|
||||
| 文件 | 状态 | 关键导出 |
|
||||
|------|------|----------|
|
||||
| `src/utils/array.ts` | **已有测试** | intersperse, count, uniq |
|
||||
| `src/utils/set.ts` | **已有测试** | difference, intersects, every, union |
|
||||
| `src/utils/xml.ts` | 待测 | escapeXml, escapeXmlAttr |
|
||||
| `src/utils/hash.ts` | 待测 | djb2Hash, hashContent, hashPair |
|
||||
| `src/utils/stringUtils.ts` | 待测 | escapeRegExp, capitalize, plural, firstLineOf, countCharInString, normalizeFullWidthDigits, normalizeFullWidthSpace, safeJoinLines, truncateToLines, EndTruncatingAccumulator |
|
||||
| `src/utils/semver.ts` | 待测 | gt, gte, lt, lte, satisfies, order |
|
||||
| `src/utils/uuid.ts` | 待测 | validateUuid, createAgentId |
|
||||
| `src/utils/format.ts` | 待测 | formatFileSize, formatSecondsShort, formatDuration, formatNumber, formatTokens, formatRelativeTime, formatRelativeTimeAgo |
|
||||
| `src/utils/json.ts` | 待测 | safeParseJSON, safeParseJSONC, parseJSONL, addItemToJSONCArray |
|
||||
| `src/utils/truncate.ts` | 待测 | truncatePathMiddle, truncateToWidth, truncateStartToWidth, truncateToWidthNoEllipsis, truncate, wrapText |
|
||||
| `src/utils/diff.ts` | 待测 | adjustHunkLineNumbers, getPatchFromContents |
|
||||
| `src/utils/frontmatterParser.ts` | 待测 | parseFrontmatter, splitPathInFrontmatter, parsePositiveIntFromFrontmatter, parseBooleanFrontmatter, parseShellFrontmatter |
|
||||
| `src/utils/file.ts` | 待测(纯函数部分) | convertLeadingTabsToSpaces, addLineNumbers, stripLineNumberPrefix, pathsEqual, normalizePathForComparison |
|
||||
| `src/utils/glob.ts` | 待测(纯函数部分) | extractGlobBaseDirectory |
|
||||
| `src/utils/tokens.ts` | 待测 | getTokenCountFromUsage |
|
||||
| `src/utils/path.ts` | 待测(纯函数部分) | containsPathTraversal, normalizePathForConfigKey |
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### src/utils/xml.ts — 测试文件: `src/utils/__tests__/xml.test.ts`
|
||||
|
||||
#### describe('escapeXml')
|
||||
|
||||
- test('escapes ampersand') — `&` → `&`
|
||||
- test('escapes less-than') — `<` → `<`
|
||||
- test('escapes greater-than') — `>` → `>`
|
||||
- test('does not escape quotes') — `"` 和 `'` 保持原样
|
||||
- test('handles empty string') — `""` → `""`
|
||||
- test('handles string with no special chars') — `"hello"` 原样返回
|
||||
- test('escapes multiple special chars in one string') — `<a & b>` → `<a & b>`
|
||||
|
||||
#### describe('escapeXmlAttr')
|
||||
|
||||
- test('escapes all xml chars plus quotes') — `"` → `"`, `'` → `'`
|
||||
- test('escapes double quotes') — `he said "hi"` 正确转义
|
||||
- test('escapes single quotes') — `it's` 正确转义
|
||||
|
||||
---
|
||||
|
||||
### src/utils/hash.ts — 测试文件: `src/utils/__tests__/hash.test.ts`
|
||||
|
||||
#### describe('djb2Hash')
|
||||
|
||||
- test('returns consistent hash for same input') — 相同输入返回相同结果
|
||||
- test('returns different hashes for different inputs') — 不同输入大概率不同
|
||||
- test('returns a 32-bit integer') — 结果在 int32 范围内
|
||||
- test('handles empty string') — 空字符串有确定的哈希值
|
||||
- test('handles unicode strings') — 中文/emoji 等正确处理
|
||||
|
||||
#### describe('hashContent')
|
||||
|
||||
- test('returns consistent hash for same content') — 确定性
|
||||
- test('returns string result') — 返回值为字符串
|
||||
|
||||
#### describe('hashPair')
|
||||
|
||||
- test('returns consistent hash for same pair') — 确定性
|
||||
- test('order matters') — hashPair(a, b) ≠ hashPair(b, a)
|
||||
- test('handles empty strings')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/stringUtils.ts — 测试文件: `src/utils/__tests__/stringUtils.test.ts`
|
||||
|
||||
#### describe('escapeRegExp')
|
||||
|
||||
- test('escapes dots') — `.` → `\\.`
|
||||
- test('escapes asterisks') — `*` → `\\*`
|
||||
- test('escapes brackets') — `[` → `\\[`
|
||||
- test('escapes all special chars') — `.*+?^${}()|[]\` 全部转义
|
||||
- test('leaves normal chars unchanged') — `hello` 原样
|
||||
- test('escaped string works in RegExp') — `new RegExp(escapeRegExp('a.b'))` 精确匹配 `a.b`
|
||||
|
||||
#### describe('capitalize')
|
||||
|
||||
- test('uppercases first char') — `"foo"` → `"Foo"`
|
||||
- test('does NOT lowercase rest') — `"fooBar"` → `"FooBar"`(区别于 lodash capitalize)
|
||||
- test('handles single char') — `"a"` → `"A"`
|
||||
- test('handles empty string') — `""` → `""`
|
||||
- test('handles already capitalized') — `"Foo"` → `"Foo"`
|
||||
|
||||
#### describe('plural')
|
||||
|
||||
- test('returns singular for n=1') — `plural(1, 'file')` → `'file'`
|
||||
- test('returns plural for n=0') — `plural(0, 'file')` → `'files'`
|
||||
- test('returns plural for n>1') — `plural(3, 'file')` → `'files'`
|
||||
- test('uses custom plural form') — `plural(2, 'entry', 'entries')` → `'entries'`
|
||||
|
||||
#### describe('firstLineOf')
|
||||
|
||||
- test('returns first line of multi-line string') — `"a\nb\nc"` → `"a"`
|
||||
- test('returns full string when no newline') — `"hello"` → `"hello"`
|
||||
- test('handles empty string') — `""` → `""`
|
||||
- test('handles string starting with newline') — `"\nhello"` → `""`
|
||||
|
||||
#### describe('countCharInString')
|
||||
|
||||
- test('counts occurrences') — `countCharInString("aabac", "a")` → `3`
|
||||
- test('returns 0 when char not found') — `countCharInString("hello", "x")` → `0`
|
||||
- test('handles empty string') — `countCharInString("", "a")` → `0`
|
||||
- test('respects start position') — `countCharInString("aaba", "a", 2)` → `1`
|
||||
|
||||
#### describe('normalizeFullWidthDigits')
|
||||
|
||||
- test('converts full-width digits to half-width') — `"0123"` → `"0123"`
|
||||
- test('leaves half-width digits unchanged') — `"0123"` → `"0123"`
|
||||
- test('mixed content') — `"port 8080"` → `"port 8080"`
|
||||
|
||||
#### describe('normalizeFullWidthSpace')
|
||||
|
||||
- test('converts ideographic space to regular space') — `"\u3000"` → `" "`
|
||||
- test('converts multiple spaces') — `"a\u3000b\u3000c"` → `"a b c"`
|
||||
|
||||
#### describe('safeJoinLines')
|
||||
|
||||
- test('joins lines with default delimiter') — `["a","b"]` → `"a,b"`
|
||||
- test('truncates when exceeding maxSize') — 超限时截断并添加 `...[truncated]`
|
||||
- test('handles empty array') — `[]` → `""`
|
||||
- test('uses custom delimiter') — delimiter 为 `"\n"` 时按行连接
|
||||
|
||||
#### describe('truncateToLines')
|
||||
|
||||
- test('returns full text when within limit') — 行数不超限时原样返回
|
||||
- test('truncates and adds ellipsis') — 超限时截断并加 `…`
|
||||
- test('handles exact limit') — 刚好等于 maxLines 时不截断
|
||||
- test('handles single line') — 单行文本不截断
|
||||
|
||||
#### describe('EndTruncatingAccumulator')
|
||||
|
||||
- test('accumulates strings normally within limit')
|
||||
- test('truncates when exceeding maxSize')
|
||||
- test('reports truncated status correctly')
|
||||
- test('reports totalBytes including truncated content')
|
||||
- test('toString includes truncation marker')
|
||||
- test('clear resets all state')
|
||||
- test('append with Buffer works') — 接受 Buffer 类型
|
||||
|
||||
---
|
||||
|
||||
### src/utils/semver.ts — 测试文件: `src/utils/__tests__/semver.test.ts`
|
||||
|
||||
#### describe('gt / gte / lt / lte')
|
||||
|
||||
- test('gt: 2.0.0 > 1.0.0') → true
|
||||
- test('gt: 1.0.0 > 1.0.0') → false
|
||||
- test('gte: 1.0.0 >= 1.0.0') → true
|
||||
- test('lt: 1.0.0 < 2.0.0') → true
|
||||
- test('lte: 1.0.0 <= 1.0.0') → true
|
||||
- test('handles pre-release versions') — `1.0.0-beta < 1.0.0`
|
||||
|
||||
#### describe('satisfies')
|
||||
|
||||
- test('version satisfies caret range') — `satisfies('1.2.3', '^1.0.0')` → true
|
||||
- test('version does not satisfy range') — `satisfies('2.0.0', '^1.0.0')` → false
|
||||
- test('exact match') — `satisfies('1.0.0', '1.0.0')` → true
|
||||
|
||||
#### describe('order')
|
||||
|
||||
- test('returns -1 for lesser') — `order('1.0.0', '2.0.0')` → -1
|
||||
- test('returns 0 for equal') — `order('1.0.0', '1.0.0')` → 0
|
||||
- test('returns 1 for greater') — `order('2.0.0', '1.0.0')` → 1
|
||||
|
||||
---
|
||||
|
||||
### src/utils/uuid.ts — 测试文件: `src/utils/__tests__/uuid.test.ts`
|
||||
|
||||
#### describe('validateUuid')
|
||||
|
||||
- test('accepts valid v4 UUID') — `'550e8400-e29b-41d4-a716-446655440000'` → 返回 UUID
|
||||
- test('returns null for invalid format') — `'not-a-uuid'` → null
|
||||
- test('returns null for empty string') — `''` → null
|
||||
- test('returns null for null/undefined input')
|
||||
- test('accepts uppercase UUIDs') — 大写字母有效
|
||||
|
||||
#### describe('createAgentId')
|
||||
|
||||
- test('returns string starting with "a"') — 前缀为 `a`
|
||||
- test('has correct length') — 前缀 + 16 hex 字符
|
||||
- test('generates unique ids') — 连续两次调用结果不同
|
||||
|
||||
---
|
||||
|
||||
### src/utils/format.ts — 测试文件: `src/utils/__tests__/format.test.ts`
|
||||
|
||||
#### describe('formatFileSize')
|
||||
|
||||
- test('formats bytes') — `500` → `"500 bytes"`
|
||||
- test('formats kilobytes') — `1536` → `"1.5KB"`
|
||||
- test('formats megabytes') — `1572864` → `"1.5MB"`
|
||||
- test('formats gigabytes') — `1610612736` → `"1.5GB"`
|
||||
- test('removes trailing .0') — `1024` → `"1KB"` (不是 `"1.0KB"`)
|
||||
|
||||
#### describe('formatSecondsShort')
|
||||
|
||||
- test('formats milliseconds to seconds') — `1234` → `"1.2s"`
|
||||
- test('formats zero') — `0` → `"0.0s"`
|
||||
|
||||
#### describe('formatDuration')
|
||||
|
||||
- test('formats seconds') — `5000` → `"5s"`
|
||||
- test('formats minutes and seconds') — `65000` → `"1m 5s"`
|
||||
- test('formats hours') — `3661000` → `"1h 1m 1s"`
|
||||
- test('formats days') — `90061000` → `"1d 1h 1m"`
|
||||
- test('returns "0s" for zero') — `0` → `"0s"`
|
||||
- test('hideTrailingZeros omits zero components') — `3600000` + `hideTrailingZeros` → `"1h"`
|
||||
- test('mostSignificantOnly returns largest unit') — `3661000` + `mostSignificantOnly` → `"1h"`
|
||||
|
||||
#### describe('formatNumber')
|
||||
|
||||
- test('formats thousands') — `1321` → `"1.3k"`
|
||||
- test('formats small numbers as-is') — `900` → `"900"`
|
||||
- test('lowercase output') — `1500` → `"1.5k"` (不是 `"1.5K"`)
|
||||
|
||||
#### describe('formatTokens')
|
||||
|
||||
- test('strips .0 suffix') — `1000` → `"1k"` (不是 `"1.0k"`)
|
||||
- test('keeps non-zero decimal') — `1500` → `"1.5k"`
|
||||
|
||||
#### describe('formatRelativeTime')
|
||||
|
||||
- test('formats past time') — now - 3600s → `"1h ago"` (narrow style)
|
||||
- test('formats future time') — now + 3600s → `"in 1h"` (narrow style)
|
||||
- test('formats less than 1 second') — now → `"0s ago"`
|
||||
- test('uses custom now parameter for deterministic output')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/json.ts — 测试文件: `src/utils/__tests__/json.test.ts`
|
||||
|
||||
#### describe('safeParseJSON')
|
||||
|
||||
- test('parses valid JSON') — `'{"a":1}'` → `{ a: 1 }`
|
||||
- test('returns null for invalid JSON') — `'not json'` → null
|
||||
- test('returns null for null input') — `null` → null
|
||||
- test('returns null for undefined input') — `undefined` → null
|
||||
- test('returns null for empty string') — `""` → null
|
||||
- test('handles JSON with BOM') — BOM 前缀不影响解析
|
||||
- test('caches results for repeated calls') — 同一输入不重复解析
|
||||
|
||||
#### describe('safeParseJSONC')
|
||||
|
||||
- test('parses JSON with comments') — 含 `//` 注释的 JSON 正确解析
|
||||
- test('parses JSON with trailing commas') — 宽松模式
|
||||
- test('returns null for invalid input')
|
||||
- test('returns null for null input')
|
||||
|
||||
#### describe('parseJSONL')
|
||||
|
||||
- test('parses multiple JSON lines') — `'{"a":1}\n{"b":2}'` → `[{a:1}, {b:2}]`
|
||||
- test('skips malformed lines') — 含错误行时跳过该行
|
||||
- test('handles empty input') — `""` → `[]`
|
||||
- test('handles trailing newline') — 尾部换行不产生空元素
|
||||
- test('accepts Buffer input') — Buffer 类型同样工作
|
||||
- test('handles BOM prefix')
|
||||
|
||||
#### describe('addItemToJSONCArray')
|
||||
|
||||
- test('adds item to existing array') — `[1, 2]` + 3 → `[1, 2, 3]`
|
||||
- test('creates new array for empty content') — `""` + item → `[item]`
|
||||
- test('creates new array for non-array content') — `'"hello"'` + item → `[item]`
|
||||
- test('preserves comments in JSONC') — 注释不被丢弃
|
||||
- test('handles empty array') — `"[]"` + item → `[item]`
|
||||
|
||||
---
|
||||
|
||||
### src/utils/diff.ts — 测试文件: `src/utils/__tests__/diff.test.ts`
|
||||
|
||||
#### describe('adjustHunkLineNumbers')
|
||||
|
||||
- test('shifts line numbers by positive offset') — 所有 hunk 的 oldStart/newStart 增加 offset
|
||||
- test('shifts by negative offset') — 负 offset 减少行号
|
||||
- test('handles empty hunk array') — `[]` → `[]`
|
||||
|
||||
#### describe('getPatchFromContents')
|
||||
|
||||
- test('returns empty array for identical content') — 相同内容无差异
|
||||
- test('detects added lines') — 新内容多出行
|
||||
- test('detects removed lines') — 旧内容缺少行
|
||||
- test('detects modified lines') — 行内容变化
|
||||
- test('handles empty old content') — 从空文件到有内容
|
||||
- test('handles empty new content') — 删除所有内容
|
||||
|
||||
---
|
||||
|
||||
### src/utils/frontmatterParser.ts — 测试文件: `src/utils/__tests__/frontmatterParser.test.ts`
|
||||
|
||||
#### describe('parseFrontmatter')
|
||||
|
||||
- test('extracts YAML frontmatter between --- delimiters') — 正确提取 frontmatter 并返回 body
|
||||
- test('returns empty frontmatter for content without ---') — 无 frontmatter 时 data 为空
|
||||
- test('handles empty content') — `""` 正确处理
|
||||
- test('handles frontmatter-only content') — 只有 frontmatter 无 body
|
||||
- test('falls back to quoting on YAML parse error') — 无效 YAML 不崩溃
|
||||
|
||||
#### describe('splitPathInFrontmatter')
|
||||
|
||||
- test('splits comma-separated paths') — `"a.ts, b.ts"` → `["a.ts", "b.ts"]`
|
||||
- test('expands brace patterns') — `"*.{ts,tsx}"` → `["*.ts", "*.tsx"]`
|
||||
- test('handles string array input') — `["a.ts", "b.ts"]` → `["a.ts", "b.ts"]`
|
||||
- test('respects braces in comma splitting') — 大括号内的逗号不作为分隔符
|
||||
|
||||
#### describe('parsePositiveIntFromFrontmatter')
|
||||
|
||||
- test('returns number for valid positive int') — `5` → `5`
|
||||
- test('returns undefined for negative') — `-1` → undefined
|
||||
- test('returns undefined for non-number') — `"abc"` → undefined
|
||||
- test('returns undefined for float') — `1.5` → undefined
|
||||
|
||||
#### describe('parseBooleanFrontmatter')
|
||||
|
||||
- test('returns true for true') — `true` → true
|
||||
- test('returns true for "true"') — `"true"` → true
|
||||
- test('returns false for false') — `false` → false
|
||||
- test('returns false for other values') — `"yes"`, `1` → false
|
||||
|
||||
#### describe('parseShellFrontmatter')
|
||||
|
||||
- test('returns bash for "bash"') — 正确识别
|
||||
- test('returns powershell for "powershell"')
|
||||
- test('returns undefined for invalid value') — `"zsh"` → undefined
|
||||
|
||||
---
|
||||
|
||||
### src/utils/file.ts(纯函数部分)— 测试文件: `src/utils/__tests__/file.test.ts`
|
||||
|
||||
#### describe('convertLeadingTabsToSpaces')
|
||||
|
||||
- test('converts single tab to 2 spaces') — `"\thello"` → `" hello"`
|
||||
- test('converts multiple leading tabs') — `"\t\thello"` → `" hello"`
|
||||
- test('does not convert tabs within line') — `"a\tb"` 保持原样
|
||||
- test('handles mixed content')
|
||||
|
||||
#### describe('addLineNumbers')
|
||||
|
||||
- test('adds line numbers starting from 1') — 每行添加 `N\t` 前缀
|
||||
- test('respects startLine parameter') — 从指定行号开始
|
||||
- test('handles empty content')
|
||||
|
||||
#### describe('stripLineNumberPrefix')
|
||||
|
||||
- test('strips tab-prefixed line number') — `"1\thello"` → `"hello"`
|
||||
- test('strips padded line number') — `" 1\thello"` → `"hello"`
|
||||
- test('returns line unchanged when no prefix')
|
||||
|
||||
#### describe('pathsEqual')
|
||||
|
||||
- test('returns true for identical paths')
|
||||
- test('handles trailing slashes') — 带/不带尾部斜杠视为相同
|
||||
- test('handles case sensitivity based on platform')
|
||||
|
||||
#### describe('normalizePathForComparison')
|
||||
|
||||
- test('normalizes forward slashes')
|
||||
- test('resolves path for comparison')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/glob.ts(纯函数部分)— 测试文件: `src/utils/__tests__/glob.test.ts`
|
||||
|
||||
#### describe('extractGlobBaseDirectory')
|
||||
|
||||
- test('extracts static prefix from glob') — `"src/**/*.ts"` → `{ baseDir: "src", relativePattern: "**/*.ts" }`
|
||||
- test('handles root-level glob') — `"*.ts"` → `{ baseDir: ".", relativePattern: "*.ts" }`
|
||||
- test('handles deep static path') — `"src/utils/model/*.ts"` → baseDir 为 `"src/utils/model"`
|
||||
- test('handles Windows drive root') — `"C:\\Users\\**\\*.ts"` 正确分割
|
||||
|
||||
---
|
||||
|
||||
### src/utils/tokens.ts(纯函数部分)— 测试文件: `src/utils/__tests__/tokens.test.ts`
|
||||
|
||||
#### describe('getTokenCountFromUsage')
|
||||
|
||||
- test('sums input and output tokens') — `{ input_tokens: 100, output_tokens: 50 }` → 150
|
||||
- test('includes cache tokens') — cache_creation + cache_read 加入总数
|
||||
- test('handles zero values') — 全 0 时返回 0
|
||||
|
||||
---
|
||||
|
||||
### src/utils/path.ts(纯函数部分)— 测试文件: `src/utils/__tests__/path.test.ts`
|
||||
|
||||
#### describe('containsPathTraversal')
|
||||
|
||||
- test('detects ../ traversal') — `"../etc/passwd"` → true
|
||||
- test('detects mid-path traversal') — `"foo/../../bar"` → true
|
||||
- test('returns false for safe paths') — `"src/utils/file.ts"` → false
|
||||
- test('returns false for paths containing .. in names') — `"foo..bar"` → false
|
||||
|
||||
#### describe('normalizePathForConfigKey')
|
||||
|
||||
- test('converts backslashes to forward slashes') — `"src\\utils"` → `"src/utils"`
|
||||
- test('leaves forward slashes unchanged')
|
||||
|
||||
---
|
||||
|
||||
## Mock 需求
|
||||
|
||||
本计划中的函数大部分为纯函数,**不需要 mock**。少数例外:
|
||||
|
||||
| 函数 | 依赖 | 处理 |
|
||||
|------|------|------|
|
||||
| `hashContent` / `hashPair` | `Bun.hash` | Bun 运行时下自动可用 |
|
||||
| `formatRelativeTime` | `Date` | 使用 `now` 参数注入确定性时间 |
|
||||
| `safeParseJSON` | `logError` | 可通过 `shouldLogError: false` 跳过 |
|
||||
| `safeParseJSONC` | `logError` | mock `logError` 避免测试输出噪音 |
|
||||
@@ -1,134 +0,0 @@
|
||||
# Context 构建测试计划
|
||||
|
||||
## 概述
|
||||
|
||||
Context 构建系统负责组装发送给 Claude API 的系统提示和用户上下文。包括 git 状态获取、CLAUDE.md 文件发现与加载、系统提示拼装三部分。
|
||||
|
||||
## 被测文件
|
||||
|
||||
| 文件 | 关键导出 |
|
||||
|------|----------|
|
||||
| `src/context.ts` | `getSystemContext`, `getUserContext`, `getGitStatus`, `setSystemPromptInjection` |
|
||||
| `src/utils/claudemd.ts` | `stripHtmlComments`, `getClaudeMds`, `isMemoryFilePath`, `getLargeMemoryFiles`, `filterInjectedMemoryFiles`, `getExternalClaudeMdIncludes`, `hasExternalClaudeMdIncludes`, `processMemoryFile`, `getMemoryFiles` |
|
||||
| `src/utils/systemPrompt.ts` | `buildEffectiveSystemPrompt` |
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### src/utils/claudemd.ts — 纯函数部分
|
||||
|
||||
#### describe('stripHtmlComments')
|
||||
|
||||
- test('strips block-level HTML comments') — `"text <!-- comment --> more"` → content 不含注释
|
||||
- test('preserves inline content') — 行内文本保留
|
||||
- test('preserves code block content') — ` ```html\n<!-- not stripped -->\n``` ` 内的注释不移除
|
||||
- test('returns stripped: false when no comments') — 无注释时 stripped 为 false
|
||||
- test('returns stripped: true when comments exist')
|
||||
- test('handles empty string') — `""` → `{ content: "", stripped: false }`
|
||||
- test('handles multiple comments') — 多个注释全部移除
|
||||
|
||||
#### describe('getClaudeMds')
|
||||
|
||||
- test('assembles memory files with type descriptions') — 不同 type 的文件有不同前缀描述
|
||||
- test('includes instruction prompt prefix') — 输出包含指令前缀
|
||||
- test('handles empty memory files array') — 空数组返回空字符串或最小前缀
|
||||
- test('respects filter parameter') — filter 函数可过滤特定类型
|
||||
- test('concatenates multiple files with separators')
|
||||
|
||||
#### describe('isMemoryFilePath')
|
||||
|
||||
- test('returns true for CLAUDE.md path') — `"/project/CLAUDE.md"` → true
|
||||
- test('returns true for .claude/rules/ path') — `"/project/.claude/rules/foo.md"` → true
|
||||
- test('returns true for memory file path') — `"~/.claude/memory/foo.md"` → true
|
||||
- test('returns false for regular file') — `"/project/src/main.ts"` → false
|
||||
- test('returns false for unrelated .md file') — `"/project/README.md"` → false
|
||||
|
||||
#### describe('getLargeMemoryFiles')
|
||||
|
||||
- test('returns files exceeding 40K chars') — 内容 > MAX_MEMORY_CHARACTER_COUNT 的文件被返回
|
||||
- test('returns empty array when all files are small')
|
||||
- test('correctly identifies threshold boundary')
|
||||
|
||||
#### describe('filterInjectedMemoryFiles')
|
||||
|
||||
- test('filters out AutoMem type files') — feature flag 开启时移除自动记忆
|
||||
- test('filters out TeamMem type files')
|
||||
- test('preserves other types') — 非 AutoMem/TeamMem 的文件保留
|
||||
|
||||
#### describe('getExternalClaudeMdIncludes')
|
||||
|
||||
- test('returns includes from outside CWD') — 外部 @include 路径被识别
|
||||
- test('returns empty array when all includes are internal')
|
||||
|
||||
#### describe('hasExternalClaudeMdIncludes')
|
||||
|
||||
- test('returns true when external includes exist')
|
||||
- test('returns false when no external includes')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/systemPrompt.ts
|
||||
|
||||
#### describe('buildEffectiveSystemPrompt')
|
||||
|
||||
- test('returns default system prompt when no overrides') — 无任何覆盖时使用默认提示
|
||||
- test('overrideSystemPrompt replaces everything') — override 模式替换全部内容
|
||||
- test('customSystemPrompt replaces default') — `--system-prompt` 参数替换默认
|
||||
- test('appendSystemPrompt is appended after main prompt') — append 在主提示之后
|
||||
- test('agent definition replaces default prompt') — agent 模式使用 agent prompt
|
||||
- test('agent definition with append combines both') — agent prompt + append
|
||||
- test('override takes precedence over agent and custom') — 优先级最高
|
||||
- test('returns array of strings') — 返回值为 SystemPrompt 类型(字符串数组)
|
||||
|
||||
---
|
||||
|
||||
### src/context.ts — 需 Mock 的部分
|
||||
|
||||
#### describe('getGitStatus')
|
||||
|
||||
- test('returns formatted git status string') — 包含 branch、status、log、user
|
||||
- test('truncates status at 2000 chars') — 超长 status 被截断
|
||||
- test('returns null in test environment') — `NODE_ENV=test` 时返回 null
|
||||
- test('returns null in non-git directory') — 非 git 仓库返回 null
|
||||
- test('runs git commands in parallel') — 多个 git 命令并行执行
|
||||
|
||||
#### describe('getSystemContext')
|
||||
|
||||
- test('includes gitStatus key') — 返回对象包含 gitStatus
|
||||
- test('returns memoized result on subsequent calls') — 多次调用返回同一结果
|
||||
- test('skips git when instructions disabled')
|
||||
|
||||
#### describe('getUserContext')
|
||||
|
||||
- test('includes currentDate key') — 返回对象包含当前日期
|
||||
- test('includes claudeMd key when CLAUDE.md exists') — 加载 CLAUDE.md 内容
|
||||
- test('respects CLAUDE_CODE_DISABLE_CLAUDE_MDS env') — 设置后不加载 CLAUDE.md
|
||||
- test('returns memoized result')
|
||||
|
||||
#### describe('setSystemPromptInjection')
|
||||
|
||||
- test('clears memoized context caches') — 调用后下次 getSystemContext/getUserContext 重新计算
|
||||
- test('injection value is accessible via getSystemPromptInjection')
|
||||
|
||||
---
|
||||
|
||||
## Mock 需求
|
||||
|
||||
| 依赖 | Mock 方式 | 用途 |
|
||||
|------|-----------|------|
|
||||
| `execFileNoThrow` | `mock.module` | `getGitStatus` 中的 git 命令 |
|
||||
| `getMemoryFiles` | `mock.module` | `getUserContext` 中的 CLAUDE.md 加载 |
|
||||
| `getCwd` | `mock.module` | 路径解析上下文 |
|
||||
| `process.env.NODE_ENV` | 直接设置 | 测试环境检测 |
|
||||
| `process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS` | 直接设置 | 禁用 CLAUDE.md |
|
||||
|
||||
## 集成测试场景
|
||||
|
||||
放在 `tests/integration/context-build.test.ts`:
|
||||
|
||||
### describe('Context assembly pipeline')
|
||||
|
||||
- test('getUserContext produces claudeMd containing CLAUDE.md content') — 端到端验证 CLAUDE.md 被正确加载到 context
|
||||
- test('buildEffectiveSystemPrompt + getUserContext produces complete prompt') — 系统提示 + 用户上下文完整性
|
||||
- test('setSystemPromptInjection invalidates and rebuilds context') — 注入后重新构建上下文
|
||||
@@ -1,104 +0,0 @@
|
||||
# 权限系统测试计划
|
||||
|
||||
## 概述
|
||||
|
||||
权限系统控制工具是否可以执行,包含规则解析器、权限检查管线和权限模式判断。测试重点是纯函数解析器和规则匹配逻辑。
|
||||
|
||||
## 被测文件
|
||||
|
||||
| 文件 | 关键导出 |
|
||||
|------|----------|
|
||||
| `src/utils/permissions/permissionRuleParser.ts` | `permissionRuleValueFromString`, `permissionRuleValueToString`, `escapeRuleContent`, `unescapeRuleContent`, `normalizeLegacyToolName`, `getLegacyToolNames` |
|
||||
| `src/utils/permissions/PermissionMode.ts` | 权限模式常量和辅助函数 |
|
||||
| `src/utils/permissions/permissions.ts` | `hasPermissionsToUseTool`, `getDenyRuleForTool`, `checkRuleBasedPermissions` |
|
||||
| `src/types/permissions.ts` | `PermissionMode`, `PermissionBehavior`, `PermissionRule` 类型定义 |
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### src/utils/permissions/permissionRuleParser.ts
|
||||
|
||||
#### describe('escapeRuleContent')
|
||||
|
||||
- test('escapes backslashes first') — `'test\\value'` → `'test\\\\value'`
|
||||
- test('escapes opening parentheses') — `'print(1)'` → `'print\\(1\\)'`
|
||||
- test('escapes closing parentheses') — `'func()'` → `'func\\(\\)'`
|
||||
- test('handles combined escape') — `'echo "test\\nvalue"'` 中的 `\\` 先转义
|
||||
- test('handles empty string') — `''` → `''`
|
||||
- test('no-op for string without special chars') — `'npm install'` 原样返回
|
||||
|
||||
#### describe('unescapeRuleContent')
|
||||
|
||||
- test('unescapes parentheses') — `'print\\(1\\)'` → `'print(1)'`
|
||||
- test('unescapes backslashes last') — `'test\\\\nvalue'` → `'test\\nvalue'`
|
||||
- test('handles empty string')
|
||||
- test('roundtrip: escape then unescape returns original') — `unescapeRuleContent(escapeRuleContent(x)) === x`
|
||||
|
||||
#### describe('permissionRuleValueFromString')
|
||||
|
||||
- test('parses tool name only') — `'Bash'` → `{ toolName: 'Bash' }`
|
||||
- test('parses tool name with content') — `'Bash(npm install)'` → `{ toolName: 'Bash', ruleContent: 'npm install' }`
|
||||
- test('parses content with escaped parentheses') — `'Bash(python -c "print\\(1\\)")'` → ruleContent 为 `'python -c "print(1)"'`
|
||||
- test('treats empty parens as tool-wide rule') — `'Bash()'` → `{ toolName: 'Bash' }`(无 ruleContent)
|
||||
- test('treats wildcard content as tool-wide rule') — `'Bash(*)'` → `{ toolName: 'Bash' }`
|
||||
- test('normalizes legacy tool names') — `'Task'` → `{ toolName: 'Agent' }`(或对应的 AGENT_TOOL_NAME)
|
||||
- test('handles malformed input: no closing paren') — `'Bash(npm'` → 整个字符串作为 toolName
|
||||
- test('handles malformed input: content after closing paren') — `'Bash(npm)extra'` → 整个字符串作为 toolName
|
||||
- test('handles missing tool name') — `'(foo)'` → 整个字符串作为 toolName
|
||||
|
||||
#### describe('permissionRuleValueToString')
|
||||
|
||||
- test('serializes tool name only') — `{ toolName: 'Bash' }` → `'Bash'`
|
||||
- test('serializes with content') — `{ toolName: 'Bash', ruleContent: 'npm install' }` → `'Bash(npm install)'`
|
||||
- test('escapes content with parentheses') — ruleContent 含 `()` 时正确转义
|
||||
- test('roundtrip: fromString then toString preserves value') — 往返一致
|
||||
|
||||
#### describe('normalizeLegacyToolName')
|
||||
|
||||
- test('maps Task to Agent tool name') — `'Task'` → AGENT_TOOL_NAME
|
||||
- test('maps KillShell to TaskStop tool name') — `'KillShell'` → TASK_STOP_TOOL_NAME
|
||||
- test('maps AgentOutputTool to TaskOutput tool name')
|
||||
- test('returns unknown names unchanged') — `'UnknownTool'` → `'UnknownTool'`
|
||||
|
||||
#### describe('getLegacyToolNames')
|
||||
|
||||
- test('returns legacy names for canonical name') — 给定 AGENT_TOOL_NAME 返回包含 `'Task'`
|
||||
- test('returns empty array for name with no legacy aliases')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/permissions/permissions.ts — 需 Mock
|
||||
|
||||
#### describe('getDenyRuleForTool')
|
||||
|
||||
- test('returns deny rule matching tool name') — 匹配到 blanket deny 规则时返回
|
||||
- test('returns null when no deny rules match') — 无匹配时返回 null
|
||||
- test('matches MCP tools by server prefix') — `mcp__server` 规则匹配该 server 下的 MCP 工具
|
||||
- test('does not match content-specific deny rules') — 有 ruleContent 的 deny 规则不作为 blanket deny
|
||||
|
||||
#### describe('checkRuleBasedPermissions')(集成级别)
|
||||
|
||||
- test('deny rule takes precedence over allow') — 同时有 allow 和 deny 时 deny 优先
|
||||
- test('ask rule prompts user') — 匹配 ask 规则返回 `{ behavior: 'ask' }`
|
||||
- test('allow rule permits execution') — 匹配 allow 规则返回 `{ behavior: 'allow' }`
|
||||
- test('passthrough when no rules match') — 无匹配规则返回 passthrough
|
||||
|
||||
---
|
||||
|
||||
## Mock 需求
|
||||
|
||||
| 依赖 | Mock 方式 | 说明 |
|
||||
|------|-----------|------|
|
||||
| `bun:bundle` (feature) | 已 polyfill | BRIEF_TOOL_NAME 条件加载 |
|
||||
| Tool 常量导入 | 实际值 | AGENT_TOOL_NAME 等从常量文件导入 |
|
||||
| `appState` | mock object | `hasPermissionsToUseTool` 中的状态依赖 |
|
||||
| Tool 对象 | mock object | 模拟 tool 的 name, checkPermissions 等 |
|
||||
|
||||
## 集成测试场景
|
||||
|
||||
### describe('Permission pipeline end-to-end')
|
||||
|
||||
- test('deny rule blocks tool before it runs') — deny 规则在 call 前拦截
|
||||
- test('bypassPermissions mode allows all') — bypass 模式下 ask → allow
|
||||
- test('dontAsk mode converts ask to deny') — dontAsk 模式下 ask → deny
|
||||
@@ -1,113 +0,0 @@
|
||||
# 模型路由测试计划
|
||||
|
||||
## 概述
|
||||
|
||||
模型路由系统负责 API provider 选择、模型别名解析、模型名规范化和运行时模型决策。测试重点是纯函数和环境变量驱动的逻辑。
|
||||
|
||||
## 被测文件
|
||||
|
||||
| 文件 | 关键导出 |
|
||||
|------|----------|
|
||||
| `src/utils/model/aliases.ts` | `MODEL_ALIASES`, `MODEL_FAMILY_ALIASES`, `isModelAlias`, `isModelFamilyAlias` |
|
||||
| `src/utils/model/providers.ts` | `APIProvider`, `getAPIProvider`, `isFirstPartyAnthropicBaseUrl` |
|
||||
| `src/utils/model/model.ts` | `firstPartyNameToCanonical`, `getCanonicalName`, `parseUserSpecifiedModel`, `normalizeModelStringForAPI`, `getRuntimeMainLoopModel`, `getDefaultMainLoopModelSetting` |
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### src/utils/model/aliases.ts
|
||||
|
||||
#### describe('isModelAlias')
|
||||
|
||||
- test('returns true for "sonnet"') — 有效别名
|
||||
- test('returns true for "opus"')
|
||||
- test('returns true for "haiku"')
|
||||
- test('returns true for "best"')
|
||||
- test('returns true for "sonnet[1m]"')
|
||||
- test('returns true for "opus[1m]"')
|
||||
- test('returns true for "opusplan"')
|
||||
- test('returns false for full model ID') — `'claude-sonnet-4-6-20250514'` → false
|
||||
- test('returns false for unknown string') — `'gpt-4'` → false
|
||||
- test('is case-sensitive') — `'Sonnet'` → false(别名是小写)
|
||||
|
||||
#### describe('isModelFamilyAlias')
|
||||
|
||||
- test('returns true for "sonnet"')
|
||||
- test('returns true for "opus"')
|
||||
- test('returns true for "haiku"')
|
||||
- test('returns false for "best"') — best 不是 family alias
|
||||
- test('returns false for "opusplan"')
|
||||
- test('returns false for "sonnet[1m]"')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/model/providers.ts
|
||||
|
||||
#### describe('getAPIProvider')
|
||||
|
||||
- test('returns "firstParty" by default') — 无相关 env 时返回 firstParty
|
||||
- test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set') — env 为 truthy 值
|
||||
- test('returns "vertex" when CLAUDE_CODE_USE_VERTEX is set')
|
||||
- test('returns "foundry" when CLAUDE_CODE_USE_FOUNDRY is set')
|
||||
- test('bedrock takes precedence over vertex') — 多个 env 同时设置时 bedrock 优先
|
||||
|
||||
#### describe('isFirstPartyAnthropicBaseUrl')
|
||||
|
||||
- test('returns true when ANTHROPIC_BASE_URL is not set') — 默认 API
|
||||
- test('returns true for api.anthropic.com') — `'https://api.anthropic.com'` → true
|
||||
- test('returns false for custom URL') — `'https://my-proxy.com'` → false
|
||||
- test('returns false for invalid URL') — 非法 URL → false
|
||||
- test('returns true for staging URL when USER_TYPE is ant') — `'https://api-staging.anthropic.com'` + ant → true
|
||||
|
||||
---
|
||||
|
||||
### src/utils/model/model.ts
|
||||
|
||||
#### describe('firstPartyNameToCanonical')
|
||||
|
||||
- test('maps opus-4-6 full name to canonical') — `'claude-opus-4-6-20250514'` → `'claude-opus-4-6'`
|
||||
- test('maps sonnet-4-6 full name') — `'claude-sonnet-4-6-20250514'` → `'claude-sonnet-4-6'`
|
||||
- test('maps haiku-4-5') — `'claude-haiku-4-5-20251001'` → `'claude-haiku-4-5'`
|
||||
- test('maps 3P provider format') — `'us.anthropic.claude-opus-4-6-v1:0'` → `'claude-opus-4-6'`
|
||||
- test('maps claude-3-7-sonnet') — `'claude-3-7-sonnet-20250219'` → `'claude-3-7-sonnet'`
|
||||
- test('maps claude-3-5-sonnet') → `'claude-3-5-sonnet'`
|
||||
- test('maps claude-3-5-haiku') → `'claude-3-5-haiku'`
|
||||
- test('maps claude-3-opus') → `'claude-3-opus'`
|
||||
- test('is case insensitive') — `'Claude-Opus-4-6'` → `'claude-opus-4-6'`
|
||||
- test('falls back to input for unknown model') — `'unknown-model'` → `'unknown-model'`
|
||||
- test('differentiates opus-4 vs opus-4-5 vs opus-4-6') — 更具体的版本优先匹配
|
||||
|
||||
#### describe('parseUserSpecifiedModel')
|
||||
|
||||
- test('resolves "sonnet" to default sonnet model')
|
||||
- test('resolves "opus" to default opus model')
|
||||
- test('resolves "haiku" to default haiku model')
|
||||
- test('resolves "best" to best model')
|
||||
- test('resolves "opusplan" to default sonnet model') — opusplan 默认用 sonnet
|
||||
- test('appends [1m] suffix when alias has [1m]') — `'sonnet[1m]'` → 模型名 + `'[1m]'`
|
||||
- test('preserves original case for custom model names') — `'my-Custom-Model'` 保留大小写
|
||||
- test('handles [1m] suffix on non-alias models') — `'custom-model[1m]'` → `'custom-model[1m]'`
|
||||
- test('trims whitespace') — `' sonnet '` → 正确解析
|
||||
|
||||
#### describe('getRuntimeMainLoopModel')
|
||||
|
||||
- test('returns mainLoopModel by default') — 无特殊条件时原样返回
|
||||
- test('returns opus in plan mode when opusplan is set') — opusplan + plan mode → opus
|
||||
- test('returns sonnet in plan mode when haiku is set') — haiku + plan mode → sonnet 升级
|
||||
- test('returns mainLoopModel in non-plan mode') — 非 plan 模式不做替换
|
||||
|
||||
---
|
||||
|
||||
## Mock 需求
|
||||
|
||||
| 依赖 | Mock 方式 | 说明 |
|
||||
|------|-----------|------|
|
||||
| `process.env.CLAUDE_CODE_USE_BEDROCK/VERTEX/FOUNDRY` | 直接设置/恢复 | provider 选择 |
|
||||
| `process.env.ANTHROPIC_BASE_URL` | 直接设置/恢复 | URL 检测 |
|
||||
| `process.env.USER_TYPE` | 直接设置/恢复 | staging URL 和 ant 功能 |
|
||||
| `getModelStrings()` | mock.module | 返回固定模型 ID |
|
||||
| `getMainLoopModelOverride` | mock.module | 会话中模型覆盖 |
|
||||
| `getSettings_DEPRECATED` | mock.module | 用户设置中的模型 |
|
||||
| `getUserSpecifiedModelSetting` | mock.module | `getRuntimeMainLoopModel` 依赖 |
|
||||
| `isModelAllowed` | mock.module | allowlist 检查 |
|
||||
@@ -1,165 +0,0 @@
|
||||
# 消息处理测试计划
|
||||
|
||||
## 概述
|
||||
|
||||
消息处理系统负责消息的创建、查询、规范化和文本提取。覆盖消息类型定义、消息工厂函数、消息过滤/查询工具和 API 规范化管线。
|
||||
|
||||
## 被测文件
|
||||
|
||||
| 文件 | 关键导出 |
|
||||
|------|----------|
|
||||
| `src/types/message.ts` | `MessageType`, `Message`, `AssistantMessage`, `UserMessage`, `SystemMessage` 等类型 |
|
||||
| `src/utils/messages.ts` | 消息创建、查询、规范化、文本提取等函数(~3100 行) |
|
||||
| `src/utils/messages/mappers.ts` | 消息映射工具 |
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### src/utils/messages.ts — 消息创建
|
||||
|
||||
#### describe('createAssistantMessage')
|
||||
|
||||
- test('creates message with type "assistant"') — type 字段正确
|
||||
- test('creates message with role "assistant"') — role 正确
|
||||
- test('creates message with empty content array') — 默认 content 为空
|
||||
- test('generates unique uuid') — 每次调用 uuid 不同
|
||||
- test('includes costUsd as 0')
|
||||
|
||||
#### describe('createUserMessage')
|
||||
|
||||
- test('creates message with type "user"') — type 字段正确
|
||||
- test('creates message with provided content') — content 正确传入
|
||||
- test('generates unique uuid')
|
||||
|
||||
#### describe('createSystemMessage')
|
||||
|
||||
- test('creates system message with correct type')
|
||||
- test('includes message content')
|
||||
|
||||
#### describe('createProgressMessage')
|
||||
|
||||
- test('creates progress message with data')
|
||||
- test('has correct type "progress"')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/messages.ts — 消息查询
|
||||
|
||||
#### describe('getLastAssistantMessage')
|
||||
|
||||
- test('returns last assistant message from array') — 多条消息中返回最后一条 assistant
|
||||
- test('returns undefined for empty array')
|
||||
- test('returns undefined when no assistant messages exist')
|
||||
|
||||
#### describe('hasToolCallsInLastAssistantTurn')
|
||||
|
||||
- test('returns true when last assistant has tool_use content') — content 含 tool_use block
|
||||
- test('returns false when last assistant has only text')
|
||||
- test('returns false for empty messages')
|
||||
|
||||
#### describe('isSyntheticMessage')
|
||||
|
||||
- test('identifies interrupt message as synthetic') — INTERRUPT_MESSAGE 标记
|
||||
- test('identifies cancel message as synthetic')
|
||||
- test('returns false for normal user messages')
|
||||
|
||||
#### describe('isNotEmptyMessage')
|
||||
|
||||
- test('returns true for message with content')
|
||||
- test('returns false for message with empty content array')
|
||||
- test('returns false for message with empty text content')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/messages.ts — 文本提取
|
||||
|
||||
#### describe('getAssistantMessageText')
|
||||
|
||||
- test('extracts text from text blocks') — content 含 `{ type: 'text', text: 'hello' }` 时提取
|
||||
- test('returns empty string for non-text content') — 仅含 tool_use 时返回空
|
||||
- test('concatenates multiple text blocks')
|
||||
|
||||
#### describe('getUserMessageText')
|
||||
|
||||
- test('extracts text from string content') — content 为纯字符串
|
||||
- test('extracts text from content array') — content 为数组时提取 text 块
|
||||
- test('handles empty content')
|
||||
|
||||
#### describe('extractTextContent')
|
||||
|
||||
- test('extracts text items from mixed content') — 过滤出 type: 'text' 的项
|
||||
- test('returns empty array for all non-text content')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/messages.ts — 规范化
|
||||
|
||||
#### describe('normalizeMessages')
|
||||
|
||||
- test('converts raw messages to normalized format') — 消息数组规范化
|
||||
- test('handles empty array') — `[]` → `[]`
|
||||
- test('preserves message order')
|
||||
- test('handles mixed message types')
|
||||
|
||||
#### describe('normalizeMessagesForAPI')
|
||||
|
||||
- test('filters out system messages') — 系统消息不发送给 API
|
||||
- test('filters out progress messages')
|
||||
- test('filters out attachment messages')
|
||||
- test('preserves user and assistant messages')
|
||||
- test('reorders tool results to match API expectations')
|
||||
- test('handles empty array')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/messages.ts — 合并
|
||||
|
||||
#### describe('mergeUserMessages')
|
||||
|
||||
- test('merges consecutive user messages') — 相邻用户消息合并
|
||||
- test('does not merge non-consecutive user messages')
|
||||
- test('preserves assistant messages between user messages')
|
||||
|
||||
#### describe('mergeAssistantMessages')
|
||||
|
||||
- test('merges consecutive assistant messages')
|
||||
- test('combines content arrays')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/messages.ts — 辅助函数
|
||||
|
||||
#### describe('buildMessageLookups')
|
||||
|
||||
- test('builds index by message uuid') — 按 uuid 建立查找表
|
||||
- test('returns empty lookups for empty messages')
|
||||
- test('handles duplicate uuids gracefully')
|
||||
|
||||
---
|
||||
|
||||
## Mock 需求
|
||||
|
||||
| 依赖 | Mock 方式 | 说明 |
|
||||
|------|-----------|------|
|
||||
| `crypto.randomUUID` | `mock` 或 spy | 消息创建中的 uuid 生成 |
|
||||
| Message 对象 | 手动构造 | 创建符合类型的 mock 消息对象 |
|
||||
|
||||
### Mock 消息工厂(放在 `tests/mocks/messages.ts`)
|
||||
|
||||
```typescript
|
||||
// 通用 mock 消息构造器
|
||||
export function mockAssistantMessage(overrides?: Partial<AssistantMessage>): AssistantMessage
|
||||
export function mockUserMessage(content: string, overrides?: Partial<UserMessage>): UserMessage
|
||||
export function mockSystemMessage(overrides?: Partial<SystemMessage>): SystemMessage
|
||||
export function mockToolUseBlock(name: string, input: unknown): ToolUseBlock
|
||||
export function mockToolResultMessage(toolUseId: string, content: string): UserMessage
|
||||
```
|
||||
|
||||
## 集成测试场景
|
||||
|
||||
### describe('Message pipeline')
|
||||
|
||||
- test('create → normalize → API format produces valid request') — 创建消息 → normalizeMessagesForAPI → 验证输出结构
|
||||
- test('tool use and tool result pairing is preserved through normalization')
|
||||
- test('merge + normalize handles conversation with interruptions')
|
||||
@@ -1,112 +0,0 @@
|
||||
# Cron 调度测试计划
|
||||
|
||||
## 概述
|
||||
|
||||
Cron 模块提供 cron 表达式解析、下次运行时间计算和人类可读描述。全部为纯函数,无外部依赖,是最适合单元测试的模块之一。
|
||||
|
||||
## 被测文件
|
||||
|
||||
| 文件 | 关键导出 |
|
||||
|------|----------|
|
||||
| `src/utils/cron.ts` | `CronFields`, `parseCronExpression`, `computeNextCronRun`, `cronToHuman` |
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### describe('parseCronExpression')
|
||||
|
||||
#### 有效表达式
|
||||
|
||||
- test('parses wildcard fields') — `'* * * * *'` → 每个字段为完整范围
|
||||
- test('parses specific values') — `'30 14 1 6 3'` → minute=[30], hour=[14], dom=[1], month=[6], dow=[3]
|
||||
- test('parses step syntax') — `'*/5 * * * *'` → minute=[0,5,10,...,55]
|
||||
- test('parses range syntax') — `'1-5 * * * *'` → minute=[1,2,3,4,5]
|
||||
- test('parses range with step') — `'1-10/3 * * * *'` → minute=[1,4,7,10]
|
||||
- test('parses comma-separated list') — `'1,15,30 * * * *'` → minute=[1,15,30]
|
||||
- test('parses day-of-week 7 as Sunday alias') — `'0 0 * * 7'` → dow=[0]
|
||||
- test('parses range with day-of-week 7') — `'0 0 * * 5-7'` → dow=[0,5,6]
|
||||
- test('parses complex combined expression') — `'0,30 9-17 * * 1-5'` → 工作日 9-17 每半小时
|
||||
|
||||
#### 无效表达式
|
||||
|
||||
- test('returns null for wrong field count') — `'* * *'` → null
|
||||
- test('returns null for out-of-range values') — `'60 * * * *'` → null(minute max=59)
|
||||
- test('returns null for invalid step') — `'*/0 * * * *'` → null(step=0)
|
||||
- test('returns null for reversed range') — `'10-5 * * * *'` → null(lo>hi)
|
||||
- test('returns null for empty string') — `''` → null
|
||||
- test('returns null for non-numeric tokens') — `'abc * * * *'` → null
|
||||
|
||||
#### 字段范围验证
|
||||
|
||||
- test('minute: 0-59')
|
||||
- test('hour: 0-23')
|
||||
- test('dayOfMonth: 1-31')
|
||||
- test('month: 1-12')
|
||||
- test('dayOfWeek: 0-6 (plus 7 alias)')
|
||||
|
||||
---
|
||||
|
||||
### describe('computeNextCronRun')
|
||||
|
||||
#### 基本匹配
|
||||
|
||||
- test('finds next minute') — from 14:30:45, cron `'31 14 * * *'` → 14:31:00 同天
|
||||
- test('finds next hour') — from 14:30, cron `'0 15 * * *'` → 15:00 同天
|
||||
- test('rolls to next day') — from 14:30, cron `'0 10 * * *'` → 10:00 次日
|
||||
- test('rolls to next month') — from 1月31日, cron `'0 0 1 * *'` → 2月1日
|
||||
- test('is strictly after from date') — from 恰好匹配时应返回下一次而非当前时间
|
||||
|
||||
#### DOM/DOW 语义
|
||||
|
||||
- test('OR semantics when both dom and dow constrained') — dom=15, dow=3 → 匹配 15 号 OR 周三
|
||||
- test('only dom constrained uses dom') — dom=15, dow=* → 只匹配 15 号
|
||||
- test('only dow constrained uses dow') — dom=*, dow=3 → 只匹配周三
|
||||
- test('both wildcarded matches every day') — dom=*, dow=* → 每天
|
||||
|
||||
#### 边界情况
|
||||
|
||||
- test('handles month boundary') — 从 2 月 28 日寻找 2 月 29 日或 3 月 1 日
|
||||
- test('returns null after 366-day search') — 不可能匹配的表达式返回 null(理论上不会发生)
|
||||
- test('handles step across midnight') — `'0 0 * * *'` 从 23:59 → 次日 0:00
|
||||
|
||||
#### 每 N 分钟
|
||||
|
||||
- test('every 5 minutes from arbitrary time') — `'*/5 * * * *'` from 14:32 → 14:35
|
||||
- test('every minute') — `'* * * * *'` from 14:32:45 → 14:33:00
|
||||
|
||||
---
|
||||
|
||||
### describe('cronToHuman')
|
||||
|
||||
#### 常见模式
|
||||
|
||||
- test('every N minutes') — `'*/5 * * * *'` → `'Every 5 minutes'`
|
||||
- test('every minute') — `'*/1 * * * *'` → `'Every minute'`
|
||||
- test('every hour at :00') — `'0 * * * *'` → `'Every hour'`
|
||||
- test('every hour at :30') — `'30 * * * *'` → `'Every hour at :30'`
|
||||
- test('every N hours') — `'0 */2 * * *'` → `'Every 2 hours'`
|
||||
- test('daily at specific time') — `'30 9 * * *'` → `'Every day at 9:30 AM'`
|
||||
- test('specific day of week') — `'0 9 * * 3'` → `'Every Wednesday at 9:00 AM'`
|
||||
- test('weekdays') — `'0 9 * * 1-5'` → `'Weekdays at 9:00 AM'`
|
||||
|
||||
#### Fallback
|
||||
|
||||
- test('returns raw cron for complex patterns') — 非常见模式返回原始 cron 字符串
|
||||
- test('returns raw cron for wrong field count') — `'* * *'` → 原样返回
|
||||
|
||||
#### UTC 模式
|
||||
|
||||
- test('UTC option formats time in local timezone') — `{ utc: true }` 时 UTC 时间转本地显示
|
||||
- test('UTC midnight crossing adjusts day name') — UTC 时间跨天时本地星期名正确
|
||||
|
||||
---
|
||||
|
||||
## Mock 需求
|
||||
|
||||
**无需 Mock**。所有函数为纯函数,唯一的外部依赖是 `Date` 构造器和 `toLocaleTimeString`,可通过传入确定性的 `from` 参数控制。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `cronToHuman` 的时间格式化依赖系统 locale,测试中建议使用 `'en-US'` locale 或只验证部分输出
|
||||
- `computeNextCronRun` 使用本地时区,DST 相关测试需注意运行环境
|
||||
@@ -1,106 +0,0 @@
|
||||
# Git 工具测试计划
|
||||
|
||||
## 概述
|
||||
|
||||
Git 工具模块提供 git 远程 URL 规范化、仓库根目录查找、裸仓库安全检测等功能。测试重点是纯函数的 URL 规范化和需要文件系统 mock 的仓库发现逻辑。
|
||||
|
||||
## 被测文件
|
||||
|
||||
| 文件 | 关键导出 |
|
||||
|------|----------|
|
||||
| `src/utils/git.ts` | `normalizeGitRemoteUrl`, `findGitRoot`, `findCanonicalGitRoot`, `getIsGit`, `isAtGitRoot`, `getRepoRemoteHash`, `isCurrentDirectoryBareGitRepo`, `gitExe`, `getGitState`, `stashToCleanState`, `preserveGitStateForIssue` |
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### describe('normalizeGitRemoteUrl')(纯函数)
|
||||
|
||||
#### SSH 格式
|
||||
|
||||
- test('normalizes SSH URL') — `'git@github.com:owner/repo.git'` → `'github.com/owner/repo'`
|
||||
- test('normalizes SSH URL without .git suffix') — `'git@github.com:owner/repo'` → `'github.com/owner/repo'`
|
||||
- test('handles GitLab SSH') — `'git@gitlab.com:group/subgroup/repo.git'` → `'gitlab.com/group/subgroup/repo'`
|
||||
|
||||
#### HTTPS 格式
|
||||
|
||||
- test('normalizes HTTPS URL') — `'https://github.com/owner/repo.git'` → `'github.com/owner/repo'`
|
||||
- test('normalizes HTTPS URL without .git suffix') — `'https://github.com/owner/repo'` → `'github.com/owner/repo'`
|
||||
- test('normalizes HTTP URL') — `'http://github.com/owner/repo.git'` → `'github.com/owner/repo'`
|
||||
|
||||
#### SSH:// 协议格式
|
||||
|
||||
- test('normalizes ssh:// URL') — `'ssh://git@github.com/owner/repo'` → `'github.com/owner/repo'`
|
||||
- test('handles user prefix in ssh://') — `'ssh://user@host/path'` → `'host/path'`
|
||||
|
||||
#### 代理 URL(CCR git proxy)
|
||||
|
||||
- test('normalizes legacy proxy URL') — `'http://local_proxy@127.0.0.1:16583/git/owner/repo'` → `'github.com/owner/repo'`
|
||||
- test('normalizes GHE proxy URL') — `'http://user@127.0.0.1:8080/git/ghe.company.com/owner/repo'` → `'ghe.company.com/owner/repo'`
|
||||
|
||||
#### 边界情况
|
||||
|
||||
- test('returns null for empty string') — `''` → null
|
||||
- test('returns null for whitespace') — `' '` → null
|
||||
- test('returns null for unrecognized format') — `'not-a-url'` → null
|
||||
- test('output is lowercase') — `'git@GitHub.com:Owner/Repo.git'` → `'github.com/owner/repo'`
|
||||
- test('SSH and HTTPS for same repo produce same result') — 相同仓库不同协议 → 相同输出
|
||||
|
||||
---
|
||||
|
||||
### describe('findGitRoot')(需文件系统 Mock)
|
||||
|
||||
- test('finds git root from nested directory') — `/project/src/utils/` → `/project/`(假设 `/project/.git` 存在)
|
||||
- test('finds git root from root directory') — `/project/` → `/project/`
|
||||
- test('returns null for non-git directory') — 无 `.git` → null
|
||||
- test('handles worktree .git file') — `.git` 为文件时也识别
|
||||
- test('memoizes results') — 同一路径不重复查找
|
||||
|
||||
### describe('findCanonicalGitRoot')
|
||||
|
||||
- test('returns same as findGitRoot for regular repo')
|
||||
- test('resolves worktree to main repo root') — worktree 路径 → 主仓库根目录
|
||||
- test('returns null for non-git directory')
|
||||
|
||||
### describe('gitExe')
|
||||
|
||||
- test('returns git path string') — 返回字符串
|
||||
- test('memoizes the result') — 多次调用返回同一值
|
||||
|
||||
---
|
||||
|
||||
### describe('getRepoRemoteHash')(需 Mock)
|
||||
|
||||
- test('returns 16-char hex hash') — 返回值为 16 位十六进制字符串
|
||||
- test('returns null when no remote') — 无 remote URL 时返回 null
|
||||
- test('same repo SSH/HTTPS produce same hash') — 不同协议同一仓库 hash 相同
|
||||
|
||||
---
|
||||
|
||||
### describe('isCurrentDirectoryBareGitRepo')(需文件系统 Mock)
|
||||
|
||||
- test('detects bare git repo attack vector') — 目录含 HEAD + objects/ + refs/ 但无有效 .git/HEAD → true
|
||||
- test('returns false for normal directory') — 普通目录 → false
|
||||
- test('returns false for regular git repo') — 有效 .git 目录 → false
|
||||
|
||||
---
|
||||
|
||||
## Mock 需求
|
||||
|
||||
| 依赖 | Mock 方式 | 说明 |
|
||||
|------|-----------|------|
|
||||
| `statSync` | mock module | `findGitRoot` 中的 .git 检测 |
|
||||
| `readFileSync` | mock module | worktree .git 文件读取 |
|
||||
| `realpathSync` | mock module | 路径解析 |
|
||||
| `execFileNoThrow` | mock module | git 命令执行 |
|
||||
| `whichSync` | mock module | `gitExe` 中的 git 路径查找 |
|
||||
| `getCwd` | mock module | 当前工作目录 |
|
||||
| `getRemoteUrl` | mock module | `getRepoRemoteHash` 依赖 |
|
||||
| 临时目录 | `mkdtemp` | 集成测试中创建临时 git 仓库 |
|
||||
|
||||
## 集成测试场景
|
||||
|
||||
### describe('Git repo discovery')(放在 tests/integration/)
|
||||
|
||||
- test('findGitRoot works in actual git repo') — 在临时 git init 的目录中验证
|
||||
- test('normalizeGitRemoteUrl + getRepoRemoteHash produces stable hash') — URL → hash 端到端验证
|
||||
@@ -1,161 +0,0 @@
|
||||
# 配置系统测试计划
|
||||
|
||||
## 概述
|
||||
|
||||
配置系统包含全局配置(GlobalConfig)、项目配置(ProjectConfig)和设置(Settings)三层。测试重点是纯函数校验逻辑、Zod schema 验证和配置合并策略。
|
||||
|
||||
## 被测文件
|
||||
|
||||
| 文件 | 关键导出 |
|
||||
|------|----------|
|
||||
| `src/utils/config.ts` | `getGlobalConfig`, `saveGlobalConfig`, `getCurrentProjectConfig`, `checkHasTrustDialogAccepted`, `isPathTrusted`, `getOrCreateUserID`, `isAutoUpdaterDisabled` |
|
||||
| `src/utils/settings/settings.ts` | `getSettingsForSource`, `parseSettingsFile`, `getSettingsFilePathForSource`, `getInitialSettings` |
|
||||
| `src/utils/settings/types.ts` | `SettingsSchema`(Zod schema) |
|
||||
| `src/utils/settings/validation.ts` | 设置验证函数 |
|
||||
| `src/utils/settings/constants.ts` | 设置常量 |
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### src/utils/config.ts — 纯函数/常量
|
||||
|
||||
#### describe('DEFAULT_GLOBAL_CONFIG')
|
||||
|
||||
- test('has all required fields') — 默认配置对象包含所有必需字段
|
||||
- test('has null auth fields by default') — oauthAccount 等为 null
|
||||
|
||||
#### describe('DEFAULT_PROJECT_CONFIG')
|
||||
|
||||
- test('has empty allowedTools') — 默认为空数组
|
||||
- test('has empty mcpServers') — 默认为空对象
|
||||
|
||||
#### describe('isAutoUpdaterDisabled')
|
||||
|
||||
- test('returns true when CLAUDE_CODE_DISABLE_AUTOUPDATER is set') — env 设置时禁用
|
||||
- test('returns true when disableAutoUpdater config is true')
|
||||
- test('returns false by default')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/config.ts — 需 Mock
|
||||
|
||||
#### describe('getGlobalConfig')
|
||||
|
||||
- test('returns cached config on subsequent calls') — 缓存机制
|
||||
- test('returns TEST_GLOBAL_CONFIG_FOR_TESTING in test mode')
|
||||
- test('reads config from ~/.claude.json')
|
||||
- test('returns default config when file does not exist')
|
||||
|
||||
#### describe('saveGlobalConfig')
|
||||
|
||||
- test('applies updater function to current config') — updater 修改被保存
|
||||
- test('creates backup before writing') — 写入前备份
|
||||
- test('prevents auth state loss') — `wouldLoseAuthState` 检查
|
||||
|
||||
#### describe('getCurrentProjectConfig')
|
||||
|
||||
- test('returns project config for current directory')
|
||||
- test('returns default config when no project config exists')
|
||||
|
||||
#### describe('checkHasTrustDialogAccepted')
|
||||
|
||||
- test('returns true when trust is accepted in current directory')
|
||||
- test('returns true when parent directory is trusted') — 父目录信任传递
|
||||
- test('returns false when no trust accepted')
|
||||
- test('caches positive results')
|
||||
|
||||
#### describe('isPathTrusted')
|
||||
|
||||
- test('returns true for trusted path')
|
||||
- test('returns false for untrusted path')
|
||||
|
||||
#### describe('getOrCreateUserID')
|
||||
|
||||
- test('returns existing user ID from config')
|
||||
- test('creates and persists new ID when none exists')
|
||||
- test('returns consistent ID across calls')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/settings/settings.ts
|
||||
|
||||
#### describe('getSettingsFilePathForSource')
|
||||
|
||||
- test('returns ~/.claude/settings.json for userSettings') — 全局用户设置路径
|
||||
- test('returns .claude/settings.json for projectSettings') — 项目设置路径
|
||||
- test('returns .claude/settings.local.json for localSettings') — 本地设置路径
|
||||
|
||||
#### describe('parseSettingsFile')(需 Mock 文件读取)
|
||||
|
||||
- test('parses valid settings JSON') — 有效 JSON → `{ settings, errors: [] }`
|
||||
- test('returns errors for invalid fields') — 无效字段 → errors 非空
|
||||
- test('returns empty settings for non-existent file')
|
||||
- test('handles JSON with comments') — JSONC 格式支持
|
||||
|
||||
#### describe('getInitialSettings')
|
||||
|
||||
- test('merges settings from all sources') — user + project + local 合并
|
||||
- test('later sources override earlier ones') — 优先级:policy > user > project > local
|
||||
|
||||
---
|
||||
|
||||
### src/utils/settings/types.ts — Zod Schema 验证
|
||||
|
||||
#### describe('SettingsSchema validation')
|
||||
|
||||
- test('accepts valid minimal settings') — `{}` → 有效
|
||||
- test('accepts permissions block') — `{ permissions: { allow: ['Bash(*)'] } }` → 有效
|
||||
- test('accepts model setting') — `{ model: 'sonnet' }` → 有效
|
||||
- test('accepts hooks configuration') — 有效的 hooks 对象被接受
|
||||
- test('accepts env variables') — `{ env: { FOO: 'bar' } }` → 有效
|
||||
- test('rejects unknown top-level keys') — 未知字段被拒绝或忽略(取决于 schema 配置)
|
||||
- test('rejects invalid permission mode') — `{ permissions: { defaultMode: 'invalid' } }` → 错误
|
||||
- test('rejects non-string model') — `{ model: 123 }` → 错误
|
||||
- test('accepts mcpServers configuration') — MCP server 配置有效
|
||||
- test('accepts sandbox configuration')
|
||||
|
||||
---
|
||||
|
||||
### src/utils/settings/validation.ts
|
||||
|
||||
#### describe('settings validation')
|
||||
|
||||
- test('validates permission rules format') — `'Bash(npm install)'` 格式正确
|
||||
- test('rejects malformed permission rules')
|
||||
- test('validates hook configuration structure')
|
||||
- test('provides helpful error messages') — 错误信息包含字段路径
|
||||
|
||||
---
|
||||
|
||||
## Mock 需求
|
||||
|
||||
| 依赖 | Mock 方式 | 说明 |
|
||||
|------|-----------|------|
|
||||
| 文件系统 | 临时目录 + mock | config 文件读写 |
|
||||
| `lockfile` | mock module | 文件锁 |
|
||||
| `getCwd` | mock module | 项目路径判断 |
|
||||
| `findGitRoot` | mock module | 项目根目录 |
|
||||
| `process.env` | 直接设置/恢复 | `CLAUDE_CODE_DISABLE_AUTOUPDATER` 等 |
|
||||
|
||||
### 测试用临时文件结构
|
||||
|
||||
```
|
||||
/tmp/claude-test-xxx/
|
||||
├── .claude/
|
||||
│ ├── settings.json # projectSettings
|
||||
│ └── settings.local.json # localSettings
|
||||
├── home/
|
||||
│ └── .claude/
|
||||
│ └── settings.json # userSettings(mock HOME)
|
||||
└── project/
|
||||
└── .git/
|
||||
```
|
||||
|
||||
## 集成测试场景
|
||||
|
||||
### describe('Config + Settings merge pipeline')
|
||||
|
||||
- test('user settings + project settings merge correctly') — 验证合并优先级
|
||||
- test('deny rules from settings are reflected in tool permission context')
|
||||
- test('trust dialog state persists across config reads')
|
||||
@@ -1,361 +0,0 @@
|
||||
# Plan 10 — 修复 WEAK 评分测试文件
|
||||
|
||||
> 优先级:高 | 8 个文件 | 预估新增/修改 ~60 个测试用例
|
||||
|
||||
本计划修复 testing-spec.md 中评定为 WEAK 的 8 个测试文件的断言缺陷和覆盖缺口。
|
||||
|
||||
---
|
||||
|
||||
## 10.1 `src/utils/__tests__/format.test.ts`
|
||||
|
||||
**问题**:`formatNumber`、`formatTokens`、`formatRelativeTime` 使用 `toContain` 代替精确匹配,无法检测格式回归。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### formatNumber — toContain → toBe
|
||||
|
||||
```typescript
|
||||
// 当前(弱)
|
||||
expect(formatNumber(1321)).toContain("k");
|
||||
expect(formatNumber(1500000)).toContain("m");
|
||||
|
||||
// 修复为
|
||||
expect(formatNumber(1321)).toBe("1.3k");
|
||||
expect(formatNumber(1500000)).toBe("1.5m");
|
||||
```
|
||||
|
||||
> 注意:`Intl.NumberFormat` 输出可能因 locale 不同。若 CI locale 不一致,改用 `toMatch(/^\d+(\.\d)?[km]$/)` 正则匹配。
|
||||
|
||||
#### formatTokens — 补精确断言
|
||||
|
||||
```typescript
|
||||
expect(formatTokens(1000)).toBe("1k");
|
||||
expect(formatTokens(1500)).toBe("1.5k");
|
||||
```
|
||||
|
||||
#### formatRelativeTime — toContain → toBe
|
||||
|
||||
```typescript
|
||||
// 当前(弱)
|
||||
expect(formatRelativeTime(diff, now)).toContain("30");
|
||||
expect(formatRelativeTime(diff, now)).toContain("ago");
|
||||
|
||||
// 修复为
|
||||
expect(formatRelativeTime(diff, now)).toBe("30s ago");
|
||||
```
|
||||
|
||||
#### 新增:formatDuration 进位边界
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| 59.5s 进位 | 59500ms | 至少含 `1m` |
|
||||
| 59m59s 进位 | 3599000ms | 至少含 `1h` |
|
||||
| sub-millisecond | 0.5ms | `"<1ms"` 或 `"0ms"` |
|
||||
|
||||
#### 新增:未测试函数
|
||||
|
||||
| 函数 | 最少用例 |
|
||||
|------|---------|
|
||||
| `formatRelativeTimeAgo` | 2(过去 / 未来) |
|
||||
| `formatLogMetadata` | 1(基本调用不抛错) |
|
||||
| `formatResetTime` | 2(有值 / null) |
|
||||
| `formatResetText` | 1(基本调用) |
|
||||
|
||||
---
|
||||
|
||||
## 10.2 `src/tools/shared/__tests__/gitOperationTracking.test.ts`
|
||||
|
||||
**问题**:`detectGitOperation` 内部调用 `getCommitCounter()`、`getPrCounter()`、`logEvent()`,测试产生分析副作用。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 添加 analytics mock
|
||||
|
||||
在文件顶部添加 `mock.module`:
|
||||
|
||||
```typescript
|
||||
import { mock, afterAll, afterEach, beforeEach } from "bun:test";
|
||||
|
||||
mock.module("src/services/analytics/index.ts", () => ({
|
||||
logEvent: mock(() => {}),
|
||||
}));
|
||||
|
||||
mock.module("src/bootstrap/state.ts", () => ({
|
||||
getCommitCounter: mock(() => ({ increment: mock(() => {}) })),
|
||||
getPrCounter: mock(() => ({ increment: mock(() => {}) })),
|
||||
}));
|
||||
```
|
||||
|
||||
> 需验证 `detectGitOperation` 的实际导入路径,按需调整 mock 目标。
|
||||
|
||||
#### 新增:缺失的 GH PR actions
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| gh pr edit | `'gh pr edit 123 --title "fix"'` | `result.pr.number === 123` |
|
||||
| gh pr close | `'gh pr close 456'` | `result.pr.number === 456` |
|
||||
| gh pr ready | `'gh pr ready 789'` | `result.pr.number === 789` |
|
||||
| gh pr comment | `'gh pr comment 123 --body "done"'` | `result.pr.number === 123` |
|
||||
|
||||
#### 新增:parseGitCommitId 边界
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| 完整 40 字符 SHA | `'[abcdef0123456789abcdef0123456789abcdef01] ...'` | 返回完整 40 字符 |
|
||||
| 畸形括号输出 | `'create mode 100644 file.txt'` | 返回 `null` |
|
||||
|
||||
---
|
||||
|
||||
## 10.3 `src/utils/permissions/__tests__/PermissionMode.test.ts`
|
||||
|
||||
**问题**:`isExternalPermissionMode` 在非 ant 环境永远返回 true,false 路径从未执行;mode 覆盖不完整。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 补全 mode 覆盖
|
||||
|
||||
| 函数 | 缺失的 mode |
|
||||
|------|-------------|
|
||||
| `permissionModeTitle` | `bypassPermissions`, `dontAsk` |
|
||||
| `permissionModeShortTitle` | `dontAsk`, `acceptEdits` |
|
||||
| `getModeColor` | `dontAsk`, `acceptEdits`, `plan` |
|
||||
| `permissionModeFromString` | `acceptEdits`, `bypassPermissions` |
|
||||
| `toExternalPermissionMode` | `acceptEdits`, `bypassPermissions` |
|
||||
|
||||
#### 修复 isExternalPermissionMode
|
||||
|
||||
```typescript
|
||||
// 当前:只测了非 ant 环境(永远 true)
|
||||
// 需要新增 ant 环境测试
|
||||
describe("when USER_TYPE is 'ant'", () => {
|
||||
beforeEach(() => {
|
||||
process.env.USER_TYPE = "ant";
|
||||
});
|
||||
afterEach(() => {
|
||||
delete process.env.USER_TYPE;
|
||||
});
|
||||
|
||||
test("returns false for 'auto' in ant context", () => {
|
||||
expect(isExternalPermissionMode("auto")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for 'bubble' in ant context", () => {
|
||||
expect(isExternalPermissionMode("bubble")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns true for non-ant modes in ant context", () => {
|
||||
expect(isExternalPermissionMode("plan")).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增:permissionModeSchema
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| 有效 mode | `'plan'` | `success: true` |
|
||||
| 无效 mode | `'invalid'` | `success: false` |
|
||||
|
||||
---
|
||||
|
||||
## 10.4 `src/utils/permissions/__tests__/dangerousPatterns.test.ts`
|
||||
|
||||
**问题**:纯数据 smoke test,无行为验证。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 新增:重复值检查
|
||||
|
||||
```typescript
|
||||
test("CROSS_PLATFORM_CODE_EXEC has no duplicates", () => {
|
||||
const set = new Set(CROSS_PLATFORM_CODE_EXEC);
|
||||
expect(set.size).toBe(CROSS_PLATFORM_CODE_EXEC.length);
|
||||
});
|
||||
|
||||
test("DANGEROUS_BASH_PATTERNS has no duplicates", () => {
|
||||
const set = new Set(DANGEROUS_BASH_PATTERNS);
|
||||
expect(set.size).toBe(DANGEROUS_BASH_PATTERNS.length);
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增:全量成员断言(用 Set 确保精确)
|
||||
|
||||
```typescript
|
||||
test("CROSS_PLATFORM_CODE_EXEC contains expected interpreters", () => {
|
||||
const expected = ["node", "python", "python3", "ruby", "perl", "php",
|
||||
"bun", "deno", "npx", "tsx"];
|
||||
const set = new Set(CROSS_PLATFORM_CODE_EXEC);
|
||||
for (const entry of expected) {
|
||||
expect(set.has(entry)).toBe(true);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增:空字符串不匹配
|
||||
|
||||
```typescript
|
||||
test("empty string does not match any pattern", () => {
|
||||
for (const pattern of DANGEROUS_BASH_PATTERNS) {
|
||||
expect("".startsWith(pattern)).toBe(false);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10.5 `src/utils/__tests__/zodToJsonSchema.test.ts`
|
||||
|
||||
**问题**:object 属性仅 `toBeDefined` 未验证类型结构;optional 字段未验证 absence。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 修复 object schema 测试
|
||||
|
||||
```typescript
|
||||
// 当前(弱)
|
||||
expect(schema.properties!.name).toBeDefined();
|
||||
expect(schema.properties!.age).toBeDefined();
|
||||
|
||||
// 修复为
|
||||
expect(schema.properties!.name).toEqual({ type: "string" });
|
||||
expect(schema.properties!.age).toEqual({ type: "number" });
|
||||
```
|
||||
|
||||
#### 修复 optional 字段测试
|
||||
|
||||
```typescript
|
||||
test("optional field is not in required array", () => {
|
||||
const schema = zodToJsonSchema(z.object({
|
||||
required: z.string(),
|
||||
optional: z.string().optional(),
|
||||
}));
|
||||
expect(schema.required).toEqual(["required"]);
|
||||
expect(schema.required).not.toContain("optional");
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增:缺失的 schema 类型
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| `z.literal("foo")` | `z.literal("foo")` | `{ const: "foo" }` |
|
||||
| `z.null()` | `z.null()` | `{ type: "null" }` |
|
||||
| `z.union()` | `z.union([z.string(), z.number()])` | `{ anyOf: [...] }` |
|
||||
| `z.record()` | `z.record(z.string(), z.number())` | `{ type: "object", additionalProperties: { type: "number" } }` |
|
||||
| `z.tuple()` | `z.tuple([z.string(), z.number()])` | `{ type: "array", items: [...], additionalItems: false }` |
|
||||
| 嵌套 object | `z.object({ a: z.object({ b: z.string() }) })` | 验证嵌套属性结构 |
|
||||
|
||||
---
|
||||
|
||||
## 10.6 `src/utils/__tests__/envValidation.test.ts`
|
||||
|
||||
**问题**:`validateBoundedIntEnvVar` lower bound=100 时 value=1 返回 `status: "valid"`,疑似源码 bug。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 验证 lower bound 行为
|
||||
|
||||
```typescript
|
||||
// 当前测试
|
||||
test("value of 1 with lower bound 100", () => {
|
||||
const result = validateBoundedIntEnvVar("1", { defaultValue: 100, upperLimit: 1000, lowerLimit: 100 });
|
||||
// 如果源码有 bug,这里应该暴露
|
||||
expect(result.effective).toBeGreaterThanOrEqual(100);
|
||||
expect(result.status).toBe(result.effective !== 100 ? "capped" : "valid");
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增边界用例
|
||||
|
||||
| 用例 | value | lowerLimit | 期望 |
|
||||
|------|-------|------------|------|
|
||||
| 低于 lower bound | `"50"` | 100 | `effective: 100, status: "capped"` |
|
||||
| 等于 lower bound | `"100"` | 100 | `effective: 100, status: "valid"` |
|
||||
| 浮点截断 | `"50.7"` | 100 | `effective: 100`(parseInt 截断后 cap) |
|
||||
| 空白字符 | `" 500 "` | 1 | `effective: 500, status: "valid"` |
|
||||
| defaultValue 为 0 | `"0"` | 0 | 需确认 `parsed <= 0` 逻辑 |
|
||||
|
||||
> **行动**:先确认 `validateBoundedIntEnvVar` 源码中 lower bound 的实际执行路径。如果确实不生效,需先修源码再补测试。
|
||||
|
||||
---
|
||||
|
||||
## 10.7 `src/utils/__tests__/file.test.ts`
|
||||
|
||||
**问题**:`addLineNumbers` 仅 `toContain`,未验证完整格式。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 修复 addLineNumbers 断言
|
||||
|
||||
```typescript
|
||||
// 当前(弱)
|
||||
expect(result).toContain("1");
|
||||
expect(result).toContain("hello");
|
||||
|
||||
// 修复为(需确定 isCompactLinePrefixEnabled 行为)
|
||||
// 假设 compact=false,格式为 " 1→hello"
|
||||
test("formats single line with tab prefix", () => {
|
||||
// 先确认环境,如果 compact 模式不确定,用正则
|
||||
expect(result).toMatch(/^\s*\d+[→\t]hello$/m);
|
||||
});
|
||||
```
|
||||
|
||||
#### 新增:stripLineNumberPrefix 边界
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| 纯数字行 | `"123"` | `""` |
|
||||
| 无内容前缀 | `"→"` | `""` |
|
||||
| compact 格式 `"1\thello"` | `"1\thello"` | `"hello"` |
|
||||
|
||||
#### 新增:pathsEqual 边界
|
||||
|
||||
| 用例 | a | b | 期望 |
|
||||
|------|---|---|------|
|
||||
| 尾部斜杠差异 | `"/a/b"` | `"/a/b/"` | `false` |
|
||||
| `..` 段 | `"/a/../b"` | `"/b"` | 视实现而定 |
|
||||
|
||||
---
|
||||
|
||||
## 10.8 `src/utils/__tests__/notebook.test.ts`
|
||||
|
||||
**问题**:`mapNotebookCellsToToolResult` 内容检查用 `toContain`,未验证 XML 格式。
|
||||
|
||||
### 修改清单
|
||||
|
||||
#### 修复 content 断言
|
||||
|
||||
```typescript
|
||||
// 当前(弱)
|
||||
expect(result).toContain("cell-0");
|
||||
expect(result).toContain("print('hello')");
|
||||
|
||||
// 修复为
|
||||
expect(result).toContain('<cell id="cell-0">');
|
||||
expect(result).toContain("</cell>");
|
||||
```
|
||||
|
||||
#### 新增:parseCellId 边界
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| 负数 | `"cell--1"` | `null` |
|
||||
| 前导零 | `"cell-007"` | `7` |
|
||||
| 极大数 | `"cell-999999999"` | `999999999` |
|
||||
|
||||
#### 新增:mapNotebookCellsToToolResult 边界
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| 空 data 数组 | `{ cells: [] }` | 空字符串或空结果 |
|
||||
| 无 cell_id | `{ cell_type: "code", source: "x" }` | fallback 到 `cell-${index}` |
|
||||
| error output | `{ output_type: "error", ename: "Error", evalue: "msg" }` | 包含 error 信息 |
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] `bun test` 全部通过
|
||||
- [ ] 8 个文件评分从 WEAK 提升至 ACCEPTABLE 或 GOOD
|
||||
- [ ] `toContain` 仅用于警告文本等确实不确定精确值的场景
|
||||
- [ ] envValidation bug 确认并修复(或确认非 bug 并更新测试)
|
||||
@@ -1,177 +0,0 @@
|
||||
# Plan 11 — 加强 ACCEPTABLE 评分测试
|
||||
|
||||
> 优先级:中 | ~15 个文件 | 预估新增 ~80 个测试用例
|
||||
|
||||
本计划对 ACCEPTABLE 评分文件中的具体缺陷进行定向加强。每个条目只列出需要改动的部分,不做全量重写。
|
||||
|
||||
---
|
||||
|
||||
## 11.1 `src/utils/__tests__/diff.test.ts`
|
||||
|
||||
| 改动 | 当前 | 改为 |
|
||||
|------|------|------|
|
||||
| `getPatchFromContents` 断言 | `hunks.length > 0` | 验证具体 `+`/`-` 行内容 |
|
||||
| `$` 字符转义 | 未测试 | 新增含 `$` 的内容测试 |
|
||||
| `ignoreWhitespace` 选项 | 未测试 | 新增 `ignoreWhitespace: true` 用例 |
|
||||
| 删除全部内容 | 未测试 | `newContent: ""` |
|
||||
| 多 hunk 偏移 | `adjustHunkLineNumbers` 仅单 hunk | 新增多 hunk 同数组测试 |
|
||||
|
||||
---
|
||||
|
||||
## 11.2 `src/utils/__tests__/path.test.ts`
|
||||
|
||||
当前仅覆盖 2/5+ 导出函数。新增:
|
||||
|
||||
| 函数 | 最少用例 | 关键边界 |
|
||||
|------|---------|---------|
|
||||
| `expandPath` | 6 | `~/` 展开、绝对路径直通、相对路径、空串、含 null 字节、`~user` 格式 |
|
||||
| `toRelativePath` | 3 | 同级文件、子目录、父目录 |
|
||||
| `sanitizePath` | 3 | 正常路径、含 `..` 段、空串 |
|
||||
|
||||
`containsPathTraversal` 补充:
|
||||
- URL 编码 `%2e%2e%2f`(确认不匹配,记录为非需求)
|
||||
- 混合分隔符 `foo/..\bar`
|
||||
|
||||
`normalizePathForConfigKey` 补充:
|
||||
- 混合分隔符 `foo/bar\baz`
|
||||
- 冗余分隔符 `foo//bar`
|
||||
- Windows 盘符 `C:\foo\bar`
|
||||
|
||||
---
|
||||
|
||||
## 11.3 `src/utils/__tests__/uuid.test.ts`
|
||||
|
||||
| 改动 | 说明 |
|
||||
|------|------|
|
||||
| 大写测试断言强化 | `not.toBeNull()` → 验证标准化输出(小写+连字符格式) |
|
||||
| 新增 `createAgentId` | 3 用例:无 label / 有 label / 输出格式正则 `/^a[a-z]*-[a-f0-9]{16}$/` |
|
||||
| 前后空白 | `" 550e8400-... "` 期望 `null` |
|
||||
|
||||
---
|
||||
|
||||
## 11.4 `src/utils/__tests__/semver.test.ts`
|
||||
|
||||
| 用例 | 输入 | 期望 |
|
||||
|------|------|------|
|
||||
| pre-release 比较 | `gt("1.0.0", "1.0.0-alpha")` | `true` |
|
||||
| pre-release 间比较 | `order("1.0.0-alpha", "1.0.0-beta")` | `-1` |
|
||||
| tilde range | `satisfies("1.2.5", "~1.2.3")` | `true` |
|
||||
| `*` 通配符 | `satisfies("2.0.0", "*")` | `true` |
|
||||
| 畸形版本 | `order("abc", "1.0.0")` | 确认不抛错 |
|
||||
| `0.0.0` | `gt("0.0.0", "0.0.0")` | `false` |
|
||||
|
||||
---
|
||||
|
||||
## 11.5 `src/utils/__tests__/hash.test.ts`
|
||||
|
||||
| 改动 | 当前 | 改为 |
|
||||
|------|------|------|
|
||||
| djb2 32 位检查 | `hash \| 0`(恒 true) | `Number.isSafeInteger(hash) && Math.abs(hash) <= 0x7FFFFFFF` |
|
||||
| hashContent 空串 | 未测试 | 新增 |
|
||||
| hashContent 格式 | 未验证输出为数字串 | `toMatch(/^\d+$/)` |
|
||||
| hashPair 空串 | 未测试 | `hashPair("", "b")`, `hashPair("", "")` |
|
||||
| 已知答案 | 无 | 断言 `djb2Hash("hello")` 为特定值(需先在控制台运行一次确定) |
|
||||
|
||||
---
|
||||
|
||||
## 11.6 `src/utils/__tests__/claudemd.test.ts`
|
||||
|
||||
当前仅覆盖 3 个辅助函数。新增:
|
||||
|
||||
| 用例 | 函数 | 说明 |
|
||||
|------|------|------|
|
||||
| 未闭合注释 | `stripHtmlComments` | `"<!-- no close some text"` → 原样返回 |
|
||||
| 跨行注释 | `stripHtmlComments` | `"<!--\nmulti\nline\n-->text"` → `"text"` |
|
||||
| 同行注释+内容 | `stripHtmlComments` | `"<!-- note -->some text"` → `"some text"` |
|
||||
| 内联代码中的注释 | `stripHtmlComments` | `` `<!-- kept -->` `` → 保留 |
|
||||
| 大小写不敏感 | `isMemoryFilePath` | `"claude.md"`, `"CLAUDE.MD"` |
|
||||
| 非 .md 规则文件 | `isMemoryFilePath` | `.claude/rules/foo.txt` → `false` |
|
||||
| 空数组 | `getLargeMemoryFiles` | `[]` → `[]` |
|
||||
|
||||
---
|
||||
|
||||
## 11.7 `src/tools/FileEditTool/__tests__/utils.test.ts`
|
||||
|
||||
| 函数 | 新增用例 |
|
||||
|------|---------|
|
||||
| `normalizeQuotes` | 混合引号 `"`she said 'hello'"` |
|
||||
| `stripTrailingWhitespace` | CR-only `\r`、无尾部换行、全空白串 |
|
||||
| `findActualString` | 空 content、Unicode content |
|
||||
| `preserveQuoteStyle` | 单引号、缩写中的撇号(如 `it's`)、空串 |
|
||||
| `applyEditToFile` | `replaceAll=true` 零匹配、`oldString` 无尾部 `\n`、多行内容 |
|
||||
|
||||
---
|
||||
|
||||
## 11.8 `src/utils/model/__tests__/providers.test.ts`
|
||||
|
||||
| 改动 | 说明 |
|
||||
|------|------|
|
||||
| 删除 `originalEnv` | 未使用,消除死代码 |
|
||||
| env 恢复改为快照 | `beforeEach` 保存 `process.env`,`afterEach` 恢复 |
|
||||
| 新增三变量同时设置 | bedrock + vertex + foundry 全部为 `"1"`,验证优先级 |
|
||||
| 新增非 `"1"` 值 | `"true"`, `"0"`, `""` |
|
||||
| `isFirstPartyAnthropicBaseUrl` | URL 含路径 `/v1`、含尾斜杠、非 HTTPS |
|
||||
|
||||
---
|
||||
|
||||
## 11.9 `src/utils/__tests__/hyperlink.test.ts`
|
||||
|
||||
| 用例 | 说明 |
|
||||
|------|------|
|
||||
| 空 URL | `createHyperlink("http://x.com", "", { supported: true })` 不抛错 |
|
||||
| undefined supportsHyperlinks | 选项未传时走默认检测 |
|
||||
| 非 ant staging URL | `USER_TYPE !== "ant"` 时 staging 返回 `false` |
|
||||
|
||||
---
|
||||
|
||||
## 11.10 `src/utils/__tests__/objectGroupBy.test.ts`
|
||||
|
||||
| 用例 | 说明 |
|
||||
|------|------|
|
||||
| key 返回 undefined | `(_, i) => undefined` → 全部归入 `undefined` 组 |
|
||||
| key 为特殊字符 | `({ name }) => name` 含空格/中文 |
|
||||
|
||||
---
|
||||
|
||||
## 11.11 `src/utils/__tests__/CircularBuffer.test.ts`
|
||||
|
||||
| 用例 | 说明 |
|
||||
|------|------|
|
||||
| capacity=1 | 添加 2 个元素,仅保留最后一个 |
|
||||
| 空 buffer 调用 getRecent | 返回空数组 |
|
||||
| getRecent(0) | 返回空数组 |
|
||||
|
||||
---
|
||||
|
||||
## 11.12 `src/utils/__tests__/contentArray.test.ts`
|
||||
|
||||
| 用例 | 说明 |
|
||||
|------|------|
|
||||
| 混合交替 | `[tool_result, text, tool_result]` — 验证插入到正确位置 |
|
||||
|
||||
---
|
||||
|
||||
## 11.13 `src/utils/__tests__/argumentSubstitution.test.ts`
|
||||
|
||||
| 用例 | 说明 |
|
||||
|------|------|
|
||||
| 转义引号 | `"he said \"hello\""` |
|
||||
| 越界索引 | `$ARGUMENTS[99]`(参数不够时) |
|
||||
| 多占位符 | `"cmd $0 $1 $0"` |
|
||||
|
||||
---
|
||||
|
||||
## 11.14 `src/utils/__tests__/messages.test.ts`
|
||||
|
||||
| 改动 | 说明 |
|
||||
|------|------|
|
||||
| `normalizeMessages` 断言加强 | 验证拆分后的消息内容,不只是长度 |
|
||||
| `isNotEmptyMessage` 空白 | `[{ type: "text", text: " " }]` |
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] `bun test` 全部通过
|
||||
- [ ] 目标文件评分从 ACCEPTABLE 提升至 GOOD
|
||||
- [ ] 无 `toContain` 用于精确值检查的场景
|
||||
@@ -1,145 +0,0 @@
|
||||
# Plan 12 — Mock 可靠性修复
|
||||
|
||||
> 优先级:高 | 影响 4 个测试文件 | 预估修改 ~15 处
|
||||
|
||||
本计划修复测试中 mock 相关的副作用、状态泄漏和虚假测试。
|
||||
|
||||
---
|
||||
|
||||
## 12.1 `gitOperationTracking.test.ts` — 消除分析副作用
|
||||
|
||||
**当前问题**:`detectGitOperation` 内部调用 `logEvent()`、`getCommitCounter().increment()`、`getPrCounter().increment()`,每次测试运行都触发真实分析代码。
|
||||
|
||||
**修复步骤**:
|
||||
|
||||
1. 读取 `src/tools/shared/gitOperationTracking.ts`,确认 analytics 导入路径
|
||||
2. 在测试文件顶部添加 `mock.module`:
|
||||
|
||||
```typescript
|
||||
import { mock } from "bun:test";
|
||||
|
||||
mock.module("src/services/analytics/index.ts", () => ({
|
||||
logEvent: mock(() => {}),
|
||||
// 按需补充其他导出
|
||||
}));
|
||||
```
|
||||
|
||||
3. 如果 `getCommitCounter` / `getPrCounter` 来自 `src/bootstrap/state.ts`:
|
||||
|
||||
```typescript
|
||||
mock.module("src/bootstrap/state.ts", () => ({
|
||||
getCommitCounter: mock(() => ({ increment: mock(() => {}) })),
|
||||
getPrCounter: mock(() => ({ increment: mock(() => {}) })),
|
||||
// 保留其他被测函数实际需要的导出
|
||||
}));
|
||||
```
|
||||
|
||||
4. 使用 `await import()` 模式加载被测模块
|
||||
5. 运行测试验证无副作用
|
||||
|
||||
**风险**:`mock.module` 会替换整个模块。如果 `detectGitOperation` 还需要其他来自这些模块的导出,需在 mock 工厂中提供。
|
||||
|
||||
---
|
||||
|
||||
## 12.2 `PermissionMode.test.ts` — 修复 `isExternalPermissionMode` 虚假测试
|
||||
|
||||
**当前问题**:`isExternalPermissionMode` 依赖 `process.env.USER_TYPE`。非 ant 环境下所有 mode 都返回 true,测试从未覆盖 false 分支。
|
||||
|
||||
**修复步骤**:
|
||||
|
||||
1. 新增 ant 环境测试组(见 Plan 10.3 详细用例)
|
||||
2. 使用 `beforeEach`/`afterEach` 管理 `process.env.USER_TYPE`
|
||||
|
||||
```typescript
|
||||
describe("when USER_TYPE is 'ant'", () => {
|
||||
const originalUserType = process.env.USER_TYPE;
|
||||
beforeEach(() => { process.env.USER_TYPE = "ant"; });
|
||||
afterEach(() => {
|
||||
if (originalUserType !== undefined) {
|
||||
process.env.USER_TYPE = originalUserType;
|
||||
} else {
|
||||
delete process.env.USER_TYPE;
|
||||
}
|
||||
});
|
||||
|
||||
test("returns false for 'auto'", () => {
|
||||
expect(isExternalPermissionMode("auto")).toBe(false);
|
||||
});
|
||||
test("returns false for 'bubble'", () => {
|
||||
expect(isExternalPermissionMode("bubble")).toBe(false);
|
||||
});
|
||||
test("returns true for 'plan'", () => {
|
||||
expect(isExternalPermissionMode("plan")).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
3. 验证新增测试确实执行 false 路径
|
||||
|
||||
---
|
||||
|
||||
## 12.3 `providers.test.ts` — 环境变量快照恢复
|
||||
|
||||
**当前问题**:
|
||||
- `originalEnv` 声明后未使用
|
||||
- `afterEach` 仅删除已知 3 个 key,如果源码新增 env var,测试间状态泄漏
|
||||
|
||||
**修复步骤**:
|
||||
|
||||
```typescript
|
||||
let savedEnv: Record<string, string | undefined>;
|
||||
|
||||
beforeEach(() => {
|
||||
savedEnv = {};
|
||||
for (const key of Object.keys(process.env)) {
|
||||
savedEnv[key] = process.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 删除所有当前 env,恢复快照
|
||||
for (const key of Object.keys(process.env)) {
|
||||
delete process.env[key];
|
||||
}
|
||||
for (const [key, value] of Object.entries(savedEnv)) {
|
||||
if (value !== undefined) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
> 简化方案:只保存/恢复相关 key 列表 `["CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY", "ANTHROPIC_BASE_URL", "USER_TYPE"]`,但需注释说明新增 env var 时需同步更新。
|
||||
|
||||
---
|
||||
|
||||
## 12.4 `envUtils.test.ts` — 验证环境变量恢复完整性
|
||||
|
||||
**当前状态**:已有 `afterEach` 恢复。需审查:
|
||||
|
||||
1. 确认所有 `describe` 块中的 `afterEach` 都完整恢复了修改的 env var
|
||||
2. 确认 `process.argv` 修改也被恢复(`getClaudeConfigHomeDir` 测试修改了 argv)
|
||||
3. 新增:`afterEach` 中断言无意外 env 泄漏(可选,CI-only)
|
||||
|
||||
---
|
||||
|
||||
## 12.5 `sleep.test.ts` / `memoize.test.ts` — 时间敏感测试加固
|
||||
|
||||
**当前状态**:已有合理 margin。可选加固:
|
||||
|
||||
| 文件 | 用例 | 当前 | 加固 |
|
||||
|------|------|------|------|
|
||||
| `sleep.test.ts` | `resolves after timeout` | `sleep(50)`, check `>= 40ms` | 增大 margin:`sleep(50)`, check `>= 30ms` |
|
||||
| `memoize.test.ts` | stale serve & refresh | TTL=1ms, wait 10ms | 增大 margin:TTL=5ms, wait 50ms |
|
||||
|
||||
> 仅在 CI 出现 flaky 时执行此加固。
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] `gitOperationTracking.test.ts` 无分析副作用(可通过在 mock 中增加 `expect(logEvent).toHaveBeenCalledTimes(N)` 验证)
|
||||
- [ ] `PermissionMode.test.ts` 的 `isExternalPermissionMode` 覆盖 true + false 分支
|
||||
- [ ] `providers.test.ts` 的 `originalEnv` 死代码已删除
|
||||
- [ ] 所有修改 env 的测试文件恢复完整
|
||||
- [ ] `bun test` 全部通过
|
||||
@@ -1,71 +0,0 @@
|
||||
# Plan 13 — truncate CJK/Emoji 补充测试
|
||||
|
||||
> 优先级:中 | 1 个文件 | 预估新增 ~15 个测试用例
|
||||
|
||||
`truncate.ts` 使用 `stringWidth` 和 grapheme segmentation 实现宽度感知截断,但现有测试仅覆盖 ASCII。这是核心场景缺失。
|
||||
|
||||
---
|
||||
|
||||
## 被测函数
|
||||
|
||||
- `truncateToWidth(text, maxWidth)` — 尾部截断加 `…`
|
||||
- `truncateStartToWidth(text, maxWidth)` — 头部截断加 `…`
|
||||
- `truncateToWidthNoEllipsis(text, maxWidth)` — 尾部截断无省略号
|
||||
- `truncatePathMiddle(path, maxLength)` — 路径中间截断
|
||||
- `wrapText(text, maxWidth)` — 按宽度换行
|
||||
|
||||
---
|
||||
|
||||
## 新增用例
|
||||
|
||||
### CJK 全角字符
|
||||
|
||||
| 用例 | 函数 | 输入 | maxWidth | 期望行为 |
|
||||
|------|------|------|----------|----------|
|
||||
| 纯中文截断 | `truncateToWidth` | `"你好世界"` | 4 | `"你好…"` (每个中文字占 2 宽度) |
|
||||
| 中英混合 | `truncateToWidth` | `"hello你好"` | 8 | `"hello你…"` |
|
||||
| 全角不截断 | `truncateToWidth` | `"你好"` | 4 | `"你好"` (恰好 4) |
|
||||
| emoji 单字符 | `truncateToWidth` | `"👋"` | 2 | `"👋"` (emoji 通常 2 宽度) |
|
||||
| emoji 截断 | `truncateToWidth` | `"hello 👋 world"` | 8 | 确认宽度计算正确 |
|
||||
| 头部中文 | `truncateStartToWidth` | `"你好世界"` | 4 | `"…界"` |
|
||||
| 无省略中文 | `truncateToWidthNoEllipsis` | `"你好世界"` | 4 | `"你好"` |
|
||||
|
||||
> **注意**:`stringWidth` 对 CJK/emoji 的宽度计算取决于具体实现。先在 REPL 中运行确认实际宽度再写断言:
|
||||
> ```typescript
|
||||
> import { stringWidth } from "src/utils/truncate.ts";
|
||||
> console.log(stringWidth("你好")); // 确认是 4 还是 2
|
||||
> console.log(stringWidth("👋")); // 确认 emoji 宽度
|
||||
> ```
|
||||
|
||||
### 路径中间截断补充
|
||||
|
||||
| 用例 | 输入 | maxLength | 期望 |
|
||||
|------|------|-----------|------|
|
||||
| 文件名超长 | `"/very/long/path/to/MyComponent.tsx"` | 10 | 含 `…` 且以 `.tsx` 结尾 |
|
||||
| 无斜杠短串 | `"abc"` | 1 | 确认行为不抛错 |
|
||||
| maxLength 极小 | `"/a/b"` | 1 | 确认不抛错 |
|
||||
| maxLength=4 | `"/a/b/c.ts"` | 4 | 确认行为 |
|
||||
|
||||
### wrapText 补充
|
||||
|
||||
| 用例 | 输入 | maxWidth | 期望 |
|
||||
|------|------|----------|------|
|
||||
| 含换行符 | `"hello\nworld"` | 10 | 保留原有换行 |
|
||||
| 宽度=0 | `"hello"` | 0 | 空串或原串(确认不抛错) |
|
||||
|
||||
---
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 在 REPL 中确认 `stringWidth` 对 CJK/emoji 的实际返回值
|
||||
2. 按实际值编写精确断言
|
||||
3. 如果 `stringWidth` 依赖 ICU 或平台特性,添加平台检查(`process.platform !== "win32"` 跳过条件)
|
||||
4. 运行测试
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 至少 5 个 CJK/emoji 相关测试通过
|
||||
- [ ] 断言基于实际 `stringWidth` 返回值,非猜测
|
||||
- [ ] `bun test` 全部通过
|
||||
@@ -1,191 +0,0 @@
|
||||
# Plan 14 — 集成测试搭建
|
||||
|
||||
> 优先级:中 | 新建 ~3 个测试文件 | 预估 ~30 个测试用例
|
||||
|
||||
当前 `tests/integration/` 目录为空,spec 设计的三个集成测试均未创建。本计划搭建 mock 基础设施并实现核心集成测试。
|
||||
|
||||
---
|
||||
|
||||
## 14.1 搭建 `tests/mocks/` 基础设施
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
tests/
|
||||
├── mocks/
|
||||
│ ├── api-responses.ts # Claude API mock 响应
|
||||
│ ├── file-system.ts # 临时文件系统工具
|
||||
│ └── fixtures/
|
||||
│ ├── sample-claudemd.md # CLAUDE.md 样本
|
||||
│ └── sample-messages.json # 消息样本
|
||||
├── integration/
|
||||
│ ├── tool-chain.test.ts
|
||||
│ ├── context-build.test.ts
|
||||
│ └── message-pipeline.test.ts
|
||||
└── helpers/
|
||||
└── setup.ts # 共享 beforeAll/afterAll
|
||||
```
|
||||
|
||||
### `tests/mocks/file-system.ts`
|
||||
|
||||
```typescript
|
||||
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
export async function createTempDir(prefix = "claude-test-"): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), prefix));
|
||||
return dir;
|
||||
}
|
||||
|
||||
export async function cleanupTempDir(dir: string): Promise<void> {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
export async function writeTempFile(dir: string, name: string, content: string): Promise<string> {
|
||||
const path = join(dir, name);
|
||||
await writeFile(path, content, "utf-8");
|
||||
return path;
|
||||
}
|
||||
```
|
||||
|
||||
### `tests/mocks/fixtures/sample-claudemd.md`
|
||||
|
||||
```markdown
|
||||
# Project Instructions
|
||||
|
||||
This is a sample CLAUDE.md file for testing.
|
||||
```
|
||||
|
||||
### `tests/mocks/api-responses.ts`
|
||||
|
||||
```typescript
|
||||
export const mockStreamResponse = {
|
||||
type: "message_start" as const,
|
||||
message: {
|
||||
id: "msg_mock_001",
|
||||
type: "message" as const,
|
||||
role: "assistant",
|
||||
content: [],
|
||||
model: "claude-sonnet-4-20250514",
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
usage: { input_tokens: 100, output_tokens: 0 },
|
||||
},
|
||||
};
|
||||
|
||||
export const mockTextBlock = {
|
||||
type: "content_block_start" as const,
|
||||
index: 0,
|
||||
content_block: { type: "text" as const, text: "Mock response" },
|
||||
};
|
||||
|
||||
export const mockToolUseBlock = {
|
||||
type: "content_block_start" as const,
|
||||
index: 1,
|
||||
content_block: {
|
||||
type: "tool_use" as const,
|
||||
id: "toolu_mock_001",
|
||||
name: "Read",
|
||||
input: { file_path: "/tmp/test.txt" },
|
||||
},
|
||||
};
|
||||
|
||||
export const mockEndEvent = {
|
||||
type: "message_stop" as const,
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14.2 `tests/integration/tool-chain.test.ts`
|
||||
|
||||
**目标**:验证 Tool 注册 → 发现 → 权限检查链路。
|
||||
|
||||
### 前置条件
|
||||
|
||||
`src/tools.ts` 的 `getAllBaseTools` / `getTools` 导入链过重。策略:
|
||||
- 尝试直接 import 并 mock 最重依赖
|
||||
- 若不可行,改为测试 `src/Tool.ts` 的 `findToolByName` + 手动构造 tool 列表
|
||||
|
||||
### 用例
|
||||
|
||||
| # | 用例 | 验证点 |
|
||||
|---|------|--------|
|
||||
| 1 | `findToolByName("Bash")` 在已注册列表中查找 | 返回正确的 tool 定义 |
|
||||
| 2 | `findToolByName("NonExistent")` | 返回 `undefined` |
|
||||
| 3 | `findToolByName` 大小写不敏感 | `"bash"` 也能找到 |
|
||||
| 4 | `filterToolsByDenyRules` 拒绝特定工具 | 被拒绝工具不在结果中 |
|
||||
| 5 | `parseToolPreset("default")` 返回已知列表 | 包含核心 tools |
|
||||
| 6 | `buildTool` 构建的 tool 可被 `findToolByName` 发现 | 端到端验证 |
|
||||
|
||||
> 如果 `getAllBaseTools` 确实不可导入,改用 mock tool list 替代。
|
||||
|
||||
---
|
||||
|
||||
## 14.3 `tests/integration/context-build.test.ts`
|
||||
|
||||
**目标**:验证系统提示组装流程(CLAUDE.md 加载 + git status + 日期注入)。
|
||||
|
||||
### 前置条件
|
||||
|
||||
`src/context.ts` 依赖链极重。策略:
|
||||
- Mock `src/bootstrap/state.ts`(提供 cwd、projectRoot)
|
||||
- Mock `src/utils/git.ts`(提供 git status)
|
||||
- 使用真实 `src/utils/claudemd.ts` + 临时文件
|
||||
|
||||
### 用例
|
||||
|
||||
| # | 用例 | 验证点 |
|
||||
|---|------|--------|
|
||||
| 1 | 基本 context 构建 | 返回值包含系统提示字符串 |
|
||||
| 2 | CLAUDE.md 内容出现在 context 中 | `stripHtmlComments` 后的内容被包含 |
|
||||
| 3 | 多层目录 CLAUDE.md 合并 | 父目录 + 子目录 CLAUDE.md 都被加载 |
|
||||
| 4 | 无 CLAUDE.md 时不报错 | context 正常返回,无 crash |
|
||||
| 5 | git status 为 null | context 正常构建(测试环境中 git 不可用时) |
|
||||
|
||||
> **风险评估**:如果 mock `context.ts` 的依赖链成本过高,退化为测试 `buildEffectiveSystemPrompt`(已在 systemPrompt.test.ts 中完成),记录为已知限制。
|
||||
|
||||
---
|
||||
|
||||
## 14.4 `tests/integration/message-pipeline.test.ts`
|
||||
|
||||
**目标**:验证用户输入 → 消息格式化 → API 请求构建。
|
||||
|
||||
### 前置条件
|
||||
|
||||
`src/services/api/claude.ts` 构建最终 API 请求。策略:
|
||||
- Mock Anthropic SDK 的 streaming endpoint
|
||||
- 验证请求参数结构
|
||||
|
||||
### 用例
|
||||
|
||||
| # | 用例 | 验证点 |
|
||||
|---|------|--------|
|
||||
| 1 | 文本消息格式化 | `createUserMessage` 生成正确 role+content |
|
||||
| 2 | tool_result 消息格式化 | 包含 tool_use_id 和 content |
|
||||
| 3 | 多轮消息序列化 | messages 数组保持顺序 |
|
||||
| 4 | 系统提示注入到请求 | API 请求的 system 字段非空 |
|
||||
| 5 | 消息 normalize 后格式一致 | `normalizeMessages` 输出结构正确 |
|
||||
|
||||
> **现实评估**:消息格式化的大部分已在 `messages.test.ts` 覆盖。API 请求构建需要 mock SDK,复杂度高。如果投入产出比低,仅实现用例 1-3 和 5,用例 4 标记为 stretch goal。
|
||||
|
||||
---
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 创建 `tests/mocks/` 目录和基础文件
|
||||
2. 实现 `tool-chain.test.ts`(最低风险,最高价值)
|
||||
3. 评估 `context-build.test.ts` 可行性,决定是否实施
|
||||
4. 实现 `message-pipeline.test.ts`(可降级为单元测试)
|
||||
5. 更新 `testing-spec.md` 状态
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] `tests/mocks/` 基础设施可用
|
||||
- [ ] 至少 `tool-chain.test.ts` 实现并通过
|
||||
- [ ] 集成测试独立于单元测试运行:`bun test tests/integration/`
|
||||
- [ ] 所有集成测试使用 `createTempDir` + `cleanupTempDir`,不留文件系统残留
|
||||
- [ ] `bun test` 全部通过
|
||||
@@ -1,67 +0,0 @@
|
||||
# Plan 15 — CLI 参数测试 + 覆盖率基线
|
||||
|
||||
> 优先级:低 | 预估 ~15 个测试用例
|
||||
|
||||
---
|
||||
|
||||
## 15.1 `src/main.tsx` CLI 参数测试
|
||||
|
||||
**目标**:覆盖 Commander.js 配置的参数解析和模式切换。
|
||||
|
||||
### 前置条件
|
||||
|
||||
`src/main.tsx` 的 Commander 实例通常在模块顶层创建。测试策略:
|
||||
- 直接构造 Commander 实例或 mock `main.tsx` 的 program 导出
|
||||
- 使用 `parseArgs` 而非 `parse`(不触发 `process.exit`)
|
||||
|
||||
### 用例
|
||||
|
||||
| # | 用例 | 输入 | 期望 |
|
||||
|---|------|------|------|
|
||||
| 1 | 默认模式 | `[]` | 模式为 REPL |
|
||||
| 2 | pipe 模式 | `["-p"]` | 模式为 pipe |
|
||||
| 3 | pipe 带输入 | `["-p", "say hello"]` | 输入为 `"say hello"` |
|
||||
| 4 | print 模式 | `["--print", "hello"]` | 等效于 pipe |
|
||||
| 5 | verbose | `["-v"]` | verbose 标志为 true |
|
||||
| 6 | model 选择 | `["--model", "claude-opus-4-6"]` | model 值正确传递 |
|
||||
| 7 | system prompt | `["--system-prompt", "custom"]` | system prompt 被设置 |
|
||||
| 8 | help | `["--help"]` | 显示帮助信息,不报错 |
|
||||
| 9 | version | `["--version"]` | 显示版本号 |
|
||||
| 10 | unknown flag | `["--nonexistent"]` | 不报错(Commander 允许未知参数时) |
|
||||
|
||||
> **风险**:`main.tsx` 可能执行初始化逻辑(auth、analytics),需要在 mock 环境中运行。如果复杂度过高,降级为只测试参数解析部分。
|
||||
|
||||
---
|
||||
|
||||
## 15.2 覆盖率基线
|
||||
|
||||
### 运行命令
|
||||
|
||||
```bash
|
||||
bun test --coverage 2>&1 | tail -50
|
||||
```
|
||||
|
||||
### 记录内容
|
||||
|
||||
| 模块 | 当前覆盖率 | 目标 |
|
||||
|------|-----------|------|
|
||||
| `src/utils/` | 待测量 | >= 80% |
|
||||
| `src/utils/permissions/` | 待测量 | >= 60% |
|
||||
| `src/utils/model/` | 待测量 | >= 60% |
|
||||
| `src/Tool.ts` + `src/tools.ts` | 待测量 | >= 80% |
|
||||
| `src/utils/claudemd.ts` | 待测量 | >= 40%(核心逻辑难测) |
|
||||
| 整体 | 待测量 | 不设强制指标 |
|
||||
|
||||
### 后续行动
|
||||
|
||||
- 将基线数据填入 `testing-spec.md` §4
|
||||
- 识别覆盖率最低的 10 个文件,排入后续测试计划
|
||||
- 如 `bun test --coverage` 输出不可用(Bun 版本限制),改用手动计算已测/总导出函数比
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] CLI 参数至少覆盖 5 个核心 flag
|
||||
- [ ] 覆盖率基线数据记录到 testing-spec.md
|
||||
- [ ] `bun test` 全部通过
|
||||
@@ -1,188 +0,0 @@
|
||||
# Phase 16 — 零依赖纯函数测试
|
||||
|
||||
> 创建日期:2026-04-02
|
||||
> 预计:+120 tests / 8 files
|
||||
> 目标:覆盖所有零外部依赖的纯函数/类模块
|
||||
|
||||
所有模块均为纯函数或零外部依赖类,mock 成本为零,ROI 最高。
|
||||
|
||||
---
|
||||
|
||||
## 16.1 `src/utils/__tests__/stream.test.ts`(~15 tests)
|
||||
|
||||
**目标模块**: `src/utils/stream.ts`(76 行)
|
||||
**导出**: `Stream<T>` class — 手动异步队列,实现 `AsyncIterator<T>`
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| enqueue then read | 单条消息正确传递 |
|
||||
| enqueue multiple then drain | 多条消息顺序消费 |
|
||||
| done resolves pending readers | `done()` 后迭代结束 |
|
||||
| done with no pending readers | 无等待时安全关闭 |
|
||||
| error rejects pending readers | `error(e)` 传播异常 |
|
||||
| error after done | 后续操作安全处理 |
|
||||
| single-iteration guard | `return()` 后不可再迭代 |
|
||||
| empty stream done immediately | 无数据时 done 返回 `{ done: true }` |
|
||||
| concurrent enqueue | 多次 enqueue 不丢失 |
|
||||
| backpressure | reader 慢于 writer 时不丢数据 |
|
||||
|
||||
---
|
||||
|
||||
## 16.2 `src/utils/__tests__/abortController.test.ts`(~12 tests)
|
||||
|
||||
**目标模块**: `src/utils/abortController.ts`(99 行)
|
||||
**导出**: `createAbortController()`, `createChildAbortController()`
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| parent abort propagates to child | `parent.abort()` → child aborted |
|
||||
| child abort does NOT propagate to parent | `child.abort()` → parent still active |
|
||||
| already-aborted parent → child immediately aborted | 创建时即继承 abort 状态 |
|
||||
| child listener cleanup after parent abort | WeakRef 回收后无泄漏 |
|
||||
| multiple children of same parent | 独立 abort 传播 |
|
||||
| child abort then parent abort | 顺序无关 |
|
||||
| signal.maxListeners raised | MaxListenersExceededWarning 不触发 |
|
||||
|
||||
---
|
||||
|
||||
## 16.3 `src/utils/__tests__/bufferedWriter.test.ts`(~14 tests)
|
||||
|
||||
**目标模块**: `src/utils/bufferedWriter.ts`(100 行)
|
||||
**导出**: `createBufferedWriter()`
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| single write buffered | write → buffer 累积 |
|
||||
| flush on size threshold | 超过 maxSize 时自动 flush |
|
||||
| flush on timer | 定时器触发 flush |
|
||||
| immediate mode | `{ immediate: true }` 跳过缓冲 |
|
||||
| overflow coalescing | overflow 内容合并到下次 flush |
|
||||
| empty buffer flush | 无数据时 flush 无副作用 |
|
||||
| close flushes remaining | close 触发最终 flush |
|
||||
| multiple writes before flush | 批量写入合并 |
|
||||
| flush callback receives concatenated data | writeFn 参数正确 |
|
||||
|
||||
**Mock**: 注入 `writeFn` 回调,可选 fake timers
|
||||
|
||||
---
|
||||
|
||||
## 16.4 `src/utils/__tests__/gitDiff.test.ts`(~20 tests)
|
||||
|
||||
**目标模块**: `src/utils/gitDiff.ts`(532 行)
|
||||
**可测函数**: `parseGitNumstat()`, `parseGitDiff()`, `parseShortstat()`
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| parseGitNumstat — single file | `1\t2\tpath` → { added: 1, deleted: 2, file: "path" } |
|
||||
| parseGitNumstat — binary file | `-\t-\timage.png` → binary flag |
|
||||
| parseGitNumstat — rename | `{ old => new }` 格式解析 |
|
||||
| parseGitNumstat — empty diff | 空字符串 → [] |
|
||||
| parseGitNumstat — multiple files | 多行正确分割 |
|
||||
| parseGitDiff — added lines | `+` 开头行计数 |
|
||||
| parseGitDiff — deleted lines | `-` 开头行计数 |
|
||||
| parseGitDiff — hunk header | `@@ -a,b +c,d @@` 解析 |
|
||||
| parseGitDiff — new file mode | `new file mode 100644` 检测 |
|
||||
| parseGitDiff — deleted file mode | `deleted file mode` 检测 |
|
||||
| parseGitDiff — binary diff | Binary files differ 处理 |
|
||||
| parseShortstat — all components | `1 file changed, 5 insertions(+), 3 deletions(-)` |
|
||||
| parseShortstat — insertions only | 无 deletions |
|
||||
| parseShortstat — deletions only | 无 insertions |
|
||||
| parseShortstat — files only | 仅 file changed |
|
||||
| parseShortstat — empty | 空字符串 → 默认值 |
|
||||
| parseShortstat — rename | `1 file changed, ...` 重命名 |
|
||||
|
||||
**Mock**: 无需 mock — 全部是纯字符串解析
|
||||
|
||||
---
|
||||
|
||||
## 16.5 `src/__tests__/history.test.ts`(~18 tests)
|
||||
|
||||
**目标模块**: `src/history.ts`(464 行)
|
||||
**可测函数**: `parseReferences()`, `expandPastedTextRefs()`, `formatPastedTextRef()`, `formatImageRef()`, `getPastedTextRefNumLines()`
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| parseReferences — text ref | `#1` → [{ type: "text", ref: 1 }] |
|
||||
| parseReferences — image ref | `@1` → [{ type: "image", ref: 1 }] |
|
||||
| parseReferences — multiple refs | `#1 #2 @3` → 3 refs |
|
||||
| parseReferences — no refs | `"hello"` → [] |
|
||||
| parseReferences — duplicate refs | `#1 #1` → 去重或保留 |
|
||||
| parseReferences — zero ref | `#0` → 边界 |
|
||||
| parseReferences — large ref | `#999` → 正常 |
|
||||
| formatPastedTextRef — basic | 输出格式验证 |
|
||||
| formatPastedTextRef — multiline | 多行内容格式 |
|
||||
| getPastedTextRefNumLines — 1 line | 返回 1 |
|
||||
| getPastedTextRefNumLines — multiple lines | 换行计数 |
|
||||
| expandPastedTextRefs — single ref | 替换单个引用 |
|
||||
| expandPastedTextRefs — multiple refs | 替换多个引用 |
|
||||
| expandPastedTextRefs — no refs | 原样返回 |
|
||||
| expandPastedTextRefs — mixed content | 文本 + 引用混合 |
|
||||
| formatImageRef — basic | 输出格式 |
|
||||
|
||||
**Mock**: `mock.module("src/bootstrap/state.ts", ...)` 解锁模块
|
||||
|
||||
---
|
||||
|
||||
## 16.6 `src/utils/__tests__/sliceAnsi.test.ts`(~16 tests)
|
||||
|
||||
**目标模块**: `src/utils/sliceAnsi.ts`(91 行)
|
||||
**导出**: `sliceAnsi()` — ANSI 感知的字符串切片
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| plain text slice | `"hello".slice(1,3)` 等价 |
|
||||
| preserve ANSI codes | `\x1b[31mhello\x1b[0m` 切片后保留颜色 |
|
||||
| close opened styles | 切片点在 ANSI 样式中间时正确关闭 |
|
||||
| hyperlink handling | OSC 8 超链接不被切断 |
|
||||
| combining marks (diacritics) | `é` = `e\u0301` 不被切开 |
|
||||
| Devanagari matras | 零宽字符不被切断 |
|
||||
| full-width characters | CJK 字符宽度 = 2 |
|
||||
| empty slice | 返回空字符串 |
|
||||
| full slice | 返回完整字符串 |
|
||||
| boundary at ANSI code | 边界恰好在 escape 序列上 |
|
||||
| nested ANSI styles | 多层嵌套时正确处理 |
|
||||
| slice start > end | 空结果 |
|
||||
|
||||
**Mock**: `mock.module("@alcalzone/ansi-tokenize", ...)`, `mock.module("ink/stringWidth", ...)`
|
||||
|
||||
---
|
||||
|
||||
## 16.7 `src/utils/__tests__/treeify.test.ts`(~15 tests)
|
||||
|
||||
**目标模块**: `src/utils/treeify.ts`(170 行)
|
||||
**导出**: `treeify()` — 递归树渲染
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| simple flat tree | `{ a: {}, b: {} }` → 2 行 |
|
||||
| nested tree | `{ a: { b: { c: {} } } }` → 3 行缩进 |
|
||||
| array values | `[1, 2, 3]` 渲染为列表 |
|
||||
| circular reference | 不无限递归 |
|
||||
| empty object | `{}` 处理 |
|
||||
| single key | 布局适配 |
|
||||
| branch vs last-branch character | ├─ vs └─ |
|
||||
| custom prefix | options 前缀传递 |
|
||||
| deep nesting | 5+ 层缩进正确 |
|
||||
| mixed object/array | 混合结构 |
|
||||
|
||||
**Mock**: `mock.module("figures", ...)`, color 模块 mock
|
||||
|
||||
---
|
||||
|
||||
## 16.8 `src/utils/__tests__/words.test.ts`(~10 tests)
|
||||
|
||||
**目标模块**: `src/utils/words.ts`(800 行,大部分是词表数据)
|
||||
**导出**: `generateWordSlug()`, `generateShortWordSlug()`
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| generateWordSlug format | `adjective-verb-noun` 三段式 |
|
||||
| generateShortWordSlug format | `adjective-noun` 两段式 |
|
||||
| all parts non-empty | 无空段 |
|
||||
| hyphen separator | `-` 分隔 |
|
||||
| all parts from word lists | 成分来自预定义词表 |
|
||||
| multiple calls uniqueness | 连续调用不总是相同 |
|
||||
| no consecutive hyphens | 无 `--` |
|
||||
| lowercase only | 全小写 |
|
||||
|
||||
**Mock**: `mock.module("crypto", ...)` 控制 `randomBytes` 实现确定性测试
|
||||
@@ -1,203 +0,0 @@
|
||||
# Phase 17 — Tool 子模块纯逻辑测试
|
||||
|
||||
> 创建日期:2026-04-02
|
||||
> 预计:+150 tests / 11 files
|
||||
> 目标:覆盖 Tool 目录下有丰富纯逻辑但零测试的子模块
|
||||
|
||||
---
|
||||
|
||||
## 17.1 `src/tools/PowerShellTool/__tests__/powershellSecurity.test.ts`(~25 tests)
|
||||
|
||||
**目标模块**: `src/tools/PowerShellTool/powershellSecurity.ts`(1091 行)
|
||||
|
||||
**安全关键** — 检测 ~20 种攻击向量。
|
||||
|
||||
| 测试分组 | 测试数 | 验证点 |
|
||||
|---------|-------|--------|
|
||||
| Invoke-Expression 检测 | 3 | `IEX`, `Invoke-Expression`, 变形 |
|
||||
| Download cradle 检测 | 3 | `Net.WebClient`, `Invoke-WebRequest`, pipe |
|
||||
| Privilege escalation | 3 | `Start-Process -Verb RunAs`, `runas.exe` |
|
||||
| COM object | 2 | `New-Object -ComObject`, WScript.Shell |
|
||||
| Scheduled tasks | 2 | `schtasks`, `Register-ScheduledTask` |
|
||||
| WMI | 2 | `Invoke-WmiMethod`, `Get-WmiObject` |
|
||||
| Module loading | 2 | `Import-Module` 从网络路径 |
|
||||
| 安全命令通过 | 3 | `Get-Process`, `Get-ChildItem`, `Write-Host` |
|
||||
| 混淆绕过尝试 | 3 | base64, 字符串拼接, 空格变形 |
|
||||
| 组合命令 | 2 | `;` 分隔的多命令 |
|
||||
|
||||
**Mock**: 构造 `ParsedPowerShellCommand` 对象(不需要真实 AST)
|
||||
|
||||
---
|
||||
|
||||
## 17.2 `src/tools/PowerShellTool/__tests__/commandSemantics.test.ts`(~10 tests)
|
||||
|
||||
**目标模块**: `src/tools/PowerShellTool/commandSemantics.ts`(143 行)
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| grep exit 0/1/2 | 语义映射 |
|
||||
| robocopy exit codes | Windows 特殊退出码 |
|
||||
| findstr exit codes | Windows find 工具 |
|
||||
| unknown command | 默认语义 |
|
||||
| extractBaseCommand — basic | `grep "pattern" file` → `grep` |
|
||||
| extractBaseCommand — path | `C:\tools\rg.exe` → `rg` |
|
||||
| heuristicallyExtractBaseCommand | 模糊匹配 |
|
||||
|
||||
---
|
||||
|
||||
## 17.3 `src/tools/PowerShellTool/__tests__/destructiveCommandWarning.test.ts`(~15 tests)
|
||||
|
||||
**目标模块**: `src/tools/PowerShellTool/destructiveCommandWarning.ts`(110 行)
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| Remove-Item -Recurse -Force | 危险 |
|
||||
| Format-Volume | 危险 |
|
||||
| git reset --hard | 危险 |
|
||||
| DROP TABLE | 危险 |
|
||||
| Remove-Item (no -Force) | 安全 |
|
||||
| Get-ChildItem | 安全 |
|
||||
| 管道组合 | `rm -rf` + pipe |
|
||||
| 大小写混合 | `ReMoVe-ItEm` |
|
||||
|
||||
---
|
||||
|
||||
## 17.4 `src/tools/PowerShellTool/__tests__/gitSafety.test.ts`(~12 tests)
|
||||
|
||||
**目标模块**: `src/tools/PowerShellTool/gitSafety.ts`(177 行)
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| normalizeGitPathArg — forward slash | 规范化 |
|
||||
| normalizeGitPathArg — backslash | Windows 路径规范化 |
|
||||
| normalizeGitPathArg — NTFS short name | `GITFI~1` → `.git` |
|
||||
| isGitInternalPathPS — .git/config | true |
|
||||
| isGitInternalPathPS — normal file | false |
|
||||
| isDotGitPathPS — hidden git dir | true |
|
||||
| isDotGitPathPS — .gitignore | false |
|
||||
| bare repo attack | `.git` 路径遍历 |
|
||||
|
||||
---
|
||||
|
||||
## 17.5 `src/tools/LSPTool/__tests__/formatters.test.ts`(~20 tests)
|
||||
|
||||
**目标模块**: `src/tools/LSPTool/formatters.ts`(593 行)
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| formatGoToDefinitionResult — single | 单个定义 |
|
||||
| formatGoToDefinitionResult — multiple | 多个定义(分组) |
|
||||
| formatFindReferencesResult | 引用列表 |
|
||||
| formatHoverResult — markdown | markdown 内容 |
|
||||
| formatHoverResult — plaintext | 纯文本 |
|
||||
| formatDocumentSymbolResult — classes | 类符号 |
|
||||
| formatDocumentSymbolResult — functions | 函数符号 |
|
||||
| formatDocumentSymbolResult — nested | 嵌套符号 |
|
||||
| formatWorkspaceSymbolResult | 工作区符号 |
|
||||
| formatPrepareCallHierarchyResult | 调用层次 |
|
||||
| formatIncomingCallsResult | 入调用 |
|
||||
| formatOutgoingCallsResult | 出调用 |
|
||||
| empty results | 各函数空结果 |
|
||||
| groupByFile helper | 文件分组逻辑 |
|
||||
|
||||
---
|
||||
|
||||
## 17.6 `src/tools/GrepTool/__tests__/utils.test.ts`(~10 tests)
|
||||
|
||||
**目标模块**: `src/tools/GrepTool/GrepTool.ts`(577 行)
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| applyHeadLimit — within limit | 不截断 |
|
||||
| applyHeadLimit — exceeds limit | 正确截断 |
|
||||
| applyHeadLimit — offset + limit | 分页逻辑 |
|
||||
| applyHeadLimit — zero limit | 边界 |
|
||||
| formatLimitInfo — basic | 格式化输出 |
|
||||
|
||||
**Mock**: `mock.module("src/utils/log.ts", ...)` 解锁导入
|
||||
|
||||
---
|
||||
|
||||
## 17.7 `src/tools/WebFetchTool/__tests__/utils.test.ts`(~15 tests)
|
||||
|
||||
**目标模块**: `src/tools/WebFetchTool/utils.ts`(531 行)
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| validateURL — valid http | 通过 |
|
||||
| validateURL — valid https | 通过 |
|
||||
| validateURL — ftp | 拒绝 |
|
||||
| validateURL — no protocol | 拒绝 |
|
||||
| validateURL — localhost | 处理 |
|
||||
| isPermittedRedirect — same host | 允许 |
|
||||
| isPermittedRedirect — different host | 拒绝 |
|
||||
| isPermittedRedirect — subdomain | 处理 |
|
||||
| isRedirectInfo — valid object | true |
|
||||
| isRedirectInfo — invalid | false |
|
||||
|
||||
---
|
||||
|
||||
## 17.8 `src/tools/WebFetchTool/__tests__/preapproved.test.ts`(~10 tests)
|
||||
|
||||
**目标模块**: `src/tools/WebFetchTool/preapproved.ts`(167 行)
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| exact hostname match | 通过 |
|
||||
| subdomain match | 处理 |
|
||||
| path prefix match | `/docs/api` 匹配 |
|
||||
| path non-match | `/internal` 不匹配 |
|
||||
| unknown hostname | false |
|
||||
| empty pathname | 边界 |
|
||||
|
||||
---
|
||||
|
||||
## 17.9 `src/tools/FileReadTool/__tests__/utils.test.ts`(~15 tests)
|
||||
|
||||
**目标模块**: `src/tools/FileReadTool/FileReadTool.ts`(1184 行)
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| isBlockedDevicePath — /dev/sda | true |
|
||||
| isBlockedDevicePath — /dev/null | 处理 |
|
||||
| isBlockedDevicePath — normal file | false |
|
||||
| detectSessionFileType — .jsonl | 会话文件类型 |
|
||||
| detectSessionFileType — unknown | 未知类型 |
|
||||
| formatFileLines — basic | 行号格式 |
|
||||
| formatFileLines — empty | 空文件 |
|
||||
|
||||
---
|
||||
|
||||
## 17.10 `src/tools/AgentTool/__tests__/agentToolUtils.test.ts`(~18 tests)
|
||||
|
||||
**目标模块**: `src/tools/AgentTool/agentToolUtils.ts`(688 行)
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| filterToolsForAgent — builtin only | 只返回内置工具 |
|
||||
| filterToolsForAgent — exclude async | 排除异步工具 |
|
||||
| filterToolsForAgent — permission mode | 权限过滤 |
|
||||
| resolveAgentTools — wildcard | 通配符展开 |
|
||||
| resolveAgentTools — explicit list | 显式列表 |
|
||||
| countToolUses — multiple | 消息中工具调用计数 |
|
||||
| countToolUses — zero | 无工具调用 |
|
||||
| extractPartialResult — text only | 提取文本 |
|
||||
| extractPartialResult — mixed | 混合内容 |
|
||||
| getLastToolUseName — basic | 最后工具名 |
|
||||
| getLastToolUseName — no tool use | 无工具调用 |
|
||||
|
||||
**Mock**: `mock.module("src/bootstrap/state.ts", ...)`, `mock.module("src/utils/log.ts", ...)`
|
||||
|
||||
---
|
||||
|
||||
## 17.11 `src/tools/LSPTool/__tests__/schemas.test.ts`(~5 tests)
|
||||
|
||||
**目标模块**: `src/tools/LSPTool/schemas.ts`(216 行)
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| isValidLSPOperation — goToDefinition | true |
|
||||
| isValidLSPOperation — findReferences | true |
|
||||
| isValidLSPOperation — hover | true |
|
||||
| isValidLSPOperation — invalid | false |
|
||||
| isValidLSPOperation — empty string | false |
|
||||
@@ -1,110 +0,0 @@
|
||||
# Phase 18 — WEAK 修复 + ACCEPTABLE 加固
|
||||
|
||||
> 创建日期:2026-04-02
|
||||
> 预计:+30 tests / 4 files (修改现有)
|
||||
> 目标:修复所有 WEAK 评分测试文件,消除系统性问题
|
||||
|
||||
---
|
||||
|
||||
## 18.1 `src/utils/__tests__/format.test.ts` — 断言精确化(+5 tests)
|
||||
|
||||
**问题**: `formatNumber`/`formatTokens`/`formatRelativeTime` 使用 `toContain`
|
||||
**修复**: 改为 `toBe` 精确匹配
|
||||
|
||||
```diff
|
||||
- expect(formatNumber(1500000)).toContain("1.5")
|
||||
+ expect(formatNumber(1500000)).toBe("1.5m")
|
||||
```
|
||||
|
||||
新增测试:
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| formatNumber — 0 | `"0"` |
|
||||
| formatNumber — billions | `"1.5b"` |
|
||||
| formatTokens — thousands | 精确匹配 |
|
||||
| formatRelativeTime — hours ago | 精确匹配 |
|
||||
| formatRelativeTime — days ago | 精确匹配 |
|
||||
|
||||
---
|
||||
|
||||
## 18.2 `src/utils/__tests__/envValidation.test.ts` — Bug 确认(+3 tests)
|
||||
|
||||
**问题**: `value=1, lowerBound=100` 返回 `status: "valid"` — 函数名暗示有下界检查
|
||||
**计划**: 先读取源码确认 `defaultValue` 和 `lowerBound` 的语义关系,然后:
|
||||
- 如果是源码 bug → 在测试中注释标记,不修改源码
|
||||
- 如果是设计意图 → 更新测试描述明确语义
|
||||
|
||||
新增测试:
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| parseFloat truncation | `"50.9"` → 50 |
|
||||
| whitespace handling | `" 500 "` → 500 |
|
||||
| very large number | overflow 处理 |
|
||||
|
||||
---
|
||||
|
||||
## 18.3 `src/utils/permissions/__tests__/PermissionMode.test.ts` — false 路径(+8 tests)
|
||||
|
||||
**问题**: `isExternalPermissionMode` false 路径从未执行
|
||||
**修复**: 覆盖所有 5 种 mode 的 true/false 期望
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| isExternalPermissionMode — plan | false |
|
||||
| isExternalPermissionMode — auto | false |
|
||||
| isExternalPermissionMode — default | false |
|
||||
| permissionModeFromString — all modes | 5 种 mode 全覆盖 |
|
||||
| permissionModeFromString — invalid | 默认值 |
|
||||
| permissionModeFromString — case insensitive | 大小写 |
|
||||
| isPermissionMode — valid strings | true |
|
||||
| isPermissionMode — invalid strings | false |
|
||||
|
||||
---
|
||||
|
||||
## 18.4 `src/tools/shared/__tests__/gitOperationTracking.test.ts` — mock analytics(+4 tests)
|
||||
|
||||
**问题**: 未 mock analytics 依赖,测试产生副作用
|
||||
**修复**: 添加 `mock.module("src/services/analytics/...", ...)`
|
||||
|
||||
新增测试:
|
||||
|
||||
| 测试用例 | 验证点 |
|
||||
|---------|--------|
|
||||
| parseGitCommitId — all GH PR actions | 补齐 6 个 action |
|
||||
| detectGitOperation — no analytics call | mock 验证 |
|
||||
| detectGitCommitId — various formats | SHA/短 SHA/HEAD |
|
||||
| git operation tracking — edge cases | 空输入、畸形输入 |
|
||||
|
||||
---
|
||||
|
||||
## 排除清单
|
||||
|
||||
以下模块 **不纳入测试**,原因合理:
|
||||
|
||||
| 模块 | 行数 | 排除原因 |
|
||||
|------|------|---------|
|
||||
| `query.ts` | 1732 | 核心循环,40+ 依赖,需完整集成环境 |
|
||||
| `QueryEngine.ts` | 1320 | 编排器,30+ 依赖 |
|
||||
| `utils/hooks.ts` | 5121 | 51 exports,spawn 子进程 |
|
||||
| `utils/config.ts` | 1817 | 文件系统 + lockfile + 全局状态 |
|
||||
| `utils/auth.ts` | 2002 | 多 provider 认证,平台特定 |
|
||||
| `utils/fileHistory.ts` | 1115 | 重 I/O 文件备份 |
|
||||
| `utils/sessionRestore.ts` | 551 | 恢复状态涉及多个子系统 |
|
||||
| `utils/ripgrep.ts` | 679 | spawn 子进程 |
|
||||
| `utils/yaml.ts` | 15 | 两行 wrapper |
|
||||
| `utils/lockfile.ts` | 43 | trivial wrapper |
|
||||
| `screens/` / `components/` | — | Ink 渲染测试环境 |
|
||||
| `bridge/` / `remote/` / `ssh/` | — | 网络层 |
|
||||
| `daemon/` / `server/` | — | 进程管理 |
|
||||
|
||||
---
|
||||
|
||||
## 预期成果
|
||||
|
||||
| 指标 | Phase 16 后 | Phase 17 后 | Phase 18 后 |
|
||||
|------|-----------|-----------|-----------|
|
||||
| 测试数 | ~1417 | ~1567 | ~1597 |
|
||||
| 文件数 | 76 | 87 | 91 |
|
||||
| WEAK 文件 | 6 | 4 | **0** |
|
||||
@@ -1,435 +0,0 @@
|
||||
# Phase 19 - Batch 1: 零依赖微型 utils
|
||||
|
||||
> 预计 ~154 tests / 13 文件 | 全部纯函数,无需 mock
|
||||
|
||||
---
|
||||
|
||||
## 1. `src/utils/__tests__/semanticBoolean.test.ts` (~8 tests)
|
||||
|
||||
**源文件**: `src/utils/semanticBoolean.ts` (30 行)
|
||||
**依赖**: `zod/v4`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("semanticBoolean", () => {
|
||||
// 基本 Zod 行为
|
||||
test("parses boolean true to true")
|
||||
test("parses boolean false to false")
|
||||
test("parses string 'true' to true")
|
||||
test("parses string 'false' to false")
|
||||
// 边界
|
||||
test("rejects string 'TRUE' (case-sensitive)")
|
||||
test("rejects string 'FALSE' (case-sensitive)")
|
||||
test("rejects number 1")
|
||||
test("rejects null")
|
||||
test("rejects undefined")
|
||||
// 自定义 inner schema
|
||||
test("works with custom inner schema (z.boolean().optional())")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无
|
||||
|
||||
---
|
||||
|
||||
## 2. `src/utils/__tests__/semanticNumber.test.ts` (~10 tests)
|
||||
|
||||
**源文件**: `src/utils/semanticNumber.ts` (37 行)
|
||||
**依赖**: `zod/v4`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("semanticNumber", () => {
|
||||
test("parses number 42")
|
||||
test("parses number 0")
|
||||
test("parses negative number -5")
|
||||
test("parses float 3.14")
|
||||
test("parses string '42' to 42")
|
||||
test("parses string '-7.5' to -7.5")
|
||||
test("rejects string 'abc'")
|
||||
test("rejects empty string ''")
|
||||
test("rejects null")
|
||||
test("rejects boolean true")
|
||||
test("works with custom inner schema (z.number().int().min(0))")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无
|
||||
|
||||
---
|
||||
|
||||
## 3. `src/utils/__tests__/lazySchema.test.ts` (~6 tests)
|
||||
|
||||
**源文件**: `src/utils/lazySchema.ts` (9 行)
|
||||
**依赖**: 无
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("lazySchema", () => {
|
||||
test("returns a function")
|
||||
test("calls factory on first invocation")
|
||||
test("returns cached result on subsequent invocations")
|
||||
test("factory is called only once (call count verification)")
|
||||
test("works with different return types")
|
||||
test("each call to lazySchema returns independent cache")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无
|
||||
|
||||
---
|
||||
|
||||
## 4. `src/utils/__tests__/withResolvers.test.ts` (~8 tests)
|
||||
|
||||
**源文件**: `src/utils/withResolvers.ts` (14 行)
|
||||
**依赖**: 无
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("withResolvers", () => {
|
||||
test("returns object with promise, resolve, reject")
|
||||
test("promise resolves when resolve is called")
|
||||
test("promise rejects when reject is called")
|
||||
test("resolve passes value through")
|
||||
test("reject passes error through")
|
||||
test("promise is instanceof Promise")
|
||||
test("works with generic type parameter")
|
||||
test("resolve/reject can be called asynchronously")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无
|
||||
|
||||
---
|
||||
|
||||
## 5. `src/utils/__tests__/userPromptKeywords.test.ts` (~12 tests)
|
||||
|
||||
**源文件**: `src/utils/userPromptKeywords.ts` (28 行)
|
||||
**依赖**: 无
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("matchesNegativeKeyword", () => {
|
||||
test("matches 'wtf'")
|
||||
test("matches 'shit'")
|
||||
test("matches 'fucking broken'")
|
||||
test("does not match normal input like 'fix the bug'")
|
||||
test("is case-insensitive")
|
||||
test("matches partial word in sentence")
|
||||
})
|
||||
|
||||
describe("matchesKeepGoingKeyword", () => {
|
||||
test("matches exact 'continue'")
|
||||
test("matches 'keep going'")
|
||||
test("matches 'go on'")
|
||||
test("does not match 'cont'")
|
||||
test("does not match empty string")
|
||||
test("matches within larger sentence 'please continue'")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无
|
||||
|
||||
---
|
||||
|
||||
## 6. `src/utils/__tests__/xdg.test.ts` (~15 tests)
|
||||
|
||||
**源文件**: `src/utils/xdg.ts` (66 行)
|
||||
**依赖**: 无(通过 options 参数注入)
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("getXDGStateHome", () => {
|
||||
test("returns ~/.local/state by default")
|
||||
test("respects XDG_STATE_HOME env var")
|
||||
test("uses custom homedir from options")
|
||||
})
|
||||
|
||||
describe("getXDGCacheHome", () => {
|
||||
test("returns ~/.cache by default")
|
||||
test("respects XDG_CACHE_HOME env var")
|
||||
})
|
||||
|
||||
describe("getXDGDataHome", () => {
|
||||
test("returns ~/.local/share by default")
|
||||
test("respects XDG_DATA_HOME env var")
|
||||
})
|
||||
|
||||
describe("getUserBinDir", () => {
|
||||
test("returns ~/.local/bin")
|
||||
test("uses custom homedir from options")
|
||||
})
|
||||
|
||||
describe("resolveOptions", () => {
|
||||
test("defaults env to process.env")
|
||||
test("defaults homedir to os.homedir()")
|
||||
test("merges partial options")
|
||||
})
|
||||
|
||||
describe("path construction", () => {
|
||||
test("all paths end with correct subdirectory")
|
||||
test("respects HOME env via homedir override")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无(通过 options.env 和 options.homedir 注入)
|
||||
|
||||
---
|
||||
|
||||
## 7. `src/utils/__tests__/horizontalScroll.test.ts` (~20 tests)
|
||||
|
||||
**源文件**: `src/utils/horizontalScroll.ts` (138 行)
|
||||
**依赖**: 无
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("calculateHorizontalScrollWindow", () => {
|
||||
// 基本场景
|
||||
test("all items fit within available width")
|
||||
test("single item selected within view")
|
||||
test("selected item at beginning")
|
||||
test("selected item at end")
|
||||
test("selected item beyond visible range scrolls right")
|
||||
test("selected item before visible range scrolls left")
|
||||
|
||||
// 箭头指示器
|
||||
test("showLeftArrow when items hidden on left")
|
||||
test("showRightArrow when items hidden on right")
|
||||
test("no arrows when all items visible")
|
||||
test("both arrows when items hidden on both sides")
|
||||
|
||||
// 边界条件
|
||||
test("empty itemWidths array")
|
||||
test("single item")
|
||||
test("available width is 0")
|
||||
test("item wider than available width")
|
||||
test("all items same width")
|
||||
test("varying item widths")
|
||||
test("firstItemHasSeparator adds separator width to first item")
|
||||
test("selectedIdx in middle of overflow")
|
||||
test("scroll snaps to show selected at left edge")
|
||||
test("scroll snaps to show selected at right edge")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无
|
||||
|
||||
---
|
||||
|
||||
## 8. `src/utils/__tests__/generators.test.ts` (~18 tests)
|
||||
|
||||
**源文件**: `src/utils/generators.ts` (89 行)
|
||||
**依赖**: 无
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("lastX", () => {
|
||||
test("returns last yielded value")
|
||||
test("returns only value from single-yield generator")
|
||||
test("throws on empty generator")
|
||||
})
|
||||
|
||||
describe("returnValue", () => {
|
||||
test("returns generator return value")
|
||||
test("returns undefined for void return")
|
||||
})
|
||||
|
||||
describe("toArray", () => {
|
||||
test("collects all yielded values")
|
||||
test("returns empty array for empty generator")
|
||||
test("preserves order")
|
||||
})
|
||||
|
||||
describe("fromArray", () => {
|
||||
test("yields all array elements")
|
||||
test("yields nothing for empty array")
|
||||
})
|
||||
|
||||
describe("all", () => {
|
||||
test("merges multiple generators preserving yield order")
|
||||
test("respects concurrency cap")
|
||||
test("handles empty generator array")
|
||||
test("handles single generator")
|
||||
test("handles generators of different lengths")
|
||||
test("yields all values from all generators")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无(用 fromArray 构造测试数据)
|
||||
|
||||
---
|
||||
|
||||
## 9. `src/utils/__tests__/sequential.test.ts` (~12 tests)
|
||||
|
||||
**源文件**: `src/utils/sequential.ts` (57 行)
|
||||
**依赖**: 无
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("sequential", () => {
|
||||
test("wraps async function, returns same result")
|
||||
test("single call resolves normally")
|
||||
test("concurrent calls execute sequentially (FIFO order)")
|
||||
test("preserves arguments correctly")
|
||||
test("error in first call does not block subsequent calls")
|
||||
test("preserves rejection reason")
|
||||
test("multiple args passed correctly")
|
||||
test("returns different wrapper for each call to sequential")
|
||||
test("handles rapid concurrent calls")
|
||||
test("execution order matches call order")
|
||||
test("works with functions returning different types")
|
||||
test("wrapper has same arity expectations")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无
|
||||
|
||||
---
|
||||
|
||||
## 10. `src/utils/__tests__/fingerprint.test.ts` (~15 tests)
|
||||
|
||||
**源文件**: `src/utils/fingerprint.ts` (77 行)
|
||||
**依赖**: `crypto` (内置)
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("FINGERPRINT_SALT", () => {
|
||||
test("has expected value '59cf53e54c78'")
|
||||
})
|
||||
|
||||
describe("extractFirstMessageText", () => {
|
||||
test("extracts text from first user message")
|
||||
test("extracts text from single user message with array content")
|
||||
test("returns empty string when no user messages")
|
||||
test("skips assistant messages")
|
||||
test("handles mixed content blocks (text + image)")
|
||||
})
|
||||
|
||||
describe("computeFingerprint", () => {
|
||||
test("returns deterministic 3-char hex string")
|
||||
test("same input produces same fingerprint")
|
||||
test("different message text produces different fingerprint")
|
||||
test("different version produces different fingerprint")
|
||||
test("handles short strings (length < 21)")
|
||||
test("handles empty string")
|
||||
test("fingerprint is valid hex")
|
||||
})
|
||||
|
||||
describe("computeFingerprintFromMessages", () => {
|
||||
test("end-to-end: messages -> fingerprint")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需要 `mock.module` 处理 `UserMessage`/`AssistantMessage` 类型依赖(查看实际 import 情况)
|
||||
|
||||
---
|
||||
|
||||
## 11. `src/utils/__tests__/configConstants.test.ts` (~8 tests)
|
||||
|
||||
**源文件**: `src/utils/configConstants.ts` (22 行)
|
||||
**依赖**: 无
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("NOTIFICATION_CHANNELS", () => {
|
||||
test("contains expected channels")
|
||||
test("is readonly array")
|
||||
test("includes 'auto', 'iterm2', 'terminal_bell'")
|
||||
})
|
||||
|
||||
describe("EDITOR_MODES", () => {
|
||||
test("contains 'normal' and 'vim'")
|
||||
test("has exactly 2 entries")
|
||||
})
|
||||
|
||||
describe("TEAMMATE_MODES", () => {
|
||||
test("contains 'auto', 'tmux', 'in-process'")
|
||||
test("has exactly 3 entries")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无
|
||||
|
||||
---
|
||||
|
||||
## 12. `src/utils/__tests__/directMemberMessage.test.ts` (~12 tests)
|
||||
|
||||
**源文件**: `src/utils/directMemberMessage.ts` (70 行)
|
||||
**依赖**: 仅类型(可 mock)
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("parseDirectMemberMessage", () => {
|
||||
test("parses '@agent-name hello world'")
|
||||
test("parses '@agent-name single-word'")
|
||||
test("returns null for non-matching input")
|
||||
test("returns null for empty string")
|
||||
test("returns null for '@name' without message")
|
||||
test("handles hyphenated agent names like '@my-agent msg'")
|
||||
test("handles multiline message content")
|
||||
test("extracts correct recipientName and message")
|
||||
})
|
||||
|
||||
// sendDirectMemberMessage 需要 mock teamContext/writeToMailbox
|
||||
describe("sendDirectMemberMessage", () => {
|
||||
test("returns error when no team context")
|
||||
test("returns error for unknown recipient")
|
||||
test("calls writeToMailbox with correct args for valid recipient")
|
||||
test("returns success for valid message")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
`sendDirectMemberMessage` 需要 mock `AppState['teamContext']` 和 `WriteToMailboxFn`
|
||||
|
||||
---
|
||||
|
||||
## 13. `src/utils/__tests__/collapseHookSummaries.test.ts` (~12 tests)
|
||||
|
||||
**源文件**: `src/utils/collapseHookSummaries.ts` (60 行)
|
||||
**依赖**: 仅类型
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("collapseHookSummaries", () => {
|
||||
test("returns same messages when no hook summaries")
|
||||
test("collapses consecutive messages with same hookLabel")
|
||||
test("does not collapse messages with different hookLabels")
|
||||
test("aggregates hookCount across collapsed messages")
|
||||
test("merges hookInfos arrays")
|
||||
test("merges hookErrors arrays")
|
||||
test("takes max totalDurationMs")
|
||||
test("takes any truthy preventContinuation")
|
||||
test("leaves single hook summary unchanged")
|
||||
test("handles three consecutive same-label summaries")
|
||||
test("preserves non-hook messages in between")
|
||||
test("returns empty array for empty input")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需要构造 `RenderableMessage` mock 对象
|
||||
@@ -1,287 +0,0 @@
|
||||
# Phase 19 - Batch 2: 更多 utils + state + commands
|
||||
|
||||
> 预计 ~120 tests / 8 文件 | 部分需轻量 mock
|
||||
|
||||
---
|
||||
|
||||
## 1. `src/utils/__tests__/collapseTeammateShutdowns.test.ts` (~10 tests)
|
||||
|
||||
**源文件**: `src/utils/collapseTeammateShutdowns.ts` (56 行)
|
||||
**依赖**: 仅类型
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("collapseTeammateShutdowns", () => {
|
||||
test("returns same messages when no teammate shutdowns")
|
||||
test("leaves single shutdown message unchanged")
|
||||
test("collapses consecutive shutdown messages into batch")
|
||||
test("batch attachment has correct count")
|
||||
test("does not collapse non-consecutive shutdowns")
|
||||
test("preserves non-shutdown messages between shutdowns")
|
||||
test("handles empty array")
|
||||
test("handles mixed message types")
|
||||
test("collapses more than 2 consecutive shutdowns")
|
||||
test("non-teammate task_status messages are not collapsed")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
构造 `RenderableMessage` mock 对象(带 `task_status` attachment,`status=completed`,`taskType=in_process_teammate`)
|
||||
|
||||
---
|
||||
|
||||
## 2. `src/utils/__tests__/privacyLevel.test.ts` (~12 tests)
|
||||
|
||||
**源文件**: `src/utils/privacyLevel.ts` (56 行)
|
||||
**依赖**: `process.env`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("getPrivacyLevel", () => {
|
||||
test("returns 'default' when no env vars set")
|
||||
test("returns 'essential-traffic' when CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is set")
|
||||
test("returns 'no-telemetry' when DISABLE_TELEMETRY is set")
|
||||
test("'essential-traffic' takes priority over 'no-telemetry'")
|
||||
})
|
||||
|
||||
describe("isEssentialTrafficOnly", () => {
|
||||
test("returns true for 'essential-traffic' level")
|
||||
test("returns false for 'default' level")
|
||||
test("returns false for 'no-telemetry' level")
|
||||
})
|
||||
|
||||
describe("isTelemetryDisabled", () => {
|
||||
test("returns true for 'no-telemetry' level")
|
||||
test("returns true for 'essential-traffic' level")
|
||||
test("returns false for 'default' level")
|
||||
})
|
||||
|
||||
describe("getEssentialTrafficOnlyReason", () => {
|
||||
test("returns env var name when restricted")
|
||||
test("returns null when unrestricted")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
`process.env` 保存/恢复模式(参考现有 `envUtils.test.ts`)
|
||||
|
||||
---
|
||||
|
||||
## 3. `src/utils/__tests__/textHighlighting.test.ts` (~18 tests)
|
||||
|
||||
**源文件**: `src/utils/textHighlighting.ts` (167 行)
|
||||
**依赖**: `@alcalzone/ansi-tokenize`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("segmentTextByHighlights", () => {
|
||||
// 基本
|
||||
test("returns single segment with no highlights")
|
||||
test("returns highlighted segment for single highlight")
|
||||
test("returns two segments for highlight covering middle portion")
|
||||
test("returns three segments for highlight in the middle")
|
||||
|
||||
// 多高亮
|
||||
test("handles non-overlapping highlights")
|
||||
test("handles overlapping highlights (priority-based)")
|
||||
test("handles adjacent highlights")
|
||||
|
||||
// 边界
|
||||
test("highlight starting at 0")
|
||||
test("highlight ending at text length")
|
||||
test("highlight covering entire text")
|
||||
test("empty text with highlights")
|
||||
test("empty highlights array returns single segment")
|
||||
|
||||
// ANSI 处理
|
||||
test("correctly segments text with ANSI escape codes")
|
||||
test("handles text with mixed ANSI and highlights")
|
||||
|
||||
// 属性
|
||||
test("preserves highlight color property")
|
||||
test("preserves highlight priority property")
|
||||
test("preserves dimColor and inverse flags")
|
||||
test("highlights with start > end are handled gracefully")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
可能需要 mock `@alcalzone/ansi-tokenize`,或直接使用(如果有安装)
|
||||
|
||||
---
|
||||
|
||||
## 4. `src/utils/__tests__/detectRepository.test.ts` (~15 tests)
|
||||
|
||||
**源文件**: `src/utils/detectRepository.ts` (179 行)
|
||||
**依赖**: git 命令(`getRemoteUrl`)
|
||||
|
||||
### 重点测试函数
|
||||
|
||||
**`parseGitRemote(input: string): ParsedRepository | null`** — 纯正则解析
|
||||
**`parseGitHubRepository(input: string): string | null`** — 纯函数
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("parseGitRemote", () => {
|
||||
// HTTPS
|
||||
test("parses HTTPS URL: https://github.com/owner/repo.git")
|
||||
test("parses HTTPS URL without .git suffix")
|
||||
test("parses HTTPS URL with subdirectory path (only takes first 2 segments)")
|
||||
|
||||
// SSH
|
||||
test("parses SSH URL: git@github.com:owner/repo.git")
|
||||
test("parses SSH URL without .git suffix")
|
||||
|
||||
// ssh://
|
||||
test("parses ssh:// URL: ssh://git@github.com/owner/repo.git")
|
||||
|
||||
// git://
|
||||
test("parses git:// URL")
|
||||
|
||||
// 边界
|
||||
test("returns null for invalid URL")
|
||||
test("returns null for empty string")
|
||||
test("handles GHE hostname")
|
||||
test("handles port number in URL")
|
||||
})
|
||||
|
||||
describe("parseGitHubRepository", () => {
|
||||
test("extracts 'owner/repo' from valid remote URL")
|
||||
test("handles plain 'owner/repo' string input")
|
||||
test("returns null for non-GitHub host (if restricted)")
|
||||
test("returns null for invalid input")
|
||||
test("is case-sensitive for owner/repo")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
仅测试 `parseGitRemote` 和 `parseGitHubRepository`(纯函数),不需要 mock git
|
||||
|
||||
---
|
||||
|
||||
## 5. `src/utils/__tests__/markdown.test.ts` (~20 tests)
|
||||
|
||||
**源文件**: `src/utils/markdown.ts` (382 行)
|
||||
**依赖**: `marked`, `cli-highlight`, theme types
|
||||
|
||||
### 重点测试函数
|
||||
|
||||
**`padAligned(content, displayWidth, targetWidth, align)`** — 纯函数
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("padAligned", () => {
|
||||
test("left-aligns: pads with spaces on right")
|
||||
test("right-aligns: pads with spaces on left")
|
||||
test("center-aligns: pads with spaces on both sides")
|
||||
test("no padding when displayWidth equals targetWidth")
|
||||
test("handles content wider than targetWidth")
|
||||
test("null/undefined align defaults to left")
|
||||
test("handles empty string content")
|
||||
test("handles zero displayWidth")
|
||||
test("handles zero targetWidth")
|
||||
test("center alignment with odd padding distribution")
|
||||
})
|
||||
```
|
||||
|
||||
注意:`numberToLetter`/`numberToRoman`/`getListNumber` 是私有函数,除非从模块导出否则无法直接测试。如果确实私有,则通过 `applyMarkdown` 间接测试列表渲染:
|
||||
|
||||
```typescript
|
||||
describe("list numbering (via applyMarkdown)", () => {
|
||||
test("numbered list renders with digits")
|
||||
test("nested ordered list uses letters (a, b, c)")
|
||||
test("deep nested list uses roman numerals")
|
||||
test("unordered list uses bullet markers")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
`padAligned` 无需 mock。`applyMarkdown` 可能需要 mock theme 依赖。
|
||||
|
||||
---
|
||||
|
||||
## 6. `src/state/__tests__/store.test.ts` (~15 tests)
|
||||
|
||||
**源文件**: `src/state/store.ts` (35 行)
|
||||
**依赖**: 无
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("createStore", () => {
|
||||
test("returns object with getState, setState, subscribe")
|
||||
test("getState returns initial state")
|
||||
test("setState updates state via updater function")
|
||||
test("setState does not notify when state unchanged (Object.is)")
|
||||
test("setState notifies subscribers on change")
|
||||
test("subscribe returns unsubscribe function")
|
||||
test("unsubscribe stops notifications")
|
||||
test("multiple subscribers all get notified")
|
||||
test("onChange callback is called on state change")
|
||||
test("onChange is not called when state unchanged")
|
||||
test("works with complex state objects")
|
||||
test("works with primitive state")
|
||||
test("updater receives previous state")
|
||||
test("sequential setState calls produce final state")
|
||||
test("subscriber called after all state changes in synchronous batch")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无
|
||||
|
||||
---
|
||||
|
||||
## 7. `src/commands/plugin/__tests__/parseArgs.test.ts` (~18 tests)
|
||||
|
||||
**源文件**: `src/commands/plugin/parseArgs.ts` (104 行)
|
||||
**依赖**: 无
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("parsePluginArgs", () => {
|
||||
// 无参数
|
||||
test("returns { type: 'menu' } for undefined")
|
||||
test("returns { type: 'menu' } for empty string")
|
||||
test("returns { type: 'menu' } for whitespace only")
|
||||
|
||||
// help
|
||||
test("returns { type: 'help' } for 'help'")
|
||||
|
||||
// install
|
||||
test("parses 'install my-plugin' -> { type: 'install', name: 'my-plugin' }")
|
||||
test("parses 'install my-plugin@github' with marketplace")
|
||||
test("parses 'install https://github.com/...' as URL marketplace")
|
||||
|
||||
// uninstall
|
||||
test("returns { type: 'uninstall', name: '...' }")
|
||||
|
||||
// enable/disable
|
||||
test("returns { type: 'enable', name: '...' }")
|
||||
test("returns { type: 'disable', name: '...' }")
|
||||
|
||||
// validate
|
||||
test("returns { type: 'validate', name: '...' }")
|
||||
|
||||
// manage
|
||||
test("returns { type: 'manage' }")
|
||||
|
||||
// marketplace 子命令
|
||||
test("parses 'marketplace add ...'")
|
||||
test("parses 'marketplace remove ...'")
|
||||
test("parses 'marketplace list'")
|
||||
|
||||
// 边界
|
||||
test("handles extra whitespace")
|
||||
test("handles unknown subcommand gracefully")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
无
|
||||
@@ -1,258 +0,0 @@
|
||||
# Phase 19 - Batch 3: Tool 子模块纯逻辑
|
||||
|
||||
> 预计 ~113 tests / 6 文件 | 采用 `mock.module()` + `await import()` 模式
|
||||
|
||||
---
|
||||
|
||||
## 1. `src/tools/GrepTool/__tests__/headLimit.test.ts` (~20 tests)
|
||||
|
||||
**源文件**: `src/tools/GrepTool/GrepTool.ts` (578 行)
|
||||
**目标函数**: `applyHeadLimit<T>`, `formatLimitInfo` (非导出,需确认可测性)
|
||||
|
||||
### 测试策略
|
||||
如果函数是文件内导出的,直接 `await import()` 获取。如果私有,则通过 GrepTool 的输出间接测试,或提取到独立文件。
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("applyHeadLimit", () => {
|
||||
test("returns full array when limit is undefined (default 250)")
|
||||
test("applies limit correctly: limits to N items")
|
||||
test("limit=0 means no limit (returns all)")
|
||||
test("applies offset correctly")
|
||||
test("offset + limit combined")
|
||||
test("offset beyond array length returns empty")
|
||||
test("returns appliedLimit when truncation occurred")
|
||||
test("returns appliedLimit=undefined when no truncation")
|
||||
test("limit larger than array returns all items with appliedLimit=undefined")
|
||||
test("empty array returns empty with appliedLimit=undefined")
|
||||
test("offset=0 is default")
|
||||
test("negative limit behavior")
|
||||
})
|
||||
|
||||
describe("formatLimitInfo", () => {
|
||||
test("formats 'limit: N, offset: M' when both present")
|
||||
test("formats 'limit: N' when only limit")
|
||||
test("formats 'offset: M' when only offset")
|
||||
test("returns empty string when both undefined")
|
||||
test("handles limit=0 (no limit, should not appear)")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock 重依赖链(`log`, `slowOperations` 等),通过 `mock.module()` + `await import()` 只取目标函数
|
||||
|
||||
---
|
||||
|
||||
## 2. `src/tools/MCPTool/__tests__/classifyForCollapse.test.ts` (~25 tests)
|
||||
|
||||
**源文件**: `src/tools/MCPTool/classifyForCollapse.ts` (605 行)
|
||||
**目标函数**: `classifyMcpToolForCollapse`, `normalize`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("normalize", () => {
|
||||
test("leaves snake_case unchanged: 'search_issues'")
|
||||
test("converts camelCase to snake_case: 'searchIssues' -> 'search_issues'")
|
||||
test("converts kebab-case to snake_case: 'search-issues' -> 'search_issues'")
|
||||
test("handles mixed: 'searchIssuesByStatus' -> 'search_issues_by_status'")
|
||||
test("handles already lowercase single word")
|
||||
test("handles empty string")
|
||||
test("handles PascalCase: 'SearchIssues' -> 'search_issues'")
|
||||
})
|
||||
|
||||
describe("classifyMcpToolForCollapse", () => {
|
||||
// 搜索工具
|
||||
test("classifies Slack search_messages as search")
|
||||
test("classifies GitHub search_code as search")
|
||||
test("classifies Linear search_issues as search")
|
||||
test("classifies Datadog search_logs as search")
|
||||
test("classifies Notion search as search")
|
||||
|
||||
// 读取工具
|
||||
test("classifies Slack get_message as read")
|
||||
test("classifies GitHub get_file_contents as read")
|
||||
test("classifies Linear get_issue as read")
|
||||
test("classifies Filesystem read_file as read")
|
||||
|
||||
// 双重分类
|
||||
test("some tools are both search and read")
|
||||
test("some tools are neither search nor read")
|
||||
|
||||
// 未知工具
|
||||
test("unknown tool returns { isSearch: false, isRead: false }")
|
||||
test("tool name with camelCase variant still matches")
|
||||
test("tool name with kebab-case variant still matches")
|
||||
|
||||
// server name 不影响分类
|
||||
test("server name parameter is accepted but unused in current logic")
|
||||
|
||||
// 边界
|
||||
test("empty tool name returns false/false")
|
||||
test("case sensitivity check (should match after normalize)")
|
||||
test("handles tool names with numbers")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
文件自包含(仅内部 Set + normalize 函数),需确认 `normalize` 是否导出
|
||||
|
||||
---
|
||||
|
||||
## 3. `src/tools/FileReadTool/__tests__/blockedPaths.test.ts` (~18 tests)
|
||||
|
||||
**源文件**: `src/tools/FileReadTool/FileReadTool.ts` (1184 行)
|
||||
**目标函数**: `isBlockedDevicePath`, `getAlternateScreenshotPath`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("isBlockedDevicePath", () => {
|
||||
// 阻止的设备
|
||||
test("blocks /dev/zero")
|
||||
test("blocks /dev/random")
|
||||
test("blocks /dev/urandom")
|
||||
test("blocks /dev/full")
|
||||
test("blocks /dev/stdin")
|
||||
test("blocks /dev/tty")
|
||||
test("blocks /dev/console")
|
||||
test("blocks /dev/stdout")
|
||||
test("blocks /dev/stderr")
|
||||
test("blocks /dev/fd/0")
|
||||
test("blocks /dev/fd/1")
|
||||
test("blocks /dev/fd/2")
|
||||
|
||||
// 阻止 /proc
|
||||
test("blocks /proc/self/fd/0")
|
||||
test("blocks /proc/123/fd/2")
|
||||
|
||||
// 允许的路径
|
||||
test("allows /dev/null")
|
||||
test("allows regular file paths")
|
||||
test("allows /home/user/file.txt")
|
||||
})
|
||||
|
||||
describe("getAlternateScreenshotPath", () => {
|
||||
test("returns undefined for path without AM/PM")
|
||||
test("returns alternate path for macOS screenshot with regular space before AM")
|
||||
test("returns alternate path for macOS screenshot with U+202F before PM")
|
||||
test("handles path without time component")
|
||||
test("handles multiple AM/PM occurrences")
|
||||
test("returns undefined when no space variant difference")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock 重依赖链,通过 `await import()` 获取函数
|
||||
|
||||
---
|
||||
|
||||
## 4. `src/tools/AgentTool/__tests__/agentDisplay.test.ts` (~15 tests)
|
||||
|
||||
**源文件**: `src/tools/AgentTool/agentDisplay.ts` (105 行)
|
||||
**目标函数**: `resolveAgentOverrides`, `compareAgentsByName`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("resolveAgentOverrides", () => {
|
||||
test("marks no overrides when all agents active")
|
||||
test("marks inactive agent as overridden")
|
||||
test("overriddenBy shows the overriding agent source")
|
||||
test("deduplicates agents by (agentType, source)")
|
||||
test("preserves agent definition properties")
|
||||
test("handles empty arrays")
|
||||
test("handles agent from git worktree (duplicate detection)")
|
||||
})
|
||||
|
||||
describe("compareAgentsByName", () => {
|
||||
test("sorts alphabetically ascending")
|
||||
test("returns negative when a.name < b.name")
|
||||
test("returns positive when a.name > b.name")
|
||||
test("returns 0 for same name")
|
||||
test("is case-sensitive")
|
||||
})
|
||||
|
||||
describe("AGENT_SOURCE_GROUPS", () => {
|
||||
test("contains expected source groups in order")
|
||||
test("has unique labels")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock `AgentDefinition`, `AgentSource` 类型依赖
|
||||
|
||||
---
|
||||
|
||||
## 5. `src/tools/AgentTool/__tests__/agentToolUtils.test.ts` (~20 tests)
|
||||
|
||||
**源文件**: `src/tools/AgentTool/agentToolUtils.ts` (688 行)
|
||||
**目标函数**: `countToolUses`, `getLastToolUseName`, `extractPartialResult`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("countToolUses", () => {
|
||||
test("counts tool_use blocks in messages")
|
||||
test("returns 0 for messages without tool_use")
|
||||
test("returns 0 for empty array")
|
||||
test("counts multiple tool_use blocks across messages")
|
||||
test("counts tool_use in single message with multiple blocks")
|
||||
})
|
||||
|
||||
describe("getLastToolUseName", () => {
|
||||
test("returns last tool name from assistant message")
|
||||
test("returns undefined for message without tool_use")
|
||||
test("returns the last tool when multiple tool_uses present")
|
||||
test("handles message with non-array content")
|
||||
})
|
||||
|
||||
describe("extractPartialResult", () => {
|
||||
test("extracts text from last assistant message")
|
||||
test("returns undefined for messages without assistant content")
|
||||
test("handles interrupted agent with partial text")
|
||||
test("returns undefined for empty messages")
|
||||
test("concatenates multiple text blocks")
|
||||
test("skips non-text content blocks")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock 消息类型依赖
|
||||
|
||||
---
|
||||
|
||||
## 6. `src/tools/SkillTool/__tests__/skillSafety.test.ts` (~15 tests)
|
||||
|
||||
**源文件**: `src/tools/SkillTool/SkillTool.ts` (1110 行)
|
||||
**目标函数**: `skillHasOnlySafeProperties`, `extractUrlScheme`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("skillHasOnlySafeProperties", () => {
|
||||
test("returns true for command with only safe properties")
|
||||
test("returns true for command with undefined extra properties")
|
||||
test("returns false for command with unsafe meaningful property")
|
||||
test("returns true for command with null extra properties")
|
||||
test("returns true for command with empty array extra property")
|
||||
test("returns true for command with empty object extra property")
|
||||
test("returns false for command with non-empty unsafe array")
|
||||
test("returns false for command with non-empty unsafe object")
|
||||
test("returns true for empty command object")
|
||||
})
|
||||
|
||||
describe("extractUrlScheme", () => {
|
||||
test("extracts 'gs' from 'gs://bucket/path'")
|
||||
test("extracts 'https' from 'https://example.com'")
|
||||
test("extracts 'http' from 'http://example.com'")
|
||||
test("extracts 's3' from 's3://bucket/path'")
|
||||
test("defaults to 'gs' for unknown scheme")
|
||||
test("defaults to 'gs' for path without scheme")
|
||||
test("defaults to 'gs' for empty string")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock 重依赖链,`await import()` 获取函数
|
||||
@@ -1,215 +0,0 @@
|
||||
# Phase 19 - Batch 4: Services 纯逻辑
|
||||
|
||||
> 预计 ~84 tests / 5 文件 | 部分需轻量 mock
|
||||
|
||||
---
|
||||
|
||||
## 1. `src/services/compact/__tests__/grouping.test.ts` (~15 tests)
|
||||
|
||||
**源文件**: `src/services/compact/grouping.ts` (64 行)
|
||||
**目标函数**: `groupMessagesByApiRound`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("groupMessagesByApiRound", () => {
|
||||
test("returns single group for single API round")
|
||||
test("splits at new assistant message ID")
|
||||
test("keeps tool_result messages with their parent assistant message")
|
||||
test("handles streaming chunks (same assistant ID stays grouped)")
|
||||
test("returns empty array for empty input")
|
||||
test("handles all user messages (no assistant)")
|
||||
test("handles alternating assistant IDs")
|
||||
test("three API rounds produce three groups")
|
||||
test("user messages before first assistant go in first group")
|
||||
test("consecutive user messages stay in same group")
|
||||
test("does not produce empty groups")
|
||||
test("handles single message")
|
||||
test("preserves message order within groups")
|
||||
test("handles system messages")
|
||||
test("tool_result after assistant stays in same round")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需构造 `Message` mock 对象(type: 'user'/'assistant', message: { id, content })
|
||||
|
||||
---
|
||||
|
||||
## 2. `src/services/compact/__tests__/stripMessages.test.ts` (~20 tests)
|
||||
|
||||
**源文件**: `src/services/compact/compact.ts` (1709 行)
|
||||
**目标函数**: `stripImagesFromMessages`, `collectReadToolFilePaths` (私有)
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("stripImagesFromMessages", () => {
|
||||
// user 消息处理
|
||||
test("replaces image block with [image] text")
|
||||
test("replaces document block with [document] text")
|
||||
test("preserves text blocks unchanged")
|
||||
test("handles multiple image/document blocks in single message")
|
||||
test("returns original message when no media blocks")
|
||||
|
||||
// tool_result 内嵌套
|
||||
test("replaces image inside tool_result content")
|
||||
test("replaces document inside tool_result content")
|
||||
test("preserves non-media tool_result content")
|
||||
|
||||
// 非用户消息
|
||||
test("passes through assistant messages unchanged")
|
||||
test("passes through system messages unchanged")
|
||||
|
||||
// 边界
|
||||
test("handles empty message array")
|
||||
test("handles string content (non-array) in user message")
|
||||
test("does not mutate original messages")
|
||||
})
|
||||
|
||||
describe("collectReadToolFilePaths", () => {
|
||||
// 注意:这是私有函数,可能需要通过 stripImagesFromMessages 或其他导出间接测试
|
||||
// 如果不可直接测试,则跳过或通过集成测试覆盖
|
||||
test("collects file_path from Read tool_use blocks")
|
||||
test("skips tool_use with FILE_UNCHANGED_STUB result")
|
||||
test("returns empty set for messages without Read tool_use")
|
||||
test("handles multiple Read calls across messages")
|
||||
test("normalizes paths via expandPath")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock `expandPath`(如果 collectReadToolFilePaths 要测)
|
||||
需 mock `log`, `slowOperations` 等重依赖
|
||||
构造 `Message` mock 对象
|
||||
|
||||
---
|
||||
|
||||
## 3. `src/services/compact/__tests__/prompt.test.ts` (~12 tests)
|
||||
|
||||
**源文件**: `src/services/compact/prompt.ts` (375 行)
|
||||
**目标函数**: `formatCompactSummary`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("formatCompactSummary", () => {
|
||||
test("strips <analysis>...</analysis> block")
|
||||
test("replaces <summary>...</summary> with 'Summary:\\n' prefix")
|
||||
test("handles analysis + summary together")
|
||||
test("handles summary without analysis")
|
||||
test("handles analysis without summary")
|
||||
test("collapses multiple newlines to double")
|
||||
test("trims leading/trailing whitespace")
|
||||
test("handles empty string")
|
||||
test("handles plain text without tags")
|
||||
test("handles multiline analysis content")
|
||||
test("preserves content between analysis and summary")
|
||||
test("handles nested-like tags gracefully")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock 重依赖链(`log`, feature flags 等)
|
||||
`formatCompactSummary` 是纯字符串处理,如果 import 链不太重则无需复杂 mock
|
||||
|
||||
---
|
||||
|
||||
## 4. `src/services/mcp/__tests__/channelPermissions.test.ts` (~25 tests)
|
||||
|
||||
**源文件**: `src/services/mcp/channelPermissions.ts` (241 行)
|
||||
**目标函数**: `hashToId`, `shortRequestId`, `truncateForPreview`, `filterPermissionRelayClients`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("hashToId", () => {
|
||||
test("returns 5-char string")
|
||||
test("uses only letters a-z excluding 'l'")
|
||||
test("is deterministic (same input = same output)")
|
||||
test("different inputs produce different outputs (with high probability)")
|
||||
test("handles empty string")
|
||||
})
|
||||
|
||||
describe("shortRequestId", () => {
|
||||
test("returns 5-char string from tool use ID")
|
||||
test("is deterministic")
|
||||
test("avoids profanity substrings (retries with salt)")
|
||||
test("returns a valid ID even if all retries hit bad words (unlikely)")
|
||||
})
|
||||
|
||||
describe("truncateForPreview", () => {
|
||||
test("returns JSON string for object input")
|
||||
test("truncates to <=200 chars when input is long")
|
||||
test("adds ellipsis or truncation indicator")
|
||||
test("returns short input unchanged")
|
||||
test("handles string input")
|
||||
test("handles null/undefined input")
|
||||
})
|
||||
|
||||
describe("filterPermissionRelayClients", () => {
|
||||
test("keeps connected clients in allowlist with correct capabilities")
|
||||
test("filters out disconnected clients")
|
||||
test("filters out clients not in allowlist")
|
||||
test("filters out clients missing required capabilities")
|
||||
test("returns empty array for empty input")
|
||||
test("type predicate narrows correctly")
|
||||
})
|
||||
|
||||
describe("PERMISSION_REPLY_RE", () => {
|
||||
test("matches 'y abcde'")
|
||||
test("matches 'yes abcde'")
|
||||
test("matches 'n abcde'")
|
||||
test("matches 'no abcde'")
|
||||
test("is case-insensitive")
|
||||
test("does not match without ID")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
`hashToId` 可能需要确认导出状态
|
||||
`filterPermissionRelayClients` 需要 mock 客户端类型
|
||||
`truncateForPreview` 可能依赖 `jsonStringify`(需 mock `slowOperations`)
|
||||
|
||||
---
|
||||
|
||||
## 5. `src/services/mcp/__tests__/officialRegistry.test.ts` (~12 tests)
|
||||
|
||||
**源文件**: `src/services/mcp/officialRegistry.ts` (73 行)
|
||||
**目标函数**: `normalizeUrl` (私有), `isOfficialMcpUrl`, `resetOfficialMcpUrlsForTesting`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("normalizeUrl", () => {
|
||||
// 注意:如果是私有的,通过 isOfficialMcpUrl 间接测试
|
||||
test("removes trailing slash")
|
||||
test("removes query parameters")
|
||||
test("preserves path")
|
||||
test("handles URL with port")
|
||||
test("handles URL with hash fragment")
|
||||
})
|
||||
|
||||
describe("isOfficialMcpUrl", () => {
|
||||
test("returns false when registry not loaded (initial state)")
|
||||
test("returns true for URL added to registry")
|
||||
test("returns false for non-registered URL")
|
||||
test("uses normalized URL for comparison")
|
||||
})
|
||||
|
||||
describe("resetOfficialMcpUrlsForTesting", () => {
|
||||
test("clears the cached URLs")
|
||||
test("allows fresh start after reset")
|
||||
})
|
||||
|
||||
describe("URL normalization + lookup integration", () => {
|
||||
test("URL with trailing slash matches normalized version")
|
||||
test("URL with query params matches normalized version")
|
||||
test("different URLs do not match")
|
||||
test("case sensitivity check")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock `axios`(避免网络请求)
|
||||
使用 `resetOfficialMcpUrlsForTesting` 做测试隔离
|
||||
@@ -1,200 +0,0 @@
|
||||
# Phase 19 - Batch 5: MCP 配置 + modelCost
|
||||
|
||||
> 预计 ~80 tests / 4 文件 | 需中等 mock
|
||||
|
||||
---
|
||||
|
||||
## 1. `src/services/mcp/__tests__/configUtils.test.ts` (~30 tests)
|
||||
|
||||
**源文件**: `src/services/mcp/config.ts` (1580 行)
|
||||
**目标函数**: `unwrapCcrProxyUrl`, `urlPatternToRegex` (私有), `commandArraysMatch` (私有), `toggleMembership` (私有), `addScopeToServers` (私有), `dedupPluginMcpServers`, `getMcpServerSignature` (如导出)
|
||||
|
||||
### 测试策略
|
||||
私有函数如不可直接测试,通过公开的 `dedupPluginMcpServers` 间接覆盖。导出函数直接测。
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("unwrapCcrProxyUrl", () => {
|
||||
test("returns original URL when no CCR proxy markers")
|
||||
test("extracts mcp_url from CCR proxy URL with /v2/session_ingress/shttp/mcp/")
|
||||
test("extracts mcp_url from CCR proxy URL with /v2/ccr-sessions/")
|
||||
test("returns original URL when mcp_url param is missing")
|
||||
test("handles malformed URL gracefully")
|
||||
test("handles URL with both proxy marker and mcp_url")
|
||||
test("preserves non-CCR URLs unchanged")
|
||||
})
|
||||
|
||||
describe("dedupPluginMcpServers", () => {
|
||||
test("keeps unique plugin servers")
|
||||
test("suppresses plugin server duplicated by manual config")
|
||||
test("suppresses plugin server duplicated by earlier plugin")
|
||||
test("keeps servers with null signature")
|
||||
test("returns empty for empty inputs")
|
||||
test("reports suppressed with correct duplicateOf name")
|
||||
test("handles multiple plugins with same config")
|
||||
})
|
||||
|
||||
describe("toggleMembership (via integration)", () => {
|
||||
test("adds item when shouldContain=true and not present")
|
||||
test("removes item when shouldContain=false and present")
|
||||
test("returns same array when already in desired state")
|
||||
})
|
||||
|
||||
describe("addScopeToServers (via integration)", () => {
|
||||
test("adds scope to each server config")
|
||||
test("returns empty object for undefined input")
|
||||
test("returns empty object for empty input")
|
||||
test("preserves all original config properties")
|
||||
})
|
||||
|
||||
describe("urlPatternToRegex (via integration)", () => {
|
||||
test("matches exact URL")
|
||||
test("matches wildcard pattern *.example.com")
|
||||
test("matches multiple wildcards")
|
||||
test("does not match non-matching URL")
|
||||
test("escapes regex special characters in pattern")
|
||||
})
|
||||
|
||||
describe("commandArraysMatch (via integration)", () => {
|
||||
test("returns true for identical arrays")
|
||||
test("returns false for different lengths")
|
||||
test("returns false for same length different elements")
|
||||
test("returns true for empty arrays")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock `feature()` (bun:bundle), `jsonStringify`, `safeParseJSON`, `log` 等
|
||||
通过 `mock.module()` + `await import()` 解锁
|
||||
|
||||
---
|
||||
|
||||
## 2. `src/services/mcp/__tests__/filterUtils.test.ts` (~20 tests)
|
||||
|
||||
**源文件**: `src/services/mcp/utils.ts` (576 行)
|
||||
**目标函数**: `filterToolsByServer`, `hashMcpConfig`, `isToolFromMcpServer`, `isMcpTool`, `parseHeaders`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("filterToolsByServer", () => {
|
||||
test("filters tools matching server name prefix")
|
||||
test("returns empty for no matching tools")
|
||||
test("handles empty tools array")
|
||||
test("normalizes server name for matching")
|
||||
})
|
||||
|
||||
describe("hashMcpConfig", () => {
|
||||
test("returns 16-char hex string")
|
||||
test("is deterministic")
|
||||
test("excludes scope from hash")
|
||||
test("different configs produce different hashes")
|
||||
test("key order does not affect hash (sorted)")
|
||||
})
|
||||
|
||||
describe("isToolFromMcpServer", () => {
|
||||
test("returns true when tool belongs to specified server")
|
||||
test("returns false for different server")
|
||||
test("returns false for non-MCP tool name")
|
||||
test("handles empty tool name")
|
||||
})
|
||||
|
||||
describe("isMcpTool", () => {
|
||||
test("returns true for tool name starting with 'mcp__'")
|
||||
test("returns true when tool.isMcp is true")
|
||||
test("returns false for regular tool")
|
||||
test("returns false when neither condition met")
|
||||
})
|
||||
|
||||
describe("parseHeaders", () => {
|
||||
test("parses 'Key: Value' format")
|
||||
test("parses multiple headers")
|
||||
test("trims whitespace around key and value")
|
||||
test("throws on missing colon")
|
||||
test("throws on empty key")
|
||||
test("handles value with colons (like URLs)")
|
||||
test("returns empty object for empty array")
|
||||
test("handles duplicate keys (last wins)")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock `normalizeNameForMCP`, `mcpInfoFromString`, `jsonStringify`, `createHash` 等
|
||||
`parseHeaders` 是最独立的,可能不需要太多 mock
|
||||
|
||||
---
|
||||
|
||||
## 3. `src/services/mcp/__tests__/channelNotification.test.ts` (~15 tests)
|
||||
|
||||
**源文件**: `src/services/mcp/channelNotification.ts` (317 行)
|
||||
**目标函数**: `wrapChannelMessage`, `findChannelEntry`
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("wrapChannelMessage", () => {
|
||||
test("wraps content in <channel> tag with source attribute")
|
||||
test("escapes server name in attribute")
|
||||
test("includes meta attributes when provided")
|
||||
test("escapes meta values via escapeXmlAttr")
|
||||
test("filters out meta keys not matching SAFE_META_KEY pattern")
|
||||
test("handles empty meta")
|
||||
test("handles content with special characters")
|
||||
test("formats with newlines between tags and content")
|
||||
})
|
||||
|
||||
describe("findChannelEntry", () => {
|
||||
test("finds server entry by exact name match")
|
||||
test("finds plugin entry by matching second segment")
|
||||
test("returns undefined for no match")
|
||||
test("handles empty channels array")
|
||||
test("handles server name without colon")
|
||||
test("handles 'plugin:name' format correctly")
|
||||
test("prefers exact match over partial match")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock `escapeXmlAttr`(来自 xml.ts,已有测试)或直接使用
|
||||
`CHANNEL_TAG` 常量需确认导出
|
||||
|
||||
---
|
||||
|
||||
## 4. `src/utils/__tests__/modelCost.test.ts` (~15 tests)
|
||||
|
||||
**源文件**: `src/utils/modelCost.ts` (232 行)
|
||||
**目标函数**: `formatModelPricing`, `COST_TIER_*` 常量
|
||||
|
||||
### 测试用例
|
||||
|
||||
```typescript
|
||||
describe("COST_TIER constants", () => {
|
||||
test("COST_TIER_3_15 has inputTokens=3, outputTokens=15")
|
||||
test("COST_TIER_15_75 has inputTokens=15, outputTokens=75")
|
||||
test("COST_TIER_5_25 has inputTokens=5, outputTokens=25")
|
||||
test("COST_TIER_30_150 has inputTokens=30, outputTokens=150")
|
||||
test("COST_HAIKU_35 has inputTokens=0.8, outputTokens=4")
|
||||
test("COST_HAIKU_45 has inputTokens=1, outputTokens=5")
|
||||
})
|
||||
|
||||
describe("formatModelPricing", () => {
|
||||
test("formats integer prices without decimals: '$3/$15 per Mtok'")
|
||||
test("formats float prices with 2 decimals: '$0.80/$4.00 per Mtok'")
|
||||
test("formats mixed: '$5/$25 per Mtok'")
|
||||
test("formats large prices: '$30/$150 per Mtok'")
|
||||
test("formats $1/$5 correctly (integer but small)")
|
||||
test("handles zero prices: '$0/$0 per Mtok'")
|
||||
})
|
||||
|
||||
describe("MODEL_COSTS", () => {
|
||||
test("maps known model names to cost tiers")
|
||||
test("contains entries for claude-sonnet-4-6")
|
||||
test("contains entries for claude-opus-4-6")
|
||||
test("contains entries for claude-haiku-4-5")
|
||||
})
|
||||
```
|
||||
|
||||
### Mock 需求
|
||||
需 mock `log`, `slowOperations` 等重依赖(modelCost.ts 通常 import 链较重)
|
||||
`formatModelPricing` 和 `COST_TIER_*` 是纯数据/纯函数,mock 成功后直接测
|
||||
@@ -1,296 +0,0 @@
|
||||
# Testing Specification
|
||||
|
||||
本文档定义 claude-code 项目的测试规范、当前覆盖状态和改进计划。
|
||||
|
||||
## 1. 技术栈
|
||||
|
||||
| 项 | 选型 |
|
||||
|----|------|
|
||||
| 测试框架 | `bun:test` |
|
||||
| 断言/Mock | `bun:test` 内置 |
|
||||
| 覆盖率 | `bun test --coverage` |
|
||||
| CI | GitHub Actions,push/PR 到 main 自动运行 |
|
||||
|
||||
## 2. 测试层次
|
||||
|
||||
本项目采用 **单元测试 + 集成测试** 两层结构,不做 E2E 或快照测试。
|
||||
|
||||
- **单元测试** — 纯函数、工具类、解析器。文件就近放置于 `src/**/__tests__/`。
|
||||
- **集成测试** — 多模块协作流程。集中于 `tests/integration/`。
|
||||
|
||||
## 3. 文件结构与命名
|
||||
|
||||
```
|
||||
src/
|
||||
├── utils/__tests__/ # 纯函数单元测试
|
||||
├── tools/<Tool>/__tests__/ # Tool 单元测试
|
||||
├── services/mcp/__tests__/ # MCP 单元测试
|
||||
├── utils/permissions/__tests__/
|
||||
├── utils/model/__tests__/
|
||||
├── utils/settings/__tests__/
|
||||
├── utils/shell/__tests__/
|
||||
├── utils/git/__tests__/
|
||||
└── __tests__/ # 顶层模块测试 (Tool.ts, tools.ts)
|
||||
tests/
|
||||
├── integration/ # 集成测试(尚未创建)
|
||||
├── mocks/ # 共享 mock/fixture(尚未创建)
|
||||
└── helpers/ # 测试辅助函数
|
||||
```
|
||||
|
||||
- 测试文件:`<module>.test.ts`
|
||||
- 命名风格:`describe("functionName")` + `test("行为描述")`,英文
|
||||
- 编写原则:Arrange-Act-Assert、单一职责、独立性、边界覆盖
|
||||
|
||||
## 4. 当前覆盖状态
|
||||
|
||||
> 更新日期:2026-04-02 | **1623 tests, 84 files, 0 fail, 851ms**
|
||||
|
||||
### 4.1 可靠度评分
|
||||
|
||||
每个测试文件按断言深度、边界覆盖、mock 质量、测试独立性综合评定:
|
||||
|
||||
| 等级 | 含义 |
|
||||
|------|------|
|
||||
| **GOOD** | 断言精确(exact match),边界充分,结构清晰 |
|
||||
| **ACCEPTABLE** | 正常路径覆盖完整,部分边界或断言可加强 |
|
||||
| **WEAK** | 存在明显缺陷:断言过弱、重要边界缺失、或有脆弱性风险 |
|
||||
|
||||
### 4.2 按模块分布
|
||||
|
||||
#### P0 — 核心模块
|
||||
|
||||
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|
||||
|------|-------|------|----------|----------|
|
||||
| `src/__tests__/Tool.test.ts` | 20 | GOOD | buildTool, toolMatchesName, findToolByName, filterToolProgressMessages | — |
|
||||
| `src/__tests__/tools.test.ts` | 9 | ACCEPTABLE | parseToolPreset, filterToolsByDenyRules | 预设覆盖仅测 "default";有冗余用例 |
|
||||
| `src/tools/FileEditTool/__tests__/utils.test.ts` | 22 | ACCEPTABLE | normalizeQuotes, applyEditToFile, preserveQuoteStyle | `findActualString` 断言过弱(`not.toBeNull`);`preserveQuoteStyle` 仅 2 用例 |
|
||||
| `src/tools/shared/__tests__/gitOperationTracking.test.ts` | 20 | ACCEPTABLE | parseGitCommitId, detectGitOperation | 6 个 GH PR action 全覆盖;缺 `trackGitOperations` 测试(需 mock analytics) |
|
||||
| `src/tools/BashTool/__tests__/destructiveCommandWarning.test.ts` | 21 | ACCEPTABLE | git/rm/SQL/k8s/terraform 危险模式 | safe commands 4 断言合一;缺少 `rm -rf /`、`DROP DATABASE`、管道命令 |
|
||||
| `src/tools/BashTool/__tests__/commandSemantics.test.ts` | 10 | ACCEPTABLE | grep/diff/test/rg/find 退出码语义 | mock `splitCommand_DEPRECATED` 与实现可能分歧;覆盖可更全面 |
|
||||
|
||||
**Utils 纯函数(19 文件):**
|
||||
|
||||
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|
||||
|------|-------|------|----------|----------|
|
||||
| `utils/__tests__/array.test.ts` | 12 | GOOD | intersperse, count, uniq | — |
|
||||
| `utils/__tests__/set.test.ts` | 11 | GOOD | difference, intersects, every, union | — |
|
||||
| `utils/__tests__/xml.test.ts` | 9 | GOOD | escapeXml, escapeXmlAttr | 缺 null/undefined 输入测试 |
|
||||
| `utils/__tests__/hash.test.ts` | 12 | ACCEPTABLE | djb2Hash, hashContent, hashPair | `hashContent`/`hashPair` 无已知答案断言(仅测确定性) |
|
||||
| `utils/__tests__/stringUtils.test.ts` | 30 | GOOD | 10 个函数全覆盖,含 Unicode 边界 | — |
|
||||
| `utils/__tests__/semver.test.ts` | 16 | ACCEPTABLE | gt/gte/lt/lte/satisfies/order | 缺 pre-release、tilde range、畸形版本串 |
|
||||
| `utils/__tests__/uuid.test.ts` | 6 | ACCEPTABLE | validateUuid | 大写测试仅 `not.toBeNull`,未验证标准化输出 |
|
||||
| `utils/__tests__/format.test.ts` | 27 | GOOD | formatFileSize, formatDuration, formatNumber, formatTokens, formatRelativeTime | 全部 `toBe` 精确匹配,含 billions/weeks/days 边界 |
|
||||
| `utils/__tests__/frontmatterParser.test.ts` | 22 | GOOD | parseFrontmatter, splitPathInFrontmatter, parsePositiveIntFromFrontmatter | — |
|
||||
| `utils/__tests__/file.test.ts` | 13 | ACCEPTABLE | convertLeadingTabsToSpaces, addLineNumbers, stripLineNumberPrefix | `addLineNumbers` 仅 `toContain`;缺 Windows 路径分隔符测试 |
|
||||
| `utils/__tests__/glob.test.ts` | 6 | ACCEPTABLE | extractGlobBaseDirectory | 缺绝对路径、根 `/`、Windows 路径 |
|
||||
| `utils/__tests__/diff.test.ts` | 8 | ACCEPTABLE | adjustHunkLineNumbers, getPatchFromContents | `getPatchFromContents` 仅检查结构,未验证 diff 内容正确性 |
|
||||
| `utils/__tests__/json.test.ts` | 15 | GOOD | safeParseJSON, parseJSONL, addItemToJSONCArray | — |
|
||||
| `utils/__tests__/truncate.test.ts` | 18 | ACCEPTABLE | truncateToWidth, wrapText, truncatePathMiddle | **缺 CJK/emoji/wide-char 测试**(这是宽度感知实现的核心场景) |
|
||||
| `utils/__tests__/path.test.ts` | 15 | ACCEPTABLE | containsPathTraversal, normalizePathForConfigKey | 仅覆盖 2/5+ 导出函数 |
|
||||
| `utils/__tests__/tokens.test.ts` | 18 | GOOD | getTokenCountFromUsage, doesMostRecentAssistantMessageExceed200k 等 | — |
|
||||
| `utils/__tests__/stream.test.ts` | 15 | GOOD | Stream\<T\> enqueue/read/drain/next/done/error/for-await | — |
|
||||
| `utils/__tests__/abortController.test.ts` | 13 | GOOD | createAbortController/createChildAbortController 父子传播 | — |
|
||||
| `utils/__tests__/bufferedWriter.test.ts` | 10 | GOOD | createBufferedWriter 立即/缓冲/flush/overflow | — |
|
||||
| `utils/__tests__/gitDiff.test.ts` | 25 | GOOD | parseGitNumstat/parseGitDiff/parseShortstat 纯解析 | — |
|
||||
| `utils/__tests__/sliceAnsi.test.ts` | 13 | GOOD | sliceAnsi ANSI 感知切片 + undoAnsiCodes | — |
|
||||
| `utils/__tests__/treeify.test.ts` | 13 | ACCEPTABLE | treeify 扁平/嵌套/循环引用 | 缺深度嵌套性能测试 |
|
||||
| `utils/__tests__/words.test.ts` | 11 | GOOD | slug 格式 (adjective-verb-noun)、唯一性 | — |
|
||||
|
||||
**Context 构建(3 文件):**
|
||||
|
||||
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|
||||
|------|-------|------|----------|----------|
|
||||
| `utils/__tests__/claudemd.test.ts` | 14 | ACCEPTABLE | stripHtmlComments, isMemoryFilePath, getLargeMemoryFiles | **仅测 3 个辅助函数**,核心发现/加载/`@include` 指令/memoization 未覆盖 |
|
||||
| `utils/__tests__/systemPrompt.test.ts` | 8 | GOOD | buildEffectiveSystemPrompt | — |
|
||||
| `__tests__/history.test.ts` | 26 | GOOD | parseReferences/expandPastedTextRefs/formatPastedTextRef 等 5 个函数 | — |
|
||||
|
||||
#### P1 — 重要模块
|
||||
|
||||
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|
||||
|------|-------|------|----------|----------|
|
||||
| `permissions/__tests__/permissionRuleParser.test.ts` | 16 | GOOD | escape/unescape 规则,roundtrip 完整性 | — |
|
||||
| `permissions/__tests__/permissions.test.ts` | 12 | ACCEPTABLE | getDenyRuleForTool, getAskRuleForTool, filterDeniedAgents | `as any` cast;缺 MCP tool deny 测试 |
|
||||
| `permissions/__tests__/shellRuleMatching.test.ts` | 19 | GOOD | 通配符、转义、正则特殊字符 | — |
|
||||
| `permissions/__tests__/PermissionMode.test.ts` | 22 | ACCEPTABLE | permissionModeFromString, isExternalPermissionMode 等 | isExternalPermissionMode ant false 路径已覆盖;缺 `bubble` 模式独立测试 |
|
||||
| `permissions/__tests__/dangerousPatterns.test.ts` | 7 | WEAK | CROSS_PLATFORM_CODE_EXEC, DANGEROUS_BASH_PATTERNS | 纯数据 smoke test,无行为测试;不验证数组无重复 |
|
||||
| `model/__tests__/aliases.test.ts` | 15 | ACCEPTABLE | isModelAlias, isModelFamilyAlias | 缺 null/undefined/空串输入 |
|
||||
| `model/__tests__/model.test.ts` | 13 | ACCEPTABLE | firstPartyNameToCanonical | 缺空串、非标准日期后缀 |
|
||||
| `model/__tests__/providers.test.ts` | 9 | ACCEPTABLE | getAPIProvider, isFirstPartyAnthropicBaseUrl | `originalEnv` 声明未使用;env 恢复不完整 |
|
||||
| `utils/__tests__/messages.test.ts` | 36 | GOOD | createAssistantMessage, createUserMessage, extractTag 等 16 个 describe | `normalizeMessages` 仅检查长度未验证内容 |
|
||||
|
||||
**Tool 子模块(8 文件):**
|
||||
|
||||
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|
||||
|------|-------|------|----------|----------|
|
||||
| `tools/PowerShellTool/__tests__/powershellSecurity.test.ts` | 24 | GOOD | AST 安全检测:Invoke-Expression/iex/encoded/dynamic/download/COM | — |
|
||||
| `tools/PowerShellTool/__tests__/commandSemantics.test.ts` | 21 | GOOD | grep/rg/findstr/robocopy 退出码、pipeline last-segment | — |
|
||||
| `tools/PowerShellTool/__tests__/destructiveCommandWarning.test.ts` | 38 | GOOD | Remove-Item/Format-Volume/Clear-Disk/git/SQL/COMPUTER/alias 全覆盖 | — |
|
||||
| `tools/PowerShellTool/__tests__/gitSafety.test.ts` | 29 | GOOD | .git 路径检测/NTFS 短名/反斜杠/引号/反引号转义 | — |
|
||||
| `tools/LSPTool/__tests__/formatters.test.ts` | 18 | GOOD | 全部 8 个 format 函数 null/empty/valid 输入 | — |
|
||||
| `tools/LSPTool/__tests__/schemas.test.ts` | 13 | GOOD | isValidLSPOperation 类型守卫 9 种操作 + 无效/空/大小写 | — |
|
||||
| `tools/WebFetchTool/__tests__/preapproved.test.ts` | 18 | GOOD | isPreapprovedHost 精确/路径作用域/子路径/大小写/子域名 | — |
|
||||
| `tools/WebFetchTool/__tests__/urlValidation.test.ts` | 18 | GOOD | validateURL/isPermittedRedirect 本地重实现(避免重依赖链) | — |
|
||||
|
||||
#### P2 — 补充模块
|
||||
|
||||
| 文件 | Tests | 评分 | 覆盖范围 | 主要不足 |
|
||||
|------|-------|------|----------|----------|
|
||||
| `utils/__tests__/cron.test.ts` | 31 | GOOD | parseCronExpression, computeNextCronRun, cronToHuman | 缺月边界、闰年 |
|
||||
| `utils/__tests__/git.test.ts` | 15 | ACCEPTABLE | normalizeGitRemoteUrl (SSH/HTTPS/ssh://) | 缺 git://、file://、端口号 |
|
||||
| `settings/__tests__/config.test.ts` | 38 | GOOD | SettingsSchema, type guards, validateSettingsFileContent, formatZodError | 缺 DeniedMcpServerEntrySchema |
|
||||
|
||||
#### P3-P6 — 扩展覆盖(27 文件)
|
||||
|
||||
| 文件 | Tests | 评分 | 备注 |
|
||||
|------|-------|------|------|
|
||||
| `utils/__tests__/errors.test.ts` | 33 | GOOD | — |
|
||||
| `utils/__tests__/envUtils.test.ts` | 33 | GOOD | env 保存/恢复规范 |
|
||||
| `utils/__tests__/effort.test.ts` | 30 | GOOD | 5 个 mock 模块,边界完整 |
|
||||
| `utils/__tests__/argumentSubstitution.test.ts` | 22 | ACCEPTABLE | 缺转义引号、越界索引 |
|
||||
| `utils/__tests__/sanitization.test.ts` | 14 | ACCEPTABLE | — |
|
||||
| `utils/__tests__/sleep.test.ts` | 14 | GOOD | 时间相关测试,margin 充足 |
|
||||
| `utils/__tests__/CircularBuffer.test.ts` | 11 | ACCEPTABLE | 缺 capacity=1、空 buffer getRecent |
|
||||
| `utils/__tests__/memoize.test.ts` | 18 | GOOD | 缓存 hit/stale/LRU 全覆盖 |
|
||||
| `utils/__tests__/tokenBudget.test.ts` | 21 | GOOD | — |
|
||||
| `utils/__tests__/displayTags.test.ts` | 17 | GOOD | — |
|
||||
| `utils/__tests__/taggedId.test.ts` | 10 | GOOD | — |
|
||||
| `utils/__tests__/controlMessageCompat.test.ts` | 15 | GOOD | — |
|
||||
| `utils/__tests__/gitConfigParser.test.ts` | 21 | GOOD | — |
|
||||
| `utils/__tests__/windowsPaths.test.ts` | 19 | GOOD | 双向 round-trip 测试 |
|
||||
| `utils/__tests__/envExpansion.test.ts` | 15 | GOOD | — |
|
||||
| `utils/__tests__/formatBriefTimestamp.test.ts` | 10 | GOOD | 固定 now 时间戳,确定性 |
|
||||
| `utils/__tests__/notebook.test.ts` | 9 | ACCEPTABLE | 合并断言偏弱 |
|
||||
| `utils/__tests__/hyperlink.test.ts` | 10 | ACCEPTABLE | 空串测试行为注释混乱 |
|
||||
| `utils/__tests__/zodToJsonSchema.test.ts` | 9 | WEAK | **object 属性仅 `toBeDefined` 未验证类型**;optional 字段未验证 absence |
|
||||
| `utils/__tests__/objectGroupBy.test.ts` | 5 | ACCEPTABLE | 极简,缺 undefined key 测试 |
|
||||
| `utils/__tests__/contentArray.test.ts` | 6 | ACCEPTABLE | 缺混合 tool_result+text 交替 |
|
||||
| `utils/__tests__/slashCommandParsing.test.ts` | 8 | GOOD | — |
|
||||
| `utils/__tests__/groupToolUses.test.ts` | 10 | GOOD | — |
|
||||
| `utils/__tests__/shell/__tests__/outputLimits.test.ts` | 7 | ACCEPTABLE | — |
|
||||
| `utils/__tests__/envValidation.test.ts` | 12 | GOOD | validateBoundedIntEnvVar | value=1 无下界确认为设计意图(函数仅校验 >0 和 <=upperLimit) |
|
||||
| `utils/git/__tests__/gitConfigParser.test.ts` | 20 | GOOD | — |
|
||||
| `services/mcp/__tests__/mcpStringUtils.test.ts` | 16 | GOOD | — |
|
||||
| `services/mcp/__tests__/normalization.test.ts` | 10 | GOOD | — |
|
||||
|
||||
### 4.3 评分汇总
|
||||
|
||||
| 等级 | 文件数 | 占比 |
|
||||
|------|--------|------|
|
||||
| **GOOD** | 46 | 55% |
|
||||
| **ACCEPTABLE** | 32 | 38% |
|
||||
| **WEAK** | 6 | 7% |
|
||||
|
||||
## 5. 系统性问题
|
||||
|
||||
### 5.1 断言过弱(Smell: `toContain` 代替精确匹配)
|
||||
|
||||
以下文件的部分测试使用 `toContain` 或 `not.toBeNull` 检查结果,当实现返回包含目标子串的任何字符串时测试仍通过,无法检测格式错误:
|
||||
|
||||
| 文件 | 受影响函数 | 建议 |
|
||||
|------|-----------|------|
|
||||
| `file.test.ts` | addLineNumbers | 断言完整输出格式 |
|
||||
| `diff.test.ts` | getPatchFromContents | 验证 hunk 内容正确性 |
|
||||
| `notebook.test.ts` | mapNotebookCellsToToolResult | 验证合并后内容 |
|
||||
| `uuid.test.ts` | validateUuid (uppercase) | 断言标准化后的精确值 |
|
||||
|
||||
### 5.2 集成测试空白
|
||||
|
||||
Spec 定义的三个集成测试均未创建:
|
||||
|
||||
| 计划 | 状态 | 依赖 |
|
||||
|------|------|------|
|
||||
| `tests/integration/tool-chain.test.ts` | 未创建 | 需 mock tools.ts 完整注册链 |
|
||||
| `tests/integration/context-build.test.ts` | 未创建 | 需 mock context.ts 重依赖链 |
|
||||
| `tests/integration/message-pipeline.test.ts` | 未创建 | 需 mock API 层 |
|
||||
|
||||
`tests/mocks/` 目录也不存在,无共享 mock/fixture 基础设施。
|
||||
|
||||
### 5.3 Mock 相关
|
||||
|
||||
| 问题 | 影响文件 | 说明 |
|
||||
|------|----------|------|
|
||||
| 未 mock 重依赖 | `gitOperationTracking.test.ts` | `trackGitOperations` 调用 analytics/bootstrap,测试仅覆盖 `detectGitOperation`(无副作用) |
|
||||
| env 恢复不完整 | `providers.test.ts` | 仅删除已知 key,新增 env var 会导致测试泄漏 |
|
||||
|
||||
### 5.4 潜在 Bug
|
||||
|
||||
| 文件 | 函数 | 问题 |
|
||||
|------|------|------|
|
||||
| ~~`envValidation.test.ts`~~ | ~~validateBoundedIntEnvVar~~ | ~~value=1 无下界检查~~ — **已确认**:函数仅校验 `parsed > 0` 和 `parsed <= upperLimit`,不强制 `parsed >= defaultValue`,为设计意图 |
|
||||
|
||||
### 5.5 已知限制
|
||||
|
||||
| 模块 | 问题 |
|
||||
|------|------|
|
||||
| `Bun.JSONL.parseChunk` | 畸形行时无限挂起(Bun 1.3.10 bug) |
|
||||
| `context.ts` 核心逻辑 | 依赖 bootstrap/state + git + 50+ 模块,mock 不可行 |
|
||||
| `tools.ts` (getAllBaseTools) | 导入链过重 |
|
||||
| `spawnMultiAgent.ts` | 50+ 依赖 |
|
||||
| `messages.ts` 部分函数 | 依赖 `getFeatureValue_CACHED_MAY_BE_STALE` |
|
||||
| UI 组件 (`screens/`, `components/`) | 需 Ink 渲染测试环境 |
|
||||
|
||||
### 5.6 Mock 模式
|
||||
|
||||
通过 `mock.module()` + `await import()` 解锁重依赖模块:
|
||||
|
||||
| 被 Mock 模块 | 解锁的测试 |
|
||||
|-------------|-----------|
|
||||
| `src/utils/log.ts` | json, tokens, FileEditTool/utils, permissions, memoize, PermissionMode |
|
||||
| `src/services/tokenEstimation.ts` | tokens |
|
||||
| `src/utils/slowOperations.ts` | tokens, permissions, memoize, PermissionMode |
|
||||
| `src/utils/debug.ts` | envValidation, outputLimits |
|
||||
| `src/utils/bash/commands.ts` | commandSemantics |
|
||||
| `src/utils/thinking.js` | effort |
|
||||
| `src/utils/settings/settings.js` | effort |
|
||||
| `src/utils/auth.js` | effort |
|
||||
| `src/services/analytics/growthbook.js` | effort, tokenBudget |
|
||||
| `src/utils/powershell/dangerousCmdlets.js` | powershellSecurity |
|
||||
| `src/utils/cwd.js` | gitSafety |
|
||||
| `src/utils/powershell/parser.js` | gitSafety |
|
||||
| `src/utils/stringUtils.js` | LSP formatters |
|
||||
| `figures` | treeify |
|
||||
|
||||
**约束**:`mock.module()` 必须在每个测试文件内联调用,不能从共享 helper 导入。
|
||||
|
||||
## 6. 完成状态
|
||||
|
||||
> 更新日期:2026-04-02 | **1623 tests, 84 files, 0 fail, 851ms**
|
||||
|
||||
### 已完成
|
||||
|
||||
| 计划 | 状态 | 新增测试 | 说明 |
|
||||
|------|------|---------|------|
|
||||
| Plan 12 — Mock 可靠性 | **已完成** | +9 | PermissionMode ant false 路径、providers env 快照恢复 |
|
||||
| Plan 10 — WEAK 修复 | **已完成** | +15 | format 断言精确化、envValidation 修正、zodToJsonSchema/destructors/notebook 加固 |
|
||||
| Plan 13 — CJK/Emoji | **已完成** | +17 | truncate CJK/emoji 宽度感知测试 |
|
||||
| Plan 11 — ACCEPTABLE 加强 | **已完成** | +62 | diff/uuid/hash/semver/path/claudemd/fileEdit/providers/messages 等 15 文件 |
|
||||
| Plan 14 — 集成测试 | **已完成** | +43 | 搭建 tests/mocks/ + tool-chain/context-build/message-pipeline/cli-arguments |
|
||||
| Plan 15 — CLI + 覆盖率 | **已完成** | +11 | Commander.js 参数解析、覆盖率基线 |
|
||||
| Phase 16 — 零依赖纯函数 | **已完成** | +126 | stream/abortController/bufferedWriter/gitDiff/history/sliceAnsi/treeify/words 8 文件 |
|
||||
| Phase 17 — 工具子模块 | **已完成** | +179 | PowerShell 安全/语义/破坏性/gitSafety + LSP 格式化/schema + WebFetch 预批准/URL 8 文件 |
|
||||
| Phase 18 — WEAK 修复 | **已完成** | +20 | format 精确匹配、envValidation 边界、PermissionMode 补强、gitOperationTracking PR actions |
|
||||
|
||||
### 覆盖率基线
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 总测试数 | 1623 |
|
||||
| 测试文件数 | 84 |
|
||||
| 失败数 | 0 |
|
||||
| 断言数 | 2516 |
|
||||
| 运行耗时 | ~851ms |
|
||||
| Tool.ts 行覆盖率 | 100% |
|
||||
| 整体行覆盖率 | ~33%(Bun coverage 限制:`mock.module` 模式下的模块不报告) |
|
||||
|
||||
> **注意**:Bun `--coverage` 仅报告测试 import 链中直接加载的文件。使用 `mock.module()` + `await import()` 模式的源文件(大多数 `src/utils/` 纯函数)不显示在覆盖率报告中。实际测试覆盖率高于报告值。
|
||||
|
||||
### 不纳入计划
|
||||
|
||||
| 模块 | 原因 |
|
||||
|------|------|
|
||||
| `query.ts` / `QueryEngine.ts` | 核心循环,需完整集成环境 |
|
||||
| `services/api/claude.ts` | 需 mock SDK 流式响应 |
|
||||
| `spawnMultiAgent.ts` | 50+ 依赖 |
|
||||
| `modelCost.ts` | 依赖 bootstrap/state + analytics |
|
||||
| `mcp/dateTimeParser.ts` | 调用 Haiku API |
|
||||
| `screens/` / `components/` | 需 Ink 渲染测试 |
|
||||
@@ -139,137 +139,11 @@ function getDeferredToolsCacheKey(deferredTools: Tools): string {
|
||||
|
||||
AI 的信息获取不局限于本地代码:
|
||||
|
||||
- **WebSearch**(`src/tools/WebSearchTool/`):调用 Anthropic API 的 `web_search_20250305` server tool 搜索互联网
|
||||
- **WebFetch**(`src/tools/WebFetchTool/`):抓取特定 URL 内容,转换为 Markdown 供 AI 阅读
|
||||
- **WebSearch**:搜索互联网获取最新信息
|
||||
- **WebFetch**:抓取特定网页内容,转换为 Markdown 供 AI 阅读
|
||||
|
||||
这让 AI 可以查阅文档、搜索 Stack Overflow、阅读 GitHub issue——和人类开发者的工作方式一致。
|
||||
|
||||
### WebSearch 实现机制
|
||||
|
||||
WebSearch 通过适配器模式支持两种搜索后端,由 `src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
|
||||
|
||||
```
|
||||
适配器架构:
|
||||
WebSearchTool.call()
|
||||
→ createAdapter() 选择后端
|
||||
├─ ApiSearchAdapter — Anthropic API 服务端搜索(需官方 API 密钥)
|
||||
└─ BingSearchAdapter — 直接抓取 Bing 搜索页面解析(无需 API 密钥)
|
||||
→ adapter.search(query, options)
|
||||
→ 转换为统一 SearchResult[] 格式返回
|
||||
```
|
||||
|
||||
#### 适配器选择逻辑
|
||||
|
||||
`adapters/index.ts` 中的工厂函数按以下优先级选择后端:
|
||||
|
||||
| 优先级 | 条件 | 适配器 |
|
||||
|--------|------|--------|
|
||||
| 1 | 环境变量 `WEB_SEARCH_ADAPTER=api` | `ApiSearchAdapter` |
|
||||
| 2 | 环境变量 `WEB_SEARCH_ADAPTER=bing` | `BingSearchAdapter` |
|
||||
| 3 | API Base URL 指向 Anthropic 官方 | `ApiSearchAdapter` |
|
||||
| 4 | 第三方代理 / 非官方端点 | `BingSearchAdapter` |
|
||||
|
||||
适配器是无状态的,同一会话内缓存复用。
|
||||
|
||||
#### ApiSearchAdapter — API 服务端搜索
|
||||
|
||||
将搜索请求委托给 Anthropic API 的 `web_search_20250305` server tool:
|
||||
|
||||
```
|
||||
调用链:
|
||||
ApiSearchAdapter.search(query, options)
|
||||
→ queryModelWithStreaming() 发起独立的 API 调用
|
||||
→ 携带 extraToolSchemas: [BetaWebSearchTool20250305]
|
||||
→ API 服务端执行搜索,返回流式事件
|
||||
→ server_tool_use / web_search_tool_result / text 交替返回
|
||||
→ extractSearchResults() 从 content blocks 提取 SearchResult[]
|
||||
```
|
||||
|
||||
| 特性 | 实现 |
|
||||
|------|------|
|
||||
| **模型选择** | Feature flag `tengu_plum_vx3` 控制用 Haiku(强制 tool_choice)还是主模型 |
|
||||
| **搜索上限** | 每次调用最多 8 次搜索(`max_uses: 8`) |
|
||||
| **域过滤** | 支持 `allowedDomains` / `blockedDomains` |
|
||||
| **进度追踪** | 流式解析 `input_json_delta` 提取 query,实时回调 `onProgress` |
|
||||
|
||||
#### BingSearchAdapter — Bing 搜索页面解析
|
||||
|
||||
直接抓取 Bing 搜索 HTML 并用正则提取结果,无需 API 密钥:
|
||||
|
||||
```
|
||||
调用链:
|
||||
BingSearchAdapter.search(query, options)
|
||||
→ axios.get(bing.com/search?q=...) — 使用浏览器级别 headers 绕过反爬
|
||||
→ extractBingResults(html)
|
||||
→ 正则匹配 <li class="b_algo"> 块
|
||||
→ 提取 <h2><a> 标题和 URL
|
||||
→ resolveBingUrl() 解码 Bing 重定向链接
|
||||
→ extractSnippet() 三级降级提取摘要
|
||||
→ 客户端域过滤 (allowedDomains / blockedDomains)
|
||||
→ 返回 SearchResult[]
|
||||
```
|
||||
|
||||
**反爬策略**:Bing 对非浏览器 UA 返回需要 JS 渲染的空页面。适配器使用完整的 Edge 浏览器请求头(包含 `Sec-Ch-Ua`、`Sec-Fetch-*` 等现代浏览器标头)确保获得完整 HTML。同时使用 `setmkt=en-US` 参数统一市场定位,避免 Bing 基于用户 IP 做区域化定向(如跳转到德语/新加坡市场导致结果不相关)。
|
||||
|
||||
**URL 解码**:Bing 搜索结果中的 URL 为重定向格式(`bing.com/ck/a?...&u=a1aHR0cHM6Ly9...`),`resolveBingUrl()` 从 `u` 参数中 base64 解码出真实目标 URL(`a1` 前缀 = https,`a0` = http)。
|
||||
|
||||
**摘要提取**(`extractSnippet()`)按优先级尝试三个来源:
|
||||
1. `<p class="b_lineclamp...">` — 带行截断的摘要段落
|
||||
2. `<div class="b_caption">` 内的 `<p>` — 普通摘要段落
|
||||
3. `<div class="b_caption">` 的直接文本内容 — 兜底方案
|
||||
|
||||
| 特性 | 实现 |
|
||||
|------|------|
|
||||
| **超时** | 30 秒(`FETCH_TIMEOUT_MS`) |
|
||||
| **域过滤** | 支持 `allowedDomains` / `blockedDomains`,含子域名匹配 |
|
||||
| **进度追踪** | 发送 query_update 和 search_results_received 回调 |
|
||||
| **中止支持** | 外部 AbortSignal 传播到 axios 请求 |
|
||||
|
||||
### WebSearchTool 统一接口
|
||||
|
||||
`WebSearchTool`(`src/tools/WebSearchTool/WebSearchTool.ts`)是面向主循环的工具定义,所有 provider 均可使用(`isEnabled()` 始终返回 true)。它将适配器返回的 `SearchResult[]` 转换为内部 `Output` 格式,`mapToolResultToToolResultBlockParam` 将搜索结果格式化为带 markdown 超链接的文本,并附加 "REMINDER" 要求主模型在回复中包含 Sources。
|
||||
|
||||
### WebFetch 实现机制
|
||||
|
||||
WebFetch 是一个完整的 HTTP 客户端 + 内容处理管线:
|
||||
|
||||
```
|
||||
调用链:
|
||||
WebFetchTool.call({ url, prompt })
|
||||
→ getURLMarkdownContent(url)
|
||||
→ validateURL() — 长度≤2000、无用户名密码、公网域名
|
||||
→ URL_CACHE 命中检查(15 分钟 TTL LRU,50MB 上限)
|
||||
→ checkDomainBlocklist() — 调用 api.anthropic.com/api/web/domain_info 预检
|
||||
→ getWithPermittedRedirects() — axios 请求,自定义重定向处理
|
||||
→ HTML → Turndown 转 Markdown(懒加载单例,~1.4MB)
|
||||
→ 非 HTML → 原始文本
|
||||
→ 二进制(PDF 等)→ persistBinaryContent() 保存到磁盘
|
||||
→ applyPromptToMarkdown()
|
||||
→ 截断到 100K 字符
|
||||
→ queryHaiku() 用小模型按 prompt 提取信息
|
||||
→ 返回处理后的结果
|
||||
```
|
||||
|
||||
安全防护多层设计:
|
||||
|
||||
| 层级 | 机制 | 说明 |
|
||||
|------|------|------|
|
||||
| **域名预检** | `checkDomainBlocklist()` | 调用 `api.anthropic.com/api/web/domain_info?domain=…`,5 分钟缓存 |
|
||||
| **重定向控制** | `isPermittedRedirect()` | 仅允许同 host(±www)重定向,跨域重定向返回提示让 AI 重新调用 |
|
||||
| **重定向深度** | `MAX_REDIRECTS = 10` | 防止重定向循环无限挂起 |
|
||||
| **内容大小** | `MAX_HTTP_CONTENT_LENGTH = 10MB` | 单次响应上限 |
|
||||
| **请求超时** | `FETCH_TIMEOUT_MS = 60s` | 主请求超时;域名预检 10s |
|
||||
| **URL 验证** | `validateURL()` | 长度、协议、用户名密码、公网域名检查 |
|
||||
| **egress 检测** | `X-Proxy-Error: blocked-by-allowlist` | 检测企业代理拦截 |
|
||||
|
||||
预批准域名(`src/tools/WebFetchTool/preapproved.ts`):
|
||||
|
||||
用户无需手动授权即可抓取的域名列表,包含 ~90 个主流技术文档站点(MDN、Python docs、React docs、AWS docs 等)。列表分为 hostname-only 和 path-prefix 两类,查找复杂度 O(1)。
|
||||
|
||||
对预批准域名,WebFetch 跳过 Haiku 摘要步骤(如果内容是 Markdown 且 < 100K 字符),直接返回原文——因为技术文档本身的结构化程度已经足够好。
|
||||
|
||||
权限模型方面,WebFetch 按 hostname 生成 `domain:xxx` 规则匹配用户的 allow/deny/ask 规则,支持用户对特定域名配置永久允许或拒绝。
|
||||
|
||||
### ripgrep 的流式输出
|
||||
|
||||
对于交互式场景(如 QuickOpen),ripgrep 支持**流式输出**(`ripGrepStream()`):
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
# ULTRAPLAN(增强规划)实现分析
|
||||
|
||||
> 生成日期:2026-04-02
|
||||
> Feature Flag:`FEATURE_ULTRAPLAN=1`
|
||||
> 引用数:10(跨 8 个文件)
|
||||
|
||||
---
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
ULTRAPLAN 是一个**远程增强规划**功能,将用户的规划请求发送到 Claude Code on the Web(CCR,云端容器)执行。使用 Opus 模型在云端生成高级计划,用户可以在浏览器中编辑和审批,然后选择在云端继续执行或将计划"传送"回本地终端执行。
|
||||
|
||||
**核心卖点**:
|
||||
- 终端不被阻塞 — 远程在云端规划,本地可继续工作
|
||||
- 使用最强大的模型(Opus)
|
||||
- 用户可在浏览器中实时查看和编辑计划
|
||||
- 支持多轮迭代(云端可追问,用户在浏览器回复)
|
||||
|
||||
---
|
||||
|
||||
## 二、架构总览
|
||||
|
||||
```
|
||||
用户输入 "ultraplan xxx"
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────┐
|
||||
│ 关键字检测层 (keyword.ts) │ 识别 "ultraplan" 关键字
|
||||
│ + 输入处理层 (processUserInput) │ 重写为 /ultraplan 命令
|
||||
└───────────┬─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────┐
|
||||
│ 命令处理层 (ultraplan.tsx) │ launchUltraplan()
|
||||
│ - 前置校验(资格、防重入) │ → launchDetached()
|
||||
│ - 构建提示词 │ buildUltraplanPrompt()
|
||||
└───────────┬─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────┐
|
||||
│ 远程会话层 │ teleportToRemote()
|
||||
│ - 创建 CCR 云端会话 │ permissionMode: 'plan'
|
||||
│ - 设置 plan 权限模式 │ model: Opus
|
||||
└───────────┬─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────┐
|
||||
│ 轮询层 (ccrSession.ts) │ pollForApprovedExitPlanMode()
|
||||
│ - ExitPlanModeScanner │ 每 3 秒轮询事件流
|
||||
│ - 状态机: running → needs_input │ 超时: 30 分钟
|
||||
│ → plan_ready │
|
||||
└───────────┬─────────────────────┘
|
||||
│
|
||||
┌─────┴─────┐
|
||||
▼ ▼
|
||||
approved teleport
|
||||
(云端执行) (传送回本地)
|
||||
│ │
|
||||
│ ▼
|
||||
│ UltraplanChoiceDialog
|
||||
│ 用户选择执行方式
|
||||
▼ ▼
|
||||
完成通知 本地执行计划
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、模块详解
|
||||
|
||||
### 3.1 关键字检测 — `src/utils/ultraplan/keyword.ts`
|
||||
|
||||
负责检测用户输入中的 "ultraplan" 关键字。检测逻辑相当精细,避免误触发:
|
||||
|
||||
**触发条件**:输入中包含独立的 `ultraplan` 单词(大小写不敏感)。
|
||||
|
||||
**不触发的场景**:
|
||||
- 在引号/括号内:`` `ultraplan` ``、`"ultraplan"`、`[ultraplan]`、`{ultraplan}`
|
||||
- 路径/标识符上下文:`src/ultraplan/foo.ts`、`ultraplan.tsx`、`--ultraplan-mode`
|
||||
- 问句:`ultraplan?`
|
||||
- 斜杠命令内:`/rename ultraplan foo`
|
||||
- 已有 ultraplan 会话运行中或正在启动时
|
||||
|
||||
**关键字替换**:触发后将 `ultraplan` 替换为 `plan`,保持语法通顺(如 "please ultraplan this" → "please plan this")。
|
||||
|
||||
```typescript
|
||||
// 核心导出函数
|
||||
findUltraplanTriggerPositions(text) // 返回触发位置数组
|
||||
hasUltraplanKeyword(text) // 布尔判断
|
||||
replaceUltraplanKeyword(text) // 替换第一个触发词为 "plan"
|
||||
```
|
||||
|
||||
### 3.2 命令注册 — `src/commands.ts`
|
||||
|
||||
```typescript
|
||||
const ultraplan = feature('ULTRAPLAN')
|
||||
? require('./commands/ultraplan.js').default
|
||||
: null
|
||||
```
|
||||
|
||||
命令仅在 `FEATURE_ULTRAPLAN=1` 时加载。命令定义:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: 'local-jsx',
|
||||
name: 'ultraplan',
|
||||
description: '~10–30 min · Claude Code on the web drafts an advanced plan...',
|
||||
argumentHint: '<prompt>',
|
||||
isEnabled: () => process.env.USER_TYPE === 'ant', // 仅 ant 用户可用
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:`isEnabled` 检查 `USER_TYPE === 'ant'`(Anthropic 内部用户),这是命令级限制。关键字触发路径没有此限制,只要 feature flag 开启即可。
|
||||
|
||||
### 3.3 核心命令实现 — `src/commands/ultraplan.tsx`
|
||||
|
||||
#### 3.3.1 入口函数 `call()`
|
||||
|
||||
处理 `/ultraplan <prompt>` 斜杠命令:
|
||||
|
||||
1. **无参数调用**:显示使用帮助文本
|
||||
2. **已有活跃会话**:返回 "already polling" 提示
|
||||
3. **正常调用**:设置 `ultraplanLaunchPending` 状态,触发 `UltraplanLaunchDialog` 对话框
|
||||
|
||||
#### 3.3.2 `launchUltraplan()`
|
||||
|
||||
公共启动入口,被三个路径共享:
|
||||
- 斜杠命令 (`/ultraplan`)
|
||||
- 关键字触发 (`processUserInput.ts`)
|
||||
- Plan 审批对话框的 "Ultraplan" 按钮 (`ExitPlanModePermissionRequest`)
|
||||
|
||||
关键逻辑:
|
||||
1. 防重入检查(`ultraplanSessionUrl` / `ultraplanLaunching`)
|
||||
2. 同步设置 `ultraplanLaunching = true` 防止竞态
|
||||
3. 异步调用 `launchDetached()`
|
||||
4. 立即返回启动消息(不等远程会话创建)
|
||||
|
||||
#### 3.3.3 `launchDetached()`
|
||||
|
||||
异步后台流程:
|
||||
|
||||
1. **获取模型**:从 GrowthBook 读取 `tengu_ultraplan_model`,默认 `opus46` 的 firstParty ID
|
||||
2. **资格检查**:`checkRemoteAgentEligibility()` — 验证用户是否有权限使用远程 agent
|
||||
3. **构建提示词**:`buildUltraplanPrompt(blurb, seedPlan)`
|
||||
- 如有 `seedPlan`(来自 plan 审批对话框),作为草稿前缀
|
||||
- 加载 `prompt.txt` 中的指令模板
|
||||
- 附加用户 blurb
|
||||
4. **创建远程会话**:`teleportToRemote()`
|
||||
- `permissionMode: 'plan'` — 远程以 plan 模式运行
|
||||
- `ultraplan: true` — 标记为 ultraplan 会话
|
||||
- `useDefaultEnvironment: true` — 使用默认云端环境
|
||||
5. **注册任务**:`registerRemoteAgentTask()` 创建 `RemoteAgentTask` 追踪条目
|
||||
6. **启动轮询**:`startDetachedPoll()` 后台轮询审批状态
|
||||
|
||||
#### 3.3.4 提示词构建
|
||||
|
||||
```
|
||||
buildUltraplanPrompt(blurb, seedPlan?)
|
||||
```
|
||||
|
||||
- `prompt.txt`:当前为空文件(反编译丢失),原始内容应包含指导远程 agent 生成计划的系统指令
|
||||
- 开发者可通过 `ULTRAPLAN_PROMPT_FILE` 环境变量覆盖提示词文件(仅 `USER_TYPE=ant` 时生效)
|
||||
|
||||
#### 3.3.5 `startDetachedPoll()`
|
||||
|
||||
后台轮询管理:
|
||||
|
||||
1. 调用 `pollForApprovedExitPlanMode()` 等待计划审批
|
||||
2. 阶段变化时更新 `RemoteAgentTask.ultraplanPhase`(UI 展示)
|
||||
3. 审批完成后的两种路径:
|
||||
- **`executionTarget: 'remote'`**:用户选择在云端执行
|
||||
- 标记任务完成
|
||||
- 清除 `ultraplanSessionUrl`
|
||||
- 发送通知:结果将以 PR 形式提交
|
||||
- **`executionTarget: 'local'`**:用户选择传送回本地(teleport)
|
||||
- 设置 `ultraplanPendingChoice`
|
||||
- 触发 `UltraplanChoiceDialog` 对话框
|
||||
4. 失败时:归档远程会话、清除状态、发送错误通知
|
||||
|
||||
#### 3.3.6 `stopUltraplan()`
|
||||
|
||||
用户主动停止:
|
||||
|
||||
1. `RemoteAgentTask.kill()` 归档远程会话
|
||||
2. 清除所有 ultraplan 状态(`ultraplanSessionUrl`、`ultraplanPendingChoice`、`ultraplanLaunching`)
|
||||
3. 发送停止通知
|
||||
|
||||
### 3.4 CCR 会话轮询 — `src/utils/ultraplan/ccrSession.ts`
|
||||
|
||||
#### 3.4.1 `ExitPlanModeScanner`
|
||||
|
||||
纯状态机,无 I/O。摄入 `SDKMessage[]` 事件批次,分类 `ExitPlanMode` 工具调用的结果。
|
||||
|
||||
**状态类型**:
|
||||
|
||||
```typescript
|
||||
type ScanResult =
|
||||
| { kind: 'approved' } // 用户批准了计划
|
||||
| { kind: 'teleport' } // 用户点击"传送回本地"
|
||||
| { kind: 'rejected' } // 用户拒绝(可继续迭代)
|
||||
| { kind: 'pending' } // 等待用户审批中
|
||||
| { kind: 'terminated' } // 远程会话意外终止
|
||||
| { kind: 'unchanged' } // 无新事件,状态不变
|
||||
```
|
||||
|
||||
**优先级**:approved > terminated > rejected > pending > unchanged
|
||||
|
||||
**关键设计**:
|
||||
- 同一批事件可能包含审批和后续崩溃 — 不丢弃已审批的计划
|
||||
- 拒绝后重新扫描(`rescanAfterRejection`),因为新事件可能包含修改后的计划
|
||||
- 使用 `is_error: true` 判断拒绝,`content` 中查找标记提取计划文本
|
||||
|
||||
#### 3.4.2 `pollForApprovedExitPlanMode()`
|
||||
|
||||
轮询主循环:
|
||||
|
||||
- **轮询间隔**:3 秒
|
||||
- **超时**:30 分钟
|
||||
- **容错**:连续 5 次网络错误才放弃
|
||||
- **阶段推断**:
|
||||
- `hasPendingPlan`(有 ExitPlanMode 无结果)→ `plan_ready`
|
||||
- `quietIdle`(空闲且无新事件)→ `needs_input`(远程在等用户输入)
|
||||
- 其他 → `running`
|
||||
|
||||
#### 3.4.3 计划文本提取
|
||||
|
||||
两种提取路径:
|
||||
|
||||
1. **Approved**:从 `tool_result` 中查找 `## Approved Plan:\n` 或 `## Approved Plan (edited by user):\n` 标记
|
||||
2. **Teleport**:从 `tool_result` 中查找 `__ULTRAPLAN_TELEPORT_SENTINEL__` 标记(浏览器端嵌入)
|
||||
|
||||
### 3.5 输入处理集成 — `src/utils/processUserInput/processUserInput.ts`
|
||||
|
||||
关键字触发路径(在斜杠命令处理之前):
|
||||
|
||||
```typescript
|
||||
if (feature('ULTRAPLAN') &&
|
||||
mode === 'prompt' && // 非非交互模式
|
||||
!isNonInteractiveSession && // 非后台会话
|
||||
inputString !== null &&
|
||||
!inputString.startsWith('/') && // 非斜杠命令
|
||||
!ultraplanSessionUrl && // 无活跃会话
|
||||
!ultraplanLaunching && // 非正在启动
|
||||
hasUltraplanKeyword(inputString)) {
|
||||
// 重写为 /ultraplan 命令
|
||||
const rewritten = replaceUltraplanKeyword(inputString).trim()
|
||||
await processSlashCommand(`/ultraplan ${rewritten}`, ...)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 UI 层
|
||||
|
||||
#### 3.6.1 彩虹高亮 — `src/components/PromptInput/PromptInput.tsx`
|
||||
|
||||
当输入中检测到 `ultraplan` 关键字时:
|
||||
- 对每个字符施加**彩虹渐变色**高亮(`getRainbowColor()`)
|
||||
- 显示通知:"This prompt will launch an ultraplan session in Claude Code on the web"
|
||||
|
||||
#### 3.6.2 预启动对话框 — `UltraplanLaunchDialog`
|
||||
|
||||
在 REPL 的 `focusedInputDialog === 'ultraplan-launch'` 时渲染。
|
||||
|
||||
用户选择:
|
||||
- **确认**:调用 `launchUltraplan()`,先添加命令回显,异步启动远程会话
|
||||
- **取消**:清除 `ultraplanLaunchPending` 状态
|
||||
|
||||
#### 3.6.3 计划选择对话框 — `UltraplanChoiceDialog`
|
||||
|
||||
在 `focusedInputDialog === 'ultraplan-choice'` 时渲染。
|
||||
|
||||
当 teleport 路径返回已审批计划时,用户可选择执行方式。
|
||||
|
||||
#### 3.6.4 Plan 审批按钮 — `ExitPlanModePermissionRequest`
|
||||
|
||||
本地 Plan Mode 的审批对话框中,如果 `feature('ULTRAPLAN')` 开启,会显示额外的 "Ultraplan" 按钮:
|
||||
- 将当前本地计划作为 `seedPlan` 发送给远程
|
||||
- 按钮仅在无活跃 ultraplan 会话时显示
|
||||
|
||||
### 3.7 应用状态 — `src/state/AppStateStore.ts`
|
||||
|
||||
```typescript
|
||||
interface AppState {
|
||||
ultraplanLaunching?: boolean // 防重入锁(5 秒窗口)
|
||||
ultraplanSessionUrl?: string // 活跃远程会话 URL
|
||||
ultraplanPendingChoice?: { // 已审批计划等待选择
|
||||
plan: string
|
||||
sessionId: string
|
||||
taskId: string
|
||||
}
|
||||
ultraplanLaunchPending?: { // 预启动对话框
|
||||
blurb: string
|
||||
}
|
||||
isUltraplanMode?: boolean // 远程端:CCR 侧的 ultraplan 标记
|
||||
}
|
||||
```
|
||||
|
||||
### 3.8 远程任务追踪 — `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx`
|
||||
|
||||
Ultraplan 使用 `RemoteAgentTask` 基础设施追踪远程会话:
|
||||
|
||||
```typescript
|
||||
registerRemoteAgentTask({
|
||||
remoteTaskType: 'ultraplan',
|
||||
session: { id, title },
|
||||
command: blurb,
|
||||
isUltraplan: true // 特殊标记,跳过通用轮询逻辑
|
||||
})
|
||||
```
|
||||
|
||||
`extractPlanFromLog()` 从 `<ultraplan>...</ultraplan>` XML 标签中提取计划内容。
|
||||
|
||||
---
|
||||
|
||||
## 四、数据流时序
|
||||
|
||||
```
|
||||
时间线 →
|
||||
|
||||
用户 本地 CLI CCR 云端
|
||||
│ │ │
|
||||
│ "ultraplan xxx" │ │
|
||||
│──────────────────────>│ │
|
||||
│ │ keyword 检测 + 重写 │
|
||||
│ │ /ultraplan "plan xxx" │
|
||||
│ │ │
|
||||
│ [UltraplanLaunch │ │
|
||||
│ Dialog] │ │
|
||||
│──── confirm ─────────>│ │
|
||||
│ │ launchDetached() │
|
||||
│ │─────────────────────────────>│
|
||||
│ │ teleportToRemote() │
|
||||
│ │ (permissionMode: 'plan') │
|
||||
│ │ │
|
||||
│ "Starting..." │ │
|
||||
│<──────────────────────│ │
|
||||
│ │ │
|
||||
│ (终端空闲,可继续) │ startDetachedPoll() │
|
||||
│ │ ═══ 3s 轮询循环 ═══ │
|
||||
│ │ │
|
||||
│ │ [浏览器打开]│
|
||||
│ │ [云端生成计划]
|
||||
│ │ │
|
||||
│ │ ← needs_input ─────────────│
|
||||
│ │ (云端追问用户) │
|
||||
│ │ │
|
||||
│ │ [用户在浏览器回复]
|
||||
│ │ │
|
||||
│ │ ← plan_ready ──────────────│
|
||||
│ │ (ExitPlanMode 等待审批) │
|
||||
│ │ │
|
||||
│ │ [用户审批/编辑]
|
||||
│ │ │
|
||||
│ ┌───────┤ ← approved ────────────────│
|
||||
│ │ │ │
|
||||
│ [远程执行] │ │ │
|
||||
│ 通知完成 │ │ │
|
||||
│ │ │ │
|
||||
│ └── OR ─┤ ← teleport ───────────────│
|
||||
│ │ │
|
||||
│ [UltraplanChoice │ │
|
||||
│ Dialog] │ │
|
||||
│── 选择执行方式 ───────>│ │
|
||||
│ │ 本地执行计划 │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、关键文件清单
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/utils/ultraplan/keyword.ts` | 关键字检测、高亮位置计算、关键字替换 |
|
||||
| `src/utils/ultraplan/ccrSession.ts` | CCR 会话轮询、ExitPlanMode 状态机、计划文本提取 |
|
||||
| `src/utils/ultraplan/prompt.txt` | 远程指令模板(当前为空,需重建) |
|
||||
| `src/commands/ultraplan.tsx` | `/ultraplan` 命令、启动/停止逻辑、提示词构建 |
|
||||
| `src/utils/processUserInput/processUserInput.ts` | 关键字触发 → `/ultraplan` 命令路由 |
|
||||
| `src/components/PromptInput/PromptInput.tsx` | 彩虹高亮 + 通知提示 |
|
||||
| `src/screens/REPL.tsx` | 对话框渲染(UltraplanLaunchDialog / UltraplanChoiceDialog) |
|
||||
| `src/components/permissions/ExitPlanModePermissionRequest/` | Plan 审批中的 "Ultraplan" 按钮 |
|
||||
| `src/state/AppStateStore.ts` | ultraplan 相关状态字段定义 |
|
||||
| `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx` | 远程任务追踪 + `<ultraplan>` 标签提取 |
|
||||
| `src/constants/xml.ts` | `ULTRAPLAN_TAG = 'ultraplan'` |
|
||||
|
||||
---
|
||||
|
||||
## 六、依赖关系
|
||||
|
||||
### 外部依赖
|
||||
|
||||
| 依赖 | 用途 | 必要性 |
|
||||
|------|------|--------|
|
||||
| `teleportToRemote()` | 创建 CCR 云端会话 | 必须 — 核心功能 |
|
||||
| `checkRemoteAgentEligibility()` | 验证用户远程 agent 使用资格 | 必须 — 前置检查 |
|
||||
| `archiveRemoteSession()` | 归档/终止远程会话 | 必须 — 清理 |
|
||||
| GrowthBook `tengu_ultraplan_model` | 获取使用的模型 ID | 可选 — 默认 opus46 |
|
||||
| `@anthropic-ai/sdk` | SDKMessage 类型 | 必须 — 类型定义 |
|
||||
| `pollRemoteSessionEvents()` | 事件流分页轮询 | 必须 — 轮询基础设施 |
|
||||
|
||||
### 内部依赖
|
||||
|
||||
- **ExitPlanModeV2Tool**:远程端调用的工具,触发 plan 审批流程
|
||||
- **RemoteAgentTask**:任务追踪和状态管理基础设施
|
||||
- **AppState Store**:ultraplan 状态管理
|
||||
|
||||
---
|
||||
|
||||
## 七、当前状态与补全要点
|
||||
|
||||
| 组件 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 关键字检测 | ✅ 完整 | `keyword.ts` 逻辑完善 |
|
||||
| 命令框架 | ✅ 完整 | 注册、路由、防重入完整 |
|
||||
| 启动流程 | ✅ 完整 | `launchUltraplan` / `launchDetached` 完整 |
|
||||
| CCR 轮询 | ✅ 完整 | `ccrSession.ts` 状态机完整 |
|
||||
| UI 高亮/通知 | ✅ 完整 | 彩虹高亮 + 提示通知完整 |
|
||||
| 状态管理 | ✅ 完整 | AppState 字段完整 |
|
||||
| `prompt.txt` | ❌ 空文件 | 需要重建远程指令模板 |
|
||||
| `UltraplanLaunchDialog` | ⚠️ 全局声明 | 组件实现未找到(可能在内置包中) |
|
||||
| `UltraplanChoiceDialog` | ⚠️ 全局声明 | 组件实现未找到(可能在内置包中) |
|
||||
| `isEnabled` 限制 | ⚠️ `USER_TYPE === 'ant'` | 命令级限制,仅 Anthropic 内部用户 |
|
||||
|
||||
### 补全建议
|
||||
|
||||
1. **重建 `prompt.txt`**:这是远程 agent 的核心指令,定义如何进行多 agent 探索式规划。需要设计:
|
||||
- 规划方法论(多角度分析、风险评估、分阶段执行)
|
||||
- ExitPlanMode 工具的使用引导
|
||||
- 输出格式要求
|
||||
|
||||
2. **对话框组件**:`UltraplanLaunchDialog` 和 `UltraplanChoiceDialog` 在 `global.d.ts` 中声明但实现缺失,需要新建:
|
||||
- Launch Dialog:确认对话框(含 CCR 使用条款链接)
|
||||
- Choice Dialog:展示已审批计划 + 执行方式选择
|
||||
|
||||
3. **放宽 `isEnabled`**:如果要让非 ant 用户使用斜杠命令,需移除 `USER_TYPE === 'ant'` 检查
|
||||
|
||||
---
|
||||
|
||||
## 八、与相关 Feature 的关系
|
||||
|
||||
| Feature | 关系 |
|
||||
|---------|------|
|
||||
| `ULTRATHINK` | 类似的高能力模式,但 `ULTRATHINK` 只调高 effort,不启动远程会话 |
|
||||
| `FORK_SUBAGENT` | Ultraplan 不使用 fork subagent,使用的是 CCR 远程 agent |
|
||||
| `COORDINATOR_MODE` | 不同范式的多 agent,Coordinator 在本地编排,Ultraplan 在云端 |
|
||||
| `BRIDGE_MODE` | 底层依赖相同的 `teleportToRemote()` 基础设施 |
|
||||
| `ExitPlanModeTool` | 远程端的审批机制,Ultraplan 的核心交互模型 |
|
||||
152
learn/LEARN.md
152
learn/LEARN.md
@@ -1,152 +0,0 @@
|
||||
# Claude Code 源码学习路线
|
||||
|
||||
> 基于反编译版 Claude Code CLI (v2.1.888) 的源码学习跟踪
|
||||
>
|
||||
> 各阶段详细笔记见同目录下的 `phase-*.md` 文件
|
||||
|
||||
## 第一阶段:启动流程(入口链路) ✅
|
||||
|
||||
详细笔记:[phase-1-startup-flow.md](phase-1-startup-flow.md)
|
||||
|
||||
理解程序从命令行启动到用户看到交互界面的完整路径。
|
||||
|
||||
- [x] `src/entrypoints/cli.tsx` — 真正入口,polyfill 注入 + 快速路径分发
|
||||
- [x] 全局 polyfill:`feature()` 永远返回 false、`MACRO` 全局对象、`BUILD_*` 常量
|
||||
- [x] 快速路径设计:按开销从低到高检查,能早返回就早返回
|
||||
- [x] 动态 import 模式:`await import()` 延迟加载,减少启动时间
|
||||
- [x] 最终出口:`import("../main.jsx")` → `cliMain()`
|
||||
- [x] `src/main.tsx` — Commander.js CLI 定义,重型初始化(4683 行)
|
||||
- [x] 三段式结构:辅助函数(1-584) → main()(585-856) → run()(884-4683)
|
||||
- [x] side-effect import:profileCheckpoint、startMdmRawRead、startKeychainPrefetch 并行预加载
|
||||
- [x] preAction 钩子:MDM 等待、init()、迁移、远程设置
|
||||
- [x] Commander 参数定义:40+ CLI 选项
|
||||
- [x] action handler(2800 行):参数解析 → 服务初始化 → showSetupScreens → launchRepl()
|
||||
- [x] --print 分支走 print.ts;交互分支走 launchRepl()(7 个场景分支)
|
||||
- [x] 子命令注册:mcp/auth/plugin/doctor/update/install 等
|
||||
- [x] `src/replLauncher.tsx` — 桥梁(22 行),组合 `<App>` + `<REPL>` 渲染到终端
|
||||
- [x] `src/screens/REPL.tsx` — 交互式 REPL 界面(5009 行)
|
||||
- [x] Props:commands、tools、messages、systemPrompt、thinkingConfig 等
|
||||
- [x] 50+ 状态:messages、inputValue、screen、streamingText、queryGuard 等
|
||||
- [x] 核心数据流:onSubmit → handlePromptSubmit → onQuery → onQueryImpl → query() → onQueryEvent
|
||||
- [x] QueryGuard 并发控制:idle → running → idle,防止重复查询
|
||||
- [x] 渲染:Transcript 模式(只读历史)/ Prompt 模式(Messages + PermissionRequest + PromptInput)
|
||||
|
||||
**数据流**:`bun run dev` → `package.json scripts.dev` → `bun run src/entrypoints/cli.tsx` → 快速路径检查 → `main.tsx:main()` → `launchRepl()` → `<App><REPL /></App>`
|
||||
|
||||
---
|
||||
|
||||
## 第二阶段:核心对话循环 ✅
|
||||
|
||||
详细笔记:[phase-2-conversation-loop.md](phase-2-conversation-loop.md)
|
||||
|
||||
理解用户发一句话后,如何变成 API 请求、如何处理流式响应和工具调用。
|
||||
|
||||
- [x] `src/query.ts` — 核心查询循环(1732 行)
|
||||
- [x] `query()` AsyncGenerator 入口,委托给 `queryLoop()`
|
||||
- [x] `queryLoop()` — while(true) 主循环,State 对象管理迭代状态
|
||||
- [x] 消息预处理(autocompact、compact boundary)
|
||||
- [x] `deps.callModel()` → 流式 API 调用
|
||||
- [x] StreamingToolExecutor — API 流式返回时并行执行工具
|
||||
- [x] 工具调用循环(tool use → 执行 → result → continue)
|
||||
- [x] 错误恢复(prompt-too-long、max_output_tokens 升级+多轮恢复)
|
||||
- [x] 模型降级(FallbackTriggeredError → 切换 fallbackModel)
|
||||
- [x] Withheld 消息模式(暂扣可恢复错误)
|
||||
- [x] `src/QueryEngine.ts` — 高层编排器(1320 行)
|
||||
- [x] QueryEngine 类 — 一个 conversation 一个实例
|
||||
- [x] `submitMessage()` — 处理用户输入 → 调用 `query()` → 消费事件流
|
||||
- [x] SDK/print 模式专用(REPL 直接调用 query())
|
||||
- [x] 会话持久化(recordTranscript)
|
||||
- [x] Usage 跟踪、权限拒绝记录
|
||||
- [x] `ask()` 便捷包装函数
|
||||
- [x] `src/services/api/claude.ts` — API 客户端(3420 行)
|
||||
- [x] `queryModelWithStreaming` / `queryModelWithoutStreaming` — 两个公开入口
|
||||
- [x] `queryModel()` — 核心私有函数(2400 行)
|
||||
- [x] 请求参数组装(system prompt、betas、tools、cache control)
|
||||
- [x] Anthropic SDK 流式调用(`anthropic.beta.messages.stream()`)
|
||||
- [x] `BetaRawMessageStreamEvent` 事件处理(message_start/content_block_*/message_delta/stop)
|
||||
- [x] withRetry 重试策略(429/500/529 + 模型降级)
|
||||
- [x] Prompt Caching 策略(ephemeral/1h TTL/global scope)
|
||||
- [x] 多 provider 支持(Anthropic / Bedrock / Vertex / Azure)
|
||||
|
||||
**数据流**:REPL.onSubmit → handlePromptSubmit → onQuery → onQueryImpl → `query()` AsyncGenerator → `queryLoop()` while(true) → `deps.callModel()` → `claude.ts queryModel()` → `anthropic.beta.messages.stream()` → 流式事件 → 收集 tool_use → 执行工具 → 结果追加到 messages → continue → 无工具调用时 return
|
||||
|
||||
---
|
||||
|
||||
## 第三阶段:工具系统
|
||||
|
||||
理解 Claude 如何定义、注册、调用工具。先读框架,再挑具体工具。
|
||||
|
||||
- [ ] `src/Tool.ts` — Tool 接口定义
|
||||
- [ ] `Tool` 类型结构(name、description、inputSchema、call)
|
||||
- [ ] `findToolByName`、`toolMatchesName` 工具函数
|
||||
- [ ] `src/tools.ts` — 工具注册表
|
||||
- [ ] 工具列表组装逻辑
|
||||
- [ ] 条件加载(feature flag、USER_TYPE)
|
||||
- [ ] 具体工具实现(挑选 2-3 个深入阅读):
|
||||
- [ ] `src/tools/BashTool/` — 执行 shell 命令,最常用的工具
|
||||
- [ ] `src/tools/FileReadTool/` — 读取文件,简单直观,适合理解工具模式
|
||||
- [ ] `src/tools/FileEditTool/` — 编辑文件,理解 diff/patch 机制
|
||||
- [ ] `src/tools/AgentTool/` — 子 Agent 机制,较复杂但核心
|
||||
|
||||
---
|
||||
|
||||
## 第四阶段:上下文与系统提示
|
||||
|
||||
理解 Claude 如何"知道"项目信息、用户偏好等上下文。
|
||||
|
||||
- [ ] `src/context.ts` — 系统/用户上下文构建
|
||||
- [ ] git 状态注入
|
||||
- [ ] CLAUDE.md 内容加载
|
||||
- [ ] 内存文件(memory)注入
|
||||
- [ ] 日期、平台等环境信息
|
||||
- [ ] `src/utils/claudemd.ts` — CLAUDE.md 发现与加载
|
||||
- [ ] 项目层级搜索逻辑
|
||||
- [ ] 多级 CLAUDE.md 合并
|
||||
|
||||
---
|
||||
|
||||
## 第五阶段:UI 层(按兴趣选读)
|
||||
|
||||
理解终端 UI 的渲染机制(React/Ink)。
|
||||
|
||||
- [ ] `src/components/App.tsx` — 根组件,Provider 注入
|
||||
- [ ] `src/state/AppState.tsx` — 全局状态类型与 Context
|
||||
- [ ] `src/components/permissions/` — 工具权限审批 UI
|
||||
- [ ] `src/components/messages/` — 消息渲染组件
|
||||
|
||||
---
|
||||
|
||||
## 第六阶段:外围系统(按需探索)
|
||||
|
||||
- [ ] `src/services/mcp/` — MCP 协议(Model Context Protocol)
|
||||
- [ ] `src/skills/` — 技能系统(/commit 等斜杠命令)
|
||||
- [ ] `src/commands/` — CLI 子命令
|
||||
- [ ] `src/tasks/` — 后台任务系统
|
||||
- [ ] `src/utils/model/providers.ts` — 多 provider 选择逻辑
|
||||
|
||||
---
|
||||
|
||||
## 学习笔记
|
||||
|
||||
### 关键设计模式
|
||||
|
||||
| 模式 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| 快速路径 | cli.tsx | 按开销从低到高逐级检查,减少不必要的模块加载 |
|
||||
| 动态 import | cli.tsx / main.tsx | `await import()` 延迟加载,优化启动时间 |
|
||||
| feature flag | 全局 | `feature()` 永远返回 false,所有内部功能禁用 |
|
||||
| React/Ink | UI 层 | 用 React 组件模型渲染终端 UI |
|
||||
| 工具循环 | query.ts | AI 返回工具调用 → 执行 → 结果回传 → 继续,直到无工具调用 |
|
||||
| AsyncGenerator 链 | query.ts → claude.ts | `yield*` 透传事件流,形成管道 |
|
||||
| State 对象 | query.ts queryLoop | 循环间通过不可变 State + transition 字段传递状态 |
|
||||
| StreamingToolExecutor | query.ts | API 流式返回时并行执行工具 |
|
||||
| Withheld 消息 | query.ts | 暂扣可恢复错误,恢复成功则吞掉 |
|
||||
| withRetry | claude.ts | 429/500/529 自动重试 + 模型降级 |
|
||||
| Prompt Caching | claude.ts | 缓存系统提示和历史消息,减少 token 消耗 |
|
||||
|
||||
### 需要忽略的内容
|
||||
|
||||
- `_c()` 调用 — React Compiler 反编译产物
|
||||
- `feature('...')` 后面的代码块 — 全部是死代码
|
||||
- tsc 类型错误 — 反编译导致,不影响 Bun 运行
|
||||
- `packages/@ant/` — stub 包,无实际实现
|
||||
@@ -1,273 +0,0 @@
|
||||
# 第一阶段 Q&A
|
||||
|
||||
## Q1:cli.tsx 的快速路径分发具体在做什么?
|
||||
|
||||
**核心思想**:根据用户输入的命令参数,尽早决定走哪条路,避免加载不需要的代码。cli.tsx 充当一个轻量级路由器,把简单请求就地处理,只有真正需要完整 CLI 时才加载 main.tsx。
|
||||
|
||||
### 场景对比
|
||||
|
||||
#### 场景 1:`claude --version`(命中快速路径)
|
||||
|
||||
```
|
||||
cli.tsx main() 开始执行
|
||||
├── args = ["--version"]
|
||||
├── 命中第 64 行: args[0] === "--version" ✅
|
||||
├── console.log("2.1.888 (Claude Code)")
|
||||
└── return ← 立即退出,零 import,~10ms
|
||||
```
|
||||
|
||||
#### 场景 2:`claude --claude-in-chrome-mcp`(命中中间路径)
|
||||
|
||||
```
|
||||
cli.tsx main() 开始执行
|
||||
├── 第 64 行: --version? ❌
|
||||
├── 第 75 行: 加载 profileCheckpoint(仅此一个 import)
|
||||
├── 第 81 行: feature("DUMP_SYSTEM_PROMPT") → false ❌
|
||||
├── 第 95 行: --claude-in-chrome-mcp? ✅ 命中
|
||||
├── await import("../utils/claudeInChrome/mcpServer.js") ← 只加载这一个模块
|
||||
└── return ← 没有加载 main.tsx 的 200+ import
|
||||
```
|
||||
|
||||
#### 场景 3:`claude`(无参数,最常见,全部未命中)
|
||||
|
||||
```
|
||||
cli.tsx main() 开始执行
|
||||
├── --version? ❌
|
||||
├── profileCheckpoint 加载
|
||||
├── feature(DUMP)? ❌ (feature=false)
|
||||
├── --chrome-mcp? ❌
|
||||
├── --chrome-native? ❌
|
||||
├── feature(CHICAGO)? ❌ (feature=false)
|
||||
├── feature(DAEMON)? ❌ (feature=false)
|
||||
├── feature(BRIDGE)? ❌ (feature=false)
|
||||
├── ... 所有快速路径逐一检查,全部未命中
|
||||
│
|
||||
├── 走到第 310 行 ← 最终出口
|
||||
├── await import("../main.jsx") ← 加载完整 CLI(200+ import,~135ms)
|
||||
└── await cliMain() ← 进入 main.tsx 重型初始化
|
||||
```
|
||||
|
||||
### 性能对比
|
||||
|
||||
| 方式 | `claude --version` 耗时 |
|
||||
|------|------------------------|
|
||||
| 无快速路径(全部走 main.tsx) | ~200ms(加载 200+ import → 初始化 Commander → 解析参数 → 打印) |
|
||||
| 有快速路径(cli.tsx 拦截) | ~10ms(读 args → 打印 → 退出) |
|
||||
|
||||
### feature() 的加速作用
|
||||
|
||||
大量快速路径被 `feature()` 守护:
|
||||
|
||||
```ts
|
||||
if (feature("DAEMON") && args[0] === "daemon") { ... }
|
||||
```
|
||||
|
||||
`feature()` 返回 false → `&&` 短路求值 → 连 `args[0]` 都不检查,直接跳过。在反编译版本中这些路径等于不存在,进一步加速了"全部没命中 → 走默认路径"的过程。
|
||||
|
||||
---
|
||||
|
||||
## Q2:main.tsx 中不同命令的具体执行流程是怎样的?
|
||||
|
||||
所有命令都会经过 main() → run(),但在 run() 内部根据 Commander 路由到不同分支。
|
||||
|
||||
### 场景 1:`claude`(无参数 — 启动交互 REPL)
|
||||
|
||||
最常见的场景,走完整条主命令路径:
|
||||
|
||||
```
|
||||
main() (第 585 行)
|
||||
├── 信号处理注册(SIGINT、exit)
|
||||
├── feature flag 路径全部跳过
|
||||
├── isNonInteractive = false(有 TTY,没有 -p)
|
||||
├── clientType = 'cli'
|
||||
└── await run()
|
||||
│
|
||||
▼
|
||||
run() (第 884 行)
|
||||
├── Commander 初始化 + preAction 钩子 + 主命令选项注册
|
||||
├── isPrintMode = false → 注册所有子命令
|
||||
└── program.parseAsync(process.argv)
|
||||
│ Commander 匹配到主命令,先执行 preAction
|
||||
▼
|
||||
preAction (第 907 行)
|
||||
├── await ensureMdmSettingsLoaded() ← 等 side-effect import 的子进程完成
|
||||
├── await ensureKeychainPrefetchCompleted() ← 等 keychain 预读完成
|
||||
├── await init() ← 遥测、配置、信任
|
||||
├── initSinks() ← 分析日志
|
||||
├── runMigrations() ← 数据迁移
|
||||
└── loadRemoteManagedSettings() / loadPolicyLimits() ← 非阻塞
|
||||
│ 然后执行 action handler
|
||||
▼
|
||||
action(undefined, options) (第 1007 行) ← prompt = undefined
|
||||
├── [参数解析] permissionMode, model, thinkingConfig...
|
||||
├── [工具加载] tools = getTools(toolPermissionContext)
|
||||
├── [并行初始化]
|
||||
│ ├── setup() ← worktree、CWD
|
||||
│ ├── getCommands() ← 加载斜杠命令
|
||||
│ └── getAgentDefinitionsWithOverrides() ← 加载 agent 定义
|
||||
├── [MCP 连接] 连接配置的 MCP 服务器
|
||||
├── [构建初始状态] initialState = { tools, mcp, permissions, ... }
|
||||
│
|
||||
├── [UI 初始化](交互模式专属)
|
||||
│ ├── createRoot() ← 创建 Ink 渲染根节点
|
||||
│ └── showSetupScreens() ← 信任对话框 / OAuth / 引导
|
||||
│
|
||||
├── [后续初始化] LSP、插件版本、session 注册
|
||||
│
|
||||
└── 默认分支 (第 3760 行) ← 没有 --continue/--resume/--print
|
||||
└── await launchRepl(root, {
|
||||
initialState
|
||||
}, {
|
||||
...sessionConfig,
|
||||
initialMessages: undefined ← 全新对话,无历史消息
|
||||
}, renderAndRun)
|
||||
│
|
||||
▼
|
||||
REPL.tsx 渲染,用户看到空白对话界面
|
||||
```
|
||||
|
||||
### 场景 2:`echo "explain this" | claude -p`(管道/非交互模式)
|
||||
|
||||
```
|
||||
main() →
|
||||
├── isNonInteractive = true(-p 标志 + stdin 不是 TTY)
|
||||
├── clientType = 'sdk-cli'
|
||||
└── run()
|
||||
│
|
||||
▼
|
||||
run()
|
||||
├── Commander 初始化 + preAction + 主命令选项
|
||||
├── isPrintMode = true
|
||||
│ → ★ 跳过所有子命令注册(节省 ~65ms)
|
||||
└── program.parseAsync() ← 直接解析,Commander 路由到主命令 action
|
||||
│
|
||||
▼
|
||||
preAction → init、迁移等(同场景 1)
|
||||
│
|
||||
▼
|
||||
action("", { print: true, ... })
|
||||
├── inputPrompt = await getInputPrompt("")
|
||||
│ ├── stdin.isTTY = false → 从 stdin 读数据
|
||||
│ ├── 等待最多 3s 读入: "explain this"
|
||||
│ └── 返回 "explain this"
|
||||
├── tools = getTools()
|
||||
├── setup() + getCommands()(并行)
|
||||
│
|
||||
├── isNonInteractiveSession = true → 走 --print 分支(第 2584 行)
|
||||
│ ├── applyConfigEnvironmentVariables() ← -p 模式信任隐含
|
||||
│ ├── 构建 headlessInitialState(无 UI)
|
||||
│ ├── headlessStore = createStore(headlessInitialState)
|
||||
│ │
|
||||
│ ├── await import('src/cli/print.js')
|
||||
│ └── runHeadless(inputPrompt, ...) ★ 不走 REPL
|
||||
│ ├── 发送 API 请求
|
||||
│ ├── 流式输出到 stdout
|
||||
│ └── 完成后 process.exit()
|
||||
│
|
||||
└── ← 不走 createRoot()、showSetupScreens()、launchRepl()
|
||||
```
|
||||
|
||||
**关键差异**:
|
||||
- 检测到 `-p` 后跳过子命令注册(节省 ~65ms)
|
||||
- 不创建 Ink UI,不调用 `showSetupScreens()`
|
||||
- 从 stdin 读取输入(`getInputPrompt` 第 857 行)
|
||||
- 走 `print.js` 路径直接执行查询输出到 stdout
|
||||
|
||||
### 场景 3:`claude -c`(继续最近对话)
|
||||
|
||||
```
|
||||
... main() → run() → preAction → action(前半部分同场景 1)
|
||||
│
|
||||
▼
|
||||
action(undefined, { continue: true, ... })
|
||||
├── [参数解析 + 工具加载 + 并行初始化 + UI 初始化](同场景 1)
|
||||
│
|
||||
├── options.continue = true → 命中第 3101 行
|
||||
│ ├── clearSessionCaches() ← 清除过期缓存
|
||||
│ ├── result = await loadConversationForResume()
|
||||
│ │ └── 从 ~/.claude/projects/<cwd>/ 读最近的会话 JSONL
|
||||
│ │
|
||||
│ ├── result 为 null? → exitWithError("No conversation found")
|
||||
│ │
|
||||
│ ├── loaded = await processResumedConversation(result)
|
||||
│ │ ├── 解析 JSONL → messages[]
|
||||
│ │ ├── 恢复文件历史快照
|
||||
│ │ └── 重建 initialState
|
||||
│ │
|
||||
│ └── await launchRepl(root, {
|
||||
│ initialState: loaded.initialState
|
||||
│ }, {
|
||||
│ ...sessionConfig,
|
||||
│ initialMessages: loaded.messages, ★ 带上历史消息
|
||||
│ initialFileHistorySnapshots: loaded.fileHistorySnapshots,
|
||||
│ initialAgentName: loaded.agentName
|
||||
│ }, renderAndRun)
|
||||
│ │
|
||||
│ ▼
|
||||
│ REPL.tsx 渲染,显示历史对话,用户继续聊天
|
||||
│
|
||||
└── ← 其他分支不执行
|
||||
```
|
||||
|
||||
**关键差异**:`initialMessages` 有值(历史消息),REPL 启动时会渲染之前的对话内容。
|
||||
|
||||
### 场景 4:`claude mcp list`(子命令)
|
||||
|
||||
```
|
||||
main() → run()
|
||||
│
|
||||
▼
|
||||
run()
|
||||
├── Commander 初始化 + preAction 钩子
|
||||
├── 注册主命令 .action(...)
|
||||
├── isPrintMode = false → 注册所有子命令
|
||||
│ ├── program.command('mcp') (第 3894 行)
|
||||
│ │ ├── mcp.command('serve').action(...)
|
||||
│ │ ├── mcp.command('add').action(...)
|
||||
│ │ ├── mcp.command('list').action(async () => { ★
|
||||
│ │ │ const { mcpListHandler } = await import('./cli/handlers/mcp.js');
|
||||
│ │ │ await mcpListHandler();
|
||||
│ │ │ })
|
||||
│ │ └── ...
|
||||
│ ├── program.command('auth')
|
||||
│ ├── program.command('doctor')
|
||||
│ └── ...
|
||||
│
|
||||
└── program.parseAsync(["node", "claude", "mcp", "list"])
|
||||
│ Commander 匹配到 mcp → list
|
||||
▼
|
||||
preAction (第 907 行) ← 子命令也触发 preAction
|
||||
├── await init()
|
||||
├── initSinks()
|
||||
├── runMigrations()
|
||||
└── ...
|
||||
│
|
||||
▼ 执行子命令自己的 action(不走主命令 action)
|
||||
mcp list action
|
||||
├── await import('./cli/handlers/mcp.js')
|
||||
└── await mcpListHandler()
|
||||
├── 读取 MCP 配置(user/project/local 三级)
|
||||
├── 连接每个服务器做健康检查
|
||||
├── 格式化输出到终端
|
||||
└── 退出
|
||||
|
||||
← 主命令的 action handler 完全不执行
|
||||
← 没有 REPL、没有 Ink UI、没有 showSetupScreens
|
||||
```
|
||||
|
||||
**关键差异**:
|
||||
- Commander 路由到子命令,**主命令 action 完全跳过**
|
||||
- `preAction` 仍然执行(基础初始化所有命令都需要)
|
||||
- 子命令有自己独立的轻量 action
|
||||
|
||||
### 四种场景对比
|
||||
|
||||
| | `claude` | `claude -p` | `claude -c` | `claude mcp list` |
|
||||
|---|---------|------------|------------|-------------------|
|
||||
| preAction | 执行 | 执行 | 执行 | 执行 |
|
||||
| 主命令 action | 执行 | 执行 | 执行 | **跳过** |
|
||||
| 子命令注册 | 注册 | **跳过** | 注册 | 注册 |
|
||||
| showSetupScreens | 执行 | **跳过** | 执行 | **跳过** |
|
||||
| createRoot (Ink) | 执行 | **跳过** | 执行 | **跳过** |
|
||||
| 加载历史消息 | 否 | 否 | **是** | 否 |
|
||||
| 最终出口 | launchRepl | print.js | launchRepl | 子命令 action |
|
||||
@@ -1,597 +0,0 @@
|
||||
# 第一阶段:启动流程详解
|
||||
|
||||
> 从 `bun run dev` 到用户看到交互界面的完整路径
|
||||
|
||||
## 启动链路总览
|
||||
|
||||
```
|
||||
bun run dev
|
||||
→ package.json scripts.dev: "bun run src/entrypoints/cli.tsx"
|
||||
→ cli.tsx: polyfill 注入 + 快速路径检查
|
||||
→ import("../main.jsx") → cliMain()
|
||||
→ main.tsx: main() → run()
|
||||
→ Commander 参数解析 → preAction 钩子
|
||||
→ action handler: 服务初始化 → showSetupScreens
|
||||
→ launchRepl()
|
||||
→ replLauncher.tsx: <App><REPL /></App>
|
||||
→ REPL.tsx: 渲染交互界面,等待用户输入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. cli.tsx(321 行)— 入口与快速路径分发
|
||||
|
||||
**文件路径**: `src/entrypoints/cli.tsx`
|
||||
|
||||
### 1.1 全局 Polyfill(第 1-53 行)
|
||||
|
||||
模块加载时立即执行的 side-effect,在 `main()` 之前运行。
|
||||
|
||||
#### feature() 桩函数(第 3 行)
|
||||
|
||||
```ts
|
||||
const feature = (_name: string) => false;
|
||||
```
|
||||
|
||||
原版 Claude Code 构建时,Bun bundler 通过 `bun:bundle` 提供 `feature()` 函数,用于**编译时 feature flag**(类似 C 的 `#ifdef`)。反编译版没有构建流程,所以直接定义为永远返回 `false`。
|
||||
|
||||
**效果**:所有 Anthropic 内部功能分支全部禁用,包括:
|
||||
- `COORDINATOR_MODE` — 协调器模式
|
||||
- `KAIROS` — 助手模式
|
||||
- `DAEMON` — 后台守护进程
|
||||
- `BRIDGE_MODE` — 远程控制
|
||||
- `SSH_REMOTE` — SSH 远程
|
||||
- `BG_SESSIONS` — 后台会话
|
||||
- ... 等 20+ 个 flag
|
||||
|
||||
#### MACRO 全局对象(第 4-14 行)
|
||||
|
||||
```ts
|
||||
globalThis.MACRO = {
|
||||
VERSION: "2.1.888",
|
||||
BUILD_TIME: new Date().toISOString(),
|
||||
FEEDBACK_CHANNEL: "",
|
||||
ISSUES_EXPLAINER: "",
|
||||
NATIVE_PACKAGE_URL: "",
|
||||
PACKAGE_URL: "",
|
||||
VERSION_CHANGELOG: "",
|
||||
};
|
||||
```
|
||||
|
||||
原版构建时 Bun 会把这些值内联到代码里。这里模拟注入,让后续代码读 `MACRO.VERSION` 时能拿到值。
|
||||
|
||||
#### 构建常量(第 16-18 行)
|
||||
|
||||
```ts
|
||||
BUILD_TARGET = "external"; // 标记为"外部"构建(非 Anthropic 内部)
|
||||
BUILD_ENV = "production"; // 生产环境
|
||||
INTERFACE_TYPE = "stdio"; // 标准输入输出模式
|
||||
```
|
||||
|
||||
这三个全局变量在代码各处被读取,用来区分运行环境。`"external"` 意味着很多 `("external" as string) === 'ant'` 的检查会返回 false。
|
||||
|
||||
#### 环境修补(第 22-33 行)
|
||||
|
||||
- 禁用 corepack 自动 pin(防止污染 package.json)
|
||||
- 远程模式下设置 Node.js 堆内存上限 8GB
|
||||
|
||||
#### ABLATION_BASELINE(第 40-53 行)
|
||||
|
||||
```ts
|
||||
if (feature("ABLATION_BASELINE") && ...) { ... }
|
||||
```
|
||||
|
||||
`feature()` 返回 false,**永远不执行**。Anthropic 内部 A/B 测试代码。
|
||||
|
||||
### 1.2 main() 函数(第 60-317 行)
|
||||
|
||||
设计模式:**分层快速路径(fast path cascading)**——按开销从低到高逐级检查,命中即返回。
|
||||
|
||||
#### 快速路径列表
|
||||
|
||||
| 优先级 | 行号 | 检查条件 | 功能 | 开销 | 可执行 |
|
||||
|--------|------|---------|------|------|--------|
|
||||
| 1 | 64-72 | `--version` / `-v` | 打印版本号退出 | **零 import** | 是 |
|
||||
| 2 | 81-94 | `feature("DUMP_SYSTEM_PROMPT")` | 导出系统提示 | - | 否(flag) |
|
||||
| 3 | 95-99 | `--claude-in-chrome-mcp` | Chrome MCP 服务 | 动态 import | 是 |
|
||||
| 4 | 101-105 | `--chrome-native-host` | Chrome Native Host | 动态 import | 是 |
|
||||
| 5 | 108-116 | `feature("CHICAGO_MCP")` | Computer Use MCP | - | 否(flag) |
|
||||
| 6 | 123-127 | `feature("DAEMON")` | Daemon Worker | - | 否(flag) |
|
||||
| 7 | 133-178 | `feature("BRIDGE_MODE")` | 远程控制 | - | 否(flag) |
|
||||
| 8 | 181-190 | `feature("DAEMON")` | Daemon 主进程 | - | 否(flag) |
|
||||
| 9 | 195-225 | `feature("BG_SESSIONS")` | ps/logs/attach/kill | - | 否(flag) |
|
||||
| 10 | 228-240 | `feature("TEMPLATES")` | 模板任务 | - | 否(flag) |
|
||||
| 11 | 244-253 | `feature("BYOC_ENVIRONMENT_RUNNER")` | BYOC 运行器 | - | 否(flag) |
|
||||
| 12 | 258-264 | `feature("SELF_HOSTED_RUNNER")` | 自托管运行器 | - | 否(flag) |
|
||||
| 13 | 267-293 | `--tmux` + `--worktree` | tmux worktree | 动态 import | 是 |
|
||||
|
||||
#### 参数修正(第 296-307 行)
|
||||
|
||||
```ts
|
||||
// --update/--upgrade → 重写为 update 子命令
|
||||
if (args[0] === "--update") process.argv = [..., "update"];
|
||||
// --bare → 设置简单模式环境变量
|
||||
if (args.includes("--bare")) process.env.CLAUDE_CODE_SIMPLE = "1";
|
||||
```
|
||||
|
||||
#### 最终出口(第 310-316 行)
|
||||
|
||||
```ts
|
||||
const { startCapturingEarlyInput } = await import("../utils/earlyInput.js");
|
||||
startCapturingEarlyInput(); // 捕获用户提前输入的内容
|
||||
const { main: cliMain } = await import("../main.jsx");
|
||||
await cliMain(); // 进入 main.tsx 重型初始化
|
||||
```
|
||||
|
||||
所有快速路径都没命中时(99% 的情况),才走到这里。
|
||||
|
||||
### 1.3 启动(第 320 行)
|
||||
|
||||
```ts
|
||||
void main();
|
||||
```
|
||||
|
||||
`void` 表示不关心 Promise 返回值。
|
||||
|
||||
### 1.4 关键设计思想
|
||||
|
||||
- **快速路径**:`--version` 零开销返回,不加载任何模块
|
||||
- **动态 import**:`await import()` 替代静态 import,每条路径只加载自己需要的模块
|
||||
- **feature flag 过滤**:`feature()` 返回 false 使大量内部功能成为死代码
|
||||
|
||||
---
|
||||
|
||||
## 2. main.tsx(4683 行)— 重型初始化与 Commander CLI
|
||||
|
||||
**文件路径**: `src/main.tsx`
|
||||
|
||||
整个项目最大的单文件,但结构清晰:**辅助函数 → main() → run()**。
|
||||
|
||||
### 2.1 Import 区(第 1-215 行)
|
||||
|
||||
200+ 行 import,加载几乎所有子系统。关键的是前三个 **side-effect import**(import 即执行):
|
||||
|
||||
```ts
|
||||
// 第 9 行:记录时间戳
|
||||
profileCheckpoint('main_tsx_entry');
|
||||
|
||||
// 第 16 行:启动 MDM 子进程读取(macOS plutil)
|
||||
startMdmRawRead();
|
||||
|
||||
// 第 20 行:启动 keychain 预读取(OAuth token、API key)
|
||||
startKeychainPrefetch();
|
||||
```
|
||||
|
||||
这三个在 import 阶段就**并行启动子进程**,和后续 ~135ms 的模块加载同时进行——**用并行隐藏延迟**。
|
||||
|
||||
### 2.2 辅助函数(第 216-584 行)
|
||||
|
||||
| 函数 | 行号 | 作用 |
|
||||
|------|------|------|
|
||||
| `logManagedSettings()` | 216 | 记录企业托管设置到分析日志 |
|
||||
| `isBeingDebugged()` | 232 | 检测调试模式,**外部构建下直接 exit(1)**(第 266 行) |
|
||||
| `logSessionTelemetry()` | 279 | Session 遥测(技能、插件) |
|
||||
| `getCertEnvVarTelemetry()` | 291 | SSL 证书环境变量收集 |
|
||||
| `runMigrations()` | 326 | 数据迁移(模型重命名、设置格式升级等) |
|
||||
| `prefetchSystemContextIfSafe()` | 360 | 信任关系建立后安全预取系统上下文 |
|
||||
| `startDeferredPrefetches()` | 388 | REPL 首次渲染后的延迟预取 |
|
||||
| `eagerLoadSettings()` | 502 | 在 init() 之前提前加载 `--settings` 参数 |
|
||||
| `initializeEntrypoint()` | 517 | 根据运行模式设置 `CLAUDE_CODE_ENTRYPOINT` |
|
||||
|
||||
还有 `_pendingConnect`、`_pendingSSH`、`_pendingAssistantChat` 三个状态变量(第 542-583 行),用于暂存子命令参数。
|
||||
|
||||
### 2.3 main() 函数(第 585-856 行)
|
||||
|
||||
`main()` 本身不长,做完环境检测后调用 `run()`:
|
||||
|
||||
```
|
||||
main()
|
||||
├── 安全设置(NoDefaultCurrentDirectoryInExePath)
|
||||
├── 信号处理(SIGINT → exit, exit → 恢复光标)
|
||||
├── feature flag 保护的特殊路径(全部跳过)
|
||||
├── 检测 -p/--print / --init-only → 判断是否交互模式
|
||||
├── clientType 判断(cli / sdk-typescript / remote / github-action 等)
|
||||
├── eagerLoadSettings()
|
||||
└── await run() ← 进入真正的逻辑
|
||||
```
|
||||
|
||||
### 2.4 run() 函数(第 884-4683 行)
|
||||
|
||||
占 3800 行,是整个文件的核心。
|
||||
|
||||
#### Commander 初始化 + preAction 钩子(第 884-967 行)
|
||||
|
||||
```ts
|
||||
const program = new CommanderCommand()
|
||||
.configureHelp(createSortedHelpConfig())
|
||||
.enablePositionalOptions();
|
||||
```
|
||||
|
||||
**preAction 钩子**(所有命令执行前都会运行):
|
||||
|
||||
```
|
||||
preAction
|
||||
├── await ensureMdmSettingsLoaded() ← 等 MDM 子进程完成
|
||||
├── await ensureKeychainPrefetchCompleted() ← 等 keychain 预读完成
|
||||
├── await init() ← 一次性初始化
|
||||
├── initSinks() ← 分析日志接收器
|
||||
├── runMigrations() ← 数据迁移
|
||||
├── loadRemoteManagedSettings() ← 企业远程设置(非阻塞)
|
||||
└── loadPolicyLimits() ← 策略限制(非阻塞)
|
||||
```
|
||||
|
||||
#### 主命令 Option 定义(第 968-1006 行)
|
||||
|
||||
定义了 40+ CLI 参数,关键的包括:
|
||||
|
||||
| 参数 | 作用 |
|
||||
|------|------|
|
||||
| `-p, --print` | 非交互模式,输出后退出 |
|
||||
| `--model <model>` | 指定模型(如 sonnet、opus) |
|
||||
| `--permission-mode <mode>` | 权限模式 |
|
||||
| `-c, --continue` | 继续最近对话 |
|
||||
| `-r, --resume` | 恢复指定对话 |
|
||||
| `--mcp-config` | MCP 服务器配置文件 |
|
||||
| `--allowedTools` | 允许的工具列表 |
|
||||
| `--system-prompt` | 自定义系统提示 |
|
||||
| `--dangerously-skip-permissions` | 跳过所有权限检查 |
|
||||
| `--output-format` | 输出格式(text/json/stream-json) |
|
||||
| `--effort <level>` | 推理努力级别(low/medium/high/max) |
|
||||
| `--bare` | 最小模式 |
|
||||
|
||||
#### action 处理器(第 1006-3808 行)
|
||||
|
||||
主命令的执行逻辑,内部按阶段和场景分支:
|
||||
|
||||
```
|
||||
action(async (prompt, options) => {
|
||||
│
|
||||
├── [1007-1600] 参数解析与预处理
|
||||
│ ├── --bare 模式
|
||||
│ ├── 解析 model / permission-mode / thinking / effort
|
||||
│ ├── 解析 MCP 配置、工具列表、系统提示
|
||||
│ └── 初始化工具权限上下文
|
||||
│
|
||||
├── [1600-2220] 服务初始化
|
||||
│ ├── MCP 客户端连接
|
||||
│ ├── 插件加载 + 技能初始化
|
||||
│ ├── 工具列表组装
|
||||
│ └── 初始 AppState 构建
|
||||
│
|
||||
├── [2220-2315] UI 初始化(交互模式)
|
||||
│ ├── createRoot() — 创建 Ink 渲染根节点
|
||||
│ ├── showSetupScreens() — 信任对话框、OAuth 登录、引导
|
||||
│ └── 登录后刷新各种服务
|
||||
│
|
||||
├── [2315-2582] 后续初始化
|
||||
│ ├── LSP 管理器、插件版本管理
|
||||
│ ├── session 注册、遥测日志
|
||||
│ └── 遥测上报
|
||||
│
|
||||
├── [2584-3050] --print 非交互模式分支
|
||||
│ ├── 构建 headless AppState + store
|
||||
│ └── 交给 print.ts 执行
|
||||
│
|
||||
└── [3050-3808] 交互模式:启动 REPL(7 个分支)
|
||||
├── --continue → 加载最近对话 → launchRepl()
|
||||
├── DIRECT_CONNECT → ❌ flag 关闭
|
||||
├── SSH_REMOTE → ❌ flag 关闭
|
||||
├── KAIROS assistant → ❌ flag 关闭
|
||||
├── --resume <id> → 恢复指定对话 → launchRepl()
|
||||
├── --resume 无 ID → 显示对话选择器
|
||||
└── 默认(无参数) → launchRepl() ★最常走的路径
|
||||
})
|
||||
```
|
||||
|
||||
#### 子命令注册(第 3808-4683 行)
|
||||
|
||||
| 子命令 | 行号 | 作用 |
|
||||
|--------|------|------|
|
||||
| `claude mcp` | 3892 | MCP 服务器管理(serve/add/remove/list/get) |
|
||||
| `claude server` | 3960 | Session 服务器(❌ flag 关闭) |
|
||||
| `claude auth` | 4098 | 认证管理(login/logout/status/token) |
|
||||
| `claude plugin` | 4148 | 插件管理(install/uninstall/list/update) |
|
||||
| `claude setup-token` | 4267 | 设置长期认证 token |
|
||||
| `claude agents` | 4278 | 列出已配置的 agents |
|
||||
| `claude doctor` | 4346 | 健康检查 |
|
||||
| `claude update` | 4362 | 检查更新 |
|
||||
| `claude install` | 4394 | 安装原生构建 |
|
||||
| `claude log` | 4411 | 查看对话日志(内部) |
|
||||
| `claude completion` | 4491 | Shell 自动补全 |
|
||||
|
||||
最后执行解析:
|
||||
|
||||
```ts
|
||||
await program.parseAsync(process.argv);
|
||||
```
|
||||
|
||||
### 2.5 main.tsx 学习建议
|
||||
|
||||
- **不要通读**。记住三段结构:辅助函数 → main() → run()
|
||||
- `feature()` 返回 false 的分支全部跳过,可忽略 50%+ 代码
|
||||
- `("external" as string) === 'ant'` 的分支也跳过(内部构建专用)
|
||||
- 需要深入某功能时,通过搜索定位对应代码段
|
||||
|
||||
---
|
||||
|
||||
## 3. replLauncher.tsx(22 行)— 胶水层
|
||||
|
||||
**文件路径**: `src/replLauncher.tsx`
|
||||
|
||||
极其简单,就做一件事:
|
||||
|
||||
```tsx
|
||||
export async function launchRepl(root, appProps, replProps, renderAndRun) {
|
||||
const { App } = await import('./components/App.js');
|
||||
const { REPL } = await import('./screens/REPL.js');
|
||||
await renderAndRun(root, <App {...appProps}><REPL {...replProps} /></App>);
|
||||
}
|
||||
```
|
||||
|
||||
- `App` — 全局 Provider(AppState、Stats、FpsMetrics)
|
||||
- `REPL` — 交互界面组件
|
||||
- `renderAndRun` — 把 React 元素渲染到 Ink 终端
|
||||
|
||||
动态 import 保持了按需加载的策略。
|
||||
|
||||
---
|
||||
|
||||
## 4. REPL.tsx(5009 行)— 交互界面
|
||||
|
||||
**文件路径**: `src/screens/REPL.tsx`
|
||||
|
||||
项目第二大文件,是用户直接交互的界面。一个巨型 React 函数组件。
|
||||
|
||||
### 4.1 文件结构
|
||||
|
||||
```
|
||||
REPL.tsx (5009 行)
|
||||
├── [1-310] Import 区(150+ import)
|
||||
├── [312-525] 辅助组件
|
||||
│ ├── median() — 数学工具函数
|
||||
│ ├── TranscriptModeFooter — 转录模式底栏
|
||||
│ ├── TranscriptSearchBar — 转录搜索栏
|
||||
│ └── AnimatedTerminalTitle — 终端标题动画
|
||||
├── [527-571] Props 类型定义
|
||||
└── [573-5009] REPL() 组件主体
|
||||
├── [600-900] 状态声明(50+ 个 useState/useRef/useAppState)
|
||||
├── [900-2750] 副作用与回调(useEffect/useCallback)
|
||||
├── [2750-2860] onQueryImpl — 核心:执行 API 查询
|
||||
├── [2860-3030] onQuery — 查询守卫与并发控制
|
||||
├── [3030-3145] 查询相关辅助回调
|
||||
├── [3146-3550] onSubmit — 用户提交处理
|
||||
├── [3550-4395] 更多副作用与状态管理
|
||||
└── [4396-5009] JSX 渲染
|
||||
```
|
||||
|
||||
### 4.2 Props
|
||||
|
||||
从 main.tsx 通过 launchRepl() 传入:
|
||||
|
||||
| Prop | 类型 | 含义 |
|
||||
|------|------|------|
|
||||
| `commands` | `Command[]` | 可用的斜杠命令 |
|
||||
| `debug` | `boolean` | 调试模式 |
|
||||
| `initialTools` | `Tool[]` | 初始工具集 |
|
||||
| `initialMessages` | `MessageType[]` | 初始消息(恢复对话时有值) |
|
||||
| `pendingHookMessages` | `Promise<...>` | 延迟加载的 hook 消息 |
|
||||
| `mcpClients` | `MCPServerConnection[]` | MCP 服务器连接 |
|
||||
| `systemPrompt` | `string` | 自定义系统提示 |
|
||||
| `appendSystemPrompt` | `string` | 追加系统提示 |
|
||||
| `onBeforeQuery` | `fn` | 查询前回调,返回 false 可阻止查询 |
|
||||
| `onTurnComplete` | `fn` | 轮次完成回调 |
|
||||
| `mainThreadAgentDefinition` | `AgentDefinition` | 主线程 Agent 定义 |
|
||||
| `thinkingConfig` | `ThinkingConfig` | 思考模式配置 |
|
||||
| `disabled` | `boolean` | 禁用输入 |
|
||||
|
||||
### 4.3 状态管理
|
||||
|
||||
分三层:
|
||||
|
||||
**全局 AppState(通过 useAppState 选择器读取):**
|
||||
|
||||
```ts
|
||||
const toolPermissionContext = useAppState(s => s.toolPermissionContext);
|
||||
const verbose = useAppState(s => s.verbose);
|
||||
const mcp = useAppState(s => s.mcp);
|
||||
const plugins = useAppState(s => s.plugins);
|
||||
const agentDefinitions = useAppState(s => s.agentDefinitions);
|
||||
```
|
||||
|
||||
**本地状态(useState):**
|
||||
|
||||
```ts
|
||||
const [messages, setMessages] = useState(initialMessages ?? []);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [screen, setScreen] = useState<Screen>('prompt');
|
||||
const [streamingText, setStreamingText] = useState(null);
|
||||
const [streamingToolUses, setStreamingToolUses] = useState([]);
|
||||
// ... 50+ 个状态
|
||||
```
|
||||
|
||||
**关键 Ref:**
|
||||
|
||||
```ts
|
||||
const queryGuard = useRef(new QueryGuard()).current; // 查询并发控制
|
||||
const messagesRef = useRef(messages); // 消息的同步引用(避免闭包问题)
|
||||
const abortController = ...; // 取消请求控制器
|
||||
const responseLengthRef = useRef(0); // 响应长度追踪
|
||||
```
|
||||
|
||||
### 4.4 核心数据流:用户输入 → API 调用
|
||||
|
||||
```
|
||||
用户按回车
|
||||
│
|
||||
▼
|
||||
onSubmit (第 3146 行)
|
||||
├── 斜杠命令?→ immediate command 直接执行 或 handlePromptSubmit 路由
|
||||
├── 空输入?→ 忽略
|
||||
├── 空闲检测 → 可能弹出"是否开始新对话"对话框
|
||||
├── 加入历史记录
|
||||
│
|
||||
▼
|
||||
handlePromptSubmit (外部函数,src/utils/handlePromptSubmit.ts)
|
||||
├── 斜杠命令 → 路由到对应 Command handler
|
||||
├── 普通文本 → 构建 UserMessage,调用 onQuery()
|
||||
│
|
||||
▼
|
||||
onQuery (第 2860 行) — 并发守卫层
|
||||
├── queryGuard.tryStart() → 已有查询?排队等待
|
||||
├── setMessages([...old, ...newMessages]) — 追加用户消息
|
||||
├── onQueryImpl()
|
||||
│
|
||||
▼
|
||||
onQueryImpl (第 2750 行) — 真正执行 API 调用
|
||||
│
|
||||
├── 1. 并行加载上下文:
|
||||
│ await Promise.all([
|
||||
│ getSystemPrompt(), // 构建系统提示
|
||||
│ getUserContext(), // 用户上下文
|
||||
│ getSystemContext(), // 系统上下文(git、平台等)
|
||||
│ ])
|
||||
│
|
||||
├── 2. buildEffectiveSystemPrompt() — 合成最终系统提示
|
||||
│
|
||||
├── 3. for await (const event of query({...})) ★核心★
|
||||
│ │ 调用 src/query.ts 的 query() AsyncGenerator
|
||||
│ │ 流式产出事件
|
||||
│ │
|
||||
│ └── onQueryEvent(event) — 处理每个流式事件
|
||||
│ ├── 更新 streamingText(打字机效果)
|
||||
│ ├── 更新 messages(工具调用结果)
|
||||
│ └── 更新 inProgressToolUseIDs
|
||||
│
|
||||
└── 4. 收尾:resetLoadingState()、onTurnComplete()
|
||||
```
|
||||
|
||||
**核心代码(第 2797-2807 行)**:
|
||||
|
||||
```ts
|
||||
for await (const event of query({
|
||||
messages: messagesIncludingNewMessages,
|
||||
systemPrompt,
|
||||
userContext,
|
||||
systemContext,
|
||||
canUseTool,
|
||||
toolUseContext,
|
||||
querySource: getQuerySourceForREPL()
|
||||
})) {
|
||||
onQueryEvent(event);
|
||||
}
|
||||
```
|
||||
|
||||
`query()` 来自 `src/query.ts`,是第二阶段要学的核心函数。
|
||||
|
||||
### 4.5 QueryGuard 并发控制
|
||||
|
||||
防止同时发起多个 API 请求的状态机:
|
||||
|
||||
```
|
||||
idle ──tryStart()──▶ running ──end()──▶ idle
|
||||
│
|
||||
└── tryStart() 返回 null(已在运行)
|
||||
→ 新消息排入队列
|
||||
```
|
||||
|
||||
- `tryStart()` — 原子操作,检查并转换 idle→running,返回 generation 号
|
||||
- `end(generation)` — 检查 generation 匹配后转换 running→idle
|
||||
- 防止 cancel+resubmit 竞态条件
|
||||
|
||||
### 4.6 JSX 渲染
|
||||
|
||||
两个互斥的渲染分支:
|
||||
|
||||
#### Transcript 模式(第 4396-4493 行)
|
||||
|
||||
按 `v` 键切换,只读浏览对话历史,支持搜索:
|
||||
|
||||
```tsx
|
||||
<KeybindingSetup>
|
||||
<AnimatedTerminalTitle />
|
||||
<GlobalKeybindingHandlers />
|
||||
<ScrollKeybindingHandler />
|
||||
<CancelRequestHandler />
|
||||
<FullscreenLayout
|
||||
scrollable={<Messages />}
|
||||
bottom={<TranscriptSearchBar /> 或 <TranscriptModeFooter />}
|
||||
/>
|
||||
</KeybindingSetup>
|
||||
```
|
||||
|
||||
#### Prompt 模式(第 4552-5009 行)
|
||||
|
||||
主交互界面,从上到下:
|
||||
|
||||
```tsx
|
||||
<KeybindingSetup>
|
||||
<AnimatedTerminalTitle /> // 终端 tab 标题
|
||||
<GlobalKeybindingHandlers /> // 全局快捷键
|
||||
<CommandKeybindingHandlers /> // 命令快捷键
|
||||
<ScrollKeybindingHandler /> // 滚动快捷键
|
||||
<CancelRequestHandler /> // Ctrl+C 取消
|
||||
<MCPConnectionManager> // MCP 连接管理
|
||||
<FullscreenLayout
|
||||
overlay={<PermissionRequest />} // 权限审批覆盖层
|
||||
scrollable={ // 可滚动区域
|
||||
<>
|
||||
<Messages /> // ★ 对话消息渲染
|
||||
<UserTextMessage /> // 用户输入占位
|
||||
{toolJSX} // 工具 UI
|
||||
<SpinnerWithVerb /> // 加载动画
|
||||
</>
|
||||
}
|
||||
bottom={ // 固定底部
|
||||
<>
|
||||
{/* 各种对话框 */}
|
||||
<SandboxPermissionRequest />
|
||||
<PromptDialog />
|
||||
<ElicitationDialog />
|
||||
<CostThresholdDialog />
|
||||
<FeedbackSurvey />
|
||||
|
||||
{/* ★ 用户输入框 */}
|
||||
<PromptInput
|
||||
onSubmit={onSubmit}
|
||||
commands={commands}
|
||||
isLoading={isLoading}
|
||||
messages={messages}
|
||||
// ... 20+ props
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</MCPConnectionManager>
|
||||
</KeybindingSetup>
|
||||
```
|
||||
|
||||
### 4.7 REPL.tsx 学习建议
|
||||
|
||||
- 核心只有一条线:`onSubmit → onQuery → query() → onQueryEvent → 更新消息`
|
||||
- 其余 4000+ 行是 UI 细节:快捷键、对话框、动画、边界情况处理
|
||||
- `feature('...')` 保护的 JSX 全部跳过
|
||||
- `("external" as string) === 'ant'` 的分支也跳过
|
||||
|
||||
---
|
||||
|
||||
## 关键设计模式总结
|
||||
|
||||
| 模式 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| 快速路径 | cli.tsx | 按开销从低到高逐级检查,零开销处理简单请求 |
|
||||
| 动态 import | cli.tsx / main.tsx | `await import()` 延迟加载,每条路径只加载需要的模块 |
|
||||
| Side-effect import | main.tsx 顶部 | import 阶段就并行启动子进程,用并行隐藏延迟 |
|
||||
| feature flag | 全局 | `feature()` 永远返回 false,编译时消除死代码 |
|
||||
| preAction 钩子 | main.tsx run() | Commander.js 命令执行前统一初始化 |
|
||||
| QueryGuard | REPL.tsx | 状态机防止并发 API 请求,带 generation 计数防竞态 |
|
||||
| React/Ink | UI 层 | 用 React 组件模型渲染终端 UI,支持全屏和虚拟滚动 |
|
||||
|
||||
## 需要忽略的代码模式
|
||||
|
||||
| 模式 | 来源 | 说明 |
|
||||
|------|------|------|
|
||||
| `_c(N)` 调用 | React Compiler | 反编译产生的 memoization 样板代码 |
|
||||
| `feature('FLAG')` 后面的代码 | Bun bundler | 全部是死代码,在当前版本不会执行 |
|
||||
| `("external" as string) === 'ant'` | 构建目标检查 | 永远为 false(external !== ant) |
|
||||
| tsc 类型错误 | 反编译 | `unknown`/`never`/`{}` 类型,不影响 Bun 运行 |
|
||||
| `packages/@ant/` | stub 包 | 空实现,仅满足 import 依赖 |
|
||||
@@ -1,774 +0,0 @@
|
||||
# 第二阶段:核心对话循环详解
|
||||
|
||||
> 用户发一句话后,如何变成 API 请求、如何处理流式响应和工具调用
|
||||
|
||||
## 对话循环总览
|
||||
|
||||
```
|
||||
用户输入 "帮我读取 README.md"
|
||||
│
|
||||
▼
|
||||
REPL.tsx: onSubmit → onQuery → onQueryImpl
|
||||
│
|
||||
├── 1. 并行加载上下文:
|
||||
│ getSystemPrompt() + getUserContext() + getSystemContext()
|
||||
│
|
||||
├── 2. buildEffectiveSystemPrompt() — 合成最终系统提示
|
||||
│
|
||||
├── 3. for await (const event of query({...})) ★ 核心循环
|
||||
│ │
|
||||
│ │ query.ts: queryLoop()
|
||||
│ │ ├── while (true) {
|
||||
│ │ │ ├── autocompact / microcompact 处理
|
||||
│ │ │ ├── deps.callModel() → claude.ts 流式 API 调用
|
||||
│ │ │ │ └── for await (message of stream) { yield message }
|
||||
│ │ │ │
|
||||
│ │ │ ├── 收集 assistant 消息中的 tool_use 块
|
||||
│ │ │ │
|
||||
│ │ │ ├── needsFollowUp?
|
||||
│ │ │ │ ├── true → 执行工具 → 收集结果 → state = next → continue
|
||||
│ │ │ │ └── false → 检查错误恢复 → return { reason: 'completed' }
|
||||
│ │ │ }
|
||||
│ │
|
||||
│ └── onQueryEvent(event) — 更新 UI 状态
|
||||
│
|
||||
└── 4. 收尾: resetLoadingState(), onTurnComplete()
|
||||
```
|
||||
|
||||
### 两条数据路径
|
||||
|
||||
| 路径 | 调用方 | 说明 |
|
||||
|------|--------|------|
|
||||
| **交互式(REPL)** | REPL.tsx → `query()` | 直接调用 `query()` AsyncGenerator |
|
||||
| **非交互式(SDK/print)** | print.ts → `QueryEngine.submitMessage()` → `query()` | 通过 QueryEngine 包装,增加了会话持久化、usage 跟踪等 |
|
||||
|
||||
---
|
||||
|
||||
## 1. query.ts(1732 行)— 核心查询循环
|
||||
|
||||
**文件路径**: `src/query.ts`
|
||||
|
||||
### 1.1 文件结构
|
||||
|
||||
```
|
||||
query.ts (1732 行)
|
||||
├── [0-120] Import 区 + feature flag 条件模块加载
|
||||
├── [122-148] yieldMissingToolResultBlocks() — 为未配对的 tool_use 生成错误 tool_result
|
||||
├── [150-178] 常量与辅助函数 (MAX_OUTPUT_TOKENS_RECOVERY_LIMIT, isWithheldMaxOutputTokens)
|
||||
├── [180-198] QueryParams 类型定义
|
||||
├── [200-216] State 类型 — 循环迭代间的可变状态
|
||||
├── [218-238] query() — 导出的 AsyncGenerator,委托给 queryLoop()
|
||||
├── [240-1732] queryLoop() — 核心 while(true) 循环
|
||||
│ ├── [241-306] 初始化 State + 内存预取
|
||||
│ ├── [307-448] 循环开头:解构 state、消息预处理(snip/microcompact/context collapse)
|
||||
│ ├── [449-578] 系统提示构建(第449行) + autocompact(第453行) + StreamingToolExecutor 初始化(第562行)
|
||||
│ ├── [650-866] ★ deps.callModel()(第659行) + 流式响应处理 + tool_use 收集
|
||||
│ ├── [896-956] 错误处理(FallbackTriggeredError、通用错误)
|
||||
│ ├── [1002-1054] 中断处理(abortController.signal.aborted)
|
||||
│ ├── [1065-1360] 无 followUp 时的终止/恢复逻辑
|
||||
│ │ ├── prompt-too-long 恢复
|
||||
│ │ ├── max_output_tokens 恢复(升级 + 多轮)
|
||||
│ │ ├── stop hooks 执行
|
||||
│ │ └── return { reason: 'completed' }
|
||||
│ └── [1360-1732] 有 followUp 时的工具执行 + 下一轮准备
|
||||
│ ├── 工具执行(streaming 或 sequential)
|
||||
│ ├── attachment 注入(排队命令、内存预取、技能发现)
|
||||
│ ├── maxTurns 检查
|
||||
│ └── state = next → continue
|
||||
```
|
||||
|
||||
### 1.2 入口:query() 函数(第 219 行)
|
||||
|
||||
```ts
|
||||
export async function* query(params: QueryParams):
|
||||
AsyncGenerator<StreamEvent | Message | ..., Terminal> {
|
||||
const consumedCommandUuids: string[] = []
|
||||
const terminal = yield* queryLoop(params, consumedCommandUuids)
|
||||
// 通知所有消费的排队命令已完成
|
||||
for (const uuid of consumedCommandUuids) {
|
||||
notifyCommandLifecycle(uuid, 'completed')
|
||||
}
|
||||
return terminal
|
||||
}
|
||||
```
|
||||
|
||||
`query()` 本身很薄,只做两件事:
|
||||
1. 委托给 `queryLoop()` 执行实际逻辑
|
||||
2. 在正常返回后通知排队命令的生命周期
|
||||
|
||||
### 1.3 QueryParams(第 181 行)
|
||||
|
||||
```ts
|
||||
type QueryParams = {
|
||||
messages: Message[] // 当前对话消息
|
||||
systemPrompt: SystemPrompt // 系统提示
|
||||
userContext: { [k: string]: string } // 用户上下文(CLAUDE.md 等)
|
||||
systemContext: { [k: string]: string } // 系统上下文(git 状态等)
|
||||
canUseTool: CanUseToolFn // 工具权限检查函数
|
||||
toolUseContext: ToolUseContext // 工具执行上下文
|
||||
fallbackModel?: string // 备用模型
|
||||
querySource: QuerySource // 查询来源标识
|
||||
maxTurns?: number // 最大轮次限制
|
||||
taskBudget?: { total: number } // 令牌预算
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 State — 循环迭代间的可变状态(第 204 行)
|
||||
|
||||
```ts
|
||||
type State = {
|
||||
messages: Message[] // 累积的消息列表
|
||||
toolUseContext: ToolUseContext // 工具执行上下文
|
||||
autoCompactTracking: ... // 自动压缩跟踪
|
||||
maxOutputTokensRecoveryCount: number // 输出令牌恢复尝试次数
|
||||
hasAttemptedReactiveCompact: boolean // 是否已尝试响应式压缩
|
||||
maxOutputTokensOverride: number | undefined // 输出令牌覆盖
|
||||
pendingToolUseSummary: Promise<...> // 待处理的工具使用摘要
|
||||
stopHookActive: boolean | undefined // stop hook 是否活跃
|
||||
turnCount: number // 当前轮次
|
||||
transition: Continue | undefined // 上一次迭代为何 continue
|
||||
}
|
||||
```
|
||||
|
||||
**设计关键**:每次 `continue` 时通过 `state = { ... }` 一次性更新所有状态,而不是分散的 9 个赋值。`transition` 字段记录了为什么要继续循环(便于调试和测试)。
|
||||
|
||||
### 1.5 queryLoop() 核心流程(第 241 行)
|
||||
|
||||
`while (true)` 循环(第 307 行)的每次迭代代表一次 API 调用。循环直到:
|
||||
- 模型不需要工具调用 → `return { reason: 'completed' }`
|
||||
- 被用户中断 → `return { reason: 'aborted_*' }`
|
||||
- 达到最大轮次 → `return { reason: 'max_turns' }`
|
||||
- 遇到不可恢复的错误 → `return { reason: 'model_error' }`
|
||||
|
||||
#### 步骤 1:消息预处理
|
||||
|
||||
```
|
||||
每次迭代开头:
|
||||
├── 解构 state → messages, toolUseContext, tracking, ...
|
||||
├── getMessagesAfterCompactBoundary() — 只保留压缩边界后的消息
|
||||
├── snip 处理(feature flag,跳过)
|
||||
├── microcompact 处理(feature flag,跳过)
|
||||
└── autocompact 检查 — 消息过长时自动压缩
|
||||
```
|
||||
|
||||
#### 步骤 2:系统提示构建(第 449 行)
|
||||
|
||||
```ts
|
||||
const fullSystemPrompt = asSystemPrompt(
|
||||
appendSystemContext(systemPrompt, systemContext),
|
||||
)
|
||||
```
|
||||
|
||||
将系统上下文(git 状态、日期等)追加到系统提示。注意:用户上下文(CLAUDE.md 等)不在这里注入,而是在 `deps.callModel()` 调用时通过 `prependUserContext(messagesForQuery, userContext)` 注入到消息数组的最前面(第 660 行)。
|
||||
|
||||
#### 步骤 3:Autocompact(第 454-543 行)
|
||||
|
||||
当消息历史过长时自动压缩:
|
||||
|
||||
```
|
||||
autocompact 流程:
|
||||
├── 检查 token 数量是否超过阈值
|
||||
├── 超过 → 调用 compact API(用 Haiku 总结历史)
|
||||
│ ├── yield compactBoundaryMessage ← 标记压缩边界
|
||||
│ └── 更新 messages 为压缩后的版本
|
||||
└── 未超过 → 继续
|
||||
```
|
||||
|
||||
#### 步骤 4:调用 API(第 559-708 行)— 核心
|
||||
|
||||
StreamingToolExecutor 在第 562 行初始化,API 调用在第 659 行开始:
|
||||
|
||||
```ts
|
||||
// 第 562 行:初始化流式工具执行器
|
||||
let streamingToolExecutor = useStreamingToolExecution
|
||||
? new StreamingToolExecutor(
|
||||
toolUseContext.options.tools, canUseTool, toolUseContext,
|
||||
)
|
||||
: null
|
||||
|
||||
// 第 659 行:调用 API
|
||||
for await (const message of deps.callModel({
|
||||
messages: prependUserContext(messagesForQuery, userContext), // ← 用户上下文注入到消息最前面
|
||||
systemPrompt: fullSystemPrompt,
|
||||
thinkingConfig: toolUseContext.options.thinkingConfig,
|
||||
tools: toolUseContext.options.tools,
|
||||
signal: toolUseContext.abortController.signal,
|
||||
options: { model: currentModel, querySource, fallbackModel, ... }
|
||||
})) {
|
||||
// 处理每条流式消息(第 708-866 行)
|
||||
}
|
||||
```
|
||||
|
||||
`deps.callModel()` 最终调用 `claude.ts` 的 `queryModelWithStreaming()`。
|
||||
|
||||
#### 步骤 5:流式响应处理(第 708-866 行)
|
||||
|
||||
处理逻辑在 `for await` 循环体内(第 708 行的 `})` 之后到第 866 行):
|
||||
|
||||
```
|
||||
for await (const message of stream):
|
||||
├── message.type === 'assistant'?
|
||||
│ ├── 记录到 assistantMessages[]
|
||||
│ ├── 提取 tool_use 块 → toolUseBlocks[]
|
||||
│ ├── needsFollowUp = true(如果有 tool_use)
|
||||
│ └── streamingToolExecutor.addTool() ← 流式工具并行执行
|
||||
│
|
||||
├── withheld? (prompt-too-long / max_output_tokens)
|
||||
│ └── 暂扣不 yield,等后面恢复逻辑处理
|
||||
│
|
||||
└── yield message ← 正常 yield 给上层(REPL/QueryEngine)
|
||||
```
|
||||
|
||||
**StreamingToolExecutor**:在 API 流式返回的同时就开始执行工具(如读文件),不等流结束。通过 `addTool()` 添加待执行工具,`getCompletedResults()` 获取已完成的结果。
|
||||
|
||||
#### 步骤 6A:无 followUp — 终止/恢复(第 1065-1360 行)
|
||||
|
||||
当模型没有请求工具调用时(`needsFollowUp === false`):
|
||||
|
||||
```
|
||||
无 followUp:
|
||||
├── prompt-too-long 恢复?
|
||||
│ ├── context collapse drain(feature flag,跳过)
|
||||
│ ├── reactive compact → 压缩消息重试
|
||||
│ └── 都失败 → yield 错误 + return
|
||||
│
|
||||
├── max_output_tokens 恢复?
|
||||
│ ├── 第一次 → 升级到 64k token 限制,continue
|
||||
│ ├── 后续 → 注入恢复消息("继续,别道歉"),continue
|
||||
│ └── 超过 3 次 → yield 错误 + return
|
||||
│
|
||||
├── stop hooks 执行
|
||||
│ ├── preventContinuation? → return
|
||||
│ └── blockingErrors? → 将错误加入消息,continue
|
||||
│
|
||||
└── return { reason: 'completed' } ★ 正常结束
|
||||
```
|
||||
|
||||
**恢复消息内容(第 1229 行)**:
|
||||
```
|
||||
"Output token limit hit. Resume directly — no apology, no recap of what
|
||||
you were doing. Pick up mid-thought if that is where the cut happened.
|
||||
Break remaining work into smaller pieces."
|
||||
```
|
||||
|
||||
#### 步骤 6B:有 followUp — 工具执行 + 下一轮(第 1363-1731 行)
|
||||
|
||||
当模型请求了工具调用时(`needsFollowUp === true`):
|
||||
|
||||
```
|
||||
有 followUp:
|
||||
├── 工具执行(两种模式)
|
||||
│ ├── streamingToolExecutor? → getRemainingResults()(流式已启动)
|
||||
│ └── 否 → runTools()(传统顺序执行)
|
||||
│
|
||||
├── for await (const update of toolUpdates):
|
||||
│ ├── yield update.message ← 工具结果消息
|
||||
│ └── toolResults.push(...) ← 收集工具结果
|
||||
│
|
||||
├── 中断检查(abortController.signal.aborted)
|
||||
│ └── return { reason: 'aborted_tools' }
|
||||
│
|
||||
├── attachment 注入
|
||||
│ ├── 排队命令(其他线程提交的消息)
|
||||
│ ├── 内存预取(相关记忆文件)
|
||||
│ └── 技能发现预取
|
||||
│
|
||||
├── maxTurns 检查
|
||||
│ └── 超过 → yield max_turns_reached + return
|
||||
│
|
||||
└── state = { messages: [...old, ...assistant, ...toolResults], turnCount: +1 }
|
||||
→ continue ★ 回到循环顶部,发起下一次 API 调用
|
||||
```
|
||||
|
||||
### 1.6 错误处理与模型降级(第 897-956 行)
|
||||
|
||||
```
|
||||
API 调用出错:
|
||||
├── FallbackTriggeredError(529 过载)?
|
||||
│ ├── 切换到 fallbackModel
|
||||
│ ├── 清空本轮 assistant/tool 消息
|
||||
│ ├── yield 系统消息 "Switched to X due to high demand for Y"
|
||||
│ └── continue(重试整个请求)
|
||||
│
|
||||
└── 其他错误
|
||||
├── ImageSizeError/ImageResizeError → yield 友好错误 + return
|
||||
├── yieldMissingToolResultBlocks() — 补全未配对的 tool_result
|
||||
└── yield API 错误消息 + return
|
||||
```
|
||||
|
||||
### 1.7 关键设计思想
|
||||
|
||||
| 设计 | 说明 |
|
||||
|------|------|
|
||||
| **AsyncGenerator 模式** | `query()` 是 `async function*`,通过 `yield` 逐条产出事件,调用者用 `for await` 消费 |
|
||||
| **while(true) + state 对象** | 每次 `continue` 构建新 State 对象,避免分散的状态修改 |
|
||||
| **transition 字段** | 记录为什么要 continue(`next_turn`、`max_output_tokens_recovery`、`reactive_compact_retry`...),便于调试 |
|
||||
| **StreamingToolExecutor** | API 流式返回时就并行执行工具,不等流结束 |
|
||||
| **Withheld 消息** | 可恢复错误先暂扣,恢复成功则不 yield 错误,失败才 yield |
|
||||
|
||||
---
|
||||
|
||||
## 2. QueryEngine.ts(1320 行)— 高层编排器
|
||||
|
||||
**文件路径**: `src/QueryEngine.ts`
|
||||
|
||||
### 2.1 定位
|
||||
|
||||
QueryEngine 是 `query()` 的**上层包装**,主要用于:
|
||||
- **print 模式**(`claude -p`):通过 `ask()` → `QueryEngine.submitMessage()`
|
||||
- **SDK 模式**:外部程序通过 SDK 调用
|
||||
- **REPL 不用它**:REPL 直接调用 `query()`
|
||||
|
||||
### 2.2 文件结构
|
||||
|
||||
```
|
||||
QueryEngine.ts (1320 行)
|
||||
├── [0-130] Import 区 + feature flag 条件模块
|
||||
├── [131-174] QueryEngineConfig 类型定义
|
||||
├── [185-1202] QueryEngine 类
|
||||
│ ├── [185-208] 成员变量 + constructor
|
||||
│ ├── [210-1181] submitMessage() — 核心方法(~970 行)
|
||||
│ │ ├── [210-400] 参数解析 + processUserInputContext 构建
|
||||
│ │ ├── [400-465] 用户输入处理 + 会话持久化
|
||||
│ │ ├── [465-660] 斜杠命令处理 + 无需查询的快速返回
|
||||
│ │ ├── [660-690] 文件历史快照
|
||||
│ │ ├── [679-1074] ★ for await (const message of query({...})) — 消费 query()
|
||||
│ │ └── [1074-1181] 结果提取 + yield result
|
||||
│ ├── [1183-1202] interrupt() / getMessages() / setModel() 辅助方法
|
||||
├── [1210-1320] ask() — 便捷包装函数
|
||||
```
|
||||
|
||||
### 2.3 QueryEngineConfig
|
||||
|
||||
```ts
|
||||
type QueryEngineConfig = {
|
||||
cwd: string // 工作目录
|
||||
tools: Tools // 工具列表
|
||||
commands: Command[] // 斜杠命令
|
||||
mcpClients: MCPServerConnection[] // MCP 服务器连接
|
||||
agents: AgentDefinition[] // Agent 定义
|
||||
canUseTool: CanUseToolFn // 权限检查
|
||||
getAppState / setAppState // 全局状态存取
|
||||
initialMessages?: Message[] // 初始消息(恢复对话)
|
||||
readFileCache: FileStateCache // 文件读取缓存
|
||||
customSystemPrompt?: string // 自定义系统提示
|
||||
thinkingConfig?: ThinkingConfig // 思考模式配置
|
||||
maxTurns?: number // 最大轮次
|
||||
maxBudgetUsd?: number // USD 预算上限
|
||||
jsonSchema?: Record<...> // 结构化输出 schema
|
||||
// ... 更多配置
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 submitMessage() 核心流程
|
||||
|
||||
```
|
||||
submitMessage(prompt)
|
||||
│
|
||||
├── 1. 参数准备
|
||||
│ ├── 解构 config 获取 tools, commands, model, ...
|
||||
│ ├── 构建 wrappedCanUseTool(包装权限检查,跟踪拒绝)
|
||||
│ ├── fetchSystemPromptParts() — 获取系统提示各部分
|
||||
│ └── 构建 processUserInputContext
|
||||
│
|
||||
├── 2. 用户输入处理
|
||||
│ ├── processUserInput(prompt) — 解析斜杠命令 / 普通文本
|
||||
│ ├── mutableMessages.push(...messagesFromUserInput)
|
||||
│ └── recordTranscript(messages) — 持久化到 JSONL
|
||||
│
|
||||
├── 3. yield buildSystemInitMessage() — SDK 初始化消息
|
||||
│
|
||||
├── 4. shouldQuery === false?(斜杠命令的本地执行结果)
|
||||
│ ├── yield 命令输出
|
||||
│ ├── yield { type: 'result', subtype: 'success' }
|
||||
│ └── return
|
||||
│
|
||||
├── 5. ★ for await (const message of query({...}))
|
||||
│ │ 消费 query() 产出的每条消息
|
||||
│ │
|
||||
│ ├── message.type === 'assistant'
|
||||
│ │ ├── mutableMessages.push(msg)
|
||||
│ │ ├── recordTranscript() ← fire-and-forget
|
||||
│ │ ├── yield* normalizeMessage(msg) — 转换为 SDK 格式
|
||||
│ │ └── 捕获 stop_reason
|
||||
│ │
|
||||
│ ├── message.type === 'user'(工具结果)
|
||||
│ │ ├── mutableMessages.push(msg)
|
||||
│ │ ├── turnCount++
|
||||
│ │ └── yield* normalizeMessage(msg)
|
||||
│ │
|
||||
│ ├── message.type === 'stream_event'
|
||||
│ │ ├── 跟踪 usage(message_start/delta/stop)
|
||||
│ │ └── includePartialMessages? → yield 流事件
|
||||
│ │
|
||||
│ ├── message.type === 'system'
|
||||
│ │ ├── compact_boundary → GC 旧消息 + yield 给 SDK
|
||||
│ │ └── api_error → yield 重试信息
|
||||
│ │
|
||||
│ └── maxBudgetUsd 检查 → 超预算则 yield error + return
|
||||
│
|
||||
└── 6. yield { type: 'result', subtype: 'success', result: textResult }
|
||||
```
|
||||
|
||||
### 2.5 ask() 便捷函数(第 1211 行)
|
||||
|
||||
```ts
|
||||
export async function* ask({ prompt, tools, ... }) {
|
||||
const engine = new QueryEngine({ ... })
|
||||
try {
|
||||
yield* engine.submitMessage(prompt)
|
||||
} finally {
|
||||
setReadFileCache(engine.getReadFileState())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ask()` 是 `QueryEngine` 的一次性包装,创建 engine → 提交消息 → 清理。用于 `print.ts` 的 `--print` 模式。
|
||||
|
||||
### 2.6 QueryEngine vs REPL 直接调用 query()
|
||||
|
||||
| 特性 | QueryEngine (SDK/print) | REPL 直接调用 query() |
|
||||
|------|------------------------|---------------------|
|
||||
| 会话持久化 | 自动 recordTranscript | 由 useLogMessages 处理 |
|
||||
| Usage 跟踪 | 内部 totalUsage 累积 | 由外层 cost-tracker 处理 |
|
||||
| 权限拒绝跟踪 | 记录 permissionDenials[] | 直接 UI 交互 |
|
||||
| 结果格式 | yield SDKMessage 格式 | 原始 Message 格式 |
|
||||
| 消息 GC | compact_boundary 后释放旧消息 | UI 需要保留完整历史 |
|
||||
|
||||
---
|
||||
|
||||
## 3. claude.ts(3420 行)— API 客户端
|
||||
|
||||
**文件路径**: `src/services/api/claude.ts`
|
||||
|
||||
### 3.1 文件结构
|
||||
|
||||
```
|
||||
claude.ts (3420 行)
|
||||
├── [0-260] Import 区(大量 SDK 类型、工具函数)
|
||||
├── [272-331] getExtraBodyParams() — 构建额外请求体参数
|
||||
├── [333-502] 缓存相关(getPromptCachingEnabled, getCacheControl, should1hCacheTTL, configureEffortParams, configureTaskBudgetParams)
|
||||
├── [504-587] verifyApiKey() — API 密钥验证
|
||||
├── [589-675] 消息转换(userMessageToMessageParam, assistantMessageToMessageParam)
|
||||
├── [677-708] Options 类型定义
|
||||
├── [710-781] queryModelWithoutStreaming / queryModelWithStreaming — 公开的两个入口
|
||||
├── [783-813] 辅助函数(shouldDeferLspTool, getNonstreamingFallbackTimeoutMs)
|
||||
├── [819-918] executeNonStreamingRequest() — 非流式请求辅助
|
||||
├── [920-999] 更多辅助函数(getPreviousRequestIdFromMessages, stripExcessMediaItems)
|
||||
├── [1018-3420] ★ queryModel() — 核心私有函数(2400 行)
|
||||
│ ├── [1018-1370] 前置检查 + 工具 schema 构建 + 消息归一化 + 系统提示组装
|
||||
│ ├── [1539-1730] paramsFromContext() — 构建 API 请求参数
|
||||
│ ├── [1777-2100] withRetry + 流式 API 调用(anthropic.beta.messages.create + stream)
|
||||
│ ├── [1941-2300] 流式事件处理(for await of stream)
|
||||
│ └── [2300-3420] 非流式降级 + 日志、分析、清理
|
||||
```
|
||||
|
||||
### 3.2 两个公开入口
|
||||
|
||||
```ts
|
||||
// 入口 1:流式(主要路径)
|
||||
export async function* queryModelWithStreaming({
|
||||
messages, systemPrompt, thinkingConfig, tools, signal, options
|
||||
}) {
|
||||
yield* withStreamingVCR(messages, async function* () {
|
||||
yield* queryModel(messages, systemPrompt, thinkingConfig, tools, signal, options)
|
||||
})
|
||||
}
|
||||
|
||||
// 入口 2:非流式(compact 等内部用途)
|
||||
export async function queryModelWithoutStreaming({
|
||||
messages, systemPrompt, thinkingConfig, tools, signal, options
|
||||
}) {
|
||||
let assistantMessage
|
||||
for await (const message of ...) {
|
||||
if (message.type === 'assistant') assistantMessage = message
|
||||
}
|
||||
return assistantMessage
|
||||
}
|
||||
```
|
||||
|
||||
两者都委托给内部的 `queryModel()`。`withStreamingVCR` 是一个 VCR(录像/回放)包装器,用于调试。
|
||||
|
||||
### 3.3 Options 类型(第 677 行)
|
||||
|
||||
```ts
|
||||
type Options = {
|
||||
getToolPermissionContext: () => Promise<ToolPermissionContext>
|
||||
model: string // 模型名称
|
||||
toolChoice?: BetaToolChoiceTool // 强制使用特定工具
|
||||
isNonInteractiveSession: boolean // 是否非交互模式
|
||||
fallbackModel?: string // 备用模型
|
||||
querySource: QuerySource // 查询来源
|
||||
agents: AgentDefinition[] // Agent 定义
|
||||
enablePromptCaching?: boolean // 启用提示缓存
|
||||
effortValue?: EffortValue // 推理努力级别
|
||||
mcpTools: Tools // MCP 工具
|
||||
fastMode?: boolean // 快速模式
|
||||
taskBudget?: { total: number; remaining?: number } // 令牌预算
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 queryModel() 核心流程(第 1018 行)
|
||||
|
||||
这是整个 API 调用的核心,2400 行。关键步骤:
|
||||
|
||||
#### 阶段 1:前置准备(1018-1400 行)
|
||||
|
||||
```
|
||||
queryModel()
|
||||
├── off-switch 检查(Opus 过载时的全局关闭开关)
|
||||
├── beta headers 组装(getMergedBetas)
|
||||
│ ├── 基础 betas
|
||||
│ ├── advisor beta(如果启用)
|
||||
│ ├── tool search beta(如果启用)
|
||||
│ ├── cache scope beta
|
||||
│ └── effort / task budget betas
|
||||
│
|
||||
├── 工具过滤
|
||||
│ ├── tool search 启用 → 只包含已发现的 deferred tools
|
||||
│ └── tool search 未启用 → 过滤掉 ToolSearchTool
|
||||
│
|
||||
├── toolToAPISchema() — 每个工具转为 API 格式
|
||||
│
|
||||
├── normalizeMessagesForAPI() — 消息转换为 API 格式
|
||||
│ ├── UserMessage → { role: 'user', content: ... }
|
||||
│ ├── AssistantMessage → { role: 'assistant', content: ... }
|
||||
│ └── 跳过 system/attachment/progress 等内部消息类型
|
||||
│
|
||||
└── 系统提示最终组装
|
||||
├── getAttributionHeader(fingerprint)
|
||||
├── getCLISyspromptPrefix()
|
||||
├── ...systemPrompt
|
||||
└── advisor 指令(如果启用)
|
||||
```
|
||||
|
||||
#### 阶段 2:构建请求参数 — paramsFromContext()(第 1539-1730 行)
|
||||
|
||||
```ts
|
||||
const paramsFromContext = (retryContext: RetryContext) => {
|
||||
// ... 动态 beta headers、effort、task budget 配置 ...
|
||||
|
||||
// 思考模式配置(adaptive 或 enabled + budget)
|
||||
let thinking = undefined
|
||||
if (hasThinking && modelSupportsThinking(options.model)) {
|
||||
if (modelSupportsAdaptiveThinking(options.model)) {
|
||||
thinking = { type: 'adaptive' }
|
||||
} else {
|
||||
thinking = { type: 'enabled', budget_tokens: thinkingBudget }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
model: normalizeModelStringForAPI(options.model),
|
||||
messages: addCacheBreakpoints(messagesForAPI, ...), // 带缓存标记的消息
|
||||
system, // 系统提示块(已构建好)
|
||||
tools: allTools, // 工具 schema
|
||||
tool_choice: options.toolChoice,
|
||||
max_tokens: maxOutputTokens,
|
||||
thinking,
|
||||
...(temperature !== undefined && { temperature }),
|
||||
...(useBetas && { betas: betasParams }),
|
||||
metadata: getAPIMetadata(),
|
||||
...extraBodyParams,
|
||||
...(speed !== undefined && { speed }), // 快速模式
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 阶段 3:流式 API 调用(第 1779-1858 行)
|
||||
|
||||
```ts
|
||||
// 使用 withRetry 包装,自动处理重试
|
||||
const generator = withRetry(
|
||||
() => getAnthropicClient({ maxRetries: 0, model, source: querySource }),
|
||||
async (anthropic, attempt, context) => {
|
||||
const params = paramsFromContext(context)
|
||||
|
||||
// ★ 核心 API 调用(第 1823 行)
|
||||
// 使用 .create() + stream: true(而非 .stream())
|
||||
// 避免 BetaMessageStream 的 O(n²) partial JSON 解析开销
|
||||
const result = await anthropic.beta.messages
|
||||
.create(
|
||||
{ ...params, stream: true },
|
||||
{ signal, ...(clientRequestId && { headers: { ... } }) },
|
||||
)
|
||||
.withResponse()
|
||||
|
||||
return result.data // Stream<BetaRawMessageStreamEvent>
|
||||
},
|
||||
{ model, fallbackModel, thinkingConfig, signal, querySource }
|
||||
)
|
||||
|
||||
// 消费 withRetry 的系统错误消息(重试通知等)
|
||||
let e
|
||||
do {
|
||||
e = await generator.next()
|
||||
if (!('controller' in e.value)) yield e.value // yield API 错误消息
|
||||
} while (!e.done)
|
||||
stream = e.value // 获取最终的 Stream 对象
|
||||
|
||||
// 处理流式事件(第 1941 行)
|
||||
for await (const part of stream) {
|
||||
switch (part.type) {
|
||||
case 'message_start': // 记录 request_id、usage
|
||||
case 'content_block_start': // 新的内容块开始(text/thinking/tool_use)
|
||||
case 'content_block_delta': // 增量内容 → yield stream_event 给 UI
|
||||
case 'content_block_stop': // 内容块完成 → yield AssistantMessage
|
||||
case 'message_delta': // stop_reason、usage 更新
|
||||
case 'message_stop': // 整条消息完成
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 阶段 4:withRetry 重试策略
|
||||
|
||||
```
|
||||
withRetry 逻辑:
|
||||
├── 429 (Rate Limit) → 等待 Retry-After 后重试
|
||||
├── 529 (Overloaded) → 切换到 fallbackModel,throw FallbackTriggeredError
|
||||
├── 500 (Server Error) → 指数退避重试
|
||||
├── 408 (Timeout) → 重试
|
||||
├── 其他错误 → 不重试,直接抛出
|
||||
└── 最大重试次数: 根据模型和错误类型动态计算
|
||||
```
|
||||
|
||||
#### 阶段 5:非流式降级
|
||||
|
||||
当流式请求中途失败时,可能降级为非流式请求:
|
||||
|
||||
```
|
||||
流式失败(部分响应已收到):
|
||||
├── 已接收的内容 → yield 给上层
|
||||
├── 剩余部分 → 降级为非流式请求(anthropic.beta.messages.create)
|
||||
└── 非流式结果 → 转换格式 yield
|
||||
```
|
||||
|
||||
### 3.5 消息转换函数
|
||||
|
||||
```ts
|
||||
// UserMessage → API 格式
|
||||
userMessageToMessageParam(message, addCache, enablePromptCaching, querySource)
|
||||
→ { role: 'user', content: [...] }
|
||||
// addCache=true 时最后一个 content block 添加 cache_control
|
||||
|
||||
// AssistantMessage → API 格式
|
||||
assistantMessageToMessageParam(message, addCache, enablePromptCaching, querySource)
|
||||
→ { role: 'assistant', content: [...] }
|
||||
// thinking/redacted_thinking 块不加 cache_control
|
||||
```
|
||||
|
||||
### 3.6 Prompt Caching 策略
|
||||
|
||||
```
|
||||
缓存策略:
|
||||
├── cache_control: { type: 'ephemeral' } — 默认,5 分钟 TTL
|
||||
├── cache_control: { type: 'ephemeral', ttl: '1h' } — 订阅用户/Ant,1 小时
|
||||
├── cache_control: { ..., scope: 'global' } — 跨会话共享(无 MCP 工具时)
|
||||
└── 禁用条件:
|
||||
├── DISABLE_PROMPT_CACHING 环境变量
|
||||
├── DISABLE_PROMPT_CACHING_HAIKU(仅 Haiku)
|
||||
└── DISABLE_PROMPT_CACHING_SONNET(仅 Sonnet)
|
||||
```
|
||||
|
||||
### 3.7 多 Provider 支持
|
||||
|
||||
`getAnthropicClient()` 根据配置返回不同的 SDK 客户端:
|
||||
|
||||
| Provider | 入口 | 说明 |
|
||||
|----------|------|------|
|
||||
| Anthropic | 直接 API | 默认,`api.anthropic.com` |
|
||||
| AWS Bedrock | 通过 Bedrock | 使用 `@anthropic-ai/bedrock-sdk` |
|
||||
| Google Vertex | 通过 Vertex | 使用 `@anthropic-ai/vertex-sdk` |
|
||||
| Azure | 通过 Azure | 类似 Bedrock 的包装 |
|
||||
|
||||
Provider 选择逻辑在 `src/utils/model/providers.ts` 的 `getAPIProvider()` 中。
|
||||
|
||||
---
|
||||
|
||||
## 完整数据流:一次工具调用的生命周期
|
||||
|
||||
以用户输入 "读取 README.md" 为例:
|
||||
|
||||
```
|
||||
1. REPL.tsx: 用户按回车
|
||||
onSubmit("读取 README.md")
|
||||
└── handlePromptSubmit()
|
||||
└── onQuery([userMessage])
|
||||
|
||||
2. REPL.tsx: onQueryImpl()
|
||||
├── getSystemPrompt() + getUserContext() + getSystemContext()
|
||||
└── for await (event of query({messages, systemPrompt, ...}))
|
||||
|
||||
3. query.ts: queryLoop() — 第 1 次迭代
|
||||
├── messagesForQuery = [...messages] // 包含用户消息
|
||||
├── deps.callModel({...})
|
||||
│ └── claude.ts: queryModel()
|
||||
│ ├── 构建 API 参数
|
||||
│ └── anthropic.beta.messages.create({ ...params, stream: true })
|
||||
│
|
||||
├── API 流式返回:
|
||||
│ content_block_start: { type: 'tool_use', name: 'Read', id: 'toolu_123' }
|
||||
│ content_block_delta: { input: '{"file_path": "/path/to/README.md"}' }
|
||||
│ content_block_stop
|
||||
│ message_delta: { stop_reason: 'tool_use' }
|
||||
│
|
||||
├── 收集: toolUseBlocks = [{ name: 'Read', id: 'toolu_123', input: {...} }]
|
||||
├── needsFollowUp = true
|
||||
│
|
||||
├── 工具执行:
|
||||
│ streamingToolExecutor.getRemainingResults()
|
||||
│ └── Read 工具执行 → 返回文件内容
|
||||
│ yield toolResultMessage ← 包含文件内容
|
||||
│
|
||||
└── state = { messages: [...old, assistantMsg, toolResultMsg], turnCount: 2 }
|
||||
→ continue
|
||||
|
||||
4. query.ts: queryLoop() — 第 2 次迭代
|
||||
├── messagesForQuery 现在包含:
|
||||
│ [userMsg, assistantMsg(tool_use), userMsg(tool_result)]
|
||||
│
|
||||
├── deps.callModel({...}) ← 再次调用 API
|
||||
│
|
||||
├── API 返回:
|
||||
│ content_block_start: { type: 'text' }
|
||||
│ content_block_delta: { text: "README.md 的内容是..." }
|
||||
│ content_block_stop
|
||||
│ message_delta: { stop_reason: 'end_turn' }
|
||||
│
|
||||
├── toolUseBlocks = [] ← 没有工具调用
|
||||
├── needsFollowUp = false
|
||||
│
|
||||
└── return { reason: 'completed' } ★ 循环结束
|
||||
|
||||
5. REPL.tsx: onQueryEvent(event)
|
||||
├── 更新 streamingText(打字机效果)
|
||||
├── 更新 messages 数组
|
||||
└── 重新渲染 UI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键设计模式总结
|
||||
|
||||
| 模式 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| AsyncGenerator 链式传递 | query.ts → claude.ts | `yield*` 将底层事件透传给上层,形成事件流管道 |
|
||||
| while(true) + State 对象 | query.ts queryLoop | 循环迭代间通过不可变 State 传递,transition 字段记录原因 |
|
||||
| StreamingToolExecutor | query.ts | API 流式返回时并行执行工具,不等流结束 |
|
||||
| Withheld 消息 | query.ts | 可恢复错误先暂扣不 yield,恢复成功则吞掉错误 |
|
||||
| withRetry 重试 | claude.ts | 429/500/529 自动重试,529 触发模型降级 |
|
||||
| Prompt Caching | claude.ts | 缓存系统提示和历史消息,减少 API token 消耗 |
|
||||
| 非流式降级 | claude.ts | 流式请求中途失败时降级为非流式完成剩余部分 |
|
||||
| QueryEngine 包装 | QueryEngine.ts | 为 SDK/print 提供会话管理、持久化、usage 跟踪 |
|
||||
|
||||
## 需要忽略的代码
|
||||
|
||||
| 模式 | 说明 |
|
||||
|------|------|
|
||||
| `feature('REACTIVE_COMPACT')` / `feature('CONTEXT_COLLAPSE')` 等 | 所有 feature flag 保护的代码 — 全部是死代码 |
|
||||
| `feature('CACHED_MICROCOMPACT')` | 缓存微压缩 — 死代码 |
|
||||
| `feature('HISTORY_SNIP')` / `snipModule` | 历史截断 — 死代码 |
|
||||
| `feature('TOKEN_BUDGET')` / `budgetTracker` | 令牌预算 — 死代码 |
|
||||
| `feature('BG_SESSIONS')` / `taskSummaryModule` | 后台会话 — 死代码 |
|
||||
| `process.env.USER_TYPE === 'ant'` | Anthropic 内部专用代码 |
|
||||
| VCR (withStreamingVCR/withVCR) | 调试录像/回放包装器,不影响正常流程 |
|
||||
@@ -1,372 +0,0 @@
|
||||
# 第二阶段 Q&A
|
||||
|
||||
## Q1:query.ts 的流式消息处理具体是怎样的?
|
||||
|
||||
**核心问题**:`deps.callModel()` yield 出的每一条消息,在 `queryLoop()` 的 `for await` 循环体(L659-866)中具体经历了什么处理?
|
||||
|
||||
### 场景
|
||||
|
||||
用户说:**"帮我看看 package.json 的内容"**
|
||||
|
||||
模型回复:一段文字 "我来读取文件。" + 一个 Read 工具调用。
|
||||
|
||||
### callModel yield 的完整消息序列
|
||||
|
||||
claude.ts 的 `queryModel()` 会 yield 两种类型的消息:
|
||||
|
||||
| 类型标记 | 含义 | 产出时机 |
|
||||
|---------|------|---------|
|
||||
| `stream_event` | 原始 SSE 事件包装 | 每个 SSE 事件都产出一条 |
|
||||
| `assistant` | 完整的 AssistantMessage | 仅在 `content_block_stop` 时产出 |
|
||||
|
||||
本例中 callModel 依次 yield **共 13 条消息**:
|
||||
|
||||
```
|
||||
#1 { type: 'stream_event', event: { type: 'message_start', ... }, ttftMs: 342 }
|
||||
#2 { type: 'stream_event', event: { type: 'content_block_start', index: 0, content_block: { type: 'text' } } }
|
||||
#3 { type: 'stream_event', event: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '我来' } } }
|
||||
#4 { type: 'stream_event', event: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '读取文件。' } } }
|
||||
#5 { type: 'stream_event', event: { type: 'content_block_stop', index: 0 } }
|
||||
#6 { type: 'assistant', uuid: 'uuid-1', message: { content: [{ type: 'text', text: '我来读取文件。' }], stop_reason: null } }
|
||||
#7 { type: 'stream_event', event: { type: 'content_block_start', index: 1, content_block: { type: 'tool_use', id: 'toolu_001', name: 'Read' } } }
|
||||
#8 { type: 'stream_event', event: { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '{"file_path":' } } }
|
||||
#9 { type: 'stream_event', event: { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '"/path/package.json"}' } } }
|
||||
#10 { type: 'stream_event', event: { type: 'content_block_stop', index: 1 } }
|
||||
#11 { type: 'assistant', uuid: 'uuid-2', message: { content: [{ type: 'tool_use', id: 'toolu_001', name: 'Read', input: { file_path: '/path/package.json' } }], stop_reason: null } }
|
||||
#12 { type: 'stream_event', event: { type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 87 } } }
|
||||
#13 { type: 'stream_event', event: { type: 'message_stop' } }
|
||||
```
|
||||
|
||||
注意 `#6` 和 `#11` 是 **assistant 类型**(content_block_stop 时由 claude.ts 组装),其余全是 **stream_event 类型**。
|
||||
|
||||
### 循环体结构
|
||||
|
||||
循环体在 L708-866,结构如下:
|
||||
|
||||
```
|
||||
for await (const message of deps.callModel({...})) { // L659
|
||||
// A. 降级检查 (L712)
|
||||
// B. backfill (L747-789)
|
||||
// C. withheld 检查 (L801-824)
|
||||
// D. yield (L825-827)
|
||||
// E. assistant 收集 + addTool (L828-848)
|
||||
// F. getCompletedResults (L850-865)
|
||||
}
|
||||
```
|
||||
|
||||
### 逐条走循环体
|
||||
|
||||
#### #1 stream_event (message_start)
|
||||
|
||||
```
|
||||
A. L712: streamingFallbackOccured = false → 跳过
|
||||
|
||||
B. L748: message.type === 'assistant'?
|
||||
→ 'stream_event' !== 'assistant' → 跳过整个 backfill 块
|
||||
|
||||
C. L801-824: withheld 检查
|
||||
→ 不是 assistant 类型,各项检查均为 false → withheld = false
|
||||
|
||||
D. L825: yield message ✅ → 透传给 REPL(REPL 记录 ttftMs)
|
||||
|
||||
E. L828: message.type === 'assistant'? → 否 → 跳过
|
||||
|
||||
F. L850-854: streamingToolExecutor.getCompletedResults()
|
||||
→ tools 数组为空 → 无结果
|
||||
```
|
||||
|
||||
**净效果**:`yield` 透传。
|
||||
|
||||
---
|
||||
|
||||
#### #2 stream_event (content_block_start, type: text)
|
||||
|
||||
```
|
||||
A-C. 同 #1
|
||||
D. yield message ✅ → REPL 设置 spinner 为 "Responding..."
|
||||
E-F. 同 #1
|
||||
```
|
||||
|
||||
**净效果**:`yield` 透传。
|
||||
|
||||
---
|
||||
|
||||
#### #3 stream_event (text_delta: "我来")
|
||||
|
||||
```
|
||||
A-C. 同 #1
|
||||
D. yield message ✅ → REPL 追加 streamingText += "我来"(打字机效果)
|
||||
E-F. 同 #1
|
||||
```
|
||||
|
||||
**净效果**:`yield` 透传。
|
||||
|
||||
---
|
||||
|
||||
#### #4 stream_event (text_delta: "读取文件。")
|
||||
|
||||
```
|
||||
同 #3
|
||||
D. yield message ✅ → REPL streamingText += "读取文件。"
|
||||
```
|
||||
|
||||
**净效果**:`yield` 透传。
|
||||
|
||||
---
|
||||
|
||||
#### #5 stream_event (content_block_stop, index:0)
|
||||
|
||||
```
|
||||
同 #2
|
||||
D. yield message ✅ → REPL 无特殊操作(真正的 AssistantMessage 在下一条 #6)
|
||||
```
|
||||
|
||||
**净效果**:`yield` 透传。
|
||||
|
||||
---
|
||||
|
||||
#### #6 assistant (text block 完整消息) ★
|
||||
|
||||
第一条 `type: 'assistant'` 的消息,走**完全不同的路径**:
|
||||
|
||||
```
|
||||
A. L712: streamingFallbackOccured = false → 跳过
|
||||
|
||||
B. L748: message.type === 'assistant'? → ✅ 进入 backfill
|
||||
L750: contentArr = [{ type: 'text', text: '我来读取文件。' }]
|
||||
L752: for i=0: block.type === 'text'
|
||||
L754: block.type === 'tool_use'? → 否 → 跳过
|
||||
L783: clonedContent 为 undefined → yieldMessage = message(原样不变)
|
||||
|
||||
C. L801: let withheld = false
|
||||
L802: feature('CONTEXT_COLLAPSE') → false → 跳过
|
||||
L813: reactiveCompact?.isWithheldPromptTooLong(message) → 否 → false
|
||||
L822: isWithheldMaxOutputTokens(message)
|
||||
→ message.message.stop_reason === null → false
|
||||
→ withheld = false
|
||||
|
||||
D. L825: yield message ✅ → REPL 清除 streamingText,添加完整 text 消息到列表
|
||||
|
||||
E. L828: message.type === 'assistant'? → ✅
|
||||
L830: assistantMessages.push(message)
|
||||
→ assistantMessages = [uuid-1(text)]
|
||||
|
||||
L832-834: msgToolUseBlocks = content.filter(type === 'tool_use')
|
||||
→ [](这是 text block,没有 tool_use)
|
||||
|
||||
L835: length > 0? → 否 → 不设 needsFollowUp
|
||||
L844: msgToolUseBlocks 为空 → 不调用 addTool
|
||||
|
||||
F. L854: getCompletedResults() → 空
|
||||
```
|
||||
|
||||
**净效果**:`yield` 消息 + `assistantMessages` 增加一条。`needsFollowUp` 仍为 `false`。
|
||||
|
||||
---
|
||||
|
||||
#### #7 stream_event (content_block_start, tool_use: Read)
|
||||
|
||||
```
|
||||
A-C. 同 stream_event 通用路径
|
||||
D. yield message ✅ → REPL 设置 spinner 为 "tool-input",添加 streamingToolUse
|
||||
E. 不是 assistant → 跳过
|
||||
F. getCompletedResults() → 空
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### #8 stream_event (input_json_delta: '{"file_path":')
|
||||
|
||||
```
|
||||
D. yield message ✅ → REPL 追加工具输入 JSON 碎片
|
||||
F. getCompletedResults() → 空
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### #9 stream_event (input_json_delta: '"/path/package.json"}')
|
||||
|
||||
```
|
||||
D. yield message ✅
|
||||
F. getCompletedResults() → 空
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### #10 stream_event (content_block_stop, index:1)
|
||||
|
||||
```
|
||||
D. yield message ✅
|
||||
F. getCompletedResults() → 空
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### #11 assistant (tool_use block 完整消息) ★★
|
||||
|
||||
这条是**最关键的**——触发工具执行:
|
||||
|
||||
```
|
||||
A. L712: streamingFallbackOccured = false → 跳过
|
||||
|
||||
B. L748: message.type === 'assistant'? → ✅ 进入 backfill
|
||||
L750: contentArr = [{ type: 'tool_use', id: 'toolu_001', name: 'Read',
|
||||
input: { file_path: '/path/package.json' } }]
|
||||
L752: for i=0:
|
||||
L754: block.type === 'tool_use'? → ✅
|
||||
L756: typeof block.input === 'object' && !== null? → ✅
|
||||
L759: tool = findToolByName(tools, 'Read') → Read 工具定义
|
||||
L763: tool.backfillObservableInput 存在? → 假设存在
|
||||
L764-766: inputCopy = { file_path: '/path/package.json' }
|
||||
tool.backfillObservableInput(inputCopy)
|
||||
→ 可能添加 absolutePath 字段
|
||||
L773-776: addedFields? → 假设有新增字段
|
||||
clonedContent = [...contentArr]
|
||||
clonedContent[0] = { ...block, input: inputCopy }
|
||||
L783-788: yieldMessage = {
|
||||
...message, // uuid, type, timestamp 不变
|
||||
message: {
|
||||
...message.message, // stop_reason, usage 不变
|
||||
content: clonedContent // ★ 替换为带 absolutePath 的副本
|
||||
}
|
||||
}
|
||||
// ★ 原始 message 保持不变(回传 API 保证缓存一致)
|
||||
|
||||
C. L801-824: withheld 检查 → 全部 false → withheld = false
|
||||
|
||||
D. L825: yield yieldMessage ✅
|
||||
→ yield 的是克隆版(带 backfill 字段),给 REPL 和 SDK 用
|
||||
→ 原始 message 下面存进 assistantMessages,回传 API 保证缓存一致
|
||||
|
||||
E. L828: message.type === 'assistant'? → ✅
|
||||
L830: assistantMessages.push(message) // ★ push 原始 message,不是 yieldMessage
|
||||
→ assistantMessages = [uuid-1(text), uuid-2(tool_use)]
|
||||
|
||||
L832-834: msgToolUseBlocks = content.filter(type === 'tool_use')
|
||||
→ [{ type: 'tool_use', id: 'toolu_001', name: 'Read', input: {...} }]
|
||||
|
||||
L835: length > 0? → ✅
|
||||
L836: toolUseBlocks.push(...msgToolUseBlocks)
|
||||
→ toolUseBlocks = [Read_block]
|
||||
L837: needsFollowUp = true // ★★★ 决定 while(true) 不会终止
|
||||
|
||||
L840-842: streamingToolExecutor 存在 ✓ && !aborted ✓
|
||||
L844-846: for (const toolBlock of msgToolUseBlocks):
|
||||
streamingToolExecutor.addTool(Read_block, uuid-2消息)
|
||||
// ★★★ 工具开始执行!
|
||||
// → StreamingToolExecutor 内部:
|
||||
// isConcurrencySafe = true(Read 是安全的)
|
||||
// queued → processQueue() → canExecuteTool() → true
|
||||
// → executeTool() → runToolUse() → 后台异步读文件
|
||||
|
||||
F. L850-854: getCompletedResults()
|
||||
→ Read 刚开始执行,status = 'executing' → 无完成结果
|
||||
```
|
||||
|
||||
**净效果**:
|
||||
- `yield` 克隆消息(带 backfill 字段)
|
||||
- `assistantMessages` push 原始消息
|
||||
- `needsFollowUp = true`
|
||||
- **Read 工具在后台异步开始执行**
|
||||
|
||||
---
|
||||
|
||||
#### #12 stream_event (message_delta, stop_reason: 'tool_use')
|
||||
|
||||
```
|
||||
A-C. 同 stream_event 通用路径
|
||||
D. yield message ✅
|
||||
|
||||
E. 不是 assistant → 跳过
|
||||
|
||||
F. L854: getCompletedResults()
|
||||
→ ★ 此时 Read 可能已经完成了!(读文件通常 <1ms)
|
||||
→ 如果完成: status = 'completed', results 有值
|
||||
L428(StreamingToolExecutor): tool.status = 'yielded'
|
||||
L431-432: yield { message: UserMsg(tool_result) }
|
||||
→ 回到 query.ts:
|
||||
L855: result.message 存在
|
||||
L856: yield result.message ✅ → REPL 显示工具结果
|
||||
L857-862: toolResults.push(normalizeMessagesForAPI([result.message])...)
|
||||
→ toolResults = [Read 的 tool_result]
|
||||
```
|
||||
|
||||
**净效果**:`yield` stream_event + **可能 yield 工具结果**(如果工具已完成)。
|
||||
|
||||
---
|
||||
|
||||
#### #13 stream_event (message_stop)
|
||||
|
||||
```
|
||||
D. yield message ✅
|
||||
F. getCompletedResults()
|
||||
→ 如果 Read 在 #12 已被收割 → 空
|
||||
→ 如果 Read 此时才完成 → yield 工具结果(同 #12 的 F 逻辑)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### for await 循环退出后
|
||||
|
||||
```
|
||||
L1018: aborted? → false → 跳过
|
||||
|
||||
L1065: if (!needsFollowUp)
|
||||
→ needsFollowUp = true → 不进入 → 跳过终止逻辑
|
||||
|
||||
L1383: toolUpdates = streamingToolExecutor.getRemainingResults()
|
||||
→ 如果 Read 已在 #12/#13 被收割 → 立即返回空
|
||||
→ 如果 Read 还没完成 → 阻塞等待 → 完成后 yield 结果
|
||||
|
||||
L1387-1404: for await (const update of toolUpdates) {
|
||||
yield update.message → REPL 显示
|
||||
toolResults.push(...) → 收集
|
||||
}
|
||||
|
||||
L1718-1730: 构建 next State:
|
||||
state = {
|
||||
messages: [
|
||||
...messagesForQuery, // [UserMessage("帮我看看...")]
|
||||
...assistantMessages, // [AssistantMsg(text), AssistantMsg(tool_use)]
|
||||
...toolResults, // [UserMsg(tool_result)]
|
||||
],
|
||||
turnCount: 1,
|
||||
transition: { reason: 'next_turn' },
|
||||
}
|
||||
→ continue → while(true) 第 2 次迭代 → 带着工具结果再次调 API
|
||||
```
|
||||
|
||||
### 循环体判定树总结
|
||||
|
||||
```
|
||||
for await (const message of deps.callModel(...)) {
|
||||
│
|
||||
├─ message.type === 'stream_event'?
|
||||
│ │
|
||||
│ └─ YES → 几乎零操作
|
||||
│ ├─ yield message(透传给 REPL 做实时 UI)
|
||||
│ └─ getCompletedResults()(顺便检查有没有完成的工具)
|
||||
│
|
||||
└─ message.type === 'assistant'?
|
||||
│
|
||||
├─ B. backfill: 有 tool_use + backfillObservableInput?
|
||||
│ ├─ YES → 克隆消息,yield 克隆版(原始消息保留给 API)
|
||||
│ └─ NO → yield 原始消息
|
||||
│
|
||||
├─ C. withheld: prompt_too_long / max_output_tokens?
|
||||
│ ├─ YES → 不 yield(暂扣,等后面恢复逻辑处理)
|
||||
│ └─ NO → yield
|
||||
│
|
||||
├─ E. assistantMessages.push(原始 message)
|
||||
│
|
||||
├─ E. 有 tool_use block?
|
||||
│ ├─ YES → toolUseBlocks.push()
|
||||
│ │ + needsFollowUp = true
|
||||
│ │ + streamingToolExecutor.addTool() → ★ 立即开始执行工具
|
||||
│ └─ NO → 什么都不做
|
||||
│
|
||||
└─ F. getCompletedResults() → 收割已完成的工具结果
|
||||
}
|
||||
```
|
||||
|
||||
**一句话总结**:stream_event 透传不处理;assistant 消息才是"真正的货"——收集起来、判断要不要暂扣、有工具就立即开始执行、顺便收割已完成的工具结果。
|
||||
84
mint.json
84
mint.json
@@ -65,6 +65,15 @@
|
||||
"docs/tools/task-management"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "安全与权限",
|
||||
"pages": [
|
||||
"docs/safety/why-safety-matters",
|
||||
"docs/safety/permission-model",
|
||||
"docs/safety/sandbox",
|
||||
"docs/safety/plan-mode"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "上下文工程",
|
||||
"pages": [
|
||||
@@ -91,90 +100,17 @@
|
||||
"docs/extensibility/custom-agents"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "安全与权限",
|
||||
"pages": [
|
||||
"docs/safety/why-safety-matters",
|
||||
"docs/safety/permission-model",
|
||||
"docs/safety/sandbox",
|
||||
"docs/safety/plan-mode",
|
||||
"docs/safety/auto-mode"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "揭秘:隐藏功能与内部机制",
|
||||
"pages": [
|
||||
"docs/internals/three-tier-gating",
|
||||
"docs/internals/feature-flags",
|
||||
"docs/internals/growthbook-ab-testing",
|
||||
"docs/internals/growthbook-adapter",
|
||||
"docs/internals/sentry-setup",
|
||||
"docs/internals/hidden-features",
|
||||
"docs/internals/ant-only-world",
|
||||
"docs/features/debug-mode",
|
||||
"docs/features/buddy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "隐藏功能详解",
|
||||
"pages": [
|
||||
{
|
||||
"group": "Agent 与协作",
|
||||
"pages": [
|
||||
"docs/features/coordinator-mode",
|
||||
"docs/features/fork-subagent",
|
||||
"docs/features/daemon",
|
||||
"docs/features/teammem"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "运行模式",
|
||||
"pages": [
|
||||
"docs/features/kairos",
|
||||
"docs/features/voice-mode",
|
||||
"docs/features/bridge-mode",
|
||||
"docs/features/proactive",
|
||||
"docs/features/ultraplan"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "工具增强",
|
||||
"pages": [
|
||||
"docs/features/mcp-skills",
|
||||
"docs/features/tree-sitter-bash",
|
||||
"docs/features/bash-classifier",
|
||||
"docs/features/web-browser-tool",
|
||||
"docs/features/experimental-skill-search"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "上下文与自动化",
|
||||
"pages": [
|
||||
"docs/features/token-budget",
|
||||
"docs/features/context-collapse",
|
||||
"docs/features/workflow-scripts"
|
||||
]
|
||||
},
|
||||
"docs/features/tier3-stubs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "基础设施与依赖",
|
||||
"pages": [
|
||||
"docs/auto-updater",
|
||||
"docs/lsp-integration",
|
||||
"docs/external-dependencies",
|
||||
"docs/telemetry-remote-config-audit"
|
||||
"docs/internals/ant-only-world"
|
||||
]
|
||||
}
|
||||
],
|
||||
"excludes": [
|
||||
"docs/test-plans/**",
|
||||
"docs/testing-spec.md",
|
||||
"docs/REVISION-PLAN.md",
|
||||
"docs/feature-exploration-plan.md",
|
||||
"docs/ultraplan-implementation.md"
|
||||
],
|
||||
"footerSocials": {
|
||||
"github": "https://github.com/anthropics/claude-code"
|
||||
}
|
||||
|
||||
37
package.json
37
package.json
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "claude-code-best",
|
||||
"name": "claude-js",
|
||||
"version": "1.0.3",
|
||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||
"type": "module",
|
||||
@@ -25,21 +25,18 @@
|
||||
"bun": ">=1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"ccb": "dist/cli.js",
|
||||
"claude-code-best": "dist/cli.js"
|
||||
"claude-js": "dist/cli.js"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"packages/@ant/*"
|
||||
],
|
||||
"files": [
|
||||
"dist",
|
||||
"scripts/download-ripgrep.ts"
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "bun run build.ts",
|
||||
"dev": "bun run scripts/dev.ts",
|
||||
"dev:inspect": "bun run scripts/dev-debug.ts",
|
||||
"dev": "bun run src/entrypoints/cli.tsx",
|
||||
"prepublishOnly": "bun run build",
|
||||
"lint": "biome lint src/",
|
||||
"lint:fix": "biome lint --fix src/",
|
||||
@@ -48,12 +45,10 @@
|
||||
"test": "bun test",
|
||||
"check:unused": "knip-bun",
|
||||
"health": "bun run scripts/health-check.ts",
|
||||
"postinstall": "node dist/download-ripgrep.js || bun run scripts/download-ripgrep.ts || true",
|
||||
"docs:dev": "npx mintlify dev"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"openai": "^4.73.0",
|
||||
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||
"@ant/claude-for-chrome-mcp": "workspace:*",
|
||||
"@ant/computer-use-input": "workspace:*",
|
||||
@@ -72,7 +67,6 @@
|
||||
"@aws-sdk/credential-provider-node": "^3.972.28",
|
||||
"@aws-sdk/credential-providers": "^3.1020.0",
|
||||
"@azure/identity": "^4.13.1",
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@commander-js/extra-typings": "^14.0.0",
|
||||
"@growthbook/growthbook": "^1.6.5",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
@@ -94,16 +88,8 @@
|
||||
"@opentelemetry/sdk-metrics": "^2.6.1",
|
||||
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||
"@sentry/node": "^10.47.0",
|
||||
"@smithy/core": "^3.23.13",
|
||||
"@smithy/node-http-handler": "^4.5.1",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@types/cacache": "^20.0.1",
|
||||
"@types/plist": "^3.0.5",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-reconciler": "^0.33.0",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"ajv": "^8.18.0",
|
||||
"asciichart": "^1.5.25",
|
||||
"audio-capture-napi": "workspace:*",
|
||||
@@ -126,14 +112,12 @@
|
||||
"fuse.js": "^7.1.0",
|
||||
"get-east-asian-width": "^1.5.0",
|
||||
"google-auth-library": "^10.6.2",
|
||||
"he": "^1.2.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"https-proxy-agent": "^8.0.0",
|
||||
"ignore": "^7.0.5",
|
||||
"image-processor-napi": "workspace:*",
|
||||
"indent-string": "^5.0.0",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"knip": "^6.1.1",
|
||||
"lodash-es": "^4.17.23",
|
||||
"lru-cache": "^11.2.7",
|
||||
"marked": "^17.0.5",
|
||||
@@ -156,7 +140,6 @@
|
||||
"tree-kill": "^1.2.2",
|
||||
"turndown": "^7.2.2",
|
||||
"type-fest": "^5.5.0",
|
||||
"typescript": "^6.0.2",
|
||||
"undici": "^7.24.6",
|
||||
"url-handler-napi": "workspace:*",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
@@ -167,6 +150,16 @@
|
||||
"ws": "^8.20.0",
|
||||
"xss": "^1.0.15",
|
||||
"yaml": "^2.8.3",
|
||||
"zod": "^4.3.6"
|
||||
"zod": "^4.3.6",
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@types/cacache": "^20.0.1",
|
||||
"@types/plist": "^3.0.5",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-reconciler": "^0.33.0",
|
||||
"@types/sharp": "^0.32.0",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"knip": "^6.1.1",
|
||||
"typescript": "^6.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
137
scripts/create-type-stubs.mjs
Normal file
137
scripts/create-type-stubs.mjs
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Analyzes TypeScript errors and creates stub modules with proper named exports.
|
||||
* Run: node scripts/create-type-stubs.mjs
|
||||
*/
|
||||
import { execSync } from 'child_process';
|
||||
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const ROOT = '/Users/konghayao/code/ai/claude-code';
|
||||
|
||||
// Run tsc and capture errors (tsc exits non-zero on type errors, that's expected)
|
||||
let errors;
|
||||
try {
|
||||
errors = execSync('npx tsc --noEmit 2>&1', { encoding: 'utf-8', cwd: ROOT });
|
||||
} catch (e) {
|
||||
errors = e.stdout || '';
|
||||
}
|
||||
|
||||
// Map: resolved file path -> Set of needed named exports
|
||||
const stubExports = new Map();
|
||||
// Map: resolved file path -> Set of needed default export names
|
||||
const defaultExports = new Map();
|
||||
|
||||
for (const line of errors.split('\n')) {
|
||||
// TS2614: Module '"X"' has no exported member 'Y'. Did you mean to use 'import Y from "X"' instead?
|
||||
let m = line.match(/error TS2614: Module '"(.+?)"' has no exported member '(.+?)'\. Did you mean to use 'import .* from/);
|
||||
if (m) {
|
||||
const [, mod, member] = m;
|
||||
if (!defaultExports.has(mod)) defaultExports.set(mod, new Set());
|
||||
defaultExports.get(mod).add(member);
|
||||
continue;
|
||||
}
|
||||
|
||||
// TS2305: Module '"X"' has no exported member 'Y'
|
||||
m = line.match(/error TS2305: Module '"(.+?)"' has no exported member '(.+?)'/);
|
||||
if (m) {
|
||||
const [, mod, member] = m;
|
||||
if (!stubExports.has(mod)) stubExports.set(mod, new Set());
|
||||
stubExports.get(mod).add(member);
|
||||
}
|
||||
|
||||
// TS2724: '"X"' has no exported member named 'Y'. Did you mean 'Z'?
|
||||
m = line.match(/error TS2724: '"(.+?)"' has no exported member named '(.+?)'/);
|
||||
if (m) {
|
||||
const [, mod, member] = m;
|
||||
if (!stubExports.has(mod)) stubExports.set(mod, new Set());
|
||||
stubExports.get(mod).add(member);
|
||||
}
|
||||
|
||||
// TS2306: File 'X' is not a module
|
||||
m = line.match(/error TS2306: File '(.+?)' is not a module/);
|
||||
if (m) {
|
||||
const filePath = m[1];
|
||||
if (!stubExports.has(filePath)) stubExports.set(filePath, new Set());
|
||||
}
|
||||
|
||||
// TS2307: Cannot find module 'X'
|
||||
m = line.match(/^(.+?)\(\d+,\d+\): error TS2307: Cannot find module '(.+?)'/);
|
||||
if (m) {
|
||||
const [srcFile, mod] = [m[1], m[2]];
|
||||
if (mod.endsWith('.md')) continue;
|
||||
if (!mod.startsWith('.') && !mod.startsWith('src/')) continue;
|
||||
// Will be resolved below
|
||||
const srcDir = dirname(srcFile);
|
||||
const resolved = join(ROOT, srcDir, mod).replace(/\.js$/, '.ts');
|
||||
if (resolved.startsWith(ROOT + '/') && !existsSync(resolved)) {
|
||||
if (!stubExports.has(resolved)) stubExports.set(resolved, new Set());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also parse actual import statements from source files to find what's needed
|
||||
import { readFileSync } from 'fs';
|
||||
const allSourceFiles = execSync('find src -name "*.ts" -o -name "*.tsx"', { encoding: 'utf-8', cwd: ROOT }).trim().split('\n');
|
||||
|
||||
for (const file of allSourceFiles) {
|
||||
const content = readFileSync(join(ROOT, file), 'utf-8');
|
||||
const srcDir = dirname(file);
|
||||
|
||||
// Find all import { X, Y } from 'module'
|
||||
const importRegex = /import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"](.+?)['"]/g;
|
||||
let match;
|
||||
while ((match = importRegex.exec(content)) !== null) {
|
||||
const members = match[1].split(',').map(s => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
|
||||
let mod = match[2];
|
||||
if (!mod.startsWith('.') && !mod.startsWith('src/')) continue;
|
||||
|
||||
const resolved = join(ROOT, srcDir, mod).replace(/\.js$/, '.ts');
|
||||
if (resolved.startsWith(ROOT + '/') && !existsSync(resolved)) {
|
||||
if (!stubExports.has(resolved)) stubExports.set(resolved, new Set());
|
||||
for (const member of members) {
|
||||
stubExports.get(resolved).add(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now create/update all stub files
|
||||
let created = 0;
|
||||
for (const [filePath, exports] of stubExports) {
|
||||
const relPath = filePath.replace(ROOT + '/', '');
|
||||
const dir = dirname(filePath);
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const lines = ['// Auto-generated type stub — replace with real implementation'];
|
||||
|
||||
for (const exp of exports) {
|
||||
lines.push(`export type ${exp} = any;`);
|
||||
}
|
||||
|
||||
// Check if there are default exports needed
|
||||
for (const [mod, defs] of defaultExports) {
|
||||
// Match the module path
|
||||
const modNorm = mod.replace(/\.js$/, '').replace(/^src\//, '');
|
||||
const filePathNorm = relPath.replace(/\.ts$/, '');
|
||||
if (modNorm === filePathNorm || mod === relPath) {
|
||||
for (const def of defs) {
|
||||
lines.push(`export type ${def} = any;`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure at least export {}
|
||||
if (exports.size === 0) {
|
||||
lines.push('export {};');
|
||||
}
|
||||
|
||||
writeFileSync(filePath, lines.join('\n') + '\n');
|
||||
created++;
|
||||
}
|
||||
|
||||
console.log(`Created/updated ${created} stub files`);
|
||||
console.log(`Total named exports resolved: ${[...stubExports.values()].reduce((a, b) => a + b.size, 0)}`);
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Shared MACRO define map used by both dev.ts (runtime -d flags)
|
||||
* and build.ts (Bun.build define option).
|
||||
*
|
||||
* Each value is a JSON-stringified expression that replaces the
|
||||
* corresponding MACRO.* identifier at transpile / bundle time.
|
||||
*/
|
||||
export function getMacroDefines(): Record<string, string> {
|
||||
return {
|
||||
"MACRO.VERSION": JSON.stringify("2.1.888"),
|
||||
"MACRO.BUILD_TIME": JSON.stringify(new Date().toISOString()),
|
||||
"MACRO.FEEDBACK_CHANNEL": JSON.stringify(""),
|
||||
"MACRO.ISSUES_EXPLAINER": JSON.stringify(""),
|
||||
"MACRO.NATIVE_PACKAGE_URL": JSON.stringify(""),
|
||||
"MACRO.PACKAGE_URL": JSON.stringify(""),
|
||||
"MACRO.VERSION_CHANGELOG": JSON.stringify(""),
|
||||
};
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
process.env.BUN_INSPECT="localhost:8888/2dc3gzl5xot"
|
||||
await import("./dev")
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Dev entrypoint — launches cli.tsx with MACRO.* defines injected
|
||||
* via Bun's -d flag (bunfig.toml [define] doesn't propagate to
|
||||
* dynamically imported modules at runtime).
|
||||
*/
|
||||
import { getMacroDefines } from "./defines.ts";
|
||||
|
||||
const defines = getMacroDefines();
|
||||
|
||||
const defineArgs = Object.entries(defines).flatMap(([k, v]) => [
|
||||
"-d",
|
||||
`${k}:${v}`,
|
||||
]);
|
||||
|
||||
// Bun --feature flags: enable feature() gates at runtime.
|
||||
// Default features enabled in dev mode.
|
||||
const DEFAULT_FEATURES = ["BUDDY", "TRANSCRIPT_CLASSIFIER", "BRIDGE_MODE", "AGENT_TRIGGERS_REMOTE"];
|
||||
|
||||
// Any env var matching FEATURE_<NAME>=1 will also enable that feature.
|
||||
// e.g. FEATURE_PROACTIVE=1 bun run dev
|
||||
const envFeatures = Object.entries(process.env)
|
||||
.filter(([k]) => k.startsWith("FEATURE_"))
|
||||
.map(([k]) => k.replace("FEATURE_", ""));
|
||||
|
||||
const allFeatures = [...new Set([...DEFAULT_FEATURES, ...envFeatures])];
|
||||
const featureArgs = allFeatures.flatMap((name) => ["--feature", name]);
|
||||
|
||||
// If BUN_INSPECT is set, pass --inspect-wait to the child process
|
||||
const inspectArgs = process.env.BUN_INSPECT
|
||||
? ["--inspect-wait=" + process.env.BUN_INSPECT]
|
||||
: [];
|
||||
|
||||
const result = Bun.spawnSync(
|
||||
["bun", ...inspectArgs, "run", ...defineArgs, ...featureArgs, "src/entrypoints/cli.tsx", ...process.argv.slice(2)],
|
||||
{ stdio: ["inherit", "inherit", "inherit"] },
|
||||
);
|
||||
|
||||
process.exit(result.exitCode ?? 0);
|
||||
@@ -1,191 +0,0 @@
|
||||
/**
|
||||
* Download ripgrep binary from GitHub releases.
|
||||
*
|
||||
* Run automatically via `bun install` (postinstall hook),
|
||||
* or manually: `bun run scripts/download-ripgrep.ts [--force]`
|
||||
*
|
||||
* Idempotent — skips download if the binary already exists.
|
||||
* Use --force to re-download.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, renameSync, rmSync, statSync } from 'fs'
|
||||
import { chmodSync } from 'fs'
|
||||
import { spawnSync } from 'child_process'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const RG_VERSION = '15.0.1'
|
||||
const BASE_URL = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`
|
||||
|
||||
// --- Platform mapping ---
|
||||
|
||||
type PlatformMapping = {
|
||||
target: string
|
||||
ext: 'tar.gz' | 'zip'
|
||||
}
|
||||
|
||||
function getPlatformMapping(): PlatformMapping {
|
||||
const arch = process.arch
|
||||
const platform = process.platform
|
||||
|
||||
if (platform === 'darwin') {
|
||||
if (arch === 'arm64') return { target: 'aarch64-apple-darwin', ext: 'tar.gz' }
|
||||
if (arch === 'x64') return { target: 'x86_64-apple-darwin', ext: 'tar.gz' }
|
||||
throw new Error(`Unsupported macOS arch: ${arch}`)
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
if (arch === 'x64') return { target: 'x86_64-pc-windows-msvc', ext: 'zip' }
|
||||
if (arch === 'arm64') return { target: 'aarch64-pc-windows-msvc', ext: 'zip' }
|
||||
throw new Error(`Unsupported Windows arch: ${arch}`)
|
||||
}
|
||||
|
||||
if (platform === 'linux') {
|
||||
const isMusl = detectMusl()
|
||||
if (arch === 'x64') {
|
||||
// x64 Linux always uses musl (statically linked, most portable)
|
||||
return { target: 'x86_64-unknown-linux-musl', ext: 'tar.gz' }
|
||||
}
|
||||
if (arch === 'arm64') {
|
||||
return isMusl
|
||||
? { target: 'aarch64-unknown-linux-musl', ext: 'tar.gz' }
|
||||
: { target: 'aarch64-unknown-linux-gnu', ext: 'tar.gz' }
|
||||
}
|
||||
throw new Error(`Unsupported Linux arch: ${arch}`)
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported platform: ${platform}`)
|
||||
}
|
||||
|
||||
function detectMusl(): boolean {
|
||||
const muslArch = process.arch === 'x64' ? 'x86_64' : 'aarch64'
|
||||
try {
|
||||
statSync(`/lib/libc.musl-${muslArch}.so.1`)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// --- Target vendor path (must match ripgrep.ts logic) ---
|
||||
|
||||
function getVendorDir(): string {
|
||||
const packageRoot = path.resolve(__dirname, '..')
|
||||
|
||||
// Dev mode: package root has src/ directory
|
||||
// ripgrep.ts at src/utils/ripgrep.ts: __dirname = src/utils/
|
||||
// vendor path = src/utils/vendor/ripgrep/
|
||||
if (existsSync(path.join(packageRoot, 'src'))) {
|
||||
return path.resolve(packageRoot, 'src', 'utils', 'vendor', 'ripgrep')
|
||||
}
|
||||
|
||||
// Published mode: compiled chunks are flat in dist/
|
||||
// ripgrep chunk at dist/xxxx.js: __dirname = dist/
|
||||
// vendor path = dist/vendor/ripgrep/
|
||||
return path.resolve(packageRoot, 'dist', 'vendor', 'ripgrep')
|
||||
}
|
||||
|
||||
function getBinaryPath(): string {
|
||||
const dir = getVendorDir()
|
||||
const subdir = `${process.arch}-${process.platform}`
|
||||
const binary = process.platform === 'win32' ? 'rg.exe' : 'rg'
|
||||
return path.resolve(dir, subdir, binary)
|
||||
}
|
||||
|
||||
// --- Download & extract ---
|
||||
|
||||
async function downloadAndExtract(): Promise<void> {
|
||||
const { target, ext } = getPlatformMapping()
|
||||
const assetName = `ripgrep-v${RG_VERSION}-${target}.${ext}`
|
||||
const downloadUrl = `${BASE_URL}/${assetName}`
|
||||
|
||||
const binaryPath = getBinaryPath()
|
||||
const binaryDir = path.dirname(binaryPath)
|
||||
|
||||
// Idempotent: skip if binary exists and has content
|
||||
const force = process.argv.includes('--force')
|
||||
if (!force && existsSync(binaryPath)) {
|
||||
const stat = statSync(binaryPath)
|
||||
if (stat.size > 0) {
|
||||
console.log(`[ripgrep] Binary already exists at ${binaryPath}, skipping.`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[ripgrep] Downloading v${RG_VERSION} for ${target}...`)
|
||||
console.log(`[ripgrep] URL: ${downloadUrl}`)
|
||||
|
||||
// Prepare temp directory
|
||||
const tmpDir = path.join(binaryDir, '.tmp-download')
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
mkdirSync(tmpDir, { recursive: true })
|
||||
|
||||
try {
|
||||
const archivePath = path.join(tmpDir, assetName)
|
||||
|
||||
// Download
|
||||
const response = await fetch(downloadUrl, { redirect: 'follow' })
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await response.arrayBuffer())
|
||||
const { writeFileSync } = await import('fs')
|
||||
writeFileSync(archivePath, buffer)
|
||||
console.log(`[ripgrep] Downloaded ${Math.round(buffer.length / 1024)} KB`)
|
||||
|
||||
// Extract
|
||||
mkdirSync(binaryDir, { recursive: true })
|
||||
|
||||
if (ext === 'tar.gz') {
|
||||
const result = spawnSync('tar', ['xzf', archivePath, '-C', tmpDir], {
|
||||
stdio: 'pipe',
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`tar extract failed: ${result.stderr?.toString()}`)
|
||||
}
|
||||
} else {
|
||||
// .zip
|
||||
const result = spawnSync('unzip', ['-o', archivePath, '-d', tmpDir], {
|
||||
stdio: 'pipe',
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`unzip failed: ${result.stderr?.toString()}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Find the rg binary in the extracted directory
|
||||
// microsoft/ripgrep-prebuilt archives extract flat: ./rg (no subdirectory)
|
||||
const extractedBinary = process.platform === 'win32' ? 'rg.exe' : 'rg'
|
||||
const srcBinary = path.join(tmpDir, extractedBinary)
|
||||
|
||||
if (!existsSync(srcBinary)) {
|
||||
throw new Error(`Binary not found at expected path: ${srcBinary}`)
|
||||
}
|
||||
|
||||
// Move to final location
|
||||
renameSync(srcBinary, binaryPath)
|
||||
|
||||
// Make executable (non-Windows)
|
||||
if (process.platform !== 'win32') {
|
||||
chmodSync(binaryPath, 0o755)
|
||||
}
|
||||
|
||||
console.log(`[ripgrep] Installed to ${binaryPath}`)
|
||||
} finally {
|
||||
// Cleanup temp directory
|
||||
rmSync(tmpDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
|
||||
downloadAndExtract().catch(error => {
|
||||
console.error(`[ripgrep] Download failed: ${error.message}`)
|
||||
console.error(`[ripgrep] You can install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`)
|
||||
// Don't exit with error code — postinstall should not break bun install
|
||||
process.exit(0)
|
||||
})
|
||||
160
scripts/fix-default-stubs.mjs
Normal file
160
scripts/fix-default-stubs.mjs
Normal file
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Finds all stub files with `export default {} as any` and rewrites them
|
||||
* with proper named exports based on what the source code actually imports.
|
||||
*/
|
||||
import { execSync } from 'child_process';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||
import { dirname, join, relative, resolve } from 'path';
|
||||
|
||||
const ROOT = '/Users/konghayao/code/ai/claude-code';
|
||||
|
||||
// Step 1: Find all stub files with only `export default {} as any`
|
||||
const stubFiles = new Set();
|
||||
const allTsFiles = execSync('find src -name "*.ts" -o -name "*.tsx"', {
|
||||
encoding: 'utf-8', cwd: ROOT
|
||||
}).trim().split('\n');
|
||||
|
||||
for (const f of allTsFiles) {
|
||||
const fullPath = join(ROOT, f);
|
||||
const content = readFileSync(fullPath, 'utf-8').trim();
|
||||
if (content === 'export default {} as any') {
|
||||
stubFiles.add(f); // relative path like src/types/message.ts
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${stubFiles.size} stub files with 'export default {} as any'`);
|
||||
|
||||
// Step 2: Scan all source files for imports from these stub modules
|
||||
// Map: stub file path -> { types: Set<string>, values: Set<string> }
|
||||
const stubNeeds = new Map();
|
||||
for (const sf of stubFiles) {
|
||||
stubNeeds.set(sf, { types: new Set(), values: new Set() });
|
||||
}
|
||||
|
||||
// Helper: resolve an import path from a source file to a stub file
|
||||
function resolveImport(srcFile, importPath) {
|
||||
// Handle src/ prefix imports
|
||||
if (importPath.startsWith('src/')) {
|
||||
const resolved = importPath.replace(/\.js$/, '.ts');
|
||||
if (stubFiles.has(resolved)) return resolved;
|
||||
return null;
|
||||
}
|
||||
// Handle relative imports
|
||||
if (importPath.startsWith('.')) {
|
||||
const srcDir = dirname(srcFile);
|
||||
const resolved = join(srcDir, importPath).replace(/\.js$/, '.ts');
|
||||
if (stubFiles.has(resolved)) return resolved;
|
||||
// Try .tsx
|
||||
const resolvedTsx = join(srcDir, importPath).replace(/\.js$/, '.tsx');
|
||||
if (stubFiles.has(resolvedTsx)) return resolvedTsx;
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const srcFile of allTsFiles) {
|
||||
if (stubFiles.has(srcFile)) continue; // skip stub files themselves
|
||||
|
||||
const fullPath = join(ROOT, srcFile);
|
||||
const content = readFileSync(fullPath, 'utf-8');
|
||||
|
||||
// Match: import type { A, B } from 'path'
|
||||
const typeImportRegex = /import\s+type\s+\{([^}]+)\}\s+from\s+['"](.+?)['"]/g;
|
||||
let match;
|
||||
while ((match = typeImportRegex.exec(content)) !== null) {
|
||||
const members = match[1].split(',').map(s => {
|
||||
const parts = s.trim().split(/\s+as\s+/);
|
||||
return parts[0].trim();
|
||||
}).filter(Boolean);
|
||||
const resolved = resolveImport(srcFile, match[2]);
|
||||
if (resolved && stubNeeds.has(resolved)) {
|
||||
for (const m of members) stubNeeds.get(resolved).types.add(m);
|
||||
}
|
||||
}
|
||||
|
||||
// Match: import { A, B } from 'path' (NOT import type)
|
||||
const valueImportRegex = /import\s+(?!type\s)\{([^}]+)\}\s+from\s+['"](.+?)['"]/g;
|
||||
while ((match = valueImportRegex.exec(content)) !== null) {
|
||||
const rawMembers = match[1];
|
||||
const members = rawMembers.split(',').map(s => {
|
||||
// Handle `type Foo` inline type imports
|
||||
const trimmed = s.trim();
|
||||
if (trimmed.startsWith('type ')) {
|
||||
return { name: trimmed.replace(/^type\s+/, '').split(/\s+as\s+/)[0].trim(), isType: true };
|
||||
}
|
||||
return { name: trimmed.split(/\s+as\s+/)[0].trim(), isType: false };
|
||||
}).filter(m => m.name);
|
||||
|
||||
const resolved = resolveImport(srcFile, match[2]);
|
||||
if (resolved && stubNeeds.has(resolved)) {
|
||||
for (const m of members) {
|
||||
if (m.isType) {
|
||||
stubNeeds.get(resolved).types.add(m.name);
|
||||
} else {
|
||||
stubNeeds.get(resolved).values.add(m.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match: import Default from 'path'
|
||||
const defaultImportRegex = /import\s+(?!type\s)(\w+)\s+from\s+['"](.+?)['"]/g;
|
||||
while ((match = defaultImportRegex.exec(content)) !== null) {
|
||||
const name = match[1];
|
||||
if (name === 'type') continue;
|
||||
const resolved = resolveImport(srcFile, match[2]);
|
||||
if (resolved && stubNeeds.has(resolved)) {
|
||||
stubNeeds.get(resolved).values.add('__default__:' + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Rewrite stub files
|
||||
let updated = 0;
|
||||
for (const [stubFile, needs] of stubNeeds) {
|
||||
const fullPath = join(ROOT, stubFile);
|
||||
const lines = ['// Auto-generated stub — replace with real implementation'];
|
||||
|
||||
let hasDefault = false;
|
||||
|
||||
// Add type exports
|
||||
for (const t of needs.types) {
|
||||
// Don't add as type if also in values
|
||||
if (!needs.values.has(t)) {
|
||||
lines.push(`export type ${t} = any;`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add value exports (as const with any type)
|
||||
for (const v of needs.values) {
|
||||
if (v.startsWith('__default__:')) {
|
||||
hasDefault = true;
|
||||
continue;
|
||||
}
|
||||
// Check if it's likely a type (starts with uppercase and not a known function pattern)
|
||||
// But since it's imported without `type`, treat as value to be safe
|
||||
lines.push(`export const ${v}: any = (() => {}) as any;`);
|
||||
}
|
||||
|
||||
// Add default export if needed
|
||||
if (hasDefault) {
|
||||
lines.push(`export default {} as any;`);
|
||||
}
|
||||
|
||||
if (needs.types.size === 0 && needs.values.size === 0) {
|
||||
lines.push('export {};');
|
||||
}
|
||||
|
||||
writeFileSync(fullPath, lines.join('\n') + '\n');
|
||||
updated++;
|
||||
}
|
||||
|
||||
console.log(`Updated ${updated} stub files`);
|
||||
|
||||
// Print summary
|
||||
for (const [stubFile, needs] of stubNeeds) {
|
||||
if (needs.types.size > 0 || needs.values.size > 0) {
|
||||
console.log(` ${stubFile}: ${needs.types.size} types, ${needs.values.size} values`);
|
||||
}
|
||||
}
|
||||
228
scripts/fix-missing-exports.mjs
Normal file
228
scripts/fix-missing-exports.mjs
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Fixes TS2339 "Property X does not exist on type 'typeof import(...)'"
|
||||
* by adding missing exports to the stub module files.
|
||||
* Also re-runs TS2305/TS2724 fixes.
|
||||
*/
|
||||
import { execSync } from 'child_process';
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const ROOT = '/Users/konghayao/code/ai/claude-code';
|
||||
|
||||
// Run tsc and capture errors
|
||||
let errors;
|
||||
try {
|
||||
errors = execSync('npx tsc --noEmit 2>&1', { encoding: 'utf-8', cwd: ROOT, maxBuffer: 50 * 1024 * 1024 });
|
||||
} catch (e) {
|
||||
errors = e.stdout || '';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 1. Fix TS2339 on typeof import(...) - add missing exports
|
||||
// ============================================================
|
||||
// Map: module file path -> Set<property name>
|
||||
const missingExports = new Map();
|
||||
|
||||
for (const line of errors.split('\n')) {
|
||||
// TS2339: Property 'X' does not exist on type 'typeof import("path")'
|
||||
let m = line.match(/error TS2339: Property '(\w+)' does not exist on type 'typeof import\("(.+?)"\)'/);
|
||||
if (m) {
|
||||
const [, prop, modPath] = m;
|
||||
let filePath;
|
||||
if (modPath.startsWith('/')) {
|
||||
filePath = modPath;
|
||||
} else {
|
||||
continue; // skip non-absolute paths for now
|
||||
}
|
||||
// Try .ts then .tsx
|
||||
for (const ext of ['.ts', '.tsx']) {
|
||||
const fp = filePath + ext;
|
||||
if (existsSync(fp)) {
|
||||
if (!missingExports.has(fp)) missingExports.set(fp, new Set());
|
||||
missingExports.get(fp).add(prop);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TS2339 on type '{ default: typeof import("...") }' (namespace import)
|
||||
m = line.match(/error TS2339: Property '(\w+)' does not exist on type '\{ default: typeof import\("(.+?)"\)/);
|
||||
if (m) {
|
||||
const [, prop, modPath] = m;
|
||||
for (const ext of ['.ts', '.tsx']) {
|
||||
const fp = (modPath.startsWith('/') ? modPath : join(ROOT, modPath)) + ext;
|
||||
if (existsSync(fp)) {
|
||||
if (!missingExports.has(fp)) missingExports.set(fp, new Set());
|
||||
missingExports.get(fp).add(prop);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${missingExports.size} modules needing export additions for TS2339`);
|
||||
|
||||
let ts2339Fixed = 0;
|
||||
for (const [filePath, props] of missingExports) {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const existingExports = new Set();
|
||||
// Parse existing exports
|
||||
const exportRegex = /export\s+(?:type|const|function|class|let|var|default)\s+(\w+)/g;
|
||||
let em;
|
||||
while ((em = exportRegex.exec(content)) !== null) {
|
||||
existingExports.add(em[1]);
|
||||
}
|
||||
|
||||
const newExports = [];
|
||||
for (const prop of props) {
|
||||
if (!existingExports.has(prop) && !content.includes(`export { ${prop}`) && !content.includes(`, ${prop}`)) {
|
||||
newExports.push(`export const ${prop}: any = (() => {}) as any;`);
|
||||
ts2339Fixed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (newExports.length > 0) {
|
||||
const newContent = content.trimEnd() + '\n' + newExports.join('\n') + '\n';
|
||||
writeFileSync(filePath, newContent);
|
||||
}
|
||||
}
|
||||
console.log(`Added ${ts2339Fixed} missing exports for TS2339`);
|
||||
|
||||
// ============================================================
|
||||
// 2. Fix TS2305 - Module has no exported member
|
||||
// ============================================================
|
||||
const ts2305Fixes = new Map();
|
||||
|
||||
for (const line of errors.split('\n')) {
|
||||
let m = line.match(/^(.+?)\(\d+,\d+\): error TS2305: Module '"(.+?)"' has no exported member '(.+?)'/);
|
||||
if (!m) continue;
|
||||
const [, srcFile, mod, member] = m;
|
||||
|
||||
// Resolve module path
|
||||
let resolvedPath;
|
||||
if (mod.startsWith('.') || mod.startsWith('src/')) {
|
||||
const base = mod.startsWith('.') ? join(dirname(srcFile), mod) : mod;
|
||||
const resolved = join(ROOT, base).replace(/\.js$/, '');
|
||||
for (const ext of ['.ts', '.tsx']) {
|
||||
if (existsSync(resolved + ext)) {
|
||||
resolvedPath = resolved + ext;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedPath) {
|
||||
if (!ts2305Fixes.has(resolvedPath)) ts2305Fixes.set(resolvedPath, new Set());
|
||||
ts2305Fixes.get(resolvedPath).add(member);
|
||||
}
|
||||
}
|
||||
|
||||
let ts2305Fixed = 0;
|
||||
for (const [filePath, members] of ts2305Fixes) {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const newExports = [];
|
||||
|
||||
for (const member of members) {
|
||||
if (!content.includes(`export type ${member}`) && !content.includes(`export const ${member}`) && !content.includes(`export function ${member}`)) {
|
||||
newExports.push(`export type ${member} = any;`);
|
||||
ts2305Fixed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (newExports.length > 0) {
|
||||
writeFileSync(filePath, content.trimEnd() + '\n' + newExports.join('\n') + '\n');
|
||||
}
|
||||
}
|
||||
console.log(`Added ${ts2305Fixed} missing exports for TS2305`);
|
||||
|
||||
// ============================================================
|
||||
// 3. Fix TS2724 - no exported member named X. Did you mean Y?
|
||||
// ============================================================
|
||||
const ts2724Fixes = new Map();
|
||||
|
||||
for (const line of errors.split('\n')) {
|
||||
let m = line.match(/^(.+?)\(\d+,\d+\): error TS2724: '"(.+?)"' has no exported member named '(.+?)'/);
|
||||
if (!m) continue;
|
||||
const [, srcFile, mod, member] = m;
|
||||
|
||||
let resolvedPath;
|
||||
if (mod.startsWith('.') || mod.startsWith('src/')) {
|
||||
const base = mod.startsWith('.') ? join(dirname(srcFile), mod) : mod;
|
||||
const resolved = join(ROOT, base).replace(/\.js$/, '');
|
||||
for (const ext of ['.ts', '.tsx']) {
|
||||
if (existsSync(resolved + ext)) {
|
||||
resolvedPath = resolved + ext;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedPath) {
|
||||
if (!ts2724Fixes.has(resolvedPath)) ts2724Fixes.set(resolvedPath, new Set());
|
||||
ts2724Fixes.get(resolvedPath).add(member);
|
||||
}
|
||||
}
|
||||
|
||||
let ts2724Fixed = 0;
|
||||
for (const [filePath, members] of ts2724Fixes) {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
const newExports = [];
|
||||
|
||||
for (const member of members) {
|
||||
if (!content.includes(`export type ${member}`) && !content.includes(`export const ${member}`)) {
|
||||
newExports.push(`export type ${member} = any;`);
|
||||
ts2724Fixed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (newExports.length > 0) {
|
||||
writeFileSync(filePath, content.trimEnd() + '\n' + newExports.join('\n') + '\n');
|
||||
}
|
||||
}
|
||||
console.log(`Added ${ts2724Fixed} missing exports for TS2724`);
|
||||
|
||||
// ============================================================
|
||||
// 4. Fix TS2307 - Cannot find module (create stub files)
|
||||
// ============================================================
|
||||
let ts2307Fixed = 0;
|
||||
|
||||
for (const line of errors.split('\n')) {
|
||||
let m = line.match(/^(.+?)\(\d+,\d+\): error TS2307: Cannot find module '(.+?)'/);
|
||||
if (!m) continue;
|
||||
const [, srcFile, mod] = m;
|
||||
if (mod.endsWith('.md') || mod.endsWith('.css')) continue;
|
||||
if (!mod.startsWith('.') && !mod.startsWith('src/')) continue;
|
||||
|
||||
const srcDir = dirname(srcFile);
|
||||
let resolved;
|
||||
if (mod.startsWith('.')) {
|
||||
resolved = join(ROOT, srcDir, mod).replace(/\.js$/, '.ts');
|
||||
} else {
|
||||
resolved = join(ROOT, mod).replace(/\.js$/, '.ts');
|
||||
}
|
||||
|
||||
if (!existsSync(resolved) && resolved.startsWith(ROOT + '/src/')) {
|
||||
const dir = dirname(resolved);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
|
||||
// Collect imports from the source file for this module
|
||||
const srcContent = readFileSync(join(ROOT, srcFile), 'utf-8');
|
||||
const importRegex = new RegExp(`import\\s+(?:type\\s+)?\\{([^}]+)\\}\\s+from\\s+['"]${mod.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`, 'g');
|
||||
const members = new Set();
|
||||
let im;
|
||||
while ((im = importRegex.exec(srcContent)) !== null) {
|
||||
im[1].split(',').map(s => s.trim().replace(/^type\s+/, '').split(/\s+as\s+/)[0].trim()).filter(Boolean).forEach(m => members.add(m));
|
||||
}
|
||||
|
||||
const lines = ['// Auto-generated stub'];
|
||||
for (const member of members) {
|
||||
lines.push(`export type ${member} = any;`);
|
||||
}
|
||||
if (members.size === 0) lines.push('export {};');
|
||||
|
||||
writeFileSync(resolved, lines.join('\n') + '\n');
|
||||
ts2307Fixed++;
|
||||
}
|
||||
}
|
||||
console.log(`Created ${ts2307Fixed} new stub files for TS2307`);
|
||||
40
scripts/remove-sourcemaps.mjs
Normal file
40
scripts/remove-sourcemaps.mjs
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 清除 src/ 下所有 .ts/.tsx 文件中的 //# sourceMappingURL= 行
|
||||
* 用法: node scripts/remove-sourcemaps.mjs [--dry-run]
|
||||
*/
|
||||
import { readdir, readFile, writeFile } from "fs/promises";
|
||||
import { join, extname } from "path";
|
||||
|
||||
const SRC_DIR = new URL("../src", import.meta.url).pathname;
|
||||
const DRY_RUN = process.argv.includes("--dry-run");
|
||||
const EXTENSIONS = new Set([".ts", ".tsx"]);
|
||||
const PATTERN = /^\s*\/\/# sourceMappingURL=.*$/gm;
|
||||
|
||||
async function* walk(dir) {
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
yield* walk(full);
|
||||
} else if (EXTENSIONS.has(extname(entry.name))) {
|
||||
yield full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for await (const file of walk(SRC_DIR)) {
|
||||
const content = await readFile(file, "utf8");
|
||||
if (!PATTERN.test(content)) continue;
|
||||
// reset lastIndex after test
|
||||
PATTERN.lastIndex = 0;
|
||||
const cleaned = content.replace(PATTERN, "").replace(/\n{3,}/g, "\n\n");
|
||||
if (DRY_RUN) {
|
||||
console.log(`[dry-run] ${file}`);
|
||||
} else {
|
||||
await writeFile(file, cleaned, "utf8");
|
||||
}
|
||||
total++;
|
||||
}
|
||||
|
||||
console.log(`\n${DRY_RUN ? "[dry-run] " : ""}Processed ${total} files.`);
|
||||
@@ -1,207 +0,0 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
buildTool,
|
||||
toolMatchesName,
|
||||
findToolByName,
|
||||
getEmptyToolPermissionContext,
|
||||
filterToolProgressMessages,
|
||||
} from '../Tool'
|
||||
|
||||
// Minimal tool definition for testing buildTool
|
||||
function makeMinimalToolDef(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
name: 'TestTool',
|
||||
inputSchema: { type: 'object' as const } as any,
|
||||
maxResultSizeChars: 10000,
|
||||
call: async () => ({ data: 'ok' }),
|
||||
description: async () => 'A test tool',
|
||||
prompt: async () => 'test prompt',
|
||||
mapToolResultToToolResultBlockParam: (
|
||||
content: unknown,
|
||||
toolUseID: string,
|
||||
) => ({
|
||||
type: 'tool_result' as const,
|
||||
tool_use_id: toolUseID,
|
||||
content: String(content),
|
||||
}),
|
||||
renderToolUseMessage: () => null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('buildTool', () => {
|
||||
test('fills in default isEnabled as true', () => {
|
||||
const tool = buildTool(makeMinimalToolDef())
|
||||
expect(tool.isEnabled()).toBe(true)
|
||||
})
|
||||
|
||||
test('fills in default isConcurrencySafe as false', () => {
|
||||
const tool = buildTool(makeMinimalToolDef())
|
||||
expect(tool.isConcurrencySafe({})).toBe(false)
|
||||
})
|
||||
|
||||
test('fills in default isReadOnly as false', () => {
|
||||
const tool = buildTool(makeMinimalToolDef())
|
||||
expect(tool.isReadOnly({})).toBe(false)
|
||||
})
|
||||
|
||||
test('fills in default isDestructive as false', () => {
|
||||
const tool = buildTool(makeMinimalToolDef())
|
||||
expect(tool.isDestructive!({})).toBe(false)
|
||||
})
|
||||
|
||||
test('fills in default checkPermissions as allow', async () => {
|
||||
const tool = buildTool(makeMinimalToolDef())
|
||||
const input = { foo: 'bar' }
|
||||
const result = await tool.checkPermissions(input, {} as any)
|
||||
expect(result).toEqual({ behavior: 'allow', updatedInput: input })
|
||||
})
|
||||
|
||||
test('fills in default userFacingName from tool name', () => {
|
||||
const tool = buildTool(makeMinimalToolDef())
|
||||
expect(tool.userFacingName(undefined)).toBe('TestTool')
|
||||
})
|
||||
|
||||
test('fills in default toAutoClassifierInput as empty string', () => {
|
||||
const tool = buildTool(makeMinimalToolDef())
|
||||
expect(tool.toAutoClassifierInput({})).toBe('')
|
||||
})
|
||||
|
||||
test('preserves explicitly provided methods', () => {
|
||||
const tool = buildTool(
|
||||
makeMinimalToolDef({
|
||||
isEnabled: () => false,
|
||||
isConcurrencySafe: () => true,
|
||||
isReadOnly: () => true,
|
||||
}),
|
||||
)
|
||||
expect(tool.isEnabled()).toBe(false)
|
||||
expect(tool.isConcurrencySafe({})).toBe(true)
|
||||
expect(tool.isReadOnly({})).toBe(true)
|
||||
})
|
||||
|
||||
test('preserves all non-defaultable properties', () => {
|
||||
const tool = buildTool(makeMinimalToolDef())
|
||||
expect(tool.name).toBe('TestTool')
|
||||
expect(tool.maxResultSizeChars).toBe(10000)
|
||||
expect(typeof tool.call).toBe('function')
|
||||
expect(typeof tool.description).toBe('function')
|
||||
expect(typeof tool.prompt).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('toolMatchesName', () => {
|
||||
test('returns true for exact name match', () => {
|
||||
expect(toolMatchesName({ name: 'Bash' }, 'Bash')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for non-matching name', () => {
|
||||
expect(toolMatchesName({ name: 'Bash' }, 'Read')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns true when name matches an alias', () => {
|
||||
expect(
|
||||
toolMatchesName(
|
||||
{ name: 'Bash', aliases: ['BashTool', 'Shell'] },
|
||||
'BashTool',
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false when aliases is undefined', () => {
|
||||
expect(toolMatchesName({ name: 'Bash' }, 'BashTool')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false when aliases is empty', () => {
|
||||
expect(toolMatchesName({ name: 'Bash', aliases: [] }, 'BashTool')).toBe(
|
||||
false,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('findToolByName', () => {
|
||||
const mockTools = [
|
||||
buildTool(makeMinimalToolDef({ name: 'Bash' })),
|
||||
buildTool(makeMinimalToolDef({ name: 'Read', aliases: ['FileRead'] })),
|
||||
buildTool(makeMinimalToolDef({ name: 'Edit' })),
|
||||
]
|
||||
|
||||
test('finds tool by primary name', () => {
|
||||
const tool = findToolByName(mockTools, 'Bash')
|
||||
expect(tool).toBeDefined()
|
||||
expect(tool!.name).toBe('Bash')
|
||||
})
|
||||
|
||||
test('finds tool by alias', () => {
|
||||
const tool = findToolByName(mockTools, 'FileRead')
|
||||
expect(tool).toBeDefined()
|
||||
expect(tool!.name).toBe('Read')
|
||||
})
|
||||
|
||||
test('returns undefined when no match', () => {
|
||||
expect(findToolByName(mockTools, 'NonExistent')).toBeUndefined()
|
||||
})
|
||||
|
||||
test('returns first match when duplicates exist', () => {
|
||||
const dupeTools = [
|
||||
buildTool(makeMinimalToolDef({ name: 'Bash', maxResultSizeChars: 100 })),
|
||||
buildTool(makeMinimalToolDef({ name: 'Bash', maxResultSizeChars: 200 })),
|
||||
]
|
||||
const tool = findToolByName(dupeTools, 'Bash')
|
||||
expect(tool!.maxResultSizeChars).toBe(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getEmptyToolPermissionContext', () => {
|
||||
test('returns default permission mode', () => {
|
||||
const ctx = getEmptyToolPermissionContext()
|
||||
expect(ctx.mode).toBe('default')
|
||||
})
|
||||
|
||||
test('returns empty maps and arrays', () => {
|
||||
const ctx = getEmptyToolPermissionContext()
|
||||
expect(ctx.additionalWorkingDirectories.size).toBe(0)
|
||||
expect(ctx.alwaysAllowRules).toEqual({})
|
||||
expect(ctx.alwaysDenyRules).toEqual({})
|
||||
expect(ctx.alwaysAskRules).toEqual({})
|
||||
})
|
||||
|
||||
test('returns isBypassPermissionsModeAvailable as false', () => {
|
||||
const ctx = getEmptyToolPermissionContext()
|
||||
expect(ctx.isBypassPermissionsModeAvailable).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('filterToolProgressMessages', () => {
|
||||
test('filters out hook_progress messages', () => {
|
||||
const messages = [
|
||||
{ data: { type: 'hook_progress', hookName: 'pre' } },
|
||||
{ data: { type: 'tool_progress', toolName: 'Bash' } },
|
||||
] as any[]
|
||||
const result = filterToolProgressMessages(messages)
|
||||
expect(result).toHaveLength(1)
|
||||
expect((result[0]!.data as any).type).toBe('tool_progress')
|
||||
})
|
||||
|
||||
test('keeps tool progress messages', () => {
|
||||
const messages = [
|
||||
{ data: { type: 'tool_progress', toolName: 'Bash' } },
|
||||
{ data: { type: 'tool_progress', toolName: 'Read' } },
|
||||
] as any[]
|
||||
const result = filterToolProgressMessages(messages)
|
||||
expect(result).toHaveLength(2)
|
||||
})
|
||||
|
||||
test('returns empty array for empty input', () => {
|
||||
expect(filterToolProgressMessages([])).toEqual([])
|
||||
})
|
||||
|
||||
test('handles messages without type field', () => {
|
||||
const messages = [
|
||||
{ data: { toolName: 'Bash' } },
|
||||
{ data: { type: 'hook_progress' } },
|
||||
] as any[]
|
||||
const result = filterToolProgressMessages(messages)
|
||||
expect(result).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -1,167 +0,0 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
getPastedTextRefNumLines,
|
||||
formatPastedTextRef,
|
||||
formatImageRef,
|
||||
parseReferences,
|
||||
expandPastedTextRefs,
|
||||
} from '../history'
|
||||
|
||||
describe('getPastedTextRefNumLines', () => {
|
||||
test('returns 0 for single line (no newline)', () => {
|
||||
expect(getPastedTextRefNumLines('hello')).toBe(0)
|
||||
})
|
||||
|
||||
test('counts LF newlines', () => {
|
||||
expect(getPastedTextRefNumLines('a\nb\nc')).toBe(2)
|
||||
})
|
||||
|
||||
test('counts CRLF newlines', () => {
|
||||
expect(getPastedTextRefNumLines('a\r\nb')).toBe(1)
|
||||
})
|
||||
|
||||
test('counts CR newlines', () => {
|
||||
expect(getPastedTextRefNumLines('a\rb')).toBe(1)
|
||||
})
|
||||
|
||||
test('returns 0 for empty string', () => {
|
||||
expect(getPastedTextRefNumLines('')).toBe(0)
|
||||
})
|
||||
|
||||
test('trailing newline counts as one', () => {
|
||||
expect(getPastedTextRefNumLines('a\n')).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatPastedTextRef', () => {
|
||||
test('formats with lines count', () => {
|
||||
expect(formatPastedTextRef(1, 10)).toBe('[Pasted text #1 +10 lines]')
|
||||
})
|
||||
|
||||
test('formats without lines when 0', () => {
|
||||
expect(formatPastedTextRef(3, 0)).toBe('[Pasted text #3]')
|
||||
})
|
||||
|
||||
test('formats with large id', () => {
|
||||
expect(formatPastedTextRef(99, 5)).toBe('[Pasted text #99 +5 lines]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatImageRef', () => {
|
||||
test('formats image reference', () => {
|
||||
expect(formatImageRef(1)).toBe('[Image #1]')
|
||||
})
|
||||
|
||||
test('formats with large id', () => {
|
||||
expect(formatImageRef(42)).toBe('[Image #42]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseReferences', () => {
|
||||
test('parses Pasted text ref', () => {
|
||||
const refs = parseReferences('[Pasted text #1 +5 lines]')
|
||||
expect(refs).toHaveLength(1)
|
||||
expect(refs[0]).toEqual({
|
||||
id: 1,
|
||||
match: '[Pasted text #1 +5 lines]',
|
||||
index: 0,
|
||||
})
|
||||
})
|
||||
|
||||
test('parses Image ref', () => {
|
||||
const refs = parseReferences('[Image #2]')
|
||||
expect(refs).toHaveLength(1)
|
||||
expect(refs[0]!.id).toBe(2)
|
||||
})
|
||||
|
||||
test('parses Truncated text ref', () => {
|
||||
const refs = parseReferences('[...Truncated text #3]')
|
||||
expect(refs).toHaveLength(1)
|
||||
expect(refs[0]!.id).toBe(3)
|
||||
})
|
||||
|
||||
test('parses Pasted text without line count', () => {
|
||||
const refs = parseReferences('[Pasted text #4]')
|
||||
expect(refs).toHaveLength(1)
|
||||
expect(refs[0]!.id).toBe(4)
|
||||
})
|
||||
|
||||
test('parses multiple refs', () => {
|
||||
const refs = parseReferences('hello [Pasted text #1] world [Image #2]')
|
||||
expect(refs).toHaveLength(2)
|
||||
expect(refs[0]!.id).toBe(1)
|
||||
expect(refs[1]!.id).toBe(2)
|
||||
})
|
||||
|
||||
test('returns empty for no refs', () => {
|
||||
expect(parseReferences('plain text')).toEqual([])
|
||||
})
|
||||
|
||||
test('filters out id 0', () => {
|
||||
const refs = parseReferences('[Pasted text #0]')
|
||||
expect(refs).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('captures correct index for embedded refs', () => {
|
||||
const input = 'prefix [Pasted text #1] suffix'
|
||||
const refs = parseReferences(input)
|
||||
expect(refs[0]!.index).toBe(7)
|
||||
})
|
||||
|
||||
test('handles duplicate refs', () => {
|
||||
const refs = parseReferences('[Pasted text #1] and [Pasted text #1]')
|
||||
expect(refs).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('expandPastedTextRefs', () => {
|
||||
test('replaces single text ref', () => {
|
||||
const input = 'look at [Pasted text #1 +2 lines]'
|
||||
const pastedContents = {
|
||||
1: { id: 1, type: 'text' as const, content: 'line1\nline2\nline3' },
|
||||
}
|
||||
const result = expandPastedTextRefs(input, pastedContents)
|
||||
expect(result).toBe('look at line1\nline2\nline3')
|
||||
})
|
||||
|
||||
test('replaces multiple text refs in reverse order', () => {
|
||||
const input = '[Pasted text #1] and [Pasted text #2]'
|
||||
const pastedContents = {
|
||||
1: { id: 1, type: 'text' as const, content: 'AAA' },
|
||||
2: { id: 2, type: 'text' as const, content: 'BBB' },
|
||||
}
|
||||
const result = expandPastedTextRefs(input, pastedContents)
|
||||
expect(result).toBe('AAA and BBB')
|
||||
})
|
||||
|
||||
test('does not replace image refs', () => {
|
||||
const input = '[Image #1]'
|
||||
const pastedContents = {
|
||||
1: { id: 1, type: 'image' as const, content: 'data' },
|
||||
}
|
||||
const result = expandPastedTextRefs(input, pastedContents)
|
||||
expect(result).toBe('[Image #1]')
|
||||
})
|
||||
|
||||
test('returns original when no refs', () => {
|
||||
const input = 'no refs here'
|
||||
const result = expandPastedTextRefs(input, {})
|
||||
expect(result).toBe('no refs here')
|
||||
})
|
||||
|
||||
test('skips refs with no matching pasted content', () => {
|
||||
const input = '[Pasted text #99 +1 lines]'
|
||||
const result = expandPastedTextRefs(input, {})
|
||||
expect(result).toBe('[Pasted text #99 +1 lines]')
|
||||
})
|
||||
|
||||
test('handles mixed content', () => {
|
||||
const input = 'see [Pasted text #1] and [Image #2]'
|
||||
const pastedContents = {
|
||||
1: { id: 1, type: 'text' as const, content: 'code here' },
|
||||
2: { id: 2, type: 'image' as const, content: 'img data' },
|
||||
}
|
||||
const result = expandPastedTextRefs(input, pastedContents)
|
||||
expect(result).toBe('see code here and [Image #2]')
|
||||
})
|
||||
})
|
||||
@@ -1,85 +0,0 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { parseToolPreset, filterToolsByDenyRules } from '../tools'
|
||||
import { getEmptyToolPermissionContext } from '../Tool'
|
||||
|
||||
describe('parseToolPreset', () => {
|
||||
test('returns "default" for "default" input', () => {
|
||||
expect(parseToolPreset('default')).toBe('default')
|
||||
})
|
||||
|
||||
test('returns "default" for "Default" input (case-insensitive)', () => {
|
||||
expect(parseToolPreset('Default')).toBe('default')
|
||||
})
|
||||
|
||||
test('returns null for unknown preset', () => {
|
||||
expect(parseToolPreset('unknown')).toBeNull()
|
||||
})
|
||||
|
||||
test('returns null for empty string', () => {
|
||||
expect(parseToolPreset('')).toBeNull()
|
||||
})
|
||||
|
||||
test('returns null for random string', () => {
|
||||
expect(parseToolPreset('custom-preset')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// ─── filterToolsByDenyRules ─────────────────────────────────────────────
|
||||
|
||||
describe('filterToolsByDenyRules', () => {
|
||||
const mockTools = [
|
||||
{ name: 'Bash', mcpInfo: undefined },
|
||||
{ name: 'Read', mcpInfo: undefined },
|
||||
{ name: 'Write', mcpInfo: undefined },
|
||||
{
|
||||
name: 'mcp__server__tool',
|
||||
mcpInfo: { serverName: 'server', toolName: 'tool' },
|
||||
},
|
||||
]
|
||||
|
||||
test('returns all tools when no deny rules', () => {
|
||||
const ctx = getEmptyToolPermissionContext()
|
||||
const result = filterToolsByDenyRules(mockTools, ctx)
|
||||
expect(result).toHaveLength(4)
|
||||
})
|
||||
|
||||
test('filters out denied tool by name', () => {
|
||||
const ctx = {
|
||||
...getEmptyToolPermissionContext(),
|
||||
alwaysDenyRules: {
|
||||
localSettings: ['Bash'],
|
||||
},
|
||||
}
|
||||
const result = filterToolsByDenyRules(mockTools, ctx as any)
|
||||
expect(result.find(t => t.name === 'Bash')).toBeUndefined()
|
||||
expect(result).toHaveLength(3)
|
||||
})
|
||||
|
||||
test('filters out multiple denied tools', () => {
|
||||
const ctx = {
|
||||
...getEmptyToolPermissionContext(),
|
||||
alwaysDenyRules: {
|
||||
localSettings: ['Bash', 'Write'],
|
||||
},
|
||||
}
|
||||
const result = filterToolsByDenyRules(mockTools, ctx as any)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.map(t => t.name)).toEqual(['Read', 'mcp__server__tool'])
|
||||
})
|
||||
|
||||
test('returns empty array when all tools denied', () => {
|
||||
const ctx = {
|
||||
...getEmptyToolPermissionContext(),
|
||||
alwaysDenyRules: {
|
||||
localSettings: mockTools.map(t => t.name),
|
||||
},
|
||||
}
|
||||
const result = filterToolsByDenyRules(mockTools, ctx as any)
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('handles empty tools array', () => {
|
||||
const ctx = getEmptyToolPermissionContext()
|
||||
expect(filterToolsByDenyRules([], ctx)).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,84 +1,3 @@
|
||||
import axios from 'axios'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { errorMessage } from '../utils/errors.js'
|
||||
import { validateBridgeId } from './bridgeApi.js'
|
||||
import { getBridgeAccessToken } from './bridgeConfig.js'
|
||||
import { getReplBridgeHandle } from './replBridgeHandle.js'
|
||||
import { toCompatSessionId } from './sessionIdCompat.js'
|
||||
|
||||
/**
|
||||
* Send a plain-text message to another Claude session via the bridge API.
|
||||
*
|
||||
* Called by SendMessageTool when the target address scheme is "bridge:".
|
||||
* Uses the current ReplBridgeHandle to derive the sender identity and
|
||||
* the session ingress URL for the POST request.
|
||||
*
|
||||
* @param target - Target session ID (from the "bridge:<sessionId>" address)
|
||||
* @param message - Plain text message content (structured messages are rejected upstream)
|
||||
* @returns { ok: true } on success, { ok: false, error } on failure. Never throws.
|
||||
*/
|
||||
export async function postInterClaudeMessage(
|
||||
target: string,
|
||||
message: string,
|
||||
): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||
try {
|
||||
const handle = getReplBridgeHandle()
|
||||
if (!handle) {
|
||||
return { ok: false, error: 'Bridge not connected' }
|
||||
}
|
||||
|
||||
const normalizedTarget = target.trim()
|
||||
if (!normalizedTarget) {
|
||||
return { ok: false, error: 'No target session specified' }
|
||||
}
|
||||
|
||||
const accessToken = getBridgeAccessToken()
|
||||
if (!accessToken) {
|
||||
return { ok: false, error: 'No access token available' }
|
||||
}
|
||||
|
||||
const compatTarget = toCompatSessionId(normalizedTarget)
|
||||
// Validate against path traversal — same allowlist as bridgeApi.ts
|
||||
validateBridgeId(compatTarget, 'target sessionId')
|
||||
const from = toCompatSessionId(handle.bridgeSessionId)
|
||||
const baseUrl = handle.sessionIngressUrl
|
||||
|
||||
const url = `${baseUrl}/v1/sessions/${encodeURIComponent(compatTarget)}/messages`
|
||||
|
||||
const response = await axios.post(
|
||||
url,
|
||||
{
|
||||
type: 'peer_message',
|
||||
from,
|
||||
content: message,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
timeout: 10_000,
|
||||
validateStatus: (s: number) => s < 500,
|
||||
},
|
||||
)
|
||||
|
||||
if (response.status === 200 || response.status === 204) {
|
||||
logForDebugging(
|
||||
`[bridge:peer] Message sent to ${compatTarget} (${response.status})`,
|
||||
)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
const detail =
|
||||
typeof response.data === 'object' && response.data?.error?.message
|
||||
? response.data.error.message
|
||||
: `HTTP ${response.status}`
|
||||
logForDebugging(`[bridge:peer] Send failed: ${detail}`)
|
||||
return { ok: false, error: detail }
|
||||
} catch (err: unknown) {
|
||||
const msg = errorMessage(err)
|
||||
logForDebugging(`[bridge:peer] postInterClaudeMessage error: ${msg}`)
|
||||
return { ok: false, error: msg }
|
||||
}
|
||||
}
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const postInterClaudeMessage: (target: string, message: string) => Promise<{ ok: boolean; error?: string }> = () => Promise.resolve({ ok: false });
|
||||
|
||||
@@ -1,57 +1,3 @@
|
||||
/**
|
||||
* Sanitize inbound GitHub webhook payload content before it enters the session.
|
||||
*
|
||||
* Called from useReplBridge.tsx when feature('KAIROS_GITHUB_WEBHOOKS') is enabled.
|
||||
* Strips known secret patterns (tokens, API keys, credentials) while preserving
|
||||
* the meaningful content (PR titles, descriptions, commit messages, etc.).
|
||||
*
|
||||
* Must be synchronous and never throw — on error, returns a safe placeholder.
|
||||
*/
|
||||
|
||||
/** Patterns that match known secret/token formats. */
|
||||
const SECRET_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [
|
||||
// GitHub tokens (PAT, OAuth, App, Server-to-server)
|
||||
{ pattern: /\b(ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{10,}\b/g, replacement: '[REDACTED_GITHUB_TOKEN]' },
|
||||
// Anthropic API keys
|
||||
{ pattern: /\bsk-ant-[A-Za-z0-9_-]{10,}\b/g, replacement: '[REDACTED_ANTHROPIC_KEY]' },
|
||||
// Generic Bearer tokens in headers
|
||||
{ pattern: /(Bearer\s+)[A-Za-z0-9._\-/+=]{20,}/gi, replacement: '$1[REDACTED_TOKEN]' },
|
||||
// AWS access keys
|
||||
{ pattern: /\b(AKIA|ASIA)[A-Z0-9]{16}\b/g, replacement: '[REDACTED_AWS_KEY]' },
|
||||
// AWS secret keys (40-char base64-like strings after common labels)
|
||||
{ pattern: /(aws_secret_access_key|secret_key|SecretAccessKey)['":\s=]+[A-Za-z0-9/+=]{30,}/gi, replacement: '$1=[REDACTED_AWS_SECRET]' },
|
||||
// Generic API key patterns (key=value or "key": "value")
|
||||
{ pattern: /(api[_-]?key|apikey|secret|password|token|credential)['":\s=]+["']?[A-Za-z0-9._\-/+=]{16,}["']?/gi, replacement: '$1=[REDACTED]' },
|
||||
// npm tokens
|
||||
{ pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[REDACTED_NPM_TOKEN]' },
|
||||
// Slack tokens
|
||||
{ pattern: /\bxox[bporas]-[A-Za-z0-9-]{10,}\b/g, replacement: '[REDACTED_SLACK_TOKEN]' },
|
||||
]
|
||||
|
||||
/** Maximum content length before truncation (100KB). */
|
||||
const MAX_CONTENT_LENGTH = 100_000
|
||||
|
||||
export function sanitizeInboundWebhookContent(content: string): string {
|
||||
try {
|
||||
if (!content) return content
|
||||
|
||||
let sanitized = content
|
||||
|
||||
// Redact known secret patterns first (before truncation to avoid
|
||||
// splitting a secret across the truncation boundary)
|
||||
for (const { pattern, replacement } of SECRET_PATTERNS) {
|
||||
pattern.lastIndex = 0
|
||||
sanitized = sanitized.replace(pattern, replacement)
|
||||
}
|
||||
|
||||
// Truncate excessively large payloads after redaction
|
||||
if (sanitized.length > MAX_CONTENT_LENGTH) {
|
||||
sanitized = sanitized.slice(0, MAX_CONTENT_LENGTH) + '\n... [truncated]'
|
||||
}
|
||||
|
||||
return sanitized
|
||||
} catch {
|
||||
// Never throw, never return raw content — return a safe placeholder
|
||||
return '[webhook content redacted due to sanitization error]'
|
||||
}
|
||||
}
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const sanitizeInboundWebhookContent: (content: string) => string = (content) => content;
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
/**
|
||||
* Companion display card — shown by /buddy (no args).
|
||||
* Mirrors official vc8 component: bordered box with sprite, stats, last reaction.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../ink.js';
|
||||
import { useInput } from '../ink.js';
|
||||
import { renderSprite } from './sprites.js';
|
||||
import { RARITY_COLORS, RARITY_STARS, STAT_NAMES, type Companion } from './types.js';
|
||||
|
||||
const CARD_WIDTH = 40;
|
||||
const CARD_PADDING_X = 2;
|
||||
|
||||
function StatBar({ name, value }: { name: string; value: number }) {
|
||||
const clamped = Math.max(0, Math.min(100, value));
|
||||
const filled = Math.round(clamped / 10);
|
||||
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled);
|
||||
return (
|
||||
<Text>
|
||||
{name.padEnd(10)} {bar} {String(value).padStart(3)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export function CompanionCard({
|
||||
companion,
|
||||
lastReaction,
|
||||
onDone,
|
||||
}: {
|
||||
companion: Companion;
|
||||
lastReaction?: string;
|
||||
onDone?: (result?: string, options?: { display?: string }) => void;
|
||||
}) {
|
||||
const color = RARITY_COLORS[companion.rarity];
|
||||
const stars = RARITY_STARS[companion.rarity];
|
||||
const sprite = renderSprite(companion, 0);
|
||||
|
||||
// Press any key to dismiss
|
||||
useInput(
|
||||
() => {
|
||||
onDone?.(undefined, { display: 'skip' });
|
||||
},
|
||||
{ isActive: onDone !== undefined },
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={color}
|
||||
paddingX={CARD_PADDING_X}
|
||||
paddingY={1}
|
||||
width={CARD_WIDTH}
|
||||
flexShrink={0}
|
||||
>
|
||||
{/* Header: rarity + species */}
|
||||
<Box justifyContent="space-between">
|
||||
<Text bold color={color}>
|
||||
{stars} {companion.rarity.toUpperCase()}
|
||||
</Text>
|
||||
<Text color={color}>{companion.species.toUpperCase()}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Shiny indicator */}
|
||||
{companion.shiny && (
|
||||
<Text color="warning" bold>
|
||||
{'\u2728'} SHINY {'\u2728'}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Sprite */}
|
||||
<Box flexDirection="column" marginY={1}>
|
||||
{sprite.map((line, i) => (
|
||||
<Text key={i} color={color}>
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Name */}
|
||||
<Text bold>{companion.name}</Text>
|
||||
|
||||
{/* Personality */}
|
||||
<Box marginY={1}>
|
||||
<Text dimColor italic>
|
||||
"{companion.personality}"
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Stats */}
|
||||
<Box flexDirection="column">
|
||||
{STAT_NAMES.map(name => (
|
||||
<StatBar key={name} name={name} value={companion.stats[name] ?? 0} />
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Last reaction */}
|
||||
{lastReaction && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text dimColor>last said</Text>
|
||||
<Box borderStyle="round" borderColor="inactive" paddingX={1}>
|
||||
<Text dimColor italic>
|
||||
{lastReaction}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -116,21 +116,18 @@ export function rollWithSeed(seed: string): Roll {
|
||||
return rollFrom(mulberry32(hashString(seed)))
|
||||
}
|
||||
|
||||
export function generateSeed(): string {
|
||||
return `rehatch-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||
}
|
||||
|
||||
export function companionUserId(): string {
|
||||
const config = getGlobalConfig()
|
||||
return config.oauthAccount?.accountUuid ?? config.userID ?? 'anon'
|
||||
}
|
||||
|
||||
// Regenerate bones from seed or userId, merge with stored soul.
|
||||
// Regenerate bones from userId, merge with stored soul. Bones never persist
|
||||
// so species renames and SPECIES-array edits can't break stored companions,
|
||||
// and editing config.companion can't fake a rarity.
|
||||
export function getCompanion(): Companion | undefined {
|
||||
const stored = getGlobalConfig().companion
|
||||
if (!stored) return undefined
|
||||
const seed = stored.seed ?? companionUserId()
|
||||
const { bones } = rollWithSeed(seed)
|
||||
const { bones } = roll(companionUserId())
|
||||
// bones last so stale bones fields in old-format configs get overridden
|
||||
return { ...stored, ...bones }
|
||||
}
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
/**
|
||||
* Companion reaction system — aligns with official ZUK + Dc8 pattern.
|
||||
*
|
||||
* Called from REPL.tsx after each query turn. Checks mute state, frequency
|
||||
* limits, and @-mention detection, then calls the buddy_react API to
|
||||
* generate a reaction shown in the CompanionSprite speech bubble.
|
||||
*/
|
||||
import { getCompanion } from './companion.js'
|
||||
import { getGlobalConfig } from '../utils/config.js'
|
||||
import { getClaudeAIOAuthTokens } from '../utils/auth.js'
|
||||
import { getOauthConfig } from '../constants/oauth.js'
|
||||
import { getUserAgent } from '../utils/http.js'
|
||||
import type { Message } from '../types/message.js'
|
||||
|
||||
// ─── Rate limiting ──────────────────────────────────
|
||||
|
||||
let lastReactTime = 0
|
||||
const MIN_INTERVAL_MS = 45_000 // official is roughly 30-60s
|
||||
|
||||
// ─── Recent reactions (avoid repetition) ────────────
|
||||
|
||||
const recentReactions: string[] = []
|
||||
const MAX_RECENT = 8
|
||||
|
||||
// ─── Public API ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Trigger a companion reaction after a query turn.
|
||||
*
|
||||
* Mirrors official `ZUK()`:
|
||||
* 1. Check companion exists and is not muted
|
||||
* 2. Detect if user @-mentioned companion by name
|
||||
* 3. Apply rate limiting (skip if not addressed and too soon)
|
||||
* 4. Build conversation transcript
|
||||
* 5. Call buddy_react API
|
||||
* 6. Pass reaction text to setReaction callback
|
||||
*/
|
||||
export function triggerCompanionReaction(
|
||||
messages: Message[],
|
||||
setReaction: (text: string | undefined) => void,
|
||||
): void {
|
||||
const companion = getCompanion()
|
||||
if (!companion || getGlobalConfig().companionMuted) return
|
||||
|
||||
const addressed = isAddressed(messages, companion.name)
|
||||
|
||||
const now = Date.now()
|
||||
if (!addressed && now - lastReactTime < MIN_INTERVAL_MS) return
|
||||
|
||||
const transcript = buildTranscript(messages)
|
||||
if (!transcript.trim()) return
|
||||
|
||||
lastReactTime = now
|
||||
|
||||
void callBuddyReactAPI(companion, transcript, addressed)
|
||||
.then(reaction => {
|
||||
if (!reaction) return
|
||||
recentReactions.push(reaction)
|
||||
if (recentReactions.length > MAX_RECENT) recentReactions.shift()
|
||||
setReaction(reaction)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────
|
||||
|
||||
function isAddressed(messages: Message[], name: string): boolean {
|
||||
const pattern = new RegExp(`\\b${escapeRegex(name)}\\b`, 'i')
|
||||
for (
|
||||
let i = messages.length - 1;
|
||||
i >= Math.max(0, messages.length - 3);
|
||||
i--
|
||||
) {
|
||||
const m = messages[i]
|
||||
if (m?.type !== 'user') continue
|
||||
const content = (m as any).message?.content
|
||||
if (typeof content === 'string' && pattern.test(content)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function escapeRegex(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
function buildTranscript(messages: Message[]): string {
|
||||
return messages
|
||||
.slice(-12)
|
||||
.filter(m => m.type === 'user' || m.type === 'assistant')
|
||||
.map(m => {
|
||||
const role = m.type === 'user' ? 'user' : 'claude'
|
||||
const content = (m as any).message?.content
|
||||
const text =
|
||||
typeof content === 'string'
|
||||
? content.slice(0, 300)
|
||||
: Array.isArray(content)
|
||||
? content
|
||||
.filter((b: any) => b?.type === 'text')
|
||||
.map((b: any) => b.text)
|
||||
.join(' ')
|
||||
.slice(0, 300)
|
||||
: ''
|
||||
return `${role}: ${text}`
|
||||
})
|
||||
.join('\n')
|
||||
.slice(0, 5000)
|
||||
}
|
||||
|
||||
// ─── API call ───────────────────────────────────────
|
||||
|
||||
async function callBuddyReactAPI(
|
||||
companion: {
|
||||
name: string
|
||||
personality: string
|
||||
species: string
|
||||
rarity: string
|
||||
stats: Record<string, number>
|
||||
},
|
||||
transcript: string,
|
||||
addressed: boolean,
|
||||
): Promise<string | null> {
|
||||
const tokens = getClaudeAIOAuthTokens()
|
||||
if (!tokens?.accessToken) return null
|
||||
|
||||
const orgId = getGlobalConfig().oauthAccount?.organizationUuid
|
||||
if (!orgId) return null
|
||||
|
||||
const baseUrl = getOauthConfig().BASE_API_URL
|
||||
const url = `${baseUrl}/api/organizations/${orgId}/claude_code/buddy_react`
|
||||
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': getUserAgent(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: companion.name.slice(0, 32),
|
||||
personality: companion.personality.slice(0, 200),
|
||||
species: companion.species,
|
||||
rarity: companion.rarity,
|
||||
stats: companion.stats,
|
||||
transcript,
|
||||
reason: addressed ? 'addressed' : 'turn',
|
||||
recent: recentReactions.map(r => r.slice(0, 200)),
|
||||
addressed,
|
||||
}),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
})
|
||||
|
||||
if (!resp.ok) return null
|
||||
|
||||
try {
|
||||
const data = (await resp.json()) as { reaction?: string }
|
||||
return data.reaction?.trim() || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,6 @@ export type CompanionBones = {
|
||||
export type CompanionSoul = {
|
||||
name: string
|
||||
personality: string
|
||||
seed?: string
|
||||
}
|
||||
|
||||
export type Companion = CompanionBones &
|
||||
|
||||
@@ -10,12 +10,12 @@ import { getRainbowColor } from '../utils/thinking.js';
|
||||
// buzz instead of a single UTC-midnight spike, gentler on soul-gen load.
|
||||
// Teaser window: April 1-7, 2026 only. Command stays live forever after.
|
||||
export function isBuddyTeaserWindow(): boolean {
|
||||
if ((process.env.USER_TYPE) === 'ant') return true;
|
||||
if (("external" as string) === 'ant') return true;
|
||||
const d = new Date();
|
||||
return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7;
|
||||
}
|
||||
export function isBuddyLive(): boolean {
|
||||
if ((process.env.USER_TYPE) === 'ant') return true;
|
||||
if (("external" as string) === 'ant') return true;
|
||||
const d = new Date();
|
||||
return d.getFullYear() > 2026 || d.getFullYear() === 2026 && d.getMonth() >= 3;
|
||||
}
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
getCompanion,
|
||||
rollWithSeed,
|
||||
generateSeed,
|
||||
} from '../../buddy/companion.js'
|
||||
import { type StoredCompanion, RARITY_STARS } from '../../buddy/types.js'
|
||||
import { renderSprite } from '../../buddy/sprites.js'
|
||||
import { CompanionCard } from '../../buddy/CompanionCard.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { triggerCompanionReaction } from '../../buddy/companionReact.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type {
|
||||
LocalJSXCommandContext,
|
||||
LocalJSXCommandOnDone,
|
||||
} from '../../types/command.js'
|
||||
|
||||
// Species → default name fragments for hatch (no API needed)
|
||||
const SPECIES_NAMES: Record<string, string> = {
|
||||
duck: 'Waddles',
|
||||
goose: 'Goosberry',
|
||||
blob: 'Gooey',
|
||||
cat: 'Whiskers',
|
||||
dragon: 'Ember',
|
||||
octopus: 'Inky',
|
||||
owl: 'Hoots',
|
||||
penguin: 'Waddleford',
|
||||
turtle: 'Shelly',
|
||||
snail: 'Trailblazer',
|
||||
ghost: 'Casper',
|
||||
axolotl: 'Axie',
|
||||
capybara: 'Chill',
|
||||
cactus: 'Spike',
|
||||
robot: 'Byte',
|
||||
rabbit: 'Flops',
|
||||
mushroom: 'Spore',
|
||||
chonk: 'Chonk',
|
||||
}
|
||||
|
||||
const SPECIES_PERSONALITY: Record<string, string> = {
|
||||
duck: 'Quirky and easily amused. Leaves rubber duck debugging tips everywhere.',
|
||||
goose: 'Assertive and honks at bad code. Takes no prisoners in code reviews.',
|
||||
blob: 'Adaptable and goes with the flow. Sometimes splits into two when confused.',
|
||||
cat: 'Independent and judgmental. Watches you type with mild disdain.',
|
||||
dragon:
|
||||
'Fiery and passionate about architecture. Hoards good variable names.',
|
||||
octopus:
|
||||
'Multitasker extraordinaire. Wraps tentacles around every problem at once.',
|
||||
owl: 'Wise but verbose. Always says "let me think about that" for exactly 3 seconds.',
|
||||
penguin: 'Cool under pressure. Slides gracefully through merge conflicts.',
|
||||
turtle: 'Patient and thorough. Believes slow and steady wins the deploy.',
|
||||
snail: 'Methodical and leaves a trail of useful comments. Never rushes.',
|
||||
ghost:
|
||||
'Ethereal and appears at the worst possible moments with spooky insights.',
|
||||
axolotl: 'Regenerative and cheerful. Recovers from any bug with a smile.',
|
||||
capybara: 'Zen master. Remains calm while everything around is on fire.',
|
||||
cactus:
|
||||
'Prickly on the outside but full of good intentions. Thrives on neglect.',
|
||||
robot: 'Efficient and literal. Processes feedback in binary.',
|
||||
rabbit: 'Energetic and hops between tasks. Finishes before you start.',
|
||||
mushroom: 'Quietly insightful. Grows on you over time.',
|
||||
chonk:
|
||||
'Big, warm, and takes up the whole couch. Prioritizes comfort over elegance.',
|
||||
}
|
||||
|
||||
function speciesLabel(species: string): string {
|
||||
return species.charAt(0).toUpperCase() + species.slice(1)
|
||||
}
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ToolUseContext & LocalJSXCommandContext,
|
||||
args: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const sub = args?.trim().toLowerCase() ?? ''
|
||||
const setState = context.setAppState
|
||||
|
||||
// ── /buddy off — mute companion ──
|
||||
if (sub === 'off') {
|
||||
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: true }))
|
||||
onDone('companion muted', { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
// ── /buddy on — unmute companion ──
|
||||
if (sub === 'on') {
|
||||
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false }))
|
||||
onDone('companion unmuted', { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
// ── /buddy pet — trigger heart animation + auto unmute ──
|
||||
if (sub === 'pet') {
|
||||
const companion = getCompanion()
|
||||
if (!companion) {
|
||||
onDone('no companion yet \u00b7 run /buddy first', { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
// Auto-unmute on pet + trigger heart animation
|
||||
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false }))
|
||||
setState?.(prev => ({ ...prev, companionPetAt: Date.now() }))
|
||||
|
||||
// Trigger a post-pet reaction
|
||||
triggerCompanionReaction(context.messages ?? [], reaction =>
|
||||
setState?.(prev =>
|
||||
prev.companionReaction === reaction
|
||||
? prev
|
||||
: { ...prev, companionReaction: reaction },
|
||||
),
|
||||
)
|
||||
|
||||
onDone(`petted ${companion.name}`, { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
// ── /buddy (no args) — show existing or hatch ──
|
||||
const companion = getCompanion()
|
||||
|
||||
// Auto-unmute when viewing
|
||||
if (companion && getGlobalConfig().companionMuted) {
|
||||
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false }))
|
||||
}
|
||||
|
||||
if (companion) {
|
||||
// Return JSX card — matches official vc8 component
|
||||
const lastReaction = context.getAppState?.()?.companionReaction
|
||||
return React.createElement(CompanionCard, {
|
||||
companion,
|
||||
lastReaction,
|
||||
onDone,
|
||||
})
|
||||
}
|
||||
|
||||
// ── No companion → hatch ──
|
||||
const seed = generateSeed()
|
||||
const r = rollWithSeed(seed)
|
||||
const name = SPECIES_NAMES[r.bones.species] ?? 'Buddy'
|
||||
const personality =
|
||||
SPECIES_PERSONALITY[r.bones.species] ?? 'Mysterious and code-savvy.'
|
||||
|
||||
const stored: StoredCompanion = {
|
||||
name,
|
||||
personality,
|
||||
seed,
|
||||
hatchedAt: Date.now(),
|
||||
}
|
||||
|
||||
saveGlobalConfig(cfg => ({ ...cfg, companion: stored }))
|
||||
|
||||
const stars = RARITY_STARS[r.bones.rarity]
|
||||
const sprite = renderSprite(r.bones, 0)
|
||||
const shiny = r.bones.shiny ? ' \u2728 Shiny!' : ''
|
||||
|
||||
const lines = [
|
||||
'A wild companion appeared!',
|
||||
'',
|
||||
...sprite,
|
||||
'',
|
||||
`${name} the ${speciesLabel(r.bones.species)}${shiny}`,
|
||||
`Rarity: ${stars} (${r.bones.rarity})`,
|
||||
`"${personality}"`,
|
||||
'',
|
||||
'Your companion will now appear beside your input box!',
|
||||
'Say its name to get its take \u00b7 /buddy pet \u00b7 /buddy off',
|
||||
]
|
||||
onDone(lines.join('\n'), { display: 'system' })
|
||||
return null
|
||||
}
|
||||
@@ -1,16 +1,3 @@
|
||||
import type { Command } from '../../commands.js'
|
||||
import { isBuddyLive } from '../../buddy/useBuddyNotification.js'
|
||||
|
||||
const buddy = {
|
||||
type: 'local-jsx',
|
||||
name: 'buddy',
|
||||
description: 'Hatch a coding companion · pet, off',
|
||||
argumentHint: '[pet|off]',
|
||||
immediate: true,
|
||||
get isHidden() {
|
||||
return !isBuddyLive()
|
||||
},
|
||||
load: () => import('./buddy.js'),
|
||||
} satisfies Command
|
||||
|
||||
export default buddy
|
||||
// Auto-generated stub — replace with real implementation
|
||||
const _default: Record<string, unknown> = {};
|
||||
export default _default;
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, arg
|
||||
}
|
||||
|
||||
// Redirect base /mcp command to /plugins installed tab for ant users
|
||||
if ((process.env.USER_TYPE) === 'ant') {
|
||||
if (("external" as string) === 'ant') {
|
||||
return <PluginSettings onComplete={onDone} args="manage" showMcpRedirectMessage />;
|
||||
}
|
||||
return <MCPSettings onComplete={onDone} />;
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { parsePluginArgs } from "../parseArgs";
|
||||
|
||||
describe("parsePluginArgs", () => {
|
||||
// No args
|
||||
test("returns { type: 'menu' } for undefined", () => {
|
||||
expect(parsePluginArgs(undefined)).toEqual({ type: "menu" });
|
||||
});
|
||||
|
||||
test("returns { type: 'menu' } for empty string", () => {
|
||||
expect(parsePluginArgs("")).toEqual({ type: "menu" });
|
||||
});
|
||||
|
||||
test("returns { type: 'menu' } for whitespace only", () => {
|
||||
expect(parsePluginArgs(" ")).toEqual({ type: "menu" });
|
||||
});
|
||||
|
||||
// Help
|
||||
test("returns { type: 'help' } for 'help'", () => {
|
||||
expect(parsePluginArgs("help")).toEqual({ type: "help" });
|
||||
});
|
||||
|
||||
test("returns { type: 'help' } for '--help'", () => {
|
||||
expect(parsePluginArgs("--help")).toEqual({ type: "help" });
|
||||
});
|
||||
|
||||
test("returns { type: 'help' } for '-h'", () => {
|
||||
expect(parsePluginArgs("-h")).toEqual({ type: "help" });
|
||||
});
|
||||
|
||||
// Install
|
||||
test("parses 'install my-plugin' -> { type: 'install', plugin: 'my-plugin' }", () => {
|
||||
expect(parsePluginArgs("install my-plugin")).toEqual({
|
||||
type: "install",
|
||||
plugin: "my-plugin",
|
||||
});
|
||||
});
|
||||
|
||||
test("parses 'install my-plugin@github' with marketplace", () => {
|
||||
expect(parsePluginArgs("install my-plugin@github")).toEqual({
|
||||
type: "install",
|
||||
plugin: "my-plugin",
|
||||
marketplace: "github",
|
||||
});
|
||||
});
|
||||
|
||||
test("parses 'install https://github.com/...' as URL marketplace", () => {
|
||||
expect(parsePluginArgs("install https://github.com/plugins/my-plugin")).toEqual({
|
||||
type: "install",
|
||||
marketplace: "https://github.com/plugins/my-plugin",
|
||||
});
|
||||
});
|
||||
|
||||
test("parses 'i plugin' as install shorthand", () => {
|
||||
expect(parsePluginArgs("i plugin")).toEqual({
|
||||
type: "install",
|
||||
plugin: "plugin",
|
||||
});
|
||||
});
|
||||
|
||||
test("install without target returns type only", () => {
|
||||
expect(parsePluginArgs("install")).toEqual({ type: "install" });
|
||||
});
|
||||
|
||||
// Uninstall
|
||||
test("returns { type: 'uninstall', plugin: '...' }", () => {
|
||||
expect(parsePluginArgs("uninstall my-plugin")).toEqual({
|
||||
type: "uninstall",
|
||||
plugin: "my-plugin",
|
||||
});
|
||||
});
|
||||
|
||||
// Enable/disable
|
||||
test("returns { type: 'enable', plugin: '...' }", () => {
|
||||
expect(parsePluginArgs("enable my-plugin")).toEqual({
|
||||
type: "enable",
|
||||
plugin: "my-plugin",
|
||||
});
|
||||
});
|
||||
|
||||
test("returns { type: 'disable', plugin: '...' }", () => {
|
||||
expect(parsePluginArgs("disable my-plugin")).toEqual({
|
||||
type: "disable",
|
||||
plugin: "my-plugin",
|
||||
});
|
||||
});
|
||||
|
||||
// Validate
|
||||
test("returns { type: 'validate', path: '...' }", () => {
|
||||
expect(parsePluginArgs("validate /path/to/plugin")).toEqual({
|
||||
type: "validate",
|
||||
path: "/path/to/plugin",
|
||||
});
|
||||
});
|
||||
|
||||
// Manage
|
||||
test("returns { type: 'manage' }", () => {
|
||||
expect(parsePluginArgs("manage")).toEqual({ type: "manage" });
|
||||
});
|
||||
|
||||
// Marketplace
|
||||
test("parses 'marketplace add ...'", () => {
|
||||
expect(parsePluginArgs("marketplace add https://example.com")).toEqual({
|
||||
type: "marketplace",
|
||||
action: "add",
|
||||
target: "https://example.com",
|
||||
});
|
||||
});
|
||||
|
||||
test("parses 'marketplace remove ...'", () => {
|
||||
expect(parsePluginArgs("marketplace remove my-source")).toEqual({
|
||||
type: "marketplace",
|
||||
action: "remove",
|
||||
target: "my-source",
|
||||
});
|
||||
});
|
||||
|
||||
test("parses 'marketplace list'", () => {
|
||||
expect(parsePluginArgs("marketplace list")).toEqual({
|
||||
type: "marketplace",
|
||||
action: "list",
|
||||
});
|
||||
});
|
||||
|
||||
test("parses 'market' as alias for 'marketplace'", () => {
|
||||
expect(parsePluginArgs("market list")).toEqual({
|
||||
type: "marketplace",
|
||||
action: "list",
|
||||
});
|
||||
});
|
||||
|
||||
// Boundary
|
||||
test("handles extra whitespace", () => {
|
||||
expect(parsePluginArgs(" install my-plugin ")).toEqual({
|
||||
type: "install",
|
||||
plugin: "my-plugin",
|
||||
});
|
||||
});
|
||||
|
||||
test("handles unknown subcommand gracefully", () => {
|
||||
expect(parsePluginArgs("foobar")).toEqual({ type: "menu" });
|
||||
});
|
||||
|
||||
test("marketplace without action returns type only", () => {
|
||||
expect(parsePluginArgs("marketplace")).toEqual({ type: "marketplace" });
|
||||
});
|
||||
});
|
||||
@@ -119,7 +119,7 @@ export async function setupTerminal(theme: ThemeName): Promise<string> {
|
||||
maybeMarkProjectOnboardingComplete();
|
||||
|
||||
// Install shell completions (ant-only, since the completion command is ant-only)
|
||||
if ((process.env.USER_TYPE) === 'ant') {
|
||||
if (("external" as string) === 'ant') {
|
||||
result += await setupShellCompletion(theme);
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -29,10 +29,10 @@ const INTERNAL_MARKETPLACE_NAME = 'claude-code-marketplace';
|
||||
const INTERNAL_MARKETPLACE_REPO = 'anthropics/claude-code-marketplace';
|
||||
const OFFICIAL_MARKETPLACE_REPO = 'anthropics/claude-plugins-official';
|
||||
function getMarketplaceName(): string {
|
||||
return (process.env.USER_TYPE) === 'ant' ? INTERNAL_MARKETPLACE_NAME : OFFICIAL_MARKETPLACE_NAME;
|
||||
return ("external" as string) === 'ant' ? INTERNAL_MARKETPLACE_NAME : OFFICIAL_MARKETPLACE_NAME;
|
||||
}
|
||||
function getMarketplaceRepo(): string {
|
||||
return (process.env.USER_TYPE) === 'ant' ? INTERNAL_MARKETPLACE_REPO : OFFICIAL_MARKETPLACE_REPO;
|
||||
return ("external" as string) === 'ant' ? INTERNAL_MARKETPLACE_REPO : OFFICIAL_MARKETPLACE_REPO;
|
||||
}
|
||||
function getPluginId(): string {
|
||||
return `thinkback@${getMarketplaceName()}`;
|
||||
|
||||
@@ -53,7 +53,7 @@ const DEFAULT_INSTRUCTIONS: string = (typeof _rawPrompt === 'string' ? _rawPromp
|
||||
// Shell-set env only, so top-level process.env read is fine
|
||||
// — settings.env never injects this.
|
||||
/* eslint-disable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs -- ant-only dev override; eager top-level read is the point (crash at startup, not silently inside the slash-command try/catch) */
|
||||
const ULTRAPLAN_INSTRUCTIONS: string = (process.env.USER_TYPE) === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE ? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd() : DEFAULT_INSTRUCTIONS;
|
||||
const ULTRAPLAN_INSTRUCTIONS: string = ("external" as string) === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE ? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd() : DEFAULT_INSTRUCTIONS;
|
||||
/* eslint-enable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs */
|
||||
|
||||
/**
|
||||
@@ -464,7 +464,7 @@ export default {
|
||||
name: 'ultraplan',
|
||||
description: `~10–30 min · Claude Code on the web drafts an advanced plan you can edit and approve. See ${CCR_TERMS_URL}`,
|
||||
argumentHint: '<prompt>',
|
||||
isEnabled: () => (process.env.USER_TYPE) === 'ant',
|
||||
isEnabled: () => ("external" as string) === 'ant',
|
||||
load: () => Promise.resolve({
|
||||
call
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user