Issue 报告:billion-context ACP kernel 改写 body 导致 ZCode 405/3012
仓库:https://github.com/ranxianglei/billion-context
版本:billion-context 0.1.96
日期:2026-09-09
上游:https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages(Coding Plan 周末额度)
一、问题描述
billion-context 作为 MITM 代理时,对 Anthropic 协议请求执行 ACP kernel(anthropicToCore → processTurn → coreToAnthropic)重建 messages body 后,ZCode 上游返回 405 反作弊拦截:
HTTP 405
{"code":3012,"msg":"request has been blocked due to unusual activity."}
即使 compress.injectTool=false / compress.injectNudge=false,只要 kernel 跑过(processTurn 被调用),messages 仍会被 round-trip 重建(JSON 字段顺序 / 空白 / prompt_cache_key 变化),触发上游风控。
二、405 现场
时间:2026-09-06T04:35:48Z
路径:POST https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages
模型:GLM-5.3-Flash
响应:405 / 3012(135ms 内返回,request-id 202609060435486b04dbd894d282b1e9a9)
billion-context 日志(bili.log)关键证据:
L9291: tunnel cloudauth-device-dualstack.cn-shanghai.aliyuncs.com:443 established (blind TCP, not decrypted)
L9292: tunnel no8xfe-verify.captcha-open.aliyuncs.com:443 established (blind TCP, not decrypted)
L9297: [eea280ab-...] processTurn: 7 msgs, renderTags=text-only, 6 text tagged, 0 tool tagged
L9299: forward POST → https://<private-host>/api/v1/zcode-plan/anthropic/v1/messages
L9300: ← upstream 405 request-id=202609060435486b04dbd894d282b1e9a9: {"code":3012,...}
关键观察:
- bili 对 messages body 跑了
processTurn(L9297,7 条消息被 kernel 处理后重建)
- 同一秒前后,bili 对 Aliyun captcha 域名做了 blind TCP 隧道(未解密,非 MITM)——这是 ZCode 客户端自身的反作弊心跳
- 同一 session 中,
/api/v1/agent/configs(GET)和 /api/v1/event/report(POST)都成功返回,只有被 bili 改写 body 的 messages 接口被 405
- 请求已到达 zcode.z.ai 服务端(135ms 有响应),不是网络连接问题
三、排查过程
3.1 确认 405 来源是上游而非代理本身
通过 bili.log 确认:
- 请求确实被 bili 转发到
https://<private-host>/api/v1/zcode-plan/anthropic/v1/messages(L9299)
- 135ms 后收到上游 405(L9300)
- 同一 session 中其他路径正常(
/agent/configs、/event/report、/billing/balance)
- → 排除网络/代理配置问题,确认是上游反作弊拦截
3.2 确认 bili kernel 确实改写了 body
通过 bili.log L9297 确认:
processTurn: 7 msgs 说明 kernel 被调用并处理了 messages
- 即使
injectTool=false / injectNudge=false,anthropicToCore → processTurn → coreToAnthropic 的 round-trip 仍会改变 JSON 字段顺序、空白,并删除 prompt_cache_key
- 对比:未走 bili 的 ZCode 直连请求不会触发 405
3.3 分析 ZCode V4 签名协议,排除签名篡改假说
从 ZCode 客户端二进制(Electron app.asar)中逆向提取了 V4 签名协议完整实现(详见第四节)。关键发现:
- V4 Ed25519 签名 payload 不包含 body hash(签名内容是
{apiKeyId}.{ts}.{clientVersion}.{sessionId}.{nonce})
- 因此服务端无法通过 V4 签名本身检测 body 是否被篡改
- 405/3012 的触发条件更可能是 body fingerprint(H1)、TLS/JA3 指纹(H2)、或两者叠加(H3)
3.4 确认 Aliyun captcha 与 405 无关
- Aliyun captcha header(
X-Aliyun-Captcha-Verify-Param)只挂在 /api/v1/zcode-plan/billing/claim 等计费接口
- messages 接口不需要 captcha token
- bili 对 Aliyun captcha 域名的 blind tunnel 是客户端自身心跳,与 405 无关
四、ZCode V4 签名协议(详细)
以下是从 ZCode 客户端(Electron app.asar,版本 3.11.2)逆向提取的完整签名流程。
4.1 密钥体系
ZCode 的 API Key 格式为 {apiKeyId}.{apiKeySecret}。签名体系由两部分密钥组成,均从 apiKeySecret 通过 HKDF-SHA256 派生:
KDF_SALT = "WD_CLIENT_SIGN_KDF_SALT"
# HMAC key(用于 handshake 签名)
HKDF(ikm=apiKeySecret, salt=KDF_SALT, info="getSignKey_hmac", len=32) → HMAC-SHA256 key
# Ed25519 私钥加密 key(用于解密 handshake 返回的 privateCipher)
HKDF(ikm=apiKeySecret, salt=KDF_SALT, info="ed25519_priv", len=32) → AES-256-GCM key
4.2 握手(Handshake)
端点:POST {origin}/api/paas/c1f3a7e2/v2/client
认证:Authorization: {apiKeyFull}
请求体:
{
"apiKey": "{apiKeyId}.{apiKeySecret}",
"ts": "{unix_millis}",
"nonce": "{32_hex_random}",
"sig": "{base64_hmac}"
}
handshake 签名:
key = HKDF(apiKeySecret, salt=KDF_SALT, info="getSignKey_hmac")
msg = f"get_sign_key.{apiKeyId}.{ts}.{nonce}".encode()
sig = base64(hmac_sha256(key, msg))
响应体(HTTP 200):
{
"code": 200,
"data": {
"privateCipher": "{base64_aes256gcm_encrypted_pkcs8_ed25519_private_key}"
}
}
privateCipher 解密:
key = HKDF(apiKeySecret, salt=KDF_SALT, info="ed25519_priv")
raw = base64_decode(privateCipher) # raw = IV(12B) + ciphertext + tag(16B)
plaintext = AES256GCM_decrypt(key, iv=raw[:12], ciphertext=raw[12:-16], aad=apiKeyId)
ed25519_private_key = DER_decode_PKCS8(base64_decode(plaintext))
handshake 成功后,客户端持有从服务端派发的 Ed25519 私钥,用于后续业务请求签名。每个客户端会话生成一个 sessionId(UUID v4)。
4.3 业务请求签名
需要签名的路径:
/api/v1/zcode-plan/anthropic/v1/messages
/api/v1/zcode-plan/chat/completions
/api/v1/off-peak/anthropic/v1/messages
签名 payload(Ed25519):
{apiKeyId}.{ts}.{clientVersion}.{sessionId}.{nonce}
ts:Unix 毫秒时间戳
clientVersion:"3.11.2"(从 asar 偏移 235178431 处提取的常量 pn)
sessionId:handshake 时生成的 UUID v4
nonce:每次请求新生成的 32 hex random
签名结果:base64(ed25519_sign(privateKey, payload))
4.4 Proof of Work(PoW)
每次业务请求还需附带 PoW header。算法:
seed = sha256(f"{apiKeyId}.{appId}.{sessionId}.{ts}").hexdigest()[:32]
nonce = random_hex(6) # 6 bytes = 12 hex chars
# 找 counter 使 sha256(f"{seed}.{nonce}{counter:08x}") 的整数表示 < 2^(256-8)
# 即前 8 bits 为 0
for counter in range(0x100000000):
candidate = f"{nonce}{counter:08x}"
digest = int(sha256(f"{seed}.{candidate}").digest(), "big")
if digest < 1 << 248: # threshold
return candidate # 20 hex chars = 12 hex nonce + 8 hex counter
参数:appId = "zcode",POW_BITS = 8,最终 PoW 值为 20 hex 字符。
4.5 签名 Header 集合
业务请求需附带以下自定义 header:
| Header |
值 |
说明 |
X-Client-Ts |
{ts} |
Unix 毫秒 |
X-Client-Version |
3.11.2 |
客户端版本常量 |
X-Client-Sig |
{base64_ed25519_sig} |
签名 payload = {apiKeyId}.{ts}.{version}.{sessionId}.{nonce} |
X-Session-Id |
{uuid_v4} |
handshake 时生成 |
X-Client-Nonce |
{32_hex_random} |
每次请求重新生成 |
X-App-Id |
zcode |
固定值 |
X-Client-Pow |
{20_hex} |
PoW 结果 |
4.6 关键限制
签名 payload 不包含 body hash。这意味着:
- V4 签名无法保护 request body 的完整性
- 服务端无法通过签名本身检测 body 是否被篡改
- 405/3012 反作弊的触发条件是 body 结构/内容特征(或 TLS 指纹),而非签名校验失败
五、根因
billion-context 的 prepareAnthropic() 对所有 Anthropic 协议请求执行:
anthropicToCore() — 解析 Anthropic body 为内部格式
core.processTurn() — ACP kernel 处理(压缩/absorb/nudge)
coreToAnthropic() — 重建 Anthropic messages
delete rebuilt.prompt_cache_key — 无条件删除
JSON.stringify(rebuilt) — 整包序列化替换原始 body
即使 compress.injectTool=false / injectNudge=false 且没有活跃压缩 block,步骤 1→3 的 round-trip 仍会:
- 改变 JSON 字段顺序(JavaScript 对象属性遍历顺序)
- 改变空白/换行
- 删除
prompt_cache_key(如果客户端发送了)
- 可能添加或移除空数组/空对象
ZCode 服务端检测到 messages body 的结构特征与 ZCode 客户端预期不符,触发 405/3012 反作弊。
复现步骤:
- ZCode 客户端代理指向 billion-context(HTTP proxy
http://127.0.0.1:8787,信任 bili CA)
- ZCode 使用 Coding Plan(
builtin:bigmodel-coding-plan)发任何对话
- bili 日志出现
processTurn: N msgs → 后续 forward POST → zcode-plan/anthropic/v1/messages → upstream 405 / 3012
- 同一 session 中,不经 bili 的路径(
/agent/configs GET、/event/report POST)正常返回 200
六、修复建议
6.1 建议 A(推荐):内置 per-host 旁路配置
在 billion-context.json 中增加 per-host / per-origin 的 ACP kernel 旁路配置:
{
"compress": {
"bypassHosts": ["zcode.z.ai", "open.bigmodel.cn"]
}
}
在 kernel 入口条件(dist/index.js:58985)中检查:
if (!opts.passthrough && hopMarker === void 0 && protocol && parsed && typeof parsed === "object"
&& !opts.compress.bypassHosts?.includes(new URL(upstreamOrigin).hostname)) {
这样用户通过 config 声明哪些 upstream 不需要压缩,无需修改 dist 代码。
6.2 建议 B:Anthropic 协议 passthrough 优化
当 compress.injectTool=false && compress.injectNudge=false && !activeBlocks(session.state) 时,跳过 processTurn 并原样转发 body:
if (!opts.compress.injectTool && !opts.compress.injectNudge
&& session.state.blocks.filter(b => b.active).length === 0) {
return { body: JSON.stringify(parsed), ... }; // 不走 processTurn
}
这不会影响压缩功能(因为没有活跃 block),但避免了不必要的 body 重建。
6.3 建议 C:blind tunnel 模式
提供 isMitmHost() 可配置排除名单,让用户声明哪些域名不做 MITM(只做 blind TCP tunnel,不解密 TLS、不改 body):
BILI_BLIND_TUNNEL_HOSTS=zcode.z.ai,open.bigmodel.cn
这样 TLS 指纹也是客户端原样的,405 概率最低。
七、总结
根因:billion-context 的 ACP kernel 对 Anthropic 协议请求无条件执行 anthropicToCore → processTurn → coreToAnthropic 重建 messages body,即使 injectTool=false / injectNudge=false。ZCode 服务端检测到 body 结构与客户端预期不符,返回 405/3012。
签名协议限制:V4 Ed25519 签名 payload 不包含 body hash,服务端无法通过签名检测 body 篡改。405 的触发条件是 body 结构 fingerprint(或 TLS 指纹)。
复现:ZCode → bili MITM → Coding Plan messages 接口 → 405/3012。同一 session 中不经 bili 改写的路径正常。
建议:在官方版本中增加 per-host bypassHosts 配置(建议 A),使包更新后配置持续生效。
Issue 报告:billion-context ACP kernel 改写 body 导致 ZCode 405/3012
一、问题描述
billion-context 作为 MITM 代理时,对 Anthropic 协议请求执行 ACP kernel(
anthropicToCore → processTurn → coreToAnthropic)重建 messages body 后,ZCode 上游返回 405 反作弊拦截:即使
compress.injectTool=false/compress.injectNudge=false,只要 kernel 跑过(processTurn被调用),messages 仍会被 round-trip 重建(JSON 字段顺序 / 空白 /prompt_cache_key变化),触发上游风控。二、405 现场
时间:2026-09-06T04:35:48Z
路径:
POST https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages模型:
GLM-5.3-Flash响应:
405 / 3012(135ms 内返回,request-id202609060435486b04dbd894d282b1e9a9)billion-context 日志(
bili.log)关键证据:关键观察:
processTurn(L9297,7 条消息被 kernel 处理后重建)/api/v1/agent/configs(GET)和/api/v1/event/report(POST)都成功返回,只有被 bili 改写 body 的 messages 接口被 405三、排查过程
3.1 确认 405 来源是上游而非代理本身
通过
bili.log确认:https://<private-host>/api/v1/zcode-plan/anthropic/v1/messages(L9299)/agent/configs、/event/report、/billing/balance)3.2 确认 bili kernel 确实改写了 body
通过
bili.logL9297 确认:processTurn: 7 msgs说明 kernel 被调用并处理了 messagesinjectTool=false / injectNudge=false,anthropicToCore → processTurn → coreToAnthropic的 round-trip 仍会改变 JSON 字段顺序、空白,并删除prompt_cache_key3.3 分析 ZCode V4 签名协议,排除签名篡改假说
从 ZCode 客户端二进制(Electron
app.asar)中逆向提取了 V4 签名协议完整实现(详见第四节)。关键发现:{apiKeyId}.{ts}.{clientVersion}.{sessionId}.{nonce})3.4 确认 Aliyun captcha 与 405 无关
X-Aliyun-Captcha-Verify-Param)只挂在/api/v1/zcode-plan/billing/claim等计费接口四、ZCode V4 签名协议(详细)
以下是从 ZCode 客户端(Electron
app.asar,版本 3.11.2)逆向提取的完整签名流程。4.1 密钥体系
ZCode 的 API Key 格式为
{apiKeyId}.{apiKeySecret}。签名体系由两部分密钥组成,均从apiKeySecret通过 HKDF-SHA256 派生:4.2 握手(Handshake)
端点:
POST {origin}/api/paas/c1f3a7e2/v2/client认证:
Authorization: {apiKeyFull}请求体:
{ "apiKey": "{apiKeyId}.{apiKeySecret}", "ts": "{unix_millis}", "nonce": "{32_hex_random}", "sig": "{base64_hmac}" }handshake 签名:
响应体(HTTP 200):
{ "code": 200, "data": { "privateCipher": "{base64_aes256gcm_encrypted_pkcs8_ed25519_private_key}" } }privateCipher 解密:
handshake 成功后,客户端持有从服务端派发的 Ed25519 私钥,用于后续业务请求签名。每个客户端会话生成一个
sessionId(UUID v4)。4.3 业务请求签名
需要签名的路径:
签名 payload(Ed25519):
ts:Unix 毫秒时间戳clientVersion:"3.11.2"(从 asar 偏移 235178431 处提取的常量pn)sessionId:handshake 时生成的 UUID v4nonce:每次请求新生成的 32 hex random签名结果:
base64(ed25519_sign(privateKey, payload))4.4 Proof of Work(PoW)
每次业务请求还需附带 PoW header。算法:
参数:
appId = "zcode",POW_BITS = 8,最终 PoW 值为 20 hex 字符。4.5 签名 Header 集合
业务请求需附带以下自定义 header:
X-Client-Ts{ts}X-Client-Version3.11.2X-Client-Sig{base64_ed25519_sig}{apiKeyId}.{ts}.{version}.{sessionId}.{nonce}X-Session-Id{uuid_v4}X-Client-Nonce{32_hex_random}X-App-IdzcodeX-Client-Pow{20_hex}4.6 关键限制
签名 payload 不包含 body hash。这意味着:
五、根因
billion-context 的
prepareAnthropic()对所有 Anthropic 协议请求执行:anthropicToCore()— 解析 Anthropic body 为内部格式core.processTurn()— ACP kernel 处理(压缩/absorb/nudge)coreToAnthropic()— 重建 Anthropic messagesdelete rebuilt.prompt_cache_key— 无条件删除JSON.stringify(rebuilt)— 整包序列化替换原始 body即使
compress.injectTool=false / injectNudge=false且没有活跃压缩 block,步骤 1→3 的 round-trip 仍会:prompt_cache_key(如果客户端发送了)ZCode 服务端检测到 messages body 的结构特征与 ZCode 客户端预期不符,触发 405/3012 反作弊。
复现步骤:
http://127.0.0.1:8787,信任 bili CA)builtin:bigmodel-coding-plan)发任何对话processTurn: N msgs→ 后续forward POST → zcode-plan/anthropic/v1/messages→upstream 405 / 3012/agent/configsGET、/event/reportPOST)正常返回 200六、修复建议
6.1 建议 A(推荐):内置 per-host 旁路配置
在
billion-context.json中增加 per-host / per-origin 的 ACP kernel 旁路配置:{ "compress": { "bypassHosts": ["zcode.z.ai", "open.bigmodel.cn"] } }在 kernel 入口条件(
dist/index.js:58985)中检查:这样用户通过 config 声明哪些 upstream 不需要压缩,无需修改 dist 代码。
6.2 建议 B:Anthropic 协议 passthrough 优化
当
compress.injectTool=false && compress.injectNudge=false && !activeBlocks(session.state)时,跳过processTurn并原样转发 body:这不会影响压缩功能(因为没有活跃 block),但避免了不必要的 body 重建。
6.3 建议 C:blind tunnel 模式
提供
isMitmHost()可配置排除名单,让用户声明哪些域名不做 MITM(只做 blind TCP tunnel,不解密 TLS、不改 body):这样 TLS 指纹也是客户端原样的,405 概率最低。
七、总结
根因:billion-context 的 ACP kernel 对 Anthropic 协议请求无条件执行
anthropicToCore → processTurn → coreToAnthropic重建 messages body,即使injectTool=false / injectNudge=false。ZCode 服务端检测到 body 结构与客户端预期不符,返回 405/3012。签名协议限制:V4 Ed25519 签名 payload 不包含 body hash,服务端无法通过签名检测 body 篡改。405 的触发条件是 body 结构 fingerprint(或 TLS 指纹)。
复现:ZCode → bili MITM → Coding Plan messages 接口 → 405/3012。同一 session 中不经 bili 改写的路径正常。
建议:在官方版本中增加 per-host bypassHosts 配置(建议 A),使包更新后配置持续生效。