mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
6913 字
18 分钟
我给workbuddy账号做了个经纪人
2026-10-06
2026-10-08

WorkBuddy2API

强烈安利这个网关,顺便聊聊我给它做的二开#

WorkBuddy2API Panel 全记录:它是什么 · 好在哪 · 我加了什么 · 踩了哪些坑。

深夜桌面上的数字管家与它的猫

▲ 它大概就是这幅样子:你睡了,它还在值班。(插图由 AI 生成,下同)

开场:一场持续三天的”家政事故”#

事情的起因是我的桌面越来越像一个手工作坊。

左边终端跑 Cursor,中间跑 Claude Code,右边跑 Codex CLI。浏览器上挂着两个标签,一个看签到、一个查余额;桌上贴着便利贴,写着五个账号和我约定的签到时刻。我当时的状态是:人在写代码,脑子在排班。

三个客户端还各说各的方言:一个要 OpenAI 格式、一个要 Anthropic 格式、还有一个要 Responses 格式。而我的账号们缩在角落里,只会说”官方客户端”那一门土话。

翻译、排班、签到、防限流、善后……干到第三天我明白了:这不是搞开发,这是开家政公司。

后来我遇到了 WorkBuddy2API Panel。用了一阵子之后,我甚至撸起袖子给它做了二开——现在它的版本号上顶着我的标识 1.12.5-zhima。

这篇文章分两半:前半安利(它是什么、能帮你干什么),后半实录(我二开加了什么、又踩了哪些坑)。赶时间的朋友可以直接跳到后半看笑话。


上半场:安利#

它是个什么东西#

一句话:自托管的 OpenAI 兼容反向代理网关——把 CodeBuddy 账号包装成统一的 /v1/chat/completions 服务。你本来只有账号,它能让你得到一个 API。

它没有官方背景(官方也不提供 OpenAI 形态的开放 API),走的是 OAuth 设备授权拿凭证,然后由网关负责 token 自动刷新、账号池调度和流量治理。它也不是石头里蹦出来的:基线是 linguo2625469/workbuddy2api-panel,再往上追溯是 Sliverkiss/workbuddy2api——Web 面板、浏览器内 OAuth 登录、在线配置热更新这些家当都来自那两代。我接手这轮 fork 时,先把上游两批共 34 个提交同步了进来(净化增强、出站请求头族、并发修复、签到幂等、粘性判活……),才开始动刀。

整体长这样:

WorkBuddy2API 整体架构

▲ 一张图说清:四种方言进同一扇门,账号池在后面排班。

它能帮你干什么(安利正文)#

1. 客户端零改造,四种方言都认#

不管你手里的客户端说哪种协议——OpenAI、Anthropic、Responses、Gemini——把 base_url 指向它就行,代码一行不用改:

Cursor / 任意 OpenAI SDK   →  /v1/chat/completions
Claude Code(ANTHROPIC_BASE_URL)→  /v1/messages(连 count_tokens 都实现了)
Codex CLI(wire_api=responses)  →  /v1/responses
Gemini CLI / Gemini SDK          →  /v1beta/models/{model}:generateContent

再配一个免鉴权的 /healthz 和一个”报课表”的 /status,齐了。

(这部分的多协议入口其实是我二开加的,它的出身故事在后面。)

2. 账号调度:它给账号排班,比 HR 还专业#

几个账号一起用,最怕”旱的旱死,涝的涝死”。它自己会挑,而且挑法很有道理:

weight = 1 + 积分占比 × 10  +  闲置补偿  +  快过期占比 × 8
            有钱的多接客       冷板凳先上场     临期酸奶先喝

积分多的多干活(反正积分放着也不生利息)、坐冷板凳的每小时加分(防止被永久遗忘)、快过期的积分(默认 7 天内到期)加权优先消耗。Top-5 候选里加权随机,不往死里薅”首富”;100ms 内刚被选中的账号先歇口气,防止并发请求一窝蜂全压上去——这个 100ms 我愿称之为”给账号留的一口喘息”。

顺带一提:早年公式里还有个”成功率 × 3”因子,后来被我请出去了——理由是一桩冤案,记录在下半场第 ⑦ 条。

公式之外还有一圈保险丝:失败 3 次熔断(30 分钟起步、翻倍退避、6 小时封顶)、连败 5 次降权 10 分钟、单账号在途请求最多 3 个(海外域账号 2 个)、会话粘性 30 分钟(同一段对话尽量落在同一个号上)。池子全冷的时候,它会捞”最早解冻”的号顶班,并在日志里留一条 WARN——不是摆烂,是留证据。

还有两个我会心一笑的设计:“快过期积分优先消耗”像先吃掉冰箱里快过期的酸奶;“成本探索窗口”每隔 30 分钟派一个真实请求搭车去试试池子里的”未知账号”贵不贵,成功就毕业——我管这叫”让新员工跟着跑一单试试水”。

3. 一个不知道累的夜班团队#

这是我推荐它的最大理由。它内置六类定时任务,默认作息如下——注意右边的对照:

09:00  签到 + 猫猫旅行      ← 你没醒
10:00  活跃上报             ← 你在开会
21:00  签到第二班 + 猫猫旅行  ← 你在加班
22:00  token 保活           ← 你在刷手机
23:00  夜猫子补对话          ← 你准备睡了
01:00  成长任务队列自动推进   ← 你已经睡了

(另外每 5 分钟刷新一次余额,算是夜班巡更。)

“猫猫旅行”是真的:账号里养了只虚拟猫,它每天准点带猫出门旅行、到站领奖。日志里会一本正经地打印 ★ 兑换 7 天档(+xxc +xxe 卡×1 抽奖×2)、🎲 第 1 抽 {…}、🎊 新手礼包 +100c——我第一次在 docker logs 里看到烟花 emoji 的时候,觉得这项目有点可爱。

“连登管家”负责签到末尾的收尾:兑换已解锁的连登档位、把抽奖次数抽完、用补签卡补昨天,一条龙。你唯一的职责是别关服务器。

深夜出发旅行的橘猫

▲ 你的猫正在上夜班的路上。

4. 官方成长任务:18 个,它替你干 17 个#

官方”成长计划”的 18 个任务,17 个可以在面板上一键纯 API 完成:推进进度、等异步计分(实测对话类要等 5–8 秒才计上分,一次性回读会漏领奖)、自动领奖。全新账号一键全做完,大约 +1950 credits、+78 能量。

剩下那个为什么不行?需要真实捐款,服务端领奖时校验捐赠回执。

——AI 可以替你加班,但不能替你掏钱。这是技术与现实的边界。

(顺带一提:刚过去的开学季活动也是这个画风——5 个任务自动做掉 4 个,唯一做不了的那个叫”学生认证”,它真的要看学生证。)

5. 一个说得清楚账的面板#

内嵌的 Web 面板(同一个端口,/panel/)自带明暗主题,一共 9 个视图。我最常用的几个:

调用记录(每次请求发生了什么,失败点开有完整现场)、用量(输入输出之外,还分开统计缓存命中、缓存写入、思考 token 和实扣积分)、积分构成(到期分布标红标黄,像看食品保质期)、Key 管理(下面细说)、任务中心(一键全做)、模型与档位、运行日志、配置。

其中”模型与档位”要单独夸一句:上游 /v3/config 声称的输出上限普遍虚高——实测 16 个模型里有 8 个被静默钳制,最狠的一个声称 1M,实际只有 32K。面板会直接标红 32K ⚠ 钳制 12×。这功能没别的用,就是让你写 max_tokens 的时候别做梦。

6. 部署:一条命令,或者一个 exe#

docker run -d --name workbuddy2api \
  -p 127.0.0.1:7863:7863 -e TZ=Asia/Shanghai \
  -v ./auths:/app/auths -v ./data:/app/data -v ./config.json:/app/config.json \
  ghcr.io/moonkey0510/workbuddy_api:latest

Windows 用户还有单文件版:前端资源用 go:embed 打进了二进制,一个 exe 拷过去就能跑。默认只绑本机回环地址(安全默认),要远程访问必须自己改端口映射并设 api_key。

拉镜像困难的朋友,仓库里还备了一份 Dockerfile.cn(国内镜像源 + apk 源 + 代理开关)。CI 会打五个平台的二进制和 checksums.txt,但有个硬性规矩:tag 必须等于剥掉 -zhima 后缀的版本号——防止我手滑打错版本,也防止版本号和二进制对不上。

7. 一些”不需要你知道”的体贴#

/healthz 返回里带 service: workbuddy2api 身份证,防止同端口残留服务”假成功”;前端资源带 no-store,升级后不用手动强刷;请求日志只记账号 uid 前 8 位,不含任何密钥明文;面板 CSP 直接写 default-src 'none'。备份也简单,拷三样就够:auths/(凭证)、data/state.json(账号状态)、data/api_keys.json(托管密钥)。

适合谁:手里有多个账号、桌上有一群客户端、且不想再手工签到的人。 前提:用自己授权的账号、跑在自己的机器上。它是非官方项目,平台服务条款和账号风险,自己心里要有数。


下半场:二开实录#

缘起:从”够用”到”我来改”#

安利完了,说点私心话:上面那些功能里有一整块,是因为”它当时还不够好”才出现的。

用着用着,毛病就具体了:

  • 我用 Claude Code,它说 Anthropic 方言;偶尔用 Codex,它说 Responses 方言。而网关当时只有一个 OpenAI 入口——我只能在外面再套一层转换器。那我为什么不把转换器搬进来?
  • 模型级限流一撞,整个账号陪葬,还经常”解冻即再撞”;
  • 请求失败后我只能 docker logs | grep,但我想知道”刚才那次请求到底发生了什么”;
  • api_key 只有一把,给同事发 key 等于把管理面板的钥匙一起递过去;
  • 用量面板里看不见缓存命中、思考 token 和积分——一份账单里最重要的三列缺席。

于是,从 1.12.0-zhima 开始,我把它独立出来自己维护。-zhima 是这块地的定制标识,CI 比对 tag 时会先把它剥掉——防止我手滑打错版本号。

下面是我动过的四刀,外加一次面板重装修。

第一刀:多协议入口——让三个方言的客人进同一家店#

三种方言,一个入口

▲ 三种方言进门,统一翻成 OpenAI 帧,再走既有管线。

三个客户端,三种协议。以前的方案是”网关讲 OpenAI,我在外面自己翻译”;现在把翻译层搬进了网关——internal/convert 里三个文件(anthropic.go / responses.go / gemini.go)做双向转换,internal/server 开出四个新入口。

做的时候才发现,请求转换只是开胃菜,流式事件编码才是正餐。

把 Anthropic 的请求转成 OpenAI 格式,逻辑还算直观;但流式响应是另一回事——每个协议的事件序列都不一样:Anthropic 是 message_start → content_block_delta → message_stop,工具参数还要以 input_json_delta 分片;Responses 有一串 response.created / output_item.delta / completed 和递增的 sequence_number;Gemini 流式默认吐 JSON 数组,带 ?alt=sse 才吐 SSE。我要做的是读懂上游的 OpenAI 帧,再”重建”成对方协议的形状实时吐出去——客户端中断时还得同步掐掉上游读取,不然就漏了。

顺手还做了一件小事:不同协议的凭证位置不一样(Anthropic 用 x-api-key,Gemini 用 x-goog-api-key 或 ?key=,OpenAI 用 Bearer),鉴权层加了个统一入口,四种位置全兼容;校验统一走 SHA-256 摘要 + 常量时间比较。

结果:现在我的三个客户端指向同一个端口,各说各话,谁也不尴尬。既有的 /v1/chat/completions 行为零改动——毕竟我自己也在用它。

第二刀:模型级限流的”精确制导”#

这一刀是凌晨修的,起因是一个让我血压升高的观察。

上游有个 429 code=6004,含义是”这个账号的这个模型用量超限了,将在 XX 重置”。而旧逻辑把它当账号级限流处理——整个账号进冷却,时长还被 2 小时上限截断。于是就有了这个名场面:

旧逻辑(截断到 2 小时封顶):
01:33 撞限 ──────────────► 03:33 解冻放行 ──► 立刻又撞 ──► 又进冷却……
                            ↑ 上游明明说 03:46 才恢复,白等还挨打

新逻辑(采信上游墙钟):
01:33 撞限 ─────────────────────────────────► 03:46 放行,一次成功
        「将在 03:46 重置」——听话,不猜。

6004 模型级限流的精确制导

▲ 冷却键从”账号”缩到”账号 × 模型”,剩下的交给上游墙钟。

改动集中在 internal/pool/cooldown.go,核心三条原则:

  1. 只锁”账号 × 模型”这一个组合——同一账号的其它模型、池里的其它账号,一概不受影响;
  2. 冷却截止直接采信上游给的墙钟——不自作聪明地截断(真想封顶可以配 cooldown.model_rate_max,默认不封顶);
  3. 判定前置——就算上游文案缺失或变体,也只做模型级有界退避,账号级状态一律不动。

还有两个体验层的小心思:某个模型全池都没号可用时,不回笼统的 503,而是 429 加一句人话——几个号受限、最早何时恢复、建议先换模型;面板的账号池里,被限流的号会挂”模型限流 ×N”标签,逐行列明是哪个模型、什么时候恢复。账号级状态该显示”可用”就显示”可用”,不制造恐慌。

第三刀:调用记录——给每次请求留一份现场#

一次请求的一生

▲ 一次请求的一生:选号、转发、流式回吐,最后都落成一行记录。

需求来自我自己的排查姿势:翻 docker logs | grep 太原始了。我要一个能回答”刚才那次请求到底发生了什么”的地方。

于是有了 internal/calls:纯内存环形缓冲,保留最近 300 条。每次上游尝试记一行:

14:32:07  deepseek-v4-flash  200  TTFB 0.8s  输入 1,024  输出 512  积分 0.4
14:35:19  glm-5.2            429  6004 · 模型级限流 · 预计 03:46 恢复

它有我很满意的”克制”:不落盘、重启即清、不含凭证与请求内容;失败行额外存错误分类、HTTP 状态和上游错误原文,原文截断 600 字节防体积失控(上游急了眼会回一整页 HTML 错误页,见过一次就懂了)。

前端的戏份也不少:字段过滤(按账号 / 模型 / 状态码 / 失败分类……)、成功失败快捷筛选、分页(每页 20/30/50/100 可调,页数超 10 中间折叠成省略号)、失败行点开就是完整现场。还处理了一个细节:详情展开状态跟着 seq 走——翻页或自动刷新后,你展开的那一行还是展开的,不会”手一抖就收起来”。

第四刀:Key 管理——房本和门钥匙#

房本与门钥匙:两把钥匙,两种权限

▲ 主密钥是房本,托管密钥是门钥匙,权限在入口处就分好了。

api_key 一把钥匙开所有门的问题困扰我很久:它既是面板的登录凭证,又是 API 的调用凭证。给同事发 key,等于连管理面一起送出去。

所以加了”Key 管理”,核心是权限解耦:主密钥(config.json 里的 api_key)继续负责面板登录和全部 API,是”房本”;托管密钥只能授权 /v1/* 和 /status,进不了管理面,是”门钥匙”。哪把钥匙泄露了,单独停用或删除那一把就行,不用惊动整个家。

实现上没偷懒:data/api_keys.json 以 0600 权限原子落盘,校验走 SHA-256 摘要加常量时间比较(而且遍历全部密钥不提前返回——不让响应时间出卖”命中的是第几把”),“最近使用”防抖 30 秒落盘,上限 50 枚。前端也顺手治了个老毛病——新建密钥必须”弹窗先填名称再生成”,避免误点直接产出一枚”未命名”密钥。

顺手的:面板重装修#

四个功能之外,面板前端基本被翻新过一轮(加起来 8 处改造)。挑三个有代表性的说:

用量视图补齐三列:缓存命中、思考 token、实际扣除积分。流式(SSE 末帧)和非流式(本地聚合)两条路径都得接——只接一条,账单就会骗你。

趋势图自绘:没引第三方图表库,纯 SVG 手画——面积加折线、渐变填充、跨天分隔线、悬停十字线和浮动卡片。y 轴刻度按”好看的数”取整(1/2/5 乘以 10 的 n 次方),灵感来自初中数学课本。自绘的理由很朴素:少一个依赖,将来就少更新一个版本。

积分构成视图重做:账号对比卡片、积分到期分布柱状图、逐包明细(按需加载,≤3 天到期标红、≤7 天标黄)。做完之后我发现它最大的价值是——看着临期积分被优先消耗掉,有种清冰箱的快感。

最花时间的从来不是功能,是这些坑#

以下内容按”当时的自信程度”排序,自信越高,摔得越响。

① 主按钮一悬停就”变白”。 我给按钮写了漂亮的渐变背景,鼠标一放上去,变成白底白字。查了半天:CSS 特异性打架,通用的 button:hover 把 button.primary 的渐变覆盖成了浅色。

button:hover       特异性 (0,2,1)   ← 赢家(把渐变换成了浅灰)
button.primary     特异性 (0,1,1)   ← 输家(白底白字,社死现场)

修法:给 button.primary:hover 显式写回渐变。CSS 优先级,前端界的黑暗森林。

CSS 黑暗森林里的火把

▲ 在黑暗森林里,谁特异性高谁说了算。

② 弹窗底部长了两个”尖角”。 圆角做得挺精致,就是底部两个角总露出方形背景色,像没剪干净的指甲。原因:.dlg 少了 overflow: hidden,footer 的底色方角从圆角外探了出来。

③ 92px 的幽灵。 积分到期分布的宽轨道,怎么调都被钳成 92px。最后发现是全局 .bar(账号池积分条)带着 max-width: 92px,泄漏进了新组件的同行类。更扎心的是——同一个坑我踩了第二次,上一版 .us-wrapbar 就栽过。所以这次修复记录写得特别详细,用于将来嘲笑自己。

④ 一次”文档打脸现场”。 README 明明白白写着”日志不含密钥明文”,结果首次自动生成配置时,api_key 被原样打印进了启动日志——docker logs 和终端历史里都能翻到。修法:只提示”已写入配置文件”。承诺和代码,隔着一整个实现细节。

⑤ “成功 0 个,跳过 12 个”之谜。 批量导入账号整批失败,界面只说跳过了 N 个,服务端日志里什么都没有——跳过原因只回传了前端,而前端只是 console.warn 一下,等于对着空气说话。修复分两头:服务端日志记原因,前端把前 3 条直接显示出来。追根究底,最初整批失败的根因也极其朴实:凭证目录不存在时写入全部失败——登录路径早有兜底,导入路径忘了抄作业。

⑥ 浏览器说”没有新功能”。 升级二进制后打开面板,新功能一个不见。原因:前端资源用 go:embed 打进二进制,没有 ETag 也没有 Last-Modified,浏览器理直气壮地用着旧缓存。修法:给前端响应加 Cache-Control: no-store。新功能没做出来,和做出来了看不见,是两种不同的绝望。

⑦ 账号集体”垮掉”事件。 有一阵子,池里的账号会一个个无缘无故被降权,直到没号可用。排查出三处根因:

  • 客户端(比如某个代理工具)中途断开时,旧逻辑把”传输层失败”无条件记成账号的失败——客户先走了,锅却让账号背了;
  • 上游的 11148(请求内容级错误)同样在喂连败计数——一条坏会话能把整池账号拖下水;
  • 面板上的”解冻”按钮对被降权的账号可见,但点击后不清除降权和连败记录——按钮摆在那儿,账号却还在门外。

三处一起修掉的那个晚上,我盯着面板看了很久,感觉像给一群被冤枉的员工平了反。后来还做了两件事:一是保活的判死标准改成”session 失效连续 3 次才禁用”,因为侦察发现——13 个被禁用的账号,刷新凭证其实全部成功,它们全是历史误判的受害者;二是把权重公式里的”成功率 × 3”请了出去,这位选手的问题是 errTotal 终身累计、只增不减,一个账号早年翻过车就会被永久压权、永世不得翻身——用终身案底做绩效,本身就是冤案制造机。

⑧ “失败行查户口”事件。 调用记录上线后,冒出一批”分类未知、状态 —“的空失败行,真正的失败反而被淹没。查下来:流式请求的结局过去只看”有没有拿到 usage”——上游 200 后断流、客户端中途断开、上游漏发 usage,三种结局全落成同一张”查无此人”的脸。修法是采集流内证据再按优先级归因:流内 error 帧按上游 code 分类、客户端断开记 client_abort(且不算失败)、空流记 upstream_parse、断流记 upstream_truncated。给每一行失败查清了户口,日志才配叫日志。

⑨ Codex 破案记。 某段时间 Codex 会话频繁被上游一句”工具调用和工具结果对不上”顶死。kimi、deepseek 都命中,我判断这是形态问题不是模型问题,于是做了一整套加固:调用 id 去重、结果按调用顺序落位、残缺参数的调用两侧剔除、没有对应形态的 item 两侧一起丢弃;再遇到 11148,先剥离工具历史自愈重试一次——同一账号、不罚号;还把上游那句笼统的”对不上”打印成可核对的数字(calls / results / dup_ids / bad_args / unpaired_calls / orphan_results)。上游负责说”对不上”,我负责破案。

橘猫侦探的线索板

▲ 案发现场:一条坏会话掀翻整个账号池。

⑩ “有输入、没有返回”。 还有一种更阴间的形态:客户端显示输入有值、输出为空,且看不到任何报错。三处成因都是同一类”体贴”造成的:上游流被掐断(没有 [DONE])时,旧代码好心地补了一个 [DONE],客户端以为是正常结束;转换层在收到 error 帧之后,还在补成功语义的收尾事件;handler 判断”正常收尾”的判据太宽松。三处一起纠正后,这类请求现在会明确告诉你:上游断流了。善意要是不精确,就是事故。

⑪ 一个字符串搞垮整池。 某天开始,偶发的整池 503。查到最后是一个非常不起眼的细节:客户端把 stop 参数写成字符串(而不是数组)传进来,网关原样透传,上游直接 11101 拒接;这个错误被归类成”换个号重试”——于是坏请求把池子里的号挨个试了一遍,全池阵亡。修法朴素得让人心碎:出站前把 stop 的字符串形态归一成数组。一个字符串,换来我半个晚上的排查。

⑫ 402 冷却的”白冷一天”。 余额耗尽的账号要硬冷却到”下一个 04:00”,旧逻辑里有个 nextDay4AM 的边界 bug:凌晨 00:00–04:00 之间撞上 402,它会算成明天的 04:00——账号白白多冷一天。修法:凌晨时段落当天的 04:00。这类 bug 的可怕之处在于——它不报错、不崩溃,只是让你默默地少一个账号。

尾巴:除了上面这些”案子”,还有两个不算案子的小尾巴。一是脱敏开关的热更新在面板保存配置时会直接写字段,和并发的出站读取构成数据竞争——顺手加了互斥保护;二是请求体无上限带来的内存放大面——恢复了 server.max_body_mb(默认 128MB,超限 413)。这些修复写进 README 之后,那一章的长度快追上功能章了。

收个尾#

整洁的桌面与排队打卡的小队

▲ 现在的桌面:一个浏览器标签,一队按时打卡的账号。

现在,这个网关跑着我四个新功能和一打修复,面板上基本看不到”上古遗迹”了;我的桌面则只剩一个浏览器标签页(面板)和几个指向同一个端口的终端。账号们在网关里排班、签到、养猫、准点打卡。我偶尔打开面板,看一眼它们有没有摸鱼——说出来有点丢人,最自律的是它们。

二开一个活着的项目,和从零写一个,是两种体验。从零写,你面对的是自己的想象力;二开,你面对的是真实用户的反馈、真实上游的脾气,和一堆”看起来没问题但一用就露馅”的细节。一年下来我最大的收获其实是那些坑——每一条修复背后,都有一个想雇个 CSS 专家和上游破案组的深夜。

当然,得把话说清楚:这是个非官方项目,我用的都是自己的账号、跑在自己的机器上;平台服务条款和账号风险,用的人自己心里要有数,我的二开也仅限于让它在私有环境里更顺手、更透明、更好排查。

如果你也在多账号 + 多客户端的泥潭里,或者也在给某个开源项目做二开——欢迎来仓库里翻翻那些修复记录,那里有我最真诚的部分。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

我给workbuddy账号做了个经纪人
http://www.zhima778.cloud/posts/003/
作者
知不知马
发布于
2026-10-06
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录