diff --git a/modules/story-summary.js b/modules/story-summary.js deleted file mode 100644 index e18dc3e..0000000 --- a/modules/story-summary.js +++ /dev/null @@ -1,1100 +0,0 @@ -// ═══════════════════════════════════════════════════════════════════════════ -// 导入 -// ═══════════════════════════════════════════════════════════════════════════ - -import { extension_settings, getContext, saveMetadataDebounced } from "../../../../../extensions.js"; -import { - chat_metadata, - extension_prompts, - extension_prompt_types, - extension_prompt_roles, -} from "../../../../../../script.js"; -import { EXT_ID, extensionFolderPath } from "../../core/constants.js"; -import { createModuleEvents, event_types } from "../../core/event-manager.js"; -import { xbLog, CacheRegistry } from "../../core/debug-core.js"; -import { postToIframe, isTrustedMessage } from "../../core/iframe-messaging.js"; -import { CommonSettingStorage } from "../../core/server-storage.js"; -import { generateSummary, parseSummaryJson } from "./llm-service.js"; - -// ═══════════════════════════════════════════════════════════════════════════ -// 常量 -// ═══════════════════════════════════════════════════════════════════════════ - -const MODULE_ID = 'storySummary'; -const events = createModuleEvents(MODULE_ID); -const SUMMARY_SESSION_ID = 'xb9'; -const SUMMARY_PROMPT_KEY = 'LittleWhiteBox_StorySummary'; -const SUMMARY_CONFIG_KEY = 'storySummaryPanelConfig'; -const iframePath = `${extensionFolderPath}/modules/story-summary/story-summary.html`; -const VALID_SECTIONS = ['keywords', 'events', 'characters', 'arcs']; - -// ═══════════════════════════════════════════════════════════════════════════ -// 状态变量 -// ═══════════════════════════════════════════════════════════════════════════ - -let summaryGenerating = false; -let overlayCreated = false; -let frameReady = false; -let currentMesId = null; -let pendingFrameMessages = []; -let eventsRegistered = false; - -// ═══════════════════════════════════════════════════════════════════════════ -// 工具函数 -// ═══════════════════════════════════════════════════════════════════════════ - -const sleep = ms => new Promise(r => setTimeout(r, ms)); - -function getKeepVisibleCount() { - const store = getSummaryStore(); - return store?.keepVisibleCount ?? 3; -} - -function calcHideRange(lastSummarized) { - const keepCount = getKeepVisibleCount(); - const hideEnd = lastSummarized - keepCount; - if (hideEnd < 0) return null; - return { start: 0, end: hideEnd }; -} - -function getSettings() { - const ext = extension_settings[EXT_ID] ||= {}; - ext.storySummary ||= { enabled: true }; - return ext; -} - -function getSummaryStore() { - const { chatId } = getContext(); - if (!chatId) return null; - chat_metadata.extensions ||= {}; - chat_metadata.extensions[EXT_ID] ||= {}; - chat_metadata.extensions[EXT_ID].storySummary ||= {}; - return chat_metadata.extensions[EXT_ID].storySummary; -} - -function saveSummaryStore() { - saveMetadataDebounced?.(); -} - -async function executeSlashCommand(command) { - try { - const executeCmd = window.executeSlashCommands - || window.executeSlashCommandsOnChatInput - || (typeof SillyTavern !== 'undefined' && SillyTavern.getContext()?.executeSlashCommands); - if (executeCmd) { - await executeCmd(command); - } else if (typeof window.STscript === 'function') { - await window.STscript(command); - } - } catch (e) { - xbLog.error(MODULE_ID, `执行命令失败: ${command}`, e); - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 总结数据工具(保留在主模块,因为依赖 store 对象) -// ═══════════════════════════════════════════════════════════════════════════ - -function formatExistingSummaryForAI(store) { - if (!store?.json) return "(空白,这是首次总结)"; - const data = store.json; - const parts = []; - - if (data.events?.length) { - parts.push("【已记录事件】"); - data.events.forEach((ev, i) => parts.push(`${i + 1}. [${ev.timeLabel}] ${ev.title}:${ev.summary}`)); - } - if (data.characters?.main?.length) { - const names = data.characters.main.map(m => typeof m === 'string' ? m : m.name); - parts.push(`\n【主要角色】${names.join("、")}`); - } - if (data.characters?.relationships?.length) { - parts.push("【人物关系】"); - data.characters.relationships.forEach(r => parts.push(`- ${r.from} → ${r.to}:${r.label}(${r.trend})`)); - } - if (data.arcs?.length) { - parts.push("【角色弧光】"); - data.arcs.forEach(a => parts.push(`- ${a.name}:${a.trajectory}(进度${Math.round(a.progress * 100)}%)`)); - } - if (data.keywords?.length) { - parts.push(`\n【关键词】${data.keywords.map(k => k.text).join("、")}`); - } - - return parts.join("\n") || "(空白,这是首次总结)"; -} - -function getNextEventId(store) { - const events = store?.json?.events || []; - if (events.length === 0) return 1; - const maxId = Math.max(...events.map(e => { - const match = e.id?.match(/evt-(\d+)/); - return match ? parseInt(match[1]) : 0; - })); - return maxId + 1; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 快照与数据合并 -// ═══════════════════════════════════════════════════════════════════════════ - -function addSummarySnapshot(store, endMesId) { - store.summaryHistory ||= []; - store.summaryHistory.push({ endMesId }); -} - -function mergeNewData(oldJson, parsed, endMesId) { - const merged = structuredClone(oldJson || {}); - merged.keywords ||= []; - merged.events ||= []; - merged.characters ||= {}; - merged.characters.main ||= []; - merged.characters.relationships ||= []; - merged.arcs ||= []; - - // 关键词:完全替换(全局关键词) - if (parsed.keywords?.length) { - merged.keywords = parsed.keywords.map(k => ({ ...k, _addedAt: endMesId })); - } - - // 事件:追加 - (parsed.events || []).forEach(e => { - e._addedAt = endMesId; - merged.events.push(e); - }); - - // 新角色:追加不重复 - const existingMain = new Set( - (merged.characters.main || []).map(m => typeof m === 'string' ? m : m.name) - ); - (parsed.newCharacters || []).forEach(name => { - if (!existingMain.has(name)) { - merged.characters.main.push({ name, _addedAt: endMesId }); - } - }); - - // 关系:更新或追加 - const relMap = new Map( - (merged.characters.relationships || []).map(r => [`${r.from}->${r.to}`, r]) - ); - (parsed.newRelationships || []).forEach(r => { - const key = `${r.from}->${r.to}`; - const existing = relMap.get(key); - if (existing) { - existing.label = r.label; - existing.trend = r.trend; - } else { - r._addedAt = endMesId; - relMap.set(key, r); - } - }); - merged.characters.relationships = Array.from(relMap.values()); - - // 弧光:更新或追加 - const arcMap = new Map((merged.arcs || []).map(a => [a.name, a])); - (parsed.arcUpdates || []).forEach(update => { - const existing = arcMap.get(update.name); - if (existing) { - existing.trajectory = update.trajectory; - existing.progress = update.progress; - if (update.newMoment) { - existing.moments = existing.moments || []; - existing.moments.push({ text: update.newMoment, _addedAt: endMesId }); - } - } else { - arcMap.set(update.name, { - name: update.name, - trajectory: update.trajectory, - progress: update.progress, - moments: update.newMoment ? [{ text: update.newMoment, _addedAt: endMesId }] : [], - _addedAt: endMesId, - }); - } - }); - merged.arcs = Array.from(arcMap.values()); - - return merged; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 回滚逻辑 -// ═══════════════════════════════════════════════════════════════════════════ - -function rollbackSummaryIfNeeded() { - const { chat } = getContext(); - const currentLength = Array.isArray(chat) ? chat.length : 0; - const store = getSummaryStore(); - - if (!store || store.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) { - return false; - } - - const lastSummarized = store.lastSummarizedMesId; - - if (currentLength <= lastSummarized) { - const deletedCount = lastSummarized + 1 - currentLength; - - if (deletedCount < 2) { - return false; - } - - xbLog.warn(MODULE_ID, `删除已总结楼层 ${deletedCount} 条,当前${currentLength},原总结到${lastSummarized + 1},触发回滚`); - - const history = store.summaryHistory || []; - let targetEndMesId = -1; - - for (let i = history.length - 1; i >= 0; i--) { - if (history[i].endMesId < currentLength) { - targetEndMesId = history[i].endMesId; - break; - } - } - - executeFilterRollback(store, targetEndMesId, currentLength); - return true; - } - - return false; -} - -function executeFilterRollback(store, targetEndMesId, currentLength) { - const oldLastSummarized = store.lastSummarizedMesId ?? -1; - const wasHidden = store.hideSummarizedHistory; - const oldHideRange = wasHidden ? calcHideRange(oldLastSummarized) : null; - - if (targetEndMesId < 0) { - store.lastSummarizedMesId = -1; - store.json = null; - store.summaryHistory = []; - store.hideSummarizedHistory = false; - } else { - const json = store.json || {}; - - json.events = (json.events || []).filter(e => (e._addedAt ?? 0) <= targetEndMesId); - json.keywords = (json.keywords || []).filter(k => (k._addedAt ?? 0) <= targetEndMesId); - json.arcs = (json.arcs || []).filter(a => (a._addedAt ?? 0) <= targetEndMesId); - json.arcs.forEach(a => { - a.moments = (a.moments || []).filter(m => - typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId - ); - }); - - if (json.characters) { - json.characters.main = (json.characters.main || []).filter(m => - typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId - ); - json.characters.relationships = (json.characters.relationships || []).filter(r => - (r._addedAt ?? 0) <= targetEndMesId - ); - } - - store.json = json; - store.lastSummarizedMesId = targetEndMesId; - store.summaryHistory = (store.summaryHistory || []).filter(h => h.endMesId <= targetEndMesId); - } - - if (oldHideRange && oldHideRange.end >= 0) { - const newHideRange = (targetEndMesId >= 0 && store.hideSummarizedHistory) - ? calcHideRange(targetEndMesId) - : null; - - const unhideStart = newHideRange ? Math.min(newHideRange.end + 1, currentLength) : 0; - const unhideEnd = Math.min(oldHideRange.end, currentLength - 1); - - if (unhideStart <= unhideEnd) { - executeSlashCommand(`/unhide ${unhideStart}-${unhideEnd}`); - } - } - - store.updatedAt = Date.now(); - saveSummaryStore(); - updateSummaryExtensionPrompt(); - notifyFrameAfterRollback(store); -} - -function notifyFrameAfterRollback(store) { - const { chat } = getContext(); - const totalFloors = Array.isArray(chat) ? chat.length : 0; - const lastSummarized = store.lastSummarizedMesId ?? -1; - - if (store.json) { - postToFrame({ - type: "SUMMARY_FULL_DATA", - payload: { - keywords: store.json.keywords || [], - events: store.json.events || [], - characters: store.json.characters || { main: [], relationships: [] }, - arcs: store.json.arcs || [], - lastSummarizedMesId: lastSummarized, - }, - }); - } else { - postToFrame({ type: "SUMMARY_CLEARED", payload: { totalFloors } }); - } - - postToFrame({ - type: "SUMMARY_BASE_DATA", - stats: { - totalFloors, - summarizedUpTo: lastSummarized + 1, - eventsCount: store.json?.events?.length || 0, - pendingFloors: totalFloors - lastSummarized - 1, - }, - }); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 生成状态管理 -// ═══════════════════════════════════════════════════════════════════════════ - -function setSummaryGenerating(flag) { - summaryGenerating = !!flag; - postToFrame({ type: "GENERATION_STATE", isGenerating: summaryGenerating }); -} - -function isSummaryGenerating() { - return summaryGenerating; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// iframe 通讯 -// ═══════════════════════════════════════════════════════════════════════════ - -function postToFrame(payload) { - const iframe = document.getElementById("xiaobaix-story-summary-iframe"); - if (!iframe?.contentWindow || !frameReady) { - pendingFrameMessages.push(payload); - return; - } - postToIframe(iframe, payload, "LittleWhiteBox"); -} - -function flushPendingFrameMessages() { - if (!frameReady) return; - const iframe = document.getElementById("xiaobaix-story-summary-iframe"); - if (!iframe?.contentWindow) return; - pendingFrameMessages.forEach(p => postToIframe(iframe, p, "LittleWhiteBox")); - pendingFrameMessages = []; -} - -function handleFrameMessage(event) { - const iframe = document.getElementById("xiaobaix-story-summary-iframe"); - if (!isTrustedMessage(event, iframe, "LittleWhiteBox-StoryFrame")) return; - const data = event.data; - - switch (data.type) { - case "FRAME_READY": - frameReady = true; - flushPendingFrameMessages(); - setSummaryGenerating(summaryGenerating); - sendSavedConfigToFrame(); - break; - - case "SETTINGS_OPENED": - case "FULLSCREEN_OPENED": - case "EDITOR_OPENED": - $(".xb-ss-close-btn").hide(); - break; - - case "SETTINGS_CLOSED": - case "FULLSCREEN_CLOSED": - case "EDITOR_CLOSED": - $(".xb-ss-close-btn").show(); - break; - - case "REQUEST_GENERATE": { - const ctx = getContext(); - currentMesId = (ctx.chat?.length ?? 1) - 1; - runSummaryGeneration(currentMesId, data.config || {}); - break; - } - - case "REQUEST_CANCEL": - window.xiaobaixStreamingGeneration?.cancel?.(SUMMARY_SESSION_ID); - setSummaryGenerating(false); - postToFrame({ type: "SUMMARY_STATUS", statusText: "已停止" }); - break; - - case "REQUEST_CLEAR": { - const { chat } = getContext(); - const store = getSummaryStore(); - if (store) { - delete store.json; - store.lastSummarizedMesId = -1; - store.updatedAt = Date.now(); - saveSummaryStore(); - } - clearSummaryExtensionPrompt(); - postToFrame({ - type: "SUMMARY_CLEARED", - payload: { totalFloors: Array.isArray(chat) ? chat.length : 0 }, - }); - xbLog.info(MODULE_ID, '总结数据已清空'); - break; - } - - case "CLOSE_PANEL": - hideOverlay(); - break; - - case "UPDATE_SECTION": { - const store = getSummaryStore(); - if (!store) break; - store.json ||= {}; - if (VALID_SECTIONS.includes(data.section)) { - store.json[data.section] = data.data; - } - store.updatedAt = Date.now(); - saveSummaryStore(); - updateSummaryExtensionPrompt(); - break; - } - - case "TOGGLE_HIDE_SUMMARIZED": { - const store = getSummaryStore(); - if (!store) break; - const lastSummarized = store.lastSummarizedMesId ?? -1; - // 无论是否有总结,都保存用户的偏好设置 - store.hideSummarizedHistory = !!data.enabled; - saveSummaryStore(); - // 只有有总结时才执行隐藏/显示命令 - if (lastSummarized >= 0) { - if (data.enabled) { - const range = calcHideRange(lastSummarized); - if (range) executeSlashCommand(`/hide ${range.start}-${range.end}`); - } else { - executeSlashCommand(`/unhide 0-${lastSummarized}`); - } - } - break; - } - - case "UPDATE_KEEP_VISIBLE": { - const store = getSummaryStore(); - if (!store) break; - - const oldCount = store.keepVisibleCount ?? 3; - const newCount = Math.max(0, Math.min(50, parseInt(data.count) || 3)); - - if (newCount === oldCount) break; - - store.keepVisibleCount = newCount; - saveSummaryStore(); - - const lastSummarized = store.lastSummarizedMesId ?? -1; - - if (store.hideSummarizedHistory && lastSummarized >= 0) { - (async () => { - await executeSlashCommand(`/unhide 0-${lastSummarized}`); - const range = calcHideRange(lastSummarized); - if (range) { - await executeSlashCommand(`/hide ${range.start}-${range.end}`); - } - const { chat } = getContext(); - sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0); - })(); - } else { - const { chat } = getContext(); - sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0); - } - break; - } - - case "SAVE_PANEL_CONFIG": - if (data.config) { - CommonSettingStorage.set(SUMMARY_CONFIG_KEY, data.config); - xbLog.info(MODULE_ID, '面板配置已保存到服务器'); - } - break; - - case "REQUEST_PANEL_CONFIG": - sendSavedConfigToFrame(); - break; - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Overlay 面板 -// ═══════════════════════════════════════════════════════════════════════════ - -function createOverlay() { - if (overlayCreated) return; - overlayCreated = true; - - const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent); - const isNarrow = window.matchMedia?.('(max-width: 768px)').matches; - const overlayHeight = (isMobile || isNarrow) ? '92.5vh' : '100vh'; - - const $overlay = $(` - - `); - - $overlay.on("click", ".xb-ss-backdrop, .xb-ss-close-btn", hideOverlay); - document.body.appendChild($overlay[0]); - // eslint-disable-next-line no-restricted-syntax - window.addEventListener("message", handleFrameMessage); -} - -function showOverlay() { - if (!overlayCreated) createOverlay(); - $("#xiaobaix-story-summary-overlay").show(); -} - -function hideOverlay() { - $("#xiaobaix-story-summary-overlay").hide(); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 楼层按钮 -// ═══════════════════════════════════════════════════════════════════════════ - -function createSummaryBtn(mesId) { - const btn = document.createElement('div'); - btn.className = 'mes_btn xiaobaix-story-summary-btn'; - btn.title = '剧情总结'; - btn.dataset.mesid = mesId; - btn.innerHTML = ''; - btn.addEventListener('click', e => { - e.preventDefault(); - e.stopPropagation(); - if (!getSettings().storySummary?.enabled) return; - currentMesId = Number(mesId); - openPanelForMessage(currentMesId); - }); - return btn; -} - -function addSummaryBtnToMessage(mesId) { - if (!getSettings().storySummary?.enabled) return; - const msg = document.querySelector(`#chat .mes[mesid="${mesId}"]`); - if (!msg || msg.querySelector('.xiaobaix-story-summary-btn')) return; - const btn = createSummaryBtn(mesId); - if (window.registerButtonToSubContainer?.(mesId, btn)) return; - msg.querySelector('.flex-container.flex1.alignitemscenter')?.appendChild(btn); -} - -function initButtonsForAll() { - if (!getSettings().storySummary?.enabled) return; - $("#chat .mes").each((_, el) => { - const mesId = el.getAttribute("mesid"); - if (mesId != null) addSummaryBtnToMessage(mesId); - }); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 打开面板与数据发送 -// ═══════════════════════════════════════════════════════════════════════════ - -async function sendSavedConfigToFrame() { - try { - const savedConfig = await CommonSettingStorage.get(SUMMARY_CONFIG_KEY, null); - if (savedConfig) { - postToFrame({ type: "LOAD_PANEL_CONFIG", config: savedConfig }); - xbLog.info(MODULE_ID, '已从服务器加载面板配置'); - } - } catch (e) { - xbLog.warn(MODULE_ID, '加载面板配置失败', e); - } -} - -function sendFrameBaseData(store, totalFloors) { - const lastSummarized = store?.lastSummarizedMesId ?? -1; - const range = calcHideRange(lastSummarized); - const hiddenCount = range ? range.end + 1 : 0; - - postToFrame({ - type: "SUMMARY_BASE_DATA", - stats: { - totalFloors, - summarizedUpTo: lastSummarized + 1, - eventsCount: store?.json?.events?.length || 0, - pendingFloors: totalFloors - lastSummarized - 1, - hiddenCount, - }, - hideSummarized: store?.hideSummarizedHistory || false, - keepVisibleCount: store?.keepVisibleCount ?? 3, - }); -} - -function sendFrameFullData(store, totalFloors) { - const lastSummarized = store?.lastSummarizedMesId ?? -1; - if (store?.json) { - postToFrame({ - type: "SUMMARY_FULL_DATA", - payload: { - keywords: store.json.keywords || [], - events: store.json.events || [], - characters: store.json.characters || { main: [], relationships: [] }, - arcs: store.json.arcs || [], - lastSummarizedMesId: lastSummarized, - }, - }); - } else { - postToFrame({ type: "SUMMARY_CLEARED", payload: { totalFloors } }); - } -} - -function openPanelForMessage(mesId) { - createOverlay(); - showOverlay(); - const { chat } = getContext(); - const store = getSummaryStore(); - const totalFloors = chat.length; - sendFrameBaseData(store, totalFloors); - sendFrameFullData(store, totalFloors); - setSummaryGenerating(summaryGenerating); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 增量总结生成 -// ═══════════════════════════════════════════════════════════════════════════ - -function buildIncrementalSlice(targetMesId, lastSummarizedMesId, maxPerRun = 100) { - const { chat, name1, name2 } = getContext(); - const start = Math.max(0, (lastSummarizedMesId ?? -1) + 1); - const rawEnd = Math.min(targetMesId, chat.length - 1); - const end = Math.min(rawEnd, start + maxPerRun - 1); - if (start > end) return { text: "", count: 0, range: "", endMesId: -1 }; - - const userLabel = name1 || '用户'; - const charLabel = name2 || '角色'; - const slice = chat.slice(start, end + 1); - - const text = slice.map((m, i) => { - const speaker = m.name || (m.is_user ? userLabel : charLabel); - return `#${start + i + 1} 【${speaker}】\n${m.mes}`; - }).join('\n\n'); - - return { text, count: slice.length, range: `${start + 1}-${end + 1}楼`, endMesId: end }; -} - -function getSummaryPanelConfig() { - const defaults = { - api: { provider: 'st', url: '', key: '', model: '', modelCache: [] }, - gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null }, - trigger: { enabled: false, interval: 20, timing: 'after_ai', useStream: true, maxPerRun: 100 }, - }; - try { - const raw = localStorage.getItem('summary_panel_config'); - if (!raw) return defaults; - const parsed = JSON.parse(raw); - - const result = { - api: { ...defaults.api, ...(parsed.api || {}) }, - gen: { ...defaults.gen, ...(parsed.gen || {}) }, - trigger: { ...defaults.trigger, ...(parsed.trigger || {}) }, - }; - - if (result.trigger.timing === 'manual') result.trigger.enabled = false; - if (result.trigger.useStream === undefined) result.trigger.useStream = true; - - return result; - } catch { - return defaults; - } -} - -async function runSummaryGeneration(mesId, configFromFrame) { - if (isSummaryGenerating()) { - postToFrame({ type: "SUMMARY_STATUS", statusText: "上一轮总结仍在进行中..." }); - return false; - } - - setSummaryGenerating(true); - xbLog.info(MODULE_ID, `开始总结 mesId=${mesId}`); - - const cfg = configFromFrame || {}; - const store = getSummaryStore(); - const lastSummarized = store?.lastSummarizedMesId ?? -1; - const maxPerRun = cfg.trigger?.maxPerRun || 100; - const slice = buildIncrementalSlice(mesId, lastSummarized, maxPerRun); - - if (slice.count === 0) { - postToFrame({ type: "SUMMARY_STATUS", statusText: "没有新的对话需要总结" }); - setSummaryGenerating(false); - return true; - } - - postToFrame({ type: "SUMMARY_STATUS", statusText: `正在总结 ${slice.range}(${slice.count}楼新内容)...` }); - - const existingSummary = formatExistingSummaryForAI(store); - const nextEventId = getNextEventId(store); - const existingEventCount = store?.json?.events?.length || 0; - const useStream = cfg.trigger?.useStream !== false; - const apiCfg = cfg.api || {}; - const genCfg = cfg.gen || {}; - - let raw; - try { - raw = await generateSummary({ - existingSummary, - newHistoryText: slice.text, - historyRange: slice.range, - nextEventId, - existingEventCount, - llmApi: { - provider: apiCfg.provider, - url: apiCfg.url, - key: apiCfg.key, - model: apiCfg.model, - }, - genParams: genCfg, - useStream, - timeout: 120000, - sessionId: SUMMARY_SESSION_ID, - }); - } catch (err) { - xbLog.error(MODULE_ID, '生成失败', err); - postToFrame({ type: "SUMMARY_ERROR", message: err?.message || "生成失败" }); - setSummaryGenerating(false); - return false; - } - - if (!raw?.trim()) { - xbLog.error(MODULE_ID, 'AI返回为空'); - postToFrame({ type: "SUMMARY_ERROR", message: "AI返回为空" }); - setSummaryGenerating(false); - return false; - } - - const parsed = parseSummaryJson(raw); - if (!parsed) { - xbLog.error(MODULE_ID, 'JSON解析失败'); - postToFrame({ type: "SUMMARY_ERROR", message: "AI未返回有效JSON" }); - setSummaryGenerating(false); - return false; - } - - const oldJson = store?.json || {}; - const merged = mergeNewData(oldJson, parsed, slice.endMesId); - - store.lastSummarizedMesId = slice.endMesId; - store.json = merged; - store.updatedAt = Date.now(); - addSummarySnapshot(store, slice.endMesId); - saveSummaryStore(); - - postToFrame({ - type: "SUMMARY_FULL_DATA", - payload: { - keywords: merged.keywords || [], - events: merged.events || [], - characters: merged.characters || { main: [], relationships: [] }, - arcs: merged.arcs || [], - lastSummarizedMesId: slice.endMesId, - }, - }); - - postToFrame({ - type: "SUMMARY_STATUS", - statusText: `已更新至 ${slice.endMesId + 1} 楼 · ${merged.events?.length || 0} 个事件`, - }); - - const { chat } = getContext(); - const totalFloors = Array.isArray(chat) ? chat.length : 0; - const newHideRange = calcHideRange(slice.endMesId); - let actualHiddenCount = 0; - - if (store.hideSummarizedHistory && newHideRange) { - const oldHideRange = calcHideRange(lastSummarized); - const newHideStart = oldHideRange ? oldHideRange.end + 1 : 0; - if (newHideStart <= newHideRange.end) { - executeSlashCommand(`/hide ${newHideStart}-${newHideRange.end}`); - } - actualHiddenCount = newHideRange.end + 1; - } - - postToFrame({ - type: "SUMMARY_BASE_DATA", - stats: { - totalFloors, - summarizedUpTo: slice.endMesId + 1, - eventsCount: merged.events?.length || 0, - pendingFloors: totalFloors - slice.endMesId - 1, - hiddenCount: actualHiddenCount, - }, - }); - - updateSummaryExtensionPrompt(); - setSummaryGenerating(false); - - xbLog.info(MODULE_ID, `总结完成,已更新至 ${slice.endMesId + 1} 楼,共 ${merged.events?.length || 0} 个事件`); - return true; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 自动触发总结 -// ═══════════════════════════════════════════════════════════════════════════ - -async function maybeAutoRunSummary(reason) { - const { chatId, chat } = getContext(); - if (!chatId || !Array.isArray(chat)) return; - if (!getSettings().storySummary?.enabled) return; - - const cfgAll = getSummaryPanelConfig(); - const trig = cfgAll.trigger || {}; - - if (trig.timing === 'manual') return; - if (!trig.enabled) return; - if (trig.timing === 'after_ai' && reason !== 'after_ai') return; - if (trig.timing === 'before_user' && reason !== 'before_user') return; - - if (isSummaryGenerating()) return; - - const store = getSummaryStore(); - const lastSummarized = store?.lastSummarizedMesId ?? -1; - const pending = chat.length - lastSummarized - 1; - if (pending < (trig.interval || 1)) return; - - xbLog.info(MODULE_ID, `自动触发剧情总结: reason=${reason}, pending=${pending}`); - await autoRunSummaryWithRetry(chat.length - 1, { api: cfgAll.api, gen: cfgAll.gen, trigger: trig }); -} - -async function autoRunSummaryWithRetry(targetMesId, configForRun) { - for (let attempt = 1; attempt <= 3; attempt++) { - if (await runSummaryGeneration(targetMesId, configForRun)) return; - if (attempt < 3) await sleep(1000); - } - xbLog.error(MODULE_ID, '自动总结失败(已重试3次)'); - await executeSlashCommand('/echo severity=error 剧情总结失败(已自动重试 3 次)。请稍后再试。'); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// extension_prompts 注入 -// ═══════════════════════════════════════════════════════════════════════════ - -function formatSummaryForPrompt(store) { - const data = store.json || {}; - const parts = []; - parts.push("【此处是对以上可见历史,及因上下文限制被省略历史的所有总结。请严格依据此总结理解剧情背景。】"); - - if (data.keywords?.length) { - parts.push(`关键词:${data.keywords.map(k => k.text).join(" / ")}`); - } - if (data.events?.length) { - const lines = data.events.map(ev => `- [${ev.timeLabel}] ${ev.title}:${ev.summary}`).join("\n"); - parts.push(`事件:\n${lines}`); - } - if (data.arcs?.length) { - const lines = data.arcs.map(a => { - const moments = (a.moments || []).map(m => typeof m === 'string' ? m : m.text); - if (!moments.length) return `- ${a.name}:${a.trajectory}`; - return `- ${a.name}:${moments.join(" → ")}(当前:${a.trajectory})`; - }).join("\n"); - parts.push(`角色弧光:\n${lines}`); - } - - return `<剧情总结>\n${parts.join("\n\n")}\n\n以下是总结后新发生的情节:`; -} - -function updateSummaryExtensionPrompt() { - if (!getSettings().storySummary?.enabled) { - delete extension_prompts[SUMMARY_PROMPT_KEY]; - return; - } - - const { chat } = getContext(); - const store = getSummaryStore(); - - if (!store?.json) { - delete extension_prompts[SUMMARY_PROMPT_KEY]; - return; - } - - const cfg = getSummaryPanelConfig(); - let text = formatSummaryForPrompt(store); - - if (cfg.trigger?.wrapperHead) { - text = cfg.trigger.wrapperHead + '\n' + text; - } - if (cfg.trigger?.wrapperTail) { - text = text + '\n' + cfg.trigger.wrapperTail; - } - if (!text.trim()) { - delete extension_prompts[SUMMARY_PROMPT_KEY]; - return; - } - - const lastIdx = store.lastSummarizedMesId ?? 0; - const length = Array.isArray(chat) ? chat.length : 0; - if (lastIdx >= length) { - delete extension_prompts[SUMMARY_PROMPT_KEY]; - return; - } - - let depth = length - lastIdx - 1; - if (depth < 0) depth = 0; - - if (cfg.trigger?.forceInsertAtEnd) { - depth = 10000; - } - extension_prompts[SUMMARY_PROMPT_KEY] = { - value: text, - position: extension_prompt_types.IN_CHAT, - depth, - role: extension_prompt_roles.ASSISTANT, - }; -} - -function clearSummaryExtensionPrompt() { - delete extension_prompts[SUMMARY_PROMPT_KEY]; -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 事件处理器 -// ═══════════════════════════════════════════════════════════════════════════ - -function handleChatChanged() { - const { chat } = getContext(); - const newLength = Array.isArray(chat) ? chat.length : 0; - - rollbackSummaryIfNeeded(); - initButtonsForAll(); - updateSummaryExtensionPrompt(); - - const store = getSummaryStore(); - const lastSummarized = store?.lastSummarizedMesId ?? -1; - - if (lastSummarized >= 0 && store?.hideSummarizedHistory === true) { - const range = calcHideRange(lastSummarized); - if (range) executeSlashCommand(`/hide ${range.start}-${range.end}`); - } - - if (frameReady) { - sendFrameBaseData(store, newLength); - sendFrameFullData(store, newLength); - } -} - -function handleMessageDeleted() { - rollbackSummaryIfNeeded(); - updateSummaryExtensionPrompt(); -} - -function handleMessageReceived() { - updateSummaryExtensionPrompt(); - initButtonsForAll(); - setTimeout(() => maybeAutoRunSummary('after_ai'), 1000); -} - -function handleMessageSent() { - updateSummaryExtensionPrompt(); - initButtonsForAll(); - setTimeout(() => maybeAutoRunSummary('before_user'), 1000); -} - -function handleMessageUpdated() { - rollbackSummaryIfNeeded(); - updateSummaryExtensionPrompt(); - initButtonsForAll(); -} - -function handleMessageRendered(data) { - const mesId = data?.element ? $(data.element).attr("mesid") : data?.messageId; - if (mesId != null) { - addSummaryBtnToMessage(mesId); - } else { - initButtonsForAll(); - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 事件注册 -// ═══════════════════════════════════════════════════════════════════════════ - -function registerEvents() { - if (eventsRegistered) return; - eventsRegistered = true; - - xbLog.info(MODULE_ID, '模块初始化'); - - CacheRegistry.register(MODULE_ID, { - name: '待发送消息队列', - getSize: () => pendingFrameMessages.length, - getBytes: () => { - try { return JSON.stringify(pendingFrameMessages || []).length * 2; } - catch { return 0; } - }, - clear: () => { - pendingFrameMessages = []; - frameReady = false; - }, - }); - - initButtonsForAll(); - - events.on(event_types.CHAT_CHANGED, () => setTimeout(handleChatChanged, 80)); - events.on(event_types.MESSAGE_DELETED, () => setTimeout(handleMessageDeleted, 50)); - events.on(event_types.MESSAGE_RECEIVED, () => setTimeout(handleMessageReceived, 150)); - events.on(event_types.MESSAGE_SENT, () => setTimeout(handleMessageSent, 150)); - events.on(event_types.MESSAGE_SWIPED, () => setTimeout(handleMessageUpdated, 100)); - events.on(event_types.MESSAGE_UPDATED, () => setTimeout(handleMessageUpdated, 100)); - events.on(event_types.MESSAGE_EDITED, () => setTimeout(handleMessageUpdated, 100)); - events.on(event_types.USER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50)); - events.on(event_types.CHARACTER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50)); -} - -function unregisterEvents() { - xbLog.info(MODULE_ID, '模块清理'); - events.cleanup(); - CacheRegistry.unregister(MODULE_ID); - eventsRegistered = false; - $(".xiaobaix-story-summary-btn").remove(); - hideOverlay(); - clearSummaryExtensionPrompt(); -} - -// ═══════════════════════════════════════════════════════════════════════════ -// Toggle 监听 -// ═══════════════════════════════════════════════════════════════════════════ - -$(document).on("xiaobaix:storySummary:toggle", (_e, enabled) => { - if (enabled) { - registerEvents(); - initButtonsForAll(); - updateSummaryExtensionPrompt(); - } else { - unregisterEvents(); - } -}); - -// ═══════════════════════════════════════════════════════════════════════════ -// 初始化 -// ═══════════════════════════════════════════════════════════════════════════ - -jQuery(() => { - if (!getSettings().storySummary?.enabled) { - clearSummaryExtensionPrompt(); - return; - } - registerEvents(); - updateSummaryExtensionPrompt(); -});