首页
/ opencode 服务器包拆分实践:基于 Effect HttpApi 的 @opencode-ai/server 独立化路线

opencode 服务器包拆分实践:基于 Effect HttpApi 的 @opencode-ai/server 独立化路线

2026-09-06 15:43:47作者:裴麒琰

本文基于 opencode 仓库中的规格文档 server-package.md,系统讲解 opencode 的 HTTP 服务器如何从 packages/opencode 中逐步剥离为独立的 @opencode-ai/server 工作区包。读完本文,你将理解:为什么必须“不要制造包循环”、契约(contract)与宿主(host)之间的依赖方向应如何设计、五步 PR 拆分序列的具体依据,以及当前仓库中 packages/server 已经落地的“可嵌入服务器 API”的真实形态,便于你在类似 Monorepo 中复刻这一拆分策略。

一、背景:服务器代码的“现状”与拆分动机

server-package.md 的开篇即明确了本文档的定位:这是“在 opencode 服务器迁移到 Effect HttpApi 后端之后,面向未来 packages/server 拆分的实践参考”。规格文档列出了拆分启动前的五项现状,每一条都能在当前仓库中找到对应实现:

  • 服务器仍位于 packages/opencode。入口是 packages/opencode/src/server/server.ts,其中的 listen(opts) 负责监听端口(支持可选的 mDNS 广播),Default 导出了一个基于 HttpApiApp.webHandler().handler 的 fetch 式应用,openapi() 则通过 OpenApi.fromApi(PublicApi) 生成 OpenAPI 文档。
  • 运行时与应用层被集中到两个文件packages/opencode/src/effect/app-runtime.tsAppNodeBuilderV1.build(LayerNode.group([...])) 组装出 AppLayer,一次性聚合了 DatabaseSessionAgentProviderPluginLLM 等数十个领域服务;packages/opencode/src/effect/run-service.ts 则提供了 attach / attachWith(把 InstanceRefWorkspaceRef 注入到 Effect 上下文)与 makeRuntime(构造带 memoMapManagedRuntime,暴露 runSync / runPromise / runFork / runCallback 等边界方法)。
  • 路由树位于 src/server/routes/instance/httpapi,由 src/server/server.ts 承载。实际的 group/handler 文件在 packages/opencode/src/server/routes/instance/httpapi 目录中,按 config.tssession.tsevent.tspty.ts 等主题划分为 20 余个 group 与同名 handler 模块。
  • OpenAPI 生成基于 HttpApi 契约 + 兼容性转换,转换逻辑集中在 packages/opencode/src/server/routes/instance/httpapi/public.ts。这正是规格文档反复强调要“持续收缩”的兼容 shim 层。
  • 规格撰写时尚无独立的 packages/server 工作区

二、目标包布局:五个各司其职的工作区

规格文档给出的“Future State”是一个五包目标布局,其设计意图是让“领域、传输、入口、客户端、扩展面”互不越界:

目标包 职责
packages/core 共享的领域服务与 schema
packages/server HTTP 契约、处理器(handlers)、OpenAPI 生成,以及可嵌入的服务器 API
packages/cli TUI 与 CLI 入口
packages/sdk 由服务器 OpenAPI 规范生成
packages/plugin 插件编写面(authoring surface)

对照当前仓库,这个布局已经基本成型:packages/corepackages/serverpackages/clipackages/sdkpackages/plugin 均已存在。尤其值得关注的是 packages/server/package.json 的依赖声明:

{
  "name": "@opencode-ai/server",
  "version": "1.18.29",
  "private": true,
  "dependencies": {
    "@opencode-ai/core": "workspace:*",
    "@opencode-ai/protocol": "workspace:*",
    "drizzle-orm": "catalog:",
    "effect": "catalog:"
  }
}

注意它只依赖 coreprotocol,不依赖 @opencode-ai/opencode。这个依赖方向就是下一条核心规则的落地形态。

三、拆分的核心规则:绝不制造包循环

规格文档将整条拆分策略压缩成一句话:“Do not create a package cycle.”(不要制造包循环),并给出在“足够多的共享服务代码离开 packages/opencode 之前”,未来 packages/server 必须二选一的约束:

  1. 只拥有纯粹的 HttpApi 契约(pure HttpApi contracts only);或
  2. 接受由宿主(packages/opencode)提供的 services / layers / callbacks

同时明确禁止的反模式是:packages/server 一边 import packages/opencode 的 services,一边又被 packages/opencode import 去承载路由——两者互为依赖即成环,TypeScript 编译、打包与测试隔离都会随之劣化。

当前仓库提供了两处可以直接验证该规则的证据:

证据一:依赖方向的静态约束。 如上所述,@opencode-ai/server 的依赖清单里没有 @opencode-ai/opencode;而 packages/opencode/src/server/server.ts 中可以看到反方向的引用:

import type { CorsOptions } from "@opencode-ai/server/cors"

opencode → server 的引用是被允许的(宿主承载路由时引用被拆出的包),而 server → opencode 是被规格明确禁止的。

证据二:“宿主注入”模式的实际实现。 packages/server/src/routes.ts 展示了“接受宿主提供 services”这条路线的完整形态——它并不 import 任何 opencode 内部实现,而是从 @opencode-ai/coreeffect 组装自己的服务层:

const applicationServices = LayerNode.group([
  Database.node,
  EventV2.node,
  httpClient,
  ToolOutputStore.cleanupNode,
  SessionV2.node,
  PermissionSaved.node,
  PtyTicket.node,
  Credential.node,
  PtyEnvironment.node,
  LocationServiceMap.node,
])

export function createRoutes(password?: string) {
  return makeRoutes(
    password
      ? ServerAuth.Config.configLayer({ username: "opencode", password: Option.some(password) })
      : ServerAuth.Config.layer,
  )
}

export function createEmbeddedRoutes() {
  return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }))
}

这里有三个值得注意的设计点:

  • applicationServices 是一个 LayerNode.group,只聚合 core 层的服务节点,SessionExecution 通过 AppNodeBuilder.build(..., [[SessionExecution.node, SessionExecutionLocal.node]]) 以“接口 + 本地实现”的替换方式接入,避免把某个具体实现的节点硬编码进 group。
  • 认证是参数化的createRoutes(password?) 面向独立监听场景,密码可空;createEmbeddedRoutes() 则固定为 Option.none()(不启用密码认证),专为被宿主内嵌的场景准备。
  • 暴露 Web 处理器而非端口,这正是规格中“embeddable server API”一词的落地:
export const routes = createRoutes()

export const webHandler = () =>
  HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)), { disableLogger: true })

webHandler() 返回 toWebHandler 包装好的 handler,宿主可以自行决定挂在哪个 Node server / 端口 / 代理之后——服务器包只交付“请求进、响应出”的能力,把“在哪里托管”的决策权完全留给宿主。

契约本体则进一步下沉到了 protocol 包:packages/server/src/api.ts 全文仅 8 行:

import { makeDefaultApi } from "@opencode-ai/protocol/api"
import { LocationMiddleware } from "./location"
import { SessionLocationMiddleware } from "./middleware/session-location"

export const Api = makeDefaultApi({
  locationMiddleware: LocationMiddleware,
  sessionLocationMiddleware: SessionLocationMiddleware,
})

也就是说,HttpApi 的“形状”(endpoint、schema、错误契约)由 @opencode-ai/protocol 统一生产,packages/server 只负责注入两个与位置(Location)语义相关的中间件。这比规格设想的“只拥有纯契约”走得更彻底:连契约的生成逻辑也独立在了 server 包之外。

四、处理器工厂:Layer.mergeAll 的分组装配

规格中 PR 序列的第 4 步要求“在 handler 工厂的服务依赖可以被宿主层供给、而不是直接 import 之后,才提取 handler 工厂”。packages/server/src/handlers.ts 展示了这一步的完成态——18 个 handler 分组以 Effect Layer 的形式各自独立,最后用 Layer.mergeAll 合并:

export const handlers = Layer.mergeAll(
  HealthHandler,
  LocationHandler,
  AgentHandler,
  SessionHandler,
  MessageHandler,
  ModelHandler,
  ProviderHandler,
  IntegrationHandler,
  CredentialHandler,
  PermissionHandler,
  FileSystemHandler,
  CommandHandler,
  SkillHandler,
  EventHandler,
  PtyHandler,
  QuestionHandler,
  ReferenceHandler,
  ProjectCopyHandler,
)

每个 handler 对应 packages/server/src/handlers/ 下的一个文件(agent.tssession.tspty.ts 等),依赖在各自 Layer 内部声明,由 makeRoutes 中的 Layer.provide(handlers, ...) 统一解析。配套的横切中间件位于 packages/server/src/middleware/authorization.ts(认证)、schema-error.ts(schema 校验错误的统一映射)、session-location.ts(会话与位置解析)。这种“每个 group 自带依赖声明 + mergeAll 装配 + 统一 provide”的结构,使得未来新增或移除任何一个 API 分组都不需要触碰其余 handler 的代码,符合 routes.md 中“稳定服务在 handler layer 构建时一次性 yield,请求级只提供派生上下文”的模式约定。

五、建议的 PR 序列:五步走法与当前进度

规格文档给出的五步序列,本质是“按依赖倒序、从最纯的模块开始”的拆分纪律:

  1. 持续收缩 public.ts 中的 OpenAPI 兼容 shim。 该文件承担 SDK/OpenAPI 兼容转换;routes.md 进一步给出操作准则:每收紧一个 source schema 就删掉一个 workaround,OpenAPI 可见 schema 变化时要确认生成的 SDK diff 是有意的,且“优先修 source schema,而不是新增后处理规则”。
  2. 把稳定的领域 schema 移入共享包,前提是它们不再依赖 opencode 本地运行时模块。 当前 packages/schemapackages/core 就是这类 schema/领域代码的落点。
  3. 当契约模块可以不再 import packages/opencode 的任何实现细节时,将纯 HttpApi 契约模块提取进 packages/server 从当前仓库看,契约生成已进一步下沉到 packages/protocolpackages/server/src/api.ts 只是薄封装,这一步可以视为已完成。
  4. 在服务依赖可以由宿主层供给之后,提取 handler 工厂。 对应 packages/server/src/handlers/ 的 18 个分组实现,依赖全部来自 core,同样已落地。
  5. 最后才移动服务器托管(hosting),且必须在包归属清晰之后。 当前 packages/opencode/src/server/server.tslisten / Default / openapi 仍留在 opencode 包内,符合“hosting last”的排序。

换言之,对照规格的五步清单:第 1 步是长期任务(shim 尚未清零),第 3、4 步已经由 protocol + server 两个包承接,第 5 步(托管迁移)被刻意保留在 opencode 内,等待包边界进一步固化。

六、非目标(Non-Goals):三条明确的“不做”

规格文档用一整节 Non-Goals 锁死了拆分的边界,这三条对后续维护者依然有效:

  • 不要复活旧的双后端(dual-backend)迁移形态。 即不再维护两套并行的后端实现;这也与 routes.md 中“保留 { name, data } 错误体直到一次有意的破坏性 API 变更”“通用 middleware 不做领域错误映射”等约束一脉相承——迁移是一次性的,而不是长期并存的双轨。
  • 不要在服务依赖拥有清晰的包边界之前先拆服务器托管。 即 hosting 是最后一步,不能因为“看起来好拆”就提前动 server.tslisten/openapi 导出面。
  • 不要在生成物兼容性被证实之前切换 SDK 生成来源。 packages/sdk 的生成链路(经由 OpenAPI)只有在确认输出保持兼容后,才允许切换到新的包产出。

七、如何在仓库中验证与继续阅读

如果你希望亲手验证本文所述的包边界与拆分进度,以下路径是最直接的入口(仓库为只读,以下均为查看方式):

  1. 依赖方向:对比 packages/server/package.json(依赖 coreprotocol)与 packages/opencode/package.json,确认 server → opencode 方向不存在;再在 packages/opencode/src/server/server.ts 中检索 @opencode-ai/server,查看宿主对被拆包的反向引用(目前为 CORS 选项类型)。
  2. 可嵌入 API 的形态:阅读 packages/server/src/routes.tscreateRoutes / createEmbeddedRoutes / webHandler 三个导出,理解“带密码认证”与“内嵌免密”两种装配方式的差异。
  3. 契约下沉路径:从 packages/server/src/api.ts 追到 packages/protocolmakeDefaultApi,再对照 packages/opencode/src/server/routes/instance/httpapi/public.ts 中尚未收缩的兼容转换层。
  4. 装配与中间件:查看 packages/server/src/handlers.tspackages/server/src/middleware/ 下的三个中间件文件。
  5. 规范文档族:server-package.md 所在目录 packages/opencode/specs/effect/ 还有配套的路由模式(routes.md)、迁移模式(migration.md)与总体路线图(todo.md),是理解本次拆分约束的上下文文档。

八、小结

server-package.md 虽然篇幅不长,却为 opencode 的服务器独立化定下了可执行的纪律:以“不产生包循环”为唯一铁律,用“纯契约 / 宿主注入”两条互斥路线约束 packages/server 的职责,再以五步 PR 序列保证每一次移动都有独立的编译与兼容性验证点,最后用三条 Non-Goals 排除掉“双后端长期并存”“过早拆托管”“抢跑 SDK 切换”这三类典型返工路径。从当前仓库的实际状态看,契约(下沉至 packages/protocol)与 handler 工厂(packages/server/src/handlers/)已经完成迁移并以 webHandler() 形式提供了可嵌入 API,而托管逻辑仍按规格留驻在 packages/opencode 中等待最后一步——这正是该文档所描述的路线在真实代码中的兑现过程。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388