Merge branch 'main' into main

This commit is contained in:
Hao19911125
2026-04-10 18:43:42 +08:00
committed by GitHub
11 changed files with 1103 additions and 76 deletions

View File

@@ -26,6 +26,157 @@ export function isBmeManagedHiddenMessage(
);
}
export function isDialogueGreetingMessage(
message,
{ index = null } = {},
) {
if (!Number.isFinite(index) || index !== 0) return false;
if (!message || typeof message !== "object") return false;
return String(message?.mes ?? "").trim().length > 0;
}
export function isTrueSystemMessage(
message,
{ index = null, chat = null } = {},
) {
if (!message?.is_system) return false;
if (isDialogueGreetingMessage(message, { index, chat })) return false;
return !isBmeManagedHiddenMessage(message, { index, chat });
}
export function isDialogueCountedMessage(
message,
{ index = null, chat = null } = {},
) {
if (!message || typeof message !== "object") return false;
if (!String(message?.mes ?? "").trim()) return false;
return !isTrueSystemMessage(message, { index, chat });
}
export function isDialogueAssistantMessage(
message,
{ index = null, chat = null } = {},
) {
if (!isDialogueCountedMessage(message, { index, chat })) return false;
if (isDialogueGreetingMessage(message, { index, chat })) return false;
return Boolean(message) && !message.is_user;
}
export function buildDialogueFloorMap(chat = []) {
const floorToChatIndex = [];
const chatIndexToFloor = {};
const floorToRole = {};
const assistantDialogueFloors = [];
const assistantChatIndices = [];
if (!Array.isArray(chat)) {
return {
latestDialogueFloor: -1,
floorToChatIndex,
chatIndexToFloor,
floorToRole,
assistantDialogueFloors,
assistantChatIndices,
};
}
let currentFloor = -1;
for (let index = 0; index < chat.length; index += 1) {
const message = chat[index];
if (!isDialogueCountedMessage(message, { index, chat })) continue;
currentFloor += 1;
floorToChatIndex[currentFloor] = index;
chatIndexToFloor[index] = currentFloor;
if (isDialogueGreetingMessage(message, { index, chat })) {
floorToRole[currentFloor] = "greeting";
continue;
}
const role = message?.is_user ? "user" : "assistant";
floorToRole[currentFloor] = role;
if (role === "assistant") {
assistantDialogueFloors.push(currentFloor);
assistantChatIndices.push(index);
}
}
return {
latestDialogueFloor: currentFloor,
floorToChatIndex,
chatIndexToFloor,
floorToRole,
assistantDialogueFloors,
assistantChatIndices,
};
}
export function normalizeDialogueFloorRange(
chat = [],
startFloor = null,
endFloor = null,
) {
const map = buildDialogueFloorMap(chat);
const latestDialogueFloor = Number(map.latestDialogueFloor);
const hasStart =
startFloor !== null &&
startFloor !== undefined &&
startFloor !== "" &&
Number.isFinite(Number(startFloor));
const hasEnd =
endFloor !== null &&
endFloor !== undefined &&
endFloor !== "" &&
Number.isFinite(Number(endFloor));
if (latestDialogueFloor < 0) {
return {
map,
latestDialogueFloor,
valid: false,
reason: "empty-dialogue",
startFloor: null,
endFloor: null,
};
}
if (!hasStart && hasEnd) {
return {
map,
latestDialogueFloor,
valid: false,
reason: "end-without-start",
startFloor: null,
endFloor: null,
};
}
const normalizedStart = hasStart
? Math.max(0, Math.min(latestDialogueFloor, Math.floor(Number(startFloor))))
: null;
const normalizedEnd = hasEnd
? Math.max(
normalizedStart ?? 0,
Math.min(latestDialogueFloor, Math.floor(Number(endFloor))),
)
: hasStart
? latestDialogueFloor
: null;
return {
map,
latestDialogueFloor,
valid: true,
reason: "",
startFloor: normalizedStart,
endFloor: normalizedEnd,
};
}
export function getDialogueFloorForChatIndex(chat = [], chatIndex = null) {
if (!Number.isFinite(Number(chatIndex))) return null;
const map = buildDialogueFloorMap(chat);
const floor = map.chatIndexToFloor[Math.floor(Number(chatIndex))];
return Number.isFinite(Number(floor)) ? Number(floor) : null;
}
function cloneChatMessageForPluginView(message) {
if (!message || typeof message !== "object") {
return message;

View File

@@ -2,6 +2,10 @@
// 通过 runtime 依赖注入,避免直接访问 index.js 模块级状态。
import { debugLog } from "../runtime/debug-logging.js";
import {
buildDialogueFloorMap,
normalizeDialogueFloorRange,
} from "./chat-history.js";
function toSafeFloor(value, fallback = null) {
if (value == null || value === "") return fallback;
@@ -92,6 +96,122 @@ function cloneSerializable(value, fallback = null) {
}
}
function resolveLatestAssistantDialogueFloor(chat = []) {
const map = buildDialogueFloorMap(chat);
const assistantDialogueFloors = Array.isArray(map.assistantDialogueFloors)
? map.assistantDialogueFloors
: [];
return assistantDialogueFloors.length > 0
? assistantDialogueFloors[assistantDialogueFloors.length - 1]
: null;
}
function resolveRerunDialogueTask(chat = [], options = {}) {
const hasStart = Number.isFinite(Number(options?.startFloor));
const hasEnd = Number.isFinite(Number(options?.endFloor));
if (!hasStart && !hasEnd) {
const latestAssistantDialogueFloor = resolveLatestAssistantDialogueFloor(chat);
if (!Number.isFinite(Number(latestAssistantDialogueFloor))) {
return {
valid: false,
reason: "当前没有可重提的 AI 回复",
};
}
const normalizedRange = normalizeDialogueFloorRange(
chat,
latestAssistantDialogueFloor,
latestAssistantDialogueFloor,
);
return {
...normalizedRange,
mode: "current",
requestedStartFloor: null,
requestedEndFloor: null,
};
}
const normalizedRange = normalizeDialogueFloorRange(
chat,
options?.startFloor,
options?.endFloor,
);
return {
...normalizedRange,
mode: "range",
requestedStartFloor: hasStart ? Number(options.startFloor) : null,
requestedEndFloor: hasEnd ? Number(options.endFloor) : null,
};
}
function resolveAssistantTargetRange(chat = [], dialogueRange = [-1, -1]) {
const map = buildDialogueFloorMap(chat);
const assistantDialogueFloors = Array.isArray(map.assistantDialogueFloors)
? map.assistantDialogueFloors
: [];
const assistantChatIndices = Array.isArray(map.assistantChatIndices)
? map.assistantChatIndices
: [];
const [startFloor, endFloor] = Array.isArray(dialogueRange)
? dialogueRange
: [-1, -1];
const targeted = [];
for (let index = 0; index < assistantDialogueFloors.length; index += 1) {
const floor = Number(assistantDialogueFloors[index]);
const chatIndex = Number(assistantChatIndices[index]);
if (!Number.isFinite(floor) || !Number.isFinite(chatIndex)) continue;
if (floor < startFloor || floor > endFloor) continue;
targeted.push({
dialogueFloor: floor,
chatIndex,
});
}
return {
map,
targeted,
startAssistantChatIndex: targeted.length > 0 ? targeted[0].chatIndex : null,
endAssistantChatIndex:
targeted.length > 0 ? targeted[targeted.length - 1].chatIndex : null,
latestAssistantDialogueFloor:
assistantDialogueFloors.length > 0
? assistantDialogueFloors[assistantDialogueFloors.length - 1]
: null,
};
}
function buildRerunFallbackInfo(chat = [], targetDialogueRange = [-1, -1]) {
const assistantRange = resolveAssistantTargetRange(chat, targetDialogueRange);
if (!assistantRange.targeted.length) {
return {
valid: false,
reason: "目标范围内没有可重提的 AI 回复",
fallbackToLatest: false,
...assistantRange,
};
}
const latestTargetedDialogueFloor = Number(
assistantRange.targeted[assistantRange.targeted.length - 1]?.dialogueFloor,
);
const latestAssistantDialogueFloor = Number(
assistantRange.latestAssistantDialogueFloor,
);
const fallbackToLatest =
Number.isFinite(latestTargetedDialogueFloor) &&
Number.isFinite(latestAssistantDialogueFloor) &&
latestTargetedDialogueFloor < latestAssistantDialogueFloor;
return {
valid: true,
reason: fallbackToLatest
? "当前图谱对中段范围重提的后缀保留证据不足,已退化为从起始楼层到最新重提"
: "",
fallbackToLatest,
...assistantRange,
};
}
function buildCommittedBatchPersistSnapshot(
runtime,
{
@@ -119,6 +239,29 @@ function buildCommittedBatchPersistSnapshot(
const range = Array.isArray(processedRange) ? processedRange : [null, null];
const rangeStart = Number.isFinite(Number(range[0])) ? Number(range[0]) : null;
const rangeEnd = Number.isFinite(Number(range[1])) ? Number(range[1]) : null;
const dialogueMap = buildDialogueFloorMap(chat);
const processedDialogueRange = [
Number.isFinite(Number(rangeStart))
? dialogueMap.chatIndexToFloor[rangeStart]
: null,
Number.isFinite(Number(rangeEnd))
? dialogueMap.chatIndexToFloor[rangeEnd]
: null,
];
const sourceChatIndexRange = [
Number.isFinite(Number(rangeStart))
? Math.max(
0,
Number(rangeStart) -
Math.max(
0,
Number(runtime?.getSettings?.()?.extractContextTurns) || 0,
) *
2,
)
: null,
rangeEnd,
];
const afterSnapshot = runtime.cloneGraphSnapshot(graph);
const effectiveArtifacts = Array.isArray(postProcessArtifacts)
? [...postProcessArtifacts]
@@ -149,6 +292,8 @@ function buildCommittedBatchPersistSnapshot(
typeof runtime.createBatchJournalEntry === "function"
? runtime.createBatchJournalEntry(beforeSnapshot, afterSnapshot, {
processedRange: [rangeStart, rangeEnd],
processedDialogueRange,
sourceChatIndexRange,
postProcessArtifacts: effectiveArtifacts,
vectorHashesInserted: Array.isArray(vectorHashesInserted)
? vectorHashesInserted
@@ -686,13 +831,17 @@ export async function onManualExtractController(runtime, options = {}) {
runtime.toastr.info("记忆提取正在进行中,请稍候");
return;
}
if (!runtime.ensureGraphMutationReady("手动提取")) return;
const taskLabel = String(options?.taskLabel || "手动提取").trim() || "手动提取";
const toastTitle = String(options?.toastTitle || `ST-BME ${taskLabel}`).trim() ||
`ST-BME ${taskLabel}`;
const lockedEndFloor = toSafeFloor(options?.lockedEndFloor, null);
if (!runtime.ensureGraphMutationReady(taskLabel)) return;
const pendingPersistGate = await maybeRetryPendingPersistence(
runtime,
"manual-extraction-persist-retry",
);
const pendingPersistMessage = pendingPersistGate
? formatPendingPersistenceGateMessage(runtime, "手动提取")
? formatPendingPersistenceGateMessage(runtime, taskLabel)
: "";
if (pendingPersistMessage) {
runtime.setLastExtractionStatus(
@@ -745,17 +894,22 @@ export async function onManualExtractController(runtime, options = {}) {
const extractionController = runtime.beginStageAbortController("extraction");
const extractionSignal = extractionController.signal;
runtime.setLastExtractionStatus(
"手动提取中",
`待处理 assistant 楼层 ${pendingAssistantTurns.length}`,
`${taskLabel}`,
lockedEndFloor != null
? `待处理 assistant 楼层 ${pendingAssistantTurns.length} 条 · 截止 chatIndex ${lockedEndFloor}`
: `待处理 assistant 楼层 ${pendingAssistantTurns.length}`,
"running",
{ syncRuntime: true, toastKind: "info", toastTitle: "ST-BME 手动提取" },
{ syncRuntime: true, toastKind: "info", toastTitle },
);
try {
while (true) {
const pendingTurns = runtime
.getAssistantTurns(chat)
.filter((i) => i > runtime.getLastProcessedAssistantFloor());
.filter((i) => {
if (i <= runtime.getLastProcessedAssistantFloor()) return false;
if (lockedEndFloor != null && i > lockedEndFloor) return false;
return true;
});
if (pendingTurns.length === 0) break;
const batchAssistantTurns = pendingTurns.slice(0, extractEvery);
@@ -802,7 +956,9 @@ export async function onManualExtractController(runtime, options = {}) {
if (totals.batches === 0) {
runtime.setLastExtractionStatus(
"无待提取内容",
"没有新的 assistant 回复需要处理",
lockedEndFloor != null
? "指定范围内没有新的 assistant 回复需要处理"
: "没有新的 assistant 回复需要处理",
"info",
{
syncRuntime: true,
@@ -818,13 +974,13 @@ export async function onManualExtractController(runtime, options = {}) {
`提取完成但持久化待确认:${pendingAfterRun.reason || pendingAfterRun.outcome || "unknown"}`,
);
runtime.setLastExtractionStatus(
"手动提取完成,持久化待确认",
`${taskLabel}完成,持久化待确认`,
`${totals.batches} 批 · 新建 ${totals.newNodes} · 更新 ${totals.updatedNodes} · 新边 ${totals.newEdges}${pendingAfterRun.reason ? ` · ${pendingAfterRun.reason}` : ""}`,
"warning",
{
syncRuntime: true,
toastKind: "",
toastTitle: "ST-BME 手动提取",
toastTitle,
},
);
} else {
@@ -832,13 +988,13 @@ export async function onManualExtractController(runtime, options = {}) {
`提取完成:${totals.batches} 批,新建 ${totals.newNodes},更新 ${totals.updatedNodes},新边 ${totals.newEdges}`,
);
runtime.setLastExtractionStatus(
"手动提取完成",
`${taskLabel}完成`,
`${totals.batches} 批 · 新建 ${totals.newNodes} · 更新 ${totals.updatedNodes} · 新边 ${totals.newEdges}`,
"success",
{
syncRuntime: true,
toastKind: "success",
toastTitle: "ST-BME 手动提取",
toastTitle,
},
);
}
@@ -850,7 +1006,7 @@ export async function onManualExtractController(runtime, options = {}) {
} catch (e) {
if (runtime.isAbortError(e)) {
runtime.setLastExtractionStatus(
"手动提取已终止",
`${taskLabel}已终止`,
e?.message || "已手动终止当前提取",
"warning",
{
@@ -860,12 +1016,12 @@ export async function onManualExtractController(runtime, options = {}) {
return;
}
runtime.console.error("[ST-BME] 手动提取失败:", e);
runtime.setLastExtractionStatus("手动提取失败", e?.message || String(e), "error", {
runtime.setLastExtractionStatus(`${taskLabel}失败`, e?.message || String(e), "error", {
syncRuntime: true,
toastKind: "",
toastTitle: "ST-BME 手动提取",
toastTitle,
});
runtime.toastr.error(`手动提取失败: ${e.message || e}`);
runtime.toastr.error(`${taskLabel}失败: ${e.message || e}`);
} finally {
runtime.finishStageAbortController("extraction", extractionController);
runtime.setIsExtracting(false);
@@ -873,6 +1029,116 @@ export async function onManualExtractController(runtime, options = {}) {
}
}
export async function onExtractionTaskController(runtime, options = {}) {
const requestedMode = String(options?.mode || "pending").trim().toLowerCase();
const context = runtime.getContext?.() || {};
const chat = Array.isArray(context?.chat) ? context.chat : [];
const runManualExtract = async (manualOptions = {}) => {
if (typeof runtime?.onManualExtract === "function") {
return await runtime.onManualExtract(manualOptions);
}
return await onManualExtractController(runtime, manualOptions);
};
if (requestedMode === "pending") {
return await runManualExtract({
...options,
taskLabel: "提取未处理",
toastTitle: "ST-BME 重新提取",
});
}
const rerunTask = resolveRerunDialogueTask(chat, options);
if (!rerunTask.valid) {
runtime.toastr?.info?.(rerunTask.reason || "当前没有可重提的范围");
return {
success: false,
rerunPerformed: false,
fallbackToLatest: false,
requestedRange: [null, null],
effectiveDialogueRange: [null, null],
reason: rerunTask.reason || "invalid-rerun-range",
};
}
const fallbackInfo = buildRerunFallbackInfo(chat, [
rerunTask.startFloor,
rerunTask.endFloor,
]);
if (!fallbackInfo.valid) {
runtime.toastr?.info?.(fallbackInfo.reason || "目标范围内没有可重提的 AI 回复");
return {
success: false,
rerunPerformed: false,
fallbackToLatest: false,
requestedRange: [rerunTask.requestedStartFloor, rerunTask.requestedEndFloor],
effectiveDialogueRange: [rerunTask.startFloor, rerunTask.endFloor],
reason: fallbackInfo.reason || "no-assistant-in-range",
};
}
const effectiveLockedEndFloor = fallbackInfo.fallbackToLatest
? null
: fallbackInfo.endAssistantChatIndex;
const effectiveDialogueRange = [
rerunTask.startFloor,
fallbackInfo.fallbackToLatest
? Number.isFinite(Number(fallbackInfo.latestAssistantDialogueFloor))
? Number(fallbackInfo.latestAssistantDialogueFloor)
: rerunTask.endFloor
: rerunTask.endFloor,
];
runtime.setRuntimeStatus(
"重新提取中",
fallbackInfo.fallbackToLatest
? `范围 ${rerunTask.startFloor} ~ ${rerunTask.endFloor} 命中旧批次,但当前将退化为从 ${effectiveDialogueRange[0]} 到最新重提`
: `准备重提范围 ${rerunTask.startFloor} ~ ${rerunTask.endFloor}`,
fallbackInfo.fallbackToLatest ? "warning" : "running",
);
const rollbackResult = await runtime.rollbackGraphForReroll(
fallbackInfo.startAssistantChatIndex,
context,
);
if (!rollbackResult?.success) {
return {
...rollbackResult,
rerunPerformed: false,
fallbackToLatest: fallbackInfo.fallbackToLatest,
requestedRange: [rerunTask.requestedStartFloor, rerunTask.requestedEndFloor],
effectiveDialogueRange,
};
}
if (fallbackInfo.reason) {
runtime.toastr?.warning?.(fallbackInfo.reason, "ST-BME 重新提取", {
timeOut: 3500,
});
}
await runManualExtract({
drainAll: true,
lockedEndFloor: effectiveLockedEndFloor,
taskLabel: "重新提取",
toastTitle: "ST-BME 重新提取",
});
return {
success: true,
rerunPerformed: true,
fallbackToLatest: fallbackInfo.fallbackToLatest,
requestedRange: [rerunTask.requestedStartFloor, rerunTask.requestedEndFloor],
effectiveDialogueRange,
effectiveAssistantChatRange: [
fallbackInfo.startAssistantChatIndex,
effectiveLockedEndFloor,
],
rollbackResult,
reason: fallbackInfo.reason || "",
};
}
export async function onRerollController(runtime, { fromFloor } = {}) {
if (runtime.getIsExtracting?.()) {
runtime.toastr?.info?.("记忆提取正在进行中,请稍候");

View File

@@ -9,13 +9,16 @@ import { applyTaskRegex } from "../prompting/task-regex.js";
import { getActiveTaskProfile } from "../prompting/prompt-profiles.js";
import {
appendSummaryEntry,
createSummaryEntry,
createDefaultSummaryState,
getActiveSummaryEntries,
markSummaryEntriesFolded,
normalizeGraphSummaryState,
} from "../graph/summary-state.js";
import { buildSummarySourceMessages } from "./chat-history.js";
import {
buildDialogueFloorMap,
buildSummarySourceMessages,
getDialogueFloorForChatIndex,
} from "./chat-history.js";
import { getSTContextForPrompt } from "../host/st-context.js";
import {
deriveStoryTimeSpanFromNodes,
@@ -128,6 +131,63 @@ function collectJournalTouchedNodeIds(journal = {}) {
]);
}
function intersectsRange(leftRange, rightRange) {
const [leftStart, leftEnd] = normalizeRange(leftRange);
const [rightStart, rightEnd] = normalizeRange(rightRange);
if (leftStart < 0 || leftEnd < 0 || rightStart < 0 || rightEnd < 0) {
return false;
}
return leftStart <= rightEnd && rightStart <= leftEnd;
}
function buildDialogueRangeFromMessageRange(chat = [], messageRange = [-1, -1]) {
const [messageStart, messageEnd] = normalizeRange(messageRange);
if (messageStart < 0 || messageEnd < 0) {
return [-1, -1];
}
const startFloor = getDialogueFloorForChatIndex(chat, messageStart);
const endFloor = getDialogueFloorForChatIndex(chat, messageEnd);
return [
Number.isFinite(Number(startFloor)) ? Number(startFloor) : -1,
Number.isFinite(Number(endFloor)) ? Number(endFloor) : -1,
];
}
function getSummaryEntryDialogueRange(chat = [], entry = {}) {
const directRange = normalizeRange(entry?.dialogueRange);
if (directRange[0] >= 0 && directRange[1] >= 0) {
return directRange;
}
return buildDialogueRangeFromMessageRange(chat, entry?.messageRange);
}
function removeSummaryEntriesByIds(graph, entryIds = []) {
normalizeGraphSummaryState(graph);
const targetIds = new Set(uniqueIds(entryIds));
if (targetIds.size === 0) return 0;
const queue = [...targetIds];
while (queue.length > 0) {
const currentId = queue.shift();
for (const entry of graph.summaryState.entries || []) {
if (targetIds.has(entry.id)) continue;
const sourceSummaryIds = Array.isArray(entry?.sourceSummaryIds)
? entry.sourceSummaryIds
: [];
if (!sourceSummaryIds.includes(currentId)) continue;
targetIds.add(entry.id);
queue.push(entry.id);
}
}
graph.summaryState.entries = (graph.summaryState.entries || []).filter(
(entry) => !targetIds.has(entry.id),
);
graph.summaryState.activeEntryIds = (graph.summaryState.activeEntryIds || []).filter(
(entryId) => !targetIds.has(entryId),
);
return targetIds.size;
}
function findJournalForExtractionCount(graph, extractionCountBefore) {
const target = Number(extractionCountBefore);
const journals = Array.isArray(graph?.batchJournal) ? graph.batchJournal : [];
@@ -143,12 +203,18 @@ function findJournalForExtractionCount(graph, extractionCountBefore) {
return null;
}
function buildPseudoCurrentSlice(currentExtractionCount, currentRange, currentNodeIds = []) {
function buildPseudoCurrentSlice(
currentExtractionCount,
currentRange,
currentNodeIds = [],
currentDialogueRange = null,
) {
return {
id: `summary-pending-${currentExtractionCount}`,
extractionCountBefore: Math.max(0, currentExtractionCount - 1),
extractionCountAfter: currentExtractionCount,
processedRange: normalizeRange(currentRange),
processedDialogueRange: normalizeRange(currentDialogueRange),
touchedNodeIds: uniqueIds(currentNodeIds),
};
}
@@ -160,7 +226,11 @@ function buildSliceFromJournal(journal = {}) {
extractionCountAfter:
clampInt(journal?.stateBefore?.extractionCount, 0, 0, 999999) + 1,
processedRange: normalizeRange(journal?.processedRange),
touchedNodeIds: collectJournalTouchedNodeIds(journal),
processedDialogueRange: normalizeRange(journal?.processedDialogueRange),
touchedNodeIds: uniqueIds([
...(Array.isArray(journal?.touchedNodeIds) ? journal.touchedNodeIds : []),
...collectJournalTouchedNodeIds(journal),
]),
};
}
@@ -170,6 +240,7 @@ function collectSlicesForSummaryWindow(
lastSummarizedExtractionCount = 0,
currentExtractionCount = 0,
currentRange = null,
currentDialogueRange = null,
currentNodeIds = [],
includeCurrentPending = false,
} = {},
@@ -194,7 +265,12 @@ function collectSlicesForSummaryWindow(
}
if (hasCurrentPendingRange && safeCurrentCount > safeLastCount) {
slices.push(
buildPseudoCurrentSlice(safeCurrentCount, currentRange, currentNodeIds),
buildPseudoCurrentSlice(
safeCurrentCount,
currentRange,
currentNodeIds,
currentDialogueRange,
),
);
}
return slices.sort(
@@ -348,6 +424,7 @@ export async function generateSmallSummary({
lastSummarizedExtractionCount: summaryState.lastSummarizedExtractionCount,
currentExtractionCount,
currentRange,
currentDialogueRange: buildDialogueRangeFromMessageRange(chat, currentRange),
currentNodeIds,
includeCurrentPending: true,
});
@@ -384,6 +461,7 @@ export async function generateSmallSummary({
? Number(sourceMessages[sourceMessages.length - 1].seq)
: messageEnd,
];
const dialogueRange = buildDialogueRangeFromMessageRange(chat, messageRange);
const sourceNodeIds = uniqueIds(
slices.flatMap((slice) => Array.isArray(slice.touchedNodeIds) ? slice.touchedNodeIds : []),
);
@@ -446,6 +524,7 @@ export async function generateSmallSummary({
sourceTask: "synopsis",
extractionRange: [firstSlice.extractionCountAfter, lastSlice.extractionCountAfter],
messageRange,
dialogueRange,
sourceBatchIds: uniqueIds(slices.map((slice) => slice.id)),
sourceSummaryIds: [],
sourceNodeIds,
@@ -466,6 +545,7 @@ export async function generateSmallSummary({
sourceMessages,
sourceNodeIds,
messageRange,
dialogueRange,
};
}
@@ -574,6 +654,20 @@ export async function rollupSummaryFrontier({
Math.min(...candidates.map((entry) => normalizeRange(entry.messageRange)[0])),
Math.max(...candidates.map((entry) => normalizeRange(entry.messageRange)[1])),
];
const dialogueRange = [
Math.min(
...candidates.map((entry) => {
const range = normalizeRange(entry?.dialogueRange, normalizeRange(entry?.messageRange));
return range[0];
}),
),
Math.max(
...candidates.map((entry) => {
const range = normalizeRange(entry?.dialogueRange, normalizeRange(entry?.messageRange));
return range[1];
}),
),
];
const storyTimeSpan = deriveStoryTimeSpanFromNodes(
graph,
nodeHints.nodes,
@@ -592,6 +686,7 @@ export async function rollupSummaryFrontier({
sourceTask: "summary_rollup",
extractionRange,
messageRange,
dialogueRange,
sourceBatchIds: uniqueIds(
candidates.flatMap((entry) =>
Array.isArray(entry.sourceBatchIds) ? entry.sourceBatchIds : [],
@@ -684,14 +779,121 @@ function clearSummaryState(graph) {
graph.summaryState = createDefaultSummaryState();
}
function getSliceDialogueRange(chat = [], slice = {}) {
const directRange = normalizeRange(slice?.processedDialogueRange);
if (directRange[0] >= 0 && directRange[1] >= 0) {
return directRange;
}
return buildDialogueRangeFromMessageRange(chat, slice?.processedRange);
}
function getSuffixRebuildStartFromDialogueRange(
graph,
chat = [],
targetDialogueRange = [-1, -1],
) {
const slices = collectSlicesForSummaryWindow(graph, {
lastSummarizedExtractionCount: 0,
currentExtractionCount: clampInt(
graph?.historyState?.extractionCount,
0,
0,
999999,
),
currentRange: null,
currentNodeIds: [],
includeCurrentPending: false,
});
const affectedSlices = slices.filter((slice) =>
intersectsRange(getSliceDialogueRange(chat, slice), targetDialogueRange),
);
if (affectedSlices.length === 0) {
return null;
}
return {
rebuildFromExtractionCount: Math.min(
...affectedSlices.map((slice) =>
clampInt(slice.extractionCountBefore, 0, 0, 999999),
),
),
affectedSlices,
};
}
function resolveCurrentSummaryDialogueRange(graph, chat = []) {
const activeEntries = getActiveSummaryEntries(graph);
if (activeEntries.length > 0) {
return getSummaryEntryDialogueRange(
chat,
activeEntries[activeEntries.length - 1],
);
}
const slices = collectSlicesForSummaryWindow(graph, {
lastSummarizedExtractionCount: 0,
currentExtractionCount: clampInt(
graph?.historyState?.extractionCount,
0,
0,
999999,
),
currentRange: null,
currentNodeIds: [],
includeCurrentPending: false,
});
if (slices.length === 0) {
return [-1, -1];
}
return getSliceDialogueRange(chat, slices[slices.length - 1]);
}
function trimSummaryStateForSuffixRebuild(graph, rebuildFromExtractionCount = 0) {
normalizeGraphSummaryState(graph);
const entries = Array.isArray(graph.summaryState?.entries)
? graph.summaryState.entries
: [];
const removeIds = entries
.filter((entry) => {
const extractionRange = normalizeRange(entry?.extractionRange);
return extractionRange[1] >= rebuildFromExtractionCount;
})
.map((entry) => entry.id);
const removedCount = removeSummaryEntriesByIds(graph, removeIds);
const remainingEntries = Array.isArray(graph.summaryState?.entries)
? graph.summaryState.entries
: [];
graph.summaryState.lastSummarizedExtractionCount =
remainingEntries.length > 0
? Math.max(
0,
...remainingEntries.map((entry) =>
normalizeRange(entry?.extractionRange)[1],
),
)
: Math.max(0, rebuildFromExtractionCount);
graph.summaryState.lastSummarizedAssistantFloor =
remainingEntries.length > 0
? Math.max(
-1,
...remainingEntries.map((entry) =>
normalizeRange(entry?.messageRange)[1],
),
)
: -1;
return {
removedCount,
};
}
export async function rebuildHierarchicalSummaryState({
graph,
chat = [],
settings = {},
signal,
mode = "current",
startFloor = null,
endFloor = null,
} = {}) {
normalizeGraphSummaryState(graph);
clearSummaryState(graph);
const currentExtractionCount = clampInt(
graph?.historyState?.extractionCount,
0,
@@ -707,9 +909,63 @@ export async function rebuildHierarchicalSummaryState({
};
}
let targetDialogueRange = [-1, -1];
if (String(mode || "current") === "range") {
if (!Number.isFinite(Number(startFloor))) {
return {
rebuilt: false,
smallSummaryCount: 0,
rollupCount: 0,
reason: "范围重建必须填写起始楼层",
};
}
const latestDialogueFloor = buildDialogueFloorMap(chat).latestDialogueFloor;
targetDialogueRange = [
clampInt(startFloor, 0, 0, Math.max(0, latestDialogueFloor)),
Number.isFinite(Number(endFloor))
? clampInt(endFloor, 0, 0, Math.max(0, latestDialogueFloor))
: Math.max(0, latestDialogueFloor),
];
targetDialogueRange[1] = Math.max(
targetDialogueRange[0],
targetDialogueRange[1],
);
} else {
targetDialogueRange = resolveCurrentSummaryDialogueRange(graph, chat);
}
if (targetDialogueRange[0] < 0 || targetDialogueRange[1] < 0) {
return {
rebuilt: false,
smallSummaryCount: 0,
rollupCount: 0,
reason: "当前没有可重建的总结范围",
};
}
const rebuildWindow = getSuffixRebuildStartFromDialogueRange(
graph,
chat,
targetDialogueRange,
);
if (!rebuildWindow) {
return {
rebuilt: false,
smallSummaryCount: 0,
rollupCount: 0,
reason: "目标范围内没有命中的总结切片",
targetDialogueRange,
};
}
const trimmed = trimSummaryStateForSuffixRebuild(
graph,
rebuildWindow.rebuildFromExtractionCount,
);
const threshold = clampInt(settings.smallSummaryEveryNExtractions, 3, 1, 100);
const slices = collectSlicesForSummaryWindow(graph, {
lastSummarizedExtractionCount: 0,
lastSummarizedExtractionCount: rebuildWindow.rebuildFromExtractionCount,
currentExtractionCount,
currentRange: null,
currentNodeIds: [],
@@ -770,6 +1026,10 @@ export async function rebuildHierarchicalSummaryState({
});
const summaryText = String(result?.summary || "").trim();
if (summaryText) {
const entryMessageRange = [
Number(sourceMessages[0]?.seq ?? -1),
Number(sourceMessages[sourceMessages.length - 1]?.seq ?? -1),
];
const entry = appendSummaryEntry(graph, {
level: 0,
kind: "small",
@@ -777,10 +1037,8 @@ export async function rebuildHierarchicalSummaryState({
text: summaryText,
sourceTask: "synopsis",
extractionRange: [firstSlice.extractionCountAfter, lastSlice.extractionCountAfter],
messageRange: [
Number(sourceMessages[0]?.seq ?? -1),
Number(sourceMessages[sourceMessages.length - 1]?.seq ?? -1),
],
messageRange: entryMessageRange,
dialogueRange: buildDialogueRangeFromMessageRange(chat, entryMessageRange),
sourceBatchIds: pendingSlices.map((item) => item.id),
sourceSummaryIds: [],
sourceNodeIds,
@@ -813,6 +1071,9 @@ export async function rebuildHierarchicalSummaryState({
rebuilt: smallSummaryCount > 0 || rollupCount > 0,
smallSummaryCount,
rollupCount,
targetDialogueRange,
rebuildFromExtractionCount: rebuildWindow.rebuildFromExtractionCount,
removedEntryCount: trimmed.removedCount,
reason:
smallSummaryCount > 0 || rollupCount > 0
? ""