【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
标题
198、【Agent】【OpenCode】TuiThreadCommand handler:transport 双形态的触发与误区
背景
上篇 blog
【Agent】【OpenCode】TuiThreadCommand handler:stop 的幂等清理
拆了 handler 的收尾清理:stop用stopped闭包标志(thread.ts:155-168)实现"调用 N 次 = 1 次"——首次撤监听、发 shutdown RPC(5 秒超时)、terminate兜底强杀;它只在tui()的finally(thread.ts:217-219)里被触发一次,触发链是exit() → onExit → resolve → await tui 返回 → finally → stop → 外层 finally → exit(0);幂等是防御性设计(防多入口 + async 穿插),与unguard的done标志同源。197 拆了"怎么收尾",本篇往前看启动链路的核心transport——193 篇拆过它的"判定与组装",本篇专门回答两个问题:内部/外部模式各自如何被触发,以及一个常见误区:internal/external 到底是不是"内部大模型 / 外部大模型"的意思
OpenCode
先说结论,避免带着错误预期往下读:internal/external说的是"TUI 与后端服务之间怎么通信",跟用哪个大模型没有半点关系。模型是另一条独立的轴。下面先讲透这个误区,再讲触发条件。
⚠️误区澄清:internal/external ≠ 内部/外部大模型
这是两条完全正交的轴:
| 轴 | 回答的问题 | 判断依据 |
|---|---|---|
| transport(内部/外部模式) | TUI 与"后端服务(worker 里的 opencode 核心)"之间怎么传数据 | 有没有监听端口;走进程内 RPC 还是真实 HTTP |
| 模型(内部/外部大模型) | worker 用哪个模型提供方(本地 Ollama / 云端 DeepSeek·OpenAI…) | opencode 配置的 provider/model,与 transport 无关 |
代码佐证:thread.ts:211的model: args.model只是把用户选的provider/model传进 TUI 的 args,模型解析与调用全在 worker 里,无论 internal 还是 external,worker 都用配置好的那一个模型。“内部模式” = “TUI 和 worker 同进程、走 RPC 通信”,绝不是"用本地模型";“外部模式” = “给服务开个端口让外部客户端连”,也不是"用云端模型"。两者互不决定。
🧩如何触发外部模式(external)
判定代码在 thread.ts:177-195:
constexternal=process.argv.includes("--port")||// ① CLI 显式给了 --portprocess.argv.includes("--hostname")||// ② CLI 显式给了 --hostnameprocess.argv.includes("--mdns")||// ③ CLI 显式给了 --mdnsnetwork.mdns||// ④ 解析后 mdns 为 truenetwork.port!==0||// ⑤ 端口不是 0network.hostname!=="127.0.0.1"// ⑥ hostname 不是默认回环地址network来自resolveNetworkOptions(network.ts:39-59),注意它不止看命令行,还会读全局配置config.server.port/hostname/mdns。所以外部模式有六大触发路径:
| 触发方式 | 例子 |
|---|---|
CLI 显式--port | opencode --port 8083 |
CLI 显式--hostname | opencode --hostname 0.0.0.0 |
CLI 显式--mdns | opencode --mdns(hostname 会被抬成 0.0.0.0) |
配置server.mdns | config 里开 mdns |
配置server.port非 0 | config 里server.port = 8083 |
配置server.hostname非 127.0.0.1 | config 里改 hostname |
opencode serve --port 8083 --hostname 0.0.0.0这种对外服务写法,必然命中外部模式。
🧩如何触发内部模式(internal)
内部模式 =上面六条全不满足,即:交互式opencode不带任何--port/--hostname/--mdns,且全局配置里也没设server.port/hostname/mdns(保持默认:port=0、hostname="127.0.0.1"、mdns=false,见 network.ts:4-31)。
| 场景 | 命令 | 模式 |
|---|---|---|
| 本机交互式 TUI | opencode(不带网络参数) | internal |
| 对外服务 | opencode serve --port 8083 | external |
| 局域网发现 | opencode --mdns | external |
🌐外部模式的行为:worker 真起 HTTP server
命中 external 时,transport 长这样:
{url:(awaitclient.call("server",network)).url,// 让 worker 真起 server,返回真实 URLfetch:undefined,// TUI 用标准 fetch 直连events:undefined,// TUI 用标准 EventSource 直连}- worker 通过 RPC 指令真实启动一个 HTTP server,
url是能真连的地址 fetch/events都是undefined—— 有真实网络,TUI 直接用标准 fetch + EventSource 连那个 url 即可,不需要任何代理- 代价:暴露端口,有真实网络栈开销
🔌内部模式的行为:RPC 把 worker 伪装成服务
命中 internal 时:
{url:"http://opencode.internal",// 伪地址,不真连接fetch:createWorkerFetch(client),// RPC 代理events:createEventSource(client),// RPC 事件转发}- 不监听任何端口;
http://opencode.internal是占位伪地址——不会被 DNS 解析、不会建连接,因为 TUI 的请求全被createWorkerFetch拦截走 RPC、事件走 RPCevent频道,根本不会发起真实 HTTP createWorkerFetch(thread.ts:24-40):请求序列化成{url, method, headers, body}→ RPC 发给 worker 执行 → 结果包成标准Response返回,TUI 无感知createEventSource(thread.ts:42-49):订阅 RPCevent频道收事件流,零端口开销
📊两种模式对比
| 维度 | 内部模式(internal) | 外部模式(external) |
|---|---|---|
| 触发 | 全默认(无网络参数/config) | 任一 CLI 标志或 config server 非默认 |
| 端口 | 零端口 | 真实监听 |
| url | 伪地址http://opencode.internal | server RPC 返回的真实 URL |
| fetch | createWorkerFetch(RPC 代理) | undefined(标准 fetch 直连) |
| events | createEventSource(RPC 频道) | undefined(标准 EventSource 直连) |
| 数据通道 | 进程内 RPC | 真实网络栈 |
| 模型 | 与模型无关 | 与模型无关 |
📌一句话记忆
internal/external 是"TUI 怎么够到 worker":默认交互式 = 内部模式,走进程内 RPC、零端口、伪地址;带
--port/--hostname/--mdns或 config 设了 server = 外部模式,开真端口、标准 HTTP。它不是"内部/外部大模型"——模型由 worker 按配置调用,与 transport 完全正交。
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog