From d39e81cde517d9303a4c711c65646b2f2ea75b28 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Mon, 6 Apr 2026 20:18:40 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20MESSAGE=5FSENT=20=E6=97=B6=E6=8A=8A?= =?UTF-8?q?=E5=8F=AC=E5=9B=9E=E8=AE=B0=E5=BD=95=E9=87=8D=E7=BB=91=E5=88=B0?= =?UTF-8?q?=E5=88=9A=E5=85=A5=E5=9C=BA=E7=9A=84=20user=20=E6=A5=BC?= =?UTF-8?q?=E5=B1=82=EF=BC=88=E5=AF=B9=E9=BD=90=E9=85=92=E9=A6=86=20GENERA?= =?UTF-8?q?TION=5FAFTER=5FCOMMANDS=20=E6=97=A9=E4=BA=8E=20sendMessageAsUse?= =?UTF-8?q?r=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- event-binding.js | 4 ++++ index.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/event-binding.js b/event-binding.js index e8c3743..af6525d 100644 --- a/event-binding.js +++ b/event-binding.js @@ -261,6 +261,10 @@ export function onMessageSentController(runtime, messageId) { resolvedMessageId, message.mes || "", ); + // GENERATION_AFTER_COMMANDS 在 sendMessageAsUser 之前触发,此时新用户消息 + // 尚未进入 chat,recall 记录会被写到上一条 user 上。这里用户消息刚入场, + // transaction 仍在桥接窗口内,立即把记录重新绑定到正确的楼层。 + runtime.rebindRecallRecordToNewUserMessage?.(resolvedMessageId); runtime.refreshPersistedRecallMessageUi?.(); } diff --git a/index.js b/index.js index b1a29a0..75c73e6 100644 --- a/index.js +++ b/index.js @@ -1543,6 +1543,53 @@ function doesChatUserMessageMatchRecallCandidates(message, candidateHashes) { return candidateHashes.has(hashRecallInput(normalizedMessage)); } +function rebindRecallRecordToNewUserMessage(newUserMessageIndex) { + const chat = getContext()?.chat; + if ( + !Array.isArray(chat) || + !Number.isFinite(newUserMessageIndex) || + !chat[newUserMessageIndex]?.is_user + ) { + return; + } + if (readPersistedRecallFromUserMessage(chat, newUserMessageIndex)) { + return; + } + const recentTransaction = findRecentGenerationRecallTransactionForChat(); + const recallResult = getGenerationRecallTransactionResult(recentTransaction); + if ( + !recallResult || + recallResult.status !== "completed" || + !recallResult.didRecall || + !String(recallResult.injectionText || "").trim() + ) { + return; + } + const record = buildPersistedRecallRecord( + { + injectionText: String(recallResult.injectionText || "").trim(), + selectedNodeIds: recallResult.selectedNodeIds || [], + recallInput: String( + recallResult.recallInput || recallResult.userMessage || "", + ), + recallSource: String(recallResult.source || ""), + hookName: String( + recallResult.hookName || + recentTransaction?.lastRecallMeta?.hookName || + "", + ), + tokenEstimate: estimateTokens( + String(recallResult.injectionText || "").trim(), + ), + manuallyEdited: false, + }, + null, + ); + if (writePersistedRecallToUserMessage(chat, newUserMessageIndex, record)) { + triggerChatMetadataSave(getContext(), { immediate: false }); + } +} + function resolveRecallPersistenceTargetUserMessageIndex( chat, { @@ -9235,6 +9282,7 @@ function onMessageSent(messageId) { getContext, isTrivialUserInput, recordRecallSentUserMessage, + rebindRecallRecordToNewUserMessage, refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, }, messageId, From 36e2a2c45101ab0c0a9e996393eff97912d954dc Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Mon, 6 Apr 2026 20:28:35 +0800 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20=E7=A7=BB=E9=99=A4=20Recall=20Card?= =?UTF-8?q?=20=E6=8E=92=E6=9F=A5=E7=94=A8=E7=9A=84=20ST-BME:DIAG=20?= =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- event-binding.js | 22 ---------------------- recall-controller.js | 12 +----------- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/event-binding.js b/event-binding.js index af6525d..8650016 100644 --- a/event-binding.js +++ b/event-binding.js @@ -1,5 +1,3 @@ -import { debugWarn } from "./debug-logging.js"; - function getTimerApi(runtime) { const rawSetTimeout = typeof runtime?.setTimeout === "function" @@ -40,14 +38,8 @@ export function registerBeforeCombinePromptsController(runtime, listener) { export function registerGenerationAfterCommandsController(runtime, listener) { const makeFirst = runtime.getEventMakeFirst(); const eventName = runtime.eventTypes.GENERATION_AFTER_COMMANDS; - debugWarn("[ST-BME:DIAG] Registering GENERATION_AFTER_COMMANDS:", { - eventName, - hasMakeFirst: typeof makeFirst === "function", - hasListener: typeof listener === "function", - }); if (typeof makeFirst === "function") { const cleanup = makeFirst(eventName, listener); - debugWarn("[ST-BME:DIAG] Registered via makeFirst, cleanup:", typeof cleanup); return cleanup; } @@ -412,9 +404,7 @@ export async function onGenerationAfterCommandsController( params = {}, dryRun = false, ) { - debugWarn("[ST-BME:DIAG] GENERATION_AFTER_COMMANDS fired", { type, dryRun, paramsKeys: Object.keys(params || {}) }); if (dryRun) { - debugWarn("[ST-BME:DIAG] EXIT: dryRun=true"); return; } @@ -424,11 +414,9 @@ export async function onGenerationAfterCommandsController( ? runtime.consumeHostGenerationInputSnapshot?.({ preserve: true }) || runtime.consumeHostGenerationInputSnapshot?.() : null; - debugWarn("[ST-BME:DIAG] frozenInputSnapshot:", frozenInputSnapshot?.text ? `"${frozenInputSnapshot.text.slice(0,50)}"` : "(empty)", "fresh:", !!frozenInputSnapshot?.at); const context = runtime.getContext(); const chat = context?.chat; - debugWarn("[ST-BME:DIAG] chat length:", chat?.length, "last msg:", chat?.length ? { is_user: chat[chat.length-1]?.is_user, mes: (chat[chat.length-1]?.mes||"").slice(0,50) } : "(no chat)"); const recallOptions = runtime.buildGenerationAfterCommandsRecallInput( type, @@ -439,14 +427,11 @@ export async function onGenerationAfterCommandsController( chat, ); if (!recallOptions) { - debugWarn("[ST-BME:DIAG] EXIT: buildGenerationAfterCommandsRecallInput returned null"); return; } if (recallOptions?.__trivialSkip) { - debugWarn("[ST-BME:DIAG] EXIT: trivial-input-skip"); return; } - debugWarn("[ST-BME:DIAG] recallOptions:", { generationType: recallOptions.generationType, overrideUserMessage: recallOptions.overrideUserMessage?.slice(0,50), overrideSource: recallOptions.overrideSource, targetIdx: recallOptions.targetUserMessageIndex }); const recallContext = runtime.createGenerationRecallContext({ hookName: "GENERATION_AFTER_COMMANDS", @@ -454,10 +439,8 @@ export async function onGenerationAfterCommandsController( recallOptions, }); if (!recallContext.shouldRun && !recallContext.transaction) { - debugWarn("[ST-BME:DIAG] EXIT: shouldRun=false, no transaction. guardReason:", recallContext.guardReason); return; } - debugWarn("[ST-BME:DIAG] recallContext:", { shouldRun: recallContext.shouldRun, guardReason: recallContext.guardReason, transactionId: recallContext.transaction?.id }); const runtimeRecallOptions = recallContext.recallOptions || recallOptions || {}; @@ -470,7 +453,6 @@ export async function onGenerationAfterCommandsController( let recallResult = runtime.getGenerationRecallTransactionResult?.( recallContext.transaction, ); - debugWarn("[ST-BME:DIAG] deliveryMode:", deliveryMode, "shouldRun:", recallContext.shouldRun); if (recallContext.shouldRun) { runtime.markGenerationRecallTransactionHookState( @@ -481,7 +463,6 @@ export async function onGenerationAfterCommandsController( if (deliveryMode === "deferred") { runtime.clearLiveRecallInjectionPromptForRewrite?.(); } - debugWarn("[ST-BME:DIAG] >>> Starting runRecall..."); recallResult = await runtime.runRecall({ ...runtimeRecallOptions, deliveryMode, @@ -489,7 +470,6 @@ export async function onGenerationAfterCommandsController( hookName: recallContext.hookName, signal: params?.signal, }); - debugWarn("[ST-BME:DIAG] <<< runRecall finished:", { status: recallResult?.status, ok: recallResult?.ok, reason: recallResult?.reason, injectionText: recallResult?.injectionText?.slice(0,80) }); runtime.storeGenerationRecallTransactionResult?.( recallContext.transaction, recallResult, @@ -522,7 +502,6 @@ export async function onGenerationAfterCommandsController( // 上面的兜底补写会把 fresh recall 绑定回最终 user 楼层。 // 这里再补一次 UI 刷新,避免需要等到消息编辑/历史恢复后才看到 Recall Card。 runtime.refreshPersistedRecallMessageUi?.(); - debugWarn("[ST-BME:DIAG] DONE: immediate mode, injection via setExtensionPrompt in runRecall"); return recallResult; } @@ -556,7 +535,6 @@ export async function onBeforeCombinePromptsController( frozenInputSnapshot, }); if (normalInput?.__trivialSkip) { - debugWarn("[ST-BME:DIAG] EXIT: trivial-input-skip"); return { skipped: true, reason: `trivial:${normalInput.trivialReason || ""}`, diff --git a/recall-controller.js b/recall-controller.js index d1a70bb..8a74407 100644 --- a/recall-controller.js +++ b/recall-controller.js @@ -1,6 +1,6 @@ // ST-BME: 召回输入解析与注入控制器(纯函数) -import { debugLog, debugWarn } from "./debug-logging.js"; +import { debugLog } from "./debug-logging.js"; export function buildRecallRecentMessagesController( chat, @@ -289,12 +289,10 @@ export function applyRecallInjectionController( } export async function runRecallController(runtime, options = {}) { - debugWarn("[ST-BME:DIAG:RECALL] runRecallController entered"); if (runtime.getIsRecalling()) { runtime.abortRecallStageWithReason("旧召回已取消,正在启动新的召回"); const settle = await runtime.waitForActiveRecallToSettle(); if (!settle.settled && runtime.getIsRecalling()) { - debugWarn("[ST-BME:DIAG:RECALL] EXIT: 上一轮召回仍在清理"); runtime.setLastRecallStatus( "召回忙", "上一轮召回仍在清理,请稍后重试", @@ -310,18 +308,14 @@ export async function runRecallController(runtime, options = {}) { } const hasGraph = !!runtime.getCurrentGraph(); - debugWarn("[ST-BME:DIAG:RECALL] hasGraph:", hasGraph); if (!hasGraph) { - debugWarn("[ST-BME:DIAG:RECALL] EXIT: 当前无图谱"); return runtime.createRecallRunResult("skipped", { reason: "当前无图谱", }); } const settings = runtime.getSettings(); - debugWarn("[ST-BME:DIAG:RECALL] settings.enabled:", settings.enabled, "recallEnabled:", settings.recallEnabled); if (!settings.enabled || !settings.recallEnabled) { - debugWarn("[ST-BME:DIAG:RECALL] EXIT: 召回功能未启用"); return runtime.createRecallRunResult("skipped", { reason: "召回功能未启用", }); @@ -330,12 +324,8 @@ export async function runRecallController(runtime, options = {}) { typeof runtime.isGraphReadableForRecall === "function" ? runtime.isGraphReadableForRecall() : runtime.isGraphReadable(); - const chatId = typeof runtime.getCurrentChatId === "function" ? runtime.getCurrentChatId() : "(no fn)"; - const loadState = runtime.getGraphPersistenceLoadState?.() || "(no fn)"; - debugWarn("[ST-BME:DIAG:RECALL] isReadableForRecall:", isReadableForRecall, "chatId:", chatId, "loadState:", loadState); if (!isReadableForRecall) { const reason = runtime.getGraphMutationBlockReason("召回"); - debugWarn("[ST-BME:DIAG:RECALL] EXIT: 图谱不可读 -", reason); runtime.setLastRecallStatus("等待图谱加载", reason, "warning", { syncRuntime: true, }); From 6d861fd39c72aba26452774c1c79b9ae0580fa23 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Mon, 6 Apr 2026 20:39:51 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E5=9B=BE=E8=B0=B1=E5=88=86?= =?UTF-8?q?=E5=8C=BA=E7=94=A8=E9=85=92=E9=A6=86=20name1=20=E5=85=9C?= =?UTF-8?q?=E5=BA=95=EF=BC=8C=E8=AF=AF=E6=A0=87=E4=B8=BA=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E7=9A=84=E7=94=A8=E6=88=B7=20POV=20=E5=BD=92=E5=85=A5=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- graph-renderer.js | 66 ++++++++++++++++++++++++++++++++++++++++++-- panel.js | 28 ++++++++++++++++--- recall-message-ui.js | 19 ++++++++++++- 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/graph-renderer.js b/graph-renderer.js index 6754398..80ba2ae 100644 --- a/graph-renderer.js +++ b/graph-renderer.js @@ -95,6 +95,46 @@ function normalizeKeyForPartition(value) { return String(value ?? '').trim().toLowerCase(); } +/** + * 将宿主侧「用户显示名」候选归一为分区用 Set,用于把误标为 character 的用户 POV 拉回用户区。 + * @param {string|string[]|{name1?:string,userName?:string,personaName?:string,aliases?:string[]}|null|undefined} hints + * @returns {Set} + */ +export function buildUserPovAliasNormalizedSet(hints) { + const set = new Set(); + const add = (v) => { + const k = normalizeKeyForPartition(v); + if (k) set.add(k); + }; + if (hints == null) return set; + if (typeof hints === 'string') { + add(hints); + return set; + } + if (Array.isArray(hints)) { + for (const item of hints) add(item); + return set; + } + if (typeof hints === 'object') { + add(hints.name1); + add(hints.userName); + add(hints.personaName); + if (Array.isArray(hints.aliases)) { + for (const a of hints.aliases) add(a); + } + } + return set; +} + +function scopeMatchesHostUserAliases(scope, aliasSet) { + if (!(aliasSet instanceof Set) || aliasSet.size === 0) return false; + const nameKey = normalizeKeyForPartition(scope.ownerName); + const idKey = normalizeKeyForPartition(scope.ownerId); + if (nameKey && aliasSet.has(nameKey)) return true; + if (idKey && aliasSet.has(idKey)) return true; + return false; +} + function characterPovLabelFromNodes(arr) { if (!arr?.length) return '·'; for (const n of arr) { @@ -108,10 +148,12 @@ function characterPovLabelFromNodes(arr) { return '·'; } -function partitionNodesByScope(nodes) { +function partitionNodesByScope(nodes, userPovAliasSet = null) { const objective = []; const userPov = []; const charMap = new Map(); + const aliasSet = + userPovAliasSet instanceof Set ? userPovAliasSet : new Set(); for (const node of nodes) { const scope = normalizeMemoryScope(node.raw?.scope); @@ -120,6 +162,12 @@ function partitionNodesByScope(nodes) { node.regionKey = 'objective'; continue; } + // 优先:宿主用户显示名与 ownerName/ownerId 一致时一律归用户 POV(修正提取阶段误标 character) + if (scopeMatchesHostUserAliases(scope, aliasSet)) { + userPov.push(node); + node.regionKey = 'user'; + continue; + } if (scope.ownerType === 'user') { userPov.push(node); node.regionKey = 'user'; @@ -166,6 +214,9 @@ export class GraphRenderer { this.colors = getNodeColors(themeName); this.themeName = themeName; this.config = { ...DEFAULT_LAYOUT_CONFIG, ...fromForce, ...layoutOverride }; + this._userPovAliasSet = buildUserPovAliasNormalizedSet( + isLegacy ? null : options?.userPovAliases, + ); this._regionPanels = []; this._lastGraph = null; @@ -200,10 +251,19 @@ export class GraphRenderer { * 加载图谱数据 * @param {object} graph - 完整的 graph state */ - loadGraph(graph) { + /** + * @param {object} graph + * @param {{ userPovAliases?: string|string[]|object }} [layoutHints] + */ + loadGraph(graph, layoutHints = {}) { const prevSelectedId = this.selectedNode?.id || null; this.nodeMap.clear(); this._lastGraph = graph; + if (layoutHints && Object.prototype.hasOwnProperty.call(layoutHints, 'userPovAliases')) { + this._userPovAliasSet = buildUserPovAliasNormalizedSet( + layoutHints.userPovAliases, + ); + } const dpr = window.devicePixelRatio || 1; const W = this.canvas.width / dpr; @@ -239,7 +299,7 @@ export class GraphRenderer { relation: e.relation || 'related', })); - const parts = partitionNodesByScope(this.nodes); + const parts = partitionNodesByScope(this.nodes, this._userPovAliasSet); this._regionPanels = this._computeRegionPanels(W, H, parts); this._layoutAllPartitions(parts); this._simulateNeuralWithinRegions(this.config.neuralIterations); diff --git a/panel.js b/panel.js index b3f897c..65132a1 100644 --- a/panel.js +++ b/panel.js @@ -1,6 +1,7 @@ // ST-BME: 操控面板交互逻辑 import { callGenericPopup, POPUP_TYPE } from "../../../popup.js"; +import { getContext } from "../../../extensions.js"; import { renderTemplateAsync } from "../../../templates.js"; import { GraphRenderer } from "./graph-renderer.js"; import { getNodeDisplayName } from "./node-labels.js"; @@ -672,15 +673,19 @@ export function openPanel() { const settings = _getSettings?.() || {}; const themeName = settings.panelTheme || "crimson"; + const graphOpts = { + theme: themeName, + userPovAliases: _hostUserPovAliasHintsForGraph(), + }; const canvas = document.getElementById("bme-graph-canvas"); if (canvas && !graphRenderer && !isMobile) { - graphRenderer = new GraphRenderer(canvas, themeName); + graphRenderer = new GraphRenderer(canvas, graphOpts); graphRenderer.onNodeSelect = (node) => _showNodeDetail(node); } const mobileCanvas = document.getElementById("bme-mobile-graph-canvas"); if (mobileCanvas && !mobileGraphRenderer && isMobile) { - mobileGraphRenderer = new GraphRenderer(mobileCanvas, themeName); + mobileGraphRenderer = new GraphRenderer(mobileCanvas, graphOpts); mobileGraphRenderer.onNodeSelect = (node) => _showNodeDetail(node); } @@ -1203,11 +1208,26 @@ async function _refreshInjectionPreview() { // ==================== 图谱 ==================== +/** SillyTavern 用户显示名(name1),用于图谱分区:误标为角色的用户 POV 强制归用户区 */ +function _hostUserPovAliasHintsForGraph() { + try { + const ctx = typeof getContext === "function" ? getContext() : null; + const out = []; + if (ctx?.name1 && String(ctx.name1).trim()) { + out.push(String(ctx.name1).trim()); + } + return out; + } catch { + return []; + } +} + function _refreshGraph() { const graph = _getGraph?.(); if (!graph) return; - graphRenderer?.loadGraph(graph); - mobileGraphRenderer?.loadGraph(graph); + const hints = { userPovAliases: _hostUserPovAliasHintsForGraph() }; + graphRenderer?.loadGraph(graph, hints); + mobileGraphRenderer?.loadGraph(graph, hints); } function _buildLegend() { diff --git a/recall-message-ui.js b/recall-message-ui.js index 7a37b97..78b8d91 100644 --- a/recall-message-ui.js +++ b/recall-message-ui.js @@ -1,8 +1,22 @@ // ST-BME: 消息级召回卡片 UI // 纯 DOM 构建模块,不含模块级 mutable state +import { getContext } from "../../../extensions.js"; import { GraphRenderer } from "./graph-renderer.js"; +function _hostUserPovAliasHintsForRecallCanvas() { + try { + const ctx = typeof getContext === "function" ? getContext() : null; + const out = []; + if (ctx?.name1 && String(ctx.name1).trim()) { + out.push(String(ctx.name1).trim()); + } + return out; + } catch { + return []; + } +} + // ==================== 常量 ==================== export const RECALL_CARD_FORCE_CONFIG = { @@ -297,6 +311,7 @@ export function createRecallCardElement({ renderer = new GraphRenderer(canvas, { theme: themeName, forceConfig: RECALL_CARD_FORCE_CONFIG, + userPovAliases: _hostUserPovAliasHintsForRecallCanvas(), onNodeClick: (node) => { if (typeof activeCallbacks.onNodeClick === "function") { activeCallbacks.onNodeClick(messageIndex, node); @@ -308,7 +323,9 @@ export function createRecallCardElement({ } }, }); - renderer.loadGraph(resolvedSubGraph); + renderer.loadGraph(resolvedSubGraph, { + userPovAliases: _hostUserPovAliasHintsForRecallCanvas(), + }); } // 元信息行 From 757421d16b09f749462e3b168700bc27498ad78e Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Mon, 6 Apr 2026 20:42:41 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=20POV=20?= =?UTF-8?q?=E5=88=AB=E5=90=8D=E5=8C=B9=E9=85=8D=E5=BF=BD=E7=95=A5=E7=A9=BA?= =?UTF-8?q?=E7=99=BD/=E6=A0=87=E7=82=B9/=E5=A4=A7=E5=B0=8F=E5=86=99?= =?UTF-8?q?=E5=B9=B6=20NFKC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- graph-renderer.js | 69 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/graph-renderer.js b/graph-renderer.js index 80ba2ae..12f4f61 100644 --- a/graph-renderer.js +++ b/graph-renderer.js @@ -95,6 +95,49 @@ function normalizeKeyForPartition(value) { return String(value ?? '').trim().toLowerCase(); } +/** + * 宿主别名与 POV owner 比对:忽略大小写、多空格、常见中英文标点/符号差(NFKC)。 + * 不用于 charMap 主键,仅用于「是否同一用户」的宽松匹配。 + */ +function normalizeAliasMatchKey(value) { + let s = String(value ?? ''); + if (typeof s.normalize === 'function') { + try { + s = s.normalize('NFKC'); + } catch { + /* ignore */ + } + } + s = s.trim().toLowerCase(); + // 标点、间隔号、各类空白等统一成空格,再压成单空格 + s = s.replace( + /[\s!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~\u00b7\u3000-\u303f\uff01-\uff0f\uff1a-\uff20\uff3b-\uff40\uff5b-\uff65\u2000-\u206f\u2e00-\u2e7f]+/g, + ' ', + ); + s = s.replace(/\s+/g, ' ').trim(); + return s; +} + +/** 同一名称的多种可比形式(兼容老数据只做了 trim+lower) */ +function collectAliasMatchVariants(raw) { + const variants = []; + const leg = normalizeKeyForPartition(raw); + if (leg) variants.push(leg); + const soft = normalizeAliasMatchKey(raw); + if (soft) { + variants.push(soft); + const compact = soft.replace(/\s/g, ''); + if (compact && compact !== soft) variants.push(compact); + } + return variants; +} + +function addAliasMatchVariantsToSet(set, raw) { + for (const k of collectAliasMatchVariants(raw)) { + if (k) set.add(k); + } +} + /** * 将宿主侧「用户显示名」候选归一为分区用 Set,用于把误标为 character 的用户 POV 拉回用户区。 * @param {string|string[]|{name1?:string,userName?:string,personaName?:string,aliases?:string[]}|null|undefined} hints @@ -102,25 +145,22 @@ function normalizeKeyForPartition(value) { */ export function buildUserPovAliasNormalizedSet(hints) { const set = new Set(); - const add = (v) => { - const k = normalizeKeyForPartition(v); - if (k) set.add(k); - }; if (hints == null) return set; + const ingest = (v) => addAliasMatchVariantsToSet(set, v); if (typeof hints === 'string') { - add(hints); + ingest(hints); return set; } if (Array.isArray(hints)) { - for (const item of hints) add(item); + for (const item of hints) ingest(item); return set; } if (typeof hints === 'object') { - add(hints.name1); - add(hints.userName); - add(hints.personaName); + ingest(hints.name1); + ingest(hints.userName); + ingest(hints.personaName); if (Array.isArray(hints.aliases)) { - for (const a of hints.aliases) add(a); + for (const a of hints.aliases) ingest(a); } } return set; @@ -128,10 +168,11 @@ export function buildUserPovAliasNormalizedSet(hints) { function scopeMatchesHostUserAliases(scope, aliasSet) { if (!(aliasSet instanceof Set) || aliasSet.size === 0) return false; - const nameKey = normalizeKeyForPartition(scope.ownerName); - const idKey = normalizeKeyForPartition(scope.ownerId); - if (nameKey && aliasSet.has(nameKey)) return true; - if (idKey && aliasSet.has(idKey)) return true; + for (const field of [scope.ownerName, scope.ownerId]) { + for (const k of collectAliasMatchVariants(field)) { + if (k && aliasSet.has(k)) return true; + } + } return false; } From 935e1d78392357c105820ba62bf2f21b34a69bad Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Mon, 6 Apr 2026 21:26:47 +0800 Subject: [PATCH 5/5] Refine extraction stage notices for post-processing --- index.js | 43 ++++++++++++++++++++++++ tests/p0-regressions.mjs | 72 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/index.js b/index.js index 75c73e6..7b00c7f 100644 --- a/index.js +++ b/index.js @@ -7685,12 +7685,27 @@ async function handleExtractionSuccess( typeof recordMaintenanceAction === "function" ? recordMaintenanceAction : () => null; + const updateExtractionPostProcessStatus = ( + text, + meta, + { noticeMarquee = false } = {}, + ) => { + if (typeof setLastExtractionStatus !== "function") return; + setLastExtractionStatus(text, meta, "running", { + syncRuntime: true, + noticeMarquee, + }); + }; throwIfAborted(signal, "提取已终止"); extractionCount++; ensureCurrentGraphRuntimeState(); currentGraph.historyState.extractionCount = extractionCount; updateLastExtractedItems(result.newNodeIds || []); setBatchStageOutcome(status, "core", "success"); + updateExtractionPostProcessStatus( + "提取收尾中", + `已抽取 ${newNodeCount} 个新节点,正在处理后续阶段`, + ); if (settings.enableConsolidation && result.newNodeIds?.length > 0) { let consolidationAnalysis = null; @@ -7702,6 +7717,10 @@ async function handleExtractionSuccess( ), ); if (newNodeCount < minNewNodes) { + updateExtractionPostProcessStatus( + "整合判定中", + `本批新增 ${newNodeCount} 个节点,正在检查是否需要自动整合/进化`, + ); consolidationAnalysis = await analyzeConsolidationGate({ graph: currentGraph, newNodeIds: result.newNodeIds, @@ -7729,6 +7748,10 @@ async function handleExtractionSuccess( pushBatchStageArtifact(status, "structural", "consolidation-skipped"); } else { try { + updateExtractionPostProcessStatus( + "整合/进化中", + String(gate.reason || "").trim() || "正在自动整合新旧记忆", + ); const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph); const consolidationResult = await consolidateMemories({ graph: currentGraph, @@ -7772,6 +7795,10 @@ async function handleExtractionSuccess( extractionCount % settings.synopsisEveryN === 0 ) { try { + updateExtractionPostProcessStatus( + "概要更新中", + `第 ${extractionCount} 次提取,正在生成全局概要`, + ); await generateSynopsis({ graph: currentGraph, schema: getSchema(), @@ -7799,6 +7826,10 @@ async function handleExtractionSuccess( extractionCount % settings.reflectEveryN === 0 ) { try { + updateExtractionPostProcessStatus( + "反思生成中", + `第 ${extractionCount} 次提取,正在生成长期反思`, + ); await generateReflection({ graph: currentGraph, currentSeq: endIdx, @@ -7825,6 +7856,10 @@ async function handleExtractionSuccess( extractionCount % settings.sleepEveryN === 0 ) { try { + updateExtractionPostProcessStatus( + "主动遗忘中", + `第 ${extractionCount} 次提取,正在归档低价值记忆`, + ); const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph); const sleepResult = sleepCycle(currentGraph, settings); if ((sleepResult?.forgotten || 0) > 0) { @@ -7872,6 +7907,10 @@ async function handleExtractionSuccess( "已到自动压缩周期,但当前没有达到内部压缩阈值的候选组"; pushBatchStageArtifact(status, "structural", "compression-skipped"); } else { + updateExtractionPostProcessStatus( + "自动压缩中", + `已到第 ${extractionCount} 次提取周期,正在压缩层级记忆`, + ); status.autoCompressionSkippedReason = ""; const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph); const compressionResult = await compressAll( @@ -7916,6 +7955,10 @@ async function handleExtractionSuccess( let vectorSync = null; try { + updateExtractionPostProcessStatus( + "向量同步中", + "正在同步本批提取后的向量索引", + ); vectorSync = await syncVectorState({ signal }); } catch (error) { if (isAbortError(error)) throw error; diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs index 77caf93..8028d37 100644 --- a/tests/p0-regressions.mjs +++ b/tests/p0-regressions.mjs @@ -236,6 +236,7 @@ function createBatchStageHarness() { result: null, extractionCount: 0, currentGraph: null, + extractionStatuses: [], consolidateMemories: async () => {}, generateSynopsis: async () => {}, generateReflection: async () => {}, @@ -271,6 +272,9 @@ function createBatchStageHarness() { pushBatchStageArtifact, finalizeBatchStatus, createUiStatus, + setLastExtractionStatus(...args) { + context.extractionStatuses.push(args); + }, }; vm.createContext(context); vm.runInContext( @@ -2604,6 +2608,73 @@ async function testBatchStatusSemanticFailureDoesNotHideCoreSuccess() { assert.match(effects.batchStatus.errors[0], /概要生成失败/); } +async function testExtractionPostProcessStatusesExposeMaintenancePhases() { + const harness = await createBatchStageHarness(); + const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result; + harness.currentGraph = { + historyState: { extractionCount: 0 }, + vectorIndexState: {}, + }; + harness.ensureCurrentGraphRuntimeState = () => { + harness.currentGraph.historyState ||= {}; + harness.currentGraph.vectorIndexState ||= {}; + }; + harness.consolidateMemories = async () => ({ + merged: 1, + skipped: 0, + kept: 0, + evolved: 1, + connections: 0, + updates: 0, + }); + harness.generateSynopsis = async () => ({ ok: true }); + harness.generateReflection = async () => ({ ok: true }); + harness.sleepCycle = () => ({ forgotten: 0 }); + harness.inspectAutoCompressionCandidates = () => ({ + hasCandidates: true, + reason: "", + }); + harness.compressAll = async () => ({ created: 1, archived: 2 }); + harness.syncVectorState = async () => ({ + insertedHashes: ["hash-stage"], + stats: { pending: 0, indexed: 3 }, + }); + + const batchStatus = createBatchStatusSkeleton({ + processedRange: [8, 8], + extractionCountBefore: 0, + }); + await handleExtractionSuccess( + { + newNodeIds: ["node-stage"], + }, + 8, + { + enableConsolidation: true, + consolidationAutoMinNewNodes: 1, + enableSynopsis: true, + synopsisEveryN: 1, + enableReflection: true, + reflectEveryN: 1, + enableSleepCycle: true, + sleepEveryN: 1, + enableAutoCompression: true, + compressionEveryN: 1, + }, + undefined, + batchStatus, + ); + + const statusTexts = harness.extractionStatuses.map((entry) => entry[0]); + assert.ok(statusTexts.includes("提取收尾中")); + assert.ok(statusTexts.includes("整合/进化中")); + assert.ok(statusTexts.includes("概要更新中")); + assert.ok(statusTexts.includes("反思生成中")); + assert.ok(statusTexts.includes("主动遗忘中")); + assert.ok(statusTexts.includes("自动压缩中")); + assert.ok(statusTexts.includes("向量同步中")); +} + async function testAutoConsolidationRunsOnHighDuplicateRiskSingleNode() { const harness = await createBatchStageHarness(); const { createBatchStatusSkeleton, handleExtractionSuccess } = harness.result; @@ -5474,6 +5545,7 @@ await testReverseJournalRollbackStateFormsReplayClosure(); await testReverseJournalRecoveryPlanMixedLegacyAndCurrentRetainsRepairSet(); await testBatchStatusStructuralPartialRemainsRecoverable(); await testBatchStatusSemanticFailureDoesNotHideCoreSuccess(); +await testExtractionPostProcessStatusesExposeMaintenancePhases(); await testAutoConsolidationRunsOnHighDuplicateRiskSingleNode(); await testAutoConsolidationSkipsLowRiskSingleNode(); await testAutoCompressionRunsOnlyOnConfiguredInterval();