首页
/ opencode 客户端 SDK 深度解析:@opencode-ai/client 的契约驱动代码生成与双入口架构

opencode 客户端 SDK 深度解析:@opencode-ai/client 的契约驱动代码生成与双入口架构

2026-09-06 18:14:55作者:牧宁李

本篇技术指南以 packages/client/README.md 为主体,深入拆解 OpenCode 仓库中的 @opencode-ai/client 包——一个从权威 Effect HttpApi 契约直接生成、面向零依赖 Promise 场景与 Effect 运行时场景双轨交付的 HTTP 客户端。读完本文,你将掌握该客户端的两个公开入口(Promise 根与 /effect 子路径)的适用边界、契约到代码的完整生成链路(bun run generate / bun run check:generated)、生成物的错误模型,以及仓库如何用打包边界测试锁定两套入口的依赖图谱。

1. 包定位:一个私有的“生成目标”

@opencode-ai/clientpackage.json 中被标记为 "private": true,其自我定位(见 README 首行)是 Private generation target for clients derived directly from OpenCode's authoritative Effect HttpApi——即“直接从 OpenCode 权威 Effect HttpApi 派生客户端的私有生成目标”。这意味着该包的核心价值不在手写逻辑,而在由契约单向生成的确定性与可验证性:

  • 生成产物全部落在 src/generated/src/generated-effect/ 两个目录,文件头明确标注 // Generated by @opencode-ai/httpapi-codegen. Do not edit.
  • 生成链路以仓库内的 httpapi-codegen 包 为编译器,以 src/contract.ts 中声明的 ClientApi 为输入;
  • 契约变更后,README 给出了标准工作流:运行 bun run generate 重新生成,再运行 bun run check:generated 检测已提交产物的漂移。对照 package.json 的 scripts 可见,check:generated 的实际实现就是 bun run generate && git diff --exit-code -- src/generated src/generated-effect——重新生成后对两个产物目录做零差异断言。

依赖关系也印证了“生成物只依赖契约侧”的设计:运行时依赖仅有 @opencode-ai/schema@opencode-ai/protocol 两个 workspace 包;effect 被声明为可选 peerDependency(peerDependenciesMeta 中标记 optional: true,版本固定为 4.0.0-beta.83),这保证纯 Promise 使用者不必安装 Effect。而 @opencode-ai/server@opencode-ai/core@opencode-ai/httpapi-codegen 均只出现在 devDependencies 中——README 所述“构建编译器读取 Server 的具体 API”正是通过开发期依赖实现的,不进入任何生产导入图。

2. 双入口架构

README 的 “Entrypoints” 一节定义了包的公开表面,与 package.jsonexports 字段一一对应:

入口 exports 映射 运行时依赖 定位
@opencode-ai/client ./src/index.ts 无 Effect、无 Core,仅 fetch 零 Effect 的 Promise 客户端
@opencode-ai/client/effect ./src/effect.ts Effect、Schema、Protocol 基于环境提供 HttpClient 的 Effect 网络客户端

2.1 Promise 根入口:结构化的 fetch 客户端

根入口 src/index.ts 只做两件事:转发 generated/index 的全部导出,并把生成的 EventsSubscribeOutput 类型以 OpenCodeEvent 别名再导出一次。生成后的 generated/client.ts 展示了这套客户端的完整骨架,可直接作为阅读参考:

export interface ClientOptions {
  readonly baseUrl: string
  readonly fetch?: typeof globalThis.fetch   // 可注入自定义 fetch(测试/代理场景)
  readonly headers?: HeadersInit             // 全局默认请求头
}

export interface RequestOptions {
  readonly signal?: AbortSignal             // 逐请求中止
  readonly headers?: HeadersInit
}

make(options) 工厂内部以 RequestDescriptor(method、path、query、headers、body、successStatusdeclaredStatuses)描述每个端点,请求处理链遵循清晰的错误分类:

  • 传输层异常(fetch 本身抛错)→ ClientError("Transport", { cause })
  • 响应状态命中契约声明的错误状态(declaredStatuses)→ 直接解析 JSON 错误体抛出;
  • 其他非预期状态 → ClientError("UnexpectedStatus", { cause: { status } })
  • SSE 端点额外校验 Content-Type 必须为 text/event-stream,否则抛 ClientError("UnsupportedContentType")

这种“按契约声明的状态码分流”的语义,是纯手写 fetch 封装很容易缺失、而由代码生成保证一致性的典型收益。

2.2 /effect 入口:规范化解码值 + 环境注入的 HttpClient

README 强调 Effect 入口使用 canonical decoded values,如 Session.IDLocation.RefPrompt。这些数据类型来自轻量级 @opencode-ai/schema 包,并由 src/effect.ts 再导出——AgentLocationModelSessionSessionMessagePromptAbsolutePath/RelativePath 等二十余个命名导出都列在该文件中,其目标是让调用方只需依赖客户端公开表面,内部模型重组不会迫使调用方迁移。文件头部的注释还固化了一条架构约束:/effect 永远不得导入 Core 或 Server。

生成的 Effect 客户端 generated-effect/client.ts 基于 Effect 的 HttpApiClient.ForApi<typeof ClientApi>,把 HttpClientErrorSchemaErrorSse.Retry 统一归一为包的 ClientError,而 OpenCode.make({ baseUrl }) 即 README 示例中构造客户端的工厂。

README 中的官方示例(可直接复制运行,前提是本地或远端已有 OpenCode Server)完整展示了 Effect 侧调用方式:

import { AbsolutePath, Location, OpenCode, Prompt } from "@opencode-ai/client/effect"

const client = yield * OpenCode.make({ baseUrl: "https://opencode.example" })
yield *
  client.sessions.create({
    location: Location.Ref.make({ directory: AbsolutePath.make("/workspace") }),
  })
yield * client.sessions.prompt({ sessionID, prompt: Prompt.make({ text: "Hello" }) })

注意示例中三个要点:baseUrl 指向一个已运行的 OpenCode Server 实例;sessions.createlocation 参数用 Location.Ref.make 包装绝对路径,而非裸字符串;prompt 载荷同样通过 Prompt.make 构造——调用方全程接触的是规范化解码类型而非原始 JSON 结构。

3. 生成表面:覆盖了哪些 HTTP 组,又刻意排除了什么

“生成表面包含 Server 具体 API 的每一个标准 HTTP 组”并非空话。src/contract.tsmakeDefaultApi 从 Protocol 构建客户端本地的 ClientApi 投影,并维护了三份清单:

组名映射 groupNames:把 18 个服务端组键映射为客户端命名空间,例如 server.healthhealthserver.sessionsessionsserver.messagemessagesserver.ptyptysserver.projectCopyprojectCopies。这就是为什么 README 示例里能直接写 client.sessions.*

端点别名 endpointNames:对少量组内端点重命名,如 session.messageslistintegration.connect.keyconnectKeypermission.saved.removeremoveSaved,使生成物读起来符合 REST 客户端惯例。

显式排除 omitEndpointsnew Set(["fs.read", "pty.connect", "pty.connectToken"])。这与 README 中“PTY WebSocket 连接等自定义传输保持在通用 HTTP 客户端之外”的说明互为印证——文件读取与 PTY 建连走的是各自专用传输(WebSocket 等),不经过通用 HTTP 客户端表面。

中间件方面,contract 声明了 LocationMiddleware 与带错误声明(InvalidRequestErrorSessionNotFoundError)的 SessionLocationMiddleware。README 的职责划分表述是:“Protocol 拥有端点构建与中间件放置;Server 提供构建期 API 所用的具体中间件键”——即抽象契约归 Protocol,具体键归 Server,客户端包自身只持有一份客户端本地投影。

4. 代码生成链路:从 ClientApi 到两套产物

生成入口是 package.json"generate": "bun run script/build.ts",实现见 script/build.ts,核心流程为:

  1. compile(ClientApi, { groupNames, endpointNames, omitEndpoints }) 把契约编译为中间表示;
  2. emitPromise(contract, { outputTypes: ... }) 产出 Promise 客户端,其中特化了 events.subscribe 的输出类型:以 OpenCodeEventEncoded 命名并直接 import type@opencode-ai/protocol/groups/event,避免在生成物中复制事件联合类型;
  3. emitEffectImported(contract, { module: "../contract", api: "ClientApi" }) 产出 Effect 客户端,且让它 import { ClientApi } from "../contract"——这正是 README 所说“生成的 Effect 运行时导入一份由 Protocol 构建的客户端本地投影”;
  4. write 并发(concurrency: 2)落盘到 src/generatedsrc/generated-effect

“生成等价测试防止传输漂移”(generation-equivalence test)对应 test/contract-identity.test.ts;配合 check:generated 的 git 零差异断言,仓库对“契约、生成脚本、已提交产物”三者一致性做了双重锁定。

5. 打包边界测试:两套入口的依赖图谱

README 最后一句——“Promise 根保持结构化、无 Core 或 Effect 运行时依赖;/effect 仅依赖 Effect、Schema、Protocol,且对浏览器打包安全。打包边界测试强制约束这两套导入图”——有明确的测试佐证:test/import-boundaries.test.ts

该测试用 Bun bundler 以 browser 目标把每个入口打包成临时产物,读取 metafile 的 inputs 集合,然后断言:

  • 根入口 @opencode-ai/client 的 inputs 中 effectschemaprotocolcoreserver 五个目录的命中数全部为 0
  • /effect 入口的 inputs 中 effectschemaprotocol 命中数必须大于 0,而 coreserver 命中数必须为 0

换言之,这不是依赖 lint 层面的静态规则,而是真实浏览器打包结果的机器验证,保证客户端可安全嵌入浏览器 bundle 而不把服务端实现卷进来。包内测试还包括 promise.test.tseffect.test.ts,分别覆盖两条入口的行为正确性。

6. 实践要点小结

  • 选择入口:浏览器或任何不想引入 Effect 的场景用 @opencode-ai/client(纯 Promise + fetch,可注入自定义 fetch/headers/AbortSignal);Effect 运行时中需要结构化错误与环境注入 HttpClient 的场景用 @opencode-ai/client/effect
  • 类型来源:Effect 侧所有构造器(SessionLocationPromptAbsolutePath 等)从客户端入口本身导入即可,不必直接依赖 @opencode-ai/schema
  • 契约变更流程:修改 Protocol/Server 契约后先 bun run generatebun run check:generated;CI 中两者组合即可拦截产物漂移。
  • 不要手写生成物src/generatedsrc/generated-effect 均由 httpapi-codegen 产出,所有定制(组名、端点别名、排除项、中间件)都应回写到 src/contract.ts 后再重新生成。

综合来看,@opencode-ai/client 展示了 OpenCode 仓库中“契约单一来源 + 生成客户端 + 测试锁定边界”的完整工程范式:README 给出入口与示例,contract 定义表面与排除项,codegen 脚本产出双轨客户端,而 identity 测试与打包边界测试则把上述承诺固化为可执行的持续验证。

登录后查看全文
热门项目推荐
相关项目推荐