mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Fix mobile action status cleanup
This commit is contained in:
63
index.js
63
index.js
@@ -2084,7 +2084,7 @@ async function syncVectorState({
|
|||||||
currentGraph.vectorIndexState.lastWarning = validation.error;
|
currentGraph.vectorIndexState.lastWarning = validation.error;
|
||||||
currentGraph.vectorIndexState.dirty = true;
|
currentGraph.vectorIndexState.dirty = true;
|
||||||
setLastVectorStatus("向量不可用", validation.error, "warning", {
|
setLastVectorStatus("向量不可用", validation.error, "warning", {
|
||||||
syncRuntime: false,
|
syncRuntime: true,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
insertedHashes: [],
|
insertedHashes: [],
|
||||||
@@ -2105,13 +2105,13 @@ async function syncVectorState({
|
|||||||
"向量完成",
|
"向量完成",
|
||||||
`${scopeLabel} · indexed ${result.stats?.indexed ?? 0} · pending ${result.stats?.pending ?? 0}`,
|
`${scopeLabel} · indexed ${result.stats?.indexed ?? 0} · pending ${result.stats?.pending ?? 0}`,
|
||||||
"success",
|
"success",
|
||||||
{ syncRuntime: false },
|
{ syncRuntime: true },
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isAbortError(error)) {
|
if (isAbortError(error)) {
|
||||||
setLastVectorStatus("向量已终止", scopeLabel, "warning", {
|
setLastVectorStatus("向量已终止", scopeLabel, "warning", {
|
||||||
syncRuntime: false,
|
syncRuntime: true,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
insertedHashes: [],
|
insertedHashes: [],
|
||||||
@@ -5235,6 +5235,11 @@ async function onRebuild() {
|
|||||||
);
|
);
|
||||||
const previousUiState = snapshotRuntimeUiState();
|
const previousUiState = snapshotRuntimeUiState();
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
setRuntimeStatus(
|
||||||
|
"图谱重建中",
|
||||||
|
`当前聊天 ${Array.isArray(chat) ? chat.length : 0} 条消息`,
|
||||||
|
"running",
|
||||||
|
);
|
||||||
|
|
||||||
currentGraph = normalizeGraphRuntimeState(
|
currentGraph = normalizeGraphRuntimeState(
|
||||||
createEmptyGraph(),
|
createEmptyGraph(),
|
||||||
@@ -5259,12 +5264,30 @@ async function onRebuild() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
saveGraphToChat({ reason: "manual-rebuild-complete" });
|
saveGraphToChat({ reason: "manual-rebuild-complete" });
|
||||||
|
setLastExtractionStatus(
|
||||||
|
"图谱重建完成",
|
||||||
|
`已回放 ${replayedBatches} 批提取`,
|
||||||
|
"success",
|
||||||
|
{
|
||||||
|
syncRuntime: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (currentGraph.vectorIndexState?.lastWarning) {
|
if (currentGraph.vectorIndexState?.lastWarning) {
|
||||||
|
setRuntimeStatus(
|
||||||
|
"图谱重建完成",
|
||||||
|
`已回放 ${replayedBatches} 批,但向量仍待修复`,
|
||||||
|
"warning",
|
||||||
|
);
|
||||||
toastr.warning(
|
toastr.warning(
|
||||||
`图谱已重建,但向量索引仍待修复: ${currentGraph.vectorIndexState.lastWarning}`,
|
`图谱已重建,但向量索引仍待修复: ${currentGraph.vectorIndexState.lastWarning}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
setRuntimeStatus(
|
||||||
|
"图谱重建完成",
|
||||||
|
`已回放 ${replayedBatches} 批,图谱与向量索引已刷新`,
|
||||||
|
"success",
|
||||||
|
);
|
||||||
toastr.success("图谱与向量索引已按当前聊天全量重建");
|
toastr.success("图谱与向量索引已按当前聊天全量重建");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -5274,9 +5297,19 @@ async function onRebuild() {
|
|||||||
);
|
);
|
||||||
restoreRuntimeUiState(previousUiState);
|
restoreRuntimeUiState(previousUiState);
|
||||||
saveGraphToChat({ reason: "manual-rebuild-restore-previous" });
|
saveGraphToChat({ reason: "manual-rebuild-restore-previous" });
|
||||||
|
setLastExtractionStatus(
|
||||||
|
"图谱重建失败",
|
||||||
|
error?.message || String(error),
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
syncRuntime: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`图谱重建失败,已恢复到重建前状态: ${error?.message || error}`,
|
`图谱重建失败,已恢复到重建前状态: ${error?.message || error}`,
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
refreshPanelLiveState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5569,6 +5602,14 @@ async function onManualExtract() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (totals.batches === 0) {
|
if (totals.batches === 0) {
|
||||||
|
setLastExtractionStatus(
|
||||||
|
"无待提取内容",
|
||||||
|
"没有新的 assistant 回复需要处理",
|
||||||
|
"info",
|
||||||
|
{
|
||||||
|
syncRuntime: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
toastr.info("没有待提取的新回复");
|
toastr.info("没有待提取的新回复");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -5613,6 +5654,7 @@ async function onManualExtract() {
|
|||||||
} finally {
|
} finally {
|
||||||
finishStageAbortController("extraction", extractionController);
|
finishStageAbortController("extraction", extractionController);
|
||||||
isExtracting = false;
|
isExtracting = false;
|
||||||
|
refreshPanelLiveState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5702,6 +5744,14 @@ async function onReroll({ fromFloor } = {}) {
|
|||||||
targetFloor = assistantTurns[assistantTurns.length - 1];
|
targetFloor = assistantTurns[assistantTurns.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRuntimeStatus(
|
||||||
|
"重新提取中",
|
||||||
|
Number.isFinite(targetFloor)
|
||||||
|
? `准备从楼层 ${targetFloor} 开始回滚并重新提取`
|
||||||
|
: "准备回滚最新 AI 楼并重新提取",
|
||||||
|
"running",
|
||||||
|
);
|
||||||
|
|
||||||
const lastProcessed = getLastProcessedAssistantFloor();
|
const lastProcessed = getLastProcessedAssistantFloor();
|
||||||
const alreadyExtracted = targetFloor <= lastProcessed;
|
const alreadyExtracted = targetFloor <= lastProcessed;
|
||||||
|
|
||||||
@@ -5727,6 +5777,11 @@ async function onReroll({ fromFloor } = {}) {
|
|||||||
console.log(`[ST-BME] 重 Roll 开始,目标楼层: ${targetFloor}`);
|
console.log(`[ST-BME] 重 Roll 开始,目标楼层: ${targetFloor}`);
|
||||||
const rollbackResult = await rollbackGraphForReroll(targetFloor, context);
|
const rollbackResult = await rollbackGraphForReroll(targetFloor, context);
|
||||||
if (!rollbackResult.success) {
|
if (!rollbackResult.success) {
|
||||||
|
setRuntimeStatus(
|
||||||
|
"重新提取失败",
|
||||||
|
rollbackResult.error || "回滚失败",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
toastr.error(rollbackResult.error, "ST-BME 重 Roll");
|
toastr.error(rollbackResult.error, "ST-BME 重 Roll");
|
||||||
return rollbackResult;
|
return rollbackResult;
|
||||||
}
|
}
|
||||||
@@ -5740,6 +5795,7 @@ async function onReroll({ fromFloor } = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await onManualExtract();
|
await onManualExtract();
|
||||||
|
refreshPanelLiveState();
|
||||||
return {
|
return {
|
||||||
...rollbackResult,
|
...rollbackResult,
|
||||||
extractionTriggered: true,
|
extractionTriggered: true,
|
||||||
@@ -5843,6 +5899,7 @@ async function onRebuildVectorIndex(range = null) {
|
|||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
finishStageAbortController("vector", vectorController);
|
finishStageAbortController("vector", vectorController);
|
||||||
|
refreshPanelLiveState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
panel.js
3
panel.js
@@ -1316,6 +1316,7 @@ function _bindActions() {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
btn.style.opacity = "";
|
btn.style.opacity = "";
|
||||||
|
_refreshRuntimeStatus();
|
||||||
_refreshGraphAvailabilityState();
|
_refreshGraphAvailabilityState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1355,6 +1356,7 @@ function _bindActions() {
|
|||||||
if (btn) {
|
if (btn) {
|
||||||
btn.style.opacity = "";
|
btn.style.opacity = "";
|
||||||
}
|
}
|
||||||
|
_refreshRuntimeStatus();
|
||||||
_refreshGraphAvailabilityState();
|
_refreshGraphAvailabilityState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1401,6 +1403,7 @@ function _bindActions() {
|
|||||||
if (btn) {
|
if (btn) {
|
||||||
btn.style.opacity = "";
|
btn.style.opacity = "";
|
||||||
}
|
}
|
||||||
|
_refreshRuntimeStatus();
|
||||||
_refreshGraphAvailabilityState();
|
_refreshGraphAvailabilityState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
260
tests/mobile-status-regressions.mjs
Normal file
260
tests/mobile-status-regressions.mjs
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import vm from "node:vm";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const indexPath = path.resolve(__dirname, "../index.js");
|
||||||
|
const indexSource = await fs.readFile(indexPath, "utf8");
|
||||||
|
|
||||||
|
function extractSnippet(startMarker, endMarker) {
|
||||||
|
const start = indexSource.indexOf(startMarker);
|
||||||
|
const end = indexSource.indexOf(endMarker);
|
||||||
|
if (start < 0 || end < 0 || end <= start) {
|
||||||
|
throw new Error(`无法提取 index.js 片段: ${startMarker} -> ${endMarker}`);
|
||||||
|
}
|
||||||
|
return indexSource.slice(start, end).replace(/^export\s+/gm, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusSnippet = extractSnippet(
|
||||||
|
"function setRuntimeStatus(",
|
||||||
|
"function notifyExtractionIssue(",
|
||||||
|
);
|
||||||
|
const vectorSnippet = extractSnippet(
|
||||||
|
"async function syncVectorState({",
|
||||||
|
"async function ensureVectorReadyIfNeeded(",
|
||||||
|
);
|
||||||
|
const manualExtractSnippet = extractSnippet(
|
||||||
|
"async function onManualExtract() {",
|
||||||
|
"async function onReroll(",
|
||||||
|
);
|
||||||
|
const rebuildSnippet = extractSnippet(
|
||||||
|
"async function onRebuild() {",
|
||||||
|
"async function onManualCompress() {",
|
||||||
|
);
|
||||||
|
|
||||||
|
function createBaseStatusContext() {
|
||||||
|
return {
|
||||||
|
console,
|
||||||
|
Date,
|
||||||
|
createUiStatus(text = "待命", meta = "", level = "idle") {
|
||||||
|
return {
|
||||||
|
text: String(text || "待命"),
|
||||||
|
meta: String(meta || ""),
|
||||||
|
level,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
runtimeStatus: { text: "待命", meta: "", level: "idle" },
|
||||||
|
lastExtractionStatus: { text: "待命", meta: "", level: "idle" },
|
||||||
|
lastVectorStatus: { text: "待命", meta: "", level: "idle" },
|
||||||
|
lastRecallStatus: { text: "待命", meta: "", level: "idle" },
|
||||||
|
lastStatusToastAt: {},
|
||||||
|
STATUS_TOAST_THROTTLE_MS: 1500,
|
||||||
|
_panelModule: {
|
||||||
|
updateFloatingBallStatus() {},
|
||||||
|
},
|
||||||
|
refreshPanelLiveState() {},
|
||||||
|
updateStageNotice() {},
|
||||||
|
notifyStatusToast() {},
|
||||||
|
toastr: {
|
||||||
|
info() {},
|
||||||
|
success() {},
|
||||||
|
warning() {},
|
||||||
|
error() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testVectorSyncTerminalStateUpdatesRuntime() {
|
||||||
|
const context = {
|
||||||
|
...createBaseStatusContext(),
|
||||||
|
currentGraph: {
|
||||||
|
vectorIndexState: {
|
||||||
|
dirty: true,
|
||||||
|
lastWarning: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensureCurrentGraphRuntimeState() {
|
||||||
|
return context.currentGraph;
|
||||||
|
},
|
||||||
|
getEmbeddingConfig() {
|
||||||
|
return { mode: "direct" };
|
||||||
|
},
|
||||||
|
validateVectorConfig() {
|
||||||
|
return { valid: true };
|
||||||
|
},
|
||||||
|
async syncGraphVectorIndex() {
|
||||||
|
return {
|
||||||
|
insertedHashes: [],
|
||||||
|
stats: {
|
||||||
|
indexed: 12,
|
||||||
|
pending: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getCurrentChatId() {
|
||||||
|
return "chat-mobile";
|
||||||
|
},
|
||||||
|
getVectorIndexStats() {
|
||||||
|
return { indexed: 12, pending: 0 };
|
||||||
|
},
|
||||||
|
isAbortError() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
markVectorStateDirty() {},
|
||||||
|
result: null,
|
||||||
|
};
|
||||||
|
vm.createContext(context);
|
||||||
|
vm.runInContext(
|
||||||
|
`${statusSnippet}\n${vectorSnippet}\nresult = { syncVectorState };`,
|
||||||
|
context,
|
||||||
|
{ filename: indexPath },
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await context.result.syncVectorState({ force: true });
|
||||||
|
assert.equal(result.stats.indexed, 12);
|
||||||
|
assert.equal(context.lastVectorStatus.text, "向量完成");
|
||||||
|
assert.equal(context.runtimeStatus.text, "向量完成");
|
||||||
|
assert.equal(context.runtimeStatus.level, "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testManualExtractNoBatchesDoesNotStayRunning() {
|
||||||
|
let assistantTurnCallCount = 0;
|
||||||
|
const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }];
|
||||||
|
const context = {
|
||||||
|
...createBaseStatusContext(),
|
||||||
|
isExtracting: false,
|
||||||
|
currentGraph: {},
|
||||||
|
getCurrentChatId() {
|
||||||
|
return "chat-mobile";
|
||||||
|
},
|
||||||
|
ensureGraphMutationReady() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async recoverHistoryIfNeeded() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
normalizeGraphRuntimeState(graph) {
|
||||||
|
return graph;
|
||||||
|
},
|
||||||
|
createEmptyGraph() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
getContext() {
|
||||||
|
return { chat };
|
||||||
|
},
|
||||||
|
getAssistantTurns() {
|
||||||
|
assistantTurnCallCount += 1;
|
||||||
|
return assistantTurnCallCount === 1 ? [1] : [];
|
||||||
|
},
|
||||||
|
getLastProcessedAssistantFloor() {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
clampInt(value, fallback) {
|
||||||
|
return Number.isFinite(Number(value)) ? Number(value) : fallback;
|
||||||
|
},
|
||||||
|
getSettings() {
|
||||||
|
return { extractEvery: 1 };
|
||||||
|
},
|
||||||
|
beginStageAbortController() {
|
||||||
|
return { signal: {} };
|
||||||
|
},
|
||||||
|
async executeExtractionBatch() {
|
||||||
|
throw new Error("不应进入批次执行");
|
||||||
|
},
|
||||||
|
isAbortError() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
finishStageAbortController() {},
|
||||||
|
result: null,
|
||||||
|
};
|
||||||
|
vm.createContext(context);
|
||||||
|
vm.runInContext(
|
||||||
|
`${statusSnippet}\n${manualExtractSnippet}\nresult = { onManualExtract };`,
|
||||||
|
context,
|
||||||
|
{ filename: indexPath },
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.result.onManualExtract();
|
||||||
|
assert.equal(context.isExtracting, false);
|
||||||
|
assert.equal(context.lastExtractionStatus.text, "无待提取内容");
|
||||||
|
assert.equal(context.runtimeStatus.text, "无待提取内容");
|
||||||
|
assert.notEqual(context.runtimeStatus.level, "running");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testManualRebuildSetsTerminalRuntimeStatus() {
|
||||||
|
const chat = [{ is_user: true, mes: "u" }, { is_user: false, mes: "a" }];
|
||||||
|
const context = {
|
||||||
|
...createBaseStatusContext(),
|
||||||
|
currentGraph: {
|
||||||
|
vectorIndexState: {
|
||||||
|
lastWarning: "",
|
||||||
|
},
|
||||||
|
batchJournal: [],
|
||||||
|
},
|
||||||
|
confirm() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
ensureGraphMutationReady() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
getContext() {
|
||||||
|
return { chat };
|
||||||
|
},
|
||||||
|
cloneGraphSnapshot(graph) {
|
||||||
|
return graph;
|
||||||
|
},
|
||||||
|
snapshotRuntimeUiState() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
getSettings() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
normalizeGraphRuntimeState(graph) {
|
||||||
|
return graph;
|
||||||
|
},
|
||||||
|
createEmptyGraph() {
|
||||||
|
return {
|
||||||
|
vectorIndexState: {
|
||||||
|
lastWarning: "",
|
||||||
|
},
|
||||||
|
batchJournal: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getCurrentChatId() {
|
||||||
|
return "chat-mobile";
|
||||||
|
},
|
||||||
|
clearInjectionState() {},
|
||||||
|
async prepareVectorStateForReplay() {},
|
||||||
|
async replayExtractionFromHistory() {
|
||||||
|
context.currentGraph.vectorIndexState.lastWarning = "";
|
||||||
|
return 2;
|
||||||
|
},
|
||||||
|
clearHistoryDirty() {},
|
||||||
|
buildRecoveryResult(status, extra = {}) {
|
||||||
|
return { status, ...extra };
|
||||||
|
},
|
||||||
|
saveGraphToChat() {},
|
||||||
|
restoreRuntimeUiState() {},
|
||||||
|
result: null,
|
||||||
|
};
|
||||||
|
vm.createContext(context);
|
||||||
|
vm.runInContext(
|
||||||
|
`${statusSnippet}\n${rebuildSnippet}\nresult = { onRebuild };`,
|
||||||
|
context,
|
||||||
|
{ filename: indexPath },
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.result.onRebuild();
|
||||||
|
assert.equal(context.lastExtractionStatus.text, "图谱重建完成");
|
||||||
|
assert.equal(context.runtimeStatus.text, "图谱重建完成");
|
||||||
|
assert.equal(context.runtimeStatus.level, "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
await testVectorSyncTerminalStateUpdatesRuntime();
|
||||||
|
await testManualExtractNoBatchesDoesNotStayRunning();
|
||||||
|
await testManualRebuildSetsTerminalRuntimeStatus();
|
||||||
|
|
||||||
|
console.log("mobile-status-regressions tests passed");
|
||||||
@@ -372,6 +372,9 @@ function createRerollHarness() {
|
|||||||
refreshPanelLiveState() {
|
refreshPanelLiveState() {
|
||||||
context.refreshPanelCalls += 1;
|
context.refreshPanelCalls += 1;
|
||||||
},
|
},
|
||||||
|
setRuntimeStatus(text, meta = "", level = "info") {
|
||||||
|
context.runtimeStatus = { text, meta, level };
|
||||||
|
},
|
||||||
clearInjectionState() {
|
clearInjectionState() {
|
||||||
context.clearInjectionCalls += 1;
|
context.clearInjectionCalls += 1;
|
||||||
},
|
},
|
||||||
@@ -1384,7 +1387,7 @@ async function testRerollUsesBatchBoundaryRollbackAndPersistsState() {
|
|||||||
assert.equal(harness.prepareVectorStateCalls.length, 1);
|
assert.equal(harness.prepareVectorStateCalls.length, 1);
|
||||||
assert.equal(harness.prepareVectorStateCalls[0][2].skipBackendPurge, true);
|
assert.equal(harness.prepareVectorStateCalls[0][2].skipBackendPurge, true);
|
||||||
assert.equal(harness.saveGraphToChatCalls, 1);
|
assert.equal(harness.saveGraphToChatCalls, 1);
|
||||||
assert.equal(harness.refreshPanelCalls, 1);
|
assert.equal(harness.refreshPanelCalls, 2);
|
||||||
assert.equal(harness.clearInjectionCalls, 1);
|
assert.equal(harness.clearInjectionCalls, 1);
|
||||||
assert.equal(harness.onManualExtractCalls, 1);
|
assert.equal(harness.onManualExtractCalls, 1);
|
||||||
assert.equal(harness.currentGraph.historyState.processedMessageHashes[3], undefined);
|
assert.equal(harness.currentGraph.historyState.processedMessageHashes[3], undefined);
|
||||||
|
|||||||
Reference in New Issue
Block a user