import { getContext, extension_settings } from "/scripts/extensions.js"; import { characters } from "/script.js"; import { extractBlocksByTags, applyExclusionRules } from './utils/rag-tag-extractor.js'; import { world_names, loadWorldInfo, createNewWorldInfo, createWorldInfoEntry, saveWorldInfo, } from "/scripts/world-info.js"; import { saveBook as loreSaveBook } from "./lore-service.js"; import { extensionName } from "../utils/settings.js"; import { getChatIdentifier } from "./lore.js"; import { compatibleWriteToLorebook } from "./tavernhelper-compatibility.js"; import { ingestTextToHanlinyuan } from "./rag-processor.js"; import { showSummaryModal, showHtmlModal } from "../ui/page-window.js"; import { getPresetPrompts, getMixedOrder } from '../PresetSettings/index.js'; import { generateRandomSeed } from "./api.js"; import { callNgmsAI } from "./api/Ngms_api.js"; import { executeAutoHide } from "./autoHideManager.js"; import { resolveHistoriographyRuleConfig } from "../utils/config/RuleProfileManager.js"; let reloadEditor = () => { console.warn("[大史官] reloadEditor 函数不可用,可能是旧版本。已使用空函数代替。"); }; (async () => { try { const { reloadEditor: importedReloadEditor } = await import("/scripts/world-info.js"); if (importedReloadEditor) { reloadEditor = importedReloadEditor; console.log("[大史官] 已成功动态导入 reloadEditor。"); } } catch (error) { console.warn("[大史官] 动态导入 reloadEditor 失败,将使用空函数。错误信息:", error.message); } })(); let isExpeditionRunning = false; let manualStopRequested = false; const RUNNING_LOG_COMMENT = "【敕史局】对话流水总帐"; const PROGRESS_SEAL_REGEX = /本条勿动【前(\d+)楼总结已完成】否则后续总结无法进行。$/; export async function readGoldenLedgerProgress(targetLorebookName) { if (!targetLorebookName) return 0; try { const bookData = await loadWorldInfo(targetLorebookName); if (!bookData || !bookData.entries) return 0; const ledgerEntry = Object.values(bookData.entries).find( (e) => e.comment === RUNNING_LOG_COMMENT && !e.disable, ); if (!ledgerEntry) return 0; const match = ledgerEntry.content.match(PROGRESS_SEAL_REGEX); return match ? parseInt(match[1], 10) : 0; } catch (error) { console.error(`[大史官] 阅览《${targetLorebookName}》天机时出错:`, error); return 0; } } export async function checkAndTriggerAutoSummary() { if (isExpeditionRunning) { return; } const settings = extension_settings[extensionName]; if (!settings.historiographySmallAutoEnable) return; const context = getContext(); let targetLorebookName = null; switch (settings.lorebookTarget) { case "character_main": targetLorebookName = characters[context.characterId]?.data?.extensions?.world; break; case "dedicated": const chatIdentifier = await getChatIdentifier(); targetLorebookName = `Amily2-Lore-${chatIdentifier}`; break; default: return; } if (!targetLorebookName) return; const characterCount = await readGoldenLedgerProgress(targetLorebookName); const currentChatLength = context.chat.length; const retentionCount = settings.historiographyRetentionCount ?? 5; const summarizableLength = currentChatLength - retentionCount; const unsummarizedCount = summarizableLength - characterCount; if (unsummarizedCount >= settings.historiographySmallTriggerThreshold) { const batchSize = settings.historiographySmallTriggerThreshold; const startFloor = characterCount + 1; const endFloor = Math.min(characterCount + batchSize, summarizableLength); console.log(`[大史官] 自动微言录已触发,处理 ${startFloor} 至 ${endFloor} 楼。`); const isInteractive = settings.historiographyAutoSummaryInteractive ?? false; await executeManualSummary(startFloor, endFloor, !isInteractive); } } export async function getAvailableWorldbooks() { return [...world_names]; } export async function getLoresForWorldbook(bookName) { if (!bookName) return []; try { const bookData = await loadWorldInfo(bookName); if (!bookData || !bookData.entries) return []; return Object.entries(bookData.entries) .filter(([, entry]) => !entry.disable) .map(([key, entry]) => ({ key: key, comment: entry.comment || "无标题条目", })); } catch (error) { console.error(`[大史官] 检阅《${bookName}》时出错:`, error); return []; } } function escapeHtml(text) { if (!text) return ''; return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } export async function executeManualSummary(startFloor, endFloor, isAuto = false) { return new Promise(async (resolve) => { const toastTitle = isAuto ? "微言录 (自动)" : "微言录 (手动)"; const context = getContext(); if (isAuto) { const messages = getRawMessagesForSummary(startFloor, endFloor); if (!messages || messages.length === 0) { toastr.warning("自动巡录:未找到符合条件的消息。", toastTitle); return resolve(false); } const textToSummarize = messages.map(m => `【第 ${m.floor} 楼】 ${m.author}: ${m.content}`).join('\n'); const summary = await getSummary(textToSummarize, toastTitle); if (summary) { showSummaryModal(summary, { onConfirm: async (finalSummary) => { const success = await writeSummary(finalSummary, startFloor, endFloor, toastTitle); resolve(success); }, onRegenerate: async (summaryDialog) => { summaryDialog.find('textarea').prop('disabled', true).val('正在重新生成,请稍候...'); const newSummary = await getSummary(textToSummarize, toastTitle); summaryDialog.find('textarea').prop('disabled', false).val(newSummary || summary); summaryDialog[0].showModal(); // 重新显示弹窗 if (!newSummary) { toastr.error("重新生成失败,已恢复原始内容。", "模型召唤失败"); } }, onCancel: () => { toastr.info("本批次总结已取消。", toastTitle); resolve(false); }, }); } else { resolve(false); } return; } const messages = getRawMessagesForSummary(startFloor, endFloor); if (!messages || messages.length === 0) { toastr.warning("选定的楼层范围内无有效对话或内容被规则排除。", "圣谕有误"); return resolve(false); } const generateModalHtml = (msgList) => { const messageHtml = msgList.map(msg => `
【第 ${msg.floor} 楼】 ${escapeHtml(msg.author)}
`).join(''); return `
${messageHtml}
`; }; const modalHtml = generateModalHtml(messages); showHtmlModal('原文预览与编辑', modalHtml, { okText: '确认原文并总结', cancelText: '取消', onOpen: (dialog) => { const userCheckbox = dialog.find('#hist-include-user'); const charCheckbox = dialog.find('#hist-include-char'); const container = dialog.find('#historiography-preview-container'); const updateVisibility = () => { const includeUser = userCheckbox.is(':checked'); const includeChar = charCheckbox.is(':checked'); container.find('.historiography-message-item').each(function() { const item = $(this); const authorType = item.data('author-type'); const shouldBeHidden = (authorType === 'user' && !includeUser) || (authorType === 'char' && !includeChar); item.toggle(!shouldBeHidden); }); }; userCheckbox.on('change', updateVisibility); charCheckbox.on('change', updateVisibility); }, onOk: async (dialog) => { const includeUser = dialog.find('#hist-include-user').is(':checked'); const includeChar = dialog.find('#hist-include-char').is(':checked'); const textToSummarize = dialog.find('.historiography-message-item') .filter(function() { const authorType = $(this).data('author-type'); if (authorType === 'user' && !includeUser) return false; if (authorType === 'char' && !includeChar) return false; return true; }) .find('textarea') .map(function() { const floor = $(this).data('floor'); const author = $(this).closest('.historiography-message-item').find('summary').text().replace(`【第 ${floor} 楼】 `, ''); return `【第 ${floor} 楼】 ${author}: ${$(this).val()}`; }).get().join('\n'); if (!textToSummarize.trim()) { toastr.error("请至少选择一条消息进行总结!", "圣谕有误"); return; } const dialogElement = dialog[0]; if (dialogElement && typeof dialogElement.close === 'function') { dialogElement.close(); } dialog.remove(); const summary = await getSummary(textToSummarize, toastTitle); if (summary) { showSummaryModal(summary, { onConfirm: async (finalSummary) => { const success = await writeSummary(finalSummary, startFloor, endFloor, toastTitle); resolve(success); }, onRegenerate: async (summaryDialog) => { summaryDialog.find('textarea').prop('disabled', true).val('正在重新生成,请稍候...'); const newSummary = await getSummary(textToSummarize, toastTitle); summaryDialog.find('textarea').prop('disabled', false).val(newSummary || summary); summaryDialog[0].showModal(); // 重新显示弹窗 if (!newSummary) { toastr.error("重新生成失败,已恢复原始内容。", "模型召唤失败"); } }, onCancel: () => { toastr.info("本批次总结已取消。", "操作已取消"); resolve(false); }, }); } else { resolve(false); } }, onCancel: () => { toastr.info("操作已取消。", toastTitle); resolve(false); } }); }); } function getRawMessagesForSummary(startFloor, endFloor) { const context = getContext(); const chat = context.chat; const settings = extension_settings[extensionName]; const historySlice = chat.slice(startFloor - 1, endFloor); if (historySlice.length === 0) return null; const userName = context.name1 || '用户'; const characterName = context.name2 || '角色'; const historiographyRuleConfig = resolveHistoriographyRuleConfig(settings); const useTagExtraction = historiographyRuleConfig.tagExtractionEnabled ?? false; const tagsToExtract = useTagExtraction ? (historiographyRuleConfig.tags || '').split(',').map(t => t.trim()).filter(Boolean) : []; const exclusionRules = historiographyRuleConfig.exclusionRules || []; const excludeUserMessages = historiographyRuleConfig.excludeUserMessages ?? false; const messages = historySlice.map((msg, index) => { if (excludeUserMessages && msg.is_user) return null; let content = msg.mes; if (useTagExtraction && tagsToExtract.length > 0) { const blocks = extractBlocksByTags(content, tagsToExtract); if (blocks.length > 0) { content = blocks.join('\n\n'); } } content = applyExclusionRules(content, exclusionRules); if (!content.trim()) return null; return { floor: startFloor + index, author: msg.is_user ? userName : characterName, authorType: msg.is_user ? 'user' : 'char', content: content.trim() }; }).filter(Boolean); return messages; } async function getSummary(formattedHistory, toastTitle, retryCount = 0) { toastr.info(`正在为您熔铸对话历史...`, toastTitle); const settings = extension_settings[extensionName]; const presetPrompts = await getPresetPrompts('small_summary'); // 获取混合排序 let mixedOrder; try { const savedOrder = localStorage.getItem('amily2_prompt_presets_v2_mixed_order'); if (savedOrder) { mixedOrder = JSON.parse(savedOrder); } } catch (e) { console.error("[大史官] 加载混合顺序失败:", e); } const order = getMixedOrder('small_summary') || []; const messages = [ { role: 'system', content: generateRandomSeed() } ]; // 根据混合排序添加提示词 let promptCounter = 0; // 用于跟踪已处理的提示词数量 for (const item of order) { if (item.type === 'prompt') { // 处理普通提示词 - getPresetPrompts已经按照mixedOrder排序,直接按顺序使用 if (presetPrompts && presetPrompts[promptCounter]) { messages.push(presetPrompts[promptCounter]); promptCounter++; // 递增计数器 } } else if (item.type === 'conditional') { // 处理条件块 switch (item.id) { case 'jailbreakPrompt': if (settings.historiographySmallJailbreakPrompt) { messages.push({ role: "system", content: settings.historiographySmallJailbreakPrompt }); } break; case 'summaryPrompt': if (settings.historiographySmallSummaryPrompt) { messages.push({ role: "system", content: settings.historiographySmallSummaryPrompt }); } break; case 'coreContent': messages.push({ role: 'user', content: `请严格根据以下"对话记录"中的内容进行总结,不要添加任何额外信息。\n\n<对话记录>\n${formattedHistory}\n` }); break; } } } // 历史总结统一走 NGMS slot;ngms 未配置时 callNgmsAI 自带模块名错误提示。 // 旧 ngmsEnabled 三元式 fallback 到 main 的设计已在主 API 移除后失效。 const summary = await callNgmsAI(messages); console.log('[大史官-微言录] AI回复的全部内容:', summary); if (!summary || !summary.trim()) { const maxRetries = settings.historiographyMaxRetries ?? 2; if (retryCount < maxRetries) { console.warn(`[大史官-微言录] AI返回空内容,正在进行第 ${retryCount + 1}/${maxRetries} 次重试...`); toastr.warning(`AI返回空内容,正在进行第 ${retryCount + 1}/${maxRetries} 次重试...`, toastTitle); await new Promise(resolve => setTimeout(resolve, 3000)); // 等待3秒后重试 return await getSummary(formattedHistory, toastTitle, retryCount + 1); } else { console.error(`[大史官-微言录] 达到最大重试次数 (${maxRetries}),总结失败。`); toastr.error(`达到最大重试次数 (${maxRetries}),总结失败。`, toastTitle); return null; } } return summary; } async function writeSummary(summary, startFloor, endFloor, toastTitle) { const settings = extension_settings[extensionName]; const context = getContext(); const shouldWriteToLorebook = settings.historiographyWriteToLorebook ?? true; const shouldIngestToRag = settings.historiographyIngestToRag ?? false; if (!shouldWriteToLorebook && !shouldIngestToRag) { toastr.warning("“写入史册”和“存入翰林院”均未启用,总结任务已完成但未保存。", toastTitle); return true; } if (shouldIngestToRag) { try { let targetLorebookName; switch (settings.lorebookTarget) { case "character_main": targetLorebookName = characters[context.characterId]?.data?.extensions?.world; if (!targetLorebookName) throw new Error("当前角色未绑定主世界书,无法为翰林院确定目标。"); break; case "dedicated": const chatIdentifier = await getChatIdentifier(); targetLorebookName = `Amily2-Lore-${chatIdentifier}`; break; default: throw new Error("未知的史册写入指令,无法为翰林院确定目标。"); } toastr.info('正在将此份“微言录”送往翰林院...', '翰林院'); const metadata = { bookName: targetLorebookName, entryName: `微言录总结: ${startFloor}-${endFloor}楼` }; const result = await ingestTextToHanlinyuan(summary, 'lorebook', metadata); if (result.success) toastr.success(`翰林院已成功接收记忆碎片!`, '翰林院'); else throw new Error(result.error); } catch (ragError) { console.error('[翰林院] 向量化处理失败:', ragError); toastr.error(`送往翰林院的文书处理失败: ${ragError.message}`, '翰林院'); } } if (shouldWriteToLorebook) { try { let targetLorebookName; switch (settings.lorebookTarget) { case "character_main": targetLorebookName = characters[context.characterId]?.data?.extensions?.world; if (!targetLorebookName) throw new Error("当前角色未绑定主世界书。"); break; case "dedicated": const chatIdentifier = await getChatIdentifier(); targetLorebookName = `Amily2-Lore-${chatIdentifier}`; break; default: throw new Error("未知的史册写入指令。"); } const contentUpdateCallback = (oldContent) => { const newSeal = `\n\n本条勿动【前${endFloor}楼总结已完成】否则后续总结无法进行。`; const newChapter = `\n\n---\n\n【${startFloor}楼至${endFloor}楼详细总结记录】\n${summary}`; if (oldContent) { const contentWithoutSeal = oldContent.replace(PROGRESS_SEAL_REGEX, "").trim(); return contentWithoutSeal + newChapter + newSeal; } else { const firstChapter = `以下是依照顺序已发生剧情` + newChapter; return firstChapter + newSeal; } }; console.log('[大史官-调试] 读取到的原始设置:', { loreActivationMode: settings.loreActivationMode, loreInsertionPosition: settings.loreInsertionPosition, loreDepth: settings.loreDepth, loreKeywords: settings.loreKeywords }); const optionsForNewEntry = { keys: (settings.loreKeywords.split(",").map(k => k.trim()).filter(Boolean)), isConstant: settings.loreActivationMode !== 'keyed', insertion_position: settings.loreInsertionPosition, depth: settings.loreDepth, }; console.log('[大史官-调试] 构建并传递的选项:', optionsForNewEntry); const success = await compatibleWriteToLorebook( targetLorebookName, RUNNING_LOG_COMMENT, contentUpdateCallback, optionsForNewEntry ); if (success) { toastr.success(`编年史已成功更新!`, `${toastTitle} - 国史馆`); executeAutoHide(); // 总结成功后立即触发自动隐藏 return true; } else { // 错误已在 compatibleWriteToLorebook 内部处理和记录 return false; } } catch (error) { console.error(`[大史官] ${toastTitle}写入国史馆失败:`, error); toastr.error(`写入国史馆时发生错误: ${error.message}`, "国史馆"); return false; } } return true; } const CHAPTER_SEAL_REGEX = /【前(\d+)楼篇章编撰已完成】/; export async function executeRefinement(worldbook, loreKey) { toastr.info(`遵旨!正在为您重铸《${worldbook}》中的【微言录合集】...`, "宏史卷重铸"); try { const bookData = await loadWorldInfo(worldbook); const entry = bookData?.entries[loreKey]; if (!entry) { toastr.error("找不到指定的史册条目,重铸任务中止。", "圣谕有误"); return; } const originalContent = entry.content; const settings = extension_settings[extensionName]; const progressSealMatch = originalContent.match(PROGRESS_SEAL_REGEX); if (!progressSealMatch) { toastr.error("史册缺少【流水金印】,无法执行重铸。", "结构异常"); return; } const progressSeal = progressSealMatch[0]; const totalFloors = parseInt(progressSealMatch[1], 10); const chapterSealMatch = originalContent.match(CHAPTER_SEAL_REGEX); let lockedContent = ""; let contentToRefine = ""; let oldChapterFloor = 0; if (chapterSealMatch) { const chapterSealText = chapterSealMatch[0]; oldChapterFloor = parseInt(chapterSealMatch[1], 10); const contentParts = originalContent.split(chapterSealText); lockedContent = contentParts[0].trim(); contentToRefine = contentParts[1].replace(PROGRESS_SEAL_REGEX, '').trim(); } else { contentToRefine = originalContent.replace(PROGRESS_SEAL_REGEX, '').trim(); } if (!contentToRefine.trim()) { toastr.warning("史册条目中没有新的内容可供重铸。", "国库无新事"); return; } const presetPrompts = await getPresetPrompts('large_summary'); let mixedOrder; try { const savedOrder = localStorage.getItem('amily2_prompt_presets_v2_mixed_order'); if (savedOrder) { mixedOrder = JSON.parse(savedOrder); } } catch (e) { console.error("[大史官] 加载混合顺序失败:", e); } const order = getMixedOrder('large_summary') || []; const messages = [ { role: 'system', content: generateRandomSeed() } ]; let promptCounter = 0; for (const item of order) { if (item.type === 'prompt') { if (presetPrompts && presetPrompts[promptCounter]) { messages.push(presetPrompts[promptCounter]); promptCounter++; } } else if (item.type === 'conditional') { switch (item.id) { case 'jailbreakPrompt': if (settings.historiographyLargeJailbreakPrompt) { messages.push({ role: "system", content: settings.historiographyLargeJailbreakPrompt }); } break; case 'summaryPrompt': if (settings.historiographyLargeRefinePrompt) { messages.push({ role: "system", content: settings.historiographyLargeRefinePrompt }); } break; case 'coreContent': messages.push({ role: "user", content: `<核心处理内容>\n\n${contentToRefine}\n\n` }); break; } } } const getRefinedContent = async (retryCount = 0) => { toastr.info("正在召唤模型进行内容精炼...", "宏史卷重铸"); // 历史总结统一走 NGMS slot;ngms 未配置时 callNgmsAI 自带错误提示。 const content = await callNgmsAI(messages); if (!content || !content.trim()) { const maxRetries = settings.historiographyMaxRetries ?? 2; if (retryCount < maxRetries) { console.warn(`[大史官-宏史卷重铸] AI返回空内容,正在进行第 ${retryCount + 1}/${maxRetries} 次重试...`); toastr.warning(`AI返回空内容,正在进行第 ${retryCount + 1}/${maxRetries} 次重试...`, "宏史卷重铸"); await new Promise(resolve => setTimeout(resolve, 3000)); return await getRefinedContent(retryCount + 1); } else { console.error(`[大史官-宏史卷重铸] 达到最大重试次数 (${maxRetries}),重铸失败。`); toastr.error(`达到最大重试次数 (${maxRetries}),重铸失败。`, "宏史卷重铸失败"); return null; } } return content; }; const initialRefinedContent = await getRefinedContent(); if (!initialRefinedContent) { return; // 错误提示已在 getRefinedContent 中处理 } const processLoop = async (currentRefinedContent) => { showSummaryModal(currentRefinedContent, { onConfirm: async (editedText) => { let finalContent; const newChapterSeal = `\n\n【前${totalFloors}楼篇章编撰已完成】`; const shouldVectorize = document.getElementById('amily2_vectorize_summary_content')?.checked ?? false; if (shouldVectorize && chapterSealMatch) { try { toastr.info(`正在将前 ${oldChapterFloor} 楼的“宏史卷”内容送往翰林院...`, '翰林院'); const metadata = { bookName: worldbook, entryName: `宏史卷总结: 1-${oldChapterFloor}楼` }; const ingestResult = await ingestTextToHanlinyuan(lockedContent, 'lorebook', metadata); if (!ingestResult.success) { throw new Error(ingestResult.error || "未知错误"); } toastr.success(`翰林院已成功接收旧“宏史卷”记忆!新增 ${ingestResult.count} 条。`, '翰林院'); const replacementText = `AI你好,以上内容为rag向量化后注入的相关剧情,以下内容是已发生的剧情回顾。\n\n(前${oldChapterFloor}楼聊天记录总结已由翰林院向量化注入。)\n\n【以下内容为${oldChapterFloor}楼以后的总结内容】`; finalContent = `${replacementText}\n\n---\n\n${editedText}${newChapterSeal}\n\n${progressSeal}`; } catch (error) { console.error('[大史官-宏史卷向量化] 失败:', error); toastr.error(`宏史卷向量化失败: ${error.message},将执行标准保存。`, '翰林院'); const divider = `\n\n===【截止至第${oldChapterFloor}楼的宏史卷】===\n\n`; finalContent = `${lockedContent}${divider}${editedText}${newChapterSeal}\n\n${progressSeal}`; } } else { if (chapterSealMatch) { const divider = `\n\n===【截止至第${oldChapterFloor}楼的宏史卷】===\n\n`; finalContent = `${lockedContent}${divider}${editedText}${newChapterSeal}\n\n${progressSeal}`; } else { const header = `以下内容是【1楼-${totalFloors}楼】已发生的剧情回顾。\n\n---\n\n`; finalContent = `${header}${editedText}${newChapterSeal}\n\n${progressSeal}`; } } entry.content = finalContent; await loreSaveBook(worldbook, bookData); reloadEditor(worldbook); toastr.success(`史册已成功重铸,并保存于《${worldbook}》!`, "宏史卷重铸完毕"); }, onRegenerate: async (dialog) => { dialog.find('textarea').prop('disabled', true).val('正在重新生成,请稍候...'); const newContent = await getRefinedContent(); dialog.find('textarea').prop('disabled', false).val(newContent || currentRefinedContent); dialog[0].showModal(); // 重新显示弹窗 if (!newContent) { toastr.error("重新生成失败,已恢复原始内容。", "模型召唤失败"); } }, onCancel: () => { toastr.info("宏史卷重铸操作已取消。", "操作已取消"); }, }); }; await processLoop(initialRefinedContent); } catch (error) { console.error("[大史官] 重铸任务失败:", error); toastr.error(`重铸史册时发生严重错误: ${error.message}`, "国史馆"); } } export async function executeExpedition() { if (isExpeditionRunning) { toastr.info("远征军已在途中,无需重复下令。", "圣谕悉知"); return; } isExpeditionRunning = true; manualStopRequested = false; document.dispatchEvent(new CustomEvent('amily2-expedition-state-change', { detail: { isRunning: true } })); try { const settings = extension_settings[extensionName]; const context = getContext(); let targetLorebookName = null; switch (settings.lorebookTarget) { case "character_main": targetLorebookName = characters[context.characterId]?.data?.extensions?.world; if (!targetLorebookName) { toastr.error("当前角色未绑定主世界书,远征军无法开拔!", "圣谕不明"); isExpeditionRunning = false; document.dispatchEvent(new CustomEvent('amily2-expedition-state-change', { detail: { isRunning: false, manualStop: false } })); return; } break; case "dedicated": const chatIdentifier = await getChatIdentifier(); targetLorebookName = `Amily2-Lore-${chatIdentifier}`; break; default: toastr.error("未知的史册写入目标,远征军无法开拔!", "圣谕不明"); isExpeditionRunning = false; document.dispatchEvent(new CustomEvent('amily2-expedition-state-change', { detail: { isRunning: false, manualStop: false } })); return; } const summarizedCount = await readGoldenLedgerProgress(targetLorebookName); const retentionCount = settings.historiographyRetentionCount ?? 5; const totalHistory = context.chat.length; const summarizableLength = totalHistory - retentionCount; const remainingHistory = summarizableLength - summarizedCount; if (remainingHistory <= 0) { toastr.info("国史已是最新,远征军无需出动。", "凯旋"); isExpeditionRunning = false; document.dispatchEvent(new CustomEvent('amily2-expedition-state-change', { detail: { isRunning: false, manualStop: false } })); return; } const batchSize = settings.historiographySmallTriggerThreshold; const totalBatches = Math.ceil(remainingHistory / batchSize); toastr.info(`远征军已开拔!目标:${remainingHistory} 层历史,分 ${totalBatches} 批次征服!`, "远征开始"); let currentProgress = summarizedCount; for (let i = 0; i < totalBatches; i++) { if (manualStopRequested) { toastr.warning("远征已遵从您的敕令暂停!随时可以【继续远征】。", "鸣金收兵"); break; } const startFloor = currentProgress + 1; const endFloor = Math.min(currentProgress + batchSize, summarizableLength); const toastTitle = `远征战役 (${i + 1}/${totalBatches})`; const delay = 2000; if (i > 0) { toastr.info(`第 ${i + 1} 批次战役准备中... (${delay / 1000}秒后接敌)`, toastTitle); await new Promise(resolve => setTimeout(resolve, delay)); } if (manualStopRequested) { toastr.warning("远征已在准备阶段遵令暂停!", "鸣金收兵"); break; } const success = await executeManualSummary(startFloor, endFloor, false); if (success) { currentProgress = endFloor; } else { toastr.warning(`远征因第 ${i + 1} 批次任务失败而中止。`, "远征中止"); manualStopRequested = true; break; } } if(!manualStopRequested) { toastr.success("凯旋!远征大捷!所有未载之史均已化为帝国永恒的记忆!", "远征完毕"); } } catch (error) { console.error("[大史官-远征失败]", error); toastr.error("远征途中遭遇重大挫折,任务中止!您可以随时【继续远征】。", "远征失败"); } finally { isExpeditionRunning = false; document.dispatchEvent(new CustomEvent('amily2-expedition-state-change', { detail: { isRunning: false, manualStop: manualStopRequested } })); } } export function stopExpedition() { if (isExpeditionRunning) { manualStopRequested = true; toastr.info("停战敕令已下达!远征军将在完成当前批次的任务后休整。", "圣谕传达"); } else { toastr.warning("远征军已在营中,无需下达停战敕令。", "圣谕悉知"); } } export async function executeCompilation(worldbook, loreKeys) { if (!Array.isArray(loreKeys) || loreKeys.length === 0) { toastr.warning("未选择任何条目进行编纂。", "圣谕不明"); return { success: false, error: "No lore keys provided." }; } toastr.info(`遵旨!开始对《${worldbook}》中的 ${loreKeys.length} 个条目进行批量编纂...`, "翰林院入库"); let totalSuccessCount = 0; let totalVectorCount = 0; let errors = []; try { const bookData = await loadWorldInfo(worldbook); if (!bookData || !bookData.entries) { throw new Error(`无法加载书库《${worldbook}》的数据。`); } for (const loreKey of loreKeys) { const entry = bookData.entries[loreKey]; if (!entry) { errors.push(`条目【${loreKey}】未找到。`); continue; } const contentToIngest = entry.content; if (!contentToIngest.trim()) { errors.push(`条目【${entry.comment || loreKey}】内容为空。`); continue; } const metadata = { bookName: worldbook, entryName: entry.comment || loreKey }; try { const ingestResult = await ingestTextToHanlinyuan(contentToIngest, 'lorebook', metadata); if (ingestResult.success) { totalSuccessCount++; totalVectorCount += ingestResult.count; } else { errors.push(`条目【${entry.comment || loreKey}】处理失败: ${ingestResult.error}`); } } catch (ingestError) { errors.push(`条目【${entry.comment || loreKey}】处理时发生严重错误: ${ingestError.message}`); } } let finalMessage = `批量编纂完成!\n成功处理 ${totalSuccessCount} / ${loreKeys.length} 个条目,共新增 ${totalVectorCount} 条忆识。`; if (errors.length > 0) { finalMessage += `\n\n发生以下错误:\n- ${errors.join('\n- ')}`; toastr.warning("批量编纂期间发生部分错误,详情请查看控制台。", "翰林院"); console.warn("[翰林院] 批量编纂错误详情:", errors); } else { toastr.success(`批量编纂大功告成!新增 ${totalVectorCount} 条忆识。`, '翰林院'); } return { success: errors.length === 0, content: finalMessage, totalSuccess: totalSuccessCount, totalVectors: totalVectorCount, errors: errors }; } catch (error) { console.error("[翰林院] 批量条目入库失败:", error); toastr.error(`批量入库失败: ${error.message}`, "翰林院"); return { success: false, error: error.message }; } } // ========== 史册归档与回溯系统 ========== async function getTargetLorebookName() { const settings = extension_settings[extensionName]; const context = getContext(); let targetLorebookName = null; switch (settings.lorebookTarget) { case "character_main": targetLorebookName = characters[context.characterId]?.data?.extensions?.world; break; case "dedicated": const chatIdentifier = await getChatIdentifier(); targetLorebookName = `Amily2-Lore-${chatIdentifier}`; break; } return targetLorebookName; } export async function archiveCurrentLedger() { try { const targetLorebookName = await getTargetLorebookName(); if (!targetLorebookName) { toastr.error("无法确定目标世界书,归档失败。", "圣谕不明"); return false; } const bookData = await loadWorldInfo(targetLorebookName); if (!bookData || !bookData.entries) { toastr.error(`无法读取世界书《${targetLorebookName}》。`, "国史馆"); return false; } const ledgerEntryKey = Object.keys(bookData.entries).find( (key) => bookData.entries[key].comment === RUNNING_LOG_COMMENT && !bookData.entries[key].disable ); if (!ledgerEntryKey) { toastr.info("当前没有活跃的【对话流水总帐】,无需归档。", "国史馆"); return false; } const entry = bookData.entries[ledgerEntryKey]; const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); const newComment = `${RUNNING_LOG_COMMENT}_归档_${timestamp}`; entry.comment = newComment; entry.disable = true; await loreSaveBook(targetLorebookName, bookData); reloadEditor(targetLorebookName); toastr.success(`已将当前流水总帐归档为:\n${newComment}`, "归档成功"); return true; } catch (error) { console.error("[大史官] 归档失败:", error); toastr.error(`归档失败: ${error.message}`, "国史馆"); return false; } } export async function getArchivedLedgers() { try { const targetLorebookName = await getTargetLorebookName(); if (!targetLorebookName) return []; const bookData = await loadWorldInfo(targetLorebookName); if (!bookData || !bookData.entries) return []; const archivedLedgers = Object.entries(bookData.entries) .filter(([, entry]) => entry.comment && entry.comment.startsWith(`${RUNNING_LOG_COMMENT}_归档_`)) .map(([key, entry]) => ({ key: key, comment: entry.comment })) .sort((a, b) => b.comment.localeCompare(a.comment)); // 按时间倒序排列 return archivedLedgers; } catch (error) { console.error("[大史官] 获取归档列表失败:", error); return []; } } export async function restoreArchivedLedger(targetLoreKey) { try { const targetLorebookName = await getTargetLorebookName(); if (!targetLorebookName) { toastr.error("无法确定目标世界书,回溯失败。", "圣谕不明"); return false; } const bookData = await loadWorldInfo(targetLorebookName); if (!bookData || !bookData.entries) { toastr.error(`无法读取世界书《${targetLorebookName}》。`, "国史馆"); return false; } const targetEntry = bookData.entries[targetLoreKey]; if (!targetEntry) { toastr.error("找不到指定的归档史册。", "圣谕有误"); return false; } const currentActiveKey = Object.keys(bookData.entries).find( (key) => bookData.entries[key].comment === RUNNING_LOG_COMMENT && !bookData.entries[key].disable ); if (currentActiveKey) { if (currentActiveKey !== targetLoreKey) { const activeEntry = bookData.entries[currentActiveKey]; const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); activeEntry.comment = `${RUNNING_LOG_COMMENT}_归档_${timestamp}`; activeEntry.disable = true; toastr.info(`已自动归档原有的活跃史册为: ${activeEntry.comment}`, "自动归档"); } } targetEntry.comment = RUNNING_LOG_COMMENT; targetEntry.disable = false; await loreSaveBook(targetLorebookName, bookData); reloadEditor(targetLorebookName); toastr.success("史册回溯成功!时光已倒流,旧史重现。", "回溯成功"); return true; } catch (error) { console.error("[大史官] 回溯失败:", error); toastr.error(`回溯失败: ${error.message}`, "国史馆"); return false; } }