fix: 缩短事件节点图谱标签

This commit is contained in:
Youzini-afk
2026-03-25 00:37:44 +08:00
parent 63ee782028
commit 4319fd2496
6 changed files with 86 additions and 41 deletions

View File

@@ -15,6 +15,10 @@ import {
updateNode,
} from "./graph.js";
import { callLLMForJSON } from "./llm.js";
import {
ensureEventTitle,
getNodeDisplayName,
} from "./node-labels.js";
import { RELATION_TYPES } from "./schema.js";
import {
buildNodeVectorText,
@@ -206,6 +210,8 @@ export async function extractMemories({
* 处理 create 操作
*/
function handleCreate(graph, op, seq, schema, refMap, stats) {
const normalizedFields =
op.type === "event" ? ensureEventTitle(op.fields || {}) : (op.fields || {});
const typeDef = schema.find((s) => s.id === op.type);
if (!typeDef) {
console.warn(`[ST-BME] 未知节点类型: ${op.type}`);
@@ -233,7 +239,7 @@ function handleCreate(graph, op, seq, schema, refMap, stats) {
// 创建新节点
const node = createNode({
type: op.type,
fields: op.fields || {},
fields: normalizedFields,
seq,
importance: op.importance ?? 5.0,
clusters: op.clusters || [],
@@ -271,7 +277,10 @@ function handleUpdate(graph, op, currentSeq, stats) {
}
const previousFields = { ...(previousNode.fields || {}) };
const nextFields = { ...previousFields, ...(op.fields || {}) };
const nextFields =
previousNode.type === "event"
? ensureEventTitle({ ...previousFields, ...(op.fields || {}) })
: { ...previousFields, ...(op.fields || {}) };
const changeSummary = buildFieldChangeSummary(previousFields, nextFields);
const updateSeq = Number.isFinite(op.seq) ? op.seq : currentSeq;
@@ -323,6 +332,7 @@ function handleUpdate(graph, op, currentSeq, stats) {
const updateEventNode = createNode({
type: "event",
fields: {
title: `${previousNode.fields?.name || previousNode.fields?.title || previousNode.type} 状态更新`,
summary: `${previousNode.type} 状态更新:${changeSummary}`,
participants:
previousNode.fields?.name ||
@@ -462,9 +472,7 @@ function buildGraphOverview(graph, schema) {
lines.push(`### ${typeDef.label} (${nodesOfType.length} 个节点)`);
for (const node of nodesOfType.slice(-10)) {
// 只展示最近 10 个
const summary =
node.fields.summary || node.fields.name || node.fields.title || "(无)";
lines.push(` - [${node.id}] ${summary}`);
lines.push(` - [${node.id}] ${getNodeDisplayName(node)}`);
}
}
@@ -503,7 +511,7 @@ function buildDefaultExtractPrompt(schema) {
" {",
' "action": "create",',
' "type": "event",',
' "fields": {"summary": "...", "participants": "...", "status": "ongoing"},',
' "fields": {"title": "简短事件名", "summary": "...", "participants": "...", "status": "ongoing"},',
' "importance": 6,',
' "ref": "evt1",',
' "links": [',
@@ -528,6 +536,7 @@ function buildDefaultExtractPrompt(schema) {
"- temporal_update 关系用于实体状态的时序变化",
"- 不要虚构内容,只提取对话中有证据支持的信息",
"- importance 范围 1-10普通事件 5关键转折 8+",
"- event.fields.title 需要是简短事件名,建议 6-18 字,只用于图谱和列表显示",
"- summary 应该是摘要抽象,不要复制原文",
].join("\n");
}

View File

@@ -2,6 +2,7 @@
// 零依赖,纯 Canvas 2D 实现
import { getNodeColors } from './themes.js';
import { getGraphNodeLabel, getNodeDisplayName } from './node-labels.js';
/**
* @typedef {Object} GraphNode
@@ -90,6 +91,7 @@ export class GraphRenderer {
id: n.id,
type: n.type || 'event',
name: getNodeDisplayName(n),
label: getGraphNodeLabel(n),
importance: n.importance || 5,
x: viewportWidth / 2 + r * Math.cos(angle) + (Math.random() - 0.5) * 40,
y: viewportHeight / 2 + r * Math.sin(angle) + (Math.random() - 0.5) * 40,
@@ -252,7 +254,7 @@ export class GraphRenderer {
ctx.fillStyle = `rgba(255,255,255,${isHovered || isSelected ? 0.95 : 0.65})`;
ctx.font = `${FORCE_CONFIG.labelFontSize}px Inter, sans-serif`;
ctx.textAlign = 'center';
ctx.fillText(node.name, node.x, node.y + r + 14);
ctx.fillText(node.label || node.name, node.x, node.y + r + 14);
}
ctx.restore();
@@ -462,14 +464,3 @@ export class GraphRenderer {
this._resizeObserver?.disconnect();
}
}
function getNodeDisplayName(node) {
return (
node?.fields?.name ||
node?.fields?.title ||
node?.fields?.summary ||
node?.fields?.insight ||
node?.id?.slice(0, 8) ||
'—'
);
}

View File

@@ -31,6 +31,7 @@ import {
} from "./graph.js";
import { estimateTokens, formatInjection } from "./injector.js";
import { fetchMemoryLLMModels, testLLMConnection } from "./llm.js";
import { getNodeDisplayName } from "./node-labels.js";
import { retrieve } from "./retriever.js";
import {
appendBatchJournal,
@@ -190,17 +191,6 @@ function createUiStatus(text = "待命", meta = "", level = "idle") {
};
}
function getNodeDisplayName(node) {
return (
node?.fields?.name ||
node?.fields?.title ||
node?.fields?.summary ||
node?.fields?.insight ||
node?.id ||
"—"
);
}
function toPanelNodeItem(node, meta = "") {
return {
id: node.id,

63
node-labels.js Normal file
View File

@@ -0,0 +1,63 @@
const DEFAULT_GRAPH_LABEL_LENGTH = 14;
const GRAPH_LABEL_LENGTH_BY_TYPE = {
character: 12,
event: 14,
location: 12,
thread: 14,
rule: 14,
synopsis: 16,
reflection: 14,
};
function normalizeLabelText(value) {
return String(value ?? "")
.replace(/\s+/g, " ")
.trim();
}
export function truncateNodeLabel(text, maxLength = DEFAULT_GRAPH_LABEL_LENGTH) {
const normalized = normalizeLabelText(text);
if (!normalized) return "—";
if (!Number.isFinite(maxLength) || maxLength < 2) return normalized;
if (normalized.length <= maxLength) return normalized;
return `${normalized.slice(0, Math.max(1, maxLength - 1)).trimEnd()}`;
}
export function deriveEventTitleFromSummary(summary, maxLength = 18) {
const normalized = normalizeLabelText(summary).replace(/^事件[:]\s*/, "");
if (!normalized) return "";
const clause =
normalized.split(/[\r\n]+/, 1)[0]?.split(/[。!?!?]/, 1)[0]?.split(/[;]/, 1)[0]?.split(/[,]/, 1)[0] ||
normalized;
return truncateNodeLabel(clause || normalized, maxLength);
}
export function ensureEventTitle(fields = {}) {
const nextFields = { ...(fields || {}) };
if (!nextFields.title && nextFields.summary) {
nextFields.title = deriveEventTitleFromSummary(nextFields.summary);
}
return nextFields;
}
export function getNodeDisplayName(node) {
const label = normalizeLabelText(
node?.fields?.name ||
node?.fields?.title ||
node?.fields?.summary ||
node?.fields?.insight ||
node?.name ||
node?.id?.slice(0, 8) ||
"—",
);
return label || "—";
}
export function getGraphNodeLabel(node) {
const maxLength =
GRAPH_LABEL_LENGTH_BY_TYPE[node?.type] || DEFAULT_GRAPH_LABEL_LENGTH;
return truncateNodeLabel(getNodeDisplayName(node), maxLength);
}

View File

@@ -2,6 +2,7 @@
import { renderTemplateAsync } from "../../../templates.js";
import { GraphRenderer } from "./graph-renderer.js";
import { getNodeDisplayName } from "./node-labels.js";
import { getNodeColors } from "./themes.js";
import {
getSuggestedBackendModel,
@@ -21,7 +22,7 @@ const DEFAULT_PROMPTS = {
" {",
' "action": "create",',
' "type": "event",',
' "fields": {"summary": "...", "participants": "...", "status": "ongoing"},',
' "fields": {"title": "简短事件名", "summary": "...", "participants": "...", "status": "ongoing"},',
' "importance": 6,',
' "ref": "evt1",',
' "links": [',
@@ -41,6 +42,7 @@ const DEFAULT_PROMPTS = {
"- 角色/地点节点:如果图中已有同名节点,用 update 而非 create",
"- 不要虚构内容,只提取对话中有证据支持的信息",
"- importance 范围 1-10普通事件 5关键转折 8+",
"- event.fields.title 需要是简短事件名,建议 6-18 字,只用于图谱和列表显示",
"- summary 应该是摘要抽象,不要复制原文",
].join("\n"),
@@ -1441,17 +1443,6 @@ function _getNodeSnippet(node) {
return "无补充字段";
}
function getNodeDisplayName(node) {
return (
node?.fields?.name ||
node?.fields?.title ||
node?.fields?.summary ||
node?.fields?.insight ||
node?.id?.slice(0, 8) ||
"—"
);
}
function _isMobile() {
return window.innerWidth <= 768;
}

View File

@@ -27,6 +27,7 @@ export const DEFAULT_NODE_SCHEMA = [
label: "事件",
tableName: "event_table",
columns: [
{ name: "title", hint: "简短事件名(建议 6-18 字,用于图谱显示)", required: false },
{ name: "summary", hint: "事件摘要,包含因果关系和结果", required: true },
{ name: "participants", hint: "参与角色名,逗号分隔", required: false },
{