Files
ST-Bionic-Memory-Ecology/graph/graph.js

885 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ST-BME: 图数据模型
// 管理节点、边的 CRUD 操作,以及序列化到 chat_metadata
import {
createDefaultBatchJournal,
createDefaultHistoryState,
createDefaultMaintenanceJournal,
createDefaultVectorIndexState,
normalizeGraphRuntimeState,
PROCESSED_MESSAGE_HASH_VERSION,
} from "../runtime/runtime-state.js";
import {
hasSameScopeIdentity,
normalizeEdgeMemoryScope,
normalizeMemoryScope,
normalizeNodeMemoryScope,
isSameLatestScopeBucket,
} from "./memory-scope.js";
import {
createDefaultKnowledgeState,
createDefaultRegionState,
} from "./knowledge-state.js";
import {
createDefaultStoryTime,
createDefaultStoryTimeSpan,
createDefaultTimelineState,
normalizeGraphStoryTimeline,
normalizeNodeStoryTimeline,
normalizeStoryTime,
normalizeStoryTimeSpan,
} from "./story-timeline.js";
import {
createDefaultSummaryState,
importLegacySynopsisToSummaryState,
normalizeGraphSummaryState,
} from "./summary-state.js";
import { debugLog } from "../runtime/debug-logging.js";
/**
* 图状态版本号
*/
const GRAPH_VERSION = 9;
/**
* 生成 UUID v4
*/
function uuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* 创建空的图状态
* @returns {GraphState}
*/
export function createEmptyGraph() {
return normalizeGraphRuntimeState({
version: GRAPH_VERSION,
lastProcessedSeq: -1,
nodes: [],
edges: [],
lastRecallResult: null,
historyState: createDefaultHistoryState(),
vectorIndexState: createDefaultVectorIndexState(),
batchJournal: createDefaultBatchJournal(),
maintenanceJournal: createDefaultMaintenanceJournal(),
knowledgeState: createDefaultKnowledgeState(),
regionState: createDefaultRegionState(),
timelineState: createDefaultTimelineState(),
summaryState: createDefaultSummaryState(),
});
}
// ==================== 节点操作 ====================
/**
* 创建新节点
* @param {object} params
* @returns {object} 新节点
*/
export function createNode({
type,
fields = {},
seq = 0,
seqRange = null,
importance = 5.0,
clusters = [],
scope = undefined,
}) {
const now = Date.now();
return {
id: uuid(),
type,
level: 0,
parentId: null,
childIds: [],
seq,
seqRange: seqRange || [seq, seq],
archived: false,
fields,
embedding: null,
importance: Math.max(0, Math.min(10, importance)),
accessCount: 0,
updatedAt: now,
lastAccessTime: now,
createdTime: now,
prevId: null,
nextId: null,
clusters,
scope: normalizeMemoryScope(scope),
storyTime: createDefaultStoryTime(),
storyTimeSpan: createDefaultStoryTimeSpan(),
};
}
/**
* 在图中添加节点
* @param {GraphState} graph
* @param {object} node
* @returns {object} 添加的节点
*/
export function addNode(graph, node) {
// 同类型节点的时间链表:连接到最后一个同类型节点
const sameTypeNodes = graph.nodes
.filter(
(n) =>
n.type === node.type &&
!n.archived &&
n.level === 0 &&
hasSameScopeIdentity(n.scope, node.scope),
)
.sort((a, b) => a.seq - b.seq);
if (sameTypeNodes.length > 0) {
const lastNode = sameTypeNodes[sameTypeNodes.length - 1];
lastNode.nextId = node.id;
node.prevId = lastNode.id;
}
graph.nodes.push(node);
return node;
}
/**
* 根据 ID 获取节点
* @param {GraphState} graph
* @param {string} nodeId
* @returns {object|null}
*/
export function getNode(graph, nodeId) {
return graph.nodes.find((n) => n.id === nodeId) || null;
}
/**
* 更新节点字段(部分更新)
* @param {GraphState} graph
* @param {string} nodeId
* @param {object} updates - 要更新的字段
* @returns {boolean} 是否找到并更新
*/
export function updateNode(graph, nodeId, updates) {
const node = getNode(graph, nodeId);
if (!node) return false;
const nextUpdatedAt = Number.isFinite(Number(updates?.updatedAt))
? Number(updates.updatedAt)
: Date.now();
if (updates.fields) {
node.fields = { ...node.fields, ...updates.fields };
delete updates.fields;
}
if (Object.prototype.hasOwnProperty.call(updates, "scope")) {
node.scope = normalizeMemoryScope(updates.scope, node.scope || {});
delete updates.scope;
}
if (Object.prototype.hasOwnProperty.call(updates, "storyTime")) {
node.storyTime = normalizeStoryTime(updates.storyTime, node.storyTime || {});
delete updates.storyTime;
}
if (Object.prototype.hasOwnProperty.call(updates, "storyTimeSpan")) {
node.storyTimeSpan = normalizeStoryTimeSpan(
updates.storyTimeSpan,
node.storyTimeSpan || {},
);
delete updates.storyTimeSpan;
}
Object.assign(node, updates);
node.updatedAt = nextUpdatedAt;
return true;
}
/**
* 删除节点及其相关边
* @param {GraphState} graph
* @param {string} nodeId
* @returns {boolean}
*/
export function removeNode(graph, nodeId, visited = new Set()) {
const normalizedNodeId = String(nodeId || "");
if (!normalizedNodeId) return false;
if (visited.has(normalizedNodeId)) return false;
visited.add(normalizedNodeId);
const node = getNode(graph, normalizedNodeId);
if (!node) return false;
// 修复时间链表
if (node.prevId) {
const prev = getNode(graph, node.prevId);
if (prev) prev.nextId = node.nextId;
}
if (node.nextId) {
const next = getNode(graph, node.nextId);
if (next) next.prevId = node.prevId;
}
// 递归删除子节点(带环保护)
for (const childId of node.childIds) {
removeNode(graph, childId, visited);
}
// 从父节点中移除引用
if (node.parentId) {
const parent = getNode(graph, node.parentId);
if (parent) {
parent.childIds = parent.childIds.filter((id) => id !== normalizedNodeId);
}
}
// 同时清理其它节点上可能残留的脏 child 引用,避免导入脏图残留环
for (const candidate of graph.nodes) {
if (
!Array.isArray(candidate?.childIds) ||
candidate.id === normalizedNodeId
) {
continue;
}
candidate.childIds = candidate.childIds.filter(
(id) => id !== normalizedNodeId,
);
}
// 删除相关边
graph.edges = graph.edges.filter(
(e) => e.fromId !== normalizedNodeId && e.toId !== normalizedNodeId,
);
// 删除节点本身
graph.nodes = graph.nodes.filter((n) => n.id !== normalizedNodeId);
return true;
}
/**
* 获取所有未归档的节点
* @param {GraphState} graph
* @param {string} [typeFilter] - 可选类型过滤
* @returns {object[]}
*/
export function getActiveNodes(graph, typeFilter = null) {
let nodes = graph.nodes.filter((n) => !n.archived);
if (typeFilter) {
nodes = nodes.filter((n) => n.type === typeFilter);
}
return nodes;
}
/**
* 按类型查找最新版本的节点(用于 latestOnly 类型)
* @param {GraphState} graph
* @param {string} type
* @param {string} primaryKeyValue - 主键值(如角色名)
* @param {string} primaryKeyField - 主键字段名(默认 'name'
* @returns {object|null}
*/
export function findLatestNode(
graph,
type,
primaryKeyValue,
primaryKeyField = "name",
scope = undefined,
) {
const candidates = graph.nodes.filter(
(n) =>
n.type === type &&
!n.archived &&
n.fields[primaryKeyField] === primaryKeyValue &&
(scope == null ||
isSameLatestScopeBucket(n, {
type,
primaryKeyValue,
primaryKeyField,
scope,
})),
);
if (candidates.length === 0) return null;
return candidates.sort((a, b) => b.seq - a.seq)[0];
}
// ==================== 边操作 ====================
/**
* 创建边
* @param {object} params
* @returns {object} 新边
*/
export function createEdge({
fromId,
toId,
relation = "related",
strength = 0.8,
edgeType = 0,
scope = undefined,
}) {
const now = Date.now();
return {
id: uuid(),
fromId,
toId,
relation,
strength: Math.max(0, Math.min(1, strength)),
edgeType,
createdTime: now,
updatedAt: now,
// Graphiti 启发的时序字段
validAt: now, // 关系生效时间
invalidAt: null, // 关系失效时间null = 当前有效)
expiredAt: null, // 系统标记过期时间
scope: normalizeMemoryScope(scope),
};
}
/**
* 在图中添加边(检查节点存在性)
* @param {GraphState} graph
* @param {object} edge
* @returns {object|null} 添加的边或 null
*/
export function addEdge(graph, edge) {
const from = getNode(graph, edge.fromId);
const to = getNode(graph, edge.toId);
if (!from || !to) return null;
if (edge.fromId === edge.toId) return null;
const isCurrentEdgeValid = (candidate) => {
if (candidate.invalidAt) return false;
if (candidate.expiredAt) return false;
return true;
};
// 对当前有效边去重;历史边保留,避免历史污染当前检索
const existing = graph.edges.find(
(e) =>
e.fromId === edge.fromId &&
e.toId === edge.toId &&
e.relation === edge.relation &&
JSON.stringify(normalizeMemoryScope(e.scope)) ===
JSON.stringify(normalizeMemoryScope(edge.scope)) &&
isCurrentEdgeValid(e),
);
if (existing) {
existing.strength = Math.max(existing.strength, edge.strength ?? 0);
existing.validAt = Math.max(
existing.validAt || 0,
edge.validAt || Date.now(),
);
existing.updatedAt = Math.max(
Number(existing.updatedAt || 0),
Number(edge.updatedAt || 0),
Number(existing.validAt || 0),
);
if (edge.invalidAt) {
existing.invalidAt = edge.invalidAt;
existing.updatedAt = Math.max(
Number(existing.updatedAt || 0),
Number(existing.invalidAt || 0),
);
}
if (edge.expiredAt) {
existing.expiredAt = edge.expiredAt;
existing.updatedAt = Math.max(
Number(existing.updatedAt || 0),
Number(existing.expiredAt || 0),
);
}
return existing;
}
if (!Number.isFinite(Number(edge.updatedAt))) {
edge.updatedAt = Math.max(
Number(edge.validAt || 0),
Number(edge.createdTime || Date.now()),
);
}
graph.edges.push(edge);
return edge;
}
/**
* 移除边
* @param {GraphState} graph
* @param {string} edgeId
* @returns {boolean}
*/
export function removeEdge(graph, edgeId) {
const idx = graph.edges.findIndex((e) => e.id === edgeId);
if (idx === -1) return false;
graph.edges.splice(idx, 1);
return true;
}
/**
* 获取节点的所有出边
* @param {GraphState} graph
* @param {string} nodeId
* @returns {object[]}
*/
export function getOutEdges(graph, nodeId) {
return graph.edges.filter((e) => e.fromId === nodeId);
}
/**
* 获取节点的所有入边
* @param {GraphState} graph
* @param {string} nodeId
* @returns {object[]}
*/
export function getInEdges(graph, nodeId) {
return graph.edges.filter((e) => e.toId === nodeId);
}
/**
* 获取连接到节点的所有边(入+出)
* @param {GraphState} graph
* @param {string} nodeId
* @returns {object[]}
*/
export function getNodeEdges(graph, nodeId) {
return graph.edges.filter((e) => e.fromId === nodeId || e.toId === nodeId);
}
// ==================== 查询辅助 ====================
/**
* 构建邻接表(用于扩散引擎)
* @param {GraphState} graph
* @returns {Map<string, Array<{targetId: string, strength: number, edgeType: number}>>}
*/
export function buildAdjacencyMap(graph) {
const adj = new Map();
const activeNodeIds = new Set(
graph.nodes.filter((node) => !node.archived).map((node) => node.id),
);
for (const edge of graph.edges) {
if (!isEdgeActive(edge)) continue;
if (!activeNodeIds.has(edge.fromId) || !activeNodeIds.has(edge.toId)) {
continue;
}
if (!adj.has(edge.fromId)) adj.set(edge.fromId, []);
adj.get(edge.fromId).push({
targetId: edge.toId,
strength: edge.strength,
edgeType: edge.edgeType,
});
if (!adj.has(edge.toId)) adj.set(edge.toId, []);
adj.get(edge.toId).push({
targetId: edge.fromId,
strength: edge.strength,
edgeType: edge.edgeType,
});
}
return adj;
}
/**
* 构建时序感知邻接表(过滤失效边)
* Graphiti 启发:只纳入 "当前有效" 的边
* @param {GraphState} graph
* @returns {Map}
*/
export function buildTemporalAdjacencyMap(graph, options = {}) {
const adj = new Map();
adj.syntheticEdgeCount = 0;
const activeNodeIds = new Set(
graph.nodes.filter((node) => !node.archived).map((node) => node.id),
);
const includeTemporalLinks = options.includeTemporalLinks !== false;
const temporalLinkStrength = Math.max(
0,
Math.min(1, Number(options.temporalLinkStrength) || 0.2),
);
for (const edge of graph.edges) {
if (!isEdgeActive(edge)) continue;
if (!activeNodeIds.has(edge.fromId) || !activeNodeIds.has(edge.toId)) {
continue;
}
addAdjacencyPair(adj, edge.fromId, edge.toId, edge.strength, edge.edgeType);
}
if (includeTemporalLinks && temporalLinkStrength > 0) {
const activeNodes = graph.nodes.filter(
(node) => !node.archived && activeNodeIds.has(node.id),
);
const seenPairs = new Set();
for (const node of activeNodes) {
for (const neighborId of [node.prevId, node.nextId]) {
if (!neighborId || !activeNodeIds.has(neighborId)) continue;
const key = [node.id, neighborId].sort().join("::");
if (seenPairs.has(key)) continue;
seenPairs.add(key);
addAdjacencyPair(adj, node.id, neighborId, temporalLinkStrength, 0);
adj.syntheticEdgeCount += 1;
}
}
}
return adj;
}
function addAdjacencyPair(adj, fromId, toId, strength, edgeType) {
if (!adj.has(fromId)) adj.set(fromId, []);
adj.get(fromId).push({
targetId: toId,
strength,
edgeType,
});
if (!adj.has(toId)) adj.set(toId, []);
adj.get(toId).push({
targetId: fromId,
strength,
edgeType,
});
}
function isEdgeActive(edge, now = Date.now()) {
if (!edge) return false;
if (edge.invalidAt && edge.invalidAt <= now) return false;
if (edge.expiredAt && edge.expiredAt <= now) return false;
return true;
}
/**
* 将边标记为失效(不删除,保留历史)
* @param {object} edge
*/
export function invalidateEdge(edge) {
if (!edge) return;
const now = Date.now();
if (!edge.invalidAt) {
edge.invalidAt = now;
}
edge.updatedAt = Math.max(
Number(edge.updatedAt || 0),
Number(edge.invalidAt || now),
);
}
/**
* 获取图的统计信息
* @param {GraphState} graph
* @returns {object}
*/
export function getGraphStats(graph) {
const activeNodes = graph.nodes.filter((n) => !n.archived);
const archivedNodes = graph.nodes.filter((n) => n.archived);
const typeCounts = {};
for (const node of activeNodes) {
typeCounts[node.type] = (typeCounts[node.type] || 0) + 1;
}
return {
totalNodes: graph.nodes.length,
activeNodes: activeNodes.length,
archivedNodes: archivedNodes.length,
totalEdges: graph.edges.length,
lastProcessedSeq: graph.lastProcessedSeq,
typeCounts,
};
}
// ==================== 序列化 ====================
/**
* 序列化图状态为 JSON 字符串
* @param {GraphState} graph
* @returns {string}
*/
export function serializeGraph(graph) {
return JSON.stringify(graph);
}
/**
* 从 JSON 反序列化图状态
* @param {string} json
* @returns {GraphState}
*/
export function deserializeGraph(json) {
try {
const data = typeof json === "string" ? JSON.parse(json) : json;
const shouldImportLegacySynopsis =
!data?.summaryState ||
typeof data.summaryState !== "object" ||
Array.isArray(data.summaryState);
if (!data || data.version === undefined) {
return createEmptyGraph();
}
if (data.version < GRAPH_VERSION) {
debugLog(`[ST-BME] 图版本迁移 v${data.version} → v${GRAPH_VERSION}`);
if (data.version < 2 && data.edges) {
for (const edge of data.edges) {
if (edge.validAt === undefined)
edge.validAt = edge.createdTime || Date.now();
if (edge.invalidAt === undefined) edge.invalidAt = null;
if (edge.expiredAt === undefined) edge.expiredAt = null;
}
}
if (data.version < 3) {
if (typeof data.lastProcessedSeq !== "number") {
data.lastProcessedSeq = -1;
}
for (const node of data.nodes || []) {
if (!Array.isArray(node.seqRange)) {
const seq = Number.isFinite(node.seq) ? node.seq : 0;
node.seqRange = [seq, seq];
}
}
}
if (data.version < 4) {
data.historyState = {
...createDefaultHistoryState(),
...(data.historyState || {}),
lastProcessedAssistantFloor: Number.isFinite(data.lastProcessedSeq)
? data.lastProcessedSeq
: -1,
};
data.vectorIndexState = {
...createDefaultVectorIndexState(),
...(data.vectorIndexState || {}),
dirty: true,
lastWarning: "旧版本图谱已迁移,需要重建向量运行时状态",
};
data.batchJournal = Array.isArray(data.batchJournal)
? data.batchJournal
: createDefaultBatchJournal();
}
if (data.version < 5) {
data.historyState = {
...createDefaultHistoryState(),
...(data.historyState || {}),
extractionCount: Number.isFinite(data?.historyState?.extractionCount)
? data.historyState.extractionCount
: 0,
lastMutationSource: String(
data?.historyState?.lastMutationSource || "",
),
};
data.batchJournal = Array.isArray(data.batchJournal)
? data.batchJournal
: createDefaultBatchJournal();
}
if (data.version < 6) {
for (const node of data.nodes || []) {
node.scope = normalizeMemoryScope(node?.scope);
}
for (const edge of data.edges || []) {
edge.scope = normalizeMemoryScope(edge?.scope);
}
}
if (data.version < 7) {
data.historyState = {
...createDefaultHistoryState(),
...(data.historyState || {}),
activeRegionSource: String(
data?.historyState?.activeRegionSource ||
(data?.historyState?.activeRegion ? "history" : ""),
),
activeRecallOwnerKey: String(
data?.historyState?.activeRecallOwnerKey || "",
),
recentRecallOwnerKeys: Array.isArray(
data?.historyState?.recentRecallOwnerKeys,
)
? data.historyState.recentRecallOwnerKeys
: [],
};
data.maintenanceJournal = Array.isArray(data.maintenanceJournal)
? data.maintenanceJournal
: createDefaultMaintenanceJournal();
data.knowledgeState = createDefaultKnowledgeState(data.knowledgeState);
data.regionState = createDefaultRegionState(data.regionState);
}
if (data.version < 8) {
data.historyState = {
...createDefaultHistoryState(),
...(data.historyState || {}),
activeStorySegmentId: String(
data?.historyState?.activeStorySegmentId || "",
),
activeStoryTimeLabel: String(
data?.historyState?.activeStoryTimeLabel || "",
),
activeStoryTimeSource: String(
data?.historyState?.activeStoryTimeSource ||
(data?.historyState?.activeStorySegmentId ||
data?.historyState?.activeStoryTimeLabel
? "history"
: ""),
),
lastExtractedStorySegmentId: String(
data?.historyState?.lastExtractedStorySegmentId || "",
),
};
data.timelineState = createDefaultTimelineState(data.timelineState);
for (const node of data.nodes || []) {
normalizeNodeStoryTimeline(node);
}
}
if (data.version < 9) {
data.summaryState = createDefaultSummaryState(data.summaryState);
}
data.version = GRAPH_VERSION;
}
data.nodes = (data.nodes || []).map((node) => {
const seq = Number.isFinite(node.seq) ? node.seq : 0;
return {
level: 0,
parentId: null,
childIds: [],
accessCount: 0,
lastAccessTime: node.createdTime || Date.now(),
prevId: null,
nextId: null,
clusters: [],
...node,
seq,
seqRange: Array.isArray(node.seqRange) ? node.seqRange : [seq, seq],
scope: normalizeNodeMemoryScope(node),
storyTime: createDefaultStoryTime(node?.storyTime || {}),
storyTimeSpan: createDefaultStoryTimeSpan(node?.storyTimeSpan || {}),
};
});
data.edges = (data.edges || []).map((edge) => {
const normalizedEdge = {
createdTime: Date.now(),
validAt: edge?.createdTime || Date.now(),
invalidAt: null,
expiredAt: null,
...edge,
};
normalizedEdge.scope = normalizeEdgeMemoryScope(normalizedEdge);
return normalizedEdge;
});
data.lastProcessedSeq = Number.isFinite(data.lastProcessedSeq)
? data.lastProcessedSeq
: -1;
data.lastRecallResult = Array.isArray(data.lastRecallResult)
? data.lastRecallResult
: null;
data.historyState = {
...createDefaultHistoryState(),
...(data.historyState || {}),
lastProcessedAssistantFloor: Number.isFinite(
data?.historyState?.lastProcessedAssistantFloor,
)
? data.historyState.lastProcessedAssistantFloor
: data.lastProcessedSeq,
extractionCount: Number.isFinite(data?.historyState?.extractionCount)
? data.historyState.extractionCount
: 0,
lastMutationSource: String(data?.historyState?.lastMutationSource || ""),
};
data.vectorIndexState = {
...createDefaultVectorIndexState(data?.historyState?.chatId || ""),
...(data.vectorIndexState || {}),
};
data.batchJournal = Array.isArray(data.batchJournal)
? data.batchJournal
: createDefaultBatchJournal();
data.maintenanceJournal = Array.isArray(data.maintenanceJournal)
? data.maintenanceJournal
: createDefaultMaintenanceJournal();
data.knowledgeState = createDefaultKnowledgeState(data.knowledgeState);
data.regionState = createDefaultRegionState(data.regionState);
data.timelineState = createDefaultTimelineState(data.timelineState);
data.summaryState = createDefaultSummaryState(data.summaryState);
normalizeGraphStoryTimeline(data);
const normalizedGraph = normalizeGraphRuntimeState(
data,
data?.historyState?.chatId || "",
);
normalizeGraphSummaryState(normalizedGraph);
if (shouldImportLegacySynopsis) {
importLegacySynopsisToSummaryState(normalizedGraph);
}
return normalizedGraph;
} catch (e) {
console.error("[ST-BME] 图反序列化失败:", e);
return createEmptyGraph();
}
}
/**
* 导出图数据(不含 embedding 以减小体积)
* @param {GraphState} graph
* @returns {string} JSON 字符串
*/
export function exportGraph(graph) {
const exportData = {
...graph,
historyState: {
...createDefaultHistoryState(graph?.historyState?.chatId || ""),
...(graph?.historyState || {}),
lastProcessedAssistantFloor:
graph?.historyState?.lastProcessedAssistantFloor ??
graph?.lastProcessedSeq ??
-1,
},
vectorIndexState: {
...createDefaultVectorIndexState(graph?.historyState?.chatId || ""),
dirty: true,
lastWarning: "导出图谱不包含运行时向量索引",
},
batchJournal: createDefaultBatchJournal(),
maintenanceJournal: createDefaultMaintenanceJournal(),
knowledgeState: createDefaultKnowledgeState(graph?.knowledgeState || {}),
regionState: createDefaultRegionState(graph?.regionState || {}),
timelineState: createDefaultTimelineState(graph?.timelineState || {}),
summaryState: createDefaultSummaryState(graph?.summaryState || {}),
nodes: graph.nodes.map((n) => ({ ...n, embedding: null })),
};
return JSON.stringify(exportData, null, 2);
}
/**
* 导入图数据
* @param {string} json
* @returns {GraphState}
*/
export function importGraph(json) {
const graph = normalizeGraphRuntimeState(deserializeGraph(json));
// 导入的节点需要重新生成 embedding
for (const node of graph.nodes) {
node.embedding = null;
}
graph.batchJournal = createDefaultBatchJournal();
graph.historyState.processedMessageHashVersion =
PROCESSED_MESSAGE_HASH_VERSION;
graph.historyState.processedMessageHashes = {};
graph.historyState.processedMessageHashesNeedRefresh = true;
graph.historyState.historyDirtyFrom = null;
graph.vectorIndexState.hashToNodeId = {};
graph.vectorIndexState.nodeToHash = {};
graph.vectorIndexState.dirty = true;
graph.vectorIndexState.lastWarning = "导入图谱后需要重建向量索引";
return graph;
}