拆分 chat.ts 文件,明确功能边界

This commit is contained in:
2026-05-20 16:50:11 +08:00
parent 23d8249286
commit 872570ac3a
3 changed files with 947 additions and 778 deletions
+144
View File
@@ -0,0 +1,144 @@
import { logger } from "../logger.js";
import { MemoryStore } from "../memory/store.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import { collectTextContent } from "./chatStream.js";
const TITLE_PROMPT_TIMEOUT_MS = 2500;
const buildSessionTitle = (message: string) => {
const normalized = message.replace(/\s+/g, " ").trim();
if (!normalized) {
return "新对话";
}
return normalized.length > 24 ? `${normalized.slice(0, 24)}...` : normalized;
};
const buildTitleConversationContext = async (
runtime: OpencodeRuntimeAdapter,
sessionId: string,
) => {
const messages = await runtime.messages(sessionId, 12);
const recentMessages = messages
.filter(
(message) =>
message.info.role === "user" || message.info.role === "assistant",
)
.map((message) => ({
role: message.info.role,
content: collectTextContent(message.parts).replace(/\s+/g, " ").trim(),
}))
.filter((message) => message.content.length > 0)
.slice(-6);
if (recentMessages.length === 0) {
return "";
}
return recentMessages
.map((message) => `${message.role === "user" ? "用户" : "助手"}${message.content}`)
.join("\n")
.slice(0, 2400);
};
const normalizeGeneratedTitle = (rawTitle: string, fallback: string) => {
const normalized = rawTitle
.replace(/\s+/g, " ")
.replace(/["'“”‘’`]/g, "")
.trim();
if (!normalized) {
return fallback;
}
return normalized.length > 24 ? `${normalized.slice(0, 24)}...` : normalized;
};
export const generateSessionTitle = async (
runtime: OpencodeRuntimeAdapter,
options: {
sessionId: string;
latestUserMessage: string;
fallbackTitle?: string;
},
) => {
const fallback = options.fallbackTitle?.trim() || buildSessionTitle(options.latestUserMessage);
let titleSessionId: string | undefined;
try {
const conversation = await buildTitleConversationContext(runtime, options.sessionId);
if (!conversation) {
return fallback;
}
const titleSession = await runtime.createSession(`title-${Date.now().toString(36)}`);
titleSessionId = titleSession.id;
const request = runtime
.prompt(
titleSession.id,
[
"你是会话标题生成器。",
"请根据用户问题生成一个 8-16 字中文标题。",
"要求:简洁、可读、避免标点、不要引号、不要解释。",
"请优先概括最近这轮对话的核心任务或结论。",
"只输出标题本身。",
"",
conversation,
].join("\n"),
)
.then(async () => {
const messages = await runtime.messages(titleSession.id, 20);
const assistantMessage = [...messages]
.reverse()
.find((message) => message.info.role === "assistant");
const title = collectTextContent(assistantMessage?.parts ?? []);
return normalizeGeneratedTitle(title, fallback);
});
const timeout = new Promise<string>((resolve) => {
setTimeout(() => resolve(fallback), TITLE_PROMPT_TIMEOUT_MS);
});
return await Promise.race([request, timeout]);
} catch (error) {
logger.warn({ err: error }, "failed to generate session title, using fallback");
return fallback;
} finally {
if (titleSessionId) {
await runtime.abortSession(titleSessionId).catch((error) => {
logger.debug({ sessionId: titleSessionId, err: error }, "failed to cleanup title session");
});
}
}
};
export const getConversationTurnStats = async (
runtime: OpencodeRuntimeAdapter,
sessionId: string,
) => {
const messages = await runtime.messages(sessionId, 12);
return messages.reduce(
(stats, message) => {
if (message.info.role === "user") {
stats.userMessageCount += 1;
} else if (message.info.role === "assistant") {
stats.assistantMessageCount += 1;
}
return stats;
},
{
userMessageCount: 0,
assistantMessageCount: 0,
},
);
};
export const buildPromptWithLearningContext = async (
memoryStore: MemoryStore,
actorKey: string,
projectKey: string,
message: string,
) => {
const snapshot = await memoryStore.buildPromptSnapshot({ actorKey, projectKey });
if (!snapshot) {
return message;
}
return `${snapshot}\n\n[Current user request]\n${message}`;
};