开源终端 Agent opencode:服务端、SDK 与会话分享
本文基于 opencode 仓库 commit 7fe9938(2026-08-04)梳理,该项目仍在持续更新,具体行为以仓库 https://github.com/anomalyco/opencode 最新代码与文档为准。
先说清楚指的是哪个东西:这里的 opencode 是一个具体的开源项目,package.json 里包名就叫 opencode,仓库地址 github.com/anomalyco/opencode,MIT 许可证,不是泛指「开源的代码」,也跟任何名字相近的模型没有关系。
你在终端里看到的那个界面,在 opencode 的架构里只是个客户端。 文档 packages/web/src/content/docs/server.mdx 写得很直白:运行 opencode 时会同时起一个 TUI 和一个服务端,TUI 是那个跟服务端说话的客户端。这句话决定了它跟很多「一个二进制跑完就退出」的编码工具不是一类东西——服务端能被别的东西连上,就意味着你可以不用它的界面,只用它的能力。
这篇拆三块:headless 服务端、发布在 npm 上的 JS/TS SDK、以及把会话变成公开链接的分享功能。三块合起来,才是「把 opencode 当组件用」这个判断的完整依据。
一、先把三块的位置摆清楚
这个仓库不小:packages/ 下 32 个包,英文文档 packages/web/src/content/docs/ 有 36 份 mdx,全仓 6358 个受版本控制文件,许可证是 MIT(LICENSE,Copyright 2025 opencode)。刚上手容易在包目录里迷路,所以先给一张定位表,路径都是能直接打开核对的:
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| headless 服务端 | 起 HTTP 监听、装配路由、暴露 OpenAPI | packages/opencode/src/server/server.ts | 想用 opencode serve 让别的程序连上来 |
| 路由分组 | 按 session、file、tui、mcp 等切分接口 | packages/opencode/src/server/routes/instance/httpapi/groups/ | 想知道某个能力到底有没有对应接口 |
| basic auth | 用环境变量决定要不要校验、怎么校验 | packages/opencode/src/server/auth.ts | 服务端不只监听 127.0.0.1 的时候 |
| JS/TS SDK | 拉起服务端进程 + 生成类型安全客户端 | packages/sdk/js/src/(index.ts、server.ts、client.ts) | 用 Node/Bun 写脚本批量驱动会话 |
| 会话分享 | 把会话数据同步出去、换回一个公开链接 | packages/opencode/src/share/session.ts、share-next.ts | 用 /share 或把 share 配成 auto 时 |
| 服务端文档 | 接口清单与参数说明 | packages/web/src/content/docs/server.mdx、sdk.mdx、share.mdx | 查某条路径的请求体长什么样 |
看表就能发现一件事:这三块不是三个独立特性,是同一条链路的三个断面。服务端定义了能力边界,SDK 是这套边界的类型化包装,分享则是把会话这一份状态搬到进程外面去。
二、服务端:一份 OpenAPI 撑起多个客户端
opencode serve 起的是一个 headless HTTP 服务。命令行签名文档里写得很清楚:
opencode serve [--port <number>] [--hostname <string>] [--cors <origin>]
文档表格里写的默认值是端口 4096、主机名 127.0.0.1,--cors 可以传多次追加允许的浏览器来源,另外还有 --mdns 和 --mdns-domain 两个服务发现相关的开关。
这些选项的解析逻辑在 packages/opencode/src/cli/network.ts 里,值得单独读一遍,因为它跟你从文档表格里得到的印象有几处不一样。第一,优先级不是一句话能概括的:port、hostname、mdns、mdns-domain 这四个都先判断你有没有在命令行里显式写过,写过就用命令行的,没写才回落到配置文件的 server 段;但 cors 是个例外,配置文件里的和命令行传的会拼成一个数组,两边都生效,不存在谁覆盖谁。第二,port 在这份选项定义里的默认值其实是 0 而不是 4096——4096 这个值是在后面的监听环节才出现的。第三,mdns 打开时如果配置文件里没显式给 hostname,主机名会被默认成 0.0.0.0。最后这条要留意:你开服务发现的动机通常是「让局域网里别的设备找到它」,而它顺手把监听地址从本机放宽到了所有网卡,这两件事在一个开关里绑着走。
端口这块有个细节值得知道:server.ts 里的 startWithPortFallback 分两条路走——端口不是 0 就直接按你给的值监听;是 0 则先尝试 4096,这一步失败再退回让系统分配任意空闲端口,代码注释把这个行为解释为对旧版监听逻辑的兼容。把它跟上一段的默认值串起来看就清楚了:你不传 --port 时拿到的正是 0,走的就是这条回退路径,所以 4096 更像「优先争取的那个端口」而不是「保证会用的那个端口」。别把 4096 硬编码进下游配置,正确做法是读 opencode serve 打印的那行监听地址。
接口面铺得相当宽。除了预料之中的会话增删改查(/session)与发消息(POST /session/:id/message),还有几类值得单独点名:
POST /session/:id/prompt_async:发消息但不等回复,直接返回204 No Content。长任务不想把 HTTP 连接挂住时用这个。POST /session/:id/shell:让会话跑一条 shell 命令。POST /session/:id/permissions/:permissionID:响应一次权限请求,请求体带response和可选的remember。这条是把「人工确认」这一步搬到外部系统里的接口。GET /event:服务端推送流,文档说第一个事件是server.connected,之后是总线事件。/tui/*一整组:append-prompt、submit-prompt、execute-command、show-toast等等,用来从外部驱动那个终端界面。文档提到 IDE 插件走的就是这条路。GET /lsp与GET /mcp:查语言服务与 MCP 服务的状态。POST /mcp:动态添加一个 MCP server,请求体是{ name, config },返回状态对象。这条的含义比字面重——它意味着服务端跑起来之后,还能从外部给这个 Agent 挂新的工具来源。GET /doc:OpenAPI 3.1 规范本体。SDK 就是从这份规范生成的。
还有一个不在接口表里、但你迟早会撞上的机制:请求级的工作区路由。packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts 里那行取值顺序是:
return url.searchParams.get("directory") || request.headers["x-opencode-directory"] || process.cwd()
serve.ts 的注释也点明了同一件事:服务端按请求加载实例,启动时不需要一个环境级的项目上下文。这意味着一个服务端进程可以服务多个目录,前提是调用方每次都说清楚自己要操作哪个目录——不说清楚就落到进程的当前工作目录,这是个安静的坑。
三、SDK:把服务端封成能调用的对象
SDK 发在 npm 上,包名 @opencode-ai/sdk。它其实做了两件不同的事,分清楚很重要。
第一件是拉起进程。packages/sdk/js/src/server.ts 里的 createOpencodeServer 做的是 spawn 一个子进程,参数就是 serve 加上 --hostname= 与 --port=,配置通过环境变量 OPENCODE_CONFIG_CONTENT 以 JSON 字符串传进去,然后监听子进程 stdout,等那行以 opencode server listening 开头的输出,从里面把 URL 抠出来。超时时间默认 5000 毫秒,超了就把进程停掉并抛错。
理解这一步很关键:SDK 并没有把 Agent 逻辑用库的形式塞进你的进程,它是在你旁边起了一个 opencode 进程。所以你的机器上必须能找到 opencode 这个命令,进程的生命周期也得你自己收尾。
第二件是生成客户端。如果服务端已经在跑,直接连就行:
import { createOpencodeClient } from "@opencode-ai/sdk"
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
})
createOpencodeClient 支持 baseUrl、fetch、parseAs、responseStyle、throwOnError 几个选项。createOpencode 则是把上面两件事合成一步,返回 { client, server }。类型定义从 OpenAPI 规范生成,Session、Message、Part 这些可以直接 import。
仓库里 packages/sdk/js/example/example.ts 那个例子把这套用法的意图交代得很清楚——扫一批文件,每个文件开一个会话,把文件作为 part 传进去让模型写测试:
const session = await client.session.create()
await client.session.prompt({
path: { id: session.data.id },
body: {
parts: [
{
type: "file",
mime: "text/plain",
url: pathToFileURL(file).href,
},
{
type: "text",
text: `Write tests for every public function in this file.`,
},
],
},
})
会话在这里是并发单位,一个文件一个会话,互不共享上下文——这对成本和隔离的影响都很直接。
另外两个能力顺带记一下。事件订阅是流式的:
const events = await client.event.subscribe()
for await (const event of events.stream) {
console.log("Event:", event.type, event.properties)
}
结构化输出走 format 字段,type 设成 json_schema 并给出 schema,模型会通过一个叫 StructuredOutput 的工具返回校验过的 JSON,结果读 result.data.info.structured_output。校验重试次数由 retryCount 控制,文档写默认 2;重试完还不合规,错误对象的 name 会是 StructuredOutputError。这条对「Agent 输出要进下游系统」的场景是刚需。
这里也是本篇跟站内几篇相邻文章的分工点:pi 的 server 进程模型讲的是另一个项目怎么把 Agent 跑成常驻进程,Agent SDK 与框架的区别是抽象层面的选型辨析,把 Agent 嵌进自己的程序讲的是嵌入式集成的通用套路;本篇只做一件事——把 opencode 这一个项目的服务端、SDK、分享三块的真实接口面和代价摊开给你看。
四、会话分享:链接一发,数据就在进程外了
分享功能的行为文档写得很坦白:创建一个公开 URL、把会话历史同步到他们的服务器、然后给你一条 opncd.ai/s/<share-id> 形式的链接。文档里那句提示也直接:分享出去的会话,任何拿到链接的人都能访问。
三种模式由配置项 share 控制,取值是 manual(默认)、auto、disabled:
{
"$schema": "https://opencode.ai/config.json",
"share": "disabled"
}
manual 下用 /share 命令生成链接,/unshare 撤回并删除相关数据。auto 是每个新会话自动分享。代码侧能对上:packages/opencode/src/share/session.ts 里 share 方法开头就判断配置,disabled 时直接抛错;create 方法则在有父会话时跳过、否则看运行时标记或配置是否为 auto 再决定要不要自动分享。另外 share-next.ts 顶上还认一个环境变量 OPENCODE_DISABLE_SHARE,值为 true 或 1 时关闭。
分享出去的东西,在数据模型上是一个带 id、url、secret 三个字段的结构,会话与消息按队列往外同步。换句话说,这不是「生成一张静态截图」,是持续把会话内容推到远端。
企业侧的说法在 enterprise.mdx 里:文档强调常规使用下代码与上下文不落他们的存储,唯一的例外就是这个可选的 /share——一旦启用,会话及相关数据会送到他们托管分享页的服务,并经 CDN 边缘缓存;文档自己给的建议是试用期先关掉,并把 "share": "disabled" 写进项目里的 opencode.json 提交到 Git,从而对整个团队生效。至于把分享页自托管到自己基础设施上,那份文档写的是仍在他们的计划中,不是现成能用的东西。
五、边界与代价:这个设计放弃了什么
把能力开成 HTTP 接口,代价是攻击面从「一个本地进程」变成「一个可被网络访问的端点」。这几条要认清楚:
默认不设防,防护靠你自己配。 认证机制只有 HTTP basic auth:设置 OPENCODE_SERVER_PASSWORD 才开启,用户名默认 opencode,可用 OPENCODE_SERVER_USERNAME 覆盖,这套对 opencode serve 和 opencode web 都生效。不设的话,serve.ts 会往标准输出打一句警告,说服务端处于不安全状态——但它只是警告,照样起。而这个端点背后是能读文件、能改文件、能跑 shell 命令的 Agent。绑到 0.0.0.0 又没密码,等于把一台机器的执行权放出去了。这里还要把前面那条 mdns 的默认行为接上:开服务发现会把主机名默认成 0.0.0.0,而密码是另一个完全独立的开关,两者之间没有任何联动检查。也就是说「让同事的机器能发现我这个服务」这一个动作,可能同时完成了「谁都能连上来」这件事,而你只会看到一行警告。相关的权限收敛思路见最小权限怎么设计。
工具面是活的,不是启动时定死的。 既然有 POST /mcp 这种运行期挂载接口,那么「这个 Agent 手上有哪些工具」就不是看一眼配置文件能回答完的问题。能调这个接口的人,等于能给 Agent 接入新的外部能力来源,而 MCP server 本身的行为并不在 opencode 的审查范围内。风险有两层:一层是接进来的东西可能读到不该读的目录或把内容送到别处,另一层是这类变更没有配置文件那样的评审留痕。真要放开这个端点,就得先把「谁能调 POST /mcp」当成一个权限问题来设计,而不是当成一个便利功能。
它不替你做多租户。 工作区靠请求里的目录参数选择,没有目录级的租户隔离概念。多个调用方共用一个服务端进程时,谁能操作哪个目录,得由你在外面兜住。
它不替你兜模型侧的账。 服务端提供的是编排与执行,代码内容仍然要发给你配置的模型服务商。计费、并发限制、可用性这些各家规则不同且会调整,以官方最新说明为准,opencode 这一层不做承诺。
分享功能不是私有化的。 它明确是把数据同步到外部服务再换回公开链接。文档自己列的注意事项就包括:只分享不含敏感信息的会话、分享前先看一遍内容、协作完就撤回、避免分享涉及私有代码或机密数据的会话、敏感项目直接整个关掉。这些不是客套话,是这个设计的直接后果。
SDK 不是纯库。 它要求宿主机上有 opencode 可执行文件,靠 spawn 子进程和解析 stdout 来握手。在容器、CI、受限 PATH 的环境里,这条链路比「装个 npm 包就能跑」脆弱得多。
六、上手与避坑清单
别把端口写死。 会踩是因为文档里的默认值 4096 太醒目,而实际端口受配置、命令行、以及显式传 0 时的回退逻辑三方影响。避法是启动后读那行 opencode server listening on ... 输出,或者用 SDK 拿返回对象里的 URL,而不是自己拼。
绑非本机地址前先设密码。 会踩是因为改 --hostname 是为了让别的机器能连,而认证是另一个开关,两件事没有绑定,改完照样能起。避法是把 OPENCODE_SERVER_PASSWORD 当成改 hostname 的前置动作,同时确认下游客户端也带上了对应的 Authorization 头。
每个请求都显式声明目录。 会踩是因为不带目录信息时会静默回退到服务端进程的当前工作目录,接口不会报错,你只会发现「它改的不是我要它改的那个仓库」。避法是调用侧统一带上目录参数或 x-opencode-directory 头,并在集成测试里断言这一点。
先把分享模式定死再分发配置。 会踩是因为 auto 是每个新会话都自动分享,一旦某个模板配置里带了它,团队里所有人都在无感知地往外推会话内容。避法是在项目根的 opencode.json 里显式写死 share,提交进版本库,别依赖每个人的全局配置。
SDK 起服务端时留足超时并做进程清理。 会踩是因为默认握手超时只有 5000 毫秒,冷启动慢的机器或首次拉起依赖时很容易超,而超时后你手上可能还留着孤儿进程。避法是显式传 timeout,并用 signal 或在流程收尾时关闭返回的 server 对象。
结构化输出要处理失败分支。 会踩是因为 schema 一复杂,模型填不对的概率就上去了,而重试次数是有限的。避法是判断错误名是否为 StructuredOutputError 并给出降级路径,schema 尽量扁平、字段带清楚的描述。相关的判断可以对照Agent 输出约束与终止。
权限响应要有人或有策略。 会踩是因为接口把权限决策暴露成了 POST /session/:id/permissions/:permissionID,自动化程序很容易图省事一律通过,而这些请求背后是写文件和跑命令。避法是把哪些操作可自动放行写成显式白名单,其余走人工,别用一个默认 yes 糊过去。
收尾
判断要不要把 opencode 当组件用,可以过三个问题:你需要的能力在 server.mdx 的接口表里能不能找到对应路径;你的运行环境能不能容忍「旁边多一个进程」这个前提;会话内容外泄的风险你打算用 disabled 硬关,还是靠流程管住。三个都有答案,接入方案基本就定了。
接着往下读的话,按这个顺序花的时间最划算:先把 packages/web/src/content/docs/server.mdx 的接口表扫一遍建立地图,再翻 packages/opencode/src/server/routes/instance/httpapi/groups/ 下的分组文件确认某个能力的真实入参,最后看 packages/sdk/js/example/example.ts,那五十来行代码就是这套设计想让你怎么用它的最短说明。
本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 opencode 扩展指南:插件钩子与自定义工具,两条路怎么选 和 开源终端 Agent opencode 的 ACP 层拆解:重映射的代价。