OpenCode源码学习

13 阅读3分钟

代码仓 github.com/anomalyco/o…

核心技术栈

  • Runtime : Bun
  • Web 框架 : Hono
  • AI SDK : Vercel AI SDK
  • 数据库 : Drizzle ORM + SQLite
  • LSP : Language Server Protocol 支持
  • 文件系统 : tree-sitter, ripgrep

命令

# 开发
bun run dev              # 启动 CLI
bun run dev:web          # 启动 Web 应用
bun run dev:desktop      # 启动桌面应用

# 构建
bun run build            # 构建 CLI
bun run typecheck        # 类型检查

# 测试
bun run test             # 运行测试

# debug server
bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts serve --port 4096

# 客户端连接本地启动的服务器
opencode attach http://localhost:4096

# Debug TUI
bun run --inspect=ws://localhost:6499/ --cwd packages/opencode --conditions=browser ./src/index.ts

CLI端

初始化过程

主入口文件 opencode/src/index.ts
  • 错误处理
  • 初始化数据库
  • 注册以opencode开头的命令,其中TuiThreadCommand为默认入口
TuiThreadCommand
command: "$0 [project]" // $0 执行命令本身  []可选参数 会默认执行
新建worker线程

worker线程和主线程互相监听通讯,worker线程的主要作用是发起请求

rpc.ts

function listen () {
    onmessage = () => {} 监听主线程,执行后返回结果
}

function client(target) {
    主线程监听worker线程,触发已注册的回调
    target.onmessage = () => {}
    return {
        call(), 主线程发消息给worker线程
        on() 注册监听回调
    }
}
渲染UI
const tuiPromise = tui()

一次完整的请求链路

用户输入

prompt/index.tsx

function submit() {
    if (store.mode === "shell") {
        // 以 `!` 开头的消息会作为 shell 命令执行。
        sdk.client.session.shell()
    } else if (inputText.startsWith("/") && isCommand) {
        // 执行`/`开头的命令
        sdk.client.session.command() 
    } else {
        // 普通提问
        sdk.client.session.prompt()
    }
}
发起请求

sdk.gen.ts

funciton prompt() {
    return client.post({
        url: "/session/{sessionID}/message",
    })
}
server请求处理

session.ts

new Hono()
    .post({
        "/:sessionID/message",
        async (c) => {
            return stream(c, async (stream) => {
                // 重点处理方法
                const msg = await SessionPrompt.prompt({ ...body, sessionID })
            })
      },
    })
server message接口prompt处理

更新操作

// 发布更新事件
const updatePart = (part) => {
  Bus.publish(MessageV2.Event.Updated, part)
}
const updateMessage = (info) => {
  Bus.publish(MessageV2.Event.Updated, info)
})

await Session.updateMessage(info)
await Session.updatePart(part)

server event接口监听事件总线,并持续返回给客户端

server.ts


.get(
   "/event",
   async (c) => {
       const unsub = Bus.subscribeAll(async (event) => {
           await stream.writeSSE({
               data: JSON.stringify(event),
           })
           if (event.type === Bus.InstanceDisposed.type) {
               stream.close()
           }
       })
   }
)

客户端监听server
  // sdk.gen.ts
  public subscribe<ThrowOnError extends boolean = false>(
    parameters?: {
      directory?: string
    },
    options?: Options<never, ThrowOnError>,
  ) {
    const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }])
    return (options?.client ?? this.client).sse.get<EventSubscribeResponses, unknown, ThrowOnError>({
      url: "/event",
      ...options,
      ...params,
    })
  }
  
  // sdk.tsx 
  // 客户端持续监听SSE
  while (true) {
    const events = await sdk.event.subscribe(
      {},
      {
        signal: abort.signal,
      },
    )

    for await (const event of events.stream) {
      handleEvent(event)
    }
  }
页面数据更新
 // sync.tsx
 // 更新页面信息
 sdk.event.listen((e) => {
  const event = e.details
  switch (event.type) {
    case "message.updated": {
      setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
      break
    }
    case "message.part.updated": {
      setStore("part", event.properties.part.messageID, [event.properties.part])
      break
    }
  }
})

prompt方法

const prompt = (input) => {
   // 处理用户输入,解析各种类型参数
   const message = await createUserMessage(input)
   // 构造请求参数,发起请求,循环处理结果
   return loop({ sessionID: input.sessionID })
}
loop
  const loop = () => {
    while(true) {
      let lastUser, lastAssistant, lastFinished, tasks
      for (let i = msgs.length - 1; i >= 0; i--) {
        // 反向遍历找到最后一个用户消息、助手消息、完成的助手消息
      }
      // 检查退出条件
      if (lastAssistant?.finish && !["tool-calls", "unknown"].includes(lastAssistant.finish)) {
        break
      }

      const task = tasks.pop()
      // 情况A: 待处理的子任务
      if (task?.type === "subtask") {
        continue
      }
      // 情况B: 待处理的压缩任务
      if (task?.type === "compaction") {
        continue
      }
      // 情况C: 上下文溢出,需要压缩
      if (await SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model })) {
        continue
      }
      // 情况D: 正常处理 执行 LLM 调用
      const result = await processor.process()
    }
  }
AI SDK

跨供应商的AI大模型标准化请求库,各个供应商提供封装了交互逻辑ai-sdk库
文档 ai-sdk.dev/docs/introd…

请求代码
const stream = await LLM.stream(streamInput)

// 核心代码
const [language, cfg, provider, auth] = await Promise.all([
    Provider.getLanguage(input.model),
    Config.get(),
    Provider.getProvider(input.model.providerID),
    Auth.get(input.model.providerID),
])

// language就是模型实例
const stream = () => {
    return streamText({
        model: language
    }
}
模型实例获取
const language = s.modelLoaders[model.providerID]
    ? await s.modelLoaders[model.providerID](sdk, model.api.id, provider.options)
    : sdk.languageModel(model.api.id)
供应商SDK加载getSDK()

内置直接使用
const bundledFn = BUNDLED_PROVIDERS[model.api.npm]

在线下载
installedPath = await BunProc.install(model.api.npm, "latest")

初始化供应商和模型信息

provides.ts

const state = Instance.state(async () => {
    // 初始化模型信息,第一次会请求https://models.dev/api.json,然后保存到本地
    ModelsDev.refresh() 
    
    // 从环境变量加载 API 密钥,只加载已配置密钥的提供商
    const apiKey = provider.env.map((item) => env[item]).find(Boolean)
    
    // 从本地缓存的认证信息加载 API 密钥
    Object.entries(await Auth.all())
    
    // 件加载认证信息
    Plugin.list()
    
    // 自定义模型加载器
    Object.entries(CUSTOM_LOADERS)
    
    // 最终过滤和清理所有可用的供应商和模型信息
    Object.entries(providers)
}