Fix vector recall pending user message
This commit is contained in:
@@ -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 || "") };
|
||||
}
|
||||
|
||||
@@ -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); // 不需要 await,fire-and-forget
|
||||
sendFrameBaseData(store, totalFloors);
|
||||
sendFrameFullData(store, totalFloors);
|
||||
setSummaryGenerating(summaryGenerating);
|
||||
|
||||
@@ -646,9 +693,9 @@ function openPanelForMessage(mesId) {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Hide/Unhide:向量模式联动("已总结"的定义切换)
|
||||
// - 非向量:boundary = lastSummarizedMesId(LLM总结边界)
|
||||
// - 向量: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} 个事件`,
|
||||
});
|
||||
|
||||
// 隐藏逻辑:统一走 boundary(vector 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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user