story-summary: facts migration + recall enhancements
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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 为键)。
|
||||
</task_settings>
|
||||
---
|
||||
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,字符串值内部避免英文双引号
|
||||
- 用朴实、白描、有烟火气的笔触记录,避免比喻和意象
|
||||
</meta_protocol>`,
|
||||
@@ -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 <meta_protocol>
|
||||
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,
|
||||
|
||||
@@ -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 || "") };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user