opencode 在服务器上启动卡死:IPv6 不通与 @opentui 依赖缺失
八月 03, 2026 [AI] #opencode #调试 #ipv6 #bun #plugin现象
在一台服务器上启动 opencode,无论 serve 还是 run 模式都会卡死,没有任何报错:
cd ~/project/debug-agent/ && \
OPENCODE_CONFIG_DIR=$PWD/.global-opencode \
OPENCODE_DISABLE_MODELS_FETCH=1 \
WFUZZ_PRODUCT=code \
"/path/to/wfuzz-agent/bin/opencode" --print-logs --log-level=DEBUG run hello
日志停在某一行后就没有下文,进程一直存活,只能靠 timeout 杀掉(exit 124)。同样的命令在本机(IPv6 正常)跑得好好的,换到服务器就卡。
排查后确认这是两个独立的问题叠加:
- 插件加载阶段:opencode 内置的 Bun 运行时解析 TS 插件时,因
@opentui/*依赖缺失而联网 npm registry,IPv6 不通导致连接挂死。 - 模型请求阶段:即使跳过插件加载,opencode 请求模型 API 时默认优先尝试 IPv6,而服务器没有真实的 IPv6 连通性。
两个问题的共同放大器,是这台服务器的网络特殊性:有 IPv6 地址,但没有 IPv6 路由。
背景:opencode 的配置体系
先理解 opencode 启动时会读哪些配置,这决定了排查方向。
配置加载优先级
opencode 按以下顺序加载配置(后面的覆盖前面的同名字段):
| 配置位置 | 路径 | 作用 |
|---|---|---|
| 全局配置 | ~/.config/opencode/opencode.json[c] | 用户级默认配置 |
| 自定义目录 | OPENCODE_CONFIG_DIR 指向的目录(如 .global-opencode/) | 平台注入的独立配置目录,拥有独立 plugins/skills/node_modules |
| 项目配置 | 项目根/.opencode/opencode.json[c] | 项目级配置 |
| 用户目录 | ~/.opencode/opencode.json[c] | 用户级补充 |
注意:
OPENCODE_CONFIG_DIR指向的目录会被完整地当作一个 opencode 配置目录来扫描(opencode.jsonc、plugins/、skills/、node_modules/等),而不是只读其中的配置文件。
.global-opencode/:运行时生成的独立配置目录
由 wfuzz-agent(或其他引导程序)在启动前把打包好的配置模板复制到工作目录下生成,包含:
.global-opencode/
├── opencode.jsonc # 模型/provider/permission 配置
├── plugins/ # TS 插件源码(wfuzz-agent.ts + 本地库)
├── node_modules/ # 预装的 @opencode-ai/plugin 及依赖(避免运行时 npm install)
├── skills/ # 平台技能目录
├── AGENTS.md # 全局指令
├── tsconfig.json # 插件类型检查配置
└── package.json # 声明 @opencode-ai/plugin 依赖
.opencode/:项目级配置
与 .global-opencode 不同,.opencode/ 是 opencode 标准的项目级配置目录,通常包含:
.opencode/
├── opencode.json # 项目级模型/skills 路径配置
├── skills/ # 项目级技能
└── node_modules/ # 同步来的依赖(供插件解析)
影响启动的关键差异:.global-opencode 中的 opencode.jsonc 定义了 provider(决定模型请求发往哪个 baseURL)和默认模型;plugins/ 决定是否有插件要加载;node_modules 决定插件依赖是否能离线解析。
根因一:@opentui 依赖缺失,Bun 加载插件时联网
现象定位
对比实验快速锁定问题在插件加载:
opencode run --pure hello(跳过外部插件)→ 能跑起来opencode run hello(加载.global-opencode/plugins/wfuzz-agent.ts)→ 卡死- 临时移走
plugins/目录 → 能跑起来
插件文件本身是纯 TypeScript(import type + node:fs 等),没有明显的联网代码,为什么加载会联网?
根因:optional peer dependencies 缺失
opencode 底层是 Bun 运行时。加载 .ts 插件时,Bun 解析 @opencode-ai/plugin 的整个模块图,包括 dist/tui.js 中对 @opentui/* 的运行时导入:
// node_modules/@opencode-ai/plugin/dist/tui.js
import { createBindingLookup } from "@opentui/keymap/extras"
export { stringifyKeySequence } from "@opentui/keymap"
@opentui/core、@opentui/keymap、@opentui/solid 在 @opencode-ai/plugin 的 package.json 里是 optional peer dependencies:
"peerDependencies": {
"@opentui/core": ">=0.3.4",
"@opentui/keymap": ">=0.3.4",
"@opentui/solid": ">=0.3.4"
},
"peerDependenciesMeta": {
"@opentui/core": { "optional": true },
...
}
npm install --omit=dev --legacy-peer-deps 不会安装 optional peer dependencies,所以预装的 node_modules 里缺了这三个包。Bun 解析模块图时发现缺失,就去 npm registry 拉取——在 IPv6 不通的服务器上,这一步永远完不成。
strace 铁证:进程反复尝试连接 npm registry 的 IPv6 地址,connect() 返回 EINPROGRESS 后永不完成:
connect(..., "2606:4700::6810:622", 443) = EINPROGRESS ← 永不完成
修复:把 optional peer 依赖变成显式依赖
在打包 node_modules 的 package.json 里显式声明这三个包,让它们进入预装依赖:
"dependencies": {
"@opencode-ai/plugin": "1.17.12",
"@opentui/core": "0.3.4",
"@opentui/keymap": "0.3.4",
"@opentui/solid": "0.3.4"
}
验证:npm install --omit=dev --legacy-peer-deps 后 node_modules/@opentui/ 存在,peer 要求(>=0.3.4)被满足;在无缓存环境下 bun -e "await import('@opencode-ai/plugin/tui')" 从报错 Cannot find module '@opentui/keymap/extras' 变为成功,且全程零网络连接。
根因二:DNS 默认 IPv6 优先,模型请求连接挂起
现象定位
修好 @opentui 后,插件能加载了,但 run hello 仍然卡住。日志停在:
stream providerID=opencode modelID=... ← 模型请求发出后无响应
这次不是插件问题。OPENCODE_DISABLE_MODELS_FETCH=1 只禁用了 models.dev 元数据获取,但模型 API 请求(opencode.jsonc 中 provider 的 baseURL: https://opencode.ai/zen/v1)仍会发起网络请求。
根因:/etc/gai.conf 默认 IPv6 优先
用 curl 验证连通性:
curl -6 -s -o /dev/null -w "%{http_code}" https://opencode.ai/zen/v1 # 超时(exit 124)
curl -4 -s -o /dev/null -w "%{http_code}" https://opencode.ai/zen/v1 # 正常返回
重要:本机和服务器处于同一个 IPv6 黑洞网络(同一网关 fe80::c0b1:65ff:fe3e:68c3)。ip -6 addr 可见一堆 3000::/64 的全局地址,ip -6 route 也有 default via ... 路由,但所有 IPv6 出站连接都挂在半路上——本机 curl -6 同样超时,并非"服务器 IPv6 不通、本机正常"。
而 Linux 的 getaddrinfo() 默认按 /etc/gai.conf 的优先级表排序解析结果,IPv6 优先级高于 IPv4:
# /etc/gai.conf 默认(注释状态)
precedence ::1/128 50 ← IPv6 loopback
precedence ::/0 40 ← 所有 IPv6,优先级 40
precedence ::ffff:0:0/96 10 ← IPv4,优先级 10(值越大越优先)
Bun/opencode 按 getaddrinfo 排序后的地址顺序连接:先试 IPv6 → 挂死 → 整个请求超时(默认超时很长),表现为"卡住"。
版本对照实验:1.17.12 与 1.18.9 没有差异
排查中一度怀疑"新版 opencode(1.18.9)内置了 IPv6→IPv4 回退,旧版(1.17.12)没有"。用完全相同的配置(显式 opencode provider + opencode.ai baseURL)在同一台机器交替测试,结论是没有版本差异:
| 测试批次 | 1.18.9 | 1.17.12 |
|---|---|---|
| 批次 A(20s 超时) | 卡死 3/3 | 卡死 3/3 |
| 批次 B(60s 超时) | ✅ 6.2s 完成 | ✅ 4.8s 完成 |
| 批次 C(40s 超时) | — | 卡死 2/2 |
两个版本总是成对成功或成对失败——行为完全一致,由网络状态决定,与版本无关。
真实原因是 IPv6 黑洞状态分钟级波动:
- 挂起时段:IPv6 SYN 发出后无任何响应(黑洞丢弃),opencode 连接挂起 40s+ 无快速回退 → 表现为卡死
- 快速失败时段:网关偶发回 ICMP unreachable,opencode 秒级回退 IPv4 → 4~6 秒正常完成
这也解释了最初"本机正常、服务器卡"的观察:本机测试恰好赶上快速失败时段(或默认模型走无 AAAA 记录的 deepseek 域名,压根不触发 IPv6)。
修复:让 IPv4 优先
sudo sed -i 's/^#precedence ::ffff:0:0\/96 10/precedence ::ffff:0:0\/96 100/' /etc/gai.conf
把 IPv4 的优先级从 10 改成 100(取消注释即生效),getaddrinfo 就会把 IPv4 排在前面:
precedence ::ffff:0:0/96 100 ← IPv4 现在最高优先
验证:
getent ahosts opencode.ai # 172.65.90.x(IPv4)排在最前
修复后的完整验证
1. 插件加载 + 模型回复正常
cd ~/project/debug-agent/ && \
OPENCODE_CONFIG_DIR=$PWD/.global-opencode \
OPENCODE_DISABLE_MODELS_FETCH=1 \
WFUZZ_PRODUCT=code \
"/path/to/wfuzz-agent/bin/opencode" --print-logs --log-level=DEBUG run hello
[wfuzz-agent] plugin loaded hooks.trigger=11 hooks.lifecycle=6
Hello! How can I help you today?
[wfuzz-agent] plugin disposed after 4.5s
EXIT: 0
2. 启动阶段零公网连接
用 strace 跟踪启动 + 空闲运行 30 秒的全部网络连接:
strace -f -e trace=connect -o /tmp/net-trace.log \
opencode serve & # 带上 OPENCODE_CONFIG_DIR 等环境变量
sleep 30
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:4096/ # 200
grep -oE 'sin_addr=inet_addr\("[0-9.]+"\)' /tmp/net-trace.log | sort | uniq -c
结果只有内网(WFUZZ_SERVER 192.168.7.16)的连接,公网 IPv4/IPv6 连接数为 0,服务正常响应 HTTP 200。
注:
serve出现实际会话(用户发消息调模型)时,模型请求仍会访问公网opencode.ai(现在走 IPv4),这是功能需求,不再是卡死。
经验总结
optional peer dependencies会被--legacy-peer-deps跳过安装,但 Bun 解析模块图时仍然会尝试解析它们,缺失就联网。打包预装node_modules时,把运行链路可能触及的包显式声明为dependencies最稳妥。- "有 IPv6 地址" ≠ "IPv6 可用"。本机和服务器都有全局 IPv6 地址和默认路由,但出站全被黑洞丢弃。遇到"某应用在这台机器卡死"时,先测
curl -6对比curl -4。 - 黑洞网络的状态是波动而非恒定的:有时 IPv6 连接快速失败(应用秒级回退 IPv4),有时挂起几十秒。因此"本机能跑、服务器不能"这类对比实验必须在同一时段进行,否则会把网络波动误判为环境/版本差异。用多批次交替测试 + 每次测量耗时,避免单次样本误导。
/etc/gai.conf控制 getaddrinfo 的地址排序,影响所有应用的连接顺序(Bun/Node 也遵循)。内网环境统一加precedence ::ffff:0:0/96 100可以绕开 IPv6 黑洞,无论黑洞状态如何都稳定。.global-opencode(OPENCODE_CONFIG_DIR)与.opencode(项目级)是两个不同的配置来源:前者由引导程序整目录注入(含独立的 node_modules/plugins),后者是标准项目级配置。排查启动问题时,先分清插件/模型配置来自哪一层,再看对应的依赖是否完整。