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 正常)跑得好好的,换到服务器就卡。

排查后确认这是两个独立的问题叠加:

  1. 插件加载阶段:opencode 内置的 Bun 运行时解析 TS 插件时,因 @opentui/* 依赖缺失而联网 npm registry,IPv6 不通导致连接挂死。
  2. 模型请求阶段:即使跳过插件加载,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 加载插件时联网

现象定位

对比实验快速锁定问题在插件加载:

插件文件本身是纯 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.91.17.12
批次 A(20s 超时)卡死 3/3卡死 3/3
批次 B(60s 超时)✅ 6.2s 完成✅ 4.8s 完成
批次 C(40s 超时)—卡死 2/2

两个版本总是成对成功或成对失败——行为完全一致,由网络状态决定,与版本无关。

真实原因是 IPv6 黑洞状态分钟级波动:

这也解释了最初"本机正常、服务器卡"的观察:本机测试恰好赶上快速失败时段(或默认模型走无 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),这是功能需求,不再是卡死。


经验总结

  1. optional peer dependencies 会被 --legacy-peer-deps 跳过安装,但 Bun 解析模块图时仍然会尝试解析它们,缺失就联网。打包预装 node_modules 时,把运行链路可能触及的包显式声明为 dependencies 最稳妥。
  2. "有 IPv6 地址" ≠ "IPv6 可用"。本机和服务器都有全局 IPv6 地址和默认路由,但出站全被黑洞丢弃。遇到"某应用在这台机器卡死"时,先测 curl -6 对比 curl -4。
  3. 黑洞网络的状态是波动而非恒定的:有时 IPv6 连接快速失败(应用秒级回退 IPv4),有时挂起几十秒。因此"本机能跑、服务器不能"这类对比实验必须在同一时段进行,否则会把网络波动误判为环境/版本差异。用多批次交替测试 + 每次测量耗时,避免单次样本误导。
  4. /etc/gai.conf 控制 getaddrinfo 的地址排序,影响所有应用的连接顺序(Bun/Node 也遵循)。内网环境统一加 precedence ::ffff:0:0/96 100 可以绕开 IPv6 黑洞,无论黑洞状态如何都稳定。
  5. .global-opencode(OPENCODE_CONFIG_DIR)与 .opencode(项目级)是两个不同的配置来源:前者由引导程序整目录注入(含独立的 node_modules/plugins),后者是标准项目级配置。排查启动问题时,先分清插件/模型配置来自哪一层,再看对应的依赖是否完整。