将 Command Code API 转换为 OpenAI / Anthropic 兼容接口的反代代理。单文件,零外部依赖。
逐条对齐官方 npm 包源码(command-code@1.53.1;dist/cli.mjs 只是压缩、没有混淆)—— 详见 PROTOCOL-FACTS-1.53.1.md:
完整功能:OpenAI Chat Completions + Anthropic Messages API | 流式/非流式输出 | 工具调用 (tool_use) | 多模态图片输入 | 推理强度 (reasoning_effort) | 动态模型列表 | 缓存命中指标 | 设备指纹伪装(per-key 绑定、自动刷新)| x-api-key 鉴权(Anthropic SDK)| 客户端断连检测(上游中止) | 零输出 → 429 自动重试 | 连续超时 → 429 自动重试 | 隐私保护日志
社区: Linux.do — 一个友好的中文技术社区。
npm start # 启动(仓库自带 config.json,监听 http://0.0.0.0:3050)
npm run dev # watch 模式(文件修改自动重启)API Key 通过 Authorization 请求头(Anthropic SDK 可用 x-api-key)传入,无需配置到文件中。Key 必须以 user_ 开头(自动匹配任意前缀,如 Bearer token_user_xxx):
curl http://127.0.0.1:3050/v1/chat/completions \
-H "Authorization: Bearer user_xxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"model":"deepseek/deepseek-v4-flash","messages":[{"role":"user","content":"hi"}]}'commandcode/
├── config.json # 端口 / 日志路径等
├── LICENSE # MIT License
├── package.json # npm start / npm run dev
├── proxy.mjs # 单文件核心代理(~1900 行)
├── Dockerfile # 容器构建文件(node:22-alpine)
├── docker-compose.yml # 容器编排
├── .dockerignore # 构建上下文排除规则
├── .github/
│ └── workflows/
│ └── docker-publish.yml # 打 v* tag 时自动发布 GHCR 多架构镜像
├── captured-requests/ # CLI 抓包数据(协议逆向参考)
├── README.md # 英文文档
└── README_zh.md # 本文档(中文)
| 字段 | 默认值 | 说明 |
|---|---|---|
port |
3000 |
监听端口(仓库自带 config.json 为 3050) |
host |
0.0.0.0 |
监听地址 |
apiBase |
https://api.commandcode.ai |
CC API 地址 |
projectSlug |
cc-proxy |
x-project-slug header |
apiKey |
"" |
可选兜底 API Key(请求也可通过 header 传入) |
logFile |
"" |
日志文件路径(空=仅控制台) |
logLevel |
info |
日志级别 |
useProviderModels |
true |
从 Provider API 动态拉取模型列表 |
modelRefreshIntervalMs |
300000 |
模型列表缓存刷新间隔(5min) |
zdr |
false |
请求 Command Code 使用 ZDR-only 路由 |
| 变量 | 对应 config 字段 |
|---|---|
PORT |
port |
HOST |
host |
CC_API_BASE |
apiBase |
PROJECT_SLUG |
projectSlug |
LOG_FILE |
logFile |
CC_USE_PROVIDER_MODELS |
useProviderModels |
CC_STREAM_IDLE_MS |
流式上游读空闲超时(默认 30000) |
CC_NONSTREAM_IDLE_MS |
非流式上游读空闲超时(默认 90000) |
CC_MAX_INFLIGHT |
进程内在途请求上限(默认 0 = 不限) |
CMD_ZDR |
zdr(1 开启) |
CC_UPSTREAM_PROXY |
upstreamProxy |
CC_FINGERPRINT_SALT |
fingerprintSalt(成批更换设备身份) |
CC_DEVICE_PROJECT_DIR |
deviceProjectDir(伪造的项目目录,与 x-project-slug 同源) |
CC_CLI_MODE |
cliMode(信封 mode) |
CC_CLI_SESSION_MODE |
cliSessionMode(lifecycle metadata 的 mode,另一个枚举) |
开启后,代理会在 Command Code 生成请求以及 fingerprint/lifecycle 初始化请求中附加
x-cmd-zdr: 1。npm 版本检查和代理自己的 /provider/v1/models 模型目录请求不会附加该
header。该开关只是请求 Command Code 使用 ZDR-only 路由,实际数据留存和上游可用性仍由上游服务决定。
请求体上限:独立于 config.json —— 超过 100MB 的请求会被拒绝并返回 HTTP 413(连接保持可排空,不会直接 reset)。可用 CC_MAX_BODY_MB(正整数,单位 MB)覆盖。
⚠️ 内存放大:请求体在转发到上游前会存在多份副本,实测峰值 ≈ body 大小 × 5.1~7.4(7MB→+52MB、20MB→+116MB;被413拒绝的请求只要 ×1.05)。因此默认CC_MAX_BODY_MB=100意味着单个请求最坏可吃 ~550MB,且该上限是每请求的、不是全局的。详见内存与部署。
相关配置:fingerprintSalt / CC_FINGERPRINT_SALT、deviceProjectDir / CC_DEVICE_PROJECT_DIR。
上报给 /alpha/fingerprint/record 的设备指纹由 API key 确定性派生(fpDigest(apiKey, field) = sha256(salt + "\\0" + apiKey + "\\0" + field)),因此一个 key 恒定对应一台设备:
| 事件 | 原行为(随机) | 现行为(派生) |
|---|---|---|
| 进程重启 | Map 清空 → 换一台机器 | 同一台机器 |
| 第二个实例 | 同一 key = 两台机器 | 同一台机器 |
| session 过期(12h) | keyStateStore.delete → 每 12h 换一台机器 |
同一台机器 |
12h 那条最明显:真实用户不会一天换两次电脑。而上游
device_fingerprints表是按(userId, thumbmark)建唯一索引的。
为什么用「派生」而不是「按哈希取桶选设备」 —— 固定池的熵上限就是池的大小,key 数一旦超过池容量,多个 key 就必然共用指纹:约 50 个 key 配 1000 个池 → 约 2 个碰撞;配 100 个池 → 约 20 个碰撞。同一个 thumbmark 出现在两个不同 userId 下,就是「多账号同机」的直接证据 —— 这正是最不该主动制造的东西。派生方案每个 key 仍是独立设备(碰撞概率 2⁻²⁵⁶),同时保持稳定。
CC_FINGERPRINT_SALT=some-local-secret npm start # 可选:成批更换所有 key 的设备身份
CC_DEVICE_PROJECT_DIR='C:\\Users\\you\\projects\\app' npm start # 可选:改伪造的项目目录(slug 随之改变)盐是可选的但建议设:不设时派生是 API key 的纯函数,知道算法的人可以反推出你所有用户的指纹;设了之后同一个 key 在不同部署上得到不同设备,且没有额外成本。
信号值也是伪造的:上游 CLI 读真实机器(Windows 注册表 MachineGuid、网卡 MAC、os.userInfo、git config),本代理按 API key 派生出一组形状逼真的替代值 —— MachineGuid 的 8-4-4-4-12 形状、xx:xx:xx:xx:xx:xx 的 MAC、DESKTOP-xxxxxx 主机名、可读的 git 邮箱。这些原始值只存在于进程内存,出网的只有它们的哈希。
候选池选取用「打分取最大」而非取模 —— 取模在池子扩容时会让所有 key 一起换设备;打分取最大只影响「新候选恰好胜出」的那部分 key。
哈希构造对齐官方 CLI(command-code 的 buildMachineFingerprint / hashSignal):thumbmark = sha256(IB + "\0machine\0" + [machineId, macs.join(",")].join("|")),其中 IB = "command-code:device-fingerprint:v1";各 component 按 sha256(IB + "\0" + value.toLowerCase()) 计算。原实现直接对随机 hex 求 sha256(缺 IB 前缀),且 thumbmark 由各 component 的哈希拼成 —— 上游两种都无从验算(它拿不到原始 machineId),所以检测不到;现在已对齐。
本次未处理:外观池仍是清一色高端桌面 CPU,时区仍从全球池里均匀取。
timezone是客户端本机操作系统的时区(Intl.DateTimeFormat().resolvedOptions().timeZone),不是出口 IP 的时区。所以不要把它绑定到出口 IP:中国大陆用户通过代理访问本服务时,本机时区与流量出口地不一致是常态而非异常。真正有意义的性质是它在你自身用户群里的分布,而不是与 IP 是否一致。如果你的用户集中在一个地区,却从 15 个全球时区里均匀取,就会让每个账号看起来来自不同的大洲。应当让池子匹配实际使用这个部署的人群 —— 这是运维决策:单一地区用户群应当收窄(或加权)
FINGERPRINT_TZS,而不是全球随机。
让代理发往 Command Code 的请求走本地 HTTP 代理 —— 用于出口地区调整,或排查风控 403 时做 IP 维度对照。
{ "upstreamProxy": "http://127.0.0.1:7890" }CC_UPSTREAM_PROXY=http://127.0.0.1:7890 npm start- 作用于
/alpha/generate、/alpha/fingerprint/record、/alpha/lifecycle-events与/provider/v1/models。 - 不影响本地监听、
/health与 npm 版本检查。 - 仅支持
http://(CONNECT)代理。实现方式是自建 CONNECT 隧道 +node:https复用同一 socket,不新增任何依赖,Node 18+ 即可用。 - 每个上游请求各自建立一条隧道连接。TLS 为端到端:证书按
api.commandcode.ai校验,绝不针对代理降级。 - 指纹/lifecycle 预请求也走代理是刻意的:若它们直连而上游生成走代理,同一账号会从两个不同 IP 注册 —— 正是你想避免的那种矛盾。
Node 原生
fetch不读HTTPS_PROXY/HTTP_PROXY。官方环境变量路线需要 Node ≥ 22.21 / 24.5 且设NODE_USE_ENV_PROXY=1;本选项两者都不需要。
OpenAI Chat Completions 兼容。支持流式和非流式、工具调用、多模态图片输入、推理强度。
请求体参数:
| 参数 | 必填 | 说明 |
|---|---|---|
model |
是 | 模型 ID(见模型列表) |
messages |
是 | 对话消息,支持 system/user/assistant/tool 角色 |
max_tokens |
否 | 最大生成 token(默认 64000) |
stream |
否 | 是否 SSE 流式(默认 false) |
temperature |
否 | 采样温度(0-2) |
reasoning_effort |
否 | 推理强度 low/medium/high/max |
tools |
否 | 工具定义(OpenAI function calling 格式) |
tool_choice |
否 | 工具选择策略 |
parallel_tool_calls |
否 | 是否允许并行工具调用 |
简单请求:
{
"model": "deepseek/deepseek-v4-flash",
"messages": [{ "role": "user", "content": "hello" }],
"stream": true
}多模态图片输入(需 vision 模型):
{
"model": "xiaomi/mimo-v2.5",
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "描述这张图片" },
{ "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,..." } }
]
}]
}工具调用:
{
"model": "deepseek/deepseek-v4-flash",
"messages": [...],
"tools": [{
"type": "function",
"function": { "name": "get_weather", "description": "...", "parameters": {...} }
}],
"tool_choice": "auto"
}流式响应(SSE):
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"思考过程"}}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"}}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30,"prompt_tokens_details":{"cached_tokens":8}}}
data: [DONE]
非流式响应(含缓存命中):
{
"id": "chatcmpl-xxx",
"object": "chat.completion",
"created": 1234567890,
"model": "deepseek/deepseek-v4-flash",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello!",
"reasoning_content": "The user said hello, I should respond."
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 7558,
"completion_tokens": 42,
"total_tokens": 7600,
"prompt_tokens_details": { "cached_tokens": 7552 }
}
}Anthropic Messages API 兼容端点。支持流式和非流式、工具调用。
请求体:
{
"model": "claude-sonnet-4-6",
"max_tokens": 1000,
"system": "你是一个有用的助手。",
"messages": [
{ "role": "user", "content": "hello" }
],
"stream": true
}Anthropic 协议差异(自动转换):
| 概念 | Anthropic 原始格式 | 转换说明 |
|---|---|---|
| System prompt | 顶层 system 字段 |
自动转为 OpenAI system message |
| 消息内容 | content 数组(text/tool_use/tool_result) |
自动映射为对应角色 |
| 工具结果 | user 消息中的 tool_result 块 |
自动转为 role: "tool" |
| 工具定义 | input_schema |
自动映射为 parameters |
tool_choice |
{type:"auto"/"any"/"tool"} |
any→required,tool→function 对象 |
| 推理强度 | thinking.budget_tokens |
自动映射为 reasoning_effort(≥10000→high, ≥5000→medium, ≥2000→low) |
| 停止原因 | end_turn/max_tokens/tool_use |
自动映射为 stop/length/tool_calls |
| Token 用量 | input_tokens/output_tokens + 缓存 |
透传,缓存字段映射为 Anthropic 格式 |
流式响应(SSE,Anthropic 格式):
event: message_start
data: {"type":"message_start","message":{"id":"msg_xxx","type":"message","role":"assistant","content":[],"model":"...","usage":{"input_tokens":0,"output_tokens":0}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":10,"cache_read_input_tokens":0,"input_tokens":100}}
event: message_stop
data: {"type":"message_stop"}
非流式响应:
{
"id": "msg_xxx",
"type": "message",
"role": "assistant",
"model": "deepseek/deepseek-v4-flash",
"content": [{ "type": "text", "text": "Hello!" }],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 7558,
"output_tokens": 42,
"cache_read_input_tokens": 7552,
"cache_creation_input_tokens": null
}
}返回可用模型列表。优先从 Provider API 动态拉取(5min 缓存),失败回退硬编码列表。
健康检查。返回 OK。
| HTTP 状态 | 说明 |
|---|---|
| 400 | 请求格式错误 |
| 401 | API Key 缺失/格式不对/无效(Key 必须以 user_ 开头;通过 Authorization: Bearer 或 x-api-key 传入) |
| 429 | 零输出 token,或流空闲超时(30s 流式 / 90s 非流式)——带 Retry-After,SDK 自动重试;连续 3 次超时返回"压缩上下文"提示 |
| 502 | CC 上游错误 |
代理访问 GET /v1/models 会返回实时模型列表。以下为常见模型参考,完整列表以实际接口返回为准——各模型套餐可参考 Command Code Pricing。
| 模型 ID | 提供商 |
|---|---|
claude-sonnet-4-6 / claude-opus-4-8 / claude-opus-4-7 / claude-haiku-4-5-20251001 |
Anthropic |
gpt-5.5 / gpt-5.4 / gpt-5.4-mini / gpt-5.3-codex |
OpenAI |
deepseek/deepseek-v4-pro / deepseek/deepseek-v4-flash |
DeepSeek |
moonshotai/Kimi-K2.6 / moonshotai/Kimi-K2.5 |
Kimi |
zai-org/GLM-5.1 / zai-org/GLM-5 |
GLM |
MiniMaxAI/MiniMax-M3 / MiniMaxAI/MiniMax-M2.7 / MiniMaxAI/MiniMax-M2.5 |
MiniMax |
Qwen/Qwen3.7-Max / Qwen/Qwen3.6-Max-Preview / Qwen/Qwen3.6-Plus |
Qwen |
stepfun/Step-3.7-Flash / stepfun/Step-3.5-Flash |
Step |
xiaomi/mimo-v2.5-pro / xiaomi/mimo-v2.5 |
Xiaomi(支持图片输入) |
google/gemini-3.5-flash / google/gemini-3.1-flash-lite |
Gemini |
⚠️ 部分模型(如deepseek-v4-flash、claude-sonnet-4-6)不支持图片输入。如需多模态请用xiaomi/mimo-v2.5、Kimi-K2.5等 vision 模型。
from openai import OpenAI
client = OpenAI(
api_key="user_xxxxxxxxx",
base_url="http://127.0.0.1:3050/v1",
)
response = client.chat.completions.create(
model="deepseek/deepseek-v4-flash",
messages=[{"role": "user", "content": "hello"}],
stream=True,
)
for chunk in response:
print(chunk.choices[0].delta.content or "", end="")curl http://127.0.0.1:3050/v1/chat/completions \
-H "Authorization: Bearer user_xxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek/deepseek-v4-flash",
"messages": [{"role": "user", "content": "hello"}],
"stream": true
}'在 Cursor 设置中添加 Custom Provider:
- API Base URL:
http://127.0.0.1:3050/v1 - API Key:
user_xxxxxxxxx - Model: 从模型列表中选择
import anthropic
client = anthropic.Anthropic(
api_key="user_xxxxxxxxx",
base_url="http://127.0.0.1:3050",
)
message = client.messages.create(
model="deepseek/deepseek-v4-flash",
max_tokens=1000,
system="You are helpful.",
messages=[{"role": "user", "content": "hello"}],
)
print(message.content[0].text)Anthropic SDK 通过 x-api-key 头鉴权——代理已原生支持(无需 Authorization 头)。
{
"provider": "openai-compatible",
"baseUrl": "http://127.0.0.1:3050/v1",
"apiKey": "user_xxxxxxxxx"
}基于对官方 CLI 网络流量的分析(版本号从 npm registry 动态拉取),实现了以下兼容适配:
| 机制 | 实现 |
|---|---|
| 设备指纹 | 每个 Key 首次请求前发送 POST /alpha/fingerprint/record;信号值(Windows MachineGuid 形状、真实形状的 MAC、DESKTOP-xxxxxx 主机名)由 API key 确定性派生,并按 CLI 的算法哈希 —— 同一个 key 永远报告同一台设备:重启、内存回收、多实例都一致(用 CC_FINGERPRINT_SALT 成批换身份) |
| 生命周期声明 | Key 初始化时与指纹并行发送 POST /alpha/lifecycle-events(cli_session_exists,metadata {sessionId, cliVersion, mode, os}),与生成请求共用 User-Agent: cli |
| 按 Key 分 Session | 每个 API Key 独立 session,12h 过期 + 1h 随机抖动 |
| 协议版本号 | x-command-code-version 报实际实现的协议版本(当前 1.53.1);npm 上有新版本只打漂移告警,不会静默改版本号 |
| CLI 信封格式 | 9 键:config / memory / taste / skills / permissionMode / threadId / mode / promptCache / params |
| OpenTelemetry | traceparent (W3C Trace Context) |
| 环境标识 | x-cli-environment: production、x-taste-learning: "false"、User-Agent: cli |
| Project Slug | x-project-slug = slugify(process.cwd()),与 config.workingDir 同源 |
| 思考强度 | reasoning_effort 透传 (low/medium/high/max) |
| API Key 格式验证 | 对 Authorization: Bearer 或 x-api-key 用正则 user_[a-zA-Z0-9_-]+ 提取,自动清理多余路径/前缀,sk-xxx 等非 user_ 格式拒 |
| 流式超时保护 | 流式 30s、非流式 90s → 429 + SDK 自动重试 |
| 连续超时阈值 | 连续 3 次超时后才提示压缩上下文 |
| 零输出防护 | outputTokens=0 → 429 rate_limit_error(SDK 自动重试,反异常计费) |
| 上游中止 | 客户端断连 + 全部错误路径 AbortController 打断 CC |
| 隐私保护日志 | 日志不含 API Key 片段、错误 body、stack trace |
{
"config": {
"workingDir": "C:\\project",
"date": "2026-06-07",
"environment": "win32-x64, Node.js v24.16.0",
"structure": [],
"isGitRepo": false,
"currentBranch": "",
"mainBranch": "",
"gitStatus": "",
"recentCommits": []
},
"memory": null,
"taste": null,
"skills": "",
"permissionMode": "standard",
"params": {
"model": "deepseek/deepseek-v4-flash",
"messages": [...],
"max_tokens": 64000,
"stream": true,
"reasoning_effort": "max"
}
}条件字段:system(从 system 消息提取)、temperature、reasoning_effort、tools(映射为 CC input_schema 格式)。
CLI 发送图片的格式:
{
"role": "user",
"content": [
{ "type": "image", "image": "data:image/jpeg;base64,..." },
{ "type": "text", "text": "图里写了什么" }
]
}代理收到 OpenAI image_url 格式后自动转为上述 CC 格式透传。
每次打 v* tag 时 GitHub Actions 会自动构建并推送多架构镜像(linux/amd64 + linux/arm64)到 GitHub Container Registry:
docker pull ghcr.io/maxeaglet/commandcode-proxy:latest
docker run -d --name cc-proxy -p 3050:3050 -e PORT=3050 ghcr.io/maxeaglet/commandcode-proxy:latest每次发版都会更新 latest 标签。镜像为公共可见,拉取无需登录。
docker compose up -d代理将在 http://0.0.0.0:3050 监听。通过 PROXY_PORT 自定义主机端口:
PROXY_PORT=13050 docker compose up -ddocker build -t commandcode-proxy:latest .
docker run -d -p 3050:3050 -e PORT=3050 commandcode-proxy:latestnpm run docker:build:multi| 变量 | 默认值 | 说明 |
|---|---|---|
PORT |
3050 |
容器内监听端口 |
PROXY_PORT |
3050 |
主机映射端口(仅 compose) |
CC_MAX_BODY_MB |
100 |
请求体大小上限(MB),超限请求返回 HTTP 413 |
CC_UPSTREAM_PROXY |
空 | 仅作用于 CC 上游请求的 http://host:port CONNECT 代理 |
CC_FINGERPRINT_SALT |
空 | 指纹派生用的盐;成批更换所有 key 的设备身份 |
CC_DEVICE_PROJECT_DIR |
空 | 伪造的项目目录;留空用内置 C:\Users\dev\projects\app |
CC_CLI_MODE |
agent |
信封 mode:agent|learning|custom-agent|… |
CC_CLI_SESSION_MODE |
interactive |
lifecycle 的 mode:interactive|non-interactive |
CC_CLIENT_DRAIN_TIMEOUT_MS |
空(禁用) | 下游背压阻塞超过该毫秒数则断开该客户端并中止上游请求,见僵死连接 |
CC_STREAM_IDLE_MS |
30000 |
流式上游读空闲超时(毫秒),见上游空闲超时 |
CC_NONSTREAM_IDLE_MS |
90000 |
非流式上游读空闲超时(毫秒) |
CC_MAX_INFLIGHT |
0(不限) |
进程内在途请求上限,超限返回 503 + Retry-After,见在途上限 |
默认关闭(CC_MAX_INFLIGHT 未设置 = 不限制并发),既有行为不变。
本项目定位是纯反代层,并发控制属于下游 —— 按 IP / 按 key 的限流请用反向代理(见内存与部署里的 limit_conn)。
本项不是那套方案的替代品,只为「不挂反代裸跑」(Dockerfile 与 npm start 都支持这种用法)提供一个进程内、仅全局的兜底:
CC_MAX_INFLIGHT=32 npm start # 最多同时处理 32 个请求超限时快速返回 503 + Retry-After: 5 + type: server_busy —— OpenAI / Anthropic 官方 SDK 认得这个组合会自动退避重试,而不是拿到连接被重置。/health 与 / 不计入、也不受限制,避免探活与编排器因业务繁忙收到 503。
为什么需要它:内存 = 在途数 × (0.13MB + 5.5 × body_MB)。CC_MAX_BODY_MB 只管住单请求量级,乘数无人管 —— 默认 100MB 时 N 个并发最坏可达 N × 550MB。
为什么 body 默认值保持 100MB:#7 记录了一个合法的多模态长会话(21 张 base64 图片,约 10.11 MiB)会撞上旧的 10MB 上限 —— 阈值降不下去,正因为此才必须去约束并发侧。启动时那条「最坏内存」warn 只是提示,
CC_MAX_INFLIGHT才是执行层。
⚠️ 开启本项不等于内存安全:32 × 550MB 仍远超小机器容量。要拿到硬性上界,需同时下调CC_MAX_BODY_MB。
两个上游读空闲看门狗,超时后返回 429(带 retry_after)让 SDK 自动重试:
| 环境变量 | 默认 | 作用于 |
|---|---|---|
CC_STREAM_IDLE_MS |
30000 |
流式请求 |
CC_NONSTREAM_IDLE_MS |
90000 |
非流式请求 |
语义:只计「reader.read() 的等待时间」,每收到一个 chunk 就重置 —— 不是整个请求的总时长。
只要上游在持续吐流就不会触发,哪怕单个请求已经跑了几十分钟。
默认值与官方 CLI 不一致,这是已知取舍(#19):
官方 CLI 对上游没有任何 idle timeout —— 反编译 command-code@1.50.0 可见所有 createApiClient({ baseUrl }) 调用点都未传 timeout,实测 700+ 秒的停顿可正常完成。
本代理保留 30s 是为了兜住真正死掉的连接;代价是推理模型的长思考停顿可能被误杀。
若遇到「429 Response timeout」「zero output tokens」且日志里 elapsedMs ≈ 30000、bytesReceived = 0,
说明是看门狗误杀了 prefill / 首 token 阶段的正常停顿 —— 调大即可:
CC_STREAM_IDLE_MS=300000 npm start # 5 分钟
⚠️ 误杀的成本不止一次失败:被 abort 后返回429 + retry_after,SDK 会自动重试, 而重试等于完整重发整个上下文,长会话下每次误杀都要重付一次全量 prefill。
数据来自 issue #20 的实测复现(Node v24,loopback mock 上游)。
单请求内存开销的经验公式:
RSS ≈ 70 MB + 在途请求数 × (0.13 MB + 5.5 × body_MB)
res.write() 返回 false(socket 写缓冲超过 highWaterMark)时会暂停读取上游,响应不再在内存中无界堆积:
| 场景(200MB 上游流,客户端发完请求即停止读取) | 峰值 RSS 增量 |
|---|---|
| 修复前 | +586 MB(66 → 652 MB) |
| 修复后 | +4 MB(背压一路传回上游,上游只吐出 ~8MB 即停住) |
这不只是恶意客户端问题 —— 弱网/移动端、客户端卡在工具执行、客户端已放弃但 TCP 还没发 RST,都会触发。
body 在转发到上游前同时存在多份副本:chunks[] / Buffer.concat / utf8 字符串 / JSON.parse 对象树 / buildCcRequest 重建对象树 / JSON.stringify 序列化体。
| body | 上限 | 峰值增量 | 结果 |
|---|---|---|---|
| 7 MB | 100 MB | +52 MB(7.4×) | 200 |
| 20 MB | 100 MB | +116 MB(5.8×) | 200 |
| 20 MB | 8 MB | +21 MB(1.05×) | 413 |
启动时若隐含最坏峰值 ≥ 500MB,日志会输出 warn 提示。body 上限是按请求的 —— 乘数用 CC_MAX_INFLIGHT(进程内、仅全局)封顶,按 IP / 按 key 的限流在反向代理层补上。公网部署建议两者都做。
client_max_body_size 在 nginx 拒绝时,body 根本不会进入 Node 进程:
map $http_authorization $cc_key { default $http_authorization; "" $http_x_api_key; }
map "" $cc_global_key { default "global"; }
limit_conn_zone $binary_remote_addr zone=cc_ip:10m;
limit_conn_zone $cc_key zone=cc_key:10m;
limit_conn_zone $cc_global_key zone=cc_global:10m;
location /v1/ {
client_max_body_size 4m; # 需 <= CC_MAX_BODY_MB
limit_conn cc_ip 8;
limit_conn cc_key 4;
limit_conn cc_global 32; # 这一项就是内存天花板
limit_conn_status 429;
proxy_pass http://127.0.0.1:3050;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 300s; # 需大于 30s 的流空闲超时
}背压生效后,客户端既不读也不断开时该请求会连带上游连接一直挂着。实测残留在途成本:
| 僵死连接数 | RSS 增量 | 上游连接持有 |
|---|---|---|
| 1 | +5 MB | 1 |
| 10 | +45 MB | 10 |
| 50 | +248 MB | 50(永久持有) |
特性是有界、不泄漏、客户端断开即回收(RSS 曲线完全持平),但连接数本身无上限。
默认不处理,因为僵死客户端与「卡在工具执行的合法客户端」在协议层无法区分;且官方 CLI 对上游没有任何 idle timeout(见 #19),贸然加超时会重蹈「误杀健康请求」。
需要封顶时启用(opt-in):
# 下游持续阻塞超过 60s 才断开,正常客户端只要在推进 drain 就不会触发
CC_CLIENT_DRAIN_TIMEOUT_MS=60000 npm start启用后实测(50 个僵死连接):上游连接持有数由 50(永久)→ 0,且丢弃后不会继续抽干上游。
更稳妥的封顶仍在反向代理层(limit_conn),因为只有它知道该部署能承受多少并发。
logFile是同步写(appendFileSync),公网负载下会阻塞事件循环 —— 建议保持留空,从 stdout 收集。- systemd 兜底:配
MemoryMax=与NODE_OPTIONS=--max-old-space-size=,让超限杀掉 proxy 而不是sshd/nginx。 - 多账号 + 多实例:
sessionStore仍是进程内Map,同一个 API key 打到两个实例会得到两个不同 session。设备指纹已不再是问题 —— 它现在是派生出来的,跨实例、跨重启都是同一台设备(见设备指纹)。横向扩展仍建议按 API key 做一致性哈希(hash $cc_key consistent)以保持 session 亲和,不要轮询。
本项目仅供学习和研究使用。
- 非官方:本项目与 Command Code 无任何关联,非官方产品。
- 个人使用:使用者应自行承担所有责任。请遵守 Command Code 服务条款。
- API Key:本项目不会收集、上传或泄露你的 API Key。Key 通过每次请求的
Authorization: Bearer <key>或x-api-key头传入,日志中不记录;config.json中的可选apiKey字段仅作本地兜底,不会离开你的机器。 - 合规性:协议基于对本地 CLI 网络流量的被动观察,未对服务端进行任何未授权访问、破解或篡改。
- 账号风险:建议和正常 CLI 使用频率保持一致,超高并发调用可能触发风控。
# 带 watch 模式启动(文件修改自动重启)
npm run dev