把 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 走通双协议四条路径 + 管理面 APInpm 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),这是"面板数字不是自说自话"的最终裁判。
- 面板 →「+ BigModel 登录」或「+ Z.AI 登录」→ 浏览器完成授权 → 自动入库。
- 面板 →「+ API Key 账号」→ 填备注名与 key → 入库(付费 GLM Coding Plan 通道,不需要 captcha)。
- 面板 →「⤓ 扫描本机 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(对外可见的错误数据)。
上游网关会对请求做内容检查:如果 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。
/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 ← 真实消耗官方额度
定位过程中走了很长弯路,以下方法最终有效,也澄清了几个误区:
- 以余额变化为最终判据 —— 只有
used_units增长才证明请求被接受。3007/3012都发生在计费之前,故余额不变。 - 区分中间环节与最终结果 —— captcha 验证通过 ≠ 模型请求成功; 连接建立 ≠ 响应返回。(曾把 captcha SDK 回调误读为"请求成功"。)
- 对比一个已知可用的第三方实现是最高效的定位手段—— 同样的账号/机器它能通,就能确定问题在自身实现而非环境。
- 已排除的因素(供参考):captcha 参数格式与新鲜度、certifyId 重复、
账号状态、额度、出口 IP、浏览器指纹(真实 Chrome 同)、TLS 指纹
(Electron 原生网络栈同)、
x-client-sig/x-client-pow签名头(上游不校验)。 这些都不是原因——真正的原因是请求体形态。
上游对"账号级业务拒绝"回 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 透传,不循环、不放大、不误杀。
耗尽型的号在午夜日额度重置、缓存刷新后自动回归轮询,全程无需人工干预。
自愈的复检依赖"刷新后的余额缓存"——但实测两套上游系统口径会打架: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。
官方客户端 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 窗口)不影响可用性;
留着它则请求继续带参,与旧版行为一致。两种模式都正确,按喜好选。
症状:面板记账里所有流式请求的 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/ prefixno8xfe)。该配置由服务端下发, 可用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(并发/落盘类测试在慢机器上波动较大)。 - 仅供个人学习研究,遵守上游服务条款。