# wb-proxy **Repository Path**: 164587694/wb-proxy ## Basic Information - **Project Name**: wb-proxy - **Description**: workbuddy反代api - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2026-09-21 - **Last Updated**: 2026-09-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # workbuddy-bridge 把 **WorkBuddy 账号**反代成两套兼容 API,并给出本地访问 token: - **Anthropic Messages** 协议 —— `POST /workbuddy/v1/messages`(Claude Code 等) - **OpenAI Responses** 协议 —— `POST /codex/v1/responses`(Codex CLI 等) 特点: - **Node.js 实现**,仅用内置模块(`node:http` / 内置 `fetch`),**零第三方依赖** - **唯一上游**:全部走 WorkBuddy - **只提供接口和 token**:不写入、不修改任何客户端配置(不碰 cc-switch) - **模型名不写死**:各档位对应的模型在启动时从 WorkBuddy 实时推断(见下) - 完整支持 **SSE 流式**、**工具调用**(Anthropic `tool_use` / Responses `function_call`)、`system` 数组、`cache_control` - 模型映射:Anthropic 侧把 `sonnet / opus / haiku / fable` 档位自动映射到 WorkBuddy 模型; Responses 侧**直接写上游真实模型名**,不做映射 --- ## 一、工作原理 ``` Claude Code 等 Codex CLI 等 │ Anthropic Messages 协议 │ OpenAI Responses 协议 │ + 本地 token (x-api-key) │ + 本地 token (Bearer) ▼ ▼ └──────────────► workbuddy-bridge (127.0.0.1:8788) ◄──────────────┘ │ ┌─────────────────────┴─────────────────────┐ │ │ /workbuddy/v1/messages /codex/v1/responses │ │ ├─ Anthropic → OpenAI Chat ├─ Responses → OpenAI Chat └─────────────────────┬─────────────────────┘ ▼ https://copilot.tencent.com (Bearer WorkBuddy OAuth) │ │ 1) 校验本地 token │ 2) 协议转换 → OpenAI Chat Completions │ 3) 模型名解析(档位映射 / 原样透传) ▼ 上游 delta → Anthropic SSE 事件流 / Responses SSE 事件流 ``` **路由表:** | 路径前缀 | 对外协议 | 上游 | 典型客户端 | |---|---|---|---| | `/workbuddy` | Anthropic Messages | WorkBuddy | Claude Code | | `/codex` | OpenAI Responses | WorkBuddy | Codex CLI | | `/openai` | OpenAI Responses(同 `/codex`,别名) | WorkBuddy | 任意 Responses 客户端 | | *(无前缀)* | 按 `config.upstream.kind`(默认 Anthropic) | WorkBuddy | — | 三条路由共用**同一份 WorkBuddy 凭证与额度**,区别只在「对外说哪套协议」和「路径怎么写」。 --- ## 二、一步一步跑起来 从零开始,按顺序做这 5 步即可。**首次全程约 3 分钟**,之后每次只要第 4 步。 ### 第 1 步:装 Node.js 要求 **Node.js ≥ 22.5**(本项目用内置 `fetch`,低版本跑不起来)。 ```bash node -v ``` 输出 `v22.5.0` 或更高即可。低于此版本请去 https://nodejs.org 装 LTS 版。 ### 第 2 步:拿到代码 ```bash git clone https://gitee.com/164587694/wb-proxy.git cd wb-proxy ``` > **不需要 `npm install`** —— 本项目**零第三方依赖**,只用 Node 内置模块。 > 仓库里没有 `node_modules` 是正常的,不是漏传了。 ### 第 3 步:登录账号 ```bash node bin/bridge.js --login-workbuddy ``` 首先会让你**选择用哪个账号**: ``` 选择要使用的账号 [1] CodeBuddy (cn) 已登录:your-name(codebuddy.cn) [2] WorkBuddy (global) 未登录(workbuddy.ai) cn 是 CodeBuddy 账号(codebuddy.cn),global 是 WorkBuddy 账号(workbuddy.ai)。 两者不通用,同一时间只能用一个;想换另一个请重启并重新选择。 提示:加 --realm cn|global 可跳过本提问。 请输入编号 [1-2](回车 = 1): ``` 选完之后: 1. 终端打印一条授权链接,并**自动打开浏览器** 2. 在浏览器里完成登录(首次需扫码 / 输账号) 3. 登录成功后终端自动轮询拿到 token,凭证存到 `auths/workbuddy-.json` 4. 看到 `登录成功` 就可以关掉浏览器了 想跳过提问直接指定,加 `--realm`: ```bash node bin/bridge.js --login-workbuddy --realm global # WorkBuddy 国际版 node bin/bridge.js --login-workbuddy --realm cn # CodeBuddy ``` > **两个 realm 的账号是分开的,同一时间只用其中一个。** > 详见下面「CodeBuddy 与 WorkBuddy 的区别」。 > > 这一步**也可以跳过** —— 直接做第 4 步,服务启动时发现该 realm 没凭证会自动引导登录。 > 单独跑这一步的好处是:登录失败时不会影响后面启动,错误信息也更清楚。 #### CodeBuddy 与 WorkBuddy 的区别 | | CodeBuddy (`cn`) | WorkBuddy (`global`) | |---|---|---| | 站点 | codebuddy.cn | workbuddy.ai | | 接口端点 | copilot.tencent.com | www.workbuddy.ai | | 凭证文件 | `auths/workbuddy-.json`(realm 字段为 `cn`) | 同左(realm 为 `global`) | | 模型目录 | 约 30 个 | 约 18 个(含 gpt-5.x / gemini 等) | **两者凭证互不通用**,且**上游校验严格程度不同**:WorkBuddy (global) 要求请求的 `messages[0]` 必须是 system 角色,CodeBuddy (cn) 不要求。所以: - 账号**不放在同一个池里轮询** —— 早期版本混着用会表现为「每隔一个请求必失败 `code=11-128`」,现在按 realm 隔离,启动时选定哪个就只发哪个。 - 想换账号:重启并加 `--realm`,或在交互提示里重新选。 - 两个 realm 都登录过也没关系,启动横幅会提示另一个已登录但未启用。 ### 第 4 步:启动服务 ```bash npm start ``` 或者等价的 `node bin/bridge.js`。 **看到这段就成功了**(模型名因人而异): ``` workbuddy-bridge v4.0.0 服务已启动 监听 : http://127.0.0.1:8788 上游 : WorkBuddy(your-name(realm=cn,剩余 79051 分钟)) Auth Token : sk-local-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 客户端 Base URL : Anthropic 客户端 : http://127.0.0.1:8788/workbuddy Responses 客户端 : http://127.0.0.1:8788/codex ``` **这一屏信息先记下来,第 5 步要用**:`Auth Token` 和两个 `Base URL`。 > 服务要**一直开着**(前台运行)。关掉终端 = 服务停止,客户端就连不上了。 > 停止:`Ctrl+C`。 ### 第 5 步:让客户端用上它 本服务**只提供接口,不修改任何客户端配置**,需要你手动填。按你用的客户端二选一: **A. Claude Code / cc-switch(Anthropic 协议)** | 配置项 | 填什么 | |---|---| | Base URL | `http://127.0.0.1:8788/workbuddy` | | API Key / Auth Token | 第 4 步启动横幅里的 `Auth Token` 值 | > ⚠️ **Base URL 不要带 `/v1/messages`**。客户端会自己拼,写成完整地址会变成 > `/workbuddy/v1/messages/v1/messages`,直接 `404`。 **B. Codex CLI(OpenAI Responses 协议)** 编辑 `~/.codex/config.toml`: ```toml model = "deepseek-v4-flash" # 必须是上游真实模型名,不能写 sonnet model_provider = "wb-bridge" [model_providers.wb-bridge] name = "workbuddy-bridge" base_url = "http://127.0.0.1:8788/codex" wire_api = "responses" experimental_bearer_token = "sk-local-xxxxxxxx" # 换成第 4 步的 Auth Token ``` > Responses 侧的 `model` **不能填 `sonnet` 这种档位名**,要填真实模型名 > (`deepseek-v4-flash` 等)。不知道有哪些?浏览器打开 > `http://127.0.0.1:8788/v1/models` 看一遍。 ### 验证跑通了没有 先用 curl 直接打服务,排除客户端自身的问题: ```bash TOK=$(tr -d '\r\n' < token.txt) curl -s http://127.0.0.1:8788/workbuddy/v1/messages \ -H "x-api-key: $TOK" -H "content-type: application/json" \ -d '{"model":"sonnet","max_tokens":64,"messages":[{"role":"user","content":"Reply with exactly: pong"}]}' ``` 返回里带 `"pong"` 就说明服务本身没问题,接着再去查客户端配置。 ### 常见卡住的地方 | 现象 | 原因 | |---|---| | `node: command not found` | 第 1 步没做,或 Node 版本 < 22.5 | | 启动后档位全是 `auto` | 凭证过期或上游不可达,重跑第 3 步 | | 客户端 `401` | API Key 填错了,用启动横幅里的 `Auth Token`(不是 WorkBuddy 账号密码) | | 客户端 `404 not_found_error` | Base URL 多写了 `/v1/messages`,只填到 `/workbuddy` 或 `/codex` | | `400 code=11-128` | 客户端指纹被上游拦截。默认已自动脱敏,仍报错见[第七节](#七上游内容指纹拦截400-code11-128) | | 端口被占用 | `node bin/bridge.js --port 8899`,客户端地址同步改 | --- ## 三、启动方式与命令行参数 ### 启动服务 ```bash # 方式一:npm(推荐,自动用 package.json 里的脚本) npm start # 方式二:直接 node node bin/bridge.js # 方式三:靠 shebang 直接执行(macOS / Linux / Git Bash) ./bin/bridge.js ``` > 项目**不含任何 `.cmd` / `.bat` 脚本** —— 早先的 `.cmd` 启动器在中文 Windows 上 > 会因 UTF-8/GBK 编码不匹配而乱码报错,已全部移除。直接用上面的命令即可。 > **未登录时会自动引导登录**:启动时若发现没有凭证,会先打印提示并进入 OAuth 授权流程 > (打开浏览器 / 给出授权链接),登录成功后自动继续启动服务,不需要分两步跑。 > 加 `--no-browser` 可只打印链接不自动打开浏览器;`--realm cn|global` 可跳过账号选择提问。 启动后会打印接入参数: ``` 正在读取 WorkBuddy 模型目录… ====================================================================== workbuddy-bridge v4.0.0 服务已启动 ====================================================================== 监听 : http://127.0.0.1:8788 上游 : WorkBuddy(your-name(realm=cn,剩余 79051 分钟)) Auth Token : sk-local-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 默认协议 : WorkBuddy(Anthropic Messages 协议) 模型总数 : 30 sonnet : deepseek-v4-flash opus : deepseek-v4-pro haiku : deepseek-v4.1-flash fable : hy4-preview 兜底模型 : auto API 地址(完整) : Anthropic Messages : http://127.0.0.1:8788/workbuddy/v1/messages OpenAI Responses : http://127.0.0.1:8788/codex/v1/responses (OpenAI Responses 亦可用别名:http://127.0.0.1:8788/openai/v1/responses) 客户端 Base URL : Anthropic 客户端 : http://127.0.0.1:8788/workbuddy (cc-switch / Claude Code 填到这里) Responses 客户端 : http://127.0.0.1:8788/codex ↑ /v1/messages、/v1/responses 由客户端自己拼接,不要重复写,否则会 404 辅助端点 : /v1/models · /health 停止 : Ctrl+C ====================================================================== ``` > **填 Base URL 时注意**:客户端(cc-switch / Claude Code / Codex CLI)会自己在 Base URL > 后面拼 `/v1/messages` 或 `/v1/responses`,所以 Base URL 只填到路由前缀 > (`.../workbuddy` 或 `.../codex`)。写成 `.../workbuddy/v1/messages` 会变成 > `/workbuddy/v1/messages/v1/messages`,直接 `404 not_found_error`。 > 上面「API 地址(完整)」那一组是给 curl / 调试用的完整 URL。 ### npm scripts | 命令 | 等价于 | 说明 | |---|---|---| | `npm start` | `node bin/bridge.js` | 启动服务 | | `npm run check` | `node bin/bridge.js --check` | 自检 | | `npm run login` | `node bin/bridge.js --login-workbuddy` | WorkBuddy 登录 | ### 全部参数 | 命令 | 说明 | |---|---| | `node bin/bridge.js` | 启动服务 | | `node bin/bridge.js --check` | 自检:凭证、上游连通性、接入参数 | | `node bin/bridge.js --login-workbuddy` | OAuth 登录(不带 `--realm` 时交互选择账号) | | `node bin/bridge.js --logout` | 退出登录:删除本地凭证文件(交互确认) | | `node bin/bridge.js --logout --all --yes` | 退出全部账号,跳过确认(脚本用) | | `node bin/bridge.js --port 8899` | 覆盖监听端口 | | `node bin/bridge.js --host 0.0.0.0` | 覆盖监听地址(**不建议**,见安全章节) | | `node bin/bridge.js --token xxx` | 覆盖本地访问 token | | `node bin/bridge.js --no-auth` | 关闭本地鉴权(**不建议**) | | `node bin/bridge.js --thinking` | 把上游 `reasoning_content` 输出为 Anthropic `thinking` block | | `node bin/bridge.js --realm cn\|global` | 指定用哪套账号(cn = CodeBuddy,global = WorkBuddy)。**跳过交互提问**,对启动、登录、退出都生效 | | `node bin/bridge.js --yes` | `--logout` 时跳过二次确认(非交互环境**必须**加,否则拒绝执行) | | `node bin/bridge.js --all` | `--logout` 时退出全部 realm 的账号 | | `node bin/bridge.js --config ` | 指定配置文件 | | `node bin/bridge.js --version` | 打印版本 | > `bin/bridge.js` 带 shebang 且已置可执行位,macOS / Linux / Git Bash 下可直接 `./bin/bridge.js`。 --- ## 四、端点 | 方法 | 路径 | 说明 | |---|---|---| | GET | `/health` | 健康检查(免鉴权) | | GET | `/` | 运行状态:路由表、凭证摘要、模型映射 | | GET | `/v1/models` | 模型列表 | | GET | `/workbuddy/v1/models` | 同上(指定路由) | | POST | `/v1/messages` | **Anthropic 主接口**(流式 / 非流式) | | POST | `/workbuddy/v1/messages` | 同上(显式指定路由) | | POST | `/v1/messages/count_tokens` | 粗略 token 估算 | | POST | `/v1/responses` | **Responses 主接口**(流式 / 非流式) | | POST | `/codex/v1/responses` | 同上 | | POST | `/codex/responses` | 同上(Codex CLI 的 `base_url` 到 `/codex` 时会拼 `/responses`) | | POST | `/openai/v1/responses` | 同上(别名) | 也可用 `?channel=codex` 查询参数切换路由。 鉴权:`x-api-key: ` 或 `Authorization: Bearer `。 错误按 Anthropic 格式返回 `{"type":"error","error":{...}}`, Responses 端点则返回 `{"error":{...}}`。 ### 直连验证(curl) ```bash TOK=$(cat token.txt) # Anthropic 协议,非流式 curl -s http://127.0.0.1:8788/workbuddy/v1/messages \ -H "x-api-key: $TOK" -H "content-type: application/json" \ -d '{"model":"sonnet","max_tokens":64,"messages":[{"role":"user","content":"Reply with exactly: pong"}]}' # Anthropic 协议,流式 curl -N http://127.0.0.1:8788/workbuddy/v1/messages \ -H "x-api-key: $TOK" -H "content-type: application/json" \ -d '{"model":"sonnet","max_tokens":64,"stream":true,"messages":[{"role":"user","content":"hi"}]}' # Responses 协议,非流式 curl -s http://127.0.0.1:8788/codex/v1/responses \ -H "Authorization: Bearer $TOK" -H "content-type: application/json" \ -d '{"model":"deepseek-v4-flash","input":"Reply with exactly: pong"}' # Responses 协议,流式 curl -N http://127.0.0.1:8788/codex/responses \ -H "Authorization: Bearer $TOK" -H "content-type: application/json" \ -d '{"model":"deepseek-v4-flash","stream":true,"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}]}' ``` --- ## 五、模型档位:运行时从上游推断 Anthropic 协议的客户端(Claude Code 等)只会说 `sonnet` / `opus` / `haiku` / `fable` 这几个**档位名**,而 WorkBuddy 上游认的是它自己的模型 ID。所以需要一张「档位 → 上游模型」的映射表。 **这张表不写死在代码里。** 每次启动时会: 1. 调 `GET /v2/enterprises/personal/models` 拉取 WorkBuddy 的真实模型目录 2. 按关键字规则推断各档位该用哪个模型 3. 把结果挂在内存里(**不落盘、不改配置文件**) 这样上游加减模型、换命名,服务都能自动跟上,不需要改配置。 ### 推断规则 `src/paths.js` 里只放**关键字**,不放具体模型名: ```js export const PHASE_RULES = { strong: ["pro", "ultra", "opus", "large", "premium"], // → opus light: ["flash", "turbo", "lite", "mini", "small", "air"], // → haiku thinky: ["thinking", "reason", "preview"], // → fable }; ``` 各档位从对应候选池里选优,优先级: | 优先级 | 依据 | 说明 | |---|---|---| | 1 | 上游标记的默认模型 | `isDefault` | | 2 | **上下文长度** | `maxInputTokens`,跨品牌可比 | | 3 | **同品牌内版本更新** | 版本号只在同品牌内有意义 | | 4 | ID 字典序 | 仅保证结果稳定可复现 | > `sonnet`(均衡主力)的候选池是「所有非思考型模型」,所以 flash 类也能入选 —— > 很多厂商的 flash 就是均衡主力,不必然只配当 `haiku`。 **关键字按词边界匹配**(不是 `includes`)。早期用子串匹配时,`minimax` 里的 `max` 被误判成「强能力」信号,把 `minimax-m3` 选成了 opus 档 —— 这类误判已修复。 ### 推断不出来怎么办 - 某个档位推断不出 → 该档位**退回兜底模型**(通常是上游的 `auto`),并打印警告,**不阻断启动** - 整个模型目录拉取失败 → 全部档位退回 `auto`,同样只警告不停机 - 档位名(如 `sonnet`)**绝不会**被原样透传给上游 —— 上游不认这种名字 ### 手工覆盖 `config.json` 的 `models.map` 默认为空(即完全走推断)。想固定某个档位就填进去, **填了的档位优先于推断结果,没填的仍走推断**: ```json "models": { "map": { "opus": "kimi-k3-1" }, "default": "", "responsesDefault": "" } ``` > 想看当前实际推断出什么,跑 `node bin/bridge.js --check`, > 或访问运行中的服务 `GET /`(返回 `modelMap` 与 `modelMapSource`)。 --- ## 六、Responses 协议细节 Responses API 与 Chat Completions 差异较大,以下是转发层做的事: | 方向 | 处理 | |---|---| | 请求 | `input`(string 或数组)→ `messages`:`input_text`→text part、`input_image`→image_url、`function_call`→`assistant.tool_calls`、`function_call_output`→`role:"tool"` | | 请求 | `instructions` → `system` message | | 工具 | Responses 的**扁平工具** `{type:"function",name,parameters}` → Chat 的**嵌套工具** `{type:"function",function:{name,parameters}}` | | 响应(非流式) | `choices[0].message` → `{output:[{type:"message",content:[{type:"output_text",text}]}]}`,工具调用转为 `{type:"function_call",call_id,name,arguments}` | | 响应(流式) | Chat delta → 完整 Responses 事件链 | Responses 流式事件序列(实测顺序): ``` response.created response.in_progress response.output_item.added response.content_part.added response.output_text.delta × N response.output_text.done response.content_part.done response.output_item.done response.completed ``` 工具调用时,把 `output_text.*` 换成 `response.function_call_arguments.delta` → `response.function_call_arguments.done`。 ### 失败时怎么返回(重要) 上游对**非法模型名**之类的错误,并不总是用 HTTP 状态码表达 —— 实测它在 HTTP 200 的流里塞一帧业务错误: ``` event: error data: {"code":11102,"msg":"model [xxx] is only available for authorized users"} ``` 如果这帧被当成普通 delta 丢掉,客户端只会看到「请求开始了、然后什么也没有」, 完全不知道是模型名写错了。所以转发层做了两件事: 1. **延迟提交响应头**:首个有内容的 delta 到达前不写 `200`。 这样上游错误能用**正确的 HTTP 状态码**返回: | 场景 | 返回 | |---|---| | 模型名非法 / 上游拒绝(首块即错) | `400` + `{"error":{"message":"code=11102 …","code":"11102"}}` | | 上游已开始推送后才中断 | `200` + 流内 `event: error` + **`response.failed`**(终态带 `error` 字段,`status:"failed"`) | | 正常完成 | `200` + … + `response.completed` | 2. **识别流内错误帧**:`WorkBuddyUpstream.detectStreamError()` 会把这类帧拦下来转成错误, 无论流式还是非流式聚合路径都不会被静默吞掉。 > 对流式客户端来说,**务必同时处理 `response.failed` 与 `event: error`**, > 不要只等 `response.completed`。 ### 模型名解析 **Anthropic 侧(`/workbuddy`、无前缀)** —— 支持档位映射: 1. **精确键匹配**:`models.map.sonnet` 等 2. **关键字匹配**:模型名里**包含** `fable` / `haiku` / `opus` / `sonnet` 3. **含 `/` 的名字**视为上游模型名,直接透传 4. 其它情况直接透传 **Responses 侧(`/codex`、`/openai`)** —— 不做映射: 1. 请求里**带了 `model`** → **原样透传**(上游模型名已经是真实名) 2. 请求里**没带 `model`** → 用 `models.responsesDefault` 兜底 所以 Responses 侧的 `model` 一定要填**上游真实模型名**(`deepseek-v4-flash`、`glm-5.3-flash`……), 而不是 `sonnet` 这种档位名。 > 填错了不会「假装成功」:上游会回 `code=11102`,本服务按上面的规则返回 > `400` + 明确错误信息,并把上游错误码放在 `error.code` 里。 > 想确认某个名字是否合法,先 `GET /v1/models` 看一遍。 --- ## 七、上游内容指纹拦截(400 code=11128) 这是接 Claude Code / Claude Desktop / Codex CLI 时**最容易撞上**的一个坑: ``` 400 code=11128 Illegal API invocation from an unapproved channel ``` ### 它不是封号,也不是限流 上游(WorkBuddy / CodeBuddy)对**请求体**做**逐字精确匹配**的内容审核。只要请求里出现 某些「竞品客户端指纹」字符串,整单直接打回 —— **一字之差即可绕过**。 所以:账号没问题、额度没问题、模型也没问题。同样的账号,只改一个词就能过。 ### 指纹从哪来 这些客户端会在 **system prompt** 里注入固定模板句,转发时就原样带上去了: | 来源 | 指纹串 | |---|---| | Claude Code / Claude Desktop 身份句 | `You are Claude Code, Anthropic's official CLI for Claude` | | Claude Code 注入指令 | `Main branch (you will usually use this for PRs)` | | Claude Code 反馈句 | `To give feedback, users should report the issue at https://github.com/anthropics/claude-code/issues` | | Codex CLI 身份句 | `You are a coding agent running in the Codex CLI, a terminal-based coding assistant.` | | 计费 header | `x-anthropic-billing-header`(键值形态与裸键名形态都算) | | 尾随裸键值 | `cc_entrypoint=` 等 `cc_xxx=...;` | | **上游反探测** | 请求体里出现裸数字 `11128` 就整单拦截(与上下文无关) | > 身份句的匹配串**不含结尾标点**:CLI 版以句号收尾,桌面版(claude-desktop / > Agent SDK)是逗号接 `running within the Claude Agent SDK.`。两种形态都要覆盖。 ### 本服务怎么处理 `src/sanitize.js` 在**出站方向**做脱敏(只改发给上游的副本,客户端收到的不受影响): - **改写层** —— 承载语义的模板句只换一个词,语义不变: - `Anthropic's official CLI for Claude` → `Anthropic's official CLI tool for Claude` - `Main branch (…)` → `Default branch (…)` - `Codex CLI, a terminal-based` → `Codex CLI tool, a terminal-based` - `To give feedback, users should` → `To provide feedback, users should` - `11128` → `11-128`(保留可读性与指代;零宽空格无效,上游会归一化) - **剥离层** —— header / 键值型指纹整段删除(本来也没有信息价值) - **兜底层** —— 残留的裸键名缩写为 `x-anthropic-billing-hdr`,破坏逐字匹配 覆盖的位置:`messages[].content`(含多模态 part 与 `tool_result` 嵌套)、 `messages[].tool_calls[].function.arguments`、`messages[].reasoning_content`、 以及工具定义的 `description`。工具名与 JSON Schema **不动** —— 那是结构化契约。 开关是 `config.json` 的 `upstream.sanitizeFingerprints`(默认 `true`)。 每次剥离都会打日志: ``` [15:54:54] [sanitize] 已剥离客户端指纹:Claude Code 身份句、注入指令句 Main branch ``` ### 如果还是报 11128 说明出现了**新的指纹**(上游的规则会变)。排查步骤: 1. 确认 `config.json` 里 `upstream.sanitizeFingerprints` 是 `true` 2. 看服务日志里 `[sanitize]` 那行有没有命中 3. 把你所用客户端的 system prompt 取出来,逐句二分:删掉哪一句后不再报错, 那一句就是新指纹 —— 把它加进 `src/sanitize.js` 的 `REWRITES` 或 `FINGERPRINT_PREFIXES` 即可 4. 想临时关闭脱敏做对照实验:设环境变量 `WORKBUDDY_NO_SANITIZE=1` 启动 (**仅用于诊断**,正常别关) > 实测对照:同一份 Claude Desktop system prompt,关掉脱敏 → `400 code=11128`; > 打开脱敏 → `200` 正常返回。 --- ## 八、配置说明(config.json) ```json { "server": { "host": "127.0.0.1", "port": 8788, "requireAuth": true, "localToken": "" }, "upstream": { "kind": "workbuddy", "timeoutSeconds": 300, "stripUnsupportedParams": true, "sanitizeFingerprints": true, "includeThinking": false }, "workbuddy": { "accessToken": "", "realm": "cn", "authDir": "", "deviceToken": "" }, "models": { "map": {}, "default": "", "responsesDefault": "" }, "logging": { "enabled": true } } ``` | 键 | 说明 | |---|---| | `server.host` / `server.port` | 监听地址与端口 | | `server.localToken` | 留空则自动生成并写入 `token.txt` | | `server.requireAuth` | 关闭后任何本机进程都能用(不建议) | | `upstream.kind` | 不带路由前缀时用哪套协议(`workbuddy` = Anthropic) | | `upstream.timeoutSeconds` | 上游请求超时 | | `upstream.stripUnsupportedParams` | `true` 时剥离 `max_tokens` / `tool_choice` / `stop` / `top_p` | | `upstream.sanitizeFingerprints` | **出站脱敏**,默认 `true`。剥离客户端 system prompt 里的模板句指纹,否则上游会返回 `400 code=11128`(见第六节) | | `upstream.includeThinking` | `true` 时把上游 `reasoning_content` 输出为 `thinking` block | | `workbuddy.accessToken` | 手工填的 WorkBuddy token(一般留空,走登录或 `auths/`) | | `workbuddy.realm` | `cn`(`copilot.tencent.com`)或 `global`(`workbuddy.ai`) | | `workbuddy.deviceToken` | 可选:官方桌面端设备令牌,仅风控 403 时需要 | | `workbuddy.authDir` | 凭证目录,默认项目下 `auths/` | | `models.map` | **可选**手工覆盖**:档位 → 上游模型。默认空 = 完全走运行时推断(见第五节) | | `models.default` | **可选**兜底模型,留空则用推断结果(通常是 `auto`) | | `models.responsesDefault` | **可选**Responses 侧未指定 model 时的默认模型,留空则用兜底模型 | > `models` 三项**默认全部为空** —— 这是正常状态,不是没配好。 > 模型名会在每次启动时从 WorkBuddy 实时读取并推断。 也可用环境变量:`WORKBUDDY_TOKEN` / `WORKBUDDY_UID` / `WORKBUDDY_REALM` / `WORKBUDDY_DEVICE_TOKEN`。 --- ## 九、实测记录 以下均在本机实际跑通: | 验证项 | 结果 | |---|---| | `--check` 自检:WorkBuddy 凭证与模型 | ✅ your-name(cn),30 个模型 | | `GET /health` | ✅ 200 | | 未带 token 访问受保护端点 | ✅ 401 `authentication_error` | | Anthropic 非流式 `/workbuddy/v1/messages` | ✅ `sonnet ⇒ deepseek-v4-flash`,`"pong"` | | Anthropic 流式 | ✅ `message_start → ping → content_block_start/delta/stop → message_delta → message_stop` | | Anthropic 工具调用 | ✅ 返回 `tool_use`,`input={"city":"Beijing"}`,`stop_reason="tool_use"` | | `POST /v1/messages/count_tokens` | ✅ 返回估算值 | | Responses 非流式 `/codex/v1/responses` | ✅ `status=completed`,`text=pong` | | Responses 流式 `/codex/responses` | ✅ 9 类事件齐全 | | Responses 工具调用 | ✅ `response.function_call_arguments.delta/done` 事件链完整 | | Responses 多轮 `function_call_output` 回填 | ✅ 模型正确读到工具结果 | | `/openai/v1/responses` 别名 | ✅ 与 `/codex` 行为一致 | | **不写入任何客户端配置** | ✅ 运行前后 cc-switch 数据库完全未改动 | | **模型档位运行时推断** | ✅ 启动即从 30 个模型中推断出 sonnet/opus/haiku/fable,见下 | | 各档位真实路由 | ✅ `sonnet ⇒ deepseek-v4-flash`、`opus ⇒ deepseek-v4-pro`、`haiku ⇒ deepseek-v4.1-flash`、`fable ⇒ hy4-preview` | | 档位不重复分配 | ✅ 4 个档位落到 4 个不同模型 | | 上游不可达降级 | ✅ 全部档位退回 `auto`,只警告不崩 | | 档位名不透传上游 | ✅ `sonnet` 无映射时返回 `auto`,不会把 `sonnet` 发给上游 | | `config.json` 手工覆盖 | ✅ 填了的档位优先,未填的仍走推断 | | 配置中无写死模型名 | ✅ `models.map` / `default` / `responsesDefault` 默认为空 | | 三种启动方式 | ✅ `npm start` / `node bin/bridge.js` / `./bin/bridge.js` 均可 | | **不含任何 `.cmd` 脚本** | ✅ 已移除(中文 Windows 上 UTF-8/GBK 编码不匹配会乱码报错) | | **非法模型名:Responses 流式** | ✅ `400` + `{"error":{"message":"code=11102 …","code":"11102"}}`,不再是「假成功」 | | **非法模型名:Responses 非流式** | ✅ `400` + 同上 | | **非法模型名:Anthropic 流式** | ✅ `400` + `{"type":"error","error":{"type":"api_error",…}}` | | **非法模型名:7 条路径全覆盖** | ✅ `/codex/v1/responses`、`/codex/responses`、`/openai/v1/responses`、`/v1/responses`、`/workbuddy/v1/messages`、`/v1/messages` 全部返回 `400` | | **中途失败语义** | ✅ 已提交后报错 → `event: error` + `response.failed`(`status:"failed"`,带 `error`) | | **真流式未被破坏** | ✅ 47 个 TCP 数据块、39 个 delta 分 320ms 到达,非「攒完再吐」 | | 整体回归 | ✅ 9/9 通过(健康检查 / 鉴权 / 两协议流式与非流式 / 工具调用 / 模型列表) | | **客户端指纹拦截(11128)** | ✅ 关掉脱敏 → 稳定复现 `400 code=11128`;打开脱敏 → `200` 正常 | | **脱敏单元用例** | ✅ 10/10(身份句两种形态 / Main branch / Codex 句 / 反馈句 / header 键值与裸键名 / 混合大小写 / 裸 11128 / 普通文本不动) | | **脱敏覆盖位置** | ✅ `content`(含多模态与 `tool_result` 嵌套)、`tool_calls.arguments`、`reasoning_content`、工具 `description` | | **脱敏不误伤** | ✅ 普通文本原样返回;`You are Claude Code` / `for Claude` 等语义与标点保留 | | **两种协议 + 流式均受益** | ✅ Anthropic 流式、Responses 流式/非流式在带指纹时均返回 `200` | --- ## 十、文件结构 ``` workbuddy-bridge/ ├── README.md 本文档 ├── package.json 项目定义(无第三方依赖,含 npm scripts) ├── .gitignore 忽略本地凭据与运行产物 ├── config.example.json 配置模板(首次运行自动复制为 config.json) ├── bin/ │ └── bridge.js CLI 入口(启动 / 自检 / 登录,含 shebang) └── src/ ├── paths.js 路径、常量、路由表(ROUTES)、档位推断规则 ├── model-catalog.js 从上游目录推断各档位模型(只存内存) ├── sanitize.js 出站脱敏:剥离客户端指纹(防 400 code=11128) ├── config.js 配置加载、模型解析、token 管理 ├── credentials.js WorkBuddy 账号加载 ├── convert.js Anthropic ↔ OpenAI 双向转换(含 SSE 重建) ├── responses.js OpenAI Responses ↔ Chat Completions 双向转换 ├── server.js HTTP 服务、鉴权、按路由分派协议 ├── workbuddy-login.js WorkBuddy OAuth 设备授权登录 └── upstream/ ├── http.js 基于内置 fetch 的 HTTP / SSE 工具 └── workbuddy.js WorkBuddy 上游客户端 ``` **以下文件在仓库里不存在,是运行后本地生成的**(已在 `.gitignore` 中忽略,不会上传): | 文件 | 何时生成 | |---|---| | `config.json` | 首次运行时自动从 `config.example.json` 复制 | | `token.txt` | 首次启动时自动生成随机本地 token | | `auths/workbuddy-*.json` | 第 3 步登录成功后写入的凭证 | > 所以 `git clone` 下来**看不到这三个文件是正常的**,跑一次就都有了。 --- ## 十一、凭证从哪来 **凭证不来自运行中的 WorkBuddy 客户端进程**,而是磁盘上的文件。`src/credentials.js` 按以下优先级收集(`loadAccounts()`): | 优先级 | 来源 | 说明 | |---|---|---| | 1 | `config.json` → `workbuddy.accessToken` | 手工填,一般留空 | | 2 | **`auths/workbuddy-*.json`** | ← 实际用的就是这个 | | 3 | 环境变量 `WORKBUDDY_TOKEN` | 可选 | 所以: - **不需要保持 WorkBuddy 客户端处于运行状态** —— 本服务只认 `auths/` 里的 token - 删掉 `auths/` 目录 → 账号数为 0,服务会以 401 / 降级模式运行 - **token 有效期**:CodeBuddy (cn) 约 55 天;WorkBuddy (global) 实测约 362 天。 只有到期、换账号、或换 realm 时才需要重跑 `npm run login` **凭证按 realm 隔离。** `loadAccounts()` 收集到的账号会由 `loadAccountsForRealm()` 按 `realm` 字段分组,本次启动只加载选中的那个 realm(见「CodeBuddy 与 WorkBuddy 的区别」)。 把两种账号混进同一个池轮询会导致间隔性失败,所以服务现在会拒绝这种状态。 #### 退出登录 ```bash node bin/bridge.js --logout ``` 会列出所有本地凭证并让你选择退出哪个(`0` = 全部),**确认后才删除**。想跳过提问: ```bash node bin/bridge.js --logout --realm global # 只退出 WorkBuddy node bin/bridge.js --logout --all --yes # 退出全部,不确认 ``` 几点说明: - **只删本地凭证文件**(`auths/workbuddy-.json`),不向服务端注销 —— token 服务端本来就会自行过期,本地删掉即等同于登出本服务。 - 删除是**不可逆**的,重新使用需要再走一次 `--login-workbuddy`。 - 非交互环境(管道 / CI)下**必须加 `--yes`**,否则拒绝执行并返回退出码 1。 这是刻意的:确认不了的事情不做。 - 来自 `config.json` 或环境变量的 token **不会被删除**(不是本服务写的文件), 只会提示你手工移除。 - 退出后启动服务,若该 realm 已无凭证会自动引导重新登录。 > **本服务不读取桌面端的凭证文件。** > `%LOCALAPPDATA%/CodeBuddyExtension/Data/Public/auth/` 下的 > `workbuddy-desktop*.info` 确实含明文 token,但它按 realm 区分站点、且会过期失效, > 曾经尝试复用后已回退(2026-09-20)。走官方 OAuth 拿一个自己的 token 更稳定。 > > `refreshToken` 虽然被保存下来了,但**当前没有实现自动刷新**。 > 到期后需要重新走一次 OAuth 登录。 --- ## 十二、故障排查 | 现象 | 处理 | |---|---| | 启动时提示"无法推断以下档位" | 上游目录里没有匹配规则关键字的模型。换个模型名匹配,或在 `config.json` 的 `models.map` 里手工指定 | | 某档位的模型不合预期 | 推断是启发式的。直接在 `config.json` 的 `models.map` 里覆盖该档位即可,重启生效 | | 模型列表拉不到,档位全变 `auto` | 上游不可达或凭证失效。跑 `npm run login` 重新登录;服务本身不会因此启动失败 | | `401 authentication_error` | 客户端 token 不对 —— 用 `token.txt` 里的值 | | `'xxx' 不是内部或外部命令` / 中文注释乱码 | 你跑的是**已删除**的旧 `.cmd` 脚本。改用 `npm start` 或 `node bin/bridge.js` | | `/workbuddy` 或 `/codex` 返回 401 | 当前 realm 未登录或过期:重跑 `--login-workbuddy` | | 报 `code=11-128` 且 message 含 `first message is not system prompt` | **不是内容审核**,是 upstream 的请求形态校验(仅 global realm 有)。请求里必须带 system(Anthropic 协议下是顶层 `system` 字段)。同一个请求在 cn realm 可能能过 | | 报 `code=11-128` 但 message 是 `Illegal API invocation…` | 客户端指纹命中内容审核。检查 `config.json` 的 `upstream.sanitizeFingerprints` 是否为 `true` | | 每隔一个请求就失败一次 | 账号池里混了不同 realm 的账号(历史问题,已修复)。重启服务,启动时会按 realm 隔离 | | `--logout` 报 `[REFUSED] 无法确认` | 非交互环境必须显式确认:加 `--yes` | | 想换账号 / token 泄漏了 | `node bin/bridge.js --logout` 删掉本地凭证,再重新 `--login-workbuddy` | | Responses 侧报模型不存在 / `event: error` | `model` 填的不是上游真实名(例如误填 `sonnet`),改成 `deepseek-v4-flash` 之类。现在会直接返回 `400` + `code=11102`,看错误信息即可定位 | | Responses 流里出现 `response.failed` | 上游中途失败。检查 `response.error` 字段;若 `code=6004` 是用量超限,换模型 | | Codex CLI 报 `missing field` / 协议错误 | 确认 `wire_api = "responses"`,且 `base_url` 末尾是 `/codex`(不要带 `/v1`) | | 端口被占用 | `--port 8899`,或改 `config.json` 的 `server.port` | | 上游连接失败 / 走了代理 | 本服务强制对 `127.0.0.1` 绕过 `HTTP_PROXY`;若上游也需走代理,确保 `HTTPS_PROXY` 正常 | | WorkBuddy 风控 403(code 11140) | 把官方桌面端状态文件里的 `deviceToken` 填进 `config.json` | | **`400 code=11128 Illegal API invocation from an unapproved channel`** | **客户端 system prompt 指纹被上游内容审核拦截**(不是封号/限流)。本服务默认已脱敏,见第七节;仍报错说明出现了新指纹,按第七节步骤定位 | | WorkBuddy 报 `code=6004` | 该模型用量超限,换一个模型试试 | --- ## 十三、安全与合规 - **只监听 `127.0.0.1`**:本地 token 是唯一防线。改成 `0.0.0.0` 会让局域网内任何人白用你的账号。 - `token.txt`、`config.json`(若填了 token)、`auths/*.json` 都是敏感文件, **不要提交到 git、不要分享**。 - **WorkBuddy 上游是对私有接口的非官方调用**,与官方桌面端共用同一账号额度, **仅限本人本机测试**。高频调用、多账号轮换、自动签到会显著增加封号风险, 请勿在生产或团队场景使用。 - 使用上游服务受其服务条款约束,请勿共享、转售或多人共用同一凭证。 --- ## License 仅供个人本机使用。使用者需自行遵守 WorkBuddy 及其上游模型供应商的服务条款。