Files

432 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Story Summary - Store
// L2 (events/characters/arcs) + L3 (facts) 统一存储
import { getContext, saveMetadataDebounced } from "../../../../../../extensions.js";
import { chat_metadata } from "../../../../../../../script.js";
import { EXT_ID } from "../../../core/constants.js";
import { xbLog } from "../../../core/debug-core.js";
import { clearEventVectors, deleteEventVectorsByIds } from "../vector/chunk-store.js";
import { clearEventTextIndex } from '../vector/text-search.js';
const MODULE_ID = 'summaryStore';
// ═══════════════════════════════════════════════════════════════════════════
// 基础存取
// ═══════════════════════════════════════════════════════════════════════════
export function getSummaryStore() {
const { chatId } = getContext();
if (!chatId) return null;
chat_metadata.extensions ||= {};
chat_metadata.extensions[EXT_ID] ||= {};
chat_metadata.extensions[EXT_ID].storySummary ||= {};
const store = chat_metadata.extensions[EXT_ID].storySummary;
// ★ 自动迁移旧数据
if (store.json && !store.json.facts) {
const hasOldData = store.json.world?.length || store.json.characters?.relationships?.length;
if (hasOldData) {
store.json.facts = migrateToFacts(store.json);
// 删除旧字段
delete store.json.world;
if (store.json.characters) {
delete store.json.characters.relationships;
}
store.updatedAt = Date.now();
saveSummaryStore();
xbLog.info(MODULE_ID, `自动迁移完成: ${store.json.facts.length} 条 facts`);
}
}
return store;
}
export function saveSummaryStore() {
saveMetadataDebounced?.();
}
export function getKeepVisibleCount() {
const store = getSummaryStore();
return store?.keepVisibleCount ?? 3;
}
export function calcHideRange(boundary) {
if (boundary == null || boundary < 0) return null;
const keepCount = getKeepVisibleCount();
const hideEnd = boundary - keepCount;
if (hideEnd < 0) return null;
return { start: 0, end: hideEnd };
}
export function addSummarySnapshot(store, endMesId) {
store.summaryHistory ||= [];
store.summaryHistory.push({ endMesId });
}
// ═══════════════════════════════════════════════════════════════════════════
// Fact 工具函数
// ═══════════════════════════════════════════════════════════════════════════
/**
* 判断是否为关系类 fact
*/
export function isRelationFact(f) {
return /^对.+的/.test(f.p);
}
// ═══════════════════════════════════════════════════════════════════════════
// 从 facts 提取关系(供关系图 UI 使用)
// ═══════════════════════════════════════════════════════════════════════════
export function extractRelationshipsFromFacts(facts) {
return (facts || [])
.filter(f => !f.retracted && isRelationFact(f))
.map(f => {
const match = f.p.match(/^对(.+)的/);
const to = match ? match[1] : '';
if (!to) return null;
return {
from: f.s,
to,
label: f.o,
trend: f.trend || '陌生',
};
})
.filter(Boolean);
}
/**
* 生成 fact 的唯一键s + p
*/
function factKey(f) {
return `${f.s}::${f.p}`;
}
/**
* 生成下一个 fact ID
*/
function getNextFactId(existingFacts) {
let maxId = 0;
for (const f of existingFacts || []) {
const match = f.id?.match(/^f-(\d+)$/);
if (match) {
maxId = Math.max(maxId, parseInt(match[1], 10));
}
}
return maxId + 1;
}
// ═══════════════════════════════════════════════════════════════════════════
// Facts 合并KV 覆盖模型)
// ═══════════════════════════════════════════════════════════════════════════
export function mergeFacts(existingFacts, updates, floor) {
const map = new Map();
// 加载现有 facts
for (const f of existingFacts || []) {
if (!f.retracted) {
map.set(factKey(f), f);
}
}
// 获取下一个 ID
let nextId = getNextFactId(existingFacts);
// 应用更新
for (const u of updates || []) {
if (!u.s || !u.p) continue;
const key = factKey(u);
// 删除操作
if (u.retracted === true) {
map.delete(key);
continue;
}
// 无 o 则跳过
if (!u.o || !String(u.o).trim()) continue;
// 覆盖或新增
const existing = map.get(key);
const newFact = {
id: existing?.id || `f-${nextId++}`,
s: u.s.trim(),
p: u.p.trim(),
o: String(u.o).trim(),
since: floor,
};
// 关系类保留 trend
if (isRelationFact(newFact) && u.trend) {
newFact.trend = u.trend;
}
// 保留原始 _addedAt如果是更新
if (existing?._addedAt != null) {
newFact._addedAt = existing._addedAt;
} else {
newFact._addedAt = floor;
}
map.set(key, newFact);
}
return Array.from(map.values());
}
// ═══════════════════════════════════════════════════════════════════════════
// 旧数据迁移
// ═══════════════════════════════════════════════════════════════════════════
export function migrateToFacts(json) {
if (!json) return [];
// 已有 facts 则跳过迁移
if (json.facts?.length) return json.facts;
const facts = [];
let nextId = 1;
// 迁移 worldworldUpdate 的持久化结果)
for (const w of json.world || []) {
if (!w.category || !w.topic || !w.content) continue;
let s, p;
// 解析 topic 格式status/knowledge/relation 用 "::" 分隔
if (w.topic.includes('::')) {
[s, p] = w.topic.split('::').map(x => x.trim());
} else {
// inventory/rule 类
s = w.topic.trim();
p = w.category;
}
if (!s || !p) continue;
facts.push({
id: `f-${nextId++}`,
s,
p,
o: w.content.trim(),
since: w.floor ?? w._addedAt ?? 0,
_addedAt: w._addedAt ?? w.floor ?? 0,
});
}
// 迁移 relationships
for (const r of json.characters?.relationships || []) {
if (!r.from || !r.to) continue;
facts.push({
id: `f-${nextId++}`,
s: r.from,
p: `${r.to}的看法`,
o: r.label || '未知',
trend: r.trend,
since: r._addedAt ?? 0,
_addedAt: r._addedAt ?? 0,
});
}
return facts;
}
// ═══════════════════════════════════════════════════════════════════════════
// 数据合并L2 + L3
// ═══════════════════════════════════════════════════════════════════════════
export function mergeNewData(oldJson, parsed, endMesId) {
const merged = structuredClone(oldJson || {});
// L2 初始化
merged.keywords ||= [];
merged.events ||= [];
merged.characters ||= {};
merged.characters.main ||= [];
merged.arcs ||= [];
// L3 初始化不再迁移getSummaryStore 已处理)
merged.facts ||= [];
// L2 数据合并
if (parsed.keywords?.length) {
merged.keywords = parsed.keywords.map(k => ({ ...k, _addedAt: endMesId }));
}
(parsed.events || []).forEach(e => {
e._addedAt = endMesId;
merged.events.push(e);
});
// newCharacters
const existingMain = new Set(
(merged.characters.main || []).map(m => typeof m === 'string' ? m : m.name)
);
(parsed.newCharacters || []).forEach(name => {
if (!existingMain.has(name)) {
merged.characters.main.push({ name, _addedAt: endMesId });
}
});
// arcUpdates
const arcMap = new Map((merged.arcs || []).map(a => [a.name, a]));
(parsed.arcUpdates || []).forEach(update => {
const existing = arcMap.get(update.name);
if (existing) {
existing.trajectory = update.trajectory;
existing.progress = update.progress;
if (update.newMoment) {
existing.moments = existing.moments || [];
existing.moments.push({ text: update.newMoment, _addedAt: endMesId });
}
} else {
arcMap.set(update.name, {
name: update.name,
trajectory: update.trajectory,
progress: update.progress,
moments: update.newMoment ? [{ text: update.newMoment, _addedAt: endMesId }] : [],
_addedAt: endMesId,
});
}
});
merged.arcs = Array.from(arcMap.values());
// L3 factUpdates 合并
merged.facts = mergeFacts(merged.facts, parsed.factUpdates || [], endMesId);
return merged;
}
// ═══════════════════════════════════════════════════════════════════════════
// 回滚
// ═══════════════════════════════════════════════════════════════════════════
export async function rollbackSummaryIfNeeded() {
const { chat, chatId } = getContext();
const currentLength = Array.isArray(chat) ? chat.length : 0;
const store = getSummaryStore();
if (!store || store.lastSummarizedMesId == null || store.lastSummarizedMesId < 0) {
return false;
}
const lastSummarized = store.lastSummarizedMesId;
if (currentLength <= lastSummarized) {
const deletedCount = lastSummarized + 1 - currentLength;
if (deletedCount < 2) {
return false;
}
xbLog.warn(MODULE_ID, `删除已总结楼层 ${deletedCount} 条,触发回滚`);
const history = store.summaryHistory || [];
let targetEndMesId = -1;
for (let i = history.length - 1; i >= 0; i--) {
if (history[i].endMesId < currentLength) {
targetEndMesId = history[i].endMesId;
break;
}
}
await executeRollback(chatId, store, targetEndMesId, currentLength);
return true;
}
return false;
}
export async function executeRollback(chatId, store, targetEndMesId, currentLength) {
const oldEvents = store.json?.events || [];
if (targetEndMesId < 0) {
store.lastSummarizedMesId = -1;
store.json = null;
store.summaryHistory = [];
store.hideSummarizedHistory = false;
await clearEventVectors(chatId);
} else {
const deletedEventIds = oldEvents
.filter(e => (e._addedAt ?? 0) > targetEndMesId)
.map(e => e.id);
const json = store.json || {};
// L2 回滚
json.events = (json.events || []).filter(e => (e._addedAt ?? 0) <= targetEndMesId);
json.keywords = (json.keywords || []).filter(k => (k._addedAt ?? 0) <= targetEndMesId);
json.arcs = (json.arcs || []).filter(a => (a._addedAt ?? 0) <= targetEndMesId);
json.arcs.forEach(a => {
a.moments = (a.moments || []).filter(m =>
typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId
);
});
if (json.characters) {
json.characters.main = (json.characters.main || []).filter(m =>
typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId
);
}
// L3 facts 回滚
json.facts = (json.facts || []).filter(f => (f._addedAt ?? 0) <= targetEndMesId);
store.json = json;
store.lastSummarizedMesId = targetEndMesId;
store.summaryHistory = (store.summaryHistory || []).filter(h => h.endMesId <= targetEndMesId);
if (deletedEventIds.length > 0) {
await deleteEventVectorsByIds(chatId, deletedEventIds);
xbLog.info(MODULE_ID, `回滚删除 ${deletedEventIds.length} 个事件向量`);
}
}
store.updatedAt = Date.now();
saveSummaryStore();
xbLog.info(MODULE_ID, `回滚完成,目标楼层: ${targetEndMesId}`);
}
export async function clearSummaryData(chatId) {
const store = getSummaryStore();
if (store) {
delete store.json;
store.lastSummarizedMesId = -1;
store.updatedAt = Date.now();
saveSummaryStore();
}
if (chatId) {
await clearEventVectors(chatId);
}
clearEventTextIndex();
xbLog.info(MODULE_ID, '总结数据已清空');
}
// ═══════════════════════════════════════════════════════════════════════════
// L3 数据读取(供 prompt.js / recall.js 使用)
// ═══════════════════════════════════════════════════════════════════════════
export function getFacts() {
const store = getSummaryStore();
return (store?.json?.facts || []).filter(f => !f.retracted);
}
export function getNewCharacters() {
const store = getSummaryStore();
return (store?.json?.characters?.main || []).map(m =>
typeof m === 'string' ? m : m.name
);
}