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-62、packages/sdk/js/src/client.ts:33-57、packages/opencode/src/plugin/index.ts:261-274。
2-5. 它的位置与最小源码路径
源码路径卡片
server/server.ts:58-67:in-process fetch。httpapi/api.ts:30-62:API group 组合。httpapi/server.ts:182-240:routes 和 runtime services 装配。groups/session.ts:312-363:session prompt/command/shell endpoint。handlers/event.ts:21-53:SSE event response。sdk/js/src/client.ts:33-57:SDK client wrapper。plugin/index.ts:43-55、261-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 语法复习
- 类型编程:
TriggerName从Hooks中筛选可 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. 费曼复述区
- RootHttpApi、InstanceHttpApi、EventApi 的差异是什么?
- SDK 为什么要处理 directory?
- Event API 为什么要 heartbeat?
- 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?