Gralliry's Blog

记录学习与生活

2026/08/08

Zed 无法打开 opencode Agent:Internal error service: directory 排查实录

by Gralliry

问题现象

在 Zed 里点击 opencode Agent,线程一直无法打开,顶部弹出报错:

Internal error: OpenCode service failure: {
  "service": "directory"
}

注意最后那个 "service": "directory"——这是定位问题的关键线索。

而终端里 opencode 用得好好的,只是 Zed 的 agent 面板起不来。


环境


第一轮排查:网上资料

这类报错在 GitHub 上并不罕见,先搜了一下相关 issue:

链接 内容
issue #31091 ACP session/new 失败,service: "directory",指向 loadDirectorySnapshot()
issue #17285 Windows 下 ACP session/new 返回 Internal error
PR #31096 根因:设置了 http_proxy/https_proxy 环境变量时,SDK 客户端连 127.0.0.1 的请求被代理劫持,代理转发不了 loopback,导致内部调用全挂

参考的结论是:代理环境变量 会导致这个错误。但我的机器上 http_proxy/https_proxy 环境变量(Machine/User 级)都是空的,而且 Clash 明明设置了 localhost 直连,为什么会挂?——这个疑问留到了后面。


复现:手搓一个 ACP 握手

Zed 和 opencode 之间走的是 ACP(Agent Client Protocol)——一个基于 stdio 的 JSON-RPC 协议。不借助 Zed,我直接用管道喂两条 JSON 给 opencode acp,就能精确复现:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientInfo":{"name":"test","version":"1.0"}}}
{"jsonrpc":"2.0","id":2,"method":"session/new","params":{"cwd":"/path/to/project","mcpServers":[]}}

结果:

initialize:  ✅ 返回正常(版本、能力、模型列表)
session/new: ❌ {"code":-32603,"message":"Internal error: OpenCode service failure","data":{"service":"directory"}}

initialize 成功、session/new 失败——和 Zed 里的表现完全一致。

从 opencode 源码看,session/new 会调用 loadDirectorySnapshot(),它对内部 HTTP 服务并发发起 5 个请求

  1. /config/providers
  2. /app/agents
  3. /command/list
  4. /app/skills
  5. /config

任何一个失败,整个快照加载就抛 service: "directory"


关键一步:把「服务器」和「客户端」分开测

服务器端:全绿

先直接探测正在运行的 ACP 内部 HTTP 服务(监听 127.0.0.1:4096):

GET /config/providers  → 200 (337KB)
GET /app/agents        → 200
GET /command/list      → 200
GET /app/skills        → 200
GET /config            → 200

服务器完全健康。 那问题只可能出在客户端那一侧。

客户端:连对地址了吗?

opencode 的 SDK 客户端会用配置里的 server.hostname 来拼内部 API 的 URL。而我的全局配置里写着:

"server": {
  "hostname": "0.0.0.0",   //  问题就在这里
  "port": 4096
}

对比一下客户端连两种地址的结果:

GET http://127.0.0.1:4096/config/providers   → 200   ✅
GET http://0.0.0.0:4096/config/providers     → 502   ❌

真相大白。


根因

server.hostname: "0.0.0.0" 是「服务器监听地址」,服务器绑在 0.0.0.0 上没问题(任何本机地址都能访问到它)。但 SDK 客户端也拿它当目标地址去连,于是发出 http://0.0.0.0:4096/... 的请求。

0.0.0.0 不是一个合法的目标 IP,而且它不是 loopback——所以:

5 个内部调用全部失败 → loadDirectorySnapshot 抛错 → Zed 报 service: "directory"

这也解释了为什么「Clash 设置 localhost 直连」没用——目标地址是 0.0.0.0,压根不匹配 localhost 的绕过规则。

我最初以为根因是代理,复现测试也确实证明「设置 http_proxy 环境变量 → 必挂」。但用户机器上根本没有代理环境变量。真正的一击是:配置里把 hostname 写成 0.0.0.0。代理只是让失败以 502 的形式暴露出来而已。


修复

把 hostname 改成 127.0.0.1

"server": {
  "hostname": "127.0.0.1",
  "port": 4096
}

如果你确实需要远程访问 opencode 服务,可以改成你的局域网 IP,而不是 0.0.0.0;但仅本机使用的话,127.0.0.1 是最稳的。

顺带清理:由于之前多次启动失败,残留的 opencode acp 进程占着 4096 端口,先杀掉旧进程再让 Zed 重新拉起 agent,避免端口冲突。


验证

改完配置、清掉旧进程后,用之前那份 ACP 握手脚本在真实项目里再跑一次:

initialize:  ✅
session/new: ✅ 返回 sessionId + 完整模型列表

Zed 里重开一个 opencode 线程,模型能正常选择、消息能正常发出,问题解决。


补充:另一种触发方式

同一个报错还有另一个独立触发条件,就是 GitHub 上大家讨论最多的那个:进程环境里设置了 http_proxy/https_proxy。此时 SDK 客户端连 127.0.0.1 的请求也会被显式地丢给代理,而代理转发不了 loopback → 同样的 service: "directory"

如果你属于这种情况,给 ACP 进程加上环境变量即可:

"agent_servers": {
  "opencode": {
    "type": "custom",
    "command": "opencode",
    "args": ["acp"],
    "env": {
      "NO_PROXY": "127.0.0.1,localhost"
    }
  }
}

相关修复 PR #31096 至今未合入,所以目前(1.18.x)这个问题还需要在用户侧规避。


小结

这次排查收获最大的是方法,而不是答案本身:

  1. 用报错里的 service 名锁定代码路径——service: "directory" 直接指向 loadDirectorySnapshot()
  2. 绕开 UI,用原始协议复现——手搓 ACP 的 JSON-RPC 握手,几秒钟就能把问题从「Zed 的 bug」里摘出来。
  3. 把「服务器」和「客户端」拆开测——服务器端 5 个端点全绿,就证明是客户端连错地址,而不是服务不可用。
  4. 警惕「配置既能当监听地址、又被客户端当目标地址」的双重语义——server.hostname 就是这样一个坑。
tags: zed - opencode - acp - 排查 - windows