mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
Merge branch 'main' into main
This commit is contained in:
@@ -73,6 +73,7 @@ export function normalizeSummaryEntry(entry = {}, options = {}) {
|
||||
sourceTask: String(source.sourceTask || "synopsis").trim() || "synopsis",
|
||||
extractionRange: normalizeNumberRange(source.extractionRange),
|
||||
messageRange: normalizeNumberRange(source.messageRange),
|
||||
dialogueRange: normalizeNumberRange(source.dialogueRange),
|
||||
sourceBatchIds: normalizeStringArray(source.sourceBatchIds),
|
||||
sourceSummaryIds: normalizeStringArray(source.sourceSummaryIds),
|
||||
sourceNodeIds: normalizeStringArray(source.sourceNodeIds),
|
||||
|
||||
47
index.js
47
index.js
@@ -78,6 +78,7 @@ import {
|
||||
} from "./host/event-binding.js";
|
||||
import {
|
||||
executeExtractionBatchController,
|
||||
onExtractionTaskController,
|
||||
onManualExtractController,
|
||||
onRerollController,
|
||||
resolveAutoExtractionPlanController,
|
||||
@@ -12762,6 +12763,47 @@ async function onManualExtract(options = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
async function onExtractionTask(options = {}) {
|
||||
return await onExtractionTaskController(
|
||||
{
|
||||
beginStageAbortController,
|
||||
clampInt,
|
||||
console,
|
||||
createEmptyGraph,
|
||||
ensureGraphMutationReady,
|
||||
executeExtractionBatch,
|
||||
finishStageAbortController,
|
||||
getAssistantTurns,
|
||||
getContext,
|
||||
getCurrentChatId,
|
||||
getCurrentGraph: () => currentGraph,
|
||||
getGraphMutationBlockReason,
|
||||
getGraphPersistenceState: () => graphPersistenceState,
|
||||
getIsExtracting: () => isExtracting,
|
||||
getLastExtractionStatusLevel: () => lastExtractionStatus?.level || "idle",
|
||||
getLastProcessedAssistantFloor,
|
||||
getSettings,
|
||||
isAbortError,
|
||||
normalizeGraphRuntimeState,
|
||||
onManualExtract,
|
||||
recoverHistoryIfNeeded,
|
||||
refreshPanelLiveState,
|
||||
retryPendingGraphPersist,
|
||||
rollbackGraphForReroll,
|
||||
setCurrentGraph: (graph) => {
|
||||
currentGraph = graph;
|
||||
},
|
||||
setIsExtracting: (value) => {
|
||||
isExtracting = value;
|
||||
},
|
||||
setLastExtractionStatus,
|
||||
setRuntimeStatus,
|
||||
toastr,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
async function onReroll({ fromFloor } = {}) {
|
||||
return await onRerollController(
|
||||
{
|
||||
@@ -12830,7 +12872,7 @@ async function onManualSummaryRollup() {
|
||||
});
|
||||
}
|
||||
|
||||
async function onRebuildSummaryState() {
|
||||
async function onRebuildSummaryState(options = {}) {
|
||||
return await onRebuildSummaryStateController({
|
||||
ensureGraphMutationReady,
|
||||
getContext,
|
||||
@@ -12841,7 +12883,7 @@ async function onRebuildSummaryState() {
|
||||
saveGraphToChat,
|
||||
setRuntimeStatus,
|
||||
toastr,
|
||||
});
|
||||
}, options);
|
||||
}
|
||||
|
||||
async function onClearSummaryState() {
|
||||
@@ -13181,6 +13223,7 @@ async function onRollbackLastRestore() {
|
||||
syncGraphLoadFromLiveContext({
|
||||
source: "panel-open-sync",
|
||||
}),
|
||||
extractTask: onExtractionTask,
|
||||
extract: onManualExtract,
|
||||
compress: onManualCompress,
|
||||
sleep: onManualSleep,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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?.("记忆提取正在进行中,请稍候");
|
||||
|
||||
@@ -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
|
||||
? ""
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Youzini",
|
||||
"version": "4.3.7",
|
||||
"version": "4.3.6",
|
||||
"homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology"
|
||||
}
|
||||
|
||||
@@ -811,10 +811,22 @@ export function createBatchJournalEntry(
|
||||
journalVersion: BATCH_JOURNAL_VERSION,
|
||||
createdAt: Date.now(),
|
||||
processedRange: meta.processedRange || [-1, -1],
|
||||
processedDialogueRange: Array.isArray(meta.processedDialogueRange)
|
||||
? meta.processedDialogueRange
|
||||
: [-1, -1],
|
||||
sourceChatIndexRange: Array.isArray(meta.sourceChatIndexRange)
|
||||
? meta.sourceChatIndexRange
|
||||
: [-1, -1],
|
||||
createdNodeIds,
|
||||
createdEdgeIds,
|
||||
previousNodeSnapshots,
|
||||
previousEdgeSnapshots,
|
||||
touchedNodeIds: normalizeStringArray(
|
||||
meta.touchedNodeIds || [
|
||||
...createdNodeIds,
|
||||
...previousNodeSnapshots.map((node) => node?.id),
|
||||
],
|
||||
),
|
||||
stateBefore: buildJournalStateBefore(snapshotBefore, meta),
|
||||
vectorDelta: buildVectorDelta(snapshotBefore, snapshotAfter, meta),
|
||||
postProcessArtifacts: Array.isArray(meta.postProcessArtifacts)
|
||||
|
||||
174
tests/dialogue-floor-range-tasks.mjs
Normal file
174
tests/dialogue-floor-range-tasks.mjs
Normal file
@@ -0,0 +1,174 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
buildDialogueFloorMap,
|
||||
normalizeDialogueFloorRange,
|
||||
} from "../maintenance/chat-history.js";
|
||||
import { onExtractionTaskController } from "../maintenance/extraction-controller.js";
|
||||
import { onRebuildSummaryStateController } from "../ui/ui-actions-controller.js";
|
||||
|
||||
const chat = [
|
||||
{ is_system: true, is_user: false, mes: "greeting" },
|
||||
{ is_user: true, mes: "user-1" },
|
||||
{ is_user: false, mes: "assistant-1" },
|
||||
{ is_system: true, is_user: false, mes: "real-system" },
|
||||
{
|
||||
is_system: true,
|
||||
is_user: false,
|
||||
mes: "managed-hidden-assistant",
|
||||
extra: { __st_bme_hide_managed: true },
|
||||
},
|
||||
{ is_user: true, mes: "user-2" },
|
||||
{ is_user: false, mes: "assistant-2" },
|
||||
];
|
||||
|
||||
{
|
||||
const mapping = buildDialogueFloorMap(chat);
|
||||
assert.equal(mapping.latestDialogueFloor, 5);
|
||||
assert.deepEqual(Array.from(mapping.floorToChatIndex), [0, 1, 2, 4, 5, 6]);
|
||||
assert.equal(mapping.floorToRole[0], "greeting");
|
||||
assert.deepEqual(Array.from(mapping.assistantDialogueFloors), [2, 3, 5]);
|
||||
assert.deepEqual(Array.from(mapping.assistantChatIndices), [2, 4, 6]);
|
||||
}
|
||||
|
||||
{
|
||||
const normalized = normalizeDialogueFloorRange(chat, 2, null);
|
||||
assert.equal(normalized.valid, true);
|
||||
assert.equal(normalized.startFloor, 2);
|
||||
assert.equal(normalized.endFloor, 5);
|
||||
}
|
||||
|
||||
{
|
||||
const normalized = normalizeDialogueFloorRange(chat, null, 4);
|
||||
assert.equal(normalized.valid, false);
|
||||
assert.equal(normalized.reason, "end-without-start");
|
||||
}
|
||||
|
||||
{
|
||||
const calls = {
|
||||
rollback: [],
|
||||
manual: [],
|
||||
warning: [],
|
||||
info: [],
|
||||
};
|
||||
const runtime = {
|
||||
getContext() {
|
||||
return { chat };
|
||||
},
|
||||
getIsExtracting() {
|
||||
return false;
|
||||
},
|
||||
ensureGraphMutationReady() {
|
||||
return true;
|
||||
},
|
||||
setRuntimeStatus() {},
|
||||
rollbackGraphForReroll: async (fromFloor) => {
|
||||
calls.rollback.push(fromFloor);
|
||||
return { success: true, effectiveFromFloor: fromFloor };
|
||||
},
|
||||
onManualExtract: async (options = {}) => {
|
||||
calls.manual.push({ ...options });
|
||||
},
|
||||
toastr: {
|
||||
warning(message) {
|
||||
calls.warning.push(String(message || ""));
|
||||
},
|
||||
info(message) {
|
||||
calls.info.push(String(message || ""));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await onExtractionTaskController(runtime, {
|
||||
mode: "rerun",
|
||||
startFloor: 2,
|
||||
endFloor: 2,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.fallbackToLatest, true);
|
||||
assert.deepEqual(calls.rollback, [2]);
|
||||
assert.equal(calls.manual.length, 1);
|
||||
assert.equal(calls.manual[0].lockedEndFloor, null);
|
||||
assert.equal(calls.manual[0].taskLabel, "重新提取");
|
||||
assert.match(result.reason, /退化为从起始楼层到最新重提/);
|
||||
}
|
||||
|
||||
{
|
||||
const calls = {
|
||||
rollback: [],
|
||||
manual: [],
|
||||
};
|
||||
const runtime = {
|
||||
getContext() {
|
||||
return { chat };
|
||||
},
|
||||
getIsExtracting() {
|
||||
return false;
|
||||
},
|
||||
ensureGraphMutationReady() {
|
||||
return true;
|
||||
},
|
||||
setRuntimeStatus() {},
|
||||
rollbackGraphForReroll: async (fromFloor) => {
|
||||
calls.rollback.push(fromFloor);
|
||||
return { success: true, effectiveFromFloor: fromFloor };
|
||||
},
|
||||
onManualExtract: async (options = {}) => {
|
||||
calls.manual.push({ ...options });
|
||||
},
|
||||
toastr: {
|
||||
warning() {},
|
||||
info() {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await onExtractionTaskController(runtime, {
|
||||
mode: "rerun",
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.fallbackToLatest, false);
|
||||
assert.deepEqual(calls.rollback, [6]);
|
||||
assert.equal(calls.manual[0].lockedEndFloor, 6);
|
||||
}
|
||||
|
||||
{
|
||||
const captured = [];
|
||||
const runtime = {
|
||||
getCurrentGraph() {
|
||||
return {};
|
||||
},
|
||||
ensureGraphMutationReady() {
|
||||
return true;
|
||||
},
|
||||
getContext() {
|
||||
return { chat };
|
||||
},
|
||||
getSettings() {
|
||||
return {};
|
||||
},
|
||||
rebuildHierarchicalSummaryState: async (payload) => {
|
||||
captured.push(payload);
|
||||
return { rebuilt: false, reason: "noop" };
|
||||
},
|
||||
saveGraphToChat() {},
|
||||
refreshPanelLiveState() {},
|
||||
setRuntimeStatus() {},
|
||||
toastr: {
|
||||
info() {},
|
||||
success() {},
|
||||
},
|
||||
};
|
||||
|
||||
await onRebuildSummaryStateController(runtime, {});
|
||||
await onRebuildSummaryStateController(runtime, { startFloor: 1, endFloor: 3 });
|
||||
|
||||
assert.equal(captured[0].mode, "current");
|
||||
assert.equal(captured[0].startFloor, null);
|
||||
assert.equal(captured[1].mode, "range");
|
||||
assert.equal(captured[1].startFloor, 1);
|
||||
assert.equal(captured[1].endFloor, 3);
|
||||
}
|
||||
|
||||
console.log("dialogue-floor-range-tasks tests passed");
|
||||
@@ -324,8 +324,8 @@
|
||||
</div>
|
||||
<div class="bme-action-grid">
|
||||
<button class="bme-action-btn" id="bme-act-extract" type="button">
|
||||
<i class="fa-solid fa-download"></i>
|
||||
<span>手动提取</span>
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
<span>重新提取</span>
|
||||
</button>
|
||||
<button class="bme-action-btn" id="bme-act-compress" type="button">
|
||||
<i class="fa-solid fa-compress"></i>
|
||||
@@ -359,26 +359,72 @@
|
||||
<i class="fa-solid fa-rotate-left"></i>
|
||||
<span>撤销最近维护</span>
|
||||
</button>
|
||||
<button class="bme-action-btn" id="bme-act-reroll" type="button">
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
<span>重新提取</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bme-action-group-extra">
|
||||
<div class="bme-config-help">
|
||||
重新提取:回滚指定楼层及之后的提取结果并重做。留空则只重做最新 AI 楼。
|
||||
重新提取:按总楼层计数(用户+AI,首条 greeting 为 0)。真正 system 不计入,且不受隐藏助手影响。
|
||||
</div>
|
||||
<div class="bme-action-range-row">
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-extract-mode">提取模式</label>
|
||||
<select id="bme-extract-mode" class="bme-config-select">
|
||||
<option value="pending">提取未处理</option>
|
||||
<option value="rerun">重新提取范围</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-reroll-floor">起始楼层</label>
|
||||
<label for="bme-extract-start-floor">起始楼层</label>
|
||||
<input
|
||||
id="bme-reroll-floor"
|
||||
id="bme-extract-start-floor"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
min="0"
|
||||
max="999999"
|
||||
placeholder="留空 = 最新 AI 楼"
|
||||
placeholder="留空 = 当前重提"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-extract-end-floor">终止楼层</label>
|
||||
<input
|
||||
id="bme-extract-end-floor"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
min="0"
|
||||
max="999999"
|
||||
placeholder="留空 = 到最新"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-help" style="margin-top:8px">
|
||||
重新提取范围:起始/终止都留空 = 当前重提;只填起始 = 从起始到最新。
|
||||
</div>
|
||||
<div class="bme-config-help" style="margin-top:12px">
|
||||
重建总结状态:默认按当前总结相关范围重建;填写楼层后按范围重建。
|
||||
</div>
|
||||
<div class="bme-action-range-row">
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-summary-rebuild-start-floor">总结起始楼层</label>
|
||||
<input
|
||||
id="bme-summary-rebuild-start-floor"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
min="0"
|
||||
max="999999"
|
||||
placeholder="留空 = 当前重建"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-summary-rebuild-end-floor">总结终止楼层</label>
|
||||
<input
|
||||
id="bme-summary-rebuild-end-floor"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
min="0"
|
||||
max="999999"
|
||||
placeholder="留空 = 到最新"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1397,7 +1443,7 @@
|
||||
<span>自动提取晚一楼</span>
|
||||
</label>
|
||||
<div class="bme-config-help">
|
||||
开启后,最新 AI 楼先不自动提取,要等下一条 AI 楼出现后,才提取前一批内容。手动提取和重 Roll 不受影响。
|
||||
开启后,最新 AI 楼先不自动提取,要等下一条 AI 楼出现后,才提取前一批内容。提取未处理和范围重提不受影响。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
112
ui/panel.js
112
ui/panel.js
@@ -112,7 +112,6 @@ const GRAPH_WRITE_ACTION_IDS = [
|
||||
"bme-act-vector-rebuild",
|
||||
"bme-act-vector-range",
|
||||
"bme-act-vector-reembed",
|
||||
"bme-act-reroll",
|
||||
"bme-detail-delete",
|
||||
"bme-detail-save",
|
||||
"bme-cog-region-apply",
|
||||
@@ -857,11 +856,11 @@ function _onFabSingleClick() {
|
||||
}
|
||||
|
||||
async function _onFabDoubleClick() {
|
||||
if (!_actionHandlers.reroll) return;
|
||||
if (!_actionHandlers.extractTask) return;
|
||||
|
||||
try {
|
||||
_fabEl?.setAttribute("data-status", "running");
|
||||
await _actionHandlers.reroll({});
|
||||
await _actionHandlers.extractTask({ mode: "rerun" });
|
||||
_fabEl?.setAttribute("data-status", "success");
|
||||
_refreshDashboard();
|
||||
_refreshGraph();
|
||||
@@ -870,7 +869,7 @@ async function _onFabDoubleClick() {
|
||||
_fabEl?.setAttribute("data-status", status.status || "idle");
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
console.error("[ST-BME] FAB reroll failed:", err);
|
||||
console.error("[ST-BME] FAB extract task failed:", err);
|
||||
_fabEl?.setAttribute("data-status", "error");
|
||||
}
|
||||
}
|
||||
@@ -1725,7 +1724,11 @@ function _refreshMobileCognition() {
|
||||
}
|
||||
|
||||
function _formatSummaryEntryCard(entry = {}) {
|
||||
const messageRange = Array.isArray(entry?.messageRange) ? entry.messageRange : ["?", "?"];
|
||||
const messageRange = Array.isArray(entry?.dialogueRange)
|
||||
? entry.dialogueRange
|
||||
: Array.isArray(entry?.messageRange)
|
||||
? entry.messageRange
|
||||
: ["?", "?"];
|
||||
const extractionRange = Array.isArray(entry?.extractionRange)
|
||||
? entry.extractionRange
|
||||
: ["?", "?"];
|
||||
@@ -3736,12 +3739,10 @@ function _bindDashboardControls() {
|
||||
|
||||
function _bindActions() {
|
||||
const bindings = {
|
||||
"bme-act-extract": "extract",
|
||||
"bme-act-compress": "compress",
|
||||
"bme-act-sleep": "sleep",
|
||||
"bme-act-synopsis": "synopsis",
|
||||
"bme-act-summary-rollup": "summaryRollup",
|
||||
"bme-act-summary-rebuild": "rebuildSummaryState",
|
||||
"bme-act-summary-clear": "clearSummaryState",
|
||||
"bme-act-export": "export",
|
||||
"bme-act-import": "import",
|
||||
@@ -3763,7 +3764,6 @@ function _bindActions() {
|
||||
};
|
||||
|
||||
const actionLabels = {
|
||||
extract: "手动提取",
|
||||
compress: "手动压缩",
|
||||
sleep: "执行遗忘",
|
||||
synopsis: "生成小总结",
|
||||
@@ -3852,6 +3852,67 @@ function _bindActions() {
|
||||
});
|
||||
}
|
||||
|
||||
document
|
||||
.getElementById("bme-act-extract")
|
||||
?.addEventListener("click", async () => {
|
||||
const btn = document.getElementById("bme-act-extract");
|
||||
if (btn?.disabled) return;
|
||||
const mode =
|
||||
String(document.getElementById("bme-extract-mode")?.value || "pending")
|
||||
.trim()
|
||||
.toLowerCase() === "rerun"
|
||||
? "rerun"
|
||||
: "pending";
|
||||
const startFloor = _parseOptionalInt(
|
||||
document.getElementById("bme-extract-start-floor")?.value,
|
||||
);
|
||||
const endFloor = _parseOptionalInt(
|
||||
document.getElementById("bme-extract-end-floor")?.value,
|
||||
);
|
||||
const desc =
|
||||
mode === "pending"
|
||||
? "提取当前尚未处理的内容"
|
||||
: Number.isFinite(startFloor) || Number.isFinite(endFloor)
|
||||
? `重提范围 ${Number.isFinite(startFloor) ? startFloor : "当前"} ~ ${Number.isFinite(endFloor) ? endFloor : "最新"}`
|
||||
: "当前重提";
|
||||
|
||||
if (!confirm(`确认要执行吗?\n\n${desc}`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = "0.5";
|
||||
}
|
||||
|
||||
_showActionProgressUi("重新提取");
|
||||
try {
|
||||
await _actionHandlers.extractTask?.({
|
||||
mode,
|
||||
startFloor: Number.isFinite(startFloor) ? startFloor : undefined,
|
||||
endFloor: Number.isFinite(endFloor) ? endFloor : undefined,
|
||||
});
|
||||
_refreshDashboard();
|
||||
_refreshGraph();
|
||||
if (
|
||||
document
|
||||
.getElementById("bme-pane-memory")
|
||||
?.classList.contains("active")
|
||||
) {
|
||||
_refreshMemoryBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[ST-BME] Action extractTask failed:", error);
|
||||
toastr.error(`重新提取失败: ${error?.message || error}`, "ST-BME");
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.style.opacity = "";
|
||||
}
|
||||
_refreshRuntimeStatus();
|
||||
_refreshGraphAvailabilityState();
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("bme-act-vector-range")
|
||||
?.addEventListener("click", async () => {
|
||||
@@ -3892,32 +3953,31 @@ function _bindActions() {
|
||||
}
|
||||
});
|
||||
|
||||
// 重新提取 (reroll) 绑定
|
||||
document
|
||||
.getElementById("bme-act-reroll")
|
||||
.getElementById("bme-act-summary-rebuild")
|
||||
?.addEventListener("click", async () => {
|
||||
const btn = document.getElementById("bme-act-reroll");
|
||||
const btn = document.getElementById("bme-act-summary-rebuild");
|
||||
if (btn?.disabled) return;
|
||||
|
||||
const floorStr = document.getElementById("bme-reroll-floor")?.value;
|
||||
const fromFloor = _parseOptionalInt(floorStr);
|
||||
const desc = Number.isFinite(fromFloor)
|
||||
? `从楼层 ${fromFloor} 开始回滚并重新提取`
|
||||
: "回滚最新 AI 楼并重新提取";
|
||||
|
||||
if (!confirm(`确认要重新提取吗?\n\n${desc}\n\n已提取的记忆节点将被回滚。`)) {
|
||||
return;
|
||||
}
|
||||
const startFloor = _parseOptionalInt(
|
||||
document.getElementById("bme-summary-rebuild-start-floor")?.value,
|
||||
);
|
||||
const endFloor = _parseOptionalInt(
|
||||
document.getElementById("bme-summary-rebuild-end-floor")?.value,
|
||||
);
|
||||
const desc = Number.isFinite(startFloor) || Number.isFinite(endFloor)
|
||||
? `按范围 ${Number.isFinite(startFloor) ? startFloor : "当前"} ~ ${Number.isFinite(endFloor) ? endFloor : "最新"} 重建总结状态`
|
||||
: "按当前总结相关范围重建总结状态";
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = "0.5";
|
||||
}
|
||||
|
||||
_showActionProgressUi("重新提取");
|
||||
_showActionProgressUi("重建总结状态");
|
||||
try {
|
||||
await _actionHandlers.reroll?.({
|
||||
fromFloor: Number.isFinite(fromFloor) ? fromFloor : undefined,
|
||||
await _actionHandlers.rebuildSummaryState?.({
|
||||
startFloor: Number.isFinite(startFloor) ? startFloor : undefined,
|
||||
endFloor: Number.isFinite(endFloor) ? endFloor : undefined,
|
||||
});
|
||||
_refreshDashboard();
|
||||
_refreshGraph();
|
||||
@@ -3929,8 +3989,8 @@ function _bindActions() {
|
||||
_refreshMemoryBrowser();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[ST-BME] Action reroll failed:", error);
|
||||
toastr.error(`重新提取失败: ${error?.message || error}`, "ST-BME");
|
||||
console.error("[ST-BME] Action rebuildSummaryState failed:", error);
|
||||
toastr.error(`重建总结状态失败: ${error?.message || error}`, "ST-BME");
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.style.opacity = "";
|
||||
|
||||
@@ -771,11 +771,21 @@ export async function onManualSummaryRollupController(runtime) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function onRebuildSummaryStateController(runtime) {
|
||||
export async function onRebuildSummaryStateController(runtime, options = {}) {
|
||||
const graph = runtime.getCurrentGraph();
|
||||
if (!graph) return;
|
||||
if (!runtime.ensureGraphMutationReady("重建总结状态")) return;
|
||||
updateManualActionUiState(runtime, "重建总结中", "正在按现有提取批次重建总结链", "running");
|
||||
const hasStart = Number.isFinite(Number(options?.startFloor));
|
||||
const hasEnd = Number.isFinite(Number(options?.endFloor));
|
||||
const mode = hasStart || hasEnd ? "range" : "current";
|
||||
updateManualActionUiState(
|
||||
runtime,
|
||||
"重建总结中",
|
||||
mode === "range"
|
||||
? `正在按范围 ${hasStart ? Number(options.startFloor) : "?"} ~ ${hasEnd ? Number(options.endFloor) : "最新"} 重建总结链`
|
||||
: "正在重建当前总结相关范围",
|
||||
"running",
|
||||
);
|
||||
|
||||
try {
|
||||
const chat = runtime.getContext?.()?.chat;
|
||||
@@ -783,6 +793,9 @@ export async function onRebuildSummaryStateController(runtime) {
|
||||
graph,
|
||||
chat: Array.isArray(chat) ? chat : [],
|
||||
settings: runtime.getSettings(),
|
||||
mode,
|
||||
startFloor: hasStart ? Number(options.startFloor) : null,
|
||||
endFloor: hasEnd ? Number(options.endFloor) : null,
|
||||
});
|
||||
runtime.saveGraphToChat?.({ reason: "rebuild-summary-state" });
|
||||
runtime.refreshPanelLiveState?.();
|
||||
|
||||
Reference in New Issue
Block a user