0-1. 学习目标与一句话

SDK/API/扩展层是外部接口层:server 组合 typed API 和 runtime services,SDK 包装 generated client,SSE 输出事件流,插件系统通过 typed hooks 让外部代码参与工具、模型参数、shell env 和事件处理。

来源:packages/opencode/src/server/routes/instance/httpapi/api.ts:30-62packages/sdk/js/src/client.ts:33-57packages/opencode/src/plugin/index.ts:261-274

2-5. 它的位置与最小源码路径

源码路径卡片

  1. server/server.ts:58-67:in-process fetch。
  2. httpapi/api.ts:30-62:API group 组合。
  3. httpapi/server.ts:182-240:routes 和 runtime services 装配。
  4. groups/session.ts:312-363:session prompt/command/shell endpoint。
  5. handlers/event.ts:21-53:SSE event response。
  6. sdk/js/src/client.ts:33-57:SDK client wrapper。
  7. plugin/index.ts:43-55261-274:Plugin interface 和 trigger。

6. 用户输入到 agent 行动的整体链路

CLI / UI / Plugin
  -> JS SDK
  -> typed HTTP endpoint
  -> HttpApi handler
  -> Effect services
  -> SessionPrompt / ToolRegistry / Provider / Permission
  -> Bus events
  -> SSE /event
  -> SDK event stream

Java 类比

HttpApiGroup 像 Spring Controller group;Effect Layer 像 auto configuration;JS SDK 像 OpenAPI generated client wrapper;SSE 像 WebFlux text/event-stream;Plugin hook 像 SPI + interceptor。

7. 核心源码逐段讲解

API group 组合

export const InstanceHttpApi = HttpApi.make("opencode-instance")
  .addHttpApi(ConfigApi)
  .addHttpApi(FileApi)
  .addHttpApi(PermissionApi)
  .addHttpApi(ProviderApi)
  .addHttpApi(SessionApi)
  .addHttpApi(SyncApi)
  .addHttpApi(V2Api)
  .addHttpApi(TuiApi)
  .middleware(SchemaErrorMiddleware)

export const OpenCodeHttpApi = HttpApi.make("opencode")
  .addHttpApi(RootHttpApi)
  .addHttpApi(EventApi)
  .addHttpApi(InstanceHttpApi)
  .addHttpApi(PtyConnectApi)

路径:packages/opencode/src/server/routes/instance/httpapi/api.ts:36-59

Routes 装配 runtime

return Layer.mergeAll(rootApiRoutes, eventApiRoutes, instanceRoutes, docRoute, uiRoute).pipe(
  Layer.provide([
    Agent.defaultLayer,
    Config.defaultLayer,
    File.defaultLayer,
    LSP.defaultLayer,
    Permission.defaultLayer,
    Plugin.defaultLayer,
    Provider.defaultLayer,
    Session.defaultLayer,
    SessionPrompt.defaultLayer,
    ToolRegistry.defaultLayer,
    Bus.layer,
  ]),
  Layer.provide(InstanceLayer.layer),
)

路径:packages/opencode/src/server/routes/instance/httpapi/server.ts:182-240

Session prompt endpoint

HttpApiEndpoint.post("prompt", SessionPaths.prompt, {
  params: { sessionID: SessionID },
  query: WorkspaceRoutingQuery,
  payload: PromptPayload,
  success: described(MessageV2.WithParts, "Created message"),
  error: [HttpApiError.BadRequest, ApiNotFoundError],
})

路径:packages/opencode/src/server/routes/instance/httpapi/groups/session.ts:312-324

SSE event handler

const events = (yield* bus.subscribeAll()).pipe(
  Stream.takeUntil((event) => event.type === Bus.InstanceDisposed.type),
)
const heartbeat = Stream.tick("10 seconds").pipe(
  Stream.drop(1),
  Stream.map(() => ({ id: Bus.createID(), type: "server.heartbeat", properties: {} })),
)

return HttpServerResponse.stream(
  Stream.make({ id: Bus.createID(), type: "server.connected", properties: {} }).pipe(
    Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
    Stream.map(eventData),
    Stream.pipeThroughChannel(Sse.encode()),
  ),
  { contentType: "text/event-stream" },
)

路径:packages/opencode/src/server/routes/instance/httpapi/handlers/event.ts:21-53

SDK client wrapper

export function createOpencodeClient(config?: Config & { directory?: string }) {
  if (config?.directory) {
    config.headers = {
      ...config.headers,
      "x-opencode-directory": encodeURIComponent(config.directory),
    }
  }

  const client = createClient(config)
  client.interceptors.request.use((request) => rewrite(request, config?.directory))
  client.interceptors.error.use(wrapClientError)
  return new OpencodeClient({ client })
}

路径:packages/sdk/js/src/client.ts:33-57

Plugin trigger

const trigger = Effect.fn("Plugin.trigger")(function* <Name extends TriggerName>(name: Name, input: Input, output: Output) {
  const s = yield* InstanceState.get(state)
  for (const hook of s.hooks) {
    const fn = hook[name] as any
    if (!fn) continue
    yield* Effect.promise(async () => fn(input, output))
  }
  return output
})

路径:packages/opencode/src/plugin/index.ts:261-274

8. 关键 TypeScript 语法复习

  • 类型编程:TriggerNameHooks 中筛选可 trigger 的 hook 名。
  • Parameters<Fn>:抽取函数参数类型。
  • ReturnType<typeof createOpencodeClient>:抽取函数返回类型。
  • namespace:把类型和函数组织到同一命名空间。
  • async stream:SSE 被 SDK 消费成异步事件流。
  • Effect Layer:类似 DI 容器装配,但显式写在代码里。

9-10. 架构思想与模块协作

  • Typed API contract:endpoint 明确 payload/success/error schema。
  • Generated SDK + wrapper:生成客户端外再加 directory/error/fetch 适配。
  • SSE event bridge:Bus events 对外变成 event stream。
  • Plugin SPI:typed hooks 接入工具、模型参数、shell env、事件。
  • In-process adapter:local CLI/插件可直接调用 server handler。

11. mini agent 对应代码

app.post("/session/:id/prompt", (req) => sessionPrompt.prompt(req.params.id, req.body))
app.get("/event", () => sse(bus.subscribeAll()))

export function createMiniClient(baseUrl) {
  return {
    session: { prompt: (id, body) => post(`${baseUrl}/session/${id}/prompt`, body) },
    event: () => connectSse(`${baseUrl}/event`),
  }
}

async function trigger(name, input, output) {
  for (const hook of hooks) await hook[name]?.(input, output)
  return output
}

12. 费曼复述区

  1. RootHttpApi、InstanceHttpApi、EventApi 的差异是什么?
  2. SDK 为什么要处理 directory?
  3. Event API 为什么要 heartbeat?
  4. Plugin.trigger 和 monkey patch 的差异是什么?

13-15. 练习与自测

  • 入门:列出 OpenCodeHttpApi 包含的 group。
  • 进阶:把 createRoutes 的 service layer 分组。
  • 源码追踪:从 client.session.prompt 追到 handler。
  • 小实现:写一个 mini SDK/API,包含 prompt、permission、event、plugin trigger。
  • 自测:如果插件要修改 LLM temperature,应该接哪个 hook?

16. 下一步阅读建议

继续读 “测试与工程化”。API/SDK/Plugin 已经说明系统边界,工程化章会看这个多 package 项目如何构建、检查和测试。