function splitConfigText(value = "") { return String(value || "") .split(/[\r\n,]+/) .map((item) => String(item || "").trim()) .filter(Boolean); } function escapeRegex(value = "") { return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function normalizeBoundaryRule(rawRule, mode = "exclude", index = 0) { if (typeof rawRule === "string") { const tag = String(rawRule || "").trim(); if (!tag) return null; return { id: `${mode}:tag:${index}:${tag}`, mode, kind: "tag", label: tag, tag, }; } if (!rawRule || typeof rawRule !== "object" || Array.isArray(rawRule)) { return null; } const tag = String(rawRule.tag || rawRule.name || "").trim(); if (tag) { return { id: `${mode}:tag:${index}:${tag}`, mode, kind: "tag", label: String(rawRule.label || tag).trim() || tag, tag, }; } const start = String(rawRule.start ?? rawRule.open ?? rawRule.begin ?? "").trim(); const end = String(rawRule.end ?? rawRule.close ?? rawRule.finish ?? "").trim(); if (!start || !end) { return null; } return { id: `${mode}:boundary:${index}`, mode, kind: "boundary", label: String(rawRule.label || `${start} … ${end}`).trim() || `${start} … ${end}`, start, end, caseSensitive: rawRule.caseSensitive === true, }; } function normalizeBoundaryRules(rawRules = null, rawTags = "", mode = "exclude") { const values = []; if (Array.isArray(rawRules)) { values.push(...rawRules); } else if (rawRules !== null && rawRules !== undefined && rawRules !== "") { values.push(rawRules); } values.push(...splitConfigText(rawTags)); return values .map((item, index) => normalizeBoundaryRule(item, mode, index)) .filter(Boolean); } function applyTagBoundaryRule(text, rule) { const input = String(text || ""); const escapedTag = escapeRegex(rule?.tag || ""); if (!escapedTag) { return { changed: false, output: input, ruleLabel: String(rule?.label || ""), matchedText: "", }; } const regex = new RegExp( `<${escapedTag}\\b[^>]*>([\\s\\S]*?)<\\/${escapedTag}>`, "gi", ); let match = null; for (const candidate of input.matchAll(regex)) { match = candidate; } if (!match) { return { changed: false, output: input, ruleLabel: String(rule?.label || ""), matchedText: "", }; } const matchedText = String(match[0] || ""); if (rule?.mode === "extract") { return { changed: true, output: String(match[1] || "").trim(), ruleLabel: String(rule?.label || rule?.tag || ""), matchedText, }; } const matchIndex = Number(match.index); if (!Number.isFinite(matchIndex) || matchIndex < 0) { return { changed: false, output: input, ruleLabel: String(rule?.label || rule?.tag || ""), matchedText: "", }; } return { changed: true, output: `${input.slice(0, matchIndex)}${input.slice(matchIndex + matchedText.length)}`.trim(), ruleLabel: String(rule?.label || rule?.tag || ""), matchedText, }; } function applyLiteralBoundaryRule(text, rule) { const input = String(text || ""); const start = String(rule?.start || ""); const end = String(rule?.end || ""); if (!start || !end) { return { changed: false, output: input, ruleLabel: String(rule?.label || ""), matchedText: "", }; } const sourceText = rule?.caseSensitive === true ? input : input.toLowerCase(); const startNeedle = rule?.caseSensitive === true ? start : start.toLowerCase(); const endNeedle = rule?.caseSensitive === true ? end : end.toLowerCase(); const startIndex = sourceText.lastIndexOf(startNeedle); if (startIndex < 0) { return { changed: false, output: input, ruleLabel: String(rule?.label || ""), matchedText: "", }; } const endIndex = sourceText.indexOf(endNeedle, startIndex + startNeedle.length); if (endIndex < 0) { return { changed: false, output: input, ruleLabel: String(rule?.label || ""), matchedText: "", }; } const matchedText = input.slice(startIndex, endIndex + end.length); if (rule?.mode === "extract") { return { changed: true, output: input.slice(startIndex + start.length, endIndex).trim(), ruleLabel: String(rule?.label || ""), matchedText, }; } return { changed: true, output: `${input.slice(0, startIndex)}${input.slice(endIndex + end.length)}`.trim(), ruleLabel: String(rule?.label || ""), matchedText, }; } function applyBoundaryRule(text, rule) { if (rule?.kind === "tag") { return applyTagBoundaryRule(text, rule); } if (rule?.kind === "boundary") { return applyLiteralBoundaryRule(text, rule); } return { changed: false, output: String(text || ""), ruleLabel: String(rule?.label || ""), matchedText: "", }; } function applyFirstExtractRule(text, rules = []) { const input = String(text || ""); for (const rule of Array.isArray(rules) ? rules : []) { const result = applyBoundaryRule(input, rule); if (result.changed) { return { changed: true, output: result.output, operation: { mode: "extract", rule: result.ruleLabel, matchedLength: String(result.matchedText || "").length, }, }; } } return { changed: false, output: input, operation: null, }; } function applyExcludeRules(text, rules = []) { const input = String(text || ""); let output = input; const operations = []; for (const rule of Array.isArray(rules) ? rules : []) { const result = applyBoundaryRule(output, rule); if (!result.changed) { continue; } output = result.output; operations.push({ mode: "exclude", rule: result.ruleLabel, matchedLength: String(result.matchedText || "").length, }); } return { changed: output !== input, output, operations, }; } function normalizeRole(value = "") { const role = String(value || "assistant").trim().toLowerCase(); if (["user", "assistant", "system"].includes(role)) { return role; } return role === "ai" ? "assistant" : "assistant"; } function resolveMessageContent(message = {}) { if (typeof message?.content === "string") { return message.content; } if (typeof message?.mes === "string") { return message.mes; } return ""; } function resolveMessageRawContent(message = {}) { if (typeof message?.rawContent === "string") { return message.rawContent; } if (typeof message?.mes === "string") { return message.mes; } if (typeof message?.content === "string") { return message.content; } return ""; } function resolveSpeakerName(message = {}, role = "assistant", names = {}) { const explicitSpeaker = String( message?.speaker ?? message?.name ?? message?.displayName ?? "", ).trim(); if (explicitSpeaker) { return explicitSpeaker; } if (role === "user") { return String(names?.userName || "用户").trim() || "用户"; } if (role === "assistant") { return String(names?.charName || "角色").trim() || "角色"; } return role || "assistant"; } function shouldHideSpeakerLabel(message = {}, role = "assistant", names = {}) { if (message?.hideSpeakerLabel === true) { return true; } if (message?.hideSpeakerLabel === false) { return false; } if (role !== "assistant") { return false; } if (String(message?.source || "").trim() === "worldInfo-atDepth") { return false; } const explicitSpeaker = String( message?.speaker ?? message?.name ?? message?.displayName ?? "", ).trim(); if (!explicitSpeaker) { return true; } const activeCharName = String(names?.charName || "").trim(); if (!activeCharName) { return false; } return explicitSpeaker === activeCharName; } function normalizeExtractionMessage(message = {}, index = 0, names = {}) { const role = normalizeRole( message?.role ?? (message?.is_user === true ? "user" : "assistant"), ); const content = String(resolveMessageContent(message) || "").trim(); const rawContent = String(resolveMessageRawContent(message) || content).trim(); const speaker = resolveSpeakerName(message, role, names); const hideSpeakerLabel = shouldHideSpeakerLabel(message, role, names); const seq = Number.isFinite(Number(message?.seq)) ? Number(message.seq) : null; return { index, seq, role, speaker, name: speaker, hideSpeakerLabel, content, rawContent, sourceType: role === "user" ? "user_input" : "ai_output", isContextOnly: message?.isContextOnly === true, }; } function countRoles(messages = []) { return (Array.isArray(messages) ? messages : []).reduce( (acc, message) => { const role = normalizeRole(message?.role || "assistant"); acc[role] = Number(acc[role] || 0) + 1; return acc; }, { user: 0, assistant: 0, system: 0 }, ); } export function formatExtractionTranscript(messages = []) { const safeMessages = Array.isArray(messages) ? messages : []; const hasContextMessages = safeMessages.some((m) => m?.isContextOnly === true); const hasTargetMessages = safeMessages.some((m) => m?.isContextOnly !== true); const lines = []; let inContext = null; for (let index = 0; index < safeMessages.length; index += 1) { const message = safeMessages[index]; const isContext = message?.isContextOnly === true; if (hasContextMessages && hasTargetMessages && isContext !== inContext) { if (isContext) { lines.push("--- 以下是上下文回顾(已提取过),仅供理解剧情 ---"); } else { lines.push("--- 以下是本次需要提取记忆的新对话内容 ---"); } inContext = isContext; } const seqLabel = Number.isFinite(Number(message?.seq)) ? `#${Number(message.seq)}` : `#${index + 1}`; const role = normalizeRole(message?.role || "assistant"); const speaker = String(message?.speaker || message?.name || "").trim(); const speakerLabel = message?.hideSpeakerLabel === true || !speaker ? "" : `|${speaker}`; const line = `${seqLabel} [${role}${speakerLabel}]: ${String(message?.content || "")}`; if (String(line || "").trim()) { lines.push(line); } } return lines.join("\n\n"); } export function buildExtractionInputContext( messages = [], { settings = {}, userName = "", charName = "" } = {}, ) { const normalizedMessages = (Array.isArray(messages) ? messages : []) .map((message, index) => normalizeExtractionMessage(message, index, { userName, charName, })) .filter( (message) => String(message?.content || "").trim().length > 0 || String(message?.rawContent || "").trim().length > 0, ); const extractRules = normalizeBoundaryRules( settings?.extractAssistantExtractRules, settings?.extractAssistantExtractTags, "extract", ); const excludeRules = normalizeBoundaryRules( settings?.extractAssistantExcludeRules, settings?.extractAssistantExcludeTags, "exclude", ); const filteredMessages = []; const messageOperations = []; let changedAssistantMessageCount = 0; let droppedAssistantMessageCount = 0; let extractedAssistantMessageCount = 0; let excludedAssistantMessageCount = 0; for (const message of normalizedMessages) { const operations = []; let nextContent = String(message.content || ""); if (message.role === "assistant") { const extractResult = applyFirstExtractRule(nextContent, extractRules); if (extractResult.changed) { nextContent = extractResult.output; extractedAssistantMessageCount += 1; operations.push(extractResult.operation); } const excludeResult = applyExcludeRules(nextContent, excludeRules); if (excludeResult.changed) { nextContent = excludeResult.output; excludedAssistantMessageCount += 1; operations.push(...excludeResult.operations); } } const normalizedContent = String(nextContent || "").trim(); if (operations.length > 0 || normalizedContent !== String(message.content || "").trim()) { if (message.role === "assistant") { changedAssistantMessageCount += 1; } messageOperations.push({ seq: message.seq, role: message.role, speaker: message.speaker, beforeLength: String(message.content || "").length, afterLength: normalizedContent.length, operations, }); } if (!normalizedContent) { if (message.role === "assistant" && String(message.content || "").trim()) { droppedAssistantMessageCount += 1; } continue; } filteredMessages.push({ ...message, content: normalizedContent, extractionFilterOperations: operations, }); } const rawTranscript = formatExtractionTranscript( normalizedMessages.filter((message) => String(message.content || "").trim()), ); const filteredTranscript = formatExtractionTranscript(filteredMessages); return { rawMessages: normalizedMessages, filteredMessages, rawTranscript, filteredTranscript, debug: { rawMessageCount: normalizedMessages.length, filteredMessageCount: filteredMessages.length, rawRoleCounts: countRoles(normalizedMessages), filteredRoleCounts: countRoles(filteredMessages), rawTranscriptLength: rawTranscript.length, filteredTranscriptLength: filteredTranscript.length, changedAssistantMessageCount, droppedAssistantMessageCount, extractedAssistantMessageCount, excludedAssistantMessageCount, assistantBoundaryConfig: { extractRuleCount: extractRules.length, excludeRuleCount: excludeRules.length, extractRules: extractRules.map((rule) => rule.label), excludeRules: excludeRules.map((rule) => rule.label), }, rawMessages: normalizedMessages, filteredMessages, messageOperations, }, }; }