mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 21:05:51 +00:00
Compare commits
118 Commits
v1.4.1
...
codex-subs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1058b7e643 | ||
|
|
d091dd8bae | ||
|
|
4427a6c6db | ||
|
|
13799b5058 | ||
|
|
cd59a88d44 | ||
|
|
bc4a2f1281 | ||
|
|
3cb4828de6 | ||
|
|
f5c3ee5b5d | ||
|
|
c2ac9a74c1 | ||
|
|
fc438bd222 | ||
|
|
4591432a1d | ||
|
|
901628b4d9 | ||
|
|
cf33c06021 | ||
|
|
e0ca1d054c | ||
|
|
6585d0f67c | ||
|
|
e4403ff010 | ||
|
|
9e61e7a90d | ||
|
|
d03af7bd4e | ||
|
|
e8ef955ff9 | ||
|
|
a8ed0cdce5 | ||
|
|
1c3b280c6a | ||
|
|
7a3cc24a00 | ||
|
|
2e7fc428cd | ||
|
|
ad09f38fd1 | ||
|
|
b0a3ef90dc | ||
|
|
c07ad4c738 | ||
|
|
e38d45460e | ||
|
|
e0c8e9dafc | ||
|
|
047c85fcbf | ||
|
|
da6d06365d | ||
|
|
8613d558a8 | ||
|
|
017c251f78 | ||
|
|
d4223abc34 | ||
|
|
5125a159d2 | ||
|
|
d09f363414 | ||
|
|
9d35f98ec7 | ||
|
|
eb833da33b | ||
|
|
eadd32ae47 | ||
|
|
3c55a8c83f | ||
|
|
5582bb47ef | ||
|
|
95bb191977 | ||
|
|
03811f973b | ||
|
|
02ab1a0307 | ||
|
|
2a5b263641 | ||
|
|
f2dd5142b3 | ||
|
|
4dcbaf1e66 | ||
|
|
0b304730d8 | ||
|
|
7a0dd3057e | ||
|
|
ca1c87f460 | ||
|
|
fc7a85f5c7 | ||
|
|
5bc12b00b2 | ||
|
|
792777d68c | ||
|
|
047634afe6 | ||
|
|
a92af99448 | ||
|
|
cfe1552ec9 | ||
|
|
9624f880e0 | ||
|
|
85e5a8cffb | ||
|
|
299953b0ee | ||
|
|
7a3fdf6e67 | ||
|
|
b642977afe | ||
|
|
781188862e | ||
|
|
b966eef5a9 | ||
|
|
c3d63c8fe2 | ||
|
|
7d4c4278c0 | ||
|
|
93bfdabff1 | ||
|
|
1173a62301 | ||
|
|
7ea69ca279 | ||
|
|
4e82fb5974 | ||
|
|
f43350e600 | ||
|
|
23fcbf9004 | ||
|
|
23bb09d240 | ||
|
|
d208855f07 | ||
|
|
7881cc617c | ||
|
|
c7e1c50b86 | ||
|
|
2247026bd5 | ||
|
|
eec961352b | ||
|
|
fb41513b32 | ||
|
|
94c4b37eed | ||
|
|
6c5df395c3 | ||
|
|
be97a0b010 | ||
|
|
59f8675fa3 | ||
|
|
c4775fff58 | ||
|
|
31b2fdd97a | ||
|
|
1837df5f88 | ||
|
|
04c7ed4250 | ||
|
|
711927f01b | ||
|
|
956e98a445 | ||
|
|
cee62bc654 | ||
|
|
5fc7c8e13d | ||
|
|
300faa18d0 | ||
|
|
96ec96c720 | ||
|
|
13a0bfc479 | ||
|
|
84f0271813 | ||
|
|
ed4bdb9338 | ||
|
|
e4ce08fe39 | ||
|
|
92f8a92fbb | ||
|
|
a67e2d0e97 | ||
|
|
8c629858ab | ||
|
|
494eab7204 | ||
|
|
b83c3008d0 | ||
|
|
66d2671c98 | ||
|
|
c7bc8c8636 | ||
|
|
673ccd1800 | ||
|
|
d1ab38c089 | ||
|
|
f9d011164a | ||
|
|
481e2a58a9 | ||
|
|
c5edee431f | ||
|
|
a57ca08566 | ||
|
|
6536757428 | ||
|
|
a0dc4540ca | ||
|
|
7e4df5c3e9 | ||
|
|
4d939e5722 | ||
|
|
2e9aaf4993 | ||
|
|
34154ee3f5 | ||
|
|
29cc74a170 | ||
|
|
d2b66d9d2c | ||
|
|
d70e7f7f05 | ||
|
|
72a2093cd6 |
@@ -41,7 +41,8 @@ All teach-me data is stored under `.claude/skills/teach-me/records/`:
|
||||
.claude/skills/teach-me/records/
|
||||
├── learner-profile.md # Cross-topic notes (created on first session)
|
||||
└── {topic-slug}/
|
||||
└── session.md # Learning state: concepts, status, notes
|
||||
├── session.md # Learning state: concepts, status, notes
|
||||
└── {topic-slug}-notes.md # Learner-facing summary notes (generated at session end)
|
||||
```
|
||||
|
||||
**Slug**: Topic in kebab-case, 2-5 words. Example: "Python decorators" → `python-decorators`
|
||||
@@ -275,7 +276,8 @@ Update `session.md` after each round:
|
||||
When all concepts mastered or user ends session:
|
||||
|
||||
1. Update `session.md` with final state.
|
||||
2. Update `.claude/skills/teach-me/records/learner-profile.md` (keep under 30 lines):
|
||||
2. **Generate learner-facing notes** — write `{topic-slug}-notes.md` in the topic directory. This is a standalone reference document the learner can review later. See "Notes Generation" below for format.
|
||||
3. Update `.claude/skills/teach-me/records/learner-profile.md` (keep under 30 lines):
|
||||
|
||||
```markdown
|
||||
# Learner Profile
|
||||
@@ -293,7 +295,48 @@ Updated: {timestamp}
|
||||
- Python decorators (8/10 concepts, 2025-01-15)
|
||||
```
|
||||
|
||||
3. Give a brief text summary of what was covered, key insights, and areas for further study.
|
||||
4. Give a brief text summary of what was covered, key insights, and areas for further study.
|
||||
|
||||
## Notes Generation
|
||||
|
||||
At session end, generate a learner-facing notes file at `{topic-slug}/{topic-slug}-notes.md`. This file is **written for the learner to review later**, not for the tutor. It should be self-contained and organized as a quick-reference.
|
||||
|
||||
### Notes Structure
|
||||
|
||||
```markdown
|
||||
# {Topic} 核心笔记
|
||||
|
||||
## 1. {Section Name}
|
||||
{Key concept, mechanism, or principle}
|
||||
* **One-line summary**: {what it does / why it matters}
|
||||
* **Detail**: {brief explanation, 2-4 sentences max}
|
||||
* **Example** (if applicable): {code snippet, command, or concrete scenario}
|
||||
|
||||
---
|
||||
|
||||
## 2. {Section Name}
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
## n. 实战参数 / Cheat Sheet (if applicable)
|
||||
{Practical commands, config, or quick-reference table}
|
||||
|
||||
| Parameter / Concept | What it does | Tuning tip |
|
||||
|---------------------|-------------|------------|
|
||||
| ... | ... | ... |
|
||||
```
|
||||
|
||||
### Notes Writing Rules
|
||||
|
||||
1. **Start with "what & why"** before "how". Each section should answer: what is this, why does it exist, what problem does it solve.
|
||||
2. **Use analogies sparingly but effectively**. Only include an analogy if it clarifies a non-obvious mechanism (e.g., "PagedAttention is like OS virtual memory paging").
|
||||
3. **Include trade-offs**. Every optimization or design choice has a cost. Always state it (e.g., "TP improves throughput but increases communication latency").
|
||||
4. **Code / command examples should be minimal**. Under 10 lines, self-contained, with comments explaining the key flags.
|
||||
5. **Organize by concept dependency**, not by chronological teaching order. Foundation concepts first, advanced ones last.
|
||||
6. **No quiz questions, no misconceptions, no tutor-side notes**. This is a clean reference document.
|
||||
7. **Language matches the session**. If the session was in Chinese, notes are in Chinese (technical terms can stay in English).
|
||||
8. **Keep it under 150 lines**. If it gets too long, the learner won't review it. Be ruthless about cutting fluff.
|
||||
|
||||
## Resuming Sessions
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/sh
|
||||
# pre-commit hook: 对暂存的文件运行 Biome 检查
|
||||
# 仅检查 src/ 下的 .ts/.tsx/.js/.jsx 文件
|
||||
|
||||
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^src/.*\.(ts|tsx|js|jsx)$')
|
||||
|
||||
if [ -z "$STAGED_FILES" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Running Biome lint on staged files..."
|
||||
|
||||
# 使用 biome lint 对暂存文件进行检查(仅 lint,不格式化,不自动修复)
|
||||
echo "$STAGED_FILES" | xargs bunx biome lint --no-errors-on-unmatched
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Biome lint failed. Fix errors or use --no-verify to bypass."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@@ -6,32 +6,48 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, 2026-04-25
|
||||
env:
|
||||
GIT_CONFIG_COUNT: 2
|
||||
GIT_CONFIG_KEY_0: init.defaultBranch
|
||||
GIT_CONFIG_VALUE_0: main
|
||||
GIT_CONFIG_KEY_1: advice.defaultBranchName
|
||||
GIT_CONFIG_VALUE_1: "false"
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2, 2026-04-25
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
CLAUDE_CODE_SKIP_CHROME_MCP_SETUP: "1"
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: bunx tsc --noEmit
|
||||
run: bun run typecheck
|
||||
|
||||
- name: Test with Coverage
|
||||
run: |
|
||||
set -o pipefail
|
||||
bun test --coverage --coverage-reporter=lcov 2>&1 | grep -vE '^\s*(\(pass\)|\(skip\))' | sed '/^.*\/__tests__\/.*:$/d' | cat -s
|
||||
bun test --coverage --coverage-reporter lcov --coverage-dir coverage 2>&1 | grep -vE '^\s*(\(pass\)|\(skip\))' | sed '/^.*\/__tests__\/.*:$/d' | cat -s
|
||||
test -s coverage/lcov.info
|
||||
grep -q '^SF:' coverage/lcov.info
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5, 2026-04-25
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
fail_ci_if_error: true
|
||||
files: ./coverage/lcov.info
|
||||
disable_search: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Build
|
||||
|
||||
79
.github/workflows/publish-npm.yml
vendored
Normal file
79
.github/workflows/publish-npm.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: Publish to npm
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: '版本号 (例如: v1.9.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, 2026-04-25
|
||||
with:
|
||||
ref: ${{ github.event.inputs.version || github.ref }}
|
||||
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6, 2026-04-25
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2, 2026-04-25
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Type check
|
||||
run: bun run typecheck
|
||||
|
||||
- name: Run tests
|
||||
run: bun test
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.version || github.ref_name }}"
|
||||
PREV_TAG=$(git tag --sort=-version:refname | grep -v "^${VERSION#v}$" | head -1)
|
||||
|
||||
if [ -n "$PREV_TAG" ]; then
|
||||
COMMITS=$(git log "${PREV_TAG}..${VERSION}" --pretty=format:"- %s (%h)" --no-merges)
|
||||
else
|
||||
COMMITS=$(git log --pretty=format:"- %s (%h)" --no-merges -20)
|
||||
fi
|
||||
|
||||
{
|
||||
echo "commits<<EOF"
|
||||
echo "$COMMITS"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2, 2026-04-25
|
||||
with:
|
||||
name: ${{ github.event.inputs.version || github.ref_name }}
|
||||
body: |
|
||||
## What's Changed
|
||||
|
||||
${{ steps.changelog.outputs.commits }}
|
||||
|
||||
**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ github.event.inputs.version || github.ref_name }}^...${{ github.event.inputs.version || github.ref_name }}
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.event.inputs.version || github.ref_name, 'rc') || contains(github.event.inputs.version || github.ref_name, 'beta') || contains(github.event.inputs.version || github.ref_name, 'alpha') }}
|
||||
8
.github/workflows/release-rcs.yml
vendored
8
.github/workflows/release-rcs.yml
vendored
@@ -17,17 +17,17 @@ jobs:
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, 2026-04-25
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3, 2026-04-25
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3, 2026-04-25
|
||||
|
||||
- name: Extract version
|
||||
id: version
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5, 2026-04-25
|
||||
with:
|
||||
context: .
|
||||
file: packages/remote-control-server/Dockerfile
|
||||
|
||||
11
.github/workflows/update-contributors.yml
vendored
11
.github/workflows/update-contributors.yml
vendored
@@ -1,11 +1,8 @@
|
||||
name: Update Contributors
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # 每天更新一次
|
||||
- cron: '0 0 * * 1' # 每周一更新一次
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -14,17 +11,17 @@ jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2, 2026-04-25
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: jaywcjlove/github-action-contributors@main
|
||||
- uses: jaywcjlove/github-action-contributors@86707f6d4c2469ce6b46bc3367253ebd41ee242c # main, 2026-04-25
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
output: "contributors.svg"
|
||||
repository: ${{ github.repository }}
|
||||
|
||||
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||
- uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5, 2026-04-25
|
||||
with:
|
||||
commit_message: "docs: update contributors"
|
||||
file_pattern: "contributors.svg"
|
||||
|
||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -13,13 +13,17 @@ src/utils/vendor/
|
||||
# AI tool runtime directories
|
||||
.agents/
|
||||
.claude/
|
||||
.codex/
|
||||
.omx/
|
||||
.docs/task/
|
||||
# Binary / screenshot files (root only)
|
||||
/*.png
|
||||
*.bmp
|
||||
|
||||
# Internal system prompt documents
|
||||
Claude-Opus-*.txt
|
||||
Claude-Sonnet-*.txt
|
||||
Claude-Haiku-*.txt
|
||||
|
||||
# Agent / tool state dirs
|
||||
.swarm/
|
||||
.agents/__pycache__/
|
||||
@@ -30,3 +34,14 @@ __pycache__/
|
||||
logs
|
||||
|
||||
data
|
||||
.omc
|
||||
.codex/*
|
||||
!.codex/agents/
|
||||
!.codex/agents/**
|
||||
!.codex/skills/
|
||||
!.codex/skills/**
|
||||
.codex/skills/.system/**
|
||||
!.codex/prompts/
|
||||
!.codex/prompts/**
|
||||
teach-me
|
||||
credentials.json
|
||||
|
||||
78
.impeccable.md
Normal file
78
.impeccable.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Impeccable Design Context
|
||||
|
||||
## Users
|
||||
|
||||
**Primary**: Technical teams and enterprises using AI-assisted coding in production workflows.
|
||||
- DevOps engineers managing remote agents via RCS dashboard
|
||||
- Development teams collaborating through shared sessions
|
||||
- Individual developers using terminal CLI daily
|
||||
|
||||
**Context**: Used during focused work sessions — debugging, code review, agent orchestration. Users are in "get things done" mode, not browsing. They value efficiency but also appreciate warmth and personality.
|
||||
|
||||
**Job to be done**: Make advanced AI coding tools accessible and controllable, especially features that normally require enterprise accounts or Anthropic OAuth.
|
||||
|
||||
## Brand Personality
|
||||
|
||||
**3 words**: Warm, Considered, Human
|
||||
|
||||
**Voice**: Like a knowledgeable colleague who's genuinely enthusiastic about the craft — not a corporate product manager. Community-first, open, slightly playful. Chinese developer community culture (贴吧/discord 温暖氛围).
|
||||
|
||||
**Emotional goals**: Confidence (this tool is solid), Warmth (this community is welcoming), Delight (small moments of personality make the difference).
|
||||
|
||||
**References**:
|
||||
- **Anthropic's own design language** — their clean, considered aesthetic with warm undertones. The terra cotta/burnt orange as a human accent. Lots of breathing room. Typography-forward.
|
||||
- **NOT**: Generic AI product (no ChatGPT blue, no gradient text, no "AI slop"). NOT corporate SaaS (no Salesforce-blue dashboards, no enterprise sterility).
|
||||
|
||||
**Anti-references**: Corporate enterprise dashboards, generic AI product pages, anything that looks like it was "designed by committee."
|
||||
|
||||
## Aesthetic Direction
|
||||
|
||||
**Theme**: Light + Dark dual mode (user/system preference switch)
|
||||
|
||||
**Tone**: Anthropomorphic warmth meets terminal precision. The brand orange (Claude's terra cotta) is the thread that ties everything together — it's the human element in a technical world.
|
||||
|
||||
**Typography**: Clean, considered, with good hierarchy. Terminal-native for CLI; modern web fonts for Web UI (RCS dashboard, docs). Favor readability and personality.
|
||||
|
||||
**Color**:
|
||||
- Primary: Claude orange family (`#D77757` / terra cotta)
|
||||
- Accent: Warm neutrals tinted toward orange
|
||||
- Semantic: Success/Error/Warning following Anthropic's established palette
|
||||
- Dark mode: Warm dark surfaces (not cold blue-black)
|
||||
|
||||
**Differentiation**: The CCB brand sits at the intersection of "serious tool" and "community project." It should feel like Anthropic's design principles applied to an open-source context — less corporate polish, more human craft. The mascot "Clawd" and the playful "踩踩背" naming hint at personality that the design should honor.
|
||||
|
||||
**Scope**: All Web UI — RCS control panel, documentation site, landing pages.
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Considered over clever** — Every design choice should feel intentional, not trendy. If it doesn't serve the user, it doesn't ship.
|
||||
2. **Warmth through subtlety** — Orange tints on neutrals, breathing room in layouts, personality in copy. Not giant emoji or aggressive color.
|
||||
3. **Density with clarity** — Technical users need information density, but not chaos. Every pixel earns its place.
|
||||
4. **Community voice** — The design should feel like it was made by people who use it, not by a distant design team. Slightly rough edges are fine if they're honest.
|
||||
5. **Anthropic's shadow** — When in doubt, follow Anthropic's design instincts — the clean layouts, the generous spacing, the warm color temperature. Then add the community touch.
|
||||
|
||||
## Existing Design Assets
|
||||
|
||||
### Brand Colors (from theme system)
|
||||
- Claude Orange: `rgb(215,119,87)` / `#D77757`
|
||||
- Claude Blue: `rgb(87,105,247)` / `#5769F7`
|
||||
- Permission Blue: `rgb(87,105,247)`
|
||||
- Auto Accept Violet: `rgb(135,0,255)`
|
||||
- Plan Mode Teal: `rgb(0,102,102)`
|
||||
- Success: `rgb(78,186,101)`
|
||||
- Error: `rgb(255,107,128)`
|
||||
- Warning: `rgb(255,193,7)`
|
||||
|
||||
### Logo
|
||||
- CCB text + orange play button icon
|
||||
- Dark/Light SVG variants in `docs/logo/`
|
||||
- Favicon: Orange circle `#D97706` with white play triangle
|
||||
|
||||
### Mascot
|
||||
- "Clawd" — terminal-art character with multiple poses
|
||||
- Theme-aware coloring
|
||||
|
||||
### Theme System
|
||||
- 7 variants: dark, light, dark-ansi, light-ansi, dark-daltonized, light-daltonized, auto
|
||||
- 89+ semantic color tokens
|
||||
- Full documentation in `packages/@ant/ink/docs/04-theme-system.md`
|
||||
357
AGENTS.md
Normal file
357
AGENTS.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) and other AI coding agents when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a **reverse-engineered / decompiled** version of Anthropic's official Claude Code CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. TypeScript strict mode is enforced — **`bunx tsc --noEmit` must pass with zero errors**.
|
||||
|
||||
## Git Commit Message Convention
|
||||
|
||||
使用 **Conventional Commits** 规范:
|
||||
|
||||
```
|
||||
<type>: <描述>
|
||||
```
|
||||
|
||||
常见 type:`feat`、`fix`、`docs`、`chore`、`refactor`
|
||||
|
||||
示例:
|
||||
- `feat: 添加模型 1M 上下文切换`
|
||||
- `fix: 修复初次登陆的校验问题`
|
||||
- `chore: remove prefetchOfficialMcpUrls call on startup`
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Dev mode (runs cli.tsx with MACRO defines injected via -d flags)
|
||||
bun run dev
|
||||
|
||||
# Dev mode with debugger (set BUN_INSPECT=9229 to pick port)
|
||||
bun run dev:inspect
|
||||
|
||||
# Pipe mode
|
||||
echo "say hello" | bun run src/entrypoints/cli.tsx -p
|
||||
|
||||
# Build (code splitting, outputs dist/cli.js + chunk files)
|
||||
bun run build
|
||||
|
||||
# Build with Vite (alternative build pipeline)
|
||||
bun run build:vite
|
||||
|
||||
# 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
|
||||
|
||||
# Full check (typecheck + lint + test) — run after completing any task
|
||||
bun run test:all
|
||||
bun run typecheck
|
||||
|
||||
# Remote Control Server
|
||||
bun run rcs
|
||||
|
||||
# Docs dev server (Mintlify)
|
||||
bun run docs:dev
|
||||
```
|
||||
|
||||
详细的测试规范、覆盖状态和改进计划见 `docs/testing-spec.md`。
|
||||
|
||||
## 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。Build 默认启用 19 个 feature(见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。
|
||||
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines,运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。
|
||||
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
|
||||
- **Monorepo**: Bun workspaces — 15 个 workspace packages + 若干辅助目录 in `packages/` resolved via `workspace:*`。
|
||||
- **Lint/Format**: Biome (`biome.json`)。`bun run lint` / `bun run lint:fix` / `bun run format`。
|
||||
- **Defines**: 集中管理在 `scripts/defines.ts`。当前版本 `2.1.888`。
|
||||
- **CI**: GitHub Actions — `ci.yml`(构建+测试)、`release-rcs.yml`(RCS 发布)、`update-contributors.yml`(自动更新贡献者)。
|
||||
|
||||
### Entry & Bootstrap
|
||||
|
||||
1. **`src/entrypoints/cli.tsx`** — True entrypoint。`main()` 函数按优先级处理多条快速路径:
|
||||
- `--version` / `-v` — 零模块加载
|
||||
- `--dump-system-prompt` — feature-gated (DUMP_SYSTEM_PROMPT)
|
||||
- `--claude-in-chrome-mcp` / `--chrome-native-host`
|
||||
- `--computer-use-mcp` — 独立 MCP server 模式
|
||||
- `--daemon-worker=<kind>` — feature-gated (DAEMON)
|
||||
- `remote-control` / `rc` / `remote` / `sync` / `bridge` — feature-gated (BRIDGE_MODE)
|
||||
- `daemon` [subcommand] — feature-gated (DAEMON)
|
||||
- `ps` / `logs` / `attach` / `kill` / `--bg` — feature-gated (BG_SESSIONS)
|
||||
- `new` / `list` / `reply` — Template job commands
|
||||
- `environment-runner` / `self-hosted-runner` — BYOC runner
|
||||
- `--tmux` + `--worktree` 组合
|
||||
- 默认路径:加载 `main.tsx` 启动完整 CLI
|
||||
2. **`src/main.tsx`** (~6981 行) — Commander.js CLI definition。注册大量 subcommands:`mcp` (serve/add/remove/list...)、`server`、`ssh`、`open`、`auth`、`plugin`、`agents`、`auto-mode`、`doctor`、`update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。
|
||||
3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog)。
|
||||
|
||||
### Core Loop
|
||||
|
||||
- **`src/query.ts`** — The main API query function. Sends messages to Claude API, handles streaming responses, processes tool calls, and manages the conversation turn loop.
|
||||
- **`src/QueryEngine.ts`** — Higher-level orchestrator wrapping `query()`. Manages conversation state, compaction, file history snapshots, attribution, and turn-level bookkeeping. Used by the REPL screen.
|
||||
- **`src/screens/REPL.tsx`** — The interactive REPL screen (React/Ink component). Handles user input, message display, tool permission prompts, and keyboard shortcuts.
|
||||
|
||||
### API Layer
|
||||
|
||||
- **`src/services/api/claude.ts`** — Core API client. Builds request params (system prompt, messages, tools, betas), calls the Anthropic SDK streaming endpoint, and processes `BetaRawMessageStreamEvent` events.
|
||||
- **7 providers**: `firstParty` (Anthropic direct), `bedrock` (AWS), `vertex` (Google Cloud), `foundry`, `openai`, `gemini`, `grok` (xAI)。
|
||||
- Provider selection in `src/utils/model/providers.ts`。优先级:modelType 参数 > 环境变量 > 默认 firstParty。
|
||||
|
||||
### Tool System
|
||||
|
||||
- **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`).
|
||||
- **`src/tools.ts`** — Tool registry. Assembles the tool list; tools are imported from `@claude-code-best/builtin-tools` package. Some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
|
||||
- **`packages/builtin-tools/src/tools/`** — 59 个子目录(含 shared/testing 等工具目录),通过 `@claude-code-best/builtin-tools` 包导出。主要分类:
|
||||
- **文件操作**: FileEditTool, FileReadTool, FileWriteTool, GlobTool, GrepTool
|
||||
- **Shell/执行**: BashTool, PowerShellTool, REPLTool
|
||||
- **Agent 系统**: AgentTool, TaskCreateTool, TaskUpdateTool, TaskListTool, TaskGetTool
|
||||
- **规划**: EnterPlanModeTool, ExitPlanModeV2Tool, VerifyPlanExecutionTool
|
||||
- **Web/MCP**: WebFetchTool, WebSearchTool, MCPTool, McpAuthTool
|
||||
- **调度**: CronCreateTool, CronDeleteTool, CronListTool
|
||||
- **其他**: LSPTool, ConfigTool, SkillTool, EnterWorktreeTool, ExitWorktreeTool 等
|
||||
- **`src/tools/shared/`** / **`packages/builtin-tools/src/tools/shared/`** — Tool 共享工具函数。
|
||||
|
||||
### UI Layer (Ink)
|
||||
|
||||
- **`src/ink.ts`** — Ink render wrapper with ThemeProvider injection.
|
||||
- **`packages/@ant/ink/`** — Custom Ink framework(forked/internal),包含 components、core、hooks、keybindings、theme、utils。注意:不是 `src/ink/`。
|
||||
- **`src/components/`** — 149 个组件目录/文件,渲染于终端 Ink 环境中。关键组件:
|
||||
- `App.tsx` — Root provider (AppState, Stats, FpsMetrics)
|
||||
- `Messages.tsx` / `MessageRow.tsx` — Conversation message rendering
|
||||
- `PromptInput/` — User input handling
|
||||
- `permissions/` — Tool permission approval UI
|
||||
- `design-system/` — 复用 UI 组件(Dialog, FuzzyPicker, ProgressBar, ThemeProvider 等)
|
||||
- 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).
|
||||
|
||||
### Workspace Packages
|
||||
|
||||
| Package | 说明 |
|
||||
|---------|------|
|
||||
| `packages/@ant/ink/` | Forked Ink 框架(components、hooks、keybindings、theme) |
|
||||
| `packages/@ant/computer-use-mcp/` | Computer Use MCP server(截图/键鼠/剪贴板/应用管理) |
|
||||
| `packages/@ant/computer-use-input/` | 键鼠模拟(dispatcher + darwin/win32/linux backend) |
|
||||
| `packages/@ant/computer-use-swift/` | 截图 + 应用管理(dispatcher + per-platform backend) |
|
||||
| `packages/@ant/claude-for-chrome-mcp/` | Chrome 浏览器控制(通过 `--chrome` 启用) |
|
||||
| `packages/@ant/model-provider/` | Model provider 抽象层 |
|
||||
| `packages/builtin-tools/` | 内置工具集(60 个 tool 实现,通过 `@claude-code-best/builtin-tools` 导出) |
|
||||
| `packages/agent-tools/` | Agent 工具集 |
|
||||
| `packages/acp-link/` | ACP 代理服务器(WebSocket → ACP agent 桥接) |
|
||||
| `packages/cc-knowledge/` | Claude Code 知识库(非 workspace 包) |
|
||||
| `packages/langfuse-dashboard/` | Langfuse 可观测性面板(非 workspace 包) |
|
||||
| `packages/mcp-client/` | MCP 客户端库 |
|
||||
| `packages/mcp-server/` | MCP 服务端库(非 workspace 包) |
|
||||
| `packages/remote-control-server/` | 自托管 Remote Control Server(Docker 部署,含 Web UI)— Web UI 已重构为 React + Vite + Radix UI,支持 ACP agent 接入 |
|
||||
| `packages/swarm/` | Swarm 解耦模块(非 workspace 包) |
|
||||
| `packages/shell/` | Shell 抽象(非 workspace 包) |
|
||||
| `packages/audio-capture-napi/` | 原生音频捕获(已恢复) |
|
||||
| `packages/color-diff-napi/` | 颜色差异计算(完整实现,11 tests) |
|
||||
| `packages/image-processor-napi/` | 图像处理(已恢复) |
|
||||
| `packages/modifiers-napi/` | 键盘修饰键检测(macOS FFI 实现) |
|
||||
| `packages/url-handler-napi/` | URL scheme 处理(环境变量 + CLI 参数读取) |
|
||||
|
||||
### Bridge / Remote Control
|
||||
|
||||
- **`src/bridge/`** — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`。
|
||||
- **`packages/remote-control-server/`** — 自托管 RCS,支持 Docker 部署,含 Web UI 控制面板(React 19 + Vite + Radix UI)。支持 ACP agent 通过 acp-link 接入(ACP WebSocket handler、relay handler、SSE event stream)。通过 `bun run rcs` 启动。
|
||||
- CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`。
|
||||
- 详见 `docs/features/remote-control-self-hosting.md`。
|
||||
|
||||
### ACP Protocol (Agent Client Protocol)
|
||||
|
||||
- **`src/services/acp/`** — ACP agent 实现,包含 `agent.ts`(AcpAgent 类)、`bridge.ts`(Claude Code ↔ ACP 桥接)、`permissions.ts`(权限处理)、`entry.ts`(入口)。
|
||||
- **`packages/acp-link/`** — ACP 代理服务器,将 WebSocket 客户端桥接到 ACP agent。提供 `acp-link` CLI 命令,支持自定义端口/HTTPS/认证/会话管理、RCS 集成(REST 注册 + WS identify 两步流程)、权限模式透传(fallback: 客户端传值 > config > `ACP_PERMISSION_MODE` 环境变量)。
|
||||
- ACP 权限管道改进:`createAcpCanUseTool` 统一权限流水线,`applySessionMode` 模式同步,`bypassPermissions` 可用性检测(非 root/sandbox 环境)。
|
||||
- ACP Plan 可视化已支持 `session/update plan` 类型的消息展示(PlanView 组件,含进度条/状态图标/优先级标签)。
|
||||
|
||||
### Daemon Mode
|
||||
|
||||
- **`src/daemon/`** — Daemon 模式(长驻 supervisor)。feature-gated by `DAEMON`。包含 `main.ts`(entry)和 `workerRegistry.ts`(worker 管理)。
|
||||
|
||||
### Context & System Prompt
|
||||
|
||||
- **`src/context.ts`** — Builds system/user context for the API call (git status, date, CLAUDE.md contents, memory files).
|
||||
- **`src/utils/claudemd.ts`** — Discovers and loads CLAUDE.md files from project hierarchy.
|
||||
|
||||
### Feature Flag System
|
||||
|
||||
Feature flags control which functionality is enabled at runtime. 代码中统一通过 `import { feature } from 'bun:bundle'` 导入,调用 `feature('FLAG_NAME')` 返回 `boolean`。
|
||||
|
||||
**启用方式**: 环境变量 `FEATURE_<FLAG_NAME>=1`。例如 `FEATURE_BUDDY=1 bun run dev`。
|
||||
|
||||
**Build 默认 features**(19 个,见 `build.ts`):
|
||||
- 基础: `BUDDY`, `TRANSCRIPT_CLASSIFIER`, `BRIDGE_MODE`, `AGENT_TRIGGERS_REMOTE`, `CHICAGO_MCP`, `VOICE_MODE`
|
||||
- 统计/缓存: `SHOT_STATS`, `PROMPT_CACHE_BREAK_DETECTION`, `TOKEN_BUDGET`
|
||||
- P0 本地: `AGENT_TRIGGERS`, `ULTRATHINK`, `BUILTIN_EXPLORE_PLAN_AGENTS`, `LODESTONE`
|
||||
- P1 API 依赖: `EXTRACT_MEMORIES`, `VERIFICATION_AGENT`, `KAIROS_BRIEF`, `AWAY_SUMMARY`, `ULTRAPLAN`
|
||||
- P2: `DAEMON`
|
||||
|
||||
**Dev mode 默认**: 全部启用(见 `scripts/dev.ts`)。
|
||||
|
||||
**类型声明**: `src/types/internal-modules.d.ts` 中声明了 `bun:bundle` 模块的 `feature` 函数签名。
|
||||
|
||||
**新增功能的正确做法**: 保留 `import { feature } from 'bun:bundle'` + `feature('FLAG_NAME')` 的标准模式,在运行时通过环境变量或配置控制,不要绕过 feature flag 直接 import。
|
||||
|
||||
### Multi-API 兼容层
|
||||
|
||||
所有兼容层均采用流适配器模式:将第三方 API 格式转为 Anthropic 内部格式,下游代码完全不改。通过 `/login` 命令配置。
|
||||
|
||||
#### OpenAI 兼容层
|
||||
|
||||
通过 `CLAUDE_CODE_USE_OPENAI=1` 启用,支持 Ollama/DeepSeek/vLLM 等任意 OpenAI Chat Completions 协议端点。含 DeepSeek thinking mode 支持。
|
||||
|
||||
- **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射
|
||||
- 关键环境变量:`CLAUDE_CODE_USE_OPENAI`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`
|
||||
|
||||
#### Gemini 兼容层
|
||||
|
||||
通过 `CLAUDE_CODE_USE_GEMINI=1` 启用。独立环境变量体系。
|
||||
|
||||
- **`src/services/api/gemini/`** — client、模型映射、类型定义
|
||||
- 关键环境变量:`GEMINI_API_KEY`(必填)、`GEMINI_MODEL`(直接指定)、`GEMINI_DEFAULT_SONNET_MODEL`/`GEMINI_DEFAULT_OPUS_MODEL`(按能力映射)
|
||||
- 模型映射优先级:`GEMINI_MODEL` > `GEMINI_DEFAULT_*_MODEL` > `ANTHROPIC_DEFAULT_*_MODEL`(已废弃) > 原样返回
|
||||
|
||||
#### Grok 兼容层
|
||||
|
||||
通过 `CLAUDE_CODE_USE_GROK=1` 启用。自定义模型映射支持 xAI Grok API。
|
||||
|
||||
- **`src/services/api/grok/`** — client、模型映射
|
||||
|
||||
详见各兼容层的 docs 文档。
|
||||
|
||||
### 穷鬼模式(Budget Mode)
|
||||
|
||||
- 通过 `/poor` 命令切换,持久化到 `settings.json`。
|
||||
- 启用后跳过 `extract_memories`、`prompt_suggestion` 和 `verification_agent`,显著减少 token 消耗。
|
||||
- 实现在 `src/commands/poor/poorMode.ts`。
|
||||
|
||||
### Stubbed/Deleted Modules
|
||||
|
||||
| Module | Status |
|
||||
|--------|--------|
|
||||
| Computer Use (`@ant/*`) | Restored — macOS + Windows + Linux(后端完整度不一) |
|
||||
| `*-napi` packages | 全部已恢复/实现:`audio-capture-napi`、`image-processor-napi` 已恢复;`color-diff-napi` 完整;`modifiers-napi`(macOS FFI);`url-handler-napi`(环境变量+CLI) |
|
||||
| Voice Mode | Restored — Push-to-Talk 语音输入(需 Anthropic OAuth) |
|
||||
| OpenAI/Gemini/Grok 兼容层 | Restored |
|
||||
| Remote Control Server | Restored — 自托管 RCS + Web UI |
|
||||
| Analytics / GrowthBook / Sentry | Empty implementations |
|
||||
| Magic Docs / LSP Server | Restored — Magic Docs 自动更新 + LSP 服务器管理器 |
|
||||
| Plugins / Marketplace | Restored — 插件安装/卸载/启用/禁用 + Marketplace 浏览 |
|
||||
| MCP OAuth | Simplified |
|
||||
|
||||
### Key Type Files
|
||||
|
||||
- **`src/types/global.d.ts`** — Declares `MACRO`, `BUILD_TARGET`, `BUILD_ENV` and internal Anthropic-only identifiers.
|
||||
- **`src/types/internal-modules.d.ts`** — Type declarations for `bun:bundle`, `bun:ffi`, `@anthropic-ai/mcpb`.
|
||||
- **`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")`,英文
|
||||
- **包测试**: `packages/` 下各包也有独立测试(如 `color-diff-napi` 11 tests)
|
||||
|
||||
### Mock 使用规范
|
||||
|
||||
**只 mock 有副作用的依赖链,不 mock 纯函数/纯数据模块。**
|
||||
|
||||
被迫 mock 的根源:`log.ts` / `debug.ts` → `bootstrap/state.ts`(模块级 `realpathSync` / `randomUUID` 副作用)。必须 mock 的模块:`log.ts`、`debug.ts`、`bun:bundle`、`settings/settings.js`、`config.ts`、`auth.ts`、第三方网络库。
|
||||
|
||||
**`log.ts` 和 `debug.ts` 使用共享 mock**(`tests/mocks/log.ts` / `tests/mocks/debug.ts`),不要在测试文件中内联 mock 定义。使用方式:
|
||||
|
||||
```ts
|
||||
import { logMock } from "../../../tests/mocks/log";
|
||||
mock.module("src/utils/log.ts", logMock);
|
||||
|
||||
import { debugMock } from "../../../../tests/mocks/debug";
|
||||
mock.module("src/utils/debug.ts", debugMock);
|
||||
```
|
||||
|
||||
源文件导出变更时只需更新 `tests/mocks/` 下的对应文件,不需要逐个修改测试。
|
||||
|
||||
不要 mock:纯函数模块(`errors.ts`、`stringUtils.js`)、mock 值与真实实现相同的模块、mock 路径与实际 import 不匹配的模块。
|
||||
|
||||
路径规则:统一用 `.ts` 扩展名 + `src/*` 别名路径,禁止双重 mock 同一模块。
|
||||
|
||||
### 类型检查
|
||||
|
||||
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:
|
||||
|
||||
```bash
|
||||
bun run typecheck
|
||||
```
|
||||
|
||||
**类型规范**:
|
||||
- 生产代码禁止 `as any`;测试文件中 mock 数据可用 `as any`
|
||||
- 类型不匹配优先用 `as unknown as SpecificType` 双重断言,或补充 interface
|
||||
- 未知结构对象用 `Record<string, unknown>` 替代 `any`
|
||||
- 联合类型用类型守卫(type guard)收窄,不要强转
|
||||
- `msg.request` 属性访问:`const req = msg.request as Record<string, unknown>`
|
||||
- Ink `color` prop:用 `as keyof Theme` 而非 `as any`
|
||||
|
||||
## Working with This Codebase
|
||||
|
||||
- **tsc must pass** — `bun run typecheck` 必须零错误,任何修改都不能引入新的类型错误。
|
||||
- **Feature flags** — 默认全部关闭(`feature()` 返回 `false`)。Dev/build 各有自己的默认启用列表。不要在 `cli.tsx` 中重定义 `feature` 函数。
|
||||
- **React Compiler output** — Components have decompiled memoization boilerplate (`const $ = _c(N)`). This is normal.
|
||||
- **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。**`feature()` 只能直接用在 `if` 语句或三元表达式的条件位置**(Bun 编译器限制),不能赋值给变量、不能放在箭头函数体里、不能作为 `&&` 链的一部分。正确:`if (feature('X')) {}` 或 `feature('X') ? a : b`。
|
||||
- **`src/` path alias** — tsconfig maps `src/*` to `./src/*`. Imports like `import { ... } from 'src/utils/...'` are valid.
|
||||
- **MACRO defines** — 集中管理在 `scripts/defines.ts`。Dev mode 通过 `bun -d` 注入,build 通过 `Bun.build({ define })` 注入。修改版本号等常量只改这个文件。
|
||||
- **构建产物兼容 Node.js** — `build.ts` 会自动后处理 `import.meta.require`,产物可直接用 `node dist/cli.js` 运行。
|
||||
- **Biome 配置** — 大量 lint 规则被关闭(decompiled 代码不适合严格 lint)。`.tsx` 文件用 120 行宽 + 强制分号;其他文件 80 行宽 + 按需分号。
|
||||
- **Ink 框架在 `packages/@ant/ink/`** — 不是 `src/ink/`(该目录不存在)。Ink 相关的组件、hooks、keybindings 都在 packages 中。
|
||||
- **Provider 优先级** — `modelType` 参数 > 环境变量 > 默认 `firstParty`。新增 provider 需在 `src/utils/model/providers.ts` 注册。
|
||||
|
||||
## Design Context
|
||||
|
||||
Impeccable 设计上下文保存在 `.impeccable.md` 中。设计 Web UI(RCS 控制面板、文档站、着陆页)时必须参考该文件。
|
||||
|
||||
### 核心设计原则
|
||||
|
||||
1. **Considered over clever** — 每个设计选择都应感觉有意为之,而非追逐潮流
|
||||
2. **Warmth through subtlety** — 通过橙色色调的中性色、留白布局、有温度的文案来传达温暖
|
||||
3. **Density with clarity** — 技术用户需要信息密度,但不能混乱
|
||||
4. **Community voice** — 设计应感觉是由使用者创造的,而非遥远的设计团队
|
||||
5. **Anthropic's shadow** — 遵循 Anthropic 的设计直觉:干净的布局、充足的间距、温暖的色温
|
||||
|
||||
### 品牌色
|
||||
|
||||
- 主色:Claude Orange `#D77757`(terra cotta)
|
||||
- 辅色:Claude Blue `#5769F7`
|
||||
- 暗色模式使用温暖的深色表面(非冷蓝黑色)
|
||||
|
||||
### 目标用户
|
||||
|
||||
技术团队/企业,在专业工作流中使用 AI 辅助编程。友好的开源社区氛围,非企业 SaaS 风格。
|
||||
|
||||
### 视觉参考
|
||||
|
||||
Anthropic 公司的设计风格 — 干净、考究、温暖的底色。大量留白,以排版为核心。避免 AI 产品常见的设计套路(渐变文字、玻璃态、霓虹色)。
|
||||
113
CLAUDE.md
113
CLAUDE.md
@@ -1,6 +1,6 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
This file provides guidance to Claude Code (claude.ai/code) and other AI coding agents when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
@@ -39,10 +39,13 @@ echo "say hello" | bun run src/entrypoints/cli.tsx -p
|
||||
# Build (code splitting, outputs dist/cli.js + chunk files)
|
||||
bun run build
|
||||
|
||||
# Build with Vite (alternative build pipeline)
|
||||
bun run build:vite
|
||||
|
||||
# Test
|
||||
bun test # run all tests (2453 tests / 137 files / 0 fail)
|
||||
bun test # run all tests
|
||||
bun test src/utils/__tests__/hash.test.ts # run single file
|
||||
bun test --coverage # with coverage report
|
||||
bun test --coverage # with coverage report
|
||||
|
||||
# Lint & Format (Biome)
|
||||
bun run lint # check only
|
||||
@@ -55,6 +58,8 @@ bun run health
|
||||
# Check unused exports
|
||||
bun run check:unused
|
||||
|
||||
# Full check (typecheck + lint + test) — run after completing any task
|
||||
bun run test:all
|
||||
bun run typecheck
|
||||
|
||||
# Remote Control Server
|
||||
@@ -71,17 +76,19 @@ bun run docs:dev
|
||||
### Runtime & Build
|
||||
|
||||
- **Runtime**: Bun (not Node.js). All imports, builds, and execution use Bun APIs.
|
||||
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。Build 默认启用 19 个 feature(见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。
|
||||
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。Build 默认启用 19 个 feature(见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。构建时会将 `vendor/audio-capture/` 和 `src/utils/vendor/ripgrep/` 复制到 `dist/vendor/` 下。
|
||||
- **Build (Vite)**: `vite.config.ts` + `scripts/post-build.ts`,chunk 输出到 `dist/chunks/`。post-build 同样复制 vendor 文件到 `dist/vendor/`。
|
||||
- **Vendor 路径解析**: 构建后 chunk 文件位于 `dist/` 或 `dist/chunks/` 下,vendor 二进制在 `dist/vendor/`。`src/utils/ripgrep.ts` 和 `packages/audio-capture-napi/src/index.ts` 均通过 `import.meta.url` 路径中 `lastIndexOf('dist')` 定位 dist 根目录,再拼接 `vendor/` 子路径,确保不同构建产物层级下路径一致。
|
||||
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines,运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。
|
||||
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
|
||||
- **Monorepo**: Bun workspaces — 14 个 internal packages in `packages/` resolved via `workspace:*`。
|
||||
- **Monorepo**: Bun workspaces — 15 个 workspace packages + 若干辅助目录 in `packages/` resolved via `workspace:*`。
|
||||
- **Lint/Format**: Biome (`biome.json`)。`bun run lint` / `bun run lint:fix` / `bun run format`。
|
||||
- **Defines**: 集中管理在 `scripts/defines.ts`。当前版本 `2.1.888`。
|
||||
- **CI**: GitHub Actions — `ci.yml`(构建+测试)、`release-rcs.yml`(RCS 发布)、`update-contributors.yml`(自动更新贡献者)。
|
||||
|
||||
### Entry & Bootstrap
|
||||
|
||||
1. **`src/entrypoints/cli.tsx`** (323 行) — True entrypoint。`main()` 函数按优先级处理多条快速路径:
|
||||
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`
|
||||
@@ -94,7 +101,7 @@ bun run docs:dev
|
||||
- `environment-runner` / `self-hosted-runner` — BYOC runner
|
||||
- `--tmux` + `--worktree` 组合
|
||||
- 默认路径:加载 `main.tsx` 启动完整 CLI
|
||||
2. **`src/main.tsx`** (~6970 行) — Commander.js CLI definition。注册大量 subcommands:`mcp` (serve/add/remove/list...)、`server`、`ssh`、`open`、`auth`、`plugin`、`agents`、`auto-mode`、`doctor`、`update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。
|
||||
2. **`src/main.tsx`** (~6981 行) — Commander.js CLI definition。注册大量 subcommands:`mcp` (serve/add/remove/list...)、`server`、`ssh`、`open`、`auth`、`plugin`、`agents`、`auto-mode`、`doctor`、`update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。
|
||||
3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog)。
|
||||
|
||||
### Core Loop
|
||||
@@ -112,8 +119,8 @@ bun run docs:dev
|
||||
### Tool System
|
||||
|
||||
- **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`).
|
||||
- **`src/tools.ts`** (387 行) — Tool registry. Assembles the tool list; some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
|
||||
- **`src/tools/<ToolName>/`** — 55 个 tool 目录。主要分类:
|
||||
- **`src/tools.ts`** — Tool registry. Assembles the tool list; tools are imported from `@claude-code-best/builtin-tools` package. Some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
|
||||
- **`packages/builtin-tools/src/tools/`** — 59 个子目录(含 shared/testing 等工具目录),通过 `@claude-code-best/builtin-tools` 包导出。主要分类:
|
||||
- **文件操作**: FileEditTool, FileReadTool, FileWriteTool, GlobTool, GrepTool
|
||||
- **Shell/执行**: BashTool, PowerShellTool, REPLTool
|
||||
- **Agent 系统**: AgentTool, TaskCreateTool, TaskUpdateTool, TaskListTool, TaskGetTool
|
||||
@@ -121,7 +128,7 @@ bun run docs:dev
|
||||
- **Web/MCP**: WebFetchTool, WebSearchTool, MCPTool, McpAuthTool
|
||||
- **调度**: CronCreateTool, CronDeleteTool, CronListTool
|
||||
- **其他**: LSPTool, ConfigTool, SkillTool, EnterWorktreeTool, ExitWorktreeTool 等
|
||||
- **`src/tools/shared/`** — Tool 共享工具函数。
|
||||
- **`src/tools/shared/`** / **`packages/builtin-tools/src/tools/shared/`** — Tool 共享工具函数。
|
||||
|
||||
### UI Layer (Ink)
|
||||
|
||||
@@ -152,22 +159,37 @@ bun run docs:dev
|
||||
| `packages/@ant/computer-use-input/` | 键鼠模拟(dispatcher + darwin/win32/linux backend) |
|
||||
| `packages/@ant/computer-use-swift/` | 截图 + 应用管理(dispatcher + per-platform backend) |
|
||||
| `packages/@ant/claude-for-chrome-mcp/` | Chrome 浏览器控制(通过 `--chrome` 启用) |
|
||||
| `packages/remote-control-server/` | 自托管 Remote Control Server(Docker 部署,含 Web UI) |
|
||||
| `packages/swarm/` | Swarm 解耦模块 |
|
||||
| `packages/shell/` | Shell 抽象 |
|
||||
| `packages/@ant/model-provider/` | Model provider 抽象层 |
|
||||
| `packages/builtin-tools/` | 内置工具集(60 个 tool 实现,通过 `@claude-code-best/builtin-tools` 导出) |
|
||||
| `packages/agent-tools/` | Agent 工具集 |
|
||||
| `packages/acp-link/` | ACP 代理服务器(WebSocket → ACP agent 桥接) |
|
||||
| `packages/cc-knowledge/` | Claude Code 知识库(非 workspace 包) |
|
||||
| `packages/langfuse-dashboard/` | Langfuse 可观测性面板(非 workspace 包) |
|
||||
| `packages/mcp-client/` | MCP 客户端库 |
|
||||
| `packages/mcp-server/` | MCP 服务端库(非 workspace 包) |
|
||||
| `packages/remote-control-server/` | 自托管 Remote Control Server(Docker 部署,含 Web UI)— Web UI 已重构为 React + Vite + Radix UI,支持 ACP agent 接入 |
|
||||
| `packages/swarm/` | Swarm 解耦模块(非 workspace 包) |
|
||||
| `packages/shell/` | Shell 抽象(非 workspace 包) |
|
||||
| `packages/audio-capture-napi/` | 原生音频捕获(已恢复) |
|
||||
| `packages/color-diff-napi/` | 颜色差异计算(完整实现,11 tests) |
|
||||
| `packages/image-processor-napi/` | 图像处理(已恢复) |
|
||||
| `packages/modifiers-napi/` | 键盘修饰键检测(stub) |
|
||||
| `packages/url-handler-napi/` | URL scheme 处理(stub) |
|
||||
| `packages/modifiers-napi/` | 键盘修饰键检测(macOS FFI 实现) |
|
||||
| `packages/url-handler-napi/` | URL scheme 处理(环境变量 + CLI 参数读取) |
|
||||
|
||||
### Bridge / Remote Control
|
||||
|
||||
- **`src/bridge/`** (~37 files) — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`。
|
||||
- **`packages/remote-control-server/`** — 自托管 RCS,支持 Docker 部署,含 Web UI 控制面板。通过 `bun run rcs` 启动。
|
||||
- **`src/bridge/`** — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`。
|
||||
- **`packages/remote-control-server/`** — 自托管 RCS,支持 Docker 部署,含 Web UI 控制面板(React 19 + Vite + Radix UI)。支持 ACP agent 通过 acp-link 接入(ACP WebSocket handler、relay handler、SSE event stream)。通过 `bun run rcs` 启动。
|
||||
- CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`。
|
||||
- 详见 `docs/features/remote-control-self-hosting.md`。
|
||||
|
||||
### ACP Protocol (Agent Client Protocol)
|
||||
|
||||
- **`src/services/acp/`** — ACP agent 实现,包含 `agent.ts`(AcpAgent 类)、`bridge.ts`(Claude Code ↔ ACP 桥接)、`permissions.ts`(权限处理)、`entry.ts`(入口)。
|
||||
- **`packages/acp-link/`** — ACP 代理服务器,将 WebSocket 客户端桥接到 ACP agent。提供 `acp-link` CLI 命令,支持自定义端口/HTTPS/认证/会话管理、RCS 集成(REST 注册 + WS identify 两步流程)、权限模式透传(fallback: 客户端传值 > config > `ACP_PERMISSION_MODE` 环境变量)。
|
||||
- ACP 权限管道改进:`createAcpCanUseTool` 统一权限流水线,`applySessionMode` 模式同步,`bypassPermissions` 可用性检测(非 root/sandbox 环境)。
|
||||
- ACP Plan 可视化已支持 `session/update plan` 类型的消息展示(PlanView 组件,含进度条/状态图标/优先级标签)。
|
||||
|
||||
### Daemon Mode
|
||||
|
||||
- **`src/daemon/`** — Daemon 模式(长驻 supervisor)。feature-gated by `DAEMON`。包含 `main.ts`(entry)和 `workerRegistry.ts`(worker 管理)。
|
||||
@@ -198,7 +220,7 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
|
||||
|
||||
### Multi-API 兼容层
|
||||
|
||||
所有兼容层均采用流适配器模式:将第三方 API 格式转为 Anthropic 内部格式,下游代码完全不改。
|
||||
所有兼容层均采用流适配器模式:将第三方 API 格式转为 Anthropic 内部格式,下游代码完全不改。通过 `/login` 命令配置。
|
||||
|
||||
#### OpenAI 兼容层
|
||||
|
||||
@@ -223,18 +245,24 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
|
||||
|
||||
详见各兼容层的 docs 文档。
|
||||
|
||||
### 穷鬼模式(Budget Mode)
|
||||
|
||||
- 通过 `/poor` 命令切换,持久化到 `settings.json`。
|
||||
- 启用后跳过 `extract_memories`、`prompt_suggestion` 和 `verification_agent`,显著减少 token 消耗。
|
||||
- 实现在 `src/commands/poor/poorMode.ts`。
|
||||
|
||||
### Stubbed/Deleted Modules
|
||||
|
||||
| Module | Status |
|
||||
|--------|--------|
|
||||
| Computer Use (`@ant/*`) | Restored — macOS + Windows + Linux(后端完整度不一) |
|
||||
| `*-napi` packages | `audio-capture-napi`、`image-processor-napi` 已恢复;`color-diff-napi` 完整;`modifiers-napi`、`url-handler-napi` 仍为 stub |
|
||||
| `*-napi` packages | 全部已恢复/实现:`audio-capture-napi`、`image-processor-napi` 已恢复;`color-diff-napi` 完整;`modifiers-napi`(macOS FFI);`url-handler-napi`(环境变量+CLI) |
|
||||
| Voice Mode | Restored — Push-to-Talk 语音输入(需 Anthropic OAuth) |
|
||||
| OpenAI/Gemini/Grok 兼容层 | Restored |
|
||||
| Remote Control Server | Restored — 自托管 RCS + Web UI |
|
||||
| Analytics / GrowthBook / Sentry | Empty implementations |
|
||||
| Magic Docs / LSP Server | Removed |
|
||||
| Plugins / Marketplace | Removed |
|
||||
| Magic Docs / LSP Server | Restored — Magic Docs 自动更新 + LSP 服务器管理器 |
|
||||
| Plugins / Marketplace | Restored — 插件安装/卸载/启用/禁用 + Marketplace 浏览 |
|
||||
| MCP OAuth | Simplified |
|
||||
|
||||
### Key Type Files
|
||||
@@ -247,7 +275,6 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
|
||||
## Testing
|
||||
|
||||
- **框架**: `bun:test`(内置断言 + mock)
|
||||
- **当前状态**: 2992 tests / 188 files / 0 fail
|
||||
- **单元测试**: 就近放置于 `src/**/__tests__/`,文件名 `<module>.test.ts`
|
||||
- **集成测试**: `tests/integration/` — 4 个文件(cli-arguments, context-build, message-pipeline, tool-chain)
|
||||
- **共享 mock/fixture**: `tests/mocks/`(api-responses, file-system, fixtures/)
|
||||
@@ -260,6 +287,18 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
|
||||
|
||||
被迫 mock 的根源:`log.ts` / `debug.ts` → `bootstrap/state.ts`(模块级 `realpathSync` / `randomUUID` 副作用)。必须 mock 的模块:`log.ts`、`debug.ts`、`bun:bundle`、`settings/settings.js`、`config.ts`、`auth.ts`、第三方网络库。
|
||||
|
||||
**`log.ts` 和 `debug.ts` 使用共享 mock**(`tests/mocks/log.ts` / `tests/mocks/debug.ts`),不要在测试文件中内联 mock 定义。使用方式:
|
||||
|
||||
```ts
|
||||
import { logMock } from "../../../tests/mocks/log";
|
||||
mock.module("src/utils/log.ts", logMock);
|
||||
|
||||
import { debugMock } from "../../../../tests/mocks/debug";
|
||||
mock.module("src/utils/debug.ts", debugMock);
|
||||
```
|
||||
|
||||
源文件导出变更时只需更新 `tests/mocks/` 下的对应文件,不需要逐个修改测试。
|
||||
|
||||
不要 mock:纯函数模块(`errors.ts`、`stringUtils.js`)、mock 值与真实实现相同的模块、mock 路径与实际 import 不匹配的模块。
|
||||
|
||||
路径规则:统一用 `.ts` 扩展名 + `src/*` 别名路径,禁止双重 mock 同一模块。
|
||||
@@ -269,7 +308,7 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
|
||||
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:
|
||||
|
||||
```bash
|
||||
bunx tsc --noEmit
|
||||
bun run typecheck
|
||||
```
|
||||
|
||||
**类型规范**:
|
||||
@@ -282,7 +321,7 @@ bunx tsc --noEmit
|
||||
|
||||
## Working with This Codebase
|
||||
|
||||
- **tsc must pass** — `bunx tsc --noEmit` 必须零错误,任何修改都不能引入新的类型错误。
|
||||
- **tsc must pass** — `bun run typecheck` 必须零错误,任何修改都不能引入新的类型错误。
|
||||
- **Feature flags** — 默认全部关闭(`feature()` 返回 `false`)。Dev/build 各有自己的默认启用列表。不要在 `cli.tsx` 中重定义 `feature` 函数。
|
||||
- **React Compiler output** — Components have decompiled memoization boilerplate (`const $ = _c(N)`). This is normal.
|
||||
- **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。**`feature()` 只能直接用在 `if` 语句或三元表达式的条件位置**(Bun 编译器限制),不能赋值给变量、不能放在箭头函数体里、不能作为 `&&` 链的一部分。正确:`if (feature('X')) {}` 或 `feature('X') ? a : b`。
|
||||
@@ -292,3 +331,29 @@ bunx tsc --noEmit
|
||||
- **Biome 配置** — 大量 lint 规则被关闭(decompiled 代码不适合严格 lint)。`.tsx` 文件用 120 行宽 + 强制分号;其他文件 80 行宽 + 按需分号。
|
||||
- **Ink 框架在 `packages/@ant/ink/`** — 不是 `src/ink/`(该目录不存在)。Ink 相关的组件、hooks、keybindings 都在 packages 中。
|
||||
- **Provider 优先级** — `modelType` 参数 > 环境变量 > 默认 `firstParty`。新增 provider 需在 `src/utils/model/providers.ts` 注册。
|
||||
|
||||
## Design Context
|
||||
|
||||
Impeccable 设计上下文保存在 `.impeccable.md` 中。设计 Web UI(RCS 控制面板、文档站、着陆页)时必须参考该文件。
|
||||
|
||||
### 核心设计原则
|
||||
|
||||
1. **Considered over clever** — 每个设计选择都应感觉有意为之,而非追逐潮流
|
||||
2. **Warmth through subtlety** — 通过橙色色调的中性色、留白布局、有温度的文案来传达温暖
|
||||
3. **Density with clarity** — 技术用户需要信息密度,但不能混乱
|
||||
4. **Community voice** — 设计应感觉是由使用者创造的,而非遥远的设计团队
|
||||
5. **Anthropic's shadow** — 遵循 Anthropic 的设计直觉:干净的布局、充足的间距、温暖的色温
|
||||
|
||||
### 品牌色
|
||||
|
||||
- 主色:Claude Orange `#D77757`(terra cotta)
|
||||
- 辅色:Claude Blue `#5769F7`
|
||||
- 暗色模式使用温暖的深色表面(非冷蓝黑色)
|
||||
|
||||
### 目标用户
|
||||
|
||||
技术团队/企业,在专业工作流中使用 AI 辅助编程。友好的开源社区氛围,非企业 SaaS 风格。
|
||||
|
||||
### 视觉参考
|
||||
|
||||
Anthropic 公司的设计风格 — 干净、考究、温暖的底色。大量留白,以排版为核心。避免 AI 产品常见的设计套路(渐变文字、玻璃态、霓虹色)。
|
||||
|
||||
130
README.md
130
README.md
@@ -6,44 +6,52 @@
|
||||
[](https://github.com/claude-code-best/claude-code/blob/main/LICENSE)
|
||||
[](https://github.com/claude-code-best/claude-code/commits/main)
|
||||
[](https://bun.sh/)
|
||||
[](https://discord.gg/qZU6zS7Q)
|
||||
[](https://discord.gg/uApuzJWGKX)
|
||||
|
||||
> Which Claude do you like? The open source one is the best.
|
||||
|
||||
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)... 而且, 我们实现了企业版或者需要登陆 Claude 账号才能使用的特性, 实现技术普惠
|
||||
|
||||
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/qZU6zS7Q)
|
||||
> 我们将会在五一期间进行整个代码仓库的 lint 规范化, 这个期间提交的 PR 可能会有非常多的冲突, 所以大的功能请尽量在这之前提交哈
|
||||
|
||||
| 特性 | 说明 | 文档 |
|
||||
|------|------|------|
|
||||
| **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/pipes-and-lan) / [LAN](https://ccb.agent-aura.top/docs/features/lan-pipes) |
|
||||
| **ACP 协议一等一支持** | 支持接入 Zed、Cursor 等 IDE,支持会话恢复、Skills、权限桥接 | [文档](https://ccb.agent-aura.top/docs/features/acp-zed) |
|
||||
| **Remote Control 私有部署** | Docker 自托管远程界面, 可以手机上看 CC | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) |
|
||||
| **Langfuse 监控** | 企业级 Agent 监控, 可以清晰看到每次 agent loop 细节, 可以一键转化为数据集 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
|
||||
| **Web Search** | 内置网页搜索工具, 支持 bing 和 brave 搜索 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
|
||||
| **Poor Mode** | 穷鬼模式,关闭记忆提取和键入建议,大幅度减少并发请求 | /poor 可以开关 |
|
||||
| **自定义模型供应商** | OpenAI/Anthropic/Gemini/Grok 兼容 | [文档](https://ccb.agent-aura.top/docs/features/custom-platform-login) |
|
||||
| Voice Mode | Push-to-Talk 语音输入 | [文档](https://ccb.agent-aura.top/docs/features/voice-mode) |
|
||||
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
|
||||
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](https://ccb.agent-aura.top/docs/features/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) |
|
||||
| Sentry | 企业级错误追踪 | [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup) |
|
||||
| GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
|
||||
| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
|
||||
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/uApuzJWGKX)
|
||||
|
||||
|
||||
| 特性 | 说明 | 文档 |
|
||||
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Claude 群控技术** | Pipe IPC 多实例协作:同机 main/sub 自动编排 + LAN 跨机器零配置发现与通讯,`/pipes` 选择面板 + `Shift+↓` 交互 + 消息广播路由 | [Pipe IPC](https://ccb.agent-aura.top/docs/features/uds-inbox) / [LAN](https://ccb.agent-aura.top/docs/features/lan-pipes) |
|
||||
| **ACP 协议一等一支持** | 支持接入 Zed、Cursor 等 IDE,支持会话恢复、Skills、权限桥接 | [文档](https://ccb.agent-aura.top/docs/features/acp-zed) |
|
||||
| **Remote Control 私有部署** | Docker 自托管远程界面, 可以手机上看 CC | [文档](https://ccb.agent-aura.top/docs/features/remote-control-self-hosting) |
|
||||
| **Langfuse 监控** | 企业级 Agent 监控, 可以清晰看到每次 agent loop 细节, 可以一键转化为数据集 | [文档](https://ccb.agent-aura.top/docs/features/langfuse-monitoring) |
|
||||
| **Web Search** | 内置网页搜索工具, 支持 bing 和 brave 搜索 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
|
||||
| **Poor Mode** | 穷鬼模式,关闭记忆提取和键入建议,大幅度减少并发请求 | /poor 可以开关 |
|
||||
| **Channels 频道通知** | MCP 服务器推送外部消息到会话(飞书/Slack/Discord/微信等),`--channels plugin:name@marketplace` 启用 | [文档](https://ccb.agent-aura.top/docs/features/channels) |
|
||||
| **自定义模型供应商** | OpenAI/Anthropic/Gemini/Grok 兼容 (`/login`) | [文档](https://ccb.agent-aura.top/docs/features/all-features-guide) |
|
||||
| Voice Mode | 语音输入,支持豆包语言输入(`/voice doubao`) | [文档](https://ccb.agent-aura.top/docs/features/voice-mode) |
|
||||
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
|
||||
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](https://ccb.agent-aura.top/docs/features/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) |
|
||||
| Sentry | 企业级错误追踪 | [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup) |
|
||||
| GrowthBook | 企业级特性开关 | [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) |
|
||||
| /dream 记忆整理 | 自动整理和优化记忆文件 | [文档](https://ccb.agent-aura.top/docs/features/auto-dream) |
|
||||
|
||||
- 🚀 [想要启动项目](#快速开始源码版)
|
||||
- 🐛 [想要调试项目](#vs-code-调试)
|
||||
- 📖 [想要学习项目](#teach-me-学习项目)
|
||||
|
||||
|
||||
## ⚡ 快速开始(安装版)
|
||||
|
||||
不用克隆仓库, 从 NPM 下载后, 直接使用
|
||||
|
||||
```sh
|
||||
bun i -g claude-code-best
|
||||
bun pm -g trust claude-code-best
|
||||
npm i -g claude-code-best
|
||||
|
||||
# bun 安装比较多问题, 推荐 npm 装
|
||||
# bun i -g claude-code-best
|
||||
# bun pm -g trust claude-code-best @claude-code-best/mcp-chrome-bridge
|
||||
|
||||
ccb # 以 nodejs 打开 claude code
|
||||
ccb-bun # 以 bun 形态打开
|
||||
ccb update # 更新到最新版本
|
||||
CLAUDE_BRIDGE_BASE_URL=https://remote-control.claude-code-best.win/ CLAUDE_BRIDGE_OAUTH_TOKEN=test-my-key ccb --remote-control # 我们有自部署的远程控制
|
||||
```
|
||||
|
||||
@@ -54,11 +62,66 @@ CLAUDE_BRIDGE_BASE_URL=https://remote-control.claude-code-best.win/ CLAUDE_BRIDG
|
||||
一定要最新版本的 bun 啊, 不然一堆奇奇怪怪的 BUG!!! bun upgrade!!!
|
||||
|
||||
- 📦 [Bun](https://bun.sh/) >= 1.3.11
|
||||
|
||||
**安装 Bun:**
|
||||
|
||||
```bash
|
||||
# Linux 和 macOS
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# Windows (PowerShell)
|
||||
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||
```
|
||||
|
||||
**安装后的操作:**
|
||||
|
||||
1. **让当前终端识别 `bun` 命令**
|
||||
|
||||
安装脚本会把 `~/.bun/bin` 写入对应的 shell 配置文件。macOS 默认 zsh 环境通常会看到:
|
||||
|
||||
```text
|
||||
Added "~/.bun/bin" to $PATH in "~/.zshrc"
|
||||
```
|
||||
|
||||
可以按安装脚本提示重启当前 shell:
|
||||
|
||||
```bash
|
||||
exec /bin/zsh
|
||||
```
|
||||
|
||||
如果你使用 bash,重新加载 bash 配置:
|
||||
|
||||
```bash
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
Windows PowerShell 用户关闭并重新打开 PowerShell 即可。
|
||||
|
||||
2. **验证 Bun 是否可用**
|
||||
|
||||
```bash
|
||||
bun --help
|
||||
bun --version
|
||||
```
|
||||
|
||||
3. **如果已经安装过 Bun,更新到最新版本**
|
||||
|
||||
```bash
|
||||
bun upgrade
|
||||
```
|
||||
|
||||
- ⚙️ 常规的配置 CC 的方式, 各大提供商都有自己的配置方式
|
||||
|
||||
### 📍 命令执行位置
|
||||
|
||||
- 安装或检查 Bun 的命令可以在任意目录执行:
|
||||
`curl -fsSL https://bun.sh/install | bash`、`bun --help`、`bun --version`、`bun upgrade`
|
||||
- 安装本项目依赖、启动开发模式、构建项目时,必须先进入本仓库根目录,也就是包含 `package.json` 的目录。
|
||||
|
||||
### 📥 安装
|
||||
|
||||
```bash
|
||||
cd /path/to/claude-code
|
||||
bun install
|
||||
```
|
||||
|
||||
@@ -85,17 +148,17 @@ bun run build
|
||||
|
||||
需要填写的字段:
|
||||
|
||||
| 📌 字段 | 📝 说明 | 💡 示例 |
|
||||
|------|------|------|
|
||||
| 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` |
|
||||
|
||||
| 📌 字段 | 📝 说明 | 💡 示例 |
|
||||
| ------------ | ------------- | ---------------------------- |
|
||||
| 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 保存
|
||||
|
||||
|
||||
> ℹ️ 支持所有 Anthropic API 兼容服务(如 OpenRouter、AWS Bedrock 代理等),只要接口兼容 Messages API 即可。
|
||||
|
||||
## Feature Flags
|
||||
@@ -115,16 +178,17 @@ TUI (REPL) 模式需要真实终端,无法直接通过 VS Code launch 启动
|
||||
### 步骤
|
||||
|
||||
1. **终端启动 inspect 服务**:
|
||||
|
||||
```bash
|
||||
bun run dev:inspect
|
||||
```
|
||||
会输出类似 `ws://localhost:8888/xxxxxxxx` 的地址。
|
||||
|
||||
会输出类似 `ws://localhost:8888/xxxxxxxx` 的地址。
|
||||
2. **VS Code 附着调试器**:
|
||||
|
||||
- 在 `src/` 文件中打断点
|
||||
- F5 → 选择 **"Attach to Bun (TUI debug)"**
|
||||
|
||||
|
||||
## Teach Me 学习项目
|
||||
|
||||
我们新加了一个 teach-me skills, 通过问答式引导帮你理解这个项目的任何模块。(调整 [sigma skill 而来](https://github.com/sanyuan0704/sanyuan-skills))
|
||||
@@ -151,7 +215,7 @@ TUI (REPL) 模式需要真实终端,无法直接通过 VS Code launch 启动
|
||||
## 相关文档及网站
|
||||
|
||||
- **在线文档(Mintlify)**: [ccb.agent-aura.top](https://ccb.agent-aura.top/) — 文档源码位于 [`docs/`](docs/) 目录,欢迎投稿 PR
|
||||
- **DeepWiki**: <https://deepwiki.com/claude-code-best/claude-code>
|
||||
- **DeepWiki**: [https://deepwiki.com/claude-code-best/claude-code](https://deepwiki.com/claude-code-best/claude-code)
|
||||
|
||||
## Contributors
|
||||
|
||||
@@ -169,6 +233,10 @@ TUI (REPL) 模式需要真实终端,无法直接通过 VS Code launch 启动
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 致谢
|
||||
|
||||
- [doubaoime-asr](https://github.com/starccy/doubaoime-asr) — 豆包 ASR 语音识别 SDK,为 Voice Mode 提供无需 Anthropic OAuth 的语音输入方案
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目仅供学习研究用途。Claude Code 的所有权利归 [Anthropic](https://www.anthropic.com/) 所有。
|
||||
|
||||
55
README_EN.md
55
README_EN.md
@@ -48,11 +48,64 @@ Sponsor placeholder.
|
||||
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
|
||||
|
||||
**Install Bun:**
|
||||
|
||||
```bash
|
||||
# Linux and macOS
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# Windows (PowerShell)
|
||||
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||
```
|
||||
|
||||
**Post-installation steps:**
|
||||
|
||||
1. **Make `bun` available in the current terminal**
|
||||
|
||||
The installer adds `~/.bun/bin` to the matching shell configuration file. On macOS with the default zsh shell, you may see:
|
||||
|
||||
```text
|
||||
Added "~/.bun/bin" to $PATH in "~/.zshrc"
|
||||
```
|
||||
|
||||
Restart the current shell as the installer suggests:
|
||||
|
||||
```bash
|
||||
exec /bin/zsh
|
||||
```
|
||||
|
||||
If you use bash, reload the bash configuration:
|
||||
|
||||
```bash
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
Windows PowerShell users can close and reopen PowerShell.
|
||||
|
||||
2. **Verify that Bun is available:**
|
||||
```bash
|
||||
bun --help
|
||||
bun --version
|
||||
```
|
||||
|
||||
3. **Update to latest version (if already installed):**
|
||||
```bash
|
||||
bun upgrade
|
||||
```
|
||||
|
||||
- Standard Claude Code configuration — each provider has its own setup method
|
||||
|
||||
### Command Execution Location
|
||||
|
||||
- Bun installation and checking commands can be run from any directory:
|
||||
`curl -fsSL https://bun.sh/install | bash`, `bun --help`, `bun --version`, `bun upgrade`
|
||||
- Project dependency installation, development mode, and builds must be run from this repository root, the directory containing `package.json`.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
cd /path/to/claude-code
|
||||
bun install
|
||||
```
|
||||
|
||||
@@ -135,7 +188,7 @@ The TUI (REPL) mode requires a real terminal and cannot be launched directly via
|
||||
## 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>
|
||||
- **DeepWiki**: https://deepwiki.com/claude-code-best/claude-code
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
70
build.ts
70
build.ts
@@ -1,6 +1,7 @@
|
||||
import { readdir, readFile, writeFile, cp } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { getMacroDefines } from './scripts/defines.ts'
|
||||
import { DEFAULT_BUILD_FEATURES } from './scripts/defines.ts'
|
||||
|
||||
const outdir = 'dist'
|
||||
|
||||
@@ -8,47 +9,6 @@ const outdir = 'dist'
|
||||
const { rmSync } = await import('fs')
|
||||
rmSync(outdir, { recursive: true, force: true })
|
||||
|
||||
// Default features that match the official CLI build.
|
||||
// Additional features can be enabled via FEATURE_<NAME>=1 env vars.
|
||||
const DEFAULT_BUILD_FEATURES = [
|
||||
'AGENT_TRIGGERS_REMOTE',
|
||||
'CHICAGO_MCP',
|
||||
'VOICE_MODE',
|
||||
'SHOT_STATS',
|
||||
'PROMPT_CACHE_BREAK_DETECTION',
|
||||
'TOKEN_BUDGET',
|
||||
// P0: local features
|
||||
'AGENT_TRIGGERS',
|
||||
'ULTRATHINK',
|
||||
'BUILTIN_EXPLORE_PLAN_AGENTS',
|
||||
'LODESTONE',
|
||||
// P1: API-dependent features
|
||||
'EXTRACT_MEMORIES',
|
||||
'VERIFICATION_AGENT',
|
||||
'KAIROS_BRIEF',
|
||||
'AWAY_SUMMARY',
|
||||
'ULTRAPLAN',
|
||||
// P2: daemon + remote control server
|
||||
'DAEMON',
|
||||
// ACP (Agent Client Protocol) agent mode
|
||||
'ACP',
|
||||
// PR-package restored features
|
||||
'WORKFLOW_SCRIPTS',
|
||||
'HISTORY_SNIP',
|
||||
'CONTEXT_COLLAPSE',
|
||||
'MONITOR_TOOL',
|
||||
'FORK_SUBAGENT',
|
||||
// 'UDS_INBOX',
|
||||
'KAIROS',
|
||||
'COORDINATOR_MODE',
|
||||
'LAN_PIPES',
|
||||
'BG_SESSIONS',
|
||||
'TEMPLATES',
|
||||
// 'REVIEW_ARTIFACT', // API 请求无响应,需进一步排查 schema 兼容性
|
||||
// P3: poor mode (disable extract_memories + prompt_suggestion)
|
||||
'POOR',
|
||||
]
|
||||
|
||||
// Collect FEATURE_* env vars → Bun.build features
|
||||
const envFeatures = Object.keys(process.env)
|
||||
.filter(k => k.startsWith('FEATURE_'))
|
||||
@@ -115,28 +75,16 @@ console.log(
|
||||
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for import.meta.require, ${bunPatched} for Bun destructure)`,
|
||||
)
|
||||
|
||||
// Step 4: Copy native .node addon files (audio-capture)
|
||||
const vendorDir = join(outdir, 'vendor', 'audio-capture')
|
||||
await cp('vendor/audio-capture', vendorDir, { recursive: true })
|
||||
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`)
|
||||
// Step 4: Copy native .node addon files (audio-capture) and vendored binaries (ripgrep)
|
||||
const audioCaptureDir = join(outdir, 'vendor', 'audio-capture')
|
||||
await cp('vendor/audio-capture', audioCaptureDir, { recursive: true })
|
||||
console.log(`Copied vendor/audio-capture/ → ${audioCaptureDir}/`)
|
||||
|
||||
// Step 5: Bundle download-ripgrep script as standalone JS for postinstall
|
||||
const rgScript = await Bun.build({
|
||||
entrypoints: ['scripts/download-ripgrep.ts'],
|
||||
outdir,
|
||||
target: 'node',
|
||||
})
|
||||
if (!rgScript.success) {
|
||||
console.error('Failed to bundle download-ripgrep script:')
|
||||
for (const log of rgScript.logs) {
|
||||
console.error(log)
|
||||
}
|
||||
// Non-fatal — postinstall fallback to bun run scripts/download-ripgrep.ts
|
||||
} else {
|
||||
console.log(`Bundled download-ripgrep script to ${outdir}/`)
|
||||
}
|
||||
const ripgrepDir = join(outdir, 'vendor', 'ripgrep')
|
||||
await cp('src/utils/vendor/ripgrep', ripgrepDir, { recursive: true })
|
||||
console.log(`Copied src/utils/vendor/ripgrep/ → ${ripgrepDir}/`)
|
||||
|
||||
// Step 6: Generate cli-bun and cli-node executable entry points
|
||||
// Step 5: Generate cli-bun and cli-node executable entry points
|
||||
const cliBun = join(outdir, 'cli-bun.js')
|
||||
const cliNode = join(outdir, 'cli-node.js')
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
@@ -13,14 +13,14 @@ keywords: ["子 Agent", "AgentTool", "任务委派", "forkSubagent", "子进程
|
||||
```
|
||||
AI 生成 tool_use: { prompt: "修复 bug", subagent_type: "Explore" }
|
||||
↓
|
||||
AgentTool.call() ← 入口(AgentTool.tsx:239)
|
||||
AgentTool.call() ← 入口(AgentTool.tsx:387)
|
||||
├── 解析 effectiveType(fork vs 命名 agent vs GP 回退)
|
||||
├── filterDeniedAgents() ← 仅命名 Agent 路径执行:权限过滤
|
||||
├── 检查 requiredMcpServers ← MCP 依赖验证(最长等 30s)
|
||||
├── assembleToolPool(workerPermissionContext) ← 独立组装工具池
|
||||
├── createAgentWorktree() ← 可选 worktree 隔离
|
||||
↓
|
||||
runAgent() ← 核心执行(runAgent.ts:248)
|
||||
runAgent() ← 核心执行(runAgent.ts)
|
||||
├── getAgentSystemPrompt() ← 构建 agent 专属 system prompt
|
||||
├── initializeAgentMcpServers() ← agent 级 MCP 服务器
|
||||
├── executeSubagentStartHooks() ← Hook 注入
|
||||
@@ -54,7 +54,7 @@ Fork 实验的门控函数 `isForkSubagentEnabled()` 需要同时满足三个前
|
||||
Fork 路径的设计核心是 **Prompt Cache 共享**:所有 fork 子进程共享父 Agent 的完整 `assistant` 消息(所有 `tool_use` 块),用相同的占位符 `tool_result` 填充,只有最后一个 `text` 块包含各自的指令。这使得 API 请求前缀字节完全一致,最大化缓存命中。
|
||||
|
||||
```typescript
|
||||
// forkSubagent.ts:142 — 所有 fork 子进程的占位结果
|
||||
// forkSubagent.ts:93 — 所有 fork 子进程的占位结果
|
||||
const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
|
||||
|
||||
// buildForkedMessages() 构建:
|
||||
@@ -63,7 +63,7 @@ const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
|
||||
|
||||
### Fork 递归防护
|
||||
|
||||
Fork 子进程保留 Agent 工具(为了 cache-identical tool defs),但通过两道防线防止递归 fork(`AgentTool.tsx:332`):
|
||||
Fork 子进程保留 Agent 工具(为了 cache-identical tool defs),但通过两道防线防止递归 fork:
|
||||
|
||||
1. **`querySource` 检查**(压缩安全):`context.options.querySource === 'agent:builtin:fork'`
|
||||
2. **消息扫描**(降级兜底):检测 `<fork-boilerplate>` 标签
|
||||
@@ -88,7 +88,7 @@ Fork 子进程保留 Agent 工具(为了 cache-identical tool defs),但通
|
||||
|
||||
### 内置 Agent
|
||||
|
||||
系统预定义了几个内置 Agent(`src/tools/AgentTool/builtinAgents.ts`),各有明确的职责和模型配置:
|
||||
系统预定义了几个内置 Agent(`packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts`),各有明确的职责和模型配置:
|
||||
|
||||
| Agent | 模型 | 权限 | 用途 |
|
||||
|-------|------|------|------|
|
||||
@@ -119,7 +119,7 @@ const workerTools = assembleToolPool(workerPermissionContext, appState.mcp.tools
|
||||
|
||||
### 工具过滤的 resolveAgentTools
|
||||
|
||||
`runAgent.ts:500-502` 在工具组装后进一步过滤:
|
||||
`runAgent.ts:508` 在工具组装后进一步过滤:
|
||||
|
||||
```typescript
|
||||
const resolvedTools = useExactTools
|
||||
@@ -142,7 +142,7 @@ const resolvedTools = useExactTools
|
||||
|
||||
## Worktree 隔离机制
|
||||
|
||||
`isolation: "worktree"` 参数让子 Agent 在独立的 git worktree 中工作(`AgentTool.tsx:590-593`):
|
||||
`isolation: "worktree"` 参数让子 Agent 在独立的 git worktree 中工作(`AgentTool.tsx:863`):
|
||||
|
||||
```typescript
|
||||
const slug = `agent-${earlyAgentId.slice(0, 8)}`
|
||||
@@ -183,7 +183,7 @@ runAsyncAgentLifecycle() ← 后台执行(agentToolUtils.ts)
|
||||
|
||||
### 同步 Agent(前台运行)
|
||||
|
||||
同步 Agent 的关键特性是 **可后台化**(`AgentTool.tsx:818-833`):
|
||||
同步 Agent 的关键特性是 **可后台化**(`AgentTool.tsx:1107`):
|
||||
|
||||
```typescript
|
||||
const registration = registerAgentForeground({
|
||||
@@ -218,7 +218,7 @@ const raceResult = await Promise.race([
|
||||
|
||||
## MCP 依赖的等待机制
|
||||
|
||||
如果 Agent 声明了 `requiredMcpServers`,`call()` 会等待这些服务器连接完成(`AgentTool.tsx:371-410`):
|
||||
如果 Agent 声明了 `requiredMcpServers`,`call()` 会等待这些服务器连接完成(`AgentTool.tsx:576`):
|
||||
|
||||
```typescript
|
||||
const MAX_WAIT_MS = 30_000 // 最长等 30 秒
|
||||
|
||||
@@ -37,7 +37,7 @@ Worktree 文件统一存放在仓库根目录下的 `.claude/worktrees/`:
|
||||
|
||||
## 创建流程:EnterWorktreeTool
|
||||
|
||||
`EnterWorktreeTool`(`src/tools/EnterWorktreeTool/EnterWorktreeTool.ts`)的执行链路:
|
||||
`EnterWorktreeTool`(`packages/builtin-tools/src/tools/EnterWorktreeTool/EnterWorktreeTool.ts`)的执行链路:
|
||||
|
||||
```
|
||||
EnterWorktreeTool.call({ name? })
|
||||
@@ -83,7 +83,7 @@ EnterWorktreeTool.call({ name? })
|
||||
|
||||
## 退出流程:ExitWorktreeTool
|
||||
|
||||
`ExitWorktreeTool`(`src/tools/ExitWorktreeTool/ExitWorktreeTool.ts`)支持两种退出策略:
|
||||
`ExitWorktreeTool`(`packages/builtin-tools/src/tools/ExitWorktreeTool/ExitWorktreeTool.ts`)支持两种退出策略:
|
||||
|
||||
### keep:保留 worktree
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ useInterval(checkForUpdates, 30 * 60 * 1000); // 每 30 分钟
|
||||
|
||||
任何更新尝试之前,系统会依次检查:
|
||||
|
||||
1. **自动更新是否被禁用?** — `getAutoUpdaterDisabledReason()`(`src/utils/config.ts:1735`)
|
||||
1. **自动更新是否被禁用?** — `getAutoUpdaterDisabledReason()`(`src/utils/config.ts:1737`)
|
||||
- `NODE_ENV === 'development'`
|
||||
- 设置了 `DISABLE_AUTOUPDATER` 环境变量
|
||||
- 仅限必要流量模式
|
||||
@@ -81,7 +81,7 @@ useInterval(checkForUpdates, 30 * 60 * 1000); // 每 30 分钟
|
||||
|
||||
`src/utils/autoUpdater.ts:70` — `assertMinVersion()`
|
||||
|
||||
从 `src/main.tsx:1775` 在启动时调用:
|
||||
定义于 `src/utils/autoUpdater.ts:70`,设计上在启动时调用(当前未接入启动流程):
|
||||
|
||||
```typescript
|
||||
void assertMinVersion();
|
||||
@@ -200,7 +200,7 @@ Windows 系统使用文件复制而非符号链接。
|
||||
|
||||
**文件**: `src/migrations/migrateAutoUpdatesToSettings.ts`
|
||||
|
||||
一次性将旧版 `globalConfig.autoUpdates = false` 迁移为 settings 中的 `DISABLE_AUTOUPDATER=1` 环境变量。从 `src/main.tsx:325` 在启动时调用。
|
||||
一次性将旧版 `globalConfig.autoUpdates = false` 迁移为 settings 中的 `DISABLE_AUTOUPDATER=1` 环境变量。定义于 `src/migrations/migrateAutoUpdatesToSettings.ts`(当前未接入启动流程)。
|
||||
|
||||
---
|
||||
|
||||
@@ -270,7 +270,7 @@ React hook `useUpdateNotification(updatedVersion)` — 确保每次 semver 变
|
||||
| `src/utils/releaseNotes.ts` | Changelog 获取、缓存与展示 |
|
||||
| `src/utils/semver.ts` | Semver 版本比较(Bun 原生 + npm 回退) |
|
||||
| `src/utils/doctorDiagnostic.ts` | 安装类型检测与健康诊断 |
|
||||
| `src/utils/config.ts:1735` | `getAutoUpdaterDisabledReason()` — 禁用检查逻辑 |
|
||||
| `src/utils/config.ts:1737` | `getAutoUpdaterDisabledReason()` — 禁用检查逻辑 |
|
||||
| `src/migrations/migrateAutoUpdatesToSettings.ts` | 旧版配置迁移 |
|
||||
| `src/screens/Doctor.tsx` | Doctor 命令 UI,展示自动更新状态 |
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ const messagesForCompact = microcompactResult.messages
|
||||
MicroCompact 不压缩整个对话,而是**清除旧工具输出的内容**。它维护一个白名单:
|
||||
|
||||
```typescript
|
||||
// src/services/compact/microCompact.ts:41-48
|
||||
// src/services/compact/microCompact.ts:41-50
|
||||
const COMPACTABLE_TOOLS = new Set([
|
||||
FILE_READ_TOOL_NAME, // 'Read' - 文件读取
|
||||
...SHELL_TOOL_NAMES, // 'Bash' - 命令输出
|
||||
@@ -143,7 +143,7 @@ const stripped2 = stripReinjectedAttachments(stripped) // 移除会被重新注
|
||||
压缩后,系统会从摘要中**重新注入关键上下文**:
|
||||
|
||||
```typescript
|
||||
// compact.ts:124-132
|
||||
// compact.ts:126-134
|
||||
export const POST_COMPACT_TOKEN_BUDGET = 50_000 // 总预算
|
||||
export const POST_COMPACT_MAX_FILES_TO_RESTORE = 5 // 最多恢复 5 个文件
|
||||
export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000 // 每文件 5K token
|
||||
|
||||
@@ -39,7 +39,7 @@ Claude Code 的记忆系统是**纯文件**的——没有数据库、没有向
|
||||
`MEMORY.md` 是记忆的入口索引,每次对话都完整加载到上下文中:
|
||||
|
||||
```typescript
|
||||
// memdir.ts:35-38
|
||||
// memdir.ts:34-38
|
||||
export const ENTRYPOINT_NAME = 'MEMORY.md'
|
||||
export const MAX_ENTRYPOINT_LINES = 200
|
||||
export const MAX_ENTRYPOINT_BYTES = 25_000
|
||||
|
||||
@@ -20,12 +20,12 @@ buildSystemPromptBlocks() → TextBlockParam[] (分块 + cache_control 标
|
||||
|
||||
1. **`getSystemPrompt()`**(`src/constants/prompts.ts:444`)—— 收集静态段 + 动态段,插入 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 分界标记
|
||||
2. **`buildEffectiveSystemPrompt()`**(`src/utils/systemPrompt.ts:41`)—— 按 Override > Coordinator > Agent > Custom > Default 优先级选择
|
||||
3. **`buildSystemPromptBlocks()`**(`src/services/api/claude.ts:3214`)—— 调用 `splitSysPromptPrefix()` 分块,为每个块附加 `cache_control`
|
||||
3. **`buildSystemPromptBlocks()`**(`src/services/api/claude.ts:3279`)—— 调用 `splitSysPromptPrefix()` 分块,为每个块附加 `cache_control`
|
||||
|
||||
## SystemPrompt 品牌类型
|
||||
|
||||
```typescript
|
||||
// src/utils/systemPromptType.ts:8
|
||||
// packages/@ant/model-provider/src/types/systemPrompt.ts:4
|
||||
export type SystemPrompt = readonly string[] & {
|
||||
readonly __brand: 'SystemPrompt'
|
||||
}
|
||||
@@ -185,7 +185,7 @@ export function shouldUseGlobalCacheScope(): boolean {
|
||||
|
||||
### `getCacheControl()`:TTL 决策
|
||||
|
||||
`src/services/api/claude.ts:359` 生成的 `cache_control` 对象:
|
||||
`src/services/api/claude.ts:348` 生成的 `cache_control` 对象:
|
||||
|
||||
```typescript
|
||||
{
|
||||
@@ -195,14 +195,14 @@ export function shouldUseGlobalCacheScope(): boolean {
|
||||
}
|
||||
```
|
||||
|
||||
1 小时 TTL 的判定逻辑(`should1hCacheTTL()`,第 394 行):
|
||||
1 小时 TTL 的判定逻辑(`should1hCacheTTL()`,第 383 行):
|
||||
- **Bedrock 用户**:通过环境变量 `ENABLE_PROMPT_CACHING_1H_BEDROCK` 启用
|
||||
- **1P 用户**:通过 GrowthBook 配置的 `allowlist` 数组匹配 `querySource`,支持前缀通配符(如 `"repl_main_thread*"`)
|
||||
- **会话级锁定**:资格判定结果在 bootstrap state 中缓存,防止 GrowthBook 配置中途变化导致同一会话内 TTL 不一致
|
||||
|
||||
### 缓存破坏:Session-Specific Guidance 的放置
|
||||
|
||||
`getSessionSpecificGuidanceSection()`(`src/constants/prompts.ts:352`)的内容必须放在 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` **之后**。因为它包含:
|
||||
`getSessionSpecificGuidanceSection()`(`src/constants/prompts.ts:354`)的内容必须放在 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` **之后**。因为它包含:
|
||||
- 当前会话的 enabledTools 集合
|
||||
- `isForkSubagentEnabled()` 的运行时判定
|
||||
- `getIsNonInteractiveSession()` 的结果
|
||||
|
||||
@@ -32,7 +32,7 @@ message_stop ← 消息结束
|
||||
|
||||
### 事件处理状态机
|
||||
|
||||
`src/services/api/claude.ts` 中 `queryStreamRaw()` 函数的事件处理循环实现了一个基于 `switch(part.type)` 的状态机:
|
||||
`src/services/api/claude.ts` 中 `queryModelWithStreaming()` 函数的事件处理循环实现了一个基于 `switch(part.type)` 的状态机:
|
||||
|
||||
| 事件类型 | 处理逻辑 | 状态变更 |
|
||||
|----------|----------|----------|
|
||||
@@ -167,10 +167,13 @@ UI 层通过 `useToolCallProgress` hook 实时展示命令输出,而不是等
|
||||
|
||||
| Provider | 流式协议 | 特殊处理 |
|
||||
|----------|----------|----------|
|
||||
| **Anthropic Direct** | 原生 SSE | 延迟最低,TTFT 最快 |
|
||||
| **firstParty** (Anthropic Direct) | 原生 SSE | 延迟最低,TTFT 最快 |
|
||||
| **AWS Bedrock** | AWS SDK 流式接口 | 需要额外的 beta header 和认证 |
|
||||
| **Google Vertex** | gRPC → 事件流 | 通过 `getMergedBetas()` 适配 |
|
||||
| **Azure** | Anthropic 兼容 API | 自定义 base URL |
|
||||
| **foundry** | Anthropic 兼容 API | 内部部署 |
|
||||
| **openai** | OpenAI 流式适配器 | 转换为 Anthropic 内部格式 |
|
||||
| **gemini** | Gemini 流式适配器 | 转换为 Anthropic 内部格式 |
|
||||
| **grok** (xAI) | Grok 流式适配器 | 转换为 Anthropic 内部格式 |
|
||||
|
||||
所有 Provider 通过统一的 `Stream<BetaRawMessageStreamEvent>` 抽象层屏蔽差异。上层代码(QueryEngine、REPL)不需要关心底层用的是哪个 Provider。
|
||||
|
||||
|
||||
@@ -74,17 +74,17 @@ const toolUpdates = streamingToolExecutor
|
||||
|
||||
| 终止原因 | 触发位置 | 机制 |
|
||||
|----------|---------|------|
|
||||
| **blocking_limit** | 第 646 行 | Token 计数超过硬限制(非 autocompact 模式)→ 生成 PTL 错误消息 → 返回 |
|
||||
| **image_error** | 第 980 行 | `ImageSizeError` / `ImageResizeError` 异常 → 直接返回 |
|
||||
| **model_error** | 第 999 行 | `callModel()` 抛出不可恢复异常 → 生成错误消息 → 返回 |
|
||||
| **aborted_streaming** | 第 1054 行 | `abortController.signal.aborted`(流式阶段)→ 为未完成的 tool_use 生成合成 tool_result → 返回 |
|
||||
| **prompt_too_long** | 第 1178/1185 行 | 413 错误且 reactive compact 无法恢复 → 暂扣的错误消息被释放 → 返回 |
|
||||
| **completed** | 第 1267 行 | API 错误(限流、认证失败等)导致无法继续 → 返回 |
|
||||
| **stop_hook_prevented** | 第 1282 行 | Stop hook 返回 `preventContinuation: true` → 返回 |
|
||||
| **completed** | 第 1360 行 | 正常完成:AI 未发出 tool_use → `needsFollowUp = false` → 经过 stop hooks → 返回 |
|
||||
| **aborted_tools** | 第 1518 行 | `abortController.signal.aborted`(工具执行阶段)→ 返回 |
|
||||
| **hook_stopped** | 第 1523 行 | 工具执行期间 hook 返回 `shouldPreventContinuation` → 返回 |
|
||||
| **max_turns** | 第 1714 行 | 轮次计数超过 `maxTurns` 限制 → 返回 |
|
||||
| **blocking_limit** | 第 686 行 | Token 计数超过硬限制(非 autocompact 模式)→ 生成 PTL 错误消息 → 返回 |
|
||||
| **image_error** | 第 1021 行 | `ImageSizeError` / `ImageResizeError` 异常 → 直接返回 |
|
||||
| **model_error** | 第 1040 行 | `callModel()` 抛出不可恢复异常 → 生成错误消息 → 返回 |
|
||||
| **aborted_streaming** | 第 1095 行 | `abortController.signal.aborted`(流式阶段)→ 为未完成的 tool_use 生成合成 tool_result → 返回 |
|
||||
| **prompt_too_long** | 第 1219/1226 行 | 413 错误且 reactive compact 无法恢复 → 暂扣的错误消息被释放 → 返回 |
|
||||
| **completed** | 第 1308 行 | API 错误(限流、认证失败等)导致无法继续 → 返回 |
|
||||
| **stop_hook_prevented** | 第 1323 行 | Stop hook 返回 `preventContinuation: true` → 返回 |
|
||||
| **completed** | 第 1401 行 | 正常完成:AI 未发出 tool_use → `needsFollowUp = false` → 经过 stop hooks → 返回 |
|
||||
| **aborted_tools** | 第 1559 行 | `abortController.signal.aborted`(工具执行阶段)→ 返回 |
|
||||
| **hook_stopped** | 第 1564 行 | 工具执行期间 hook 返回 `shouldPreventContinuation` → 返回 |
|
||||
| **max_turns** | 第 1755 行 | 轮次计数超过 `maxTurns` 限制 → 返回 |
|
||||
|
||||
## 继续条件(恢复路径)
|
||||
|
||||
@@ -158,7 +158,7 @@ type State = {
|
||||
- **每一步都产生真实信息**:`runTools()` 返回的 `toolResults` 是 API 不可能预知的——命令输出、文件内容、错误信息
|
||||
- **动态上下文管理**:每轮迭代前都重新评估压缩需求(autocompact → microcompact → snip),基于最新的 token 计数
|
||||
- **错误即时恢复**:工具失败不需要推倒重来——stop hook 可以注入阻塞错误让 AI 修正策略
|
||||
- **用户可控**:`abortController.signal` 在循环的多个检查点被检测(第 1018、1048、1488 行),用户按 ESC 可以优雅中断
|
||||
- **用户可控**:`abortController.signal` 在循环的多个检查点被检测(第 1059、1095、1529 行),用户按 ESC 可以优雅中断
|
||||
- **成本控制**:Token Budget 在每轮终止前检查,防止 AI 无效循环
|
||||
|
||||
## 一个完整的迭代示例
|
||||
|
||||
@@ -12,7 +12,7 @@ Claude Code 的 Agent 不仅仅来自用户自定义——系统有三类来源
|
||||
|
||||
| 来源 | 位置 | 优先级 |
|
||||
|------|------|--------|
|
||||
| **Built-in** | `src/tools/AgentTool/built-in/` 硬编码 | 最低(可被覆盖) |
|
||||
| **Built-in** | `packages/builtin-tools/src/tools/AgentTool/built-in/` 硬编码 | 最低(可被覆盖) |
|
||||
| **Plugin** | 通过插件系统注册 | 中 |
|
||||
| **User/Project/Policy** | `.claude/agents/*.md` 或 settings.json | 最高 |
|
||||
|
||||
@@ -127,7 +127,7 @@ color: "blue" # 终端中的 Agent 颜色标识
|
||||
以内置 Explore Agent 为例:
|
||||
|
||||
```typescript
|
||||
// src/tools/AgentTool/built-in/exploreAgent.ts
|
||||
// packages/builtin-tools/src/tools/AgentTool/built-in/exploreAgent.ts
|
||||
disallowedTools: [
|
||||
'Agent', // 不能嵌套调用 Agent
|
||||
'ExitPlanMode', // 不需要 plan mode
|
||||
|
||||
@@ -240,7 +240,7 @@ SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 tr
|
||||
|
||||
## Session Hook 的生命周期
|
||||
|
||||
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(调用位置:`src/tools/AgentTool/runAgent.ts`;定义位置:`src/utils/hooks/registerFrontmatterHooks.ts`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()`(定义位置:`src/utils/hooks/sessionHooks.ts`)清理。
|
||||
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(调用位置:`packages/builtin-tools/src/tools/AgentTool/runAgent.ts`;定义位置:`src/utils/hooks/registerFrontmatterHooks.ts`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()`(定义位置:`src/utils/hooks/sessionHooks.ts`)清理。
|
||||
|
||||
```typescript
|
||||
// runAgent.ts — 注册 agent 的前置 Hook
|
||||
|
||||
@@ -304,7 +304,7 @@ timer.unref?.() // 不阻止进程退出
|
||||
|
||||
## 工具发现:从 MCP 到 Tool 接口
|
||||
|
||||
`fetchToolsForClient()`(`client.ts:1745-2000`)使用 `memoizeWithLRU` 缓存(上限 20),将 MCP 工具转换为 Claude Code 的统一 Tool 接口:
|
||||
`fetchToolsForClient()`(`client.ts:1744-2000`)使用 `memoizeWithLRU` 缓存(上限 100),将 MCP 工具转换为 Claude Code 的统一 Tool 接口:
|
||||
|
||||
```typescript
|
||||
const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
|
||||
|
||||
@@ -22,7 +22,7 @@ Skill 的核心洞见:**复杂任务的关键不在代码逻辑,而在 Promp
|
||||
|
||||
### 1. 内置命令(Built-in Commands)
|
||||
|
||||
硬编码在 `src/commands.ts:258` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown,但实现了相同的 `Command` 接口(`src/types/command.ts`)。
|
||||
硬编码在 `src/commands.ts:299` 的 `COMMANDS` memoize 数组中,包含 70+ 条命令(`/commit`、`/review`、`/compact` 等)。这些是 TypeScript 模块而非 Markdown,但实现了相同的 `Command` 接口(`src/types/command.ts`)。
|
||||
|
||||
### 2. Bundled Skills(编译时打包)
|
||||
|
||||
@@ -98,7 +98,7 @@ shell: ["bash"] # Shell 执行环境
|
||||
|
||||
## 两条执行路径:Inline vs Fork
|
||||
|
||||
SkillTool(`src/tools/SkillTool/SkillTool.ts:332`)在 `call()` 中根据 `command.context` 分流:
|
||||
SkillTool(`packages/builtin-tools/src/tools/SkillTool/SkillTool.ts:332`)在 `call()` 中根据 `command.context` 分流:
|
||||
|
||||
### Inline 模式(默认)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
- **端点**: `{region}-aiplatform.googleapis.com`
|
||||
- **认证**: `GoogleAuth` + `cloud-platform` scope
|
||||
- **文件**: `src/services/api/client.ts:228-298`
|
||||
- **文件**: `src/services/api/client.ts:221-298`
|
||||
|
||||
### 4. Azure Foundry
|
||||
|
||||
@@ -129,12 +129,12 @@ WebSearch 工具支持直接抓取 Bing 搜索结果页面,也支持通过 Bra
|
||||
- **Bing 端点**: `https://www.bing.com/search?q={query}&setmkt=en-US`
|
||||
- **Brave 端点**: `https://api.search.brave.com/res/v1/llm/context?q={query}`
|
||||
- **文件**:
|
||||
- `src/tools/WebSearchTool/adapters/bingAdapter.ts`
|
||||
- `src/tools/WebSearchTool/adapters/braveAdapter.ts`
|
||||
- `packages/builtin-tools/src/tools/WebSearchTool/adapters/bingAdapter.ts`
|
||||
- `packages/builtin-tools/src/tools/WebSearchTool/adapters/braveAdapter.ts`
|
||||
|
||||
另外还有 Domain Blocklist 查询:
|
||||
- **端点**: `https://api.anthropic.com/api/web/domain_info?domain={domain}`
|
||||
- **文件**: `src/tools/WebFetchTool/utils.ts`
|
||||
- **文件**: `packages/builtin-tools/src/tools/WebFetchTool/utils.ts`
|
||||
|
||||
### 15. Google Cloud Storage (自动更新)
|
||||
|
||||
|
||||
207
docs/features/acp-link.md
Normal file
207
docs/features/acp-link.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# acp-link — ACP 代理服务器
|
||||
|
||||
> 源码目录:`packages/acp-link/`
|
||||
> PR: #292
|
||||
> 新增时间:2026-04-18
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
`acp-link` 是一个 ACP (Agent Client Protocol) 代理服务器,将 WebSocket 客户端桥接到 ACP agent 的 stdio 接口。它让 ACP agent(如 Claude Code)可以通过 WebSocket 远程访问,而不仅限于本地 stdio。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **WebSocket → stdio 桥接**:将浏览器/远程客户端的 WebSocket 连接转换为 ACP agent 的 stdin/stdout NDJSON 流
|
||||
- **会话管理**:创建、加载、恢复、列出、关闭会话
|
||||
- **权限审批流程**:客户端可远程审批 agent 的工具权限请求
|
||||
- **RCS 集成**:可与 Remote Control Server (RCS) 连接,将 ACP agent 注册到 RCS 并通过 Web UI 交互
|
||||
- **HTTPS 支持**:内置自签名证书生成,支持安全连接
|
||||
- **Token 认证**:自动生成或通过环境变量配置认证 token
|
||||
|
||||
## 二、架构
|
||||
|
||||
### 独立模式
|
||||
|
||||
```
|
||||
┌──────────────────┐ WebSocket ┌──────────────────┐ stdio/NDJSON ┌──────────────┐
|
||||
│ 浏览器/客户端 │ ◄──────────────►│ acp-link │ ◄────────────────►│ ACP Agent │
|
||||
│ (WS Client) │ ws://host:port │ (Proxy Server) │ spawn subprocess │ (Claude等) │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### RCS 集成模式
|
||||
|
||||
```
|
||||
┌──────────────┐ WebSocket ┌──────────────────┐ stdio/NDJSON ┌──────────────┐
|
||||
│ RCS Web UI │ ◄──────────────►│ Remote Control │ ◄─────────────────►│ acp-link │
|
||||
│ (/code/*) │ ACP Relay WS │ Server (RCS) │ ACP events │ + Agent │
|
||||
└──────────────┘ └──────────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
packages/acp-link/
|
||||
├── src/
|
||||
│ ├── server.ts # 主服务器:WS 连接管理、会话管理、权限处理、消息桥接
|
||||
│ ├── rcs-upstream.ts # RCS 上游客户端:REST 注册 + WS identify 两步流程
|
||||
│ ├── cert.ts # TLS 证书生成(自签名)
|
||||
│ ├── logger.ts # 日志模块
|
||||
│ ├── types.ts # JSON-RPC 和 ACP 协议类型定义
|
||||
│ ├── cli/
|
||||
│ │ ├── bin.ts # CLI 入口
|
||||
│ │ ├── command.ts # 命令行参数解析
|
||||
│ │ ├── app.ts # 应用启动
|
||||
│ │ └── context.ts # 上下文配置
|
||||
│ └── __tests__/ # 测试(cert, server, types)
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
## 三、安装与使用
|
||||
|
||||
### 基本用法
|
||||
|
||||
```bash
|
||||
# 直接运行(在 monorepo 中)
|
||||
# 注意:claude 本身不支持 ACP,需要用 ccb-bun --acp 启动 ACP agent
|
||||
bun packages/acp-link/src/cli/bin.ts ccb-bun -- --acp
|
||||
|
||||
# 指定端口和主机
|
||||
acp-link --port 9000 --host 0.0.0.0 ccb-bun -- --acp
|
||||
|
||||
# 启用 HTTPS(自签名证书)
|
||||
acp-link --https ccb-bun -- --acp
|
||||
|
||||
# 调试模式
|
||||
acp-link --debug ccb-bun -- --acp
|
||||
```
|
||||
|
||||
### CLI 参考
|
||||
|
||||
```
|
||||
USAGE
|
||||
acp-link [--port value] [--host value] [--debug] [--no-auth] [--https] <command>...
|
||||
acp-link --help
|
||||
acp-link --version
|
||||
|
||||
FLAGS
|
||||
[--port] Port to listen on [default = 9315]
|
||||
[--host] Host to bind to [default = localhost]
|
||||
[--debug] Enable debug logging to file
|
||||
[--no-auth] Disable authentication (dangerous)
|
||||
[--https] Enable HTTPS with self-signed cert
|
||||
-h --help Print help information and exit
|
||||
-v --version Print version information and exit
|
||||
|
||||
ARGUMENTS
|
||||
command... Agent command followed by its arguments (e.g. "ccb-bun -- --acp")
|
||||
```
|
||||
|
||||
## 四、认证
|
||||
|
||||
默认启动时自动生成随机 token。客户端连接时不要把 token 放在 URL 中:
|
||||
|
||||
```
|
||||
ws://localhost:9315/ws
|
||||
```
|
||||
|
||||
无法发送 `Authorization` header 的 WebSocket 客户端需要使用
|
||||
`rcs.auth.<base64url-token>` 子协议传递 token。
|
||||
|
||||
配置固定 token:
|
||||
|
||||
```bash
|
||||
ACP_AUTH_TOKEN=my-fixed-token acp-link ccb-bun -- --acp
|
||||
```
|
||||
|
||||
禁用认证(不推荐,仅用于开发):
|
||||
|
||||
```bash
|
||||
acp-link --no-auth ccb-bun -- --acp
|
||||
```
|
||||
|
||||
## 五、RCS 集成
|
||||
|
||||
acp-link 支持将 ACP agent 注册到 Remote Control Server,通过 Web UI 远程操控。
|
||||
|
||||
### 连接方式
|
||||
|
||||
```bash
|
||||
# 通过环境变量配置 RCS 连接
|
||||
ACP_RCS_URL=http://localhost:3000 \
|
||||
ACP_RCS_TOKEN=sk-rcs-your-key \
|
||||
acp-link ccb-bun -- --acp
|
||||
```
|
||||
|
||||
### 注册流程(两步)
|
||||
|
||||
1. **REST 注册**:通过 `POST /v1/environments/bridge` 向 RCS 注册环境
|
||||
2. **WS identify**:建立 WebSocket 连接后发送 `identify` 消息(携带 agentId),替代完整 `register`
|
||||
|
||||
RCS 的 ACP WebSocket 连接不接受 URL query token。acp-link 会通过
|
||||
`rcs.auth.<base64url-token>` WebSocket 子协议发送 `ACP_RCS_TOKEN`。
|
||||
|
||||
```
|
||||
acp-link RCS
|
||||
│ │
|
||||
│── POST /v1/environments/bridge ──►│ (REST 注册)
|
||||
│◄── { agentId, sessionId } ───────│
|
||||
│ │
|
||||
│── WS connect ─────────────────►│ (WebSocket)
|
||||
│── identify { agentId } ────────►│ (WS 标识)
|
||||
│◄── identified ─────────────────│
|
||||
│ │
|
||||
│── ACP events ─────────────────►│ (双向消息转发)
|
||||
│◄── user prompts/permissions ───│
|
||||
```
|
||||
|
||||
## 六、权限模式
|
||||
|
||||
### permissionMode 传递链
|
||||
|
||||
权限模式通过整条链路传递:Web UI → RCS → acp-link → ACP agent。
|
||||
|
||||
支持的权限模式:
|
||||
- `default` — 每次请求权限确认
|
||||
- `auto` — 自动判断
|
||||
- `acceptEdits` — 自动接受编辑
|
||||
- `plan` — 规划模式
|
||||
- `dontAsk` — 不询问
|
||||
- `bypassPermissions` — 绕过权限(需 sandbox 环境)
|
||||
|
||||
### fallback 链
|
||||
|
||||
当客户端未显式传递 permissionMode 时,使用以下 fallback 链:
|
||||
|
||||
```
|
||||
客户端传值 > config.permissionMode > ACP_PERMISSION_MODE 环境变量
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
ACP_PERMISSION_MODE=auto acp-link ccb-bun -- --acp
|
||||
```
|
||||
|
||||
## 七、权限管道(2026-04-18 改进)
|
||||
|
||||
### 模式同步
|
||||
|
||||
`applySessionMode` 在 agent 切换权限模式时同步 `appState.toolPermissionContext.mode`,确保内部权限上下文与 ACP 客户端状态一致。
|
||||
|
||||
### 统一权限流水线
|
||||
|
||||
`createAcpCanUseTool` 接入 `hasPermissionsToUseTool` 统一权限流水线,替代原来分散的处理逻辑。支持 `onModeChange` 回调,模式变更时实时同步。
|
||||
|
||||
### bypass 检测
|
||||
|
||||
`bypassPermissions` 模式增加可用性检测 — 仅在非 root 或 sandbox 环境中允许启用,防止权限绕过的安全风险。
|
||||
|
||||
## 八、环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `ACP_AUTH_TOKEN` | 固定认证 token(默认自动生成) |
|
||||
| `ACP_PERMISSION_MODE` | 默认权限模式 fallback |
|
||||
| `ACP_RCS_URL` | RCS 服务器地址(启用 RCS 集成) |
|
||||
| `ACP_RCS_TOKEN` | RCS API token |
|
||||
@@ -516,25 +516,37 @@ AI 也可通过 `SnipTool` 自动截断过长的对话:
|
||||
|
||||
| Flag | 默认 | 说明 |
|
||||
|------|------|------|
|
||||
| `BUDDY` | ✅ dev/build | 伴侣系统 |
|
||||
| `BRIDGE_MODE` | ✅ dev/build | 远程控制 |
|
||||
| `VOICE_MODE` | ✅ dev/build | 语音模式 |
|
||||
| `CHICAGO_MCP` | ✅ dev/build | Computer Use + Chrome |
|
||||
| `AGENT_TRIGGERS_REMOTE` | ✅ dev/build | 定时任务 |
|
||||
| `SHOT_STATS` | ✅ dev/build | API 统计 |
|
||||
| `TOKEN_BUDGET` | ✅ dev/build | Token 预算 |
|
||||
| `PROMPT_CACHE_BREAK_DETECTION` | ✅ dev/build | 缓存检测 |
|
||||
| `ULTRAPLAN` | ✅ dev/build | 高级规划 |
|
||||
| `DAEMON` | ✅ dev/build | 后台守护 |
|
||||
| `UDS_INBOX` | ✅ dev/build | Pipe IPC |
|
||||
| `LAN_PIPES` | ✅ dev/build | LAN 群控 |
|
||||
| `MONITOR_TOOL` | ✅ dev/build | 后台监控 |
|
||||
| `WORKFLOW_SCRIPTS` | ✅ dev/build | 工作流脚本 |
|
||||
| `FORK_SUBAGENT` | ✅ dev/build | 子 Agent |
|
||||
| `KAIROS` | ✅ dev/build | Kairos 调度 |
|
||||
| `COORDINATOR_MODE` | ✅ dev/build | 多 Worker |
|
||||
| `HISTORY_SNIP` | ✅ dev/build | 历史管理 |
|
||||
| `CONTEXT_COLLAPSE` | ✅ dev/build | 上下文折叠 |
|
||||
| `BUDDY` | ✅ dev only | 伴侣系统 |
|
||||
| `BRIDGE_MODE` | ✅ dev only | 远程控制 |
|
||||
| `VOICE_MODE` | ✅ dev+build | 语音模式 |
|
||||
| `CHICAGO_MCP` | ✅ dev+build | Computer Use + Chrome |
|
||||
| `AGENT_TRIGGERS_REMOTE` | ✅ dev+build | 定时任务 |
|
||||
| `SHOT_STATS` | ✅ dev+build | API 统计 |
|
||||
| `TOKEN_BUDGET` | ✅ dev+build | Token 预算 |
|
||||
| `PROMPT_CACHE_BREAK_DETECTION` | ✅ dev+build | 缓存检测 |
|
||||
| `ULTRAPLAN` | ✅ dev+build | 高级规划 |
|
||||
| `DAEMON` | ✅ dev+build | 后台守护 |
|
||||
| `UDS_INBOX` | ✅ dev only | Pipe IPC |
|
||||
| `LAN_PIPES` | ✅ dev only | LAN 群控 |
|
||||
| `MONITOR_TOOL` | ✅ dev+build | 后台监控 |
|
||||
| `WORKFLOW_SCRIPTS` | ✅ dev+build | 工作流脚本 |
|
||||
| `FORK_SUBAGENT` | ✅ dev+build | 子 Agent |
|
||||
| `KAIROS` | ✅ dev+build | Kairos 调度 |
|
||||
| `COORDINATOR_MODE` | ✅ dev+build | 多 Worker |
|
||||
| `HISTORY_SNIP` | ✅ dev+build | 历史管理 |
|
||||
| `CONTEXT_COLLAPSE` | ✅ dev+build | 上下文折叠 |
|
||||
| `ULTRATHINK` | ✅ dev+build | 扩展思考 |
|
||||
| `EXTRACT_MEMORIES` | ✅ dev+build | 自动记忆提取 |
|
||||
| `VERIFICATION_AGENT` | ✅ dev+build | 验证 Agent |
|
||||
| `KAIROS_BRIEF` | ✅ dev+build | Brief 模式 |
|
||||
| `AWAY_SUMMARY` | ✅ dev+build | 离开摘要 |
|
||||
| `ACP` | ✅ dev+build | ACP 协议 |
|
||||
| `LODESTONE` | ✅ dev+build | 深度链接 |
|
||||
| `BUILTIN_EXPLORE_PLAN_AGENTS` | ✅ dev+build | 内置 Explore/Plan agent |
|
||||
| `AGENT_TRIGGERS` | ✅ dev+build | 本地定时任务 |
|
||||
| `BG_SESSIONS` | ✅ dev only | 后台会话 |
|
||||
| `TEMPLATES` | ✅ dev only | 模板系统 |
|
||||
| `TRANSCRIPT_CLASSIFIER` | ✅ dev only | 对话分类 |
|
||||
|
||||
手动启用任意 flag:
|
||||
```bash
|
||||
|
||||
@@ -102,6 +102,6 @@ FEATURE_BASH_CLASSIFIER=1 FEATURE_TREE_SITTER_BASH=1 bun run dev
|
||||
| `src/utils/permissions/bashClassifier.ts` | — | Bash 分类器(stub,ANT-ONLY) |
|
||||
| `src/utils/permissions/yoloClassifier.ts` | 1496 | YOLO 分类器(完整参考实现) |
|
||||
| `src/utils/classifierApprovals.ts` | — | 分类器审批信号管理 |
|
||||
| `src/components/permissions/BashPermissionRequest.tsx:261-469` | — | 分类器 UI |
|
||||
| `src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx` | — | 分类器 UI |
|
||||
| `src/hooks/toolPermission/handlers/interactiveHandler.ts` | — | 交互式权限处理 |
|
||||
| `src/services/api/withRetry.ts:81` | — | API beta 标头 |
|
||||
| `src/services/api/withRetry.ts` | — | API beta 标头 |
|
||||
|
||||
@@ -30,7 +30,7 @@ BRIDGE_MODE 将本地 CLI 注册为"bridge 环境",可从 claude.ai 或其他
|
||||
|
||||
文件:`src/bridge/bridgeApi.ts`
|
||||
|
||||
Bridge API Client 提供 7 个核心操作:
|
||||
Bridge API Client 提供 9 个核心操作:
|
||||
|
||||
| 操作 | HTTP | 说明 |
|
||||
|------|------|------|
|
||||
@@ -137,7 +137,7 @@ FEATURE_BRIDGE_MODE=1 FEATURE_DAEMON=1 bun run dev
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/bridge/bridgeApi.ts` | 540 | API Client(核心) |
|
||||
| `src/bridge/bridgeApi.ts` | 541 | API Client(核心) |
|
||||
| `src/bridge/sessionRunner.ts` | — | 会话运行器 |
|
||||
| `src/bridge/bridgeConfig.ts` | — | 配置管理 |
|
||||
| `src/bridge/replBridgeTransport.ts` | — | 传输层 |
|
||||
|
||||
@@ -78,10 +78,13 @@ FEATURE_BUDDY=1 bun run dev
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `src/commands/buddy/index.ts` | `/buddy` 命令注册 |
|
||||
| `src/commands/buddy/buddy.ts` | `/buddy` 命令处理 |
|
||||
| `src/buddy/companion.ts` | 宠物生成与加载 |
|
||||
| `src/buddy/companionReact.ts` | 宠物反应系统(REPL 每轮查询后触发) |
|
||||
| `src/buddy/types.ts` | 类型定义(物种、稀有度、属性) |
|
||||
| `src/buddy/sprites.ts` | 终端像素画渲染 |
|
||||
| `src/buddy/CompanionSprite.tsx` | React 组件(输入框旁显示) |
|
||||
| `src/buddy/CompanionCard.tsx` | 宠物信息卡片(`/buddy` 无参数时展示) |
|
||||
| `src/buddy/useBuddyNotification.tsx` | 启动提示通知 |
|
||||
| `src/buddy/prompt.ts` | 宠物相关 prompt 模板 |
|
||||
|
||||
89
docs/features/channels.md
Normal file
89
docs/features/channels.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Channels — 外部频道消息接入
|
||||
|
||||
> 启动参数:`--channels` / `--dangerously-load-development-channels`
|
||||
> 状态:已解除 feature flag 和 OAuth 限制,可直接使用
|
||||
|
||||
## 概述
|
||||
|
||||
Channel 是一个 MCP 服务器,它将外部事件推送到你运行中的 Claude Code 会话中,以便 Claude 可以在你不在终端时做出反应。详细使用说明请参考以下文档:
|
||||
|
||||
- **官方文档**:[使用 channels 将事件推送到运行中的会话](https://code.claude.com/docs/zh-CN/channels)
|
||||
- **飞书插件**:[claude-code-feishu-channel](https://github.com/whobot-ai/claude-code-feishu-channel) — 社区首个飞书 Channel 插件,支持双向消息、配对认证、群组聊天、文件附件
|
||||
|
||||
本仓库现在内置了 **微信 WeChat channel**,不需要单独安装外部 marketplace 插件。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 启用频道监听(plugin 格式)
|
||||
ccb --channels plugin:feishu@claude-code-feishu-channel
|
||||
|
||||
# 启用内置微信 channel
|
||||
ccb weixin login
|
||||
ccb --channels plugin:weixin@builtin
|
||||
|
||||
# 启用频道监听(server 格式)
|
||||
ccb --channels server:my-slack-bridge
|
||||
|
||||
# 同时启用多个频道
|
||||
ccb --channels plugin:feishu@claude-code-feishu-channel --channels server:discord-bot
|
||||
|
||||
# 开发模式(跳过 allowlist 检查,用于测试自定义 channel)
|
||||
ccb --dangerously-load-development-channels server:my-custom-channel
|
||||
```
|
||||
|
||||
## 支持的 Channel
|
||||
|
||||
| Channel | 说明 | 来源 |
|
||||
|---------|------|------|
|
||||
| **Telegram** | 官方 Telegram Bot 集成 | `/plugin install telegram@claude-plugins-official` |
|
||||
| **Discord** | 官方 Discord Bot 集成 | `/plugin install discord@claude-plugins-official` |
|
||||
| **iMessage** | macOS 原生消息 | `/plugin install imessage@claude-plugins-official` |
|
||||
| **飞书 (Feishu/Lark)** | 双向消息、群组聊天、文件附件 | `/plugin install feishu@claude-code-feishu-channel` |
|
||||
| **微信 (WeChat)** | 内置 channel,支持扫码登录、双向消息、附件透传 | `ccb weixin login` + `ccb --channels plugin:weixin@builtin` |
|
||||
|
||||
## 微信内置 Channel
|
||||
|
||||
### 登录
|
||||
|
||||
```bash
|
||||
ccb weixin login
|
||||
```
|
||||
|
||||
已登录状态可清除:
|
||||
|
||||
```bash
|
||||
ccb weixin login clear
|
||||
```
|
||||
|
||||
### 会话启用
|
||||
|
||||
```bash
|
||||
ccb --channels plugin:weixin@builtin
|
||||
```
|
||||
|
||||
### 配对授权
|
||||
|
||||
首次收到未授权微信用户消息时,weixin channel 会回一条 6 位 pairing code。运营侧可在终端执行:
|
||||
|
||||
```bash
|
||||
ccb weixin access pair <code>
|
||||
```
|
||||
|
||||
确认后,该微信用户后续消息才会进入 Claude Code 会话。
|
||||
|
||||
## 相关文件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/services/mcp/channelNotification.ts` | 频道 gate 逻辑、消息包装 |
|
||||
| `src/services/mcp/channelAllowlist.ts` | 频道开关(已默认开启) |
|
||||
| `src/services/mcp/useManageMCPConnections.ts` | MCP 连接管理中的频道注册 |
|
||||
| `src/components/LogoV2/ChannelsNotice.tsx` | 启动时频道状态提示 |
|
||||
| `src/main.tsx` | `--channels` 参数解析 |
|
||||
| `src/interactiveHelpers.tsx` | Dev channels 确认对话框 |
|
||||
|
||||
## 参考链接
|
||||
|
||||
- [官方 Channels 文档](https://code.claude.com/docs/zh-CN/channels) — 完整使用说明、安全性、Enterprise 控制
|
||||
- [飞书 Channel 插件](https://github.com/whobot-ai/claude-code-feishu-channel) — 安装配置教程、MCP 工具、Skill 命令参考
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
## 概览
|
||||
|
||||
Computer Use 提供 37 个工具,分为三类:
|
||||
Computer Use 提供 38 个工具,分为三类:
|
||||
|
||||
| 分类 | 平台 | 工具数 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| 通用工具 | 全平台 | 24 | 官方 Computer Use 标准能力 |
|
||||
| Windows 专属工具 | Win32 | 10 | 绑定窗口模式下的增强能力 |
|
||||
| Windows 专属工具 | Win32 | 11 | 绑定窗口模式下的增强能力 |
|
||||
| 教学工具 | 全平台 | 3 | 分步引导模式(需 teachMode 开启) |
|
||||
|
||||
---
|
||||
@@ -82,7 +82,7 @@ Computer Use 提供 37 个工具,分为三类:
|
||||
|
||||
---
|
||||
|
||||
## 二、Windows 专属工具(10 个)
|
||||
## 二、Windows 专属工具(12 个)
|
||||
|
||||
仅 Windows 平台可见。核心能力:**绑定窗口后的独立操作——不抢占用户鼠标键盘**。
|
||||
|
||||
@@ -235,8 +235,19 @@ Computer Use 提供 37 个工具,分为三类:
|
||||
|
||||
| 工具 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| `open_terminal` | `agent`, `command?` | 打开新终端窗口并启动 AI agent(claude/codex/gemini/custom)。自动绑定窗口并截图验证 |
|
||||
| `activate_window` | `click_x?`, `click_y?` | 激活绑定窗口:SetForegroundWindow + BringWindowToTop + 点击确保焦点 |
|
||||
| `prompt_respond` | `response_type`, `arrow_direction?`, `arrow_count?`, `text?` | 处理终端 Yes/No/选择提示 |
|
||||
|
||||
**open_terminal agent 类型:**
|
||||
|
||||
| agent | 命令 | 说明 |
|
||||
|-------|------|------|
|
||||
| `claude` | `claude` | 启动 Claude Code |
|
||||
| `codex` | `codex` | 启动 Codex |
|
||||
| `gemini` | `gemini` | 启动 Gemini |
|
||||
| `custom` | 用户指定 | 自定义命令 |
|
||||
|
||||
**response_type 详情:**
|
||||
|
||||
| response_type | 操作 | 场景 |
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
- ✅ `@ant/computer-use-input` 拆为 dispatcher + backends(darwin + win32)
|
||||
- ✅ `@ant/computer-use-swift` 拆为 dispatcher + backends(darwin + win32)
|
||||
- ✅ `CHICAGO_MCP` 编译开关已开
|
||||
- ❌ `src/` 层有 6 处 macOS 硬编码阻塞
|
||||
- ✅ `src/` 层 macOS 硬编码已移除(Phase 2 已完成)
|
||||
|
||||
## 2. 阻塞点全景
|
||||
|
||||
@@ -19,25 +19,25 @@
|
||||
|
||||
| # | 文件:行号 | 阻塞代码 | 影响 |
|
||||
|---|----------|---------|------|
|
||||
| 1 | `src/main.tsx:1605` | `getPlatform() === 'macos'` | 整个 CU 初始化被跳过 |
|
||||
| 1 | `src/main.tsx:2366` | `feature("CHICAGO_MCP")` 门控 | CU 初始化入口 |
|
||||
|
||||
### 2.2 加载层
|
||||
|
||||
| # | 文件:行号 | 阻塞代码 | 影响 |
|
||||
|---|----------|---------|------|
|
||||
| 2 | `src/utils/computerUse/swiftLoader.ts:16` | `process.platform !== 'darwin'` → throw | 截图、应用管理全部不可用 |
|
||||
| 3 | `src/utils/computerUse/executor.ts:263` | `process.platform !== 'darwin'` → throw | 整个 executor 工厂函数不可用 |
|
||||
| 2 | `src/utils/computerUse/swiftLoader.ts` | macOS-only loader(已改为仅 darwin 加载) | 非 darwin 使用 platforms/ 替代 |
|
||||
| 3 | `src/utils/computerUse/executor.ts:302` | `process.platform !== 'darwin'` → cross-platform executor | 非 darwin 走跨平台路径 |
|
||||
|
||||
### 2.3 macOS 特有依赖
|
||||
|
||||
| # | 文件:行号 | 依赖 | macOS 实现 | 需要替代方案 |
|
||||
|---|----------|------|-----------|------------|
|
||||
| 4 | `executor.ts:70-88` | 剪贴板 | `pbcopy`/`pbpaste` | Win: PowerShell `Get/Set-Clipboard`;Linux: `xclip`/`wl-copy` |
|
||||
| 5 | `drainRunLoop.ts:21` | CFRunLoop pump | `cu._drainMainRunLoop()` | 非 darwin:直接执行 fn(),不需要 pump |
|
||||
| 6 | `escHotkey.ts:28` | ESC 热键 | CGEventTap | 非 darwin:返回 false(已有 Ctrl+C fallback) |
|
||||
| 7 | `hostAdapter.ts:48-54` | 系统权限 | TCC accessibility + screenRecording | Win:直接 granted;Linux:检查 xdotool |
|
||||
| 8 | `common.ts:56` | 平台标识 | `platform: 'darwin'` 硬编码 | 动态获取 |
|
||||
| 9 | `executor.ts:180` | 粘贴快捷键 | `command+v` | Win/Linux:`ctrl+v` |
|
||||
| 4 | `executor.ts:72-96` | 剪贴板 | `pbcopy`/`pbpaste` / PowerShell / xclip | Win: PowerShell `Get/Set-Clipboard`;Linux: `xclip`/`wl-copy` |
|
||||
| 5 | `drainRunLoop.ts` | CFRunLoop pump | `cu._drainMainRunLoop()` | 非 darwin:直接执行 fn(),不需要 pump |
|
||||
| 6 | `escHotkey.ts` | ESC 热键 | CGEventTap | 非 darwin:返回 false(已有 Ctrl+C fallback) |
|
||||
| 7 | `hostAdapter.ts` | 系统权限 | TCC accessibility + screenRecording | Win:直接 granted;Linux:检查 xdotool |
|
||||
| 8 | `common.ts:55-58` | 平台标识 | 动态获取 | 已改为 `process.platform` 分发 |
|
||||
| 9 | `executor.ts:232` | 粘贴快捷键 | `command`/`ctrl` 分发 | 已按平台分发粘贴快捷键 |
|
||||
|
||||
### 2.4 缺失的 Linux 后端
|
||||
|
||||
@@ -100,19 +100,19 @@
|
||||
|
||||
| 步骤 | 文件 | 改动 |
|
||||
|------|------|------|
|
||||
| 2.1 | `src/main.tsx:1605` | `getPlatform() === 'macos'` → 去掉平台限制,或改为 `!== 'unknown'` |
|
||||
| 2.2 | `src/utils/computerUse/swiftLoader.ts:16-18` | 移除 `process.platform !== 'darwin'` throw。`@ant/computer-use-swift/index.ts` 已有跨平台 dispatch |
|
||||
| 2.3 | `src/utils/computerUse/executor.ts:263-267` | 移除 `process.platform !== 'darwin'` throw。改为检查 input/swift isSupported |
|
||||
| 2.4 | `src/utils/computerUse/executor.ts:70-88` | 剪贴板函数按平台分发:darwin→pbcopy/pbpaste,win32→PowerShell Get/Set-Clipboard,linux→xclip |
|
||||
| 2.5 | `src/utils/computerUse/executor.ts:180` | `typeViaClipboard` 中 `command+v` → 非 darwin 时用 `ctrl+v` |
|
||||
| 2.6 | `src/utils/computerUse/executor.ts:273` | `const cu = requireComputerUseSwift()` → 改为 `new ComputerUseAPI()`(从 package 直接实例化,不走 swiftLoader throw) |
|
||||
| 2.7 | `src/utils/computerUse/drainRunLoop.ts` | 开头加 `if (process.platform !== 'darwin') return fn()` |
|
||||
| 2.8 | `src/utils/computerUse/escHotkey.ts` | `registerEscHotkey` 非 darwin 返回 false(已有 Ctrl+C fallback) |
|
||||
| 2.9 | `src/utils/computerUse/hostAdapter.ts:48-54` | `ensureOsPermissions` 非 darwin 返回 `{ granted: true }` |
|
||||
| 2.10 | `src/utils/computerUse/common.ts:56` | `platform: 'darwin'` → `platform: process.platform === 'win32' ? 'windows' : process.platform === 'linux' ? 'linux' : 'darwin'` |
|
||||
| 2.11 | `src/utils/computerUse/common.ts:55` | `screenshotFiltering: 'native'` → 非 darwin 时 `'none'`(Windows/Linux 截图不支持 per-app 过滤) |
|
||||
| 2.12 | `src/utils/computerUse/gates.ts:13` | `enabled: false` → `enabled: true`(无 GrowthBook 时默认可用) |
|
||||
| 2.13 | `src/utils/computerUse/gates.ts:39-43` | `hasRequiredSubscription()` → 直接返回 `true` |
|
||||
| 2.1 | `src/main.tsx:2366` | `feature("CHICAGO_MCP")` → 已为跨平台入口 |
|
||||
| 2.2 | `src/utils/computerUse/swiftLoader.ts` | 已改为仅 darwin 加载,非 darwin 使用 platforms/ |
|
||||
| 2.3 | `src/utils/computerUse/executor.ts:302-309` | 已改为 cross-platform dispatch(非 darwin → createCrossPlatformExecutor) |
|
||||
| 2.4 | `src/utils/computerUse/executor.ts:72-96` | 剪贴板已按平台分发:darwin→pbcopy/pbpaste,win32→PowerShell,linux→xclip |
|
||||
| 2.5 | `src/utils/computerUse/executor.ts:232` | 粘贴快捷键已按平台分发:darwin→command,其他→ctrl |
|
||||
| 2.6 | `src/utils/computerUse/executor.ts:302-309` | 非 darwin 已改为 `createCrossPlatformExecutor()` |
|
||||
| 2.7 | `src/utils/computerUse/drainRunLoop.ts` | 非 darwin 无需 pump(直接执行 fn) |
|
||||
| 2.8 | `src/utils/computerUse/escHotkey.ts` | 非 darwin 返回 false(已有 Ctrl+C fallback) |
|
||||
| 2.9 | `src/utils/computerUse/hostAdapter.ts` | 非 darwin 权限检查逻辑已实现 |
|
||||
| 2.10 | `src/utils/computerUse/common.ts:58` | 已改为动态 `process.platform` 分发 |
|
||||
| 2.11 | `src/utils/computerUse/common.ts:55` | 已改为 darwin→'native',其他→'none' |
|
||||
| 2.12 | `src/utils/computerUse/gates.ts:55` | 已更新(需验证 enabled 默认值) |
|
||||
| 2.13 | `src/utils/computerUse/gates.ts:39` | `hasRequiredSubscription()` 已更新 |
|
||||
|
||||
### Phase 3:新增 Linux 后端
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ CONTEXT_COLLAPSE 让模型内省上下文窗口使用情况,并智能压缩旧
|
||||
| 折叠核心 | `src/services/contextCollapse/index.ts` | **Stub** — 接口完整(`ContextCollapseStats`、`CollapseResult`、`DrainResult`),函数全部空操作 |
|
||||
| 折叠操作 | `src/services/contextCollapse/operations.ts` | **Stub** — `projectView` 为恒等函数 |
|
||||
| 折叠持久化 | `src/services/contextCollapse/persist.ts` | **Stub** — `restoreFromEntries` 为空操作 |
|
||||
| CtxInspectTool | `src/tools/CtxInspectTool/` | **缺失** — 目录不存在 |
|
||||
| CtxInspectTool | `packages/builtin-tools/src/tools/CtxInspectTool/CtxInspectTool.ts` | **实现** — 上下文内省工具 |
|
||||
| SnipTool 提示 | `src/tools/SnipTool/prompt.ts` | **Stub** — 空工具名 |
|
||||
| SnipTool 实现 | `src/tools/SnipTool/SnipTool.ts` | **缺失** |
|
||||
| force-snip 命令 | `src/commands/force-snip.js` | **缺失** |
|
||||
@@ -106,7 +106,7 @@ SnipTool 提供手动折叠能力:
|
||||
| 1 | `services/contextCollapse/index.ts` | 大 | 折叠状态机、LLM 调用、消息压缩 |
|
||||
| 2 | `services/contextCollapse/operations.ts` | 中 | `projectView()` 消息过滤 |
|
||||
| 3 | `services/contextCollapse/persist.ts` | 小 | `restoreFromEntries()` 磁盘持久化 |
|
||||
| 4 | `tools/CtxInspectTool/` | 中 | 上下文内省工具(token 计数、已折叠范围) |
|
||||
| 4 | `tools/CtxInspectTool/` | 已完成 | 上下文内省工具已实现(`packages/builtin-tools/src/tools/CtxInspectTool/`) |
|
||||
| 5 | `tools/SnipTool/SnipTool.ts` | 中 | Snip 工具实现 |
|
||||
| 6 | `commands/force-snip.js` | 小 | `/force-snip` 命令 |
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# DAEMON — 后台守护进程
|
||||
|
||||
> Feature Flag: `FEATURE_DAEMON=1`
|
||||
> 实现状态:主进程和 worker 注册为 Stub,CLI 路由完整
|
||||
> 实现状态:Supervisor 和 remoteControl Worker 已实现
|
||||
> 引用数:3
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
DAEMON 将 Claude Code 变为后台守护进程。主进程(supervisor)管理多个 worker 进程的生命周期,通过 Unix 域套接字进行 IPC。适用于持续运行的后台服务场景(如配合 BRIDGE_MODE 提供远程控制服务)。
|
||||
DAEMON 将 Claude Code 变为后台守护进程。主进程(supervisor)管理多个 worker 子进程的生命周期,通过文件系统状态文件进行通信。适用于持续运行的后台服务场景(如配合 BRIDGE_MODE 提供远程控制服务)。
|
||||
|
||||
## 二、实现架构
|
||||
|
||||
@@ -14,8 +14,9 @@ DAEMON 将 Claude Code 变为后台守护进程。主进程(supervisor)管
|
||||
|
||||
| 模块 | 文件 | 状态 |
|
||||
|------|------|------|
|
||||
| 守护主进程 | `src/daemon/main.ts` | **Stub** — `daemonMain: () => Promise.resolve()` |
|
||||
| Worker 注册 | `src/daemon/workerRegistry.ts` | **Stub** — `runDaemonWorker: () => Promise.resolve()` |
|
||||
| 守护主进程 | `src/daemon/main.ts` | **已实现** — Supervisor 含子命令、Worker 生命周期管理、指数退避重启 |
|
||||
| Worker 注册 | `src/daemon/workerRegistry.ts` | **已实现** — remoteControl Worker(headless bridge) |
|
||||
| Daemon 状态 | `src/daemon/state.ts` | **已实现** — PID/状态文件的读写与查询 |
|
||||
| CLI 路由 | `src/entrypoints/cli.tsx` | **布线** — `--daemon-worker` 和 `daemon` 子命令 |
|
||||
| 命令注册 | `src/commands.ts` | **布线** — DAEMON + BRIDGE_MODE 门控 |
|
||||
|
||||
@@ -23,34 +24,49 @@ DAEMON 将 Claude Code 变为后台守护进程。主进程(supervisor)管
|
||||
|
||||
```
|
||||
# 启动守护进程
|
||||
claude daemon
|
||||
claude daemon start
|
||||
|
||||
# 以 worker 身份启动
|
||||
claude --daemon-worker=<kind>
|
||||
# 查看状态(默认子命令)
|
||||
claude daemon status
|
||||
claude daemon ps
|
||||
|
||||
# 停止守护进程
|
||||
claude daemon stop
|
||||
|
||||
# 以 worker 身份启动(由 supervisor 自动调用)
|
||||
claude --daemon-worker=remoteControl
|
||||
|
||||
# 后台会话管理
|
||||
claude daemon bg
|
||||
claude daemon attach <session>
|
||||
claude daemon logs <session>
|
||||
claude daemon kill <session>
|
||||
```
|
||||
|
||||
### 2.3 预期架构
|
||||
### 2.3 架构
|
||||
|
||||
```
|
||||
Supervisor (daemonMain)
|
||||
│
|
||||
├── Worker 1: assistant-mode
|
||||
│ └── 接收和处理 assistant 会话
|
||||
│
|
||||
├── Worker 2: bridge-sync
|
||||
│ └── bridge 消息同步
|
||||
│
|
||||
└── Worker 3: proactive
|
||||
└── 主动任务执行
|
||||
├── Worker: remoteControl
|
||||
│ └── runBridgeHeadless() — 远程控制 headless 模式
|
||||
│ 接收远程会话、处理消息、权限审批
|
||||
│
|
||||
▼
|
||||
IPC via Unix Domain Sockets
|
||||
- 生命周期管理(启动、停止、重启)
|
||||
- 工作分发
|
||||
- 状态报告
|
||||
文件系统状态文件 (daemon-state.json)
|
||||
- PID、CWD、启动时间、Worker 类型
|
||||
- queryDaemonStatus() / stopDaemonByPid()
|
||||
```
|
||||
|
||||
### 2.4 与 BRIDGE_MODE 的关系
|
||||
### 2.4 Worker 生命周期管理
|
||||
|
||||
Supervisor 为每个 worker 实现:
|
||||
- **指数退避重启**:初始 2s,上限 120s,倍数 ×2
|
||||
- **快速失败检测**:10s 内连续崩溃 5 次则 parking(不再重启)
|
||||
- **永久错误退出码**:78 (EXIT_CODE_PERMANENT) 导致直接 parking
|
||||
- **优雅关闭**:SIGTERM/SIGINT → abort signal → 30s 强制 SIGKILL
|
||||
|
||||
### 2.5 与 BRIDGE_MODE 的关系
|
||||
|
||||
DAEMON 和 BRIDGE_MODE 常组合使用:
|
||||
|
||||
@@ -63,40 +79,39 @@ if (feature('DAEMON') && feature('BRIDGE_MODE')) {
|
||||
|
||||
双重门控:两个 feature 都需要开启才能使用远程控制服务器。
|
||||
|
||||
## 三、需要补全的内容
|
||||
|
||||
| 模块 | 工作量 | 说明 |
|
||||
|------|--------|------|
|
||||
| `daemon/main.ts` | 大 | Supervisor 主进程:启动 worker、生命周期管理、IPC |
|
||||
| `daemon/workerRegistry.ts` | 中 | Worker 类型分发(assistant/bridge-sync/proactive) |
|
||||
| Worker 实现 | 大 | 各类型 worker 的具体实现 |
|
||||
| IPC 协议 | 中 | Supervisor-Worker 通信层 |
|
||||
|
||||
## 四、关键设计决策
|
||||
## 三、关键设计决策
|
||||
|
||||
1. **多进程架构**:一个 supervisor + 多个 worker,进程隔离
|
||||
2. **Unix 域套接字 IPC**:本地进程间通信,低延迟
|
||||
2. **文件系统状态通信**:通过 `daemon-state.json` 文件进行状态共享(非 Unix 域套接字)
|
||||
3. **与 BRIDGE_MODE 强绑定**:守护进程最常见的用途是提供远程控制服务
|
||||
4. **CLI 子命令路由**:`daemon` 子命令和 `--daemon-worker` 参数在 `cli.tsx` 中路由
|
||||
5. **Worker 环境变量**:supervisor 通过环境变量(`DAEMON_WORKER_*`)向 worker 传递配置
|
||||
|
||||
## 五、使用方式
|
||||
## 四、使用方式
|
||||
|
||||
```bash
|
||||
# 启用守护进程模式
|
||||
FEATURE_DAEMON=1 FEATURE_BRIDGE_MODE=1 bun run dev
|
||||
|
||||
# 启动守护进程
|
||||
claude daemon
|
||||
claude daemon start
|
||||
|
||||
# 以特定 worker 启动
|
||||
claude --daemon-worker=assistant
|
||||
# 查看状态
|
||||
claude daemon status
|
||||
|
||||
# 停止守护进程
|
||||
claude daemon stop
|
||||
|
||||
# 以特定 worker 启动(通常由 supervisor 自动调用)
|
||||
claude --daemon-worker=remoteControl
|
||||
```
|
||||
|
||||
## 六、文件索引
|
||||
## 五、文件索引
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/daemon/main.ts` | Supervisor 主进程(stub) |
|
||||
| `src/daemon/workerRegistry.ts` | Worker 注册(stub) |
|
||||
| `src/entrypoints/cli.tsx:95,149` | CLI 路由 |
|
||||
| `src/commands.ts:77` | 命令注册(双重门控) |
|
||||
| `src/daemon/main.ts` | Supervisor 主进程:子命令分发、Worker 生命周期管理、退避重启 |
|
||||
| `src/daemon/workerRegistry.ts` | Worker 入口:remoteControl worker 实现 |
|
||||
| `src/daemon/state.ts` | Daemon 状态管理:PID 文件读写、状态查询 |
|
||||
| `src/entrypoints/cli.tsx` | CLI 路由 |
|
||||
| `src/commands.ts` | 命令注册(双重门控) |
|
||||
|
||||
@@ -27,13 +27,15 @@ bun run dev:inspect
|
||||
|
||||
## 原理
|
||||
|
||||
`dev:inspect` 脚本实际执行的是:
|
||||
`dev:inspect` 脚本实际执行的是 `scripts/dev-debug.ts`:
|
||||
|
||||
```bash
|
||||
bun --inspect-wait=localhost:8888/<token> run scripts/dev.ts
|
||||
```typescript
|
||||
// scripts/dev-debug.ts
|
||||
process.env.BUN_INSPECT = "localhost:8888/<token>"
|
||||
await import("./dev")
|
||||
```
|
||||
|
||||
Bun 的 `--inspect-wait` 参数启动一个 Chrome DevTools Protocol 兼容的 inspect 服务,等待调试器连接后才开始执行。VS Code 的 `bun` 扩展通过 WebSocket 连接到这个地址实现 attach。
|
||||
通过设置 `BUN_INSPECT` 环境变量启动一个 Chrome DevTools Protocol 兼容的 inspect 服务,然后导入 dev 模式入口。VS Code 的 `bun` 扩展通过 WebSocket 连接到输出的地址实现 attach。
|
||||
|
||||
## JetBrains IDE
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,160 +0,0 @@
|
||||
# Feature Flags 审查报告 — Codex 复核
|
||||
|
||||
> 审查日期: 2026-04-05
|
||||
> 审查工具: Codex CLI v0.118.0 (本地, full-auto mode)
|
||||
> 消耗 tokens: 240,306
|
||||
> 审查范围: docs/feature-flags-audit-complete.md 中标记为 COMPLETE 的 22 个编译时 feature flag
|
||||
|
||||
---
|
||||
|
||||
## 审查背景
|
||||
|
||||
原始审计报告 (`docs/feature-flags-audit-complete.md`) 声称 22 个 feature flag 被标记为 "COMPLETE",只需在 `build.ts` / `scripts/dev.ts` 中启用即可工作。
|
||||
|
||||
Claude Code 团队通过 6 个并行子代理实际读取源码后初步发现大量误判,随后将分析结果传递给 Codex CLI 进行独立二次验证。
|
||||
|
||||
---
|
||||
|
||||
## Codex 发现摘要
|
||||
|
||||
### High 级发现
|
||||
|
||||
1. **`CONTEXT_COLLAPSE` 不是 COMPLETE**
|
||||
- `src/services/contextCollapse/index.ts:43` — `isContextCollapseEnabled()` 硬编码为 `false`
|
||||
- `src/services/contextCollapse/index.ts:47` — `applyCollapsesIfNeeded()` 只是原样返回消息
|
||||
- `src/services/contextCollapse/index.ts:59` — `recoverFromOverflow()` 也是 no-op
|
||||
- `src/services/contextCollapse/operations.ts:3` 和 `persist.ts:3` 同样是 stub
|
||||
- 审计报告把 UI/命令文件算进去了,但真正被查询循环消费的是 stub 后端
|
||||
|
||||
2. **原分类"真正只需编译开关"的 7 个 flag,只有 3 个准确**
|
||||
- ✅ `SHOT_STATS` — 零额外门控,compile-only
|
||||
- ✅ `PROMPT_CACHE_BREAK_DETECTION` — 有 try-catch 兜底,compile-only
|
||||
- ✅ `TOKEN_BUDGET` — 纯本地计算,compile-only
|
||||
- ❌ `TEAMMEM` — 还要求 AutoMem + GrowthBook `tengu_herring_clock` + GitHub repo (`teamMemPaths.ts:73`, `watcher.ts:256`, `watcher.ts:259`)
|
||||
- ❌ `AGENT_TRIGGERS` — 受 `isKairosCronEnabled()` GrowthBook 控制 (`useScheduledTasks.ts:61`, `useScheduledTasks.ts:119`)
|
||||
- ❌ `EXTRACT_MEMORIES` — 受 `tengu_passport_quail` + AutoMem + 非 remote 限制 (`extractMemories.ts:536`, `:545`, `:550`)
|
||||
- ❌ `KAIROS_BRIEF` — 受 `tengu_kairos_brief` + opt-in/kairosActive 限制 (`BriefTool.ts:95`, `:126`, `:132`)
|
||||
|
||||
### Medium 级发现
|
||||
|
||||
3. **`BG_SESSIONS` 和 `BASH_CLASSIFIER` 不适合简单归为"全 stub"**
|
||||
- `BG_SESSIONS` — 会话注册/清理是真实现 (`concurrentSessions.ts:44`, `:55`),但任务摘要核心是 stub (`taskSummary.ts:2`)
|
||||
- `BASH_CLASSIFIER` — 权限编排很大一块是真实现 (`bashPermissions.ts` 2621行),但分类后端 `bashClassifier.ts:24` 永远返回 disabled
|
||||
|
||||
4. **审计口径问题**
|
||||
- 把"代码量/周边 UI 很多"误当成"可独立启用"
|
||||
- `PROACTIVE` — `index.ts:3` 只有 state stub,`commands.ts:64` 和 `REPL.tsx:415` 引用缺失文件
|
||||
- `REACTIVE_COMPACT` — `reactiveCompact.ts:13` 整块是 stub
|
||||
- `CACHED_MICROCOMPACT` — `cachedMicrocompact.ts:22` 全部 stub
|
||||
|
||||
---
|
||||
|
||||
## Codex 修正后的分类
|
||||
|
||||
### 第一类:真正 compile-only(3 个)
|
||||
|
||||
| Flag | 说明 | Crash 风险 |
|
||||
|------|------|-----------|
|
||||
| **SHOT_STATS** | 纯本地 shot 分布统计,ant-only 数据路径 | 低 |
|
||||
| **PROMPT_CACHE_BREAK_DETECTION** | 本地 cache key 变化检测,写 diff 有兜底 | 低 |
|
||||
| **TOKEN_BUDGET** | 本地 token 预算追踪,纯计算逻辑 | 低 |
|
||||
|
||||
### 第二类:compile + 运行时条件(7 个)
|
||||
|
||||
| Flag | 条件 | Crash 风险 |
|
||||
|------|------|-----------|
|
||||
| **TEAMMEM** | AutoMem + GrowthBook `tengu_herring_clock` + GitHub repo | 低 (clean no-op) |
|
||||
| **AGENT_TRIGGERS** | GrowthBook `isKairosCronEnabled()` | 低 (clean no-op) |
|
||||
| **EXTRACT_MEMORIES** | `tengu_passport_quail` + AutoMem + 非 remote | 低 (clean no-op) |
|
||||
| **KAIROS_BRIEF** | `tengu_kairos_brief` + opt-in/kairosActive,可用 `CLAUDE_CODE_BRIEF=1` 绕过 | 低 |
|
||||
| **COORDINATOR_MODE** | 需 `CLAUDE_CODE_COORDINATOR_MODE=1`,`workerAgent.ts` 是 stub 但不阻塞 | 低 |
|
||||
| **COMMIT_ATTRIBUTION** | 仅对 `isInternal=true` 的 repo 生效 | 低 |
|
||||
| **VERIFICATION_AGENT** | 受 GrowthBook `tengu_hive_evidence` 双重门控 | 低 |
|
||||
|
||||
### 第三类:混合型 — 部分实现 + stub 核心(5 个)
|
||||
|
||||
| Flag | 真实现部分 | Stub 核心 |
|
||||
|------|-----------|----------|
|
||||
| **BG_SESSIONS** | 会话注册/清理 (`concurrentSessions.ts`) | `bg.ts`/`taskSummary.ts`/`udsClient.ts` 全 stub + 依赖 tmux |
|
||||
| **BASH_CLASSIFIER** | 权限编排 (`bashPermissions.ts` 2621行) | `bashClassifier.ts` 分类后端 stub + 需 API beta |
|
||||
| **PROACTIVE** | REPL/命令注册框架 | `index.ts` stub + 3 文件缺失 |
|
||||
| **REACTIVE_COMPACT** | 调用点已在主查询环路 | `reactiveCompact.ts` 22行全 no-op |
|
||||
| **CACHED_MICROCOMPACT** | 调用点已布线 | `cachedMicrocompact.ts` 全 stub + 需未公开 API |
|
||||
|
||||
### 第四类:纯 stub(1 个)
|
||||
|
||||
| Flag | 问题 |
|
||||
|------|------|
|
||||
| **CONTEXT_COLLAPSE** | 3 核心文件全 stub + CtxInspectTool 目录不存在 |
|
||||
|
||||
### 第五类:依赖远程服务(3 个)
|
||||
|
||||
| Flag | 依赖 |
|
||||
|------|------|
|
||||
| **ULTRAPLAN** | CCR 远程 agent 基础设施 + OAuth |
|
||||
| **CCR_REMOTE_SETUP** | claude.ai OAuth + GitHub CLI + CCR 后端 |
|
||||
| **BRIDGE_MODE** (build端) | claude.ai 订阅 + GrowthBook + WebSocket 后端 |
|
||||
|
||||
---
|
||||
|
||||
## 第三类恢复优先级建议
|
||||
|
||||
Codex 推荐的恢复顺序:
|
||||
|
||||
1. **REACTIVE_COMPACT** — 收益最直接,调用点在主查询环路,改完最容易立刻见效
|
||||
2. **BG_SESSIONS** — 已有会话注册基础,补齐摘要和后台运行链路的 ROI 高
|
||||
3. **PROACTIVE** — 产品面大,但缺文件比 stub 更严重,范围比前两项大
|
||||
4. **CONTEXT_COLLAPSE** — collapse engine 全 stub,恢复成本和设计不确定性都高
|
||||
5. **BASH_CLASSIFIER** — 若无 API beta 能力不值得优先;若有则升到第 2
|
||||
6. **CACHED_MICROCOMPACT** — 受未公开 API 约束,最后做
|
||||
|
||||
---
|
||||
|
||||
## 审计报告分类标准修正建议
|
||||
|
||||
Codex 建议将原来的单轴分类(COMPLETE/PARTIAL/STUB)改为**三轴**:
|
||||
|
||||
| 轴 | 取值 | 说明 |
|
||||
|----|------|------|
|
||||
| **实现完整度** | `full` / `mixed` / `stub` | 活跃调用链上的核心模块是否有真实现 |
|
||||
| **激活条件** | `compile-only` / `compile+env` / `compile+GrowthBook` / `compile+remote` / `compile+private API` | 启用需要什么 |
|
||||
| **运行风险** | `safe no-op` / `background IO` / `startup critical` | 启用后条件不满足时的行为 |
|
||||
|
||||
**COMPLETE 的最低标准应满足:**
|
||||
1. 活跃调用链上的核心模块不能是 stub
|
||||
2. "可启用"不能只看编译 flag,还要单列运行时 gate
|
||||
|
||||
按此标准,`CONTEXT_COLLAPSE`、`BG_SESSIONS`、`BASH_CLASSIFIER`、`PROACTIVE`、`REACTIVE_COMPACT`、`CACHED_MICROCOMPACT` 都应从 COMPLETE 降级。
|
||||
|
||||
---
|
||||
|
||||
## 已采取的行动
|
||||
|
||||
基于审查结果,已将以下 3 个确认安全的 flag 加入默认构建:
|
||||
|
||||
**build.ts:**
|
||||
```typescript
|
||||
const DEFAULT_BUILD_FEATURES = [
|
||||
"AGENT_TRIGGERS_REMOTE", "CHICAGO_MCP", "VOICE_MODE",
|
||||
"SHOT_STATS", "PROMPT_CACHE_BREAK_DETECTION", "TOKEN_BUDGET"
|
||||
];
|
||||
```
|
||||
|
||||
**scripts/dev.ts:**
|
||||
```typescript
|
||||
const DEFAULT_FEATURES = [
|
||||
"BUDDY", "TRANSCRIPT_CLASSIFIER", "BRIDGE_MODE",
|
||||
"AGENT_TRIGGERS_REMOTE", "CHICAGO_MCP", "VOICE_MODE",
|
||||
"SHOT_STATS", "PROMPT_CACHE_BREAK_DETECTION", "TOKEN_BUDGET"
|
||||
];
|
||||
```
|
||||
|
||||
### 验证结果
|
||||
|
||||
| 项目 | 结果 |
|
||||
|------|------|
|
||||
| `bun run build` | ✅ 成功 (475 files) |
|
||||
| `bun test` | ✅ 无新增失败 (23 fail 为已有问题) |
|
||||
| SHOT_STATS 代码路径 | ✅ 完整 — stats 面板显示 shot 分布 |
|
||||
| TOKEN_BUDGET 代码路径 | ✅ 完整 — 支持 `+500k` 语法,带进度条 |
|
||||
| PROMPT_CACHE_BREAK_DETECTION 代码路径 | ✅ 完整 — 内部诊断,debug 模式可见 |
|
||||
@@ -37,7 +37,7 @@ Agent({ subagent_type: "general-purpose", prompt: "..." })
|
||||
|
||||
### 3.1 门控与互斥
|
||||
|
||||
文件:`src/tools/AgentTool/forkSubagent.ts:32-39`
|
||||
文件:`packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts:32-39`
|
||||
|
||||
```ts
|
||||
export function isForkSubagentEnabled(): boolean {
|
||||
@@ -105,7 +105,7 @@ isForkSubagentEnabled() && !subagent_type?
|
||||
|
||||
### 3.4 消息构建:buildForkedMessages
|
||||
|
||||
文件:`src/tools/AgentTool/forkSubagent.ts:107-169`
|
||||
文件:`packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts:107-169`
|
||||
|
||||
构建的消息结构:
|
||||
|
||||
@@ -185,11 +185,11 @@ FEATURE_FORK_SUBAGENT=1 bun run dev
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/tools/AgentTool/forkSubagent.ts` | ~210 | 核心定义 + 消息构建 + 递归防护 |
|
||||
| `src/tools/AgentTool/AgentTool.tsx` | — | Fork 路由 + 强制异步 |
|
||||
| `src/tools/AgentTool/prompt.ts` | — | "When to Fork" 提示词段落 |
|
||||
| `src/tools/AgentTool/runAgent.ts` | — | useExactTools 路径 |
|
||||
| `src/tools/AgentTool/resumeAgent.ts` | — | Fork agent 恢复 |
|
||||
| `packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts` | ~210 | 核心定义 + 消息构建 + 递归防护 |
|
||||
| `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` | — | Fork 路由 + 强制异步 |
|
||||
| `packages/builtin-tools/src/tools/AgentTool/prompt.ts` | — | "When to Fork" 提示词段落 |
|
||||
| `packages/builtin-tools/src/tools/AgentTool/runAgent.ts` | — | useExactTools 路径 |
|
||||
| `packages/builtin-tools/src/tools/AgentTool/resumeAgent.ts` | — | Fork agent 恢复 |
|
||||
| `src/constants/xml.ts` | — | XML 标签常量 |
|
||||
| `src/utils/forkedAgent.ts` | — | CacheSafeParams + ContentReplacementState 克隆 |
|
||||
| `src/commands/fork/index.ts` | — | /fork 命令(stub) |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# KAIROS — 常驻助手模式
|
||||
|
||||
> Feature Flag: `FEATURE_KAIROS=1`(及子 Feature)
|
||||
> 实现状态:核心框架完整,部分子模块为 stub
|
||||
> 实现状态:核心框架完整,部分子模块为 stub;proactive/sleep 节奏控制已可用
|
||||
> 引用数:154(全库最大)
|
||||
|
||||
## 一、功能概述
|
||||
@@ -34,13 +34,13 @@ KAIROS 在系统提示中注入两大段落:
|
||||
|
||||
### 2.1 Brief 段落 (`getBriefSection`)
|
||||
|
||||
文件:`src/constants/prompts.ts:843-858`
|
||||
文件:`src/constants/prompts.ts:847-858`
|
||||
|
||||
当 `feature('KAIROS') || feature('KAIROS_BRIEF')` 时注入。Brief 工具(`SendUserMessage`)的结构化消息输出指令。`/brief` toggle 和 `--brief` flag 只控制显示过滤,不影响模型行为。
|
||||
|
||||
### 2.2 Proactive/Autonomous Work 段落 (`getProactiveSection`)
|
||||
|
||||
文件:`src/constants/prompts.ts:860-914`
|
||||
文件:`src/constants/prompts.ts:864-918`
|
||||
|
||||
当 `feature('PROACTIVE') || feature('KAIROS')` 且 `isProactiveActive()` 时注入。核心行为指令:
|
||||
|
||||
@@ -74,8 +74,9 @@ KAIROS 在系统提示中注入两大段落:
|
||||
|
||||
SleepTool 是 KAIROS/Proactive 的节奏控制核心。工具描述让模型理解"休眠"概念:
|
||||
- 工具名:`Sleep`
|
||||
- 功能:等待指定时间后响应 tick prompt
|
||||
- 功能:等待指定时间后响应 tick prompt;若队列出现新工作或 proactive 被关闭,会提前唤醒
|
||||
- 与 `<tick_tag>` 配合实现心跳式自主工作
|
||||
- 远程控制 surfaces 可通过 `automation_state` 看到 `standby` / `sleeping` 两种状态
|
||||
|
||||
### 3.3 Bridge 集成
|
||||
|
||||
@@ -172,8 +173,10 @@ FEATURE_KAIROS=1 FEATURE_TOKEN_BUDGET=1 bun run dev
|
||||
| `src/assistant/AssistantSessionChooser.ts` | — | Session 选择 UI(stub) |
|
||||
| `src/tools/BriefTool/` | — | BriefTool 实现(stub) |
|
||||
| `src/tools/SleepTool/prompt.ts` | ~30 | SleepTool 工具提示 |
|
||||
| `src/tools/SleepTool/SleepTool.ts` | ~200 | 休眠/唤醒与 automation metadata |
|
||||
| `src/services/mcp/channelNotification.ts` | 5 | 频道消息接入(stub) |
|
||||
| `src/memdir/memdir.ts` | — | 记忆目录管理(stub) |
|
||||
| `src/constants/prompts.ts:552-554,843-914` | 72 | 系统提示注入 |
|
||||
| `src/constants/prompts.ts:557,847-918` | 72 | 系统提示注入 |
|
||||
| `src/components/tasks/src/tasks/DreamTask/` | 3 | Dream 任务(stub) |
|
||||
| `src/proactive/index.ts` | — | Proactive 核心(stub,KAIROS 共享) |
|
||||
| `src/proactive/index.ts` | — | Proactive 核心(KAIROS 共享) |
|
||||
| `src/utils/sessionState.ts` | — | 向 bridge/CCR 暴露 automation 状态 |
|
||||
|
||||
@@ -281,7 +281,7 @@ CLI-B (192.168.50.27) 心跳循环
|
||||
|
||||
## SendMessageTool TCP 支持
|
||||
|
||||
`src/tools/SendMessageTool/SendMessageTool.ts`
|
||||
`packages/builtin-tools/src/tools/SendMessageTool/SendMessageTool.ts`
|
||||
|
||||
- `to` 字段支持 `tcp:host:port` 格式
|
||||
- `checkPermissions`:`tcp:` scheme 返回 `behavior: 'ask'`,`classifierApprovable: false`
|
||||
|
||||
@@ -202,4 +202,4 @@ docker run -d \
|
||||
| `src/services/langfuse/__tests__/langfuse.test.ts` | 测试(568 行) |
|
||||
| `src/query.ts` | 主查询流程中的 Trace 集成 |
|
||||
| `src/services/tools/toolExecution.ts` | 工具执行中的观察记录 |
|
||||
| `src/tools/AgentTool/runAgent.ts` | 子 Agent Trace 创建 |
|
||||
| `packages/builtin-tools/src/tools/AgentTool/runAgent.ts` | 子 Agent Trace 创建 |
|
||||
|
||||
@@ -41,7 +41,7 @@ getMcpSkillCommands() 过滤 → SkillTool 调用
|
||||
|
||||
### 2.2 技能筛选
|
||||
|
||||
文件:`src/commands.ts:547-558`
|
||||
文件:`src/commands.ts:604-616`
|
||||
|
||||
`getMcpSkillCommands(mcpCommands)` 过滤条件:
|
||||
|
||||
@@ -54,7 +54,7 @@ feature('MCP_SKILLS') // feature flag 必须开启
|
||||
|
||||
### 2.3 条件加载
|
||||
|
||||
文件:`src/services/mcp/client.ts:117-121`
|
||||
文件:`src/services/mcp/client.ts:129-133`
|
||||
|
||||
`fetchMcpSkillsForClient` 通过 `require()` 条件加载,feature flag 关闭时不加载任何模块:
|
||||
|
||||
@@ -79,8 +79,8 @@ const fetchMcpSkillsForClient = feature('MCP_SKILLS')
|
||||
|
||||
| 文件 | 行 | 说明 |
|
||||
|------|------|------|
|
||||
| `src/commands.ts` | 547-558, 561-608 | 命令过滤和 SkillTool 命令收集 |
|
||||
| `src/services/mcp/client.ts` | 117-121, 1394, 1672, 2173-2181, 2346-2358 | 技能获取、缓存清除、连接时获取 |
|
||||
| `src/commands.ts` | 604-616, 620-633 | 命令过滤和 SkillTool 命令收集 |
|
||||
| `src/services/mcp/client.ts` | 129-133, 1394, 1672, 2176 | 技能获取、缓存清除、连接时获取 |
|
||||
| `src/services/mcp/useManageMCPConnections.ts` | 22-26, 682-740 | 实时刷新(prompts/resources 变化) |
|
||||
|
||||
## 三、关键设计决策
|
||||
|
||||
@@ -145,8 +145,8 @@ M 键(或 ← / →)用于在两种路由模式之间切换,**无需展开
|
||||
|
||||
```
|
||||
/pipes — 显示所有实例 + 切换选择面板
|
||||
/pipes select <name> — 选中某实例(消息会广播到它)
|
||||
/pipes deselect <name> — 取消选中
|
||||
/pipes select <name> — 选中某实例(消息会广播到它)
|
||||
/pipes deselect <name> — 取消选中
|
||||
/pipes all — 全选
|
||||
/pipes none — 全部取消
|
||||
```
|
||||
@@ -169,7 +169,7 @@ LAN Peers:
|
||||
Selected: cli-da029538
|
||||
```
|
||||
|
||||
### /attach <name>
|
||||
### /attach <name>
|
||||
|
||||
手动 attach 到一个实例,使其成为你的 slave。
|
||||
|
||||
@@ -179,7 +179,7 @@ Selected: cli-da029538
|
||||
|
||||
attach 后,对方变为 slave,你变为 master。可以向它发送 prompt。通常不需要手动 attach——heartbeat 会自动发现并连接。
|
||||
|
||||
### /detach <name>
|
||||
### /detach <name>
|
||||
|
||||
断开与某个 slave 的连接。
|
||||
|
||||
@@ -187,7 +187,7 @@ attach 后,对方变为 slave,你变为 master。可以向它发送 prompt
|
||||
/detach cli-04d67950
|
||||
```
|
||||
|
||||
### /send <name> <message>
|
||||
### /send <name> <message>
|
||||
|
||||
向指定 pipe 发送消息(不依赖选择状态,直接指定目标)。
|
||||
|
||||
@@ -318,7 +318,7 @@ sub 角色:
|
||||
| `src/commands/pipes/pipes.ts` | /pipes 命令 |
|
||||
| `src/commands/attach/attach.ts` | /attach 命令 |
|
||||
| `src/commands/send/send.ts` | /send 命令 |
|
||||
| `src/tools/SendMessageTool/SendMessageTool.ts` | AI 发消息工具(含 tcp: 支持) |
|
||||
| `packages/builtin-tools/src/tools/SendMessageTool/SendMessageTool.ts` | AI 发消息工具(含 tcp: 支持) |
|
||||
|
||||
## 后续优化方向
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# PROACTIVE — 主动模式
|
||||
|
||||
> Feature Flag: `FEATURE_PROACTIVE=1`(与 `FEATURE_KAIROS=1` 共享功能)
|
||||
> 实现状态:核心模块全部 Stub,布线完整
|
||||
> 实现状态:核心循环与 SleepTool 已落地,部分外围文档仍在补齐
|
||||
> 引用数:37
|
||||
|
||||
## 一、功能概述
|
||||
@@ -21,13 +21,13 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
|
||||
|
||||
| 模块 | 文件 | 状态 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 核心逻辑 | `src/proactive/index.ts` | **Stub** | `activateProactive()`、`deactivateProactive()`、`isProactiveActive() => false` |
|
||||
| 核心逻辑 | `src/proactive/index.ts` | **已实现** | `activateProactive()`、`deactivateProactive()`、`pause/resume`、`nextTickAt` 调度状态 |
|
||||
| SleepTool 提示 | `src/tools/SleepTool/prompt.ts` | **完整** | 工具提示定义(工具名:`Sleep`) |
|
||||
| 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` |
|
||||
| 工具注册 | `src/tools.ts:26-28` | **布线** | SleepTool 动态加载 |
|
||||
| REPL 集成 | `src/screens/REPL.tsx` | **布线** | tick 驱动逻辑、占位符、页脚 UI |
|
||||
| 系统提示 | `src/constants/prompts.ts:860-914` | **完整** | 自主工作行为指令(~55 行详细 prompt) |
|
||||
| 会话存储 | `src/utils/sessionStorage.ts:4892-4912` | **布线** | tick 消息注入对话流 |
|
||||
| REPL 集成 | `src/screens/REPL.tsx` | **已实现** | tick 驱动、standby/sleeping 状态、页脚与 bridge automation metadata 上报 |
|
||||
| 系统提示 | `src/constants/prompts.ts:864-918` | **完整** | 自主工作行为指令(~55 行详细 prompt) |
|
||||
| 远控状态镜像 | `src/utils/sessionState.ts` | **已实现** | 向 remote-control/CCR 暴露 `automation_state` 元数据 |
|
||||
|
||||
### 2.2 系统提示内容
|
||||
|
||||
@@ -46,7 +46,7 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
|
||||
### 2.3 数据流
|
||||
|
||||
```
|
||||
activateProactive() [需要实现]
|
||||
activateProactive()
|
||||
│
|
||||
▼
|
||||
Tick 调度器启动
|
||||
@@ -62,20 +62,22 @@ Tick 调度器启动
|
||||
└── 无事可做 → 必须调用 SleepTool
|
||||
│
|
||||
▼
|
||||
SleepTool 等待 [需要实现]
|
||||
SleepTool 等待
|
||||
│
|
||||
├── 用户插入新工作 / 队列中有命令 → 立即唤醒
|
||||
├── proactive 被关闭 → 立即中断
|
||||
└── 进入休眠时向远端 surfaces 上报 `automation_state = sleeping`
|
||||
│
|
||||
▼
|
||||
下一个 tick 到达
|
||||
```
|
||||
|
||||
## 三、需要补全的内容
|
||||
## 三、当前行为补充
|
||||
|
||||
| 优先级 | 模块 | 工作量 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| 1 | `src/proactive/index.ts` | 中 | Tick 调度器、activate/deactivate 状态机、pause/resume |
|
||||
| 2 | `src/tools/SleepTool/SleepTool.ts` | 小 | 工具执行(等待指定时间后触发 tick) |
|
||||
| 3 | `src/commands/proactive.js` | 小 | `/proactive` 斜杠命令处理器 |
|
||||
| 4 | `src/hooks/useProactive.ts` | 中 | React hook(REPL 引用但不存在) |
|
||||
- `standby`:proactive 已开启,当前没有执行中的 turn,且已调度下一个 tick。
|
||||
- `sleeping`:模型显式调用 `SleepTool` 进入等待窗口。
|
||||
- remote-control/CCR 通过 `external_metadata.automation_state` 接收这两个状态,用于 Web UI 的 Autopilot 状态显示。
|
||||
- `SleepTool` 现在不是纯定时器;它会在共享命令队列出现新工作时提前醒来。
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
@@ -101,9 +103,11 @@ FEATURE_PROACTIVE=1 FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 bun run dev
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/proactive/index.ts` | 核心逻辑(stub) |
|
||||
| `src/proactive/index.ts` | 核心逻辑与 next-tick 状态 |
|
||||
| `src/tools/SleepTool/prompt.ts` | SleepTool 工具提示 |
|
||||
| `src/constants/prompts.ts:860-914` | 自主工作系统提示 |
|
||||
| `src/screens/REPL.tsx` | REPL tick 集成 |
|
||||
| `src/tools/SleepTool/SleepTool.ts` | 休眠/唤醒执行逻辑 |
|
||||
| `src/constants/prompts.ts:864-918` | 自主工作系统提示 |
|
||||
| `src/screens/REPL.tsx` | REPL tick 集成与 automation 状态上报 |
|
||||
| `src/utils/sessionStorage.ts:4892-4912` | Tick 消息注入 |
|
||||
| `src/utils/sessionState.ts` | bridge/CCR metadata 镜像 |
|
||||
| `src/components/PromptInput/PromptInputFooterLeftSide.tsx` | 页脚 UI 状态 |
|
||||
|
||||
@@ -13,17 +13,22 @@
|
||||
┌──────────────────┐ HTTP/SSE │ │ In-Memory │ │
|
||||
│ Web UI 控制面板 │ ◄─────────────── │ │ Store │ │
|
||||
│ (/code/*) │ │ └──────────────┘ │
|
||||
└──────────────────┘ │ ┌──────────────┐ │
|
||||
│ │ JWT Auth │ │
|
||||
│ (React + Vite) │ │ ┌──────────────┐ │
|
||||
└──────────────────┘ │ │ JWT Auth │ │
|
||||
│ └──────────────┘ │
|
||||
└──────────────────────┘
|
||||
┌──────────────────┐ │ ┌──────────────┐ │
|
||||
│ acp-link │ ◄── ACP Relay ─── │ │ ACP Handler │ │
|
||||
│ + ACP Agent │ WebSocket │ └──────────────┘ │
|
||||
└──────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
**RCS 是一个纯内存的中间服务**,它的职责是:
|
||||
- 接收 Claude Code CLI 的环境注册和工作轮询
|
||||
- 接收 acp-link 的 ACP agent 注册,支持 WebSocket relay 桥接
|
||||
- 提供 Web UI 供操作者远程监控和审批
|
||||
- 通过 WebSocket/SSE 双向传输消息
|
||||
- 管理会话、环境、权限请求
|
||||
- 提供 ACP SSE event stream 供外部消费者订阅 channel group 事件
|
||||
|
||||
## 前置条件
|
||||
|
||||
@@ -99,6 +104,8 @@ docker compose up -d
|
||||
| `RCS_HEARTBEAT_INTERVAL` | 否 | `20` | 心跳间隔(秒) |
|
||||
| `RCS_JWT_EXPIRES_IN` | 否 | `3600` | JWT 令牌有效期(秒) |
|
||||
| `RCS_DISCONNECT_TIMEOUT` | 否 | `300` | 断线判定超时(秒) |
|
||||
| `RCS_WS_IDLE_TIMEOUT` | 否 | `30` | WebSocket 空闲超时(秒),Bun 发送协议级 ping |
|
||||
| `RCS_WS_KEEPALIVE_INTERVAL` | 否 | `20` | 服务端→客户端 keep_alive 帧间隔(秒),防止反向代理关闭空闲连接 |
|
||||
|
||||
### 客户端(Claude Code CLI)
|
||||
|
||||
@@ -169,15 +176,74 @@ claude bridge
|
||||
|
||||
## Web UI 控制面板
|
||||
|
||||
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。功能:
|
||||
通过 `/remote-control` 命令获取 URL 后,在浏览器打开即可使用。
|
||||
|
||||
- 查看已注册的运行环境(environment 模式)
|
||||
### 技术栈(v2,2026-04-18 重构)
|
||||
|
||||
Web UI 已从原生 JS 重构为 **React + Vite + Radix UI**:
|
||||
|
||||
- **框架**: React 19 + Vite 构建,TypeScript
|
||||
- **UI 组件**: Radix UI primitives(Dialog、Tabs、Select、Popover 等)
|
||||
- **聊天组件**: 完整的 ACP 聊天界面,支持 Plan 可视化、工具调用展示、权限审批
|
||||
- **AI Elements**: 独立的 AI 交互组件库(message、reasoning、tool、code-block、prompt-input 等)
|
||||
- **ACP 直连**: 支持 QR 码扫描自动跳转 ACP 直连视图(`ACPDirectView`)
|
||||
- **主题系统**: 暗色/亮色主题切换,遵循 Impeccable 设计系统
|
||||
|
||||
### 功能
|
||||
|
||||
- 查看已注册的运行环境(environment 模式),区分 ACP Agent 和 Claude Code 类型
|
||||
- 创建和管理会话
|
||||
- 实时查看对话消息和工具调用
|
||||
- 查看 Autopilot 状态(`standby` / `sleeping`)和自动运行指示
|
||||
- 查看 authoritative task snapshots 驱动的 Tasks 面板
|
||||
- 审批 Claude Code 的工具权限请求
|
||||
- 权限模式选择器(6 种模式:默认/自动接受编辑/跳过权限/规划/不询问/自动判断)
|
||||
- 模型选择器(可选可用模型)
|
||||
- Plan 可视化(进度条、状态图标、优先级标签)
|
||||
- ACP QR 扫描自动跳转到 ACP 聊天界面
|
||||
|
||||
Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境。
|
||||
|
||||
## ACP 支持
|
||||
|
||||
RCS 支持 ACP (Agent Client Protocol) agent 通过 `acp-link` 包接入。
|
||||
|
||||
### 架构
|
||||
|
||||
```
|
||||
acp-link ──REST注册──► RCS POST /v1/environments/bridge
|
||||
acp-link ──WS identify──► RCS WebSocket (携带 agentId)
|
||||
acp-link ◄──ACP relay──► RCS ◄──Web UI WS──► 浏览器
|
||||
```
|
||||
|
||||
### 后端组件
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/routes/acp/index.ts` | ACP REST 路由:agents 列表、channel groups、relay |
|
||||
| `src/transport/acp-ws-handler.ts` | ACP WebSocket 处理:agent 注册、心跳、消息转发 |
|
||||
| `src/transport/acp-relay-handler.ts` | 前端 WS → acp-link 透传 + EventBus inbound 转发 |
|
||||
| `src/transport/acp-sse-writer.ts` | SSE event stream 供外部消费者订阅 |
|
||||
|
||||
ACP 的 agents、channel groups、relay 和 channel-group SSE 端点都要求有效
|
||||
API key。浏览器 `EventSource` 不能发送 `Authorization` header,外部订阅
|
||||
`/acp/channel-groups/:id/events` 时需要使用 `fetch` + `ReadableStream` 并带
|
||||
`Authorization: Bearer <api-key>`。
|
||||
|
||||
### acp-link 连接
|
||||
|
||||
详见 [acp-link 文档](./acp-link.md)。
|
||||
|
||||
```bash
|
||||
# 在 RCS 环境中启动 acp-link
|
||||
# 注意:claude 本身不支持 ACP,需要用 ccb-bun --acp
|
||||
ACP_RCS_URL=http://localhost:3000 \
|
||||
ACP_RCS_TOKEN=sk-rcs-your-key \
|
||||
acp-link ccb-bun -- --acp
|
||||
```
|
||||
|
||||
ACP session 在 Web UI 中显示品牌色标签,与普通 Claude Code session 区分。
|
||||
|
||||
## 工作流程详解
|
||||
|
||||
```
|
||||
@@ -215,6 +281,7 @@ Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境
|
||||
9. 双向通信
|
||||
CLI ──消息/工具调用结果──► RCS ──► Browser
|
||||
CLI ◄──权限审批/指令───── RCS ◄──── Browser
|
||||
CLI ──automation_state / task_state──► RCS ──► Browser
|
||||
|
||||
10. 心跳保活(每 20 秒)
|
||||
CLI ──POST /v1/environments/:id/work/:workId/heartbeat──► RCS
|
||||
@@ -224,6 +291,13 @@ Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境
|
||||
|
||||
## 故障排查
|
||||
|
||||
### Web UI 看不到当前 Autopilot 状态
|
||||
|
||||
- `standby`:proactive 已开启,正在等待下一个 tick
|
||||
- `sleeping`:模型正在 `SleepTool` 等待窗口中
|
||||
|
||||
这两个状态通过 worker `external_metadata.automation_state` 上报。如果页面只显示普通 working spinner,优先检查 CLI 和 RCS 之间的 worker metadata PUT 是否成功。
|
||||
|
||||
### CLI 无法连接
|
||||
|
||||
```
|
||||
|
||||
426
docs/features/ssh-remote.md
Normal file
426
docs/features/ssh-remote.md
Normal file
@@ -0,0 +1,426 @@
|
||||
# SSH Remote — 远程主机运行 Claude Code
|
||||
|
||||
## 概述
|
||||
|
||||
SSH Remote 提供两种方式在远程 Linux 主机上运行 Claude Code:
|
||||
|
||||
1. **SSH Remote 模块**(`ccb ssh <host>`)— 本地 REPL + 远程工具执行,自动部署二进制 + 认证隧道
|
||||
2. **直接 SSH 运行**(`ssh <host> -t ccb`)— 远程已安装 ccb,直接启动交互式会话
|
||||
|
||||
## 架构
|
||||
|
||||
### 方式一:SSH Remote 模块(完整模式)
|
||||
|
||||
适用场景:远端没有 API 凭据或没有安装 ccb。
|
||||
|
||||
```
|
||||
┌──────────────── 本地 Windows/Mac/Linux ───────────┐
|
||||
│ │
|
||||
│ ccb ssh <host> [dir] │
|
||||
│ │ │
|
||||
│ ├── 1. SSHProbe: 探测远端平台/架构/已有二进制 │
|
||||
│ ├── 2. SSHDeploy: 部署 dist/ 到远端 │
|
||||
│ ├── 3. SSHAuthProxy: 启动本地认证代理 │
|
||||
│ │ ├─ Unix Socket (Linux/Mac) │
|
||||
│ │ └─ TCP 127.0.0.1:<port> (Windows) │
|
||||
│ │ │
|
||||
│ └── 4. SSH -R 反向隧道 + 启动远端 CLI │
|
||||
│ ssh -R <remote>:<local> <host> \ │
|
||||
│ ANTHROPIC_BASE_URL=... \ │
|
||||
│ ANTHROPIC_AUTH_NONCE=... \ │
|
||||
│ ccb --output-format stream-json │
|
||||
│ │
|
||||
│ ┌─────── 本地 REPL (Ink TUI) ───────┐ │
|
||||
│ │ 用户输入 → NDJSON → SSH stdin │ │
|
||||
│ │ SSH stdout → NDJSON → 渲染消息 │ │
|
||||
│ │ 工具权限请求 → 本地审批 → 回传 │ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
└────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ SSH 连接 (加密通道)
|
||||
│
|
||||
┌───────────────── 远端 Linux ──────────────────────┐
|
||||
│ │
|
||||
│ ccb (自动部署或已存在) │
|
||||
│ ├── --output-format stream-json │
|
||||
│ ├── --input-format stream-json │
|
||||
│ ├── --verbose -p │
|
||||
│ │ │
|
||||
│ ├── API 请求 → ANTHROPIC_BASE_URL │
|
||||
│ │ → SSH 反向隧道 → 本地 AuthProxy │
|
||||
│ │ → 注入真实凭据 → api.anthropic.com │
|
||||
│ │ │
|
||||
│ └── 工具执行 (Bash/Read/Write/...) │
|
||||
│ 直接在远端文件系统上操作 │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 方式二:直接 SSH 运行(简单模式)
|
||||
|
||||
适用场景:远端已安装 ccb 且已有 API 凭据(订阅或 API Key)。
|
||||
|
||||
```
|
||||
┌─────── 本地终端 ───────┐ ┌──────── 远端 Linux ────────┐
|
||||
│ │ SSH │ │
|
||||
│ ssh <host> -t ccb │ ──────→ │ ccb (全局安装) │
|
||||
│ │ │ ├── 使用远端自身凭据 │
|
||||
│ 终端直接显示远端 TUI │ ←────── │ ├── 远端文件系统操作 │
|
||||
│ │ TTY │ └── API 直连 Anthropic │
|
||||
└─────────────────────────┘ └─────────────────────────────┘
|
||||
```
|
||||
|
||||
### 适用场景对比
|
||||
|
||||
| | SSH Remote 模块 | 直接 SSH 运行 |
|
||||
|---|---|---|
|
||||
| 远端需要安装 ccb | 不需要(自动部署) | 需要 |
|
||||
| 远端需要 API 凭据 | 不需要(本地隧道) | 需要 |
|
||||
| 本地需要安装 ccb | 需要 | 不需要(任何终端) |
|
||||
| 斜杠命令 | 本地处理 | 远端处理 |
|
||||
| 网络延迟敏感 | 高(NDJSON 双向) | 低(仅 TTY) |
|
||||
| 推荐场景 | 远端无凭据/无安装 | 远端已配置完整 |
|
||||
|
||||
---
|
||||
|
||||
## 前置准备:SSH 密钥配置
|
||||
|
||||
两种方式都依赖 SSH 免密连接。以下是完整的密钥配置步骤。
|
||||
|
||||
### 1. 生成 SSH 密钥对(本地)
|
||||
|
||||
```bash
|
||||
# 生成 Ed25519 密钥(推荐)
|
||||
ssh-keygen -t ed25519 -C "your-email@example.com" -f ~/.ssh/id_remote
|
||||
|
||||
# 或 RSA 4096 位
|
||||
ssh-keygen -t rsa -b 4096 -C "your-email@example.com" -f ~/.ssh/id_remote
|
||||
```
|
||||
|
||||
生成两个文件:
|
||||
- `~/.ssh/id_remote` — 私钥(不可泄露)
|
||||
- `~/.ssh/id_remote.pub` — 公钥(部署到远端)
|
||||
|
||||
### 2. 将公钥部署到远端
|
||||
|
||||
```bash
|
||||
# 方式 A:ssh-copy-id(推荐)
|
||||
ssh-copy-id -i ~/.ssh/id_remote.pub user@remote-host
|
||||
|
||||
# 方式 B:手动复制
|
||||
cat ~/.ssh/id_remote.pub | ssh user@remote-host "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
|
||||
```
|
||||
|
||||
### 3. 配置 SSH Config(本地)
|
||||
|
||||
编辑 `~/.ssh/config`(不存在则创建):
|
||||
|
||||
```
|
||||
Host my-server
|
||||
HostName 192.168.1.100 # 远端 IP 或域名
|
||||
User root # 远端用户名
|
||||
IdentityFile ~/.ssh/id_remote # 私钥路径
|
||||
ServerAliveInterval 60 # 防止连接超时断开
|
||||
ServerAliveCountMax 3
|
||||
```
|
||||
|
||||
配置后可直接用别名连接:
|
||||
|
||||
```bash
|
||||
ssh my-server # 等同于 ssh -i ~/.ssh/id_remote root@192.168.1.100
|
||||
```
|
||||
|
||||
### 4. 文件权限设置
|
||||
|
||||
#### Linux / macOS
|
||||
|
||||
```bash
|
||||
chmod 700 ~/.ssh
|
||||
chmod 600 ~/.ssh/config
|
||||
chmod 600 ~/.ssh/id_remote
|
||||
chmod 644 ~/.ssh/id_remote.pub
|
||||
```
|
||||
|
||||
#### Windows(OpenSSH 强制 ACL 检查)
|
||||
|
||||
```powershell
|
||||
# 重置 .ssh 目录权限:仅允许当前用户 + SYSTEM
|
||||
icacls "$env:USERPROFILE\.ssh" /inheritance:r /grant:r "$($env:USERNAME):(OI)(CI)F" /grant "SYSTEM:(OI)(CI)F"
|
||||
|
||||
# 修复 config 文件权限
|
||||
icacls "$env:USERPROFILE\.ssh\config" /inheritance:r /grant:r "$($env:USERNAME):F" /grant "SYSTEM:F"
|
||||
|
||||
# 修复私钥权限
|
||||
icacls "$env:USERPROFILE\.ssh\id_remote" /inheritance:r /grant:r "$($env:USERNAME):F" /grant "SYSTEM:F"
|
||||
```
|
||||
|
||||
> **Windows 常见错误**:如果 `icacls` 显示 `UNKNOWN\UNKNOWN` ACL 条目,需要先移除再重新授权。权限错误会导致 SSH 拒绝使用密钥。
|
||||
|
||||
### 5. 验证免密连接
|
||||
|
||||
```bash
|
||||
ssh my-server "echo 'SSH connection OK'"
|
||||
# 应直接输出 "SSH connection OK",不要求输入密码
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 方式一:SSH Remote 模块
|
||||
|
||||
```bash
|
||||
# 基本用法 — 自动探测、部署、启动
|
||||
ccb ssh user@remote-host
|
||||
|
||||
# 使用 SSH Config 别名
|
||||
ccb ssh my-server
|
||||
|
||||
# 指定远端工作目录
|
||||
ccb ssh my-server /home/user/project
|
||||
|
||||
# 使用自定义远端二进制(跳过探测/部署)
|
||||
ccb ssh my-server --remote-bin "bun /opt/ccb/dist/cli.js"
|
||||
|
||||
# 权限控制
|
||||
ccb ssh my-server --permission-mode auto
|
||||
ccb ssh my-server --dangerously-skip-permissions
|
||||
|
||||
# 恢复远端会话
|
||||
ccb ssh my-server --continue
|
||||
ccb ssh my-server --resume <session-uuid>
|
||||
|
||||
# 选择模型
|
||||
ccb ssh my-server --model claude-sonnet-4-6-20250514
|
||||
|
||||
# 本地测试模式(不连接远端,测试 auth proxy 管道)
|
||||
ccb ssh localhost --local
|
||||
```
|
||||
|
||||
### 方式二:直接 SSH 运行
|
||||
|
||||
```bash
|
||||
# 启动交互式会话
|
||||
ssh my-server -t ccb
|
||||
|
||||
# 指定工作目录
|
||||
ssh my-server -t "ccb --cwd /home/user/project"
|
||||
|
||||
# 使用特定模型
|
||||
ssh my-server -t "ccb --model claude-sonnet-4-6-20250514"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 构建与部署
|
||||
|
||||
### 构建产物
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
bun install
|
||||
|
||||
# 构建(输出到 dist/)
|
||||
bun run build
|
||||
```
|
||||
|
||||
产物说明:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `dist/cli.js` | Bun 入口(`#!/usr/bin/env bun`) |
|
||||
| `dist/cli-node.js` | Node.js 入口(`#!/usr/bin/env node` → `import ./cli.js`) |
|
||||
| `dist/cli-bun.js` | Bun 专用入口 |
|
||||
| `dist/chunk-*.js` | 代码分割 chunk 文件(约 668 个) |
|
||||
|
||||
### 运行方式
|
||||
|
||||
```bash
|
||||
# 方式 A:通过 bun 直接运行(开发/调试)
|
||||
bun run dev
|
||||
|
||||
# 方式 B:运行构建产物(bun 运行时)
|
||||
bun dist/cli.js
|
||||
|
||||
# 方式 C:运行构建产物(node 运行时)
|
||||
node dist/cli-node.js
|
||||
|
||||
# 方式 D:全局安装后使用命令名
|
||||
ccb
|
||||
```
|
||||
|
||||
### 全局安装
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
# bun 全局安装(推荐)
|
||||
bun install -g .
|
||||
|
||||
# 创建的命令:
|
||||
# ccb → dist/cli-node.js
|
||||
# ccb-bun → dist/cli-bun.js
|
||||
# claude-code-best → dist/cli-node.js
|
||||
|
||||
# 安装位置:~/.bun/bin/ccb
|
||||
```
|
||||
|
||||
或使用 npm:
|
||||
|
||||
```bash
|
||||
npm install -g .
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
ccb --version
|
||||
# → x.x.x (Claude Code)
|
||||
```
|
||||
|
||||
### 远端部署(全流程)
|
||||
|
||||
```bash
|
||||
# 1. 登录远端
|
||||
ssh my-server
|
||||
|
||||
# 2. 克隆或同步项目代码
|
||||
git clone <repo-url> ~/ccb-project
|
||||
cd ~/ccb-project
|
||||
|
||||
# 3. 安装运行时(如果没有 bun)
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
source ~/.bashrc
|
||||
|
||||
# 4. 安装依赖 + 构建
|
||||
bun install
|
||||
bun run build
|
||||
|
||||
# 5. 全局安装
|
||||
bun install -g .
|
||||
|
||||
# 6. 确保非交互式 SSH 可访问 ccb 命令
|
||||
# bun install -g 安装到 ~/.bun/bin/,但非交互式 SSH 不加载 .bashrc,
|
||||
# 所以 PATH 中不包含 ~/.bun/bin/
|
||||
# 解决方式(任选其一):
|
||||
|
||||
# 方式 A:符号链接到系统 PATH(推荐)
|
||||
ln -sf ~/.bun/bin/ccb /usr/local/bin/ccb
|
||||
|
||||
# 方式 B:添加到 /etc/profile.d/(所有用户生效)
|
||||
echo 'export PATH="$HOME/.bun/bin:$PATH"' > /etc/profile.d/bun-path.sh
|
||||
|
||||
# 方式 C:添加到 ~/.bash_profile(当前用户,ssh -t 时生效)
|
||||
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.bash_profile
|
||||
|
||||
# 7. 验证
|
||||
ccb --version
|
||||
|
||||
# 8. 从本地测试
|
||||
# (在本地终端)
|
||||
ssh my-server -t ccb
|
||||
```
|
||||
|
||||
### SSH Remote 自动部署
|
||||
|
||||
使用 `ccb ssh <host>` 时,模块自动处理:
|
||||
|
||||
1. **SSHProbe** 探测远端 `~/.local/bin/claude` 或 `command -v claude`
|
||||
2. 若二进制不存在或版本不匹配,**SSHDeploy** 通过 `scp` 传输 `dist/` 目录
|
||||
3. 在远端创建 wrapper 脚本(`~/.local/bin/claude`)
|
||||
4. 无需手动安装
|
||||
|
||||
---
|
||||
|
||||
## 模块结构
|
||||
|
||||
```
|
||||
src/ssh/
|
||||
├── createSSHSession.ts — 会话工厂:编排 probe → deploy → proxy → spawn
|
||||
├── SSHSessionManager.ts — 双向 NDJSON 通信管理 + 权限转发 + 重连
|
||||
├── SSHAuthProxy.ts — 本地认证代理(API 凭据隧道)
|
||||
├── SSHProbe.ts — 远端主机探测(平台/架构/已有二进制)
|
||||
├── SSHDeploy.ts — 远端二进制部署(scp + wrapper 脚本)
|
||||
└── __tests__/
|
||||
└── SSHSessionManager.test.ts — 17 个单元测试
|
||||
```
|
||||
|
||||
## 关键技术细节
|
||||
|
||||
### 认证隧道
|
||||
|
||||
- **AuthProxy** 在本地监听(Unix socket 或 TCP),接收远端 CLI 的 API 请求
|
||||
- 通过 SSH `-R` 反向端口转发隧道到远端
|
||||
- AuthProxy 注入本地真实凭据(API key 或 OAuth token),转发到 `api.anthropic.com`
|
||||
- `ANTHROPIC_AUTH_NONCE` header 防止未授权访问(nonce 通过环境变量传递给远端 CLI,远端 CLI 在每个 API 请求中携带此 header)
|
||||
|
||||
### waitForInit vs 存活检查
|
||||
|
||||
- **标准模式**:`waitForInit` 等待远端 CLI 发送 `{type:'system', subtype:'init'}` JSON 消息
|
||||
- **`--remote-bin` 模式**:跳过 `waitForInit`(print+stream-json 模式下 init 只在首次查询后发送),改用 3 秒进程存活检查
|
||||
|
||||
### 重连机制
|
||||
|
||||
- `SSHSessionManager` 检测 SSH 连接断开后自动重连
|
||||
- 重连时在远端 CLI 命令中追加 `--continue` 恢复会话
|
||||
- 指数退避重试(最多 5 次,间隔 1s → 2s → 4s → 8s → 16s)
|
||||
|
||||
## Feature Flag
|
||||
|
||||
SSH Remote 功能受 `SSH_REMOTE` feature flag 控制:
|
||||
|
||||
- **Dev 模式**:默认启用
|
||||
- **Build 模式**:需在 `build.ts` 的 `DEFAULT_BUILD_FEATURES` 中添加 `'SSH_REMOTE'`
|
||||
- **运行时**:`FEATURE_SSH_REMOTE=1` 环境变量
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### `ccb: command not found`(SSH 远程执行时)
|
||||
|
||||
非交互式 SSH 不加载 `.bashrc`,`~/.bun/bin` 不在 PATH 中。
|
||||
|
||||
```bash
|
||||
# 解决:创建符号链接
|
||||
ln -sf ~/.bun/bin/ccb /usr/local/bin/ccb
|
||||
```
|
||||
|
||||
### SSH 密钥被拒绝
|
||||
|
||||
```
|
||||
Permission denied (publickey)
|
||||
```
|
||||
|
||||
1. 确认公钥已添加到远端 `~/.ssh/authorized_keys`
|
||||
2. 确认本地私钥文件权限正确(`chmod 600`)
|
||||
3. 确认 `~/.ssh/config` 中 `IdentityFile` 路径正确
|
||||
4. Windows 用户检查 ACL 权限(见上方 Windows 权限设置)
|
||||
|
||||
### SSH 连接超时
|
||||
|
||||
```
|
||||
ssh: connect to host x.x.x.x port 22: Connection timed out
|
||||
```
|
||||
|
||||
1. 确认远端 SSH 服务正在运行:`systemctl status sshd`
|
||||
2. 确认防火墙允许 22 端口
|
||||
3. 确认 IP 地址/域名正确
|
||||
4. 在 `~/.ssh/config` 中添加 `ConnectTimeout 10`
|
||||
|
||||
### 403 Forbidden(SSH Remote 模块)
|
||||
|
||||
AuthProxy 的 nonce 验证失败。确认:
|
||||
1. 远端 CLI 版本包含 nonce header 注入修复
|
||||
2. `ANTHROPIC_AUTH_NONCE` 环境变量正确传递到远端
|
||||
3. `src/services/api/client.ts` 中 `x-auth-nonce` header 已启用
|
||||
|
||||
### 远端 CLI 启动后立即退出
|
||||
|
||||
```
|
||||
Remote process exited immediately (code 1)
|
||||
```
|
||||
|
||||
1. 确认远端 `bun` / `node` 运行时可用
|
||||
2. 手动在远端执行 `ccb --version` 验证安装
|
||||
3. 检查 `--remote-bin` 路径是否正确
|
||||
4. 查看 stderr 输出获取详细错误信息
|
||||
@@ -16,12 +16,12 @@
|
||||
### 现状
|
||||
|
||||
- `start` 路径已有完整 supervisor + worker 生命周期:
|
||||
[src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:1>)
|
||||
[src/daemon/workerRegistry.ts](</e:/Source_code/Claude-code-bast/src/daemon/workerRegistry.ts:1>)
|
||||
`src/daemon/main.ts`
|
||||
`src/daemon/workerRegistry.ts`
|
||||
- `status` / `stop` 目前只是占位输出:
|
||||
[src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:49>)
|
||||
`src/daemon/main.ts`
|
||||
- `/remote-control-server` 有自己的命令内 UI 状态,但只维护当前进程内的 `daemonProcess`,并不适合作为跨进程 CLI 管理基础:
|
||||
[src/commands/remoteControlServer/remoteControlServer.tsx](</e:/Source_code/Claude-code-bast/src/commands/remoteControlServer/remoteControlServer.tsx:32>)
|
||||
`src/commands/remoteControlServer/remoteControlServer.tsx`
|
||||
|
||||
### 目标
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
### 代码范围
|
||||
|
||||
- 新增 `src/daemon/state.ts`
|
||||
- 修改 [src/daemon/main.ts](</e:/Source_code/Claude-code-bast/src/daemon/main.ts:1>)
|
||||
- 轻量修改 [src/commands/remoteControlServer/remoteControlServer.tsx](</e:/Source_code/Claude-code-bast/src/commands/remoteControlServer/remoteControlServer.tsx:32>),让 UI 尽量读取同一份状态文件
|
||||
- 修改 `src/daemon/main.ts`
|
||||
- 轻量修改 `src/commands/remoteControlServer/remoteControlServer.tsx`,让 UI 尽量读取同一份状态文件
|
||||
|
||||
### 验证
|
||||
|
||||
@@ -78,15 +78,15 @@
|
||||
### 现状
|
||||
|
||||
- fast-path 已接好:
|
||||
[src/entrypoints/cli.tsx](</e:/Source_code/Claude-code-bast/src/entrypoints/cli.tsx:218>)
|
||||
`src/entrypoints/cli.tsx`
|
||||
- session registry 已有真实实现:
|
||||
[src/utils/concurrentSessions.ts](</e:/Source_code/Claude-code-bast/src/utils/concurrentSessions.ts:1>)
|
||||
`src/utils/concurrentSessions.ts`
|
||||
- `exit` 在 bg session 内已会 `tmux detach-client`:
|
||||
[src/commands/exit/exit.tsx](</e:/Source_code/Claude-code-bast/src/commands/exit/exit.tsx:20>)
|
||||
`src/commands/exit/exit.tsx`
|
||||
- 但 CLI handler 仍全空:
|
||||
[src/cli/bg.ts](</e:/Source_code/Claude-code-bast/src/cli/bg.ts:1>)
|
||||
`src/cli/bg.ts`
|
||||
- task summary 仍然是 stub:
|
||||
[src/utils/taskSummary.ts](</e:/Source_code/Claude-code-bast/src/utils/taskSummary.ts:1>)
|
||||
`src/utils/taskSummary.ts`
|
||||
|
||||
### 目标
|
||||
|
||||
@@ -122,12 +122,12 @@
|
||||
|
||||
### 代码范围
|
||||
|
||||
- 修改 [src/cli/bg.ts](</e:/Source_code/Claude-code-bast/src/cli/bg.ts:1>)
|
||||
- 修改 [src/utils/concurrentSessions.ts](</e:/Source_code/Claude-code-bast/src/utils/concurrentSessions.ts:1>) 以便后续 attach/--bg 扩展
|
||||
- 修改 [src/utils/taskSummary.ts](</e:/Source_code/Claude-code-bast/src/utils/taskSummary.ts:1>)
|
||||
- 修改 `src/cli/bg.ts`
|
||||
- 修改 `src/utils/concurrentSessions.ts` 以便后续 attach/--bg 扩展
|
||||
- 修改 `src/utils/taskSummary.ts`
|
||||
- 复用:
|
||||
[src/utils/sessionStorage.ts](</e:/Source_code/Claude-code-bast/src/utils/sessionStorage.ts:3870>)
|
||||
[src/utils/udsClient.ts](</e:/Source_code/Claude-code-bast/src/utils/udsClient.ts:1>)
|
||||
`src/utils/sessionStorage.ts`
|
||||
`src/utils/udsClient.ts`
|
||||
|
||||
### 验证
|
||||
|
||||
@@ -150,15 +150,15 @@
|
||||
### 现状
|
||||
|
||||
- 命令入口只有 fast-path:
|
||||
[src/entrypoints/cli.tsx](</e:/Source_code/Claude-code-bast/src/entrypoints/cli.tsx:249>)
|
||||
`src/entrypoints/cli.tsx`
|
||||
- handler 是空的:
|
||||
[src/cli/handlers/templateJobs.ts](</e:/Source_code/Claude-code-bast/src/cli/handlers/templateJobs.ts:1>)
|
||||
`src/cli/handlers/templateJobs.ts`
|
||||
- `markdownConfigLoader` 已把 `templates` 纳入配置目录:
|
||||
[src/utils/markdownConfigLoader.ts](</e:/Source_code/Claude-code-bast/src/utils/markdownConfigLoader.ts:29>)
|
||||
`src/utils/markdownConfigLoader.ts`
|
||||
- `query / stopHooks` 已预留 job classifier 链路:
|
||||
[src/query/stopHooks.ts](</e:/Source_code/Claude-code-bast/src/query/stopHooks.ts:103>)
|
||||
`src/query/stopHooks.ts`
|
||||
- `jobs/classifier.ts` 仍是 stub:
|
||||
[src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
|
||||
`src/jobs/classifier.ts`
|
||||
|
||||
### 目标
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
|
||||
### Phase 2
|
||||
|
||||
- 恢复 [src/jobs/classifier.ts](</e:/Source_code/Claude-code-bast/src/jobs/classifier.ts:1>)
|
||||
- 恢复 `src/jobs/classifier.ts`
|
||||
- 让带 `CLAUDE_JOB_DIR` 的 job session 在 turn 完成后自动更新 `state.json`
|
||||
- 再决定是否补自动 job runner
|
||||
|
||||
|
||||
@@ -7,50 +7,13 @@
|
||||
|
||||
| Feature | 引用 | 状态 | 类别 | 简要说明 |
|
||||
|---------|------|------|------|---------|
|
||||
| CHICAGO_MCP | 16 | N/A | 内部基础设施 | Anthropic 内部 MCP 基础设施,非外部可用 |
|
||||
| MONITOR_TOOL | 13 | Stub | 工具 | 文件/进程监控工具,检测变更并通知 |
|
||||
| BG_SESSIONS | 11 | Stub | 会话管理 | 后台会话管理,支持多会话并行 |
|
||||
| SHOT_STATS | 10 | 无实现 | 统计 | 逐 prompt 统计信息收集 |
|
||||
| EXTRACT_MEMORIES | 7 | 无实现 | 记忆 | 自动从对话中提取重要信息作为记忆 |
|
||||
| TEMPLATES | 6 | Stub | 项目管理 | 项目/提示模板系统 |
|
||||
| LODESTONE | 6 | N/A | 内部基础设施 | 内部基础设施模块 |
|
||||
| STREAMLINED_OUTPUT | 1 | — | 输出 | 精简输出模式,减少终端输出量 |
|
||||
| HOOK_PROMPTS | 1 | — | 钩子 | Hook 提示词,自定义钩子的提示注入 |
|
||||
| CCR_AUTO_CONNECT | 3 | — | 远程控制 | CCR 自动连接,自动建立远程控制会话 |
|
||||
| CCR_MIRROR | 4 | — | 远程控制 | CCR 镜像模式,会话状态同步 |
|
||||
| CCR_REMOTE_SETUP | 1 | — | 远程控制 | CCR 远程设置,初始化远程控制配置 |
|
||||
| NATIVE_CLIPBOARD_IMAGE | 2 | — | 系统集成 | 原生剪贴板图片,从剪贴板读取图片 |
|
||||
| CONNECTOR_TEXT | 7 | — | 连接器 | 连接器文本,外部系统文本适配 |
|
||||
| COMMIT_ATTRIBUTION | 12 | — | Git | Commit 归因,标记 commit 来源 |
|
||||
| CACHED_MICROCOMPACT | 12 | — | 压缩 | 缓存微压缩,优化 compaction 性能 |
|
||||
| PROMPT_CACHE_BREAK_DETECTION | 9 | — | 性能 | Prompt cache 中断检测,监控 cache miss |
|
||||
| MEMORY_SHAPE_TELEMETRY | 3 | — | 遥测 | 记忆形态遥测,记忆使用模式追踪 |
|
||||
| MCP_RICH_OUTPUT | 3 | — | MCP | MCP 富输出,增强 MCP 工具输出格式 |
|
||||
| FILE_PERSISTENCE | 3 | — | 持久化 | 文件持久化,跨会话保持状态 |
|
||||
| TREE_SITTER_BASH_SHADOW | 5 | Shadow | 安全 | Bash AST Shadow 模式(见 tree-sitter-bash.md) |
|
||||
| QUICK_SEARCH | 5 | — | 搜索 | 快速搜索,优化的文件/内容搜索 |
|
||||
| MESSAGE_ACTIONS | 5 | — | UI | 消息操作,对消息执行后处理动作 |
|
||||
| DOWNLOAD_USER_SETTINGS | 5 | — | 配置 | 下载用户设置,从服务端同步配置 |
|
||||
| DIRECT_CONNECT | 5 | — | 网络 | 直连模式,绕过代理直接连接 API |
|
||||
| VERIFICATION_AGENT | 4 | — | Agent | 验证 Agent,专门用于验证代码变更 |
|
||||
| TERMINAL_PANEL | 4 | — | UI | 终端面板,嵌入式终端输出显示 |
|
||||
| SSH_REMOTE | 4 | — | 远程 | SSH 远程,通过 SSH 连接远程 Claude |
|
||||
| REVIEW_ARTIFACT | 4 | — | 审查 | Review Artifact,代码审查产出物 |
|
||||
| REACTIVE_COMPACT | 4 | — | 压缩 | 响应式压缩,基于上下文变化触发 compaction |
|
||||
| HISTORY_PICKER | 4 | — | UI | 历史选择器,浏览和选择历史对话 |
|
||||
| UPLOAD_USER_SETTINGS | 2 | — | 配置 | 上传用户设置,同步配置到服务端 |
|
||||
| POWERSHELL_AUTO_MODE | 2 | — | 平台 | PowerShell 自动模式,Windows 权限自动化 |
|
||||
| OVERFLOW_TEST_TOOL | 2 | — | 测试 | 溢出测试工具,测试上下文溢出处理 |
|
||||
| NEW_INIT | 2 | — | 初始化 | 新版初始化流程 |
|
||||
| HARD_FAIL | 2 | — | 错误处理 | 硬失败模式,不可恢复错误直接终止 |
|
||||
| ENHANCED_TELEMETRY_BETA | 2 | — | 遥测 | 增强遥测 Beta,详细的性能指标收集 |
|
||||
| COWORKER_TYPE_TELEMETRY | 2 | — | 遥测 | 协作者类型遥测,追踪协作模式 |
|
||||
| BREAK_CACHE_COMMAND | 2 | — | 缓存 | 中断缓存命令,强制刷新 prompt cache |
|
||||
| AWAY_SUMMARY | 2 | — | 摘要 | 离开摘要,用户返回时总结期间工作 |
|
||||
| AUTO_THEME | 2 | — | UI | 自动主题,根据终端设置切换主题 |
|
||||
| ALLOW_TEST_VERSIONS | 2 | — | 版本 | 允许测试版本,跳过版本检查 |
|
||||
| AGENT_TRIGGERS_REMOTE | 2 | — | Agent | Agent 远程触发,从远程触发 Agent 任务 |
|
||||
| AGENT_MEMORY_SNAPSHOT | 2 | — | Agent | Agent 记忆快照,保存/恢复 Agent 状态 |
|
||||
| CHICAGO_MCP | 16 | 已实现 | 工具 | Computer Use + Chrome MCP 控制(build 默认启用) |
|
||||
| MONITOR_TOOL | 13 | 已实现 | 工具 | 后台监控工具,持续监视 shell 输出(build 默认启用) |
|
||||
| BG_SESSIONS | 11 | 部分实现 | 会话管理 | 后台会话注册/清理已实现,任务摘要是 stub(dev 默认启用) |
|
||||
| SHOT_STATS | 10 | 已实现 | 统计 | API 调用统计面板(build 默认启用) |
|
||||
| EXTRACT_MEMORIES | 7 | 已实现 | 记忆 | 自动记忆提取(build 默认启用,受 GrowthBook 门控) |
|
||||
| TEMPLATES | 6 | 部分实现 | 项目管理 | 项目/提示模板系统(dev 默认启用) |
|
||||
| LODESTONE | 6 | 已实现 | 深度链接 | URL 协议处理器(build 默认启用) |
|
||||
|
||||
## 单引用 Feature(40+ 个)
|
||||
|
||||
@@ -66,10 +29,9 @@ BUILDING_CLAUDE_APPS, ANTI_DISTILLATION_CC, AGENT_TRIGGERS, ABLATION_BASELINE
|
||||
|
||||
这些 feature 被列为 Tier 3 的原因:
|
||||
|
||||
1. **内部基础设施**(CHICAGO_MCP, LODESTONE):Anthropic 内部使用,外部无法运行
|
||||
2. **纯 Stub 且引用低**(MONITOR_TOOL, BG_SESSIONS):需要大量工作才能实现
|
||||
3. **实验性功能**(SHOT_STATS, EXTRACT_MEMORIES):尚在概念阶段
|
||||
4. **辅助功能**(STREAMLINED_OUTPUT, HOOK_PROMPTS):影响范围小
|
||||
5. **CCR 系列**:依赖远程控制基础设施,需要 BRIDGE_MODE 先完善
|
||||
1. **已实现但影响范围小**(CHICAGO_MCP, LODESTONE, SHOT_STATS, EXTRACT_MEMORIES, MONITOR_TOOL):已在 build/dev 默认启用,主要作为其他功能的基础设施
|
||||
2. **部分实现**(BG_SESSIONS, TEMPLATES):核心注册已实现,但部分功能如任务摘要仍是 stub
|
||||
3. **辅助功能**(STREAMLINED_OUTPUT, HOOK_PROMPTS):影响范围小
|
||||
4. **CCR 系列**:依赖远程控制基础设施,需要 BRIDGE_MODE 先完善
|
||||
|
||||
如需深入了解某个 Tier 3 feature,可以在代码库中搜索 `feature('FEATURE_NAME')` 查看具体使用场景。
|
||||
|
||||
@@ -191,7 +191,7 @@ FEATURE_TOKEN_BUDGET=1 bun run dev
|
||||
| `src/query/tokenBudget.ts` | 93 | 预算追踪器 + continue/stop 决策 |
|
||||
| `src/bootstrap/state.ts:724-743` | 20 | turn 级 token 快照状态 |
|
||||
| `src/constants/prompts.ts:538-551` | 14 | 系统提示注入 |
|
||||
| `src/utils/attachments.ts:3829-3845` | 17 | API attachment 附加 |
|
||||
| `src/utils/attachments.ts:3830-3844` | 17 | API attachment 附加 |
|
||||
| `src/query.ts:280,1311-1358` | 48 | 主循环集成 |
|
||||
| `src/screens/REPL.tsx:2897,2963,2138` | 20 | REPL 提交/完成/取消处理 |
|
||||
| `src/components/Spinner.tsx:319-338` | 20 | 进度条 UI |
|
||||
|
||||
@@ -158,4 +158,4 @@ FEATURE_TREE_SITTER_BASH_SHADOW=1 bun run dev
|
||||
| `src/utils/bash/bashParser.ts` | 4437 | 纯 TS bash 解析器 |
|
||||
| `src/utils/bash/ast.ts` | 2680 | 安全分析器(核心) |
|
||||
| `src/utils/bash/treeSitterAnalysis.ts` | 507 | AST 分析辅助 |
|
||||
| `src/tools/BashTool/bashPermissions.ts:1670-1810` | ~140 | 权限集成 + Shadow 遥测 |
|
||||
| `packages/builtin-tools/src/tools/BashTool/bashPermissions.ts` | ~140 | 权限集成 + Shadow 遥测 |
|
||||
|
||||
@@ -22,16 +22,16 @@ ULTRAPLAN 在用户输入中检测 "ultraplan" 关键字时,自动进入增强
|
||||
|
||||
| 模块 | 文件 | 行数 | 状态 |
|
||||
|------|------|------|------|
|
||||
| 命令处理器 | `src/commands/ultraplan.tsx` | 472 | **完整** |
|
||||
| CCR 会话 | `src/utils/ultraplan/ccrSession.ts` | 350 | **完整** |
|
||||
| 关键字检测 | `src/utils/ultraplan/keyword.ts` | 128 | **完整** |
|
||||
| 命令处理器 | `src/commands/ultraplan.tsx` | 525 | **完整** |
|
||||
| CCR 会话 | `src/utils/ultraplan/ccrSession.ts` | 349 | **完整** |
|
||||
| 关键字检测 | `src/utils/ultraplan/keyword.ts` | 127 | **完整** |
|
||||
| 嵌入式提示 | `src/utils/ultraplan/prompt.txt` | 1 | **完整** |
|
||||
| REPL 对话框 | `src/screens/REPL.tsx` | — | **布线** |
|
||||
| 关键字高亮 | `src/components/PromptInput/PromptInput.tsx` | — | **布线** |
|
||||
|
||||
### 2.2 关键字检测
|
||||
|
||||
文件:`src/utils/ultraplan/keyword.ts`(128 行)
|
||||
文件:`src/utils/ultraplan/keyword.ts`(127 行)
|
||||
|
||||
`findUltraplanTriggerPositions(text)` 智能过滤:
|
||||
- 排除引号内的 "ultraplan"
|
||||
@@ -41,7 +41,7 @@ ULTRAPLAN 在用户输入中检测 "ultraplan" 关键字时,自动进入增强
|
||||
|
||||
### 2.3 CCR 远程会话
|
||||
|
||||
文件:`src/utils/ultraplan/ccrSession.ts`(350 行)
|
||||
文件:`src/utils/ultraplan/ccrSession.ts`(349 行)
|
||||
|
||||
`ExitPlanModeScanner` 类实现完整的事件状态机:
|
||||
- `pollForApprovedExitPlanMode()` — 3 秒轮询间隔
|
||||
@@ -99,9 +99,9 @@ FEATURE_ULTRAPLAN=1 bun run dev
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/commands/ultraplan.tsx` | 472 | 斜杠命令处理器 |
|
||||
| `src/utils/ultraplan/ccrSession.ts` | 350 | CCR 远程会话管理 |
|
||||
| `src/utils/ultraplan/keyword.ts` | 128 | 关键字检测和替换 |
|
||||
| `src/commands/ultraplan.tsx` | 525 | 斜杠命令处理器 |
|
||||
| `src/utils/ultraplan/ccrSession.ts` | 349 | CCR 远程会话管理 |
|
||||
| `src/utils/ultraplan/keyword.ts` | 127 | 关键字检测和替换 |
|
||||
| `src/utils/ultraplan/prompt.txt` | 1 | 嵌入式提示 |
|
||||
| `src/utils/processUserInput/processUserInput.ts:468` | — | 关键字重定向 |
|
||||
| `src/components/PromptInput/PromptInput.tsx` | — | 彩虹高亮 |
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
# VOICE_MODE — 语音输入
|
||||
|
||||
> Feature Flag: `FEATURE_VOICE_MODE=1`
|
||||
> 实现状态:完整可用(需要 Anthropic OAuth)
|
||||
> 实现状态:完整可用(双后端:Anthropic OAuth / 豆包 ASR)
|
||||
> 引用数:46
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
VOICE_MODE 实现"按键说话"(Push-to-Talk)语音输入。用户按住空格键录音,音频通过 WebSocket 流式传输到 Anthropic STT 端点(Nova 3),实时转录显示在终端中。
|
||||
VOICE_MODE 实现"按键说话"(Push-to-Talk)语音输入。用户按住空格键录音,音频流式传输到 STT 后端,实时转录显示在终端中。支持两个后端:
|
||||
|
||||
- **Anthropic STT(默认)**:通过 WebSocket 流式传输到 Nova 3 端点,需要 Anthropic OAuth
|
||||
- **豆包 ASR(Doubao)**:通过 `doubaoime-asr` 包的 AsyncGenerator 协议流式识别,使用独立凭证文件,无需 Anthropic OAuth
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **Push-to-Talk**:长按空格键录音,释放后自动发送
|
||||
- **流式转录**:录音过程中实时显示中间转录结果
|
||||
- **无缝集成**:转录文本直接作为用户消息提交到对话
|
||||
- **双后端切换**:通过 `/voice` 命令参数选择 STT 后端,持久化到 settings.json
|
||||
|
||||
## 二、用户交互
|
||||
|
||||
| 操作 | 行为 |
|
||||
|------|------|
|
||||
| 长按空格 | 开始录音,显示录音状态 |
|
||||
| 释放空格 | 停止录音,等待最终转录 |
|
||||
| 转录完成 | 自动插入到输入框并提交 |
|
||||
| `/voice` 命令 | 切换语音模式开关 |
|
||||
| 释放空格 | 停止录音,转录结果自动提交 |
|
||||
| `/voice` | 切换语音模式开关(默认使用 Anthropic 后端) |
|
||||
| `/voice doubao` | 启用语音模式并使用豆包 ASR 后端 |
|
||||
| `/voice anthropic` | 切换回 Anthropic STT 后端 |
|
||||
|
||||
### UI 反馈
|
||||
|
||||
@@ -35,26 +40,37 @@ VOICE_MODE 实现"按键说话"(Push-to-Talk)语音输入。用户按住空
|
||||
|
||||
文件:`src/voice/voiceModeEnabled.ts`
|
||||
|
||||
三层检查:
|
||||
两层检查函数:
|
||||
|
||||
```ts
|
||||
// Anthropic 后端(需要 OAuth)
|
||||
isVoiceModeEnabled() = hasVoiceAuth() && isVoiceGrowthBookEnabled()
|
||||
|
||||
// 豆包后端 / 通用可用性检查(不需要 OAuth)
|
||||
isVoiceAvailable() = 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. **Auth 检查(仅 Anthropic)**:`hasVoiceAuth()` — 需要 Anthropic OAuth token(非 API key)
|
||||
4. **Provider 检查**:`voiceProvider` 设置决定使用哪个后端,豆包后端跳过 OAuth 检查
|
||||
|
||||
### 3.2 核心模块
|
||||
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| `src/voice/voiceModeEnabled.ts` | Feature flag + GrowthBook + Auth 三层门控 |
|
||||
| `src/hooks/useVoice.ts` | React hook 管理录音状态和 WebSocket 连接 |
|
||||
| `src/services/voiceStreamSTT.ts` | WebSocket 流式传输到 Anthropic STT |
|
||||
| `src/hooks/useVoice.ts` | React hook 管理录音状态和后端连接 |
|
||||
| `src/services/voiceStreamSTT.ts` | Anthropic WebSocket 流式 STT |
|
||||
| `src/services/doubaoSTT.ts` | 豆包 ASR 适配器(AsyncGenerator → VoiceStreamConnection) |
|
||||
| `src/commands/voice/voice.ts` | `/voice` 命令实现,处理后端选择和持久化 |
|
||||
| `src/hooks/useVoiceEnabled.ts` | 语音启用状态 hook,根据 provider 决定是否跳过 OAuth |
|
||||
| `src/utils/settings/types.ts` | `voiceProvider: 'anthropic' | 'doubao'` 设置类型定义 |
|
||||
|
||||
### 3.3 数据流
|
||||
|
||||
#### Anthropic 后端
|
||||
|
||||
```
|
||||
用户按下空格键
|
||||
│
|
||||
@@ -79,20 +95,108 @@ WebSocket 连接到 Anthropic STT 端点
|
||||
转录文本 → 插入输入框 → 自动提交
|
||||
```
|
||||
|
||||
#### 豆包 ASR 后端
|
||||
|
||||
```
|
||||
用户按下空格键
|
||||
│
|
||||
▼
|
||||
useVoice hook 激活(检测到 voiceProvider === 'doubao')
|
||||
│
|
||||
▼
|
||||
macOS 原生音频 / SoX 开始录音
|
||||
│
|
||||
▼
|
||||
connectDoubaoStream() 创建 AudioChunkQueue + VoiceStreamConnection
|
||||
│
|
||||
├──→ onReady 立即触发(无需等待握手)
|
||||
│
|
||||
▼
|
||||
音频数据通过 AudioChunkQueue 传入 transcribeRealtime()
|
||||
│
|
||||
├──→ INTERIM_RESULT → 实时显示中间转录
|
||||
├──→ FINAL_RESULT → 显示最终转录
|
||||
│
|
||||
▼
|
||||
用户释放空格键
|
||||
│
|
||||
▼
|
||||
finalize() 立即返回(豆包在录音过程中已返回结果,无需等待)
|
||||
│
|
||||
▼
|
||||
转录文本 → 插入输入框 → 自动提交
|
||||
```
|
||||
|
||||
### 3.4 音频录制
|
||||
|
||||
支持两种音频后端:
|
||||
支持两种音频后端(两个 STT 后端共享):
|
||||
- **macOS 原生音频**:优先使用,低延迟
|
||||
- **SoX(Sound eXchange)**:回退方案,跨平台
|
||||
|
||||
音频流通过 WebSocket 发送到 Anthropic 的 Nova 3 STT 模型。
|
||||
### 3.5 豆包 ASR 适配器设计
|
||||
|
||||
文件:`src/services/doubaoSTT.ts`
|
||||
|
||||
豆包后端使用适配器模式,将 `doubaoime-asr` 的 AsyncGenerator 协议桥接到 `VoiceStreamConnection` 接口:
|
||||
|
||||
**AudioChunkQueue** — push 式异步队列:
|
||||
- 实现 `AsyncIterable<Uint8Array>` 接口
|
||||
- `push(chunk)` 将音频数据入队,`push(null)` 发送结束信号
|
||||
- 内部维护等待者(waiting)和缓冲队列(chunks)两个状态
|
||||
|
||||
**connectDoubaoStream()** — 连接入口:
|
||||
- 动态导入 `doubaoime-asr`(optionalDependencies)
|
||||
- 从 `~/.claude/tts/doubao/credentials.json` 加载凭证
|
||||
- 创建 AudioChunkQueue 和 VoiceStreamConnection
|
||||
- 立即触发 `onReady`(避免与 useVoice 的音频缓冲死锁)
|
||||
- `finalize()` 立即返回(豆包在录音过程中已返回结果)
|
||||
- 后台 async IIFE 消费 `transcribeRealtime` generator,映射响应类型到回调
|
||||
|
||||
**响应类型映射**:
|
||||
|
||||
| doubaoime-asr ResponseType | 回调映射 |
|
||||
|----------------------------|----------|
|
||||
| SESSION_STARTED | 日志记录 |
|
||||
| VAD_START | 日志记录 |
|
||||
| INTERIM_RESULT | `onTranscript(text, false)` |
|
||||
| FINAL_RESULT | `onTranscript(text, true)` |
|
||||
| ERROR | `onError(errorMsg)` |
|
||||
| SESSION_FINISHED | 日志记录 |
|
||||
|
||||
### 3.6 后端选择逻辑
|
||||
|
||||
文件:`src/hooks/useVoice.ts`
|
||||
|
||||
```ts
|
||||
// 判断当前 provider
|
||||
isDoubaoProvider() → 读取 settings.voiceProvider
|
||||
|
||||
// handleKeyEvent 中的可用性检查
|
||||
const sttAvailable = isDoubaoProvider()
|
||||
? isDoubaoAvailableSync() // 乐观检查(首次返回 true)
|
||||
: isVoiceStreamAvailable() // Anthropic WebSocket 检查
|
||||
|
||||
// attemptConnect 中的连接函数选择
|
||||
const connectFn = isDoubaoProvider()
|
||||
? connectDoubaoStream
|
||||
: connectVoiceStream
|
||||
```
|
||||
|
||||
豆包后端的特殊处理:
|
||||
- 跳过 `getVoiceKeyterms()` 调用(豆包无需关键词提示)
|
||||
- 跳过 Focus Mode(`if (!enabled || !focusMode || isDoubaoProvider())`)
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
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`,不触发任何模块加载
|
||||
1. **双后端共存**:豆包后端作为独立适配器与 Anthropic 后端并存,不替换原有流程,通过 `voiceProvider` 设置切换
|
||||
2. **设置持久化**:`voiceProvider` 存储在 `settings.json`,通过 `/voice` 命令修改,跨会话生效
|
||||
3. **OAuth 独占(Anthropic)**:Anthropic 后端使用 `voice_stream` 端点(claude.ai),仅 OAuth 用户可用
|
||||
4. **豆包无需 OAuth**:豆包后端使用独立凭证文件,不依赖 Anthropic 认证,通过 `isVoiceAvailable()` 放宽门控
|
||||
5. **GrowthBook 负向门控**:`tengu_amber_quartz_disabled` 默认 `false`,新安装自动可用
|
||||
6. **onReady 立即触发**:豆包后端在连接建立后立即触发 `onReady`,避免与 useVoice 音频缓冲的时序死锁(Anthropic 需要等待 WebSocket 握手)
|
||||
7. **finalize() 立即返回**:豆包在录音过程中已返回所有结果,用户抬手时无需等待处理
|
||||
8. **乐观可用性检查**:`isDoubaoAvailableSync()` 在首次调用时返回 `true`,实际导入错误在 `connectDoubaoStream` 中处理
|
||||
9. **optionalDependencies**:`doubaoime-asr` 作为可选依赖,安装失败不影响 Anthropic 后端
|
||||
|
||||
## 五、使用方式
|
||||
|
||||
@@ -100,26 +204,60 @@ WebSocket 连接到 Anthropic STT 端点
|
||||
# 启用 feature
|
||||
FEATURE_VOICE_MODE=1 bun run dev
|
||||
|
||||
# 在 REPL 中使用
|
||||
# 在 REPL 中使用 Anthropic 后端
|
||||
# 1. 确保已通过 OAuth 登录(claude.ai 订阅)
|
||||
# 2. 按住空格键说话
|
||||
# 3. 释放空格键等待转录
|
||||
# 4. 或使用 /voice 命令切换开关
|
||||
# 2. 输入 /voice 启用
|
||||
# 3. 按住空格键说话
|
||||
# 4. 释放空格键等待转录
|
||||
|
||||
# 在 REPL 中使用豆包 ASR 后端
|
||||
# 1. 确保 doubaoime-asr 已安装(bun add doubaoime-asr)
|
||||
# 2. 配置凭证文件:~/.claude/tts/doubao/credentials.json
|
||||
# 3. 输入 /voice doubao 启用
|
||||
# 4. 按住空格键说话
|
||||
# 5. 释放空格键,转录结果即刻显示
|
||||
|
||||
# 切换后端
|
||||
/voice doubao # 切换到豆包 ASR
|
||||
/voice anthropic # 切换回 Anthropic STT
|
||||
/voice # 关闭语音模式
|
||||
```
|
||||
|
||||
### 豆包凭证配置
|
||||
|
||||
凭证文件路径:`~/.claude/tts/doubao/credentials.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceId": "...",
|
||||
"installId": "...",
|
||||
"cdid": "...",
|
||||
"openudid": "...",
|
||||
"clientudid": "...",
|
||||
"token": "..."
|
||||
}
|
||||
```
|
||||
|
||||
## 六、外部依赖
|
||||
|
||||
| 依赖 | 说明 |
|
||||
|------|------|
|
||||
| Anthropic OAuth | claude.ai 订阅登录,非 API key |
|
||||
| GrowthBook | `tengu_amber_quartz_disabled` 紧急关闭 |
|
||||
| macOS 原生音频 或 SoX | 音频录制 |
|
||||
| Nova 3 STT | 语音转文本模型 |
|
||||
| 依赖 | 说明 | 适用后端 |
|
||||
|------|------|----------|
|
||||
| Anthropic OAuth | claude.ai 订阅登录,非 API key | Anthropic |
|
||||
| GrowthBook | `tengu_amber_quartz_disabled` 紧急关闭 | 通用 |
|
||||
| macOS 原生音频 或 SoX | 音频录制 | 通用 |
|
||||
| Nova 3 STT | Anthropic 语音转文本模型 | Anthropic |
|
||||
| doubaoime-asr | 豆包 ASR SDK(optionalDependencies) | 豆包 |
|
||||
| 凭证文件 | `~/.claude/tts/doubao/credentials.json` | 豆包 |
|
||||
|
||||
## 七、文件索引
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/voice/voiceModeEnabled.ts` | 55 | 三层门控逻辑 |
|
||||
| `src/hooks/useVoice.ts` | — | React hook(录音状态 + WebSocket) |
|
||||
| `src/services/voiceStreamSTT.ts` | — | STT WebSocket 流式传输 |
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/voice/voiceModeEnabled.ts` | 三层门控逻辑 + `isVoiceAvailable()` |
|
||||
| `src/hooks/useVoice.ts` | React hook(录音状态 + 后端选择 + 连接管理) |
|
||||
| `src/hooks/useVoiceEnabled.ts` | 语音启用状态 hook(按 provider 决定 OAuth 检查) |
|
||||
| `src/services/voiceStreamSTT.ts` | Anthropic STT WebSocket 流式传输 |
|
||||
| `src/services/doubaoSTT.ts` | 豆包 ASR 适配器(AudioChunkQueue + connectDoubaoStream) |
|
||||
| `src/commands/voice/voice.ts` | `/voice` 命令(开关 + 后端选择) |
|
||||
| `src/commands/voice/index.ts` | 命令注册(去除 availability 限制) |
|
||||
| `src/utils/settings/types.ts` | `voiceProvider` 类型定义 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# WEB_BROWSER_TOOL — 浏览器工具
|
||||
|
||||
> Feature Flag: `FEATURE_WEB_BROWSER_TOOL=1`
|
||||
> 实现状态:核心实现缺失,面板为 Stub,布线完整
|
||||
> 实现状态:核心工具已实现,面板为 Stub,布线完整
|
||||
> 引用数:4
|
||||
|
||||
## 一、功能概述
|
||||
@@ -14,8 +14,8 @@ WEB_BROWSER_TOOL 让模型可以启动浏览器实例、导航网页、与页面
|
||||
|
||||
| 模块 | 文件 | 状态 |
|
||||
|------|------|------|
|
||||
| 浏览器面板 | `src/tools/WebBrowserTool/WebBrowserPanel.ts` | **Stub** — 返回 null |
|
||||
| 浏览器工具 | `src/tools/WebBrowserTool/WebBrowserTool.ts` | **缺失** |
|
||||
| 浏览器面板 | `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserPanel.ts` | **Stub** — 返回 null |
|
||||
| 浏览器工具 | `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserTool.ts` | **已实现** |
|
||||
| REPL 集成 | `src/screens/REPL.tsx` | **布线** — 渲染 WebBrowserPanel |
|
||||
| 工具注册 | `src/tools.ts` | **布线** — 动态加载 |
|
||||
| WebView 检测 | `src/main.tsx` | **布线** — `'WebView' in Bun` 检测 |
|
||||
@@ -44,8 +44,8 @@ WebBrowserPanel 在 REPL 侧边显示浏览器状态
|
||||
|
||||
| 模块 | 工作量 | 说明 |
|
||||
|------|--------|------|
|
||||
| `WebBrowserTool.ts` | 大 | 工具 schema + Bun WebView API 执行 |
|
||||
| `WebBrowserPanel.tsx` | 中 | REPL 侧边栏浏览器状态面板 |
|
||||
| `WebBrowserTool.ts` | ✅ 已实现 | 工具 schema + Bun WebView API 执行 |
|
||||
| `WebBrowserPanel.tsx` | 中 | REPL 侧边栏浏览器状态面板(仍为 Stub) |
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
@@ -63,7 +63,7 @@ FEATURE_WEB_BROWSER_TOOL=1 bun run dev
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/tools/WebBrowserTool/WebBrowserPanel.ts` | 面板组件(stub) |
|
||||
| `src/tools/WebBrowserTool/WebBrowserTool.ts` | 工具实现(缺失) |
|
||||
| `src/screens/REPL.tsx:273,4582` | 面板渲染 |
|
||||
| `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserPanel.ts` | 面板组件(stub) |
|
||||
| `packages/builtin-tools/src/tools/WebBrowserTool/WebBrowserTool.ts` | 工具实现(已实现) |
|
||||
| `src/screens/REPL.tsx:471,5676` | 面板渲染 |
|
||||
| `src/tools.ts:115-116` | 工具注册 |
|
||||
|
||||
@@ -34,16 +34,16 @@ WebSearchTool.call()
|
||||
|
||||
| 模块 | 文件 | 说明 |
|
||||
|------|------|------|
|
||||
| 工具入口 | `src/tools/WebSearchTool/WebSearchTool.ts` | `buildTool()` 定义:schema、权限、执行、输出格式化 |
|
||||
| 工具 prompt | `src/tools/WebSearchTool/prompt.ts` | 搜索工具的系统提示词 |
|
||||
| UI 渲染 | `src/tools/WebSearchTool/UI.tsx` | 搜索结果的终端渲染组件 |
|
||||
| 适配器接口 | `src/tools/WebSearchTool/adapters/types.ts` | `WebSearchAdapter` 接口、`SearchResult`/`SearchOptions`/`SearchProgress` 类型 |
|
||||
| 适配器工厂 | `src/tools/WebSearchTool/adapters/index.ts` | `createAdapter()` 工厂函数,选择后端 |
|
||||
| API 适配器 | `src/tools/WebSearchTool/adapters/apiAdapter.ts` | 封装原有 `queryModelWithStreaming` 逻辑,使用 server tool |
|
||||
| Bing 适配器 | `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 抓取 + 正则解析 |
|
||||
| Brave 适配器 | `src/tools/WebSearchTool/adapters/braveAdapter.ts` | Brave LLM Context API 适配与结果映射 |
|
||||
| 单元测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts`, `src/tools/WebSearchTool/__tests__/braveAdapter*.test.ts`, `src/tools/WebSearchTool/__tests__/adapterFactory.test.ts` | Bing / Brave 解析与工厂逻辑测试 |
|
||||
| 集成测试 | `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts`, `src/tools/WebSearchTool/__tests__/braveAdapter.integration.ts` | 真实网络请求验证 |
|
||||
| 工具入口 | `packages/builtin-tools/src/tools/WebSearchTool/WebSearchTool.ts` | `buildTool()` 定义:schema、权限、执行、输出格式化 |
|
||||
| 工具 prompt | `packages/builtin-tools/src/tools/WebSearchTool/prompt.ts` | 搜索工具的系统提示词 |
|
||||
| UI 渲染 | `packages/builtin-tools/src/tools/WebSearchTool/UI.tsx` | 搜索结果的终端渲染组件 |
|
||||
| 适配器接口 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/types.ts` | `WebSearchAdapter` 接口、`SearchResult`/`SearchOptions`/`SearchProgress` 类型 |
|
||||
| 适配器工厂 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/index.ts` | `createAdapter()` 工厂函数,选择后端 |
|
||||
| API 适配器 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/apiAdapter.ts` | 封装原有 `queryModelWithStreaming` 逻辑,使用 server tool |
|
||||
| Bing 适配器 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 抓取 + 正则解析 |
|
||||
| Brave 适配器 | `packages/builtin-tools/src/tools/WebSearchTool/adapters/braveAdapter.ts` | Brave LLM Context API 适配与结果映射 |
|
||||
| 单元测试 | `packages/builtin-tools/src/tools/WebSearchTool/__tests__/bingAdapter.test.ts`, `packages/builtin-tools/src/tools/WebSearchTool/__tests__/braveAdapter*.test.ts`, `packages/builtin-tools/src/tools/WebSearchTool/__tests__/adapterFactory.test.ts` | Bing / Brave 解析与工厂逻辑测试 |
|
||||
| 集成测试 | `packages/builtin-tools/src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts`, `packages/builtin-tools/src/tools/WebSearchTool/__tests__/braveAdapter.integration.ts` | 真实网络请求验证 |
|
||||
|
||||
### 2.3 数据流
|
||||
|
||||
@@ -176,13 +176,13 @@ interface SearchProgress {
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/tools/WebSearchTool/WebSearchTool.ts` | 工具定义入口 |
|
||||
| `src/tools/WebSearchTool/prompt.ts` | 搜索工具 prompt |
|
||||
| `src/tools/WebSearchTool/UI.tsx` | 终端 UI 渲染 |
|
||||
| `src/tools/WebSearchTool/adapters/types.ts` | 适配器接口 |
|
||||
| `src/tools/WebSearchTool/adapters/index.ts` | 适配器工厂 |
|
||||
| `src/tools/WebSearchTool/adapters/apiAdapter.ts` | API 服务端搜索适配器 |
|
||||
| `src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 解析适配器 |
|
||||
| `src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 单元测试 (32 cases) |
|
||||
| `src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 集成测试 |
|
||||
| `packages/builtin-tools/src/tools/WebSearchTool/WebSearchTool.ts` | 工具定义入口 |
|
||||
| `packages/builtin-tools/src/tools/WebSearchTool/prompt.ts` | 搜索工具 prompt |
|
||||
| `packages/builtin-tools/src/tools/WebSearchTool/UI.tsx` | 终端 UI 渲染 |
|
||||
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/types.ts` | 适配器接口 |
|
||||
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/index.ts` | 适配器工厂 |
|
||||
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/apiAdapter.ts` | API 服务端搜索适配器 |
|
||||
| `packages/builtin-tools/src/tools/WebSearchTool/adapters/bingAdapter.ts` | Bing HTML 解析适配器 |
|
||||
| `packages/builtin-tools/src/tools/WebSearchTool/__tests__/bingAdapter.test.ts` | 单元测试 (32 cases) |
|
||||
| `packages/builtin-tools/src/tools/WebSearchTool/__tests__/bingAdapter.integration.ts` | 集成测试 |
|
||||
| `src/tools.ts` | 工具注册 |
|
||||
|
||||
@@ -14,17 +14,17 @@ WORKFLOW_SCRIPTS 实现基于文件的多步自动化工作流。用户可以定
|
||||
|
||||
| 模块 | 文件 | 状态 |
|
||||
|------|------|------|
|
||||
| WorkflowTool | `src/tools/WorkflowTool/WorkflowTool.ts` | **Stub** — 空对象 |
|
||||
| Workflow 权限 | `src/tools/WorkflowTool/WorkflowPermissionRequest.ts` | **Stub** — 返回 null |
|
||||
| 常量 | `src/tools/WorkflowTool/constants.ts` | **Stub** — 空工具名 |
|
||||
| 命令创建 | `src/tools/WorkflowTool/createWorkflowCommand.ts` | **Stub** — 空操作 |
|
||||
| 捆绑工作流 | `src/tools/WorkflowTool/bundled/` | **缺失** — 目录不存在 |
|
||||
| WorkflowTool | `packages/builtin-tools/src/tools/WorkflowTool/WorkflowTool.ts` | **部分实现** — tool schema + 渲染完整,call 返回运行时缺失提示 |
|
||||
| Workflow 权限 | `packages/builtin-tools/src/tools/WorkflowTool/WorkflowPermissionRequest.tsx` | **部分实现** — 权限请求组件 |
|
||||
| 常量 | `packages/builtin-tools/src/tools/WorkflowTool/constants.ts` | **实现** — 工具名 + 目录名 + 文件扩展名常量 |
|
||||
| 命令创建 | `packages/builtin-tools/src/tools/WorkflowTool/createWorkflowCommand.ts` | **实现** — 扫描 .claude/workflows/ 目录创建 Command 对象 |
|
||||
| 捆绑工作流 | `packages/builtin-tools/src/tools/WorkflowTool/bundled/index.ts` | **实现** — 内置工作流初始化 |
|
||||
| 本地工作流任务 | `src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts` | **Stub** — 类型 + 空操作 |
|
||||
| UI 任务组件 | `src/components/tasks/src/tasks/LocalWorkflowTask/` | **Stub** — 空导出 |
|
||||
| 详情对话框 | `src/components/tasks/WorkflowDetailDialog.ts` | **Stub** — 返回 null |
|
||||
| 任务注册 | `src/tasks.ts` | **布线** — 动态加载 |
|
||||
| 工具注册 | `src/tools.ts` | **布线** — 包含 bundled 工作流初始化 |
|
||||
| 命令注册 | `src/commands.ts` | **布线** — `/workflows` 命令 |
|
||||
| 工具注册 | `src/tools.ts` | **布线** — 动态加载 + bundled 工作流初始化 (行 131-134,235) |
|
||||
| 命令注册 | `src/commands.ts` | **布线** — `/workflows` 命令 (行 93-95,395,460) |
|
||||
|
||||
### 2.2 预期数据流
|
||||
|
||||
@@ -69,13 +69,9 @@ steps:
|
||||
|
||||
| 优先级 | 模块 | 工作量 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| 1 | `WorkflowTool.ts` | 大 | Schema 定义 + 多步执行引擎 |
|
||||
| 2 | `bundled/index.js` | 中 | 内置工作流定义(initBundledWorkflows) |
|
||||
| 3 | `createWorkflowCommand.ts` | 中 | 从文件解析创建命令对象 |
|
||||
| 4 | `LocalWorkflowTask.ts` | 大 | 步骤协调、kill/skip/retry |
|
||||
| 5 | `WorkflowDetailDialog.ts` | 中 | 进度详情 UI |
|
||||
| 6 | `WorkflowPermissionRequest.ts` | 小 | 权限对话框 |
|
||||
| 7 | `constants.ts` | 小 | 工具名常量 |
|
||||
| 1 | `WorkflowTool.ts` call 方法 | 中 | 实际工作流执行逻辑(当前返回运行时缺失提示) |
|
||||
| 2 | `LocalWorkflowTask.ts` | 大 | 步骤协调、kill/skip/retry |
|
||||
| 3 | `WorkflowDetailDialog.ts` | 中 | 进度详情 UI |
|
||||
|
||||
## 四、关键设计决策
|
||||
|
||||
@@ -95,11 +91,12 @@ FEATURE_WORKFLOW_SCRIPTS=1 bun run dev
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `src/tools/WorkflowTool/WorkflowTool.ts` | 工具定义(stub) |
|
||||
| `src/tools/WorkflowTool/WorkflowPermissionRequest.ts` | 权限对话框(stub) |
|
||||
| `src/tools/WorkflowTool/constants.ts` | 常量(stub) |
|
||||
| `src/tools/WorkflowTool/createWorkflowCommand.ts` | 命令创建(stub) |
|
||||
| `packages/builtin-tools/src/tools/WorkflowTool/WorkflowTool.ts` | 工具定义(部分实现) |
|
||||
| `packages/builtin-tools/src/tools/WorkflowTool/WorkflowPermissionRequest.tsx` | 权限请求组件 |
|
||||
| `packages/builtin-tools/src/tools/WorkflowTool/constants.ts` | 常量定义 |
|
||||
| `packages/builtin-tools/src/tools/WorkflowTool/createWorkflowCommand.ts` | 命令创建(已实现) |
|
||||
| `packages/builtin-tools/src/tools/WorkflowTool/bundled/index.ts` | 内置工作流初始化 |
|
||||
| `src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts` | 任务协调(stub) |
|
||||
| `src/components/tasks/WorkflowDetailDialog.ts` | 详情对话框(stub) |
|
||||
| `src/tools.ts:127-132` | 工具注册 |
|
||||
| `src/commands.ts:86-89` | 命令注册 |
|
||||
| `src/tools.ts:131-134,235` | 工具注册 |
|
||||
| `src/commands.ts:93-95,395,460` | 命令注册 |
|
||||
|
||||
564
docs/internals/agent-comm-fix-jira-tasks.md
Normal file
564
docs/internals/agent-comm-fix-jira-tasks.md
Normal file
@@ -0,0 +1,564 @@
|
||||
# Agent 通讯修复 Jira Task
|
||||
|
||||
- 版本:v1.0
|
||||
- 生成日期:2026-04-25
|
||||
- 来源:由按文件执行清单、Claude 交叉验证意见整理合并
|
||||
- 范围:ACP Agent / Bridge / Remote Control Server / REPL Hook 生命周期
|
||||
- 使用方式:这是唯一执行任务文档;每个 `JIRA-*` 小节可直接拆成一个 Jira issue,字段保持统一,便于复制或二次导入。
|
||||
|
||||
---
|
||||
|
||||
## 方案性质
|
||||
|
||||
本文档是目标状态式执行方案,不是临时补丁清单。每张 ticket 必须交付明确的代码终态、测试覆盖和回归边界;不得只用局部 workaround 掩盖问题。
|
||||
|
||||
---
|
||||
|
||||
## 执行总则
|
||||
|
||||
1. 先边界安全,后内部优化:先修 WS 入站大小与输入校验,避免线上风险扩大。
|
||||
2. 单文件可回滚:每个文件内修改保持内聚,便于回滚与 bisect。
|
||||
3. 不改协议语义,只修实现缺陷:除 `resource_link` 表达形式统一外,不改变主流程契约。
|
||||
4. 每个文件必须有验收输出:要么测试用例,要么日志/指标验证。
|
||||
5. 发布前必须确认协议层行为无回归:`stopReason` 决策与 `sessionUpdate` 发送顺序保持稳定。
|
||||
|
||||
---
|
||||
|
||||
## Epic
|
||||
|
||||
### JIRA-EPIC-001:提升 Agent 通讯链路稳定性与边界安全
|
||||
|
||||
- Issue Type:Epic
|
||||
- Priority:P0
|
||||
- Owner:核心通讯 / 后端网关 / QA
|
||||
- Scope:ACP Agent、ACP Bridge、Remote Control Server、REPL 初始化生命周期
|
||||
- Goal:修复长会话资源泄漏、补齐 WebSocket 入站边界、统一 prompt 转换、收敛类型风险,并补充关键回归测试。
|
||||
|
||||
#### Epic 验收标准
|
||||
|
||||
- `bun run typecheck` 0 error。
|
||||
- P0 WebSocket 超大消息拒绝逻辑已实现并覆盖测试。
|
||||
- ACP bridge abort listener 生命周期无累积。
|
||||
- prompt 转换实现单源化。
|
||||
- settings/defaultMode 能真实影响 ACP permission mode,且 `_meta.permissionMode` 保持最高优先级。
|
||||
- REPL 目标 hook suppress 清理完成,timer cleanup 完整。
|
||||
|
||||
---
|
||||
|
||||
## P0 Tickets
|
||||
|
||||
### JIRA-001:为 session ingress WebSocket 补齐消息大小限制
|
||||
|
||||
- Issue Type:Bug
|
||||
- Priority:P0
|
||||
- Story Points:3
|
||||
- Owner:后端/网关
|
||||
- Files:
|
||||
- `packages/remote-control-server/src/routes/v1/session-ingress.ts`
|
||||
- 后续票:JIRA-008(同文件 P1 类型与 decode path 收尾)
|
||||
|
||||
#### 参考代码位置
|
||||
|
||||
- `packages/remote-control-server/src/routes/v1/session-ingress.ts:100-106`
|
||||
|
||||
#### 背景
|
||||
|
||||
`session-ingress` 当前缺少 WebSocket message size limit。ACP 路由已有类似限制,两个入口边界不一致,可能导致大包占用内存或绕过入口保护。
|
||||
|
||||
#### 实施要求
|
||||
|
||||
- 新增 `MAX_WS_MESSAGE_SIZE = 10 * 1024 * 1024`,与 ACP 路由的 10MB 上限保持一致。
|
||||
- 在 `onMessage` decode 后优先检查 payload size。
|
||||
- 超限时执行 `ws.close(1009, "message too large")`。
|
||||
- 日志记录 `sessionId`、payload size、limit。
|
||||
- 对 `string`、`ArrayBuffer`、`Uint8Array` 进行统一 decode 分流。
|
||||
- 非支持类型直接拒绝并记录,不进入业务 handler。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
- 11MB payload 被 1009 close。
|
||||
- 1KB 合法 payload 仍正常进入 handler。
|
||||
- 非支持类型 payload 不进入 handler。
|
||||
- 不改变 URL、auth、session 解析逻辑。
|
||||
|
||||
#### 回归范围
|
||||
|
||||
- Remote Control Server session ingress WebSocket。
|
||||
- 正常会话消息转发。
|
||||
- WebSocket close code 行为。
|
||||
|
||||
#### 风险等级
|
||||
|
||||
- 中。入口逻辑变更可能影响特殊客户端 payload 类型。
|
||||
|
||||
#### 必须验证
|
||||
|
||||
- 在 `packages/remote-control-server/src/__tests__/routes.test.ts` 增加 session-ingress WebSocket 大包、小包、坏类型 payload 用例。
|
||||
- 运行 `bun run typecheck`。
|
||||
|
||||
---
|
||||
|
||||
### JIRA-002:修复 ACP bridge abort listener 生命周期泄漏
|
||||
|
||||
- Issue Type:Bug
|
||||
- Priority:P0
|
||||
- Story Points:3
|
||||
- Owner:核心通讯
|
||||
- Files:
|
||||
- `src/services/acp/bridge.ts`
|
||||
|
||||
#### 参考代码位置
|
||||
|
||||
- `src/services/acp/bridge.ts:576-585`
|
||||
|
||||
#### 背景
|
||||
|
||||
ACP bridge 的 `Promise.race` abort 分支注册 listener 后缺少完整 cleanup。长会话或高频 next 场景可能出现 listener 累积。
|
||||
|
||||
#### 实施要求
|
||||
|
||||
- 将 abort race 改为可清理监听器写法。
|
||||
- 注册 listener 后保留 handler 引用。
|
||||
- `sdkMessages.next()` 先返回时必须 `removeEventListener`。
|
||||
- abort、throw、return 等路径都在 `finally` 中清理。
|
||||
- 不改变 `stopReason` 决策逻辑。
|
||||
- 不改变 `sessionUpdate` 发送顺序。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
- 模拟 10k 次 next 且不 abort,listener 不增长。
|
||||
- abort 场景仍返回 `cancelled`。
|
||||
- 原有 streaming/session update 行为无回归。
|
||||
|
||||
#### 回归范围
|
||||
|
||||
- ACP bridge streaming loop。
|
||||
- 用户取消请求。
|
||||
- SDK generator 异常路径。
|
||||
|
||||
#### 风险等级
|
||||
|
||||
- 中。异步控制流变更需要覆盖取消与异常路径。
|
||||
|
||||
#### 必须验证
|
||||
|
||||
- 新增 listener cleanup 单元测试。
|
||||
- 运行 `bun run typecheck`。
|
||||
|
||||
---
|
||||
|
||||
## P1 Tickets
|
||||
|
||||
### JIRA-003:优化 ACP agent pending prompt 队列为 O(1) 出队
|
||||
|
||||
- Issue Type:Task
|
||||
- Priority:P1
|
||||
- Story Points:5
|
||||
- Owner:核心通讯
|
||||
- Files:
|
||||
- `src/services/acp/agent.ts`
|
||||
|
||||
#### 参考代码位置
|
||||
|
||||
- `src/services/acp/agent.ts:332-339`
|
||||
|
||||
#### 背景
|
||||
|
||||
当前 pending prompt 队列使用 `Map + sort` 获取下一项,排队量上升时会带来不必要的排序成本。
|
||||
|
||||
#### 实施要求
|
||||
|
||||
- 改为 `queue: string[]` + `pendingMap: Map<string, PendingPrompt>` 组合。
|
||||
- 入队执行 `queue.push(id)` 与 `pendingMap.set(id, prompt)`。
|
||||
- 出队从队首惰性跳过已取消项。
|
||||
- 取消只从 `pendingMap` 删除,不做数组中间删除。
|
||||
- 保持现有取消语义和出队顺序。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
- 1000 pending prompt 场景下出队顺序正确。
|
||||
- 已取消 prompt 不会被 resolve。
|
||||
- 出队不再依赖全量 sort。
|
||||
- 1000 排队场景下出队耗时低于旧实现;测试记录旧实现复杂度风险和新实现 O(1) 出队路径。
|
||||
- 行为与旧实现兼容。
|
||||
|
||||
#### 回归范围
|
||||
|
||||
- ACP prompt queue。
|
||||
- 并发 prompt 请求。
|
||||
- prompt cancel / resolve 边界。
|
||||
|
||||
#### 风险等级
|
||||
|
||||
- 中。队列结构变更可能引入取消边界问题。
|
||||
|
||||
#### 必须验证
|
||||
|
||||
- 新增 queue 顺序与取消测试。
|
||||
- 对 1000 prompt 场景做性能断言或日志记录。
|
||||
|
||||
---
|
||||
|
||||
### JIRA-004:接入真实 settings 读取并校验 ACP permission mode
|
||||
|
||||
- Issue Type:Bug
|
||||
- Priority:P1
|
||||
- Story Points:3
|
||||
- Owner:核心通讯
|
||||
- Files:
|
||||
- `src/services/acp/agent.ts`
|
||||
|
||||
#### 参考代码位置
|
||||
|
||||
- `src/services/acp/agent.ts:465-467`
|
||||
|
||||
#### 背景
|
||||
|
||||
`getSetting()` 当前未真正接入项目配置,导致默认 permission mode 配置无法按预期生效。
|
||||
|
||||
#### 实施要求
|
||||
|
||||
- 接入项目现有 settings/config 读取逻辑。
|
||||
- 仅接受合法 permission mode 枚举值。
|
||||
- 非法值 fallback 到 `default`。
|
||||
- `_meta.permissionMode` 继续保持最高优先级。
|
||||
- 不改变外部协议字段。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
- settings/defaultMode 能影响默认 permission mode。
|
||||
- `_meta.permissionMode` 能覆盖 settings。
|
||||
- 非法 settings 值不会传播到运行时。
|
||||
- 类型检查通过。
|
||||
|
||||
#### 回归范围
|
||||
|
||||
- ACP agent session 初始化。
|
||||
- 权限模式同步。
|
||||
- 客户端 `_meta` 覆盖逻辑。
|
||||
|
||||
#### 风险等级
|
||||
|
||||
- 中。配置优先级错误会影响权限行为。
|
||||
|
||||
#### 必须验证
|
||||
|
||||
- 新增 defaultMode / `_meta.permissionMode` 优先级测试。
|
||||
- 运行 `bun run typecheck`。
|
||||
|
||||
---
|
||||
|
||||
### JIRA-005:单源化 ACP prompt 转换逻辑
|
||||
|
||||
- Issue Type:Refactor
|
||||
- Priority:P1
|
||||
- Story Points:5
|
||||
- Owner:核心通讯
|
||||
- Files:
|
||||
- `src/services/acp/agent.ts`
|
||||
- `src/services/acp/bridge.ts`
|
||||
- `src/services/acp/promptConversion.ts`(新增)
|
||||
|
||||
#### 参考代码位置
|
||||
|
||||
- `src/services/acp/agent.ts:754-758`
|
||||
- `src/services/acp/agent.ts:764-785`
|
||||
- `src/services/acp/bridge.ts:522-537`
|
||||
|
||||
#### 背景
|
||||
|
||||
ACP agent 与 bridge 存在重复 prompt 转换逻辑,`resource_link` 等 block 的输出策略容易分叉。
|
||||
|
||||
#### 实施要求
|
||||
|
||||
- 新增共享转换模块 `src/services/acp/promptConversion.ts`。
|
||||
- `agent.ts` 与 `bridge.ts` 改为调用共享转换函数。
|
||||
- 删除 `bridge.ts` 中 `promptToQueryContent` 的真实实现;如导出仍需保留,则只允许保留调用共享函数的 wrapper。
|
||||
- `resource_link` 输出改为稳定纯文本元信息,禁止 markdown link。
|
||||
- 保持其他 block 转换语义不变。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
- 全仓库仅保留一个真实 prompt 转换实现。
|
||||
- 相同 input block 在 agent/bridge 输出一致。
|
||||
- `resource_link` 不再输出 `[name](uri)` 形式。
|
||||
- 相关测试覆盖转换一致性。
|
||||
|
||||
#### 回归范围
|
||||
|
||||
- ACP prompt input。
|
||||
- bridge query content。
|
||||
- resource link prompt 表达。
|
||||
|
||||
#### 风险等级
|
||||
|
||||
- 中。文本格式变化可能影响下游 prompt 快照或断言。
|
||||
|
||||
#### 必须验证
|
||||
|
||||
- 新增 shared conversion 单元测试。
|
||||
- 全仓库搜索重复转换函数。
|
||||
- 运行 `bun run typecheck`。
|
||||
|
||||
---
|
||||
|
||||
### JIRA-006:治理 REPL onInit effect 依赖并补齐 timer cleanup
|
||||
|
||||
- Issue Type:Task
|
||||
- Priority:P1
|
||||
- Story Points:3
|
||||
- Owner:终端 UI
|
||||
- Files:
|
||||
- `src/screens/REPL.tsx`
|
||||
|
||||
#### 参考代码位置
|
||||
|
||||
- `src/screens/REPL.tsx:654-662`
|
||||
- `src/screens/REPL.tsx:4996-5005`
|
||||
|
||||
#### 背景
|
||||
|
||||
REPL 中目标初始化 effect 存在 hook dependency suppress,warm-up timer 也需要显式 cleanup,避免频繁挂载/卸载时留下悬挂任务。
|
||||
|
||||
#### 实施要求
|
||||
|
||||
- 整理 `onInit` 生命周期,使用稳定引用或 effect 内联。
|
||||
- 移除目标段 `exhaustive-deps` suppress。
|
||||
- 保持 unmount cleanup 行为不变。
|
||||
- warm-up effect 中记录 timeout id。
|
||||
- cleanup 中执行 `clearTimeout(timeoutId)`。
|
||||
- 保留 `alive` 判定作为并发保护。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
- 目标段不再需要 hooks lint suppress。
|
||||
- 高频打开/关闭搜索栏无悬挂 timer 增长。
|
||||
- REPL 初始化行为无回归。
|
||||
|
||||
#### 回归范围
|
||||
|
||||
- REPL 初始化。
|
||||
- 搜索栏 warm-up。
|
||||
- 组件卸载 cleanup。
|
||||
|
||||
#### 风险等级
|
||||
|
||||
- 中。React effect 依赖治理可能改变初始化时机。
|
||||
|
||||
#### 必须验证
|
||||
|
||||
- 运行 lint/typecheck。
|
||||
- 手动或测试覆盖 REPL mount/unmount。
|
||||
|
||||
---
|
||||
|
||||
### JIRA-007:收敛 ACP route WebSocket 事件 any 类型
|
||||
|
||||
- Issue Type:Task
|
||||
- Priority:P1
|
||||
- Story Points:2
|
||||
- Owner:后端/网关
|
||||
- Files:
|
||||
- `packages/remote-control-server/src/routes/acp/index.ts`
|
||||
|
||||
#### 参考代码位置
|
||||
|
||||
- `packages/remote-control-server/src/routes/acp/index.ts:108-146`
|
||||
|
||||
#### 背景
|
||||
|
||||
ACP route 中 WebSocket 事件和 socket 参数存在 `any`,降低编译期保护。
|
||||
|
||||
#### 实施要求
|
||||
|
||||
- 定义最小 WebSocket 事件类型:open/message/close/error。
|
||||
- 将 `_evt: any`、`evt: any`、`ws: any` 替换为窄类型。
|
||||
- 不改变 payload decode 与大小检查策略。
|
||||
- 不改变现有 handler 行为。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
- 编译期能捕获错误事件字段访问。
|
||||
- 现有 WebSocket 行为不变。
|
||||
- `bun run typecheck` 通过。
|
||||
|
||||
#### 回归范围
|
||||
|
||||
- ACP WebSocket route。
|
||||
- message decode。
|
||||
- close/error handler。
|
||||
|
||||
#### 风险等级
|
||||
|
||||
- 低。类型收敛为主。
|
||||
|
||||
#### 必须验证
|
||||
|
||||
- 运行 `bun run typecheck`。
|
||||
- 保留现有测试通过。
|
||||
|
||||
---
|
||||
|
||||
### JIRA-008:收敛 session ingress WebSocket 事件类型与 decode path
|
||||
|
||||
- Issue Type:Task
|
||||
- Priority:P1
|
||||
- Story Points:3
|
||||
- Owner:后端/网关
|
||||
- Files:
|
||||
- `packages/remote-control-server/src/routes/v1/session-ingress.ts`
|
||||
- 前置依赖:JIRA-001 已合并
|
||||
|
||||
#### 参考代码位置
|
||||
|
||||
- `packages/remote-control-server/src/routes/v1/session-ingress.ts:100-106`
|
||||
|
||||
#### 背景
|
||||
|
||||
在完成 P0 size guard 后,session ingress 仍需要进一步收敛事件类型与 decode path,减少隐式类型风险。
|
||||
|
||||
#### 实施要求
|
||||
|
||||
- 定义或复用最小 WebSocket message event 类型。
|
||||
- 将 message decode 分支集中到一个小函数。
|
||||
- 保持 P0 size guard 与 close code 语义。
|
||||
- 不改变 auth/session 解析。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
- decode path 单一清晰。
|
||||
- 不支持 payload 类型有明确拒绝路径。
|
||||
- `bun run typecheck` 通过。
|
||||
|
||||
#### 回归范围
|
||||
|
||||
- Session ingress WebSocket message handling。
|
||||
- P0 大包拒绝逻辑。
|
||||
|
||||
#### 风险等级
|
||||
|
||||
- 低到中。与 P0 同文件,注意避免重复改动冲突。
|
||||
|
||||
#### 必须验证
|
||||
|
||||
- 与 JIRA-001 同批测试。
|
||||
- 运行 `bun run typecheck`。
|
||||
|
||||
---
|
||||
|
||||
## QA Tickets
|
||||
|
||||
### JIRA-009:补充 ACP 通讯回归测试
|
||||
|
||||
- Issue Type:Test
|
||||
- Priority:P1
|
||||
- Story Points:5
|
||||
- Owner:QA/核心通讯
|
||||
- Files:
|
||||
- `src/services/acp/agent.ts`
|
||||
- `src/services/acp/bridge.ts`
|
||||
- `src/services/acp/promptConversion.ts`
|
||||
- `src/services/acp/__tests__/agent.test.ts`
|
||||
- `src/services/acp/__tests__/bridge.test.ts`
|
||||
- `src/services/acp/__tests__/promptConversion.test.ts`
|
||||
|
||||
#### 覆盖场景
|
||||
|
||||
- 长会话 10k turn,无 abort listener 累积。
|
||||
- prompt queue 1000 并发排队,取消/出队顺序正确。
|
||||
- settings/defaultMode 与 `_meta.permissionMode` 优先级正确。
|
||||
- `resource_link` 转换在 agent 与 bridge 输出一致。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
- 新增测试在本地稳定通过。
|
||||
- 不依赖真实网络或外部服务。
|
||||
- 测试 mock 遵守仓库规范,只 mock 有副作用链路。
|
||||
|
||||
#### 回归范围
|
||||
|
||||
- ACP bridge。
|
||||
- ACP agent。
|
||||
- prompt conversion。
|
||||
- permission mode resolution。
|
||||
|
||||
#### 风险等级
|
||||
|
||||
- 中。异步测试可能有稳定性问题,需要避免时间敏感断言。
|
||||
|
||||
#### 必须验证
|
||||
|
||||
- 运行相关 `bun test`。
|
||||
- 运行 `bun run typecheck`。
|
||||
|
||||
---
|
||||
|
||||
### JIRA-010:补充 Remote Control Server WebSocket 入站回归测试
|
||||
|
||||
- Issue Type:Test
|
||||
- Priority:P1
|
||||
- Story Points:3
|
||||
- Owner:QA/后端
|
||||
- Files:
|
||||
- `packages/remote-control-server/src/__tests__/routes.test.ts`
|
||||
- `packages/remote-control-server/src/routes/v1/session-ingress.ts`
|
||||
|
||||
#### 覆盖场景
|
||||
|
||||
- 11MB session ingress payload 被 1009 close(与 10MB 上限对齐)。
|
||||
- 合法小 payload 正常进入 handler。
|
||||
- 非支持 payload 类型被拒绝。
|
||||
- 日志或可观测输出包含 sessionId、payload size、limit。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
- 11MB payload 被 1009 close(与 10MB 上限对齐)。
|
||||
- 新增测试稳定通过。
|
||||
- 不启动真实外部服务。
|
||||
- 不改变现有 route public contract。
|
||||
|
||||
#### 回归范围
|
||||
|
||||
- RCS session ingress route。
|
||||
- WebSocket message handling。
|
||||
- close code 行为。
|
||||
|
||||
#### 风险等级
|
||||
|
||||
- 中。测试需要适配现有 WebSocket/mock 基础设施。
|
||||
|
||||
#### 必须验证
|
||||
|
||||
- 运行 RCS package 相关测试。
|
||||
- 运行 `bun run typecheck`。
|
||||
|
||||
---
|
||||
|
||||
## 推荐执行顺序
|
||||
|
||||
执行节奏与原计划保持一致:先完成 P0 全部改动和冒烟验证,再启动 P1 改造;测试票可穿插执行,但不得绕过 P0 gate。
|
||||
|
||||
1. JIRA-001:先封入口大包风险。
|
||||
2. JIRA-002:修长会话 listener 生命周期。
|
||||
3. JIRA-010:补 RCS 入站测试,锁住 P0 行为。
|
||||
4. JIRA-003:优化 pending prompt queue。
|
||||
5. JIRA-004:接入 settings/defaultMode。
|
||||
6. JIRA-005:单源化 prompt 转换。
|
||||
7. JIRA-009:补 ACP 回归测试。
|
||||
8. JIRA-006:治理 REPL effect/timer。
|
||||
9. JIRA-007:收敛 ACP route 类型。
|
||||
10. JIRA-008:收敛 session ingress 类型与 decode path。
|
||||
|
||||
---
|
||||
|
||||
## Release Checklist
|
||||
|
||||
- [ ] `bun run typecheck` 0 error
|
||||
- [ ] P0 tickets 已合并并测试通过
|
||||
- [ ] ACP 回归测试通过
|
||||
- [ ] RCS WebSocket 入站测试通过
|
||||
- [ ] prompt conversion 单源化已通过代码搜索确认
|
||||
- [ ] permission mode 优先级测试通过
|
||||
- [ ] 协议层行为无回归(stopReason 决策、sessionUpdate 发送顺序)
|
||||
- [ ] REPL hook/timer 改动通过 lint/typecheck
|
||||
- [ ] 最终变更说明包含风险与未覆盖项
|
||||
74
docs/internals/agent-comm-fix-questions.md
Normal file
74
docs/internals/agent-comm-fix-questions.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Agent 通讯修复问题文档
|
||||
|
||||
- 版本:v1.0
|
||||
- 生成日期:2026-04-25
|
||||
- 范围:ACP Agent / Bridge / Remote Control Server / REPL Hook 生命周期
|
||||
- 配套执行文档:`docs/internals/agent-comm-fix-jira-tasks.md`
|
||||
- 目的:保留决策前要问的问题、交叉验证提示词和已确认结论;不要在这里写 Jira 执行步骤。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前已确认结论
|
||||
|
||||
- 只保留两份交付文档:本问题文档 + Jira Task 文档。
|
||||
- Jira Task 文档是唯一执行入口,包含 Owner、优先级、文件范围、验收标准、风险和验证建议。
|
||||
- Claude 交叉验证结论:整体通过,无 blocking findings;建议补充协议回归 gate、JIRA-001/008 依赖、代码参考位置和阈值一致性,这些建议已合并到 Jira Task 文档。
|
||||
- 本次已进入业务代码修复阶段,必须运行 `bun run typecheck` 和相关回归测试。
|
||||
|
||||
---
|
||||
|
||||
## 2. 执行前必须问清的问题
|
||||
|
||||
1. `session-ingress` 的 WebSocket 上限是否固定为 10MB,并与 ACP route 保持一致?
|
||||
2. 超限 close code 是否统一使用 `1009`,close reason 是否固定为 `message too large`?
|
||||
3. `resource_link` 的纯文本格式是否已有下游依赖,能否替代当前 markdown link 表达?
|
||||
4. ACP permission mode 的真实 settings key 是哪个,非法值 fallback 是否统一为 `default`?
|
||||
5. `_meta.permissionMode` 是否必须始终覆盖 settings/defaultMode?
|
||||
6. abort listener 测试中,是否能通过 mock signal 或计数器稳定证明 10k next 后无 listener 累积?
|
||||
7. pending prompt queue 的取消语义是否允许惰性清理,而不是立刻从数组中删除?
|
||||
8. REPL hook suppress 的清理范围是否只限目标段,不顺手改其他 decompiled React Compiler 结构?
|
||||
9. RCS WebSocket 测试应放在现有哪个 `__tests__` 布局下,是否已有 route/mock 基础设施可复用?
|
||||
10. 发布 gate 是否必须包含 `stopReason` 决策与 `sessionUpdate` 发送顺序不回归?
|
||||
|
||||
---
|
||||
|
||||
## 3. 给 Claude 或 Reviewer 的复核问题
|
||||
|
||||
```text
|
||||
请作为外部审查者,复核 docs/internals/agent-comm-fix-jira-tasks.md。
|
||||
|
||||
请检查:
|
||||
1. 是否仍满足“按文件分工的执行清单”和“Jira task 文档”要求。
|
||||
2. 是否存在遗漏的文件、验收标准、风险或前置依赖。
|
||||
3. 是否有重复、误导执行者、优先级不合理或测试不可落地的问题。
|
||||
4. 是否还有必须阻断实施的 finding。
|
||||
|
||||
请用中文输出:
|
||||
- Verdict
|
||||
- Blocking Findings
|
||||
- Non-blocking Findings
|
||||
- Suggested Edits
|
||||
- Final Recommendation
|
||||
|
||||
不要修改文件,只输出审查意见。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 已处理的复核建议
|
||||
|
||||
- Release Checklist 已补充协议层行为无回归 gate。
|
||||
- JIRA-001 与 JIRA-008 已明确同文件前后置关系。
|
||||
- JIRA-001 到 JIRA-008 已补充参考代码位置。
|
||||
- JIRA-003 已补回 1000 排队场景下的出队耗时验收。
|
||||
- JIRA-008 story points 已从 2 调整为 3。
|
||||
- JIRA-010 已明确 11MB payload 对齐 10MB 上限并触发 1009 close。
|
||||
- 推荐执行顺序已明确 P0 gate:P0 全部改动和冒烟验证完成后,再启动 P1 改造。
|
||||
|
||||
---
|
||||
|
||||
## 5. 不在本文档维护的内容
|
||||
|
||||
- 不维护 Jira ticket 正文;统一在 `docs/internals/agent-comm-fix-jira-tasks.md` 修改。
|
||||
- 不维护业务代码实现方案;实现时按具体 ticket 读取对应文件。
|
||||
- 不维护历史中间稿;旧执行清单已合并进 Jira Task 文档。
|
||||
@@ -17,7 +17,7 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic
|
||||
|
||||
`BUILD_TARGET` 等构建时常量在反编译版本中已被移除。`USER_TYPE` 通过 Bun 的 `--define` 或环境变量注入,Bun 会进行**常量折叠**——所有 `process.env.USER_TYPE === 'ant'` 在外部构建中直接变为 `false`,后续代码被 DCE 移除。但在反编译版本中,这些代码保留完整。
|
||||
|
||||
`USER_TYPE === 'ant'` 在代码库中出现 **377+ 次**(含 `=== 'ant'` 291 次、`(process.env.USER_TYPE) === 'ant'` 86 次),另有 `!== 'ant'` 53 次、其他引用约 35 次,总计 **465 处引用**,控制着工具、命令、API、UI 等方方面面。
|
||||
`USER_TYPE === 'ant'` 在代码库中出现 **351+ 次**(跨 163 个文件),另有 `!== 'ant'` 59 次(跨 38 个文件),总计 **410+ 处引用**,控制着工具、命令、API、UI 等方方面面。
|
||||
|
||||
## Ant-Only 工具
|
||||
|
||||
@@ -25,10 +25,10 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic
|
||||
|
||||
| 工具 | 代码位置 | 用途 |
|
||||
|------|---------|------|
|
||||
| **REPLTool** | `src/tools/REPLTool/` | 高级 REPL 模式——在 VM 中包装 Bash/Read/Edit/Glob/Grep/Agent 等工具 |
|
||||
| **SuggestBackgroundPRTool** | `src/tools/SuggestBackgroundPRTool/` | 建议在后台创建 PR |
|
||||
| **ConfigTool** | `src/tools/ConfigTool/` | 交互式配置编辑器,包含 Gates 标签页用于覆盖 GrowthBook flags |
|
||||
| **TungstenTool** | `src/tools/TungstenTool/` | 基于 tmux 的终端面板工具(反编译版中已 stub) |
|
||||
| **REPLTool** | `packages/builtin-tools/src/tools/REPLTool/` | 高级 REPL 模式——在 VM 中包装 Bash/Read/Edit/Glob/Grep/Agent 等工具 |
|
||||
| **SuggestBackgroundPRTool** | `packages/builtin-tools/src/tools/SuggestBackgroundPRTool/` | 建议在后台创建 PR |
|
||||
| **ConfigTool** | `packages/builtin-tools/src/tools/ConfigTool/` | 交互式配置编辑器,包含 Gates 标签页用于覆盖 GrowthBook flags |
|
||||
| **TungstenTool** | `packages/builtin-tools/src/tools/TungstenTool/` | 基于 tmux 的终端面板工具(反编译版中已 stub) |
|
||||
|
||||
```typescript
|
||||
// src/tools.ts 第 14-24 行——条件导入 + Dead Code Elimination 标记
|
||||
@@ -36,18 +36,18 @@ keywords: ["Ant 特权", "USER_TYPE", "身份门控", "内部功能", "Anthropic
|
||||
/* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
|
||||
const REPLTool =
|
||||
process.env.USER_TYPE === 'ant'
|
||||
? require('./tools/REPLTool/REPLTool.js').REPLTool
|
||||
? require('@claude-code-best/builtin-tools/tools/REPLTool/REPLTool.js').REPLTool
|
||||
: null
|
||||
const SuggestBackgroundPRTool =
|
||||
process.env.USER_TYPE === 'ant'
|
||||
? require('./tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js')
|
||||
? require('@claude-code-best/builtin-tools/tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js')
|
||||
.SuggestBackgroundPRTool
|
||||
: null
|
||||
```
|
||||
|
||||
## Ant-Only 命令
|
||||
|
||||
`src/commands.ts` 注册了 **28** 个仅在内部构建中可用的斜杠命令(`INTERNAL_ONLY_COMMANDS`,lines 225-254),在 `USER_TYPE === 'ant' && !IS_DEMO` 时才加载(line 343-345):
|
||||
`src/commands.ts` 注册了 **24+** 个仅在内部构建中可用的斜杠命令(`INTERNAL_ONLY_COMMANDS`,lines 267-295),在 `USER_TYPE === 'ant' && !IS_DEMO` 时才加载(line 400-401):
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="调试类">
|
||||
@@ -74,7 +74,7 @@ const SuggestBackgroundPRTool =
|
||||
- `summary` — 生成摘要
|
||||
- `subscribePr` — 订阅 PR(需要 `KAIROS_GITHUB_WEBHOOKS` feature flag)
|
||||
- `forceSnip` — 强制截断历史(需要 `HISTORY_SNIP` feature flag)
|
||||
- `ultraplan` — 超级规划(需要 `ULTRAPLAN` feature flag)
|
||||
- `ultraplan` — 超级规划(需要 `ULTRAPLAN` feature flag,单独注册于 `commands.ts:396`)
|
||||
</Accordion>
|
||||
<Accordion title="基础设施类">
|
||||
- `backfillSessions` — 回填会话数据
|
||||
|
||||
@@ -15,17 +15,19 @@ Claude Code 使用 Bun 打包器的 `bun:bundle` 模块提供编译时特性门
|
||||
import { feature } from 'bun:bundle'
|
||||
|
||||
const SleepTool = feature('PROACTIVE') || feature('KAIROS')
|
||||
? require('./tools/SleepTool/SleepTool.js').SleepTool
|
||||
? require('@claude-code-best/builtin-tools/tools/SleepTool/SleepTool.js').SleepTool
|
||||
: null
|
||||
```
|
||||
|
||||
在 Anthropic 的内部构建中,`feature()` 在打包时被求值——返回 `true` 的代码会被保留,返回 `false` 的代码会被 **Dead Code Elimination (DCE)** 彻底移除。
|
||||
|
||||
在我们的反编译版本中,这个函数被兜底为:
|
||||
在我们的反编译版本中,`feature` 从 `bun:bundle` 导入(声明在 `src/types/internal-modules.d.ts`),在运行时始终返回 `false`:
|
||||
|
||||
```typescript
|
||||
// src/entrypoints/cli.tsx 第 3 行
|
||||
const feature = (_name: string) => false;
|
||||
// src/types/internal-modules.d.ts
|
||||
declare module 'bun:bundle' {
|
||||
export function feature(name: string): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
这意味着所有 88+ 个 feature flag 后的代码**在运行时永远不会执行**,但代码本身完整保留,可以阅读和分析。
|
||||
@@ -79,7 +81,7 @@ Feature flags 在代码中主要有三种使用模式:
|
||||
```typescript
|
||||
// src/tools.ts — 最常见的模式
|
||||
const MonitorTool = feature('MONITOR_TOOL')
|
||||
? require('./tools/MonitorTool/MonitorTool.js').MonitorTool
|
||||
? require('@claude-code-best/builtin-tools/tools/MonitorTool/MonitorTool.js').MonitorTool
|
||||
: null
|
||||
```
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ keywords: ["GrowthBook", "A/B 测试", "运行时门控", "tengu", "渐进式发
|
||||
|
||||
## 集成架构
|
||||
|
||||
GrowthBook 的完整实现位于 `src/services/analytics/growthbook.ts`(1156 行),工作流程如下:
|
||||
GrowthBook 的完整实现位于 `src/services/analytics/growthbook.ts`(1258 行),工作流程如下:
|
||||
|
||||
<Steps>
|
||||
<Step title="启动时获取远程配置">
|
||||
|
||||
@@ -84,7 +84,7 @@ keywords: ["隐藏功能", "未公开功能", "秘密功能", "Claude Code 彩
|
||||
<Accordion title="VOICE_MODE:语音交互">
|
||||
**门控**: `feature('VOICE_MODE')`
|
||||
|
||||
代码中存在语音输入模式的注册点,但核心实现依赖于 `audio-napi` 包(在反编译版本中已 stub):
|
||||
代码中存在语音输入模式的注册点,核心实现依赖 `audio-capture-napi` 包(已恢复):
|
||||
|
||||
- 通过 `/voice` 命令激活
|
||||
- "按住说话"(hold-to-talk)交互模式
|
||||
|
||||
@@ -64,24 +64,27 @@ Claude Code 从上到下分为五个层次,每一层职责清晰、边界分
|
||||
needsFollowUp ? continue : return { reason }
|
||||
```
|
||||
|
||||
完整的状态机通过 `State` 类型(`src/query.ts:204`)在迭代间传递,包含 10 个字段(messages、autoCompactTracking、maxOutputTokensRecoveryCount 等)。
|
||||
完整的状态机通过 `State` 类型(`src/query.ts:207`)在迭代间传递,包含 10 个字段(messages、autoCompactTracking、maxOutputTokensRecoveryCount 等)。
|
||||
|
||||
### 4. 工具层(`src/tools.ts` → `src/Tool.ts`)
|
||||
|
||||
`getAllBaseTools()`(`src/tools.ts:191`)组装 50+ 工具列表,经过 `filterToolsByDenyRules()` 权限过滤后传给 API。
|
||||
`getAllBaseTools()`(`src/tools.ts:195`)组装 50+ 工具列表,经过 `filterToolsByDenyRules()` 权限过滤后传给 API。
|
||||
|
||||
每个工具实现 `Tool<Input, Output, Progress>` 接口(`src/Tool.ts:362`),核心方法链:
|
||||
每个工具实现 `Tool<Input, Output, Progress>` 接口(`src/Tool.ts:368`),核心方法链:
|
||||
```
|
||||
validateInput() → canUseTool()(UI 层)→ checkPermissions() → call() → ToolResult
|
||||
```
|
||||
|
||||
### 5. 通信层(`src/services/api/claude.ts`)
|
||||
|
||||
API 客户端支持 4 种 Provider:
|
||||
- **Anthropic Direct**:默认
|
||||
API 客户端支持 7 种 Provider:
|
||||
- **Anthropic Direct (firstParty)**:默认
|
||||
- **AWS Bedrock**:`ANTHROPIC_BEDROCK_BASE_URL`
|
||||
- **Google Vertex**:`ANTHROPIC_VERTEX_PROJECT_ID`
|
||||
- **Azure**:通过自定义 base URL
|
||||
- **Foundry**:`ANTHROPIC_CODE_USE_FOUNDRY`
|
||||
- **OpenAI**:兼容层
|
||||
- **Gemini**:兼容层
|
||||
- **Grok (xAI)**:兼容层
|
||||
|
||||
`deps.callModel()` 发起流式请求,返回 `BetaRawMessageStreamEvent` 事件流。支持 Prompt Cache(`cache_control`)、thinking blocks、multi-turn tool use。
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ Claude Code 是一个**运行在本地终端中的 agentic coding system**。它
|
||||
│ 实际执行: 读文件、运行命令、搜索代码... │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 6. 通信层 (claude.ts → Anthropic API) │
|
||||
│ 流式 HTTP, 支持 Bedrock/Vertex/Azure 多 provider │
|
||||
│ 流式 HTTP, 支持 Bedrock/Vertex/Foundry 等 7 种 provider │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ AI 没有真正的"记忆",Claude Code 通过精心分层营造了这个幻觉
|
||||
|
||||
### 3. 工具系统的权限双轨制
|
||||
|
||||
`src/tools/BashTool/shouldUseSandbox.ts` 展示了一个精巧的双重安全模型:
|
||||
`packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.ts` 展示了一个精巧的双重安全模型:
|
||||
|
||||
- **应用层**:权限规则决定"能不能执行"(白名单/黑名单/用户确认)
|
||||
- **OS 层**:沙箱决定"执行时能做什么"(文件系统/网络/进程隔离)
|
||||
|
||||
@@ -65,7 +65,7 @@ ENABLE_LSP_TOOL=1 bun run dev
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ LSP Tool │
|
||||
│ src/tools/LSPTool/LSPTool.ts │
|
||||
│ packages/builtin-tools/src/tools/LSPTool/LSPTool.ts│
|
||||
│ (Claude 可调用的工具,9 种操作) │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
@@ -128,10 +128,10 @@ LSP 服务器会异步推送 `textDocument/publishDiagnostics` 通知,经去
|
||||
| `src/services/lsp/config.ts` | 从插件加载 LSP 服务器配置 |
|
||||
| `src/services/lsp/LSPDiagnosticRegistry.ts` | 诊断信息注册、去重、容量限制 |
|
||||
| `src/services/lsp/passiveFeedback.ts` | 注册 `publishDiagnostics` 通知处理器 |
|
||||
| `src/tools/LSPTool/LSPTool.ts` | LSP Tool 实现(暴露给 Claude) |
|
||||
| `src/tools/LSPTool/schemas.ts` | 输入 schema(9 种操作的 discriminated union) |
|
||||
| `src/tools/LSPTool/formatters.ts` | 各操作结果的格式化 |
|
||||
| `src/tools/LSPTool/prompt.ts` | Tool 描述文本 |
|
||||
| `packages/builtin-tools/src/tools/LSPTool/LSPTool.ts` | LSP Tool 实现(暴露给 Claude) |
|
||||
| `packages/builtin-tools/src/tools/LSPTool/schemas.ts` | 输入 schema(9 种操作的 discriminated union) |
|
||||
| `packages/builtin-tools/src/tools/LSPTool/formatters.ts` | 各操作结果的格式化 |
|
||||
| `packages/builtin-tools/src/tools/LSPTool/prompt.ts` | Tool 描述文本 |
|
||||
| `src/utils/plugins/lspPluginIntegration.ts` | 从插件加载、验证、环境变量解析、作用域管理 |
|
||||
|
||||
## LSP Tool 支持的操作
|
||||
@@ -200,9 +200,9 @@ LSP 服务器通过插件提供。插件的 `manifest.json` 中可以声明 LSP
|
||||
|------|------|------|------|
|
||||
| `command` | string | 是 | LSP 服务器可执行命令(不含空格) |
|
||||
| `args` | string[] | 否 | 命令行参数 |
|
||||
| `extensionToLanguage` | Record<string, string> | 是 | 文件扩展名到语言 ID 的映射(至少一个) |
|
||||
| `extensionToLanguage` | `Record<string, string>` | 是 | 文件扩展名到语言 ID 的映射(至少一个) |
|
||||
| `transport` | `"stdio"` \| `"socket"` | 否 | 通信方式,默认 `stdio` |
|
||||
| `env` | Record<string, string> | 否 | 启动服务器时设置的环境变量 |
|
||||
| `env` | `Record<string, string>` | 否 | 启动服务器时设置的环境变量 |
|
||||
| `initializationOptions` | unknown | 否 | 传给服务器的初始化选项 |
|
||||
| `settings` | unknown | 否 | 通过 `workspace/didChangeConfiguration` 传递的设置 |
|
||||
| `workspaceFolder` | string | 否 | 工作区目录路径 |
|
||||
|
||||
@@ -137,7 +137,7 @@ Auto mode 可通过以下方式激活:
|
||||
|
||||
### 进入时(Full Instructions)
|
||||
|
||||
注入到对话中的指令(`messages.ts:3464`):
|
||||
注入到对话中的指令(`messages.ts:3481`):
|
||||
|
||||
> Auto mode is active. The user chose continuous, autonomous execution. You should:
|
||||
>
|
||||
|
||||
@@ -18,17 +18,19 @@ keywords: ["权限模型", "Allow Ask Deny", "PermissionRule", "checkPermissions
|
||||
|
||||
这些行为由 `PermissionResult` 类型定义(`src/utils/permissions/PermissionResult.ts`)。
|
||||
|
||||
## 权限规则的五层来源
|
||||
## 权限规则的来源
|
||||
|
||||
规则从 5 个来源汇聚(`PERMISSION_RULE_SOURCES`,`permissions.ts:109`),优先级从高到低:
|
||||
规则从 8 个来源汇聚(`PERMISSION_RULE_SOURCES`,`permissions.ts:109`),优先级从低到高(后者覆盖前者):
|
||||
|
||||
```
|
||||
1. session — 用户在当前对话中手动授权("Always allow")
|
||||
2. cliArg — 命令行 --allow/--deny 参数
|
||||
3. command — Skill 工具的 allowedTools 白名单
|
||||
4. projectSettings — .claude/settings.json(团队共享)
|
||||
5. userSettings — ~/.claude/settings.json(跨项目)
|
||||
6. policySettings — 企业管理员下发的策略(用户不可覆盖)
|
||||
1. userSettings — ~/.claude/settings.json(跨项目)
|
||||
2. projectSettings — .claude/settings.json(团队共享)
|
||||
3. localSettings — .claude/settings.local.json(gitignored,个人覆盖)
|
||||
4. flagSettings — --settings 命令行参数
|
||||
5. policySettings — 企业管理员下发的策略(用户不可覆盖)
|
||||
6. cliArg — 命令行 --allow/--deny 参数
|
||||
7. command — Skill 工具的 allowedTools 白名单
|
||||
8. session — 用户在当前对话中手动授权("Always allow")
|
||||
```
|
||||
|
||||
每个来源维护三个数组:`alwaysAllowRules[source]`、`alwaysAskRules[source]`、`alwaysDenyRules[source]`。
|
||||
@@ -65,7 +67,7 @@ MCP 工具使用 `getToolNameForPermissionCheck()` 获取匹配名称,支持
|
||||
|
||||
**2. 命令模式匹配**(BashTool 的 `checkPermissions()`)
|
||||
|
||||
BashTool 通过 `preparePermissionMatcher()`(`Tool.ts:514`)解析命令模式:
|
||||
BashTool 通过 `preparePermissionMatcher()`(`Tool.ts:520`)解析命令模式:
|
||||
```json
|
||||
{"tool": "Bash", "ruleContent": "git *"} → 匹配 "git commit -m 'fix'"
|
||||
```
|
||||
@@ -120,7 +122,9 @@ Read/Edit/Write 工具通过 `getPath()` 提取文件路径,与 `ruleContent`
|
||||
|------|---------------------|---------|------|
|
||||
| **Default** | `'default'` | 日常使用 | 敏感操作逐一确认 |
|
||||
| **Plan Mode** | `'plan'` | 探索阶段 | 只能读不能写(`isReadOnly()` 检查) |
|
||||
| **Auto** | `'auto'` | 信任 AI | 通过 transcript classifier 自动决策 |
|
||||
| **Accept Edits** | `'acceptEdits'` | 快速迭代 | 工作区内文件编辑自动放行,其他操作仍需确认 |
|
||||
| **Don't Ask** | `'dontAsk'` | 减少打断 | 尽量自动决策,减少确认弹窗 |
|
||||
| **Auto** | `'auto'` | 信任 AI | 通过 transcript classifier 自动决策(需 `TRANSCRIPT_CLASSIFIER` feature flag) |
|
||||
| **Bypass** | `'bypassPermissions'` | 完全信任 | 所有操作自动放行(需显式 `--dangerously-skip-permissions`) |
|
||||
|
||||
Plan Mode 切换由 `EnterPlanModeTool.call()` 触发:
|
||||
@@ -143,8 +147,8 @@ context.setAppState(prev => ({
|
||||
|
||||
```typescript
|
||||
const DENIAL_LIMITS = {
|
||||
maxDenialsPerTool: 3, // 同一工具连续拒绝上限
|
||||
cooldownPeriodMs: 30_000, // 冷却期 30 秒
|
||||
maxConsecutive: 3, // 同一工具连续拒绝上限
|
||||
maxTotal: 20, // 总拒绝上限
|
||||
}
|
||||
```
|
||||
|
||||
@@ -162,9 +166,12 @@ const DENIAL_LIMITS = {
|
||||
|
||||
```typescript
|
||||
type PermissionUpdate =
|
||||
| { type: 'addRule', behavior, rule, destination }
|
||||
| { type: 'removeRule', behavior, rule, destination }
|
||||
| { type: 'setMode', mode, destination }
|
||||
| { type: 'addRules', destination, rules, behavior }
|
||||
| { type: 'replaceRules', destination, rules, behavior }
|
||||
| { type: 'removeRules', destination, rules, behavior }
|
||||
| { type: 'setMode', destination, mode }
|
||||
| { type: 'addDirectories', destination, directories }
|
||||
| { type: 'removeDirectories', destination, directories }
|
||||
```
|
||||
|
||||
当用户在 Ask 对话框中选择 "Always allow",系统调用 `persistPermissionUpdates()` 将规则写入对应层级的 settings 文件(project/user/managed),同时更新内存中的 `toolPermissionContext`。
|
||||
|
||||
@@ -16,13 +16,13 @@ keywords: ["Plan Mode", "计划模式", "EnterPlanMode", "ExitPlanMode", "prepar
|
||||
|
||||
<Steps>
|
||||
<Step title="EnterPlanMode — 进入计划模式">
|
||||
AI 自主判断(或用户触发)任务需要规划,调用 `EnterPlanModeTool`(`src/tools/EnterPlanModeTool/EnterPlanModeTool.ts:36`)。该工具需要**用户审批**(`checkPermissions` 返回 `ask`)。
|
||||
AI 自主判断(或用户触发)任务需要规划,调用 `EnterPlanModeTool`(`packages/builtin-tools/src/tools/EnterPlanModeTool/EnterPlanModeTool.ts:36`)。该工具需要**用户审批**(`checkPermissions` 返回 `ask`)。
|
||||
</Step>
|
||||
<Step title="探索阶段 — 只读工具集">
|
||||
权限模式切换为 `'plan'`,AI 只能使用 `isReadOnly()` 为 true 的工具(Read、Grep、Glob、Agent 等)。写操作被自动拒绝。
|
||||
</Step>
|
||||
<Step title="ExitPlanMode — 提交方案审批">
|
||||
AI 完成探索后,调用 `ExitPlanModeV2Tool`(`src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts:147`),将计划文件提交给用户审阅。这是第二个**需要用户审批**的节点。
|
||||
AI 完成探索后,调用 `ExitPlanModeV2Tool`(`packages/builtin-tools/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts:147`),将计划文件提交给用户审阅。这是第二个**需要用户审批**的节点。
|
||||
</Step>
|
||||
<Step title="恢复执行 — 全部工具权限">
|
||||
用户批准后,权限模式恢复为进入前的状态,AI 按计划执行。
|
||||
@@ -107,7 +107,7 @@ if (isTeammate()) {
|
||||
|
||||
## 什么时候该用计划模式
|
||||
|
||||
`EnterPlanModeTool` 的 Prompt(`src/tools/EnterPlanModeTool/prompt.ts`)定义了两套触发标准——外部版本更积极(鼓励规划),内部版本更克制(仅在真正模糊时使用):
|
||||
`EnterPlanModeTool` 的 Prompt(`packages/builtin-tools/src/tools/EnterPlanModeTool/prompt.ts`)定义了两套触发标准——外部版本更积极(鼓励规划),内部版本更克制(仅在真正模糊时使用):
|
||||
|
||||
| 场景 | 外部版本 | 内部版本 |
|
||||
|------|---------|---------|
|
||||
|
||||
@@ -166,7 +166,7 @@ keywords: ["沙箱", "sandbox", "权限", "Bash", "PowerShell", "bubblewrap", "s
|
||||
5. 这条命令没有被显式排除
|
||||
6. 这次调用没有被允许以 `dangerouslyDisableSandbox` 绕过
|
||||
|
||||
对应入口在 `src/tools/BashTool/shouldUseSandbox.ts` 和 `src/utils/sandbox/sandbox-adapter.ts`。
|
||||
对应入口在 `packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.ts` 和 `src/utils/sandbox/sandbox-adapter.ts`。
|
||||
|
||||
### 3. PowerShell 只在支持平台上走
|
||||
|
||||
@@ -518,11 +518,11 @@ REPL / CLI 启动
|
||||
|
||||
如果你想继续顺着源码深入,推荐按下面顺序看:
|
||||
|
||||
1. `src/tools/BashTool/shouldUseSandbox.ts`
|
||||
1. `packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.ts`
|
||||
2. `src/utils/Shell.ts`
|
||||
3. `src/utils/sandbox/sandbox-adapter.ts`
|
||||
4. `src/utils/permissions/permissions.ts`
|
||||
5. `src/tools/BashTool/bashPermissions.ts`
|
||||
5. `packages/builtin-tools/src/tools/BashTool/bashPermissions.ts`
|
||||
6. `src/utils/permissions/pathValidation.ts`
|
||||
7. `src/utils/permissions/filesystem.ts`
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ Claude 的 System Prompt 中包含安全指令——这是"软性"约束,依
|
||||
| `deny` | 直接拒绝 | 匹配 deny 规则 |
|
||||
| `ask` | 弹窗确认 | 未匹配任何规则 或 匹配 ask 规则 |
|
||||
|
||||
以 BashTool 为例(`src/tools/BashTool/bashPermissions.ts`),`bashToolHasPermission()` 执行了极其细致的检查链:
|
||||
以 BashTool 为例(`packages/builtin-tools/src/tools/BashTool/bashPermissions.ts`),`bashToolHasPermission()` 执行了极其细致的检查链:
|
||||
|
||||
1. **AST 安全解析**:用 tree-sitter 解析 bash AST,检测命令注入(`$()`、反引号等)
|
||||
2. **语义检查**:识别危险命令(`eval`、`exec`、`source` 等)
|
||||
@@ -169,7 +169,7 @@ Bash("rm -rf node_modules") → ⚠️ 需确认(不可逆)
|
||||
攻击:cd /malicious/dir && git status
|
||||
/malicious/dir 包含 bare repo + 恶意钩子
|
||||
防御:bashToolHasPermission() 检测 cd + git 组合
|
||||
强制 require approval(src/tools/BashTool/bashPermissions.ts:2209)
|
||||
强制 require approval(packages/builtin-tools/src/tools/BashTool/bashPermissions.ts:2209)
|
||||
```
|
||||
|
||||
### 场景3:管道注入
|
||||
@@ -178,5 +178,5 @@ Bash("rm -rf node_modules") → ⚠️ 需确认(不可逆)
|
||||
攻击:echo 'x' | xargs printf '%s' >> /etc/passwd
|
||||
splitCommand 会剥离重定向,导致路径检查遗漏
|
||||
防御:即使管道段独立检查通过,仍对原始命令重新验证路径约束
|
||||
检查重定向目标中的危险模式(反引号、$())(bashPermissions.ts:1992-2056)
|
||||
检查重定向目标中的危险模式(反引号、$())(packages/builtin-tools/src/tools/BashTool/bashPermissions.ts:1992-2056)
|
||||
```
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
## 背景
|
||||
|
||||
- 命令入口只有 fast-path (`src/entrypoints/cli.tsx:249`)
|
||||
- 命令入口只有 fast-path (`src/entrypoints/cli.tsx:272`)
|
||||
- handler 是空的 (`src/cli/handlers/templateJobs.ts`)
|
||||
- `markdownConfigLoader` 已把 `templates` 纳入配置目录 (`src/utils/markdownConfigLoader.ts:29`)
|
||||
- `markdownConfigLoader` 已把 `templates` 纳入配置目录 (`src/utils/markdownConfigLoader.ts:35`)
|
||||
- `query/stopHooks` 已预留 job classifier 链路 (`src/query/stopHooks.ts:103`)
|
||||
- `jobs/classifier.ts` 仍是 stub (`src/jobs/classifier.ts`)
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
- `src/assistant/sessionHistory.ts`
|
||||
- 真正 stub 的主要是:
|
||||
- `src/assistant/sessionDiscovery.ts`
|
||||
- `src/assistant/AssistantSessionChooser.ts`
|
||||
- `src/commands/assistant/assistant.ts:7`
|
||||
- `src/assistant/AssistantSessionChooser.tsx`
|
||||
- `src/commands/assistant/assistant.tsx:7`
|
||||
- `src/assistant/index.ts`
|
||||
|
||||
## 分阶段实现
|
||||
|
||||
@@ -22,14 +22,14 @@ Read 的 `maxResultSizeChars` 是 `Infinity`,但这并不意味着无限制输
|
||||
|
||||
## FileRead:多模态文件读取引擎
|
||||
|
||||
源码路径:`src/tools/FileReadTool/FileReadTool.ts`
|
||||
源码路径:`packages/builtin-tools/src/tools/FileReadTool/FileReadTool.ts`
|
||||
|
||||
### 读取去重机制
|
||||
|
||||
Read 工具有一个常被忽视但至关重要的**去重层**。当 AI 重复读取同一个文件的同一范围时,系统不会浪费 token 发送两份完整内容:
|
||||
|
||||
```typescript
|
||||
// FileReadTool.ts:530-573 — 去重逻辑
|
||||
// FileReadTool.ts — 去重逻辑
|
||||
const existingState = readFileState.get(fullFilePath)
|
||||
if (existingState && !existingState.isPartialView && existingState.offset !== undefined) {
|
||||
const rangeMatch = existingState.offset === offset && existingState.limit === limit
|
||||
@@ -83,7 +83,7 @@ Read 工具在 `validateInput()` 中设置了多层安全门:
|
||||
当文件不存在时,Read 不会只报一个 "file not found":
|
||||
|
||||
```typescript
|
||||
// FileReadTool.ts:639-647
|
||||
// FileReadTool.ts
|
||||
const similarFilename = findSimilarFile(fullFilePath) // 相似扩展名
|
||||
const cwdSuggestion = await suggestPathUnderCwd(fullFilePath) // cwd 相对路径建议
|
||||
// macOS 截图特殊处理:薄空格(U+202F) vs 普通空格
|
||||
@@ -94,7 +94,7 @@ const altPath = getAlternateScreenshotPath(fullFilePath)
|
||||
|
||||
## FileEdit:精确字符串替换引擎
|
||||
|
||||
源码路径:`src/tools/FileEditTool/FileEditTool.ts` + `utils.ts`
|
||||
源码路径:`packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts` + `utils.ts`
|
||||
|
||||
### 引号标准化:AI 无法输出的字符怎么办
|
||||
|
||||
@@ -138,7 +138,7 @@ Edit 工具在 `validateInput()` 中检查两个条件:
|
||||
2. **文件未被外部修改**(`mtime` 未变,或全量读取时内容完全一致)
|
||||
|
||||
```typescript
|
||||
// FileEditTool.ts:290-311 — Windows 特殊处理
|
||||
// FileEditTool.ts — Windows 特殊处理
|
||||
const isFullRead = readTimestamp.offset === undefined && readTimestamp.limit === undefined
|
||||
if (isFullRead && fileContent === readTimestamp.content) {
|
||||
// 内容不变,安全继续(Windows 云同步/杀毒可能改 mtime)
|
||||
@@ -157,7 +157,7 @@ const MAX_EDIT_FILE_SIZE = 1024 * 1024 * 1024 // 1 GiB
|
||||
|
||||
## FileWrite:全量写入与创建
|
||||
|
||||
源码路径:`src/tools/FileWriteTool/FileWriteTool.ts`
|
||||
源码路径:`packages/builtin-tools/src/tools/FileWriteTool/FileWriteTool.ts`
|
||||
|
||||
Write 工具与 Edit 共享大部分基础设施(权限检查、mtime 校验、fileHistory 备份),但有两个关键差异:
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ Glob 默认把**最近修改的文件排在前面**。这不是默认的文件
|
||||
实际效果:AI 优先看到"活"的代码,而不是沉寂的历史文件
|
||||
```
|
||||
|
||||
在 `src/tools/GlobTool/` 中,ripgrep 的输出在返回给 AI 前按 mtime 排序。
|
||||
在 `packages/builtin-tools/src/tools/GlobTool/` 中,ripgrep 的输出在返回给 AI 前按 mtime 排序。
|
||||
|
||||
### ripgrep 的错误处理
|
||||
|
||||
@@ -92,7 +92,7 @@ ripgrep 执行有专门的错误恢复链(`src/utils/ripgrep.ts`):
|
||||
|
||||
## ToolSearch:在 50+ 工具中发现目标
|
||||
|
||||
当可用工具超过 50 个时(含 MCP 提供的外部工具),AI 可能不知道该用哪个。**ToolSearch**(`src/tools/ToolSearchTool/`)提供了工具发现机制。
|
||||
当可用工具超过 50 个时(含 MCP 提供的外部工具),AI 可能不知道该用哪个。**ToolSearch**(`packages/builtin-tools/src/tools/ToolSearchTool/`)提供了工具发现机制。
|
||||
|
||||
### 搜索算法
|
||||
|
||||
@@ -139,14 +139,14 @@ function getDeferredToolsCacheKey(deferredTools: Tools): string {
|
||||
|
||||
AI 的信息获取不局限于本地代码:
|
||||
|
||||
- **WebSearch**(`src/tools/WebSearchTool/`):调用 Anthropic API 的 `web_search_20250305` server tool 搜索互联网
|
||||
- **WebFetch**(`src/tools/WebFetchTool/`):抓取特定 URL 内容,转换为 Markdown 供 AI 阅读
|
||||
- **WebSearch**(`packages/builtin-tools/src/tools/WebSearchTool/`):调用 Anthropic API 的 `web_search_20250305` server tool 搜索互联网
|
||||
- **WebFetch**(`packages/builtin-tools/src/tools/WebFetchTool/`):抓取特定 URL 内容,转换为 Markdown 供 AI 阅读
|
||||
|
||||
这让 AI 可以查阅文档、搜索 Stack Overflow、阅读 GitHub issue——和人类开发者的工作方式一致。
|
||||
|
||||
### WebSearch 实现机制
|
||||
|
||||
WebSearch 通过适配器模式支持三种搜索后端,由 `src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
|
||||
WebSearch 通过适配器模式支持三种搜索后端,由 `packages/builtin-tools/src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
|
||||
|
||||
```
|
||||
适配器架构:
|
||||
@@ -229,7 +229,7 @@ WebSearch 通过适配器模式支持三种搜索后端,由 `src/tools/WebSear
|
||||
|
||||
### WebSearchTool 统一接口
|
||||
|
||||
`WebSearchTool`(`src/tools/WebSearchTool/WebSearchTool.ts`)是面向主循环的工具定义,所有 provider 均可使用(`isEnabled()` 始终返回 true)。它将适配器返回的 `SearchResult[]` 转换为内部 `Output` 格式,`mapToolResultToToolResultBlockParam` 将搜索结果格式化为带 markdown 超链接的文本,并附加 "REMINDER" 要求主模型在回复中包含 Sources。
|
||||
`WebSearchTool`(`packages/builtin-tools/src/tools/WebSearchTool/WebSearchTool.ts`)是面向主循环的工具定义,所有 provider 均可使用(`isEnabled()` 始终返回 true)。它将适配器返回的 `SearchResult[]` 转换为内部 `Output` 格式,`mapToolResultToToolResultBlockParam` 将搜索结果格式化为带 markdown 超链接的文本,并附加 "REMINDER" 要求主模型在回复中包含 Sources。
|
||||
|
||||
### WebFetch 实现机制
|
||||
|
||||
@@ -264,7 +264,7 @@ WebFetch 是一个完整的 HTTP 客户端 + 内容处理管线:
|
||||
| **URL 验证** | `validateURL()` | 长度、协议、用户名密码、公网域名检查 |
|
||||
| **egress 检测** | `X-Proxy-Error: blocked-by-allowlist` | 检测企业代理拦截 |
|
||||
|
||||
预批准域名(`src/tools/WebFetchTool/preapproved.ts`):
|
||||
预批准域名(`packages/builtin-tools/src/tools/WebFetchTool/preapproved.ts`):
|
||||
|
||||
用户无需手动授权即可抓取的域名列表,包含 ~90 个主流技术文档站点(MDN、Python docs、React docs、AWS docs 等)。列表分为 hostname-only 和 path-prefix 两类,查找复杂度 O(1)。
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ spawn(wrapped_command) ← 实际进程创建
|
||||
|
||||
## 只读命令的判定:为什么 Read 免审批而 Bash 不一定
|
||||
|
||||
BashTool 的 `isReadOnly()` 方法(`BashTool.tsx:437`)决定一条命令是否被视为"只读":
|
||||
BashTool 的 `isReadOnly()` 方法(`packages/builtin-tools/src/tools/BashTool/BashTool.tsx:655`)决定一条命令是否被视为"只读":
|
||||
|
||||
```typescript
|
||||
isReadOnly(input) {
|
||||
@@ -41,7 +41,7 @@ isReadOnly(input) {
|
||||
}
|
||||
```
|
||||
|
||||
判定逻辑基于 4 个命令集合(`BashTool.tsx:60-78`):
|
||||
判定逻辑基于 4 个命令集合(`BashTool.tsx:120-166`):
|
||||
|
||||
| 集合 | 命令 | 性质 |
|
||||
|------|------|------|
|
||||
@@ -53,7 +53,7 @@ isReadOnly(input) {
|
||||
对于复合命令(`ls dir && echo "---" && ls dir2`),系统拆分后逐段检查——**所有非中性段都必须属于上述集合**,整条命令才被视为只读。
|
||||
|
||||
```typescript
|
||||
// BashTool.tsx:95 — 简化的判定逻辑
|
||||
// BashTool.tsx — 简化的判定逻辑
|
||||
for (const part of partsWithOperators) {
|
||||
if (BASH_SEMANTIC_NEUTRAL_COMMANDS.has(baseCommand)) continue // 跳过中性段
|
||||
if (!isPartSearch && !isPartRead && !isPartList) {
|
||||
@@ -64,7 +64,7 @@ for (const part of partsWithOperators) {
|
||||
|
||||
## AST 安全解析:tree-sitter bash 解析
|
||||
|
||||
`preparePermissionMatcher()`(`BashTool.tsx:445`)在权限检查前用 `parseForSecurity()` 解析命令结构:
|
||||
`preparePermissionMatcher()`(`BashTool.tsx:663`)在权限检查前用 `parseForSecurity()` 解析命令结构:
|
||||
|
||||
```typescript
|
||||
async preparePermissionMatcher({ command }) {
|
||||
@@ -92,14 +92,14 @@ getDefaultTimeoutMs()
|
||||
└── 最大上限:600,000ms(10 分钟,用户显式设置时)
|
||||
```
|
||||
|
||||
超时后系统不会直接杀进程——`ShellCommand`(`src/utils/ShellCommand.ts:129`)通过 `onTimeout` 回调通知调用方,由调用方决定是终止还是后台化。
|
||||
超时后系统不会直接杀进程——`ShellCommand`(`src/utils/ShellCommand.ts:144`)通过 `onTimeout` 回调通知调用方,由调用方决定是终止还是后台化。
|
||||
|
||||
## 自动后台化
|
||||
|
||||
长时间运行的命令可以自动转为后台任务,不阻塞 AI 的 agentic loop:
|
||||
|
||||
```typescript
|
||||
// BashTool.tsx:880
|
||||
// BashTool.tsx:1158
|
||||
const shouldAutoBackground = !isBackgroundTasksDisabled
|
||||
&& isAutobackgroundingAllowed(command)
|
||||
```
|
||||
@@ -148,7 +148,7 @@ Claude Code 为文件读写、代码搜索等操作提供了专用工具(Read
|
||||
| **并发安全** | `isConcurrencySafe()` 返回 `true` → 可并行执行 | Bash 命令可能有副作用,串行执行 |
|
||||
| **安全审计** | 工具名精确匹配权限规则 | 需 AST 解析命令结构后匹配 |
|
||||
|
||||
`isConcurrencySafe()`(`BashTool.tsx:434`)是一个常被忽视但重要的设计——只有只读命令可以在 agentic loop 中并行执行,有副作用的命令必须串行,防止竞态条件。
|
||||
`isConcurrencySafe()`(`BashTool.tsx:652`)是一个常被忽视但重要的设计——只有只读命令可以在 agentic loop 中并行执行,有副作用的命令必须串行,防止竞态条件。
|
||||
|
||||
## 进度反馈的流式设计
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Claude Code 的任务管理并非单一系统,而是两个并存、按运行
|
||||
TodoWrite 本质是一个**全量替换**操作——每次调用传入完整的 `todos[]` 数组,完全覆盖之前的状态:
|
||||
|
||||
```typescript
|
||||
// src/tools/TodoWriteTool/TodoWriteTool.ts — call() 核心逻辑
|
||||
// packages/builtin-tools/src/tools/TodoWriteTool/TodoWriteTool.ts — call() 核心逻辑
|
||||
async call({ todos }, context) {
|
||||
const todoKey = context.agentId ?? getSessionId()
|
||||
const oldTodos = appState.todos[todoKey] ?? []
|
||||
|
||||
@@ -16,7 +16,7 @@ keywords: ["工具系统", "Tool 抽象", "AI 工具", "function calling", "buil
|
||||
|
||||
## Tool 类型:35 个字段的统一接口
|
||||
|
||||
所有工具都实现 `src/Tool.ts:362` 的 `Tool<Input, Output, Progress>` 类型。这不是一个 class,而是一个包含 35+ 字段的**结构化类型**(structural typing),任何满足该接口的对象就是一个工具:
|
||||
所有工具都实现 `src/Tool.ts:368` 的 `Tool<Input, Output, Progress>` 类型。这不是一个 class,而是一个包含 35+ 字段的**结构化类型**(structural typing),任何满足该接口的对象就是一个工具:
|
||||
|
||||
### 核心四要素
|
||||
|
||||
@@ -69,7 +69,7 @@ keywords: ["工具系统", "Tool 抽象", "AI 工具", "function calling", "buil
|
||||
|
||||
## 工具注册:`getTools()` 的分层组装
|
||||
|
||||
`src/tools.ts` 的 `getAllBaseTools()`(第 191 行)是工具注册的核心:
|
||||
`src/tools.ts` 的 `getAllBaseTools()`(第 195 行)是工具注册的核心:
|
||||
|
||||
```
|
||||
固定工具(始终可用):
|
||||
@@ -96,7 +96,7 @@ Ant-only 工具:
|
||||
← process.env.USER_TYPE === 'ant' ? [REPLTool, ConfigTool, TungstenTool]
|
||||
```
|
||||
|
||||
`getTools()`(第 269 行)在 `getAllBaseTools()` 基础上应用权限过滤:
|
||||
`getTools()`(第 274 行)在 `getAllBaseTools()` 基础上应用权限过滤:
|
||||
|
||||
```typescript
|
||||
export const getTools = (permissionContext): Tools => {
|
||||
@@ -110,7 +110,7 @@ export const getTools = (permissionContext): Tools => {
|
||||
|
||||
## `buildTool()` 工厂函数
|
||||
|
||||
大多数工具通过 `buildTool()` 创建(`src/Tool.ts:721`),它是一个类型安全的构造器:
|
||||
大多数工具通过 `buildTool()` 创建(`src/Tool.ts:789`),它是一个类型安全的构造器:
|
||||
|
||||
```typescript
|
||||
export const BashTool: Tool<...> = buildTool({
|
||||
|
||||
@@ -175,7 +175,7 @@ F. getCompletedResults() → 空
|
||||
|
||||
---
|
||||
|
||||
#### #8 stream_event (input_json_delta: '{"file_path":')
|
||||
#### #8 stream_event (input_json_delta: `'{"file_path":'`)
|
||||
|
||||
```
|
||||
D. yield message ✅ → REPL 追加工具输入 JSON 碎片
|
||||
|
||||
@@ -135,9 +135,11 @@
|
||||
"group": "运行模式",
|
||||
"pages": [
|
||||
"docs/features/kairos",
|
||||
"docs/features/channels",
|
||||
"docs/features/voice-mode",
|
||||
"docs/features/bridge-mode",
|
||||
"docs/features/remote-control-self-hosting",
|
||||
"docs/features/acp-link",
|
||||
"docs/features/proactive",
|
||||
"docs/features/ultraplan"
|
||||
]
|
||||
|
||||
112
package.json
112
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-code-best",
|
||||
"version": "1.4.1",
|
||||
"version": "1.10.4",
|
||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||
"type": "module",
|
||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||
@@ -37,6 +37,7 @@
|
||||
"files": [
|
||||
"dist",
|
||||
"scripts/postinstall.cjs",
|
||||
"scripts/run-parallel.mjs",
|
||||
"scripts/setup-chrome-mcp.mjs"
|
||||
],
|
||||
"scripts": {
|
||||
@@ -46,22 +47,28 @@
|
||||
"build:bun": "bun run build.ts",
|
||||
"dev": "bun run scripts/dev.ts",
|
||||
"dev:inspect": "bun run scripts/dev-debug.ts",
|
||||
"prepublishOnly": "bun run build",
|
||||
"prepublishOnly": "bun run build:vite",
|
||||
"lint": "biome lint src/",
|
||||
"lint:fix": "biome lint --fix src/",
|
||||
"format": "biome format --write src/",
|
||||
"prepare": "git config core.hooksPath .githooks",
|
||||
"test": "bun test",
|
||||
"test:production": "bun run scripts/production-test.ts",
|
||||
"test:production:offline": "bun run scripts/production-test.ts --offline",
|
||||
"test:production:verbose": "bun run scripts/production-test.ts --verbose",
|
||||
"test:production:bun": "bun run scripts/production-test.ts --bun",
|
||||
"check:bundle": "bun run scripts/check-bundle-integrity.ts",
|
||||
"check:unused": "knip-bun",
|
||||
"health": "bun run scripts/health-check.ts",
|
||||
"postinstall": "node scripts/run-parallel.mjs scripts/postinstall.cjs scripts/setup-chrome-mcp.mjs",
|
||||
"docs:dev": "npx mintlify dev",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:all": "bun run typecheck && bun test",
|
||||
"rcs": "bun run scripts/rcs.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.19.0",
|
||||
"@claude-code-best/mcp-chrome-bridge": "^2.0.8",
|
||||
"@claude-code-best/mcp-chrome-bridge": "^3.0.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -71,50 +78,51 @@
|
||||
"@ant/computer-use-input": "workspace:*",
|
||||
"@ant/computer-use-mcp": "workspace:*",
|
||||
"@ant/computer-use-swift": "workspace:*",
|
||||
"@anthropic-ai/bedrock-sdk": "^0.26.4",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
|
||||
"@anthropic-ai/bedrock-sdk": "^0.29.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.114",
|
||||
"@anthropic-ai/foundry-sdk": "^0.2.3",
|
||||
"@anthropic-ai/mcpb": "^2.1.2",
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.44",
|
||||
"@anthropic-ai/sdk": "^0.80.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
||||
"@anthropic-ai/sdk": "^0.81.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
||||
"@anthropic/ink": "workspace:*",
|
||||
"@aws-sdk/client-bedrock": "^3.1020.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.1020.0",
|
||||
"@aws-sdk/client-sts": "^3.1020.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.28",
|
||||
"@aws-sdk/credential-providers": "^3.1020.0",
|
||||
"@aws-sdk/client-bedrock": "^3.1037.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.1037.0",
|
||||
"@aws-sdk/client-sts": "^3.1037.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.36",
|
||||
"@aws-sdk/credential-providers": "^3.1037.0",
|
||||
"@azure/identity": "^4.13.1",
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@biomejs/biome": "^2.4.12",
|
||||
"@claude-code-best/agent-tools": "workspace:*",
|
||||
"@claude-code-best/builtin-tools": "workspace:*",
|
||||
"@claude-code-best/mcp-client": "workspace:*",
|
||||
"@claude-code-best/weixin": "workspace:*",
|
||||
"@commander-js/extra-typings": "^14.0.0",
|
||||
"@growthbook/growthbook": "^1.6.5",
|
||||
"@langfuse/otel": "^5.1.0",
|
||||
"@langfuse/tracing": "^5.1.0",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@opentelemetry/api": "^1.9.1",
|
||||
"@opentelemetry/api-logs": "^0.214.0",
|
||||
"@opentelemetry/core": "^2.6.1",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.214.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
|
||||
"@opentelemetry/exporter-logs-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.214.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.214.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.214.0",
|
||||
"@opentelemetry/exporter-trace-otlp-grpc": "^0.214.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/resources": "^2.6.1",
|
||||
"@opentelemetry/sdk-logs": "^0.214.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.6.1",
|
||||
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
||||
"@opentelemetry/api-logs": "^0.215.0",
|
||||
"@opentelemetry/core": "^2.7.0",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.215.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.215.0",
|
||||
"@opentelemetry/exporter-logs-otlp-proto": "^0.215.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.215.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.215.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.215.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.215.0",
|
||||
"@opentelemetry/exporter-trace-otlp-grpc": "^0.215.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.215.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.215.0",
|
||||
"@opentelemetry/resources": "^2.7.0",
|
||||
"@opentelemetry/sdk-logs": "^0.215.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.7.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.7.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.40.0",
|
||||
"@sentry/node": "^10.47.0",
|
||||
"@smithy/core": "^3.23.13",
|
||||
"@smithy/node-http-handler": "^4.5.1",
|
||||
"@sentry/node": "^10.49.0",
|
||||
"@smithy/core": "^3.23.15",
|
||||
"@smithy/node-http-handler": "^4.5.3",
|
||||
"@types/bun": "^1.3.12",
|
||||
"@types/cacache": "^20.0.1",
|
||||
"@types/he": "^1.2.3",
|
||||
@@ -136,7 +144,7 @@
|
||||
"asciichart": "^1.5.25",
|
||||
"audio-capture-napi": "workspace:*",
|
||||
"auto-bind": "^5.0.1",
|
||||
"axios": "^1.14.0",
|
||||
"axios": "^1.15.2",
|
||||
"bidi-js": "^1.0.3",
|
||||
"cacache": "^20.0.4",
|
||||
"chalk": "^5.6.2",
|
||||
@@ -151,31 +159,30 @@
|
||||
"execa": "^9.6.1",
|
||||
"fflate": "^0.8.2",
|
||||
"figures": "^6.1.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"fuse.js": "^7.3.0",
|
||||
"get-east-asian-width": "^1.5.0",
|
||||
"google-auth-library": "^10.6.2",
|
||||
"he": "^1.2.0",
|
||||
"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",
|
||||
"knip": "^6.4.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"lru-cache": "^11.3.5",
|
||||
"marked": "^17.0.6",
|
||||
"modifiers-napi": "workspace:*",
|
||||
"openai": "^6.33.0",
|
||||
"openai": "^6.34.0",
|
||||
"p-map": "^7.0.4",
|
||||
"picomatch": "^4.0.4",
|
||||
"plist": "^3.1.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.4",
|
||||
"react": "^19.2.5",
|
||||
"react-compiler-runtime": "^1.0.0",
|
||||
"react-reconciler": "^0.33.0",
|
||||
"rollup": "^4.60.1",
|
||||
"rollup": "^4.60.2",
|
||||
"semver": "^7.7.4",
|
||||
"sharp": "^0.34.5",
|
||||
"shell-quote": "^1.8.3",
|
||||
@@ -184,10 +191,10 @@
|
||||
"strip-ansi": "^7.2.0",
|
||||
"supports-hyperlinks": "^4.4.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"turndown": "^7.2.2",
|
||||
"type-fest": "^5.5.0",
|
||||
"typescript": "^6.0.2",
|
||||
"undici": "^7.24.6",
|
||||
"turndown": "^7.2.4",
|
||||
"type-fest": "^5.6.0",
|
||||
"typescript": "^6.0.3",
|
||||
"undici": "^7.25.0",
|
||||
"url-handler-napi": "workspace:*",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"vite": "^8.0.8",
|
||||
@@ -198,5 +205,16 @@
|
||||
"xss": "^1.0.15",
|
||||
"yaml": "^2.8.3",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"doubaoime-asr": "^0.1.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@inquirer/prompts": "8.4.2",
|
||||
"@xmldom/xmldom": "0.8.13",
|
||||
"follow-redirects": "1.16.0",
|
||||
"hono": "4.12.15",
|
||||
"postcss": "8.5.10",
|
||||
"uuid": "14.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +286,15 @@ export default class App extends PureComponent<Props, State> {
|
||||
// ignore calling setRawMode on an handle stdin it cannot be called
|
||||
if (this.isRawModeSupported()) {
|
||||
this.handleSetRawMode(false)
|
||||
} else {
|
||||
// Even when raw mode was never enabled (e.g. non-TTY stdin on
|
||||
// Windows Node.js), ensure stdin is unref'd so the process can
|
||||
// exit. earlyInput may have called ref() before Ink mounted.
|
||||
try {
|
||||
this.props.stdin.unref()
|
||||
} catch {
|
||||
// stdin may already be destroyed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"./client": "./src/client/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.80.0",
|
||||
"@anthropic-ai/sdk": "^0.81.0",
|
||||
"openai": "^6.33.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,3 +61,10 @@ export { anthropicMessagesToOpenAI } from './shared/openaiConvertMessages.js'
|
||||
export type { ConvertMessagesOptions } from './shared/openaiConvertMessages.js'
|
||||
export { anthropicToolsToOpenAI, anthropicToolChoiceToOpenAI } from './shared/openaiConvertTools.js'
|
||||
export { adaptOpenAIStreamToAnthropic } from './shared/openaiStreamAdapter.js'
|
||||
|
||||
// Codex provider utilities
|
||||
export { normalizeCodexCallId, resolveCodexCallId, createCodexFallbackCallId } from './providers/codex/callIds.js'
|
||||
export { resolveCodexModel, resolveCodexMaxTokens } from './providers/codex/modelMapping.js'
|
||||
export { anthropicMessagesToCodexInput } from './providers/codex/convertMessages.js'
|
||||
export type { CodexImageConversionOptions } from './providers/codex/convertMessages.js'
|
||||
export { anthropicToolsToCodex } from './providers/codex/convertTools.js'
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, test, beforeEach, afterEach } from 'bun:test'
|
||||
import { resolveCodexModel } from '../modelMapping.js'
|
||||
|
||||
describe('resolveCodexModel', () => {
|
||||
const originalEnv = {
|
||||
CODEX_MODEL: process.env.CODEX_MODEL,
|
||||
CODEX_DEFAULT_HAIKU_MODEL: process.env.CODEX_DEFAULT_HAIKU_MODEL,
|
||||
CODEX_DEFAULT_SONNET_MODEL: process.env.CODEX_DEFAULT_SONNET_MODEL,
|
||||
CODEX_DEFAULT_OPUS_MODEL: process.env.CODEX_DEFAULT_OPUS_MODEL,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.CODEX_MODEL
|
||||
delete process.env.CODEX_DEFAULT_HAIKU_MODEL
|
||||
delete process.env.CODEX_DEFAULT_SONNET_MODEL
|
||||
delete process.env.CODEX_DEFAULT_OPUS_MODEL
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.assign(process.env, originalEnv)
|
||||
})
|
||||
|
||||
test('CODEX_MODEL env var overrides all', () => {
|
||||
process.env.CODEX_MODEL = 'my-custom-model'
|
||||
expect(resolveCodexModel('claude-sonnet-4-6')).toBe('my-custom-model')
|
||||
})
|
||||
|
||||
test('CODEX_DEFAULT_SONNET_MODEL overrides default map', () => {
|
||||
process.env.CODEX_DEFAULT_SONNET_MODEL = 'my-sonnet'
|
||||
expect(resolveCodexModel('claude-sonnet-4-6')).toBe('my-sonnet')
|
||||
})
|
||||
|
||||
test('CODEX_DEFAULT_HAIKU_MODEL overrides default map', () => {
|
||||
process.env.CODEX_DEFAULT_HAIKU_MODEL = 'my-haiku'
|
||||
expect(resolveCodexModel('claude-haiku-4-5-20251001')).toBe('my-haiku')
|
||||
})
|
||||
|
||||
test('CODEX_DEFAULT_OPUS_MODEL overrides default map', () => {
|
||||
process.env.CODEX_DEFAULT_OPUS_MODEL = 'my-opus'
|
||||
expect(resolveCodexModel('claude-opus-4-6')).toBe('my-opus')
|
||||
})
|
||||
|
||||
test('maps known sonnet model via DEFAULT_MODEL_MAP', () => {
|
||||
expect(resolveCodexModel('claude-sonnet-4-6')).toBe('gpt-5.4-mini')
|
||||
})
|
||||
|
||||
test('maps known haiku model via DEFAULT_MODEL_MAP', () => {
|
||||
expect(resolveCodexModel('claude-haiku-4-5-20251001')).toBe('gpt-5.4-mini')
|
||||
})
|
||||
|
||||
test('maps known opus model via DEFAULT_MODEL_MAP', () => {
|
||||
expect(resolveCodexModel('claude-opus-4-6')).toBe('gpt-5.4')
|
||||
})
|
||||
|
||||
test('maps legacy sonnet models', () => {
|
||||
expect(resolveCodexModel('claude-sonnet-4-20250514')).toBe('gpt-5.4-mini')
|
||||
expect(resolveCodexModel('claude-3-5-sonnet-20241022')).toBe('gpt-5.4-mini')
|
||||
})
|
||||
|
||||
test('maps legacy haiku models', () => {
|
||||
expect(resolveCodexModel('claude-3-5-haiku-20241022')).toBe('gpt-5.4-mini')
|
||||
})
|
||||
|
||||
test('maps legacy opus models', () => {
|
||||
expect(resolveCodexModel('claude-opus-4-20250514')).toBe('gpt-5.4')
|
||||
expect(resolveCodexModel('claude-opus-4-5-20251101')).toBe('gpt-5.4')
|
||||
})
|
||||
|
||||
test('uses family default for unrecognized haiku model', () => {
|
||||
expect(resolveCodexModel('claude-haiku-99')).toBe('gpt-5.4-mini')
|
||||
})
|
||||
|
||||
test('uses family default for unrecognized sonnet model', () => {
|
||||
expect(resolveCodexModel('claude-sonnet-99')).toBe('gpt-5.4-mini')
|
||||
})
|
||||
|
||||
test('uses family default for unrecognized opus model', () => {
|
||||
expect(resolveCodexModel('claude-opus-99')).toBe('gpt-5.4')
|
||||
})
|
||||
|
||||
test('passes through unknown model name without family', () => {
|
||||
expect(resolveCodexModel('some-random-model')).toBe('some-random-model')
|
||||
})
|
||||
|
||||
test('strips [1m] suffix', () => {
|
||||
expect(resolveCodexModel('claude-sonnet-4-6[1m]')).toBe('gpt-5.4-mini')
|
||||
})
|
||||
|
||||
test('CODEX_MODEL takes precedence over family-specific vars', () => {
|
||||
process.env.CODEX_MODEL = 'global-override'
|
||||
process.env.CODEX_DEFAULT_SONNET_MODEL = 'family-override'
|
||||
expect(resolveCodexModel('claude-sonnet-4-6')).toBe('global-override')
|
||||
})
|
||||
})
|
||||
31
packages/@ant/model-provider/src/providers/codex/callIds.ts
Normal file
31
packages/@ant/model-provider/src/providers/codex/callIds.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
const MAX_CODEX_CALL_ID_LENGTH = 96
|
||||
|
||||
export function normalizeCodexCallId(value: unknown): string | null {
|
||||
if (typeof value !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
const sanitized = value
|
||||
.trim()
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/[^A-Za-z0-9._:-]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.slice(0, MAX_CODEX_CALL_ID_LENGTH)
|
||||
|
||||
return sanitized.length > 0 ? sanitized : null
|
||||
}
|
||||
|
||||
export function createCodexFallbackCallId(seed: string): string {
|
||||
const hash = createHash('sha1')
|
||||
.update(seed.length > 0 ? seed : 'codex-call')
|
||||
.digest('hex')
|
||||
.slice(0, 24)
|
||||
|
||||
return `call_${hash}`
|
||||
}
|
||||
|
||||
export function resolveCodexCallId(value: unknown, seed: string): string {
|
||||
return normalizeCodexCallId(value) ?? createCodexFallbackCallId(seed)
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
import type {
|
||||
ResponseFunctionToolCallOutputItem,
|
||||
ResponseInputImage,
|
||||
ResponseInputItem,
|
||||
ResponseInputText,
|
||||
} from 'openai/resources/responses/responses.mjs'
|
||||
import type { Message } from '../../types/index.js'
|
||||
import {
|
||||
normalizeCodexCallId,
|
||||
resolveCodexCallId,
|
||||
} from './callIds.js'
|
||||
|
||||
type ContentBlock = {
|
||||
type: string
|
||||
text?: string
|
||||
source?: {
|
||||
type?: string
|
||||
data?: string
|
||||
media_type?: string
|
||||
url?: string
|
||||
}
|
||||
}
|
||||
|
||||
type ToolUseLikeBlock = {
|
||||
type: 'tool_use'
|
||||
id: string
|
||||
name: string
|
||||
input: unknown
|
||||
}
|
||||
|
||||
type ToolResultLikeBlock = {
|
||||
type: 'tool_result'
|
||||
tool_use_id: string
|
||||
content?: string | ReadonlyArray<ContentBlock>
|
||||
}
|
||||
|
||||
export type CodexImageConversionOptions = {
|
||||
resolveBase64ImageUrl?: (
|
||||
data: string,
|
||||
mediaType?: string,
|
||||
) => Promise<string | null>
|
||||
}
|
||||
|
||||
type CodexCallIdState = {
|
||||
byOriginalId: Map<string, string>
|
||||
sequence: number
|
||||
}
|
||||
|
||||
function createInputText(text: string): ResponseInputText {
|
||||
return {
|
||||
type: 'input_text',
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
function createInputImage(imageUrl: string): ResponseInputImage {
|
||||
return {
|
||||
type: 'input_image',
|
||||
image_url: imageUrl,
|
||||
detail: 'high',
|
||||
}
|
||||
}
|
||||
|
||||
function getUnsupportedBlockText(type: string): string | null {
|
||||
switch (type) {
|
||||
case 'image':
|
||||
return '[Image omitted: codex gateway currently requires remote image URLs. Configure CODEX_IMGBB_API_KEY to auto-convert local images.]'
|
||||
case 'document':
|
||||
return '[Document omitted: codex gateway does not support document replay.]'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function getImageUrl(block: ContentBlock): string | null {
|
||||
const source = block.source
|
||||
if (!source) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (source.type === 'url' && typeof source.url === 'string' && source.url.length > 0) {
|
||||
return source.url
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function resolveImageUrl(
|
||||
block: ContentBlock,
|
||||
options: CodexImageConversionOptions,
|
||||
): Promise<string | null> {
|
||||
const directUrl = getImageUrl(block)
|
||||
if (directUrl) {
|
||||
return directUrl
|
||||
}
|
||||
|
||||
if (block.source?.type !== 'base64') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (options.resolveBase64ImageUrl && typeof block.source.data === 'string') {
|
||||
const uploadedUrl = await options.resolveBase64ImageUrl(
|
||||
block.source.data,
|
||||
block.source.media_type,
|
||||
)
|
||||
if (uploadedUrl) {
|
||||
return uploadedUrl
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function convertBlocksToInputContent(
|
||||
content: ReadonlyArray<ContentBlock>,
|
||||
options: CodexImageConversionOptions,
|
||||
): Promise<Array<ResponseInputText | ResponseInputImage>> {
|
||||
const output: Array<ResponseInputText | ResponseInputImage> = []
|
||||
|
||||
for (const block of content) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
output.push(createInputText(block.text))
|
||||
continue
|
||||
}
|
||||
|
||||
if (block.type === 'image') {
|
||||
const imageUrl = await resolveImageUrl(block, options)
|
||||
if (imageUrl) {
|
||||
output.push(createInputImage(imageUrl))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = getUnsupportedBlockText(block.type)
|
||||
if (fallback) {
|
||||
output.push(createInputText(fallback))
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
async function convertToolResultOutput(
|
||||
content: string | ReadonlyArray<ContentBlock> | undefined,
|
||||
options: CodexImageConversionOptions,
|
||||
): Promise<ResponseFunctionToolCallOutputItem['output']> {
|
||||
if (!content) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
return content
|
||||
}
|
||||
|
||||
const output = await convertBlocksToInputContent(content, options)
|
||||
|
||||
if (output.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (output.length === 1 && output[0].type === 'input_text') {
|
||||
return output[0].text
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function pushUserMessage(
|
||||
items: ResponseInputItem[],
|
||||
textParts: string[],
|
||||
imageUrls: string[] = [],
|
||||
): void {
|
||||
const text = textParts.join('\n').trim()
|
||||
if (text.length === 0 && imageUrls.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
items.push({
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
content: [
|
||||
...(text.length > 0 ? [createInputText(text)] : []),
|
||||
...imageUrls.map(createInputImage),
|
||||
],
|
||||
} as unknown as ResponseInputItem)
|
||||
}
|
||||
|
||||
function pushAssistantMessage(
|
||||
items: ResponseInputItem[],
|
||||
textParts: string[],
|
||||
): void {
|
||||
const text = textParts.join('\n').trim()
|
||||
if (text.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
items.push({
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'output_text',
|
||||
text,
|
||||
annotations: [],
|
||||
},
|
||||
],
|
||||
} as unknown as ResponseInputItem)
|
||||
}
|
||||
|
||||
function stringifyToolInput(input: unknown): string {
|
||||
if (typeof input === 'string') {
|
||||
return input
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(input ?? {})
|
||||
} catch {
|
||||
return '{}'
|
||||
}
|
||||
}
|
||||
|
||||
function createCodexCallIdState(): CodexCallIdState {
|
||||
return {
|
||||
byOriginalId: new Map(),
|
||||
sequence: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAssistantCallId(
|
||||
block: ToolUseLikeBlock,
|
||||
state: CodexCallIdState,
|
||||
): string {
|
||||
const originalId = typeof block.id === 'string' ? block.id : ''
|
||||
const seed = `${block.name}:${stringifyToolInput(block.input)}:${state.sequence}`
|
||||
const callId = resolveCodexCallId(originalId, seed)
|
||||
|
||||
if (originalId.length > 0) {
|
||||
state.byOriginalId.set(originalId, callId)
|
||||
}
|
||||
state.sequence += 1
|
||||
|
||||
return callId
|
||||
}
|
||||
|
||||
function resolveToolResultCallId(
|
||||
toolUseId: unknown,
|
||||
state: CodexCallIdState,
|
||||
): string | null {
|
||||
if (typeof toolUseId !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
return state.byOriginalId.get(toolUseId) ?? normalizeCodexCallId(toolUseId)
|
||||
}
|
||||
|
||||
async function convertUserContentToInputItems(
|
||||
items: ResponseInputItem[],
|
||||
content: ReadonlyArray<string | ContentBlock>,
|
||||
options: CodexImageConversionOptions,
|
||||
callIdState: CodexCallIdState,
|
||||
): Promise<void> {
|
||||
const textParts: string[] = []
|
||||
const imageUrls: string[] = []
|
||||
|
||||
for (const block of content) {
|
||||
if (typeof block === 'string') {
|
||||
textParts.push(block)
|
||||
continue
|
||||
}
|
||||
|
||||
if (block.type === 'tool_result') {
|
||||
pushUserMessage(items, textParts, imageUrls)
|
||||
textParts.length = 0
|
||||
imageUrls.length = 0
|
||||
|
||||
const toolResultBlock = block as ToolResultLikeBlock
|
||||
const callId = resolveToolResultCallId(
|
||||
toolResultBlock.tool_use_id,
|
||||
callIdState,
|
||||
)
|
||||
if (!callId) {
|
||||
continue
|
||||
}
|
||||
|
||||
items.push({
|
||||
type: 'function_call_output',
|
||||
call_id: callId,
|
||||
output: await convertToolResultOutput(toolResultBlock.content, options),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (block.type === 'text' && block.text) {
|
||||
textParts.push(block.text)
|
||||
continue
|
||||
}
|
||||
|
||||
if (block.type === 'image') {
|
||||
const imageUrl = await resolveImageUrl(block, options)
|
||||
if (imageUrl) {
|
||||
imageUrls.push(imageUrl)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = getUnsupportedBlockText(block.type)
|
||||
if (fallback) {
|
||||
textParts.push(fallback)
|
||||
}
|
||||
}
|
||||
|
||||
pushUserMessage(items, textParts, imageUrls)
|
||||
}
|
||||
|
||||
function convertAssistantContentToInputItems(
|
||||
items: ResponseInputItem[],
|
||||
content: ReadonlyArray<string | ContentBlock>,
|
||||
callIdState: CodexCallIdState,
|
||||
): void {
|
||||
const textParts: string[] = []
|
||||
|
||||
for (const block of content) {
|
||||
if (typeof block === 'string') {
|
||||
textParts.push(block)
|
||||
continue
|
||||
}
|
||||
|
||||
if (block.type === 'tool_use') {
|
||||
pushAssistantMessage(items, textParts)
|
||||
textParts.length = 0
|
||||
|
||||
const toolUseBlock = block as unknown as ToolUseLikeBlock
|
||||
items.push({
|
||||
type: 'function_call',
|
||||
call_id: resolveAssistantCallId(toolUseBlock, callIdState),
|
||||
name: toolUseBlock.name,
|
||||
arguments: stringifyToolInput(toolUseBlock.input),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (block.type === 'text' && block.text) {
|
||||
textParts.push(block.text)
|
||||
}
|
||||
}
|
||||
|
||||
pushAssistantMessage(items, textParts)
|
||||
}
|
||||
|
||||
export async function anthropicMessagesToCodexInput(
|
||||
messages: Message[],
|
||||
options: CodexImageConversionOptions = {},
|
||||
): Promise<ResponseInputItem[]> {
|
||||
const items: ResponseInputItem[] = []
|
||||
const callIdState = createCodexCallIdState()
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.type !== 'user' && message.type !== 'assistant') {
|
||||
continue
|
||||
}
|
||||
|
||||
const apiMessage = message.message
|
||||
if (!apiMessage?.content) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof apiMessage.content === 'string') {
|
||||
if (message.type === 'user') {
|
||||
pushUserMessage(items, [apiMessage.content])
|
||||
} else {
|
||||
pushAssistantMessage(items, [apiMessage.content])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (message.type === 'user') {
|
||||
await convertUserContentToInputItems(
|
||||
items,
|
||||
apiMessage.content as ReadonlyArray<string | ContentBlock>,
|
||||
options,
|
||||
callIdState,
|
||||
)
|
||||
} else {
|
||||
convertAssistantContentToInputItems(
|
||||
items,
|
||||
apiMessage.content as ReadonlyArray<string | ContentBlock>,
|
||||
callIdState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import type { Tool as CodexTool } from 'openai/resources/responses/responses.mjs'
|
||||
|
||||
function isClientFunctionTool(
|
||||
tool: BetaToolUnion,
|
||||
): tool is BetaToolUnion & {
|
||||
name: string
|
||||
description?: string
|
||||
input_schema?: { [key: string]: unknown }
|
||||
strict?: boolean
|
||||
defer_loading?: boolean
|
||||
} {
|
||||
const value = tool as unknown as Record<string, unknown>
|
||||
return typeof value.name === 'string'
|
||||
}
|
||||
|
||||
export function anthropicToolsToCodex(
|
||||
tools: BetaToolUnion[],
|
||||
): CodexTool[] {
|
||||
return tools.flatMap(tool => {
|
||||
const value = tool as unknown as Record<string, unknown>
|
||||
if (
|
||||
value.type === 'advisor_20260301' ||
|
||||
value.type === 'computer_20250124' ||
|
||||
!isClientFunctionTool(tool)
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{
|
||||
type: 'function',
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.input_schema ?? {},
|
||||
strict: tool.strict ?? null,
|
||||
...(tool.defer_loading && { defer_loading: true }),
|
||||
}]
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Default mapping from Anthropic model names to Codex (OpenAI Responses API) model names.
|
||||
* Used only when CODEX_DEFAULT_{FAMILY}_MODEL env vars are not set.
|
||||
*/
|
||||
const DEFAULT_MODEL_MAP: Record<string, string> = {
|
||||
'claude-sonnet-4-20250514': 'gpt-5.4-mini',
|
||||
'claude-sonnet-4-5-20250929': 'gpt-5.4-mini',
|
||||
'claude-sonnet-4-6': 'gpt-5.4-mini',
|
||||
'claude-3-7-sonnet-20250219': 'gpt-5.4-mini',
|
||||
'claude-3-5-sonnet-20241022': 'gpt-5.4-mini',
|
||||
'claude-opus-4-20250514': 'gpt-5.4',
|
||||
'claude-opus-4-1-20250805': 'gpt-5.4',
|
||||
'claude-opus-4-5-20251101': 'gpt-5.4',
|
||||
'claude-opus-4-6': 'gpt-5.4',
|
||||
'claude-opus-4-7': 'gpt-5.5',
|
||||
'claude-haiku-4-5-20251001': 'gpt-5.4-mini',
|
||||
'claude-3-5-haiku-20241022': 'gpt-5.4-mini',
|
||||
}
|
||||
|
||||
/**
|
||||
* Default model for each family when an exact match is not in DEFAULT_MODEL_MAP.
|
||||
*/
|
||||
const DEFAULT_FAMILY_MAP: Record<string, string> = {
|
||||
haiku: 'gpt-5.4-mini',
|
||||
sonnet: 'gpt-5.4-mini',
|
||||
opus: 'gpt-5.4',
|
||||
}
|
||||
|
||||
function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
|
||||
if (/haiku/i.test(model)) return 'haiku'
|
||||
if (/opus/i.test(model)) return 'opus'
|
||||
if (/sonnet/i.test(model)) return 'sonnet'
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Codex (OpenAI Responses API) model name for a given Anthropic model.
|
||||
*
|
||||
* Priority:
|
||||
* 1. CODEX_MODEL env var (override all)
|
||||
* 2. CODEX_DEFAULT_{FAMILY}_MODEL env var (e.g. CODEX_DEFAULT_SONNET_MODEL)
|
||||
* 3. DEFAULT_MODEL_MAP lookup (exact Anthropic model name match)
|
||||
* 4. DEFAULT_FAMILY_MAP lookup (family-based default)
|
||||
* 5. Pass through original model name
|
||||
*/
|
||||
export function resolveCodexModel(model: string): string {
|
||||
if (process.env.CODEX_MODEL) {
|
||||
return process.env.CODEX_MODEL
|
||||
}
|
||||
|
||||
const cleanModel = model.replace(/\[1m\]$/, '')
|
||||
const family = getModelFamily(cleanModel)
|
||||
if (family) {
|
||||
const familyOverride = process.env[`CODEX_DEFAULT_${family.toUpperCase()}_MODEL`]
|
||||
if (familyOverride) {
|
||||
return familyOverride
|
||||
}
|
||||
}
|
||||
|
||||
const mapped = DEFAULT_MODEL_MAP[cleanModel]
|
||||
if (mapped) {
|
||||
return mapped
|
||||
}
|
||||
|
||||
if (family) {
|
||||
return DEFAULT_FAMILY_MAP[family]
|
||||
}
|
||||
|
||||
return cleanModel
|
||||
}
|
||||
|
||||
export function resolveCodexMaxTokens(
|
||||
upperLimit: number,
|
||||
maxOutputTokensOverride?: number,
|
||||
): number {
|
||||
return (
|
||||
maxOutputTokensOverride ??
|
||||
(process.env.CODEX_MAX_TOKENS
|
||||
? parseInt(process.env.CODEX_MAX_TOKENS, 10) || undefined
|
||||
: undefined) ??
|
||||
(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
||||
? parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, 10) || undefined
|
||||
: undefined) ??
|
||||
upperLimit
|
||||
)
|
||||
}
|
||||
@@ -21,26 +21,22 @@ function makeAssistantMsg(content: string | any[]): AssistantMessage {
|
||||
|
||||
describe('anthropicMessagesToOpenAI', () => {
|
||||
test('converts system prompt to system message', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg('hello')],
|
||||
['You are helpful.'] as any,
|
||||
)
|
||||
const result = anthropicMessagesToOpenAI([makeUserMsg('hello')], [
|
||||
'You are helpful.',
|
||||
] as any)
|
||||
expect(result[0]).toEqual({ role: 'system', content: 'You are helpful.' })
|
||||
})
|
||||
|
||||
test('joins multiple system prompt strings', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg('hi')],
|
||||
['Part 1', 'Part 2'] as any,
|
||||
)
|
||||
const result = anthropicMessagesToOpenAI([makeUserMsg('hi')], [
|
||||
'Part 1',
|
||||
'Part 2',
|
||||
] as any)
|
||||
expect(result[0]).toEqual({ role: 'system', content: 'Part 1\n\nPart 2' })
|
||||
})
|
||||
|
||||
test('skips empty system prompt', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg('hi')],
|
||||
[] as any,
|
||||
)
|
||||
const result = anthropicMessagesToOpenAI([makeUserMsg('hi')], [] as any)
|
||||
expect(result[0].role).toBe('user')
|
||||
})
|
||||
|
||||
@@ -54,10 +50,12 @@ describe('anthropicMessagesToOpenAI', () => {
|
||||
|
||||
test('converts user message with content array', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg([
|
||||
{ type: 'text', text: 'line 1' },
|
||||
{ type: 'text', text: 'line 2' },
|
||||
])],
|
||||
[
|
||||
makeUserMsg([
|
||||
{ type: 'text', text: 'line 1' },
|
||||
{ type: 'text', text: 'line 2' },
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
)
|
||||
expect(result).toEqual([{ role: 'user', content: 'line 1\nline 2' }])
|
||||
@@ -73,55 +71,67 @@ describe('anthropicMessagesToOpenAI', () => {
|
||||
|
||||
test('converts assistant message with tool_use', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeAssistantMsg([
|
||||
{ type: 'text', text: 'Let me help.' },
|
||||
{
|
||||
type: 'tool_use' as const,
|
||||
id: 'toolu_123',
|
||||
name: 'bash',
|
||||
input: { command: 'ls' },
|
||||
},
|
||||
])],
|
||||
[
|
||||
makeAssistantMsg([
|
||||
{ type: 'text', text: 'Let me help.' },
|
||||
{
|
||||
type: 'tool_use' as const,
|
||||
id: 'toolu_123',
|
||||
name: 'bash',
|
||||
input: { command: 'ls' },
|
||||
},
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
)
|
||||
expect(result).toEqual([{
|
||||
role: 'assistant',
|
||||
content: 'Let me help.',
|
||||
tool_calls: [{
|
||||
id: 'toolu_123',
|
||||
type: 'function',
|
||||
function: { name: 'bash', arguments: '{"command":"ls"}' },
|
||||
}],
|
||||
}])
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'Let me help.',
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'toolu_123',
|
||||
type: 'function',
|
||||
function: { name: 'bash', arguments: '{"command":"ls"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('converts tool_result to tool message', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg([
|
||||
{
|
||||
type: 'tool_result' as const,
|
||||
tool_use_id: 'toolu_123',
|
||||
content: 'file1.txt\nfile2.txt',
|
||||
},
|
||||
])],
|
||||
[
|
||||
makeUserMsg([
|
||||
{
|
||||
type: 'tool_result' as const,
|
||||
tool_use_id: 'toolu_123',
|
||||
content: 'file1.txt\nfile2.txt',
|
||||
},
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
)
|
||||
expect(result).toEqual([{
|
||||
role: 'tool',
|
||||
tool_call_id: 'toolu_123',
|
||||
content: 'file1.txt\nfile2.txt',
|
||||
}])
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: 'tool',
|
||||
tool_call_id: 'toolu_123',
|
||||
content: 'file1.txt\nfile2.txt',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('strips thinking blocks', () => {
|
||||
test('preserves thinking blocks as reasoning_content', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: 'internal thoughts...' },
|
||||
{ type: 'text', text: 'visible response' },
|
||||
])],
|
||||
[
|
||||
makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: 'internal thoughts...' },
|
||||
{ type: 'text', text: 'visible response' },
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
)
|
||||
expect(result).toEqual([{ role: 'assistant', content: 'visible response' }])
|
||||
expect(result).toEqual([{ role: 'assistant', content: 'visible response', reasoning_content: 'internal thoughts...' }] as any)
|
||||
})
|
||||
|
||||
test('handles full conversation with tools', () => {
|
||||
@@ -157,91 +167,105 @@ describe('anthropicMessagesToOpenAI', () => {
|
||||
|
||||
test('converts base64 image to image_url', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg([
|
||||
{ type: 'text', text: 'what is this?' },
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: 'image/png',
|
||||
data: 'iVBORw0KGgo=',
|
||||
[
|
||||
makeUserMsg([
|
||||
{ type: 'text', text: 'what is this?' },
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: 'image/png',
|
||||
data: 'iVBORw0KGgo=',
|
||||
},
|
||||
},
|
||||
},
|
||||
])],
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
)
|
||||
expect(result).toEqual([{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'what is this?' },
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: 'data:image/png;base64,iVBORw0KGgo=' },
|
||||
},
|
||||
],
|
||||
}])
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'what is this?' },
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: 'data:image/png;base64,iVBORw0KGgo=' },
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('converts url image to image_url', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg([
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'url',
|
||||
url: 'https://example.com/img.png',
|
||||
[
|
||||
makeUserMsg([
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'url',
|
||||
url: 'https://example.com/img.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
])],
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
)
|
||||
expect(result).toEqual([{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: 'https://example.com/img.png' },
|
||||
},
|
||||
],
|
||||
}])
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: 'https://example.com/img.png' },
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('converts image-only message without text', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg([
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: 'image/jpeg',
|
||||
data: '/9j/4AAQ',
|
||||
[
|
||||
makeUserMsg([
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: 'image/jpeg',
|
||||
data: '/9j/4AAQ',
|
||||
},
|
||||
},
|
||||
},
|
||||
])],
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
)
|
||||
expect(result).toEqual([{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: 'data:image/jpeg;base64,/9j/4AAQ' },
|
||||
},
|
||||
],
|
||||
}])
|
||||
expect(result).toEqual([
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: 'data:image/jpeg;base64,/9j/4AAQ' },
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('defaults to image/png when media_type is missing', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg([
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'base64',
|
||||
data: 'ABC123',
|
||||
[
|
||||
makeUserMsg([
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'base64',
|
||||
data: 'ABC123',
|
||||
},
|
||||
},
|
||||
},
|
||||
])],
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
)
|
||||
expect((result[0].content as any[])[0].image_url.url).toBe(
|
||||
@@ -253,10 +277,16 @@ describe('anthropicMessagesToOpenAI', () => {
|
||||
describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||
test('preserves thinking block as reasoning_content when enabled', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg('question'), makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: 'Let me reason about this...' },
|
||||
{ type: 'text', text: 'The answer is 42.' },
|
||||
])],
|
||||
[
|
||||
makeUserMsg('question'),
|
||||
makeAssistantMsg([
|
||||
{
|
||||
type: 'thinking' as const,
|
||||
thinking: 'Let me reason about this...',
|
||||
},
|
||||
{ type: 'text', text: 'The answer is 42.' },
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
{ enableThinking: true },
|
||||
)
|
||||
@@ -269,17 +299,19 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||
expect(assistant.reasoning_content).toBe('Let me reason about this...')
|
||||
})
|
||||
|
||||
test('drops thinking block when enableThinking is false (default)', () => {
|
||||
test('preserves thinking block as reasoning_content even without enableThinking', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: 'internal thoughts...' },
|
||||
{ type: 'text', text: 'visible response' },
|
||||
])],
|
||||
[
|
||||
makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: 'internal thoughts...' },
|
||||
{ type: 'text', text: 'visible response' },
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
)
|
||||
const assistant = result[0] as any
|
||||
expect(assistant.content).toBe('visible response')
|
||||
expect(assistant.reasoning_content).toBeUndefined()
|
||||
expect(assistant.reasoning_content).toBe('internal thoughts...')
|
||||
})
|
||||
|
||||
test('preserves reasoning_content with tool_calls in same turn', () => {
|
||||
@@ -287,7 +319,10 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||
[
|
||||
makeUserMsg('what is the weather?'),
|
||||
makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: 'I need to call the weather tool.' },
|
||||
{
|
||||
type: 'thinking' as const,
|
||||
thinking: 'I need to call the weather tool.',
|
||||
},
|
||||
{ type: 'text', text: '' },
|
||||
{
|
||||
type: 'tool_use' as const,
|
||||
@@ -317,7 +352,7 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||
expect(assistant.tool_calls[0].function.name).toBe('get_weather')
|
||||
})
|
||||
|
||||
test('strips reasoning_content from previous turns', () => {
|
||||
test('always preserves reasoning_content from all turns', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[
|
||||
// Turn 1: user → assistant (with thinking)
|
||||
@@ -326,7 +361,8 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||
{ type: 'thinking' as const, thinking: 'Turn 1 reasoning...' },
|
||||
{ type: 'text', text: 'Turn 1 answer' },
|
||||
]),
|
||||
// Turn 2: new user message → previous reasoning should be stripped
|
||||
// Turn 2: new user message → reasoning should still be preserved
|
||||
// (DeepSeek requires reasoning_content to be passed back when tool calls are involved)
|
||||
makeUserMsg('question 2'),
|
||||
makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: 'Turn 2 reasoning...' },
|
||||
@@ -338,10 +374,9 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||
)
|
||||
|
||||
const assistants = result.filter(m => m.role === 'assistant')
|
||||
// Turn 1 assistant: reasoning should be stripped (previous turn)
|
||||
expect((assistants[0] as any).reasoning_content).toBeUndefined()
|
||||
// Both turns preserve reasoning_content (DeepSeek API requires it for tool calls)
|
||||
expect((assistants[0] as any).reasoning_content).toBe('Turn 1 reasoning...')
|
||||
expect((assistants[0] as any).content).toBe('Turn 1 answer')
|
||||
// Turn 2 assistant: reasoning should be preserved (current turn)
|
||||
expect((assistants[1] as any).reasoning_content).toBe('Turn 2 reasoning...')
|
||||
expect((assistants[1] as any).content).toBe('Turn 2 answer')
|
||||
})
|
||||
@@ -399,18 +434,27 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||
const assistants = result.filter(m => m.role === 'assistant')
|
||||
expect(assistants.length).toBe(3)
|
||||
// All iterations within the same turn preserve reasoning
|
||||
expect((assistants[0] as any).reasoning_content).toBe('I need the date first.')
|
||||
expect((assistants[1] as any).reasoning_content).toBe('Now I can get the weather.')
|
||||
expect((assistants[2] as any).reasoning_content).toBe('I have the info now.')
|
||||
expect((assistants[0] as any).reasoning_content).toBe(
|
||||
'I need the date first.',
|
||||
)
|
||||
expect((assistants[1] as any).reasoning_content).toBe(
|
||||
'Now I can get the weather.',
|
||||
)
|
||||
expect((assistants[2] as any).reasoning_content).toBe(
|
||||
'I have the info now.',
|
||||
)
|
||||
})
|
||||
|
||||
test('handles multiple thinking blocks in single assistant message', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg('question'), makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: 'First thought.' },
|
||||
{ type: 'thinking' as const, thinking: 'Second thought.' },
|
||||
{ type: 'text', text: 'Final answer.' },
|
||||
])],
|
||||
[
|
||||
makeUserMsg('question'),
|
||||
makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: 'First thought.' },
|
||||
{ type: 'thinking' as const, thinking: 'Second thought.' },
|
||||
{ type: 'text', text: 'Final answer.' },
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
{ enableThinking: true },
|
||||
)
|
||||
@@ -420,10 +464,13 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||
|
||||
test('skips empty thinking blocks', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg('question'), makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: '' },
|
||||
{ type: 'text', text: 'Answer.' },
|
||||
])],
|
||||
[
|
||||
makeUserMsg('question'),
|
||||
makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: '' },
|
||||
{ type: 'text', text: 'Answer.' },
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
{ enableThinking: true },
|
||||
)
|
||||
@@ -481,15 +528,18 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
|
||||
|
||||
test('sets content to null when only thinking and tool_calls present', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg('question'), makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: 'Reasoning only.' },
|
||||
{
|
||||
type: 'tool_use' as const,
|
||||
id: 'toolu_001',
|
||||
name: 'bash',
|
||||
input: { command: 'ls' },
|
||||
},
|
||||
])],
|
||||
[
|
||||
makeUserMsg('question'),
|
||||
makeAssistantMsg([
|
||||
{ type: 'thinking' as const, thinking: 'Reasoning only.' },
|
||||
{
|
||||
type: 'tool_use' as const,
|
||||
id: 'toolu_001',
|
||||
name: 'bash',
|
||||
input: { command: 'ls' },
|
||||
},
|
||||
]),
|
||||
],
|
||||
[] as any,
|
||||
{ enableThinking: true },
|
||||
)
|
||||
|
||||
@@ -18,25 +18,29 @@ describe('anthropicToolsToOpenAI', () => {
|
||||
|
||||
const result = anthropicToolsToOpenAI(tools as any)
|
||||
|
||||
expect(result).toEqual([{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'bash',
|
||||
description: 'Run a bash command',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: { command: { type: 'string' } },
|
||||
required: ['command'],
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'bash',
|
||||
description: 'Run a bash command',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: { command: { type: 'string' } },
|
||||
required: ['command'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}])
|
||||
])
|
||||
})
|
||||
|
||||
test('uses empty schema when input_schema missing', () => {
|
||||
const tools = [{ type: 'custom', name: 'noop', description: 'no-op' }]
|
||||
const result = anthropicToolsToOpenAI(tools as any)
|
||||
|
||||
expect((result[0] as { function: { parameters: unknown } }).function.parameters).toEqual({ type: 'object', properties: {} })
|
||||
expect(
|
||||
(result[0] as { function: { parameters: unknown } }).function.parameters,
|
||||
).toEqual({ type: 'object', properties: {} })
|
||||
})
|
||||
|
||||
test('strips Anthropic-specific fields', () => {
|
||||
@@ -76,7 +80,8 @@ describe('anthropicToolsToOpenAI', () => {
|
||||
},
|
||||
]
|
||||
const result = anthropicToolsToOpenAI(tools as any)
|
||||
const props = (result[0] as { function: { parameters: any } }).function.parameters as any
|
||||
const props = (result[0] as { function: { parameters: any } }).function
|
||||
.parameters as any
|
||||
expect(props.properties.mode).toEqual({ enum: ['read'] })
|
||||
expect(props.properties.mode.const).toBeUndefined()
|
||||
expect(props.properties.name).toEqual({ type: 'string' })
|
||||
@@ -110,8 +115,11 @@ describe('anthropicToolsToOpenAI', () => {
|
||||
},
|
||||
]
|
||||
const result = anthropicToolsToOpenAI(tools as any)
|
||||
const params = (result[0] as { function: { parameters: any } }).function.parameters as any
|
||||
expect(params.properties.outer.properties.inner).toEqual({ enum: ['fixed'] })
|
||||
const params = (result[0] as { function: { parameters: any } }).function
|
||||
.parameters as any
|
||||
expect(params.properties.outer.properties.inner).toEqual({
|
||||
enum: ['fixed'],
|
||||
})
|
||||
expect(params.definitions.MyType.properties.field).toEqual({ enum: [42] })
|
||||
})
|
||||
|
||||
@@ -125,18 +133,17 @@ describe('anthropicToolsToOpenAI', () => {
|
||||
type: 'object',
|
||||
properties: {
|
||||
val: {
|
||||
anyOf: [
|
||||
{ const: 'a' },
|
||||
{ const: 'b' },
|
||||
{ type: 'string' },
|
||||
],
|
||||
anyOf: [{ const: 'a' }, { const: 'b' }, { type: 'string' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
const result = anthropicToolsToOpenAI(tools as any)
|
||||
const anyOf = ((result[0] as { function: { parameters: any } }).function.parameters as any).properties.val.anyOf
|
||||
const anyOf = (
|
||||
(result[0] as { function: { parameters: any } }).function
|
||||
.parameters as any
|
||||
).properties.val.anyOf
|
||||
expect(anyOf[0]).toEqual({ enum: ['a'] })
|
||||
expect(anyOf[1]).toEqual({ enum: ['b'] })
|
||||
expect(anyOf[2]).toEqual({ type: 'string' })
|
||||
|
||||
@@ -26,16 +26,16 @@ export interface ConvertMessagesOptions {
|
||||
* - system prompt → role: "system" message prepended
|
||||
* - tool_use blocks → tool_calls[] on assistant message
|
||||
* - tool_result blocks → role: "tool" messages
|
||||
* - thinking blocks → silently dropped (or preserved as reasoning_content when enableThinking=true)
|
||||
* - thinking blocks → preserved as reasoning_content (DeepSeek requires passing it back)
|
||||
* - cache_control → stripped
|
||||
*/
|
||||
export function anthropicMessagesToOpenAI(
|
||||
messages: (UserMessage | AssistantMessage)[],
|
||||
systemPrompt: SystemPrompt,
|
||||
options?: ConvertMessagesOptions,
|
||||
// options retained for API compatibility; thinking blocks are now always preserved
|
||||
_options?: ConvertMessagesOptions,
|
||||
): ChatCompletionMessageParam[] {
|
||||
const result: ChatCompletionMessageParam[] = []
|
||||
const enableThinking = options?.enableThinking ?? false
|
||||
|
||||
// Prepend system prompt as system message
|
||||
const systemText = systemPromptToText(systemPrompt)
|
||||
@@ -46,50 +46,13 @@ export function anthropicMessagesToOpenAI(
|
||||
} satisfies ChatCompletionSystemMessageParam)
|
||||
}
|
||||
|
||||
// When thinking mode is on, detect turn boundaries so that reasoning_content
|
||||
// from *previous* user turns is stripped (saves bandwidth; DeepSeek ignores it).
|
||||
// A "new turn" starts when a user text message appears after at least one assistant response.
|
||||
const turnBoundaries = new Set<number>()
|
||||
if (enableThinking) {
|
||||
let hasSeenAssistant = false
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i]
|
||||
if (msg.type === 'assistant') {
|
||||
hasSeenAssistant = true
|
||||
}
|
||||
if (msg.type === 'user' && hasSeenAssistant) {
|
||||
const content = msg.message.content
|
||||
// A user message starts a new turn if it contains any non-tool_result content
|
||||
// (text, image, or other media). Tool results alone do NOT start a new turn
|
||||
// because they are continuations of the previous assistant tool call.
|
||||
const startsNewUserTurn = typeof content === 'string'
|
||||
? content.length > 0
|
||||
: Array.isArray(content) && content.some(
|
||||
(b: any) =>
|
||||
typeof b === 'string' ||
|
||||
(b &&
|
||||
typeof b === 'object' &&
|
||||
'type' in b &&
|
||||
b.type !== 'tool_result'),
|
||||
)
|
||||
if (startsNewUserTurn) {
|
||||
turnBoundaries.add(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i]
|
||||
for (const msg of messages) {
|
||||
switch (msg.type) {
|
||||
case 'user':
|
||||
result.push(...convertInternalUserMessage(msg))
|
||||
break
|
||||
case 'assistant':
|
||||
// Preserve reasoning_content unless we're before a turn boundary
|
||||
// (i.e., from a previous user Q&A round)
|
||||
const preserveReasoning = enableThinking && !isBeforeAnyTurnBoundary(i, turnBoundaries)
|
||||
result.push(...convertInternalAssistantMessage(msg, preserveReasoning))
|
||||
result.push(...convertInternalAssistantMessage(msg))
|
||||
break
|
||||
default:
|
||||
break
|
||||
@@ -101,20 +64,7 @@ export function anthropicMessagesToOpenAI(
|
||||
|
||||
function systemPromptToText(systemPrompt: SystemPrompt): string {
|
||||
if (!systemPrompt || systemPrompt.length === 0) return ''
|
||||
return systemPrompt
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if index `i` falls before any turn boundary (i.e. it belongs to a previous turn).
|
||||
* A message at index i is "before" a boundary if there exists a boundary j where i < j.
|
||||
*/
|
||||
function isBeforeAnyTurnBoundary(i: number, boundaries: Set<number>): boolean {
|
||||
for (const b of boundaries) {
|
||||
if (i < b) return true
|
||||
}
|
||||
return false
|
||||
return systemPrompt.filter(Boolean).join('\n\n')
|
||||
}
|
||||
|
||||
function convertInternalUserMessage(
|
||||
@@ -131,7 +81,8 @@ function convertInternalUserMessage(
|
||||
} else if (Array.isArray(content)) {
|
||||
const textParts: string[] = []
|
||||
const toolResults: BetaToolResultBlockParam[] = []
|
||||
const imageParts: Array<{ type: 'image_url'; image_url: { url: string } }> = []
|
||||
const imageParts: Array<{ type: 'image_url'; image_url: { url: string } }> =
|
||||
[]
|
||||
|
||||
for (const block of content) {
|
||||
if (typeof block === 'string') {
|
||||
@@ -141,7 +92,9 @@ function convertInternalUserMessage(
|
||||
} else if (block.type === 'tool_result') {
|
||||
toolResults.push(block as BetaToolResultBlockParam)
|
||||
} else if (block.type === 'image') {
|
||||
const imagePart = convertImageBlockToOpenAI(block as unknown as Record<string, unknown>)
|
||||
const imagePart = convertImageBlockToOpenAI(
|
||||
block as unknown as Record<string, unknown>,
|
||||
)
|
||||
if (imagePart) {
|
||||
imageParts.push(imagePart)
|
||||
}
|
||||
@@ -158,7 +111,10 @@ function convertInternalUserMessage(
|
||||
|
||||
// 如果有图片,构建多模态 content 数组
|
||||
if (imageParts.length > 0) {
|
||||
const multiContent: Array<{ type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }> = []
|
||||
const multiContent: Array<
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'image_url'; image_url: { url: string } }
|
||||
> = []
|
||||
if (textParts.length > 0) {
|
||||
multiContent.push({ type: 'text', text: textParts.join('\n') })
|
||||
}
|
||||
@@ -206,7 +162,6 @@ function convertToolResult(
|
||||
|
||||
function convertInternalAssistantMessage(
|
||||
msg: AssistantMessage,
|
||||
preserveReasoning = false,
|
||||
): ChatCompletionMessageParam[] {
|
||||
const content = msg.message.content
|
||||
|
||||
@@ -229,7 +184,9 @@ function convertInternalAssistantMessage(
|
||||
}
|
||||
|
||||
const textParts: string[] = []
|
||||
const toolCalls: NonNullable<ChatCompletionAssistantMessageParam['tool_calls']> = []
|
||||
const toolCalls: NonNullable<
|
||||
ChatCompletionAssistantMessageParam['tool_calls']
|
||||
> = []
|
||||
const reasoningParts: string[] = []
|
||||
|
||||
for (const block of content) {
|
||||
@@ -248,9 +205,12 @@ function convertInternalAssistantMessage(
|
||||
typeof tu.input === 'string' ? tu.input : JSON.stringify(tu.input),
|
||||
},
|
||||
})
|
||||
} else if (block.type === 'thinking' && preserveReasoning) {
|
||||
// DeepSeek thinking mode: preserve reasoning_content for tool call iterations
|
||||
const thinkingText = (block as unknown as Record<string, unknown>).thinking
|
||||
} else if (block.type === 'thinking') {
|
||||
// DeepSeek thinking mode: always preserve reasoning_content.
|
||||
// DeepSeek requires reasoning_content to be passed back in subsequent requests,
|
||||
// especially when tool calls are involved (returns 400 if missing).
|
||||
const thinkingText = (block as unknown as Record<string, unknown>)
|
||||
.thinking
|
||||
if (typeof thinkingText === 'string' && thinkingText) {
|
||||
reasoningParts.push(thinkingText)
|
||||
}
|
||||
@@ -262,7 +222,9 @@ function convertInternalAssistantMessage(
|
||||
role: 'assistant',
|
||||
content: textParts.length > 0 ? textParts.join('\n') : null,
|
||||
...(toolCalls.length > 0 && { tool_calls: toolCalls }),
|
||||
...(reasoningParts.length > 0 && { reasoning_content: reasoningParts.join('\n') }),
|
||||
...(reasoningParts.length > 0 && {
|
||||
reasoning_content: reasoningParts.join('\n'),
|
||||
}),
|
||||
}
|
||||
|
||||
return [result]
|
||||
|
||||
@@ -16,21 +16,27 @@ export function anthropicToolsToOpenAI(
|
||||
.filter(tool => {
|
||||
// Only convert standard tools (skip server tools like computer_use, etc.)
|
||||
const toolType = (tool as unknown as { type?: string }).type
|
||||
return tool.type === 'custom' || !('type' in tool) || toolType !== 'server'
|
||||
return (
|
||||
tool.type === 'custom' || !('type' in tool) || toolType !== 'server'
|
||||
)
|
||||
})
|
||||
.map(tool => {
|
||||
// Handle the various tool shapes from Anthropic SDK
|
||||
const anyTool = tool as unknown as Record<string, unknown>
|
||||
const name = (anyTool.name as string) || ''
|
||||
const description = (anyTool.description as string) || ''
|
||||
const inputSchema = anyTool.input_schema as Record<string, unknown> | undefined
|
||||
const inputSchema = anyTool.input_schema as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
|
||||
return {
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name,
|
||||
description,
|
||||
parameters: sanitizeJsonSchema(inputSchema || { type: 'object', properties: {} }),
|
||||
parameters: sanitizeJsonSchema(
|
||||
inputSchema || { type: 'object', properties: {} },
|
||||
),
|
||||
},
|
||||
} satisfies ChatCompletionTool
|
||||
})
|
||||
@@ -43,7 +49,9 @@ export function anthropicToolsToOpenAI(
|
||||
* support the `const` keyword in JSON Schema. Convert it to `enum` with a
|
||||
* single-element array, which is semantically equivalent.
|
||||
*/
|
||||
function sanitizeJsonSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
||||
function sanitizeJsonSchema(
|
||||
schema: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
if (!schema || typeof schema !== 'object') return schema
|
||||
|
||||
const result = { ...schema }
|
||||
@@ -55,20 +63,37 @@ function sanitizeJsonSchema(schema: Record<string, unknown>): Record<string, unk
|
||||
}
|
||||
|
||||
// Recursively process nested schemas
|
||||
const objectKeys = ['properties', 'definitions', '$defs', 'patternProperties'] as const
|
||||
const objectKeys = [
|
||||
'properties',
|
||||
'definitions',
|
||||
'$defs',
|
||||
'patternProperties',
|
||||
] as const
|
||||
for (const key of objectKeys) {
|
||||
const nested = result[key]
|
||||
if (nested && typeof nested === 'object') {
|
||||
const sanitized: Record<string, unknown> = {}
|
||||
for (const [k, v] of Object.entries(nested as Record<string, unknown>)) {
|
||||
sanitized[k] = v && typeof v === 'object' ? sanitizeJsonSchema(v as Record<string, unknown>) : v
|
||||
sanitized[k] =
|
||||
v && typeof v === 'object'
|
||||
? sanitizeJsonSchema(v as Record<string, unknown>)
|
||||
: v
|
||||
}
|
||||
result[key] = sanitized
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process single-schema keys
|
||||
const singleKeys = ['items', 'additionalProperties', 'not', 'if', 'then', 'else', 'contains', 'propertyNames'] as const
|
||||
const singleKeys = [
|
||||
'items',
|
||||
'additionalProperties',
|
||||
'not',
|
||||
'if',
|
||||
'then',
|
||||
'else',
|
||||
'contains',
|
||||
'propertyNames',
|
||||
] as const
|
||||
for (const key of singleKeys) {
|
||||
const nested = result[key]
|
||||
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
||||
@@ -82,7 +107,9 @@ function sanitizeJsonSchema(schema: Record<string, unknown>): Record<string, unk
|
||||
const nested = result[key]
|
||||
if (Array.isArray(nested)) {
|
||||
result[key] = nested.map(item =>
|
||||
item && typeof item === 'object' ? sanitizeJsonSchema(item as Record<string, unknown>) : item
|
||||
item && typeof item === 'object'
|
||||
? sanitizeJsonSchema(item as Record<string, unknown>)
|
||||
: item,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
let currentContentIndex = -1
|
||||
|
||||
// Track tool_use blocks: tool_calls index → { contentIndex, id, name, arguments }
|
||||
const toolBlocks = new Map<number, { contentIndex: number; id: string; name: string; arguments: string }>()
|
||||
const toolBlocks = new Map<
|
||||
number,
|
||||
{ contentIndex: number; id: string; name: string; arguments: string }
|
||||
>()
|
||||
|
||||
// Track thinking block state
|
||||
let thinkingBlockOpen = false
|
||||
@@ -197,7 +200,8 @@ export async function* adaptOpenAIStreamToAnthropic(
|
||||
|
||||
// Start new tool_use block
|
||||
currentContentIndex++
|
||||
const toolId = tc.id || `toolu_${randomUUID().replace(/-/g, '').slice(0, 24)}`
|
||||
const toolId =
|
||||
tc.id || `toolu_${randomUUID().replace(/-/g, '').slice(0, 24)}`
|
||||
const toolName = tc.function?.name || ''
|
||||
|
||||
toolBlocks.set(tcIndex, {
|
||||
|
||||
34
packages/acp-link/.gitignore
vendored
Normal file
34
packages/acp-link/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
125
packages/acp-link/README.md
Normal file
125
packages/acp-link/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# acp-link
|
||||
|
||||
ACP proxy server that bridges WebSocket clients to ACP (Agent Client Protocol) agents.
|
||||
|
||||
> Source code adapted from [chrome-acp](https://github.com/Areo-Joe/chrome-acp).
|
||||
|
||||
## Installation
|
||||
|
||||
### From source
|
||||
|
||||
```bash
|
||||
# From monorepo root
|
||||
bun install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Via global install
|
||||
acp-link /path/to/agent
|
||||
|
||||
# Via source
|
||||
bun src/cli/bin.ts /path/to/agent
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Basic usage
|
||||
acp-link /path/to/agent
|
||||
|
||||
# With custom port and host
|
||||
acp-link --port 9000 --host 0.0.0.0 /path/to/agent
|
||||
|
||||
# With debug logging
|
||||
acp-link --debug /path/to/agent
|
||||
|
||||
# Enable HTTPS with self-signed certificate
|
||||
acp-link --https /path/to/agent
|
||||
|
||||
# Disable authentication (dangerous)
|
||||
acp-link --no-auth /path/to/agent
|
||||
|
||||
# Register to RCS with a specific channel group
|
||||
acp-link --group my-team /path/to/agent
|
||||
|
||||
# Pass arguments to the agent (use -- to separate)
|
||||
acp-link /path/to/agent -- --verbose --model gpt-4
|
||||
```
|
||||
|
||||
## CLI Reference
|
||||
|
||||
```
|
||||
USAGE
|
||||
acp-link [--port value] [--host value] [--debug] [--no-auth] [--https] [--group value] <command>...
|
||||
acp-link --help
|
||||
acp-link --version
|
||||
|
||||
FLAGS
|
||||
[--port] Port to listen on [default = 9315]
|
||||
[--host] Host to bind to [default = localhost]
|
||||
[--debug] Enable debug logging to file
|
||||
[--no-auth] Disable authentication (dangerous)
|
||||
[--https] Enable HTTPS with self-signed cert
|
||||
[--group] Channel group ID for RCS registration (letters, digits, hyphens, underscores only)
|
||||
-h --help Print help information and exit
|
||||
-v --version Print version information and exit
|
||||
|
||||
ARGUMENTS
|
||||
command... Agent command followed by its arguments
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Listens for WebSocket connections from clients
|
||||
2. When a "connect" message is received, spawns the configured ACP agent as a subprocess
|
||||
3. Bridges messages between the WebSocket (client) and stdin/stdout (agent via ACP protocol)
|
||||
4. Supports session management: create, load, resume, list sessions
|
||||
5. Handles permission approval flow and heartbeat keepalive
|
||||
|
||||
## Authentication
|
||||
|
||||
By default, a random token is auto-generated on startup. Connect to the
|
||||
WebSocket endpoint without putting the token in the URL:
|
||||
|
||||
```
|
||||
ws://localhost:9315/ws
|
||||
```
|
||||
|
||||
Set `ACP_AUTH_TOKEN` env var to use a fixed token, or use `--no-auth` to
|
||||
disable (not recommended). Clients that cannot send an `Authorization` header
|
||||
must send the token in a WebSocket subprotocol named
|
||||
`rcs.auth.<base64url-token>`.
|
||||
|
||||
## RCS Upstream
|
||||
|
||||
acp-link can register to a Remote Control Server (RCS) for remote access. Set the following environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `ACP_RCS_URL` | RCS server URL (e.g. `http://rcs.example.com:3000`) |
|
||||
| `ACP_RCS_TOKEN` | API token for RCS authentication |
|
||||
| `ACP_RCS_GROUP` | Channel group ID to lock the agent into (letters, digits, `-`, `_` only) |
|
||||
|
||||
You can also use `--group <id>` on the CLI. The CLI flag takes priority over the env var.
|
||||
|
||||
## Manager UI
|
||||
|
||||
通过 `--manager` flag 启动独立的管理服务(不启动代理):
|
||||
|
||||
```bash
|
||||
# 启动 Manager(默认端口 9315)
|
||||
acp-link --manager
|
||||
|
||||
# 指定端口
|
||||
acp-link --manager --port 3210
|
||||
```
|
||||
|
||||
在浏览器打开 `http://localhost:<port>` 即可访问管理界面,创建、停止、删除多个 acp-link 子进程实例并实时查看日志。
|
||||
|
||||
通过 Manager UI 创建的子进程会自动跳过 Manager UI。
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user