story-summary: facts migration + recall enhancements
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Story Summary - Store
|
// Story Summary - Store
|
||||||
// L2 (events/characters/arcs) + L3 (world) 统一存储
|
// L2 (events/characters/arcs) + L3 (facts) 统一存储
|
||||||
|
|
||||||
import { getContext, saveMetadataDebounced } from "../../../../../../extensions.js";
|
import { getContext, saveMetadataDebounced } from "../../../../../../extensions.js";
|
||||||
import { chat_metadata } from "../../../../../../../script.js";
|
import { chat_metadata } from "../../../../../../../script.js";
|
||||||
@@ -20,7 +20,26 @@ export function getSummaryStore() {
|
|||||||
chat_metadata.extensions ||= {};
|
chat_metadata.extensions ||= {};
|
||||||
chat_metadata.extensions[EXT_ID] ||= {};
|
chat_metadata.extensions[EXT_ID] ||= {};
|
||||||
chat_metadata.extensions[EXT_ID].storySummary ||= {};
|
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() {
|
export function saveSummaryStore() {
|
||||||
@@ -32,7 +51,6 @@ export function getKeepVisibleCount() {
|
|||||||
return store?.keepVisibleCount ?? 3;
|
return store?.keepVisibleCount ?? 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// boundary:隐藏边界(由调用方决定语义:LLM总结边界 or 向量边界)
|
|
||||||
export function calcHideRange(boundary) {
|
export function calcHideRange(boundary) {
|
||||||
if (boundary == null || boundary < 0) return null;
|
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();
|
const map = new Map();
|
||||||
|
|
||||||
(existingList || []).forEach(item => {
|
// 加载现有 facts
|
||||||
const key = `${item.category}:${item.topic}`;
|
for (const f of existingFacts || []) {
|
||||||
map.set(key, item);
|
if (!f.retracted) {
|
||||||
});
|
map.set(factKey(f), f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(updates || []).forEach(up => {
|
// 获取下一个 ID
|
||||||
if (!up.category || !up.topic) return;
|
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);
|
map.delete(key);
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = up.content?.trim();
|
// 无 o 则跳过
|
||||||
if (!content) return;
|
if (!u.o || !String(u.o).trim()) continue;
|
||||||
|
|
||||||
map.set(key, {
|
// 覆盖或新增
|
||||||
category: up.category,
|
const existing = map.get(key);
|
||||||
topic: up.topic,
|
const newFact = {
|
||||||
content: content,
|
id: existing?.id || `f-${nextId++}`,
|
||||||
floor: floor,
|
s: u.s.trim(),
|
||||||
_addedAt: floor,
|
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());
|
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)
|
// 数据合并(L2 + L3)
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -96,11 +227,10 @@ export function mergeNewData(oldJson, parsed, endMesId) {
|
|||||||
merged.events ||= [];
|
merged.events ||= [];
|
||||||
merged.characters ||= {};
|
merged.characters ||= {};
|
||||||
merged.characters.main ||= [];
|
merged.characters.main ||= [];
|
||||||
merged.characters.relationships ||= [];
|
|
||||||
merged.arcs ||= [];
|
merged.arcs ||= [];
|
||||||
|
|
||||||
// L3 初始化
|
// L3 初始化(不再迁移,getSummaryStore 已处理)
|
||||||
merged.world ||= [];
|
merged.facts ||= [];
|
||||||
|
|
||||||
// L2 数据合并
|
// L2 数据合并
|
||||||
if (parsed.keywords?.length) {
|
if (parsed.keywords?.length) {
|
||||||
@@ -112,6 +242,7 @@ export function mergeNewData(oldJson, parsed, endMesId) {
|
|||||||
merged.events.push(e);
|
merged.events.push(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// newCharacters
|
||||||
const existingMain = new Set(
|
const existingMain = new Set(
|
||||||
(merged.characters.main || []).map(m => typeof m === 'string' ? m : m.name)
|
(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(
|
// arcUpdates
|
||||||
(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());
|
|
||||||
|
|
||||||
const arcMap = new Map((merged.arcs || []).map(a => [a.name, a]));
|
const arcMap = new Map((merged.arcs || []).map(a => [a.name, a]));
|
||||||
(parsed.arcUpdates || []).forEach(update => {
|
(parsed.arcUpdates || []).forEach(update => {
|
||||||
const existing = arcMap.get(update.name);
|
const existing = arcMap.get(update.name);
|
||||||
@@ -159,12 +275,8 @@ export function mergeNewData(oldJson, parsed, endMesId) {
|
|||||||
});
|
});
|
||||||
merged.arcs = Array.from(arcMap.values());
|
merged.arcs = Array.from(arcMap.values());
|
||||||
|
|
||||||
// L3 世界状态合并
|
// L3 factUpdates 合并
|
||||||
merged.world = mergeWorldState(
|
merged.facts = mergeFacts(merged.facts, parsed.factUpdates || [], endMesId);
|
||||||
merged.world || [],
|
|
||||||
parsed.worldUpdate || [],
|
|
||||||
endMesId
|
|
||||||
);
|
|
||||||
|
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
@@ -242,13 +354,10 @@ export async function executeRollback(chatId, store, targetEndMesId, currentLeng
|
|||||||
json.characters.main = (json.characters.main || []).filter(m =>
|
json.characters.main = (json.characters.main || []).filter(m =>
|
||||||
typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId
|
typeof m === 'string' || (m._addedAt ?? 0) <= targetEndMesId
|
||||||
);
|
);
|
||||||
json.characters.relationships = (json.characters.relationships || []).filter(r =>
|
|
||||||
(r._addedAt ?? 0) <= targetEndMesId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// L3 回滚
|
// L3 facts 回滚
|
||||||
json.world = (json.world || []).filter(w => (w._addedAt ?? 0) <= targetEndMesId);
|
json.facts = (json.facts || []).filter(f => (f._addedAt ?? 0) <= targetEndMesId);
|
||||||
|
|
||||||
store.json = json;
|
store.json = json;
|
||||||
store.lastSummarizedMesId = targetEndMesId;
|
store.lastSummarizedMesId = targetEndMesId;
|
||||||
@@ -285,10 +394,17 @@ export async function clearSummaryData(chatId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// L3 数据读取(供 prompt.js 使用)
|
// L3 数据读取(供 prompt.js / recall.js 使用)
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
export function getWorldSnapshot() {
|
export function getFacts() {
|
||||||
const store = getSummaryStore();
|
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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import { getContext } from "../../../../../../extensions.js";
|
import { getContext } from "../../../../../../extensions.js";
|
||||||
import { xbLog } from "../../../core/debug-core.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";
|
import { generateSummary, parseSummaryJson } from "./llm.js";
|
||||||
|
|
||||||
const MODULE_ID = 'summaryGenerator';
|
const MODULE_ID = 'summaryGenerator';
|
||||||
@@ -11,46 +11,48 @@ const SUMMARY_SESSION_ID = 'xb9';
|
|||||||
const MAX_CAUSED_BY = 2;
|
const MAX_CAUSED_BY = 2;
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// worldUpdate 清洗
|
// factUpdates 清洗
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
function sanitizeWorldUpdate(parsed) {
|
function sanitizeFacts(parsed) {
|
||||||
if (!parsed) return;
|
if (!parsed) return;
|
||||||
|
|
||||||
const wu = Array.isArray(parsed.worldUpdate) ? parsed.worldUpdate : [];
|
const updates = Array.isArray(parsed.factUpdates) ? parsed.factUpdates : [];
|
||||||
const ok = [];
|
const ok = [];
|
||||||
|
|
||||||
for (const item of wu) {
|
for (const item of updates) {
|
||||||
const category = String(item?.category || '').trim().toLowerCase();
|
const s = String(item?.s || '').trim();
|
||||||
const topic = String(item?.topic || '').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('::')) {
|
if (item.retracted === true) {
|
||||||
xbLog.warn(MODULE_ID, `丢弃不合格 worldUpdate: ${category}/${topic}`);
|
ok.push({ s, p, retracted: true });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.cleared === true) {
|
const o = String(item?.o || '').trim();
|
||||||
ok.push({ category, topic, cleared: true });
|
if (!o) continue;
|
||||||
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();
|
ok.push(fact);
|
||||||
if (!content) continue;
|
|
||||||
|
|
||||||
ok.push({ category, topic, content });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed.worldUpdate = ok;
|
parsed.factUpdates = ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// causedBy 清洗(事件因果边)
|
// causedBy 清洗(事件因果边)
|
||||||
// - 允许引用:已存在事件 + 本次新输出事件
|
|
||||||
// - 限制长度:0-2
|
|
||||||
// - 去重、剔除非法ID、剔除自引用
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
function sanitizeEventsCausality(parsed, existingEventIds) {
|
function sanitizeEventsCausality(parsed, existingEventIds) {
|
||||||
@@ -61,7 +63,6 @@ function sanitizeEventsCausality(parsed, existingEventIds) {
|
|||||||
|
|
||||||
const idRe = /^evt-\d+$/;
|
const idRe = /^evt-\d+$/;
|
||||||
|
|
||||||
// 本次新输出事件ID集合(允许引用)
|
|
||||||
const newIds = new Set(
|
const newIds = new Set(
|
||||||
events
|
events
|
||||||
.map(e => String(e?.id || '').trim())
|
.map(e => String(e?.id || '').trim())
|
||||||
@@ -73,7 +74,6 @@ function sanitizeEventsCausality(parsed, existingEventIds) {
|
|||||||
for (const e of events) {
|
for (const e of events) {
|
||||||
const selfId = String(e?.id || '').trim();
|
const selfId = String(e?.id || '').trim();
|
||||||
if (!idRe.test(selfId)) {
|
if (!idRe.test(selfId)) {
|
||||||
// id 不合格的话,causedBy 直接清空,避免污染
|
|
||||||
e.causedBy = [];
|
e.causedBy = [];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -117,11 +117,6 @@ export function formatExistingSummaryForAI(store) {
|
|||||||
parts.push(`\n【主要角色】${names.join("、")}`);
|
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) {
|
if (data.arcs?.length) {
|
||||||
parts.push("【角色弧光】");
|
parts.push("【角色弧光】");
|
||||||
data.arcs.forEach(a => parts.push(`- ${a.name}:${a.trajectory}(进度${Math.round(a.progress * 100)}%)`));
|
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}楼新内容)...`);
|
onStatus?.(`正在总结 ${slice.range}(${slice.count}楼新内容)...`);
|
||||||
|
|
||||||
const existingSummary = formatExistingSummaryForAI(store);
|
const existingSummary = formatExistingSummaryForAI(store);
|
||||||
const existingWorld = store?.json?.world || [];
|
const existingFacts = getFacts();
|
||||||
const nextEventId = getNextEventId(store);
|
const nextEventId = getNextEventId(store);
|
||||||
const existingEventCount = store?.json?.events?.length || 0;
|
const existingEventCount = store?.json?.events?.length || 0;
|
||||||
const useStream = config.trigger?.useStream !== false;
|
const useStream = config.trigger?.useStream !== false;
|
||||||
@@ -196,7 +191,7 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
|
|||||||
try {
|
try {
|
||||||
raw = await generateSummary({
|
raw = await generateSummary({
|
||||||
existingSummary,
|
existingSummary,
|
||||||
existingWorld,
|
existingFacts,
|
||||||
newHistoryText: slice.text,
|
newHistoryText: slice.text,
|
||||||
historyRange: slice.range,
|
historyRange: slice.range,
|
||||||
nextEventId,
|
nextEventId,
|
||||||
@@ -231,7 +226,7 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
|
|||||||
return { success: false, error: "parse" };
|
return { success: false, error: "parse" };
|
||||||
}
|
}
|
||||||
|
|
||||||
sanitizeWorldUpdate(parsed);
|
sanitizeFacts(parsed);
|
||||||
const existingEventIds = new Set((store?.json?.events || []).map(e => e?.id).filter(Boolean));
|
const existingEventIds = new Set((store?.json?.events || []).map(e => e?.id).filter(Boolean));
|
||||||
sanitizeEventsCausality(parsed, existingEventIds);
|
sanitizeEventsCausality(parsed, existingEventIds);
|
||||||
|
|
||||||
@@ -245,8 +240,8 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
|
|||||||
|
|
||||||
xbLog.info(MODULE_ID, `总结完成,已更新至 ${slice.endMesId + 1} 楼`);
|
xbLog.info(MODULE_ID, `总结完成,已更新至 ${slice.endMesId + 1} 楼`);
|
||||||
|
|
||||||
if (parsed.worldUpdate?.length) {
|
if (parsed.factUpdates?.length) {
|
||||||
xbLog.info(MODULE_ID, `世界状态更新: ${parsed.worldUpdate.length} 条`);
|
xbLog.info(MODULE_ID, `Facts 更新: ${parsed.factUpdates.length} 条`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEventIds = (parsed.events || []).map(e => e.id);
|
const newEventIds = (parsed.events || []).map(e => e.id);
|
||||||
@@ -255,7 +250,7 @@ export async function runSummaryGeneration(mesId, config, callbacks = {}) {
|
|||||||
merged,
|
merged,
|
||||||
endMesId: slice.endMesId,
|
endMesId: slice.endMesId,
|
||||||
newEventIds,
|
newEventIds,
|
||||||
l3Stats: { worldUpdate: parsed.worldUpdate?.length || 0 },
|
factStats: { updated: parsed.factUpdates?.length || 0 },
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true, merged, endMesId: slice.endMesId, newEventIds };
|
return { success: true, merged, endMesId: slice.endMesId, newEventIds };
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// LLM Service
|
// LLM Service
|
||||||
|
|
||||||
const PROVIDER_MAP = {
|
const PROVIDER_MAP = {
|
||||||
// ...
|
|
||||||
openai: "openai",
|
openai: "openai",
|
||||||
google: "gemini",
|
google: "gemini",
|
||||||
gemini: "gemini",
|
gemini: "gemini",
|
||||||
@@ -39,43 +38,37 @@ Incremental_Summary_Requirements:
|
|||||||
- Causal_Chain: 为每个新事件标注直接前因事件ID(causedBy)。仅在因果关系明确(直接导致/明确动机/承接后果)时填写;不明确时填[]完全正常。0-2个,只填 evt-数字,指向已存在或本次新输出事件。
|
- Causal_Chain: 为每个新事件标注直接前因事件ID(causedBy)。仅在因果关系明确(直接导致/明确动机/承接后果)时填写;不明确时填[]完全正常。0-2个,只填 evt-数字,指向已存在或本次新输出事件。
|
||||||
- Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融)
|
- Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融)
|
||||||
- Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0)
|
- Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0)
|
||||||
- World_State_Tracking: 维护当前世界的硬性约束。解决"什么不能违反"。采用 KV 覆盖模型,追踪生死、物品归属、秘密知情、关系状态、环境规则等不可违背的事实。(覆盖式更新)
|
- Fact_Tracking: 维护 SPO 三元组知识图谱。追踪生死、物品归属、位置、关系等硬性事实。采用 KV 覆盖模型(s+p 为键)。
|
||||||
categories:
|
|
||||||
- status: 角色生死、位置锁定、重大状态
|
|
||||||
- inventory: 重要物品归属
|
|
||||||
- knowledge: 秘密的知情状态
|
|
||||||
- relation: 硬性关系(在一起/决裂)
|
|
||||||
- rule: 环境规则/契约限制
|
|
||||||
</task_settings>
|
</task_settings>
|
||||||
---
|
---
|
||||||
Story Analyst:
|
Story Analyst:
|
||||||
[Responsibility Definition]
|
[Responsibility Definition]
|
||||||
\`\`\`yaml
|
\`\`\`yaml
|
||||||
analysis_task:
|
analysis_task:
|
||||||
title: Incremental Story Summarization with World State
|
title: Incremental Story Summarization with Knowledge Graph
|
||||||
Story Analyst:
|
Story Analyst:
|
||||||
role: Antigravity
|
role: Antigravity
|
||||||
task: >-
|
task: >-
|
||||||
To analyze provided dialogue content against existing summary state,
|
To analyze provided dialogue content against existing summary state,
|
||||||
extract only NEW plot elements, character developments, relationship
|
extract only NEW plot elements, character developments, relationship
|
||||||
changes, arc progressions, AND world state changes, outputting
|
changes, arc progressions, AND fact updates, outputting
|
||||||
structured JSON for incremental summary database updates.
|
structured JSON for incremental summary database updates.
|
||||||
assistant:
|
assistant:
|
||||||
role: Summary Specialist
|
role: Summary Specialist
|
||||||
description: Incremental Story Summary & World State Analyst
|
description: Incremental Story Summary & Knowledge Graph Analyst
|
||||||
behavior: >-
|
behavior: >-
|
||||||
To compare new dialogue against existing summary, identify genuinely
|
To compare new dialogue against existing summary, identify genuinely
|
||||||
new events and character interactions, classify events by narrative
|
new events and character interactions, classify events by narrative
|
||||||
type and weight, track character arc progression with percentage,
|
type and weight, track character arc progression with percentage,
|
||||||
maintain world state as key-value updates with clear flags,
|
maintain facts as SPO triples with clear semantics,
|
||||||
and output structured JSON containing only incremental updates.
|
and output structured JSON containing only incremental updates.
|
||||||
Must strictly avoid repeating any existing summary content.
|
Must strictly avoid repeating any existing summary content.
|
||||||
user:
|
user:
|
||||||
role: Content Provider
|
role: Content Provider
|
||||||
description: Supplies existing summary state and new dialogue
|
description: Supplies existing summary state and new dialogue
|
||||||
behavior: >-
|
behavior: >-
|
||||||
To provide existing summary state (events, characters, relationships,
|
To provide existing summary state (events, characters, arcs, facts)
|
||||||
arcs, world state) and new dialogue content for incremental analysis.
|
and new dialogue content for incremental analysis.
|
||||||
interaction_mode:
|
interaction_mode:
|
||||||
type: incremental_analysis
|
type: incremental_analysis
|
||||||
output_format: structured_json
|
output_format: structured_json
|
||||||
@@ -84,7 +77,7 @@ execution_context:
|
|||||||
summary_active: true
|
summary_active: true
|
||||||
incremental_only: true
|
incremental_only: true
|
||||||
memory_album_style: true
|
memory_album_style: true
|
||||||
world_state_tracking: true
|
fact_tracking: true
|
||||||
\`\`\`
|
\`\`\`
|
||||||
---
|
---
|
||||||
Summary Specialist:
|
Summary Specialist:
|
||||||
@@ -103,15 +96,17 @@ Acknowledged. Now reviewing the incremental summarization specifications:
|
|||||||
破裂 ← 厌恶 ← 反感 ← 陌生 → 投缘 → 亲密 → 交融
|
破裂 ← 厌恶 ← 反感 ← 陌生 → 投缘 → 亲密 → 交融
|
||||||
|
|
||||||
[Arc Progress Tracking]
|
[Arc Progress Tracking]
|
||||||
├─ trajectory: 完整弧光链描述(30字内)
|
├─ trajectory: 当前阶段描述(15字内)
|
||||||
├─ progress: 0.0 to 1.0
|
├─ progress: 0.0 to 1.0
|
||||||
└─ newMoment: 仅记录本次新增的关键时刻
|
└─ newMoment: 仅记录本次新增的关键时刻
|
||||||
|
|
||||||
[World State Maintenance]
|
[Fact Tracking - SPO Triples]
|
||||||
├─ 维护方式: Key-Value 覆盖(category + topic 为键)
|
├─ s: 主体(角色名/物品名)
|
||||||
├─ 只输出有变化的条目
|
├─ p: 谓词(属性名/对X的看法)
|
||||||
├─ 清除时使用 cleared: true,不要填 content
|
├─ o: 值(当前状态)
|
||||||
└─ 不记录情绪、衣着、临时动作
|
├─ trend: 仅关系类填写
|
||||||
|
├─ retracted: 删除标记
|
||||||
|
└─ s+p 为键,相同键会覆盖旧值
|
||||||
|
|
||||||
Ready to process incremental summary requests with strict deduplication.`,
|
Ready to process incremental summary requests with strict deduplication.`,
|
||||||
|
|
||||||
@@ -119,19 +114,19 @@ Ready to process incremental summary requests with strict deduplication.`,
|
|||||||
Summary Specialist:
|
Summary Specialist:
|
||||||
Specifications internalized. Please provide the existing summary state so I can:
|
Specifications internalized. Please provide the existing summary state so I can:
|
||||||
1. Index all recorded events to avoid duplication
|
1. Index all recorded events to avoid duplication
|
||||||
2. Map current character relationships as baseline
|
2. Map current character list as baseline
|
||||||
3. Note existing arc progress levels
|
3. Note existing arc progress levels
|
||||||
4. Identify established keywords
|
4. Identify established keywords
|
||||||
5. Review current world state (category + topic baseline)`,
|
5. Review current facts (SPO triples baseline)`,
|
||||||
|
|
||||||
assistantAskContent: `
|
assistantAskContent: `
|
||||||
Summary Specialist:
|
Summary Specialist:
|
||||||
Existing summary fully analyzed and indexed. I understand:
|
Existing summary fully analyzed and indexed. I understand:
|
||||||
├─ Recorded events: Indexed for deduplication
|
├─ Recorded events: Indexed for deduplication
|
||||||
├─ Character relationships: Baseline mapped
|
├─ Character list: Baseline mapped
|
||||||
├─ Arc progress: Levels noted
|
├─ Arc progress: Levels noted
|
||||||
├─ Keywords: Current state acknowledged
|
├─ Keywords: Current state acknowledged
|
||||||
└─ World state: Baseline loaded
|
└─ Facts: SPO baseline loaded
|
||||||
|
|
||||||
I will extract only genuinely NEW elements from the upcoming dialogue.
|
I will extract only genuinely NEW elements from the upcoming dialogue.
|
||||||
Please provide the new dialogue content requiring incremental analysis.`,
|
Please provide the new dialogue content requiring incremental analysis.`,
|
||||||
@@ -152,7 +147,7 @@ Before generating, observe the USER and analyze carefully:
|
|||||||
- What NEW characters appeared for the first time?
|
- What NEW characters appeared for the first time?
|
||||||
- What relationship CHANGES happened?
|
- What relationship CHANGES happened?
|
||||||
- What arc PROGRESS was made?
|
- What arc PROGRESS was made?
|
||||||
- What world state changes occurred? (status/inventory/knowledge/relation/rule)
|
- What facts changed? (status/position/ownership/relationships)
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
\`\`\`json
|
\`\`\`json
|
||||||
@@ -160,7 +155,7 @@ Before generating, observe the USER and analyze carefully:
|
|||||||
"mindful_prelude": {
|
"mindful_prelude": {
|
||||||
"user_insight": "用户的幻想是什么时空、场景,是否反应出存在严重心理问题需要建议?",
|
"user_insight": "用户的幻想是什么时空、场景,是否反应出存在严重心理问题需要建议?",
|
||||||
"dedup_analysis": "已有X个事件,本次识别Y个新事件",
|
"dedup_analysis": "已有X个事件,本次识别Y个新事件",
|
||||||
"world_changes": "识别到的世界状态变化概述,仅精选不记录则可能导致吃书的硬状态变化"
|
"fact_changes": "识别到的事实变化概述"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
{"text": "综合已有+新内容的全局关键词(5-10个)", "weight": "核心|重要|一般"}
|
{"text": "综合已有+新内容的全局关键词(5-10个)", "weight": "核心|重要|一般"}
|
||||||
@@ -178,45 +173,35 @@ Before generating, observe the USER and analyze carefully:
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"newCharacters": ["仅本次首次出现的角色名"],
|
"newCharacters": ["仅本次首次出现的角色名"],
|
||||||
"newRelationships": [
|
|
||||||
{"from": "A", "to": "B", "label": "基于全局的关系描述", "trend": "破裂|厌恶|反感|陌生|投缘|亲密|交融"}
|
|
||||||
],
|
|
||||||
"arcUpdates": [
|
"arcUpdates": [
|
||||||
{"name": "角色名", "trajectory": "完整弧光链(30字内)", "progress": 0.0-1.0, "newMoment": "本次新增的关键时刻"}
|
{"name": "角色名", "trajectory": "当前阶段描述(15字内)", "progress": 0.0-1.0, "newMoment": "本次新增的关键时刻"}
|
||||||
],
|
],
|
||||||
"worldUpdate": [
|
"factUpdates": [
|
||||||
{
|
{
|
||||||
"category": "status|inventory|knowledge|relation|rule",
|
"s": "主体(角色名/物品名)",
|
||||||
"topic": "主体名称(人/物/关系/规则)",
|
"p": "谓词(属性名/对X的看法)",
|
||||||
"content": "当前状态描述",
|
"o": "当前值",
|
||||||
"cleared": true
|
"trend": "破裂|厌恶|反感|陌生|投缘|亲密|交融",
|
||||||
|
"retracted": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
## Field Guidelines
|
## factUpdates 规则
|
||||||
|
- s+p 为键,相同键会覆盖旧值
|
||||||
### worldUpdate(世界状态·硬约束KV表)
|
- 状态类:s=角色名, p=属性(生死/位置/状态等), o=值
|
||||||
- category 固定 5 选 1:status / inventory / knowledge / relation / rule
|
- 关系类:s=角色A, p="对B的看法", o=描述, trend=趋势
|
||||||
- topic 命名规范:
|
- 删除:设置 retracted: true(不需要填 o)
|
||||||
- status:「角色名::状态类型」如 张三::生死、李四::位置、王五::伤势
|
- 只输出有变化的条目
|
||||||
- knowledge:「角色名::知情事项」如 张三::知道某秘密、李四::知道真相
|
- 硬约束才记录,避免叙事化,确保少、硬、稳定
|
||||||
- relation:「角色A::与角色B关系」如 张三::与李四关系
|
|
||||||
- inventory:物品名称,如 钥匙、信物、武器
|
|
||||||
- rule:规则/契约名称,如 门禁时间、魔法契约、禁令
|
|
||||||
- content:当前状态的简短描述
|
|
||||||
- cleared: true 表示该条目已失效需删除(不填 content)
|
|
||||||
- status/knowledge/relation 的 topic 必须包含「::」分隔符
|
|
||||||
- 硬约束才记录,避免叙事化,确保少、硬、稳定、可覆盖
|
|
||||||
- 动态清理:若发现已有条目中存在不适合作为硬约束的内容(如衣着打扮、临时情绪、琐碎动作),本次输出中用 cleared: true 删除
|
|
||||||
|
|
||||||
## CRITICAL NOTES
|
## CRITICAL NOTES
|
||||||
- events.id 从 evt-{nextEventId} 开始编号
|
- events.id 从 evt-{nextEventId} 开始编号
|
||||||
- 仅输出【增量】内容,已有事件绝不重复
|
- 仅输出【增量】内容,已有事件绝不重复
|
||||||
- keywords 是全局关键词,综合已有+新增
|
- keywords 是全局关键词,综合已有+新增
|
||||||
- causedBy 仅在因果明确时填写,允许为[],0-2个,详见上方 Causal_Chain 规则
|
- causedBy 仅在因果明确时填写,允许为[],0-2个
|
||||||
- worldUpdate 可为空数组
|
- factUpdates 可为空数组
|
||||||
- 合法JSON,字符串值内部避免英文双引号
|
- 合法JSON,字符串值内部避免英文双引号
|
||||||
- 用朴实、白描、有烟火气的笔触记录,避免比喻和意象
|
- 用朴实、白描、有烟火气的笔触记录,避免比喻和意象
|
||||||
</meta_protocol>`,
|
</meta_protocol>`,
|
||||||
@@ -227,15 +212,14 @@ Before generating, observe the USER and analyze carefully:
|
|||||||
├─ New dialogue received: ✓ Content parsed
|
├─ New dialogue received: ✓ Content parsed
|
||||||
├─ Deduplication engine: ✓ Active
|
├─ Deduplication engine: ✓ Active
|
||||||
├─ Event classification: ✓ Ready
|
├─ Event classification: ✓ Ready
|
||||||
├─ World state tracking: ✓ Enabled
|
├─ Fact tracking: ✓ Enabled
|
||||||
└─ Output format: ✓ JSON specification loaded
|
└─ Output format: ✓ JSON specification loaded
|
||||||
|
|
||||||
[Material Verification]
|
[Material Verification]
|
||||||
├─ Existing events: Indexed ({existingEventCount} recorded)
|
├─ Existing events: Indexed ({existingEventCount} recorded)
|
||||||
├─ Character baseline: Mapped
|
├─ Character baseline: Mapped
|
||||||
├─ Relationship baseline: Mapped
|
|
||||||
├─ Arc progress baseline: Noted
|
├─ Arc progress baseline: Noted
|
||||||
├─ World state: Baseline loaded
|
├─ Facts baseline: Loaded
|
||||||
└─ Output specification: ✓ Defined in <meta_protocol>
|
└─ Output specification: ✓ Defined in <meta_protocol>
|
||||||
All checks passed. Beginning incremental extraction...
|
All checks passed. Beginning incremental extraction...
|
||||||
{
|
{
|
||||||
@@ -280,39 +264,23 @@ function waitForStreamingComplete(sessionId, streamingMod, timeout = 120000) {
|
|||||||
// 提示词构建
|
// 提示词构建
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
function formatWorldForLLM(worldList) {
|
function formatFactsForLLM(facts) {
|
||||||
if (!worldList?.length) {
|
if (!facts?.length) {
|
||||||
return '(空白,尚无世界状态记录)';
|
return '(空白,尚无事实记录)';
|
||||||
}
|
}
|
||||||
|
|
||||||
const grouped = { status: [], inventory: [], knowledge: [], relation: [], rule: [] };
|
const lines = facts.map(f => {
|
||||||
const labels = {
|
if (f.trend) {
|
||||||
status: '状态(生死/位置锁定)',
|
return `- ${f.s} | ${f.p} | ${f.o} [${f.trend}]`;
|
||||||
inventory: '物品归属',
|
|
||||||
knowledge: '秘密/认知',
|
|
||||||
relation: '关系状态',
|
|
||||||
rule: '规则/约束'
|
|
||||||
};
|
|
||||||
|
|
||||||
worldList.forEach(w => {
|
|
||||||
if (grouped[w.category]) {
|
|
||||||
grouped[w.category].push(w);
|
|
||||||
}
|
}
|
||||||
|
return `- ${f.s} | ${f.p} | ${f.o}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const parts = [];
|
return lines.join('\n') || '(空白,尚无事实记录)';
|
||||||
for (const [cat, items] of Object.entries(grouped)) {
|
|
||||||
if (items.length > 0) {
|
|
||||||
const lines = items.map(w => ` - ${w.topic}: ${w.content}`).join('\n');
|
|
||||||
parts.push(`【${labels[cat]}】\n${lines}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join('\n\n') || '(空白,尚无世界状态记录)';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSummaryMessages(existingSummary, existingWorld, newHistoryText, historyRange, nextEventId, existingEventCount) {
|
function buildSummaryMessages(existingSummary, existingFacts, newHistoryText, historyRange, nextEventId, existingEventCount) {
|
||||||
const worldStateText = formatWorldForLLM(existingWorld);
|
const factsText = formatFactsForLLM(existingFacts);
|
||||||
|
|
||||||
const jsonFormat = LLM_PROMPT_CONFIG.userJsonFormat
|
const jsonFormat = LLM_PROMPT_CONFIG.userJsonFormat
|
||||||
.replace(/\{nextEventId\}/g, String(nextEventId));
|
.replace(/\{nextEventId\}/g, String(nextEventId));
|
||||||
@@ -324,7 +292,7 @@ function buildSummaryMessages(existingSummary, existingWorld, newHistoryText, hi
|
|||||||
{ role: 'system', content: LLM_PROMPT_CONFIG.topSystem },
|
{ role: 'system', content: LLM_PROMPT_CONFIG.topSystem },
|
||||||
{ role: 'assistant', content: LLM_PROMPT_CONFIG.assistantDoc },
|
{ role: 'assistant', content: LLM_PROMPT_CONFIG.assistantDoc },
|
||||||
{ role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskSummary },
|
{ role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskSummary },
|
||||||
{ role: 'user', content: `<已有总结状态>\n${existingSummary}\n</已有总结状态>\n\n<当前世界状态>\n${worldStateText}\n</当前世界状态>` },
|
{ role: 'user', content: `<已有总结状态>\n${existingSummary}\n</已有总结状态>\n\n<当前事实图谱>\n${factsText}\n</当前事实图谱>` },
|
||||||
{ role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskContent },
|
{ role: 'assistant', content: LLM_PROMPT_CONFIG.assistantAskContent },
|
||||||
{ role: 'user', content: `<新对话内容>(${historyRange})\n${newHistoryText}\n</新对话内容>` }
|
{ role: 'user', content: `<新对话内容>(${historyRange})\n${newHistoryText}\n</新对话内容>` }
|
||||||
];
|
];
|
||||||
@@ -378,7 +346,7 @@ export function parseSummaryJson(raw) {
|
|||||||
export async function generateSummary(options) {
|
export async function generateSummary(options) {
|
||||||
const {
|
const {
|
||||||
existingSummary,
|
existingSummary,
|
||||||
existingWorld,
|
existingFacts,
|
||||||
newHistoryText,
|
newHistoryText,
|
||||||
historyRange,
|
historyRange,
|
||||||
nextEventId,
|
nextEventId,
|
||||||
@@ -401,7 +369,7 @@ export async function generateSummary(options) {
|
|||||||
|
|
||||||
const promptData = buildSummaryMessages(
|
const promptData = buildSummaryMessages(
|
||||||
existingSummary,
|
existingSummary,
|
||||||
existingWorld,
|
existingFacts,
|
||||||
newHistoryText,
|
newHistoryText,
|
||||||
historyRange,
|
historyRange,
|
||||||
nextEventId,
|
nextEventId,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { getContext } from "../../../../../../extensions.js";
|
import { getContext } from "../../../../../../extensions.js";
|
||||||
import { xbLog } from "../../../core/debug-core.js";
|
import { xbLog } from "../../../core/debug-core.js";
|
||||||
import { getSummaryStore } from "../data/store.js";
|
import { getSummaryStore, getFacts, isRelationFact } from "../data/store.js";
|
||||||
import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
|
import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/config.js";
|
||||||
import { recallMemory, buildQueryText } from "../vector/recall.js";
|
import { recallMemory, buildQueryText } from "../vector/recall.js";
|
||||||
import { getChunksByFloors, getAllChunkVectors, getAllEventVectors, getMeta } from "../vector/chunk-store.js";
|
import { getChunksByFloors, getAllChunkVectors, getAllEventVectors, getMeta } from "../vector/chunk-store.js";
|
||||||
@@ -111,10 +111,18 @@ function buildPostscript() {
|
|||||||
// 格式化函数
|
// 格式化函数
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function formatWorldLines(world) {
|
function formatFactsForInjection(facts) {
|
||||||
return [...(world || [])]
|
const activeFacts = (facts || []).filter(f => !f.retracted);
|
||||||
.sort((a, b) => (b.floor || 0) - (a.floor || 0))
|
if (!activeFacts.length) return [];
|
||||||
.map(w => `- ${w.topic}:${w.content}`);
|
return activeFacts
|
||||||
|
.sort((a, b) => (b.since || 0) - (a.since || 0))
|
||||||
|
.map(f => {
|
||||||
|
const since = f.since ? ` (#${f.since + 1})` : '';
|
||||||
|
if (isRelationFact(f) && f.trend) {
|
||||||
|
return `- ${f.s} ${f.p}: ${f.o} [${f.trend}]${since}`;
|
||||||
|
}
|
||||||
|
return `- ${f.s}的${f.p}: ${f.o}${since}`;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatArcLine(a) {
|
function formatArcLine(a) {
|
||||||
@@ -189,7 +197,7 @@ function formatInjectionLog(stats, details, recentOrphanStats = null) {
|
|||||||
|
|
||||||
// [1] 世界约束
|
// [1] 世界约束
|
||||||
lines.push(` [1] 世界约束 (上限 2000)`);
|
lines.push(` [1] 世界约束 (上限 2000)`);
|
||||||
lines.push(` 选入: ${stats.world.count} 条 | 消耗: ${stats.world.tokens} tokens`);
|
lines.push(` 选入: ${stats.facts.count} 条 | 消耗: ${stats.facts.tokens} tokens`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
// [2] 核心经历 + 过往背景
|
// [2] 核心经历 + 过往背景
|
||||||
@@ -229,7 +237,7 @@ function formatInjectionLog(stats, details, recentOrphanStats = null) {
|
|||||||
const pctStr = pct(tokens, total) + '%';
|
const pctStr = pct(tokens, total) + '%';
|
||||||
return ` ${label.padEnd(6)} ${'█'.repeat(width).padEnd(30)} ${String(tokens).padStart(5)} (${pctStr})`;
|
return ` ${label.padEnd(6)} ${'█'.repeat(width).padEnd(30)} ${String(tokens).padStart(5)} (${pctStr})`;
|
||||||
};
|
};
|
||||||
lines.push(bar(stats.world.tokens, '约束'));
|
lines.push(bar(stats.facts.tokens, '约束'));
|
||||||
lines.push(bar(stats.events.tokens + stats.evidence.tokens, '经历'));
|
lines.push(bar(stats.events.tokens + stats.evidence.tokens, '经历'));
|
||||||
lines.push(bar(stats.orphans.tokens, '远期'));
|
lines.push(bar(stats.orphans.tokens, '远期'));
|
||||||
lines.push(bar(recentOrphanStats?.tokens || 0, '待整理'));
|
lines.push(bar(recentOrphanStats?.tokens || 0, '待整理'));
|
||||||
@@ -263,9 +271,9 @@ function buildNonVectorPrompt(store) {
|
|||||||
const data = store.json || {};
|
const data = store.json || {};
|
||||||
const sections = [];
|
const sections = [];
|
||||||
|
|
||||||
if (data.world?.length) {
|
const factLines = formatFactsForInjection(getFacts(store));
|
||||||
const lines = formatWorldLines(data.world);
|
if (factLines.length) {
|
||||||
sections.push(`[世界约束] 已确立的事实\n${lines.join("\n")}`);
|
sections.push(`[定了的事] 已确立的事实\n${factLines.join("\n")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.events?.length) {
|
if (data.events?.length) {
|
||||||
@@ -330,7 +338,7 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
|||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const assembled = {
|
const assembled = {
|
||||||
world: { lines: [], tokens: 0 },
|
facts: { lines: [], tokens: 0 },
|
||||||
arcs: { lines: [], tokens: 0 },
|
arcs: { lines: [], tokens: 0 },
|
||||||
events: { direct: [], similar: [] },
|
events: { direct: [], similar: [] },
|
||||||
orphans: { lines: [], tokens: 0 },
|
orphans: { lines: [], tokens: 0 },
|
||||||
@@ -339,7 +347,7 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
|||||||
|
|
||||||
const injectionStats = {
|
const injectionStats = {
|
||||||
budget: { max: TOTAL_BUDGET_MAX, used: 0 },
|
budget: { max: TOTAL_BUDGET_MAX, used: 0 },
|
||||||
world: { count: 0, tokens: 0 },
|
facts: { count: 0, tokens: 0 },
|
||||||
arcs: { count: 0, tokens: 0 },
|
arcs: { count: 0, tokens: 0 },
|
||||||
events: { selected: 0, tokens: 0 },
|
events: { selected: 0, tokens: 0 },
|
||||||
evidence: { attached: 0, tokens: 0 },
|
evidence: { attached: 0, tokens: 0 },
|
||||||
@@ -360,16 +368,16 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
|||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// [优先级 1] 世界约束 - 最高优先级
|
// [优先级 1] 世界约束 - 最高优先级
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
const worldLines = formatWorldLines(data.world);
|
const factLines = formatFactsForInjection(getFacts(store));
|
||||||
if (worldLines.length) {
|
if (factLines.length) {
|
||||||
const l3Budget = { used: 0, max: Math.min(L3_MAX, total.max - total.used) };
|
const l3Budget = { used: 0, max: Math.min(L3_MAX, total.max - total.used) };
|
||||||
for (const line of worldLines) {
|
for (const line of factLines) {
|
||||||
if (!pushWithBudget(assembled.world.lines, line, l3Budget)) break;
|
if (!pushWithBudget(assembled.facts.lines, line, l3Budget)) break;
|
||||||
}
|
}
|
||||||
assembled.world.tokens = l3Budget.used;
|
assembled.facts.tokens = l3Budget.used;
|
||||||
total.used += l3Budget.used;
|
total.used += l3Budget.used;
|
||||||
injectionStats.world.count = assembled.world.lines.length;
|
injectionStats.facts.count = assembled.facts.lines.length;
|
||||||
injectionStats.world.tokens = l3Budget.used;
|
injectionStats.facts.tokens = l3Budget.used;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
@@ -599,8 +607,8 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities
|
|||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
const sections = [];
|
const sections = [];
|
||||||
// 1. 世界约束 → 定了的事
|
// 1. 世界约束 → 定了的事
|
||||||
if (assembled.world.lines.length) {
|
if (assembled.facts.lines.length) {
|
||||||
sections.push(`[定了的事] 已确立的事实\n${assembled.world.lines.join("\n")}`);
|
sections.push(`[定了的事] 已确立的事实\n${assembled.facts.lines.join("\n")}`);
|
||||||
}
|
}
|
||||||
// 2. 核心经历 → 印象深的事
|
// 2. 核心经历 → 印象深的事
|
||||||
if (assembled.events.direct.length) {
|
if (assembled.events.direct.length) {
|
||||||
@@ -632,6 +640,8 @@ if (!sections.length) {
|
|||||||
`<剧情记忆>\n\n${sections.join("\n\n")}\n\n</剧情记忆>\n` +
|
`<剧情记忆>\n\n${sections.join("\n\n")}\n\n</剧情记忆>\n` +
|
||||||
`${buildPostscript()}`;
|
`${buildPostscript()}`;
|
||||||
|
|
||||||
|
// ★ 修复:先写回预算统计,再生成日志
|
||||||
|
injectionStats.budget.used = total.used + (assembled.recentOrphans.tokens || 0);
|
||||||
const injectionLogText = formatInjectionLog(injectionStats, details, recentOrphanStats);
|
const injectionLogText = formatInjectionLog(injectionStats, details, recentOrphanStats);
|
||||||
|
|
||||||
return { promptText, injectionLogText, injectionStats };
|
return { promptText, injectionLogText, injectionStats };
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
events: { title: '编辑事件时间线', hint: '编辑时,每个事件要素都应完整' },
|
events: { title: '编辑事件时间线', hint: '编辑时,每个事件要素都应完整' },
|
||||||
characters: { title: '编辑人物关系', hint: '编辑时,每个要素都应完整' },
|
characters: { title: '编辑人物关系', hint: '编辑时,每个要素都应完整' },
|
||||||
arcs: { title: '编辑角色弧光', hint: '编辑时,每个要素都应完整' },
|
arcs: { title: '编辑角色弧光', hint: '编辑时,每个要素都应完整' },
|
||||||
world: { title: '编辑世界状态', hint: '每行一条:category|topic|content。清除用:category|topic|(留空)或 category|topic|cleared' }
|
facts: { title: '编辑事实图谱', hint: '每行一条:主体|谓词|值|趋势(可选)。删除用:主体|谓词|(留空值)' }
|
||||||
};
|
};
|
||||||
|
|
||||||
const TREND_COLORS = {
|
const TREND_COLORS = {
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } }
|
vector: { enabled: false, engine: 'online', local: { modelId: 'bge-small-zh' }, online: { provider: 'siliconflow', url: '', key: '', model: '' } }
|
||||||
};
|
};
|
||||||
|
|
||||||
let summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], world: [] };
|
let summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], facts: [] };
|
||||||
let localGenerating = false;
|
let localGenerating = false;
|
||||||
let vectorGenerating = false;
|
let vectorGenerating = false;
|
||||||
let relationChart = null;
|
let relationChart = null;
|
||||||
@@ -1415,9 +1415,14 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
|||||||
|
|
||||||
if (section === 'keywords') {
|
if (section === 'keywords') {
|
||||||
ta.value = summaryData.keywords.map(k => `${k.text}|${k.weight || '一般'}`).join('\n');
|
ta.value = summaryData.keywords.map(k => `${k.text}|${k.weight || '一般'}`).join('\n');
|
||||||
} else if (section === 'world') {
|
} else if (section === 'facts') {
|
||||||
ta.value = (summaryData.world || [])
|
ta.value = (summaryData.facts || [])
|
||||||
.map(w => `${w.category || ''}|${w.topic || ''}|${w.content || ''}`)
|
.filter(f => !f.retracted)
|
||||||
|
.map(f => {
|
||||||
|
const parts = [f.s, f.p, f.o];
|
||||||
|
if (f.trend) parts.push(f.trend);
|
||||||
|
return parts.join('|');
|
||||||
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
} else {
|
} else {
|
||||||
ta.classList.add('hidden');
|
ta.classList.add('hidden');
|
||||||
@@ -1496,21 +1501,32 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
|||||||
moments
|
moments
|
||||||
}, oldArc);
|
}, oldArc);
|
||||||
}).filter(a => a.name || a.trajectory || a.moments?.length);
|
}).filter(a => a.name || a.trajectory || a.moments?.length);
|
||||||
} else if (section === 'world') {
|
} else if (section === 'facts') {
|
||||||
const oldWorldMap = new Map((summaryData.world || []).map(w => [`${w.category}|${w.topic}`, w]));
|
const oldMap = new Map((summaryData.facts || []).map(f => [`${f.s}::${f.p}`, f]));
|
||||||
parsed = ta.value
|
parsed = ta.value
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(l => l.trim())
|
.map(l => l.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map(line => {
|
.map(line => {
|
||||||
const parts = line.split('|').map(s => s.trim());
|
const parts = line.split('|').map(s => s.trim());
|
||||||
const category = parts[0];
|
const s = parts[0];
|
||||||
const topic = parts[1];
|
const p = parts[1];
|
||||||
const content = parts.slice(2).join('|').trim();
|
const o = parts[2];
|
||||||
if (!category || !topic) return null;
|
const trend = parts[3];
|
||||||
if (!content || content.toLowerCase() === 'cleared') return null;
|
if (!s || !p) return null;
|
||||||
const key = `${category}|${topic}`;
|
if (!o) return null;
|
||||||
return preserveAddedAt({ category, topic, content }, oldWorldMap.get(key));
|
const key = `${s}::${p}`;
|
||||||
|
const old = oldMap.get(key);
|
||||||
|
const fact = {
|
||||||
|
id: old?.id || `f-${Date.now()}`,
|
||||||
|
s, p, o,
|
||||||
|
since: old?.since ?? 0,
|
||||||
|
_addedAt: old?._addedAt ?? 0,
|
||||||
|
};
|
||||||
|
if (/^对.+的/.test(p) && trend) {
|
||||||
|
fact.trend = trend;
|
||||||
|
}
|
||||||
|
return fact;
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
@@ -1526,7 +1542,7 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
|||||||
else if (section === 'events') { renderTimeline(parsed); $('stat-events').textContent = parsed.length; }
|
else if (section === 'events') { renderTimeline(parsed); $('stat-events').textContent = parsed.length; }
|
||||||
else if (section === 'characters') renderRelations(parsed);
|
else if (section === 'characters') renderRelations(parsed);
|
||||||
else if (section === 'arcs') renderArcs(parsed);
|
else if (section === 'arcs') renderArcs(parsed);
|
||||||
else if (section === 'world') renderWorldState(parsed);
|
else if (section === 'facts') renderFacts(parsed);
|
||||||
|
|
||||||
closeEditor();
|
closeEditor();
|
||||||
}
|
}
|
||||||
@@ -1565,7 +1581,7 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
|||||||
if (p.events) renderTimeline(p.events);
|
if (p.events) renderTimeline(p.events);
|
||||||
if (p.characters) renderRelations(p.characters);
|
if (p.characters) renderRelations(p.characters);
|
||||||
if (p.arcs) renderArcs(p.arcs);
|
if (p.arcs) renderArcs(p.arcs);
|
||||||
if (p.world) renderWorldState(p.world);
|
if (p.facts) renderFacts(p.facts);
|
||||||
$('stat-events').textContent = p.events?.length || 0;
|
$('stat-events').textContent = p.events?.length || 0;
|
||||||
if (p.lastSummarizedMesId != null) $('stat-summarized').textContent = p.lastSummarizedMesId + 1;
|
if (p.lastSummarizedMesId != null) $('stat-summarized').textContent = p.lastSummarizedMesId + 1;
|
||||||
if (p.stats) updateStats(p.stats);
|
if (p.stats) updateStats(p.stats);
|
||||||
@@ -1582,12 +1598,12 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
|||||||
$('stat-summarized').textContent = 0;
|
$('stat-summarized').textContent = 0;
|
||||||
$('stat-pending').textContent = t;
|
$('stat-pending').textContent = t;
|
||||||
$('summarized-count').textContent = 0;
|
$('summarized-count').textContent = 0;
|
||||||
summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], world: [] };
|
summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], facts: [] };
|
||||||
renderKeywords([]);
|
renderKeywords([]);
|
||||||
renderTimeline([]);
|
renderTimeline([]);
|
||||||
renderRelations(null);
|
renderRelations(null);
|
||||||
renderArcs([]);
|
renderArcs([]);
|
||||||
renderWorldState([]);
|
renderFacts([]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1829,7 +1845,7 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
|||||||
renderKeywords([]);
|
renderKeywords([]);
|
||||||
renderTimeline([]);
|
renderTimeline([]);
|
||||||
renderArcs([]);
|
renderArcs([]);
|
||||||
renderWorldState([]);
|
renderFacts([]);
|
||||||
|
|
||||||
bindEvents();
|
bindEvents();
|
||||||
|
|
||||||
@@ -1845,51 +1861,53 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderWorldState(world) {
|
function renderFacts(facts) {
|
||||||
summaryData.world = world || [];
|
summaryData.facts = facts || [];
|
||||||
|
|
||||||
const container = $('world-state-list');
|
const container = $('facts-list');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
if (!world?.length) {
|
const activeFacts = (facts || []).filter(f => !f.retracted);
|
||||||
setHtml(container, '<div class="empty">暂无世界状态</div>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const labels = {
|
if (!activeFacts.length) {
|
||||||
status: '状态',
|
setHtml(container, '<div class="empty">暂无事实记录</div>');
|
||||||
inventory: '物品',
|
return;
|
||||||
knowledge: '认知',
|
|
||||||
relation: '关系',
|
|
||||||
rule: '规则'
|
|
||||||
};
|
|
||||||
|
|
||||||
const categoryOrder = ['status', 'inventory', 'relation', 'knowledge', 'rule'];
|
|
||||||
|
|
||||||
const grouped = {};
|
|
||||||
world.forEach(w => {
|
|
||||||
const cat = w.category || 'other';
|
|
||||||
if (!grouped[cat]) grouped[cat] = [];
|
|
||||||
grouped[cat].push(w);
|
|
||||||
});
|
|
||||||
|
|
||||||
const html = categoryOrder
|
|
||||||
.filter(cat => grouped[cat]?.length)
|
|
||||||
.map(cat => {
|
|
||||||
const items = grouped[cat].sort((a, b) => (b.floor || 0) - (a.floor || 0));
|
|
||||||
return `
|
|
||||||
<div class="world-group">
|
|
||||||
<div class="world-group-title">${labels[cat] || cat}</div>
|
|
||||||
${items.map(w => `
|
|
||||||
<div class="world-item">
|
|
||||||
<span class="world-topic">${h(w.topic)}</span>
|
|
||||||
<span class="world-content">${h(w.content)}</span>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
setHtml(container, html || '<div class="empty">暂无世界状态</div>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relations = activeFacts.filter(f => /^对.+的/.test(f.p));
|
||||||
|
const states = activeFacts.filter(f => !/^对.+的/.test(f.p));
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
if (states.length) {
|
||||||
|
html += `<div class="fact-group">
|
||||||
|
<div class="fact-group-title">状态/属性</div>
|
||||||
|
${states.map(f => `
|
||||||
|
<div class="fact-item">
|
||||||
|
<span class="fact-subject">${h(f.s)}</span>
|
||||||
|
<span class="fact-predicate">${h(f.p)}</span>
|
||||||
|
<span class="fact-object">${h(f.o)}</span>
|
||||||
|
<span class="fact-since">#${(f.since || 0) + 1}</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relations.length) {
|
||||||
|
html += `<div class="fact-group">
|
||||||
|
<div class="fact-group-title">人物关系</div>
|
||||||
|
${relations.map(f => `
|
||||||
|
<div class="fact-item">
|
||||||
|
<span class="fact-subject">${h(f.s)}</span>
|
||||||
|
<span class="fact-predicate">${h(f.p)}</span>
|
||||||
|
<span class="fact-object">${h(f.o)}</span>
|
||||||
|
${f.trend ? `<span class="fact-trend ${TREND_CLASS[f.trend] || ''}">${h(f.trend)}</span>` : ''}
|
||||||
|
<span class="fact-since">#${(f.since || 0) + 1}</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHtml(container, html);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -6,6 +6,101 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
Facts (替换 World State)
|
||||||
|
═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
.facts {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facts-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-group-title {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--txt3);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid var(--bdr2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
background: var(--bg3);
|
||||||
|
border: 1px solid var(--bdr2);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-item:hover {
|
||||||
|
border-color: var(--bdr);
|
||||||
|
background: var(--bg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-subject {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-predicate {
|
||||||
|
color: var(--txt3);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-predicate::before {
|
||||||
|
content: '→';
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-object {
|
||||||
|
color: var(--hl);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-trend {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-since {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
color: var(--txt3);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.facts-list {
|
||||||
|
max-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fact-item {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #fafafa;
|
--bg: #fafafa;
|
||||||
--bg2: #fff;
|
--bg2: #fff;
|
||||||
|
|||||||
@@ -80,13 +80,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<!-- World State -->
|
<!-- Facts -->
|
||||||
<section class="card world-state">
|
<section class="card facts">
|
||||||
<div class="sec-head">
|
<div class="sec-head">
|
||||||
<div class="sec-title">世界状态</div>
|
<div class="sec-title">事实图谱</div>
|
||||||
<button class="sec-btn" data-section="world">编辑</button>
|
<button class="sec-btn" data-section="facts">编辑</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="world-state-list scroll" id="world-state-list"></div>
|
<div class="facts-list scroll" id="facts-list"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Relations -->
|
<!-- Relations -->
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { getAllEventVectors, getAllChunkVectors, getChunksByFloors, getMeta } fr
|
|||||||
import { embed, getEngineFingerprint } from './embedder.js';
|
import { embed, getEngineFingerprint } from './embedder.js';
|
||||||
import { xbLog } from '../../../core/debug-core.js';
|
import { xbLog } from '../../../core/debug-core.js';
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { getSummaryStore } from '../data/store.js';
|
import { getSummaryStore, getFacts, getNewCharacters, isRelationFact } from '../data/store.js';
|
||||||
import { filterText } from './text-filter.js';
|
import { filterText } from './text-filter.js';
|
||||||
import {
|
import {
|
||||||
searchStateAtoms,
|
searchStateAtoms,
|
||||||
@@ -258,6 +258,23 @@ function buildEntityLexicon(store, allEvents) {
|
|||||||
const userName = normalize(name1);
|
const userName = normalize(name1);
|
||||||
const set = new Set();
|
const set = new Set();
|
||||||
|
|
||||||
|
const facts = getFacts(store);
|
||||||
|
for (const f of facts) {
|
||||||
|
if (f?.retracted) continue;
|
||||||
|
const s = normalize(f?.s);
|
||||||
|
if (s) set.add(s);
|
||||||
|
if (isRelationFact(f)) {
|
||||||
|
const o = normalize(f?.o);
|
||||||
|
if (o) set.add(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chars = getNewCharacters(store);
|
||||||
|
for (const m of chars || []) {
|
||||||
|
const s = normalize(typeof m === 'string' ? m : m?.name);
|
||||||
|
if (s) set.add(s);
|
||||||
|
}
|
||||||
|
|
||||||
for (const e of allEvents || []) {
|
for (const e of allEvents || []) {
|
||||||
for (const p of e.participants || []) {
|
for (const p of e.participants || []) {
|
||||||
const s = normalize(p);
|
const s = normalize(p);
|
||||||
@@ -265,30 +282,11 @@ function buildEntityLexicon(store, allEvents) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = store?.json || {};
|
for (const a of store?.json?.arcs || []) {
|
||||||
|
|
||||||
for (const m of json.characters?.main || []) {
|
|
||||||
const s = normalize(typeof m === 'string' ? m : m?.name);
|
|
||||||
if (s) set.add(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const a of json.arcs || []) {
|
|
||||||
const s = normalize(a?.name);
|
const s = normalize(a?.name);
|
||||||
if (s) set.add(s);
|
if (s) set.add(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const w of json.world || []) {
|
|
||||||
const t = normalize(w?.topic);
|
|
||||||
if (t && !t.includes('::')) set.add(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const r of json.characters?.relationships || []) {
|
|
||||||
const from = normalize(r?.from);
|
|
||||||
const to = normalize(r?.to);
|
|
||||||
if (from) set.add(from);
|
|
||||||
if (to) set.add(to);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stop = new Set([userName, '我', '你', '他', '她', '它', '用户', '角色', 'assistant'].map(normalize).filter(Boolean));
|
const stop = new Set([userName, '我', '你', '他', '她', '它', '用户', '角色', 'assistant'].map(normalize).filter(Boolean));
|
||||||
|
|
||||||
return Array.from(set)
|
return Array.from(set)
|
||||||
@@ -296,6 +294,79 @@ function buildEntityLexicon(store, allEvents) {
|
|||||||
.slice(0, 5000);
|
.slice(0, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildFactGraph(facts) {
|
||||||
|
const graph = new Map();
|
||||||
|
|
||||||
|
for (const f of facts || []) {
|
||||||
|
if (f?.retracted) continue;
|
||||||
|
if (!isRelationFact(f)) continue;
|
||||||
|
|
||||||
|
const s = normalize(f?.s);
|
||||||
|
const o = normalize(f?.o);
|
||||||
|
if (!s || !o) continue;
|
||||||
|
|
||||||
|
if (!graph.has(s)) graph.set(s, new Set());
|
||||||
|
if (!graph.has(o)) graph.set(o, new Set());
|
||||||
|
graph.get(s).add(o);
|
||||||
|
graph.get(o).add(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandByFacts(presentEntities, facts, maxDepth = 2) {
|
||||||
|
const graph = buildFactGraph(facts);
|
||||||
|
const expanded = new Map();
|
||||||
|
const seeds = Array.from(presentEntities || []).map(normalize).filter(Boolean);
|
||||||
|
|
||||||
|
seeds.forEach(e => expanded.set(e, 1.0));
|
||||||
|
|
||||||
|
let frontier = [...seeds];
|
||||||
|
for (let d = 1; d <= maxDepth && frontier.length; d++) {
|
||||||
|
const next = [];
|
||||||
|
const decay = Math.pow(0.5, d);
|
||||||
|
|
||||||
|
for (const e of frontier) {
|
||||||
|
const neighbors = graph.get(e);
|
||||||
|
if (!neighbors) continue;
|
||||||
|
|
||||||
|
for (const neighbor of neighbors) {
|
||||||
|
if (!expanded.has(neighbor)) {
|
||||||
|
expanded.set(neighbor, decay);
|
||||||
|
next.push(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frontier = next.slice(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(expanded.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 15)
|
||||||
|
.map(([term]) => term);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripFloorTag(s) {
|
||||||
|
return String(s || '').replace(/\s*\(#\d+(?:-\d+)?\)\s*$/, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildEventEmbeddingText(ev) {
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
if (ev?.title) parts.push(ev.title);
|
||||||
|
|
||||||
|
const people = (ev?.participants || []).join(' ');
|
||||||
|
if (people) parts.push(people);
|
||||||
|
|
||||||
|
if (ev?.type) parts.push(ev.type);
|
||||||
|
|
||||||
|
const summary = stripFloorTag(ev?.summary);
|
||||||
|
if (summary) parts.push(summary);
|
||||||
|
|
||||||
|
return parts.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从分段消息中提取实体,继承消息权重
|
* 从分段消息中提取实体,继承消息权重
|
||||||
* @param {string[]} segments
|
* @param {string[]} segments
|
||||||
@@ -621,6 +692,7 @@ function formatRecallLog({
|
|||||||
chunkPreFilterStats = null,
|
chunkPreFilterStats = null,
|
||||||
l0Results = [],
|
l0Results = [],
|
||||||
textGapInfo = null,
|
textGapInfo = null,
|
||||||
|
expandedTerms = [],
|
||||||
}) {
|
}) {
|
||||||
const lines = [
|
const lines = [
|
||||||
'\u2554' + '\u2550'.repeat(62) + '\u2557',
|
'\u2554' + '\u2550'.repeat(62) + '\u2557',
|
||||||
@@ -663,6 +735,9 @@ function formatRecallLog({
|
|||||||
} else {
|
} else {
|
||||||
lines.push(' (无)');
|
lines.push(' (无)');
|
||||||
}
|
}
|
||||||
|
if (expandedTerms?.length) {
|
||||||
|
lines.push(` 扩散: ${expandedTerms.join('、')}`);
|
||||||
|
}
|
||||||
|
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('\u250c' + '\u2500'.repeat(61) + '\u2510');
|
lines.push('\u250c' + '\u2500'.repeat(61) + '\u2510');
|
||||||
@@ -757,12 +832,15 @@ export async function recallMemory(queryText, allEvents, vectorConfig, options =
|
|||||||
const lexicon = buildEntityLexicon(store, allEvents);
|
const lexicon = buildEntityLexicon(store, allEvents);
|
||||||
const queryEntityWeights = extractEntitiesWithWeights(segments, weights, lexicon);
|
const queryEntityWeights = extractEntitiesWithWeights(segments, weights, lexicon);
|
||||||
const queryEntities = Array.from(queryEntityWeights.keys());
|
const queryEntities = Array.from(queryEntityWeights.keys());
|
||||||
|
const facts = getFacts(store);
|
||||||
|
const expandedTerms = expandByFacts(queryEntities, facts, 2);
|
||||||
|
|
||||||
// 构建文本查询串:最后一条消息 + 实体 + 关键词
|
// 构建文本查询串:最后一条消息 + 实体 + 关键词
|
||||||
const lastSeg = segments[segments.length - 1] || '';
|
const lastSeg = segments[segments.length - 1] || '';
|
||||||
const queryTextForSearch = [
|
const queryTextForSearch = [
|
||||||
lastSeg,
|
lastSeg,
|
||||||
...queryEntities,
|
...queryEntities,
|
||||||
|
...expandedTerms,
|
||||||
...(store?.json?.keywords || []).slice(0, 5).map(k => k.text),
|
...(store?.json?.keywords || []).slice(0, 5).map(k => k.text),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
@@ -824,6 +902,7 @@ export async function recallMemory(queryText, allEvents, vectorConfig, options =
|
|||||||
chunkPreFilterStats,
|
chunkPreFilterStats,
|
||||||
l0Results,
|
l0Results,
|
||||||
textGapInfo,
|
textGapInfo,
|
||||||
|
expandedTerms,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.group('%c[Recall]', 'color: #7c3aed; font-weight: bold');
|
console.group('%c[Recall]', 'color: #7c3aed; font-weight: bold');
|
||||||
|
|||||||
Reference in New Issue
Block a user