diff --git a/modules/story-summary/generate/prompt.js b/modules/story-summary/generate/prompt.js index ce6117b..fe51911 100644 --- a/modules/story-summary/generate/prompt.js +++ b/modules/story-summary/generate/prompt.js @@ -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 || "") }; } diff --git a/modules/story-summary/story-summary.js b/modules/story-summary/story-summary.js index 362eba7..bfa0680 100644 --- a/modules/story-summary/story-summary.js +++ b/modules/story-summary/story-summary.js @@ -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(); }); diff --git a/modules/story-summary/vector/recall.js b/modules/story-summary/vector/recall.js index de8bd92..84a3776 100644 --- a/modules/story-summary/vector/recall.js +++ b/modules/story-summary/vector/recall.js @@ -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.' }; - } - - const segments = buildQuerySegments(chat, CONFIG.QUERY_MSG_COUNT, !!options.excludeLastAi); +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; try {