1196 lines
45 KiB
JavaScript
1196 lines
45 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════════════════
|
||
// Story Summary - 主入口
|
||
// UI 交互、事件监听、iframe 通讯
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
import { getContext } from "../../../../../extensions.js";
|
||
import { eventSource, event_types } 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";
|
||
|
||
// 拆分模块
|
||
import { getSettings, getSummaryPanelConfig, getVectorConfig, saveVectorConfig } from "./data/config.js";
|
||
import {
|
||
getSummaryStore,
|
||
saveSummaryStore,
|
||
calcHideRange,
|
||
rollbackSummaryIfNeeded,
|
||
clearSummaryData,
|
||
} from "./data/store.js";
|
||
|
||
import {
|
||
recallAndInjectPrompt,
|
||
updateSummaryExtensionPrompt,
|
||
clearSummaryExtensionPrompt,
|
||
} from "./generate/prompt.js";
|
||
|
||
import { runSummaryGeneration } from "./generate/generator.js";
|
||
|
||
// 向量服务
|
||
import {
|
||
embed,
|
||
getEngineFingerprint,
|
||
checkLocalModelStatus,
|
||
downloadLocalModel,
|
||
cancelDownload,
|
||
deleteLocalModelCache,
|
||
testOnlineService,
|
||
fetchOnlineModels,
|
||
isLocalModelLoaded,
|
||
DEFAULT_LOCAL_MODEL,
|
||
} from "./vector/embedder.js";
|
||
|
||
import {
|
||
getMeta,
|
||
updateMeta,
|
||
getAllEventVectors,
|
||
saveEventVectors as saveEventVectorsToDb,
|
||
clearEventVectors,
|
||
clearAllChunks,
|
||
saveChunks,
|
||
saveChunkVectors,
|
||
getStorageStats,
|
||
ensureFingerprintMatch,
|
||
} from "./vector/chunk-store.js";
|
||
|
||
import {
|
||
buildIncrementalChunks,
|
||
getChunkBuildStatus,
|
||
chunkMessage,
|
||
syncOnMessageDeleted,
|
||
syncOnMessageSwiped,
|
||
syncOnMessageReceived,
|
||
} from "./vector/chunk-builder.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', 'world'];
|
||
const MESSAGE_EVENT = 'message';
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 状态变量
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
let summaryGenerating = false;
|
||
let overlayCreated = false;
|
||
let frameReady = false;
|
||
let currentMesId = null;
|
||
let pendingFrameMessages = [];
|
||
let eventsRegistered = false;
|
||
let vectorGenerating = false;
|
||
let vectorCancelled = false;
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 工具函数
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||
|
||
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 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 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 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,
|
||
},
|
||
mismatch
|
||
});
|
||
}
|
||
|
||
async function sendLocalModelStatusToFrame(modelId) {
|
||
if (!modelId) {
|
||
const cfg = getVectorConfig();
|
||
modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
|
||
}
|
||
const status = await checkLocalModelStatus(modelId);
|
||
postToFrame({
|
||
type: 'VECTOR_LOCAL_MODEL_STATUS',
|
||
status: status.status,
|
||
message: status.message
|
||
});
|
||
}
|
||
|
||
async function handleDownloadLocalModel(modelId) {
|
||
try {
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'downloading', message: '下载中...' });
|
||
|
||
await downloadLocalModel(modelId, (percent) => {
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_PROGRESS', percent });
|
||
});
|
||
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'ready', message: '已就绪' });
|
||
} catch (e) {
|
||
if (e.message === '下载已取消') {
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'not_downloaded', message: '已取消' });
|
||
} else {
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'error', message: e.message });
|
||
}
|
||
}
|
||
}
|
||
|
||
function handleCancelDownload() {
|
||
cancelDownload();
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'not_downloaded', message: '已取消' });
|
||
}
|
||
|
||
async function handleDeleteLocalModel(modelId) {
|
||
try {
|
||
await deleteLocalModelCache(modelId);
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'not_downloaded', message: '未下载' });
|
||
} catch (e) {
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'error', message: e.message });
|
||
}
|
||
}
|
||
|
||
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 handleFetchOnlineModels(config) {
|
||
try {
|
||
postToFrame({ type: 'VECTOR_ONLINE_STATUS', status: 'downloading', message: '拉取中...' });
|
||
const models = await fetchOnlineModels(config);
|
||
postToFrame({ type: 'VECTOR_ONLINE_MODELS', models });
|
||
postToFrame({ type: 'VECTOR_ONLINE_STATUS', status: 'success', message: `找到 ${models.length} 个模型` });
|
||
} catch (e) {
|
||
postToFrame({ type: 'VECTOR_ONLINE_STATUS', status: 'error', message: e.message });
|
||
}
|
||
}
|
||
|
||
async function handleGenerateVectors(vectorCfg) {
|
||
if (vectorGenerating) return;
|
||
|
||
if (!vectorCfg?.enabled) {
|
||
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L1', current: -1, total: 0 });
|
||
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L2', current: -1, total: 0 });
|
||
return;
|
||
}
|
||
|
||
const { chatId, chat } = getContext();
|
||
if (!chatId || !chat?.length) return;
|
||
|
||
if (vectorCfg.engine === 'online') {
|
||
if (!vectorCfg.online?.key || !vectorCfg.online?.model) {
|
||
postToFrame({ type: 'VECTOR_ONLINE_STATUS', status: 'error', message: '请配置在线服务 API' });
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (vectorCfg.engine === 'local') {
|
||
const modelId = vectorCfg.local?.modelId || DEFAULT_LOCAL_MODEL;
|
||
const status = await checkLocalModelStatus(modelId);
|
||
if (status.status !== 'ready') {
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'downloading', message: '正在加载模型...' });
|
||
try {
|
||
await downloadLocalModel(modelId, (percent) => {
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_PROGRESS', percent });
|
||
});
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'ready', message: '已就绪' });
|
||
} catch (e) {
|
||
xbLog.error(MODULE_ID, '模型加载失败', e);
|
||
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'error', message: e.message });
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
vectorGenerating = true;
|
||
vectorCancelled = false;
|
||
|
||
const fingerprint = getEngineFingerprint(vectorCfg);
|
||
const isLocal = vectorCfg.engine === 'local';
|
||
const batchSize = isLocal ? 5 : 20;
|
||
const concurrency = isLocal ? 1 : 2;
|
||
|
||
await clearAllChunks(chatId);
|
||
await updateMeta(chatId, { lastChunkFloor: -1, fingerprint });
|
||
|
||
const allChunks = [];
|
||
for (let floor = 0; floor < chat.length; floor++) {
|
||
const chunks = chunkMessage(floor, chat[floor]);
|
||
allChunks.push(...chunks);
|
||
}
|
||
|
||
if (allChunks.length > 0) {
|
||
await saveChunks(chatId, allChunks);
|
||
}
|
||
|
||
const l1Texts = allChunks.map(c => c.text);
|
||
const l1Batches = [];
|
||
for (let i = 0; i < l1Texts.length; i += batchSize) {
|
||
l1Batches.push({
|
||
phase: 'L1',
|
||
texts: l1Texts.slice(i, i + batchSize),
|
||
startIdx: i,
|
||
});
|
||
}
|
||
|
||
const store = getSummaryStore();
|
||
const events = store?.json?.events || [];
|
||
|
||
await ensureFingerprintMatch(chatId, fingerprint);
|
||
const existingVectors = await getAllEventVectors(chatId);
|
||
const existingIds = new Set(existingVectors.map(v => v.eventId));
|
||
|
||
const l2Pairs = events
|
||
.filter(e => !existingIds.has(e.id))
|
||
.map(e => ({ id: e.id, text: `${e.title || ''} ${e.summary || ''}`.trim() }))
|
||
.filter(p => p.text);
|
||
|
||
const l2Batches = [];
|
||
for (let i = 0; i < l2Pairs.length; i += batchSize) {
|
||
const batch = l2Pairs.slice(i, i + batchSize);
|
||
l2Batches.push({
|
||
phase: 'L2',
|
||
texts: batch.map(p => p.text),
|
||
ids: batch.map(p => p.id),
|
||
startIdx: i,
|
||
});
|
||
}
|
||
|
||
const l1Total = allChunks.length;
|
||
const l2Total = events.length;
|
||
let l1Completed = 0;
|
||
let l2Completed = existingIds.size;
|
||
|
||
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L1', current: 0, total: l1Total });
|
||
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L2', current: l2Completed, total: l2Total });
|
||
|
||
const allTasks = [...l1Batches, ...l2Batches];
|
||
const l1Vectors = new Array(l1Texts.length);
|
||
const l2VectorItems = [];
|
||
|
||
let taskIndex = 0;
|
||
|
||
async function worker() {
|
||
while (taskIndex < allTasks.length) {
|
||
if (vectorCancelled) break;
|
||
|
||
const i = taskIndex++;
|
||
if (i >= allTasks.length) break;
|
||
|
||
const task = allTasks[i];
|
||
|
||
try {
|
||
const vectors = await embed(task.texts, vectorCfg);
|
||
|
||
if (task.phase === 'L1') {
|
||
for (let j = 0; j < vectors.length; j++) {
|
||
l1Vectors[task.startIdx + j] = vectors[j];
|
||
}
|
||
l1Completed += task.texts.length;
|
||
postToFrame({
|
||
type: 'VECTOR_GEN_PROGRESS',
|
||
phase: 'L1',
|
||
current: Math.min(l1Completed, l1Total),
|
||
total: l1Total,
|
||
});
|
||
} else {
|
||
for (let j = 0; j < vectors.length; j++) {
|
||
l2VectorItems.push({ eventId: task.ids[j], vector: vectors[j] });
|
||
}
|
||
l2Completed += task.texts.length;
|
||
postToFrame({
|
||
type: 'VECTOR_GEN_PROGRESS',
|
||
phase: 'L2',
|
||
current: Math.min(l2Completed, l2Total),
|
||
total: l2Total,
|
||
});
|
||
}
|
||
} catch (e) {
|
||
xbLog.error(MODULE_ID, `${task.phase} batch 向量化失败`, e);
|
||
}
|
||
}
|
||
}
|
||
|
||
await Promise.all(
|
||
Array(Math.min(concurrency, allTasks.length))
|
||
.fill(null)
|
||
.map(() => worker())
|
||
);
|
||
|
||
if (allChunks.length > 0 && l1Vectors.filter(Boolean).length > 0) {
|
||
const chunkVectorItems = allChunks
|
||
.map((chunk, idx) => l1Vectors[idx] ? { chunkId: chunk.chunkId, vector: l1Vectors[idx] } : null)
|
||
.filter(Boolean);
|
||
await saveChunkVectors(chatId, chunkVectorItems, fingerprint);
|
||
await updateMeta(chatId, { lastChunkFloor: chat.length - 1 });
|
||
}
|
||
|
||
if (l2VectorItems.length > 0) {
|
||
await saveEventVectorsToDb(chatId, l2VectorItems, fingerprint);
|
||
}
|
||
|
||
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L1', current: -1, total: 0 });
|
||
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L2', current: -1, total: 0 });
|
||
await sendVectorStatsToFrame();
|
||
|
||
vectorGenerating = false;
|
||
vectorCancelled = false;
|
||
|
||
xbLog.info(MODULE_ID, `向量生成完成: L1=${l1Vectors.filter(Boolean).length}, L2=${l2VectorItems.length}`);
|
||
}
|
||
|
||
async function handleClearVectors() {
|
||
const { chatId } = getContext();
|
||
if (!chatId) return;
|
||
|
||
await clearEventVectors(chatId);
|
||
await clearAllChunks(chatId);
|
||
await updateMeta(chatId, { lastChunkFloor: -1 });
|
||
await sendVectorStatsToFrame();
|
||
xbLog.info(MODULE_ID, '向量数据已清除');
|
||
}
|
||
|
||
async function autoVectorizeNewEvents(newEventIds) {
|
||
const cfg = getVectorConfig();
|
||
if (!cfg?.enabled || !newEventIds?.length) return;
|
||
|
||
await sleep(3000);
|
||
|
||
const { chatId } = getContext();
|
||
if (!chatId) return;
|
||
|
||
const store = getSummaryStore();
|
||
const events = store?.json?.events || [];
|
||
|
||
const fingerprint = getEngineFingerprint(cfg);
|
||
const meta = await getMeta(chatId);
|
||
if (meta.fingerprint && meta.fingerprint !== fingerprint) {
|
||
xbLog.warn(MODULE_ID, '引擎不匹配,跳过自动向量化');
|
||
return;
|
||
}
|
||
|
||
if (cfg.engine === 'local') {
|
||
const modelId = cfg.local?.modelId || DEFAULT_LOCAL_MODEL;
|
||
if (!isLocalModelLoaded(modelId)) {
|
||
xbLog.warn(MODULE_ID, '本地模型未加载,跳过自动向量化');
|
||
return;
|
||
}
|
||
}
|
||
|
||
const toVectorize = newEventIds.filter(id => events.some(e => e.id === id));
|
||
if (toVectorize.length === 0) return;
|
||
|
||
const texts = toVectorize.map(id => {
|
||
const event = events.find(e => e.id === id);
|
||
return event ? `${event.title || ''} ${event.summary || ''}`.trim() : '';
|
||
}).filter(t => t);
|
||
|
||
if (!texts.length) return;
|
||
|
||
try {
|
||
const vectors = await embed(texts, cfg);
|
||
const newVectorItems = [];
|
||
for (let i = 0; i < toVectorize.length && i < vectors.length; i++) {
|
||
newVectorItems.push({
|
||
eventId: toVectorize[i],
|
||
vector: vectors[i],
|
||
});
|
||
}
|
||
if (newVectorItems.length > 0) {
|
||
await saveEventVectorsToDb(chatId, newVectorItems, fingerprint);
|
||
}
|
||
xbLog.info(MODULE_ID, `自动向量化 ${toVectorize.length} 个新事件`);
|
||
} catch (e) {
|
||
xbLog.error(MODULE_ID, '自动向量化失败', 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 = $(`
|
||
<div id="xiaobaix-story-summary-overlay" style="
|
||
position: fixed !important; inset: 0 !important;
|
||
width: 100vw !important; height: ${overlayHeight} !important;
|
||
z-index: 99999 !important; display: none; overflow: hidden !important;
|
||
">
|
||
<div class="xb-ss-backdrop" style="
|
||
position: absolute !important; inset: 0 !important;
|
||
background: rgba(0,0,0,.55) !important;
|
||
backdrop-filter: blur(4px) !important;
|
||
"></div>
|
||
<div class="xb-ss-frame-wrap" style="
|
||
position: absolute !important; inset: 12px !important; z-index: 1 !important;
|
||
">
|
||
<iframe id="xiaobaix-story-summary-iframe" class="xiaobaix-iframe"
|
||
src="${iframePath}"
|
||
style="width:100% !important; height:100% !important; border:none !important;
|
||
border-radius:12px !important; box-shadow:0 0 30px rgba(0,0,0,.4) !important;
|
||
background:#fafafa !important;">
|
||
</iframe>
|
||
</div>
|
||
<button class="xb-ss-close-btn" style="
|
||
position: absolute !important; top: 20px !important; right: 20px !important;
|
||
z-index: 2 !important; width: 36px !important; height: 36px !important;
|
||
border-radius: 50% !important; border: none !important;
|
||
background: rgba(0,0,0,.6) !important; color: #fff !important;
|
||
font-size: 20px !important; cursor: pointer !important;
|
||
display: flex !important; align-items: center !important;
|
||
justify-content: center !important;
|
||
">✕</button>
|
||
</div>
|
||
`);
|
||
|
||
$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 = '<i class="fa-solid fa-chart-line"></i>';
|
||
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 || [],
|
||
world: store.json.world || [],
|
||
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);
|
||
|
||
sendVectorConfigToFrame();
|
||
sendVectorStatsToFrame();
|
||
const cfg = getVectorConfig();
|
||
const modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
|
||
sendLocalModelStatusToFrame(modelId);
|
||
}
|
||
|
||
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 || [],
|
||
world: store.json.world || [],
|
||
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,
|
||
},
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 自动触发总结
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
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 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;
|
||
|
||
if (cfg.engine === 'local') {
|
||
const modelId = cfg.local?.modelId || DEFAULT_LOCAL_MODEL;
|
||
if (!isLocalModelLoaded(modelId)) return;
|
||
}
|
||
|
||
xbLog.info(MODULE_ID, `auto L1 chunks: pending=${status.pending}`);
|
||
|
||
try {
|
||
await buildIncrementalChunks({ vectorConfig: cfg });
|
||
} catch (e) {
|
||
xbLog.error(MODULE_ID, '自动 L1 构建失败', e);
|
||
}
|
||
}
|
||
|
||
async function autoRunSummaryWithRetry(targetMesId, configForRun) {
|
||
setSummaryGenerating(true);
|
||
|
||
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: ({ merged, endMesId, newEventIds }) => {
|
||
postToFrame({
|
||
type: "SUMMARY_FULL_DATA",
|
||
payload: {
|
||
keywords: merged.keywords || [],
|
||
events: merged.events || [],
|
||
characters: merged.characters || { main: [], relationships: [] },
|
||
arcs: merged.arcs || [],
|
||
world: merged.world || [],
|
||
lastSummarizedMesId: endMesId,
|
||
},
|
||
});
|
||
postToFrame({
|
||
type: "SUMMARY_STATUS",
|
||
statusText: `已更新至 ${endMesId + 1} 楼 · ${merged.events?.length || 0} 个事件`,
|
||
});
|
||
updateFrameStatsAfterSummary(endMesId, merged);
|
||
updateSummaryExtensionPrompt();
|
||
autoVectorizeNewEvents(newEventIds);
|
||
},
|
||
});
|
||
|
||
if (result.success) {
|
||
setSummaryGenerating(false);
|
||
return;
|
||
}
|
||
|
||
if (attempt < 3) await sleep(1000);
|
||
}
|
||
|
||
setSummaryGenerating(false);
|
||
xbLog.error(MODULE_ID, '自动总结失败(已重试3次)');
|
||
await executeSlashCommand('/echo severity=error 剧情总结失败(已自动重试 3 次)。请稍后再试。');
|
||
}
|
||
|
||
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();
|
||
setSummaryGenerating(summaryGenerating);
|
||
sendSavedConfigToFrame();
|
||
sendVectorConfigToFrame();
|
||
sendVectorStatsToFrame();
|
||
{
|
||
const cfg = getVectorConfig();
|
||
const modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
|
||
sendLocalModelStatusToFrame(modelId);
|
||
}
|
||
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');
|
||
setSummaryGenerating(false);
|
||
postToFrame({ type: "SUMMARY_STATUS", statusText: "已停止" });
|
||
break;
|
||
|
||
case "VECTOR_DOWNLOAD_MODEL":
|
||
handleDownloadLocalModel(data.modelId);
|
||
break;
|
||
|
||
case "VECTOR_CANCEL_DOWNLOAD":
|
||
handleCancelDownload();
|
||
break;
|
||
|
||
case "VECTOR_DELETE_MODEL":
|
||
handleDeleteLocalModel(data.modelId);
|
||
break;
|
||
|
||
case "VECTOR_CHECK_LOCAL_MODEL":
|
||
sendLocalModelStatusToFrame(data.modelId);
|
||
break;
|
||
|
||
case "VECTOR_TEST_ONLINE":
|
||
handleTestOnlineService(data.provider, data.config);
|
||
break;
|
||
|
||
case "VECTOR_FETCH_MODELS":
|
||
handleFetchOnlineModels(data.config);
|
||
break;
|
||
|
||
case "VECTOR_GENERATE":
|
||
if (data.config) saveVectorConfig(data.config);
|
||
handleGenerateVectors(data.config);
|
||
break;
|
||
|
||
case "VECTOR_CLEAR":
|
||
handleClearVectors();
|
||
break;
|
||
|
||
case "VECTOR_CANCEL_GENERATE":
|
||
vectorCancelled = true;
|
||
break;
|
||
|
||
case "REQUEST_CLEAR": {
|
||
const { chat, chatId } = getContext();
|
||
clearSummaryData(chatId);
|
||
clearSummaryExtensionPrompt();
|
||
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 ||= {};
|
||
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;
|
||
if (lastSummarized < 0) break;
|
||
store.hideSummarizedHistory = !!data.enabled;
|
||
saveSummaryStore();
|
||
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;
|
||
}
|
||
}
|
||
|
||
async function handleManualGenerate(mesId, config) {
|
||
if (isSummaryGenerating()) {
|
||
postToFrame({ type: "SUMMARY_STATUS", statusText: "上一轮总结仍在进行中..." });
|
||
return;
|
||
}
|
||
|
||
setSummaryGenerating(true);
|
||
xbLog.info(MODULE_ID, `开始手动总结 mesId=${mesId}`);
|
||
|
||
await runSummaryGeneration(mesId, config, {
|
||
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
|
||
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
|
||
onComplete: ({ merged, endMesId, newEventIds }) => {
|
||
const store = getSummaryStore();
|
||
|
||
postToFrame({
|
||
type: "SUMMARY_FULL_DATA",
|
||
payload: {
|
||
keywords: merged.keywords || [],
|
||
events: merged.events || [],
|
||
characters: merged.characters || { main: [], relationships: [] },
|
||
arcs: merged.arcs || [],
|
||
world: merged.world || [],
|
||
lastSummarizedMesId: endMesId,
|
||
},
|
||
});
|
||
|
||
postToFrame({
|
||
type: "SUMMARY_STATUS",
|
||
statusText: `已更新至 ${endMesId + 1} 楼 · ${merged.events?.length || 0} 个事件`,
|
||
});
|
||
|
||
// 处理隐藏逻辑
|
||
const lastSummarized = store?.lastSummarizedMesId ?? -1;
|
||
if (store?.hideSummarizedHistory && lastSummarized >= 0) {
|
||
const range = calcHideRange(lastSummarized);
|
||
if (range) {
|
||
executeSlashCommand(`/hide ${range.start}-${range.end}`);
|
||
}
|
||
}
|
||
|
||
updateFrameStatsAfterSummary(endMesId, merged);
|
||
updateSummaryExtensionPrompt();
|
||
autoVectorizeNewEvents(newEventIds);
|
||
},
|
||
});
|
||
|
||
setSummaryGenerating(false);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 事件处理器
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
async function handleChatChanged() {
|
||
const { chat } = getContext();
|
||
const newLength = Array.isArray(chat) ? chat.length : 0;
|
||
|
||
await 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);
|
||
notifyFrameAfterRollback(store);
|
||
}
|
||
}
|
||
|
||
async function handleMessageDeleted() {
|
||
const { chat, chatId } = getContext();
|
||
const newLength = chat?.length || 0;
|
||
|
||
await rollbackSummaryIfNeeded();
|
||
updateSummaryExtensionPrompt();
|
||
|
||
// L1 同步
|
||
await syncOnMessageDeleted(chatId, newLength);
|
||
}
|
||
|
||
async function handleMessageSwiped() {
|
||
const { chat, chatId } = getContext();
|
||
const lastFloor = (chat?.length || 1) - 1;
|
||
|
||
// L1 同步
|
||
await syncOnMessageSwiped(chatId, lastFloor);
|
||
|
||
updateSummaryExtensionPrompt();
|
||
initButtonsForAll();
|
||
}
|
||
|
||
async function handleMessageReceived() {
|
||
const { chat, chatId } = getContext();
|
||
const lastFloor = (chat?.length || 1) - 1;
|
||
const message = chat?.[lastFloor];
|
||
const vectorConfig = getVectorConfig();
|
||
|
||
updateSummaryExtensionPrompt();
|
||
initButtonsForAll();
|
||
|
||
// L1 同步
|
||
await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig);
|
||
await maybeAutoBuildChunks();
|
||
|
||
setTimeout(() => maybeAutoRunSummary('after_ai'), 1000);
|
||
}
|
||
|
||
function handleMessageSent() {
|
||
updateSummaryExtensionPrompt();
|
||
initButtonsForAll();
|
||
setTimeout(() => maybeAutoRunSummary('before_user'), 1000);
|
||
}
|
||
|
||
async function handleMessageUpdated() {
|
||
await rollbackSummaryIfNeeded();
|
||
updateSummaryExtensionPrompt();
|
||
initButtonsForAll();
|
||
}
|
||
|
||
function handleMessageRendered(data) {
|
||
const mesId = data?.element ? $(data.element).attr("mesid") : data?.messageId;
|
||
if (mesId != null) {
|
||
addSummaryBtnToMessage(mesId);
|
||
} else {
|
||
initButtonsForAll();
|
||
}
|
||
}
|
||
|
||
async function handleGenerationStarted(type, _params, isDryRun) {
|
||
if (isDryRun) return;
|
||
const excludeLastAi = type === 'swipe' || type === 'regenerate';
|
||
await recallAndInjectPrompt(excludeLastAi, postToFrame);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// 事件注册
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
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();
|
||
|
||
eventSource.on(event_types.CHAT_CHANGED, () => setTimeout(handleChatChanged, 80));
|
||
eventSource.on(event_types.MESSAGE_DELETED, () => setTimeout(handleMessageDeleted, 50));
|
||
eventSource.on(event_types.MESSAGE_RECEIVED, () => setTimeout(handleMessageReceived, 150));
|
||
eventSource.on(event_types.MESSAGE_SENT, () => setTimeout(handleMessageSent, 150));
|
||
eventSource.on(event_types.MESSAGE_SWIPED, () => setTimeout(handleMessageSwiped, 100));
|
||
eventSource.on(event_types.MESSAGE_UPDATED, () => setTimeout(handleMessageUpdated, 100));
|
||
eventSource.on(event_types.MESSAGE_EDITED, () => setTimeout(handleMessageUpdated, 100));
|
||
eventSource.on(event_types.USER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50));
|
||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50));
|
||
eventSource.on(event_types.GENERATION_STARTED, handleGenerationStarted);
|
||
}
|
||
|
||
function unregisterEvents() {
|
||
xbLog.info(MODULE_ID, '模块清理');
|
||
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();
|
||
});
|