story-summary: facts migration + recall enhancements
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Story Summary - Store
|
||||
// L2 (events/characters/arcs) + L3 (world) 统一存储
|
||||
// L2 (events/characters/arcs) + L3 (facts) 统一存储
|
||||
|
||||
import { getContext, saveMetadataDebounced } from "../../../../../../extensions.js";
|
||||
import { chat_metadata } from "../../../../../../../script.js";
|
||||
@@ -20,7 +20,26 @@ export function getSummaryStore() {
|
||||
chat_metadata.extensions ||= {};
|
||||
chat_metadata.extensions[EXT_ID] ||= {};
|
||||
chat_metadata.extensions[EXT_ID].storySummary ||= {};
|
||||
return 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() {
|
||||
@@ -32,7 +51,6 @@ export function getKeepVisibleCount() {
|
||||
return store?.keepVisibleCount ?? 3;
|
||||
}
|
||||
|
||||
// boundary:隐藏边界(由调用方决定语义:LLM总结边界 or 向量边界)
|
||||
export function calcHideRange(boundary) {
|
||||
if (boundary == null || boundary < 0) return null;
|
||||
|
||||
@@ -48,42 +66,155 @@ export function addSummarySnapshot(store, endMesId) {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// L3 世界状态合并
|
||||
// Fact 工具函数
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function mergeWorldState(existingList, updates, floor) {
|
||||
/**
|
||||
* 判断是否为关系类 fact
|
||||
*/
|
||||
export function isRelationFact(f) {
|
||||
return /^对.+的/.test(f.p);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 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();
|
||||
|
||||
(existingList || []).forEach(item => {
|
||||
const key = `${item.category}:${item.topic}`;
|
||||
map.set(key, item);
|
||||
});
|
||||
// 加载现有 facts
|
||||
for (const f of existingFacts || []) {
|
||||
if (!f.retracted) {
|
||||
map.set(factKey(f), f);
|
||||
}
|
||||
}
|
||||
|
||||
(updates || []).forEach(up => {
|
||||
if (!up.category || !up.topic) return;
|
||||
// 获取下一个 ID
|
||||
let nextId = getNextFactId(existingFacts);
|
||||
|
||||
const key = `${up.category}:${up.topic}`;
|
||||
// 应用更新
|
||||
for (const u of updates || []) {
|
||||
if (!u.s || !u.p) continue;
|
||||
|
||||
if (up.cleared === true) {
|
||||
const key = factKey(u);
|
||||
|
||||
// 删除操作
|
||||
if (u.retracted === true) {
|
||||
map.delete(key);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = up.content?.trim();
|
||||
if (!content) return;
|
||||
// 无 o 则跳过
|
||||
if (!u.o || !String(u.o).trim()) continue;
|
||||
|
||||
map.set(key, {
|
||||
category: up.category,
|
||||
topic: up.topic,
|
||||
content: content,
|
||||
floor: floor,
|
||||
_addedAt: floor,
|
||||
});
|
||||
});
|
||||
// 覆盖或新增
|
||||
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;
|
||||
|
||||
// 迁移 world(worldUpdate 的持久化结果)
|
||||
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)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -96,11 +227,10 @@ export function mergeNewData(oldJson, parsed, endMesId) {
|
||||
merged.events ||= [];
|
||||
merged.characters ||= {};
|
||||
merged.characters.main ||= [];
|
||||
merged.characters.relationships ||= [];
|
||||
merged.arcs ||= [];
|
||||
|
||||
// L3 初始化
|
||||
merged.world ||= [];
|
||||
// L3 初始化(不再迁移,getSummaryStore 已处理)
|
||||
merged.facts ||= [];
|
||||
|
||||
// L2 数据合并
|
||||
if (parsed.keywords?.length) {
|
||||
@@ -112,6 +242,7 @@ export function mergeNewData(oldJson, parsed, endMesId) {
|
||||
merged.events.push(e);
|
||||
});
|
||||
|
||||
// newCharacters
|
||||
const existingMain = new Set(
|
||||
(merged.characters.main || []).map(m => typeof m === 'string' ? m : m.name)
|
||||
);
|
||||
@@ -121,22 +252,7 @@ export function mergeNewData(oldJson, parsed, endMesId) {
|
||||
}
|
||||
});
|
||||
|
||||
const relMap = new Map(
|
||||
(merged.characters.relationships || []).map(r => [`${r.from}->${r.to}`, r])
|
||||
);
|
||||
(parsed.newRelationships || []).forEach(r => {
|
||||
const key = `${r.from}->${r.to}`;
|
||||
const existing = relMap.get(key);
|
||||
if (existing) {
|
||||
existing.label = r.label;
|
||||
existing.trend = r.trend;
|
||||
} else {
|
||||
r._addedAt = endMesId;
|
||||
relMap.set(key, r);
|
||||
}
|
||||
});
|
||||
merged.characters.relationships = Array.from(relMap.values());
|
||||
|
||||
// arcUpdates
|
||||
const arcMap = new Map((merged.arcs || []).map(a => [a.name, a]));
|
||||
(parsed.arcUpdates || []).forEach(update => {
|
||||
const existing = arcMap.get(update.name);
|
||||
@@ -159,12 +275,8 @@ export function mergeNewData(oldJson, parsed, endMesId) {
|
||||
});
|
||||
merged.arcs = Array.from(arcMap.values());
|
||||
|
||||
// L3 世界状态合并
|
||||
merged.world = mergeWorldState(
|
||||
merged.world || [],
|
||||
parsed.worldUpdate || [],
|
||||
endMesId
|
||||
);
|
||||
// L3 factUpdates 合并
|
||||
merged.facts = mergeFacts(merged.facts, parsed.factUpdates || [], endMesId);
|
||||
|
||||
return merged;
|
||||
}
|
||||
@@ -242,13 +354,10 @@ export async function executeRollback(chatId, store, targetEndMesId, currentLeng
|
||||
json.characters.main = (json.characters.main || []).filter(m =>
|
||||
typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId
|
||||
);
|
||||
json.characters.relationships = (json.characters.relationships || []).filter(r =>
|
||||
(r._addedAt ?? 0) <= targetEndMesId
|
||||
);
|
||||
}
|
||||
|
||||
// L3 回滚
|
||||
json.world = (json.world || []).filter(w => (w._addedAt ?? 0) <= targetEndMesId);
|
||||
// L3 facts 回滚
|
||||
json.facts = (json.facts || []).filter(f => (f._addedAt ?? 0) <= targetEndMesId);
|
||||
|
||||
store.json = json;
|
||||
store.lastSummarizedMesId = targetEndMesId;
|
||||
@@ -278,17 +387,24 @@ export async function clearSummaryData(chatId) {
|
||||
if (chatId) {
|
||||
await clearEventVectors(chatId);
|
||||
}
|
||||
|
||||
|
||||
clearEventTextIndex();
|
||||
|
||||
|
||||
xbLog.info(MODULE_ID, '总结数据已清空');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// L3 数据读取(供 prompt.js 使用)
|
||||
// L3 数据读取(供 prompt.js / recall.js 使用)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export function getWorldSnapshot() {
|
||||
export function getFacts() {
|
||||
const store = getSummaryStore();
|
||||
return store?.json?.world || [];
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user