// ═══════════════════════════════════════════════════════════════════════════ // Story Summary - 主入口 // // 稳定目标: // 1) "聊天时隐藏已总结" 永远只隐藏"已总结"部分,绝不影响未总结部分 // 2) 关闭隐藏 = 暴力全量 unhide,确保立刻恢复 // 3) 开启隐藏 / 改Y / 切Chat / 收新消息:先全量 unhide,再按边界重新 hide // 4) Prompt 注入:extension_prompts + IN_CHAT + depth(动态计算,最小为2) // ═══════════════════════════════════════════════════════════════════════════ import { getContext } from "../../../../../extensions.js"; import { eventSource, event_types, extension_prompts, extension_prompt_types, extension_prompt_roles, } from "../../../../../../script.js"; import { extensionFolderPath } from "../../core/constants.js"; import { xbLog, CacheRegistry } from "../../core/debug-core.js"; import { postToIframe, isTrustedMessage } from "../../core/iframe-messaging.js"; import { CommonSettingStorage } from "../../core/server-storage.js"; // config/store import { getSettings, getSummaryPanelConfig, getVectorConfig, saveVectorConfig } from "./data/config.js"; import { getSummaryStore, saveSummaryStore, calcHideRange, rollbackSummaryIfNeeded, clearSummaryData, extractRelationshipsFromFacts, } from "./data/store.js"; // prompt text builder import { buildVectorPromptText, buildNonVectorPromptText, } from "./generate/prompt.js"; // summary generation import { runSummaryGeneration } from "./generate/generator.js"; // vector service import { embed, getEngineFingerprint, testOnlineService } from "./vector/utils/embedder.js"; // tokenizer import { preload as preloadTokenizer, injectEntities, isReady as isTokenizerReady } from "./vector/utils/tokenizer.js"; // entity lexicon import { buildEntityLexicon, buildDisplayNameMap } from "./vector/retrieval/entity-lexicon.js"; import { getMeta, updateMeta, saveEventVectors as saveEventVectorsToDb, clearEventVectors, deleteEventVectorsByIds, clearAllChunks, saveChunks, saveChunkVectors, getStorageStats, } from "./vector/storage/chunk-store.js"; import { buildIncrementalChunks, getChunkBuildStatus, chunkMessage, syncOnMessageDeleted, syncOnMessageSwiped, syncOnMessageReceived, } from "./vector/pipeline/chunk-builder.js"; import { incrementalExtractAtoms, clearAllAtomsAndVectors, cancelL0Extraction, getAnchorStats, initStateIntegration, } from "./vector/pipeline/state-integration.js"; import { clearStateVectors, getStateAtoms, getStateAtomsCount, getStateVectorsCount, saveStateVectors, deleteStateAtomsFromFloor, deleteStateVectorsFromFloor, deleteL0IndexFromFloor, } from "./vector/storage/state-store.js"; // vector io import { exportVectors, importVectors } from "./vector/storage/vector-io.js"; import { invalidateLexicalIndex, warmupIndex } from "./vector/retrieval/lexical-index.js"; // ═══════════════════════════════════════════════════════════════════════════ // 常量 // ═══════════════════════════════════════════════════════════════════════════ const MODULE_ID = "storySummary"; const SUMMARY_CONFIG_KEY = "storySummaryPanelConfig"; const iframePath = `${extensionFolderPath}/modules/story-summary/story-summary.html`; const VALID_SECTIONS = ["keywords", "events", "characters", "arcs", "facts"]; const MESSAGE_EVENT = "message"; // ═══════════════════════════════════════════════════════════════════════════ // 状态变量 // ═══════════════════════════════════════════════════════════════════════════ let overlayCreated = false; let frameReady = false; let currentMesId = null; let pendingFrameMessages = []; let eventsRegistered = false; let vectorCancelled = false; let vectorAbortController = null; // ═══════════════════════════════════════════════════════════════════════════ // TaskGuard — 互斥任务管理(summary / vector / anchor) // ═══════════════════════════════════════════════════════════════════════════ class TaskGuard { #running = new Set(); acquire(taskName) { if (this.#running.has(taskName)) return null; this.#running.add(taskName); let released = false; return () => { if (!released) { released = true; this.#running.delete(taskName); } }; } isRunning(taskName) { return this.#running.has(taskName); } isAnyRunning(...taskNames) { return taskNames.some(t => this.#running.has(t)); } } const guard = new TaskGuard(); // 用户消息缓存(解决 GENERATION_STARTED 时 chat 尚未包含用户消息的问题) let lastSentUserMessage = null; let lastSentTimestamp = 0; function captureUserInput() { const text = $("#send_textarea").val(); if (text?.trim()) { lastSentUserMessage = text.trim(); lastSentTimestamp = Date.now(); } } function onSendPointerdown(e) { if (e.target?.closest?.("#send_but")) { captureUserInput(); } } function onSendKeydown(e) { if (e.key === "Enter" && !e.shiftKey && e.target?.closest?.("#send_textarea")) { captureUserInput(); } } let hideApplyTimer = null; const HIDE_APPLY_DEBOUNCE_MS = 250; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // 向量提醒节流 let lastVectorWarningAt = 0; const VECTOR_WARNING_COOLDOWN_MS = 120000; // 2分钟内不重复提醒 const EXT_PROMPT_KEY = "LittleWhiteBox_StorySummary"; const MIN_INJECTION_DEPTH = 2; // ═══════════════════════════════════════════════════════════════════════════ // 分词器预热(依赖 tokenizer.js 内部状态机,支持失败重试) // ═══════════════════════════════════════════════════════════════════════════ function maybePreloadTokenizer() { if (isTokenizerReady()) return; const vectorCfg = getVectorConfig(); if (!vectorCfg?.enabled) return; preloadTokenizer() .then((ok) => { if (ok) { xbLog.info(MODULE_ID, "分词器预热成功"); } }) .catch((e) => { xbLog.warn(MODULE_ID, "分词器预热失败(将降级运行,可稀后重试)", e); }); } // role 映射 const ROLE_MAP = { system: extension_prompt_roles.SYSTEM, user: extension_prompt_roles.USER, assistant: extension_prompt_roles.ASSISTANT, }; // ═══════════════════════════════════════════════════════════════════════════ // 工具:执行斜杠命令 // ═══════════════════════════════════════════════════════════════════════════ 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); } } function getLastMessageId() { const { chat } = getContext(); const len = Array.isArray(chat) ? chat.length : 0; return Math.max(-1, len - 1); } async function unhideAllMessages() { const last = getLastMessageId(); if (last < 0) return; await executeSlashCommand(`/unhide 0-${last}`); } // ═══════════════════════════════════════════════════════════════════════════ // 生成状态管理 // ═══════════════════════════════════════════════════════════════════════════ function isSummaryGenerating() { return guard.isRunning('summary'); } function notifySummaryState() { postToFrame({ type: "GENERATION_STATE", isGenerating: guard.isRunning('summary') }); } // ═══════════════════════════════════════════════════════════════════════════ // 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 = []; sendAnchorStatsToFrame(); } // ═══════════════════════════════════════════════════════════════════════════ // 向量功能:UI 交互/状态 // ═══════════════════════════════════════════════════════════════════════════ function sendVectorConfigToFrame() { const cfg = getVectorConfig(); postToFrame({ type: "VECTOR_CONFIG", config: cfg }); } async function sendVectorStatsToFrame() { const { chatId, chat } = getContext(); if (!chatId) return; const store = getSummaryStore(); const eventCount = store?.json?.events?.length || 0; const stats = await getStorageStats(chatId); const chunkStatus = await getChunkBuildStatus(); const totalMessages = chat?.length || 0; const stateVectorsCount = await getStateVectorsCount(chatId); const cfg = getVectorConfig(); let mismatch = false; if (cfg?.enabled && (stats.eventVectors > 0 || stats.chunks > 0)) { const fingerprint = getEngineFingerprint(cfg); const meta = await getMeta(chatId); mismatch = meta.fingerprint && meta.fingerprint !== fingerprint; } postToFrame({ type: "VECTOR_STATS", stats: { eventCount, eventVectors: stats.eventVectors, chunkCount: stats.chunkVectors, builtFloors: chunkStatus.builtFloors, totalFloors: chunkStatus.totalFloors, totalMessages, stateVectors: stateVectorsCount, }, mismatch, }); } async function sendAnchorStatsToFrame() { const stats = await getAnchorStats(); const atomsCount = getStateAtomsCount(); postToFrame({ type: "ANCHOR_STATS", stats: { ...stats, atomsCount } }); } async function handleAnchorGenerate() { const release = guard.acquire('anchor'); if (!release) return; try { const vectorCfg = getVectorConfig(); if (!vectorCfg?.enabled) { await executeSlashCommand("/echo severity=warning 请先启用向量检索"); return; } if (!vectorCfg.online?.key) { postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: "请配置 API Key" }); return; } const { chatId, chat } = getContext(); if (!chatId || !chat?.length) return; postToFrame({ type: "ANCHOR_GEN_PROGRESS", current: 0, total: 1, message: "分析中..." }); await incrementalExtractAtoms(chatId, chat, (message, current, total) => { postToFrame({ type: "ANCHOR_GEN_PROGRESS", current, total, message }); }); postToFrame({ type: "ANCHOR_GEN_PROGRESS", current: 0, total: 1, message: "向量化 L1..." }); await buildIncrementalChunks({ vectorConfig: vectorCfg }); invalidateLexicalIndex(); await sendAnchorStatsToFrame(); await sendVectorStatsToFrame(); xbLog.info(MODULE_ID, "记忆锚点生成完成"); } catch (e) { xbLog.error(MODULE_ID, "记忆锚点生成失败", e); await executeSlashCommand(`/echo severity=error 记忆锚点生成失败:${e.message}`); } finally { release(); postToFrame({ type: "ANCHOR_GEN_PROGRESS", current: -1, total: 0 }); } } async function handleAnchorClear() { const { chatId } = getContext(); if (!chatId) return; await clearAllAtomsAndVectors(chatId); await sendAnchorStatsToFrame(); await sendVectorStatsToFrame(); await executeSlashCommand("/echo severity=info 记忆锚点已清空"); xbLog.info(MODULE_ID, "记忆锚点已清空"); } function handleAnchorCancel() { cancelL0Extraction(); postToFrame({ type: "ANCHOR_GEN_PROGRESS", current: -1, total: 0 }); } async function handleTestOnlineService(provider, config) { try { postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "downloading", message: "连接中..." }); const result = await testOnlineService(provider, config); postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "success", message: `连接成功 (${result.dims}维)`, }); } catch (e) { postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: e.message }); } } async function handleGenerateVectors(vectorCfg) { const release = guard.acquire('vector'); if (!release) return; try { if (!vectorCfg?.enabled) { postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "ALL", current: -1, total: 0 }); return; } const { chatId, chat } = getContext(); if (!chatId || !chat?.length) return; if (!vectorCfg.online?.key) { postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: "请配置 API Key" }); return; } vectorCancelled = false; vectorAbortController = new AbortController(); const fingerprint = getEngineFingerprint(vectorCfg); const batchSize = 20; await clearAllChunks(chatId); await clearEventVectors(chatId); await clearStateVectors(chatId); await updateMeta(chatId, { lastChunkFloor: -1, fingerprint }); const atoms = getStateAtoms(); if (!atoms.length) { postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L0", current: 0, total: 0, message: "L0 为空,跳过" }); } else { postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L0", current: 0, total: atoms.length, message: "L0 向量化..." }); let l0Completed = 0; for (let i = 0; i < atoms.length; i += batchSize) { if (vectorCancelled) break; const batch = atoms.slice(i, i + batchSize); const texts = batch.map(a => a.semantic); try { const vectors = await embed(texts, vectorCfg, { signal: vectorAbortController.signal }); const items = batch.map((a, j) => ({ atomId: a.atomId, floor: a.floor, vector: vectors[j], })); await saveStateVectors(chatId, items, fingerprint); l0Completed += batch.length; postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L0", current: l0Completed, total: atoms.length }); } catch (e) { if (e?.name === "AbortError") break; xbLog.error(MODULE_ID, "L0 向量化失败", e); vectorCancelled = true; break; } } } if (vectorCancelled) return; const allChunks = []; for (let floor = 0; floor < chat.length; floor++) { if (vectorCancelled) break; const message = chat[floor]; if (!message) continue; const chunks = chunkMessage(floor, message); if (!chunks.length) continue; allChunks.push(...chunks); } let l1Vectors = []; if (!allChunks.length) { postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: 0, total: 0, message: "L1 为空,跳过" }); } else { postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: 0, total: allChunks.length, message: "L1 向量化..." }); await saveChunks(chatId, allChunks); let l1Completed = 0; for (let i = 0; i < allChunks.length; i += batchSize) { if (vectorCancelled) break; const batch = allChunks.slice(i, i + batchSize); const texts = batch.map(c => c.text); try { const vectors = await embed(texts, vectorCfg, { signal: vectorAbortController.signal }); const items = batch.map((c, j) => ({ chunkId: c.chunkId, vector: vectors[j], })); await saveChunkVectors(chatId, items, fingerprint); l1Vectors = l1Vectors.concat(items); l1Completed += batch.length; postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: l1Completed, total: allChunks.length }); } catch (e) { if (e?.name === "AbortError") break; xbLog.error(MODULE_ID, "L1 向量化失败", e); vectorCancelled = true; break; } } } if (vectorCancelled) return; const store = getSummaryStore(); const events = store?.json?.events || []; const l2Pairs = events .map((e) => ({ id: e.id, text: `${e.title || ""} ${e.summary || ""}`.trim() })) .filter((p) => p.text); if (!l2Pairs.length) { postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: 0, total: 0, message: "L2 为空,跳过" }); } else { postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: 0, total: l2Pairs.length, message: "L2 向量化..." }); let l2Completed = 0; for (let i = 0; i < l2Pairs.length; i += batchSize) { if (vectorCancelled) break; const batch = l2Pairs.slice(i, i + batchSize); const texts = batch.map(p => p.text); try { const vectors = await embed(texts, vectorCfg, { signal: vectorAbortController.signal }); const items = batch.map((p, idx) => ({ eventId: p.id, vector: vectors[idx], })); await saveEventVectorsToDb(chatId, items, fingerprint); l2Completed += batch.length; postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: l2Completed, total: l2Pairs.length }); } catch (e) { if (e?.name === "AbortError") break; xbLog.error(MODULE_ID, "L2 向量化失败", e); vectorCancelled = true; break; } } } postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "ALL", current: -1, total: 0 }); await sendVectorStatsToFrame(); xbLog.info(MODULE_ID, `向量生成完成: L0=${atoms.length}, L1=${l1Vectors.length}, L2=${l2Pairs.length}`); } catch (e) { xbLog.error(MODULE_ID, '向量生成失败', e); postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "ALL", current: -1, total: 0 }); await sendVectorStatsToFrame(); } finally { release(); vectorCancelled = false; vectorAbortController = null; } } async function handleClearVectors() { const { chatId } = getContext(); if (!chatId) return; await clearEventVectors(chatId); await clearAllChunks(chatId); await clearStateVectors(chatId); await updateMeta(chatId, { lastChunkFloor: -1 }); await sendVectorStatsToFrame(); await executeSlashCommand('/echo severity=info 向量数据已清除。如需恢复召回功能,请重新点击"生成向量"。'); xbLog.info(MODULE_ID, "向量数据已清除"); } // ═══════════════════════════════════════════════════════════════════════════ // 实体词典注入 + 索引预热 // ═══════════════════════════════════════════════════════════════════════════ function refreshEntityLexiconAndWarmup() { const vectorCfg = getVectorConfig(); if (!vectorCfg?.enabled) return; const store = getSummaryStore(); const { name1, name2 } = getContext(); const lexicon = buildEntityLexicon(store, { name1, name2 }); const displayMap = buildDisplayNameMap(store, { name1, name2 }); injectEntities(lexicon, displayMap); // 异步预建词法索引(不阻塞) invalidateLexicalIndex(); warmupIndex(); } // ═══════════════════════════════════════════════════════════════════════════ // L0 自动补提取(每收到新消息后检查并补提取缺失楼层) // ═══════════════════════════════════════════════════════════════════════════ async function maybeAutoExtractL0() { const vectorCfg = getVectorConfig(); if (!vectorCfg?.enabled) return; if (guard.isAnyRunning('anchor', 'vector')) return; const { chatId, chat } = getContext(); if (!chatId || !chat?.length) return; const stats = await getAnchorStats(); if (stats.pending <= 0) return; const release = guard.acquire('anchor'); if (!release) return; try { await incrementalExtractAtoms(chatId, chat, null, { maxFloors: 20 }); // 为新提取的 L0 楼层构建 L1 chunks await buildIncrementalChunks({ vectorConfig: vectorCfg }); invalidateLexicalIndex(); await sendAnchorStatsToFrame(); await sendVectorStatsToFrame(); xbLog.info(MODULE_ID, "自动 L0 补提取完成"); } catch (e) { xbLog.error(MODULE_ID, "自动 L0 补提取失败", e); } finally { release(); } } // ═══════════════════════════════════════════════════════════════════════════ // Embedding 连接预热 // ═══════════════════════════════════════════════════════════════════════════ function warmupEmbeddingConnection() { const vectorCfg = getVectorConfig(); if (!vectorCfg?.enabled) return; embed(['.'], vectorCfg, { timeout: 5000 }).catch(() => {}); } async function autoVectorizeNewEvents(newEventIds) { if (!newEventIds?.length) return; const vectorCfg = getVectorConfig(); if (!vectorCfg?.enabled) return; const { chatId } = getContext(); if (!chatId) return; const store = getSummaryStore(); const events = store?.json?.events || []; const newEventIdSet = new Set(newEventIds); const newEvents = events.filter((e) => newEventIdSet.has(e.id)); if (!newEvents.length) return; const pairs = newEvents .map((e) => ({ id: e.id, text: `${e.title || ""} ${e.summary || ""}`.trim() })) .filter((p) => p.text); if (!pairs.length) return; try { const fingerprint = getEngineFingerprint(vectorCfg); const batchSize = 20; for (let i = 0; i < pairs.length; i += batchSize) { const batch = pairs.slice(i, i + batchSize); const texts = batch.map((p) => p.text); const vectors = await embed(texts, vectorCfg); const items = batch.map((p, idx) => ({ eventId: p.id, vector: vectors[idx], })); await saveEventVectorsToDb(chatId, items, fingerprint); } xbLog.info(MODULE_ID, `L2 自动增量完成: ${pairs.length} 个事件`); await sendVectorStatsToFrame(); } catch (e) { xbLog.error(MODULE_ID, "L2 自动向量化失败", e); } } // ═══════════════════════════════════════════════════════════════════════════ // L2 跟随编辑同步(用户编辑 events 时调用) // ═══════════════════════════════════════════════════════════════════════════ async function syncEventVectorsOnEdit(oldEvents, newEvents) { const vectorCfg = getVectorConfig(); if (!vectorCfg?.enabled) return; const { chatId } = getContext(); if (!chatId) return; const oldIds = new Set((oldEvents || []).map((e) => e.id).filter(Boolean)); const newIds = new Set((newEvents || []).map((e) => e.id).filter(Boolean)); const deletedIds = [...oldIds].filter((id) => !newIds.has(id)); if (deletedIds.length > 0) { await deleteEventVectorsByIds(chatId, deletedIds); xbLog.info(MODULE_ID, `L2 同步删除: ${deletedIds.length} 个事件向量`); await sendVectorStatsToFrame(); } } // ═══════════════════════════════════════════════════════════════════════════ // 向量完整性检测(仅提醒,不自动操作) // ═══════════════════════════════════════════════════════════════════════════ async function checkVectorIntegrityAndWarn() { const vectorCfg = getVectorConfig(); if (!vectorCfg?.enabled) return; const now = Date.now(); if (now - lastVectorWarningAt < VECTOR_WARNING_COOLDOWN_MS) return; const { chat, chatId } = getContext(); if (!chatId || !chat?.length) return; const store = getSummaryStore(); const totalFloors = chat.length; const totalEvents = store?.json?.events?.length || 0; if (totalEvents === 0) return; const meta = await getMeta(chatId); const stats = await getStorageStats(chatId); const fingerprint = getEngineFingerprint(vectorCfg); const issues = []; if (meta.fingerprint && meta.fingerprint !== fingerprint) { issues.push('向量引擎/模型已变更'); } const chunkFloorGap = totalFloors - 1 - (meta.lastChunkFloor ?? -1); if (chunkFloorGap > 0) { issues.push(`${chunkFloorGap} 层片段未向量化`); } const eventVectorGap = totalEvents - stats.eventVectors; if (eventVectorGap > 0) { issues.push(`${eventVectorGap} 个事件未向量化`); } if (issues.length > 0) { lastVectorWarningAt = now; await executeSlashCommand(`/echo severity=warning 向量数据不完整:${issues.join('、')}。请打开剧情总结面板点击"生成向量"。`); } } async function maybeAutoBuildChunks() { const cfg = getVectorConfig(); if (!cfg?.enabled) return; const { chat, chatId } = getContext(); if (!chatId || !chat?.length) return; const status = await getChunkBuildStatus(); if (status.pending <= 0) return; try { await buildIncrementalChunks({ vectorConfig: cfg }); } catch (e) { xbLog.error(MODULE_ID, "自动 L1 构建失败", e); } } // ═══════════════════════════════════════════════════════════════════════════ // 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]); window.addEventListener(MESSAGE_EVENT, 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 }); } } catch (e) { xbLog.warn(MODULE_ID, "加载面板配置失败", e); } } async function sendFrameBaseData(store, totalFloors) { const boundary = await getHideBoundaryFloor(store); const range = calcHideRange(boundary); const hiddenCount = (store?.hideSummarizedHistory && range) ? (range.end + 1) : 0; const lastSummarized = store?.lastSummarizedMesId ?? -1; 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) { if (store?.json) { postToFrame({ type: "SUMMARY_FULL_DATA", payload: buildFramePayload(store), }); } else { postToFrame({ type: "SUMMARY_CLEARED", payload: { totalFloors } }); } } function buildFramePayload(store) { const json = store?.json || {}; const facts = json.facts || []; return { keywords: json.keywords || [], events: json.events || [], characters: { main: json.characters?.main || [], relationships: extractRelationshipsFromFacts(facts), }, arcs: json.arcs || [], facts, lastSummarizedMesId: store?.lastSummarizedMesId ?? -1, }; } function openPanelForMessage(mesId) { createOverlay(); showOverlay(); const { chat } = getContext(); const store = getSummaryStore(); const totalFloors = chat.length; sendFrameBaseData(store, totalFloors); sendFrameFullData(store, totalFloors); notifySummaryState(); sendVectorConfigToFrame(); sendVectorStatsToFrame(); } // ═══════════════════════════════════════════════════════════════════════════ // Hide/Unhide // - 非向量:boundary = lastSummarizedMesId // - 向量:boundary = meta.lastChunkFloor(若为 -1 则回退到 lastSummarizedMesId) // ═══════════════════════════════════════════════════════════════════════════ async function getHideBoundaryFloor(store) { // 没有总结时,不隐藏 if (store?.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) { return -1; } const vectorCfg = getVectorConfig(); if (!vectorCfg?.enabled) { return store?.lastSummarizedMesId ?? -1; } const { chatId } = getContext(); if (!chatId) return store?.lastSummarizedMesId ?? -1; const meta = await getMeta(chatId); const v = meta?.lastChunkFloor ?? -1; if (v >= 0) return v; return store?.lastSummarizedMesId ?? -1; } async function applyHideState() { const store = getSummaryStore(); if (!store?.hideSummarizedHistory) return; // 先全量 unhide,杜绝历史残留 await unhideAllMessages(); const boundary = await getHideBoundaryFloor(store); if (boundary < 0) return; const range = calcHideRange(boundary); if (!range) return; await executeSlashCommand(`/hide ${range.start}-${range.end}`); } function applyHideStateDebounced() { clearTimeout(hideApplyTimer); hideApplyTimer = setTimeout(() => { applyHideState().catch((e) => xbLog.warn(MODULE_ID, "applyHideState failed", e)); }, HIDE_APPLY_DEBOUNCE_MS); } async function clearHideState() { // 暴力全量 unhide,确保立刻恢复 await unhideAllMessages(); } // ═══════════════════════════════════════════════════════════════════════════ // 自动总结 // ═══════════════════════════════════════════════════════════════════════════ 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; await autoRunSummaryWithRetry(chat.length - 1, { api: cfgAll.api, gen: cfgAll.gen, trigger: trig }); } async function autoRunSummaryWithRetry(targetMesId, configForRun) { const release = guard.acquire('summary'); if (!release) return; notifySummaryState(); try { for (let attempt = 1; attempt <= 3; attempt++) { const result = await runSummaryGeneration(targetMesId, configForRun, { onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }), onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }), onComplete: async ({ merged, endMesId, newEventIds }) => { const store = getSummaryStore(); postToFrame({ type: "SUMMARY_FULL_DATA", payload: buildFramePayload(store) }); invalidateLexicalIndex(); applyHideStateDebounced(); updateFrameStatsAfterSummary(endMesId, store.json || {}); await autoVectorizeNewEvents(newEventIds); }, }); if (result.success) { return; } if (attempt < 3) await sleep(1000); } await executeSlashCommand("/echo severity=error 剧情总结失败(已自动重试 3 次)。请稍后再试。"); } finally { release(); notifySummaryState(); } } function updateFrameStatsAfterSummary(endMesId, merged) { const { chat } = getContext(); const totalFloors = Array.isArray(chat) ? chat.length : 0; const store = getSummaryStore(); const range = calcHideRange(endMesId); const hiddenCount = store?.hideSummarizedHistory && range ? range.end + 1 : 0; postToFrame({ type: "SUMMARY_BASE_DATA", stats: { totalFloors, summarizedUpTo: endMesId + 1, eventsCount: merged.events?.length || 0, pendingFloors: totalFloors - endMesId - 1, hiddenCount, }, }); } // ═══════════════════════════════════════════════════════════════════════════ // iframe 消息处理 // ═══════════════════════════════════════════════════════════════════════════ 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(); notifySummaryState(); sendSavedConfigToFrame(); sendVectorConfigToFrame(); sendVectorStatsToFrame(); sendAnchorStatsToFrame(); 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; handleManualGenerate(currentMesId, data.config || {}); break; } case "REQUEST_CANCEL": window.xiaobaixStreamingGeneration?.cancel?.("xb9"); postToFrame({ type: "GENERATION_STATE", isGenerating: false }); postToFrame({ type: "SUMMARY_STATUS", statusText: "已停止" }); break; case "VECTOR_TEST_ONLINE": handleTestOnlineService(data.provider, data.config); break; case "VECTOR_GENERATE": if (data.config) saveVectorConfig(data.config); maybePreloadTokenizer(); refreshEntityLexiconAndWarmup(); handleGenerateVectors(data.config); break; case "VECTOR_CLEAR": handleClearVectors(); break; case "VECTOR_CANCEL_GENERATE": vectorCancelled = true; cancelL0Extraction(); try { vectorAbortController?.abort?.(); } catch {} postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "ALL", current: -1, total: 0 }); break; case "ANCHOR_GENERATE": handleAnchorGenerate(); break; case "ANCHOR_CLEAR": handleAnchorClear(); break; case "ANCHOR_CANCEL": handleAnchorCancel(); break; case "REQUEST_ANCHOR_STATS": sendAnchorStatsToFrame(); break; case "VECTOR_EXPORT": (async () => { try { const result = await exportVectors((status) => { postToFrame({ type: "VECTOR_IO_STATUS", status }); }); postToFrame({ type: "VECTOR_EXPORT_RESULT", success: true, filename: result.filename, size: result.size, chunkCount: result.chunkCount, eventCount: result.eventCount, }); } catch (e) { postToFrame({ type: "VECTOR_EXPORT_RESULT", success: false, error: e.message }); } })(); break; case "VECTOR_IMPORT_PICK": // 在 parent 创建 file picker,避免 iframe 传大文件 (async () => { const input = document.createElement("input"); input.type = "file"; input.accept = ".zip"; input.onchange = async () => { const file = input.files?.[0]; if (!file) { postToFrame({ type: "VECTOR_IMPORT_RESULT", success: false, error: "未选择文件" }); return; } try { const result = await importVectors(file, (status) => { postToFrame({ type: "VECTOR_IO_STATUS", status }); }); postToFrame({ type: "VECTOR_IMPORT_RESULT", success: true, chunkCount: result.chunkCount, eventCount: result.eventCount, warnings: result.warnings, fingerprintMismatch: result.fingerprintMismatch, }); await sendVectorStatsToFrame(); } catch (e) { postToFrame({ type: "VECTOR_IMPORT_RESULT", success: false, error: e.message }); } }; input.click(); })(); break; case "REQUEST_VECTOR_STATS": sendVectorStatsToFrame(); maybePreloadTokenizer(); break; case "REQUEST_CLEAR": { const { chat, chatId } = getContext(); clearSummaryData(chatId); postToFrame({ type: "SUMMARY_CLEARED", payload: { totalFloors: Array.isArray(chat) ? chat.length : 0 }, }); break; } case "CLOSE_PANEL": hideOverlay(); break; case "UPDATE_SECTION": { const store = getSummaryStore(); if (!store) break; store.json ||= {}; // 如果是 events,先记录旧数据用于同步向量 const oldEvents = data.section === "events" ? [...(store.json.events || [])] : null; if (VALID_SECTIONS.includes(data.section)) { store.json[data.section] = data.data; } store.updatedAt = Date.now(); saveSummaryStore(); // 同步 L2 向量(删除被移除的事件) if (data.section === "events" && oldEvents) { syncEventVectorsOnEdit(oldEvents, data.data); } break; } case "TOGGLE_HIDE_SUMMARIZED": { const store = getSummaryStore(); if (!store) break; store.hideSummarizedHistory = !!data.enabled; saveSummaryStore(); (async () => { if (data.enabled) { await applyHideState(); } else { await clearHideState(); } })(); 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(); (async () => { if (store.hideSummarizedHistory) { await applyHideState(); } const { chat } = getContext(); await sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0); })(); break; } case "SAVE_PANEL_CONFIG": if (data.config) { CommonSettingStorage.set(SUMMARY_CONFIG_KEY, data.config); } break; case "REQUEST_PANEL_CONFIG": sendSavedConfigToFrame(); break; } } // ═══════════════════════════════════════════════════════════════════════════ // 手动总结 // ═══════════════════════════════════════════════════════════════════════════ async function handleManualGenerate(mesId, config) { if (isSummaryGenerating()) { postToFrame({ type: "SUMMARY_STATUS", statusText: "上一轮总结仍在进行中..." }); return; } const release = guard.acquire('summary'); if (!release) return; notifySummaryState(); try { await runSummaryGeneration(mesId, config, { onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }), onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }), onComplete: async ({ merged, endMesId, newEventIds }) => { const store = getSummaryStore(); postToFrame({ type: "SUMMARY_FULL_DATA", payload: buildFramePayload(store) }); invalidateLexicalIndex(); applyHideStateDebounced(); updateFrameStatsAfterSummary(endMesId, store.json || {}); await autoVectorizeNewEvents(newEventIds); }, }); } finally { release(); notifySummaryState(); } } // ═══════════════════════════════════════════════════════════════════════════ // 消息事件 // ═══════════════════════════════════════════════════════════════════════════ async function handleChatChanged() { const { chat } = getContext(); const newLength = Array.isArray(chat) ? chat.length : 0; await rollbackSummaryIfNeeded(); invalidateLexicalIndex(); initButtonsForAll(); const store = getSummaryStore(); if (store?.hideSummarizedHistory) { await applyHideState(); } if (frameReady) { await sendFrameBaseData(store, newLength); sendFrameFullData(store, newLength); sendAnchorStatsToFrame(); sendVectorStatsToFrame(); } // 实体词典注入 + 索引预热 refreshEntityLexiconAndWarmup(); // Embedding 连接预热(保持 TCP keep-alive,减少首次召回超时) warmupEmbeddingConnection(); setTimeout(() => checkVectorIntegrityAndWarn(), 2000); } async function handleMessageDeleted() { const { chat, chatId } = getContext(); const newLength = chat?.length || 0; await rollbackSummaryIfNeeded(); await syncOnMessageDeleted(chatId, newLength); // L0 同步:清理 floor >= newLength 的 atoms / index / vectors deleteStateAtomsFromFloor(newLength); deleteL0IndexFromFloor(newLength); if (chatId) { await deleteStateVectorsFromFloor(chatId, newLength); } invalidateLexicalIndex(); await sendAnchorStatsToFrame(); await sendVectorStatsToFrame(); applyHideStateDebounced(); } async function handleMessageSwiped() { const { chat, chatId } = getContext(); const lastFloor = (chat?.length || 1) - 1; await syncOnMessageSwiped(chatId, lastFloor); // L0 同步:清理 swipe 前该楼的 atoms / index / vectors deleteStateAtomsFromFloor(lastFloor); deleteL0IndexFromFloor(lastFloor); if (chatId) { await deleteStateVectorsFromFloor(chatId, lastFloor); } invalidateLexicalIndex(); initButtonsForAll(); applyHideStateDebounced(); await sendAnchorStatsToFrame(); await sendVectorStatsToFrame(); } async function handleMessageReceived() { const { chat, chatId } = getContext(); const lastFloor = (chat?.length || 1) - 1; const message = chat?.[lastFloor]; const vectorConfig = getVectorConfig(); initButtonsForAll(); // 向量全量生成中时跳过 L1 sync(避免竞争写入) if (guard.isRunning('vector')) return; await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig, () => { sendAnchorStatsToFrame(); sendVectorStatsToFrame(); }); await maybeAutoBuildChunks(); applyHideStateDebounced(); setTimeout(() => maybeAutoRunSummary("after_ai"), 1000); // 新消息后刷新实体词典(可能有新角色) refreshEntityLexiconAndWarmup(); // 自动补提取缺失的 L0(延迟执行,避免与当前楼提取竞争) setTimeout(() => maybeAutoExtractL0(), 2000); } function handleMessageSent() { initButtonsForAll(); setTimeout(() => maybeAutoRunSummary("before_user"), 1000); } async function handleMessageUpdated() { await rollbackSummaryIfNeeded(); initButtonsForAll(); applyHideStateDebounced(); } function handleMessageRendered(data) { const mesId = data?.element ? $(data.element).attr("mesid") : data?.messageId; if (mesId != null) addSummaryBtnToMessage(mesId); else initButtonsForAll(); } // ═══════════════════════════════════════════════════════════════════════════ // 用户消息缓存(供向量召回使用) // ═══════════════════════════════════════════════════════════════════════════ function handleMessageSentForRecall() { const { chat } = getContext(); const lastMsg = chat?.[chat.length - 1]; if (lastMsg?.is_user) { lastSentUserMessage = lastMsg.mes; lastSentTimestamp = Date.now(); } } function clearExtensionPrompt() { delete extension_prompts[EXT_PROMPT_KEY]; } // ═══════════════════════════════════════════════════════════════════════════ // Prompt 注入 // ═══════════════════════════════════════════════════════════════════════════ async function handleGenerationStarted(type, _params, isDryRun) { if (isDryRun) return; if (!getSettings().storySummary?.enabled) return; const excludeLastAi = type === "swipe" || type === "regenerate"; const vectorCfg = getVectorConfig(); clearExtensionPrompt(); // ★ 最后一道关卡:向量启用时,同步等待分词器就绪 if (vectorCfg?.enabled && !isTokenizerReady()) { try { await preloadTokenizer(); } catch (e) { xbLog.warn(MODULE_ID, "生成前分词器预热失败,将使用降级分词", e); } } // 判断是否使用缓存的用户消息(30秒内有效) let pendingUserMessage = null; if (type === "normal" && lastSentUserMessage && (Date.now() - lastSentTimestamp < 30000)) { pendingUserMessage = lastSentUserMessage; } // 用完清空 lastSentUserMessage = null; lastSentTimestamp = 0; const { chat, chatId } = getContext(); const chatLen = Array.isArray(chat) ? chat.length : 0; if (chatLen === 0) return; const store = getSummaryStore(); // 确定注入边界 // - 向量开:meta.lastChunkFloor(若无则回退 lastSummarizedMesId) // - 向量关:lastSummarizedMesId let boundary = -1; if (vectorCfg?.enabled) { const meta = chatId ? await getMeta(chatId) : null; boundary = meta?.lastChunkFloor ?? -1; if (boundary < 0) boundary = store?.lastSummarizedMesId ?? -1; } else { boundary = store?.lastSummarizedMesId ?? -1; } if (boundary < 0) return; // 计算深度:倒序插入,从末尾往前数 // 最小为 MIN_INJECTION_DEPTH,避免插入太靠近底部 const depth = Math.max(MIN_INJECTION_DEPTH, chatLen - boundary - 1); if (depth < 0) return; // 构建注入文本 let text = ""; if (vectorCfg?.enabled) { const r = await buildVectorPromptText(excludeLastAi, { postToFrame, echo: executeSlashCommand, pendingUserMessage, }); text = r?.text || ""; } else { text = buildNonVectorPromptText() || ""; } if (!text.trim()) return; // 获取用户配置的 role const cfg = getSummaryPanelConfig(); const roleKey = cfg.trigger?.role || 'system'; const role = ROLE_MAP[roleKey] || extension_prompt_roles.SYSTEM; // 写入 extension_prompts extension_prompts[EXT_PROMPT_KEY] = { value: text, position: extension_prompt_types.IN_CHAT, depth, role, }; } // ═══════════════════════════════════════════════════════════════════════════ // 事件注册 // ═══════════════════════════════════════════════════════════════════════════ const boundHandlers = { chatChanged: () => setTimeout(handleChatChanged, 80), messageDeleted: () => setTimeout(handleMessageDeleted, 50), messageReceived: () => setTimeout(handleMessageReceived, 150), messageSent: () => setTimeout(handleMessageSent, 150), messageSentRecall: handleMessageSentForRecall, messageSwiped: () => setTimeout(handleMessageSwiped, 100), messageUpdated: () => setTimeout(handleMessageUpdated, 100), messageEdited: () => setTimeout(handleMessageUpdated, 100), userRendered: (data) => setTimeout(() => handleMessageRendered(data), 50), charRendered: (data) => setTimeout(() => handleMessageRendered(data), 50), genStarted: handleGenerationStarted, genStopped: clearExtensionPrompt, genEnded: clearExtensionPrompt, }; function registerEvents() { if (eventsRegistered) return; eventsRegistered = true; CacheRegistry.register(MODULE_ID, { name: "待发送消息队列", getSize: () => pendingFrameMessages.length, getBytes: () => { try { return JSON.stringify(pendingFrameMessages || []).length * 2; } catch { return 0; } }, clear: () => { pendingFrameMessages = []; frameReady = false; }, }); initButtonsForAll(); eventSource.on(event_types.CHAT_CHANGED, boundHandlers.chatChanged); eventSource.on(event_types.MESSAGE_DELETED, boundHandlers.messageDeleted); eventSource.on(event_types.MESSAGE_RECEIVED, boundHandlers.messageReceived); eventSource.on(event_types.MESSAGE_SENT, boundHandlers.messageSent); eventSource.on(event_types.MESSAGE_SENT, boundHandlers.messageSentRecall); eventSource.on(event_types.MESSAGE_SWIPED, boundHandlers.messageSwiped); eventSource.on(event_types.MESSAGE_UPDATED, boundHandlers.messageUpdated); eventSource.on(event_types.MESSAGE_EDITED, boundHandlers.messageEdited); eventSource.on(event_types.USER_MESSAGE_RENDERED, boundHandlers.userRendered); eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, boundHandlers.charRendered); // 用户输入捕获(原生捕获阶段) document.addEventListener("pointerdown", onSendPointerdown, true); document.addEventListener("keydown", onSendKeydown, true); // 注入链路 eventSource.on(event_types.GENERATION_STARTED, boundHandlers.genStarted); eventSource.on(event_types.GENERATION_STOPPED, boundHandlers.genStopped); eventSource.on(event_types.GENERATION_ENDED, boundHandlers.genEnded); } function unregisterEvents() { CacheRegistry.unregister(MODULE_ID); eventsRegistered = false; eventSource.off(event_types.CHAT_CHANGED, boundHandlers.chatChanged); eventSource.off(event_types.MESSAGE_DELETED, boundHandlers.messageDeleted); eventSource.off(event_types.MESSAGE_RECEIVED, boundHandlers.messageReceived); eventSource.off(event_types.MESSAGE_SENT, boundHandlers.messageSent); eventSource.off(event_types.MESSAGE_SENT, boundHandlers.messageSentRecall); eventSource.off(event_types.MESSAGE_SWIPED, boundHandlers.messageSwiped); eventSource.off(event_types.MESSAGE_UPDATED, boundHandlers.messageUpdated); eventSource.off(event_types.MESSAGE_EDITED, boundHandlers.messageEdited); eventSource.off(event_types.USER_MESSAGE_RENDERED, boundHandlers.userRendered); eventSource.off(event_types.CHARACTER_MESSAGE_RENDERED, boundHandlers.charRendered); eventSource.off(event_types.GENERATION_STARTED, boundHandlers.genStarted); eventSource.off(event_types.GENERATION_STOPPED, boundHandlers.genStopped); eventSource.off(event_types.GENERATION_ENDED, boundHandlers.genEnded); $(".xiaobaix-story-summary-btn").remove(); hideOverlay(); clearExtensionPrompt(); document.removeEventListener("pointerdown", onSendPointerdown, true); document.removeEventListener("keydown", onSendKeydown, true); } // ═══════════════════════════════════════════════════════════════════════════ // Toggle 监听 // ═══════════════════════════════════════════════════════════════════════════ $(document).on("xiaobaix:storySummary:toggle", (_e, enabled) => { if (enabled) { registerEvents(); initButtonsForAll(); } else { unregisterEvents(); } }); // ═══════════════════════════════════════════════════════════════════════════ // 初始化 // ═══════════════════════════════════════════════════════════════════════════ jQuery(() => { if (!getSettings().storySummary?.enabled) return; registerEvents(); initStateIntegration(); maybePreloadTokenizer(); });