feat: 增强运行阶段前端状态反馈

This commit is contained in:
Youzini-afk
2026-03-25 00:05:53 +08:00
parent c847d89149
commit 63ee782028
3 changed files with 206 additions and 39 deletions

212
index.js
View File

@@ -167,11 +167,6 @@ let isRecalling = false;
let lastInjectionContent = "";
let lastExtractedItems = []; // 最近提取的节点(面板展示用)
let lastRecalledItems = []; // 最近召回的节点(面板展示用)
let lastRecallStatus = {
text: "待命",
meta: "尚未执行召回",
level: "idle",
};
let extractionCount = 0; // v2: 提取次数计数器(定期触发概要/遗忘/反思)
let serverSettingsSaveTimer = null;
let isRecoveringHistory = false;
@@ -179,6 +174,21 @@ let lastHistoryWarningAt = 0;
let lastRecallFallbackNoticeAt = 0;
let lastExtractionWarningAt = 0;
const LOCAL_VECTOR_TIMEOUT_MS = 30000;
const STATUS_TOAST_THROTTLE_MS = 1500;
let runtimeStatus = createUiStatus("待命", "准备就绪", "idle");
let lastExtractionStatus = createUiStatus("待命", "尚未执行提取", "idle");
let lastVectorStatus = createUiStatus("待命", "尚未执行向量任务", "idle");
let lastRecallStatus = createUiStatus("待命", "尚未执行召回", "idle");
const lastStatusToastAt = {};
function createUiStatus(text = "待命", meta = "", level = "idle") {
return {
text: String(text || "待命"),
meta: String(meta || ""),
level,
updatedAt: Date.now(),
};
}
function getNodeDisplayName(node) {
return (
@@ -288,11 +298,8 @@ function ensureCurrentGraphRuntimeState() {
function clearInjectionState() {
lastInjectionContent = "";
lastRecalledItems = [];
lastRecallStatus = {
text: "待命",
meta: "当前无有效注入内容",
level: "idle",
};
lastRecallStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
runtimeStatus = createUiStatus("待命", "当前无有效注入内容", "idle");
try {
const context = getContext();
@@ -313,16 +320,73 @@ function refreshPanelLiveState() {
_panelModule?.refreshLiveState?.();
}
function setLastRecallStatus(text, meta, level = "info") {
lastRecallStatus = {
text: String(text || "待命"),
meta: String(meta || ""),
level,
};
function notifyStatusToast(key, kind, message, title = "ST-BME") {
const now = Date.now();
if (now - (lastStatusToastAt[key] || 0) < STATUS_TOAST_THROTTLE_MS) return;
lastStatusToastAt[key] = now;
const method = typeof toastr?.[kind] === "function" ? kind : "info";
toastr[method](message, title, { timeOut: 2200 });
}
function setRuntimeStatus(text, meta, level = "info") {
runtimeStatus = createUiStatus(text, meta, level);
refreshPanelLiveState();
}
function setLastExtractionStatus(
text,
meta,
level = "info",
{ syncRuntime = true, toastKind = "", toastTitle = "ST-BME 提取" } = {},
) {
lastExtractionStatus = createUiStatus(text, meta, level);
if (syncRuntime) {
setRuntimeStatus(text, meta, level);
} else {
refreshPanelLiveState();
}
if (toastKind) {
notifyStatusToast(`extract:${toastKind}`, toastKind, meta || text, toastTitle);
}
}
function setLastVectorStatus(
text,
meta,
level = "info",
{ syncRuntime = false, toastKind = "", toastTitle = "ST-BME 向量" } = {},
) {
lastVectorStatus = createUiStatus(text, meta, level);
if (syncRuntime) {
setRuntimeStatus(text, meta, level);
} else {
refreshPanelLiveState();
}
if (toastKind) {
notifyStatusToast(`vector:${toastKind}`, toastKind, meta || text, toastTitle);
}
}
function setLastRecallStatus(
text,
meta,
level = "info",
{ syncRuntime = true, toastKind = "", toastTitle = "ST-BME 召回" } = {},
) {
lastRecallStatus = createUiStatus(text, meta, level);
if (syncRuntime) {
setRuntimeStatus(text, meta, level);
} else {
refreshPanelLiveState();
}
if (toastKind) {
notifyStatusToast(`recall:${toastKind}`, toastKind, meta || text, toastTitle);
}
}
function notifyExtractionIssue(message, title = "ST-BME 提取提示") {
setLastExtractionStatus("提取失败", message, "warning", { syncRuntime: true });
const now = Date.now();
if (now - lastExtractionWarningAt < 5000) return;
lastExtractionWarningAt = now;
@@ -362,6 +426,9 @@ function snapshotRuntimeUiState() {
lastRecalledItems: Array.isArray(lastRecalledItems)
? lastRecalledItems.map((item) => ({ ...item }))
: [],
runtimeStatus: { ...(runtimeStatus || {}) },
lastExtractionStatus: { ...(lastExtractionStatus || {}) },
lastVectorStatus: { ...(lastVectorStatus || {}) },
lastRecallStatus: { ...(lastRecallStatus || {}) },
};
}
@@ -377,10 +444,20 @@ function restoreRuntimeUiState(snapshot = {}) {
lastRecalledItems = Array.isArray(snapshot.lastRecalledItems)
? snapshot.lastRecalledItems.map((item) => ({ ...item }))
: [];
runtimeStatus = {
...createUiStatus("待命", "准备就绪", "idle"),
...(snapshot.runtimeStatus || {}),
};
lastExtractionStatus = {
...createUiStatus("待命", "尚未执行提取", "idle"),
...(snapshot.lastExtractionStatus || {}),
};
lastVectorStatus = {
...createUiStatus("待命", "尚未执行向量任务", "idle"),
...(snapshot.lastVectorStatus || {}),
};
lastRecallStatus = {
text: "待命",
meta: "尚未执行召回",
level: "idle",
...createUiStatus("待命", "尚未执行召回", "idle"),
...(snapshot.lastRecallStatus || {}),
};
refreshPanelLiveState();
@@ -470,12 +547,25 @@ async function syncVectorState({
range = null,
} = {}) {
ensureCurrentGraphRuntimeState();
const scopeLabel =
range && Number.isFinite(range.start) && Number.isFinite(range.end)
? `范围 ${Math.min(range.start, range.end)}-${Math.max(range.start, range.end)}`
: "当前聊天";
setLastVectorStatus(
"向量处理中",
`${scopeLabel} · ${force ? "强制同步" : "增量同步"}`,
"running",
{ syncRuntime: true },
);
const config = getEmbeddingConfig();
const validation = validateVectorConfig(config);
if (!validation.valid) {
currentGraph.vectorIndexState.lastWarning = validation.error;
currentGraph.vectorIndexState.dirty = true;
setLastVectorStatus("向量不可用", validation.error, "warning", {
syncRuntime: false,
});
return {
insertedHashes: [],
stats: getVectorIndexStats(currentGraph),
@@ -484,16 +574,27 @@ async function syncVectorState({
}
try {
return await syncGraphVectorIndex(currentGraph, config, {
const result = await syncGraphVectorIndex(currentGraph, config, {
chatId: getCurrentChatId(),
force,
purge,
range,
});
setLastVectorStatus(
"向量完成",
`${scopeLabel} · indexed ${result.stats?.indexed ?? 0} · pending ${result.stats?.pending ?? 0}`,
"success",
{ syncRuntime: false },
);
return result;
} catch (error) {
const message = error?.message || String(error) || "向量同步失败";
markVectorStateDirty(message);
console.error("[ST-BME] 向量同步失败:", error);
setLastVectorStatus("向量失败", message, "error", {
syncRuntime: true,
toastKind: "error",
});
return {
insertedHashes: [],
stats: getVectorIndexStats(currentGraph),
@@ -663,11 +764,10 @@ function updateModuleSettings(patch = {}) {
);
lastInjectionContent = "";
lastRecalledItems = [];
lastRecallStatus = {
text: "已停用",
meta: "插件已关闭,注入内容已清空",
level: "idle",
};
runtimeStatus = createUiStatus("已停用", "插件已关闭,注入内容已清空", "idle");
lastExtractionStatus = createUiStatus("已停用", "插件已关闭,自动提取已停止", "idle");
lastVectorStatus = createUiStatus("已停用", "插件已关闭,向量任务已停止", "idle");
lastRecallStatus = createUiStatus("已停用", "插件已关闭,注入内容已清空", "idle");
refreshPanelLiveState();
} catch (error) {
console.warn("[ST-BME] 关闭插件时清理注入失败:", error);
@@ -692,11 +792,10 @@ function loadGraphFromChat() {
lastExtractedItems = [];
lastRecalledItems = [];
lastInjectionContent = "";
lastRecallStatus = {
text: "待命",
meta: "当前聊天尚未建立记忆图谱",
level: "idle",
};
runtimeStatus = createUiStatus("待命", "当前聊天尚未建立记忆图谱", "idle");
lastExtractionStatus = createUiStatus("待命", "当前聊天尚未执行提取", "idle");
lastVectorStatus = createUiStatus("待命", "当前聊天尚未执行向量任务", "idle");
lastRecallStatus = createUiStatus("待命", "当前聊天尚未建立记忆图谱", "idle");
return;
}
@@ -712,11 +811,10 @@ function loadGraphFromChat() {
lastExtractedItems = [];
updateLastRecalledItems(currentGraph.lastRecallResult || []);
lastInjectionContent = "";
lastRecallStatus = {
text: "待命",
meta: "已加载聊天图谱,等待下一次召回",
level: "idle",
};
runtimeStatus = createUiStatus("待命", "已加载聊天图谱,等待下一次任务", "idle");
lastExtractionStatus = createUiStatus("待命", "已加载聊天图谱,等待下一次提取", "idle");
lastVectorStatus = createUiStatus("待命", currentGraph.vectorIndexState?.lastWarning || "已加载聊天图谱,等待下一次向量任务", "idle");
lastRecallStatus = createUiStatus("待命", "已加载聊天图谱,等待下一次召回", "idle");
}
function saveGraphToChat() {
@@ -1299,6 +1397,12 @@ async function runExtraction() {
: unprocessedAssistantTurns.slice(0, extractEvery);
const startIdx = batchAssistantTurns[0];
const endIdx = batchAssistantTurns[batchAssistantTurns.length - 1];
setLastExtractionStatus(
"提取中",
`楼层 ${startIdx}-${endIdx}${smartTriggerDecision.triggered ? " · 智能触发" : ""}`,
"running",
{ syncRuntime: true },
);
isExtracting = true;
@@ -1318,7 +1422,15 @@ async function runExtraction() {
"提取批次未返回有效结果";
console.warn("[ST-BME] 提取批次未返回有效结果:", message);
notifyExtractionIssue(message);
return;
}
setLastExtractionStatus(
"提取完成",
`楼层 ${startIdx}-${endIdx} · 新建 ${batchResult.result?.newNodes || 0} · 更新 ${batchResult.result?.updatedNodes || 0} · 新边 ${batchResult.result?.newEdges || 0}`,
"success",
{ syncRuntime: true },
);
} catch (e) {
console.error("[ST-BME] 提取失败:", e);
notifyExtractionIssue(e?.message || String(e) || "自动提取失败");
@@ -1381,6 +1493,7 @@ async function runRecall() {
"召回中",
`上下文 ${recentMessages.length} 条 · 当前用户消息长度 ${userMessage.length}`,
"running",
{ syncRuntime: true },
);
const result = await retrieve({
@@ -1454,6 +1567,10 @@ async function runRecall() {
llmLabel,
`ctx ${recentMessages.length} · vector ${retrievalMeta.vectorHits ?? 0} · diffusion ${retrievalMeta.diffusionHits ?? 0} · llm pool ${llmMeta.candidatePool ?? 0} · recall ${result.stats.recallCount}`,
llmMeta.status === "fallback" ? "warning" : "success",
{
syncRuntime: true,
toastKind: "",
},
);
if (llmMeta.status === "fallback") {
@@ -1470,7 +1587,10 @@ async function runRecall() {
} catch (e) {
console.error("[ST-BME] 召回失败:", e);
const message = e?.message || String(e);
setLastRecallStatus("召回失败", message, "error");
setLastRecallStatus("召回失败", message, "error", {
syncRuntime: true,
toastKind: "",
});
toastr.error(`召回失败: ${message}`);
} finally {
isRecalling = false;
@@ -1782,6 +1902,12 @@ async function onManualExtract() {
const warnings = [];
isExtracting = true;
setLastExtractionStatus(
"手动提取中",
`待处理 assistant 楼层 ${pendingAssistantTurns.length}`,
"running",
{ syncRuntime: true, toastKind: "info", toastTitle: "ST-BME 手动提取" },
);
try {
while (true) {
const pendingTurns = getAssistantTurns(chat).filter(
@@ -1825,6 +1951,12 @@ async function onManualExtract() {
toastr.success(
`提取完成:${totals.batches} 批,新建 ${totals.newNodes},更新 ${totals.updatedNodes},新边 ${totals.newEdges}`,
);
setLastExtractionStatus(
"手动提取完成",
`${totals.batches} 批 · 新建 ${totals.newNodes} · 更新 ${totals.updatedNodes} · 新边 ${totals.newEdges}`,
"success",
{ syncRuntime: true, toastKind: "success", toastTitle: "ST-BME 手动提取" },
);
if (warnings.length > 0) {
toastr.warning(
warnings.slice(0, 2).join(""),
@@ -1834,6 +1966,11 @@ async function onManualExtract() {
}
} catch (e) {
console.error("[ST-BME] 手动提取失败:", e);
setLastExtractionStatus("手动提取失败", e?.message || String(e), "error", {
syncRuntime: true,
toastKind: "",
toastTitle: "ST-BME 手动提取",
});
toastr.error(`手动提取失败: ${e.message || e}`);
} finally {
isExtracting = false;
@@ -1966,6 +2103,9 @@ async function onReembedDirect() {
getSettings: () => getSettings(),
getLastExtract: () => lastExtractedItems,
getLastRecall: () => lastRecalledItems,
getRuntimeStatus: () => runtimeStatus,
getLastExtractionStatus: () => lastExtractionStatus,
getLastVectorStatus: () => lastVectorStatus,
getLastRecallStatus: () => lastRecallStatus,
getLastInjection: () => lastInjectionContent,
updateSettings: (patch) => {

View File

@@ -126,6 +126,14 @@
<label>最近恢复</label>
<div class="bme-recent-meta" id="bme-status-recovery"></div>
</div>
<div class="bme-config-row">
<label>最近提取</label>
<div class="bme-recent-meta" id="bme-status-last-extract"></div>
</div>
<div class="bme-config-row">
<label>最近向量</label>
<div class="bme-recent-meta" id="bme-status-last-vector"></div>
</div>
<div class="bme-config-row">
<label>最近召回</label>
<div class="bme-recent-meta" id="bme-status-last-recall"></div>

View File

@@ -120,6 +120,9 @@ let _getGraph = null;
let _getSettings = null;
let _getLastExtract = null;
let _getLastRecall = null;
let _getRuntimeStatus = null;
let _getLastExtractionStatus = null;
let _getLastVectorStatus = null;
let _getLastRecallStatus = null;
let _getLastInjection = null;
let _updateSettings = null;
@@ -142,6 +145,9 @@ export async function initPanel({
getSettings,
getLastExtract,
getLastRecall,
getRuntimeStatus,
getLastExtractionStatus,
getLastVectorStatus,
getLastRecallStatus,
getLastInjection,
updateSettings,
@@ -151,6 +157,9 @@ export async function initPanel({
_getSettings = getSettings;
_getLastExtract = getLastExtract;
_getLastRecall = getLastRecall;
_getRuntimeStatus = getRuntimeStatus;
_getLastExtractionStatus = getLastExtractionStatus;
_getLastVectorStatus = getLastVectorStatus;
_getLastRecallStatus = getLastRecallStatus;
_getLastInjection = getLastInjection;
_updateSettings = updateSettings;
@@ -343,6 +352,8 @@ function _refreshDashboard() {
const vectorMode = graph?.vectorIndexState?.mode || "—";
const vectorSource = graph?.vectorIndexState?.source || "—";
const recovery = graph?.historyState?.lastRecoveryResult;
const extractionStatus = _getLastExtractionStatus?.() || {};
const vectorStatus = _getLastVectorStatus?.() || {};
const recallStatus = _getLastRecallStatus?.() || {};
_setText("bme-status-chat-id", chatId);
@@ -362,6 +373,14 @@ function _refreshDashboard() {
? `${recovery.status} · from ${recovery.fromFloor ?? "—"} · ${recovery.reason || "—"}`
: "暂无恢复记录",
);
_setText(
"bme-status-last-extract",
extractionStatus.meta || "尚未执行提取",
);
_setText(
"bme-status-last-vector",
vectorStatus.meta || "尚未执行向量任务",
);
_setText(
"bme-status-last-recall",
recallStatus.meta || "尚未执行召回",
@@ -1223,9 +1242,9 @@ function _setText(id, text) {
}
function _refreshRuntimeStatus() {
const recallStatus = _getLastRecallStatus?.() || {};
const text = recallStatus.text || "待命";
const meta = recallStatus.meta || "尚未执行召回";
const runtimeStatus = _getRuntimeStatus?.() || {};
const text = runtimeStatus.text || "待命";
const meta = runtimeStatus.meta || "准备就绪";
_setText("bme-status-text", text);
_setText("bme-status-meta", meta);
_setText("bme-panel-status", text);