# AIBridge **Repository Path**: wuyilong/aibridge ## Basic Information - **Project Name**: AIBridge - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 0 - **Created**: 2026-08-29 - **Last Updated**: 2026-09-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # aibridge — 统一 AI 网关 把本机已登录的 **Trae(字节)** 与 **WorkBuddy / CodeBuddy(腾讯)** 登录态、 **Qoder(阿里)** PAT 账号, 统一暴露为 **OpenAI Chat Completions** 与 **Anthropic Messages** 两种协议的一个本地网关。 ## 特性 - 一个端口同时服务两类客户端: - **Cherry Studio / LobeChat / Open WebUI / 自研 SDK** → `/{provider}/v1/chat/completions` - **Claude Code / CC Switch** → `/{provider}/v1/messages` - Provider 由 **URL 路径前缀显式选择**(`/trae/v1/...`、`/workbuddy/v1/...`、`/qoder/v1/...`), 模型列表与请求都按前缀隔离,**不提供**无前缀的裸 `/v1/*` 聚合端点,也**不按** X-Provider 头 / 模型名前缀 / 默认 Provider 自动选路——避免在多厂商模型间无感知切换。 - Trae 侧自带:登录态自动探测/解密、动态模型池、模型回退链、3 级 endpoint 回退、 原生工具调用(`tools` → 上游 `function_call`,稳定可靠)、`` 剥离、上下文截断。 - WorkBuddy 侧自带:凭据自动刷新回写、CodeBuddy IDE 登录态提取、可选脱敏、 **积分余额 / 到期提醒查询**与**每日签到**(移植自 [workbuddy-switch](https://github.com/changexbc/workbuddy-switch))、 **成长任务一键完成 / 任务中心**(参考 [workbuddy2api-panel](https://github.com/linguo2625469/workbuddy2api-panel))、 **多账号**(账号池目录 + `workbuddy-<别名>` 路径前缀)。 - Qoder 侧自带:PAT 请求级传入、cosy 会话交换缓存、服务端动态模型列表、 思考强度透传(`reasoning_effort` / Anthropic `thinking.budget_tokens` → 上游 `parameters.reasoning_effort`,CHL 映射)、**账号收集**(测试成功的 PAT 加密 存本机,额度/模型池快照下次直接可看)。 - 所有上游都归一化成「标准 OpenAI Chat SSE」中间表示,出向编码完全共用。 > 按需求,本网关**不提供** OpenAI Responses(`/v1/responses`,Codex CLI)适配端点。 ## 安装 ```bash pip install -r requirements.txt # fastapi / uvicorn / httpx / cryptography ``` ## 启动 ```bash python main.py --port 9000 ``` 启动后浏览器打开 **http://127.0.0.1:9000/workbuddy/console** 即可进入 WorkBuddy 控制台(积分 / 签到 / 账号情况 / 切换默认模型);访问根路径 `/` 会自动跳转到控制台。 常用参数: | 参数 | 说明 | |------|------| | `--host` | 监听地址(默认 0.0.0.0) | | `--port` | 监听端口(默认 9000) | | `--api-key` | 客户端鉴权 key | | `--log` | 日志文件路径 | | `--desensitize` | 启用 WorkBuddy 脱敏 | | `--no-compact` | 脱敏时保留完整 system | ## 安装为 Windows 服务 打包后可直接把网关注册成开机自启的 Windows 服务。 ### 打包 ```powershell .\build.ps1 -Service # 目录形态 dist\aibridge\(装服务推荐) .\build.ps1 # 单文件 dist\aibridge.exe .\build.ps1 -Release # 发布包 release\aibridge-<版本>-win64.zip ``` > **装服务请用目录形态(`-Service`)。** 单文件包每次启动都要先把整个包解压到 > `%TEMP%\_MEI_xxx`,再由 bootloader 拉起子进程;SCM 的启动等待窗口很短, > 这种启动开销容易撞上 > 「错误 1053:服务没有及时响应启动或控制请求」。目录形态原地加载,无解压、 > 无额外子进程,启动路径最短也最可预测。 ### 分发给别人(`-Release`) `dist\` 里只有 exe 本身,直接拷给别人是跑不起来的:装服务还得手工把 `service.ps1` / `service.bat` / `.env.example` 凑到一起,漏一个就卡住。 `-Release` 产出的是一份**自包含**的压缩包: ```powershell .\build.ps1 -Release # -> release\aibridge-1.0.0-win64.zip (41 MB 左右) # -> release\aibridge-1.0.0-win64\ (同一份内容的解压目录) ``` 包内结构(解压后同目录即为运行目录): ``` aibridge-1.0.0-win64\ ├── aibridge.exe 主程序 ├── _internal\ 运行时依赖(目录形态专有) ├── service.ps1 服务管理脚本 ├── service.bat 双击入口(自动请求管理员权限) ├── .env.example 配置模板(首次安装自动复制为 .env) └── README.txt 面向使用者的说明 ``` 对方拿到后:**解压 → 右键 `service.bat` 以管理员身份运行 → 选 1**。 不想装服务就直接双击 `aibridge.exe`,再开 `http://127.0.0.1:9000/console`。 两点设计考虑: - **发布构建不碰 `dist\`。** 它在 `release\_work\` 下打包,因此本机正跑着服务时 也能安全出新包(普通构建在这种情况下会直接被拦下,见下)。 - **不带本机 `.env` 和日志。** `.env` 含 API Key 与机器路径,日志无分发价值; 组装时会显式排除并校验,混入即中止。安装时由 `.env.example` 重新生成。 > **普通构建的护栏。** 如果 `aibridge` 服务正在运行,`.\build.ps1` / `-Service` > 会**拒绝执行**并提示先 `.\service.ps1 stop`——因为服务运行时占着 `_internal` > 里的文件,删除会删不干净,产出残缺产物(表现为 HTTPS 上游报 > `[Errno 2] No such file or directory`,控制台静态页面丢失)。确需覆盖加 `-Force`; > 只想要分发包则用 `-Release`。 ### 安装 / 卸载 / 启停 ```powershell .\service.ps1 install # 安装并启动(需管理员;默认**以当前用户运行** + 开机自启,会提示输入密码) .\service.ps1 status .\service.ps1 stop .\service.ps1 start .\service.ps1 restart .\service.ps1 uninstall ``` 也可以双击 `service.bat`,它会自动请求管理员权限并给出菜单(第 1 项就是「当前用户」)。 `install` 对**已存在的服务会重新安装**(先卸载再装):Windows 服务的运行账户只能在创建 服务项时指定,不重装就切不了账户;只想保留现状用 `-NoReinstall`。 常用安装选项: | 选项 | 说明 | |------|------| | `-Account current` | **默认值**:以当前用户运行(可直接复用 Trae / CodeBuddy 登录态,需输入密码) | | `-Account system` | 以 LocalSystem 运行(无人值守 / 账户无密码时用,需配合用户目录传递) | | `-NoUserPaths` | 不把用户目录写进 `.env`(仅 `-Account system` 下需要,见下方说明) | | `-Startup delayed` | 延迟自动启动(默认 `auto`) | | `-Port 9001` | 改端口(写入 exe 同目录的 `.env`) | | `-NoStart` | 只安装,不立即启动 | | `-NoReinstall` | 服务已存在时只提示、不重新安装 | | `-ExePath <路径>` | 指定 exe(默认自动探测 `dist\aibridge\` 再回退 `dist\`) | ### 服务账户怎么选 **默认以当前用户运行**(`install` 不加参数即是)。服务账户直接决定能否读到本机登录态: | 账户 | `%APPDATA%` / `%USERPROFILE%` | 用户 DPAPI(解密 IDE 登录态) | 代价 | |------|------|------|------| | **current(默认)** | 天然正确 | 可用 | 安装时要把账户密码交给 SCM;注销后服务不运行 | | system | 指向 `systemprofile` | **不可用** | 需把用户目录写进 `.env` 才能读到登录态 | DPAPI 那一列是关键:LocalSystem 拿不到当前用户的 DPAPI 主密钥,于是连「从 CodeBuddy IDE 的 `state.vscdb` 提取登录态」都会失败,只能退化成在 auth 目录里挑一个 `*.info`。 历史故障正是这样发生的——挑中了几个月前已被上游吊销的僵尸凭据,签到/积分一律 `HTTP 401`,而本地 `expiresAt` 还没到期,光看快照完全看不出问题。 用 LocalSystem 也能跑,只是必须把用户目录写进 exe 同目录的 `.env`(`install` 默认就会写): ```powershell .\service.ps1 install -Account system # 写入 AIBRIDGE_USERPROFILE / APPDATA / LOCALAPPDATA ``` > 为什么落到 `.env` 而不是服务级 Environment 注册表值:实测注册表值会被 > LocalSystem 账户的 profile 解析盖掉;`apply_user_dir_overrides` 是在服务进程内、 > 构建 provider **之前**改写 `os.environ`,确定生效。 三种调整方式: 1. **默认(推荐)**:`.\service.ps1 install` —— 以当前用户运行,路径与 DPAPI 天然正确, 凭据还能自动刷新回写;代价是安装时输一次账户密码。 2. **`-Account system`**:无人值守 / 账户无密码时用,靠 `.env` 里的用户目录覆盖读登录态 (IDE 提取仍不可用,走 `*.info` 兜底)。 3. **`-NoUserPaths`**:不写 `.env`,需自行保证登录态可达,例如手工设置 `CODEBUDDY_AUTH_DIR`、`WORKBUDDY_ACCOUNTS_DIR`、`QODER_ACCOUNTS_FILE`、 `TRAE_MANUAL_TOKEN` 或 `AIBRIDGE_APPDATA`。 排查提示:服务启动后可在 `aibridge.log` 里搜 `[service] 已应用用户目录覆盖`。 若看到「未配置用户目录覆盖,且 APPDATA 指向系统账户目录」的告警,说明 `.env` 里缺 `AIBRIDGE_APPDATA` 等键。 ### 服务排障 ```powershell .\service.ps1 debug # 前台以「服务身份」运行,完整走 SvcRun 路径,无需管理员 ``` `debug` 走的是与真实服务**完全相同**的启动路径(只是 SCM 通信由 pywin32 模拟), 因此它是定位 1053 类问题的首选手段。 其他排查入口: - **日志**:`dist\aibridge\aibridge.log`(与 exe 同目录)。服务模式下进程没有 控制台,uvicorn 的启动报错(如端口占用)也会被桥接进这个文件。 - **事件查看器**:Windows 日志 → 应用程序,来源 `aibridge`。 自定义退出码含义:`10` = 启动构建失败,`11` = 运行期崩溃。 (这些值随 `win32ExitCode=1066` 一起上报,即服务特定错误码。) - **崩溃自动重启**:安装时已自动配置 `sc failure`(5s/10s/30s 三次重启, 24 小时重置计数)。 ### 日志轮转 网关日志默认按大小轮转,由环境变量控制(也可写入 `.env`): | 变量 | 默认 | 说明 | |------|------|------| | `AIBRIDGE_LOG_MAX_MB` | `20` | 单文件上限(MB);`<=0` 表示关闭轮转 | | `AIBRIDGE_LOG_BACKUPS` | `5` | 保留的历史份数(`.1`、`.2`…) | > 轮转是必要的:网关长时间运行 + 打开 `AIBRIDGE_TRACE` 时日志增长很快, > 历史版本曾出现过单文件 553 MB 的情况。 ### WinSW 兜底方案 若内置方案装服务后启动报 1053、而 `service.ps1 debug` 前台运行正常, 可改用 [WinSW](https://github.com/winsw/winsw) 托管:SCM 只跟 WinSW 这个 原生 exe 对话,从而绕开 PyInstaller 打包形态带来的启动不确定性。 配置见 `scripts\winsw\`。 也可用环境变量 / `.env`(参考 [`.env.example`](./.env.example))。 ## 客户端接入 网关不提供无前缀的裸 `/v1/*` 端点;客户端**必须**用带 Provider 前缀的地址: 每个厂商一份独立 Base URL,模型列表按前缀返回该厂商自己的模型: | Provider | OpenAI 兼容 Base URL | 说明 | |----------|---------------------|------| | Trae | `http://localhost:9000/trae/v1` | 本机 Trae 登录态 | | Trae(多账号) | `http://localhost:9000/trae-<别名>/v1` | 账号池中指定账号 | | WorkBuddy / CodeBuddy | `http://localhost:9000/workbuddy/v1` | 本机登录态 | | Qoder | `http://localhost:9000/qoder/v1` | 需请求头 `X-Qoder-Pat: ` | > 前缀路径**必须带 `/v1`**:`/qoder/chat/completions` 这类不带 `/v1` 的写法不提供(404)。 例如 Cherry Studio 填 Base URL `http://localhost:9000/qoder/v1` 即只会看到并调用 Qoder 的模型; `/trae/v1/models` 只会返回 Trae 的模型,不会出现 `DeepSeek-V4-Flash-Official`(Trae)与 `dfmodel`(Qoder)混在一个列表里的情况。 ## 接口 ``` GET /health (公开探活) GET /status (公开状态) GET /console (统一控制台:左侧两级菜单,一级=网关,二级=功能页;/ 跳转到此) GET /trae/console (Trae 控制台,重定向到 /console?tab=trae) GET /workbuddy/console (WorkBuddy 控制台,重定向到 /console?tab=workbuddy) GET /qoder/console (Qoder 控制台,重定向到 /console?tab=qoder) GET /{provider}/v1/models 模型列表(trae / workbuddy / qoder) POST /{provider}/v1/chat/completions (OpenAI) POST /{provider}/v1/messages (Anthropic) POST /{provider}/v1/messages/count_tokens (Anthropic) GET/PUT /workbuddy/v1/default-account (默认账号查询 / 切换,控制台同款) GET/PUT /workbuddy/v1/gateway-key (网关鉴权 Key 查询 / 修改,写入 .env 并热生效) ``` Trae / WorkBuddy 多账号时,上述 `{provider}` 段都可写成 `-<别名>`, 例如 `/trae-a/v1/chat/completions`、`/workbuddy-b/v1/models`。 Trae 控制台端点(均不参与网关鉴权,与 WorkBuddy / Qoder 的控制台端点一致): ``` GET /trae/v1/console-info 静态接入信息(端点 / 模型池规模 / 排程,不访问上游) GET /trae/v1/accounts 账号概览(令牌状态 + 积分 + 汇总) POST /trae/v1/accounts/refresh 强制刷新全部账号积分(绕过缓存) GET /trae/v1/credits 单账号积分明细(?alias=<别名>&refresh=1) GET /trae/v1/checkin 签到状态(只查询,不领取) POST /trae/v1/checkin 对某账号签到(?alias=<别名>,幂等) POST /trae/v1/checkin/all 批量签到全部账号 GET /trae/v1/default-account 当前默认账号 PUT /trae/v1/default-account 切换默认账号(持久化,重启仍生效) ``` WorkBuddy 成长任务端点(控制台「任务中心」的数据源,均不参与网关鉴权; 同样支持 `/workbuddy-<别名>/v1/growth/*` 单账号前缀): ``` GET /workbuddy/v1/growth/tasks 单账号成长任务列表 GET /workbuddy/v1/growth/accounts 全部账号的成长任务矩阵 POST /workbuddy/v1/growth/accept 接受任务 POST /workbuddy/v1/growth/claim 领取奖励 POST /workbuddy/v1/growth/complete 单账号一键完成 POST /workbuddy/v1/growth/complete/all 全部账号一键完成 ``` Qoder 控制台专用端点(均不参与网关鉴权): ``` GET /qoder/v1/console-info 静态接入信息(站点 / 域名,不访问上游) POST /qoder/v1/diagnose PAT 连接测试(成功即自动收集该账号) GET /qoder/v1/accounts 已收集账号列表(含额度与模型池快照) GET /qoder/v1/accounts/{key}/pat 取单个账号的 PAT 明文(**仅本机**,供「复制 PAT」) POST /qoder/v1/accounts/refresh 用各账号保存的 PAT 刷新额度与模型池 DELETE /qoder/v1/accounts/{key} 删除一个已收集账号 DELETE /qoder/v1/accounts 清空全部已收集账号 ``` ### Qoder 控制台:账号收集与额度 Qoder 网关下有一个「**💰 积分与账号**」功能页(与 WorkBuddy 的同名页面同构): - **首次「测试连接」成功后自动收集该账号**,下次打开页面直接可见,无需重新粘贴 PAT; - 列表一级行显示账号名、套餐徽标(Teams / Pro…)、站点、**剩余/总 Credits 与已用百分比**、 PAT 指纹与上次检查时间,行下有剩余占比进度条; 点击展开该账号的**模型池与积分倍率**(按倍率升序,越省越靠前); - 卡头「刷新额度」用各账号保存的 PAT 重新查询额度与模型池;「清空」删除全部收集; - 账号行上的「**复制 PAT**」把该账号的 PAT 取回剪贴板,便于换机器/换客户端时 直接当网关 Key 填(PAT 存的是密文,没有这个入口就只能重新去站点建一个)。 该按钮走 `GET /qoder/v1/accounts/{key}/pat`,**仅限本机回环访问**(见下)。 **数据来源**(实测确认,接口常量见 `qoder/provider.py` 的 `QUOTA_USAGE_PATH`): | 用途 | 接口 | 认证 | |---|---|---| | **额度(Credits)** | `openapi.qoder.sh/api/v2/quota/usage` | `Bearer ` | | 身份 / 套餐 / 重置时间 | `openapi.qoder.sh/api/v3/user/status` | `Bearer ` | | 组织 / 套餐周期 | `center/algo/api/v2/user/plan` | cosy 签名 | | 模型池 / 倍率 | `center/algo/api/v2/model/list` | cosy 签名 | `jt-*` 就是网关 PAT 交换返回的 `securityOauthToken`,因此无需额外交换逻辑。 该端点即官方 qodercli 的 `getQuotaUsage()`(SDK `getUsageInfo()`)所用的同一个, 返回 `userQuota{total,used,remaining,percentage,unit}` 与组织共享资源包 `orgResourcePackage{cap,used,remaining,available}`。 ### `/qoder/v1/models` 需要 PAT,失败时如实报错 模型列表与「能不能真的调用」必须一致,因此**不再静默回退静态别名表**: | 请求 | 返回 | |---|---| | 不带 PAT | **401** — 提示 API Key 必填(`X-Qoder-Pat: `),且不访问上游 | | PAT 被上游拒绝 | **401** — 凭据无效,该换 PAT | | 上游 / 网络故障 | **502** — 与 PAT 无关,该查网络或上游 | | 带有效 PAT | **200** + 该账号在上游的**真实模型池**(含积分倍率,`source_list=remote`) | > **为什么不像以前那样"兜底"**:过去三种失败一律返回 200 + 5 个静态别名 > (`claude-sonnet-4-5` / `gpt-5-codex` / `qwen3.7-max` / `qwen-max` / `auto`)。 > 但用一个已被拒绝的 PAT 去拉列表时,这 5 个名字**一个都调不通** > (`chat`/`messages` 此时返回 401),用户看到的却是"有 5 个模型可用", > 于是把问题误判成「模型列表不全」,排查方向完全被带偏。 > 401(该换 PAT)与 502(该查网络)也刻意分开:混成一个会让用户在错误的方向 > 上浪费时间。 失败会进入 60 秒退避:客户端轮询 `/v1/models` 不会每次都重探上游。 静态别名表仍保留在 `QoderConfig.DEFAULT_ALIASES`,用于 `_resolve_model` 把 `claude-sonnet-4-5` 这类别名映射到上游 key —— 它只是不再作为列表的兜底返回。 > 浏览器里想直接看模型列表时,`/models` 也接受**查询参数**携带凭据 > (地址栏设不了请求头):`/qoder/v1/models?api_key=`,可加 > `&edition=intl|cn` 指定站点。参数名 `api_key` 另兼容 > `api-key` / `apikey` / `key` / `pat` / `token`(大小写不敏感), > 与请求头**任一匹配即通过**。该通路由 `AIBRIDGE_QUERY_KEY_AUTH` > (默认开启)控制:URL 会进浏览器历史与代理日志,因此适合本机 / 临时排查, > 对外的部署建议置 `0` 关闭。详见 > [QODER_MODELS_API_USAGE.md](QODER_MODELS_API_USAGE.md)。 > **⚠ 关于 `quota` 的坑(曾经踩过)**:`/api/v3/user/status` 也有一个 `quota` 字段, > 但它**不是 Credits**——**Teams / Enterprise 账号该字段恒为 0**。早期据此错误地 > 认定「Teams 没有个人额度数字,只有套餐共用池」,页面于是只显示「套餐共用池」。 > 实际上同一个 Teams 账号在 `/api/v2/quota/usage` 里返回 > `userQuota.total=3000`(就是用户在 Qoder 官网「用量」页看到的数字)。 > 因此**额度一律取自 `/api/v2/quota/usage`**,`user/status` 只用于身份与套餐。 > 若额度端点取不到(`quotaAvailable=false`),页面显示「额度未取到」, > **绝不把 0 当作「已耗尽」**。 其他渠道对照(均未采用):CodexBar 走浏览器 Cookie 的 `qoder.com/api/v2/me/usages/big_model_credits`,PAT/jt/Cookie 各种传法实测全部 401; Teams 组织级 OpenAPI(`api.qoder.com/v1/organizations/{org_id}/resource-packages`, 可查组织资源包的初始/已用/剩余)需要组织管理员单独创建的 **API Key**, 个人 PAT 调不通(返回 `invalid apikey`),故不采用。 ### Qoder 上游「帧内错误」:业务码绝不进客户端文本 Qoder 把配额、排队、超时这类错误编码在 **SSE 帧体里**(HTTP 仍是 200): ``` data: {"statusCodeValue":403,"body":"{\"code\":\"112\",\"message\":...}"} data: {"statusCodeValue":403,"body":"{\"code\":\"403\",\"message\":\"{\\\"modelKey\\\":\\\"degrade\\\",\\\"retryAfterSeconds\\\":2}\"}"} ``` 这些**业务码与 HTTP 状态码同形**,而客户端会拿错误文本里的数字猜失败类型: DSH(`dsh-llm-pi-ai` 的 `classifyPiAiError`)用 `/\b(?:401|403)\b/` 判 AUTH, 命中后把界面上真正的原因**替换**成「API 密钥无效」。于是「上游繁忙」被显示成 「密钥无效」,用户拿着一个**完全有效**的 PAT 反复重换——典型症状正是 「同一个 PAT 有时候正常、有时候提示 API 密钥无效」。 | 帧内业务码 | 含义 | 网关行为 | |---|---|---| | `403` + 队列 JSON(`isQueued` / `modelKey=degrade` / `retryAfterSeconds`) | 模型排队 / 降级 | 按 `retryAfterSeconds` **退避重发**(最多 3 次,单次等待 ≤ 8s),成功则客户端毫无感知 | | `504`(`First Token Timeout`)/ `5xx` / `429` | 上游超时、内部错误、限流 | 同样退避重发(默认等 1.5s) | | `112` | 额度 / 订阅到期 | **不重试**(重试不会变好),返回 `402` + 续费指引 | - **文本脱敏**:客户端可见文本里不出现 `401`/`403` 字面量,原始业务码只写网关 日志;上游原文里自己夹带的同形数字也会被中和成 `4xx`。HTTP 层就是 403、 但正文是队列 JSON 的情况,同样按「上游繁忙」而非「鉴权失败」处理。 - **重试边界**:只在**本次尝试还没向客户端吐出任何帧**时重发——已经流出去的 内容收不回来,重发只会让答案重复;等待超过 8s 或 3 次仍未成功,就如实报 「上游繁忙(与 PAT 无关)」,不把客户端拖到超时。 - 回归测试:`tests/test_qoder_queue_retry.py`(重试与脱敏行为)、 `tests/test_qoder_gateway_quota.py`(客户端实际收到的报文)。 **凭据存储(相对早期版本的行为变更)**:早期约定是「PAT 绝不落盘」,因此关掉标签页 就查不到账号。为支持「下次直接点开看」,现在测试成功的 PAT 会**加密**存到 `~/.aibridge/qoder-accounts.json`(可用 `QODER_ACCOUNTS_FILE` 覆盖): - 密钥由「本机 + 当前用户」信息派生(见 `providers/qoder/accounts.py`), **文件被拷到别的机器或别的用户下无法解密**;解密结果还会用 PAT 指纹复核, 不一致即视为无凭据; - 仍然保证:**明文 PAT 不落盘、不写日志**(常规响应只给 SHA-256 指纹前缀); 模型调用端点依旧只认请求头里的 PAT,与收集无关; - **唯一的明文出口**是「复制 PAT」用的 `GET /qoder/v1/accounts/{key}/pat`。 它做了三重收窄,全部满足才返回明文,否则 403: 1. **来源必须是回环地址**——网关默认监听 `0.0.0.0`,这一条挡住局域网与公网; 2. **Host 头必须是本机名**(`localhost` / `127.0.0.1` / `::1`)——挡 DNS rebinding: 攻击者把自己的域名解析到 127.0.0.1,浏览器会带着该域名的 Host 打进来; 3. **Origin 头必须是本机来源**——本网关 CORS 是 `allow_origins=["*"]`,若不做这一步, 用户访问的任意网站都能 fetch 本机端口把明文读走(同源请求不带 Origin,故缺席放行)。 明文也**绝不写进日志**(只记指纹前缀)。代价是用局域网 IP 打开控制台时该按钮不可用, 需改用 `127.0.0.1` / `localhost`——这是刻意的取舍; - 文件权限尽量收紧(POSIX `0600`;Windows 用 `icacls` 去掉继承,只留当前用户); - 不想留凭据就用「清空」按钮,或直接删除该文件。 ### 控制台(统一页面 + 两级菜单) 单文件 HTML 页面(无外部依赖,随 exe 分发)。`/console` 是唯一入口,页面左侧是 **两级菜单**:一级为**网关**(Trae / WorkBuddy / Qoder),二级为该网关下的**功能页**, 内容区一次只呈现一个功能页,不再把 4 张大卡片连成一屏长页。 ``` Trae ─ 💰 积分与账号 · 🧠 模型池 · 🔌 接入信息 WorkBuddy ─ 💰 积分与账号 · 🌱 任务中心 · 🎯 连登与抽奖 · 🔌 接入信息 Qoder ─ 💰 积分与账号 · 🔑 连接测试 · 🔌 接入信息 ``` Qoder 的**模型池并进「连接测试」页**、作为同卡内的次级小节:它就是那次测试的 产物(同一个 PAT、同一份上游 `model/list`),单独成页会逼用户在测试成功后再切 一次菜单才能看到模型。 位置同步到 URL(`?tab=<网关>&page=<功能页>`),可分享、刷新不丢、浏览器前后退可用; 只带 `?tab=` 的旧链接自动落到该网关的第一个功能页。各功能页数据来自同一批接口, **切页不重新请求**(只做显隐),顶部「刷新 / 自动刷新」作用于当前网关。 ### Trae 控制台 展示并操作 `/trae/v1/*` 的数据(与 WorkBuddy 的同名页面同构,复用同一套账号行样式): - **积分与账号**:顶部汇总所有账号的剩余 / 总额度;账号列表的一级行显示账号名、 就绪 / 待刷新 / token 过期徽标、**剩余 / 总额度**、uid 前缀、**设备指纹前缀**与 剩余占比进度条,行内带**签到**按钮;点击展开二级的**权益包明细**(每个签到奖励包 的限额 / 已用 / 剩余 / 到期)。卡头提供**刷新积分**(强制绕过 60 秒缓存)、 **批量签到**与**切换默认账号**; - **模型池**:读自本机 Trae IDE 缓存的模型清单(与 IDE 模型选择器同源),显示积分 消耗倍数(`x0.40` = 每次调用按 0.40 倍计费)、视觉能力、所属模型池,可筛选 「隐藏不可用」。**若读不到 IDE 缓存会明确标注当前是静态兜底表**并给出缓存路径, 而不是安静地展示一份可能过时的清单; - **接入信息**:OpenAI / Anthropic 端点、上游地址、账号池目录、自动签到排程, 以及多账号时每个账号的独立前缀。 > 页面**不写任何浏览器端持久化存储**:Trae 的凭据完全在服务端,页面没有需要 > "记住"的秘密。这与 Qoder 的「PAT 不得长期落浏览器」是同一条约定,由测试守住。 ### Trae 多账号 与 WorkBuddy 的多账号机制**同构**(同样的账号池目录、别名规则、自动归档与 `/trae-<别名>/v1/*` 端点): ```bash # .env TRAE_ACCOUNTS_DIR=C:\Users\me\.aibridge\trae-accounts # 可选:/trae/v1/* 默认使用的别名;留空取排序第一个 TRAE_DEFAULT_ACCOUNT= # 可选:启动时自动把本机各 IDE 版本的登录态归档进账号池(默认关闭) TRAE_AUTO_ARCHIVE=1 ``` 账号池里**一个文件 = 一个账号**,文件格式与参考项目 traework2api 的 `auths/trae-*.json` **完全兼容**(嵌套 `{"account": {...}, "auth": {...}}`), 因此那些登录脚本产出的文件可直接放进目录使用;也接受扁平形(手写方便): ```json { "account": {"uid": "819590633097100", "nickname": "用户…"}, "auth": { "accessToken": "…", "refreshToken": "…", "expiresAt": 1790303156, "apiHost": "https://api.trae.cn", "machineId": "…", "deviceId": "…" } } ``` 端点前缀由**文件名**推导:`a-primary.json` → `/trae-a/v1/*`; 参考项目的 `trae-.json` → `/trae-/v1/*`(`trae-` 前缀会被剥掉, 保证文件名推导与端点解析一致,不会出现"池里有账号但端点 404")。 `TRAE_AUTO_ARCHIVE=1` 时,启动会遍历本机**所有** Trae 版本(solo / cn / solo-sg / sg) 的 `storage.json`,把尚未入池的登录态按 `auto` / `auto-2` / `auto-3`… 归档; 别名固定不随登录顺序漂移,同一账号只归档一次,绝不覆盖手工归档的文件。 ### Trae 积分与签到 Trae 每日签到会送积分(实测单次 150,另有 50 额外奖励)。网关默认在启动时补签一次, 并可按每日时刻/固定间隔排程: ```bash # .env TRAE_CREDITS_ENABLED=1 # 积分/签到总开关 TRAE_AUTO_CHECKIN=1 # 自动签到(默认开,幂等) TRAE_AUTO_CHECKIN_TIMES=09:00 # 每日定时,逗号分隔;留空 = 不定时 TRAE_AUTO_CHECKIN_ON_START=1 # 启动即签(补上关机期间错过的时刻) TRAE_AUTO_CHECKIN_INTERVAL=0 # 固定间隔补签(秒);0 = 不启用 TRAE_CREDITS_CACHE_TTL=60 # 积分结果缓存秒数 ``` 签到**幂等**:先查状态,已签到就不再领取,重复执行安全;单个账号失败只记该账号 的错误,不影响其他账号。 积分口径按上游 `usage_summary` 采信,并同时用**逐包求和**做交叉校验,两者不一致时 页面会显示「与上游汇总不一致」——因为实测发现首个"免费"权益包**没有** `credits_limit` 字段,只按逐包求和会算错,交叉校验能在上游改字段时立刻暴露问题,而不是安静地显示 一个错的数字。 ### Trae 设备指纹 Trae 上游按 `x-machine-id` / `x-device-id` 做风控与额度归属。网关为**每个账号**持久化 一对固定设备身份(`~/.aibridge/trae-devices.json`,按 uid 分键),可由 `TRAE_DEVICE_FILE` 覆盖路径。 为什么必须固定:早期实现每次请求都现生成一个 `uuid4()`,等于**每个请求都是一台新 设备**——同一账号几秒内从上百个"新设备"发起请求,正是被限流/校验的直接诱因;且 这类失败极具迷惑性(重试可能恰好成功),表现为「偶发失败、像代理有问题」。 为什么按账号分而不是按机器分:实测本机 IDE 是"一台机器一个设备 id、多账号共用" (`telemetry.machineId` 在 solo / cn 两个 IDE 上相同,`usertag` 里一台设备绑了 2 个账号), 但网关**刻意不模仿**——一个设备跑多个账号正是"账号农场"的风控特征,而"不同账号来自 不同设备"才是最正常的形态。账号文件里带了 `machineId`/`deviceId` 时以它为准(那是该 账号真正登录用的设备),否则按 uid 生成一次并落盘。 **uid 缺失时返回一次性随机身份**,既不落盘也不共享:否则两个未知账号会拿到同一个 设备 id(这一漏洞在实测中被发现并已修复)。 ### WorkBuddy 控制台 展示并操作 `/workbuddy/v1/*` 的数据: - **积分与账号**(一个整行卡片搞定):卡头显示 `/workbuddy/v1/*` 当前指向的账号并 提供**切换**入口(多账号时)、**全部账号签到**与**一键完成成长任务**;顶部汇总条 显示所有账号的剩余 / 总额度 / 临期 / 已过期;下面是**账号列表**——一级行显示账号名、 就绪徽标、**剩余分 / 总分**、最近到期、剩余占比进度条,以及**该账号自己的签到状态与 连续签到天数**(各账号天数不同,因此签到按钮就在每个账号行上);点击账号行展开二级的 **套餐明细**(默认折叠已用尽的历史包,可一键「显示全部」),展开区可设为默认账号; 卡底一行说明自动签到调度; - **任务中心**:全账号成长任务矩阵——顶部汇总(已完成 / 总数 / 待领奖 / 需手动), 每个账号一级行显示完成度与「完成此账号」按钮,展开可见该账号全部 18 个任务的 进度、奖励与状态(需手动的任务带徽标与原因说明);卡头「一键完成」对全部账号执行; - **接入信息**:客户端接入地址。 卡头的「切换」按钮与 API 等价,多账号时决定 `/workbuddy/v1/*`(如 `/workbuddy/v1/chat/completions`)实际指向哪个账号;手动指定优先于自动选路并持久化 到账号池目录下的 `workbuddy-default-account.json`(重启仍生效),选择「自动选路」则 恢复按积分自动选择: ``` GET /workbuddy/v1/default-account 当前默认账号 + 候选账号清单 PUT /workbuddy/v1/default-account {"defaultAccount": "<别名>"}("auto" 恢复自动选路) ``` > 优先级:手动指定 > 自动选路(`WORKBUDDY_AUTO_SELECT`)> `WORKBUDDY_DEFAULT_ACCOUNT` > > 别名排序。手动指定的账号被移出账号池后自动失效。持久化文件路径可用 > `WORKBUDDY_DEFAULT_ACCOUNT_FILE` 覆盖。 ### WorkBuddy 积分 / 签到 复用本机 WorkBuddy 登录态,额外提供三个端点(移植自 workbuddy-switch, 账号凭据不出网关,响应中**不含任何 token**): ``` GET /workbuddy/v1/credits 积分余额、各套餐包剩余量与到期时间 GET /workbuddy/v1/credits/accounts 所有账号的积分汇总 + 套餐明细(控制台用) GET /workbuddy/v1/credits/usage 积分 + 签到状态一次返回(面板/脚本用) GET /workbuddy/v1/checkin 查询今日签到状态(只读,不触发签到) POST /workbuddy/v1/checkin 执行每日签到(幂等:已签到返回 already) POST /workbuddy/v1/checkin/all 对所有账号签到(单账号失败不影响其他账号) GET /workbuddy/v1/growth/tasks 单账号成长任务列表(进度 / 奖励 / 状态) GET /workbuddy/v1/growth/accounts 全部账号的成长任务矩阵(任务中心数据源) POST /workbuddy/v1/growth/accept 接受任务(缺省接受全部未接受的) POST /workbuddy/v1/growth/claim 领取奖励(缺省领取全部已完成的) POST /workbuddy/v1/growth/complete 单账号一键完成成长任务 POST /workbuddy/v1/growth/complete/all 全部账号一键完成成长任务 ``` `GET /workbuddy/v1/credits/accounts` 是控制台「积分与账号」卡片的数据源:一次 返回每个账号的汇总(总分 / 剩余分 / 临期 / 最近到期)与该账号完整的套餐 `resources`,因此页面展开二级明细时无需再对 N 个账号各发一次请求。多账号模式下 并发查询(各账号沿用自己 60 秒的积分缓存),**单个账号失败只影响自己那一条** (`ok: false` + `error`),不会让整个端点失败: ```json { "mode": "multi", "count": 2, "defaultAccount": "auto", "accounts": [ { "alias": "auto", "accountName": "your-nickname", "isDefault": true, "ready": true, "tokenExpired": false, "ok": true, "totalCapacity": 2300.0, "totalRemaining": 1657.82, "expiringSoonRemaining": 0, "expiredRemaining": 0, "soonestExpireAt": 1791963325000, "resources": [ { "packageName": "...", "total": 1500.0, "remaining": 1357.82 } ] } ] } ``` `GET /workbuddy/v1/credits` 响应示例: ```json { "ok": true, "accountId": "b786b007-...", "accountName": "your-nickname", "totalCapacity": 5000.0, "totalRemaining": 46.66, "expiringSoonRemaining": 0, "expiredRemaining": 0, "soonestExpireAt": 1791939633000, "expiringSoon": false, "expired": false, "resources": [ { "packageCode": "TCACA_code_007_nzdH5h4Nl0", "packageName": "CodeBuddy个人版国内运营裂变包", "total": 100.0, "remaining": 40.0, "used": 60.0, "status": 0, "expireAt": 1790911358000, "expiringSoon": false, "expired": false } ] } ``` 实现要点: - 积分优先走套餐页 **summary / paid / free 三路**接口并行查询,全部不可用时 回退旧的 `POST /v2/billing/meter/get-user-resource`;「合法的空数组」视为成功, 不会误触发回退。 - 签到先查状态(`checkin-activity-status`,回退 `checkin-status`),**未签到才** 提交 `daily-checkin`,服务端返回「已签到」也按成功处理,因此可安全重复调用。 - 遇到 401/403 用 refresh token 刷新一次并重试;**一次查询最多刷新一次**, 避免并发分支各自刷新、用旧 refresh token 覆盖刚落盘的新 token。 - 资源接口必须携带 `X-Client-Platform: web` / `Origin` / `Referer` 与**浏览器 User-Agent**:billing 接口的 WAF 会按 UA 做客户端指纹识别,httpx 默认 UA 会被 判为非法客户端并返回 `code 10085`。 - 过期判定:剩余额度在 `EXPIRING_SOON_DAYS`(默认 7 天)内到期 → `expiringSoon`; 已过 `DeductionEndTime` 且仍有剩余 → `expired`。 相关配置(`.env`): | 变量 | 默认 | 说明 | |------|------|------| | `WORKBUDDY_CREDITS_ENABLED` | `1` | 是否启用积分/签到端点 | | `WORKBUDDY_CREDITS_CACHE_TTL` | `60` | 积分结果缓存秒数 | | `WORKBUDDY_GROWTH_ENABLED` | `1` | 是否启用成长任务端点 | | `WORKBUDDY_GROWTH_TIMEOUT` | `25` | 任务列表 / 领奖等普通请求超时秒数 | | `WORKBUDDY_GROWTH_CHAT_TIMEOUT` | `90` | 对话类任务超时秒数(要等模型出字) | | `WORKBUDDY_ACCOUNTS_DIR` | `~/.aibridge/workbuddy-accounts` | 多账号池目录;留空用默认目录(多账号默认启用) | | `WORKBUDDY_DEFAULT_ACCOUNT` | 空 | `/workbuddy/v1/*` 默认别名;留空取排序第一个 | | `WORKBUDDY_AUTO_ARCHIVE` | `0` | 启动时自动归档本机当前登录态 | | `WORKBUDDY_AUTO_ARCHIVE_ALIAS` | `auto` | 自动归档的别名前缀 | | `WORKBUDDY_ACCOUNTS_MAX` | `20` | 账号池上限,达到后停止自动归档 | | `WORKBUDDY_AUTO_CHECKIN` | `1` | 启动后在后台对所有账号签到 | | `WORKBUDDY_AUTO_CHECKIN_TIMES` | `09:00` | 每日定时签到时刻,逗号分隔;留空关闭 | | `WORKBUDDY_AUTO_CHECKIN_ON_START` | `1` | 启动时立即补签一次 | | `WORKBUDDY_AUTO_CHECKIN_INTERVAL` | `0` | 固定间隔补签秒数;0 = 不启用 | | `WORKBUDDY_STREAK_REPORT` | `1` | 签到后是否顺带做一次连登活跃上报(每天每号 1 次) | | `WORKBUDDY_INTL_EXTRA_MODELS` | `deepseek-v4.1-flash` | 国际版 `/v1/models` **追加**的清单外模型,逗号分隔;留空关闭追加(见下) | ### WorkBuddy 连登与抽奖(成长中心) > ⚠️ **连登 ≠ 签到**。两者是独立体系,天数分开计: > 签到(`credits`)返回的 `streakDays` 是**签到活动周期内**的连续天数; > 连登(`streak`)返回的 `growthStreakDays` 是**成长中心**的连登天数, > 数的是「连续多少天有客户端对话活跃」,靠 `/v2/report` 活跃上报推进。 连登满 **7 / 14 / 28** 天各解锁一个档位,兑换可得积分、能量、补签卡与 **抽奖次数**——抽奖次数**只能**从兑换获得,因此没兑换过时显示 0 是正常状态。 **读写严格分离**,页面据此决定能否自动调用: ``` GET /workbuddy/v1/streak 连登状态 + 抽奖次数(只读) GET /workbuddy/v1/streak/accounts 全部账号连登矩阵(只读,面板数据源) POST /workbuddy/v1/streak/report 连登活跃上报(手动补跑;日常由签到代劳) POST /workbuddy/v1/streak/gift 领取新手礼包 + 活动补偿 POST /workbuddy/v1/streak/redeem 兑换档位(body {"tier":"7d"},缺省全部已解锁) POST /workbuddy/v1/streak/lottery/draw 抽奖(body {"times":N},上限 20) ``` **唯一的自动写操作是活跃上报**(跟随每日签到跑,`WORKBUDDY_STREAK_REPORT=1`)。 兑换 / 抽奖 / 礼包**只在页面点按钮时执行**——它们会真实发奖或消耗抽奖次数, 网关绝不会自动触发。页面加载仅读取状态。 写操作全部**幂等**:已领过的礼包、未解锁的档位、没有次数时的抽奖,都按 「无变化但成功」处理(页面不把它们显示成故障),可安全重复点击。每次请求 带一个随机 `client_token` 作幂等令牌(对齐官方前端 `randomUUID()`)。 > 接口形状移植自 [workbuddy2api-panel](https://github.com/linguo2625469/workbuddy2api-panel) > 的实测逆向结果,**本仓库未用真实账号逐条验证**。首次接入建议先只调只读 > 接口核对字段,确认无误后再使用写操作(写操作不可逆)。 ### WorkBuddy 成长任务(任务中心 / 一键完成) 官方「成长计划」共 **18 个任务**。网关把其中**纯 API 可完成的 17 个**做成 一键完成(面板上「🌱 一键完成成长任务」按钮紧挨「全部签到」,另有独立的 「🌱 任务中心」卡片展示每账号的任务矩阵)。 ``` GET /workbuddy/v1/growth/tasks 单账号任务列表(进度 / 奖励 / 状态) GET /workbuddy/v1/growth/accounts 全部账号的任务矩阵(任务中心数据源) POST /workbuddy/v1/growth/accept 接受任务(缺省接受全部未接受的) POST /workbuddy/v1/growth/claim 领取奖励(缺省领取全部已完成的) POST /workbuddy/v1/growth/complete 单账号一键完成 POST /workbuddy/v1/growth/complete/all 全部账号一键完成(账号间串行) ``` 连登与抽奖(成长中心,详见[连登与抽奖](#workbuddy-连登与抽奖成长中心)): ``` GET /workbuddy/v1/streak 连登状态 + 抽奖次数(只读) GET /workbuddy/v1/streak/accounts 全部账号连登矩阵(只读) POST /workbuddy/v1/streak/report 连登活跃上报(写) POST /workbuddy/v1/streak/gift 领取礼包 + 补偿(写,幂等) POST /workbuddy/v1/streak/redeem 兑换档位(写) POST /workbuddy/v1/streak/lottery/draw 抽奖(写,消耗次数,上限 20) ``` **幂等**:任务按天幂等,重复点击不会重复扣资源;已达标的任务自动跳过, 已领奖的不会重复领取(服务端返回 `already_claimed` 按成功处理)。 **不做的事(面板标为「需手动」)**: | 任务 | 原因 | |---|---| | `Expert_Philanthropy` | 需真实捐款并校验捐赠回执,无法以 API 完成 | > `black_cat`(夜猫子)是**跨天累计**任务(连续 3 天、每天 23:00–08:00 内一次 > glm-5.2 对话,当日第 2 次起不重复计数)。网关在窗口内自动补当天那一次, > 剩余天数由次日的运行补齐——这不是覆盖率缺口。 **客户端指纹(同一 `/v2/report`,三套指纹)**: 「需电脑端」类任务**不是独立端点**,而是同一条上报通道上的**另一套客户端指纹**。 这是本模块最容易踩的坑:只改事件字段不改请求头,上游照样返回 200 却静默丢弃。 | 指纹 | 域 | 判据事件 | |---|---|---| | CLI(`web-Agents`) | `www.codebuddy.cn` | `chat_request_send` 等活跃上报 | | 桌面(`workbuddy-desktop`,UA `WorkBuddy/5.5.6`) | `copilot.tencent.com` | `agent_task_created` / `chat_message_response(isSuccessful)` / `skill_info` / `expert_actual_use` … | | Web(浏览器 UA + `x-client-platform: web`) | `www.workbuddy.cn` | `web_element_click(library_doc_intro_click)` | 桌面指纹必须**整套一起换**(UA + `X-Domain` + `X-Product` + 事件里的 `ideName/ideType/extName/extVersion/os/arch`),缺一项就当未知客户端处理。 **几个会静默失败的关键点(实测踩过)**: - `expert_actual_use` / `skill_info` 的 `requestId` **必须是真实对话的服务端 id** (从 SSE 流里抓的 `cmb-`+32hex 或裸 32hex);自造 UUID 一律不计数。网关在拿 不到真实 id 时**放弃该事件**而不是伪造。 - 专家 id 必须是平台上**真实存在**的专家(走 `copilot.tencent.com` 的 portal 接口拉取,`expert_type`/`version` 才是判据口径);编造 id 不计数。 - 应用类任务的 `elementId` 必须是真实应用 id:网关直接读任务自带的 `jump_url`(`workbuddy://home?templateId=...`)取其权威值,硬编码只作兜底。 - 主题任务的判据是 `appearance_skin_apply` **事件**,`set` API 只是留痕; 且 `set` 要走 `copilot.tencent.com/v2/user-asset/appearance/set`(另一个域上 的同名路径无效)。 领奖**只在 Web 域生效**——用 CLI 域调 `.../reward/claim` 会长期返回 400。 **⚠️ 关于「上报成功」与「已计分」的区别(实测踩过)**: `POST /v2/report` 返回 200 **只代表上游受理了事件,不代表任务已计分**。 上游会按账号真实使用情况做校验,缺少使用历史的账号可能出现「上报全 200、 任务却停在 0」的情况(计分也可能滞后数分钟,重跑一次即可补上)。 因此接口把两者分开返回,页面也不会谎报成功: - `scored`:执行前后对比进度**确实推进了**的任务; - `unscored`:上报已受理但上游未计分的任务 → 页面提示「该账号可能需要先在 客户端真实使用过」。 实测两个账号(`auto` 有真实使用历史 / `auto-2` 为新归档账号):补齐指纹与 修正应用 id 后,**两个账号都是一轮 12/18 → 16/18**(各 +300 积分 +15 能量), `RichMeow_Chat`、`skill_1`、`Expert_lighthouse`、`Buddy_App_QQ` 四项一次点亮。 剩余 2 项为 `Expert_Philanthropy`(需真实捐款)与 `black_cat`(需第 3 个自然日, 次日运行自动补齐)。故**一键完成值得点两次**。 `POST /workbuddy/v1/growth/complete` 响应要点: ```json { "ok": true, "elapsedMs": 146476, "accountName": "your-nickname", "accepted": ["chat_5", "expert_5", "..."], "actions": [ {"taskCode": "chat_5", "result": "reported", "count": 5} ], "scored": ["RichMeow_Chat", "Expert_lighthouse", "skill_1", "Buddy_App_QQ"], "unscored": [], "claim": {"ok": true, "claimed": ["chat_5"], "credit": 300, "energy": 15}, "summary": {"total": 18, "done": 16, "claimable": 0, "manual": 1, "rewardCredit": 100, "rewardEnergy": 5}, "pending": [ {"taskCode": "black_cat", "current": 2, "target": 3, "manual": null} ] } ``` **耗时**:一轮约 3–5 分钟(含 5 次真实对话与异步计分等待)。前端 `api()` 为此支持显式 `timeout`,单账号同时只允许跑一次(并发请求被拒绝,避免重复上报)。 ## WorkBuddy 多账号 桌面端只会把**当前登录账号**写到固定的 `*.info` 文件——退出换账号就会覆盖它, 网关随之"看不见"前一个账号。多账号的做法是**不干预登录流程**:把每个账号的 `*.info` 另存到账号池目录,网关扫描该目录,每个文件成为一个账号。 ### 配置与归档 ```bash # .env # 账号池目录:留空即用默认值 ~/.aibridge/workbuddy-accounts —— 多账号模式默认启用, # 目录里有账号就用账号池,没有则回退单账号自动探测。 # 这行只有想把账号池挪到别处时才需要写。 WORKBUDDY_ACCOUNTS_DIR= # 可选:/workbuddy/v1/* 默认使用的别名;留空取排序第一个 WORKBUDDY_DEFAULT_ACCOUNT= # 可选:启动时自动归档本机当前登录态(默认关闭) WORKBUDDY_AUTO_ARCHIVE=1 ``` 国际版同理,默认目录是 `~/.aibridge/workbuddy-intl-accounts`(与国内版分开: `www.workbuddy.ai` 的登录态在国内版后端调不通,反之亦然)。 ### 自动归档(可选) 开启 `WORKBUDDY_AUTO_ARCHIVE=1` 后,**启动网关时会自动把本机所有可用的登录态 归档进账号池**,无需手动执行归档命令: ``` [workbuddy] 自动归档当前登录态 账号A -> auto.info(别名 auto,端点 /workbuddy-auto/v1/*) [workbuddy] 自动归档当前登录态 账号B -> auto-2.info(别名 auto-2,端点 /workbuddy-auto-2/v1/*) [workbuddy] 自动归档新增 2 个账号:auto, auto-2 [workbuddy] 多账号模式:2 个账号 [auto, auto-2],默认 auto ``` **会扫描全部候选文件**(CodeBuddy IDE 导出的 `workbuddy-ide.info` + 独立桌面 客户端的 `workbuddy-desktop.info` 等),因此「登录新账号后重启网关」一定能收到, 不受单文件探测的优先级影响。 > 客户端换账号时留下的带 ISO 时间戳备份文件 > (`workbuddy-desktop.2026-09-14T07-33-36-196Z.xxxx.info`)**会被排除**: > 它们保存的是切换前的旧账号,且会随每次换号不断堆积。 行为约束(刻意保守,避免账号池失控): - **别名固定**:分配 `auto` / `auto-2` / `auto-3`…,**不按登录顺序**分配 a/b/c。 否则删掉中间账号会导致别名漂移,客户端 Base URL 会静默指向另一个账号。 - **同一 uid 不重复归档**:已在池中时不做任何事,不覆盖你手工归档的文件。 - **只增不改**:从当前登录态复制,绝不修改桌面端原文件。 - **上限保护**:账号池达到 `WORKBUDDY_ACCOUNTS_MAX`(默认 20)后停止自动归档 并告警,避免账号池变成「登录历史」无限增长。 - **失败不影响启动**:单个候选损坏只跳过该候选,网关照常启动。 > ⚠️ **自动归档救不了「忘记归档就退出」**:退出登录后桌面端会覆盖当前凭据, > 前一个账号已无处可读。所以仍是**退出前先启动一次网关**(或手动归档), > 才能把该账号留住。 ### 自动签到(默认开启) 网关会在**每日定时**对所有账号签到,把当天积分领掉: ``` [workbuddy] 自动签到已启动:启动即签=True,每日定时=[09:00] [workbuddy] 每日定时签到:自动签到完成:2/2 成功(auto=success, auto-2=already) ``` 三种触发方式(可叠加): | 方式 | 配置 | 默认 | 说明 | |------|------|------|------| | **启动补签** | `WORKBUDDY_AUTO_CHECKIN_ON_START` | `1` | 启动后立即签一次,补上关机期间错过的时刻 | | **每日定时** | `WORKBUDDY_AUTO_CHECKIN_TIMES` | `09:00` | 每天在指定时刻签到;支持多个时刻 | | **固定间隔** | `WORKBUDDY_AUTO_CHECKIN_INTERVAL` | `0` | 每 N 秒补签;0 = 不启用 | ```bash WORKBUDDY_AUTO_CHECKIN_TIMES=09:00 # 每天 9 点 WORKBUDDY_AUTO_CHECKIN_TIMES=09:00,21:30 # 每天两次 WORKBUDDY_AUTO_CHECKIN_TIMES= # 留空 = 关闭每日定时 WORKBUDDY_AUTO_CHECKIN=0 # 关闭全部自动签到 ``` 特性: - **幂等**:先查状态、未签到才提交,已领过的账号返回 `already`,重复触发 不会重复领取。 - **不阻塞启动**:签到在后台线程执行,网关立刻可服务。 - **故障隔离**:某账号失败只记录该账号的错误,不影响其他账号。 - **单账号模式同样生效**:未配置账号池时也会自动签到。 - **定时容错**:时刻配置写错(如 `25:00`、`9`)只忽略该项,不会让网关起不来。 最近一次结果与**下次签到时刻**可通过 `GET /workbuddy/v1/accounts` 的 `autoCheckin` 字段查看: ```json "autoCheckin": { "ran": true, "success": 2, "total": 2, "accounts": [ {"alias": "auto", "result": "already", "streakDays": 12}, {"alias": "auto-2", "result": "success", "streakDays": 1} ], "schedule": { "onStart": true, "dailyTimes": ["09:00", "21:30"], "intervalSeconds": null, "nextRunAt": 1789392600000 } } ``` ### 手动归档 ```bash python -m aibridge.tools.archive_account a # 归档当前登录账号为别名 a python -m aibridge.tools.archive_account --list # 查看账号池 ``` 典型流程: ```bash # 1. 登录账号 A,归档为 a python -m aibridge.tools.archive_account a # 2. 在客户端退出,登录账号 B,归档为 b python -m aibridge.tools.archive_account b # 3. 重启网关,两个账号同时可用 ``` > 归档后必须**登录不同账号**再执行第二次归档。若两次归档的是同一账号, > 网关会按 uid 去重,只保留凭据较新的那一份(日志会提示)。 ### 端点 每个账号有独立的路径前缀 `/workbuddy-<别名>/v1/*`: | 端点 | 说明 | |------|------| | `/workbuddy-<别名>/v1/chat/completions` | 对话(OpenAI) | | `/workbuddy-<别名>/v1/messages` | 对话(Anthropic) | | `/workbuddy-<别名>/v1/models` | 该账号的模型列表(各账号权益不同) | | `/workbuddy-<别名>/v1/credits` | 该账号积分 | | `/workbuddy-<别名>/v1/checkin` | 该账号签到(GET 查状态 / POST 执行) | | `/workbuddy/v1/*` | 默认账号(等价于默认别名的前缀) | 新增两个多账号专用端点: ``` GET /workbuddy/v1/accounts 账号概览(默认只返回身份与就绪状态) GET /workbuddy/v1/accounts?usage=1 额外拉取各账号积分与签到状态 POST /workbuddy/v1/checkin/all 对所有账号签到(单账号失败不影响其他账号) ``` `GET /workbuddy/v1/accounts` 响应示例: ```json { "mode": "multi", "default": "a", "count": 2, "accounts": [ {"alias": "a", "provider": "workbuddy-a", "uid": "b786...", "nickname": "账号A", "ready": true, "isDefault": true}, {"alias": "b", "provider": "workbuddy-b", "uid": "d1f2...", "nickname": "账号B", "ready": true, "isDefault": false} ] } ``` > `?usage=1` 会对每个账号发起多次上游请求,因此**默认不拉取**积分与签到状态。 ### 客户端接入 每个账号一份独立 Base URL,模型列表按前缀隔离: | Provider | Base URL | |----------|----------| | 默认账号 | `http://localhost:9000/workbuddy/v1` | | 账号 a | `http://localhost:9000/workbuddy-a/v1` | | 账号 b | `http://localhost:9000/workbuddy-b/v1` | ### 文件命名与别名 别名取自文件名第一段(`a-primary.info` → `a`)。规则: - 只允许小写字母、数字、连字符; - 不能是保留字(`trae` / `workbuddy` / `qoder` / `zcode` / `v1` / `admin` 等); - 文件名恰好等于别名时优先(`a.info` 胜过 `a-extra.info`); - **同一 uid 出现多份**时保留 `expiresAt` 较新的一份。 ### 行为与边界 - **默认启用**:账号池目录默认为 `~/.aibridge/workbuddy-accounts` (可用 `WORKBUDDY_ACCOUNTS_DIR` 或 `AIBRIDGE_HOME` 改)。目录不存在或为空时 回退到原有的单账号自动探测,行为与之前一致——**配置读不到也不会再静默退化**: 打包后的服务只认 exe 同目录的 `.env`,一次重建丢掉它就曾让控制台只剩 1 个账号。 - **国际版不进国内版池**:自动归档会按 `auth.domain` 排掉 `workbuddy.ai` 的登录态 (它属于 `workbuddy-intl`,用国内后端调不通)。 - **故障隔离**:每账号一个独立的 `CredentialManager` 与缓存,token 刷新回写到 各自的文件;某账号凭据损坏只会让该账号不可用(不出现在账号池中),不影响其他账号。 - **无效条目不入池**:JSON 损坏或缺少 `accessToken` 的 `.info` 会被忽略并记日志, 不会占住别名,也不会让 `/accounts` 出现永远失败的账号。 - **凭据为明文**:账号池目录中存放的是明文 token,建议限制目录访问权限, 并避免把同一账号在两处重复登录(后登录会使先前的 refresh token 失效)。 - **多账号共用本机 IP**:网关不做风控规避,请按正常节奏使用。 网关有**两套互不相干的鉴权**,不要混淆: | | 保护对象 | 谁来校验 | 凭据 | |---|---|---|---| | **模型调用鉴权** | 模型端点 `/{provider}/v1/*`(chat/completions、messages、models、count_tokens) | 网关 `_check_auth` | `AIBRIDGE_API_KEY`(**Qoder 端点豁免**,见下) | | **上游凭据** | 不校验,仅透传给上游 | 上游厂商 | Qoder PAT | `/health`、`/status` 为公开探活端点。控制台自身的接口 (`/workbuddy/console`、`gateway-info`、`accounts`、`credits`、`checkin`、 `default-account`、`gateway-key`,以及 Qoder 的 `console-info`、`diagnose`、 `accounts`、`accounts/refresh`)**不参与网关鉴权**,因此启用 `AIBRIDGE_API_KEY` 后控制台依然可以正常使用。 启用了 `AIBRIDGE_API_KEY` 时,模型端点要求携带 `Authorization: Bearer ` 或 `X-Api-Key: `。 > **Qoder 端点豁免网关鉴权**(`/qoder/v1/*`)。客户端通常只有一个「API Key」 > 字段,Qoder 必须填 PAT;若再要求它同时是网关 Key,PAT 会被当成错的网关 Key > 直接 401(`invalid api key`),表现为「切到 Qoder 模型立刻报密钥无效」。 > 豁免是安全的:**PAT 是 Qoder 唯一的凭据来源**,网关侧不存在任何可被借用的 > 兜底凭据,不带 PAT 的请求仍然 401(由 Qoder 自己的 PAT 校验把关)。 > > 反过来说,**任何带服务端凭据兜底的 Provider 都不能获得豁免** —— 那样任何能 > 访问该端口的人都能白嫖服务器上的凭据。这条约束由 > `test_qoder_endpoints_never_use_server_side_credential` 守住。 **注意**:在非豁免端点上,同一个 `Authorization` 头先用于网关鉴权,通过后其值 仍会按上游凭据解释;若上游凭据与网关 Key 不同,请用专用头 `X-Qoder-Pat` 显式指定,避免歧义。 **在控制台里改 Key**:右上角 🔑 Key → 「服务端鉴权」→ 填写新 Key → *保存到 .env*。 它只改 `.env` 中的 `AIBRIDGE_API_KEY` 一行(其余配置与注释原样保留,原子写入), **保存后立即对所有请求生效,且重启后依然生效**。留空则关闭鉴权(会二次确认)。 也可直接调接口: ``` GET /workbuddy/v1/gateway-key 查询是否启用鉴权 + Key 掩码 + .env 路径 PUT /workbuddy/v1/gateway-key {"apiKey": "<新 Key>"}(空串 = 关闭鉴权) ``` > 注意优先级:**进程环境变量 > `.env`**。若 `AIBRIDGE_API_KEY` 已在真实环境变量中, > 写 `.env` 下次启动不会生效;此时接口/页面会返回 `envVarPinned: true` 并提示你。 > 控制台页面自身不携带任何 Key(没有输入框、不写 localStorage、不发 > `Authorization` 头)——因为它调用的都是不参与鉴权的控制台接口,无需凭据。 > 裸 `/v1/*`(如 `/v1/models`、`/v1/chat/completions`)已移除:访问会返回 404 提示, > 请改用 `/trae/v1/*`、`/workbuddy/v1/*`、`/qoder/v1/*` 等带前缀的端点。 ## 目录结构 ```text aibridge/ ├── config.py # 网关配置 ├── server.py # FastAPI 网关层(路由 / 鉴权 / 出向编码) ├── security.py # API Key 鉴权 ├── errors.py # 统一错误响应 + 上游状态码映射 ├── sse.py # SSE 字节流工具 ├── tokens.py # token 估算 ├── logging_util.py # 日志(含按大小轮转) ├── service.py # Windows 服务宿主 + install/uninstall/start/stop/status ├── service_cli.py # `aibridge service <子命令>` 命令解析 ├── protocol/ # 出向编码(Chat / Anthropic 转换) │ ├── outbound.py │ └── anthropic_adapter.py └── providers/ ├── base.py # Provider 抽象基类 ├── registry.py # Provider 构建 / 选择 ├── trae/ # Trae 上游(auth/decrypt/models/streambuf/provider) ├── workbuddy/ # WorkBuddy 上游(provider + vendor 脱敏/凭据提取) │ ├── provider.py # provider + 动态模型列表 + 多账号容器 │ ├── accounts.py # 账号池(多账号发现 / 别名解析 / uid 去重) │ ├── credits.py # 积分查询 / 每日签到(移植自 workbuddy-switch) │ ├── growth.py # 成长任务(任务列表 / 领奖 / 行为上报 / 一键完成) │ ├── auto_checkin.py # 自动签到调度(启动补签 / 每日定时 / 固定间隔) │ └── vendor/ # desensitize.py / ide_credential.py └── qoder/ # Qoder 上游(PAT 交换 / cosy 签名 / SSE 转换 / 账号收集) aibridge/tools/ └── archive_account.py # 归档当前登录态到账号池(多账号辅助工具) ``` 仓库根目录: ```text main.py # 入口(网关 / service 子命令分流) build.ps1 / build.bat # 打包脚本(-Service 目录形态,-Release 出可分发的 zip) service.ps1 / service.bat # Windows 服务安装 / 卸载 / 启停(可与 exe 同目录运行) aibridge.spec # PyInstaller 配置(onefile / onedir 双形态) scripts/winsw/ # WinSW 兜底托管方案(必要时使用) tests/test_service.py # 服务相关离线测试 ```