Files
TJWaterAgent/src/runtime/opencode.ts
T
2026-05-18 17:12:25 +08:00

134 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
createOpencode,
createOpencodeClient,
type OpencodeClient,
} from "@opencode-ai/sdk/v2";
import { config } from "../config.js";
import { logger } from "../logger.js";
export type RuntimeHealth = {
healthy: boolean;
version: string;
};
export class OpencodeRuntimeAdapter {
private clientPromise: Promise<OpencodeClient> | null = null;
private closeServer: (() => void) | null = null;
async ensureClient(): Promise<OpencodeClient> {
if (!this.clientPromise) {
this.clientPromise = this.bootstrapClient();
}
return this.clientPromise;
}
async health(): Promise<RuntimeHealth> {
const client = await this.ensureClient();
const response = await client.global.health();
return requireData(response.data, "global.health");
}
async createSession(title?: string) {
const client = await this.ensureClient();
const response = await client.session.create({
title,
});
return requireData(response.data, "session.create");
}
async getSession(id: string) {
const client = await this.ensureClient();
const response = await client.session.get({
sessionID: id,
});
return requireData(response.data, "session.get");
}
async sendPrompt(sessionId: string, text: string) {
const client = await this.ensureClient();
await client.session.prompt({
sessionID: sessionId,
parts: [{ type: "text", text }],
});
// 当前 SDK 响应风格下,prompt() 本身不会直接返回完整 assistant parts
// 所以这里紧跟一次 messages() 回读,给上层路由统一消费。
const messages = await client.session.messages({
sessionID: sessionId,
limit: 20,
});
return requireData(messages.data, "session.messages");
}
async abortSession(sessionId: string) {
const client = await this.ensureClient();
const response = await client.session.abort({
sessionID: sessionId,
});
return requireData(response.data, "session.abort");
}
async subscribeEvents() {
const client = await this.ensureClient();
const response = await client.event.subscribe();
return response.stream;
}
async dispose(): Promise<void> {
this.closeServer?.();
this.closeServer = null;
this.clientPromise = null;
}
private async bootstrapClient(): Promise<OpencodeClient> {
if (config.OPENCODE_BASE_URL) {
logger.info(
{ baseUrl: config.OPENCODE_BASE_URL },
"connecting to external opencode server",
);
return createOpencodeClient({
baseUrl: config.OPENCODE_BASE_URL,
});
}
// embedded 模式下,把服务内工具桥地址注入到 opencode 进程环境里,
// 这样 .opencode/tools 下的自定义工具可以回调本服务。
process.env.TJWATER_AGENT_INTERNAL_BASE_URL = `http://127.0.0.1:${config.PORT}`;
process.env.TJWATER_AGENT_INTERNAL_TOKEN =
config.AGENT_INTERNAL_TOKEN ?? process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? "";
logger.info(
{
hostname: config.OPENCODE_HOSTNAME,
port: config.OPENCODE_PORT,
model: config.OPENCODE_MODEL,
},
"starting embedded opencode server",
);
const runtime = await createOpencode({
hostname: config.OPENCODE_HOSTNAME,
port: config.OPENCODE_PORT,
timeout: config.OPENCODE_TIMEOUT_MS,
config: {
model: config.OPENCODE_MODEL,
},
});
this.closeServer = () => {
runtime.server.close();
};
return runtime.client;
}
}
export const opencodeRuntime = new OpencodeRuntimeAdapter();
function requireData<T>(data: T | undefined, operation: string): T {
if (data === undefined) {
throw new Error(`${operation} returned no data`);
}
return data;
}