OpenCode 源码解析
第 4 讲:Server 网络层与路由
基于 dev 分支源码 · 2026-07-10
📖 开篇导读
第 1 讲俯瞰了 Monorepo 全景,第 2 讲拆解了 CLI 入口,第 3 讲深入了 Session 会话核心。今天转向 Server 网络层——OpenCode 面向外部世界的 HTTP API 门面。Server 层横跨 packages/server/(28 个文件,1682 行)和 packages/protocol/(22 个文件,1581 行),总计约 3263 行源码。它基于 Effect 的 HttpApiBuilder 构建了完整的 RESTful API,包含 20 个 Handler 组、3 层中间件和完整的认证/CORS 体系。
一、Server 的两包分工
OpenCode 的网络层分为两个包:Protocol 定义 API 契约,Server 实现路由与处理逻辑。这种分离让 API 规范可以被 CLI、TUI、Desktop 等客户端独立引用。
packages/protocol/
API 契约层:Endpoint 定义、Schema 校验、错误类型、中间件声明
⬇️ 被引用
packages/server/
路由实现层:Handler 逻辑、中间件实例化、认证、CORS、WebSocket
⬇️ 依赖
packages/core/
业务服务层:SessionV2、Permission、Pty、Event 等 Effect Service
二、路由装配:routes.ts 的七层依赖注入
packages/server/src/routes.ts(68 行)是整个 Server 的装配中心。它通过 Effect 的 Layer 体系将 7 层依赖从底到顶堆叠起来:
function makeRoutes(auth) {
const serviceLayer = AppNodeBuilder.build(
applicationServices,
[[SessionExecution.node, SessionExecutionLocal.node]]
)
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" })
.pipe(
Layer.provide(handlers), // ① Handler 组
Layer.provide(sessionLocationLayer), // ② Session 级 Location
Layer.provide(locationLayer), // ③ 全局 Location
Layer.provide(authorizationLayer), // ④ 认证中间件
Layer.provide(schemaErrorLayer), // ⑤ Schema 校验
Layer.provide(auth), // ⑥ 认证配置
Layer.provide(serviceLayer), // ⑦ 业务服务
)
}
这 7 层从下到上的依赖关系非常清晰:最底层的 serviceLayer 提供 Database、SessionV2、EventV2 等 10 个核心服务;往上依次是认证配置、Schema 校验、授权中间件、Location 解析,最顶层是 20 个 Handler 组。这样的设计让每个 Handler 只需要声明自己依赖的 Service,不需要关心服务如何创建。
三、Protocol 层:API 契约的定义方式
packages/protocol/src/api.ts(86 行)是 API 契约的装配入口。它使用 Effect 的 HttpApi.make("server") 将 18 个 Group 组合成一个完整的 API:
HttpApi.make("server")
.add(HealthGroup)
.add(LocationGroup.middleware(locationMiddleware))
.add(AgentGroup.middleware(locationMiddleware))
.add(makeSessionGroup(sessionLocationMiddleware))
.add(MessageGroup.middleware(sessionLocationMiddleware))
.add(ModelGroup.middleware(locationMiddleware))
// ... 更多 Group
.middleware(Authorization)
.middleware(SchemaErrorMiddleware)
注意 .middleware()` 的两种用法:
1. Group.middleware(...)——Group 级别的中间件,只对该 Group 生效(如 Location、SessionLocation)
2. Api.middleware(...)——全局中间件,对所有 Endpoint 生效(如 Authorization、SchemaError)
四、20 个 Handler 组全景
packages/server/src/handlers.ts(40 行)通过 Layer.mergeAll 合并了 20 个 Handler,每个 Handler 对应一个业务领域:
| Handler | 文件 | 核心端点 | 行数 |
|---|---|---|---|
| HealthHandler | health.ts | GET /health | 7 |
| SessionHandler | session.ts | session.list/create/prompt/compact/wait/interrupt 等 | 385 |
| MessageHandler | message.ts | session.messages(游标分页) | 81 |
| PtyHandler | pty.ts | pty.create/connect(WebSocket) | 223 |
| EventHandler | event.ts | event.subscribe(SSE) | 52 |
| PermissionHandler | permission.ts | permission.ask/reply/list | 98 |
| QuestionHandler | question.ts | question.reply/reject | 62 |
| IntegrationHandler | integration.ts | integration.connect.key/oauth | 104 |
| FileSystemHandler | fs.ts | fs.read/list/find | 39 |
| 其他 Handler | model/agent/skill/command/credential/reference/location/project-copy | 各 8-22 行 | ~131 |
五、SessionHandler:最大的 Handler
session.ts(385 行)是 Server 中最大的 Handler 文件,暴露了 16 个端点。它的模式非常统一——每个端点通过 Effect.gen 获取 SessionV2.Service,然后调用对应方法,最后用 Effect.catchTag 将 Core 层的错误映射为 Protocol 层的 HTTP 错误:
.handle("session.prompt", Effect.fn(function* (ctx) {
return {
data: yield* session.prompt({
sessionID: ctx.params.sessionID,
prompt: ctx.payload.prompt,
delivery: ctx.payload.delivery,
resume: ctx.payload.resume,
}).pipe(
Effect.catchTag("Session.NotFoundError", (error) =>
new SessionNotFoundError({
sessionID: error.sessionID,
message: `Session not found: ${error.sessionID}`,
}),
),
Effect.catchTag("Session.PromptConflictError", (error) =>
new ConflictError({
message: `Prompt message ID conflicts...`,
resource: error.messageID,
}),
),
),
}
}))
这种模式在所有 Handler 中保持一致——Core 层抛出语义化错误标签(如 Session.NotFoundError),Server 层将其转换为带 HTTP 状态码的 Protocol 错误(如 SessionNotFoundError 对应 404)。
六、三个核心中间件
Server 层实现了三个关键中间件,分别解决认证、位置解析和 Schema 校验。
6.1 Authorization 中间件
middleware/authorization.ts(58 行)支持两种认证方式:
1. HTTP Basic Auth——从 Authorization 请求头解析 Base64 编码的用户名/密码
2. URL Token——从 auth_token 查询参数解析(用于 WebSocket 等无法携带自定义 Header 的场景)
认证配置来自 auth.ts(63 行),支持环境变量 OPENCODE_SERVER_PASSWORD 和 OPENCODE_SERVER_USERNAME。如果未设置密码,认证中间件退化为透传(所有请求放行)。
function credentialFromRequest(request) {
const url = new URL(request.url, "http://localhost")
const token = url.searchParams.get("auth_token")
if (token) return decodeCredential(token)
const match = /^Basic\s+(.+)$/i.exec(request.headers.authorization ?? "")
if (match) return decodeCredential(match[1])
return Effect.succeed(emptyCredential())
}6.2 Location 中间件
location.ts(60 行)实现了请求级别的 Location 解析。它从查询参数或请求头中提取 directory 和 workspaceID,然后通过 LocationServiceMap 获取对应的工作空间服务:
function ref(request) {
const query = new URL(request.url, "http://localhost").searchParams
const workspaceID = query.get("location[workspace]")
|| request.headers["x-opencode-workspace"]
const directory = query.get("location[directory]")
|| request.headers["x-opencode-directory"]
|| process.cwd()
return Location.Ref.make({
directory: AbsolutePath.make(directory),
workspaceID: workspaceID ? WorkspaceV2.ID.make(workspaceID) : undefined,
})
}
SessionLocationMiddleware(middleware/session-location.ts,67 行)更进一步——它从数据库查询 Session 的存储位置,确保后续操作在正确的上下文中执行。这对于多项目、多工作空间场景至关重要。
6.3 Schema Error 中间件
middleware/schema-error.ts(20 行)拦截所有 Schema 校验失败,将原始错误截断到 1024 字符后转换为 InvalidRequestError(HTTP 400)。这防止了超长校验信息泄露内部实现细节。
七、SSE 事件推送与 WebSocket PTY
Server 层支持两种实时通信通道:
1. SSE(Server-Sent Events)——EventHandler(52 行)通过 event.subscribe 端点推送实时事件。它使用 Effect 的 Sse.encode() 编码事件,配合 15 秒心跳保活,容量为 256 的有界流防止内存泄漏:
const output = Stream.unwrap(Effect.gen(function* () {
const live = yield* EventV2.allBounded(events, 256)
return Stream.make(connected).pipe(Stream.concat(live))
}))
.pipe(Stream.map(eventData), Stream.pipeThroughChannel(Sse.encode()))
const heartbeat = Stream.tick("15 seconds")
.pipe(Stream.map(() => ": heartbeat\n\n"))
return HttpServerResponse.stream(
output.pipe(Stream.merge(heartbeat), Stream.encodeText),
{ contentType: "text/event-stream" })2. WebSocket PTY——PtyHandler(223 行)是最复杂的 Handler,实现了完整的终端会话管理。它使用 Ticket 机制防止跨站伪造:pty.connectToken 签发一次性票据,pty.connect 消费票据后升级 WebSocket 连接。数据流通过无界队列 Queue.unbounded 保证回放、实时输出和关闭帧的顺序一致性。
八、CORS 安全策略
cors.ts(34 行)定义了严格的跨域策略,允许的 Origin 包括:
🔹 localhost: 和 127.0.0.1:——本地开发
🔹 oc://renderer——OpenCode 桌面应用内部渲染进程
🔹 tauri://localhost——Tauri 桌面运行时
🔹 *.opencode.ai——官方云服务域名
🔹 自定义 CORS 白名单(通过 CorsConfig 注入)
九、错误体系
packages/protocol/src/errors.ts(111 行)定义了 11 种标准错误类型,全部继承自 Effect Schema 的 TaggedErrorClass,每种错误映射到对应的 HTTP 状态码:
| 错误类型 | HTTP 状态码 | 用途 |
|---|---|---|
| InvalidRequestError | 400 | Schema 校验失败、参数错误 |
| UnauthorizedError | 401 | 认证失败 |
| ForbiddenError | 403 | CORS 拒绝、PTY Ticket 无效 |
| SessionNotFoundError | 404 | Session 不存在 |
| ConflictError | 409 | Prompt 消息 ID 冲突 |
| ServiceUnavailableError | 503 | 操作不可用(如 Session 未就绪) |
| UnknownError | 500 | 未知服务器错误(附带 ref 追踪) |
十、关键设计总结
🔹 Protocol/Server 分离——API 契约与实现解耦,客户端可独立引用 Protocol 包生成类型安全的请求代码
🔹 Effect Layer 依赖注入——7 层依赖从下到上堆叠,Handler 只需声明依赖、不关心创建逻辑
🔹 错误标签映射——Core 层抛出语义化错误标签,Server 层统一映射为 HTTP 状态码
🔹 Location 中间件——请求级工作空间解析,支持多项目多工作空间隔离
🔹 双通道实时通信——SSE 推送事件 + WebSocket 承载 PTY 终端
🔹 OpenAPI 自动文档——/openapi.json 端点自动生成 API 文档
📋 系列导航
← 第 3 讲:Session 会话核心
→ 第 5 讲:Plugin 插件系统
关注公众号获取更多技术干货
源码地址:https://github.com/opencode-ai/opencode
夜雨聆风