核心技术栈
- 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)
}