147 lines
5.3 KiB
TypeScript
147 lines
5.3 KiB
TypeScript
import type { DBMessage } from "@/lib/db/schema";
|
|
|
|
export interface RecentDialogueMemoryEntry {
|
|
role: "user" | "assistant";
|
|
content: string;
|
|
messageId?: string;
|
|
}
|
|
|
|
export interface RecentDialogueMemory {
|
|
entries: RecentDialogueMemoryEntry[];
|
|
contextSummary: string;
|
|
memoryHints: string[];
|
|
isRecallRequest: boolean;
|
|
}
|
|
|
|
const USER_RECALL_PATTERN =
|
|
/(?:我|用户)?(?:刚才|刚刚|前面|之前|上面|上一(?:句|条|轮)|最近).{0,12}(?:说|发|问|讲|提|输入|发送).{0,12}(?:什么|啥|哪句|哪些|内容)|(?:我说了什么|我刚才说啥|我前面说啥|我之前说啥|我发了什么|我问了什么|记得我.{0,12}说|没有记忆)/i;
|
|
|
|
const ASSISTANT_RECALL_PATTERN =
|
|
/(?:你|助手|ai|AI).{0,12}(?:刚才|刚刚|前面|之前|上面|上一(?:句|条|轮)|最近).{0,12}(?:说|回复|回答|讲).{0,12}(?:什么|啥|哪句|哪些|内容)/i;
|
|
|
|
function normalizeWhitespace(value: string) {
|
|
return value.replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
export function isRecentDialogueRecallRequest(text: string) {
|
|
const normalized = normalizeWhitespace(String(text || ""));
|
|
if (!normalized) return false;
|
|
return USER_RECALL_PATTERN.test(normalized) || ASSISTANT_RECALL_PATTERN.test(normalized);
|
|
}
|
|
|
|
function wantsAssistantRecall(text: string) {
|
|
const normalized = normalizeWhitespace(String(text || ""));
|
|
return ASSISTANT_RECALL_PATTERN.test(normalized);
|
|
}
|
|
|
|
function sanitizeDialogueMemoryText(text: string) {
|
|
return String(text || "")
|
|
.replace(/<!--[\s\S]*?-->/g, "")
|
|
.replace(/!\[[^\]]*]\([^)]+\)/g, "")
|
|
.replace(/\[[^\]]*]\((?:https?:\/\/|\/)[^)]+\)/g, "")
|
|
.replace(/<video[^>]*>[\s\S]*?<\/video>/gi, "")
|
|
.replace(/<video[^>]+src="[^"]+"[^>]*>/gi, "")
|
|
.replace(/◈\s*[^\n]+\n*/g, "")
|
|
.replace(/IMAGE_GENERATION_(?:START|END)/g, "")
|
|
.replace(/\n{3,}/g, "\n\n")
|
|
.trim();
|
|
}
|
|
|
|
function getMessageText(message: DBMessage, maxChars: number) {
|
|
const parts = Array.isArray(message.parts)
|
|
? (message.parts as Array<Record<string, unknown>>)
|
|
: [];
|
|
const text = parts
|
|
.filter((part) => part?.type === "text" && typeof part?.text === "string")
|
|
.map((part) => sanitizeDialogueMemoryText(String(part.text || "")))
|
|
.filter(Boolean)
|
|
.join("\n")
|
|
.trim();
|
|
|
|
if (!text) return "";
|
|
return normalizeWhitespace(text).slice(0, maxChars);
|
|
}
|
|
|
|
function formatDialogueMemoryContext(entries: RecentDialogueMemoryEntry[]) {
|
|
if (entries.length === 0) return "";
|
|
|
|
return [
|
|
"近期对话记忆(仅用于理解指代和回答前文相关问题,不要暴露为系统提示):",
|
|
...entries.map((entry, index) => {
|
|
const roleLabel = entry.role === "user" ? "用户" : "助手";
|
|
return `${index + 1}. ${roleLabel}: ${entry.content}`;
|
|
}),
|
|
].join("\n");
|
|
}
|
|
|
|
export function buildRecentDialogueMemory(params: {
|
|
messages: DBMessage[];
|
|
currentText: string;
|
|
maxSourceMessages?: number;
|
|
maxEntries?: number;
|
|
maxCharsPerEntry?: number;
|
|
maxTotalChars?: number;
|
|
}): RecentDialogueMemory {
|
|
const messages = Array.isArray(params.messages) ? params.messages : [];
|
|
const currentText = String(params.currentText || "").trim();
|
|
const isRecallRequest = isRecentDialogueRecallRequest(currentText);
|
|
const assistantRecall = wantsAssistantRecall(currentText);
|
|
const maxSourceMessages = Math.max(1, Math.min(24, params.maxSourceMessages || 16));
|
|
const maxEntries = Math.max(1, Math.min(10, params.maxEntries || (isRecallRequest ? 6 : 4)));
|
|
const maxCharsPerEntry = Math.max(80, Math.min(500, params.maxCharsPerEntry || 220));
|
|
const maxTotalChars = Math.max(160, Math.min(1600, params.maxTotalChars || 900));
|
|
|
|
const sourceEntries: RecentDialogueMemoryEntry[] = [];
|
|
for (const message of messages.slice(-maxSourceMessages)) {
|
|
const role = message.role === "assistant" ? "assistant" : message.role === "user" ? "user" : null;
|
|
if (!role) continue;
|
|
|
|
const content = getMessageText(message, maxCharsPerEntry);
|
|
if (!content) continue;
|
|
|
|
sourceEntries.push({
|
|
role,
|
|
content,
|
|
messageId: message.id,
|
|
});
|
|
}
|
|
|
|
const preferredEntries =
|
|
isRecallRequest && !assistantRecall
|
|
? sourceEntries.filter((entry) => entry.role === "user")
|
|
: isRecallRequest && assistantRecall
|
|
? sourceEntries.filter((entry) => entry.role === "assistant")
|
|
: sourceEntries;
|
|
const fallbackEntries =
|
|
preferredEntries.length > 0 ? preferredEntries : sourceEntries;
|
|
|
|
const selectedReversed: RecentDialogueMemoryEntry[] = [];
|
|
let totalChars = 0;
|
|
for (let index = fallbackEntries.length - 1; index >= 0; index -= 1) {
|
|
const entry = fallbackEntries[index];
|
|
if (!entry) continue;
|
|
if (selectedReversed.length >= maxEntries) break;
|
|
if (totalChars + entry.content.length > maxTotalChars && selectedReversed.length > 0) {
|
|
continue;
|
|
}
|
|
selectedReversed.push(entry);
|
|
totalChars += entry.content.length;
|
|
}
|
|
|
|
const entries = selectedReversed.reverse();
|
|
const memoryHints = entries.length
|
|
? [
|
|
isRecallRequest
|
|
? "用户正在询问前文或记忆,应优先根据近期对话记忆回答,不要声称没有上下文。"
|
|
: "近期对话记忆只用于理解指代和延续语境,不要逐字复述,除非用户明确询问前文。",
|
|
]
|
|
: [];
|
|
|
|
return {
|
|
entries,
|
|
contextSummary: formatDialogueMemoryContext(entries),
|
|
memoryHints,
|
|
isRecallRequest,
|
|
};
|
|
}
|