story-summary: facts migration + recall enhancements

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

View File

@@ -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, '<div class="empty">暂无世界状态</div>');
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 `
<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>');
if (!activeFacts.length) {
setHtml(container, '<div class="empty">暂无事实记录</div>');
return;
}
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);
}
})();