Fix vector recall pending user message

This commit is contained in:
2026-01-29 01:17:37 +08:00
parent d1caf01334
commit 3313b5efa7
3 changed files with 217 additions and 185 deletions

View File

@@ -1,17 +1,10 @@
// ═══════════════════════════════════════════════════════════════════════════
// Story Summary - Prompt Injection (Final Clean Version)
// - 注入只在 GENERATION_STARTED 发生(由 story-summary.js 调用)
// - 向量关闭:注入全量总结(世界/事件/弧光
// - 向量开启:召回 + 预算装配注入
// - 没有“快速注入”写入 extension_prompts避免覆盖/残留/竞态
// - 仅负责“构建注入文本”,不负责写入 extension_prompts
// - 注入发生在 story-summary.js 的 CHAT_COMPLETION_PROMPT_READY最终 messages 已裁剪
// ═══════════════════════════════════════════════════════════════════════════
import { getContext } from "../../../../../../extensions.js";
import {
extension_prompts,
extension_prompt_types,
extension_prompt_roles,
} from "../../../../../../../script.js";
import { xbLog } from "../../../core/debug-core.js";
import { getSummaryStore } from "../data/store.js";
import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
@@ -19,7 +12,6 @@ import { recallMemory, buildQueryText } from "../vector/recall.js";
import { getChunksByFloors, getAllChunkVectors, getAllEventVectors, getMeta } from "../vector/chunk-store.js";
const MODULE_ID = "summaryPrompt";
const SUMMARY_PROMPT_KEY = "LittleWhiteBox_StorySummary";
// ─────────────────────────────────────────────────────────────────────────────
// 召回失败提示节流(避免连续生成刷屏)
@@ -137,7 +129,8 @@ function formatArcLine(a) {
// 完整 chunk 输出(不截断)
function formatChunkFullLine(c) {
const speaker = c.isUser ? "{{user}}" : "{{char}}";
const { name1, name2 } = getContext();
const speaker = c.isUser ? (name1 || "用户") : (name2 || "角色");
return ` #${c.floor + 1} [${speaker}] ${String(c.text || "").trim()}`;
}
@@ -299,23 +292,19 @@ function buildNonVectorPrompt(store) {
);
}
export async function injectNonVectorPrompt(postToFrame = null) {
export function buildNonVectorPromptText() {
if (!getSettings().storySummary?.enabled) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
return "";
}
const { chat } = getContext();
const store = getSummaryStore();
if (!store?.json) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
return "";
}
let text = buildNonVectorPrompt(store);
if (!text.trim()) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
return "";
}
// wrapper沿用面板设置
@@ -323,21 +312,7 @@ export async function injectNonVectorPrompt(postToFrame = null) {
if (cfg.trigger?.wrapperHead) text = cfg.trigger.wrapperHead + "\n" + text;
if (cfg.trigger?.wrapperTail) text = text + "\n" + cfg.trigger.wrapperTail;
const lastIdx = store.lastSummarizedMesId ?? 0;
let depth = (chat?.length || 0) - lastIdx - 1;
if (depth < 0) depth = 0;
if (cfg.trigger?.forceInsertAtEnd) depth = 10000;
extension_prompts[SUMMARY_PROMPT_KEY] = {
value: text,
position: extension_prompt_types.IN_CHAT,
depth,
role: extension_prompt_roles.SYSTEM,
};
if (postToFrame) {
postToFrame({ type: "RECALL_LOG", text: "\n[Non-vector] Injected full summary prompt.\n" });
}
return text;
}
// ─────────────────────────────────────────────────────────────
@@ -706,19 +681,17 @@ async function attachEvidenceToCausalEvents(causalEvents, eventVectorMap, chunkV
// ✅ 向量模式:召回 + 注入(供 story-summary.js 在 GENERATION_STARTED 调用)
// ─────────────────────────────────────────────────────────────────────────────
export async function recallAndInjectPrompt(excludeLastAi = false, hooks = {}) {
const { postToFrame = null, echo = null } = hooks;
export async function buildVectorPromptText(excludeLastAi = false, hooks = {}) {
const { postToFrame = null, echo = null, pendingUserMessage = null } = hooks;
if (!getSettings().storySummary?.enabled) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
return { text: "", logText: "" };
}
const { chat } = getContext();
const store = getSummaryStore();
if (!store?.json) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
return { text: "", logText: "" };
}
const allEvents = store.json.events || [];
@@ -726,14 +699,12 @@ export async function recallAndInjectPrompt(excludeLastAi = false, hooks = {}) {
const length = chat?.length || 0;
if (lastIdx >= length) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
return { text: "", logText: "" };
}
const vectorCfg = getVectorConfig();
if (!vectorCfg?.enabled) {
// 向量没开,不该走这条
return;
return { text: "", logText: "" };
}
const { chatId } = getContext();
@@ -745,7 +716,10 @@ export async function recallAndInjectPrompt(excludeLastAi = false, hooks = {}) {
try {
const queryText = buildQueryText(chat, 2, excludeLastAi);
recallResult = await recallMemory(queryText, allEvents, vectorCfg, { excludeLastAi });
recallResult = await recallMemory(queryText, allEvents, vectorCfg, {
excludeLastAi,
pendingUserMessage,
});
recallResult = {
...recallResult,
@@ -808,9 +782,7 @@ export async function recallAndInjectPrompt(excludeLastAi = false, hooks = {}) {
});
}
// 清空本次注入,避免残留误导
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
return { text: "", logText: `\n[Vector Recall Failed]\n${String(e?.stack || e?.message || e)}\n` };
}
// 成功但结果为空:也提示,并清空注入(不降级)
@@ -831,8 +803,7 @@ export async function recallAndInjectPrompt(excludeLastAi = false, hooks = {}) {
text: "\n[Vector Recall Empty]\nNo recall candidates / vectors not ready.\n",
});
}
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
return { text: "", logText: "\n[Vector Recall Empty]\nNo recall candidates / vectors not ready.\n" };
}
// 拼装向量 prompt
@@ -844,50 +815,17 @@ export async function recallAndInjectPrompt(excludeLastAi = false, hooks = {}) {
meta
);
// 写入 extension_prompts真正注入
await writePromptToExtensionPrompts(promptText, store, chat);
// wrapper沿用面板设置——必须补回否则语义回退
const cfg = getSummaryPanelConfig();
let finalText = String(promptText || "");
if (cfg.trigger?.wrapperHead) finalText = cfg.trigger.wrapperHead + "\n" + finalText;
if (cfg.trigger?.wrapperTail) finalText = finalText + "\n" + cfg.trigger.wrapperTail;
// 发给涌现窗口:召回报告 + 装配报告
if (postToFrame) {
const recallLog = recallResult.logText || "";
postToFrame({ type: "RECALL_LOG", text: recallLog + (injectionLogText || "") });
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 写入 extension_prompts统一入口
// ─────────────────────────────────────────────────────────────────────────────
async function writePromptToExtensionPrompts(text, store, chat) {
const cfg = getSummaryPanelConfig();
let finalText = String(text || "");
if (cfg.trigger?.wrapperHead) finalText = cfg.trigger.wrapperHead + "\n" + finalText;
if (cfg.trigger?.wrapperTail) finalText = finalText + "\n" + cfg.trigger.wrapperTail;
if (!finalText.trim()) {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return;
}
const lastIdx = store.lastSummarizedMesId ?? 0;
let depth = (chat?.length || 0) - lastIdx - 1;
if (depth < 0) depth = 0;
if (cfg.trigger?.forceInsertAtEnd) depth = 10000;
extension_prompts[SUMMARY_PROMPT_KEY] = {
value: finalText,
position: extension_prompt_types.IN_CHAT,
depth,
role: extension_prompt_roles.SYSTEM,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// 清理 prompt供 story-summary.js 调用)
// ─────────────────────────────────────────────────────────────────────────────
export function clearSummaryExtensionPrompt() {
delete extension_prompts[SUMMARY_PROMPT_KEY];
return { text: finalText, logText: (recallResult.logText || "") + (injectionLogText || "") };
}

View File

@@ -1,9 +1,12 @@
// ═══════════════════════════════════════════════════════════════════════════
// Story Summary - 主入口(干净版)
// - 注入只在 GENERATION_STARTED 发生
// - 向量关闭注入全量总结L3+L2+Arcs
// - 向量开启:召回 + 1万预算装配注入
// - 删除所有 updateSummaryExtensionPrompt() 调用,避免覆盖/残留/竞态
// Story Summary - 主入口(最终版)
//
// 稳定目标:
// 1) "聊天时隐藏已总结" 永远只隐藏"已总结"部分,绝不影响未总结部分
// 2) 关闭隐藏 = 暴力全量 unhide确保立刻恢复
// 3) 开启隐藏 / 改Y / 切Chat / 收新消息:先全量 unhide再按边界重新 hide
// 4) Prompt 注入位置稳定:永远插在"最后一条 user 消息"之前
// 5) 注入不依赖 extension_prompts/depth不受 ST 裁剪/隐藏参照系影响
// ═══════════════════════════════════════════════════════════════════════════
import { getContext } from "../../../../../extensions.js";
@@ -23,11 +26,10 @@ import {
clearSummaryData,
} from "./data/store.js";
// prompt injection (ONLY on generation started)
// prompt text builder
import {
recallAndInjectPrompt,
clearSummaryExtensionPrompt,
injectNonVectorPrompt,
buildVectorPromptText,
buildNonVectorPromptText,
} from "./generate/prompt.js";
// summary generation
@@ -91,6 +93,13 @@ let pendingFrameMessages = [];
let eventsRegistered = false;
let vectorGenerating = false;
let vectorCancelled = false;
let vectorAbortController = null;
let pendingInjectText = "";
// ★ 用户消息缓存(解决 GENERATION_STARTED 时 chat 尚未包含用户消息的问题)
let lastSentUserMessage = null;
let lastSentTimestamp = 0;
let hideApplyTimer = null;
const HIDE_APPLY_DEBOUNCE_MS = 250;
@@ -118,6 +127,18 @@ async function executeSlashCommand(command) {
}
}
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}`);
}
// ═══════════════════════════════════════════════════════════════════════════
// 生成状态管理
// ═══════════════════════════════════════════════════════════════════════════
@@ -302,6 +323,8 @@ async function handleGenerateVectors(vectorCfg) {
vectorGenerating = true;
vectorCancelled = false;
vectorAbortController?.abort?.();
vectorAbortController = new AbortController();
const fingerprint = getEngineFingerprint(vectorCfg);
const isLocal = vectorCfg.engine === "local";
@@ -362,6 +385,8 @@ async function handleGenerateVectors(vectorCfg) {
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L1", current: 0, total: l1Total });
postToFrame({ type: "VECTOR_GEN_PROGRESS", phase: "L2", current: l2Completed, total: l2Total });
let rateLimitWarned = false;
const allTasks = [...l1Batches, ...l2Batches];
const l1Vectors = new Array(l1Texts.length);
const l2VectorItems = [];
@@ -371,6 +396,7 @@ async function handleGenerateVectors(vectorCfg) {
async function worker() {
while (taskIndex < allTasks.length) {
if (vectorCancelled) break;
if (vectorAbortController?.signal?.aborted) break;
const i = taskIndex++;
if (i >= allTasks.length) break;
@@ -378,7 +404,7 @@ async function handleGenerateVectors(vectorCfg) {
const task = allTasks[i];
try {
const vectors = await embed(task.texts, vectorCfg);
const vectors = await embed(task.texts, vectorCfg, { signal: vectorAbortController.signal });
if (task.phase === "L1") {
for (let j = 0; j < vectors.length; j++) {
@@ -404,7 +430,23 @@ async function handleGenerateVectors(vectorCfg) {
});
}
} catch (e) {
if (e?.name === "AbortError") {
xbLog.warn(MODULE_ID, "向量生成已取消AbortError");
break;
}
xbLog.error(MODULE_ID, `${task.phase} batch 向量化失败`, e);
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;
}
}
}
@@ -415,6 +457,13 @@ async function handleGenerateVectors(vectorCfg) {
.map(() => worker())
);
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;
}
if (allChunks.length > 0 && l1Vectors.filter(Boolean).length > 0) {
const chunkVectorItems = allChunks
.map((chunk, idx) => (l1Vectors[idx] ? { chunkId: chunk.chunkId, vector: l1Vectors[idx] } : null))
@@ -433,6 +482,7 @@ async function handleGenerateVectors(vectorCfg) {
vectorGenerating = false;
vectorCancelled = false;
vectorAbortController = null;
xbLog.info(MODULE_ID, `向量生成完成: L1=${l1Vectors.filter(Boolean).length}, L2=${l2VectorItems.length}`);
}
@@ -463,8 +513,6 @@ async function maybeAutoBuildChunks() {
if (!isLocalModelLoaded(modelId)) return;
}
xbLog.info(MODULE_ID, `auto L1 chunks: pending=${status.pending}`);
try {
await buildIncrementalChunks({ vectorConfig: cfg });
} catch (e) {
@@ -579,7 +627,6 @@ 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, "已从服务器加载面板配置");
}
} catch (e) {
xbLog.warn(MODULE_ID, "加载面板配置失败", e);
@@ -633,7 +680,7 @@ function openPanelForMessage(mesId) {
const store = getSummaryStore();
const totalFloors = chat.length;
sendFrameBaseData(store, totalFloors); // 不需要 awaitfire-and-forget
sendFrameBaseData(store, totalFloors);
sendFrameFullData(store, totalFloors);
setSummaryGenerating(summaryGenerating);
@@ -646,9 +693,9 @@ function openPanelForMessage(mesId) {
}
// ═══════════════════════════════════════════════════════════════════════════
// Hide/Unhide:向量模式联动("已总结"的定义切换)
// - 非向量boundary = lastSummarizedMesIdLLM总结边界
// - 向量boundary = meta.lastChunkFloor已向量化
// Hide/Unhide
// - 非向量boundary = lastSummarizedMesId
// - 向量boundary = meta.lastChunkFloor若为 -1 则回退到 lastSummarizedMesId
// ═══════════════════════════════════════════════════════════════════════════
async function getHideBoundaryFloor(store) {
@@ -658,16 +705,21 @@ async function getHideBoundaryFloor(store) {
}
const { chatId } = getContext();
if (!chatId) return -1;
if (!chatId) return store?.lastSummarizedMesId ?? -1;
const meta = await getMeta(chatId);
return meta?.lastChunkFloor ?? -1;
const v = meta?.lastChunkFloor ?? -1;
if (v >= 0) return v;
return store?.lastSummarizedMesId ?? -1;
}
async function applyHideState() {
const store = getSummaryStore();
if (!store?.hideSummarizedHistory) return;
// 先全量 unhide杜绝历史残留
await unhideAllMessages();
const boundary = await getHideBoundaryFloor(store);
if (boundary < 0) return;
@@ -685,15 +737,12 @@ function applyHideStateDebounced() {
}
async function clearHideState() {
const store = getSummaryStore();
const boundary = await getHideBoundaryFloor(store);
if (boundary < 0) return;
await executeSlashCommand(`/unhide 0-${boundary}`);
// 暴力全量 unhide确保立刻恢复
await unhideAllMessages();
}
// ═══════════════════════════════════════════════════════════════════════════
// 自动总结(保持原逻辑;不做 prompt 注入)
// 自动总结
// ═══════════════════════════════════════════════════════════════════════════
async function maybeAutoRunSummary(reason) {
@@ -716,7 +765,6 @@ async function maybeAutoRunSummary(reason) {
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 });
}
@@ -739,10 +787,8 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
lastSummarizedMesId: endMesId,
},
});
postToFrame({
type: "SUMMARY_STATUS",
statusText: `已更新至 ${endMesId + 1} 楼 · ${merged.events?.length || 0} 个事件`,
});
applyHideStateDebounced();
updateFrameStatsAfterSummary(endMesId, merged);
},
});
@@ -756,7 +802,6 @@ async function autoRunSummaryWithRetry(targetMesId, configForRun) {
}
setSummaryGenerating(false);
xbLog.error(MODULE_ID, "自动总结失败已重试3次");
await executeSlashCommand("/echo severity=error 剧情总结失败(已自动重试 3 次)。请稍后再试。");
}
@@ -829,7 +874,6 @@ function handleFrameMessage(event) {
postToFrame({ type: "SUMMARY_STATUS", statusText: "已停止" });
break;
// vector UI
case "VECTOR_DOWNLOAD_MODEL":
handleDownloadLocalModel(data.modelId);
break;
@@ -856,24 +900,21 @@ function handleFrameMessage(event) {
case "VECTOR_GENERATE":
if (data.config) saveVectorConfig(data.config);
clearSummaryExtensionPrompt(); // 防残留
handleGenerateVectors(data.config);
break;
case "VECTOR_CLEAR":
clearSummaryExtensionPrompt(); // 防残留
handleClearVectors();
break;
case "VECTOR_CANCEL_GENERATE":
vectorCancelled = true;
try { vectorAbortController?.abort?.(); } catch {}
break;
// summary actions
case "REQUEST_CLEAR": {
const { chat, chatId } = getContext();
clearSummaryData(chatId);
clearSummaryExtensionPrompt(); // 防残留
postToFrame({
type: "SUMMARY_CLEARED",
payload: { totalFloors: Array.isArray(chat) ? chat.length : 0 },
@@ -920,16 +961,13 @@ function handleFrameMessage(event) {
const oldCount = store.keepVisibleCount ?? 3;
const newCount = Math.max(0, Math.min(50, parseInt(data.count) || 3));
if (newCount === oldCount) break;
store.keepVisibleCount = newCount;
saveSummaryStore();
(async () => {
// 先清掉原隐藏,再按新 keepCount 重算隐藏
if (store.hideSummarizedHistory) {
await clearHideState();
await applyHideState();
}
const { chat } = getContext();
@@ -941,8 +979,6 @@ function handleFrameMessage(event) {
case "SAVE_PANEL_CONFIG":
if (data.config) {
CommonSettingStorage.set(SUMMARY_CONFIG_KEY, data.config);
clearSummaryExtensionPrompt(); // 配置变化立即清除注入,避免残留
xbLog.info(MODULE_ID, "面板配置已保存到服务器");
}
break;
@@ -963,7 +999,6 @@ async function handleManualGenerate(mesId, config) {
}
setSummaryGenerating(true);
xbLog.info(MODULE_ID, `开始手动总结 mesId=${mesId}`);
await runSummaryGeneration(mesId, config, {
onStatus: (text) => postToFrame({ type: "SUMMARY_STATUS", statusText: text }),
@@ -981,14 +1016,7 @@ async function handleManualGenerate(mesId, config) {
},
});
postToFrame({
type: "SUMMARY_STATUS",
statusText: `已更新至 ${endMesId + 1} 楼 · ${merged.events?.length || 0} 个事件`,
});
// 隐藏逻辑:统一走 boundaryvector on/off 自动切换定义)
applyHideStateDebounced();
updateFrameStatsAfterSummary(endMesId, merged);
},
});
@@ -997,7 +1025,7 @@ async function handleManualGenerate(mesId, config) {
}
// ═══════════════════════════════════════════════════════════════════════════
// 事件处理器(不做 prompt 注入)
// 消息事件
// ═══════════════════════════════════════════════════════════════════════════
async function handleChatChanged() {
@@ -1008,7 +1036,10 @@ async function handleChatChanged() {
initButtonsForAll();
const store = getSummaryStore();
applyHideStateDebounced();
if (store?.hideSummarizedHistory) {
await applyHideState();
}
if (frameReady) {
await sendFrameBaseData(store, newLength);
@@ -1022,6 +1053,7 @@ async function handleMessageDeleted() {
await rollbackSummaryIfNeeded();
await syncOnMessageDeleted(chatId, newLength);
applyHideStateDebounced();
}
async function handleMessageSwiped() {
@@ -1030,6 +1062,7 @@ async function handleMessageSwiped() {
await syncOnMessageSwiped(chatId, lastFloor);
initButtonsForAll();
applyHideStateDebounced();
}
async function handleMessageReceived() {
@@ -1043,9 +1076,7 @@ async function handleMessageReceived() {
await syncOnMessageReceived(chatId, lastFloor, message, vectorConfig);
await maybeAutoBuildChunks();
// 向量模式下lastChunkFloor 会持续推进;如果勾选隐藏,自动扩展隐藏范围
applyHideStateDebounced();
setTimeout(() => maybeAutoRunSummary("after_ai"), 1000);
}
@@ -1057,6 +1088,7 @@ function handleMessageSent() {
async function handleMessageUpdated() {
await rollbackSummaryIfNeeded();
initButtonsForAll();
applyHideStateDebounced();
}
function handleMessageRendered(data) {
@@ -1066,7 +1098,20 @@ function handleMessageRendered(data) {
}
// ═══════════════════════════════════════════════════════════════════════════
// ✅ 唯一注入入口GENERATION_STARTED
// 用户消息缓存(供向量召回使用)
// ═══════════════════════════════════════════════════════════════════════════
function handleMessageSentForRecall() {
const { chat } = getContext();
const lastMsg = chat?.[chat.length - 1];
if (lastMsg?.is_user) {
lastSentUserMessage = lastMsg.mes;
lastSentTimestamp = Date.now();
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Prompt 注入
// ═══════════════════════════════════════════════════════════════════════════
async function handleGenerationStarted(type, _params, isDryRun) {
@@ -1076,17 +1121,62 @@ async function handleGenerationStarted(type, _params, isDryRun) {
const excludeLastAi = type === "swipe" || type === "regenerate";
const vectorCfg = getVectorConfig();
// 向量模式:召回 + 预算装配
pendingInjectText = "";
// ★ 判断是否使用缓存的用户消息30秒内有效
let pendingUserMessage = null;
if (type === "normal" && lastSentUserMessage && (Date.now() - lastSentTimestamp < 30000)) {
pendingUserMessage = lastSentUserMessage;
}
// 用完清空
lastSentUserMessage = null;
lastSentTimestamp = 0;
if (vectorCfg?.enabled) {
await recallAndInjectPrompt(excludeLastAi, {
const r = await buildVectorPromptText(excludeLastAi, {
postToFrame,
echo: executeSlashCommand, // recall failure notification
echo: executeSlashCommand,
pendingUserMessage,
});
pendingInjectText = r?.text || "";
return;
}
// 非向量模式:全量总结注入(不召回)
await injectNonVectorPrompt(postToFrame);
pendingInjectText = buildNonVectorPromptText() || "";
}
function clearPendingInject() {
pendingInjectText = "";
}
function findInsertIndexBeforeLastUserMessage(chat) {
if (!Array.isArray(chat) || !chat.length) return 0;
for (let i = chat.length - 1; i >= 0; i--) {
if (chat[i]?.role === "user") return i;
}
// 没有 user退化为末尾前一位
return Math.max(0, chat.length - 1);
}
function handleChatCompletionPromptReady(eventData) {
try {
if (!pendingInjectText?.trim()) return;
if (!eventData || eventData.dryRun) return;
if (!Array.isArray(eventData.chat)) return;
// 永远插在最后一条 user 消息之前,保证三段结构稳定
const insertAt = findInsertIndexBeforeLastUserMessage(eventData.chat);
eventData.chat.splice(insertAt, 0, {
role: "assistant",
content: pendingInjectText,
});
clearPendingInject();
} catch (e) {
xbLog.error(MODULE_ID, "Prompt inject failed", e);
clearPendingInject();
}
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -1097,8 +1187,6 @@ function registerEvents() {
if (eventsRegistered) return;
eventsRegistered = true;
xbLog.info(MODULE_ID, "模块初始化");
CacheRegistry.register(MODULE_ID, {
name: "待发送消息队列",
getSize: () => pendingFrameMessages.length,
@@ -1121,26 +1209,28 @@ function registerEvents() {
eventSource.on(event_types.MESSAGE_DELETED, () => setTimeout(handleMessageDeleted, 50));
eventSource.on(event_types.MESSAGE_RECEIVED, () => setTimeout(handleMessageReceived, 150));
eventSource.on(event_types.MESSAGE_SENT, () => setTimeout(handleMessageSent, 150));
eventSource.on(event_types.MESSAGE_SENT, handleMessageSentForRecall);
eventSource.on(event_types.MESSAGE_SWIPED, () => setTimeout(handleMessageSwiped, 100));
eventSource.on(event_types.MESSAGE_UPDATED, () => setTimeout(handleMessageUpdated, 100));
eventSource.on(event_types.MESSAGE_EDITED, () => setTimeout(handleMessageUpdated, 100));
eventSource.on(event_types.USER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (data) => setTimeout(() => handleMessageRendered(data), 50));
// ✅ 只在生成开始时注入
// 注入链路
eventSource.on(event_types.GENERATION_STARTED, handleGenerationStarted);
eventSource.on(event_types.GENERATION_STOPPED, clearPendingInject);
eventSource.on(event_types.GENERATION_ENDED, clearPendingInject);
eventSource.on(event_types.CHAT_COMPLETION_PROMPT_READY, handleChatCompletionPromptReady);
}
function unregisterEvents() {
xbLog.info(MODULE_ID, "模块清理");
CacheRegistry.unregister(MODULE_ID);
eventsRegistered = false;
$(".xiaobaix-story-summary-btn").remove();
hideOverlay();
// 禁用时清理注入,避免残留
clearSummaryExtensionPrompt();
clearPendingInject();
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -1151,9 +1241,6 @@ $(document).on("xiaobaix:storySummary:toggle", (_e, enabled) => {
if (enabled) {
registerEvents();
initButtonsForAll();
// 开启时清一次,防止旧注入残留
clearSummaryExtensionPrompt();
} else {
unregisterEvents();
}
@@ -1164,12 +1251,6 @@ $(document).on("xiaobaix:storySummary:toggle", (_e, enabled) => {
// ═══════════════════════════════════════════════════════════════════════════
jQuery(() => {
if (!getSettings().storySummary?.enabled) {
clearSummaryExtensionPrompt();
return;
}
if (!getSettings().storySummary?.enabled) return;
registerEvents();
// 初始化也清一次,保证干净(注入只在生成开始发生)
clearSummaryExtensionPrompt();
});

View File

@@ -158,16 +158,28 @@ function buildExpDecayWeights(n, beta) {
// Query 构建
// ═══════════════════════════════════════════════════════════════════════════
function buildQuerySegments(chat, count, excludeLastAi) {
if (!chat?.length) return [];
let messages = chat;
if (excludeLastAi && messages.length > 0 && !messages[messages.length - 1]?.is_user) {
messages = messages.slice(0, -1);
}
return messages.slice(-count).map((m, idx, arr) => {
const speaker = m.name || (m.is_user ? '用户' : '角色');
function buildQuerySegments(chat, count, excludeLastAi, pendingUserMessage = null) {
if (!chat?.length) return [];
let messages = chat;
if (excludeLastAi && messages.length > 0 && !messages[messages.length - 1]?.is_user) {
messages = messages.slice(0, -1);
}
// ★ 如果有待处理的用户消息且 chat 中最后一条不是它,追加虚拟消息
if (pendingUserMessage) {
const lastMsg = messages[messages.length - 1];
const lastMsgText = lastMsg?.mes?.trim() || "";
const pendingText = pendingUserMessage.trim();
// 避免重复(如果 chat 已包含该消息则不追加)
if (lastMsgText !== pendingText) {
messages = [...messages, { is_user: true, name: "用户", mes: pendingUserMessage }];
}
}
return messages.slice(-count).map((m, idx, arr) => {
const speaker = m.name || (m.is_user ? '用户' : '角色');
const clean = stripNoise(m.mes);
if (!clean) return '';
const limit = idx === arr.length - 1 ? CONFIG.QUERY_MAX_CHARS : CONFIG.QUERY_CONTEXT_CHARS;
@@ -669,16 +681,17 @@ function formatRecallLog({ elapsed, segments, weights, chunkResults, eventResult
// ═══════════════════════════════════════════════════════════════════════════
// 主入口
// ═══════════════════════════════════════════════════════════════════════════
export async function recallMemory(queryText, allEvents, vectorConfig, options = {}) {
const T0 = performance.now();
const { chat } = getContext();
const store = getSummaryStore();
if (!allEvents?.length) {
return { events: [], chunks: [], elapsed: 0, logText: 'No events.' };
}
export async function recallMemory(queryText, allEvents, vectorConfig, options = {}) {
const T0 = performance.now();
const { chat } = getContext();
const store = getSummaryStore();
const { pendingUserMessage = null } = options;
if (!allEvents?.length) {
return { events: [], chunks: [], elapsed: 0, logText: 'No events.' };
}
const segments = buildQuerySegments(chat, CONFIG.QUERY_MSG_COUNT, !!options.excludeLastAi, pendingUserMessage);
let queryVector, weights;