这份文件是写给在目标电脑上工作的 AI 助手的,不是写给人的。 人类用户只需要知道最后那一步(见第 7 节)。请按顺序执行,每步都要看实际输出再往下走。
把一个本地回环代理装成随 ZCode 自动启动,并让 ZCode 的「个人 Provider 配置」 多出一组名为 OpenCode 免费模型的服务。ZCode 每秒轮询该配置文件,所以写完不需要重启。
装完之后:用户再也不用管,也永远看不见黑窗口——启动 ZCode 时自动拉起。
先读第 8 节的风险说明,再决定要不要继续。
这个包只有一个文件:opencode2zcode.exe(2.0 MB,自包含)。
用户不需要装 Node.js,也不需要装任何运行时。
先确认它在:
dir opencode2zcode.exe找不到就停下来问用户要,别自己去找路径。
如果用户拿到的是源码目录(里面有
zen-proxy.mjs而不是 exe),那才需要 Node.js 18+。但正常分发只发 exe。
背景(这是本项目踩过的坑):ZCODE_DATA_BASE_DIR 不是 Windows 持久环境变量,
注册表里查不到,它只是 ZCode 启动自己子进程时注入的。
所以如果从资源管理器双击 .cmd,这个变量是不存在的——配置会写到用户主目录,
而 ZCode 读的是另一个目录,用户就会报「设置里找不到模型分组」。
如果你(AI)就运行在目标机的 ZCode 里,你的终端环境里通常已经有它,直接看:
node -e "console.log(process.env.ZCODE_DATA_BASE_DIR || '(未设置)')"- 打印出路径(例如
D:\Program)→ 记下它,这就是数据根。跳到第 3 步。 - 打印
(未设置)→ 继续往下。
不要为此猜测或硬编码路径,用工具自己的探测:
opencode2zcode.exe --detect期望输出(本机示例):
ZCode 数据目录候选(✓ = 带 v2/runtime,即认得出来):
✓ [6] D:\Program\.zcode\v2
证据:v2/runtime、v2/provider_config.json、v2 最近写入 ...
(另有 78 个 0 分候选已忽略)
环境变量 ZCODE_DATA_BASE_DIR:(未设置)
旁路文件:(无)
<PKG>\zcode-data-dir.txt
结论:采用 D:\Program\.zcode\v2
配置将写入:D:\Program\.zcode\v2\provider_config.json
判断标准:
- 出现带 ✓ 的候选,且
结论:采用 ...→ 探测成功,直接用,跳到第 3 步。 - 一个 ✓ 都没有 → 探测失败,走第 2.1 步。
不要瞎猜路径。按这个顺序找证据:
- 问用户:让他在 ZCode 里随便开一个对话,然后在他的终端里运行
echo %ZCODE_DATA_BASE_DIR%。这是最权威的答案。 - 看 ZCode 装在哪、数据在哪:数据根下必有
.zcode\v2\runtime子目录, 里面有按版本号分的运行资源。可以在常见位置找:%USERPROFILE%\.zcode、以及各固定盘的浅层目录(如D:\Program\.zcode)。 - 拿不到就让用户指路:问他平时打开 ZCode 用哪个账号/哪个盘,或者直接让他把
<数据根>\.zcode\v2这个目录找出来告诉你。
拿到路径后,后续所有命令都显式带上:
opencode2zcode.exe --config "<数据根>\.zcode\v2\provider_config.json" --write-config-onlyopencode2zcode.exe --print-config这个模式不落盘、不起服务,而且会把所有 provider 的 apiKey 换成
***已隐藏***(不会把用户已有密钥打到终端上)。
重点确认三件事:
- 输出里有两个
providerId为opencode-free和opencode-free-responses的条目。 api.baseUrl是http://127.0.0.1:41930/v1(端口后面可以改)。providerConfigRules.providerRules里用户原有的 provider 一个都没少—— 本工具只替换自己那两个 id,其它原样保留。
不对就停下来报告,不要继续写。
opencode2zcode.exe --write-config-only期望输出:
[zen] 配置目标(自动探测):<数据根>\.zcode\v2\provider_config.json
已写入 <数据根>\.zcode\v2\provider_config.json
chat N 个模型 / responses M 个模型
chat 与 responses 各有几个随上游自动增减,两条道分别装哪些模型由官方目录里 每个模型自己声明的 SDK 决定,不按名字猜。不要拿某次实测的数字当硬性预期。 数字对不上通常意味着目录刷新没成功(
catalog.source不是live), 或者上游在变——先跑--detect和/healthz确认,不要硬凑。
检查这几行:
配置目标(...)那行括号里写的是不是自动探测/环境变量 ZCODE_DATA_BASE_DIR/上次探测记录;如果是用户主目录兜底,说明没找对地方,回第 2.1 步。- 打印的路径是不是 ZCode 真正读的那个。
- 有没有出现以
[zen] 警告:没能确认 ZCode 正在使用的数据目录开头的警告。 有警告就是没找对,不要往下走。
写入行为(可以向用户保证):
- 写之前在同目录留一份
provider_config.json.bak-<时间戳>。 - 原子写(同目录临时文件 + rename)。
- 只替换
opencode-free/opencode-free-responses两个 id,其余 provider 与providerOrder原样保留。
写完后工具会在自己目录生成 zcode-data-dir.txt(记录数据根),
它只是给后面的 .cmd 双击走快路径用的,不要把这个文件拷到别的电脑。
node -e "const c=require('<数据根>/.zcode/v2/provider_config.json');console.log(c.config.providerConfigRules.providerRules.map(r=>r.providerId).join('\n'))"应能看到 opencode-free 与 opencode-free-responses 两个 id,
且用户原有的 id 都还在。
ZCode 大约 1 秒轮询一次该文件。若设置页没刷新,让用户切走再切回来,或重开设置页; 通常不需要重启 ZCode。
前面几步只是把配置写好了,代理本身还没跑。这一步让它以后自动跑。
opencode2zcode.exe --install期望输出:
[autostart] 已写入 SessionStart 钩子:C:\Users\<用户>\.zcode\cli\config.json
[autostart] 钩子命令:<本程序的绝对路径> --ensure
[autostart] 从现在起,启动 ZCode 会自动静默拉起代理,不用再双击任何东西。
重复执行安全——这个命令是幂等的:只会保证有且只有一条属于本工具的钩子, 且永远指向当前这个位置。如果输出里出现「清掉了 N 条指向已不存在位置的旧钩子」, 说明之前装在别处过,正常。
它做了什么:往 ZCode 的 CLI 配置里加一条 SessionStart 钩子,指向本程序并带
--ensure。ZCode 启动时它会检查代理在不在,不在就静默拉起
(detached + windowsHide,不弹窗口),并最多等 6 秒;无论成败退出码恒为 0,
不会影响 ZCode 开会话。
⚠️ 必须在「最终摆放位置」上执行这一步。 ZCode 的钩子只认绝对路径,所以上面那条命令把当前这个位置的路径记进了配置。 如果你打算把文件挪到别处,先挪完再执行这一步;已经挪过了也没关系, 到新位置重跑一次即可——它会认出自己的旧钩子并替换,指向已不存在路径的那条会被清掉。 (ZCode 的${ZCODE_PROJECT_DIR}是工作区相对路径,指向用户打开的项目文件夹, 不是本工具的安装位置,所以这里用不上。)
这一步会修改 ZCode 的配置文件。 动手前告诉用户你要改什么; 程序会自动在同目录留一份
config.json.bak-<时间戳>。 用户的其它mcp/plugins/ 别人的SessionStart钩子都不会被碰。
装完立刻验证一次(不必等下次开 ZCode):
opencode2zcode.exe opencode2zcode-hook
curl http://127.0.0.1:41930/healthz期望输出:
[zen] 回环代理已监听 127.0.0.1:41930
[zen] 已同步 ZCode 配置:<数据根>\.zcode\v2\provider_config.json
[zen] chat N 个 / responses M 个模型(数字随上游增减)
然后 curl 应回 "ok":true 且 catalog.source 为 live
(网络不通时用磁盘缓存里最近一次成功的结果顶上,仍可用;缓存也没有就先空着)。
再发一条真实对话确认整条链路:
curl -s -X POST http://127.0.0.1:41930/v1/chat/completions \
-H "content-type: application/json" \
-d "{\"model\":\"big-pickle\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly: OK\"}]}"期望返回 SSE 流(data: {"id":... 开头)。能返回就说明整条链路通了。
这个代理进程是常驻的,但用户不需要看见它、也不需要留着任何窗口。 关掉 ZCode 不影响它,下次开 ZCode 时钩子会发现它还在、什么都不做。
装好了。以后你什么都不用管——每次开 ZCode 它自己就悄悄启动了, 你不会看到任何黑窗口。去 设置 → 模型服务 找 「OpenCode 免费模型」, 在聊天框的模型选择器里直接选着用,跟内置模型一样。
不想要了怎么撤销:双击本目录的 uninstall.cmd。它会删掉那条自启钩子、
并把这两个 Provider 从配置里摘掉,你的其它 Provider 和其它钩子都不受影响。
node selftest.mjs期望输出:结果:16 通过 / 0 失败。
它用本地 mock 上游跑,不打任何真实网络请求,几秒出结果。覆盖免费判定、
下架剔除、伪装头、体门禁、档位归一、Provider 增删的对称性。
改动过 rust/src/ 之后建议跑一次再交付。
① 这是客户端伪装,不是官方接入。
OpenCode Zen 的匿名免费档位接受一个公开占位符 public 作为凭据(不是偷来的密钥、
不涉及用户账号)。但它同时有一道门,明确提示「免费档位只能从 OpenCode 里使用」。
本工具靠把自己伪装成 OpenCode 官方 CLI(伪装请求头 + 会话 id 形状 + 请求体里塞
两个它家客户端才有的工具名)通过这道检查。
所以:没有盗用任何账号或密钥,但确实是在冒充身份去过一道对方明确设了限制的关卡。 上游随时可能改规矩让它失效。请如实告知用户,不要淡化。
② 匿名通道按 IP 限速。 429 会被原样透传给用户。不要用它跑大批量任务。
③ 两条线格式的规则不一样,别照搬。
一个模型走哪条线看官方目录里它自己声明的 SDK:@ai-sdk/openai 的走
/v1/responses,openai-compatible 的走 /v1/chat/completions。
两条都要过同一道门禁(请求体里必须有 bash 和 read
两个工具),否则 403。代理已经处理好了,但如果对方要自己改代码,注意两点:
- responses 的工具定义是扁平的
{type, name, description, parameters}; 套 chat 那种嵌套{type, function:{name}}会被上游判 400。 - responses 一个
tool_choice都不能带,包括"none"。代理会摘掉再转发。
这条门禁跟账号无关:不需要登录,也没有地区问题(未登录的 OpenCode 桌面版 同样能调通)。
| 现象 | 排查 |
|---|---|
| 设置里没有「OpenCode 免费模型」 | 先看 配置目标 那行的括号内容,是不是「用户主目录兜底」;再看 --detect 的结论路径对不对 |
| 有分组但一发消息就失败 | 代理没在跑。curl http://127.0.0.1:41930/healthz 确认。手动跑 --ensure 能拉起来就说明钩子没生效——重跑一次 --install |
报 403 FreeTierError |
伪装头或请求体门禁没过,上游收紧了。改 rust/src/zen.rs 里的 OPENCODE_CLIENT_VERSION 跟官方 CLI 版本号 |
| 报 429 | 匿名通道按 IP 限速,等一会儿 |
报 400 invalid request |
看代理 stderr 有没有「摘掉该字段重试」的日志;重试也失败说明这个模型当前不可用 |
| 模型列表比预期少 | 看 /healthz 的 catalog.status 与 lastError。网络中断时用缓存里最近一次成功的结果顶上;目录判定为「已下架」的会被有意剔掉,属正常 |
| ZCode 启动时什么都没发生,模型不生效 | 钩子指向的路径可能失效了(文件被移动/改名过)。在新位置重跑一次 --install,它会替换掉旧的那条 |
| 改了代码怕改坏 | 跑 node selftest.mjs,22 项全过再交付 |
代理日志走 stderr,--print-config 的 JSON 走 stdout,方便重定向。
opencode2zcode.exe --install # 装上「随 ZCode 自动启动」(幂等)
opencode2zcode.exe --uninstall # 卸掉钩子 + 摘掉本工具的 Provider
opencode2zcode.exe opencode2zcode-hook # 手动触发一次检查+拉起(无窗口)
opencode2zcode.exe --detect # 看探测到的数据目录与判据
opencode2zcode.exe --print-config # 看要写什么(apiKey 已脱敏,不落盘)
opencode2zcode.exe --write-config-only # 只写配置后退出
opencode2zcode.exe --remove-provider # 从 ZCode 配置里摘掉本工具的两个 Provider
opencode2zcode.exe # 前台起代理(调试用,窗口要开着)
opencode2zcode.exe --port 41931 # 换端口
opencode2zcode.exe --config <完整路径> # 直接指定 provider_config.json
opencode2zcode.exe --with-responses # 兼容旧命令行(responses 组现在默认就写)
opencode2zcode.exe --help # 全部参数如果用户手上是源码目录而不是 exe,把 opencode2zcode.exe 换成
node zen-proxy.mjs 即可,参数完全一样(源码目录需要 Node.js 18+)。
## 11. 本包的文件
opencode2zcode.exe 唯一要分发的文件(2.0 MB,自包含,零依赖) README.md 给人看的说明 AI-SETUP.md 本文件,给 AI 看的安装说明
装了 Node.js 18+ 的话还能拿到源码与测试:
zen-proxy.mjs 等价的 Node 实现(行为基准) selftest.mjs Node 版自测,22 项 rust/ Rust 源码与测试
**不要**把 `zcode-data-dir.txt` 拷到别的电脑——它记录的是源机器上的数据目录。
工具发现它指向的目录不带 `v2/runtime` 时会自动忽略,但最好直接别带。
---
## 12. 技术出处
移植自 [JasonYu0822/opencode2pi-desktop](https://github.com/JasonYu0822/opencode2pi-desktop)
(MIT)的 `lib/zen.js` + `main.js`,其伪装细节来自
[FishBottle7/opencode2dsh](https://github.com/FishBottle7/opencode2dsh) 与
[jasonxu114514/opencode2api](https://github.com/jasonxu114514/opencode2api)。
保留的部分:CLI 一致的伪装头、规范形状的 `ses_` 会话 id、免费通道体门禁、
思考档位归一、S1∩S2 模型目录、SSE 逐块透传、磁盘目录缓存。
有意未移植:IP 池 / 代理轮换子系统、Go sidecar。
**与上游项目的两处有意偏离**(重要,别改回去):
**① `reasoning_effort` 不做「一律归一成 none」。**
上游把 off/缺失/未知**一律归一成 `"none"`**,注释说这是关掉 always-think 的唯一写法。
但 2026-10 实测该写法会直接把请求打成 **400**,而且各模型接受的档位并不一致
(`big-pickle` 只收 `minimal`→`max`;`nemotron-3.5` 收 `none` 却拒 `off`)。
本工具改为:只透传上游确实认的主动档位,`off`/`none`/缺失/未知一律**省略该字段**,
另加一次「摘掉该字段重试」兜底。
**② 「免费」的判定对齐 OpenCode 桌面版,并排除已下架模型。**
桌面版的判定就一行(`packages/app/src/components/dialog-select-model.tsx`):
```ts
const isFree = (provider, cost) => provider === "opencode" && (!cost || cost.input === 0)
本工具照搬,外加排除 status == "deprecated"。两点容易踩:
只看 input 不看 output,且「没有 cost 数据」也算免费;
Zen 会把下架模型留在公开清单里而标价仍是 0,不排除的话模型选择器里会出现
点下去必报错的选项。过滤后剩下的与桌面版完全相同,数量随上游增减。
rust/src/catalog.rs 的单元测试里有针对这一条的回归。