Improve vector recall error handling

This commit is contained in:
2026-01-27 16:04:57 +08:00
parent a010681ea6
commit 4043e120ae
3 changed files with 1452 additions and 1686 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,9 @@
// ═══════════════════════════════════════════════════════════════════════════
// Story Summary - 主入口
// UI 交互、事件监听、iframe 通讯
// Story Summary - 主入口(干净版)
// - 注入只在 GENERATION_STARTED 发生
// - 向量关闭注入全量总结L3+L2+Arcs
// - 向量开启:召回 + 1万预算装配注入
// - 删除所有 updateSummaryExtensionPrompt() 调用,避免覆盖/残留/竞态
// ═══════════════════════════════════════════════════════════════════════════
import { getContext } from "../../../../../extensions.js";
@@ -10,7 +13,7 @@ import { xbLog, CacheRegistry } from "../../core/debug-core.js";
import { postToIframe, isTrustedMessage } from "../../core/iframe-messaging.js";
import { CommonSettingStorage } from "../../core/server-storage.js";
// 拆分模块
// config/store
import { getSettings, getSummaryPanelConfig, getVectorConfig, saveVectorConfig } from "./data/config.js";
import {
getSummaryStore,
@@ -20,15 +23,17 @@ import {
clearSummaryData,
} from "./data/store.js";
// prompt injection (ONLY on generation started)
import {
recallAndInjectPrompt,
updateSummaryExtensionPrompt,
clearSummaryExtensionPrompt,
injectNonVectorPrompt,
} from "./generate/prompt.js";
// summary generation
import { runSummaryGeneration } from "./generate/generator.js";
// 向量服务
// vector service
import {
embed,
getEngineFingerprint,
@@ -68,11 +73,11 @@ import {
// 常量
// ═══════════════════════════════════════════════════════════════════════════
const MODULE_ID = 'storySummary';
const SUMMARY_CONFIG_KEY = 'storySummaryPanelConfig';
const MODULE_ID = "storySummary";
const SUMMARY_CONFIG_KEY = "storySummaryPanelConfig";
const iframePath = `${extensionFolderPath}/modules/story-summary/story-summary.html`;
const VALID_SECTIONS = ['keywords', 'events', 'characters', 'arcs', 'world'];
const MESSAGE_EVENT = 'message';
const VALID_SECTIONS = ["keywords", "events", "characters", "arcs", "world"];
const MESSAGE_EVENT = "message";
// ═══════════════════════════════════════════════════════════════════════════
// 状态变量
@@ -87,20 +92,22 @@ let eventsRegistered = false;
let vectorGenerating = false;
let vectorCancelled = false;
// ═══════════════════════════════════════════════════════════════════════════
// 工具函数
// ═══════════════════════════════════════════════════════════════════════════
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const sleep = ms => new Promise(r => setTimeout(r, ms));
// ═══════════════════════════════════════════════════════════════════════════
// 工具:执行斜杠命令
// ═══════════════════════════════════════════════════════════════════════════
async function executeSlashCommand(command) {
try {
const executeCmd = window.executeSlashCommands
|| window.executeSlashCommandsOnChatInput
|| (typeof SillyTavern !== 'undefined' && SillyTavern.getContext()?.executeSlashCommands);
const executeCmd =
window.executeSlashCommands ||
window.executeSlashCommandsOnChatInput ||
(typeof SillyTavern !== "undefined" && SillyTavern.getContext()?.executeSlashCommands);
if (executeCmd) {
await executeCmd(command);
} else if (typeof window.STscript === 'function') {
} else if (typeof window.STscript === "function") {
await window.STscript(command);
}
} catch (e) {
@@ -138,17 +145,17 @@ function flushPendingFrameMessages() {
if (!frameReady) return;
const iframe = document.getElementById("xiaobaix-story-summary-iframe");
if (!iframe?.contentWindow) return;
pendingFrameMessages.forEach(p => postToIframe(iframe, p, "LittleWhiteBox"));
pendingFrameMessages.forEach((p) => postToIframe(iframe, p, "LittleWhiteBox"));
pendingFrameMessages = [];
}
// ═══════════════════════════════════════════════════════════════════════════
// 向量功能
// 向量功能UI 交互/状态
// ═══════════════════════════════════════════════════════════════════════════
function sendVectorConfigToFrame() {
const cfg = getVectorConfig();
postToFrame({ type: 'VECTOR_CONFIG', config: cfg });
postToFrame({ type: "VECTOR_CONFIG", config: cfg });
}
async function sendVectorStatsToFrame() {
@@ -170,7 +177,7 @@ async function sendVectorStatsToFrame() {
}
postToFrame({
type: 'VECTOR_STATS',
type: "VECTOR_STATS",
stats: {
eventCount,
eventVectors: stats.eventVectors,
@@ -179,7 +186,7 @@ async function sendVectorStatsToFrame() {
totalFloors: chunkStatus.totalFloors,
totalMessages,
},
mismatch
mismatch,
});
}
@@ -190,66 +197,66 @@ async function sendLocalModelStatusToFrame(modelId) {
}
const status = await checkLocalModelStatus(modelId);
postToFrame({
type: 'VECTOR_LOCAL_MODEL_STATUS',
type: "VECTOR_LOCAL_MODEL_STATUS",
status: status.status,
message: status.message
message: status.message,
});
}
async function handleDownloadLocalModel(modelId) {
try {
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'downloading', message: '下载中...' });
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "downloading", message: "下载中..." });
await downloadLocalModel(modelId, (percent) => {
postToFrame({ type: 'VECTOR_LOCAL_MODEL_PROGRESS', percent });
postToFrame({ type: "VECTOR_LOCAL_MODEL_PROGRESS", percent });
});
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'ready', message: '已就绪' });
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "ready", message: "已就绪" });
} catch (e) {
if (e.message === '下载已取消') {
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'not_downloaded', message: '已取消' });
if (e.message === "下载已取消") {
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "not_downloaded", message: "已取消" });
} else {
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'error', message: e.message });
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "error", message: e.message });
}
}
}
function handleCancelDownload() {
cancelDownload();
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'not_downloaded', message: '已取消' });
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "not_downloaded", message: "已取消" });
}
async function handleDeleteLocalModel(modelId) {
try {
await deleteLocalModelCache(modelId);
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'not_downloaded', message: '未下载' });
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "not_downloaded", message: "未下载" });
} catch (e) {
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'error', message: e.message });
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "error", message: e.message });
}
}
async function handleTestOnlineService(provider, config) {
try {
postToFrame({ type: 'VECTOR_ONLINE_STATUS', status: 'downloading', message: '连接中...' });
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "downloading", message: "连接中..." });
const result = await testOnlineService(provider, config);
postToFrame({
type: 'VECTOR_ONLINE_STATUS',
status: 'success',
message: `连接成功 (${result.dims}维)`
type: "VECTOR_ONLINE_STATUS",
status: "success",
message: `连接成功 (${result.dims}维)`,
});
} catch (e) {
postToFrame({ type: 'VECTOR_ONLINE_STATUS', status: 'error', message: e.message });
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: e.message });
}
}
async function handleFetchOnlineModels(config) {
try {
postToFrame({ type: 'VECTOR_ONLINE_STATUS', status: 'downloading', message: '拉取中...' });
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "downloading", message: "拉取中..." });
const models = await fetchOnlineModels(config);
postToFrame({ type: 'VECTOR_ONLINE_MODELS', models });
postToFrame({ type: 'VECTOR_ONLINE_STATUS', status: 'success', message: `找到 ${models.length} 个模型` });
postToFrame({ type: "VECTOR_ONLINE_MODELS", models });
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "success", message: `找到 ${models.length} 个模型` });
} catch (e) {
postToFrame({ type: 'VECTOR_ONLINE_STATUS', status: 'error', message: e.message });
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: e.message });
}
}
@@ -257,34 +264,34 @@ async function handleGenerateVectors(vectorCfg) {
if (vectorGenerating) return;
if (!vectorCfg?.enabled) {
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L1', current: -1, total: 0 });
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L2', current: -1, total: 0 });
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: -1, total: 0 });
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: -1, total: 0 });
return;
}
const { chatId, chat } = getContext();
if (!chatId || !chat?.length) return;
if (vectorCfg.engine === 'online') {
if (vectorCfg.engine === "online") {
if (!vectorCfg.online?.key || !vectorCfg.online?.model) {
postToFrame({ type: 'VECTOR_ONLINE_STATUS', status: 'error', message: '请配置在线服务 API' });
postToFrame({ type: "VECTOR_ONLINE_STATUS", status: "error", message: "请配置在线服务 API" });
return;
}
}
if (vectorCfg.engine === 'local') {
if (vectorCfg.engine === "local") {
const modelId = vectorCfg.local?.modelId || DEFAULT_LOCAL_MODEL;
const status = await checkLocalModelStatus(modelId);
if (status.status !== 'ready') {
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'downloading', message: '正在加载模型...' });
if (status.status !== "ready") {
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "downloading", message: "正在加载模型..." });
try {
await downloadLocalModel(modelId, (percent) => {
postToFrame({ type: 'VECTOR_LOCAL_MODEL_PROGRESS', percent });
postToFrame({ type: "VECTOR_LOCAL_MODEL_PROGRESS", percent });
});
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'ready', message: '已就绪' });
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "ready", message: "已就绪" });
} catch (e) {
xbLog.error(MODULE_ID, '模型加载失败', e);
postToFrame({ type: 'VECTOR_LOCAL_MODEL_STATUS', status: 'error', message: e.message });
xbLog.error(MODULE_ID, "模型加载失败", e);
postToFrame({ type: "VECTOR_LOCAL_MODEL_STATUS", status: "error", message: e.message });
return;
}
}
@@ -294,7 +301,7 @@ async function handleGenerateVectors(vectorCfg) {
vectorCancelled = false;
const fingerprint = getEngineFingerprint(vectorCfg);
const isLocal = vectorCfg.engine === 'local';
const isLocal = vectorCfg.engine === "local";
const batchSize = isLocal ? 5 : 20;
const concurrency = isLocal ? 1 : 2;
@@ -311,11 +318,11 @@ async function handleGenerateVectors(vectorCfg) {
await saveChunks(chatId, allChunks);
}
const l1Texts = allChunks.map(c => c.text);
const l1Texts = allChunks.map((c) => c.text);
const l1Batches = [];
for (let i = 0; i < l1Texts.length; i += batchSize) {
l1Batches.push({
phase: 'L1',
phase: "L1",
texts: l1Texts.slice(i, i + batchSize),
startIdx: i,
});
@@ -326,20 +333,20 @@ async function handleGenerateVectors(vectorCfg) {
await ensureFingerprintMatch(chatId, fingerprint);
const existingVectors = await getAllEventVectors(chatId);
const existingIds = new Set(existingVectors.map(v => v.eventId));
const existingIds = new Set(existingVectors.map((v) => v.eventId));
const l2Pairs = events
.filter(e => !existingIds.has(e.id))
.map(e => ({ id: e.id, text: `${e.title || ''} ${e.summary || ''}`.trim() }))
.filter(p => p.text);
.filter((e) => !existingIds.has(e.id))
.map((e) => ({ id: e.id, text: `${e.title || ""} ${e.summary || ""}`.trim() }))
.filter((p) => p.text);
const l2Batches = [];
for (let i = 0; i < l2Pairs.length; i += batchSize) {
const batch = l2Pairs.slice(i, i + batchSize);
l2Batches.push({
phase: 'L2',
texts: batch.map(p => p.text),
ids: batch.map(p => p.id),
phase: "L2",
texts: batch.map((p) => p.text),
ids: batch.map((p) => p.id),
startIdx: i,
});
}
@@ -349,8 +356,8 @@ async function handleGenerateVectors(vectorCfg) {
let l1Completed = 0;
let l2Completed = existingIds.size;
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L1', current: 0, total: l1Total });
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L2', current: l2Completed, total: l2Total });
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: 0, total: l1Total });
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: l2Completed, total: l2Total });
const allTasks = [...l1Batches, ...l2Batches];
const l1Vectors = new Array(l1Texts.length);
@@ -370,14 +377,14 @@ async function handleGenerateVectors(vectorCfg) {
try {
const vectors = await embed(task.texts, vectorCfg);
if (task.phase === 'L1') {
if (task.phase === "L1") {
for (let j = 0; j < vectors.length; j++) {
l1Vectors[task.startIdx + j] = vectors[j];
}
l1Completed += task.texts.length;
postToFrame({
type: 'VECTOR_GEN_PROGRESS',
phase: 'L1',
type: "VECTOR_GEN_PROGRESS",
phase: "L1",
current: Math.min(l1Completed, l1Total),
total: l1Total,
});
@@ -387,8 +394,8 @@ async function handleGenerateVectors(vectorCfg) {
}
l2Completed += task.texts.length;
postToFrame({
type: 'VECTOR_GEN_PROGRESS',
phase: 'L2',
type: "VECTOR_GEN_PROGRESS",
phase: "L2",
current: Math.min(l2Completed, l2Total),
total: l2Total,
});
@@ -407,7 +414,7 @@ async function handleGenerateVectors(vectorCfg) {
if (allChunks.length > 0 && l1Vectors.filter(Boolean).length > 0) {
const chunkVectorItems = allChunks
.map((chunk, idx) => l1Vectors[idx] ? { chunkId: chunk.chunkId, vector: l1Vectors[idx] } : null)
.map((chunk, idx) => (l1Vectors[idx] ? { chunkId: chunk.chunkId, vector: l1Vectors[idx] } : null))
.filter(Boolean);
await saveChunkVectors(chatId, chunkVectorItems, fingerprint);
await updateMeta(chatId, { lastChunkFloor: chat.length - 1 });
@@ -417,8 +424,8 @@ async function handleGenerateVectors(vectorCfg) {
await saveEventVectorsToDb(chatId, l2VectorItems, fingerprint);
}
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L1', current: -1, total: 0 });
postToFrame({ type: 'VECTOR_GEN_PROGRESS', phase: 'L2', current: -1, total: 0 });
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: -1, total: 0 });
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: -1, total: 0 });
await sendVectorStatsToFrame();
vectorGenerating = false;
@@ -435,61 +442,30 @@ async function handleClearVectors() {
await clearAllChunks(chatId);
await updateMeta(chatId, { lastChunkFloor: -1 });
await sendVectorStatsToFrame();
xbLog.info(MODULE_ID, '向量数据已清除');
xbLog.info(MODULE_ID, "向量数据已清除");
}
async function autoVectorizeNewEvents(newEventIds) {
async function maybeAutoBuildChunks() {
const cfg = getVectorConfig();
if (!cfg?.enabled || !newEventIds?.length) return;
if (!cfg?.enabled) return;
await sleep(3000);
const { chat, chatId } = getContext();
if (!chatId || !chat?.length) return;
const { chatId } = getContext();
if (!chatId) return;
const status = await getChunkBuildStatus();
if (status.pending <= 0) return;
const store = getSummaryStore();
const events = store?.json?.events || [];
const fingerprint = getEngineFingerprint(cfg);
const meta = await getMeta(chatId);
if (meta.fingerprint && meta.fingerprint !== fingerprint) {
xbLog.warn(MODULE_ID, '引擎不匹配,跳过自动向量化');
return;
}
if (cfg.engine === 'local') {
if (cfg.engine === "local") {
const modelId = cfg.local?.modelId || DEFAULT_LOCAL_MODEL;
if (!isLocalModelLoaded(modelId)) {
xbLog.warn(MODULE_ID, '本地模型未加载,跳过自动向量化');
return;
}
if (!isLocalModelLoaded(modelId)) return;
}
const toVectorize = newEventIds.filter(id => events.some(e => e.id === id));
if (toVectorize.length === 0) return;
const texts = toVectorize.map(id => {
const event = events.find(e => e.id === id);
return event ? `${event.title || ''} ${event.summary || ''}`.trim() : '';
}).filter(t => t);
if (!texts.length) return;
xbLog.info(MODULE_ID, `auto L1 chunks: pending=${status.pending}`);
try {
const vectors = await embed(texts, cfg);
const newVectorItems = [];
for (let i = 0; i < toVectorize.length && i < vectors.length; i++) {
newVectorItems.push({
eventId: toVectorize[i],
vector: vectors[i],
});
}
if (newVectorItems.length > 0) {
await saveEventVectorsToDb(chatId, newVectorItems, fingerprint);
}
xbLog.info(MODULE_ID, `自动向量化 ${toVectorize.length} 个新事件`);
await buildIncrementalChunks({ vectorConfig: cfg });
} catch (e) {
xbLog.error(MODULE_ID, '自动向量化失败', e);
xbLog.error(MODULE_ID, "自动 L1 构建失败", e);
}
}
@@ -502,8 +478,8 @@ function createOverlay() {
overlayCreated = true;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent);
const isNarrow = window.matchMedia?.('(max-width: 768px)').matches;
const overlayHeight = (isMobile || isNarrow) ? '92.5vh' : '100vh';
const isNarrow = window.matchMedia?.("(max-width: 768px)").matches;
const overlayHeight = (isMobile || isNarrow) ? "92.5vh" : "100vh";
const $overlay = $(`
<div id="xiaobaix-story-summary-overlay" style="
@@ -557,12 +533,12 @@ function hideOverlay() {
// ═══════════════════════════════════════════════════════════════════════════
function createSummaryBtn(mesId) {
const btn = document.createElement('div');
btn.className = 'mes_btn xiaobaix-story-summary-btn';
btn.title = '剧情总结';
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 => {
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (!getSettings().storySummary?.enabled) return;
@@ -575,10 +551,12 @@ function createSummaryBtn(mesId) {
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;
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);
msg.querySelector(".flex-container.flex1.alignitemscenter")?.appendChild(btn);
}
function initButtonsForAll() {
@@ -598,10 +576,10 @@ async function sendSavedConfigToFrame() {
const savedConfig = await CommonSettingStorage.get(SUMMARY_CONFIG_KEY, null);
if (savedConfig) {
postToFrame({ type: "LOAD_PANEL_CONFIG", config: savedConfig });
xbLog.info(MODULE_ID, '已从服务器加载面板配置');
xbLog.info(MODULE_ID, "已从服务器加载面板配置");
}
} catch (e) {
xbLog.warn(MODULE_ID, '加载面板配置失败', e);
xbLog.warn(MODULE_ID, "加载面板配置失败", e);
}
}
@@ -646,54 +624,25 @@ function sendFrameFullData(store, totalFloors) {
function openPanelForMessage(mesId) {
createOverlay();
showOverlay();
const { chat } = getContext();
const store = getSummaryStore();
const totalFloors = chat.length;
sendFrameBaseData(store, totalFloors);
sendFrameFullData(store, totalFloors);
setSummaryGenerating(summaryGenerating);
sendVectorConfigToFrame();
sendVectorStatsToFrame();
const cfg = getVectorConfig();
const modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
sendLocalModelStatusToFrame(modelId);
}
function notifyFrameAfterRollback(store) {
const { chat } = getContext();
const totalFloors = Array.isArray(chat) ? chat.length : 0;
const lastSummarized = store.lastSummarizedMesId ?? -1;
if (store.json) {
postToFrame({
type: "SUMMARY_FULL_DATA",
payload: {
keywords: store.json.keywords || [],
events: store.json.events || [],
characters: store.json.characters || { main: [], relationships: [] },
arcs: store.json.arcs || [],
world: store.json.world || [],
lastSummarizedMesId: lastSummarized,
},
});
} else {
postToFrame({ type: "SUMMARY_CLEARED", payload: { totalFloors } });
}
postToFrame({
type: "SUMMARY_BASE_DATA",
stats: {
totalFloors,
summarizedUpTo: lastSummarized + 1,
eventsCount: store.json?.events?.length || 0,
pendingFloors: totalFloors - lastSummarized - 1,
},
});
}
// ═══════════════════════════════════════════════════════════════════════════
// 自动触发总结
// 自动总结(保持原逻辑;不做 prompt 注入)
// ═══════════════════════════════════════════════════════════════════════════
async function maybeAutoRunSummary(reason) {
@@ -704,10 +653,10 @@ async function maybeAutoRunSummary(reason) {
const cfgAll = getSummaryPanelConfig();
const trig = cfgAll.trigger || {};
if (trig.timing === 'manual') return;
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 (trig.timing === "after_ai" && reason !== "after_ai") return;
if (trig.timing === "before_user" && reason !== "before_user") return;
if (isSummaryGenerating()) return;
@@ -720,30 +669,6 @@ async function maybeAutoRunSummary(reason) {
await autoRunSummaryWithRetry(chat.length - 1, { api: cfgAll.api, gen: cfgAll.gen, trigger: trig });
}
async function maybeAutoBuildChunks() {
const cfg = getVectorConfig();
if (!cfg?.enabled) return;
const { chat, chatId } = getContext();
if (!chatId || !chat?.length) return;
const status = await getChunkBuildStatus();
if (status.pending <= 0) return;
if (cfg.engine === 'local') {
const modelId = cfg.local?.modelId || DEFAULT_LOCAL_MODEL;
if (!isLocalModelLoaded(modelId)) return;
}
xbLog.info(MODULE_ID, `auto L1 chunks: pending=${status.pending}`);
try {
await buildIncrementalChunks({ vectorConfig: cfg });
} catch (e) {
xbLog.error(MODULE_ID, '自动 L1 构建失败', e);
}
}
async function autoRunSummaryWithRetry(targetMesId, configForRun) {
setSummaryGenerating(true);
@@ -751,7 +676,7 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
const result = await runSummaryGeneration(targetMesId, configForRun, {
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
onComplete: ({ merged, endMesId, newEventIds }) => {
onComplete: ({ merged, endMesId }) => {
postToFrame({
type: "SUMMARY_FULL_DATA",
payload: {
@@ -759,7 +684,7 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
events: merged.events || [],
characters: merged.characters || { main: [], relationships: [] },
arcs: merged.arcs || [],
world: merged.world || [],
world: merged.world || [],
lastSummarizedMesId: endMesId,
},
});
@@ -768,8 +693,6 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
statusText: `已更新至 ${endMesId + 1} 楼 · ${merged.events?.length || 0} 个事件`,
});
updateFrameStatsAfterSummary(endMesId, merged);
updateSummaryExtensionPrompt();
autoVectorizeNewEvents(newEventIds);
},
});
@@ -782,8 +705,8 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
}
setSummaryGenerating(false);
xbLog.error(MODULE_ID, '自动总结失败已重试3次');
await executeSlashCommand('/echo severity=error 剧情总结失败(已自动重试 3 次)。请稍后再试。');
xbLog.error(MODULE_ID, "自动总结失败已重试3次");
await executeSlashCommand("/echo severity=error 剧情总结失败(已自动重试 3 次)。请稍后再试。");
}
function updateFrameStatsAfterSummary(endMesId, merged) {
@@ -812,22 +735,23 @@ function updateFrameStatsAfterSummary(endMesId, merged) {
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":
case "FRAME_READY": {
frameReady = true;
flushPendingFrameMessages();
setSummaryGenerating(summaryGenerating);
sendSavedConfigToFrame();
sendVectorConfigToFrame();
sendVectorStatsToFrame();
{
const cfg = getVectorConfig();
const modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
sendLocalModelStatusToFrame(modelId);
}
const cfg = getVectorConfig();
const modelId = cfg?.local?.modelId || DEFAULT_LOCAL_MODEL;
sendLocalModelStatusToFrame(modelId);
break;
}
case "SETTINGS_OPENED":
case "FULLSCREEN_OPENED":
@@ -849,11 +773,12 @@ function handleFrameMessage(event) {
}
case "REQUEST_CANCEL":
window.xiaobaixStreamingGeneration?.cancel?.('xb9');
window.xiaobaixStreamingGeneration?.cancel?.("xb9");
setSummaryGenerating(false);
postToFrame({ type: "SUMMARY_STATUS", statusText: "已停止" });
break;
// vector UI
case "VECTOR_DOWNLOAD_MODEL":
handleDownloadLocalModel(data.modelId);
break;
@@ -880,10 +805,12 @@ function handleFrameMessage(event) {
case "VECTOR_GENERATE":
if (data.config) saveVectorConfig(data.config);
clearSummaryExtensionPrompt(); // 防残留
handleGenerateVectors(data.config);
break;
case "VECTOR_CLEAR":
clearSummaryExtensionPrompt(); // 防残留
handleClearVectors();
break;
@@ -891,10 +818,11 @@ function handleFrameMessage(event) {
vectorCancelled = true;
break;
// summary actions
case "REQUEST_CLEAR": {
const { chat, chatId } = getContext();
clearSummaryData(chatId);
clearSummaryExtensionPrompt();
clearSummaryExtensionPrompt(); // 防残留
postToFrame({
type: "SUMMARY_CLEARED",
payload: { totalFloors: Array.isArray(chat) ? chat.length : 0 },
@@ -915,7 +843,6 @@ function handleFrameMessage(event) {
}
store.updatedAt = Date.now();
saveSummaryStore();
updateSummaryExtensionPrompt();
break;
}
@@ -924,8 +851,10 @@ function handleFrameMessage(event) {
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}`);
@@ -953,9 +882,7 @@ function handleFrameMessage(event) {
(async () => {
await executeSlashCommand(`/unhide 0-${lastSummarized}`);
const range = calcHideRange(lastSummarized);
if (range) {
await executeSlashCommand(`/hide ${range.start}-${range.end}`);
}
if (range) await executeSlashCommand(`/hide ${range.start}-${range.end}`);
const { chat } = getContext();
sendFrameBaseData(store, Array.isArray(chat) ? chat.length : 0);
})();
@@ -969,7 +896,8 @@ function handleFrameMessage(event) {
case "SAVE_PANEL_CONFIG":
if (data.config) {
CommonSettingStorage.set(SUMMARY_CONFIG_KEY, data.config);
xbLog.info(MODULE_ID, '面板配置已保存到服务器');
clearSummaryExtensionPrompt(); // 配置变化立即清除注入,避免残留
xbLog.info(MODULE_ID, "面板配置已保存到服务器");
}
break;
@@ -979,6 +907,10 @@ function handleFrameMessage(event) {
}
}
// ═══════════════════════════════════════════════════════════════════════════
// 手动总结
// ═══════════════════════════════════════════════════════════════════════════
async function handleManualGenerate(mesId, config) {
if (isSummaryGenerating()) {
postToFrame({ type: "SUMMARY_STATUS", statusText: "上一轮总结仍在进行中..." });
@@ -991,7 +923,7 @@ async function handleManualGenerate(mesId, config) {
await runSummaryGeneration(mesId, config, {
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
onError: (msg) => postToFrame({ type: "SUMMARY_ERROR", message: msg }),
onComplete: ({ merged, endMesId, newEventIds }) => {
onComplete: ({ merged, endMesId }) => {
const store = getSummaryStore();
postToFrame({
@@ -1011,18 +943,14 @@ async function handleManualGenerate(mesId, config) {
statusText: `已更新至 ${endMesId + 1} 楼 · ${merged.events?.length || 0} 个事件`,
});
// 处理隐藏逻辑
// 隐藏逻辑(与注入无关)
const lastSummarized = store?.lastSummarizedMesId ?? -1;
if (store?.hideSummarizedHistory && lastSummarized >= 0) {
const range = calcHideRange(lastSummarized);
if (range) {
executeSlashCommand(`/hide ${range.start}-${range.end}`);
}
if (range) executeSlashCommand(`/hide ${range.start}-${range.end}`);
}
updateFrameStatsAfterSummary(endMesId, merged);
updateSummaryExtensionPrompt();
autoVectorizeNewEvents(newEventIds);
},
});
@@ -1030,7 +958,7 @@ async function handleManualGenerate(mesId, config) {
}
// ═══════════════════════════════════════════════════════════════════════════
// 事件处理器
// 事件处理器(不做 prompt 注入)
// ═══════════════════════════════════════════════════════════════════════════
async function handleChatChanged() {
@@ -1039,7 +967,6 @@ async function handleChatChanged() {
await rollbackSummaryIfNeeded();
initButtonsForAll();
updateSummaryExtensionPrompt();
const store = getSummaryStore();
const lastSummarized = store?.lastSummarizedMesId ?? -1;
@@ -1052,7 +979,6 @@ async function handleChatChanged() {
if (frameReady) {
sendFrameBaseData(store, newLength);
sendFrameFullData(store, newLength);
notifyFrameAfterRollback(store);
}
}
@@ -1061,9 +987,6 @@ async function handleMessageDeleted() {
const newLength = chat?.length || 0;
await rollbackSummaryIfNeeded();
updateSummaryExtensionPrompt();
// L1 同步
await syncOnMessageDeleted(chatId, newLength);
}
@@ -1071,10 +994,7 @@ async function handleMessageSwiped() {
const { chat, chatId } = getContext();
const lastFloor = (chat?.length || 1) - 1;
// L1 同步
await syncOnMessageSwiped(chatId, lastFloor);
updateSummaryExtensionPrompt();
initButtonsForAll();
}
@@ -1084,41 +1004,52 @@ async function handleMessageReceived() {
const message = chat?.[lastFloor];
const vectorConfig = getVectorConfig();
updateSummaryExtensionPrompt();
initButtonsForAll();
// L1 同步
await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig);
await maybeAutoBuildChunks();
setTimeout(() => maybeAutoRunSummary('after_ai'), 1000);
setTimeout(() => maybeAutoRunSummary("after_ai"), 1000);
}
function handleMessageSent() {
updateSummaryExtensionPrompt();
initButtonsForAll();
setTimeout(() => maybeAutoRunSummary('before_user'), 1000);
setTimeout(() => maybeAutoRunSummary("before_user"), 1000);
}
async function handleMessageUpdated() {
await rollbackSummaryIfNeeded();
updateSummaryExtensionPrompt();
initButtonsForAll();
}
function handleMessageRendered(data) {
const mesId = data?.element ? $(data.element).attr("mesid") : data?.messageId;
if (mesId != null) {
addSummaryBtnToMessage(mesId);
} else {
initButtonsForAll();
}
if (mesId != null) addSummaryBtnToMessage(mesId);
else initButtonsForAll();
}
// ═══════════════════════════════════════════════════════════════════════════
// ✅ 唯一注入入口GENERATION_STARTED
// ═══════════════════════════════════════════════════════════════════════════
async function handleGenerationStarted(type, _params, isDryRun) {
if (isDryRun) return;
const excludeLastAi = type === 'swipe' || type === 'regenerate';
await recallAndInjectPrompt(excludeLastAi, postToFrame);
if (!getSettings().storySummary?.enabled) return;
const excludeLastAi = type === "swipe" || type === "regenerate";
const vectorCfg = getVectorConfig();
// 向量模式:召回 + 预算装配
if (vectorCfg?.enabled) {
await recallAndInjectPrompt(excludeLastAi, {
postToFrame,
echo: executeSlashCommand, // recall failure notification
});
return;
}
// 非向量模式:全量总结注入(不召回)
await injectNonVectorPrompt(postToFrame);
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -1129,14 +1060,17 @@ function registerEvents() {
if (eventsRegistered) return;
eventsRegistered = true;
xbLog.info(MODULE_ID, '模块初始化');
xbLog.info(MODULE_ID, "模块初始化");
CacheRegistry.register(MODULE_ID, {
name: '待发送消息队列',
name: "待发送消息队列",
getSize: () => pendingFrameMessages.length,
getBytes: () => {
try { return JSON.stringify(pendingFrameMessages || []).length * 2; }
catch { return 0; }
try {
return JSON.stringify(pendingFrameMessages || []).length * 2;
} catch {
return 0;
}
},
clear: () => {
pendingFrameMessages = [];
@@ -1153,17 +1087,22 @@ function registerEvents() {
eventSource.on(event_types.MESSAGE_SWIPED, () => setTimeout(handleMessageSwiped, 100));
eventSource.on(event_types.MESSAGE_UPDATED, () => setTimeout(handleMessageUpdated, 100));
eventSource.on(event_types.MESSAGE_EDITED, () => setTimeout(handleMessageUpdated, 100));
eventSource.on(event_types.USER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50));
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, data => setTimeout(() => handleMessageRendered(data), 50));
eventSource.on(event_types.USER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));
// ✅ 只在生成开始时注入
eventSource.on(event_types.GENERATION_STARTED, handleGenerationStarted);
}
function unregisterEvents() {
xbLog.info(MODULE_ID, '模块清理');
xbLog.info(MODULE_ID, "模块清理");
CacheRegistry.unregister(MODULE_ID);
eventsRegistered = false;
$(".xiaobaix-story-summary-btn").remove();
hideOverlay();
// 禁用时清理注入,避免残留
clearSummaryExtensionPrompt();
}
@@ -1175,7 +1114,9 @@ $(document).on("xiaobaix:storySummary:toggle", (_e, enabled) => {
if (enabled) {
registerEvents();
initButtonsForAll();
updateSummaryExtensionPrompt();
// 开启时清一次,防止旧注入残留
clearSummaryExtensionPrompt();
} else {
unregisterEvents();
}
@@ -1191,5 +1132,7 @@ jQuery(() => {
return;
}
registerEvents();
updateSummaryExtensionPrompt();
// 初始化也清一次,保证干净(注入只在生成开始发生)
clearSummaryExtensionPrompt();
});

View File

@@ -30,8 +30,8 @@ const CONFIG = {
MAX_CHUNKS: 40,
MAX_EVENTS: 120,
MIN_SIMILARITY_CHUNK: 0.55,
MIN_SIMILARITY_EVENT: 0.6,
MIN_SIMILARITY_CHUNK: 0.6,
MIN_SIMILARITY_EVENT: 0.65,
MMR_LAMBDA: 0.72,
BONUS_PARTICIPANT_HIT: 0.08,
@@ -350,16 +350,18 @@ async function searchChunks(queryVector, vectorConfig) {
c => c.similarity
);
// floor 稀疏去重
const floorCount = new Map();
const sparse = [];
for (const s of selected.sort((a, b) => b.similarity - a.similarity)) {
const cnt = floorCount.get(s.floor) || 0;
if (cnt >= CONFIG.FLOOR_LIMIT) continue;
floorCount.set(s.floor, cnt + 1);
sparse.push(s);
// floor 稀疏去重:每个楼层只保留该楼层相似度最高的那条
const bestByFloor = new Map();
for (const s of selected) {
const prev = bestByFloor.get(s.floor);
if (!prev || s.similarity > prev.similarity) {
bestByFloor.set(s.floor, s);
}
}
// 最终结果按相似度降序
const sparse = Array.from(bestByFloor.values()).sort((a, b) => b.similarity - a.similarity);
const floors = [...new Set(sparse.map(c => c.floor))];
const chunks = await getChunksByFloors(chatId, floors);