fix: surface rerun extraction progress

This commit is contained in:
Youzini-afk
2026-04-16 00:14:05 +08:00
parent 058621b7aa
commit e3b268bb04
4 changed files with 218 additions and 13 deletions

View File

@@ -17841,6 +17841,7 @@ async function onReroll({ fromFloor } = {}) {
onManualExtract,
refreshPanelLiveState,
rollbackGraphForReroll,
setLastExtractionStatus,
setRuntimeStatus,
toastr,
},

View File

@@ -115,6 +115,22 @@ function cloneSerializable(value, fallback = null) {
}
}
function setExtractionProgressStatus(
runtime,
text,
meta = "",
level = "info",
options = {},
) {
if (typeof runtime?.setLastExtractionStatus === "function") {
runtime.setLastExtractionStatus(text, meta, level, options);
return;
}
if (options?.syncRuntime !== false && typeof runtime?.setRuntimeStatus === "function") {
runtime.setRuntimeStatus(text, meta, level);
}
}
function resolveLatestAssistantDialogueFloor(chat = []) {
const map = buildDialogueFloorMap(chat);
const assistantDialogueFloors = Array.isArray(map.assistantDialogueFloors)
@@ -866,6 +882,7 @@ export async function onManualExtractController(runtime, options = {}) {
const taskLabel = String(options?.taskLabel || "手动提取").trim() || "手动提取";
const toastTitle = String(options?.toastTitle || `ST-BME ${taskLabel}`).trim() ||
`ST-BME ${taskLabel}`;
const showStartToast = options?.showStartToast !== false;
const lockedEndFloor = toSafeFloor(options?.lockedEndFloor, null);
if (!runtime.ensureGraphMutationReady(taskLabel)) return;
const pendingPersistGate = await maybeRetryPendingPersistence(
@@ -907,6 +924,10 @@ export async function onManualExtractController(runtime, options = {}) {
const assistantTurns = runtime.getAssistantTurns(chat);
const lastProcessed = runtime.getLastProcessedAssistantFloor();
const pendingAssistantTurns = assistantTurns.filter((i) => i > lastProcessed);
const targetAssistantTurns = pendingAssistantTurns.filter((i) => {
if (lockedEndFloor != null && i > lockedEndFloor) return false;
return true;
});
if (pendingAssistantTurns.length === 0) {
runtime.toastr.info("没有待提取的新回复");
return;
@@ -920,18 +941,24 @@ export async function onManualExtractController(runtime, options = {}) {
newEdges: 0,
batches: 0,
};
let processedAssistantTurns = 0;
const warnings = [];
runtime.setIsExtracting(true);
const extractionController = runtime.beginStageAbortController("extraction");
const extractionSignal = extractionController.signal;
runtime.setLastExtractionStatus(
setExtractionProgressStatus(
runtime,
`${taskLabel}`,
lockedEndFloor != null
? `待处理 assistant 楼层 ${pendingAssistantTurns.length} 条 · 截止 chatIndex ${lockedEndFloor}`
: `待处理 assistant 楼层 ${pendingAssistantTurns.length}`,
? `待处理 AI 回复 ${targetAssistantTurns.length} 条 · 截止 chatIndex ${lockedEndFloor}`
: `待处理 AI 回复 ${targetAssistantTurns.length}`,
"running",
{ syncRuntime: true, toastKind: "info", toastTitle },
{
syncRuntime: true,
toastKind: showStartToast ? "info" : "",
toastTitle,
},
);
try {
while (true) {
@@ -967,11 +994,30 @@ export async function onManualExtractController(runtime, options = {}) {
totals.updatedNodes += batchResult.result.updatedNodes || 0;
totals.newEdges += batchResult.result.newEdges || 0;
totals.batches++;
processedAssistantTurns += batchAssistantTurns.length;
if (Array.isArray(batchResult.effects?.warnings)) {
warnings.push(...batchResult.effects.warnings);
}
const totalTurnsForDisplay = Math.max(
processedAssistantTurns,
targetAssistantTurns.length,
);
setExtractionProgressStatus(
runtime,
`${taskLabel}`,
totalTurnsForDisplay > 0
? `已处理 ${processedAssistantTurns}/${totalTurnsForDisplay} 条 AI 回复 · 当前楼层 ${startIdx}-${endIdx} · 累计 ${totals.batches}`
: `当前楼层 ${startIdx}-${endIdx} · 累计 ${totals.batches}`,
"running",
{
syncRuntime: true,
toastKind: "",
toastTitle,
},
);
if (batchResult.historyAdvanceAllowed === false) {
warnings.push(
batchResult.batchStatus?.persistence?.reason ||
@@ -986,7 +1032,8 @@ export async function onManualExtractController(runtime, options = {}) {
}
if (totals.batches === 0) {
runtime.setLastExtractionStatus(
setExtractionProgressStatus(
runtime,
"无待提取内容",
lockedEndFloor != null
? "指定范围内没有新的 assistant 回复需要处理"
@@ -1121,12 +1168,18 @@ export async function onExtractionTaskController(runtime, options = {}) {
: rerunTask.endFloor,
];
runtime.setRuntimeStatus(
"重新提取中",
setExtractionProgressStatus(
runtime,
"重新提取准备中",
fallbackInfo.fallbackToLatest
? `范围 ${rerunTask.startFloor} ~ ${rerunTask.endFloor} 命中旧批次,但当前将退化为从 ${effectiveDialogueRange[0]} 到最新重提`
: `准备重提范围 ${rerunTask.startFloor} ~ ${rerunTask.endFloor}`,
fallbackInfo.fallbackToLatest ? "warning" : "running",
{
syncRuntime: true,
toastKind: "info",
toastTitle: "ST-BME 重新提取",
},
);
const rollbackResult = await runtime.rollbackGraphForReroll(
@@ -1149,11 +1202,28 @@ export async function onExtractionTaskController(runtime, options = {}) {
});
}
const rollbackDesc =
rollbackResult.effectiveFromFloor !== fallbackInfo.startAssistantChatIndex
? `已按批次边界回滚到楼层 ${rollbackResult.effectiveFromFloor},正在开始重新提取`
: `已回滚到楼层 ${fallbackInfo.startAssistantChatIndex},正在开始重新提取`;
setExtractionProgressStatus(
runtime,
"重新提取中",
rollbackDesc,
"running",
{
syncRuntime: true,
toastKind: "",
toastTitle: "ST-BME 重新提取",
},
);
await runManualExtract({
drainAll: true,
lockedEndFloor: effectiveLockedEndFloor,
taskLabel: "重新提取",
toastTitle: "ST-BME 重新提取",
showStartToast: false,
});
return {
@@ -1254,12 +1324,18 @@ export async function onRerollController(runtime, { fromFloor } = {}) {
targetFloor = assistantTurns[assistantTurns.length - 1];
}
runtime.setRuntimeStatus(
"重新提取中",
setExtractionProgressStatus(
runtime,
"重新提取准备中",
Number.isFinite(targetFloor)
? `准备从楼层 ${targetFloor} 开始回滚并重新提取`
: "准备回滚最新 AI 楼并重新提取",
"running",
{
syncRuntime: true,
toastKind: "info",
toastTitle: "ST-BME 重 Roll",
},
);
const lastProcessed = runtime.getLastProcessedAssistantFloor();
@@ -1289,10 +1365,14 @@ export async function onRerollController(runtime, { fromFloor } = {}) {
rollbackResult = await runtime.rollbackGraphForReroll(targetFloor, context);
} catch (e) {
if (runtime.isAbortError(e)) {
runtime.setRuntimeStatus(
setExtractionProgressStatus(
runtime,
"重新提取已取消",
e.message || "聊天已切换",
"warning",
{
syncRuntime: true,
},
);
return {
success: false,
@@ -1309,10 +1389,14 @@ export async function onRerollController(runtime, { fromFloor } = {}) {
}
if (!rollbackResult?.success) {
runtime.setRuntimeStatus(
setExtractionProgressStatus(
runtime,
"重新提取失败",
rollbackResult.error || "回滚失败",
"error",
{
syncRuntime: true,
},
);
runtime.toastr?.error?.(rollbackResult.error, "ST-BME 重 Roll");
return rollbackResult;
@@ -1326,7 +1410,19 @@ export async function onRerollController(runtime, { fromFloor } = {}) {
timeOut: 2500,
});
await runtime.onManualExtract({ drainAll: false });
setExtractionProgressStatus(
runtime,
"重新提取中",
rerollDesc,
"running",
{
syncRuntime: true,
toastKind: "",
toastTitle: "ST-BME 重 Roll",
},
);
await runtime.onManualExtract({ drainAll: false, showStartToast: false });
runtime.refreshPanelLiveState();
return {
...rollbackResult,

View File

@@ -4,7 +4,10 @@ import {
buildDialogueFloorMap,
normalizeDialogueFloorRange,
} from "../maintenance/chat-history.js";
import { onExtractionTaskController } from "../maintenance/extraction-controller.js";
import {
onExtractionTaskController,
onManualExtractController,
} from "../maintenance/extraction-controller.js";
import { onRebuildSummaryStateController } from "../ui/ui-actions-controller.js";
const chat = [
@@ -50,6 +53,7 @@ const chat = [
manual: [],
warning: [],
info: [],
extractionStatus: [],
};
const runtime = {
getContext() {
@@ -62,6 +66,9 @@ const chat = [
return true;
},
setRuntimeStatus() {},
setLastExtractionStatus(text, meta, level) {
calls.extractionStatus.push({ text, meta, level });
},
rollbackGraphForReroll: async (fromFloor) => {
calls.rollback.push(fromFloor);
return { success: true, effectiveFromFloor: fromFloor };
@@ -91,6 +98,11 @@ const chat = [
assert.equal(calls.manual.length, 1);
assert.equal(calls.manual[0].lockedEndFloor, null);
assert.equal(calls.manual[0].taskLabel, "重新提取");
assert.equal(calls.manual[0].showStartToast, false);
assert.equal(calls.extractionStatus[0]?.text, "重新提取准备中");
assert.match(calls.extractionStatus[0]?.meta || "", /退化为从 2 到最新重提/);
assert.equal(calls.extractionStatus[1]?.text, "重新提取中");
assert.match(calls.extractionStatus[1]?.meta || "", /正在开始重新提取/);
assert.match(result.reason, /退化为从起始楼层到最新重提/);
}
@@ -98,6 +110,7 @@ const chat = [
const calls = {
rollback: [],
manual: [],
extractionStatus: [],
};
const runtime = {
getContext() {
@@ -110,6 +123,9 @@ const chat = [
return true;
},
setRuntimeStatus() {},
setLastExtractionStatus(text, meta, level) {
calls.extractionStatus.push({ text, meta, level });
},
rollbackGraphForReroll: async (fromFloor) => {
calls.rollback.push(fromFloor);
return { success: true, effectiveFromFloor: fromFloor };
@@ -131,6 +147,95 @@ const chat = [
assert.equal(result.fallbackToLatest, false);
assert.deepEqual(calls.rollback, [6]);
assert.equal(calls.manual[0].lockedEndFloor, 6);
assert.equal(calls.manual[0].showStartToast, false);
assert.equal(calls.extractionStatus[0]?.text, "重新提取准备中");
}
{
const statuses = [];
let lastProcessedAssistantFloor = -1;
const runtime = {
getIsExtracting() {
return false;
},
ensureGraphMutationReady() {
return true;
},
async recoverHistoryIfNeeded() {
return true;
},
getCurrentGraph() {
return { historyState: {} };
},
getContext() {
return { chat };
},
getAssistantTurns() {
return [2, 6];
},
getLastProcessedAssistantFloor() {
return lastProcessedAssistantFloor;
},
getSettings() {
return { extractEvery: 1 };
},
clampInt(value, fallback, min, max) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return fallback;
return Math.min(max, Math.max(min, Math.trunc(numeric)));
},
setIsExtracting() {},
beginStageAbortController() {
return { signal: null };
},
finishStageAbortController() {},
setLastExtractionStatus(text, meta, level) {
statuses.push({ text, meta, level });
},
async executeExtractionBatch({ endIdx }) {
lastProcessedAssistantFloor = endIdx;
return {
success: true,
result: {
newNodes: 1,
updatedNodes: 0,
newEdges: 1,
},
effects: {
warnings: [],
},
historyAdvanceAllowed: true,
};
},
isAbortError() {
return false;
},
refreshPanelLiveState() {},
retryPendingGraphPersist: async () => ({ accepted: true }),
toastr: {
info() {},
success() {},
warning() {},
error() {},
},
};
await onManualExtractController(runtime, {
taskLabel: "重新提取",
toastTitle: "ST-BME 重新提取",
showStartToast: false,
});
assert.equal(statuses[0]?.text, "重新提取中");
assert.match(statuses[0]?.meta || "", /待处理 AI 回复 2 条/);
assert.ok(
statuses.some(
(entry) =>
entry.text === "重新提取中" &&
/已处理 1\/2 条 AI 回复/.test(entry.meta || ""),
),
);
assert.equal(statuses[statuses.length - 1]?.text, "重新提取完成");
}
{

View File

@@ -731,6 +731,9 @@ function createRerollHarness() {
setRuntimeStatus(text, meta = "", level = "info") {
context.runtimeStatus = { text, meta, level };
},
setLastExtractionStatus(text, meta = "", level = "info") {
context.lastExtractionStatus = { text, meta, level };
},
clearInjectionState() {
context.clearInjectionCalls += 1;
},