内部模块: Server (HTTP 服务器)

OpenCode 的 HTTP API 服务器,提供 REST API、WebSocket 和 SSE 实时通信。

1. 概览 (Overview)

  • 路径: packages/opencode/src/server/
  • 定位: OpenCode 的网络接口层,所有客户端(Desktop, Web, CLI, VS Code 等)都通过 Server 与核心通信
  • 技术栈:
    • Hono - 高性能 Web 框架
    • WebSocket - 双向实时通信
    • SSE (Server-Sent Events) - 服务端推送
    • OpenAPI - API 文档自动生成

核心文件列表

文件行数职责
server.ts~94,353主服务器,包含所有 REST API 路由
tui.ts~1,757Terminal UI 专用路由(CLI 模式)
project.ts~2,193项目管理路由
question.ts~2,402用户交互问答路由
mdns.ts~1,295mDNS 服务发现
error.ts~839错误处理和标准化响应

2. 服务器架构

2.1 启动流程

2.2 中间件栈

// src/server/server.ts (简化)
const app = new Hono()
  // 1. 错误处理中间件
  .onError((err, c) => {
    if (err instanceof NamedError) {
      return c.json(err.toObject(), { status: getStatusCode(err) })
    }
    return c.json(new NamedError.Unknown({ message: err.stack }).toObject(), {
      status: 500,
    })
  })
  
  // 2. 日志中间件
  .use(async (c, next) => {
    log.info("request", { method: c.req.method, path: c.req.path })
    const timer = log.time("request")
    await next()
    timer.stop()
  })
  
  // 3. CORS 中间件
  .use(cors({
    origin(input) {
      // 允许 localhost
      if (input.startsWith("http://localhost:")) return input
      if (input.startsWith("http://127.0.0.1:")) return input
      
      // 允许 Tauri
      if (input === "tauri://localhost") return input
      
      // 允许 *.opencode.ai
      if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
        return input
      }
      
      return // 拒绝其他来源
    },
  }))
  
  // 4. 注册路由...

3. 路由系统

OpenCode 的 API 按功能模块组织成多个路由组:

3.1 路由分类

路由前缀模块职责
/global/*全局健康检查、全局事件、实例管理
/project/*项目项目列表、当前项目、项目更新
/session/*会话会话 CRUD、消息管理、对话流程
/message/*消息消息 CRUD、Part 更新
/config/*配置配置读取和更新
/provider/*提供商LLM 提供商和模型列表
/agent/*AgentAgent 列表和创建
/tool/*工具工具注册表和执行
/file/*文件文件系统操作
/lsp/*LSP代码智能功能
/mcp/*MCPMCP 服务器管理
/pty/*终端伪终端管理
/tui/control/*TUITerminal UI 控制(CLI 模式)
/question/*问答用户交互问答

3.2 核心 API 端点详解

Global 路由

// GET /global/health - 健康检查
{
  "healthy": true,
  "version": "1.0.0"
}
 
// GET /global/event - 全局 SSE 事件流
// 返回 text/event-stream
data: {"directory": "/path", "payload": {"type": "session.updated", ...}}
data: {"directory": "/path", "payload": {"type": "server.heartbeat", ...}}
 
// POST /global/dispose - 清理所有实例
// 返回 true

Project 路由

// GET /project/ - 列出所有项目
[
  {
    "id": "proj_xxx",
    "directory": "/path/to/project",
    "name": "My Project",
    "icon": "📦",
    "color": "#3b82f6"
  }
]
 
// GET /project/current - 获取当前项目
{
  "id": "proj_xxx",
  "directory": "/Users/...",
  ...
}
 
// PATCH /project/:projectID - 更新项目
{
  "name": "New Name",
  "icon": "🚀",
  "color": "#10b981"
}

Session 路由

// POST /session - 创建新会话
{
  "directory": "/path/to/project",
  "title": "Implement authentication"
}
// 返回 Session.Info
 
// GET /session/:sessionID - 获取会话详情
{
  "id": "ses_xxx",
  "title": "...",
  "summary": { "additions": 100, "deletions": 20, "files": 5 },
  ...
}
 
// POST /session/:sessionID/prompt - 发送消息并触发 Agent
{
  "parts": [
    { "type": "text", "text": "帮我实现登录功能" }
  ],
  "agent": "build",  // 可选
  "model": {         // 可选
    "providerID": "anthropic",
    "modelID": "claude-sonnet-4"
  }
}
 
// GET /session/:sessionID/event - 会话 SSE 事件流
// 实时推送会话状态变化

Message 路由

// GET /session/:sessionID/message - 列出所有消息
[
  {
    "id": "msg_xxx",
    "role": "user",
    "parts": [...]
  },
  {
    "id": "msg_yyy",
    "role": "assistant",
    "parts": [...]
  }
]
 
// GET /message/:messageID - 获取消息详情
{
  "id": "msg_xxx",
  "sessionID": "ses_xxx",
  "role": "assistant",
  "parts": [
    { "type": "text", "text": "我会帮你..." },
    { "type": "tool", "tool": "read", "args": {...}, "state": {...} }
  ],
  "tokens": { "input": 1000, "output": 500 }
}

4. 实时通信机制

4.1 SSE (Server-Sent Events)

OpenCode 使用 SSE 实现服务端推送,让客户端实时获取状态更新。

全局事件流

// GET /global/event
export const GlobalEventRoute = app.get("/global/event", async (c) => {
  return streamSSE(c, async (stream) => {
    // 1. 发送连接确认
    await stream.writeSSE({
      data: JSON.stringify({
        payload: { type: "server.connected", properties: {} }
      })
    })
    
    // 2. 订阅全局事件总线
    async function handler(event: any) {
      await stream.writeSSE({
        data: JSON.stringify(event)
      })
    }
    GlobalBus.on("event", handler)
    
    // 3. 心跳(防止超时)
    const heartbeat = setInterval(() => {
      stream.writeSSE({
        data: JSON.stringify({
          payload: { type: "server.heartbeat", properties: {} }
        })
      })
    }, 30000)  // 每 30 秒
    
    // 4. 清理
    await new Promise<void>((resolve) => {
      stream.onAbort(() => {
        clearInterval(heartbeat)
        GlobalBus.off("event", handler)
        resolve()
      })
    })
  })
})

会话事件流

每个会话也有独立的 SSE 连接:

// GET /session/:sessionID/event
{
  // 会话级别事件
  type: "session.updated",
  properties: {
    info: { id: "ses_xxx", title: "...", ... }
  }
}
 
{
  // 消息更新事件
  type: "message.updated",
  properties: {
    info: { id: "msg_xxx", role: "assistant", ... }
  }
}
 
{
  // Part 增量更新(流式)
  type: "message.part.updated",
  properties: {
    part: { id: "part_xxx", type: "text", ... },
    delta: "继续"  // 增量文本
  }
}

4.2 WebSocket

用于 PTY (伪终端) 的双向通信:

// WebSocket: /pty/:ptyID
app.get("/pty/:ptyID", 
  upgradeWebSocket((c) => ({
    onOpen(evt, ws) {
      const ptyID = c.req.param("ptyID")
      const pty = Pty.get(ptyID)
      
      // 监听终端输出
      pty.onData((data) => {
        ws.send(data)
      })
    },
    
    onMessage(evt, ws) {
      // 接收用户输入
      const ptyID = c.req.param("ptyID")
      const pty = Pty.get(ptyID)
      pty.write(evt.data)
    },
    
    onClose() {
      // 清理资源
    }
  }))
)

4.3 事件流程图


5. 专用路由模块

5.1 TUI 路由 (Terminal UI)

文件: src/server/tui.ts

TUI 路由用于 CLI 模式下的反向代理模式:

// CLI 运行在本地,需要与 Server 通信
// 但 CLI 不能直接调用某些需要 UI 的功能(如 question)
// 所以通过 TUI 路由让 Server "回调" CLI
 
// CLI 端:轮询获取请求
const request = await fetch("/tui/control/next")
// { "path": "/question", "body": {...} }
 
// CLI 处理请求(显示 UI)
const answer = await showQuestionUI(request.body)
 
// CLI 返回响应
await fetch("/tui/control/response", {
  method: "POST",
  body: JSON.stringify(answer)
})

使用场景:

  • 权限询问(CLI 显示提示,等待用户 Y/N)
  • 问答交互(CLI 显示选项,用户选择)

5.2 Project 路由

文件: src/server/project.ts

简单的 CRUD 操作:

export const ProjectRoute = new Hono()
  // GET / - 列出所有项目
  .get("/", async (c) => {
    const projects = await Project.list()
    return c.json(projects)
  })
  
  // GET /current - 获取当前项目
  .get("/current", async (c) => {
    return c.json(Instance.project)
  })
  
  // PATCH /:projectID - 更新项目
  .patch("/:projectID", async (c) => {
    const projectID = c.req.param("projectID")
    const body = await c.req.json()
    const project = await Project.update({ ...body, projectID })
    return c.json(project)
  })

5.3 Question 路由

文件: src/server/question.ts

处理 question 工具的用户交互:

// POST /question/:questionID/answer - 提交答案
{
  "answer": {
    "choices": ["option1"]
  }
}
 
// GET /question/:questionID - 获取问题详情
{
  "id": "q_xxx",
  "questions": [
    {
      "header": "Confirm",
      "question": "是否允许执行 bash 命令?",
      "options": [
        { "label": "允许", "description": "执行命令" },
        { "label": "拒绝", "description": "取消操作" }
      ]
    }
  ]
}

6. mDNS 服务发现

文件: src/server/mdns.ts

使用 Bonjour/mDNS 协议在局域网内发布服务:

// src/server/mdns.ts
export namespace MDNS {
  export function publish(port: number, name = "opencode") {
    const bonjour = new Bonjour()
    
    const service = bonjour.publish({
      name,
      type: "http",  // HTTP 服务
      port,
      txt: { path: "/" },
    })
    
    service.on("up", () => {
      log.info("mDNS service published", { name, port })
    })
  }
}

作用:

  • Desktop App 可以自动发现本地运行的 Server
  • 不需要手动输入 IP 和端口
  • 支持局域网内多个 OpenCode 实例

使用场景:

场景: 用户在 Mac 上运行 OpenCode Desktop
  1. Server 启动在 localhost:4096
  2. MDNS.publish(4096, "opencode")
  3. Desktop App 扫描局域网
  4. 发现 "opencode._http._tcp.local"
  5. 自动连接到 http://localhost:4096

7. 错误处理

文件: src/server/error.ts

标准化错误响应:

// src/server/error.ts
export const errors = (...codes: number[]) => {
  const result: Record<number, any> = {}
  
  for (const code of codes) {
    result[code] = {
      description: getDescription(code),
      content: {
        "application/json": {
          schema: resolver(NamedError.schema)
        }
      }
    }
  }
  
  return result
}
 
// 使用示例
app.get("/session/:sessionID", 
  describeRoute({
    responses: {
      200: { ... },
      ...errors(400, 404)  // 自动添加 400 和 404 错误响应
    }
  })
)

错误格式:

{
  "name": "Session.NotFoundError",
  "message": "Session not found: ses_xxx",
  "data": {
    "sessionID": "ses_xxx"
  }
}

8. OpenAPI 集成

OpenCode 使用 hono-openapi 自动生成 API 文档:

import { describeRoute, resolver, validator, generateSpecs } from "hono-openapi"
 
// 1. 描述路由
app.get("/session/:sessionID",
  describeRoute({
    summary: "Get session",
    description: "Retrieve detailed information about a session by its ID.",
    operationId: "session.get",  // 用于 SDK 生成
    responses: {
      200: {
        description: "Session information",
        content: {
          "application/json": {
            schema: resolver(Session.Info)  // 从 Zod 自动生成 JSON Schema
          }
        }
      }
    }
  }),
  async (c) => { ... }
)
 
// 2. 生成 OpenAPI Spec
const spec = generateSpecs(app)
// 输出到 openapi.json

优势:

  • 类型安全: 从 Zod Schema 自动生成
  • 文档自动化: 无需手写 OpenAPI YAML
  • SDK 生成: 可用于生成客户端 SDK

9. 实战场景

场景 1: 构建 Desktop App 客户端

// Desktop App (SolidJS)
import { createClient } from "@opencode-ai/sdk"
 
const client = createClient({ 
  url: "http://localhost:4096" 
})
 
// 1. 创建会话
const session = await client.session.create({
  directory: "/path/to/project",
  title: "My Task"
})
 
// 2. 订阅实时更新
const eventSource = new EventSource(
  `http://localhost:4096/session/${session.id}/event`
)
 
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data)
  
  if (data.payload.type === "message.part.updated") {
    // 更新 UI,显示增量文本
    updateUI(data.payload.properties.delta)
  }
}
 
// 3. 发送消息
await client.session.prompt({
  sessionID: session.id,
  parts: [
    { type: "text", text: "帮我重构这个函数" }
  ]
})

场景 2: VS Code Extension 集成

// VS Code Extension
const serverUrl = "http://localhost:4096"
 
// 1. 健康检查
const health = await fetch(`${serverUrl}/global/health`).then(r => r.json())
if (!health.healthy) throw new Error("Server not ready")
 
// 2. 获取当前项目
const project = await fetch(`${serverUrl}/project/current`).then(r => r.json())
 
// 3. 创建会话(使用当前打开的文件夹)
const session = await fetch(`${serverUrl}/session`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    directory: vscode.workspace.rootPath,
    title: "VS Code Session"
  })
}).then(r => r.json())
 
// 4. 发送选中的代码
const editor = vscode.window.activeTextEditor
const selection = editor.document.getText(editor.selection)
 
await fetch(`${serverUrl}/session/${session.id}/prompt`, {
  method: "POST",
  body: JSON.stringify({
    parts: [
      { type: "text", text: "解释这段代码:\n" + selection }
    ]
  })
})

场景 3: 自定义工具调用

// 直接调用工具(不通过 Agent)
const result = await fetch(`${serverUrl}/tool/read/execute`, {
  method: "POST",
  body: JSON.stringify({
    filePath: "/path/to/file.ts",
    offset: 0,
    limit: 100
  })
}).then(r => r.json())
 
console.log(result.output)  // 文件内容

10. 常见陷阱与最佳实践

❌ 陷阱 1: SSE 连接超时

问题:

// SSE 连接可能被中间件或浏览器超时关闭
const eventSource = new EventSource("/session/xxx/event")
// 60 秒后自动断开

解决方案:

// Server 端发送心跳(已内置)
const heartbeat = setInterval(() => {
  stream.writeSSE({
    data: JSON.stringify({
      payload: { type: "server.heartbeat", properties: {} }
    })
  })
}, 30000)  // 每 30 秒
 
// Client 端重连
eventSource.onerror = () => {
  eventSource.close()
  setTimeout(() => {
    eventSource = new EventSource("/session/xxx/event")
  }, 1000)
}

❌ 陷阱 2: CORS 配置错误

错误做法:

// 允许所有来源(安全隐患)
cors({ origin: "*" })

正确做法:

// 白名单模式
cors({
  origin(input) {
    const allowed = [
      /^http:\/\/localhost:\d+$/,
      /^https:\/\/.*\.opencode\.ai$/,
      "tauri://localhost"
    ]
    
    return allowed.some(pattern => 
      typeof pattern === "string" 
        ? pattern === input 
        : pattern.test(input)
    )
  }
})

✅ 最佳实践 1: 使用 OpenAPI 描述

// ✅ 完整的 API 描述
app.post("/session/:sessionID/prompt",
  describeRoute({
    summary: "Send prompt to session",
    description: "Send a user message to trigger the agent to start working.",
    operationId: "session.prompt",
    responses: {
      200: { ... },
      ...errors(400, 404, 500)
    }
  }),
  validator("param", z.object({ sessionID: z.string() })),
  validator("json", SessionPrompt.PromptInput),
  async (c) => { ... }
)
 
// ❌ 缺少描述
app.post("/session/:sessionID/prompt", async (c) => { ... })

✅ 最佳实践 2: 错误处理

// ✅ 使用 NamedError
if (!session) {
  throw new Session.NotFoundError({ sessionID })
}
 
// Server 自动处理
.onError((err, c) => {
  if (err instanceof NamedError) {
    return c.json(err.toObject(), { status: getStatusCode(err) })
  }
  // ...
})
 
// ❌ 抛出普通 Error
if (!session) {
  throw new Error("Not found")  // 客户端无法区分错误类型
}

11. 性能优化

11.1 连接池

// Server 复用 Instance
// 相同 directory 使用同一个 Instance
const instance = await Instance.get(directory)

11.2 SSE 流式响应

// 不要等待完整结果
// ❌
const fullResponse = await llm.generate()
res.json(fullResponse)
 
// ✅ 流式推送
for await (const chunk of llm.stream()) {
  stream.writeSSE({ data: JSON.stringify(chunk) })
}

12. 总结

Server 模块是 OpenCode 的网络接口层

核心职责

  • REST API: 提供完整的 CRUD 操作
  • 实时通信: SSE 和 WebSocket 支持
  • 服务发现: mDNS 自动发现
  • 错误处理: 标准化错误响应

关键设计

  • Hono 框架: 高性能、类型安全
  • OpenAPI 集成: 自动生成文档和 Schema
  • 事件驱动: 通过 Bus 获取实时更新
  • 模块化路由: 分离 TUI、Project、Question 等路由

与其他模块的关系

下一步: 阅读 ACP 协议 了解编辑器集成机制