2026-01-17 16:34:39 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 导入
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
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";
|
2026-01-18 01:48:30 +08:00
|
|
|
|
import { generateSummary, parseSummaryJson } from "./llm-service.js";
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 常量
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 01:48:30 +08:00
|
|
|
|
// 总结数据工具(保留在主模块,因为依赖 store 对象)
|
2026-01-17 16:34:39 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
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") || "(空白,这是首次总结)";
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 快照与数据合并
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
function addSummarySnapshot(store, endMesId) {
|
|
|
|
|
|
store.summaryHistory ||= [];
|
|
|
|
|
|
store.summaryHistory.push({ endMesId });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-17 16:34:39 +08:00
|
|
|
|
function mergeNewData(oldJson, parsed, endMesId) {
|
|
|
|
|
|
const merged = structuredClone(oldJson || {});
|
|
|
|
|
|
merged.keywords ||= [];
|
|
|
|
|
|
merged.events ||= [];
|
|
|
|
|
|
merged.characters ||= {};
|
|
|
|
|
|
merged.characters.main ||= [];
|
|
|
|
|
|
merged.characters.relationships ||= [];
|
|
|
|
|
|
merged.arcs ||= [];
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
// 关键词:完全替换(全局关键词)
|
2026-01-17 16:34:39 +08:00
|
|
|
|
if (parsed.keywords?.length) {
|
|
|
|
|
|
merged.keywords = parsed.keywords.map(k => ({ ...k, _addedAt: endMesId }));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
// 事件:追加
|
2026-01-17 16:34:39 +08:00
|
|
|
|
(parsed.events || []).forEach(e => {
|
|
|
|
|
|
e._addedAt = endMesId;
|
|
|
|
|
|
merged.events.push(e);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
// 新角色:追加不重复
|
2026-01-17 16:34:39 +08:00
|
|
|
|
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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
// 关系:更新或追加
|
2026-01-17 16:34:39 +08:00
|
|
|
|
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());
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
// 弧光:更新或追加
|
2026-01-17 16:34:39 +08:00
|
|
|
|
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;
|
2026-01-18 01:48:30 +08:00
|
|
|
|
pendingFrameMessages.forEach(p => postToIframe(iframe, p, "LittleWhiteBox"));
|
2026-01-17 16:34:39 +08:00
|
|
|
|
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":
|
2026-01-18 01:48:30 +08:00
|
|
|
|
window.xiaobaixStreamingGeneration?.cancel?.(SUMMARY_SESSION_ID);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
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;
|
|
|
|
|
|
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();
|
2026-01-18 01:48:30 +08:00
|
|
|
|
sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
})();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const { chat } = getContext();
|
2026-01-18 01:48:30 +08:00
|
|
|
|
sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0);
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
case "SAVE_PANEL_CONFIG":
|
2026-01-17 16:34:39 +08:00
|
|
|
|
if (data.config) {
|
|
|
|
|
|
CommonSettingStorage.set(SUMMARY_CONFIG_KEY, data.config);
|
|
|
|
|
|
xbLog.info(MODULE_ID, '面板配置已保存到服务器');
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
case "REQUEST_PANEL_CONFIG":
|
2026-01-17 16:34:39 +08:00
|
|
|
|
sendSavedConfigToFrame();
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Overlay 面板
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
function createOverlay() {
|
|
|
|
|
|
if (overlayCreated) return;
|
|
|
|
|
|
overlayCreated = true;
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
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';
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
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]);
|
|
|
|
|
|
// 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 = '<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);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
2026-01-18 01:48:30 +08:00
|
|
|
|
// 打开面板与数据发送
|
2026-01-17 16:34:39 +08:00
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
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) => {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
const speaker = m.name || (m.is_user ? userLabel : charLabel);
|
|
|
|
|
|
return `#${start + i + 1} 【${speaker}】\n${m.mes}`;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
}).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 || {}) },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-18 01:48:30 +08:00
|
|
|
|
if (result.trigger.timing === 'manual') result.trigger.enabled = false;
|
|
|
|
|
|
if (result.trigger.useStream === undefined) result.trigger.useStream = true;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-01-18 01:48:30 +08:00
|
|
|
|
const existingEventCount = store?.json?.events?.length || 0;
|
2026-01-17 16:34:39 +08:00
|
|
|
|
const useStream = cfg.trigger?.useStream !== false;
|
|
|
|
|
|
const apiCfg = cfg.api || {};
|
|
|
|
|
|
const genCfg = cfg.gen || {};
|
|
|
|
|
|
|
|
|
|
|
|
let raw;
|
|
|
|
|
|
try {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
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,
|
|
|
|
|
|
});
|
2026-01-17 16:34:39 +08:00
|
|
|
|
} 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 text = formatSummaryForPrompt(store);
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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: () => {
|
2026-01-18 01:48:30 +08:00
|
|
|
|
try { return JSON.stringify(pendingFrameMessages || []).length * 2; }
|
|
|
|
|
|
catch { return 0; }
|
2026-01-17 16:34:39 +08:00
|
|
|
|
},
|
|
|
|
|
|
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();
|
|
|
|
|
|
});
|