本文说明 zcode-docker/ 的完整工作原理、所需配置、每个组件存在的理由,以及绕过(更准确地说:复现)ZCode 桌面客户端风控链路的技术细节。

一、快速开始:跑起来需要什么配置
1.1 前置条件
| 项 | 要求 |
| Docker | 20.10+(Compose V2),Apple Silicon 或 x86 均可 |
| 出站网络 | 容器需能访问 zcode.z.ai(API)与 o.alicdn.com(阿里云 captcha SDK) |
| 一个已登录 Start Plan 的 ZCode 桌面 | 用于提取 JWT(一次性) |
1.2 必需配置(共 2 项)

① ZLIGHT_JWT —— Start Plan 的鉴权令牌(必需)
来自本机已登录 ZCode 的凭据文件:
Bash
python3 - << 'EOF'
import json, os
cfg = json.load(open(os.path.expanduser("~/.zcode/v2/config.json")))
jwt = cfg["provider"]["builtin:zai-start-plan"]["options"]["apiKey"]
print(jwt) # 一串 eyJ... 开头的 JWT
EOF
- 这就是桌面客户端访问 Start Plan 用的
zcodejwttoken(桌面把它同时放在Authorization: Bearer和x-api-key两个头里)。 - JWT 内含
user_id/sub(账号绑定),没有失效时间字段,长期有效直至重新登录轮换。 - 重新登录桌面后 JWT 会轮换,需要更新
.env并重启 client 容器。
② ZLIGHT_DEVICE_MID —— 设备身份(必需,建议保持与本机一致)
Bash
python3 -c "import json, os; print(json.load(open(os.path.expanduser('~/.zcode/v2/telemetry-state.json')))['deviceMid'])"
- 桌面客户端用它作为
X-Device-Mid计费头和 ARMS RUM 遥测的用户标识。 - 风控按
(账号, deviceMid, 出口 IP)累积历史信誉;因此从本机提取原值填入,保持设备身份连续。 - 实测(2026-09-30):
deviceMid不可随意。同一流程、同一 IP、同一新鲜 param:- 随机
deviceMid→3012拒绝(服务端按账号-设备历史绑定校验); - 且这次异常尝试会临时连坐——紧接着的真实
deviceMid请求也被3012,约 4 分钟后自行恢复。 - 结论:
deviceMid参与风控强校验,请勿用它做实验。
- 随机
可选配置
| 变量 | 默认 | 说明 |
ZLIGHT_MODEL | GLM-5.3-Flash | 兜底模型;实际以 billing 授权动态读取为准 |
ZLIGHT_API_KEY | 空(开放) | 设置后本代理要求调用方携带匹配的 x-api-key |
ORACLE_PORT / PORT | 8790 / 8791 | 两个服务的外部端口 |
1.3 生成配置并启动
Bash
cd zcode-docker
# 生成 .env(compose 自动读取)
python3 - << 'EOF'
import json, os
cfg = json.load(open(os.path.expanduser("~/.zcode/v2/config.json")))
mid = json.load(open(os.path.expanduser("~/.zcode/v2/telemetry-state.json")))["deviceMid"]
jwt = cfg['provider']['builtin:zai-start-plan']['options']['apiKey']
open('.env', 'w').write(f"ZLIGHT_JWT={jwt}\nZLIGHT_DEVICE_MID={mid}\n")
EOF
docker compose up -d --build
docker compose ps # 两个服务应显示 healthy
首次构建约 3–5 分钟(node:22 镜像 + Electron Linux 版下载);启动后 oracle 首次铸参约 4–8 秒。
1.4 验证与调用
Bash
# 授权模型列表(动态读取 billing,当前应为 GLM-5.3-Flash 一项)
curl http://127.0.0.1:8791/v1/models
# 标准非流式调用(claude-* 模型名自动映射)
curl -X POST http://127.0.0.1:8791/v1/messages \
-H 'content-type: application/json' \
-H 'anthropic-version: 2023-06-01' \
-d '{"model":"claude-sonnet-4-5","max_tokens":1024,"messages":[{"role":"user","content":"你好"}]}'
# 流式(标准 Anthropic SSE 原样透传):请求体加 "stream": true
# 接入 Claude Code:
ANTHROPIC_BASE_URL=http://127.0.0.1:8791 ANTHROPIC_API_KEY=anything claude
二、背景:为什么不能“直接调 API”
ZCode Start Plan 的模型端点:
[https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages](https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages)
走 Anthropic Messages 协议,但前面叠了三层风控,每一层都是实测确认的:
第 1 层:JWT 鉴权
Authorization: Bearer <JWT>+x-api-key: <同一 JWT>两个头并存(缺x-api-key会被部分路径拒绝)。- 失败表现:HTTP
401。
第 2 层:captcha 票据闸(3007)—— 3.14.4 起对模型请求默认关闭
- 2026-09-30 实测更新:ZCode 3.14.4 发布后,服务端
client/configs的 captcha 配置新增skip_model_request: true。captcha 机制本身保留(enabled仍true、sceneId/prefix/region原样、客户端 3.14.4 内CaptchaRequestRetry/preflight代码原样),但模型请求被服务端策略明确豁免。裸请求(无 param)不再返回3007,而是直接进入第 3 层行为评分。本项目的 client 会动态读取该标志:跳过时完全不铸参;若服务端重新要求,自动恢复铸参兜底(oracle 容器待命)。 - 每一个
/messages请求都必须携带一个未消费的阿里云 captcha param:- 请求头
X-Aliyun-Captcha-Verify-Param(280B base64:{certifyId, sceneId, isSign, securityToken}) - 请求头
X-Aliyun-Captcha-Verify-Region: cn
- 请求头
param只能由真实浏览器引擎里运行的阿里云 SDK(无痕验证)产出,Node 无法直接生成。param一次性:被任何请求消费后即失效;过期/伪造环境产出的 param 同样无效。- 失败表现:HTTP
400 {"code":3007,"msg":"captcha verify failed"}。 - 阿里侧对同一设备状态重复铸参有去重:
F008错误,需约 30–60 秒冷却。
第 3 层:行为/信誉风控(3012)
param 有效只是入场券,请求本身还要过服务端行为评分,实测确认的必要条件:
- 连接预热 —— 同一 keep-alive TLS 连接上必须先完成“应用特征”请求(
GET /api/v1/agent/configs+GET /api/v1/zcode-plan/billing/balance),冷连接第一发就 POST messages = 工具特征 →3012。 - payload 指纹 —— 请求体必须携带 ZCode 官方 system 提示词(
"You are ZCode, an interactive coding agent...",v3.14.3 共 3 个 block 约 6.7K 字符);极简 body 即使 param 正确也 →3012。 - 环境一致性 —— 请求头声明的平台应与铸参环境一致。
- 失败表现:HTTP
405 {"code":3012,"msg":"request has been blocked due to unusual activity"}。
结论:无法用纯 curl/Node“伪造”通过;必须复现客户端的三件套——浏览器铸参、连接行为、合法 payload。这就是双容器架构的由来。
三、实现原理
3.1 总体架构
Plaintext
调用方(Claude Code / Anthropic SDK / curl)
│
│ POST /v1/messages(标准 Claude 格式)
▼
┌─────────────── client 容器 :8791(纯 Node)────────────────┐
│ 1. POST oracle:8790/mint → 领取一个未消费 param │
│ 2. 预热:同连接 GET configs + GET billing(建立连接信誉) │
│ 3. 注入 ZCode system 指纹块(zcode-system.json,调用方 │
│ 自带 system 保留在后) │
│ 4. POST zcode.z.ai /v1/messages: │
│ authorization + x-api-key(同 JWT) │
│ X-Aliyun-Captcha-Verify-Param / -Region: cn │
│ 完整引擎头(UA/x-platform/x-device-mid/...) │
│ 5. 上游 Anthropic SSE ── stream? 原样透传 : 聚合为 JSON │
└────────────────────────────────────────────────────────────┘
▲ POST /mint
┌──────┴──────── oracle 容器 :8790(Electron 41 + xvfb)─────┐
│ Xvfb 虚拟显示 → Electron 无头窗口 │
│ → 加载 page.html(o.alicdn.com 的 AliyunCaptcha.js) │
│ → initAliyunCaptcha(官方场景配置) │
│ → instance.startTracelessVerification()(无痕验证) │
│ → success 回调直接产出 280B param │
│ F008 冷却:销毁页面→重载新实例→最多重试 4 次×45s │
└────────────────────────────────────────────────────────────┘
3.2 oracle 容器:param 是怎么铸出来的
真实桌面的做法(从桌面日志与渲染器 bundle 逆向得到): 每次模型请求前,渲染器里的闭源控制器跑一次“无痕验证”——阿里云 SDK 在页面里静默收集设备/行为信号,向阿里风控换取一个已签名的 certifyId,打包成 param。
oracle 容器复现了这个流程的最小必要集:
- 为什么必须 Electron 而不是纯 Node:SDK 采集 canvas/WebGL/字体/时序等浏览器环境信号,Node 环境直接抛
Captcha requires browser environment。 - 为什么可以脱离真实桌面:实测证明 param 的有效性绑定的是“浏览器环境信号的合理性”而非“桌面进程身份”——一个干净的无头 Electron(Linux 容器内、原生 UA、不伪装)铸出的 param 与桌面铸的等效。早期“新环境 param 被拒”的结论实为冷连接所致(当时误归因于设备信誉)。
- page.html 与桌面渲染器完全同构:同一 SDK 源(
o.alicdn.com)、同一场景配置(sceneId: 11xygtvd/prefix: no8xfe/region: cn)、同一元素 ID(#zcode-aliyun-captcha-element/#zcode-aliyun-captcha-button)、navigator.webdriver = false。 - main.mjs 的铸参循环:每次
/mint重载页面(销毁旧 SDK 实例、避免复用已消费的 certify 状态)→ 轮询window.__param(success 回调产出即注入)→ 60 秒超时;遇F008等 45 秒重试,最多 4 次。
3.3 client 容器:一次 /v1/messages 的完整生命周期
Plaintext
POST /v1/messages {model, max_tokens, messages, system?, stream?}
├── mapModel() GET billing 授权(5 分钟缓存)→ claude-* 映射到已授权模型
├── serialized() 并发闸(默认 1;F008 冷却下并发只会互相挤兑)
├── mintParam() oracle /mint(首次 ~4s;冷却期最长 ~2.5 分钟)
├── 预热 同一 keep-alive 连接 GET configs → GET billing(两条 200)
├── 组包 body.system = [ZCODE_SYSTEM, ...调用方system]
│ metadata.user_id = JSON{device_id, account_uuid:"", session_id}
├── POST 引擎头 + 双鉴权头 + param/region 头 → 上游
└── 响应 stream=true → SSE 字节流原样透传(含 thinking 块)
stream=false → 聚合 message_start/content_block_*/message_delta
为标准 Messages JSON
模型映射规则(实测:Start Plan 仅授权 GLM-5.3-Flash 一个模型):
/v1/models返回 billing entitlements 里model:*capability 对应的show_name(动态、5 分钟缓存,不会列出无授权的 GLM-5.3 / GLM-5-Turbo)。- 显式
glm-5.3-flash(任意大小写)→ 原样使用;其余一切名字(含claude-*)→ 首个授权模型。 - 身份字段:
metadata.user_id是 JSON 字符串{device_id, account_uuid:"", session_id},account_uuid固定空串(与桌面行为一致,源码anthropic-request-metadata.ts证实);x-session-id用裸 UUID(引擎会把内部sess_前缀剥掉再上网,runner-attribution.ts)。
3.4 为什么这样能过风控(实验依据)
以下均为逐因子替换实验的结论(当天风控策略下):
| 实验 | 结果 |
| 完整引擎头、无 param | 3007 |
| + 已消费 param | 3007 |
| + 未消费 param、冷连接、极简 body | 3012 |
| + 桌面同款会话 ID / Node24 TLS / x-api-key(单独补) | 仍 3012 |
| + 完整 95KB 真实 body、新会话 ID | 200 |
| + 真实 body 但极简 messages | 200(body envelope 是关键) |
| + 仅注入 ZCode system 提示词 | 200(指纹就是 system) |
| 容器内:oracle 铸参 + 预热 + system 注入 | 200(端到端) |
配额归属核验:调用前后 billing/balance 的 used_units 增量落在 zcode-v3-start-plan-trust-* 计划——即 Start Plan 配额本身。
四、文件清单
| 文件 | 作用 |
Dockerfile | node:22-bookworm + xvfb + Electron 运行库(libnss3/libgtk-3/字体等) |
entrypoint.sh | 按 ROLE 分流:oracle → Xvfb + electron;client → node |
compose.yaml | 双服务 + 健康检查 + 环境注入 |
oracle/page.html | 与桌面渲染器同构的铸参页(SDK + 场景配置 + 无痕验证) |
oracle/main.mjs | 铸参 HTTP 服务(POST /mint、GET /health),F008 重试 |
client/server.mjs | 标准 Claude API 代理(/v1/messages、/v1/models、/ask、/health) |
client/zcode-system.json | ZCode v3.14.3 官方 system 提示词(风控指纹,从真实请求提取) |
.env | ZLIGHT_JWT、ZLIGHT_DEVICE_MID(不入库,已 gitignore) |
五、运维
- 健康检查:
GET :8790/health(oracle 窗口状态)、GET :8791/health(client 配置);compose 每 30 秒自动探测,异常自动重启。 - 日志:
docker compose logs -f oracle|client。client 每次请求打一行param minted (280B) → done;oracle 打铸参耗时与 F008 重试。 - 扩容吞吐:param 一次性 + F008 冷却 → 单实例 ≈ 1 请求/30–60 秒。横向扩 oracle(
docker compose up -d --scale oracle=3,client 侧轮询多 oracle)可线性提升;注意所有实例共享账号配额与风控信誉,扩容不改变单请求 30–60 秒的串行延迟。
故障排查速查
| 症状 | 原因 | 处置 |
| client 启动即退(ZLIGHT_JWT 未设置) | .env 缺失/未加载 | 检查 .env 与 compose environment |
| oracle 反复重启 | Electron 缺 --no-sandbox(root) | 确认 entrypoint 参数完整 |
| 大量 401 | JWT 过期(桌面重新登录轮换) | 重新提取 JWT 更新 .env |
| 上游 3007 | param 已消费/过期 | 检查 oracle 与 client 时钟、缩短铸造到使用间隔 |
| 上游 3012 | 风控升级或预热不足 | 确认 system 注入与预热未被改动;观察是否当日策略变化 |
| 铸参超时 | F008 连续冷却 | 正常重试机制内;持续失败则重启 oracle 容器 |
六、限制与提醒
- 吞吐:单实例约 1 请求 / 30–60 秒;不适合高并发场景。
- 风控漂移:风控策略会随时间升级(本方案验证日就发生过一次),system 指纹 / 预热路径 / 双鉴权头都是与当日策略对齐的产物,未来可能需要再校准。
- 出口 IP:全部验证在同一出口 IP 完成;跨 IP 部署未验证,账号-IP 历史关联可能参与评分。
- 配额:消耗的就是 Start Plan 计划本身的 token 余额;该方案自动化的是自己账号的配额,但请控制节奏——高频工具特征可能触发账号级风控升级。
七、实测与三层分析
专项三层分析与容器适配记录:
1. 客户端二进制层面
3.14.4 的 app.asar 里验证码链路原样保留——preflight、AliyunCaptcha SDK、param 请求头、重试器一个没少。所以不是客户端删了功能。
2. 服务端配置层面(真正的变化)
GET /api/v1/client/configs 返回的 captcha 配置里新增了标志:
JSON
"captcha": {
"enabled": true,
"sceneId": "11xygtvd",
"prefix": "no8xfe",
"region": "cn",
"skip_model_request": true // ← 新增
}
- 机制本身还在(
enabled仍true),但模型请求被服务端策略明确豁免。 - 3.14.4 客户端认这个标志就跳过 preflight;旧客户端不认识所以继续铸参。
- 这是典型的服务端灰度开关——随时可以翻回来。
3. 实测确认
- 完整合法 payload(system 指纹 + 预热 + 真实 deviceMid)不带 param 裸调 →
200。 - 同时验证了行为风控层依然存在:极简 body(无 system 注入)+ 无 param →
3012。 - 所以预热和指纹注入依然必要,只是 param 这一层免了。
4. 容器自动适配方案
client 升级为动态读取该标志(2 分钟缓存):
skip_model_request: true(当前) → 完全不铸参,端到端从 30–60 秒降到 ~5 秒。- 服务端若翻回
false→ 自动恢复铸参,oracle 容器继续待命。
文章评论