From fb8ed8037c0c78e6c7fd2dd70752f569ca7cd6c9 Mon Sep 17 00:00:00 2001 From: bielie Date: Mon, 2 Feb 2026 21:45:01 +0800 Subject: [PATCH] story-summary: facts migration + recall enhancements --- modules/story-summary/data/store.js | 234 +++++++++++++++----- modules/story-summary/generate/generator.js | 65 +++--- modules/story-summary/generate/llm.js | 138 +++++------- modules/story-summary/generate/prompt.js | 54 +++-- modules/story-summary/story-summary-ui.js | 142 ++++++------ modules/story-summary/story-summary.css | 95 ++++++++ modules/story-summary/story-summary.html | 10 +- modules/story-summary/vector/recall.js | 121 ++++++++-- 8 files changed, 570 insertions(+), 289 deletions(-) diff --git a/modules/story-summary/data/store.js b/modules/story-summary/data/store.js index 08fc1d3..366ea97 100644 --- a/modules/story-summary/data/store.js +++ b/modules/story-summary/data/store.js @@ -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 + ); } diff --git a/modules/story-summary/generate/generator.js b/modules/story-summary/generate/generator.js index ffe98c2..a74f48d 100644 --- a/modules/story-summary/generate/generator.js +++ b/modules/story-summary/generate/generator.js @@ -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 }; diff --git a/modules/story-summary/generate/llm.js b/modules/story-summary/generate/llm.js index a528005..0b47329 100644 --- a/modules/story-summary/generate/llm.js +++ b/modules/story-summary/generate/llm.js @@ -1,7 +1,6 @@ // LLM Service const PROVIDER_MAP = { - // ... openai: "openai", google: "gemini", gemini: "gemini", @@ -39,43 +38,37 @@ Incremental_Summary_Requirements: - Causal_Chain: 为每个新事件标注直接前因事件ID(causedBy)。仅在因果关系明确(直接导致/明确动机/承接后果)时填写;不明确时填[]完全正常。0-2个,只填 evt-数字,指向已存在或本次新输出事件。 - Character_Dynamics: 识别新角色,追踪关系趋势(破裂/厌恶/反感/陌生/投缘/亲密/交融) - Arc_Tracking: 更新角色弧光轨迹与成长进度(0.0-1.0) - - World_State_Tracking: 维护当前世界的硬性约束。解决"什么不能违反"。采用 KV 覆盖模型,追踪生死、物品归属、秘密知情、关系状态、环境规则等不可违背的事实。(覆盖式更新) - categories: - - status: 角色生死、位置锁定、重大状态 - - inventory: 重要物品归属 - - knowledge: 秘密的知情状态 - - relation: 硬性关系(在一起/决裂) - - rule: 环境规则/契约限制 + - Fact_Tracking: 维护 SPO 三元组知识图谱。追踪生死、物品归属、位置、关系等硬性事实。采用 KV 覆盖模型(s+p 为键)。 --- Story Analyst: [Responsibility Definition] \`\`\`yaml analysis_task: - title: Incremental Story Summarization with World State + title: Incremental Story Summarization with Knowledge Graph Story Analyst: role: Antigravity task: >- To analyze provided dialogue content against existing summary state, 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. assistant: role: Summary Specialist - description: Incremental Story Summary & World State Analyst + description: Incremental Story Summary & Knowledge Graph Analyst behavior: >- To compare new dialogue against existing summary, identify genuinely new events and character interactions, classify events by narrative 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. Must strictly avoid repeating any existing summary content. user: role: Content Provider description: Supplies existing summary state and new dialogue behavior: >- - To provide existing summary state (events, characters, relationships, - arcs, world state) and new dialogue content for incremental analysis. + To provide existing summary state (events, characters, arcs, facts) + and new dialogue content for incremental analysis. interaction_mode: type: incremental_analysis output_format: structured_json @@ -84,7 +77,7 @@ execution_context: summary_active: true incremental_only: true memory_album_style: true - world_state_tracking: true + fact_tracking: true \`\`\` --- Summary Specialist: @@ -103,15 +96,17 @@ Acknowledged. Now reviewing the incremental summarization specifications: 破裂 ← 厌恶 ← 反感 ← 陌生 → 投缘 → 亲密 → 交融 [Arc Progress Tracking] -├─ trajectory: 完整弧光链描述(30字内) +├─ trajectory: 当前阶段描述(15字内) ├─ progress: 0.0 to 1.0 └─ newMoment: 仅记录本次新增的关键时刻 -[World State Maintenance] -├─ 维护方式: Key-Value 覆盖(category + topic 为键) -├─ 只输出有变化的条目 -├─ 清除时使用 cleared: true,不要填 content -└─ 不记录情绪、衣着、临时动作 +[Fact Tracking - SPO Triples] +├─ s: 主体(角色名/物品名) +├─ p: 谓词(属性名/对X的看法) +├─ o: 值(当前状态) +├─ trend: 仅关系类填写 +├─ retracted: 删除标记 +└─ s+p 为键,相同键会覆盖旧值 Ready to process incremental summary requests with strict deduplication.`, @@ -119,19 +114,19 @@ Ready to process incremental summary requests with strict deduplication.`, Summary Specialist: Specifications internalized. Please provide the existing summary state so I can: 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 4. Identify established keywords -5. Review current world state (category + topic baseline)`, +5. Review current facts (SPO triples baseline)`, assistantAskContent: ` Summary Specialist: Existing summary fully analyzed and indexed. I understand: ├─ Recorded events: Indexed for deduplication -├─ Character relationships: Baseline mapped +├─ Character list: Baseline mapped ├─ Arc progress: Levels noted ├─ Keywords: Current state acknowledged -└─ World state: Baseline loaded +└─ Facts: SPO baseline loaded I will extract only genuinely NEW elements from the upcoming dialogue. 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 relationship CHANGES happened? - What arc PROGRESS was made? -- What world state changes occurred? (status/inventory/knowledge/relation/rule) +- What facts changed? (status/position/ownership/relationships) ## Output Format \`\`\`json @@ -160,7 +155,7 @@ Before generating, observe the USER and analyze carefully: "mindful_prelude": { "user_insight": "用户的幻想是什么时空、场景,是否反应出存在严重心理问题需要建议?", "dedup_analysis": "已有X个事件,本次识别Y个新事件", - "world_changes": "识别到的世界状态变化概述,仅精选不记录则可能导致吃书的硬状态变化" + "fact_changes": "识别到的事实变化概述" }, "keywords": [ {"text": "综合已有+新内容的全局关键词(5-10个)", "weight": "核心|重要|一般"} @@ -178,45 +173,35 @@ Before generating, observe the USER and analyze carefully: } ], "newCharacters": ["仅本次首次出现的角色名"], - "newRelationships": [ - {"from": "A", "to": "B", "label": "基于全局的关系描述", "trend": "破裂|厌恶|反感|陌生|投缘|亲密|交融"} - ], "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", - "topic": "主体名称(人/物/关系/规则)", - "content": "当前状态描述", - "cleared": true + "s": "主体(角色名/物品名)", + "p": "谓词(属性名/对X的看法)", + "o": "当前值", + "trend": "破裂|厌恶|反感|陌生|投缘|亲密|交融", + "retracted": false } ] } \`\`\` -## Field Guidelines - -### worldUpdate(世界状态·硬约束KV表) -- category 固定 5 选 1:status / inventory / knowledge / relation / rule -- topic 命名规范: - - status:「角色名::状态类型」如 张三::生死、李四::位置、王五::伤势 - - knowledge:「角色名::知情事项」如 张三::知道某秘密、李四::知道真相 - - relation:「角色A::与角色B关系」如 张三::与李四关系 - - inventory:物品名称,如 钥匙、信物、武器 - - rule:规则/契约名称,如 门禁时间、魔法契约、禁令 -- content:当前状态的简短描述 -- cleared: true 表示该条目已失效需删除(不填 content) -- status/knowledge/relation 的 topic 必须包含「::」分隔符 -- 硬约束才记录,避免叙事化,确保少、硬、稳定、可覆盖 -- 动态清理:若发现已有条目中存在不适合作为硬约束的内容(如衣着打扮、临时情绪、琐碎动作),本次输出中用 cleared: true 删除 +## factUpdates 规则 +- s+p 为键,相同键会覆盖旧值 +- 状态类:s=角色名, p=属性(生死/位置/状态等), o=值 +- 关系类:s=角色A, p="对B的看法", o=描述, trend=趋势 +- 删除:设置 retracted: true(不需要填 o) +- 只输出有变化的条目 +- 硬约束才记录,避免叙事化,确保少、硬、稳定 ## CRITICAL NOTES - events.id 从 evt-{nextEventId} 开始编号 - 仅输出【增量】内容,已有事件绝不重复 - keywords 是全局关键词,综合已有+新增 -- causedBy 仅在因果明确时填写,允许为[],0-2个,详见上方 Causal_Chain 规则 -- worldUpdate 可为空数组 +- causedBy 仅在因果明确时填写,允许为[],0-2个 +- factUpdates 可为空数组 - 合法JSON,字符串值内部避免英文双引号 - 用朴实、白描、有烟火气的笔触记录,避免比喻和意象 `, @@ -227,15 +212,14 @@ Before generating, observe the USER and analyze carefully: ├─ New dialogue received: ✓ Content parsed ├─ Deduplication engine: ✓ Active ├─ Event classification: ✓ Ready -├─ World state tracking: ✓ Enabled +├─ Fact tracking: ✓ Enabled └─ Output format: ✓ JSON specification loaded [Material Verification] ├─ Existing events: Indexed ({existingEventCount} recorded) ├─ Character baseline: Mapped -├─ Relationship baseline: Mapped ├─ Arc progress baseline: Noted -├─ World state: Baseline loaded +├─ Facts baseline: Loaded └─ Output specification: ✓ Defined in All checks passed. Beginning incremental extraction... { @@ -280,39 +264,23 @@ function waitForStreamingComplete(sessionId, streamingMod, timeout = 120000) { // 提示词构建 // ═══════════════════════════════════════════════════════════════════════════ -function formatWorldForLLM(worldList) { - if (!worldList?.length) { - return '(空白,尚无世界状态记录)'; +function formatFactsForLLM(facts) { + if (!facts?.length) { + return '(空白,尚无事实记录)'; } - const grouped = { status: [], inventory: [], knowledge: [], relation: [], rule: [] }; - const labels = { - status: '状态(生死/位置锁定)', - inventory: '物品归属', - knowledge: '秘密/认知', - relation: '关系状态', - rule: '规则/约束' - }; - - worldList.forEach(w => { - if (grouped[w.category]) { - grouped[w.category].push(w); + const lines = facts.map(f => { + if (f.trend) { + return `- ${f.s} | ${f.p} | ${f.o} [${f.trend}]`; } + return `- ${f.s} | ${f.p} | ${f.o}`; }); - const parts = []; - 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') || '(空白,尚无世界状态记录)'; + return lines.join('\n') || '(空白,尚无事实记录)'; } -function buildSummaryMessages(existingSummary, existingWorld, newHistoryText, historyRange, nextEventId, existingEventCount) { - const worldStateText = formatWorldForLLM(existingWorld); +function buildSummaryMessages(existingSummary, existingFacts, newHistoryText, historyRange, nextEventId, existingEventCount) { + const factsText = formatFactsForLLM(existingFacts); const jsonFormat = LLM_PROMPT_CONFIG.userJsonFormat .replace(/\{nextEventId\}/g, String(nextEventId)); @@ -324,7 +292,7 @@ function buildSummaryMessages(existingSummary, existingWorld, newHistoryText, hi { role: 'system', content: LLM_PROMPT_CONFIG.topSystem }, { role: 'assistant', content: LLM_PROMPT_CONFIG.assistantDoc }, { 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: 'user', content: `<新对话内容>(${historyRange})\n${newHistoryText}\n` } ]; @@ -378,7 +346,7 @@ export function parseSummaryJson(raw) { export async function generateSummary(options) { const { existingSummary, - existingWorld, + existingFacts, newHistoryText, historyRange, nextEventId, @@ -401,7 +369,7 @@ export async function generateSummary(options) { const promptData = buildSummaryMessages( existingSummary, - existingWorld, + existingFacts, newHistoryText, historyRange, nextEventId, diff --git a/modules/story-summary/generate/prompt.js b/modules/story-summary/generate/prompt.js index bfb95c3..ec5c1a3 100644 --- a/modules/story-summary/generate/prompt.js +++ b/modules/story-summary/generate/prompt.js @@ -6,7 +6,7 @@ import { getContext } from "../../../../../../extensions.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 { recallMemory, buildQueryText } from "../vector/recall.js"; import { getChunksByFloors, getAllChunkVectors, getAllEventVectors, getMeta } from "../vector/chunk-store.js"; @@ -111,10 +111,18 @@ function buildPostscript() { // 格式化函数 // ───────────────────────────────────────────────────────────────────────────── -function formatWorldLines(world) { - return [...(world || [])] - .sort((a, b) => (b.floor || 0) - (a.floor || 0)) - .map(w => `- ${w.topic}:${w.content}`); +function formatFactsForInjection(facts) { + const activeFacts = (facts || []).filter(f => !f.retracted); + if (!activeFacts.length) return []; + 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) { @@ -189,7 +197,7 @@ function formatInjectionLog(stats, details, recentOrphanStats = null) { // [1] 世界约束 lines.push(` [1] 世界约束 (上限 2000)`); - lines.push(` 选入: ${stats.world.count} 条 | 消耗: ${stats.world.tokens} tokens`); + lines.push(` 选入: ${stats.facts.count} 条 | 消耗: ${stats.facts.tokens} tokens`); lines.push(''); // [2] 核心经历 + 过往背景 @@ -229,7 +237,7 @@ function formatInjectionLog(stats, details, recentOrphanStats = null) { const pctStr = pct(tokens, total) + '%'; 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.orphans.tokens, '远期')); lines.push(bar(recentOrphanStats?.tokens || 0, '待整理')); @@ -263,9 +271,9 @@ function buildNonVectorPrompt(store) { const data = store.json || {}; const sections = []; - if (data.world?.length) { - const lines = formatWorldLines(data.world); - sections.push(`[世界约束] 已确立的事实\n${lines.join("\n")}`); + const factLines = formatFactsForInjection(getFacts(store)); + if (factLines.length) { + sections.push(`[定了的事] 已确立的事实\n${factLines.join("\n")}`); } if (data.events?.length) { @@ -330,7 +338,7 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities // ═══════════════════════════════════════════════════════════════════ const assembled = { - world: { lines: [], tokens: 0 }, + facts: { lines: [], tokens: 0 }, arcs: { lines: [], tokens: 0 }, events: { direct: [], similar: [] }, orphans: { lines: [], tokens: 0 }, @@ -339,7 +347,7 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities const injectionStats = { budget: { max: TOTAL_BUDGET_MAX, used: 0 }, - world: { count: 0, tokens: 0 }, + facts: { count: 0, tokens: 0 }, arcs: { count: 0, tokens: 0 }, events: { selected: 0, tokens: 0 }, evidence: { attached: 0, tokens: 0 }, @@ -360,16 +368,16 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities // ═══════════════════════════════════════════════════════════════════ // [优先级 1] 世界约束 - 最高优先级 // ═══════════════════════════════════════════════════════════════════ - const worldLines = formatWorldLines(data.world); - if (worldLines.length) { + const factLines = formatFactsForInjection(getFacts(store)); + if (factLines.length) { const l3Budget = { used: 0, max: Math.min(L3_MAX, total.max - total.used) }; - for (const line of worldLines) { - if (!pushWithBudget(assembled.world.lines, line, l3Budget)) break; + for (const line of factLines) { + if (!pushWithBudget(assembled.facts.lines, line, l3Budget)) break; } - assembled.world.tokens = l3Budget.used; + assembled.facts.tokens = l3Budget.used; total.used += l3Budget.used; - injectionStats.world.count = assembled.world.lines.length; - injectionStats.world.tokens = l3Budget.used; + injectionStats.facts.count = assembled.facts.lines.length; + injectionStats.facts.tokens = l3Budget.used; } // ═══════════════════════════════════════════════════════════════════ @@ -599,8 +607,8 @@ async function buildVectorPrompt(store, recallResult, causalById, queryEntities // ═══════════════════════════════════════════════════════════════════════ const sections = []; // 1. 世界约束 → 定了的事 -if (assembled.world.lines.length) { - sections.push(`[定了的事] 已确立的事实\n${assembled.world.lines.join("\n")}`); +if (assembled.facts.lines.length) { + sections.push(`[定了的事] 已确立的事实\n${assembled.facts.lines.join("\n")}`); } // 2. 核心经历 → 印象深的事 if (assembled.events.direct.length) { @@ -632,6 +640,8 @@ if (!sections.length) { `<剧情记忆>\n\n${sections.join("\n\n")}\n\n\n` + `${buildPostscript()}`; + // ★ 修复:先写回预算统计,再生成日志 + injectionStats.budget.used = total.used + (assembled.recentOrphans.tokens || 0); const injectionLogText = formatInjectionLog(injectionStats, details, recentOrphanStats); return { promptText, injectionLogText, injectionStats }; @@ -835,4 +845,4 @@ export async function buildVectorPromptText(excludeLastAi = false, hooks = {}) { } return { text: finalText, logText: (recallResult.logText || "") + (injectionLogText || "") }; -} \ No newline at end of file +} diff --git a/modules/story-summary/story-summary-ui.js b/modules/story-summary/story-summary-ui.js index 39def8d..c4f25b1 100644 --- a/modules/story-summary/story-summary-ui.js +++ b/modules/story-summary/story-summary-ui.js @@ -60,7 +60,7 @@ events: { title: '编辑事件时间线', hint: '编辑时,每个事件要素都应完整' }, characters: { title: '编辑人物关系', hint: '编辑时,每个要素都应完整' }, arcs: { title: '编辑角色弧光', hint: '编辑时,每个要素都应完整' }, - world: { title: '编辑世界状态', hint: '每行一条:category|topic|content。清除用:category|topic|(留空)或 category|topic|cleared' } + facts: { title: '编辑事实图谱', hint: '每行一条:主体|谓词|值|趋势(可选)。删除用:主体|谓词|(留空值)' } }; const TREND_COLORS = { @@ -116,7 +116,7 @@ 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 vectorGenerating = false; let relationChart = null; @@ -1415,9 +1415,14 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", " if (section === 'keywords') { ta.value = summaryData.keywords.map(k => `${k.text}|${k.weight || '一般'}`).join('\n'); - } else if (section === 'world') { - ta.value = (summaryData.world || []) - .map(w => `${w.category || ''}|${w.topic || ''}|${w.content || ''}`) + } else if (section === 'facts') { + ta.value = (summaryData.facts || []) + .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'); } else { ta.classList.add('hidden'); @@ -1496,21 +1501,32 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", " moments }, oldArc); }).filter(a => a.name || a.trajectory || a.moments?.length); - } else if (section === 'world') { - const oldWorldMap = new Map((summaryData.world || []).map(w => [`${w.category}|${w.topic}`, w])); + } else if (section === 'facts') { + const oldMap = new Map((summaryData.facts || []).map(f => [`${f.s}::${f.p}`, f])); parsed = ta.value .split('\n') .map(l => l.trim()) .filter(Boolean) .map(line => { const parts = line.split('|').map(s => s.trim()); - const category = parts[0]; - const topic = parts[1]; - const content = parts.slice(2).join('|').trim(); - if (!category || !topic) return null; - if (!content || content.toLowerCase() === 'cleared') return null; - const key = `${category}|${topic}`; - return preserveAddedAt({ category, topic, content }, oldWorldMap.get(key)); + const s = parts[0]; + const p = parts[1]; + const o = parts[2]; + const trend = parts[3]; + if (!s || !p) return null; + if (!o) return null; + 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); } @@ -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 === 'characters') renderRelations(parsed); else if (section === 'arcs') renderArcs(parsed); - else if (section === 'world') renderWorldState(parsed); + else if (section === 'facts') renderFacts(parsed); 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.characters) renderRelations(p.characters); 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; if (p.lastSummarizedMesId != null) $('stat-summarized').textContent = p.lastSummarizedMesId + 1; 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-pending').textContent = t; $('summarized-count').textContent = 0; - summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], world: [] }; + summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [], facts: [] }; renderKeywords([]); renderTimeline([]); renderRelations(null); renderArcs([]); - renderWorldState([]); + renderFacts([]); break; } @@ -1829,7 +1845,7 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", " renderKeywords([]); renderTimeline([]); renderArcs([]); - renderWorldState([]); + renderFacts([]); bindEvents(); @@ -1845,51 +1861,53 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", " } - function renderWorldState(world) { - summaryData.world = world || []; + function renderFacts(facts) { + summaryData.facts = facts || []; - const container = $('world-state-list'); - if (!container) return; + const container = $('facts-list'); + if (!container) return; - if (!world?.length) { - setHtml(container, '
暂无世界状态
'); - return; - } + const activeFacts = (facts || []).filter(f => !f.retracted); - const labels = { - status: '状态', - inventory: '物品', - 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 ` -
-
${labels[cat] || cat}
- ${items.map(w => ` -
- ${h(w.topic)} - ${h(w.content)} -
- `).join('')} -
- `; - }).join(''); - - setHtml(container, html || '
暂无世界状态
'); + if (!activeFacts.length) { + setHtml(container, '
暂无事实记录
'); + return; } + + const relations = activeFacts.filter(f => /^对.+的/.test(f.p)); + const states = activeFacts.filter(f => !/^对.+的/.test(f.p)); + + let html = ''; + + if (states.length) { + html += `
+
状态/属性
+ ${states.map(f => ` +
+ ${h(f.s)} + ${h(f.p)} + ${h(f.o)} + #${(f.since || 0) + 1} +
+ `).join('')} +
`; + } + + if (relations.length) { + html += `
+
人物关系
+ ${relations.map(f => ` +
+ ${h(f.s)} + ${h(f.p)} + ${h(f.o)} + ${f.trend ? `${h(f.trend)}` : ''} + #${(f.since || 0) + 1} +
+ `).join('')} +
`; + } + + setHtml(container, html); +} })(); diff --git a/modules/story-summary/story-summary.css b/modules/story-summary/story-summary.css index 375c39c..cb70f02 100644 --- a/modules/story-summary/story-summary.css +++ b/modules/story-summary/story-summary.css @@ -6,6 +6,101 @@ 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 { --bg: #fafafa; --bg2: #fff; diff --git a/modules/story-summary/story-summary.html b/modules/story-summary/story-summary.html index 3397874..405e206 100644 --- a/modules/story-summary/story-summary.html +++ b/modules/story-summary/story-summary.html @@ -80,13 +80,13 @@
- -
+ +
-
世界状态
- +
事实图谱
+
-
+
diff --git a/modules/story-summary/vector/recall.js b/modules/story-summary/vector/recall.js index 9a250db..bf5279f 100644 --- a/modules/story-summary/vector/recall.js +++ b/modules/story-summary/vector/recall.js @@ -11,7 +11,7 @@ import { getAllEventVectors, getAllChunkVectors, getChunksByFloors, getMeta } fr import { embed, getEngineFingerprint } from './embedder.js'; import { xbLog } from '../../../core/debug-core.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 { searchStateAtoms, @@ -258,6 +258,23 @@ function buildEntityLexicon(store, allEvents) { const userName = normalize(name1); 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 p of e.participants || []) { const s = normalize(p); @@ -265,30 +282,11 @@ function buildEntityLexicon(store, allEvents) { } } - const json = store?.json || {}; - - 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 || []) { + for (const a of store?.json?.arcs || []) { const s = normalize(a?.name); 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)); return Array.from(set) @@ -296,6 +294,79 @@ function buildEntityLexicon(store, allEvents) { .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 @@ -621,6 +692,7 @@ function formatRecallLog({ chunkPreFilterStats = null, l0Results = [], textGapInfo = null, + expandedTerms = [], }) { const lines = [ '\u2554' + '\u2550'.repeat(62) + '\u2557', @@ -663,6 +735,9 @@ function formatRecallLog({ } else { lines.push(' (无)'); } + if (expandedTerms?.length) { + lines.push(` 扩散: ${expandedTerms.join('、')}`); + } lines.push(''); 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 queryEntityWeights = extractEntitiesWithWeights(segments, weights, lexicon); const queryEntities = Array.from(queryEntityWeights.keys()); + const facts = getFacts(store); + const expandedTerms = expandByFacts(queryEntities, facts, 2); // 构建文本查询串:最后一条消息 + 实体 + 关键词 const lastSeg = segments[segments.length - 1] || ''; const queryTextForSearch = [ lastSeg, ...queryEntities, + ...expandedTerms, ...(store?.json?.keywords || []).slice(0, 5).map(k => k.text), ].join(' '); @@ -824,6 +902,7 @@ export async function recallMemory(queryText, allEvents, vectorConfig, options = chunkPreFilterStats, l0Results, textGapInfo, + expandedTerms, }); console.group('%c[Recall]', 'color: #7c3aed; font-weight: bold');