拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

300 行写个协议桥:让 Claude Code 走 ACP 通道,吃 ZCode 套餐约 1.5 倍额度

300 行写个协议桥:让 Claude Code 走 ACP 通道,吃 ZCode 套餐约 1.5 倍额度

本文记录一个真实可跑的协议桥实现:拦截 Claude Code 的/v1/messages请求,转交给 ZCode 的 headless app-server,再把流式事件翻译回 Anthropic SSE。全部基于 307 行 Node.js 源码,无第三方 HTTP 依赖。文末总结了 5 个实测踩过的坑。

为什么需要这座桥

先说清楚动机——不是为了"用国产模型",而是额度经济学。Claude Code 是目前最顺手的终端 AI 编程工具,但直接用它只能消耗 Anthropic 官方套餐。而把它的请求桥接到 ZCode 的 Coding Max 套餐通道后,请求按 ZCode 的额度扣率计费——以我手上的套餐实测,抵扣系数约 0.67,同样额度的有效用量约等于放大到 1.5 倍(具体扣率以官方当前政策为准)。顺带的收益是模型本身也换成了 GLM 系列,对日常编码场景完全够用。

但两者协议完全不同:

  • Anthropic 侧:一次性 HTTP POST,回来的是message_start → content_block_delta* → message_stop这套 SSE 事件;
  • ZCode 侧:要先session/create建会话,session/send发内容,然后从事件流里订阅model.streaming、turn.completed等事件。

缺的就是中间的翻译层。先交代两个名词:ACP(Agent Client Protocol,Agent 客户端协议)是一套让编码 Agent 前端与后端模型服务解耦的 JSON-RPC 协议,zcode-acp-server是它的 Node 实现;ZCode 的 headless 模式叫app-server,本文的桥就是"Anthropic Messages 协议 → ACP 后端"的翻译官。

核心思路一句话:本地起一个只听 127.0.0.1 的 HTTP 服务,把 Anthropic 协议的请求翻给 ZCode,把 ZCode 的事件流翻回 Anthropic SSE。

架构:三段式

Claude Code ──HTTP/SSE──> 桥接服务(127.0.0.1:8080) ──JSON-RPC──> zcode app-server │ ├─ 1. 收 /v1/messages,压成 prompt 文本 ├─ 2. session/create(mode=yolo)+ subscribe ├─ 3. session/send 送入 prompt └─ 4. 轮询事件流 → 翻译成 Anthropic SSE

桥接复用了现成的zcode-acp-server构建产物(ZcodeBackend/EventStreamListener/ 凭证加载),自己只写协议转换——这是它只有 307 行的原因。

难点一:app-server 会"反向找你要东西"

最隐蔽的坑在这里:session/create过程中,app-server 会反过来向桥接发请求(server→client 方向),其中:

// zod .strict() 对象:nativeSearchEnhancementsEnabled 必填 booleanb.sendReply(req.id,{nativeSearchEnhancementsEnabled:true,memoryEnabled:false,askUserQuestionAutoResolutionEnabled:true,});

session/requestRuntimePreferences用的是 zod.strict()校验——少一个必填字段就直接报错,而你不回复它,session/create就永远挂起,表现为一个干巴巴的 timeout,日志里毫无线索。

解法是一个 100ms 轮询的drainer(排水泵):后台持续pollServerRequests(),把反向请求分类回复——运行时偏好回默认值,interaction/requestPermission回allow(yolo 模式免得工具调用卡住),interaction/requestUserInput回decline(headless 场景没人能答题),未知方法一律回空对象兜底。

这个模式可以推广:任何"客户端SDK里嵌着服务端回调"的协议,对接时第一件事就是把回调通道接住,否则主流程必然莫名超时。

难点二:两套流式协议的对齐

Anthropic 的 SSE 是严格的事件序列,少一环客户端就报错:

message_start → content_block_start → content_block_delta* → content_block_stop → message_delta(含 stop_reason 和 usage) → message_stop

而 ZCode 侧给的是model.streaming(payload.kind=text_delta)+turn.completed/failed。翻译循环的关键细节:

  1. 先发头再发送:session/send之前就先把message_start/content_block_start写出去——否则可能丢掉早到的turn.completed;
  2. 轮询 + 心跳:pollEvent(1000)循环里维护lastProgress,超过TURN_TIMEOUT_MS(默认 180 秒)没进展就按超时收尾;
  3. stop_reason 映射:turn.completed → end_turn,超时 →max_turn_requests,turn.failed→ 把错误文本塞进 delta 再正常收尾(客户端不会因异常断流);
  4. usage 编造:ZCode 不回 token 数,用output_chars / 4估算——SSE 里 usage 字段必须有,数值不准但协议合法。

难点三:多轮历史的降维

Anthropic 请求里是结构化的messages[](可能还带 tool_use/tool_result 块),而 ZCode 每次create都是全新 session。桥接的处理是把历史压平成一段文本:

functionbuildPrompt(request){// [system]\n... \n\n[user]\n... \n\n[assistant]\n...}

文本块类型的 content 直接拼接,非文本块(图片等)丢弃。这是有损压缩,但对"用 Claude Code 干活"这个场景够用——因为真正需要长上下文的是 ZCode 自己的 session 内工具调用,历史 prompt 只是开场白。

五个实测坑(都是拿报错换来的)

  1. ANTHROPIC_BASE_URL千万别带/v1后缀。Claude Code 会自动拼/v1/messages,你写成…:8080/v1就变成/v1/v1/messages→ 404。写http://127.0.0.1:8080就好;
  2. Node 必须 ≥22:底层zcode.cjs依赖内置的node:sqlite,低版本直接起不来;
  3. 只监听 127.0.0.1 并校验 remoteAddress:桥接是本地代理性质,对外开放等于把你的模型配额变成公共 API;
  4. ZCode 重启后必须重启桥接:桥接 fork 的 app-server 子进程会跟着断,而且平台刷新后的 provider key 也要重新读取;
  5. mode=yolo的边界:工具权限自动放行必须配 workspace 白名单,桥接日志里明确打了警告——自动化的代价是把安全决策交给作用域限制,别把 workspace 设成整个家目录。

收尾

做完这个桥的最大体会:AI 工具的互操作性问题,本质是"事件流方向"和"状态机生命周期"两类问题。反向请求要有人接(方向问题),session 要有人管(生命周期问题)——这两件事解决了,剩下的只是字段名对齐的体力活。

完整源码 307 行,欢迎在评论区交流你对接过的其他 AI 工具协议。

返回列表