Files
TJWaterAgent/dist/chat/sessionBridge.js
T

118 lines
5.4 KiB
JavaScript

import { randomUUID } from "node:crypto";
import { logger } from "../logger.js";
export class ChatSessionBridge {
registry;
runtime;
// 这里额外保存 session -> 用户上下文,供工具桥在服务端代发真实后端请求时复用。
sessionContexts = new Map();
constructor(registry, runtime) {
this.registry = registry;
this.runtime = runtime;
}
async resolve(context) {
const requestContext = {
clientSessionId: context.clientSessionId?.trim() || `agent-${randomUUID().slice(0, 12)}`,
accessToken: context.accessToken,
projectId: context.projectId,
traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`,
};
this.cleanupExpired();
const current = this.registry.get(requestContext);
if (current) {
this.sessionContexts.set(current.sessionId, requestContext);
try {
// 只有 opencode 侧 session 仍存在时,才复用本地映射。
await this.runtime.getSession(current.sessionId);
return { binding: current, requestContext, created: false };
}
catch (error) {
logger.warn({
clientSessionId: requestContext.clientSessionId,
sessionId: current.sessionId,
err: error,
}, "existing opencode session lookup failed, creating a new session");
}
}
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 };
}
count() {
return this.registry.count();
}
getSessionContext(sessionId) {
return this.sessionContexts.get(sessionId) ?? null;
}
async abort(context) {
const clientSessionId = context.clientSessionId?.trim();
if (!clientSessionId) {
return null;
}
const requestContext = {
clientSessionId,
accessToken: context.accessToken,
projectId: context.projectId,
traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`,
};
this.cleanupExpired();
const binding = this.registry.get(requestContext);
if (!binding) {
return null;
}
this.sessionContexts.set(binding.sessionId, requestContext);
await this.runtime.abortSession(binding.sessionId);
return binding;
}
async fork(context) {
const currentClientSessionId = context.clientSessionId?.trim();
const nextRequestContext = {
clientSessionId: `agent-${randomUUID().slice(0, 12)}`,
accessToken: context.accessToken,
projectId: context.projectId,
traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`,
};
this.cleanupExpired();
if (!currentClientSessionId || context.keepMessageCount <= 0) {
const session = await this.runtime.createSession(nextRequestContext.clientSessionId);
const binding = this.registry.upsert(nextRequestContext, session.id);
this.sessionContexts.set(binding.sessionId, nextRequestContext);
return { binding, requestContext: nextRequestContext, created: true };
}
const currentContext = {
clientSessionId: currentClientSessionId,
accessToken: context.accessToken,
projectId: context.projectId,
traceId: nextRequestContext.traceId,
};
const current = this.registry.get(currentContext);
if (!current) {
const session = await this.runtime.createSession(nextRequestContext.clientSessionId);
const binding = this.registry.upsert(nextRequestContext, session.id);
this.sessionContexts.set(binding.sessionId, nextRequestContext);
return { binding, requestContext: nextRequestContext, created: true };
}
await this.runtime.getSession(current.sessionId);
const messages = await this.runtime.messages(current.sessionId, Math.max(100, context.keepMessageCount + 20));
const chatMessages = messages.filter((message) => message.info.role === "user" || message.info.role === "assistant");
const keepMessage = chatMessages[context.keepMessageCount - 1];
if (!keepMessage) {
throw new Error(`fork keep point not found for message count ${context.keepMessageCount}`);
}
const session = await this.runtime.forkSession(current.sessionId, keepMessage.info.id);
const binding = this.registry.upsert(nextRequestContext, session.id);
this.sessionContexts.set(binding.sessionId, nextRequestContext);
return { binding, requestContext: nextRequestContext, created: true };
}
cleanupExpired() {
const expiredSessionIds = this.registry.evictExpired();
for (const sessionId of expiredSessionIds) {
this.sessionContexts.delete(sessionId);
// 这里用 abort 做轻量清理;即使失败,也不阻断本地过期回收。
void this.runtime.abortSession(sessionId).catch((error) => {
logger.debug({ sessionId, err: error }, "ignoring failed abort for expired session");
});
}
}
}