移除对 copilot 的兼容。更新示例和文档,统一使用 session_id 代替 conversationId

This commit is contained in:
2026-04-29 15:31:39 +08:00
parent 127aca466f
commit 6f15b5d7e3
11 changed files with 66 additions and 65 deletions
+3 -3
View File
@@ -11,7 +11,7 @@ export class ChatSessionBridge {
}
async resolve(context) {
const requestContext = {
conversationId: context.conversationId?.trim() || `conv-${randomUUID().slice(0, 12)}`,
clientSessionId: context.clientSessionId?.trim() || `agent-${randomUUID().slice(0, 12)}`,
accessToken: context.accessToken,
projectId: context.projectId,
traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`,
@@ -27,13 +27,13 @@ export class ChatSessionBridge {
}
catch (error) {
logger.warn({
conversationId: requestContext.conversationId,
clientSessionId: requestContext.clientSessionId,
sessionId: current.sessionId,
err: error,
}, "existing opencode session lookup failed, creating a new session");
}
}
const session = await this.runtime.createSession(requestContext.conversationId);
const session = await this.runtime.createSession(requestContext.clientSessionId);
const binding = this.registry.upsert(requestContext, session.id);
this.sessionContexts.set(binding.sessionId, requestContext);
return { binding, requestContext, created: true };
+11 -11
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import { logger } from "../logger.js";
const payloadSchema = z.object({
message: z.string().min(1).max(10000),
conversation_id: z.string().max(128).optional(),
session_id: z.string().max(128).optional(),
});
export const buildChatRouter = (sessionBridge, runtime) => {
const chatRouter = Router();
@@ -24,19 +24,19 @@ export const buildChatRouter = (sessionBridge, runtime) => {
const projectId = req.header("x-project-id") ?? undefined;
const traceId = req.header("x-trace-id") ?? undefined;
const { binding, requestContext, created } = await sessionBridge.resolve({
conversationId: parsed.data.conversation_id,
clientSessionId: parsed.data.session_id,
accessToken,
projectId,
traceId,
});
logger.info({
conversationId: requestContext.conversationId,
clientSessionId: requestContext.clientSessionId,
sessionId: binding.sessionId,
created,
traceId: requestContext.traceId,
projectId: requestContext.projectId,
}, "processing chat request");
// 当前先走“发送 prompt 后回读最近消息”的兼容实现。
// 当前先走“发送 prompt 后回读最近消息”的 opencode SDK 实现。
// 后续切到真正的 opencode 事件流时,只需要替换这里的取数方式。
const messages = await runtime.sendPrompt(binding.sessionId, parsed.data.message);
const assistantMessage = messages.find((message) => message.info.role === "assistant");
@@ -45,38 +45,38 @@ export const buildChatRouter = (sessionBridge, runtime) => {
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no");
const conversationId = requestContext.conversationId;
const clientSessionId = requestContext.clientSessionId;
const parts = assistantMessage?.parts ?? [];
const textContent = collectTextContent(parts);
if (textContent) {
res.write(toSse("token", {
conversationId,
session_id: clientSessionId,
content: textContent,
}));
}
for (const toolCall of collectToolCalls(parts)) {
res.write(toSse("tool_call", {
conversationId,
session_id: clientSessionId,
tool: toolCall.tool,
params: toolCall.params,
}));
}
if (assistantMessage?.info.role === "assistant" && assistantMessage.info.error) {
res.write(toSse("error", {
conversationId,
session_id: clientSessionId,
message: getErrorMessage(assistantMessage.info.error),
detail: assistantMessage.info.error.name,
}));
}
else if (!assistantMessage) {
res.write(toSse("error", {
conversationId,
session_id: clientSessionId,
message: "assistant response unavailable",
detail: "no assistant message found after prompt",
}));
}
else {
res.write(toSse("done", { conversationId }));
res.write(toSse("done", { session_id: clientSessionId }));
}
res.end();
}
@@ -92,7 +92,7 @@ export const buildChatRouter = (sessionBridge, runtime) => {
return chatRouter;
};
const toSse = (event, data) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
// 先把 opencode 的 Part 结构压平成前端当前消费的 SSE 语义。
// 先把 opencode 的 Part 结构压平成 Agent API 的 SSE 语义。
const collectTextContent = (parts) => parts
.filter((part) => part.type === "text")
.map((part) => part.text)
+1 -1
View File
@@ -66,7 +66,7 @@ app.post("/internal/tools/dynamic-http-call", async (req, res) => {
});
}
});
app.use("/api/v1/copilot/chat", buildChatRouter(sessionBridge, opencodeRuntime));
app.use("/api/v1/agent/chat", buildChatRouter(sessionBridge, opencodeRuntime));
const server = app.listen(config.PORT, config.HOST, () => {
logger.info({ host: config.HOST, port: config.PORT }, "TJWaterAgent listening");
});
+3 -3
View File
@@ -7,7 +7,7 @@ export class SessionRegistry {
}
upsert(context, sessionId) {
const binding = {
conversationId: context.conversationId,
clientSessionId: context.clientSessionId,
sessionId,
lastUsedAt: Date.now(),
};
@@ -43,11 +43,11 @@ export class SessionRegistry {
return expired;
}
makeKey(context) {
// 会话隔离不能只看 conversationId;同一浏览器会话切换用户或项目时必须映射到不同 opencode session。
// 会话隔离不能只看前端 session_id;同一浏览器会话切换用户或项目时必须映射到不同 opencode session。
const digest = crypto
.createHash("sha256")
.update([
context.conversationId,
context.clientSessionId,
context.accessToken ?? "",
context.projectId ?? "",
].join("|"))