mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Merge branch 'Youzini-afk:main' into main
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -261,6 +253,10 @@ export function onMessageSentController(runtime, messageId) {
|
||||
resolvedMessageId,
|
||||
message.mes || "",
|
||||
);
|
||||
// GENERATION_AFTER_COMMANDS 在 sendMessageAsUser 之前触发,此时新用户消息
|
||||
// 尚未进入 chat,recall 记录会被写到上一条 user 上。这里用户消息刚入场,
|
||||
// transaction 仍在桥接窗口内,立即把记录重新绑定到正确的楼层。
|
||||
runtime.rebindRecallRecordToNewUserMessage?.(resolvedMessageId);
|
||||
runtime.refreshPersistedRecallMessageUi?.();
|
||||
}
|
||||
|
||||
@@ -408,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;
|
||||
}
|
||||
|
||||
@@ -420,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,
|
||||
@@ -435,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",
|
||||
@@ -450,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 || {};
|
||||
@@ -466,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(
|
||||
@@ -477,7 +463,6 @@ export async function onGenerationAfterCommandsController(
|
||||
if (deliveryMode === "deferred") {
|
||||
runtime.clearLiveRecallInjectionPromptForRewrite?.();
|
||||
}
|
||||
debugWarn("[ST-BME:DIAG] >>> Starting runRecall...");
|
||||
recallResult = await runtime.runRecall({
|
||||
...runtimeRecallOptions,
|
||||
deliveryMode,
|
||||
@@ -485,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,
|
||||
@@ -518,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;
|
||||
}
|
||||
|
||||
@@ -552,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 || ""}`,
|
||||
|
||||
@@ -95,6 +95,87 @@ 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
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
export function buildUserPovAliasNormalizedSet(hints) {
|
||||
const set = new Set();
|
||||
if (hints == null) return set;
|
||||
const ingest = (v) => addAliasMatchVariantsToSet(set, v);
|
||||
if (typeof hints === 'string') {
|
||||
ingest(hints);
|
||||
return set;
|
||||
}
|
||||
if (Array.isArray(hints)) {
|
||||
for (const item of hints) ingest(item);
|
||||
return set;
|
||||
}
|
||||
if (typeof hints === 'object') {
|
||||
ingest(hints.name1);
|
||||
ingest(hints.userName);
|
||||
ingest(hints.personaName);
|
||||
if (Array.isArray(hints.aliases)) {
|
||||
for (const a of hints.aliases) ingest(a);
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
function scopeMatchesHostUserAliases(scope, aliasSet) {
|
||||
if (!(aliasSet instanceof Set) || aliasSet.size === 0) return false;
|
||||
for (const field of [scope.ownerName, scope.ownerId]) {
|
||||
for (const k of collectAliasMatchVariants(field)) {
|
||||
if (k && aliasSet.has(k)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function characterPovLabelFromNodes(arr) {
|
||||
if (!arr?.length) return '·';
|
||||
for (const n of arr) {
|
||||
@@ -108,10 +189,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 +203,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 +255,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 +292,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 +340,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);
|
||||
|
||||
91
index.js
91
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,
|
||||
{
|
||||
@@ -7638,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;
|
||||
@@ -7655,6 +7717,10 @@ async function handleExtractionSuccess(
|
||||
),
|
||||
);
|
||||
if (newNodeCount < minNewNodes) {
|
||||
updateExtractionPostProcessStatus(
|
||||
"整合判定中",
|
||||
`本批新增 ${newNodeCount} 个节点,正在检查是否需要自动整合/进化`,
|
||||
);
|
||||
consolidationAnalysis = await analyzeConsolidationGate({
|
||||
graph: currentGraph,
|
||||
newNodeIds: result.newNodeIds,
|
||||
@@ -7682,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,
|
||||
@@ -7725,6 +7795,10 @@ async function handleExtractionSuccess(
|
||||
extractionCount % settings.synopsisEveryN === 0
|
||||
) {
|
||||
try {
|
||||
updateExtractionPostProcessStatus(
|
||||
"概要更新中",
|
||||
`第 ${extractionCount} 次提取,正在生成全局概要`,
|
||||
);
|
||||
await generateSynopsis({
|
||||
graph: currentGraph,
|
||||
schema: getSchema(),
|
||||
@@ -7752,6 +7826,10 @@ async function handleExtractionSuccess(
|
||||
extractionCount % settings.reflectEveryN === 0
|
||||
) {
|
||||
try {
|
||||
updateExtractionPostProcessStatus(
|
||||
"反思生成中",
|
||||
`第 ${extractionCount} 次提取,正在生成长期反思`,
|
||||
);
|
||||
await generateReflection({
|
||||
graph: currentGraph,
|
||||
currentSeq: endIdx,
|
||||
@@ -7778,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) {
|
||||
@@ -7825,6 +7907,10 @@ async function handleExtractionSuccess(
|
||||
"已到自动压缩周期,但当前没有达到内部压缩阈值的候选组";
|
||||
pushBatchStageArtifact(status, "structural", "compression-skipped");
|
||||
} else {
|
||||
updateExtractionPostProcessStatus(
|
||||
"自动压缩中",
|
||||
`已到第 ${extractionCount} 次提取周期,正在压缩层级记忆`,
|
||||
);
|
||||
status.autoCompressionSkippedReason = "";
|
||||
const beforeSnapshot = cloneMaintenanceSnapshot(currentGraph);
|
||||
const compressionResult = await compressAll(
|
||||
@@ -7869,6 +7955,10 @@ async function handleExtractionSuccess(
|
||||
|
||||
let vectorSync = null;
|
||||
try {
|
||||
updateExtractionPostProcessStatus(
|
||||
"向量同步中",
|
||||
"正在同步本批提取后的向量索引",
|
||||
);
|
||||
vectorSync = await syncVectorState({ signal });
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) throw error;
|
||||
@@ -9235,6 +9325,7 @@ function onMessageSent(messageId) {
|
||||
getContext,
|
||||
isTrivialUserInput,
|
||||
recordRecallSentUserMessage,
|
||||
rebindRecallRecordToNewUserMessage,
|
||||
refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh,
|
||||
},
|
||||
messageId,
|
||||
|
||||
28
panel.js
28
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() {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
// 元信息行
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user