Files
LittleWhiteBox/modules/story-summary/story-summary.js

1508 lines
57 KiB
JavaScript
Raw Normal View History

2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-29 01:17:37 +08:00
// Story Summary - 主入口(最终版)
//
// 稳定目标:
// 1) "聊天时隐藏已总结" 永远只隐藏"已总结"部分,绝不影响未总结部分
// 2) 关闭隐藏 = 暴力全量 unhide确保立刻恢复
// 3) 开启隐藏 / 改Y / 切Chat / 收新消息:先全量 unhide再按边界重新 hide
// 4) Prompt 注入位置稳定:永远插在"最后一条 user 消息"之前
// 5) 注入回归 extension_prompts + IN_CHAT + depth动态计算
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-26 01:16:35 +08:00
import { getContext } from "../../../../../extensions.js";
import {
eventSource,
event_types,
extension_prompts,
extension_prompt_types,
extension_prompt_roles,
} from "../../../../../../script.js";
2026-01-26 01:16:35 +08:00
import { extensionFolderPath } from "../../core/constants.js";
2026-01-17 16:34:39 +08:00
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-26 01:16:35 +08:00
2026-01-27 16:04:57 +08:00
// config/store
2026-01-26 01:16:35 +08:00
import { getSettings, getSummaryPanelConfig, getVectorConfig, saveVectorConfig } from "./data/config.js";
import {
getSummaryStore,
saveSummaryStore,
calcHideRange,
rollbackSummaryIfNeeded,
clearSummaryData,
} from "./data/store.js";
2026-01-29 01:17:37 +08:00
// prompt text builder
2026-01-26 01:16:35 +08:00
import {
2026-01-29 01:17:37 +08:00
buildVectorPromptText,
buildNonVectorPromptText,
2026-01-26 01:16:35 +08:00
} from "./generate/prompt.js";
2026-01-27 16:04:57 +08:00
// summary generation
2026-01-26 01:16:35 +08:00
import { runSummaryGeneration } from "./generate/generator.js";
2026-01-27 16:04:57 +08:00
// vector service
2026-01-26 01:16:35 +08:00
import {
embed,
getEngineFingerprint,
checkLocalModelStatus,
downloadLocalModel,
cancelDownload,
deleteLocalModelCache,
testOnlineService,
fetchOnlineModels,
isLocalModelLoaded,
DEFAULT_LOCAL_MODEL,
} from "./vector/embedder.js";
import {
getMeta,
updateMeta,
saveEventVectors as saveEventVectorsToDb,
clearEventVectors,
deleteEventVectorsByIds,
2026-01-26 01:16:35 +08:00
clearAllChunks,
saveChunks,
saveChunkVectors,
getStorageStats,
} from "./vector/chunk-store.js";
import {
buildIncrementalChunks,
getChunkBuildStatus,
chunkMessage,
syncOnMessageDeleted,
syncOnMessageSwiped,
syncOnMessageReceived,
} from "./vector/chunk-builder.js";
2026-01-17 16:34:39 +08:00
2026-01-29 17:02:51 +08:00
// vector io
import { exportVectors, importVectors } from "./vector/vector-io.js";
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
// 常量
// ═══════════════════════════════════════════════════════════════════════════
2026-01-27 16:04:57 +08:00
const MODULE_ID = "storySummary";
const SUMMARY_CONFIG_KEY = "storySummaryPanelConfig";
2026-01-17 16:34:39 +08:00
const iframePath = `${extensionFolderPath}/modules/story-summary/story-summary.html`;
2026-01-27 16:04:57 +08:00
const VALID_SECTIONS = ["keywords", "events", "characters", "arcs", "world"];
const MESSAGE_EVENT = "message";
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
// 状态变量
// ═══════════════════════════════════════════════════════════════════════════
let summaryGenerating = false;
let overlayCreated = false;
let frameReady = false;
let currentMesId = null;
let pendingFrameMessages = [];
let eventsRegistered = false;
2026-01-26 01:16:35 +08:00
let vectorGenerating = false;
let vectorCancelled = false;
2026-01-29 01:17:37 +08:00
let vectorAbortController = null;
// ★ 用户消息缓存(解决 GENERATION_STARTED 时 chat 尚未包含用户消息的问题)
let lastSentUserMessage = null;
let lastSentTimestamp = 0;
2026-01-17 16:34:39 +08:00
2026-01-27 22:51:44 +08:00
let hideApplyTimer = null;
const HIDE_APPLY_DEBOUNCE_MS = 250;
2026-01-27 16:04:57 +08:00
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2026-01-29 17:02:51 +08:00
// 向量提醒节流
let lastVectorWarningAt = 0;
const VECTOR_WARNING_COOLDOWN_MS = 120000; // 2分钟内不重复提醒
const EXT_PROMPT_KEY = "LittleWhiteBox_StorySummary";
2026-01-29 17:45:20 +08:00
// role 映射
const ROLE_MAP = {
system: extension_prompt_roles.SYSTEM,
user: extension_prompt_roles.USER,
assistant: extension_prompt_roles.ASSISTANT,
};
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-27 16:04:57 +08:00
// 工具:执行斜杠命令
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
async function executeSlashCommand(command) {
try {
2026-01-27 16:04:57 +08:00
const executeCmd =
window.executeSlashCommands ||
window.executeSlashCommandsOnChatInput ||
(typeof SillyTavern !== "undefined" && SillyTavern.getContext()?.executeSlashCommands);
2026-01-17 16:34:39 +08:00
if (executeCmd) {
await executeCmd(command);
2026-01-27 16:04:57 +08:00
} else if (typeof window.STscript === "function") {
2026-01-17 16:34:39 +08:00
await window.STscript(command);
}
} catch (e) {
xbLog.error(MODULE_ID, `执行命令失败: ${command}`, e);
}
}
2026-01-29 01:17:37 +08:00
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}`);
}
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-26 01:16:35 +08:00
// 生成状态管理
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-26 01:16:35 +08:00
function setSummaryGenerating(flag) {
summaryGenerating = !!flag;
postToFrame({ type: "GENERATION_STATE", isGenerating: summaryGenerating });
}
2026-01-18 01:48:30 +08:00
2026-01-26 01:16:35 +08:00
function isSummaryGenerating() {
return summaryGenerating;
}
2026-01-18 01:48:30 +08:00
2026-01-26 01:16:35 +08:00
// ═══════════════════════════════════════════════════════════════════════════
// iframe 通讯
// ═══════════════════════════════════════════════════════════════════════════
function postToFrame(payload) {
const iframe = document.getElementById("xiaobaix-story-summary-iframe");
if (!iframe?.contentWindow || !frameReady) {
pendingFrameMessages.push(payload);
return;
}
postToIframe(iframe, payload, "LittleWhiteBox");
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
function flushPendingFrameMessages() {
if (!frameReady) return;
const iframe = document.getElementById("xiaobaix-story-summary-iframe");
if (!iframe?.contentWindow) return;
2026-01-27 16:04:57 +08:00
pendingFrameMessages.forEach((p) => postToIframe(iframe, p, "LittleWhiteBox"));
2026-01-26 01:16:35 +08:00
pendingFrameMessages = [];
2026-01-17 16:34:39 +08:00
}
2026-01-18 01:48:30 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-27 16:04:57 +08:00
// 向量功能UI 交互/状态
2026-01-18 01:48:30 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-26 01:16:35 +08:00
function sendVectorConfigToFrame() {
const cfg = getVectorConfig();
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_CONFIG", config: cfg });
2026-01-18 01:48:30 +08:00
}
2026-01-26 01:16:35 +08:00
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;
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
postToFrame({
2026-01-27 16:04:57 +08:00
type: "VECTOR_STATS",
2026-01-26 01:16:35 +08:00
stats: {
eventCount,
eventVectors: stats.eventVectors,
chunkCount: stats.chunkVectors,
builtFloors: chunkStatus.builtFloors,
totalFloors: chunkStatus.totalFloors,
totalMessages,
},
2026-01-27 16:04:57 +08:00
mismatch,
2026-01-17 16:34:39 +08:00
});
2026-01-26 01:16:35 +08:00
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
async function sendLocalModelStatusToFrame(modelId) {
if (!modelId) {
const cfg = getVectorConfig();
modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
}
const status = await checkLocalModelStatus(modelId);
postToFrame({
2026-01-27 16:04:57 +08:00
type: "VECTOR_LOCAL_MODEL_STATUS",
2026-01-26 01:16:35 +08:00
status: status.status,
2026-01-27 16:04:57 +08:00
message: status.message,
2026-01-17 16:34:39 +08:00
});
2026-01-26 01:16:35 +08:00
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
async function handleDownloadLocalModel(modelId) {
try {
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "downloading", message: "下载中..." });
2026-01-26 01:16:35 +08:00
await downloadLocalModel(modelId, (percent) => {
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_LOCAL_MODEL_PROGRESS", percent });
2026-01-26 01:16:35 +08:00
});
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "ready", message: "已就绪" });
2026-01-26 01:16:35 +08:00
} catch (e) {
2026-01-27 16:04:57 +08:00
if (e.message === "下载已取消") {
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "not_downloaded", message: "已取消" });
2026-01-17 16:34:39 +08:00
} else {
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "error", message: e.message });
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
}
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
function handleCancelDownload() {
cancelDownload();
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "not_downloaded", message: "已取消" });
2026-01-26 01:16:35 +08:00
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
async function handleDeleteLocalModel(modelId) {
try {
await deleteLocalModelCache(modelId);
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "not_downloaded", message: "未下载" });
2026-01-26 01:16:35 +08:00
} catch (e) {
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "error", message: e.message });
2026-01-26 01:16:35 +08:00
}
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
async function handleTestOnlineService(provider, config) {
try {
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "downloading", message: "连接中..." });
2026-01-26 01:16:35 +08:00
const result = await testOnlineService(provider, config);
postToFrame({
2026-01-27 16:04:57 +08:00
type: "VECTOR_ONLINE_STATUS",
status: "success",
message: `连接成功 (${result.dims}维)`,
2026-01-26 01:16:35 +08:00
});
} catch (e) {
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: e.message });
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
async function handleFetchOnlineModels(config) {
try {
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "downloading", message: "拉取中..." });
2026-01-26 01:16:35 +08:00
const models = await fetchOnlineModels(config);
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_ONLINE_MODELS", models });
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "success", message: `找到 ${models.length} 个模型` });
2026-01-26 01:16:35 +08:00
} catch (e) {
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: e.message });
2026-01-26 01:16:35 +08:00
}
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
async function handleGenerateVectors(vectorCfg) {
if (vectorGenerating) return;
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
if (!vectorCfg?.enabled) {
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: -1, total: 0 });
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: -1, total: 0 });
2026-01-26 01:16:35 +08:00
return;
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
const { chatId, chat } = getContext();
if (!chatId || !chat?.length) return;
2026-01-17 16:34:39 +08:00
2026-01-27 16:04:57 +08:00
if (vectorCfg.engine === "online") {
2026-01-26 01:16:35 +08:00
if (!vectorCfg.online?.key || !vectorCfg.online?.model) {
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: "请配置在线服务 API" });
2026-01-26 01:16:35 +08:00
return;
}
}
2026-01-17 16:34:39 +08:00
2026-01-27 16:04:57 +08:00
if (vectorCfg.engine === "local") {
2026-01-26 01:16:35 +08:00
const modelId = vectorCfg.local?.modelId || DEFAULT_LOCAL_MODEL;
const status = await checkLocalModelStatus(modelId);
2026-01-27 16:04:57 +08:00
if (status.status !== "ready") {
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "downloading", message: "正在加载模型..." });
2026-01-26 01:16:35 +08:00
try {
await downloadLocalModel(modelId, (percent) => {
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_LOCAL_MODEL_PROGRESS", percent });
2026-01-26 01:16:35 +08:00
});
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "ready", message: "已就绪" });
2026-01-26 01:16:35 +08:00
} catch (e) {
2026-01-27 16:04:57 +08:00
xbLog.error(MODULE_ID, "模型加载失败", e);
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "error", message: e.message });
2026-01-26 01:16:35 +08:00
return;
2026-01-17 16:34:39 +08:00
}
}
}
2026-01-26 01:16:35 +08:00
vectorGenerating = true;
vectorCancelled = false;
2026-01-29 01:17:37 +08:00
vectorAbortController?.abort?.();
vectorAbortController = new AbortController();
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
const fingerprint = getEngineFingerprint(vectorCfg);
2026-01-27 16:04:57 +08:00
const isLocal = vectorCfg.engine === "local";
2026-01-29 17:02:51 +08:00
const batchSize = isLocal ? 5 : 25;
2026-01-26 01:16:35 +08:00
const concurrency = isLocal ? 1 : 2;
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
await clearAllChunks(chatId);
await updateMeta(chatId, { lastChunkFloor: -1, fingerprint });
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
const allChunks = [];
for (let floor = 0; floor < chat.length; floor++) {
const chunks = chunkMessage(floor, chat[floor]);
allChunks.push(...chunks);
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
if (allChunks.length > 0) {
await saveChunks(chatId, allChunks);
2026-01-17 16:34:39 +08:00
}
2026-01-27 16:04:57 +08:00
const l1Texts = allChunks.map((c) => c.text);
2026-01-26 01:16:35 +08:00
const l1Batches = [];
for (let i = 0; i < l1Texts.length; i += batchSize) {
l1Batches.push({
2026-01-27 16:04:57 +08:00
phase: "L1",
2026-01-26 01:16:35 +08:00
texts: l1Texts.slice(i, i + batchSize),
startIdx: i,
});
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
const store = getSummaryStore();
const events = store?.json?.events || [];
2026-01-17 16:34:39 +08:00
2026-01-29 17:02:51 +08:00
// L2: 全量重建(先清空再重建,保持与 L1 一致性)
await clearEventVectors(chatId);
2026-01-26 01:16:35 +08:00
const l2Pairs = events
2026-01-27 16:04:57 +08:00
.map((e) => ({ id: e.id, text: `${e.title || ""} ${e.summary || ""}`.trim() }))
.filter((p) => p.text);
2026-01-26 01:16:35 +08:00
const l2Batches = [];
for (let i = 0; i < l2Pairs.length; i += batchSize) {
const batch = l2Pairs.slice(i, i + batchSize);
l2Batches.push({
2026-01-27 16:04:57 +08:00
phase: "L2",
texts: batch.map((p) => p.text),
ids: batch.map((p) => p.id),
2026-01-26 01:16:35 +08:00
startIdx: i,
2026-01-17 16:34:39 +08:00
});
}
2026-01-26 01:16:35 +08:00
const l1Total = allChunks.length;
const l2Total = events.length;
let l1Completed = 0;
2026-01-29 17:02:51 +08:00
let l2Completed = 0;
2026-01-17 16:34:39 +08:00
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: 0, total: l1Total });
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: l2Completed, total: l2Total });
2026-01-17 16:34:39 +08:00
2026-01-29 01:17:37 +08:00
let rateLimitWarned = false;
2026-01-26 01:16:35 +08:00
const allTasks = [...l1Batches, ...l2Batches];
const l1Vectors = new Array(l1Texts.length);
const l2VectorItems = [];
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
let taskIndex = 0;
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
async function worker() {
while (taskIndex < allTasks.length) {
if (vectorCancelled) break;
2026-01-29 01:17:37 +08:00
if (vectorAbortController?.signal?.aborted) break;
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
const i = taskIndex++;
if (i >= allTasks.length) break;
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
const task = allTasks[i];
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
try {
2026-01-29 01:17:37 +08:00
const vectors = await embed(task.texts, vectorCfg, { signal: vectorAbortController.signal });
2026-01-17 16:34:39 +08:00
2026-01-27 16:04:57 +08:00
if (task.phase === "L1") {
2026-01-26 01:16:35 +08:00
for (let j = 0; j < vectors.length; j++) {
l1Vectors[task.startIdx + j] = vectors[j];
}
l1Completed += task.texts.length;
postToFrame({
2026-01-27 16:04:57 +08:00
type: "VECTOR_GEN_PROGRESS",
phase: "L1",
2026-01-26 01:16:35 +08:00
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({
2026-01-27 16:04:57 +08:00
type: "VECTOR_GEN_PROGRESS",
phase: "L2",
2026-01-26 01:16:35 +08:00
current: Math.min(l2Completed, l2Total),
total: l2Total,
});
}
} catch (e) {
2026-01-29 01:17:37 +08:00
if (e?.name === "AbortError") {
xbLog.warn(MODULE_ID, "向量生成已取消AbortError");
break;
}
2026-01-26 01:16:35 +08:00
xbLog.error(MODULE_ID, `${task.phase} batch 向量化失败`, e);
2026-01-29 01:17:37 +08:00
const msg = String(e?.message || e);
const isRateLike = /429|403|rate|limit|quota/i.test(msg);
if (isRateLike && !rateLimitWarned) {
rateLimitWarned = true;
executeSlashCommand("/echo severity=warning 向量生成遇到速率/配额限制,已进入自动重试。");
}
vectorCancelled = true;
vectorAbortController?.abort?.();
break;
2026-01-26 01:16:35 +08:00
}
}
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
await Promise.all(
Array(Math.min(concurrency, allTasks.length))
.fill(null)
.map(() => worker())
);
2026-01-17 16:34:39 +08:00
2026-01-29 01:17:37 +08:00
if (vectorCancelled || vectorAbortController?.signal?.aborted) {
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: -1, total: 0 });
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: -1, total: 0 });
vectorGenerating = false;
return;
}
2026-01-26 01:16:35 +08:00
if (allChunks.length > 0 && l1Vectors.filter(Boolean).length > 0) {
const chunkVectorItems = allChunks
2026-01-27 16:04:57 +08:00
.map((chunk, idx) => (l1Vectors[idx] ? { chunkId: chunk.chunkId, vector: l1Vectors[idx] } : null))
2026-01-26 01:16:35 +08:00
.filter(Boolean);
await saveChunkVectors(chatId, chunkVectorItems, fingerprint);
await updateMeta(chatId, { lastChunkFloor: chat.length - 1 });
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
if (l2VectorItems.length > 0) {
await saveEventVectorsToDb(chatId, l2VectorItems, fingerprint);
}
2026-01-17 16:34:39 +08:00
2026-01-29 17:02:51 +08:00
// 更新 fingerprint无论之前是否匹配
await updateMeta(chatId, { fingerprint });
2026-01-27 16:04:57 +08:00
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: -1, total: 0 });
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: -1, total: 0 });
2026-01-26 01:16:35 +08:00
await sendVectorStatsToFrame();
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
vectorGenerating = false;
vectorCancelled = false;
2026-01-29 01:17:37 +08:00
vectorAbortController = null;
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
xbLog.info(MODULE_ID, `向量生成完成: L1=${l1Vectors.filter(Boolean).length}, L2=${l2VectorItems.length}`);
}
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
// L2 自动增量向量化(总结完成后调用)
// ═══════════════════════════════════════════════════════════════════════════
async function autoVectorizeNewEvents(newEventIds) {
if (!newEventIds?.length) return;
const vectorCfg = getVectorConfig();
if (!vectorCfg?.enabled) return;
const { chatId } = getContext();
if (!chatId) return;
// 本地模型未加载时跳过(不阻塞总结流程)
if (vectorCfg.engine === "local") {
const modelId = vectorCfg.local?.modelId || DEFAULT_LOCAL_MODEL;
if (!isLocalModelLoaded(modelId)) {
xbLog.warn(MODULE_ID, "L2 自动向量化跳过:本地模型未加载");
return;
}
}
const store = getSummaryStore();
const events = store?.json?.events || [];
const newEventIdSet = new Set(newEventIds);
// 只取本次新增的 events
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 = vectorCfg.engine === "local" ? 5 : 25;
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));
// 找出被删除的 eventIds
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();
}
}
2026-01-29 17:02:51 +08:00
// ═══════════════════════════════════════════════════════════════════════════
// 向量完整性检测(仅提醒,不自动操作)
// ═══════════════════════════════════════════════════════════════════════════
async function checkVectorIntegrityAndWarn() {
const vectorCfg = getVectorConfig();
if (!vectorCfg?.enabled) return;
// 节流2分钟内不重复提醒
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('向量引擎/模型已变更');
}
// L1 不完整
const chunkFloorGap = totalFloors - 1 - (meta.lastChunkFloor ?? -1);
if (chunkFloorGap > 0) {
issues.push(`${chunkFloorGap} 层片段未向量化`);
}
// L2 不完整
const eventVectorGap = totalEvents - stats.eventVectors;
if (eventVectorGap > 0) {
issues.push(`${eventVectorGap} 个事件未向量化`);
}
if (issues.length > 0) {
lastVectorWarningAt = now;
await executeSlashCommand(`/echo severity=warning 向量数据不完整:${issues.join('、')}。请打开剧情总结面板点击"生成向量"。`);
}
}
2026-01-26 01:16:35 +08:00
async function handleClearVectors() {
const { chatId } = getContext();
if (!chatId) return;
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
await clearEventVectors(chatId);
await clearAllChunks(chatId);
await updateMeta(chatId, { lastChunkFloor: -1 });
await sendVectorStatsToFrame();
await executeSlashCommand('/echo severity=info 向量数据已清除。如需恢复召回功能,请重新点击"生成向量"。');
2026-01-27 16:04:57 +08:00
xbLog.info(MODULE_ID, "向量数据已清除");
2026-01-26 01:16:35 +08:00
}
2026-01-17 16:34:39 +08:00
2026-01-27 16:04:57 +08:00
async function maybeAutoBuildChunks() {
2026-01-26 01:16:35 +08:00
const cfg = getVectorConfig();
2026-01-27 16:04:57 +08:00
if (!cfg?.enabled) return;
2026-01-17 16:34:39 +08:00
2026-01-27 16:04:57 +08:00
const { chat, chatId } = getContext();
if (!chatId || !chat?.length) return;
2026-01-17 16:34:39 +08:00
2026-01-27 16:04:57 +08:00
const status = await getChunkBuildStatus();
if (status.pending <= 0) return;
2026-01-17 16:34:39 +08:00
2026-01-27 16:04:57 +08:00
if (cfg.engine === "local") {
2026-01-26 01:16:35 +08:00
const modelId = cfg.local?.modelId || DEFAULT_LOCAL_MODEL;
2026-01-27 16:04:57 +08:00
if (!isLocalModelLoaded(modelId)) return;
2026-01-26 01:16:35 +08:00
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
try {
2026-01-27 16:04:57 +08:00
await buildIncrementalChunks({ vectorConfig: cfg });
2026-01-26 01:16:35 +08:00
} catch (e) {
2026-01-27 16:04:57 +08:00
xbLog.error(MODULE_ID, "自动 L1 构建失败", e);
2026-01-17 16:34:39 +08:00
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 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);
2026-01-27 16:04:57 +08:00
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]);
2026-01-26 01:16:35 +08:00
window.addEventListener(MESSAGE_EVENT, handleFrameMessage);
2026-01-17 16:34:39 +08:00
}
function showOverlay() {
if (!overlayCreated) createOverlay();
$("#xiaobaix-story-summary-overlay").show();
}
function hideOverlay() {
$("#xiaobaix-story-summary-overlay").hide();
}
// ═══════════════════════════════════════════════════════════════════════════
// 楼层按钮
// ═══════════════════════════════════════════════════════════════════════════
function createSummaryBtn(mesId) {
2026-01-27 16:04:57 +08:00
const btn = document.createElement("div");
btn.className = "mes_btn xiaobaix-story-summary-btn";
btn.title = "剧情总结";
2026-01-17 16:34:39 +08:00
btn.dataset.mesid = mesId;
btn.innerHTML = '<i class="fa-solid fa-chart-line"></i>';
2026-01-27 16:04:57 +08:00
btn.addEventListener("click", (e) => {
2026-01-17 16:34:39 +08:00
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}"]`);
2026-01-27 16:04:57 +08:00
if (!msg || msg.querySelector(".xiaobaix-story-summary-btn")) return;
2026-01-17 16:34:39 +08:00
const btn = createSummaryBtn(mesId);
if (window.registerButtonToSubContainer?.(mesId, btn)) return;
2026-01-27 16:04:57 +08:00
msg.querySelector(".flex-container.flex1.alignitemscenter")?.appendChild(btn);
2026-01-17 16:34:39 +08:00
}
function initButtonsForAll() {
if (!getSettings().storySummary?.enabled) return;
$("#chat .mes").each((_, el) => {
const mesId = el.getAttribute("mesid");
if (mesId != null) addSummaryBtnToMessage(mesId);
});
}
// ═══════════════════════════════════════════════════════════════════════════
2026-01-26 01:16:35 +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 });
}
} catch (e) {
2026-01-27 16:04:57 +08:00
xbLog.warn(MODULE_ID, "加载面板配置失败", e);
2026-01-17 16:34:39 +08:00
}
}
2026-01-27 22:51:44 +08:00
async function sendFrameBaseData(store, totalFloors) {
const boundary = await getHideBoundaryFloor(store);
const range = calcHideRange(boundary);
const hiddenCount = (store?.hideSummarizedHistory && range) ? (range.end + 1) : 0;
2026-01-17 16:34:39 +08:00
2026-01-27 22:51:44 +08:00
const lastSummarized = store?.lastSummarizedMesId ?? -1;
2026-01-17 16:34:39 +08:00
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 || [],
2026-01-26 01:16:35 +08:00
world: store.json.world || [],
2026-01-17 16:34:39 +08:00
lastSummarizedMesId: lastSummarized,
},
});
2026-01-26 01:16:35 +08:00
} else {
postToFrame({ type: "SUMMARY_CLEARED", payload: { totalFloors } });
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
function openPanelForMessage(mesId) {
createOverlay();
showOverlay();
2026-01-27 16:04:57 +08:00
2026-01-26 01:16:35 +08:00
const { chat } = getContext();
const store = getSummaryStore();
const totalFloors = chat.length;
2026-01-27 16:04:57 +08:00
2026-01-29 01:17:37 +08:00
sendFrameBaseData(store, totalFloors);
2026-01-26 01:16:35 +08:00
sendFrameFullData(store, totalFloors);
setSummaryGenerating(summaryGenerating);
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
sendVectorConfigToFrame();
sendVectorStatsToFrame();
2026-01-27 16:04:57 +08:00
2026-01-26 01:16:35 +08:00
const cfg = getVectorConfig();
const modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
sendLocalModelStatusToFrame(modelId);
}
2026-01-17 16:34:39 +08:00
2026-01-27 22:51:44 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-29 01:17:37 +08:00
// Hide/Unhide
// - 非向量boundary = lastSummarizedMesId
// - 向量boundary = meta.lastChunkFloor若为 -1 则回退到 lastSummarizedMesId
2026-01-27 22:51:44 +08:00
// ═══════════════════════════════════════════════════════════════════════════
async function getHideBoundaryFloor(store) {
// 没有总结时,不隐藏
if (store?.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) {
return -1;
}
2026-01-27 22:51:44 +08:00
const vectorCfg = getVectorConfig();
if (!vectorCfg?.enabled) {
return store?.lastSummarizedMesId ?? -1;
}
const { chatId } = getContext();
2026-01-29 01:17:37 +08:00
if (!chatId) return store?.lastSummarizedMesId ?? -1;
2026-01-27 22:51:44 +08:00
const meta = await getMeta(chatId);
2026-01-29 01:17:37 +08:00
const v = meta?.lastChunkFloor ?? -1;
if (v >= 0) return v;
return store?.lastSummarizedMesId ?? -1;
2026-01-27 22:51:44 +08:00
}
async function applyHideState() {
const store = getSummaryStore();
if (!store?.hideSummarizedHistory) return;
2026-01-29 01:17:37 +08:00
// 先全量 unhide杜绝历史残留
await unhideAllMessages();
2026-01-27 22:51:44 +08:00
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() {
2026-01-29 01:17:37 +08:00
// 暴力全量 unhide确保立刻恢复
await unhideAllMessages();
2026-01-27 22:51:44 +08:00
}
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-29 01:17:37 +08:00
// 自动总结
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
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 || {};
2026-01-27 16:04:57 +08:00
if (trig.timing === "manual") return;
2026-01-17 16:34:39 +08:00
if (!trig.enabled) return;
2026-01-27 16:04:57 +08:00
if (trig.timing === "after_ai" && reason !== "after_ai") return;
if (trig.timing === "before_user" && reason !== "before_user") return;
2026-01-17 16:34:39 +08:00
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) {
2026-01-26 01:16:35 +08:00
setSummaryGenerating(true);
2026-01-17 16:34:39 +08:00
for (let attempt = 1; attempt <= 3; attempt++) {
2026-01-26 01:16:35 +08:00
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 }) => {
2026-01-26 01:16:35 +08:00
postToFrame({
type: "SUMMARY_FULL_DATA",
payload: {
keywords: merged.keywords || [],
events: merged.events || [],
characters: merged.characters || { main: [], relationships: [] },
arcs: merged.arcs || [],
2026-01-27 16:04:57 +08:00
world: merged.world || [],
2026-01-26 01:16:35 +08:00
lastSummarizedMesId: endMesId,
},
});
2026-01-29 01:17:37 +08:00
applyHideStateDebounced();
2026-01-26 01:16:35 +08:00
updateFrameStatsAfterSummary(endMesId, merged);
// L2 自动增量向量化
await autoVectorizeNewEvents(newEventIds);
2026-01-26 01:16:35 +08:00
},
});
if (result.success) {
setSummaryGenerating(false);
return;
}
2026-01-17 16:34:39 +08:00
if (attempt < 3) await sleep(1000);
}
2026-01-26 01:16:35 +08:00
setSummaryGenerating(false);
2026-01-27 16:04:57 +08:00
await executeSlashCommand("/echo severity=error 剧情总结失败(已自动重试 3 次)。请稍后再试。");
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
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,
},
});
}
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-26 01:16:35 +08:00
// iframe 消息处理
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-26 01:16:35 +08:00
function handleFrameMessage(event) {
const iframe = document.getElementById("xiaobaix-story-summary-iframe");
if (!isTrustedMessage(event, iframe, "LittleWhiteBox-StoryFrame")) return;
2026-01-27 16:04:57 +08:00
2026-01-26 01:16:35 +08:00
const data = event.data;
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
switch (data.type) {
2026-01-27 16:04:57 +08:00
case "FRAME_READY": {
2026-01-26 01:16:35 +08:00
frameReady = true;
flushPendingFrameMessages();
setSummaryGenerating(summaryGenerating);
sendSavedConfigToFrame();
sendVectorConfigToFrame();
sendVectorStatsToFrame();
2026-01-27 16:04:57 +08:00
const cfg = getVectorConfig();
const modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
sendLocalModelStatusToFrame(modelId);
2026-01-26 01:16:35 +08:00
break;
2026-01-27 16:04:57 +08:00
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
case "SETTINGS_OPENED":
case "FULLSCREEN_OPENED":
case "EDITOR_OPENED":
$(".xb-ss-close-btn").hide();
break;
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
case "SETTINGS_CLOSED":
case "FULLSCREEN_CLOSED":
case "EDITOR_CLOSED":
$(".xb-ss-close-btn").show();
break;
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
case "REQUEST_GENERATE": {
const ctx = getContext();
currentMesId = (ctx.chat?.length ?? 1) - 1;
handleManualGenerate(currentMesId, data.config || {});
break;
}
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
case "REQUEST_CANCEL":
2026-01-27 16:04:57 +08:00
window.xiaobaixStreamingGeneration?.cancel?.("xb9");
2026-01-26 01:16:35 +08:00
setSummaryGenerating(false);
postToFrame({ type: "SUMMARY_STATUS", statusText: "已停止" });
break;
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
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;
2026-01-29 01:17:37 +08:00
try { vectorAbortController?.abort?.(); } catch {}
2026-01-26 01:16:35 +08:00
break;
2026-01-29 17:02:51 +08:00
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();
break;
2026-01-26 01:16:35 +08:00
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;
2026-01-26 01:16:35 +08:00
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);
}
2026-01-26 01:16:35 +08:00
break;
}
case "TOGGLE_HIDE_SUMMARIZED": {
const store = getSummaryStore();
if (!store) break;
2026-01-27 16:04:57 +08:00
2026-01-26 01:16:35 +08:00
store.hideSummarizedHistory = !!data.enabled;
saveSummaryStore();
2026-01-27 16:04:57 +08:00
2026-01-27 22:51:44 +08:00
(async () => {
if (data.enabled) {
await applyHideState();
} else {
await clearHideState();
}
})();
2026-01-26 01:16:35 +08:00
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();
2026-01-27 22:51:44 +08:00
(async () => {
if (store.hideSummarizedHistory) {
await applyHideState();
}
2026-01-26 01:16:35 +08:00
const { chat } = getContext();
2026-01-27 22:51:44 +08:00
await sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0);
})();
2026-01-26 01:16:35 +08:00
break;
}
case "SAVE_PANEL_CONFIG":
if (data.config) {
CommonSettingStorage.set(SUMMARY_CONFIG_KEY, data.config);
}
break;
case "REQUEST_PANEL_CONFIG":
sendSavedConfigToFrame();
break;
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
}
2026-01-17 16:34:39 +08:00
2026-01-27 16:04:57 +08:00
// ═══════════════════════════════════════════════════════════════════════════
// 手动总结
// ═══════════════════════════════════════════════════════════════════════════
2026-01-26 01:16:35 +08:00
async function handleManualGenerate(mesId, config) {
if (isSummaryGenerating()) {
postToFrame({ type: "SUMMARY_STATUS", statusText: "上一轮总结仍在进行中..." });
2026-01-17 16:34:39 +08:00
return;
}
2026-01-26 01:16:35 +08:00
setSummaryGenerating(true);
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
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 }) => {
2026-01-26 01:16:35 +08:00
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,
},
});
2026-01-27 22:51:44 +08:00
applyHideStateDebounced();
2026-01-26 01:16:35 +08:00
updateFrameStatsAfterSummary(endMesId, merged);
// L2 自动增量向量化
await autoVectorizeNewEvents(newEventIds);
2026-01-26 01:16:35 +08:00
},
});
2026-01-17 16:34:39 +08:00
2026-01-26 01:16:35 +08:00
setSummaryGenerating(false);
2026-01-17 16:34:39 +08:00
}
// ═══════════════════════════════════════════════════════════════════════════
2026-01-29 01:17:37 +08:00
// 消息事件
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-26 01:16:35 +08:00
async function handleChatChanged() {
2026-01-17 16:34:39 +08:00
const { chat } = getContext();
const newLength = Array.isArray(chat) ? chat.length : 0;
2026-01-26 01:16:35 +08:00
await rollbackSummaryIfNeeded();
2026-01-17 16:34:39 +08:00
initButtonsForAll();
const store = getSummaryStore();
2026-01-29 01:17:37 +08:00
if (store?.hideSummarizedHistory) {
await applyHideState();
}
2026-01-17 16:34:39 +08:00
if (frameReady) {
2026-01-27 22:51:44 +08:00
await sendFrameBaseData(store, newLength);
2026-01-17 16:34:39 +08:00
sendFrameFullData(store, newLength);
}
2026-01-29 17:02:51 +08:00
// 检测向量完整性并提醒(仅提醒,不自动操作)
setTimeout(() => checkVectorIntegrityAndWarn(), 2000);
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
async function handleMessageDeleted() {
const { chat, chatId } = getContext();
const newLength = chat?.length || 0;
await rollbackSummaryIfNeeded();
await syncOnMessageDeleted(chatId, newLength);
2026-01-29 01:17:37 +08:00
applyHideStateDebounced();
2026-01-26 01:16:35 +08:00
}
async function handleMessageSwiped() {
const { chat, chatId } = getContext();
const lastFloor = (chat?.length || 1) - 1;
await syncOnMessageSwiped(chatId, lastFloor);
initButtonsForAll();
2026-01-29 01:17:37 +08:00
applyHideStateDebounced();
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
async function handleMessageReceived() {
const { chat, chatId } = getContext();
const lastFloor = (chat?.length || 1) - 1;
const message = chat?.[lastFloor];
const vectorConfig = getVectorConfig();
2026-01-17 16:34:39 +08:00
initButtonsForAll();
2026-01-26 01:16:35 +08:00
// 向量全量生成中时跳过 L1 sync避免竞争写入
if (vectorGenerating) return;
2026-01-26 01:16:35 +08:00
await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig);
await maybeAutoBuildChunks();
2026-01-27 22:51:44 +08:00
applyHideStateDebounced();
2026-01-27 16:04:57 +08:00
setTimeout(() => maybeAutoRunSummary("after_ai"), 1000);
2026-01-17 16:34:39 +08:00
}
function handleMessageSent() {
initButtonsForAll();
2026-01-27 16:04:57 +08:00
setTimeout(() => maybeAutoRunSummary("before_user"), 1000);
2026-01-17 16:34:39 +08:00
}
2026-01-26 01:16:35 +08:00
async function handleMessageUpdated() {
await rollbackSummaryIfNeeded();
2026-01-17 16:34:39 +08:00
initButtonsForAll();
2026-01-29 01:17:37 +08:00
applyHideStateDebounced();
2026-01-17 16:34:39 +08:00
}
function handleMessageRendered(data) {
const mesId = data?.element ? $(data.element).attr("mesid") : data?.messageId;
2026-01-27 16:04:57 +08:00
if (mesId != null) addSummaryBtnToMessage(mesId);
else initButtonsForAll();
2026-01-17 16:34:39 +08:00
}
2026-01-27 16:04:57 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-29 01:17:37 +08:00
// 用户消息缓存(供向量召回使用)
// ═══════════════════════════════════════════════════════════════════════════
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];
}
2026-01-29 01:17:37 +08:00
// ═══════════════════════════════════════════════════════════════════════════
// Prompt 注入
2026-01-27 16:04:57 +08:00
// ═══════════════════════════════════════════════════════════════════════════
2026-01-26 01:16:35 +08:00
async function handleGenerationStarted(type, _params, isDryRun) {
if (isDryRun) return;
2026-01-27 16:04:57 +08:00
if (!getSettings().storySummary?.enabled) return;
const excludeLastAi = type === "swipe" || type === "regenerate";
const vectorCfg = getVectorConfig();
clearExtensionPrompt();
2026-01-29 01:17:37 +08:00
// ★ 保留判断是否使用缓存的用户消息30秒内有效
2026-01-29 01:17:37 +08:00
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;
2026-01-29 01:17:37 +08:00
const store = getSummaryStore();
2026-01-29 01:17:37 +08:00
// 1) boundary
// - 向量开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;
2026-01-29 01:17:37 +08:00
}
if (boundary < 0) return;
2026-01-29 01:17:37 +08:00
// 2) depth倒序插入从末尾往前数
// 最小为 1避免插入到最底部导致 AI 看到的最后是总结
const depth = Math.max(1, chatLen - boundary - 1);
if (depth < 0) return;
2026-01-29 01:17:37 +08:00
// 3) 构建注入文本(保持原逻辑)
let text = "";
if (vectorCfg?.enabled) {
const r = await buildVectorPromptText(excludeLastAi, {
postToFrame,
echo: executeSlashCommand,
pendingUserMessage,
2026-01-29 01:17:37 +08:00
});
text = r?.text || "";
} else {
text = buildNonVectorPromptText() || "";
2026-01-29 01:17:37 +08:00
}
if (!text.trim()) return;
// 4) 写入 extension_prompts
2026-01-29 17:45:20 +08:00
// 获取用户配置的 role
const cfg = getSummaryPanelConfig();
const roleKey = cfg.trigger?.role || 'system';
const role = ROLE_MAP[roleKey] || extension_prompt_roles.SYSTEM;
extension_prompts[EXT_PROMPT_KEY] = {
value: text,
position: extension_prompt_types.IN_CHAT,
depth,
2026-01-29 17:45:20 +08:00
role,
};
2026-01-26 01:16:35 +08:00
}
2026-01-17 16:34:39 +08:00
// ═══════════════════════════════════════════════════════════════════════════
// 事件注册
// ═══════════════════════════════════════════════════════════════════════════
function registerEvents() {
if (eventsRegistered) return;
eventsRegistered = true;
CacheRegistry.register(MODULE_ID, {
2026-01-27 16:04:57 +08:00
name: "待发送消息队列",
2026-01-17 16:34:39 +08:00
getSize: () => pendingFrameMessages.length,
getBytes: () => {
2026-01-27 16:04:57 +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();
2026-01-26 01:16:35 +08:00
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));
2026-01-29 01:17:37 +08:00
eventSource.on(event_types.MESSAGE_SENT, handleMessageSentForRecall);
2026-01-26 01:16:35 +08:00
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));
2026-01-27 16:04:57 +08:00
eventSource.on(event_types.USER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));
2026-01-29 01:17:37 +08:00
// 注入链路
2026-01-26 01:16:35 +08:00
eventSource.on(event_types.GENERATION_STARTED, handleGenerationStarted);
eventSource.on(event_types.GENERATION_STOPPED, clearExtensionPrompt);
eventSource.on(event_types.GENERATION_ENDED, clearExtensionPrompt);
2026-01-17 16:34:39 +08:00
}
function unregisterEvents() {
CacheRegistry.unregister(MODULE_ID);
eventsRegistered = false;
2026-01-27 16:04:57 +08:00
2026-01-17 16:34:39 +08:00
$(".xiaobaix-story-summary-btn").remove();
hideOverlay();
2026-01-27 16:04:57 +08:00
clearExtensionPrompt();
2026-01-17 16:34:39 +08:00
}
// ═══════════════════════════════════════════════════════════════════════════
// Toggle 监听
// ═══════════════════════════════════════════════════════════════════════════
$(document).on("xiaobaix:storySummary:toggle", (_e, enabled) => {
if (enabled) {
registerEvents();
initButtonsForAll();
} else {
unregisterEvents();
}
});
// ═══════════════════════════════════════════════════════════════════════════
// 初始化
// ═══════════════════════════════════════════════════════════════════════════
jQuery(() => {
2026-01-29 01:17:37 +08:00
if (!getSettings().storySummary?.enabled) return;
2026-01-17 16:34:39 +08:00
registerEvents();
});