mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
266 lines
9.3 KiB
JavaScript
266 lines
9.3 KiB
JavaScript
import {
|
|
createDefaultStoryTimeSpan,
|
|
normalizeStoryTimeSpan,
|
|
} from "./story-timeline.js";
|
|
|
|
export const SUMMARY_STATE_VERSION = 1;
|
|
const ACTIVE_STATUS = "active";
|
|
const FOLDED_STATUS = "folded";
|
|
const SUMMARY_KINDS = new Set(["small", "rollup", "legacy-import"]);
|
|
|
|
function summaryId() {
|
|
return `summary-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
}
|
|
|
|
function normalizeStringArray(values = []) {
|
|
return [...new Set(
|
|
(Array.isArray(values) ? values : [])
|
|
.map((value) => String(value || "").trim())
|
|
.filter(Boolean),
|
|
)];
|
|
}
|
|
|
|
function normalizeNumberRange(range, fallback = [-1, -1]) {
|
|
if (!Array.isArray(range) || range.length < 2) {
|
|
return [...fallback];
|
|
}
|
|
const start = Number.isFinite(Number(range[0])) ? Number(range[0]) : fallback[0];
|
|
const end = Number.isFinite(Number(range[1])) ? Number(range[1]) : fallback[1];
|
|
return [start, end];
|
|
}
|
|
|
|
export function createDefaultSummaryState(state = {}) {
|
|
const source =
|
|
state && typeof state === "object" && !Array.isArray(state) ? state : {};
|
|
return {
|
|
version: SUMMARY_STATE_VERSION,
|
|
enabled: source.enabled !== false,
|
|
entries: Array.isArray(source.entries)
|
|
? source.entries.map((entry, index) =>
|
|
normalizeSummaryEntry(entry, {
|
|
fallbackId: `summary-import-${index + 1}`,
|
|
}),
|
|
)
|
|
: [],
|
|
activeEntryIds: normalizeStringArray(source.activeEntryIds),
|
|
lastSummarizedExtractionCount: Number.isFinite(
|
|
Number(source.lastSummarizedExtractionCount),
|
|
)
|
|
? Math.max(0, Number(source.lastSummarizedExtractionCount))
|
|
: 0,
|
|
lastSummarizedAssistantFloor: Number.isFinite(
|
|
Number(source.lastSummarizedAssistantFloor),
|
|
)
|
|
? Number(source.lastSummarizedAssistantFloor)
|
|
: -1,
|
|
};
|
|
}
|
|
|
|
export function normalizeSummaryEntry(entry = {}, options = {}) {
|
|
const fallbackId = String(options?.fallbackId || "").trim() || summaryId();
|
|
const source =
|
|
entry && typeof entry === "object" && !Array.isArray(entry) ? entry : {};
|
|
const status = String(source.status || ACTIVE_STATUS).trim().toLowerCase();
|
|
const kind = String(source.kind || "small").trim().toLowerCase();
|
|
return {
|
|
id: String(source.id || fallbackId),
|
|
level: Number.isFinite(Number(source.level))
|
|
? Math.max(0, Number(source.level))
|
|
: 0,
|
|
kind: SUMMARY_KINDS.has(kind) ? kind : "small",
|
|
status: status === FOLDED_STATUS ? FOLDED_STATUS : ACTIVE_STATUS,
|
|
text: String(source.text || "").trim(),
|
|
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),
|
|
storyTimeSpan: normalizeStoryTimeSpan(
|
|
source.storyTimeSpan,
|
|
createDefaultStoryTimeSpan(),
|
|
),
|
|
regionHints: normalizeStringArray(source.regionHints),
|
|
ownerHints: normalizeStringArray(source.ownerHints),
|
|
createdAt: Number.isFinite(Number(source.createdAt))
|
|
? Number(source.createdAt)
|
|
: Date.now(),
|
|
updatedAt: Number.isFinite(Number(source.updatedAt))
|
|
? Number(source.updatedAt)
|
|
: Date.now(),
|
|
};
|
|
}
|
|
|
|
export function normalizeGraphSummaryState(graph) {
|
|
if (!graph || typeof graph !== "object") {
|
|
return graph;
|
|
}
|
|
const normalized = createDefaultSummaryState(graph.summaryState);
|
|
const entryMap = new Map();
|
|
for (const entry of normalized.entries) {
|
|
if (!entry?.id) continue;
|
|
entryMap.set(entry.id, entry);
|
|
}
|
|
normalized.entries = [...entryMap.values()];
|
|
normalized.activeEntryIds = normalizeStringArray(normalized.activeEntryIds)
|
|
.filter((entryId) => {
|
|
const entry = entryMap.get(entryId);
|
|
return Boolean(entry) && entry.status === ACTIVE_STATUS;
|
|
});
|
|
graph.summaryState = normalized;
|
|
return graph;
|
|
}
|
|
|
|
export function getSummaryEntry(graph, entryId = "") {
|
|
normalizeGraphSummaryState(graph);
|
|
const normalizedEntryId = String(entryId || "").trim();
|
|
if (!normalizedEntryId) return null;
|
|
return (
|
|
(Array.isArray(graph?.summaryState?.entries)
|
|
? graph.summaryState.entries
|
|
: []
|
|
).find((entry) => entry.id === normalizedEntryId) || null
|
|
);
|
|
}
|
|
|
|
export function getActiveSummaryEntries(graph) {
|
|
normalizeGraphSummaryState(graph);
|
|
const entries = Array.isArray(graph?.summaryState?.entries)
|
|
? graph.summaryState.entries
|
|
: [];
|
|
const activeIds = new Set(graph?.summaryState?.activeEntryIds || []);
|
|
return entries
|
|
.filter((entry) => entry.status === ACTIVE_STATUS && activeIds.has(entry.id))
|
|
.sort(compareSummaryEntriesForDisplay);
|
|
}
|
|
|
|
export function compareSummaryEntriesForDisplay(left, right) {
|
|
const leftMessageRange = normalizeNumberRange(left?.messageRange);
|
|
const rightMessageRange = normalizeNumberRange(right?.messageRange);
|
|
if (leftMessageRange[0] !== rightMessageRange[0]) {
|
|
return leftMessageRange[0] - rightMessageRange[0];
|
|
}
|
|
if (leftMessageRange[1] !== rightMessageRange[1]) {
|
|
return leftMessageRange[1] - rightMessageRange[1];
|
|
}
|
|
if (left?.level !== right?.level) {
|
|
return Number(left?.level || 0) - Number(right?.level || 0);
|
|
}
|
|
if (left?.createdAt !== right?.createdAt) {
|
|
return Number(left?.createdAt || 0) - Number(right?.createdAt || 0);
|
|
}
|
|
return String(left?.id || "").localeCompare(String(right?.id || ""));
|
|
}
|
|
|
|
export function createSummaryEntry(data = {}) {
|
|
return normalizeSummaryEntry(
|
|
{
|
|
...data,
|
|
id: data?.id || summaryId(),
|
|
createdAt: data?.createdAt || Date.now(),
|
|
updatedAt: data?.updatedAt || Date.now(),
|
|
},
|
|
{
|
|
fallbackId: summaryId(),
|
|
},
|
|
);
|
|
}
|
|
|
|
export function appendSummaryEntry(graph, entryLike = {}) {
|
|
normalizeGraphSummaryState(graph);
|
|
const entry = createSummaryEntry(entryLike);
|
|
graph.summaryState.entries.push(entry);
|
|
if (!graph.summaryState.activeEntryIds.includes(entry.id)) {
|
|
graph.summaryState.activeEntryIds.push(entry.id);
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
export function markSummaryEntriesFolded(graph, entryIds = []) {
|
|
normalizeGraphSummaryState(graph);
|
|
const targetIds = new Set(normalizeStringArray(entryIds));
|
|
if (targetIds.size === 0) return 0;
|
|
|
|
let changed = 0;
|
|
for (const entry of graph.summaryState.entries) {
|
|
if (!targetIds.has(entry.id)) continue;
|
|
if (entry.status !== FOLDED_STATUS) {
|
|
entry.status = FOLDED_STATUS;
|
|
entry.updatedAt = Date.now();
|
|
changed += 1;
|
|
}
|
|
}
|
|
graph.summaryState.activeEntryIds = graph.summaryState.activeEntryIds
|
|
.filter((entryId) => !targetIds.has(entryId));
|
|
return changed;
|
|
}
|
|
|
|
export function resetSummaryState(graph, state = null) {
|
|
if (!graph || typeof graph !== "object") return graph;
|
|
graph.summaryState = createDefaultSummaryState(state || {});
|
|
return graph.summaryState;
|
|
}
|
|
|
|
export function importLegacySynopsisToSummaryState(graph) {
|
|
normalizeGraphSummaryState(graph);
|
|
const summaryState = graph.summaryState;
|
|
if ((summaryState.entries || []).length > 0) {
|
|
return null;
|
|
}
|
|
const legacySynopsis = (Array.isArray(graph?.nodes) ? graph.nodes : [])
|
|
.filter((node) => node?.type === "synopsis" && node?.archived !== true)
|
|
.sort((left, right) => {
|
|
const leftSeq = Number(left?.seqRange?.[1] ?? left?.seq ?? -1);
|
|
const rightSeq = Number(right?.seqRange?.[1] ?? right?.seq ?? -1);
|
|
return rightSeq - leftSeq;
|
|
})[0];
|
|
const summaryText = String(legacySynopsis?.fields?.summary || "").trim();
|
|
if (!legacySynopsis || !summaryText) {
|
|
return null;
|
|
}
|
|
const entry = appendSummaryEntry(graph, {
|
|
kind: "legacy-import",
|
|
level: 0,
|
|
text: summaryText,
|
|
sourceTask: "synopsis",
|
|
extractionRange: normalizeNumberRange(legacySynopsis?.seqRange, [
|
|
Number.isFinite(Number(legacySynopsis?.seq)) ? Number(legacySynopsis.seq) : -1,
|
|
Number.isFinite(Number(legacySynopsis?.seq)) ? Number(legacySynopsis.seq) : -1,
|
|
]),
|
|
messageRange: normalizeNumberRange(legacySynopsis?.seqRange, [
|
|
Number.isFinite(Number(legacySynopsis?.seq)) ? Number(legacySynopsis.seq) : -1,
|
|
Number.isFinite(Number(legacySynopsis?.seq)) ? Number(legacySynopsis.seq) : -1,
|
|
]),
|
|
sourceNodeIds: [String(legacySynopsis.id || "")],
|
|
storyTimeSpan: legacySynopsis?.storyTimeSpan || createDefaultStoryTimeSpan(),
|
|
});
|
|
summaryState.lastSummarizedExtractionCount = Math.max(
|
|
summaryState.lastSummarizedExtractionCount,
|
|
Number.isFinite(Number(graph?.historyState?.extractionCount))
|
|
? Number(graph.historyState.extractionCount)
|
|
: 0,
|
|
);
|
|
summaryState.lastSummarizedAssistantFloor = Math.max(
|
|
summaryState.lastSummarizedAssistantFloor,
|
|
Number.isFinite(Number(legacySynopsis?.seqRange?.[1]))
|
|
? Number(legacySynopsis.seqRange[1])
|
|
: Number.isFinite(Number(legacySynopsis?.seq))
|
|
? Number(legacySynopsis.seq)
|
|
: -1,
|
|
);
|
|
return entry;
|
|
}
|
|
|
|
export function getSummaryEntriesByStatus(graph, status = ACTIVE_STATUS) {
|
|
normalizeGraphSummaryState(graph);
|
|
const normalizedStatus = String(status || ACTIVE_STATUS).trim().toLowerCase();
|
|
return (Array.isArray(graph?.summaryState?.entries)
|
|
? graph.summaryState.entries
|
|
: []
|
|
)
|
|
.filter((entry) => String(entry?.status || ACTIVE_STATUS) === normalizedStatus)
|
|
.sort(compareSummaryEntriesForDisplay);
|
|
}
|
|
|