Skip to content

Repository files navigation

zcode2api

把 ZCode(zcode.z.ai GLM 套餐)反代为本地 OpenAI / Anthropic 双协议 API,带管理面板。 设计文档:docs/superpowers/specs/2026-09-23-zcode2api-design.md(主体)、 docs/superpowers/specs/2026-09-25-admin-panel-design.md(管理面板)。

✅ 状态:已跑通,正在消费官方额度(2026-09-25 实测)

  • ✅ 双协议可用:/v1/messages(Anthropic)与 /v1/chat/completions(OpenAI), 含流式 SSE、工具调用、思考块(reasoning_content)。
  • ✅ 真实用上官方额度:实测请求后余额消耗;面板记账与上游计费逐单位一致 (一轮非流式 + 一轮流式 = 3,418 = 3,418)。
  • ✅ 管理面板:账号增删启停、扫描本机多实例登录(含 Coding Plan key)、套餐余量、 参数池与农场状态、用量分析(含首字延迟 / 生成速度 / 缓存命中率)、 运行参数热更新与 .env 回写、面板密码、局域网/免密访问开关。
  • ✅ 验证码降级为保险丝(2026-09-29):上游 3.14.4 起默认关闭模型请求验证码校验, 15 个账号无参直发实测全部通过验证码层(4 个完整 200 + 业务层 1005/3009,零 3007); 农场断供不再导致 503。详见 3007 与验证码的降级。
  • ✅ 515 个单元/集成测试 + 端到端冒烟全通过(npm test 一次跑完两者)。

关键突破(曾经 3012 的根因):上游网关对请求做内容检查——system 字段里 必须含 ZCode 身份块,否则直接返回 3012 "method not allowed"(对外文案为 "request has been blocked due to unusual activity")。 官方客户端的请求体是 ~8.5KB(含身份块 + 环境信息 + <system-reminder># currentDate), 而此前的实现只发"裸请求"(~100 字节)。详见 3012 的根因与修复。

快速开始

npm install
cp .env.example .env   # 至少配置 API_KEY
npm start
  • API:http://127.0.0.1:28630/v1(OpenAI /v1/chat/completions、Anthropic /v1/messages)
  • 管理面板:http://127.0.0.1:28630/ —— 本机直接开,不需要密码(见 怎么进后台)
  • farm 页:http://127.0.0.1:28631/farm
    • 2026-09-29 起农场降级为保险丝:上游 3.14.4 默认不再校验模型请求验证码(见 3007 与验证码的降级),农场断供不再导致请求失败—— 网关自动无参直发。本节说的"断供"指参数产出断供,不是 API 不可用;只有上游恢复校验 (请求回 3007)时,农场才重新成为硬依赖(届时网关的换参重试会自动接上)。
    • 默认后台模式(FARM_AUTO_BROWSER=1 + FARM_HEADLESS=0):playwright 启动有头 浏览器自动产参数。桌面会出现一个 Chrome 窗口——关掉即参数断供(API 仍可用, 详见下面"为什么默认有头")。
    • 不想看到那个窗口:设 FARM_MINIMIZED=1,农场窗口启动即最小化到任务栏, 屏幕上看不到它。它仍是有头浏览器,不是回到无头——实测最小化状态下产出正常 (total 稳定增长、fails=0)、端到端请求可用。 代价要清楚:① 它仍可被误关(任务栏右键关闭 = 断供);② 它依赖"SDK 不检查窗口可见性" 这一尚未被机制性证实的假设,哪天失效会静默退化成 F011 + 产出归零。 所以怀疑农场有问题时,先设回 FARM_MINIMIZED=0 用可见窗口复现再判断。
    • 手动模式(FARM_AUTO_BROWSER=0):用你自己的 Chrome 打开上述地址并保持标签页。 产出成功的标志是页面顶部出现绿色 param 产出并推送成功,且"池内 param"不为 -。
    • 为什么默认有头:无头模式(FARM_HEADLESS=1)已不可用。实测无头下会稳定吃 F011(success:true 但 verifyResult:false),参数产出恒为 0,整个 API 对外只会回 captcha param pool is empty;同一台机器改成有头后 F011 立即消失、参数稳定产出。 启动器的 UA 覆盖(见 src/captcha/browser.js)不足以绕过——SDK 已能从 UA 与 navigator.webdriver 之外的指纹特征(Canvas/WebGL/GPU 等)识别无头环境。 排查时已逐一排除:整页重载、清掉同 IP 的重复实例、更换出口 IP(14.146.x → 14.31.x) 均无效;既然换 IP 都不影响,就不是网络层的问题,别再往代理/换网络方向折腾。
    • 农场按可用参数数补货:服务端会丢弃超过 PARAM_USABLE_MS(默认 40s)的参数, 农场只在"可用参数不足 2 个"时产新的。不是定时滴灌——定时产出(曾经每 20s 一个, 约 180 次/小时)会在空转时也持续做验证,实测跑到约 25 次后被阿里云风控判 F001 (verifyResult:false),此后一个参数都产不出来;页面现在还会在连续失败时自动整页重载 以复位 SDK 状态(注意:该自愈对 F011 无效,那是环境问题不是页面状态问题)。
    • 农场页会把自身状态(最近一条日志、失败次数、退避时长)上报给服务端, 面板「captcha 参数池与农场」里直接显示——卡住时会标红,不必去猜。 判断有没有真恢复,看的是产出计数(面板里的 total/pushed),不是 pool 或失败次数清零: 页面自愈重载会把失败计数清零并显示"农场页就绪",但产出仍可能是 0。

改端口后:手动模式需用新地址重开 farm 页(旧标签页会持续 Failed to fetch,参数推不进池); 后台模式无需处理(服务启动时自动指向新端口)。

开机自启

已注册计划任务 zcode2api-Gateway:登录时延迟 20s 启动,无控制台窗口。

触发器   登录时(当前用户)
操作     wscript.exe "D:\code\Ai\zcode2api\launch-hidden.vbs"
工作目录  D:\code\Ai\zcode2api
设置     允许按需启动 / 电池下也启动 / 3 次重试(间隔 1min)/ 多实例 IgnoreNew

启动链路与两个脚本的职责:

计划任务 → launch-hidden.vbs  隐藏控制台窗口(只隐藏这个,不是农场窗口)
         → start-gateway.bat  写 logs\gateway-startup.log,然后 node src/server.js
         → 服务自己再拉起有头 Chrome(农场;可见,或按 FARM_MINIMIZED=1 最小化到任务栏)
  • 日志:logs\gateway-startup.log,每次启动截断重写(只看当前这次启动;要留历史就在重启前自己拷走)。 内容里中文显示为乱码是编码问题,不影响判断——看 [farm] 那两行是否出现即可确认农场起来了。
  • start-gateway.bat 必须保持纯 ASCII:cmd.exe 按 OEM 代码页(本机 GBK)读 .bat, 写 UTF-8 中文注释会被解析成乱码命令(实测报 'he' 不是内部或外部命令)并中断启动。 中文说明放 README,别写进 .bat。
  • 改了 .env 后:计划任务只在登录时读一次,改完要手动重启服务才生效: Stop-ScheduledTask -TaskName zcode2api-Gateway 不会杀掉已启动的 node, 需结束监听 28630 的进程再 Start-ScheduledTask。
  • 重复触发是安全的:MultipleInstances=IgnoreNew,服务已在跑时再触发不会起第二个实例 (已验证:PID 不变、日志无第二次启动记录)。
  • 运行期崩溃不会被自动拉起:上面的 RestartCount=3 只在启动瞬时失败时重试; node 跑起来之后再崩,计划任务感知不到,要等下次登录。要崩溃自愈,注册看门任务(见下)。
  • 端到端验证:Start-ScheduledTask -TaskName zcode2api-Gateway,然后确认 http://127.0.0.1:28630/health 返回 200 且 logs\gateway-startup.log 里有 [farm] 行。

崩溃看门(可选)

scripts/watchdog.mjs 每 5 分钟探活一次 http://<HOST>:<PORT>/health,发现服务死了就 按端口反查 PID 杀残留进程,然后 Start-ScheduledTask zcode2api-Gateway。

node scripts/watchdog.mjs --register      # 注册成每 5 分钟跑一次的计划任务 zcode2api-Watchdog
node scripts/watchdog.mjs --unregister    # 移除
node scripts/watchdog.mjs                 # 手动跑一次探活(用于验证)
  • 探活日志:logs\watchdog.log(追加,不滚动;一年也只几百 KB,定期手清即可)。
  • HOST 从 .env 读;HOST=0.0.0.0 时看门仍然探 127.0.0.1(本机回环总是可达)。
  • 不会误杀隔壁项目:按"监听端口"反查 PID,不按 node src/server.js 命令行匹配 (README 主章节对 trae2api 同命令行的警告在这里同样适用)。
  • 端到端验证:--register 之后 Stop-Process 杀掉 node,5 分钟内 http://127.0.0.1:28630/health 应重新 200。

排查这台机器上的进程时注意:另一个项目 trae2api 的进程命令行也是 node src/server.js (它和自己的 start-gateway.bat 一起在 D:\code\Ai\trae2api,占 28620/28621)。 按命令行匹配会误伤它——要定位本服务请按监听端口 28630 的 PID 找。

怎么进后台

本机(运行服务的这台电脑):浏览器打开 http://127.0.0.1:28630/,不需要密码。 启动日志里也会把入口地址打出来,面板「设置」页顶部同样列着(含局域网地址)。

从手机或其他电脑:默认进不去,因为有两道门——服务只监听 127.0.0.1,且非本机要求密码。 两种开法,面板「设置 → 从其他设备访问」里一键设置:

想要 怎么做 代价
免密直接进 ① 监听地址改 0.0.0.0(需重启)② 打开「完全免密」 同一局域网内任何人都能管理账号与 API Key
要密码 ① 监听地址改 0.0.0.0(需重启)② 在「面板访问密码」里设一个 每次要输密码
不开放 什么都不做 只有那台电脑能进

「完全免密」默认关,开启后面板上会持续显示红色警示(不是一次性提示——这个状态决定 谁能管凭据,得随时看得见)。开启/关闭即时生效,不用重启。

注意:从非本机访问时把「完全免密」关掉,那台设备会立刻失去访问权限;如果那时还没设 密码,就只能回到运行服务的那台电脑上重新打开。面板会在这种情况下先弹确认框说清后果。

启动日志也会提示当前门槛:

[zcode2api] 管理面板 → http://127.0.0.1:28630/   http://192.168.0.107:28630/
[panel] ⚠️ 已开启完全免密(PANEL_DISABLE_AUTH=1):任何能连到这个端口的人都能管理账号与 API Key。

入口清单会滤掉虚拟网卡(VMware / VirtualBox / WSL / Docker 等)。实测本机有 3 个 非回环地址,其中两个是 VMware 宿主-only 网卡,手机连不上;把它们混在列表里会让人 照着第一个去试,然后得出"从手机打不开"的错误结论。

管理面板

三个页签:网关与运维 / 用量分析 / 设置。深色主题,5 秒自动刷新,锁定状态下不轮询。

网关与运维

  • KPI 卡片:可用账号池、套餐剩余、captcha 参数池(含最新参数年龄)、累计产出参数。
  • 参数池与农场:池内数量、最新参数年龄、累计产出/消费、农场浏览器模式、农场页自报状态。
  • 账号表:账号(凭据只显示掩码)、类型、状态徽标(可用 / 已停用 / 冷却 / 需重登 / 无资源包 / 密钥无效 / 风控次数 / 最近错误 状态码/业务码)、套餐余量进度条(按模型, 低余量即黄/红细条 + 行内百分比,不再另加"余量偏低/已用尽"徽标——浮标不美观且与 进度条信息重复)、请求数、输入/缓存/输出 tokens、最近使用时间。

    徽标与卡片文案都按实际状态实时统计,不写死原因——曾经写"不可用(停用/冷却/需重登)", 而当时 6 个不可用账号的真实原因是"5 个无资源包 + 1 个密钥无效",一个都对不上。 账号行的请求数与 token 取自 usage/usage.jsonl(与「用量分析」同源), 不读账号自带的计数器——那个计数器存在账号文件里,任何整条覆盖式的写入都会让它归零, 于是面板会出现"账号行 请求 0"与"最近请求 共 5 条"自相矛盾(实测踩到)。

  • 工具栏:BigModel / Z.AI OAuth 登录、新增 API Key 账号、扫描本机 ZCode 登录 (含多开实例,见 添加账号)、刷新套餐额度、全部启用 / 停用。
  • 模型清单:只有 2 个模型(glm-5.3、glm-5.3-flash)。像 claude-*、GLM_5P3 这类是 别名——只是"客户端写这些名字也能用",不是模型,单独一张表列,不计入模型数。 (早先把两者混排在同一张"模型清单"里,2 个模型看起来像 3 个。)目录由服务端 modelCatalog() 给出,并有测试断言"别名声称的映射 == 实际 mapToZcodePlan 的结果", 面板不会与服务端各说各话。
  • 最近请求(含耗时、首字延迟、生成速度、缓存量)。

用量分析:今日↔累计切换;KPI(token、平均速度、首字延迟 P50、成功率、缓存命中率、流式占比); 账号透视(含每账号的模型细分);模型用量与性能表。数据来自 usage/usage.jsonl(重启不丢)。

设置:入口地址、从其他设备访问(监听地址 / 完全免密开关)、面板密码、对外 API Key、 运行参数(节流间隔 / 3012 冷却 / 参数 TTL / 参数可用时效 / 池上限 / 重试上限)、运行信息。 改动即时生效并写回 .env(重启后仍在);只有监听地址需重启。

面板访问密码

  • 本机访问默认免密(PANEL_LOCAL_BYPASS=1)。要在其他机器上打开,见上一节两种开法。
  • 密码以 scrypt 哈希存在 panel.json(只存盐与哈希,不存明文);改密会吊销全部已登录会话。
  • 未设置任何密码且未开完全免密时,非本机请求一律 401——不提供默认口令(弱口令比没有口令更危险: 它让"没配密码"看起来像"配了密码")。
  • 设 PANEL_LOCAL_BYPASS=0 可让本机也必须带密码(面板挂在反向代理后面时应当这么设)。

面板不显示凭据原文

jwt / apiKey / accessToken / refreshToken 从不回给前端,只回 hasJwt / hasApiKey 与掩码(如 eyJhbG…hoY4)。凭据一旦进过浏览器、日志或截图就等于多了一处泄露面。

验证

npm test            # 单测(515 项)+ 端到端冒烟,一次跑完
npm run test:unit   # 只跑单测(快,不碰网络)
npm run e2e         # 只跑端到端冒烟:真实 server + 真实 Response 走通双协议四条路径 + 管理面 API

npm test 把单测与 e2e 串在一起是因为:e2e 用本地假上游起真实服务,覆盖「OpenAI/Anthropic × 流式/非流式」四条路径, 并核对 captcha 参数送达、SSE 透传、usage 记账与管理面接口。它存在的原因是:单元测试把上游桩成 普通对象,会掩盖只有真实 Response 才暴露的缺陷(例如 Response.body 是一次性流)。它也是 "健康判据改动"的最后一道防线——ab1f86b 加 neverProvisioned 时单测全绿但 e2e 因夹具缺 planCache.balances 而 9 项红,只靠 vitest 是发现不了的;故把它挂进 npm test,不再让它 独立于提交流程之外。

真实上游实测(2026-09-25):面板记录的 total_tokens 与上游 used_units 增量完全一致 (一轮非流式 + 一轮流式 = 3,418 = 3,418),这是"面板数字不是自说自话"的最终裁判。

添加账号

  1. 面板 →「+ BigModel 登录」或「+ Z.AI 登录」→ 浏览器完成授权 → 自动入库。
  2. 面板 →「+ API Key 账号」→ 填备注名与 key → 入库(付费 GLM Coding Plan 通道,不需要 captcha)。
  3. 面板 →「⤓ 扫描本机 ZCode 登录」→ 不用重新授权、不用手工编辑文件,一次收齐本机所有凭据。 对已存在的账号重复导入只刷新凭据,请求数/token 统计、风控次数、停用状态都会保留 (早先会整条覆盖,导致面板上的历史统计被清零)。

扫描本机登录:一次收齐所有实例

它扫本机所有 ZCode 实例,而不只是默认那个:

实例 凭据文件
默认实例 ~/.zcode/v2/credentials.json
多开管理器实例 N %APPDATA%\zcode-multi\<N>\data\.zcode\v2\credentials.json

每个实例收两类东西:

  • OAuth 账号(zcodejwttoken)→ 走免费通道(3.14.4 起上游默认不校验 captcha,参数可选)
  • Coding Plan API Key(客户端里绑定的 account-provider:coding-plan:...:api-key) → 走 open.bigmodel.cn 标准通道,不需要 captcha

实测确认:多开实例的密钥派生不变(启动器只改数据目录,不改派生用的 home), 所以同一个密钥能解开所有实例的凭据。

收完会逐条实测,结果直接写进账号状态:

  • OAuth 账号 → 查余额接口(即时显示套餐余量)
  • API Key → 发一个 1 token 的最小请求,判 可用 / 无资源包(1113) / 密钥无效(401)

为什么要实测:从客户端扫进来的 Coding Plan key 里有死 key(实测一台机器 6 个里 5 个不可用:4 个 1113 无可用资源包、1 个 401 身份验证失败)。不实测的话它们会以 "可用"的样子留在选号池里,直到某个真实请求撞上去才暴露——那是一次白白失败的请求。 401/1113 都发生在计费之前(实测余额不变),所以这次探测不花额度。

扫描结果按实例分别报告:哪个实例导入了什么、哪个实例没登录、哪个实例凭据解不开。

401 对两类账号含义不同:OAuth 是"凭据失效,重新登录能救回来"(需重登); API Key 是"密钥被吊销或填错,没有重新登录这回事"(密钥无效)。混为一谈会让一个 失效的 key 在面板上显示成"需重登"——那是个点不动的死路,用户会找不到该做什么。

手工放置(等价于第 2 条,便于脚本化):把 accounts/<id>.json 放入目录, {"id":"bigmodel:manual-1","provider":"bigmodel","type":"apikey","apiKey":"<key>", ...}(其余字段同 oauth 账号默认值),重启生效。

两种账号通道

账号类型 上游 需 captcha 适用
oauth zcode.z.ai/api/v1/zcode-plan/anthropic 默认不校验(3.14.4 起):有参数则带,farm 是保险丝 Start Plan / Global Build 免费额度
apikey open.bigmodel.cn/api/anthropic 否 付费 GLM Coding Plan key

客户端接入

  • Claude Code:ANTHROPIC_BASE_URL=http://127.0.0.1:28630 + ANTHROPIC_AUTH_TOKEN=<API_KEY>
  • OpenAI SDK:base_url=http://127.0.0.1:28630/v1,api_key=<API_KEY>
  • 模型:glm-5.3、glm-5.3-flash(claude-* 自动映射 GLM-5.3-Flash)

用量账的口径(对账时看这里)

usage/usage.jsonl 每行一次请求。字段口径按 Anthropic 语义,不是简单的"输入+输出":

  • prompt_tokens = 未命中缓存的输入(对应上游 input_tokens)
  • cache_read_tokens = 命中缓存复用的输入;cache_creation_tokens = 本次写入缓存的输入
  • completion_tokens = 输出
  • total_tokens = 上面四项之和

最后一条容易踩坑:上游的 input_tokens 不含缓存部分。实测一次流式请求 input=40 / cache_read=1664 / output=8,上游计费 +1712;若按 input+output 记总账,面板会显示 48(少 35 倍)。agent 类客户端(Claude Code、dsh)每轮重发一大段系统提示,命中缓存是常态, 所以这个偏差是系统性的——面板的「缓存命中率」也正是为此而设。

另一处实测坑:流式响应里 message_start 的 input_tokens 是 0,真正的值在最后那帧 message_delta 里。只读 message_start 会让每个流式请求都记成 0 输入 token, 并让发给 OpenAI 客户端的 prompt_tokens 恒为 0(对外可见的错误数据)。

3012 的根因与修复

上游网关会对请求做内容检查:如果 system 字段里看不到 ZCode 身份块, 直接返回 3012 "method not allowed"(对外文案 request has been blocked due to unusual activity)。

这解释了此前所有排查都失败的原因——发出去的是"裸请求":

官方客户端 / 现在的实现 修复前
请求体大小 ~8.3–8.6 KB ~100 字节
system 字段 3 块官方身份块(带 cache_control: ephemeral) 无 / 仅用户 system
首轮 user 消息 前挂 <system-reminder>…# currentDate…</system-reminder> 纯用户文本
模型名 小写 glm-5.3 大写 GLM-5.3
user-agent ZCode/3.14.4 ai-sdk/anthropic/3.0.81 …ai-sdk/provider-utils/4.0.27 runtime/node.js/24
x-zcode-app-version 3.14.4 缺失
accept-encoding gzip 缺失
x-title Z Code@cli Z Code@electron
x-query-id / x-session-id 不带 带了

修复:新增 src/upstream/system-prompt.js(按官方形态组装三块身份 + 上下文前缀), src/upstream/zcode-plan.js 在发送前调用 shapeZcodePlanBody() 补齐请求体, src/upstream/headers.js 按抓包复刻请求头。文案资产在 src/upstream/zcode-system.json。

实测结果(2026-09-25)

/v1/messages        → 200 {"content":[{"type":"text","text":"SYSTEM-FIX-OK"}],"usage":{"input_tokens":1707}}
/v1/chat/completions → 200 {"choices":[{"message":{"content":"OPENAI-OK"}}]}
流式 SSE            → 200 完整事件流(message_start → content_block_delta → …)
GLM-5.3-Flash       → 200(含 thinking 块)
余额复查            → GLM-5.3 used=129333|Flash used=1730   ← 真实消耗官方额度

排查方法学(值得记下的教训)

定位过程中走了很长弯路,以下方法最终有效,也澄清了几个误区:

  1. 以余额变化为最终判据 —— 只有 used_units 增长才证明请求被接受。 3007/3012 都发生在计费之前,故余额不变。
  2. 区分中间环节与最终结果 —— captcha 验证通过 ≠ 模型请求成功; 连接建立 ≠ 响应返回。(曾把 captcha SDK 回调误读为"请求成功"。)
  3. 对比一个已知可用的第三方实现是最高效的定位手段—— 同样的账号/机器它能通,就能确定问题在自身实现而非环境。
  4. 已排除的因素(供参考):captcha 参数格式与新鲜度、certifyId 重复、 账号状态、额度、出口 IP、浏览器指纹(真实 Chrome 同)、TLS 指纹 (Electron 原生网络栈同)、x-client-sig/x-client-pow 签名头(上游不校验)。 这些都不是原因——真正的原因是请求体形态。

1005 的根因与自愈

上游对"账号级业务拒绝"回 HTTP 200 + 业务码 1005,网关原本按"客户端错误"直接透传 502 (不换号、不冷却、不置标志——noPackage 只认 1113)。实测(2026-09-28/29)它有两种成因:

成因 实测特征 自愈方式
耗尽型:免费日包在运行中途烧穿 一旦开始就每个请求都 1005,直到日额度重置。09-29 三个 500万/日的号在 08:49 / 09:38 / 09:46 陆续烧穿,其中一个连吃 15 次 刷新套餐缓存 → 调度器按 remaining=0 排除 → 换号
瞬时型:额度充足的上游短暂抽风 密集失败几分钟然后自行恢复。09-28 02:47–02:59 某号连吃 17 次(余量 8850万/1亿),之后 844 次全成功 无需处理;不能误杀这种号

旧设计为什么漏:调度器的 modelQuotaExhausted 判据本身是对的(读 planCache.balances 的 remaining),但 planCache 唯一的常规写入方是面板「刷新套餐额度」(手动)——额度在 运行中途耗尽时缓存跟不上,账号在调度器眼里始终"健康",轮询每转到一次就死一个请求, 且耗尽型的信号(持续 1005)没有任何机制去消费它。

修复(2026-09-29):

  • 新增 src/plan-cache.js:1005 事件驱动的套餐缓存刷新器。两道去重闸——同一账号的 并发刷新共享一次上游查询(in-flight 合并);30s 最小间隔窗口防打爆。刷新失败只记日志, 绝不抛错。
  • 网关收到 1005(仅 oauth 号)时触发刷新,随后复检 pool.healthy():
    • 复检不可用(缓存确认耗尽)→ 换号重试,当前请求成功,客户端无感;
    • 复检仍可用(瞬时型/刷新失败)→ 保持原有 502 透传,不循环、不放大、不误杀。

耗尽型的号在午夜日额度重置、缓存刷新后自动回归轮询,全程无需人工干预。

1005 熔断:余额接口说谎时的兜底(2026-09-30 凌晨事故)

自愈的复检依赖"刷新后的余额缓存"——但实测两套上游系统口径会打架:368d44de 的余额接口 坚称 GLM-5.3 remaining=300 万,模型端点却每请求必回 1005。当晚 21 次 1005 全部来自它, 复检永远"健康",自愈无法闭环;客户端 retryable 重试 + 网关换号又在 44 秒内连打 5 个号, 诱发上游 3012 行为风控全池级联(15 号冷却 30 分钟,strikes 全员 1~2 分)。

修复(AccountPool.benched1005):不信任余额接口的实测熔断——同一(账号, 模型) 连续 C1005_TRIP(默认 3)次 1005,不看余额直接对该模型雪藏 C1005_BENCH_MIN (默认 30)分钟;成功一次即清零计数。要点:

  • 按模型雪藏:该号 flash 正常就继续服务 flash(当晚 flash 0 错误);
  • 计数只存进程内存:重启即重新给机会,不往账号文件/面板加新状态;
  • healthy() 统一裁决,pick 自动绕开,网关 1005 复检自然走"换号"路径—— 网关侧唯一改动是 markError/markSuccess 透传 model。

3007 与验证码的降级(2026-09-29)

官方客户端 3.14.4 更新日志:"为了进一步优化免费套餐的使用体验,关闭模型请求验证码校验。"

逆向两版桌面客户端(v3.14.3 → v3.14.4,app.asar 全量 diff + 逐文件哈希)确认了机制:

  • 整套阿里云验证码机器(AliyunCaptcha.js 加载、certifyId 的 F008 去重、调度器的 captcha-retry、验证码网络诊断)两版都在,唯一变化是渲染进程的 captcha 准备函数 新增一个提前返回分支:服务端远程配置 configs.captcha 里出现 skip_model_request: true (或 enabled: false)时,客户端返回空 headers 直发——不解题、不带 x-aliyun-captcha-verify-param。3.14.3 里拿不到有效配置会直接抛错。 即:校验开关在服务端,客户端只是学会了服从。
  • 除该分支外无其他暗改:两版 host 进程 x-* 请求头集合完全一致,端点与签名未变。

实测(2026-09-29):按项目自身的 header/body 构造器复刻官方形态但去掉两个验证码头, 对本机全部 15 个 OAuth 账号各发一个 16 token 的最小请求:4 个完整 200 生成响应、 5 个 1005(额度尽)、6 个 3009(并发占用),零个 3007——无参请求全部通过验证码层, 直接到达业务层。

网关的对应改动(farm 从热依赖降级为保险丝):

  • src/upstream/headers.js:验证码头改为按需携带——有参数才带,无参数绝不带。 绝不能写 'x-…': param 了事:fetch 会把 undefined 序列化成字符串 "undefined" 污染上游风控,而单测的 toBeUndefined() 断言掩盖这一点(属性存在但值为 undefined 时照样通过)。 默认 x-zcode-app-version 同步升到 3.14.4,与官方客户端对齐。
  • src/gateway.js:paramPool.take() 失败(农场断供)不再走 __paramError → 503, 而是无参直发。上游若恢复校验(服务端把配置翻回去),请求会回 3007,走既有的 "换参重试、不换号、不冷却"路径重新取参——农场自动回归热路径,不需要改代码。
  • 回归测试:tests/captcha-skip.test.mjs(无参时验证码键不存在而非值为 undefined、 有参照常带双头、版本号 3.14.4、农场断供时请求照常 200)。

运维影响:农场浏览器停掉(FARM_AUTO_BROWSER=0 或关掉 Chrome 窗口)不影响可用性; 留着它则请求继续带参,与旧版行为一致。两种模式都正确,按喜好选。

TTFB:流式被网关整包缓冲的根因与修复(2026-09-29)

症状:面板记账里所有流式请求的 ttft 恒等于 elapsed(实测最大 216s)——客户端在整个 生成期间一个字节都收不到,响应在结束时一次性到达。"反应有点慢"的真凶。

根因:gateway.complete() 为识别"HTTP 200 包业务码"(3001/3007/3012/1005…)把响应 clone() 后 await probe.text() 读到底——tee 流的另一支被拉完,complete() 直到上游 生成完毕才返回。直连探针证明上游本身流式正常(SSE 首块随响应头 2~8s 即到,数百块连续), 缓冲全部发生在网关。这个缺陷对 e2e 不可见:测试只断言"最终内容正确",不测字节到达时刻—— 与"172 个测试全绿也掩盖了响应体一次性问题"是同一类盲区。

修复:SSE 按 content-type: text/event-stream 识别(上游实测带此头)直接放行,跳过读码; 业务码判定只对非流式 JSON 做——错误体本来就是完整 JSON,行为无损失(SSE 里即使混有业务码, 旧逻辑的 JSON.parse 也解析不了)。回归测试 tests/stream-ttft.test.mjs 用永不结束的 上游流做判定:网关若再整包缓冲,complete() 永不返回,race 超时即失败。

修复前后对照(同款"写 200 词"流式探针,经网关): TTFB 19.04s == 总 19.04s → TTFB 7.17s / 总 25.72s。 注意:总时长不变——生成速度 30 tok/s 是上游给的,大输出该几分钟还是几分钟; 变的是首字从"等于总时长"降到 28s,客户端从此能边生成边渲染。

风险与已知限制

  • 上游对模型端点有风控:3012 会触发账号冷却(默认 30min,24h 内第 3 次起 24h,5 次停用)。 请勿压测——每次失败都在消耗账号行为分。
  • system 字段形态与上游策略强耦合:若官方客户端升级后改变身份块结构, 需要同步更新 src/upstream/zcode-system.json 与组装逻辑,否则会重新出现 3012。
  • farm 依赖阿里云验证码 SDK 配置(SceneId 11xygtvd / prefix no8xfe)。该配置由服务端下发, 可用 GET https://zcode.z.ai/api/v1/client/configs(带 JWT)读取,便于核对是否变更; 其中的 configs.captcha.skip_model_request 就是 3.14.4 的校验开关(见 3007 与验证码的降级)。
  • 上游随时可恢复验证码校验:那是一次远程配置翻转,立即对全体客户端生效。届时未跑农场的 部署会开始吃 3007(换参重试耗尽后透传)——把农场开起来即可,网关的换参重试会自动接上, 无需改代码。
  • farm 的浏览器 UA 覆盖已不足以骗过 SDK:UA 必须覆盖且版本要真实(playwright 在 headless 下 默认 UA 含 HeadlessChrome/<ver>,SDK 见之即返回 F001),src/captcha/browser.js 会自动探测 本机 Chrome 版本并构造桌面 UA。但单靠 UA 已经救不回无头模式——SDK 现在还会看 Canvas/WebGL/GPU 等指纹,无头下稳定返回 F011。结论:农场必须跑有头模式(默认值已改为 FARM_HEADLESS=0)。 注:SDK 不检查 navigator.webdriver(实测该标志始终为 true,不影响结果)。
  • 手动模式:FARM_AUTO_BROWSER=0 时不启动自动浏览器,改用你自己的真实 Chrome 打开 farm 页(http://127.0.0.1:28631/farm)。
  • 纯 CLI 无法直接使用官方套餐(官方 account:* provider 不在 headless 注册表内)—— 这正是本项目存在的意义:让官方额度可被程序调用。
  • 测试超时:vitest.config.mjs 把 testTimeout 设为 30s(并发/落盘类测试在慢机器上波动较大)。
  • 仅供个人学习研究,遵守上游服务条款。

About

把 ZCode(zcode.z.ai GLM 套餐)反代为本地 OpenAI / Anthropic 双协议 API(含 OAuth 登录、账号池、captcha 农场、Web 看板)

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages