mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
fix: 缩短事件节点图谱标签
This commit is contained in:
21
extractor.js
21
extractor.js
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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) ||
|
||||
'—'
|
||||
);
|
||||
}
|
||||
|
||||
12
index.js
12
index.js
@@ -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
63
node-labels.js
Normal 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);
|
||||
}
|
||||
15
panel.js
15
panel.js
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user