Preserve hidden context for extraction

This commit is contained in:
Hao19911125
2026-04-03 12:36:29 +08:00
parent f48b84af9e
commit 97e454449b
3 changed files with 125 additions and 4 deletions

View File

@@ -6,8 +6,24 @@ import { clampInt } from "./ui-status.js";
import { sanitizePlannerMessageText } from "./planner-tag-utils.js"; import { sanitizePlannerMessageText } from "./planner-tag-utils.js";
import { rollbackBatch } from "./runtime-state.js"; import { rollbackBatch } from "./runtime-state.js";
export function isBmeManagedHiddenMessage(message) {
return Boolean(
message?.extra &&
typeof message.extra === "object" &&
message.extra.__st_bme_hide_managed === true,
);
}
export function isSystemMessageForExtraction(message) {
return Boolean(message?.is_system) && !isBmeManagedHiddenMessage(message);
}
export function isAssistantChatMessage(message) { export function isAssistantChatMessage(message) {
return Boolean(message) && !message.is_user && !message.is_system; return (
Boolean(message) &&
!message.is_user &&
!isSystemMessageForExtraction(message)
);
} }
export function getAssistantTurns(chat) { export function getAssistantTurns(chat) {
@@ -37,7 +53,7 @@ export function buildExtractionMessages(chat, startIdx, endIdx, settings) {
index++ index++
) { ) {
const msg = chat[index]; const msg = chat[index];
if (msg.is_system) continue; if (isSystemMessageForExtraction(msg)) continue;
messages.push({ messages.push({
seq: index, seq: index,
role: msg.is_user ? "user" : "assistant", role: msg.is_user ? "user" : "assistant",
@@ -54,7 +70,7 @@ export function getChatIndexForPlayableSeq(chat, playableSeq) {
let currentSeq = -1; let currentSeq = -1;
for (let index = 0; index < chat.length; index++) { for (let index = 0; index < chat.length; index++) {
const message = chat[index]; const message = chat[index];
if (message?.is_system) continue; if (isSystemMessageForExtraction(message)) continue;
currentSeq++; currentSeq++;
if (currentSeq >= playableSeq) { if (currentSeq >= playableSeq) {
return index; return index;

View File

@@ -33,6 +33,7 @@ import {
clampRecoveryStartFloor, clampRecoveryStartFloor,
getAssistantTurns, getAssistantTurns,
isAssistantChatMessage, isAssistantChatMessage,
isSystemMessageForExtraction,
pruneProcessedMessageHashesFromFloor, pruneProcessedMessageHashesFromFloor,
resolveDirtyFloorFromMutationMeta, resolveDirtyFloorFromMutationMeta,
rollbackAffectedJournals, rollbackAffectedJournals,
@@ -4359,6 +4360,22 @@ function notifyExtractionIssue(message, title = "ST-BME 提取提示") {
toastr.warning(message, title, { timeOut: 4500 }); toastr.warning(message, title, { timeOut: 4500 });
} }
function settleExtractionStatusAfterHistoryRecovery(
text = "提取完成",
meta = "",
level = "success",
) {
const currentText = String(lastExtractionStatus?.text || "");
const currentLevel = String(lastExtractionStatus?.level || "");
if (currentText !== "AI 生成中" && currentLevel !== "running") {
return;
}
setLastExtractionStatus(text, meta, level, {
syncRuntime: true,
toastKind: "",
});
}
async function fetchLocalWithTimeout( async function fetchLocalWithTimeout(
url, url,
options = {}, options = {},
@@ -5594,7 +5611,7 @@ const DEFAULT_TRIGGER_KEYWORDS = [
export function getSmartTriggerDecision(chat, lastProcessed, settings) { export function getSmartTriggerDecision(chat, lastProcessed, settings) {
const pendingMessages = chat const pendingMessages = chat
.slice(Math.max(0, (lastProcessed ?? -1) + 1)) .slice(Math.max(0, (lastProcessed ?? -1) + 1))
.filter((msg) => !msg.is_system) .filter((msg) => !isSystemMessageForExtraction(msg))
.map((msg) => ({ .map((msg) => ({
role: msg.is_user ? "user" : "assistant", role: msg.is_user ? "user" : "assistant",
content: msg.mes || "", content: msg.mes || "",
@@ -7695,6 +7712,11 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
} }
saveGraphToChat({ reason: "history-recovery-complete" }); saveGraphToChat({ reason: "history-recovery-complete" });
refreshPanelLiveState(); refreshPanelLiveState();
settleExtractionStatusAfterHistoryRecovery(
"提取完成",
`历史恢复回放 ${replayedBatches}`,
"success",
);
updateStageNotice( updateStageNotice(
"history", "history",
usedFullRebuild ? "历史恢复完成(全量重建)" : "历史恢复完成", usedFullRebuild ? "历史恢复完成(全量重建)" : "历史恢复完成",
@@ -7736,6 +7758,11 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
currentGraph.vectorIndexState.replayRequiredNodeIds = []; currentGraph.vectorIndexState.replayRequiredNodeIds = [];
currentGraph.vectorIndexState.dirty = false; currentGraph.vectorIndexState.dirty = false;
currentGraph.vectorIndexState.dirtyReason = ""; currentGraph.vectorIndexState.dirtyReason = "";
settleExtractionStatusAfterHistoryRecovery(
"提取已终止",
error?.message || "历史恢复已终止",
"warning",
);
updateStageNotice( updateStageNotice(
"history", "history",
"历史恢复已终止", "历史恢复已终止",
@@ -7782,6 +7809,11 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
currentGraph.vectorIndexState.lastIntegrityIssue = null; currentGraph.vectorIndexState.lastIntegrityIssue = null;
saveGraphToChat({ reason: "history-recovery-fallback-rebuild" }); saveGraphToChat({ reason: "history-recovery-fallback-rebuild" });
refreshPanelLiveState(); refreshPanelLiveState();
settleExtractionStatusAfterHistoryRecovery(
"提取完成",
`历史恢复已退化为全量重建,回放 ${replayedBatches}`,
"warning",
);
updateStageNotice( updateStageNotice(
"history", "history",
"历史恢复已退化为全量重建", "历史恢复已退化为全量重建",
@@ -7814,6 +7846,11 @@ async function recoverHistoryIfNeeded(trigger = "history-recovery") {
currentGraph.vectorIndexState.lastIntegrityIssue = null; currentGraph.vectorIndexState.lastIntegrityIssue = null;
saveGraphToChat({ reason: "history-recovery-failed" }); saveGraphToChat({ reason: "history-recovery-failed" });
refreshPanelLiveState(); refreshPanelLiveState();
settleExtractionStatusAfterHistoryRecovery(
"提取失败",
fallbackError?.message || String(fallbackError),
"error",
);
updateStageNotice( updateStageNotice(
"history", "history",
"历史恢复失败", "历史恢复失败",

68
tests/chat-history.mjs Normal file
View File

@@ -0,0 +1,68 @@
import assert from "node:assert/strict";
import {
buildExtractionMessages,
getAssistantTurns,
isAssistantChatMessage,
isBmeManagedHiddenMessage,
isSystemMessageForExtraction,
} from "../chat-history.js";
const visibleAssistant = {
is_user: false,
is_system: false,
mes: "visible assistant",
};
assert.equal(isAssistantChatMessage(visibleAssistant), true);
const managedHiddenAssistant = {
is_user: false,
is_system: true,
mes: "managed hidden assistant",
extra: { __st_bme_hide_managed: true },
};
assert.equal(isBmeManagedHiddenMessage(managedHiddenAssistant), true);
assert.equal(isSystemMessageForExtraction(managedHiddenAssistant), false);
assert.equal(isAssistantChatMessage(managedHiddenAssistant), true);
const realSystemMessage = {
is_user: false,
is_system: true,
mes: "real system",
};
assert.equal(isSystemMessageForExtraction(realSystemMessage), true);
assert.equal(isAssistantChatMessage(realSystemMessage), false);
const chat = [
{ is_user: false, is_system: true, mes: "greeting/system" },
{ is_user: true, is_system: false, mes: "user-1" },
managedHiddenAssistant,
{ is_user: true, is_system: false, mes: "user-2" },
visibleAssistant,
realSystemMessage,
];
assert.deepEqual(
getAssistantTurns(chat),
[2, 4],
"managed hidden assistant floors should still be extractable assistant turns",
);
const extractionMessages = buildExtractionMessages(chat, 4, 4, {
extractContextTurns: 2,
});
assert.deepEqual(
extractionMessages.map((message) => ({
seq: message.seq,
role: message.role,
content: message.content,
})),
[
{ seq: 1, role: "user", content: "user-1" },
{ seq: 2, role: "assistant", content: "managed hidden assistant" },
{ seq: 3, role: "user", content: "user-2" },
{ seq: 4, role: "assistant", content: "visible assistant" },
],
"extraction should keep BME-managed hidden context but still skip real system messages",
);
console.log("chat-history tests passed");