story-summary: facts migration + recall enhancements

This commit is contained in:
2026-02-02 21:45:01 +08:00
parent d3f772073f
commit fb8ed8037c
8 changed files with 570 additions and 289 deletions

View File

@@ -3,7 +3,7 @@
import { getContext } from "../../../../../../extensions.js";
import { xbLog } from "../../../core/debug-core.js";
import { getSummaryStore, saveSummaryStore, addSummarySnapshot, mergeNewData } from "../data/store.js";
import { getSummaryStore, saveSummaryStore, addSummarySnapshot, mergeNewData, getFacts } from "../data/store.js";
import { generateSummary, parseSummaryJson } from "./llm.js";
const MODULE_ID = 'summaryGenerator';
@@ -11,46 +11,48 @@ const SUMMARY_SESSION_ID = 'xb9';
const MAX_CAUSED_BY = 2;
// ═══════════════════════════════════════════════════════════════════════════
// worldUpdate 清洗
// factUpdates 清洗
// ═══════════════════════════════════════════════════════════════════════════
function sanitizeWorldUpdate(parsed) {
function sanitizeFacts(parsed) {
if (!parsed) return;
const wu = Array.isArray(parsed.worldUpdate) ? parsed.worldUpdate : [];
const updates = Array.isArray(parsed.factUpdates) ? parsed.factUpdates : [];
const ok = [];
for (const item of wu) {
const category = String(item?.category || '').trim().toLowerCase();
const topic = String(item?.topic || '').trim();
for (const item of updates) {
const s = String(item?.s || '').trim();
const p = String(item?.p || '').trim();
if (!category || !topic) continue;
if (!s || !p) continue;
// status/knowledge/relation 必须包含 "::"
if (['status', 'knowledge', 'relation'].includes(category) && !topic.includes('::')) {
xbLog.warn(MODULE_ID, `丢弃不合格 worldUpdate: ${category}/${topic}`);
// 删除操作
if (item.retracted === true) {
ok.push({ s, p, retracted: true });
continue;
}
if (item.cleared === true) {
ok.push({ category, topic, cleared: true });
continue;
const o = String(item?.o || '').trim();
if (!o) continue;
const fact = { s, p, o };
// 关系类保留 trend
if (/^对.+的/.test(p) && item.trend) {
const validTrends = ['破裂', '厌恶', '反感', '陌生', '投缘', '亲密', '交融'];
if (validTrends.includes(item.trend)) {
fact.trend = item.trend;
}
}
const content = String(item?.content || '').trim();
if (!content) continue;
ok.push({ category, topic, content });
ok.push(fact);
}
parsed.worldUpdate = ok;
parsed.factUpdates = ok;
}
// ═══════════════════════════════════════════════════════════════════════════
// causedBy 清洗(事件因果边)
// - 允许引用:已存在事件 + 本次新输出事件
// - 限制长度0-2
// - 去重、剔除非法ID、剔除自引用
// ═══════════════════════════════════════════════════════════════════════════
function sanitizeEventsCausality(parsed, existingEventIds) {
@@ -61,7 +63,6 @@ function sanitizeEventsCausality(parsed, existingEventIds) {
const idRe = /^evt-\d+$/;
// 本次新输出事件ID集合允许引用
const newIds = new Set(
events
.map(e => String(e?.id || '').trim())
@@ -73,7 +74,6 @@ function sanitizeEventsCausality(parsed, existingEventIds) {
for (const e of events) {
const selfId = String(e?.id || '').trim();
if (!idRe.test(selfId)) {
// id 不合格的话causedBy 直接清空,避免污染
e.causedBy = [];
continue;
}
@@ -117,11 +117,6 @@ export function formatExistingSummaryForAI(store) {
parts.push(`\n【主要角色】${names.join("、")}`);
}
if (data.characters?.relationships?.length) {
parts.push("【人物关系】");
data.characters.relationships.forEach(r => parts.push(`- ${r.from}${r.to}${r.label}${r.trend}`));
}
if (data.arcs?.length) {
parts.push("【角色弧光】");
data.arcs.forEach(a => parts.push(`- ${a.name}${a.trajectory}(进度${Math.round(a.progress * 100)}%`));
@@ -187,7 +182,7 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
onStatus?.(`正在总结 ${slice.range}${slice.count}楼新内容)...`);
const existingSummary = formatExistingSummaryForAI(store);
const existingWorld = store?.json?.world || [];
const existingFacts = getFacts();
const nextEventId = getNextEventId(store);
const existingEventCount = store?.json?.events?.length || 0;
const useStream = config.trigger?.useStream !== false;
@@ -196,7 +191,7 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
try {
raw = await generateSummary({
existingSummary,
existingWorld,
existingFacts,
newHistoryText: slice.text,
historyRange: slice.range,
nextEventId,
@@ -231,7 +226,7 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
return { success: false, error: "parse" };
}
sanitizeWorldUpdate(parsed);
sanitizeFacts(parsed);
const existingEventIds = new Set((store?.json?.events || []).map(e => e?.id).filter(Boolean));
sanitizeEventsCausality(parsed, existingEventIds);
@@ -245,8 +240,8 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
xbLog.info(MODULE_ID, `总结完成,已更新至 ${slice.endMesId + 1}`);
if (parsed.worldUpdate?.length) {
xbLog.info(MODULE_ID, `世界状态更新: ${parsed.worldUpdate.length}`);
if (parsed.factUpdates?.length) {
xbLog.info(MODULE_ID, `Facts 更新: ${parsed.factUpdates.length}`);
}
const newEventIds = (parsed.events || []).map(e => e.id);
@@ -255,7 +250,7 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
merged,
endMesId: slice.endMesId,
newEventIds,
l3Stats: { worldUpdate: parsed.worldUpdate?.length || 0 },
factStats: { updated: parsed.factUpdates?.length || 0 },
});
return { success: true, merged, endMesId: slice.endMesId, newEventIds };