mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
feat: update smart trigger and graph retrieval
This commit is contained in:
28
diffusion.js
28
diffusion.js
@@ -60,14 +60,15 @@ const DEFAULT_OPTIONS = {
|
|||||||
export function propagateActivation(adjacencyMap, seedNodes, options = {}) {
|
export function propagateActivation(adjacencyMap, seedNodes, options = {}) {
|
||||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||||
|
|
||||||
// Step 0: 初始化能量表
|
|
||||||
/** @type {Map<string, number>} */
|
/** @type {Map<string, number>} */
|
||||||
let currentEnergy = new Map();
|
let currentEnergy = new Map();
|
||||||
|
|
||||||
for (const seed of seedNodes) {
|
for (const seed of seedNodes || []) {
|
||||||
const clamped = clampEnergy(seed.energy, opts);
|
if (!seed?.id) continue;
|
||||||
|
const clamped = clampEnergy(Number(seed.energy) || 0, opts);
|
||||||
if (Math.abs(clamped) >= opts.minEnergy) {
|
if (Math.abs(clamped) >= opts.minEnergy) {
|
||||||
currentEnergy.set(seed.id, clamped);
|
const existing = currentEnergy.get(seed.id) || 0;
|
||||||
|
currentEnergy.set(seed.id, clampEnergy(existing + clamped, opts));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,11 +84,12 @@ export function propagateActivation(adjacencyMap, seedNodes, options = {}) {
|
|||||||
// 对每个当前活跃节点,传播能量到邻居
|
// 对每个当前活跃节点,传播能量到邻居
|
||||||
for (const [nodeId, energy] of currentEnergy) {
|
for (const [nodeId, energy] of currentEnergy) {
|
||||||
const neighbors = adjacencyMap.get(nodeId);
|
const neighbors = adjacencyMap.get(nodeId);
|
||||||
if (!neighbors) continue;
|
if (!Array.isArray(neighbors) || neighbors.length === 0) continue;
|
||||||
|
|
||||||
for (const neighbor of neighbors) {
|
for (const neighbor of neighbors) {
|
||||||
// 计算传播能量
|
if (!neighbor?.targetId) continue;
|
||||||
let propagated = energy * neighbor.strength * opts.decayFactor;
|
let propagated =
|
||||||
|
energy * (Number(neighbor.strength) || 0) * opts.decayFactor;
|
||||||
|
|
||||||
// 抑制边:传递负能量
|
// 抑制边:传递负能量
|
||||||
if (neighbor.edgeType === INHIBIT_EDGE_TYPE) {
|
if (neighbor.edgeType === INHIBIT_EDGE_TYPE) {
|
||||||
@@ -112,8 +114,9 @@ export function propagateActivation(adjacencyMap, seedNodes, options = {}) {
|
|||||||
|
|
||||||
// 动态剪枝:只保留 Top-K
|
// 动态剪枝:只保留 Top-K
|
||||||
if (nextEnergy.size > opts.topK) {
|
if (nextEnergy.size > opts.topK) {
|
||||||
const sorted = [...nextEnergy.entries()]
|
const sorted = [...nextEnergy.entries()].sort(
|
||||||
.sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]));
|
(a, b) => Math.abs(b[1]) - Math.abs(a[1]),
|
||||||
|
);
|
||||||
|
|
||||||
nextEnergy.clear();
|
nextEnergy.clear();
|
||||||
for (let i = 0; i < opts.topK && i < sorted.length; i++) {
|
for (let i = 0; i < opts.topK && i < sorted.length; i++) {
|
||||||
@@ -161,7 +164,10 @@ export function diffuseAndRank(adjacencyMap, seeds, options = {}) {
|
|||||||
const energyMap = propagateActivation(adjacencyMap, seeds, options);
|
const energyMap = propagateActivation(adjacencyMap, seeds, options);
|
||||||
|
|
||||||
return [...energyMap.entries()]
|
return [...energyMap.entries()]
|
||||||
.filter(([_, energy]) => energy > 0) // 只返回正能量(被激活的)
|
.filter(([_, energy]) => energy > 0)
|
||||||
.map(([nodeId, energy]) => ({ nodeId, energy }))
|
.map(([nodeId, energy]) => ({ nodeId, energy }))
|
||||||
.sort((a, b) => b.energy - a.energy);
|
.sort((a, b) => {
|
||||||
|
if (b.energy !== a.energy) return b.energy - a.energy;
|
||||||
|
return String(a.nodeId).localeCompare(String(b.nodeId));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
100
extractor.js
100
extractor.js
@@ -22,18 +22,22 @@ import { RELATION_TYPES } from "./schema.js";
|
|||||||
*
|
*
|
||||||
* @param {object} params
|
* @param {object} params
|
||||||
* @param {object} params.graph - 当前图状态
|
* @param {object} params.graph - 当前图状态
|
||||||
* @param {Array<{role: string, content: string}>} params.messages - 要处理的对话消息
|
* @param {Array<{seq?: number, role: string, content: string}>} params.messages - 要处理的对话消息
|
||||||
* @param {number} params.startSeq - 起始楼层号
|
* @param {number} params.startSeq - 本批处理的首个 assistant 消息 chat 索引
|
||||||
|
* @param {number} params.endSeq - 本批处理的末个 assistant 消息 chat 索引
|
||||||
|
* @param {number} [params.lastProcessedSeq] - 上次处理到的 chat 索引
|
||||||
* @param {object[]} params.schema - 节点类型 Schema
|
* @param {object[]} params.schema - 节点类型 Schema
|
||||||
* @param {object} params.embeddingConfig - Embedding API 配置
|
* @param {object} params.embeddingConfig - Embedding API 配置
|
||||||
* @param {string} [params.extractPrompt] - 自定义提取提示词
|
* @param {string} [params.extractPrompt] - 自定义提取提示词
|
||||||
* @param {object} [params.v2Options] - v2 增强选项
|
* @param {object} [params.v2Options] - v2 增强选项
|
||||||
* @returns {Promise<{success: boolean, newNodes: number, updatedNodes: number, newEdges: number, newNodeIds: string[]}>}
|
* @returns {Promise<{success: boolean, newNodes: number, updatedNodes: number, newEdges: number, newNodeIds: string[], processedRange: [number, number]}>}
|
||||||
*/
|
*/
|
||||||
export async function extractMemories({
|
export async function extractMemories({
|
||||||
graph,
|
graph,
|
||||||
messages,
|
messages,
|
||||||
startSeq,
|
startSeq,
|
||||||
|
endSeq,
|
||||||
|
lastProcessedSeq = -1,
|
||||||
schema,
|
schema,
|
||||||
embeddingConfig,
|
embeddingConfig,
|
||||||
extractPrompt,
|
extractPrompt,
|
||||||
@@ -46,17 +50,33 @@ export async function extractMemories({
|
|||||||
updatedNodes: 0,
|
updatedNodes: 0,
|
||||||
newEdges: 0,
|
newEdges: 0,
|
||||||
newNodeIds: [],
|
newNodeIds: [],
|
||||||
|
processedRange: [lastProcessedSeq, lastProcessedSeq],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const enablePreciseConflict = v2Options.enablePreciseConflict ?? true;
|
const enablePreciseConflict = v2Options.enablePreciseConflict ?? true;
|
||||||
const conflictThreshold = v2Options.conflictThreshold ?? 0.85;
|
const conflictThreshold = v2Options.conflictThreshold ?? 0.85;
|
||||||
|
|
||||||
console.log(`[ST-BME] 提取开始: 楼层 ${startSeq}, ${messages.length} 条消息`);
|
const effectiveStartSeq = Number.isFinite(startSeq)
|
||||||
|
? startSeq
|
||||||
|
: (messages.find((m) => Number.isFinite(m.seq))?.seq ??
|
||||||
|
lastProcessedSeq + 1);
|
||||||
|
const effectiveEndSeq = Number.isFinite(endSeq)
|
||||||
|
? endSeq
|
||||||
|
: ([...messages].reverse().find((m) => Number.isFinite(m.seq))?.seq ??
|
||||||
|
effectiveStartSeq);
|
||||||
|
const currentSeq = effectiveEndSeq;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[ST-BME] 提取开始: chat[${effectiveStartSeq}..${effectiveEndSeq}], ${messages.length} 条消息`,
|
||||||
|
);
|
||||||
|
|
||||||
// 构建对话文本
|
// 构建对话文本
|
||||||
const dialogueText = messages
|
const dialogueText = messages
|
||||||
.map((m) => `[${m.role}]: ${m.content}`)
|
.map((m) => {
|
||||||
|
const seqLabel = Number.isFinite(m.seq) ? `#${m.seq}` : "#?";
|
||||||
|
return `${seqLabel} [${m.role}]: ${m.content}`;
|
||||||
|
})
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
|
|
||||||
// 构建当前图概览(让 LLM 知道已有哪些节点,避免重复)
|
// 构建当前图概览(让 LLM 知道已有哪些节点,避免重复)
|
||||||
@@ -89,7 +109,7 @@ export async function extractMemories({
|
|||||||
maxRetries: 2,
|
maxRetries: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result || !result.operations) {
|
if (!result || !Array.isArray(result.operations)) {
|
||||||
console.warn("[ST-BME] 提取 LLM 未返回有效操作");
|
console.warn("[ST-BME] 提取 LLM 未返回有效操作");
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -97,6 +117,7 @@ export async function extractMemories({
|
|||||||
updatedNodes: 0,
|
updatedNodes: 0,
|
||||||
newEdges: 0,
|
newEdges: 0,
|
||||||
newNodeIds: [],
|
newNodeIds: [],
|
||||||
|
processedRange: [lastProcessedSeq, lastProcessedSeq],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +128,7 @@ export async function extractMemories({
|
|||||||
result.operations,
|
result.operations,
|
||||||
embeddingConfig,
|
embeddingConfig,
|
||||||
conflictThreshold,
|
conflictThreshold,
|
||||||
|
effectiveEndSeq,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +144,7 @@ export async function extractMemories({
|
|||||||
const createdId = handleCreate(
|
const createdId = handleCreate(
|
||||||
graph,
|
graph,
|
||||||
op,
|
op,
|
||||||
startSeq,
|
currentSeq,
|
||||||
schema,
|
schema,
|
||||||
refMap,
|
refMap,
|
||||||
stats,
|
stats,
|
||||||
@@ -131,7 +153,7 @@ export async function extractMemories({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "update":
|
case "update":
|
||||||
handleUpdate(graph, op, stats);
|
handleUpdate(graph, op, currentSeq, stats);
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
handleDelete(graph, op, stats);
|
handleDelete(graph, op, stats);
|
||||||
@@ -150,15 +172,22 @@ export async function extractMemories({
|
|||||||
// 为新建节点生成 embedding
|
// 为新建节点生成 embedding
|
||||||
await generateNodeEmbeddings(graph, embeddingConfig);
|
await generateNodeEmbeddings(graph, embeddingConfig);
|
||||||
|
|
||||||
// 更新处理进度
|
// 更新处理进度:统一记录为已处理到的末个 chat 索引
|
||||||
graph.lastProcessedSeq =
|
graph.lastProcessedSeq = Math.max(
|
||||||
startSeq + messages.filter((m) => m.role === "assistant").length;
|
graph.lastProcessedSeq ?? -1,
|
||||||
|
effectiveEndSeq,
|
||||||
console.log(
|
|
||||||
`[ST-BME] 提取完成: 新建 ${stats.newNodes}, 更新 ${stats.updatedNodes}, 新边 ${stats.newEdges}`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return { success: true, ...stats, newNodeIds };
|
console.log(
|
||||||
|
`[ST-BME] 提取完成: 新建 ${stats.newNodes}, 更新 ${stats.updatedNodes}, 新边 ${stats.newEdges}, lastProcessedSeq=${graph.lastProcessedSeq}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
...stats,
|
||||||
|
newNodeIds,
|
||||||
|
processedRange: [effectiveStartSeq, effectiveEndSeq],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,7 +246,7 @@ function handleCreate(graph, op, seq, schema, refMap, stats) {
|
|||||||
/**
|
/**
|
||||||
* 处理 update 操作
|
* 处理 update 操作
|
||||||
*/
|
*/
|
||||||
function handleUpdate(graph, op, stats) {
|
function handleUpdate(graph, op, currentSeq, stats) {
|
||||||
if (!op.nodeId) {
|
if (!op.nodeId) {
|
||||||
console.warn("[ST-BME] update 操作缺少 nodeId");
|
console.warn("[ST-BME] update 操作缺少 nodeId");
|
||||||
return;
|
return;
|
||||||
@@ -233,8 +262,10 @@ function handleUpdate(graph, op, stats) {
|
|||||||
const nextFields = { ...previousFields, ...(op.fields || {}) };
|
const nextFields = { ...previousFields, ...(op.fields || {}) };
|
||||||
const changeSummary = buildFieldChangeSummary(previousFields, nextFields);
|
const changeSummary = buildFieldChangeSummary(previousFields, nextFields);
|
||||||
|
|
||||||
|
const updateSeq = Number.isFinite(op.seq) ? op.seq : currentSeq;
|
||||||
const updated = updateNode(graph, op.nodeId, {
|
const updated = updateNode(graph, op.nodeId, {
|
||||||
fields: op.fields || {},
|
fields: op.fields || {},
|
||||||
|
seq: Math.max(previousNode.seq || 0, updateSeq),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
@@ -242,19 +273,22 @@ function handleUpdate(graph, op, stats) {
|
|||||||
const node = getNode(graph, op.nodeId);
|
const node = getNode(graph, op.nodeId);
|
||||||
if (node) {
|
if (node) {
|
||||||
node.embedding = null;
|
node.embedding = null;
|
||||||
node.seq = Math.max(node.seq || 0, op.seq || 0);
|
node.seq = Math.max(node.seq || 0, updateSeq);
|
||||||
node.seqRange = [
|
node.seqRange = [
|
||||||
Math.min(node.seqRange?.[0] ?? node.seq, op.seq || node.seq),
|
Math.min(node.seqRange?.[0] ?? node.seq, updateSeq),
|
||||||
Math.max(node.seqRange?.[1] ?? node.seq, op.seq || node.seq),
|
Math.max(node.seqRange?.[1] ?? node.seq, updateSeq),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// v2 Graphiti: 标记旧的 updates/temporal_update 边为失效
|
// v2 Graphiti: 标记旧的 updates/temporal_update 边为失效
|
||||||
const oldEdges = graph.edges.filter(
|
const oldEdges = graph.edges.filter(
|
||||||
(e) =>
|
(e) =>
|
||||||
|
!e.invalidAt &&
|
||||||
|
((e.relation === "updates" && e.toId === op.nodeId) ||
|
||||||
|
(e.relation === "temporal_update" &&
|
||||||
e.toId === op.nodeId &&
|
e.toId === op.nodeId &&
|
||||||
(e.relation === "updates" || e.relation === "temporal_update") &&
|
op.sourceNodeId &&
|
||||||
!e.invalidAt,
|
e.fromId === op.sourceNodeId)),
|
||||||
);
|
);
|
||||||
for (const e of oldEdges) {
|
for (const e of oldEdges) {
|
||||||
invalidateEdge(e);
|
invalidateEdge(e);
|
||||||
@@ -284,7 +318,7 @@ function handleUpdate(graph, op, stats) {
|
|||||||
previousNode.id,
|
previousNode.id,
|
||||||
status: "resolved",
|
status: "resolved",
|
||||||
},
|
},
|
||||||
seq: op.seq || previousNode.seq || 0,
|
seq: updateSeq,
|
||||||
importance: Math.max(
|
importance: Math.max(
|
||||||
4,
|
4,
|
||||||
Math.min(8, op.importance ?? previousNode.importance ?? 5),
|
Math.min(8, op.importance ?? previousNode.importance ?? 5),
|
||||||
@@ -379,7 +413,10 @@ function handleLinks(graph, sourceId, links, refMap, stats) {
|
|||||||
async function generateNodeEmbeddings(graph, embeddingConfig) {
|
async function generateNodeEmbeddings(graph, embeddingConfig) {
|
||||||
if (!embeddingConfig?.apiUrl) return;
|
if (!embeddingConfig?.apiUrl) return;
|
||||||
|
|
||||||
const needsEmbedding = graph.nodes.filter((n) => !n.embedding && !n.archived);
|
const needsEmbedding = graph.nodes.filter(
|
||||||
|
(n) =>
|
||||||
|
!n.archived && (!Array.isArray(n.embedding) || n.embedding.length === 0),
|
||||||
|
);
|
||||||
|
|
||||||
if (needsEmbedding.length === 0) return;
|
if (needsEmbedding.length === 0) return;
|
||||||
|
|
||||||
@@ -410,7 +447,9 @@ async function generateNodeEmbeddings(graph, embeddingConfig) {
|
|||||||
* 构建图谱概览文本(给 LLM 看)
|
* 构建图谱概览文本(给 LLM 看)
|
||||||
*/
|
*/
|
||||||
function buildGraphOverview(graph, schema) {
|
function buildGraphOverview(graph, schema) {
|
||||||
const activeNodes = graph.nodes.filter((n) => !n.archived);
|
const activeNodes = graph.nodes
|
||||||
|
.filter((n) => !n.archived)
|
||||||
|
.sort((a, b) => (a.seq || 0) - (b.seq || 0));
|
||||||
if (activeNodes.length === 0) return "";
|
if (activeNodes.length === 0) return "";
|
||||||
|
|
||||||
const lines = [];
|
const lines = [];
|
||||||
@@ -502,8 +541,11 @@ async function mem0ConflictCheck(
|
|||||||
operations,
|
operations,
|
||||||
embeddingConfig,
|
embeddingConfig,
|
||||||
threshold,
|
threshold,
|
||||||
|
fallbackSeq,
|
||||||
) {
|
) {
|
||||||
const activeNodes = getActiveNodes(graph).filter((n) => n.embedding);
|
const activeNodes = getActiveNodes(graph).filter(
|
||||||
|
(n) => Array.isArray(n.embedding) && n.embedding.length > 0,
|
||||||
|
);
|
||||||
if (activeNodes.length === 0) return;
|
if (activeNodes.length === 0) return;
|
||||||
|
|
||||||
for (const op of operations) {
|
for (const op of operations) {
|
||||||
@@ -553,7 +595,8 @@ async function mem0ConflictCheck(
|
|||||||
);
|
);
|
||||||
op.action = "update";
|
op.action = "update";
|
||||||
op.nodeId = decision.targetId;
|
op.nodeId = decision.targetId;
|
||||||
op.sourceNodeId = topMatch.id;
|
op.sourceNodeId = op.sourceNodeId || topMatch.id;
|
||||||
|
op.seq = Number.isFinite(op.seq) ? op.seq : fallbackSeq;
|
||||||
if (decision.mergedFields) {
|
if (decision.mergedFields) {
|
||||||
op.fields = { ...op.fields, ...decision.mergedFields };
|
op.fields = { ...op.fields, ...decision.mergedFields };
|
||||||
}
|
}
|
||||||
@@ -627,7 +670,12 @@ export async function generateSynopsis({ graph, schema, currentSeq }) {
|
|||||||
if (existingSynopsis) {
|
if (existingSynopsis) {
|
||||||
updateNode(graph, existingSynopsis.id, {
|
updateNode(graph, existingSynopsis.id, {
|
||||||
fields: { summary: result.summary, scope: `楼 1 ~ ${currentSeq}` },
|
fields: { summary: result.summary, scope: `楼 1 ~ ${currentSeq}` },
|
||||||
|
seq: Math.max(existingSynopsis.seq || 0, currentSeq),
|
||||||
});
|
});
|
||||||
|
existingSynopsis.seqRange = [
|
||||||
|
Math.min(existingSynopsis.seqRange?.[0] ?? currentSeq, currentSeq),
|
||||||
|
Math.max(existingSynopsis.seqRange?.[1] ?? currentSeq, currentSeq),
|
||||||
|
];
|
||||||
existingSynopsis.embedding = null;
|
existingSynopsis.embedding = null;
|
||||||
console.log("[ST-BME] 全局概要已更新");
|
console.log("[ST-BME] 全局概要已更新");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
161
graph.js
161
graph.js
@@ -4,15 +4,15 @@
|
|||||||
/**
|
/**
|
||||||
* 图状态版本号
|
* 图状态版本号
|
||||||
*/
|
*/
|
||||||
const GRAPH_VERSION = 2;
|
const GRAPH_VERSION = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成 UUID v4
|
* 生成 UUID v4
|
||||||
*/
|
*/
|
||||||
function uuid() {
|
function uuid() {
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||||
const r = (Math.random() * 16) | 0;
|
const r = (Math.random() * 16) | 0;
|
||||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||||
return v.toString(16);
|
return v.toString(16);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ function uuid() {
|
|||||||
export function createEmptyGraph() {
|
export function createEmptyGraph() {
|
||||||
return {
|
return {
|
||||||
version: GRAPH_VERSION,
|
version: GRAPH_VERSION,
|
||||||
lastProcessedSeq: 0,
|
lastProcessedSeq: -1,
|
||||||
nodes: [],
|
nodes: [],
|
||||||
edges: [],
|
edges: [],
|
||||||
lastRecallResult: null,
|
lastRecallResult: null,
|
||||||
@@ -77,7 +77,7 @@ export function createNode({
|
|||||||
export function addNode(graph, node) {
|
export function addNode(graph, node) {
|
||||||
// 同类型节点的时间链表:连接到最后一个同类型节点
|
// 同类型节点的时间链表:连接到最后一个同类型节点
|
||||||
const sameTypeNodes = graph.nodes
|
const sameTypeNodes = graph.nodes
|
||||||
.filter(n => n.type === node.type && !n.archived && n.level === 0)
|
.filter((n) => n.type === node.type && !n.archived && n.level === 0)
|
||||||
.sort((a, b) => a.seq - b.seq);
|
.sort((a, b) => a.seq - b.seq);
|
||||||
|
|
||||||
if (sameTypeNodes.length > 0) {
|
if (sameTypeNodes.length > 0) {
|
||||||
@@ -97,7 +97,7 @@ export function addNode(graph, node) {
|
|||||||
* @returns {object|null}
|
* @returns {object|null}
|
||||||
*/
|
*/
|
||||||
export function getNode(graph, nodeId) {
|
export function getNode(graph, nodeId) {
|
||||||
return graph.nodes.find(n => n.id === nodeId) || null;
|
return graph.nodes.find((n) => n.id === nodeId) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,15 +149,17 @@ export function removeNode(graph, nodeId) {
|
|||||||
if (node.parentId) {
|
if (node.parentId) {
|
||||||
const parent = getNode(graph, node.parentId);
|
const parent = getNode(graph, node.parentId);
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent.childIds = parent.childIds.filter(id => id !== nodeId);
|
parent.childIds = parent.childIds.filter((id) => id !== nodeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除相关边
|
// 删除相关边
|
||||||
graph.edges = graph.edges.filter(e => e.fromId !== nodeId && e.toId !== nodeId);
|
graph.edges = graph.edges.filter(
|
||||||
|
(e) => e.fromId !== nodeId && e.toId !== nodeId,
|
||||||
|
);
|
||||||
|
|
||||||
// 删除节点本身
|
// 删除节点本身
|
||||||
graph.nodes = graph.nodes.filter(n => n.id !== nodeId);
|
graph.nodes = graph.nodes.filter((n) => n.id !== nodeId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -169,9 +171,9 @@ export function removeNode(graph, nodeId) {
|
|||||||
* @returns {object[]}
|
* @returns {object[]}
|
||||||
*/
|
*/
|
||||||
export function getActiveNodes(graph, typeFilter = null) {
|
export function getActiveNodes(graph, typeFilter = null) {
|
||||||
let nodes = graph.nodes.filter(n => !n.archived);
|
let nodes = graph.nodes.filter((n) => !n.archived);
|
||||||
if (typeFilter) {
|
if (typeFilter) {
|
||||||
nodes = nodes.filter(n => n.type === typeFilter);
|
nodes = nodes.filter((n) => n.type === typeFilter);
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
@@ -184,9 +186,17 @@ export function getActiveNodes(graph, typeFilter = null) {
|
|||||||
* @param {string} primaryKeyField - 主键字段名(默认 'name')
|
* @param {string} primaryKeyField - 主键字段名(默认 'name')
|
||||||
* @returns {object|null}
|
* @returns {object|null}
|
||||||
*/
|
*/
|
||||||
export function findLatestNode(graph, type, primaryKeyValue, primaryKeyField = 'name') {
|
export function findLatestNode(
|
||||||
|
graph,
|
||||||
|
type,
|
||||||
|
primaryKeyValue,
|
||||||
|
primaryKeyField = "name",
|
||||||
|
) {
|
||||||
const candidates = graph.nodes.filter(
|
const candidates = graph.nodes.filter(
|
||||||
n => n.type === type && !n.archived && n.fields[primaryKeyField] === primaryKeyValue,
|
(n) =>
|
||||||
|
n.type === type &&
|
||||||
|
!n.archived &&
|
||||||
|
n.fields[primaryKeyField] === primaryKeyValue,
|
||||||
);
|
);
|
||||||
if (candidates.length === 0) return null;
|
if (candidates.length === 0) return null;
|
||||||
return candidates.sort((a, b) => b.seq - a.seq)[0];
|
return candidates.sort((a, b) => b.seq - a.seq)[0];
|
||||||
@@ -199,7 +209,13 @@ export function findLatestNode(graph, type, primaryKeyValue, primaryKeyField = '
|
|||||||
* @param {object} params
|
* @param {object} params
|
||||||
* @returns {object} 新边
|
* @returns {object} 新边
|
||||||
*/
|
*/
|
||||||
export function createEdge({ fromId, toId, relation = 'related', strength = 0.8, edgeType = 0 }) {
|
export function createEdge({
|
||||||
|
fromId,
|
||||||
|
toId,
|
||||||
|
relation = "related",
|
||||||
|
strength = 0.8,
|
||||||
|
edgeType = 0,
|
||||||
|
}) {
|
||||||
return {
|
return {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
fromId,
|
fromId,
|
||||||
@@ -227,13 +243,32 @@ export function addEdge(graph, edge) {
|
|||||||
if (!from || !to) return null;
|
if (!from || !to) return null;
|
||||||
if (edge.fromId === edge.toId) 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(
|
const existing = graph.edges.find(
|
||||||
e => e.fromId === edge.fromId && e.toId === edge.toId && e.relation === edge.relation,
|
(e) =>
|
||||||
|
e.fromId === edge.fromId &&
|
||||||
|
e.toId === edge.toId &&
|
||||||
|
e.relation === edge.relation &&
|
||||||
|
isCurrentEdgeValid(e),
|
||||||
);
|
);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// 更新已有边的强度
|
existing.strength = Math.max(existing.strength, edge.strength ?? 0);
|
||||||
existing.strength = Math.max(existing.strength, edge.strength);
|
existing.validAt = Math.max(
|
||||||
|
existing.validAt || 0,
|
||||||
|
edge.validAt || Date.now(),
|
||||||
|
);
|
||||||
|
if (edge.invalidAt) {
|
||||||
|
existing.invalidAt = edge.invalidAt;
|
||||||
|
}
|
||||||
|
if (edge.expiredAt) {
|
||||||
|
existing.expiredAt = edge.expiredAt;
|
||||||
|
}
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +283,7 @@ export function addEdge(graph, edge) {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function removeEdge(graph, edgeId) {
|
export function removeEdge(graph, edgeId) {
|
||||||
const idx = graph.edges.findIndex(e => e.id === edgeId);
|
const idx = graph.edges.findIndex((e) => e.id === edgeId);
|
||||||
if (idx === -1) return false;
|
if (idx === -1) return false;
|
||||||
graph.edges.splice(idx, 1);
|
graph.edges.splice(idx, 1);
|
||||||
return true;
|
return true;
|
||||||
@@ -261,7 +296,7 @@ export function removeEdge(graph, edgeId) {
|
|||||||
* @returns {object[]}
|
* @returns {object[]}
|
||||||
*/
|
*/
|
||||||
export function getOutEdges(graph, nodeId) {
|
export function getOutEdges(graph, nodeId) {
|
||||||
return graph.edges.filter(e => e.fromId === nodeId);
|
return graph.edges.filter((e) => e.fromId === nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -271,7 +306,7 @@ export function getOutEdges(graph, nodeId) {
|
|||||||
* @returns {object[]}
|
* @returns {object[]}
|
||||||
*/
|
*/
|
||||||
export function getInEdges(graph, nodeId) {
|
export function getInEdges(graph, nodeId) {
|
||||||
return graph.edges.filter(e => e.toId === nodeId);
|
return graph.edges.filter((e) => e.toId === nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -281,7 +316,7 @@ export function getInEdges(graph, nodeId) {
|
|||||||
* @returns {object[]}
|
* @returns {object[]}
|
||||||
*/
|
*/
|
||||||
export function getNodeEdges(graph, nodeId) {
|
export function getNodeEdges(graph, nodeId) {
|
||||||
return graph.edges.filter(e => e.fromId === nodeId || e.toId === nodeId);
|
return graph.edges.filter((e) => e.fromId === nodeId || e.toId === nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 查询辅助 ====================
|
// ==================== 查询辅助 ====================
|
||||||
@@ -295,7 +330,8 @@ export function buildAdjacencyMap(graph) {
|
|||||||
const adj = new Map();
|
const adj = new Map();
|
||||||
|
|
||||||
for (const edge of graph.edges) {
|
for (const edge of graph.edges) {
|
||||||
// 正向
|
if (!isEdgeActive(edge)) continue;
|
||||||
|
|
||||||
if (!adj.has(edge.fromId)) adj.set(edge.fromId, []);
|
if (!adj.has(edge.fromId)) adj.set(edge.fromId, []);
|
||||||
adj.get(edge.fromId).push({
|
adj.get(edge.fromId).push({
|
||||||
targetId: edge.toId,
|
targetId: edge.toId,
|
||||||
@@ -303,7 +339,6 @@ export function buildAdjacencyMap(graph) {
|
|||||||
edgeType: edge.edgeType,
|
edgeType: edge.edgeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 反向(图扩散是双向的)
|
|
||||||
if (!adj.has(edge.toId)) adj.set(edge.toId, []);
|
if (!adj.has(edge.toId)) adj.set(edge.toId, []);
|
||||||
adj.get(edge.toId).push({
|
adj.get(edge.toId).push({
|
||||||
targetId: edge.fromId,
|
targetId: edge.fromId,
|
||||||
@@ -323,14 +358,10 @@ export function buildAdjacencyMap(graph) {
|
|||||||
*/
|
*/
|
||||||
export function buildTemporalAdjacencyMap(graph) {
|
export function buildTemporalAdjacencyMap(graph) {
|
||||||
const adj = new Map();
|
const adj = new Map();
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
for (const edge of graph.edges) {
|
for (const edge of graph.edges) {
|
||||||
// 跳过已失效的边
|
if (!isEdgeActive(edge)) continue;
|
||||||
if (edge.invalidAt && edge.invalidAt <= now) continue;
|
|
||||||
if (edge.expiredAt) continue;
|
|
||||||
|
|
||||||
// 正向
|
|
||||||
if (!adj.has(edge.fromId)) adj.set(edge.fromId, []);
|
if (!adj.has(edge.fromId)) adj.set(edge.fromId, []);
|
||||||
adj.get(edge.fromId).push({
|
adj.get(edge.fromId).push({
|
||||||
targetId: edge.toId,
|
targetId: edge.toId,
|
||||||
@@ -338,7 +369,6 @@ export function buildTemporalAdjacencyMap(graph) {
|
|||||||
edgeType: edge.edgeType,
|
edgeType: edge.edgeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 反向
|
|
||||||
if (!adj.has(edge.toId)) adj.set(edge.toId, []);
|
if (!adj.has(edge.toId)) adj.set(edge.toId, []);
|
||||||
adj.get(edge.toId).push({
|
adj.get(edge.toId).push({
|
||||||
targetId: edge.fromId,
|
targetId: edge.fromId,
|
||||||
@@ -350,13 +380,23 @@ export function buildTemporalAdjacencyMap(graph) {
|
|||||||
return adj;
|
return adj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
* @param {object} edge
|
||||||
*/
|
*/
|
||||||
export function invalidateEdge(edge) {
|
export function invalidateEdge(edge) {
|
||||||
|
if (!edge) return;
|
||||||
|
if (!edge.invalidAt) {
|
||||||
edge.invalidAt = Date.now();
|
edge.invalidAt = Date.now();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取图的统计信息
|
* 获取图的统计信息
|
||||||
@@ -364,8 +404,8 @@ export function invalidateEdge(edge) {
|
|||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
export function getGraphStats(graph) {
|
export function getGraphStats(graph) {
|
||||||
const activeNodes = graph.nodes.filter(n => !n.archived);
|
const activeNodes = graph.nodes.filter((n) => !n.archived);
|
||||||
const archivedNodes = graph.nodes.filter(n => n.archived);
|
const archivedNodes = graph.nodes.filter((n) => n.archived);
|
||||||
const typeCounts = {};
|
const typeCounts = {};
|
||||||
for (const node of activeNodes) {
|
for (const node of activeNodes) {
|
||||||
typeCounts[node.type] = (typeCounts[node.type] || 0) + 1;
|
typeCounts[node.type] = (typeCounts[node.type] || 0) + 1;
|
||||||
@@ -399,37 +439,72 @@ export function serializeGraph(graph) {
|
|||||||
*/
|
*/
|
||||||
export function deserializeGraph(json) {
|
export function deserializeGraph(json) {
|
||||||
try {
|
try {
|
||||||
const data = typeof json === 'string' ? JSON.parse(json) : json;
|
const data = typeof json === "string" ? JSON.parse(json) : json;
|
||||||
|
|
||||||
if (!data || data.version === undefined) {
|
if (!data || data.version === undefined) {
|
||||||
return createEmptyGraph();
|
return createEmptyGraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 版本迁移
|
|
||||||
if (data.version < GRAPH_VERSION) {
|
if (data.version < GRAPH_VERSION) {
|
||||||
console.log(`[ST-BME] 图版本迁移 v${data.version} → v${GRAPH_VERSION}`);
|
console.log(`[ST-BME] 图版本迁移 v${data.version} → v${GRAPH_VERSION}`);
|
||||||
|
|
||||||
// v1→v2 迁移:给旧边补充时序字段
|
|
||||||
if (data.version < 2 && data.edges) {
|
if (data.version < 2 && data.edges) {
|
||||||
for (const edge of data.edges) {
|
for (const edge of data.edges) {
|
||||||
if (edge.validAt === undefined) edge.validAt = edge.createdTime || Date.now();
|
if (edge.validAt === undefined)
|
||||||
|
edge.validAt = edge.createdTime || Date.now();
|
||||||
if (edge.invalidAt === undefined) edge.invalidAt = null;
|
if (edge.invalidAt === undefined) edge.invalidAt = null;
|
||||||
if (edge.expiredAt === undefined) edge.expiredAt = 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data.version = GRAPH_VERSION;
|
data.version = GRAPH_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保字段完整
|
data.nodes = (data.nodes || []).map((node) => {
|
||||||
data.nodes = data.nodes || [];
|
const seq = Number.isFinite(node.seq) ? node.seq : 0;
|
||||||
data.edges = data.edges || [];
|
return {
|
||||||
data.lastProcessedSeq = data.lastProcessedSeq || 0;
|
level: 0,
|
||||||
data.lastRecallResult = data.lastRecallResult || null;
|
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],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
data.edges = (data.edges || []).map((edge) => ({
|
||||||
|
createdTime: Date.now(),
|
||||||
|
validAt: edge?.createdTime || Date.now(),
|
||||||
|
invalidAt: null,
|
||||||
|
expiredAt: null,
|
||||||
|
...edge,
|
||||||
|
}));
|
||||||
|
data.lastProcessedSeq = Number.isFinite(data.lastProcessedSeq)
|
||||||
|
? data.lastProcessedSeq
|
||||||
|
: -1;
|
||||||
|
data.lastRecallResult = Array.isArray(data.lastRecallResult)
|
||||||
|
? data.lastRecallResult
|
||||||
|
: null;
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[ST-BME] 图反序列化失败:', e);
|
console.error("[ST-BME] 图反序列化失败:", e);
|
||||||
return createEmptyGraph();
|
return createEmptyGraph();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,7 +517,7 @@ export function deserializeGraph(json) {
|
|||||||
export function exportGraph(graph) {
|
export function exportGraph(graph) {
|
||||||
const exportData = {
|
const exportData = {
|
||||||
...graph,
|
...graph,
|
||||||
nodes: graph.nodes.map(n => ({ ...n, embedding: null })),
|
nodes: graph.nodes.map((n) => ({ ...n, embedding: null })),
|
||||||
};
|
};
|
||||||
return JSON.stringify(exportData, null, 2);
|
return JSON.stringify(exportData, null, 2);
|
||||||
}
|
}
|
||||||
|
|||||||
144
index.js
144
index.js
@@ -30,7 +30,7 @@ import {
|
|||||||
} from "./graph.js";
|
} from "./graph.js";
|
||||||
import { estimateTokens, formatInjection } from "./injector.js";
|
import { estimateTokens, formatInjection } from "./injector.js";
|
||||||
import { retrieve } from "./retriever.js";
|
import { retrieve } from "./retriever.js";
|
||||||
import { DEFAULT_NODE_SCHEMA } from "./schema.js";
|
import { DEFAULT_NODE_SCHEMA, validateSchema } from "./schema.js";
|
||||||
|
|
||||||
const MODULE_NAME = "st_bme";
|
const MODULE_NAME = "st_bme";
|
||||||
const GRAPH_METADATA_KEY = "st_bme_graph";
|
const GRAPH_METADATA_KEY = "st_bme_graph";
|
||||||
@@ -121,15 +121,23 @@ let extractionCount = 0; // v2: 提取次数计数器(定期触发概要/遗
|
|||||||
// ==================== 设置管理 ====================
|
// ==================== 设置管理 ====================
|
||||||
|
|
||||||
function getSettings() {
|
function getSettings() {
|
||||||
if (!extension_settings[MODULE_NAME]) {
|
const mergedSettings = {
|
||||||
extension_settings[MODULE_NAME] = { ...defaultSettings };
|
...defaultSettings,
|
||||||
}
|
...(extension_settings[MODULE_NAME] || {}),
|
||||||
return extension_settings[MODULE_NAME];
|
};
|
||||||
|
extension_settings[MODULE_NAME] = mergedSettings;
|
||||||
|
return mergedSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSchema() {
|
function getSchema() {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
return settings.nodeTypeSchema || DEFAULT_NODE_SCHEMA;
|
const schema = settings.nodeTypeSchema || DEFAULT_NODE_SCHEMA;
|
||||||
|
const validation = validateSchema(schema);
|
||||||
|
if (!validation.valid) {
|
||||||
|
console.warn("[ST-BME] Schema 非法,回退到默认 Schema:", validation.errors);
|
||||||
|
return DEFAULT_NODE_SCHEMA;
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEmbeddingConfig() {
|
function getEmbeddingConfig() {
|
||||||
@@ -192,14 +200,15 @@ const DEFAULT_TRIGGER_KEYWORDS = [
|
|||||||
"来到",
|
"来到",
|
||||||
];
|
];
|
||||||
|
|
||||||
function getSmartTriggerDecision(chat, lastProcessed, settings) {
|
export function getSmartTriggerDecision(chat, lastProcessed, settings) {
|
||||||
const pendingMessages = chat
|
const pendingMessages = chat
|
||||||
.slice(lastProcessed + 1)
|
.slice(Math.max(0, (lastProcessed ?? -1) + 1))
|
||||||
.filter((msg) => !msg.is_system)
|
.filter((msg) => !msg.is_system)
|
||||||
.map((msg) => ({
|
.map((msg) => ({
|
||||||
role: msg.is_user ? "user" : "assistant",
|
role: msg.is_user ? "user" : "assistant",
|
||||||
content: msg.mes || "",
|
content: msg.mes || "",
|
||||||
}));
|
}))
|
||||||
|
.filter((msg) => msg.content.trim().length > 0);
|
||||||
|
|
||||||
if (pendingMessages.length === 0) {
|
if (pendingMessages.length === 0) {
|
||||||
return { triggered: false, score: 0, reasons: [] };
|
return { triggered: false, score: 0, reasons: [] };
|
||||||
@@ -266,6 +275,18 @@ function getSmartTriggerDecision(chat, lastProcessed, settings) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampInt(value, fallback, min = 0, max = Number.MAX_SAFE_INTEGER) {
|
||||||
|
const num = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isFinite(num)) return fallback;
|
||||||
|
return Math.min(max, Math.max(min, num));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampFloat(value, fallback, min = 0, max = 1) {
|
||||||
|
const num = Number.parseFloat(value);
|
||||||
|
if (!Number.isFinite(num)) return fallback;
|
||||||
|
return Math.min(max, Math.max(min, num));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取管线:处理未提取的对话楼层
|
* 提取管线:处理未提取的对话楼层
|
||||||
*/
|
*/
|
||||||
@@ -279,7 +300,7 @@ async function runExtraction() {
|
|||||||
const chat = context.chat;
|
const chat = context.chat;
|
||||||
if (!chat || chat.length === 0) return;
|
if (!chat || chat.length === 0) return;
|
||||||
|
|
||||||
// 找出 assistant 楼层序号
|
// lastProcessedSeq / startSeq / endSeq 统一使用 chat 数组索引语义
|
||||||
const assistantTurns = [];
|
const assistantTurns = [];
|
||||||
for (let i = 0; i < chat.length; i++) {
|
for (let i = 0; i < chat.length; i++) {
|
||||||
if (chat[i].is_user === false && !chat[i].is_system) {
|
if (chat[i].is_user === false && !chat[i].is_system) {
|
||||||
@@ -287,40 +308,44 @@ async function runExtraction() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastProcessed = currentGraph.lastProcessedSeq;
|
const lastProcessed = Number.isFinite(currentGraph.lastProcessedSeq)
|
||||||
const unprocessedStarts = assistantTurns.filter((i) => i > lastProcessed);
|
? currentGraph.lastProcessedSeq
|
||||||
|
: -1;
|
||||||
|
const unprocessedAssistantTurns = assistantTurns.filter(
|
||||||
|
(i) => i > lastProcessed,
|
||||||
|
);
|
||||||
|
|
||||||
if (unprocessedStarts.length === 0) return;
|
if (unprocessedAssistantTurns.length === 0) return;
|
||||||
|
|
||||||
|
const extractEvery = clampInt(settings.extractEvery, 1, 1, 50);
|
||||||
const smartTriggerDecision = settings.enableSmartTrigger
|
const smartTriggerDecision = settings.enableSmartTrigger
|
||||||
? getSmartTriggerDecision(chat, lastProcessed, settings)
|
? getSmartTriggerDecision(chat, lastProcessed, settings)
|
||||||
: { triggered: false, score: 0, reasons: [] };
|
: { triggered: false, score: 0, reasons: [] };
|
||||||
|
|
||||||
// 按 extractEvery 批次处理;若启用智能触发,则允许提前提取
|
|
||||||
if (
|
if (
|
||||||
unprocessedStarts.length < settings.extractEvery &&
|
unprocessedAssistantTurns.length < extractEvery &&
|
||||||
!smartTriggerDecision.triggered
|
!smartTriggerDecision.triggered
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const batchAssistantTurns = smartTriggerDecision.triggered
|
||||||
|
? unprocessedAssistantTurns
|
||||||
|
: unprocessedAssistantTurns.slice(0, extractEvery);
|
||||||
|
const startIdx = batchAssistantTurns[0];
|
||||||
|
const endIdx = batchAssistantTurns[batchAssistantTurns.length - 1];
|
||||||
|
|
||||||
isExtracting = true;
|
isExtracting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 收集要处理的消息
|
const contextTurns = clampInt(settings.extractContextTurns, 2, 0, 20);
|
||||||
const startIdx = unprocessedStarts[0];
|
const contextStart = Math.max(0, startIdx - contextTurns * 2);
|
||||||
const endIdx = unprocessedStarts[unprocessedStarts.length - 1];
|
|
||||||
|
|
||||||
// 包含上下文
|
|
||||||
const contextStart = Math.max(
|
|
||||||
0,
|
|
||||||
startIdx - settings.extractContextTurns * 2,
|
|
||||||
);
|
|
||||||
const messages = [];
|
const messages = [];
|
||||||
for (let i = contextStart; i <= endIdx && i < chat.length; i++) {
|
for (let i = contextStart; i <= endIdx && i < chat.length; i++) {
|
||||||
const msg = chat[i];
|
const msg = chat[i];
|
||||||
if (msg.is_system) continue;
|
if (msg.is_system) continue;
|
||||||
messages.push({
|
messages.push({
|
||||||
|
seq: i,
|
||||||
role: msg.is_user ? "user" : "assistant",
|
role: msg.is_user ? "user" : "assistant",
|
||||||
content: msg.mes || "",
|
content: msg.mes || "",
|
||||||
});
|
});
|
||||||
@@ -336,7 +361,9 @@ async function runExtraction() {
|
|||||||
const result = await extractMemories({
|
const result = await extractMemories({
|
||||||
graph: currentGraph,
|
graph: currentGraph,
|
||||||
messages,
|
messages,
|
||||||
startSeq: endIdx,
|
startSeq: startIdx,
|
||||||
|
endSeq: endIdx,
|
||||||
|
lastProcessedSeq: lastProcessed,
|
||||||
schema: getSchema(),
|
schema: getSchema(),
|
||||||
embeddingConfig: getEmbeddingConfig(),
|
embeddingConfig: getEmbeddingConfig(),
|
||||||
extractPrompt: settings.extractPrompt || undefined,
|
extractPrompt: settings.extractPrompt || undefined,
|
||||||
@@ -478,7 +505,7 @@ async function runRecall() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 格式化注入文本
|
// 格式化注入文本
|
||||||
const injectionText = formatInjection(result, getSchema());
|
const injectionText = formatInjection(result, getSchema()).trim();
|
||||||
lastInjectionContent = injectionText;
|
lastInjectionContent = injectionText;
|
||||||
|
|
||||||
if (injectionText) {
|
if (injectionText) {
|
||||||
@@ -486,15 +513,15 @@ async function runRecall() {
|
|||||||
console.log(
|
console.log(
|
||||||
`[ST-BME] 注入 ${tokens} 估算 tokens, Core=${result.stats.coreCount}, Recall=${result.stats.recallCount}`,
|
`[ST-BME] 注入 ${tokens} 估算 tokens, Core=${result.stats.coreCount}, Recall=${result.stats.recallCount}`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 使用 ST 的 extension prompt API 注入
|
// 无结果时也要清空旧注入,避免脏 prompt 残留
|
||||||
context.setExtensionPrompt(
|
context.setExtensionPrompt(
|
||||||
MODULE_NAME,
|
MODULE_NAME,
|
||||||
injectionText,
|
injectionText,
|
||||||
1, // extension_prompt_types.IN_PROMPT
|
1, // extension_prompt_types.IN_PROMPT
|
||||||
settings.injectDepth,
|
clampInt(settings.injectDepth, 4, 0, 9999),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// 保存召回结果和访问强化
|
// 保存召回结果和访问强化
|
||||||
currentGraph.lastRecallResult = result.selectedNodeIds;
|
currentGraph.lastRecallResult = result.selectedNodeIds;
|
||||||
@@ -665,7 +692,13 @@ function bindSettingsUI() {
|
|||||||
$("#st_bme_extract_every")
|
$("#st_bme_extract_every")
|
||||||
.val(settings.extractEvery)
|
.val(settings.extractEvery)
|
||||||
.on("input", function () {
|
.on("input", function () {
|
||||||
settings.extractEvery = Math.max(1, parseInt($(this).val()) || 1);
|
settings.extractEvery = clampInt($(this).val(), 1, 1, 50);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
$("#st_bme_extract_context_turns")
|
||||||
|
.val(settings.extractContextTurns)
|
||||||
|
.on("input", function () {
|
||||||
|
settings.extractContextTurns = clampInt($(this).val(), 2, 0, 20);
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -685,11 +718,24 @@ function bindSettingsUI() {
|
|||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#st_bme_recall_top_k")
|
||||||
|
.val(settings.recallTopK)
|
||||||
|
.on("input", function () {
|
||||||
|
settings.recallTopK = clampInt($(this).val(), 15, 1, 100);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
$("#st_bme_recall_max_nodes")
|
||||||
|
.val(settings.recallMaxNodes)
|
||||||
|
.on("input", function () {
|
||||||
|
settings.recallMaxNodes = clampInt($(this).val(), 8, 1, 50);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
// 注入深度
|
// 注入深度
|
||||||
$("#st_bme_inject_depth")
|
$("#st_bme_inject_depth")
|
||||||
.val(settings.injectDepth)
|
.val(settings.injectDepth)
|
||||||
.on("input", function () {
|
.on("input", function () {
|
||||||
settings.injectDepth = Math.max(0, parseInt($(this).val()) || 4);
|
settings.injectDepth = clampInt($(this).val(), 4, 0, 9999);
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -697,19 +743,19 @@ function bindSettingsUI() {
|
|||||||
$("#st_bme_graph_weight")
|
$("#st_bme_graph_weight")
|
||||||
.val(settings.graphWeight)
|
.val(settings.graphWeight)
|
||||||
.on("input", function () {
|
.on("input", function () {
|
||||||
settings.graphWeight = parseFloat($(this).val()) || 0.6;
|
settings.graphWeight = clampFloat($(this).val(), 0.6, 0, 1);
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
$("#st_bme_vector_weight")
|
$("#st_bme_vector_weight")
|
||||||
.val(settings.vectorWeight)
|
.val(settings.vectorWeight)
|
||||||
.on("input", function () {
|
.on("input", function () {
|
||||||
settings.vectorWeight = parseFloat($(this).val()) || 0.3;
|
settings.vectorWeight = clampFloat($(this).val(), 0.3, 0, 1);
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
$("#st_bme_importance_weight")
|
$("#st_bme_importance_weight")
|
||||||
.val(settings.importanceWeight)
|
.val(settings.importanceWeight)
|
||||||
.on("input", function () {
|
.on("input", function () {
|
||||||
settings.importanceWeight = parseFloat($(this).val()) || 0.1;
|
settings.importanceWeight = clampFloat($(this).val(), 0.1, 0, 1);
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -754,7 +800,13 @@ function bindSettingsUI() {
|
|||||||
$("#st_bme_evo_neighbors")
|
$("#st_bme_evo_neighbors")
|
||||||
.val(settings.evoNeighborCount)
|
.val(settings.evoNeighborCount)
|
||||||
.on("input", function () {
|
.on("input", function () {
|
||||||
settings.evoNeighborCount = Math.max(1, parseInt($(this).val()) || 5);
|
settings.evoNeighborCount = clampInt($(this).val(), 5, 1, 20);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
$("#st_bme_evo_consolidate_every")
|
||||||
|
.val(settings.evoConsolidateEvery)
|
||||||
|
.on("input", function () {
|
||||||
|
settings.evoConsolidateEvery = clampInt($(this).val(), 50, 1, 500);
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -768,7 +820,7 @@ function bindSettingsUI() {
|
|||||||
$("#st_bme_conflict_threshold")
|
$("#st_bme_conflict_threshold")
|
||||||
.val(settings.conflictThreshold)
|
.val(settings.conflictThreshold)
|
||||||
.on("input", function () {
|
.on("input", function () {
|
||||||
settings.conflictThreshold = parseFloat($(this).val()) || 0.85;
|
settings.conflictThreshold = clampFloat($(this).val(), 0.85, 0.5, 0.99);
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -782,7 +834,7 @@ function bindSettingsUI() {
|
|||||||
$("#st_bme_synopsis_every")
|
$("#st_bme_synopsis_every")
|
||||||
.val(settings.synopsisEveryN)
|
.val(settings.synopsisEveryN)
|
||||||
.on("input", function () {
|
.on("input", function () {
|
||||||
settings.synopsisEveryN = Math.max(1, parseInt($(this).val()) || 5);
|
settings.synopsisEveryN = clampInt($(this).val(), 5, 1, 100);
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -815,6 +867,12 @@ function bindSettingsUI() {
|
|||||||
settings.triggerPatterns = $(this).val();
|
settings.triggerPatterns = $(this).val();
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
$("#st_bme_smart_trigger_threshold")
|
||||||
|
.val(settings.smartTriggerThreshold)
|
||||||
|
.on("input", function () {
|
||||||
|
settings.smartTriggerThreshold = clampInt($(this).val(), 2, 1, 10);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
// P2: 主动遗忘
|
// P2: 主动遗忘
|
||||||
$("#st_bme_sleep_cycle")
|
$("#st_bme_sleep_cycle")
|
||||||
@@ -826,7 +884,13 @@ function bindSettingsUI() {
|
|||||||
$("#st_bme_forget_threshold")
|
$("#st_bme_forget_threshold")
|
||||||
.val(settings.forgetThreshold)
|
.val(settings.forgetThreshold)
|
||||||
.on("input", function () {
|
.on("input", function () {
|
||||||
settings.forgetThreshold = parseFloat($(this).val()) || 0.5;
|
settings.forgetThreshold = clampFloat($(this).val(), 0.5, 0.1, 1);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
$("#st_bme_sleep_every")
|
||||||
|
.val(settings.sleepEveryN)
|
||||||
|
.on("input", function () {
|
||||||
|
settings.sleepEveryN = clampInt($(this).val(), 10, 1, 200);
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -840,7 +904,7 @@ function bindSettingsUI() {
|
|||||||
$("#st_bme_prob_chance")
|
$("#st_bme_prob_chance")
|
||||||
.val(settings.probRecallChance)
|
.val(settings.probRecallChance)
|
||||||
.on("input", function () {
|
.on("input", function () {
|
||||||
settings.probRecallChance = parseFloat($(this).val()) || 0.15;
|
settings.probRecallChance = clampFloat($(this).val(), 0.15, 0.01, 0.5);
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -854,7 +918,7 @@ function bindSettingsUI() {
|
|||||||
$("#st_bme_reflect_every")
|
$("#st_bme_reflect_every")
|
||||||
.val(settings.reflectEveryN)
|
.val(settings.reflectEveryN)
|
||||||
.on("input", function () {
|
.on("input", function () {
|
||||||
settings.reflectEveryN = Math.max(3, parseInt($(this).val()) || 10);
|
settings.reflectEveryN = clampInt($(this).val(), 10, 1, 200);
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
25
injector.js
25
injector.js
@@ -13,6 +13,7 @@ import { getSchemaType } from "./schema.js";
|
|||||||
export function formatInjection(retrievalResult, schema) {
|
export function formatInjection(retrievalResult, schema) {
|
||||||
const { coreNodes, recallNodes, groupedRecallNodes } = retrievalResult;
|
const { coreNodes, recallNodes, groupedRecallNodes } = retrievalResult;
|
||||||
const parts = [];
|
const parts = [];
|
||||||
|
const appended = new Set();
|
||||||
|
|
||||||
// ========== Core 常驻注入 ==========
|
// ========== Core 常驻注入 ==========
|
||||||
if (coreNodes.length > 0) {
|
if (coreNodes.length > 0) {
|
||||||
@@ -24,7 +25,7 @@ export function formatInjection(retrievalResult, schema) {
|
|||||||
const typeDef = getSchemaType(schema, typeId);
|
const typeDef = getSchemaType(schema, typeId);
|
||||||
if (!typeDef) continue;
|
if (!typeDef) continue;
|
||||||
|
|
||||||
const table = formatTable(nodes, typeDef);
|
const table = formatTable(nodes, typeDef, appended);
|
||||||
if (table) parts.push(table);
|
if (table) parts.push(table);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +91,7 @@ function appendBucket(parts, title, nodes, schema) {
|
|||||||
const typeDef = getSchemaType(schema, typeId);
|
const typeDef = getSchemaType(schema, typeId);
|
||||||
if (!typeDef) continue;
|
if (!typeDef) continue;
|
||||||
|
|
||||||
const table = formatTable(groupedNodes, typeDef);
|
const table = formatTable(groupedNodes, typeDef, appended);
|
||||||
if (table) parts.push(table);
|
if (table) parts.push(table);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,12 +99,22 @@ function appendBucket(parts, title, nodes, schema) {
|
|||||||
/**
|
/**
|
||||||
* 将同类型节点格式化为 Markdown 表格
|
* 将同类型节点格式化为 Markdown 表格
|
||||||
*/
|
*/
|
||||||
function formatTable(nodes, typeDef) {
|
function formatTable(nodes, typeDef, appended = new Set()) {
|
||||||
if (nodes.length === 0) return "";
|
if (!Array.isArray(nodes) || nodes.length === 0) return "";
|
||||||
|
|
||||||
|
const uniqueNodes = nodes.filter((node) => {
|
||||||
|
if (!node?.id || appended.has(node.id)) return false;
|
||||||
|
appended.add(node.id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uniqueNodes.length === 0) return "";
|
||||||
|
|
||||||
// 确定要展示的列(有实际数据的列)
|
// 确定要展示的列(有实际数据的列)
|
||||||
const activeCols = typeDef.columns.filter((col) =>
|
const activeCols = typeDef.columns.filter((col) =>
|
||||||
nodes.some((n) => n.fields[col.name]),
|
uniqueNodes.some(
|
||||||
|
(n) => n.fields?.[col.name] != null && n.fields[col.name] !== "",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (activeCols.length === 0) return "";
|
if (activeCols.length === 0) return "";
|
||||||
@@ -113,9 +124,9 @@ function formatTable(nodes, typeDef) {
|
|||||||
const separator = `| ${activeCols.map(() => "---").join(" | ")} |`;
|
const separator = `| ${activeCols.map(() => "---").join(" | ")} |`;
|
||||||
|
|
||||||
// 数据行
|
// 数据行
|
||||||
const rows = nodes.map((node) => {
|
const rows = uniqueNodes.map((node) => {
|
||||||
const cells = activeCols.map((col) => {
|
const cells = activeCols.map((col) => {
|
||||||
const val = node.fields[col.name] || "";
|
const val = node.fields?.[col.name] ?? "";
|
||||||
// 转义管道符,限制单元格长度
|
// 转义管道符,限制单元格长度
|
||||||
return String(val)
|
return String(val)
|
||||||
.replace(/\|/g, "\\|")
|
.replace(/\|/g, "\\|")
|
||||||
|
|||||||
125
retriever.js
125
retriever.js
@@ -54,7 +54,12 @@ export async function retrieve({
|
|||||||
const enableProbRecall = options.enableProbRecall ?? false;
|
const enableProbRecall = options.enableProbRecall ?? false;
|
||||||
const probRecallChance = options.probRecallChance ?? 0.15;
|
const probRecallChance = options.probRecallChance ?? 0.15;
|
||||||
|
|
||||||
let activeNodes = getActiveNodes(graph);
|
let activeNodes = getActiveNodes(graph).filter(
|
||||||
|
(node) =>
|
||||||
|
!node.archived &&
|
||||||
|
Array.isArray(node.seqRange) &&
|
||||||
|
Number.isFinite(node.seqRange[1]),
|
||||||
|
);
|
||||||
|
|
||||||
// v2 ⑦: 认知边界过滤(RoleRAG 启发)
|
// v2 ⑦: 认知边界过滤(RoleRAG 启发)
|
||||||
if (enableVisibility && visibilityFilter) {
|
if (enableVisibility && visibilityFilter) {
|
||||||
@@ -62,6 +67,8 @@ export async function retrieve({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nodeCount = activeNodes.length;
|
const nodeCount = activeNodes.length;
|
||||||
|
const normalizedTopK = Math.max(1, topK);
|
||||||
|
const normalizedMaxRecallNodes = Math.max(1, maxRecallNodes);
|
||||||
console.log(
|
console.log(
|
||||||
`[ST-BME] 检索开始: ${nodeCount} 个活跃节点${enableVisibility ? " (认知边界已启用)" : ""}`,
|
`[ST-BME] 检索开始: ${nodeCount} 个活跃节点${enableVisibility ? " (认知边界已启用)" : ""}`,
|
||||||
);
|
);
|
||||||
@@ -81,7 +88,7 @@ export async function retrieve({
|
|||||||
userMessage,
|
userMessage,
|
||||||
activeNodes,
|
activeNodes,
|
||||||
embeddingConfig,
|
embeddingConfig,
|
||||||
topK,
|
normalizedTopK,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +136,9 @@ export async function retrieve({
|
|||||||
maxSteps: 2,
|
maxSteps: 2,
|
||||||
decayFactor: 0.6,
|
decayFactor: 0.6,
|
||||||
topK: 100,
|
topK: 100,
|
||||||
|
}).filter((item) => {
|
||||||
|
const node = getNode(graph, item.nodeId);
|
||||||
|
return node && !node.archived;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,14 +213,19 @@ export async function retrieve({
|
|||||||
candidateNodes,
|
candidateNodes,
|
||||||
graph,
|
graph,
|
||||||
schema,
|
schema,
|
||||||
maxRecallNodes,
|
normalizedMaxRecallNodes,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 中等图:直接取 Top-N
|
selectedNodeIds = scoredNodes
|
||||||
selectedNodeIds = scoredNodes.slice(0, topK).map((s) => s.nodeId);
|
.slice(0, Math.min(normalizedTopK, scoredNodes.length))
|
||||||
|
.map((s) => s.nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedNodeIds = reconstructSceneNodeIds(graph, selectedNodeIds, topK + 6);
|
selectedNodeIds = reconstructSceneNodeIds(
|
||||||
|
graph,
|
||||||
|
selectedNodeIds,
|
||||||
|
normalizedTopK + 6,
|
||||||
|
);
|
||||||
|
|
||||||
// 访问强化
|
// 访问强化
|
||||||
const selectedNodes = selectedNodeIds
|
const selectedNodes = selectedNodeIds
|
||||||
@@ -225,15 +240,19 @@ export async function retrieve({
|
|||||||
// 未被选中的高重要性节点有概率随机激活
|
// 未被选中的高重要性节点有概率随机激活
|
||||||
if (enableProbRecall && probRecallChance > 0) {
|
if (enableProbRecall && probRecallChance > 0) {
|
||||||
const selectedSet = new Set(selectedNodeIds);
|
const selectedSet = new Set(selectedNodeIds);
|
||||||
const candidates = activeNodes.filter(
|
const probability = Math.max(0.01, Math.min(0.5, probRecallChance));
|
||||||
|
const candidates = activeNodes
|
||||||
|
.filter(
|
||||||
(n) =>
|
(n) =>
|
||||||
!selectedSet.has(n.id) &&
|
!selectedSet.has(n.id) &&
|
||||||
n.importance >= 6 &&
|
n.importance >= 6 &&
|
||||||
n.type !== "synopsis" &&
|
n.type !== "synopsis" &&
|
||||||
n.type !== "rule",
|
n.type !== "rule",
|
||||||
);
|
)
|
||||||
|
.sort((a, b) => (b.importance || 0) - (a.importance || 0))
|
||||||
|
.slice(0, 3);
|
||||||
for (const c of candidates) {
|
for (const c of candidates) {
|
||||||
if (Math.random() < probRecallChance) {
|
if (Math.random() < probability) {
|
||||||
selectedNodeIds.push(c.id);
|
selectedNodeIds.push(c.id);
|
||||||
console.log(
|
console.log(
|
||||||
`[ST-BME] 概率触发: ${c.fields?.name || c.fields?.summary || c.id}`,
|
`[ST-BME] 概率触发: ${c.fields?.name || c.fields?.summary || c.id}`,
|
||||||
@@ -261,7 +280,7 @@ async function vectorPreFilter(
|
|||||||
if (!queryVec) return [];
|
if (!queryVec) return [];
|
||||||
|
|
||||||
const candidates = activeNodes
|
const candidates = activeNodes
|
||||||
.filter((n) => n.embedding)
|
.filter((n) => Array.isArray(n.embedding) && n.embedding.length > 0)
|
||||||
.map((n) => ({ nodeId: n.id, embedding: n.embedding }));
|
.map((n) => ({ nodeId: n.id, embedding: n.embedding }));
|
||||||
|
|
||||||
return searchSimilar(queryVec, candidates, topK);
|
return searchSimilar(queryVec, candidates, topK);
|
||||||
@@ -277,19 +296,21 @@ async function vectorPreFilter(
|
|||||||
*/
|
*/
|
||||||
function extractEntityAnchors(userMessage, activeNodes) {
|
function extractEntityAnchors(userMessage, activeNodes) {
|
||||||
const anchors = [];
|
const anchors = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
for (const node of activeNodes) {
|
for (const node of activeNodes) {
|
||||||
// 检查 name 字段
|
const candidates = [node.fields?.name, node.fields?.title]
|
||||||
const name = node.fields?.name;
|
.filter((value) => typeof value === "string")
|
||||||
if (name && userMessage.includes(name)) {
|
.map((value) => value.trim())
|
||||||
anchors.push({ nodeId: node.id, entity: name });
|
.filter((value) => value.length >= 2);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 title 字段
|
for (const candidate of candidates) {
|
||||||
const title = node.fields?.title;
|
if (!userMessage.includes(candidate)) continue;
|
||||||
if (title && userMessage.includes(title)) {
|
const key = `${node.id}:${candidate}`;
|
||||||
anchors.push({ nodeId: node.id, entity: title });
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
anchors.push({ nodeId: node.id, entity: candidate });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,16 +391,20 @@ async function llmRecall(
|
|||||||
* @returns {object[]}
|
* @returns {object[]}
|
||||||
*/
|
*/
|
||||||
function filterByVisibility(nodes, characterName) {
|
function filterByVisibility(nodes, characterName) {
|
||||||
|
if (!characterName || typeof characterName !== "string") return nodes;
|
||||||
return nodes.filter((node) => {
|
return nodes.filter((node) => {
|
||||||
// 没有 visibility 字段 → 对所有人可见
|
|
||||||
if (!node.fields?.visibility) return true;
|
if (!node.fields?.visibility) return true;
|
||||||
// visibility 是数组 → 检查当前角色是否在列表中
|
|
||||||
if (Array.isArray(node.fields.visibility)) {
|
if (Array.isArray(node.fields.visibility)) {
|
||||||
return node.fields.visibility.includes(characterName);
|
return (
|
||||||
|
node.fields.visibility.includes(characterName) ||
|
||||||
|
node.fields.visibility.includes("*")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// visibility 是字符串(逗号分隔)→ 解析后检查
|
|
||||||
if (typeof node.fields.visibility === "string") {
|
if (typeof node.fields.visibility === "string") {
|
||||||
const visibleTo = node.fields.visibility.split(",").map((s) => s.trim());
|
const visibleTo = node.fields.visibility
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
return visibleTo.includes(characterName) || visibleTo.includes("*");
|
return visibleTo.includes(characterName) || visibleTo.includes("*");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -391,15 +416,16 @@ function filterByVisibility(nodes, characterName) {
|
|||||||
* 分离常驻注入(Core)和召回注入(Recall)
|
* 分离常驻注入(Core)和召回注入(Recall)
|
||||||
*/
|
*/
|
||||||
function buildResult(graph, selectedNodeIds, schema) {
|
function buildResult(graph, selectedNodeIds, schema) {
|
||||||
const coreNodes = []; // 常驻注入
|
const coreNodes = [];
|
||||||
const recallNodes = []; // 召回注入
|
const recallNodes = [];
|
||||||
|
const selectedSet = new Set(uniqueNodeIds(selectedNodeIds));
|
||||||
|
|
||||||
// 常驻注入节点(alwaysInject=true 的类型)
|
// 常驻注入节点(alwaysInject=true 的类型)
|
||||||
const alwaysInjectTypes = new Set(
|
const alwaysInjectTypes = new Set(
|
||||||
schema.filter((s) => s.alwaysInject).map((s) => s.id),
|
schema.filter((s) => s.alwaysInject).map((s) => s.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeNodes = getActiveNodes(graph);
|
const activeNodes = getActiveNodes(graph).filter((node) => !node.archived);
|
||||||
|
|
||||||
for (const node of activeNodes) {
|
for (const node of activeNodes) {
|
||||||
if (alwaysInjectTypes.has(node.type)) {
|
if (alwaysInjectTypes.has(node.type)) {
|
||||||
@@ -407,21 +433,23 @@ function buildResult(graph, selectedNodeIds, schema) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const nodeId of selectedNodeIds) {
|
for (const nodeId of selectedSet) {
|
||||||
const node = getNode(graph, nodeId);
|
const node = getNode(graph, nodeId);
|
||||||
if (!node) continue;
|
if (!node || node.archived) continue;
|
||||||
if (!alwaysInjectTypes.has(node.type)) {
|
if (!alwaysInjectTypes.has(node.type)) {
|
||||||
recallNodes.push(node);
|
recallNodes.push(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
coreNodes.sort(compareNodeRecallOrder);
|
||||||
|
recallNodes.sort(compareNodeRecallOrder);
|
||||||
const groupedRecallNodes = groupRecallNodes(recallNodes);
|
const groupedRecallNodes = groupRecallNodes(recallNodes);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
coreNodes,
|
coreNodes,
|
||||||
recallNodes,
|
recallNodes,
|
||||||
groupedRecallNodes,
|
groupedRecallNodes,
|
||||||
selectedNodeIds: [...selectedNodeIds],
|
selectedNodeIds: [...selectedSet],
|
||||||
stats: {
|
stats: {
|
||||||
totalActive: activeNodes.length,
|
totalActive: activeNodes.length,
|
||||||
coreCount: coreNodes.length,
|
coreCount: coreNodes.length,
|
||||||
@@ -439,14 +467,15 @@ function reconstructSceneNodeIds(graph, seedNodeIds, limit = 16) {
|
|||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
||||||
function push(nodeId) {
|
function push(nodeId) {
|
||||||
if (!nodeId || seen.has(nodeId)) return;
|
if (!nodeId || seen.has(nodeId) || selected.length >= limit) return;
|
||||||
const node = getNode(graph, nodeId);
|
const node = getNode(graph, nodeId);
|
||||||
if (!node || node.archived) return;
|
if (!node || node.archived) return;
|
||||||
seen.add(nodeId);
|
seen.add(nodeId);
|
||||||
selected.push(nodeId);
|
selected.push(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const nodeId of seedNodeIds) {
|
for (const nodeId of uniqueNodeIds(seedNodeIds)) {
|
||||||
|
if (selected.length >= limit) break;
|
||||||
push(nodeId);
|
push(nodeId);
|
||||||
const node = getNode(graph, nodeId);
|
const node = getNode(graph, nodeId);
|
||||||
if (!node) continue;
|
if (!node) continue;
|
||||||
@@ -455,26 +484,24 @@ function reconstructSceneNodeIds(graph, seedNodeIds, limit = 16) {
|
|||||||
expandEventScene(graph, node, push);
|
expandEventScene(graph, node, push);
|
||||||
} else if (node.type === "character" || node.type === "location") {
|
} else if (node.type === "character" || node.type === "location") {
|
||||||
const relatedEvents = getNodeEdges(graph, node.id)
|
const relatedEvents = getNodeEdges(graph, node.id)
|
||||||
.filter((e) => !e.invalidAt)
|
.filter(isUsableSceneEdge)
|
||||||
.map((e) => (e.fromId === node.id ? e.toId : e.fromId))
|
.map((e) => (e.fromId === node.id ? e.toId : e.fromId))
|
||||||
.map((id) => getNode(graph, id))
|
.map((id) => getNode(graph, id))
|
||||||
.filter((n) => n && n.type === "event")
|
.filter((n) => n && !n.archived && n.type === "event")
|
||||||
.sort((a, b) => b.seq - a.seq)
|
.sort(compareNodeRecallOrder)
|
||||||
.slice(0, 2);
|
.slice(0, 2);
|
||||||
for (const eventNode of relatedEvents) {
|
for (const eventNode of relatedEvents) {
|
||||||
push(eventNode.id);
|
push(eventNode.id);
|
||||||
expandEventScene(graph, eventNode, push);
|
expandEventScene(graph, eventNode, push);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected.length >= limit) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return selected.slice(0, limit);
|
return selected.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expandEventScene(graph, eventNode, push) {
|
function expandEventScene(graph, eventNode, push) {
|
||||||
const edges = getNodeEdges(graph, eventNode.id).filter((e) => !e.invalidAt);
|
const edges = getNodeEdges(graph, eventNode.id).filter(isUsableSceneEdge);
|
||||||
for (const edge of edges) {
|
for (const edge of edges) {
|
||||||
const neighborId = edge.fromId === eventNode.id ? edge.toId : edge.fromId;
|
const neighborId = edge.fromId === eventNode.id ? edge.toId : edge.fromId;
|
||||||
const neighbor = getNode(graph, neighborId);
|
const neighbor = getNode(graph, neighborId);
|
||||||
@@ -501,11 +528,27 @@ function expandEventScene(graph, eventNode, push) {
|
|||||||
|
|
||||||
function getTemporalNeighborEvents(graph, seq, excludeId) {
|
function getTemporalNeighborEvents(graph, seq, excludeId) {
|
||||||
return getActiveNodes(graph, "event")
|
return getActiveNodes(graph, "event")
|
||||||
.filter((n) => n.id !== excludeId)
|
.filter((n) => n.id !== excludeId && !n.archived)
|
||||||
.sort((a, b) => Math.abs(a.seq - seq) - Math.abs(b.seq - seq))
|
.sort((a, b) => {
|
||||||
|
const distance =
|
||||||
|
Math.abs((a.seq || 0) - seq) - Math.abs((b.seq || 0) - seq);
|
||||||
|
if (distance !== 0) return distance;
|
||||||
|
return (b.seq || 0) - (a.seq || 0);
|
||||||
|
})
|
||||||
.slice(0, 2);
|
.slice(0, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isUsableSceneEdge(edge) {
|
||||||
|
return edge && !edge.invalidAt && !edge.expiredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareNodeRecallOrder(a, b) {
|
||||||
|
const aSeq = a?.seqRange?.[1] ?? a?.seq ?? 0;
|
||||||
|
const bSeq = b?.seqRange?.[1] ?? b?.seq ?? 0;
|
||||||
|
if (bSeq !== aSeq) return bSeq - aSeq;
|
||||||
|
return (b.importance || 0) - (a.importance || 0);
|
||||||
|
}
|
||||||
|
|
||||||
function groupRecallNodes(nodes) {
|
function groupRecallNodes(nodes) {
|
||||||
return {
|
return {
|
||||||
state: nodes.filter((n) => n.type === "character" || n.type === "location"),
|
state: nodes.filter((n) => n.type === "character" || n.type === "location"),
|
||||||
|
|||||||
189
schema.js
189
schema.js
@@ -5,8 +5,8 @@
|
|||||||
* 压缩模式
|
* 压缩模式
|
||||||
*/
|
*/
|
||||||
export const COMPRESSION_MODE = {
|
export const COMPRESSION_MODE = {
|
||||||
NONE: 'none',
|
NONE: "none",
|
||||||
HIERARCHICAL: 'hierarchical',
|
HIERARCHICAL: "hierarchical",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,13 +23,17 @@ export const COMPRESSION_MODE = {
|
|||||||
*/
|
*/
|
||||||
export const DEFAULT_NODE_SCHEMA = [
|
export const DEFAULT_NODE_SCHEMA = [
|
||||||
{
|
{
|
||||||
id: 'event',
|
id: "event",
|
||||||
label: '事件',
|
label: "事件",
|
||||||
tableName: 'event_table',
|
tableName: "event_table",
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'summary', hint: '事件摘要,包含因果关系和结果', required: true },
|
{ name: "summary", hint: "事件摘要,包含因果关系和结果", required: true },
|
||||||
{ name: 'participants', hint: '参与角色名,逗号分隔', required: false },
|
{ name: "participants", hint: "参与角色名,逗号分隔", required: false },
|
||||||
{ name: 'status', hint: '事件状态:ongoing/resolved/blocked', required: false },
|
{
|
||||||
|
name: "status",
|
||||||
|
hint: "事件状态:ongoing/resolved/blocked",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
alwaysInject: true,
|
alwaysInject: true,
|
||||||
latestOnly: false,
|
latestOnly: false,
|
||||||
@@ -40,20 +44,21 @@ export const DEFAULT_NODE_SCHEMA = [
|
|||||||
fanIn: 3,
|
fanIn: 3,
|
||||||
maxDepth: 10,
|
maxDepth: 10,
|
||||||
keepRecentLeaves: 6,
|
keepRecentLeaves: 6,
|
||||||
instruction: '将事件节点压缩为高价值的剧情里程碑摘要。保留因果关系、不可逆结果和未解决的伏笔。',
|
instruction:
|
||||||
|
"将事件节点压缩为高价值的剧情里程碑摘要。保留因果关系、不可逆结果和未解决的伏笔。",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'character',
|
id: "character",
|
||||||
label: '角色',
|
label: "角色",
|
||||||
tableName: 'character_table',
|
tableName: "character_table",
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'name', hint: '角色名(仅规范名称)', required: true },
|
{ name: "name", hint: "角色名(仅规范名称)", required: true },
|
||||||
{ name: 'traits', hint: '稳定的性格特征和外貌标记', required: false },
|
{ name: "traits", hint: "稳定的性格特征和外貌标记", required: false },
|
||||||
{ name: 'state', hint: '当前状态或处境', required: false },
|
{ name: "state", hint: "当前状态或处境", required: false },
|
||||||
{ name: 'goal', hint: '当前目标或动机', required: false },
|
{ name: "goal", hint: "当前目标或动机", required: false },
|
||||||
{ name: 'inventory', hint: '携带或拥有的关键物品', required: false },
|
{ name: "inventory", hint: "携带或拥有的关键物品", required: false },
|
||||||
{ name: 'core_note', hint: '值得长期记住的关键备注', required: false },
|
{ name: "core_note", hint: "值得长期记住的关键备注", required: false },
|
||||||
],
|
],
|
||||||
alwaysInject: false,
|
alwaysInject: false,
|
||||||
latestOnly: true,
|
latestOnly: true,
|
||||||
@@ -64,18 +69,18 @@ export const DEFAULT_NODE_SCHEMA = [
|
|||||||
fanIn: 0,
|
fanIn: 0,
|
||||||
maxDepth: 0,
|
maxDepth: 0,
|
||||||
keepRecentLeaves: 0,
|
keepRecentLeaves: 0,
|
||||||
instruction: '',
|
instruction: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'location',
|
id: "location",
|
||||||
label: '地点',
|
label: "地点",
|
||||||
tableName: 'location_table',
|
tableName: "location_table",
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'name', hint: '地点名称(仅规范名称)', required: true },
|
{ name: "name", hint: "地点名称(仅规范名称)", required: true },
|
||||||
{ name: 'state', hint: '当前状态或环境条件', required: false },
|
{ name: "state", hint: "当前状态或环境条件", required: false },
|
||||||
{ name: 'features', hint: '重要特征、资源或服务', required: false },
|
{ name: "features", hint: "重要特征、资源或服务", required: false },
|
||||||
{ name: 'danger', hint: '危险等级或威胁', required: false },
|
{ name: "danger", hint: "危险等级或威胁", required: false },
|
||||||
],
|
],
|
||||||
alwaysInject: false,
|
alwaysInject: false,
|
||||||
latestOnly: true,
|
latestOnly: true,
|
||||||
@@ -86,18 +91,22 @@ export const DEFAULT_NODE_SCHEMA = [
|
|||||||
fanIn: 0,
|
fanIn: 0,
|
||||||
maxDepth: 0,
|
maxDepth: 0,
|
||||||
keepRecentLeaves: 0,
|
keepRecentLeaves: 0,
|
||||||
instruction: '',
|
instruction: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'rule',
|
id: "rule",
|
||||||
label: '规则',
|
label: "规则",
|
||||||
tableName: 'rule_table',
|
tableName: "rule_table",
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'title', hint: '简短规则名', required: true },
|
{ name: "title", hint: "简短规则名", required: true },
|
||||||
{ name: 'constraint', hint: '不可违反的规则内容', required: true },
|
{ name: "constraint", hint: "不可违反的规则内容", required: true },
|
||||||
{ name: 'scope', hint: '适用范围/场景', required: false },
|
{ name: "scope", hint: "适用范围/场景", required: false },
|
||||||
{ name: 'status', hint: '当前有效性:active/suspended/revoked', required: false },
|
{
|
||||||
|
name: "status",
|
||||||
|
hint: "当前有效性:active/suspended/revoked",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
alwaysInject: true,
|
alwaysInject: true,
|
||||||
latestOnly: false,
|
latestOnly: false,
|
||||||
@@ -108,17 +117,21 @@ export const DEFAULT_NODE_SCHEMA = [
|
|||||||
fanIn: 0,
|
fanIn: 0,
|
||||||
maxDepth: 0,
|
maxDepth: 0,
|
||||||
keepRecentLeaves: 0,
|
keepRecentLeaves: 0,
|
||||||
instruction: '',
|
instruction: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'thread',
|
id: "thread",
|
||||||
label: '主线',
|
label: "主线",
|
||||||
tableName: 'thread_table',
|
tableName: "thread_table",
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'title', hint: '主线名称', required: true },
|
{ name: "title", hint: "主线名称", required: true },
|
||||||
{ name: 'summary', hint: '当前进展摘要', required: false },
|
{ name: "summary", hint: "当前进展摘要", required: false },
|
||||||
{ name: 'status', hint: '状态:active/completed/abandoned', required: false },
|
{
|
||||||
|
name: "status",
|
||||||
|
hint: "状态:active/completed/abandoned",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
alwaysInject: true,
|
alwaysInject: true,
|
||||||
latestOnly: false,
|
latestOnly: false,
|
||||||
@@ -129,17 +142,21 @@ export const DEFAULT_NODE_SCHEMA = [
|
|||||||
fanIn: 3,
|
fanIn: 3,
|
||||||
maxDepth: 5,
|
maxDepth: 5,
|
||||||
keepRecentLeaves: 3,
|
keepRecentLeaves: 3,
|
||||||
instruction: '将主线节点压缩为阶段性进展摘要。保留关键转折和当前目标。',
|
instruction: "将主线节点压缩为阶段性进展摘要。保留关键转折和当前目标。",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// ====== v2 新增节点类型 ======
|
// ====== v2 新增节点类型 ======
|
||||||
{
|
{
|
||||||
id: 'synopsis',
|
id: "synopsis",
|
||||||
label: '全局概要',
|
label: "全局概要",
|
||||||
tableName: 'synopsis_table',
|
tableName: "synopsis_table",
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'summary', hint: '当前故事的全局概要(前情提要)', required: true },
|
{
|
||||||
{ name: 'scope', hint: '概要覆盖的楼层范围', required: false },
|
name: "summary",
|
||||||
|
hint: "当前故事的全局概要(前情提要)",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{ name: "scope", hint: "概要覆盖的楼层范围", required: false },
|
||||||
],
|
],
|
||||||
alwaysInject: true, // 常驻注入(MemoRAG 启发)
|
alwaysInject: true, // 常驻注入(MemoRAG 启发)
|
||||||
latestOnly: true, // 只保留最新版本
|
latestOnly: true, // 只保留最新版本
|
||||||
@@ -150,17 +167,17 @@ export const DEFAULT_NODE_SCHEMA = [
|
|||||||
fanIn: 0,
|
fanIn: 0,
|
||||||
maxDepth: 0,
|
maxDepth: 0,
|
||||||
keepRecentLeaves: 0,
|
keepRecentLeaves: 0,
|
||||||
instruction: '',
|
instruction: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'reflection',
|
id: "reflection",
|
||||||
label: '反思',
|
label: "反思",
|
||||||
tableName: 'reflection_table',
|
tableName: "reflection_table",
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'insight', hint: '对角色行为或情节的元认知反思', required: true },
|
{ name: "insight", hint: "对角色行为或情节的元认知反思", required: true },
|
||||||
{ name: 'trigger', hint: '触发反思的事件/矛盾', required: false },
|
{ name: "trigger", hint: "触发反思的事件/矛盾", required: false },
|
||||||
{ name: 'suggestion', hint: '对后续叙事的建议', required: false },
|
{ name: "suggestion", hint: "对后续叙事的建议", required: false },
|
||||||
],
|
],
|
||||||
alwaysInject: false, // 需要被召回
|
alwaysInject: false, // 需要被召回
|
||||||
latestOnly: false,
|
latestOnly: false,
|
||||||
@@ -171,7 +188,7 @@ export const DEFAULT_NODE_SCHEMA = [
|
|||||||
fanIn: 3,
|
fanIn: 3,
|
||||||
maxDepth: 3,
|
maxDepth: 3,
|
||||||
keepRecentLeaves: 3,
|
keepRecentLeaves: 3,
|
||||||
instruction: '将反思条目合并为高层次的叙事指导原则。',
|
instruction: "将反思条目合并为高层次的叙事指导原则。",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -180,14 +197,14 @@ export const DEFAULT_NODE_SCHEMA = [
|
|||||||
* 规范化的关系类型
|
* 规范化的关系类型
|
||||||
*/
|
*/
|
||||||
export const RELATION_TYPES = [
|
export const RELATION_TYPES = [
|
||||||
'related', // 一般关联
|
"related", // 一般关联
|
||||||
'involved_in', // 参与事件
|
"involved_in", // 参与事件
|
||||||
'occurred_at', // 发生于地点
|
"occurred_at", // 发生于地点
|
||||||
'advances', // 推进主线
|
"advances", // 推进主线
|
||||||
'updates', // 更新实体状态
|
"updates", // 更新实体状态
|
||||||
'contradicts', // 矛盾/冲突(用于抑制边)
|
"contradicts", // 矛盾/冲突(用于抑制边)
|
||||||
'evolves', // A-MEM 进化链接(新→旧)
|
"evolves", // A-MEM 进化链接(新→旧)
|
||||||
'temporal_update', // 时序更新(Graphiti:新状态替代旧状态)
|
"temporal_update", // 时序更新(Graphiti:新状态替代旧状态)
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,7 +216,7 @@ export function validateSchema(schema) {
|
|||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
if (!Array.isArray(schema) || schema.length === 0) {
|
if (!Array.isArray(schema) || schema.length === 0) {
|
||||||
errors.push('Schema 必须是非空数组');
|
errors.push("Schema 必须是非空数组");
|
||||||
return { valid: false, errors };
|
return { valid: false, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,8 +224,13 @@ export function validateSchema(schema) {
|
|||||||
const tableNames = new Set();
|
const tableNames = new Set();
|
||||||
|
|
||||||
for (const type of schema) {
|
for (const type of schema) {
|
||||||
if (!type.id || typeof type.id !== 'string') {
|
if (!type || typeof type !== "object") {
|
||||||
errors.push('每种类型必须有 id');
|
errors.push("Schema 类型定义必须是对象");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!type.id || typeof type.id !== "string") {
|
||||||
|
errors.push("每种类型必须有 id");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +239,11 @@ export function validateSchema(schema) {
|
|||||||
}
|
}
|
||||||
ids.add(type.id);
|
ids.add(type.id);
|
||||||
|
|
||||||
if (!type.tableName || typeof type.tableName !== 'string') {
|
if (!type.label || typeof type.label !== "string") {
|
||||||
|
errors.push(`类型 ${type.id}:缺少 label`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!type.tableName || typeof type.tableName !== "string") {
|
||||||
errors.push(`类型 ${type.id}:缺少 tableName`);
|
errors.push(`类型 ${type.id}:缺少 tableName`);
|
||||||
} else if (tableNames.has(type.tableName)) {
|
} else if (tableNames.has(type.tableName)) {
|
||||||
errors.push(`表名重复:${type.tableName}`);
|
errors.push(`表名重复:${type.tableName}`);
|
||||||
@@ -227,12 +253,37 @@ export function validateSchema(schema) {
|
|||||||
|
|
||||||
if (!Array.isArray(type.columns) || type.columns.length === 0) {
|
if (!Array.isArray(type.columns) || type.columns.length === 0) {
|
||||||
errors.push(`类型 ${type.id}:至少需要一个列`);
|
errors.push(`类型 ${type.id}:至少需要一个列`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasRequired = type.columns?.some(c => c.required);
|
const columnNames = new Set();
|
||||||
|
for (const column of type.columns) {
|
||||||
|
if (!column?.name || typeof column.name !== "string") {
|
||||||
|
errors.push(`类型 ${type.id}:存在缺少 name 的列定义`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (columnNames.has(column.name)) {
|
||||||
|
errors.push(`类型 ${type.id}:列名重复 ${column.name}`);
|
||||||
|
}
|
||||||
|
columnNames.add(column.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasRequired = type.columns.some((c) => c?.required);
|
||||||
if (!hasRequired) {
|
if (!hasRequired) {
|
||||||
errors.push(`类型 ${type.id}:至少需要一个 required 列`);
|
errors.push(`类型 ${type.id}:至少需要一个 required 列`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type.latestOnly) {
|
||||||
|
const hasPrimaryLikeField = ["name", "title", "summary"].some(
|
||||||
|
(fieldName) =>
|
||||||
|
type.columns.some((column) => column?.name === fieldName),
|
||||||
|
);
|
||||||
|
if (!hasPrimaryLikeField) {
|
||||||
|
errors.push(
|
||||||
|
`类型 ${type.id}:latestOnly 类型至少需要 name/title/summary 之一作为主标识字段`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: errors.length === 0, errors };
|
return { valid: errors.length === 0, errors };
|
||||||
@@ -245,5 +296,5 @@ export function validateSchema(schema) {
|
|||||||
* @returns {object|null}
|
* @returns {object|null}
|
||||||
*/
|
*/
|
||||||
export function getSchemaType(schema, typeId) {
|
export function getSchemaType(schema, typeId) {
|
||||||
return schema.find(t => t.id === typeId) || null;
|
return schema.find((t) => t.id === typeId) || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,22 @@
|
|||||||
id="st_bme_extract_every"
|
id="st_bme_extract_every"
|
||||||
class="text_pole"
|
class="text_pole"
|
||||||
min="1"
|
min="1"
|
||||||
max="10"
|
max="50"
|
||||||
value="1"
|
value="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="st-bme-row">
|
||||||
|
<label for="st_bme_extract_context_turns">提取上下文轮数</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_extract_context_turns"
|
||||||
|
class="text_pole"
|
||||||
|
min="0"
|
||||||
|
max="20"
|
||||||
|
value="2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="st-bme-hr" />
|
<hr class="st-bme-hr" />
|
||||||
@@ -49,6 +61,30 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="st-bme-row">
|
||||||
|
<label for="st_bme_recall_top_k">召回候选上限</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_recall_top_k"
|
||||||
|
class="text_pole"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value="15"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="st-bme-row">
|
||||||
|
<label for="st_bme_recall_max_nodes">LLM 精确召回上限</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_recall_max_nodes"
|
||||||
|
class="text_pole"
|
||||||
|
min="1"
|
||||||
|
max="50"
|
||||||
|
value="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label for="st_bme_inject_depth">注入深度</label>
|
<label for="st_bme_inject_depth">注入深度</label>
|
||||||
<input
|
<input
|
||||||
@@ -138,6 +174,18 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="st-bme-row st-bme-indent">
|
||||||
|
<label for="st_bme_evo_consolidate_every">整理频率</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_evo_consolidate_every"
|
||||||
|
class="text_pole"
|
||||||
|
min="1"
|
||||||
|
max="500"
|
||||||
|
value="50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label class="checkbox_label" for="st_bme_precise_conflict">
|
<label class="checkbox_label" for="st_bme_precise_conflict">
|
||||||
<input type="checkbox" id="st_bme_precise_conflict" />
|
<input type="checkbox" id="st_bme_precise_conflict" />
|
||||||
@@ -215,6 +263,18 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="st-bme-row st-bme-indent">
|
||||||
|
<label for="st_bme_smart_trigger_threshold">触发阈值</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_smart_trigger_threshold"
|
||||||
|
class="text_pole"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
value="2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label class="checkbox_label" for="st_bme_sleep_cycle">
|
<label class="checkbox_label" for="st_bme_sleep_cycle">
|
||||||
<input type="checkbox" id="st_bme_sleep_cycle" />
|
<input type="checkbox" id="st_bme_sleep_cycle" />
|
||||||
@@ -233,6 +293,17 @@
|
|||||||
value="0.5"
|
value="0.5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="st-bme-row st-bme-indent">
|
||||||
|
<label for="st_bme_sleep_every">每 N 次提取遗忘</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="st_bme_sleep_every"
|
||||||
|
class="text_pole"
|
||||||
|
min="1"
|
||||||
|
max="200"
|
||||||
|
value="10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="st-bme-row">
|
<div class="st-bme-row">
|
||||||
<label class="checkbox_label" for="st_bme_prob_recall">
|
<label class="checkbox_label" for="st_bme_prob_recall">
|
||||||
|
|||||||
76
tests/graph-retrieval.mjs
Normal file
76
tests/graph-retrieval.mjs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { diffuseAndRank } from "../diffusion.js";
|
||||||
|
import {
|
||||||
|
addEdge,
|
||||||
|
addNode,
|
||||||
|
buildTemporalAdjacencyMap,
|
||||||
|
createEdge,
|
||||||
|
createEmptyGraph,
|
||||||
|
createNode,
|
||||||
|
invalidateEdge,
|
||||||
|
} from "../graph.js";
|
||||||
|
|
||||||
|
const graph = createEmptyGraph();
|
||||||
|
|
||||||
|
const event1 = createNode({
|
||||||
|
type: "event",
|
||||||
|
seq: 1,
|
||||||
|
fields: { summary: "初始事件" },
|
||||||
|
importance: 5,
|
||||||
|
});
|
||||||
|
const event2 = createNode({
|
||||||
|
type: "event",
|
||||||
|
seq: 2,
|
||||||
|
fields: { summary: "后续事件" },
|
||||||
|
importance: 6,
|
||||||
|
});
|
||||||
|
const character = createNode({
|
||||||
|
type: "character",
|
||||||
|
seq: 2,
|
||||||
|
fields: { name: "艾琳", state: "警觉" },
|
||||||
|
importance: 7,
|
||||||
|
});
|
||||||
|
|
||||||
|
addNode(graph, event1);
|
||||||
|
addNode(graph, event2);
|
||||||
|
addNode(graph, character);
|
||||||
|
|
||||||
|
const currentEdge = createEdge({
|
||||||
|
fromId: event2.id,
|
||||||
|
toId: character.id,
|
||||||
|
relation: "involved_in",
|
||||||
|
strength: 0.9,
|
||||||
|
});
|
||||||
|
assert.ok(addEdge(graph, currentEdge));
|
||||||
|
|
||||||
|
const historicalEdge = createEdge({
|
||||||
|
fromId: event1.id,
|
||||||
|
toId: character.id,
|
||||||
|
relation: "involved_in",
|
||||||
|
strength: 0.4,
|
||||||
|
});
|
||||||
|
assert.ok(addEdge(graph, historicalEdge));
|
||||||
|
invalidateEdge(historicalEdge);
|
||||||
|
|
||||||
|
const replacementEdge = createEdge({
|
||||||
|
fromId: event1.id,
|
||||||
|
toId: character.id,
|
||||||
|
relation: "involved_in",
|
||||||
|
strength: 0.7,
|
||||||
|
});
|
||||||
|
assert.ok(addEdge(graph, replacementEdge));
|
||||||
|
assert.notEqual(replacementEdge.id, historicalEdge.id);
|
||||||
|
|
||||||
|
const adjacencyMap = buildTemporalAdjacencyMap(graph);
|
||||||
|
const event1Neighbors = adjacencyMap.get(event1.id) || [];
|
||||||
|
assert.equal(event1Neighbors.length, 1);
|
||||||
|
assert.equal(event1Neighbors[0].targetId, character.id);
|
||||||
|
assert.equal(event1Neighbors[0].strength, 0.7);
|
||||||
|
|
||||||
|
const diffusion = diffuseAndRank(adjacencyMap, [
|
||||||
|
{ id: event2.id, energy: 1 },
|
||||||
|
{ id: event2.id, energy: 0.5 },
|
||||||
|
]);
|
||||||
|
assert.ok(diffusion.some((item) => item.nodeId === character.id));
|
||||||
|
|
||||||
|
console.log("graph-retrieval tests passed");
|
||||||
@@ -1,101 +1,35 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import vm from "node:vm";
|
||||||
|
|
||||||
function getSmartTriggerDecision(chat, lastProcessed, settings) {
|
async function loadSmartTriggerDecision() {
|
||||||
const DEFAULT_TRIGGER_KEYWORDS = [
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
"突然",
|
const indexPath = path.resolve(__dirname, "../index.js");
|
||||||
"没想到",
|
const source = await fs.readFile(indexPath, "utf8");
|
||||||
"原来",
|
const keywordMatch = source.match(
|
||||||
"其实",
|
/const DEFAULT_TRIGGER_KEYWORDS = \[[\s\S]*?\];/m,
|
||||||
"发现",
|
|
||||||
"背叛",
|
|
||||||
"死亡",
|
|
||||||
"复活",
|
|
||||||
"恢复记忆",
|
|
||||||
"失忆",
|
|
||||||
"告白",
|
|
||||||
"暴露",
|
|
||||||
"秘密",
|
|
||||||
"计划",
|
|
||||||
"规则",
|
|
||||||
"契约",
|
|
||||||
"位置",
|
|
||||||
"地点",
|
|
||||||
"离开",
|
|
||||||
"来到",
|
|
||||||
];
|
|
||||||
|
|
||||||
const pendingMessages = chat
|
|
||||||
.slice(lastProcessed + 1)
|
|
||||||
.filter((msg) => !msg.is_system)
|
|
||||||
.map((msg) => ({
|
|
||||||
role: msg.is_user ? "user" : "assistant",
|
|
||||||
content: msg.mes || "",
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (pendingMessages.length === 0) {
|
|
||||||
return { triggered: false, score: 0, reasons: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const reasons = [];
|
|
||||||
let score = 0;
|
|
||||||
const combinedText = pendingMessages.map((m) => m.content).join("\n");
|
|
||||||
|
|
||||||
const keywordHits = DEFAULT_TRIGGER_KEYWORDS.filter((keyword) =>
|
|
||||||
combinedText.includes(keyword),
|
|
||||||
);
|
);
|
||||||
if (keywordHits.length > 0) {
|
const fnMatch = source.match(
|
||||||
score += Math.min(2, keywordHits.length);
|
/export function getSmartTriggerDecision\(chat, lastProcessed, settings\) \{[\s\S]*?^\}/m,
|
||||||
reasons.push(`关键词: ${keywordHits.slice(0, 3).join(", ")}`);
|
);
|
||||||
|
|
||||||
|
if (!keywordMatch || !fnMatch) {
|
||||||
|
throw new Error("无法从 index.js 提取 smart trigger 实现");
|
||||||
}
|
}
|
||||||
|
|
||||||
const customPatterns = String(settings.triggerPatterns || "")
|
const context = vm.createContext({});
|
||||||
.split(/\r?\n|,/)
|
const script = new vm.Script(`
|
||||||
.map((s) => s.trim())
|
${keywordMatch[0]}
|
||||||
.filter(Boolean);
|
${fnMatch[0].replace("export function", "function")}
|
||||||
for (const pattern of customPatterns) {
|
this.getSmartTriggerDecision = getSmartTriggerDecision;
|
||||||
try {
|
`);
|
||||||
const regex = new RegExp(pattern, "i");
|
script.runInContext(context);
|
||||||
if (regex.test(combinedText)) {
|
return context.getSmartTriggerDecision;
|
||||||
score += 2;
|
|
||||||
reasons.push(`自定义触发: ${pattern}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore invalid regex
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleSwitchCount = pendingMessages.reduce((count, message, index) => {
|
const getSmartTriggerDecision = await loadSmartTriggerDecision();
|
||||||
if (index === 0) return count;
|
|
||||||
return count + (message.role !== pendingMessages[index - 1].role ? 1 : 0);
|
|
||||||
}, 0);
|
|
||||||
if (roleSwitchCount >= 2) {
|
|
||||||
score += 1;
|
|
||||||
reasons.push("多轮往返互动");
|
|
||||||
}
|
|
||||||
|
|
||||||
const punctuationHits = (combinedText.match(/[!?!?]/g) || []).length;
|
|
||||||
if (punctuationHits >= 2) {
|
|
||||||
score += 1;
|
|
||||||
reasons.push("情绪/冲突波动");
|
|
||||||
}
|
|
||||||
|
|
||||||
const entityLikeHits =
|
|
||||||
combinedText.match(
|
|
||||||
/[A-Z][a-z]{2,}|[\u4e00-\u9fff]{2,6}(先生|小姐|王国|城|镇|村|学院|组织|公司|小队|军团)/g,
|
|
||||||
) || [];
|
|
||||||
if (entityLikeHits.length > 0) {
|
|
||||||
score += 1;
|
|
||||||
reasons.push("疑似新实体/新地点");
|
|
||||||
}
|
|
||||||
|
|
||||||
const threshold = Math.max(1, settings.smartTriggerThreshold || 2);
|
|
||||||
return {
|
|
||||||
triggered: score >= threshold,
|
|
||||||
score,
|
|
||||||
reasons,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const noTrigger = getSmartTriggerDecision(
|
const noTrigger = getSmartTriggerDecision(
|
||||||
[
|
[
|
||||||
@@ -129,4 +63,29 @@ const customTrigger = getSmartTriggerDecision(
|
|||||||
assert.equal(customTrigger.triggered, true);
|
assert.equal(customTrigger.triggered, true);
|
||||||
assert.ok(customTrigger.reasons.some((r) => r.includes("自定义触发")));
|
assert.ok(customTrigger.reasons.some((r) => r.includes("自定义触发")));
|
||||||
|
|
||||||
|
const ignoresProcessedMessages = getSmartTriggerDecision(
|
||||||
|
[
|
||||||
|
{ is_user: true, mes: "之前突然出现了秘密。" },
|
||||||
|
{ is_user: false, mes: "这已经处理过。" },
|
||||||
|
{ is_user: true, mes: "现在只是平静地走路。" },
|
||||||
|
{ is_user: false, mes: "没有新的异常。" },
|
||||||
|
],
|
||||||
|
1,
|
||||||
|
{ triggerPatterns: "", smartTriggerThreshold: 2 },
|
||||||
|
);
|
||||||
|
assert.equal(ignoresProcessedMessages.triggered, false);
|
||||||
|
assert.equal(ignoresProcessedMessages.score, 0);
|
||||||
|
|
||||||
|
const ignoresBlankAndInvalidRegex = getSmartTriggerDecision(
|
||||||
|
[
|
||||||
|
{ is_system: true, mes: "系统消息" },
|
||||||
|
{ is_user: true, mes: " " },
|
||||||
|
{ is_user: false, mes: "Alpha城发生了什么?!" },
|
||||||
|
],
|
||||||
|
-1,
|
||||||
|
{ triggerPatterns: "([\n真相", smartTriggerThreshold: 2 },
|
||||||
|
);
|
||||||
|
assert.equal(ignoresBlankAndInvalidRegex.triggered, true);
|
||||||
|
assert.ok(ignoresBlankAndInvalidRegex.reasons.includes("情绪/冲突波动"));
|
||||||
|
|
||||||
console.log("smart-trigger tests passed");
|
console.log("smart-trigger tests passed");
|
||||||
|
|||||||
Reference in New Issue
Block a user