From 90e942aed958652613eb1cf8edb9dc8649ca5250 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Mon, 23 Mar 2026 12:29:09 +0800 Subject: [PATCH] feat: add reflective memory and scene reconstruction --- README.md | 47 ++- extractor.js | 945 ++++++++++++++++++++++++++++----------------- index.js | 1038 ++++++++++++++++++++++++++++---------------------- injector.js | 151 +++++--- retriever.js | 708 ++++++++++++++++++++-------------- 5 files changed, 1718 insertions(+), 1171 deletions(-) diff --git a/README.md b/README.md index 3371835..db831dc 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,12 @@ ST-BME(ST-Bionic-Memory-Ecology)是一个运行在 SillyTavern 第三方扩 - **聊天级持久化**:图状态写入当前聊天 `chat_metadata`,可随聊天保存与恢复。 - **自动记忆提取**:按设定频率从最近对话中抽取结构化操作并更新图谱。 - **三阶段召回编排**:支持向量预筛、图扩散排序、混合评分与可选 LLM 精确召回。 +- **情景重构召回**:会围绕命中的事件、角色、地点自动补齐相邻场景节点,使注入结果更接近连续剧情记忆。 - **层级压缩**:对事件与主线等类型执行分层摘要,控制图谱膨胀。 - **全局概要节点**:周期性生成 `synopsis` 类型节点,作为长期叙事锚点。 - **记忆进化**:新节点写入后,基于近邻分析回溯更新已有记忆并建立新连接。 +- **时序更新追踪增强**:节点更新时会补充 `updates` / `temporal_update` 语义链路,并生成可追踪的状态更新事件。 +- **反思条目生成**:支持按提取周期生成 `reflection` 节点,用于沉淀高层叙事结论、关系趋势与后续建议。 - **时序边字段**:关系边携带 `validAt` / `invalidAt` / `expiredAt` 等时间语义字段。 - **导入导出与手动操作入口**:设置面板已提供查看图谱、查看注入、重建、压缩、导入、导出等入口。 @@ -34,6 +37,7 @@ ST-BME(ST-Bionic-Memory-Ecology)是一个运行在 SillyTavern 第三方扩 - **Mem0 风格精确对照**:新记忆可与近邻旧记忆对照后再决定新增、更新或跳过。 - **认知边界过滤**:可按可见性约束过滤检索结果,适用于“角色不知道的信息不应注入”的场景。 - **交叉检索**:实体命中后沿图边扩展相关事件节点,补充情境上下文。 +- **分桶式注入编排**:召回结果会按“当前状态 / 情景事件 / 反思锚点 / 规则约束”分组组织,降低碎片化注入。 - **主动遗忘**:按保留价值归档低价值节点,缓解长期运行后的图谱膨胀。 - **概率触发回忆**:未被主流程命中的高重要性节点有概率被额外召回。 @@ -42,7 +46,6 @@ ST-BME(ST-Bionic-Memory-Ecology)是一个运行在 SillyTavern 第三方扩 以下方向在现有代码中仅有预留开关、设计痕迹或路线图描述,尚不应视为完整能力: - **惊奇度分割 / 智能触发提取** -- **反思条目自动生成** - **完整端到端测试与稳定性验证** - **图谱可视化面板** - **自定义 Schema 编辑体验增强** @@ -173,17 +176,17 @@ SillyTavern/public/scripts/extensions/third-party/ST-BME/ ### 增强能力开关 -| 能力 | 默认状态 | 当前判断 | -| ------------ | -------- | ----------------- | -| 记忆进化 | 开启 | 已实现 | -| 精确对照 | 开启 | 实验性 | -| 全局概要 | 开启 | 已实现 | -| 认知边界 | 关闭 | 实验性 | -| 交叉检索 | 关闭 | 实验性 | -| 惊奇度分割 | 关闭 | 规划中 / 预留开关 | -| 主动遗忘 | 关闭 | 实验性 | -| 概率触发回忆 | 关闭 | 实验性 | -| 反思条目 | 关闭 | 规划中 / 部分预留 | +| 能力 | 默认状态 | 当前判断 | +| ------------ | -------- | -------------------------- | +| 记忆进化 | 开启 | 已实现 | +| 精确对照 | 开启 | 实验性 | +| 全局概要 | 开启 | 已实现 | +| 认知边界 | 关闭 | 实验性 | +| 交叉检索 | 关闭 | 实验性 | +| 惊奇度分割 | 关闭 | 规划中 / 预留开关 | +| 主动遗忘 | 关闭 | 实验性 | +| 概率触发回忆 | 关闭 | 实验性 | +| 反思条目 | 关闭 | 已实现(建议先小规模验证) | ## 数据模型 @@ -203,15 +206,15 @@ SillyTavern/public/scripts/extensions/third-party/ST-BME/ 默认 Schema 定义在 [`schema.js`](ST-BME/schema.js)。当前内置节点类型包括: -| 类型 | 用途 | 当前状态 | -| ------------ | -------------------- | ------------------------------ | -| `event` | 事件、动作、剧情推进 | 已实现 | -| `character` | 角色状态快照 | 已实现 | -| `location` | 地点与环境状态 | 已实现 | -| `rule` | 世界规则、约束与设定 | 已实现 | -| `thread` | 主线与阶段性进度 | 已实现 | -| `synopsis` | 全局前情提要 | 已实现 | -| `reflection` | 反思与元认知记录 | 结构已定义,生成流程未完整落地 | +| 类型 | 用途 | 当前状态 | +| ------------ | -------------------- | ------------------------------------ | +| `event` | 事件、动作、剧情推进 | 已实现 | +| `character` | 角色状态快照 | 已实现 | +| `location` | 地点与环境状态 | 已实现 | +| `rule` | 世界规则、约束与设定 | 已实现 | +| `thread` | 主线与阶段性进度 | 已实现 | +| `synopsis` | 全局前情提要 | 已实现 | +| `reflection` | 反思与元认知记录 | 已实现周期生成,建议结合实际剧情验证 | ### 关系类型 @@ -277,12 +280,14 @@ SillyTavern/public/scripts/extensions/third-party/ST-BME/ - 交叉检索对召回质量的增益 - 主动遗忘的阈值与副作用控制 - 概率触发回忆的叙事收益与噪声控制 +- 情景重构召回在大图、复杂多线叙事中的排序稳定性 ### 规划中 - 惊奇度驱动的提取触发机制 - 反思节点自动生成闭环 - 更完整的可视化与调试工具 +- 智能触发提取与惊奇度分割的进一步增强 - 更系统的 benchmark、回归测试与使用文档 ## 路线图 diff --git a/extractor.js b/extractor.js index af598db..80e6a74 100644 --- a/extractor.js +++ b/extractor.js @@ -2,10 +2,20 @@ // 分析对话 → 提取节点和关系 → 更新图谱 // v2: 融合 Mem0 精确对照 + Graphiti 时序边 + MemoRAG 全局概要 -import { createNode, addNode, updateNode, findLatestNode, createEdge, addEdge, getActiveNodes, invalidateEdge } from './graph.js'; -import { embedText, embedBatch, searchSimilar } from './embedding.js'; -import { callLLMForJSON } from './llm.js'; -import { RELATION_TYPES } from './schema.js'; +import { embedBatch, embedText, searchSimilar } from "./embedding.js"; +import { + addEdge, + addNode, + createEdge, + createNode, + findLatestNode, + getActiveNodes, + getNode, + invalidateEdge, + updateNode, +} from "./graph.js"; +import { callLLMForJSON } from "./llm.js"; +import { RELATION_TYPES } from "./schema.js"; /** * 对未处理的对话楼层执行记忆提取 @@ -21,344 +31,464 @@ import { RELATION_TYPES } from './schema.js'; * @returns {Promise<{success: boolean, newNodes: number, updatedNodes: number, newEdges: number, newNodeIds: string[]}>} */ export async function extractMemories({ - graph, - messages, - startSeq, - schema, - embeddingConfig, - extractPrompt, - v2Options = {}, + graph, + messages, + startSeq, + schema, + embeddingConfig, + extractPrompt, + v2Options = {}, }) { - if (!messages || messages.length === 0) { - return { success: true, newNodes: 0, updatedNodes: 0, newEdges: 0, newNodeIds: [] }; - } + if (!messages || messages.length === 0) { + return { + success: true, + newNodes: 0, + updatedNodes: 0, + newEdges: 0, + newNodeIds: [], + }; + } - const enablePreciseConflict = v2Options.enablePreciseConflict ?? true; - const conflictThreshold = v2Options.conflictThreshold ?? 0.85; + const enablePreciseConflict = v2Options.enablePreciseConflict ?? true; + const conflictThreshold = v2Options.conflictThreshold ?? 0.85; - console.log(`[ST-BME] 提取开始: 楼层 ${startSeq}, ${messages.length} 条消息`); + console.log(`[ST-BME] 提取开始: 楼层 ${startSeq}, ${messages.length} 条消息`); - // 构建对话文本 - const dialogueText = messages - .map(m => `[${m.role}]: ${m.content}`) - .join('\n\n'); + // 构建对话文本 + const dialogueText = messages + .map((m) => `[${m.role}]: ${m.content}`) + .join("\n\n"); - // 构建当前图概览(让 LLM 知道已有哪些节点,避免重复) - const graphOverview = buildGraphOverview(graph, schema); + // 构建当前图概览(让 LLM 知道已有哪些节点,避免重复) + const graphOverview = buildGraphOverview(graph, schema); - // 构建 Schema 描述 - const schemaDescription = buildSchemaDescription(schema); + // 构建 Schema 描述 + const schemaDescription = buildSchemaDescription(schema); - // 系统提示词 - const systemPrompt = extractPrompt || buildDefaultExtractPrompt(schema); + // 系统提示词 + const systemPrompt = extractPrompt || buildDefaultExtractPrompt(schema); - // 用户提示词 - const userPrompt = [ - '## 当前对话内容(需提取记忆)', - dialogueText, - '', - '## 当前图谱状态', - graphOverview || '(空图谱,尚无节点)', - '', - '## 节点类型定义', - schemaDescription, - '', - '请分析对话,按 JSON 格式输出操作列表。', - ].join('\n'); + // 用户提示词 + const userPrompt = [ + "## 当前对话内容(需提取记忆)", + dialogueText, + "", + "## 当前图谱状态", + graphOverview || "(空图谱,尚无节点)", + "", + "## 节点类型定义", + schemaDescription, + "", + "请分析对话,按 JSON 格式输出操作列表。", + ].join("\n"); - // 调用 LLM - const result = await callLLMForJSON({ systemPrompt, userPrompt, maxRetries: 2 }); + // 调用 LLM + const result = await callLLMForJSON({ + systemPrompt, + userPrompt, + maxRetries: 2, + }); - if (!result || !result.operations) { - console.warn('[ST-BME] 提取 LLM 未返回有效操作'); - return { success: false, newNodes: 0, updatedNodes: 0, newEdges: 0, newNodeIds: [] }; - } + if (!result || !result.operations) { + console.warn("[ST-BME] 提取 LLM 未返回有效操作"); + return { + success: false, + newNodes: 0, + updatedNodes: 0, + newEdges: 0, + newNodeIds: [], + }; + } - // ========== v2: Mem0 精确对照阶段 ========== - if (enablePreciseConflict && embeddingConfig?.apiUrl) { - await mem0ConflictCheck(graph, result.operations, embeddingConfig, conflictThreshold); - } + // ========== v2: Mem0 精确对照阶段 ========== + if (enablePreciseConflict && embeddingConfig?.apiUrl) { + await mem0ConflictCheck( + graph, + result.operations, + embeddingConfig, + conflictThreshold, + ); + } - // 执行操作 - const stats = { newNodes: 0, updatedNodes: 0, newEdges: 0 }; - const newNodeIds = []; // v2: 收集新建节点 ID(用于进化引擎) - const refMap = new Map(); + // 执行操作 + const stats = { newNodes: 0, updatedNodes: 0, newEdges: 0 }; + const newNodeIds = []; // v2: 收集新建节点 ID(用于进化引擎) + const refMap = new Map(); - for (const op of result.operations) { - try { - switch (op.action) { - case 'create': { - const createdId = handleCreate(graph, op, startSeq, schema, refMap, stats); - if (createdId) newNodeIds.push(createdId); - break; - } - case 'update': - handleUpdate(graph, op, stats); - break; - case 'delete': - handleDelete(graph, op, stats); - break; - case '_skip': - // Mem0 对照判定为重复,跳过 - break; - default: - console.warn(`[ST-BME] 未知操作类型: ${op.action}`); - } - } catch (e) { - console.error(`[ST-BME] 操作执行失败:`, op, e); + for (const op of result.operations) { + try { + switch (op.action) { + case "create": { + const createdId = handleCreate( + graph, + op, + startSeq, + schema, + refMap, + stats, + ); + if (createdId) newNodeIds.push(createdId); + break; } + case "update": + handleUpdate(graph, op, stats); + break; + case "delete": + handleDelete(graph, op, stats); + break; + case "_skip": + // Mem0 对照判定为重复,跳过 + break; + default: + console.warn(`[ST-BME] 未知操作类型: ${op.action}`); + } + } catch (e) { + console.error(`[ST-BME] 操作执行失败:`, op, e); } + } - // 为新建节点生成 embedding - await generateNodeEmbeddings(graph, embeddingConfig); + // 为新建节点生成 embedding + await generateNodeEmbeddings(graph, embeddingConfig); - // 更新处理进度 - graph.lastProcessedSeq = startSeq + messages.filter(m => m.role === 'assistant').length; + // 更新处理进度 + graph.lastProcessedSeq = + startSeq + messages.filter((m) => m.role === "assistant").length; - console.log(`[ST-BME] 提取完成: 新建 ${stats.newNodes}, 更新 ${stats.updatedNodes}, 新边 ${stats.newEdges}`); + console.log( + `[ST-BME] 提取完成: 新建 ${stats.newNodes}, 更新 ${stats.updatedNodes}, 新边 ${stats.newEdges}`, + ); - return { success: true, ...stats, newNodeIds }; + return { success: true, ...stats, newNodeIds }; } /** * 处理 create 操作 */ function handleCreate(graph, op, seq, schema, refMap, stats) { - const typeDef = schema.find(s => s.id === op.type); - if (!typeDef) { - console.warn(`[ST-BME] 未知节点类型: ${op.type}`); - return null; + const typeDef = schema.find((s) => s.id === op.type); + if (!typeDef) { + console.warn(`[ST-BME] 未知节点类型: ${op.type}`); + return null; + } + + // latestOnly 类型:检查是否已存在同名节点 + if (typeDef.latestOnly && op.fields?.name) { + const existing = findLatestNode(graph, op.type, op.fields.name); + if (existing) { + // 转为更新操作 + updateNode(graph, existing.id, { fields: op.fields, seq }); + stats.updatedNodes++; + + if (op.ref) refMap.set(op.ref, existing.id); + + // 处理关联边 + if (op.links) { + handleLinks(graph, existing.id, op.links, refMap, stats); + } + return null; } + } - // latestOnly 类型:检查是否已存在同名节点 - if (typeDef.latestOnly && op.fields?.name) { - const existing = findLatestNode(graph, op.type, op.fields.name); - if (existing) { - // 转为更新操作 - updateNode(graph, existing.id, { fields: op.fields, seq }); - stats.updatedNodes++; + // 创建新节点 + const node = createNode({ + type: op.type, + fields: op.fields || {}, + seq, + importance: op.importance ?? 5.0, + clusters: op.clusters || [], + }); - if (op.ref) refMap.set(op.ref, existing.id); + addNode(graph, node); + stats.newNodes++; - // 处理关联边 - if (op.links) { - handleLinks(graph, existing.id, op.links, refMap, stats); - } - return null; - } - } + // 保存 ref 用于同批次引用 + if (op.ref) { + refMap.set(op.ref, node.id); + } - // 创建新节点 - const node = createNode({ - type: op.type, - fields: op.fields || {}, - seq, - importance: op.importance ?? 5.0, - clusters: op.clusters || [], - }); + // 处理关联边 + if (op.links) { + handleLinks(graph, node.id, op.links, refMap, stats); + } - addNode(graph, node); - stats.newNodes++; - - // 保存 ref 用于同批次引用 - if (op.ref) { - refMap.set(op.ref, node.id); - } - - // 处理关联边 - if (op.links) { - handleLinks(graph, node.id, op.links, refMap, stats); - } - - return node.id; + return node.id; } /** * 处理 update 操作 */ function handleUpdate(graph, op, stats) { - if (!op.nodeId) { - console.warn('[ST-BME] update 操作缺少 nodeId'); - return; + if (!op.nodeId) { + console.warn("[ST-BME] update 操作缺少 nodeId"); + return; + } + + const previousNode = getNode(graph, op.nodeId); + if (!previousNode) { + console.warn(`[ST-BME] update 目标节点不存在: ${op.nodeId}`); + return; + } + + const previousFields = { ...(previousNode.fields || {}) }; + const nextFields = { ...previousFields, ...(op.fields || {}) }; + const changeSummary = buildFieldChangeSummary(previousFields, nextFields); + + const updated = updateNode(graph, op.nodeId, { + fields: op.fields || {}, + }); + + if (updated) { + stats.updatedNodes++; + const node = getNode(graph, op.nodeId); + if (node) { + node.embedding = null; + node.seq = Math.max(node.seq || 0, op.seq || 0); + node.seqRange = [ + Math.min(node.seqRange?.[0] ?? node.seq, op.seq || node.seq), + Math.max(node.seqRange?.[1] ?? node.seq, op.seq || node.seq), + ]; } - const updated = updateNode(graph, op.nodeId, { - fields: op.fields || {}, - }); - - if (updated) { - stats.updatedNodes++; - const node = graph.nodes.find(n => n.id === op.nodeId); - if (node) node.embedding = null; - - // v2 Graphiti: 标记旧的 updates/temporal_update 边为失效 - const oldEdges = graph.edges.filter(e => - (e.fromId === op.nodeId || e.toId === op.nodeId) && - (e.relation === 'updates' || e.relation === 'temporal_update') && - !e.invalidAt - ); - for (const e of oldEdges) { - invalidateEdge(e); - } + // v2 Graphiti: 标记旧的 updates/temporal_update 边为失效 + const oldEdges = graph.edges.filter( + (e) => + e.toId === op.nodeId && + (e.relation === "updates" || e.relation === "temporal_update") && + !e.invalidAt, + ); + for (const e of oldEdges) { + invalidateEdge(e); } + + if (op.sourceNodeId && op.sourceNodeId !== op.nodeId) { + const temporalEdge = createEdge({ + fromId: op.sourceNodeId, + toId: op.nodeId, + relation: "temporal_update", + strength: op.temporalStrength ?? 0.95, + edgeType: 0, + }); + if (addEdge(graph, temporalEdge)) { + stats.newEdges++; + } + } + + if (changeSummary) { + const updateEventNode = createNode({ + type: "event", + fields: { + summary: `${previousNode.type} 状态更新:${changeSummary}`, + participants: + previousNode.fields?.name || + previousNode.fields?.title || + previousNode.id, + status: "resolved", + }, + seq: op.seq || previousNode.seq || 0, + importance: Math.max( + 4, + Math.min(8, op.importance ?? previousNode.importance ?? 5), + ), + }); + addNode(graph, updateEventNode); + stats.newNodes++; + + const updateEdge = createEdge({ + fromId: updateEventNode.id, + toId: op.nodeId, + relation: "updates", + strength: 0.9, + edgeType: 0, + }); + if (addEdge(graph, updateEdge)) { + stats.newEdges++; + } + } + } +} + +function buildFieldChangeSummary(previousFields = {}, nextFields = {}) { + const changes = []; + const keys = new Set([ + ...Object.keys(previousFields), + ...Object.keys(nextFields), + ]); + + for (const key of keys) { + const before = previousFields[key]; + const after = nextFields[key]; + if (before === after) continue; + + const beforeText = before == null || before === "" ? "空" : String(before); + const afterText = after == null || after === "" ? "空" : String(after); + changes.push(`${key}: ${beforeText} -> ${afterText}`); + } + + return changes.slice(0, 3).join(";"); } /** * 处理 delete 操作 */ function handleDelete(graph, op, stats) { - if (!op.nodeId) return; - const node = graph.nodes.find(n => n.id === op.nodeId); - if (node) { - node.archived = true; // 软删除 - } + if (!op.nodeId) return; + const node = graph.nodes.find((n) => n.id === op.nodeId); + if (node) { + node.archived = true; // 软删除 + } } /** * 处理关联边 */ function handleLinks(graph, sourceId, links, refMap, stats) { - for (const link of links) { - let targetId = link.targetNodeId || null; + for (const link of links) { + let targetId = link.targetNodeId || null; - // 通过 ref 解析目标节点 - if (!targetId && link.targetRef) { - targetId = refMap.get(link.targetRef); - } - - if (!targetId) continue; - - // 验证关系类型 - const relation = RELATION_TYPES.includes(link.relation) - ? link.relation - : 'related'; - - const edgeType = relation === 'contradicts' ? 255 : 0; - - const edge = createEdge({ - fromId: sourceId, - toId: targetId, - relation, - strength: link.strength ?? 0.8, - edgeType, - }); - - if (addEdge(graph, edge)) { - stats.newEdges++; - } + // 通过 ref 解析目标节点 + if (!targetId && link.targetRef) { + targetId = refMap.get(link.targetRef); } + + if (!targetId) continue; + + // 验证关系类型 + const relation = RELATION_TYPES.includes(link.relation) + ? link.relation + : "related"; + + const edgeType = relation === "contradicts" ? 255 : 0; + + const edge = createEdge({ + fromId: sourceId, + toId: targetId, + relation, + strength: link.strength ?? 0.8, + edgeType, + }); + + if (addEdge(graph, edge)) { + stats.newEdges++; + } + } } /** * 为缺少 embedding 的节点生成向量 */ 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.embedding && !n.archived); - if (needsEmbedding.length === 0) return; + if (needsEmbedding.length === 0) return; - const texts = needsEmbedding.map(n => { - // 用主要字段拼文本 - const parts = []; - if (n.fields.summary) parts.push(n.fields.summary); - if (n.fields.name) parts.push(n.fields.name); - if (n.fields.title) parts.push(n.fields.title); - if (n.fields.traits) parts.push(n.fields.traits); - if (n.fields.state) parts.push(n.fields.state); - if (n.fields.constraint) parts.push(n.fields.constraint); - return parts.join(' | ') || n.type; - }); + const texts = needsEmbedding.map((n) => { + // 用主要字段拼文本 + const parts = []; + if (n.fields.summary) parts.push(n.fields.summary); + if (n.fields.name) parts.push(n.fields.name); + if (n.fields.title) parts.push(n.fields.title); + if (n.fields.traits) parts.push(n.fields.traits); + if (n.fields.state) parts.push(n.fields.state); + if (n.fields.constraint) parts.push(n.fields.constraint); + return parts.join(" | ") || n.type; + }); - console.log(`[ST-BME] 为 ${texts.length} 个节点生成 embedding`); + console.log(`[ST-BME] 为 ${texts.length} 个节点生成 embedding`); - const embeddings = await embedBatch(texts, embeddingConfig); + const embeddings = await embedBatch(texts, embeddingConfig); - for (let i = 0; i < needsEmbedding.length; i++) { - if (embeddings[i]) { - needsEmbedding[i].embedding = Array.from(embeddings[i]); - } + for (let i = 0; i < needsEmbedding.length; i++) { + if (embeddings[i]) { + needsEmbedding[i].embedding = Array.from(embeddings[i]); } + } } /** * 构建图谱概览文本(给 LLM 看) */ function buildGraphOverview(graph, schema) { - const activeNodes = graph.nodes.filter(n => !n.archived); - if (activeNodes.length === 0) return ''; + const activeNodes = graph.nodes.filter((n) => !n.archived); + if (activeNodes.length === 0) return ""; - const lines = []; - for (const typeDef of schema) { - const nodesOfType = activeNodes.filter(n => n.type === typeDef.id); - if (nodesOfType.length === 0) continue; + const lines = []; + for (const typeDef of schema) { + const nodesOfType = activeNodes.filter((n) => n.type === typeDef.id); + if (nodesOfType.length === 0) continue; - lines.push(`### ${typeDef.label} (${nodesOfType.length} 个节点)`); - for (const node of nodesOfType.slice(-10)) { // 只展示最近 10 个 - const summary = node.fields.summary || node.fields.name || node.fields.title || '(无)'; - lines.push(` - [${node.id}] ${summary}`); - } + lines.push(`### ${typeDef.label} (${nodesOfType.length} 个节点)`); + for (const node of nodesOfType.slice(-10)) { + // 只展示最近 10 个 + const summary = + node.fields.summary || node.fields.name || node.fields.title || "(无)"; + lines.push(` - [${node.id}] ${summary}`); } + } - return lines.join('\n'); + return lines.join("\n"); } /** * 构建 Schema 描述文本 */ function buildSchemaDescription(schema) { - return schema.map(t => { - const cols = t.columns.map(c => `${c.name}${c.required ? '(必填)' : ''}: ${c.hint}`).join('\n '); - return `类型 "${t.id}" (${t.label}):\n ${cols}`; - }).join('\n\n'); + return schema + .map((t) => { + const cols = t.columns + .map((c) => `${c.name}${c.required ? "(必填)" : ""}: ${c.hint}`) + .join("\n "); + return `类型 "${t.id}" (${t.label}):\n ${cols}`; + }) + .join("\n\n"); } /** * 构建默认提取提示词 */ function buildDefaultExtractPrompt(schema) { - const typeNames = schema.map(s => `${s.id}(${s.label})`).join(', '); + const typeNames = schema.map((s) => `${s.id}(${s.label})`).join(", "); - return [ - '你是一个记忆提取分析器。从对话中提取结构化记忆节点并存入知识图谱。', - '', - `支持的节点类型:${typeNames}`, - '', - '输出格式为严格 JSON:', - '{', - ' "thought": "你对本段对话的分析(事件/角色变化/新信息)",', - ' "operations": [', - ' {', - ' "action": "create",', - ' "type": "event",', - ' "fields": {"summary": "...", "participants": "...", "status": "ongoing"},', - ' "importance": 6,', - ' "ref": "evt1",', - ' "links": [', - ' {"targetNodeId": "existing-id", "relation": "involved_in", "strength": 0.9},', - ' {"targetRef": "char1", "relation": "occurred_at", "strength": 0.8}', - ' ]', - ' },', - ' {', - ' "action": "update",', - ' "nodeId": "existing-node-id",', - ' "fields": {"state": "新的状态"}', - ' }', - ' ]', - '}', - '', - '规则:', - '- 每批对话最多创建 1 个事件节点,多个子事件合并为一条', - '- 角色/地点节点:如果图中已有同名节点,用 update 而非 create', - `- 关系类型限定:${RELATION_TYPES.join(', ')}`, - '- contradicts 关系用于矛盾/冲突信息', - '- evolves 关系用于新信息揭示旧记忆需修正的情况', - '- temporal_update 关系用于实体状态的时序变化', - '- 不要虚构内容,只提取对话中有证据支持的信息', - '- importance 范围 1-10,普通事件 5,关键转折 8+', - '- summary 应该是摘要抽象,不要复制原文', - ].join('\n'); + return [ + "你是一个记忆提取分析器。从对话中提取结构化记忆节点并存入知识图谱。", + "", + `支持的节点类型:${typeNames}`, + "", + "输出格式为严格 JSON:", + "{", + ' "thought": "你对本段对话的分析(事件/角色变化/新信息)",', + ' "operations": [', + " {", + ' "action": "create",', + ' "type": "event",', + ' "fields": {"summary": "...", "participants": "...", "status": "ongoing"},', + ' "importance": 6,', + ' "ref": "evt1",', + ' "links": [', + ' {"targetNodeId": "existing-id", "relation": "involved_in", "strength": 0.9},', + ' {"targetRef": "char1", "relation": "occurred_at", "strength": 0.8}', + " ]", + " },", + " {", + ' "action": "update",', + ' "nodeId": "existing-node-id",', + ' "fields": {"state": "新的状态"}', + " }", + " ]", + "}", + "", + "规则:", + "- 每批对话最多创建 1 个事件节点,多个子事件合并为一条", + "- 角色/地点节点:如果图中已有同名节点,用 update 而非 create", + `- 关系类型限定:${RELATION_TYPES.join(", ")}`, + "- contradicts 关系用于矛盾/冲突信息", + "- evolves 关系用于新信息揭示旧记忆需修正的情况", + "- temporal_update 关系用于实体状态的时序变化", + "- 不要虚构内容,只提取对话中有证据支持的信息", + "- importance 范围 1-10,普通事件 5,关键转折 8+", + "- summary 应该是摘要抽象,不要复制原文", + ].join("\n"); } // ==================== v2 增强功能 ==================== @@ -367,63 +497,75 @@ function buildDefaultExtractPrompt(schema) { * Mem0 启发的精确对照 * 对每条 create 操作搜索近邻,高相似度时让 LLM 判断 add/update/skip */ -async function mem0ConflictCheck(graph, operations, embeddingConfig, threshold) { - const activeNodes = getActiveNodes(graph).filter(n => n.embedding); - if (activeNodes.length === 0) return; +async function mem0ConflictCheck( + graph, + operations, + embeddingConfig, + threshold, +) { + const activeNodes = getActiveNodes(graph).filter((n) => n.embedding); + if (activeNodes.length === 0) return; - for (const op of operations) { - if (op.action !== 'create') continue; + for (const op of operations) { + if (op.action !== "create") continue; - const factText = op.fields?.summary || op.fields?.name || op.fields?.title || ''; - if (!factText) continue; + const factText = + op.fields?.summary || op.fields?.name || op.fields?.title || ""; + if (!factText) continue; - try { - const factVec = await embedText(factText, embeddingConfig); - if (!factVec) continue; + try { + const factVec = await embedText(factText, embeddingConfig); + if (!factVec) continue; - const candidates = activeNodes.map(n => ({ nodeId: n.id, embedding: n.embedding })); - const similar = searchSimilar(factVec, candidates, 3); + const candidates = activeNodes.map((n) => ({ + nodeId: n.id, + embedding: n.embedding, + })); + const similar = searchSimilar(factVec, candidates, 3); - if (similar.length > 0 && similar[0].score > threshold) { - const topMatch = graph.nodes.find(n => n.id === similar[0].nodeId); - if (!topMatch) continue; + if (similar.length > 0 && similar[0].score > threshold) { + const topMatch = graph.nodes.find((n) => n.id === similar[0].nodeId); + if (!topMatch) continue; - const topFields = Object.entries(topMatch.fields) - .map(([k, v]) => `${k}: ${v}`) - .join(', '); + const topFields = Object.entries(topMatch.fields) + .map(([k, v]) => `${k}: ${v}`) + .join(", "); - const decision = await callLLMForJSON({ - systemPrompt: [ - '判断新信息与已有记忆的关系。输出严格 JSON:', - '{"action": "add"|"update"|"skip", "targetId": "旧节点ID", "mergedFields": {}}', - '- add: 新信息完全不同,应新建', - '- update: 新信息是对旧记忆的修正/补充', - '- skip: 与旧记忆完全重复', - ].join('\n'), - userPrompt: [ - `新信息: [${op.type}] ${factText}`, - `最相似旧记忆: [${topMatch.id}] 类型=${topMatch.type}, ${topFields}`, - `相似度: ${similar[0].score.toFixed(3)}`, - ].join('\n'), - maxRetries: 1, - }); + const decision = await callLLMForJSON({ + systemPrompt: [ + "判断新信息与已有记忆的关系。输出严格 JSON:", + '{"action": "add"|"update"|"skip", "targetId": "旧节点ID", "mergedFields": {}}', + "- add: 新信息完全不同,应新建", + "- update: 新信息是对旧记忆的修正/补充", + "- skip: 与旧记忆完全重复", + ].join("\n"), + userPrompt: [ + `新信息: [${op.type}] ${factText}`, + `最相似旧记忆: [${topMatch.id}] 类型=${topMatch.type}, ${topFields}`, + `相似度: ${similar[0].score.toFixed(3)}`, + ].join("\n"), + maxRetries: 1, + }); - if (decision?.action === 'update' && decision.targetId) { - console.log(`[ST-BME] Mem0对照: create->update (${decision.targetId})`); - op.action = 'update'; - op.nodeId = decision.targetId; - if (decision.mergedFields) { - op.fields = { ...op.fields, ...decision.mergedFields }; - } - } else if (decision?.action === 'skip') { - console.log('[ST-BME] Mem0对照: create->skip (重复)'); - op.action = '_skip'; - } - } - } catch (e) { - console.warn('[ST-BME] Mem0对照失败,保持原操作:', e.message); + if (decision?.action === "update" && decision.targetId) { + console.log( + `[ST-BME] Mem0对照: create->update (${decision.targetId})`, + ); + op.action = "update"; + op.nodeId = decision.targetId; + op.sourceNodeId = topMatch.id; + if (decision.mergedFields) { + op.fields = { ...op.fields, ...decision.mergedFields }; + } + } else if (decision?.action === "skip") { + console.log("[ST-BME] Mem0对照: create->skip (重复)"); + op.action = "_skip"; } + } + } catch (e) { + console.warn("[ST-BME] Mem0对照失败,保持原操作:", e.message); } + } } /** @@ -437,64 +579,161 @@ async function mem0ConflictCheck(graph, operations, embeddingConfig, threshold) * @returns {Promise} */ export async function generateSynopsis({ graph, schema, currentSeq }) { - const eventNodes = getActiveNodes(graph, 'event') - .sort((a, b) => a.seq - b.seq); + const eventNodes = getActiveNodes(graph, "event").sort( + (a, b) => a.seq - b.seq, + ); - if (eventNodes.length < 3) return; + if (eventNodes.length < 3) return; - const eventSummaries = eventNodes.map(n => - `[楼${n.seq}] ${n.fields.summary || '(无)'}`, - ).join('\n'); + const eventSummaries = eventNodes + .map((n) => `[楼${n.seq}] ${n.fields.summary || "(无)"}`) + .join("\n"); - const characterNodes = getActiveNodes(graph, 'character'); - const charSummary = characterNodes.map(n => - `${n.fields.name}: ${n.fields.state || '(无状态)'}`, - ).join('; '); + const characterNodes = getActiveNodes(graph, "character"); + const charSummary = characterNodes + .map((n) => `${n.fields.name}: ${n.fields.state || "(无状态)"}`) + .join("; "); - const threadNodes = getActiveNodes(graph, 'thread'); - const threadSummary = threadNodes.map(n => - `${n.fields.title}: ${n.fields.status || 'active'}`, - ).join('; '); + const threadNodes = getActiveNodes(graph, "thread"); + const threadSummary = threadNodes + .map((n) => `${n.fields.title}: ${n.fields.status || "active"}`) + .join("; "); - const result = await callLLMForJSON({ - systemPrompt: [ - '你是故事概要生成器。根据事件线、角色和主线生成简洁的前情提要。', - '输出 JSON:{"summary": "前情提要文本(200字以内)"}', - '要求:涵盖核心冲突、关键转折、主要角色当前状态。', - ].join('\n'), - userPrompt: [ - '## 事件时间线', - eventSummaries, - '', - '## 角色状态', - charSummary || '(无)', - '', - '## 活跃主线', - threadSummary || '(无)', - ].join('\n'), - maxRetries: 1, + const result = await callLLMForJSON({ + systemPrompt: [ + "你是故事概要生成器。根据事件线、角色和主线生成简洁的前情提要。", + '输出 JSON:{"summary": "前情提要文本(200字以内)"}', + "要求:涵盖核心冲突、关键转折、主要角色当前状态。", + ].join("\n"), + userPrompt: [ + "## 事件时间线", + eventSummaries, + "", + "## 角色状态", + charSummary || "(无)", + "", + "## 活跃主线", + threadSummary || "(无)", + ].join("\n"), + maxRetries: 1, + }); + + if (!result?.summary) return; + + const existingSynopsis = graph.nodes.find( + (n) => n.type === "synopsis" && !n.archived, + ); + + if (existingSynopsis) { + updateNode(graph, existingSynopsis.id, { + fields: { summary: result.summary, scope: `楼 1 ~ ${currentSeq}` }, }); - - if (!result?.summary) return; - - const existingSynopsis = graph.nodes.find( - n => n.type === 'synopsis' && !n.archived, - ); - - if (existingSynopsis) { - updateNode(graph, existingSynopsis.id, { - fields: { summary: result.summary, scope: `楼 1 ~ ${currentSeq}` }, - }); - existingSynopsis.embedding = null; - console.log('[ST-BME] 全局概要已更新'); - } else { - const node = createNode({ - type: 'synopsis', - fields: { summary: result.summary, scope: `楼 1 ~ ${currentSeq}` }, - seq: currentSeq, - importance: 9.0, - }); - addNode(graph, node); - console.log('[ST-BME] 全局概要已创建'); - } + existingSynopsis.embedding = null; + console.log("[ST-BME] 全局概要已更新"); + } else { + const node = createNode({ + type: "synopsis", + fields: { summary: result.summary, scope: `楼 1 ~ ${currentSeq}` }, + seq: currentSeq, + importance: 9.0, + }); + addNode(graph, node); + console.log("[ST-BME] 全局概要已创建"); + } +} + +export async function generateReflection({ graph, currentSeq }) { + const recentEvents = getActiveNodes(graph, "event") + .sort((a, b) => b.seq - a.seq) + .slice(0, 6) + .reverse(); + + if (recentEvents.length < 2) return null; + + const recentCharacters = getActiveNodes(graph, "character") + .sort((a, b) => b.seq - a.seq) + .slice(0, 5); + + const recentThreads = getActiveNodes(graph, "thread") + .sort((a, b) => b.seq - a.seq) + .slice(0, 4); + + const contradictEdges = graph.edges + .filter((e) => e.relation === "contradicts" && !e.invalidAt) + .slice(-5); + + const eventSummary = recentEvents + .map((n) => `[楼${n.seq}] ${n.fields.summary || "(无)"}`) + .join("\n"); + const characterSummary = recentCharacters + .map( + (n) => + `${n.fields.name || n.fields.title || n.id}: ${n.fields.state || n.fields.summary || "(无)"}`, + ) + .join("\n"); + const threadSummary = recentThreads + .map( + (n) => + `${n.fields.title || n.fields.name || n.id}: ${n.fields.status || n.fields.summary || "active"}`, + ) + .join("\n"); + const contradictionSummary = contradictEdges + .map((e) => `${e.fromId} -> ${e.toId} (${e.relation})`) + .join("\n"); + + const result = await callLLMForJSON({ + systemPrompt: [ + "你是 RP 长期记忆系统的反思生成器。", + '输出严格 JSON:{"insight":"...","trigger":"...","suggestion":"...","importance":1-10}', + "insight 应总结最近情节中最值得长期保留的变化、关系趋势或潜在线索。", + "trigger 说明触发这条反思的关键事件或矛盾。", + "suggestion 给出后续检索或叙事上值得关注的提示。", + "不要复述全部事件,要提炼高层结论。", + ].join("\n"), + userPrompt: [ + "## 最近事件", + eventSummary, + "", + "## 近期角色状态", + characterSummary || "(无)", + "", + "## 当前主线", + threadSummary || "(无)", + "", + "## 已知矛盾", + contradictionSummary || "(无)", + ].join("\n"), + maxRetries: 1, + }); + + if (!result?.insight) return null; + + const reflectionNode = createNode({ + type: "reflection", + fields: { + insight: result.insight, + trigger: + result.trigger || + recentEvents[recentEvents.length - 1]?.fields?.summary || + "", + suggestion: result.suggestion || "", + }, + seq: currentSeq, + importance: Math.max(5, Math.min(10, result.importance ?? 7)), + }); + addNode(graph, reflectionNode); + + for (const eventNode of recentEvents.slice(-3)) { + const edge = createEdge({ + fromId: reflectionNode.id, + toId: eventNode.id, + relation: "evolves", + strength: 0.75, + edgeType: 0, + }); + addEdge(graph, edge); + } + + console.log("[ST-BME] 反思条目已生成"); + return reflectionNode.id; } diff --git a/index.js b/index.js index 37b3af4..771d7e7 100644 --- a/index.js +++ b/index.js @@ -1,94 +1,112 @@ // ST-BME: 主入口 // 事件钩子、设置管理、流程调度 -import { extension_settings, getContext, saveMetadataDebounced } from '../../extensions.js'; -import { eventSource, event_types, saveSettingsDebounced } from '../../../script.js'; -import { renderExtensionTemplateAsync } from '../../extensions.js'; +import { + eventSource, + event_types, + saveSettingsDebounced, +} from "../../../script.js"; +import { + extension_settings, + getContext, + renderExtensionTemplateAsync, + saveMetadataDebounced, +} from "../../extensions.js"; -import { createEmptyGraph, deserializeGraph, serializeGraph, exportGraph, importGraph, getGraphStats } from './graph.js'; -import { DEFAULT_NODE_SCHEMA, validateSchema } from './schema.js'; -import { retrieve } from './retriever.js'; -import { extractMemories, generateSynopsis } from './extractor.js'; -import { compressAll, sleepCycle } from './compressor.js'; -import { formatInjection, estimateTokens } from './injector.js'; -import { testConnection as testEmbeddingConnection } from './embedding.js'; -import { evolveMemories } from './evolution.js'; +import { compressAll, sleepCycle } from "./compressor.js"; +import { testConnection as testEmbeddingConnection } from "./embedding.js"; +import { evolveMemories } from "./evolution.js"; +import { + extractMemories, + generateReflection, + generateSynopsis, +} from "./extractor.js"; +import { + createEmptyGraph, + deserializeGraph, + exportGraph, + getGraphStats, + importGraph, +} from "./graph.js"; +import { estimateTokens, formatInjection } from "./injector.js"; +import { retrieve } from "./retriever.js"; +import { DEFAULT_NODE_SCHEMA } from "./schema.js"; -const MODULE_NAME = 'st_bme'; -const GRAPH_METADATA_KEY = 'st_bme_graph'; +const MODULE_NAME = "st_bme"; +const GRAPH_METADATA_KEY = "st_bme_graph"; // ==================== 默认设置 ==================== const defaultSettings = { - enabled: false, + enabled: false, - // 提取设置 - extractEvery: 1, // 每 N 条 assistant 回复提取一次 - extractContextTurns: 2, // 提取时包含的上下文楼层数 + // 提取设置 + extractEvery: 1, // 每 N 条 assistant 回复提取一次 + extractContextTurns: 2, // 提取时包含的上下文楼层数 - // 召回设置 - recallEnabled: true, - recallTopK: 15, // 混合评分 Top-K - recallMaxNodes: 8, // LLM 召回最大节点数 - recallEnableLLM: true, // 是否启用 LLM 精确召回 + // 召回设置 + recallEnabled: true, + recallTopK: 15, // 混合评分 Top-K + recallMaxNodes: 8, // LLM 召回最大节点数 + recallEnableLLM: true, // 是否启用 LLM 精确召回 - // 注入设置 - injectPosition: 'atDepth', // 注入位置 - injectDepth: 4, // 注入深度(atDepth 模式) - injectRole: 0, // 0=system, 1=user, 2=assistant + // 注入设置 + injectPosition: "atDepth", // 注入位置 + injectDepth: 4, // 注入深度(atDepth 模式) + injectRole: 0, // 0=system, 1=user, 2=assistant - // 混合评分权重 - graphWeight: 0.6, - vectorWeight: 0.3, - importanceWeight: 0.1, + // 混合评分权重 + graphWeight: 0.6, + vectorWeight: 0.3, + importanceWeight: 0.1, - // Embedding API 配置 - embeddingApiUrl: '', - embeddingApiKey: '', - embeddingModel: 'text-embedding-3-small', + // Embedding API 配置 + embeddingApiUrl: "", + embeddingApiKey: "", + embeddingModel: "text-embedding-3-small", - // Schema - nodeTypeSchema: null, // null 表示使用默认 + // Schema + nodeTypeSchema: null, // null 表示使用默认 - // 自定义提示词 - extractPrompt: '', + // 自定义提示词 + extractPrompt: "", - // ====== v2 增强设置 ====== + // ====== v2 增强设置 ====== - // ③ A-MEM 记忆进化 - enableEvolution: true, // 启用记忆进化 - evoNeighborCount: 5, // 近邻搜索数量 - evoConsolidateEvery: 50, // 每 N 次进化后整理 + // ③ A-MEM 记忆进化 + enableEvolution: true, // 启用记忆进化 + evoNeighborCount: 5, // 近邻搜索数量 + evoConsolidateEvery: 50, // 每 N 次进化后整理 - // ② Mem0 精确对照 - enablePreciseConflict: true, // 启用精确对照 - conflictThreshold: 0.85, // 相似度阈值 + // ② Mem0 精确对照 + enablePreciseConflict: true, // 启用精确对照 + conflictThreshold: 0.85, // 相似度阈值 - // ⑨ 全局故事概要 - enableSynopsis: true, // 启用全局概要 - synopsisEveryN: 5, // 每 N 次提取后更新概要 + // ⑨ 全局故事概要 + enableSynopsis: true, // 启用全局概要 + synopsisEveryN: 5, // 每 N 次提取后更新概要 - // ⑥ 认知边界过滤(P1) - enableVisibility: false, // 启用认知边界 - // ⑦ 双记忆交叉检索(P1) - enableCrossRecall: false, // 启用交叉检索 + // ⑥ 认知边界过滤(P1) + enableVisibility: false, // 启用认知边界 + // ⑦ 双记忆交叉检索(P1) + enableCrossRecall: false, // 启用交叉检索 - // ① 惊奇度分割(P2) - enableSmartTrigger: false, // 启用惊奇度分割 - triggerPatterns: '', // 自定义触发正则 + // ① 惊奇度分割(P2) + enableSmartTrigger: false, // 启用惊奇度分割 + triggerPatterns: "", // 自定义触发正则 - // ⑤ 主动遗忘(P2) - enableSleepCycle: false, // 启用主动遗忘 - forgetThreshold: 0.5, // 保留价值阈值 - sleepEveryN: 10, // 每 N 次提取后执行 + // ⑤ 主动遗忘(P2) + enableSleepCycle: false, // 启用主动遗忘 + forgetThreshold: 0.5, // 保留价值阈值 + sleepEveryN: 10, // 每 N 次提取后执行 - // ⑧ 概率触发回忆(P2) - enableProbRecall: false, // 启用概率触发 - probRecallChance: 0.15, // 触发概率 + // ⑧ 概率触发回忆(P2) + enableProbRecall: false, // 启用概率触发 + probRecallChance: 0.15, // 触发概率 - // ⑩ 反思条目(P2) - enableReflection: false, // 启用反思 - reflectEveryN: 10, // 每 N 次提取后反思 + // ⑩ 反思条目(P2) + enableReflection: false, // 启用反思 + reflectEveryN: 10, // 每 N 次提取后反思 }; // ==================== 状态 ==================== @@ -96,56 +114,56 @@ const defaultSettings = { let currentGraph = null; let isExtracting = false; let isRecalling = false; -let lastInjectionContent = ''; -let extractionCount = 0; // v2: 提取次数计数器(定期触发概要/遗忘/反思) +let lastInjectionContent = ""; +let extractionCount = 0; // v2: 提取次数计数器(定期触发概要/遗忘/反思) // ==================== 设置管理 ==================== function getSettings() { - if (!extension_settings[MODULE_NAME]) { - extension_settings[MODULE_NAME] = { ...defaultSettings }; - } - return extension_settings[MODULE_NAME]; + if (!extension_settings[MODULE_NAME]) { + extension_settings[MODULE_NAME] = { ...defaultSettings }; + } + return extension_settings[MODULE_NAME]; } function getSchema() { - const settings = getSettings(); - return settings.nodeTypeSchema || DEFAULT_NODE_SCHEMA; + const settings = getSettings(); + return settings.nodeTypeSchema || DEFAULT_NODE_SCHEMA; } function getEmbeddingConfig() { - const settings = getSettings(); - return { - apiUrl: settings.embeddingApiUrl, - apiKey: settings.embeddingApiKey, - model: settings.embeddingModel, - }; + const settings = getSettings(); + return { + apiUrl: settings.embeddingApiUrl, + apiKey: settings.embeddingApiKey, + model: settings.embeddingModel, + }; } // ==================== 图状态持久化 ==================== function loadGraphFromChat() { - const context = getContext(); - if (!context.chatMetadata) { - currentGraph = createEmptyGraph(); - return; - } + const context = getContext(); + if (!context.chatMetadata) { + currentGraph = createEmptyGraph(); + return; + } - const savedData = context.chatMetadata[GRAPH_METADATA_KEY]; - if (savedData) { - currentGraph = deserializeGraph(savedData); - console.log('[ST-BME] 从聊天数据加载图谱:', getGraphStats(currentGraph)); - } else { - currentGraph = createEmptyGraph(); - } + const savedData = context.chatMetadata[GRAPH_METADATA_KEY]; + if (savedData) { + currentGraph = deserializeGraph(savedData); + console.log("[ST-BME] 从聊天数据加载图谱:", getGraphStats(currentGraph)); + } else { + currentGraph = createEmptyGraph(); + } } function saveGraphToChat() { - const context = getContext(); - if (!context.chatMetadata || !currentGraph) return; + const context = getContext(); + if (!context.chatMetadata || !currentGraph) return; - context.chatMetadata[GRAPH_METADATA_KEY] = currentGraph; - saveMetadataDebounced(); + context.chatMetadata[GRAPH_METADATA_KEY] = currentGraph; + saveMetadataDebounced(); } // ==================== 核心流程 ==================== @@ -154,502 +172,602 @@ function saveGraphToChat() { * 提取管线:处理未提取的对话楼层 */ async function runExtraction() { - if (isExtracting || !currentGraph) return; + if (isExtracting || !currentGraph) return; - const settings = getSettings(); - if (!settings.enabled) return; + const settings = getSettings(); + if (!settings.enabled) return; - const context = getContext(); - const chat = context.chat; - if (!chat || chat.length === 0) return; + const context = getContext(); + const chat = context.chat; + if (!chat || chat.length === 0) return; - // 找出 assistant 楼层序号 - const assistantTurns = []; - for (let i = 0; i < chat.length; i++) { - if (chat[i].is_user === false && !chat[i].is_system) { - assistantTurns.push(i); - } + // 找出 assistant 楼层序号 + const assistantTurns = []; + for (let i = 0; i < chat.length; i++) { + if (chat[i].is_user === false && !chat[i].is_system) { + assistantTurns.push(i); + } + } + + const lastProcessed = currentGraph.lastProcessedSeq; + const unprocessedStarts = assistantTurns.filter((i) => i > lastProcessed); + + if (unprocessedStarts.length === 0) return; + + // 按 extractEvery 批次处理 + if (unprocessedStarts.length < settings.extractEvery) return; + + isExtracting = true; + + try { + // 收集要处理的消息 + const startIdx = unprocessedStarts[0]; + const endIdx = unprocessedStarts[unprocessedStarts.length - 1]; + + // 包含上下文 + const contextStart = Math.max( + 0, + startIdx - settings.extractContextTurns * 2, + ); + const messages = []; + for (let i = contextStart; i <= endIdx && i < chat.length; i++) { + const msg = chat[i]; + if (msg.is_system) continue; + messages.push({ + role: msg.is_user ? "user" : "assistant", + content: msg.mes || "", + }); } - const lastProcessed = currentGraph.lastProcessedSeq; - const unprocessedStarts = assistantTurns.filter(i => i > lastProcessed); + console.log(`[ST-BME] 开始提取: 楼层 ${startIdx}-${endIdx}`); - if (unprocessedStarts.length === 0) return; + const result = await extractMemories({ + graph: currentGraph, + messages, + startSeq: endIdx, + schema: getSchema(), + embeddingConfig: getEmbeddingConfig(), + extractPrompt: settings.extractPrompt || undefined, + v2Options: { + enablePreciseConflict: settings.enablePreciseConflict, + conflictThreshold: settings.conflictThreshold, + }, + }); - // 按 extractEvery 批次处理 - if (unprocessedStarts.length < settings.extractEvery) return; + if (result.success) { + extractionCount++; - isExtracting = true; - - try { - // 收集要处理的消息 - const startIdx = unprocessedStarts[0]; - const endIdx = unprocessedStarts[unprocessedStarts.length - 1]; - - // 包含上下文 - const contextStart = Math.max(0, startIdx - settings.extractContextTurns * 2); - const messages = []; - for (let i = contextStart; i <= endIdx && i < chat.length; i++) { - const msg = chat[i]; - if (msg.is_system) continue; - messages.push({ - role: msg.is_user ? 'user' : 'assistant', - content: msg.mes || '', - }); - } - - console.log(`[ST-BME] 开始提取: 楼层 ${startIdx}-${endIdx}`); - - const result = await extractMemories({ + // v2: A-MEM 记忆进化 + if (settings.enableEvolution && result.newNodeIds?.length > 0) { + try { + await evolveMemories({ graph: currentGraph, - messages, - startSeq: endIdx, - schema: getSchema(), + newNodeIds: result.newNodeIds, embeddingConfig: getEmbeddingConfig(), - extractPrompt: settings.extractPrompt || undefined, - v2Options: { - enablePreciseConflict: settings.enablePreciseConflict, - conflictThreshold: settings.conflictThreshold, - }, - }); - - if (result.success) { - extractionCount++; - - // v2: A-MEM 记忆进化 - if (settings.enableEvolution && result.newNodeIds?.length > 0) { - try { - await evolveMemories({ - graph: currentGraph, - newNodeIds: result.newNodeIds, - embeddingConfig: getEmbeddingConfig(), - options: { neighborCount: settings.evoNeighborCount }, - }); - } catch (e) { - console.error('[ST-BME] 记忆进化失败:', e); - } - } - - // v2: 全局故事概要(每 N 次提取更新一次) - if (settings.enableSynopsis && extractionCount % settings.synopsisEveryN === 0) { - try { - await generateSynopsis({ - graph: currentGraph, - schema: getSchema(), - currentSeq: endIdx, - }); - } catch (e) { - console.error('[ST-BME] 概要生成失败:', e); - } - } - - // v2: 主动遗忘(每 N 次提取执行) - if (settings.enableSleepCycle && extractionCount % settings.sleepEveryN === 0) { - try { - sleepCycle(currentGraph, settings); - } catch (e) { - console.error('[ST-BME] 主动遗忘失败:', e); - } - } - - // 压缩检查 - await compressAll(currentGraph, getSchema(), getEmbeddingConfig()); - saveGraphToChat(); + options: { neighborCount: settings.evoNeighborCount }, + }); + } catch (e) { + console.error("[ST-BME] 记忆进化失败:", e); } - } catch (e) { - console.error('[ST-BME] 提取失败:', e); - } finally { - isExtracting = false; + } + + // v2: 全局故事概要(每 N 次提取更新一次) + if ( + settings.enableSynopsis && + extractionCount % settings.synopsisEveryN === 0 + ) { + try { + await generateSynopsis({ + graph: currentGraph, + schema: getSchema(), + currentSeq: endIdx, + }); + } catch (e) { + console.error("[ST-BME] 概要生成失败:", e); + } + } + + // v2: 反思条目(每 N 次提取生成一次) + if ( + settings.enableReflection && + extractionCount % settings.reflectEveryN === 0 + ) { + try { + await generateReflection({ + graph: currentGraph, + currentSeq: endIdx, + }); + } catch (e) { + console.error("[ST-BME] 反思生成失败:", e); + } + } + + // v2: 主动遗忘(每 N 次提取执行) + if ( + settings.enableSleepCycle && + extractionCount % settings.sleepEveryN === 0 + ) { + try { + sleepCycle(currentGraph, settings); + } catch (e) { + console.error("[ST-BME] 主动遗忘失败:", e); + } + } + + // 压缩检查 + await compressAll(currentGraph, getSchema(), getEmbeddingConfig()); + saveGraphToChat(); } + } catch (e) { + console.error("[ST-BME] 提取失败:", e); + } finally { + isExtracting = false; + } } /** * 召回管线:检索并注入记忆 */ async function runRecall() { - if (isRecalling || !currentGraph) return; + if (isRecalling || !currentGraph) return; - const settings = getSettings(); - if (!settings.enabled || !settings.recallEnabled) return; + const settings = getSettings(); + if (!settings.enabled || !settings.recallEnabled) return; - const context = getContext(); - const chat = context.chat; - if (!chat || chat.length === 0) return; + const context = getContext(); + const chat = context.chat; + if (!chat || chat.length === 0) return; - isRecalling = true; + isRecalling = true; - try { - // 获取最新用户消息 - let userMessage = ''; - const recentMessages = []; + try { + // 获取最新用户消息 + let userMessage = ""; + const recentMessages = []; - for (let i = chat.length - 1; i >= 0 && recentMessages.length < 4; i--) { - const msg = chat[i]; - if (msg.is_system) continue; + for (let i = chat.length - 1; i >= 0 && recentMessages.length < 4; i--) { + const msg = chat[i]; + if (msg.is_system) continue; - if (msg.is_user && !userMessage) { - userMessage = msg.mes || ''; - } - recentMessages.unshift(`[${msg.is_user ? 'user' : 'assistant'}]: ${msg.mes || ''}`); - } - - if (!userMessage) return; - - console.log('[ST-BME] 开始召回'); - - const result = await retrieve({ - graph: currentGraph, - userMessage, - recentMessages, - embeddingConfig: getEmbeddingConfig(), - schema: getSchema(), - options: { - topK: settings.recallTopK, - maxRecallNodes: settings.recallMaxNodes, - enableLLMRecall: settings.recallEnableLLM, - weights: { - graphWeight: settings.graphWeight, - vectorWeight: settings.vectorWeight, - importanceWeight: settings.importanceWeight, - }, - // v2 options - enableVisibility: settings.enableVisibility ?? false, - visibilityFilter: context.name2 || null, - enableCrossRecall: settings.enableCrossRecall ?? false, - enableProbRecall: settings.enableProbRecall ?? false, - probRecallChance: settings.probRecallChance ?? 0.15, - }, - }); - - // 格式化注入文本 - const injectionText = formatInjection(result, getSchema()); - lastInjectionContent = injectionText; - - if (injectionText) { - const tokens = estimateTokens(injectionText); - console.log(`[ST-BME] 注入 ${tokens} 估算 tokens, Core=${result.stats.coreCount}, Recall=${result.stats.recallCount}`); - - // 使用 ST 的 extension prompt API 注入 - context.setExtensionPrompt( - MODULE_NAME, - injectionText, - 1, // extension_prompt_types.IN_PROMPT - settings.injectDepth, - ); - } - - // 保存召回结果和访问强化 - currentGraph.lastRecallResult = result.selectedNodeIds; - saveGraphToChat(); - } catch (e) { - console.error('[ST-BME] 召回失败:', e); - } finally { - isRecalling = false; + if (msg.is_user && !userMessage) { + userMessage = msg.mes || ""; + } + recentMessages.unshift( + `[${msg.is_user ? "user" : "assistant"}]: ${msg.mes || ""}`, + ); } + + if (!userMessage) return; + + console.log("[ST-BME] 开始召回"); + + const result = await retrieve({ + graph: currentGraph, + userMessage, + recentMessages, + embeddingConfig: getEmbeddingConfig(), + schema: getSchema(), + options: { + topK: settings.recallTopK, + maxRecallNodes: settings.recallMaxNodes, + enableLLMRecall: settings.recallEnableLLM, + weights: { + graphWeight: settings.graphWeight, + vectorWeight: settings.vectorWeight, + importanceWeight: settings.importanceWeight, + }, + // v2 options + enableVisibility: settings.enableVisibility ?? false, + visibilityFilter: context.name2 || null, + enableCrossRecall: settings.enableCrossRecall ?? false, + enableProbRecall: settings.enableProbRecall ?? false, + probRecallChance: settings.probRecallChance ?? 0.15, + }, + }); + + // 格式化注入文本 + const injectionText = formatInjection(result, getSchema()); + lastInjectionContent = injectionText; + + if (injectionText) { + const tokens = estimateTokens(injectionText); + console.log( + `[ST-BME] 注入 ${tokens} 估算 tokens, Core=${result.stats.coreCount}, Recall=${result.stats.recallCount}`, + ); + + // 使用 ST 的 extension prompt API 注入 + context.setExtensionPrompt( + MODULE_NAME, + injectionText, + 1, // extension_prompt_types.IN_PROMPT + settings.injectDepth, + ); + } + + // 保存召回结果和访问强化 + currentGraph.lastRecallResult = result.selectedNodeIds; + saveGraphToChat(); + } catch (e) { + console.error("[ST-BME] 召回失败:", e); + } finally { + isRecalling = false; + } } // ==================== 事件钩子 ==================== function onChatChanged() { - loadGraphFromChat(); - lastInjectionContent = ''; + loadGraphFromChat(); + lastInjectionContent = ""; } async function onGenerationAfterCommands() { - await runExtraction(); + await runExtraction(); } async function onBeforeCombinePrompts() { - await runRecall(); + await runRecall(); } function onMessageReceived() { - // 新消息到达,图状态可能需要更新 - if (currentGraph) { - saveGraphToChat(); - } + // 新消息到达,图状态可能需要更新 + if (currentGraph) { + saveGraphToChat(); + } } // ==================== UI 操作 ==================== async function onViewGraph() { - if (!currentGraph) { - toastr.warning('当前没有加载的图谱'); - return; - } + if (!currentGraph) { + toastr.warning("当前没有加载的图谱"); + return; + } - const stats = getGraphStats(currentGraph); - const statsText = [ - `节点: ${stats.activeNodes} 活跃 / ${stats.archivedNodes} 归档`, - `边: ${stats.totalEdges}`, - `最后处理楼层: ${stats.lastProcessedSeq}`, - `类型分布: ${Object.entries(stats.typeCounts).map(([k, v]) => `${k}=${v}`).join(', ') || '(空)'}`, - ].join('\n'); + const stats = getGraphStats(currentGraph); + const statsText = [ + `节点: ${stats.activeNodes} 活跃 / ${stats.archivedNodes} 归档`, + `边: ${stats.totalEdges}`, + `最后处理楼层: ${stats.lastProcessedSeq}`, + `类型分布: ${ + Object.entries(stats.typeCounts) + .map(([k, v]) => `${k}=${v}`) + .join(", ") || "(空)" + }`, + ].join("\n"); - toastr.info(statsText, 'ST-BME 图谱状态', { timeOut: 10000 }); + toastr.info(statsText, "ST-BME 图谱状态", { timeOut: 10000 }); } async function onRebuild() { - if (!confirm('确定要从当前聊天重建图谱?这将清除现有图谱数据。')) return; + if (!confirm("确定要从当前聊天重建图谱?这将清除现有图谱数据。")) return; - currentGraph = createEmptyGraph(); - saveGraphToChat(); + currentGraph = createEmptyGraph(); + saveGraphToChat(); - toastr.info('图谱已重置,将在下次生成时重新提取'); + toastr.info("图谱已重置,将在下次生成时重新提取"); } async function onManualCompress() { - if (!currentGraph) return; + if (!currentGraph) return; - const result = await compressAll(currentGraph, getSchema(), getEmbeddingConfig(), false); - saveGraphToChat(); + const result = await compressAll( + currentGraph, + getSchema(), + getEmbeddingConfig(), + false, + ); + saveGraphToChat(); - toastr.info(`压缩完成: 新建 ${result.created}, 归档 ${result.archived}`); + toastr.info(`压缩完成: 新建 ${result.created}, 归档 ${result.archived}`); } async function onExportGraph() { - if (!currentGraph) return; + if (!currentGraph) return; - const json = exportGraph(currentGraph); - const blob = new Blob([json], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `st-bme-graph-${Date.now()}.json`; - a.click(); - URL.revokeObjectURL(url); + const json = exportGraph(currentGraph); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `st-bme-graph-${Date.now()}.json`; + a.click(); + URL.revokeObjectURL(url); - toastr.success('图谱已导出'); + toastr.success("图谱已导出"); } async function onImportGraph() { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.json'; - input.onchange = async (e) => { - const file = e.target.files[0]; - if (!file) return; + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".json"; + input.onchange = async (e) => { + const file = e.target.files[0]; + if (!file) return; - try { - const text = await file.text(); - currentGraph = importGraph(text); - saveGraphToChat(); - toastr.success('图谱已导入'); - } catch (err) { - toastr.error(`导入失败: ${err.message}`); - } - }; - input.click(); + try { + const text = await file.text(); + currentGraph = importGraph(text); + saveGraphToChat(); + toastr.success("图谱已导入"); + } catch (err) { + toastr.error(`导入失败: ${err.message}`); + } + }; + input.click(); } async function onViewLastInjection() { - if (!lastInjectionContent) { - toastr.info('暂无注入内容'); - return; - } + if (!lastInjectionContent) { + toastr.info("暂无注入内容"); + return; + } - // 简单弹窗显示 - const popup = document.createElement('div'); - popup.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#1a1a2e;color:#eee;padding:24px;border-radius:12px;max-width:80vw;max-height:80vh;overflow:auto;z-index:99999;white-space:pre-wrap;font-size:13px;box-shadow:0 8px 32px rgba(0,0,0,0.5);'; - popup.textContent = lastInjectionContent; + // 简单弹窗显示 + const popup = document.createElement("div"); + popup.style.cssText = + "position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#1a1a2e;color:#eee;padding:24px;border-radius:12px;max-width:80vw;max-height:80vh;overflow:auto;z-index:99999;white-space:pre-wrap;font-size:13px;box-shadow:0 8px 32px rgba(0,0,0,0.5);"; + popup.textContent = lastInjectionContent; - const close = document.createElement('button'); - close.textContent = '关闭'; - close.style.cssText = 'position:absolute;top:8px;right:12px;background:#e94560;color:white;border:none;padding:4px 12px;border-radius:4px;cursor:pointer;'; - close.onclick = () => popup.remove(); - popup.appendChild(close); + const close = document.createElement("button"); + close.textContent = "关闭"; + close.style.cssText = + "position:absolute;top:8px;right:12px;background:#e94560;color:white;border:none;padding:4px 12px;border-radius:4px;cursor:pointer;"; + close.onclick = () => popup.remove(); + popup.appendChild(close); - document.body.appendChild(popup); + document.body.appendChild(popup); } async function onTestEmbedding() { - const config = getEmbeddingConfig(); - if (!config.apiUrl || !config.apiKey) { - toastr.warning('请先配置 Embedding API 地址和 Key'); - return; - } + const config = getEmbeddingConfig(); + if (!config.apiUrl || !config.apiKey) { + toastr.warning("请先配置 Embedding API 地址和 Key"); + return; + } - toastr.info('正在测试 Embedding API 连通性...'); - const result = await testEmbeddingConnection(config); + toastr.info("正在测试 Embedding API 连通性..."); + const result = await testEmbeddingConnection(config); - if (result.success) { - toastr.success(`连接成功!向量维度: ${result.dimensions}`); - } else { - toastr.error(`连接失败: ${result.error}`); - } + if (result.success) { + toastr.success(`连接成功!向量维度: ${result.dimensions}`); + } else { + toastr.error(`连接失败: ${result.error}`); + } } // ==================== 设置 UI ==================== function bindSettingsUI() { - const settings = getSettings(); + const settings = getSettings(); - // 开关 - $('#st_bme_enabled').prop('checked', settings.enabled).on('change', function () { - settings.enabled = $(this).prop('checked'); - saveSettingsDebounced(); + // 开关 + $("#st_bme_enabled") + .prop("checked", settings.enabled) + .on("change", function () { + settings.enabled = $(this).prop("checked"); + saveSettingsDebounced(); }); - // 提取频率 - $('#st_bme_extract_every').val(settings.extractEvery).on('input', function () { - settings.extractEvery = Math.max(1, parseInt($(this).val()) || 1); - saveSettingsDebounced(); + // 提取频率 + $("#st_bme_extract_every") + .val(settings.extractEvery) + .on("input", function () { + settings.extractEvery = Math.max(1, parseInt($(this).val()) || 1); + saveSettingsDebounced(); }); - // 召回开关 - $('#st_bme_recall_enabled').prop('checked', settings.recallEnabled).on('change', function () { - settings.recallEnabled = $(this).prop('checked'); - saveSettingsDebounced(); + // 召回开关 + $("#st_bme_recall_enabled") + .prop("checked", settings.recallEnabled) + .on("change", function () { + settings.recallEnabled = $(this).prop("checked"); + saveSettingsDebounced(); }); - // LLM 精确召回 - $('#st_bme_recall_llm').prop('checked', settings.recallEnableLLM).on('change', function () { - settings.recallEnableLLM = $(this).prop('checked'); - saveSettingsDebounced(); + // LLM 精确召回 + $("#st_bme_recall_llm") + .prop("checked", settings.recallEnableLLM) + .on("change", function () { + settings.recallEnableLLM = $(this).prop("checked"); + saveSettingsDebounced(); }); - // 注入深度 - $('#st_bme_inject_depth').val(settings.injectDepth).on('input', function () { - settings.injectDepth = Math.max(0, parseInt($(this).val()) || 4); - saveSettingsDebounced(); + // 注入深度 + $("#st_bme_inject_depth") + .val(settings.injectDepth) + .on("input", function () { + settings.injectDepth = Math.max(0, parseInt($(this).val()) || 4); + saveSettingsDebounced(); }); - // 评分权重 - $('#st_bme_graph_weight').val(settings.graphWeight).on('input', function () { - settings.graphWeight = parseFloat($(this).val()) || 0.6; - saveSettingsDebounced(); + // 评分权重 + $("#st_bme_graph_weight") + .val(settings.graphWeight) + .on("input", function () { + settings.graphWeight = parseFloat($(this).val()) || 0.6; + saveSettingsDebounced(); }); - $('#st_bme_vector_weight').val(settings.vectorWeight).on('input', function () { - settings.vectorWeight = parseFloat($(this).val()) || 0.3; - saveSettingsDebounced(); + $("#st_bme_vector_weight") + .val(settings.vectorWeight) + .on("input", function () { + settings.vectorWeight = parseFloat($(this).val()) || 0.3; + saveSettingsDebounced(); }); - $('#st_bme_importance_weight').val(settings.importanceWeight).on('input', function () { - settings.importanceWeight = parseFloat($(this).val()) || 0.1; - saveSettingsDebounced(); + $("#st_bme_importance_weight") + .val(settings.importanceWeight) + .on("input", function () { + settings.importanceWeight = parseFloat($(this).val()) || 0.1; + saveSettingsDebounced(); }); - // Embedding API - $('#st_bme_embed_url').val(settings.embeddingApiUrl).on('input', function () { - settings.embeddingApiUrl = $(this).val().trim(); - saveSettingsDebounced(); + // Embedding API + $("#st_bme_embed_url") + .val(settings.embeddingApiUrl) + .on("input", function () { + settings.embeddingApiUrl = $(this).val().trim(); + saveSettingsDebounced(); }); - $('#st_bme_embed_key').val(settings.embeddingApiKey).on('input', function () { - settings.embeddingApiKey = $(this).val().trim(); - saveSettingsDebounced(); + $("#st_bme_embed_key") + .val(settings.embeddingApiKey) + .on("input", function () { + settings.embeddingApiKey = $(this).val().trim(); + saveSettingsDebounced(); }); - $('#st_bme_embed_model').val(settings.embeddingModel).on('input', function () { - settings.embeddingModel = $(this).val().trim(); - saveSettingsDebounced(); + $("#st_bme_embed_model") + .val(settings.embeddingModel) + .on("input", function () { + settings.embeddingModel = $(this).val().trim(); + saveSettingsDebounced(); }); - // 操作按钮 - $('#st_bme_btn_view_graph').on('click', onViewGraph); - $('#st_bme_btn_rebuild').on('click', onRebuild); - $('#st_bme_btn_compress').on('click', onManualCompress); - $('#st_bme_btn_export').on('click', onExportGraph); - $('#st_bme_btn_import').on('click', onImportGraph); - $('#st_bme_btn_view_injection').on('click', onViewLastInjection); - $('#st_bme_btn_test_embed').on('click', onTestEmbedding); + // 操作按钮 + $("#st_bme_btn_view_graph").on("click", onViewGraph); + $("#st_bme_btn_rebuild").on("click", onRebuild); + $("#st_bme_btn_compress").on("click", onManualCompress); + $("#st_bme_btn_export").on("click", onExportGraph); + $("#st_bme_btn_import").on("click", onImportGraph); + $("#st_bme_btn_view_injection").on("click", onViewLastInjection); + $("#st_bme_btn_test_embed").on("click", onTestEmbedding); - // ====== v2 增强设置 UI 绑定 ====== + // ====== v2 增强设置 UI 绑定 ====== - // P0: 记忆进化 - $('#st_bme_evolution').prop('checked', settings.enableEvolution).on('change', function () { - settings.enableEvolution = $(this).prop('checked'); - saveSettingsDebounced(); + // P0: 记忆进化 + $("#st_bme_evolution") + .prop("checked", settings.enableEvolution) + .on("change", function () { + settings.enableEvolution = $(this).prop("checked"); + saveSettingsDebounced(); }); - $('#st_bme_evo_neighbors').val(settings.evoNeighborCount).on('input', function () { - settings.evoNeighborCount = Math.max(1, parseInt($(this).val()) || 5); - saveSettingsDebounced(); + $("#st_bme_evo_neighbors") + .val(settings.evoNeighborCount) + .on("input", function () { + settings.evoNeighborCount = Math.max(1, parseInt($(this).val()) || 5); + saveSettingsDebounced(); }); - // P0: 精确对照 - $('#st_bme_precise_conflict').prop('checked', settings.enablePreciseConflict).on('change', function () { - settings.enablePreciseConflict = $(this).prop('checked'); - saveSettingsDebounced(); + // P0: 精确对照 + $("#st_bme_precise_conflict") + .prop("checked", settings.enablePreciseConflict) + .on("change", function () { + settings.enablePreciseConflict = $(this).prop("checked"); + saveSettingsDebounced(); }); - $('#st_bme_conflict_threshold').val(settings.conflictThreshold).on('input', function () { - settings.conflictThreshold = parseFloat($(this).val()) || 0.85; - saveSettingsDebounced(); + $("#st_bme_conflict_threshold") + .val(settings.conflictThreshold) + .on("input", function () { + settings.conflictThreshold = parseFloat($(this).val()) || 0.85; + saveSettingsDebounced(); }); - // P0: 全局概要 - $('#st_bme_synopsis').prop('checked', settings.enableSynopsis).on('change', function () { - settings.enableSynopsis = $(this).prop('checked'); - saveSettingsDebounced(); + // P0: 全局概要 + $("#st_bme_synopsis") + .prop("checked", settings.enableSynopsis) + .on("change", function () { + settings.enableSynopsis = $(this).prop("checked"); + saveSettingsDebounced(); }); - $('#st_bme_synopsis_every').val(settings.synopsisEveryN).on('input', function () { - settings.synopsisEveryN = Math.max(1, parseInt($(this).val()) || 5); - saveSettingsDebounced(); + $("#st_bme_synopsis_every") + .val(settings.synopsisEveryN) + .on("input", function () { + settings.synopsisEveryN = Math.max(1, parseInt($(this).val()) || 5); + saveSettingsDebounced(); }); - // P1: 认知边界 - $('#st_bme_visibility').prop('checked', settings.enableVisibility ?? false).on('change', function () { - settings.enableVisibility = $(this).prop('checked'); - saveSettingsDebounced(); + // P1: 认知边界 + $("#st_bme_visibility") + .prop("checked", settings.enableVisibility ?? false) + .on("change", function () { + settings.enableVisibility = $(this).prop("checked"); + saveSettingsDebounced(); }); - // P1: 交叉检索 - $('#st_bme_cross_recall').prop('checked', settings.enableCrossRecall ?? false).on('change', function () { - settings.enableCrossRecall = $(this).prop('checked'); - saveSettingsDebounced(); + // P1: 交叉检索 + $("#st_bme_cross_recall") + .prop("checked", settings.enableCrossRecall ?? false) + .on("change", function () { + settings.enableCrossRecall = $(this).prop("checked"); + saveSettingsDebounced(); }); - // P2: 惊奇度分割 - $('#st_bme_smart_trigger').prop('checked', settings.enableSmartTrigger).on('change', function () { - settings.enableSmartTrigger = $(this).prop('checked'); - saveSettingsDebounced(); + // P2: 惊奇度分割 + $("#st_bme_smart_trigger") + .prop("checked", settings.enableSmartTrigger) + .on("change", function () { + settings.enableSmartTrigger = $(this).prop("checked"); + saveSettingsDebounced(); }); - // P2: 主动遗忘 - $('#st_bme_sleep_cycle').prop('checked', settings.enableSleepCycle).on('change', function () { - settings.enableSleepCycle = $(this).prop('checked'); - saveSettingsDebounced(); + // P2: 主动遗忘 + $("#st_bme_sleep_cycle") + .prop("checked", settings.enableSleepCycle) + .on("change", function () { + settings.enableSleepCycle = $(this).prop("checked"); + saveSettingsDebounced(); }); - $('#st_bme_forget_threshold').val(settings.forgetThreshold).on('input', function () { - settings.forgetThreshold = parseFloat($(this).val()) || 0.5; - saveSettingsDebounced(); + $("#st_bme_forget_threshold") + .val(settings.forgetThreshold) + .on("input", function () { + settings.forgetThreshold = parseFloat($(this).val()) || 0.5; + saveSettingsDebounced(); }); - // P2: 概率触发 - $('#st_bme_prob_recall').prop('checked', settings.enableProbRecall).on('change', function () { - settings.enableProbRecall = $(this).prop('checked'); - saveSettingsDebounced(); + // P2: 概率触发 + $("#st_bme_prob_recall") + .prop("checked", settings.enableProbRecall) + .on("change", function () { + settings.enableProbRecall = $(this).prop("checked"); + saveSettingsDebounced(); }); - $('#st_bme_prob_chance').val(settings.probRecallChance).on('input', function () { - settings.probRecallChance = parseFloat($(this).val()) || 0.15; - saveSettingsDebounced(); + $("#st_bme_prob_chance") + .val(settings.probRecallChance) + .on("input", function () { + settings.probRecallChance = parseFloat($(this).val()) || 0.15; + saveSettingsDebounced(); }); - // P2: 反思条目 - $('#st_bme_reflection').prop('checked', settings.enableReflection).on('change', function () { - settings.enableReflection = $(this).prop('checked'); - saveSettingsDebounced(); + // P2: 反思条目 + $("#st_bme_reflection") + .prop("checked", settings.enableReflection) + .on("change", function () { + settings.enableReflection = $(this).prop("checked"); + saveSettingsDebounced(); }); - $('#st_bme_reflect_every').val(settings.reflectEveryN).on('input', function () { - settings.reflectEveryN = Math.max(3, parseInt($(this).val()) || 10); - saveSettingsDebounced(); + $("#st_bme_reflect_every") + .val(settings.reflectEveryN) + .on("input", function () { + settings.reflectEveryN = Math.max(3, parseInt($(this).val()) || 10); + saveSettingsDebounced(); }); } // ==================== 初始化 ==================== (async function init() { - // 加载设置面板 HTML - const settingsHtml = await renderExtensionTemplateAsync('third-party/st-bme', 'settings'); - $('#extensions_settings2').append(settingsHtml); + // 加载设置面板 HTML + const settingsHtml = await renderExtensionTemplateAsync( + "third-party/st-bme", + "settings", + ); + $("#extensions_settings2").append(settingsHtml); - // 绑定 UI - bindSettingsUI(); + // 绑定 UI + bindSettingsUI(); - // 注册事件钩子 - eventSource.on(event_types.CHAT_CHANGED, onChatChanged); - eventSource.on(event_types.GENERATION_AFTER_COMMANDS, onGenerationAfterCommands); - eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onBeforeCombinePrompts); - eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived); + // 注册事件钩子 + eventSource.on(event_types.CHAT_CHANGED, onChatChanged); + eventSource.on( + event_types.GENERATION_AFTER_COMMANDS, + onGenerationAfterCommands, + ); + eventSource.on( + event_types.GENERATE_BEFORE_COMBINE_PROMPTS, + onBeforeCombinePrompts, + ); + eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived); - // 加载当前聊天的图谱 - loadGraphFromChat(); + // 加载当前聊天的图谱 + loadGraphFromChat(); - console.log('[ST-BME] 初始化完成'); + console.log("[ST-BME] 初始化完成"); })(); diff --git a/injector.js b/injector.js index e8b2ae5..0026e11 100644 --- a/injector.js +++ b/injector.js @@ -1,7 +1,7 @@ // ST-BME: Prompt 注入模块 // 将检索结果格式化为表格注入到 LLM 上下文中 -import { getSchemaType } from './schema.js'; +import { getSchemaType } from "./schema.js"; /** * 将检索结果转换为注入文本 @@ -11,84 +11,121 @@ import { getSchemaType } from './schema.js'; * @returns {string} 注入文本 */ export function formatInjection(retrievalResult, schema) { - const { coreNodes, recallNodes } = retrievalResult; - const parts = []; + const { coreNodes, recallNodes, groupedRecallNodes } = retrievalResult; + const parts = []; - // ========== Core 常驻注入 ========== - if (coreNodes.length > 0) { - parts.push('[Memory - Core]'); + // ========== Core 常驻注入 ========== + if (coreNodes.length > 0) { + parts.push("[Memory - Core]"); - // 按类型分组 - const grouped = groupByType(coreNodes); + const grouped = groupByType(coreNodes); - for (const [typeId, nodes] of grouped) { - const typeDef = getSchemaType(schema, typeId); - if (!typeDef) continue; + for (const [typeId, nodes] of grouped) { + const typeDef = getSchemaType(schema, typeId); + if (!typeDef) continue; - const table = formatTable(nodes, typeDef); - if (table) parts.push(table); - } + const table = formatTable(nodes, typeDef); + if (table) parts.push(table); } + } - // ========== Recall 召回注入 ========== - if (recallNodes.length > 0) { - parts.push(''); - parts.push('[Memory - Recalled]'); + // ========== Recall 召回注入 ========== + if (recallNodes.length > 0) { + parts.push(""); + parts.push("[Memory - Recalled]"); - const grouped = groupByType(recallNodes); + const buckets = groupedRecallNodes || { + state: recallNodes.filter( + (n) => n.type === "character" || n.type === "location", + ), + episodic: recallNodes.filter( + (n) => n.type === "event" || n.type === "thread", + ), + reflective: recallNodes.filter( + (n) => n.type === "reflection" || n.type === "synopsis", + ), + rule: recallNodes.filter((n) => n.type === "rule"), + other: recallNodes.filter( + (n) => + ![ + "character", + "location", + "event", + "thread", + "reflection", + "synopsis", + "rule", + ].includes(n.type), + ), + }; - for (const [typeId, nodes] of grouped) { - const typeDef = getSchemaType(schema, typeId); - if (!typeDef) continue; + appendBucket(parts, "当前状态记忆", buckets.state, schema); + appendBucket(parts, "情景事件记忆", buckets.episodic, schema); + appendBucket(parts, "反思与长期锚点", buckets.reflective, schema); + appendBucket(parts, "规则与约束", buckets.rule, schema); + appendBucket(parts, "其他关联记忆", buckets.other, schema); + } - const table = formatTable(nodes, typeDef); - if (table) parts.push(table); - } - } - - return parts.join('\n'); + return parts.join("\n"); } /** * 按类型分组节点 */ function groupByType(nodes) { - const map = new Map(); - for (const node of nodes) { - if (!map.has(node.type)) map.set(node.type, []); - map.get(node.type).push(node); - } - return map; + const map = new Map(); + for (const node of nodes) { + if (!map.has(node.type)) map.set(node.type, []); + map.get(node.type).push(node); + } + return map; +} + +function appendBucket(parts, title, nodes, schema) { + if (!nodes || nodes.length === 0) return; + parts.push(`## ${title}`); + + const grouped = groupByType(nodes); + for (const [typeId, groupedNodes] of grouped) { + const typeDef = getSchemaType(schema, typeId); + if (!typeDef) continue; + + const table = formatTable(groupedNodes, typeDef); + if (table) parts.push(table); + } } /** * 将同类型节点格式化为 Markdown 表格 */ function formatTable(nodes, typeDef) { - if (nodes.length === 0) return ''; + if (nodes.length === 0) return ""; - // 确定要展示的列(有实际数据的列) - const activeCols = typeDef.columns.filter(col => - nodes.some(n => n.fields[col.name]), - ); + // 确定要展示的列(有实际数据的列) + const activeCols = typeDef.columns.filter((col) => + nodes.some((n) => n.fields[col.name]), + ); - if (activeCols.length === 0) return ''; + if (activeCols.length === 0) return ""; - // 表头 - const header = `| ${activeCols.map(c => c.name).join(' | ')} |`; - const separator = `| ${activeCols.map(() => '---').join(' | ')} |`; + // 表头 + const header = `| ${activeCols.map((c) => c.name).join(" | ")} |`; + const separator = `| ${activeCols.map(() => "---").join(" | ")} |`; - // 数据行 - const rows = nodes.map(node => { - const cells = activeCols.map(col => { - const val = node.fields[col.name] || ''; - // 转义管道符,限制单元格长度 - return String(val).replace(/\|/g, '\\|').replace(/\n/g, ' ').slice(0, 200); - }); - return `| ${cells.join(' | ')} |`; + // 数据行 + const rows = nodes.map((node) => { + const cells = activeCols.map((col) => { + const val = node.fields[col.name] || ""; + // 转义管道符,限制单元格长度 + return String(val) + .replace(/\|/g, "\\|") + .replace(/\n/g, " ") + .slice(0, 200); }); + return `| ${cells.join(" | ")} |`; + }); - return `${typeDef.tableName}:\n${header}\n${separator}\n${rows.join('\n')}`; + return `${typeDef.tableName}:\n${header}\n${separator}\n${rows.join("\n")}`; } /** @@ -99,9 +136,9 @@ function formatTable(nodes, typeDef) { * @returns {number} 估算 token 数 */ export function estimateTokens(injectionText) { - if (!injectionText) return 0; - // 简单估算:中文 2 字符/token,英文 4 字符/token - const cnChars = (injectionText.match(/[\u4e00-\u9fff]/g) || []).length; - const otherChars = injectionText.length - cnChars; - return Math.ceil(cnChars / 2 + otherChars / 4); + if (!injectionText) return 0; + // 简单估算:中文 2 字符/token,英文 4 字符/token + const cnChars = (injectionText.match(/[\u4e00-\u9fff]/g) || []).length; + const otherChars = injectionText.length - cnChars; + return Math.ceil(cnChars / 2 + otherChars / 4); } diff --git a/retriever.js b/retriever.js index 9d94e4c..6ec10ca 100644 --- a/retriever.js +++ b/retriever.js @@ -2,19 +2,24 @@ // 融合向量预筛(PeroCore)+ 图扩散(PeroCore PEDSA)+ 可选 LLM 精确召回 // v2: + 认知边界过滤(RoleRAG) + 双记忆交叉检索(AriGraph) + 概率触发 -import { getActiveNodes, buildAdjacencyMap, buildTemporalAdjacencyMap, getNode, getNodeEdges } from './graph.js'; -import { propagateActivation, diffuseAndRank } from './diffusion.js'; -import { embedText, searchSimilar } from './embedding.js'; -import { hybridScore, reinforceAccessBatch } from './dynamics.js'; -import { callLLMForJSON } from './llm.js'; +import { diffuseAndRank } from "./diffusion.js"; +import { hybridScore, reinforceAccessBatch } from "./dynamics.js"; +import { embedText, searchSimilar } from "./embedding.js"; +import { + buildTemporalAdjacencyMap, + getActiveNodes, + getNode, + getNodeEdges, +} from "./graph.js"; +import { callLLMForJSON } from "./llm.js"; /** * 自适应阈值 */ const STRATEGY_THRESHOLDS = { - SMALL: 20, // < 20 节点:跳过向量,全图 + LLM - MEDIUM: 200, // 20-200 节点:向量 + 图扩散 + 评分(不调 LLM) - // > 200 节点:三层全开 + SMALL: 20, // < 20 节点:跳过向量,全图 + LLM + MEDIUM: 200, // 20-200 节点:向量 + 图扩散 + 评分(不调 LLM) + // > 200 节点:三层全开 }; /** @@ -30,213 +35,240 @@ const STRATEGY_THRESHOLDS = { * @returns {Promise} */ export async function retrieve({ - graph, - userMessage, - recentMessages = [], - embeddingConfig, - schema, - options = {}, + graph, + userMessage, + recentMessages = [], + embeddingConfig, + schema, + options = {}, }) { - const topK = options.topK ?? 15; - const maxRecallNodes = options.maxRecallNodes ?? 8; - const enableLLMRecall = options.enableLLMRecall ?? true; - const weights = options.weights ?? {}; + const topK = options.topK ?? 15; + const maxRecallNodes = options.maxRecallNodes ?? 8; + const enableLLMRecall = options.enableLLMRecall ?? true; + const weights = options.weights ?? {}; - // v2 options - const enableVisibility = options.enableVisibility ?? false; - const visibilityFilter = options.visibilityFilter ?? null; - const enableCrossRecall = options.enableCrossRecall ?? false; - const enableProbRecall = options.enableProbRecall ?? false; - const probRecallChance = options.probRecallChance ?? 0.15; + // v2 options + const enableVisibility = options.enableVisibility ?? false; + const visibilityFilter = options.visibilityFilter ?? null; + const enableCrossRecall = options.enableCrossRecall ?? false; + const enableProbRecall = options.enableProbRecall ?? false; + const probRecallChance = options.probRecallChance ?? 0.15; - let activeNodes = getActiveNodes(graph); + let activeNodes = getActiveNodes(graph); - // v2 ⑦: 认知边界过滤(RoleRAG 启发) - if (enableVisibility && visibilityFilter) { - activeNodes = filterByVisibility(activeNodes, visibilityFilter); - } + // v2 ⑦: 认知边界过滤(RoleRAG 启发) + if (enableVisibility && visibilityFilter) { + activeNodes = filterByVisibility(activeNodes, visibilityFilter); + } - const nodeCount = activeNodes.length; - console.log(`[ST-BME] 检索开始: ${nodeCount} 个活跃节点${enableVisibility ? ' (认知边界已启用)' : ''}`); + const nodeCount = activeNodes.length; + console.log( + `[ST-BME] 检索开始: ${nodeCount} 个活跃节点${enableVisibility ? " (认知边界已启用)" : ""}`, + ); - let vectorResults = []; - let diffusionResults = []; - let useLLM = false; + let vectorResults = []; + let diffusionResults = []; + let useLLM = false; - if (nodeCount === 0) { - return buildResult(graph, [], schema); - } + if (nodeCount === 0) { + return buildResult(graph, [], schema); + } - // ========== 第 1 层:向量预筛 ========== - if (nodeCount >= STRATEGY_THRESHOLDS.SMALL && embeddingConfig?.apiUrl) { - console.log('[ST-BME] 第1层: 向量预筛'); - vectorResults = await vectorPreFilter(userMessage, activeNodes, embeddingConfig, topK); - } + // ========== 第 1 层:向量预筛 ========== + if (nodeCount >= STRATEGY_THRESHOLDS.SMALL && embeddingConfig?.apiUrl) { + console.log("[ST-BME] 第1层: 向量预筛"); + vectorResults = await vectorPreFilter( + userMessage, + activeNodes, + embeddingConfig, + topK, + ); + } - // ========== 第 2 层:图扩散 ========== - if (nodeCount >= STRATEGY_THRESHOLDS.SMALL) { - console.log('[ST-BME] 第2层: PEDSA 图扩散'); - const entityAnchors = extractEntityAnchors(userMessage, activeNodes); + // ========== 第 2 层:图扩散 ========== + if (nodeCount >= STRATEGY_THRESHOLDS.SMALL) { + console.log("[ST-BME] 第2层: PEDSA 图扩散"); + const entityAnchors = extractEntityAnchors(userMessage, activeNodes); - const seeds = [ - ...vectorResults.map(v => ({ id: v.nodeId, energy: v.score })), - ...entityAnchors.map(a => ({ id: a.nodeId, energy: 2.0 })), - ]; + const seeds = [ + ...vectorResults.map((v) => ({ id: v.nodeId, energy: v.score })), + ...entityAnchors.map((a) => ({ id: a.nodeId, energy: 2.0 })), + ]; - // v2 ⑧: 双记忆交叉检索(AriGraph 启发) - // 实体锚点命中后,沿边展开关联的情景节点作为额外种子 - if (enableCrossRecall && entityAnchors.length > 0) { - for (const anchor of entityAnchors) { - const connectedEdges = getNodeEdges(graph, anchor.nodeId); - for (const edge of connectedEdges) { - if (edge.invalidAt) continue; - const neighborId = edge.fromId === anchor.nodeId ? edge.toId : edge.fromId; - const neighbor = getNode(graph, neighborId); - if (neighbor && !neighbor.archived && neighbor.type === 'event') { - seeds.push({ id: neighborId, energy: 1.5 * edge.strength }); - } - } - } - } - - // 去重种子 - const seedMap = new Map(); - for (const s of seeds) { - const existing = seedMap.get(s.id) || 0; - if (s.energy > existing) seedMap.set(s.id, s.energy); - } - const uniqueSeeds = [...seedMap.entries()].map(([id, energy]) => ({ id, energy })); - - if (uniqueSeeds.length > 0) { - const adjacencyMap = buildTemporalAdjacencyMap(graph); - diffusionResults = diffuseAndRank(adjacencyMap, uniqueSeeds, { - maxSteps: 2, - decayFactor: 0.6, - topK: 100, - }); + // v2 ⑧: 双记忆交叉检索(AriGraph 启发) + // 实体锚点命中后,沿边展开关联的情景节点作为额外种子 + if (enableCrossRecall && entityAnchors.length > 0) { + for (const anchor of entityAnchors) { + const connectedEdges = getNodeEdges(graph, anchor.nodeId); + for (const edge of connectedEdges) { + if (edge.invalidAt) continue; + const neighborId = + edge.fromId === anchor.nodeId ? edge.toId : edge.fromId; + const neighbor = getNode(graph, neighborId); + if (neighbor && !neighbor.archived && neighbor.type === "event") { + seeds.push({ id: neighborId, energy: 1.5 * edge.strength }); + } } + } } - // ========== 第 3 层:混合评分 + 可选 LLM 精确 ========== - console.log('[ST-BME] 第3层: 混合评分'); - - // 构建评分表 - const scoreMap = new Map(); - - // 添加向量得分 - for (const v of vectorResults) { - const entry = scoreMap.get(v.nodeId) || { graphScore: 0, vectorScore: 0 }; - entry.vectorScore = v.score; - scoreMap.set(v.nodeId, entry); + // 去重种子 + const seedMap = new Map(); + for (const s of seeds) { + const existing = seedMap.get(s.id) || 0; + if (s.energy > existing) seedMap.set(s.id, s.energy); } + const uniqueSeeds = [...seedMap.entries()].map(([id, energy]) => ({ + id, + energy, + })); - // 添加图扩散得分 - for (const d of diffusionResults) { - const entry = scoreMap.get(d.nodeId) || { graphScore: 0, vectorScore: 0 }; - entry.graphScore = d.energy; - scoreMap.set(d.nodeId, entry); + if (uniqueSeeds.length > 0) { + const adjacencyMap = buildTemporalAdjacencyMap(graph); + diffusionResults = diffuseAndRank(adjacencyMap, uniqueSeeds, { + maxSteps: 2, + decayFactor: 0.6, + topK: 100, + }); } + } - // 小图模式:所有节点都参与评分 - if (nodeCount < STRATEGY_THRESHOLDS.SMALL) { - for (const node of activeNodes) { - if (!scoreMap.has(node.id)) { - scoreMap.set(node.id, { graphScore: 0, vectorScore: 0 }); - } - } + // ========== 第 3 层:混合评分 + 可选 LLM 精确 ========== + console.log("[ST-BME] 第3层: 混合评分"); + + // 构建评分表 + const scoreMap = new Map(); + + // 添加向量得分 + for (const v of vectorResults) { + const entry = scoreMap.get(v.nodeId) || { graphScore: 0, vectorScore: 0 }; + entry.vectorScore = v.score; + scoreMap.set(v.nodeId, entry); + } + + // 添加图扩散得分 + for (const d of diffusionResults) { + const entry = scoreMap.get(d.nodeId) || { graphScore: 0, vectorScore: 0 }; + entry.graphScore = d.energy; + scoreMap.set(d.nodeId, entry); + } + + // 小图模式:所有节点都参与评分 + if (nodeCount < STRATEGY_THRESHOLDS.SMALL) { + for (const node of activeNodes) { + if (!scoreMap.has(node.id)) { + scoreMap.set(node.id, { graphScore: 0, vectorScore: 0 }); + } } + } - // 计算混合得分 - const scoredNodes = []; - for (const [nodeId, scores] of scoreMap) { - const node = getNode(graph, nodeId); - if (!node || node.archived) continue; + // 计算混合得分 + const scoredNodes = []; + for (const [nodeId, scores] of scoreMap) { + const node = getNode(graph, nodeId); + if (!node || node.archived) continue; - const finalScore = hybridScore({ - graphScore: scores.graphScore, - vectorScore: scores.vectorScore, - importance: node.importance, - createdTime: node.createdTime, - }, weights); - - scoredNodes.push({ nodeId, node, finalScore, ...scores }); - } - - scoredNodes.sort((a, b) => b.finalScore - a.finalScore); - - // 决定是否使用 LLM 精确召回 - useLLM = enableLLMRecall && ( - nodeCount < STRATEGY_THRESHOLDS.SMALL || // 小图:直接 LLM - nodeCount > STRATEGY_THRESHOLDS.MEDIUM // 大图:LLM 精确 + const finalScore = hybridScore( + { + graphScore: scores.graphScore, + vectorScore: scores.vectorScore, + importance: node.importance, + createdTime: node.createdTime, + }, + weights, ); - let selectedNodeIds; + scoredNodes.push({ nodeId, node, finalScore, ...scores }); + } - if (useLLM && nodeCount > 0) { - console.log('[ST-BME] LLM 精确召回'); - const candidateNodes = scoredNodes.slice(0, Math.min(30, scoredNodes.length)); - selectedNodeIds = await llmRecall( - userMessage, - recentMessages, - candidateNodes, - graph, - schema, - maxRecallNodes, + scoredNodes.sort((a, b) => b.finalScore - a.finalScore); + + // 决定是否使用 LLM 精确召回 + useLLM = + enableLLMRecall && + (nodeCount < STRATEGY_THRESHOLDS.SMALL || // 小图:直接 LLM + nodeCount > STRATEGY_THRESHOLDS.MEDIUM); // 大图:LLM 精确 + + let selectedNodeIds; + + if (useLLM && nodeCount > 0) { + console.log("[ST-BME] LLM 精确召回"); + const candidateNodes = scoredNodes.slice( + 0, + Math.min(30, scoredNodes.length), + ); + selectedNodeIds = await llmRecall( + userMessage, + recentMessages, + candidateNodes, + graph, + schema, + maxRecallNodes, + ); + } else { + // 中等图:直接取 Top-N + selectedNodeIds = scoredNodes.slice(0, topK).map((s) => s.nodeId); + } + + selectedNodeIds = reconstructSceneNodeIds(graph, selectedNodeIds, topK + 6); + + // 访问强化 + const selectedNodes = selectedNodeIds + .map((id) => getNode(graph, id)) + .filter(Boolean); + + reinforceAccessBatch(selectedNodes); + + console.log(`[ST-BME] 检索完成: 选中 ${selectedNodeIds.length} 个节点`); + + // v2 ⑧: 概率触发回忆 + // 未被选中的高重要性节点有概率随机激活 + if (enableProbRecall && probRecallChance > 0) { + const selectedSet = new Set(selectedNodeIds); + const candidates = activeNodes.filter( + (n) => + !selectedSet.has(n.id) && + n.importance >= 6 && + n.type !== "synopsis" && + n.type !== "rule", + ); + for (const c of candidates) { + if (Math.random() < probRecallChance) { + selectedNodeIds.push(c.id); + console.log( + `[ST-BME] 概率触发: ${c.fields?.name || c.fields?.summary || c.id}`, ); - } else { - // 中等图:直接取 Top-N - selectedNodeIds = scoredNodes - .slice(0, topK) - .map(s => s.nodeId); + } } + } - // 访问强化 - const selectedNodes = selectedNodeIds - .map(id => getNode(graph, id)) - .filter(Boolean); + selectedNodeIds = uniqueNodeIds(selectedNodeIds); - reinforceAccessBatch(selectedNodes); - - console.log(`[ST-BME] 检索完成: 选中 ${selectedNodeIds.length} 个节点`); - - // v2 ⑧: 概率触发回忆 - // 未被选中的高重要性节点有概率随机激活 - if (enableProbRecall && probRecallChance > 0) { - const selectedSet = new Set(selectedNodeIds); - const candidates = activeNodes.filter(n => - !selectedSet.has(n.id) && - n.importance >= 6 && - n.type !== 'synopsis' && - n.type !== 'rule', - ); - for (const c of candidates) { - if (Math.random() < probRecallChance) { - selectedNodeIds.push(c.id); - console.log(`[ST-BME] 概率触发: ${c.fields?.name || c.fields?.summary || c.id}`); - } - } - } - - return buildResult(graph, selectedNodeIds, schema); + return buildResult(graph, selectedNodeIds, schema); } /** * 向量预筛选 */ -async function vectorPreFilter(userMessage, activeNodes, embeddingConfig, topK) { - try { - const queryVec = await embedText(userMessage, embeddingConfig); - if (!queryVec) return []; +async function vectorPreFilter( + userMessage, + activeNodes, + embeddingConfig, + topK, +) { + try { + const queryVec = await embedText(userMessage, embeddingConfig); + if (!queryVec) return []; - const candidates = activeNodes - .filter(n => n.embedding) - .map(n => ({ nodeId: n.id, embedding: n.embedding })); + const candidates = activeNodes + .filter((n) => n.embedding) + .map((n) => ({ nodeId: n.id, embedding: n.embedding })); - return searchSimilar(queryVec, candidates, topK); - } catch (e) { - console.error('[ST-BME] 向量预筛失败:', e); - return []; - } + return searchSimilar(queryVec, candidates, topK); + } catch (e) { + console.error("[ST-BME] 向量预筛失败:", e); + return []; + } } /** @@ -244,75 +276,88 @@ async function vectorPreFilter(userMessage, activeNodes, embeddingConfig, topK) * 从用户消息中提取名词/实体,匹配图中的节点名称 */ function extractEntityAnchors(userMessage, activeNodes) { - const anchors = []; + const anchors = []; - for (const node of activeNodes) { - // 检查 name 字段 - const name = node.fields?.name; - if (name && userMessage.includes(name)) { - anchors.push({ nodeId: node.id, entity: name }); - continue; - } - - // 检查 title 字段 - const title = node.fields?.title; - if (title && userMessage.includes(title)) { - anchors.push({ nodeId: node.id, entity: title }); - } + for (const node of activeNodes) { + // 检查 name 字段 + const name = node.fields?.name; + if (name && userMessage.includes(name)) { + anchors.push({ nodeId: node.id, entity: name }); + continue; } - return anchors; + // 检查 title 字段 + const title = node.fields?.title; + if (title && userMessage.includes(title)) { + anchors.push({ nodeId: node.id, entity: title }); + } + } + + return anchors; } /** * LLM 精确召回 */ -async function llmRecall(userMessage, recentMessages, candidates, graph, schema, maxNodes) { - const contextStr = recentMessages.join('\n---\n'); - const candidateDescriptions = candidates.map(c => { - const node = c.node; - const typeDef = schema.find(s => s.id === node.type); - const typeLabel = typeDef?.label || node.type; - const fieldsStr = Object.entries(node.fields) - .map(([k, v]) => `${k}: ${v}`) - .join(', '); - return `[${node.id}] 类型=${typeLabel}, ${fieldsStr} (评分=${c.finalScore.toFixed(3)})`; - }).join('\n'); +async function llmRecall( + userMessage, + recentMessages, + candidates, + graph, + schema, + maxNodes, +) { + const contextStr = recentMessages.join("\n---\n"); + const candidateDescriptions = candidates + .map((c) => { + const node = c.node; + const typeDef = schema.find((s) => s.id === node.type); + const typeLabel = typeDef?.label || node.type; + const fieldsStr = Object.entries(node.fields) + .map(([k, v]) => `${k}: ${v}`) + .join(", "); + return `[${node.id}] 类型=${typeLabel}, ${fieldsStr} (评分=${c.finalScore.toFixed(3)})`; + }) + .join("\n"); - const systemPrompt = [ - '你是一个记忆召回分析器。', - '根据用户最新输入和对话上下文,从候选记忆节点中选择最相关的节点。', - '优先选择:(1) 直接相关的当前场景节点, (2) 因果关系连续性节点, (3) 有潜在影响的背景节点。', - `最多选择 ${maxNodes} 个节点。`, - '输出严格的 JSON 格式:', - '{"selected_ids": ["id1", "id2", ...], "reason": "简要说明选择理由"}', - ].join('\n'); + const systemPrompt = [ + "你是一个记忆召回分析器。", + "根据用户最新输入和对话上下文,从候选记忆节点中选择最相关的节点。", + "优先选择:(1) 直接相关的当前场景节点, (2) 因果关系连续性节点, (3) 有潜在影响的背景节点。", + `最多选择 ${maxNodes} 个节点。`, + "输出严格的 JSON 格式:", + '{"selected_ids": ["id1", "id2", ...], "reason": "简要说明选择理由"}', + ].join("\n"); - const userPrompt = [ - '## 最近对话上下文', - contextStr || '(无)', - '', - '## 用户最新输入', - userMessage, - '', - '## 候选记忆节点', - candidateDescriptions, - '', - '请选择最相关的节点并输出 JSON。', - ].join('\n'); + const userPrompt = [ + "## 最近对话上下文", + contextStr || "(无)", + "", + "## 用户最新输入", + userMessage, + "", + "## 候选记忆节点", + candidateDescriptions, + "", + "请选择最相关的节点并输出 JSON。", + ].join("\n"); - const result = await callLLMForJSON({ systemPrompt, userPrompt, maxRetries: 1 }); + const result = await callLLMForJSON({ + systemPrompt, + userPrompt, + maxRetries: 1, + }); - if (result?.selected_ids && Array.isArray(result.selected_ids)) { - // 校验 ID 有效性 - const validIds = result.selected_ids.filter( - id => candidates.some(c => c.nodeId === id), - ); - return validIds; - } + if (result?.selected_ids && Array.isArray(result.selected_ids)) { + // 校验 ID 有效性 + const validIds = result.selected_ids.filter((id) => + candidates.some((c) => c.nodeId === id), + ); + return validIds; + } - // LLM 失败时回退到纯评分排序 - return candidates.slice(0, maxNodes).map(c => c.nodeId); + // LLM 失败时回退到纯评分排序 + return candidates.slice(0, maxNodes).map((c) => c.nodeId); } // ==================== v2 辅助函数 ==================== @@ -325,20 +370,20 @@ async function llmRecall(userMessage, recentMessages, candidates, graph, schema, * @returns {object[]} */ function filterByVisibility(nodes, characterName) { - return nodes.filter(node => { - // 没有 visibility 字段 → 对所有人可见 - if (!node.fields?.visibility) return true; - // visibility 是数组 → 检查当前角色是否在列表中 - if (Array.isArray(node.fields.visibility)) { - return node.fields.visibility.includes(characterName); - } - // visibility 是字符串(逗号分隔)→ 解析后检查 - if (typeof node.fields.visibility === 'string') { - const visibleTo = node.fields.visibility.split(',').map(s => s.trim()); - return visibleTo.includes(characterName) || visibleTo.includes('*'); - } - return true; - }); + return nodes.filter((node) => { + // 没有 visibility 字段 → 对所有人可见 + if (!node.fields?.visibility) return true; + // visibility 是数组 → 检查当前角色是否在列表中 + if (Array.isArray(node.fields.visibility)) { + return node.fields.visibility.includes(characterName); + } + // visibility 是字符串(逗号分隔)→ 解析后检查 + if (typeof node.fields.visibility === "string") { + const visibleTo = node.fields.visibility.split(",").map((s) => s.trim()); + return visibleTo.includes(characterName) || visibleTo.includes("*"); + } + return true; + }); } /** @@ -346,41 +391,144 @@ function filterByVisibility(nodes, characterName) { * 分离常驻注入(Core)和召回注入(Recall) */ function buildResult(graph, selectedNodeIds, schema) { - const coreNodes = []; // 常驻注入 - const recallNodes = []; // 召回注入 + const coreNodes = []; // 常驻注入 + const recallNodes = []; // 召回注入 - // 常驻注入节点(alwaysInject=true 的类型) - const alwaysInjectTypes = new Set( - schema.filter(s => s.alwaysInject).map(s => s.id), - ); + // 常驻注入节点(alwaysInject=true 的类型) + const alwaysInjectTypes = new Set( + schema.filter((s) => s.alwaysInject).map((s) => s.id), + ); - const activeNodes = getActiveNodes(graph); + const activeNodes = getActiveNodes(graph); - for (const node of activeNodes) { - if (alwaysInjectTypes.has(node.type)) { - coreNodes.push(node); - } + for (const node of activeNodes) { + if (alwaysInjectTypes.has(node.type)) { + coreNodes.push(node); } + } - // 召回注入节点 - const selectedSet = new Set(selectedNodeIds); - for (const nodeId of selectedNodeIds) { - const node = getNode(graph, nodeId); - if (!node) continue; - // 已在 Core 中的不重复添加 - if (!alwaysInjectTypes.has(node.type)) { - recallNodes.push(node); - } + for (const nodeId of selectedNodeIds) { + const node = getNode(graph, nodeId); + if (!node) continue; + if (!alwaysInjectTypes.has(node.type)) { + recallNodes.push(node); } + } - return { - coreNodes, - recallNodes, - selectedNodeIds: [...selectedNodeIds], - stats: { - totalActive: activeNodes.length, - coreCount: coreNodes.length, - recallCount: recallNodes.length, - }, - }; + const groupedRecallNodes = groupRecallNodes(recallNodes); + + return { + coreNodes, + recallNodes, + groupedRecallNodes, + selectedNodeIds: [...selectedNodeIds], + stats: { + totalActive: activeNodes.length, + coreCount: coreNodes.length, + recallCount: recallNodes.length, + episodicCount: groupedRecallNodes.episodic.length, + stateCount: groupedRecallNodes.state.length, + reflectiveCount: groupedRecallNodes.reflective.length, + ruleCount: groupedRecallNodes.rule.length, + }, + }; +} + +function reconstructSceneNodeIds(graph, seedNodeIds, limit = 16) { + const selected = []; + const seen = new Set(); + + function push(nodeId) { + if (!nodeId || seen.has(nodeId)) return; + const node = getNode(graph, nodeId); + if (!node || node.archived) return; + seen.add(nodeId); + selected.push(nodeId); + } + + for (const nodeId of seedNodeIds) { + push(nodeId); + const node = getNode(graph, nodeId); + if (!node) continue; + + if (node.type === "event") { + expandEventScene(graph, node, push); + } else if (node.type === "character" || node.type === "location") { + const relatedEvents = getNodeEdges(graph, node.id) + .filter((e) => !e.invalidAt) + .map((e) => (e.fromId === node.id ? e.toId : e.fromId)) + .map((id) => getNode(graph, id)) + .filter((n) => n && n.type === "event") + .sort((a, b) => b.seq - a.seq) + .slice(0, 2); + for (const eventNode of relatedEvents) { + push(eventNode.id); + expandEventScene(graph, eventNode, push); + } + } + + if (selected.length >= limit) break; + } + + return selected.slice(0, limit); +} + +function expandEventScene(graph, eventNode, push) { + const edges = getNodeEdges(graph, eventNode.id).filter((e) => !e.invalidAt); + for (const edge of edges) { + const neighborId = edge.fromId === eventNode.id ? edge.toId : edge.fromId; + const neighbor = getNode(graph, neighborId); + if (!neighbor || neighbor.archived) continue; + if ( + neighbor.type === "character" || + neighbor.type === "location" || + neighbor.type === "thread" || + neighbor.type === "reflection" + ) { + push(neighbor.id); + } + } + + const adjacentEvents = getTemporalNeighborEvents( + graph, + eventNode.seq, + eventNode.id, + ); + for (const neighborEvent of adjacentEvents) { + push(neighborEvent.id); + } +} + +function getTemporalNeighborEvents(graph, seq, excludeId) { + return getActiveNodes(graph, "event") + .filter((n) => n.id !== excludeId) + .sort((a, b) => Math.abs(a.seq - seq) - Math.abs(b.seq - seq)) + .slice(0, 2); +} + +function groupRecallNodes(nodes) { + return { + state: nodes.filter((n) => n.type === "character" || n.type === "location"), + episodic: nodes.filter((n) => n.type === "event" || n.type === "thread"), + reflective: nodes.filter( + (n) => n.type === "reflection" || n.type === "synopsis", + ), + rule: nodes.filter((n) => n.type === "rule"), + other: nodes.filter( + (n) => + ![ + "character", + "location", + "event", + "thread", + "reflection", + "synopsis", + "rule", + ].includes(n.type), + ), + }; +} + +function uniqueNodeIds(nodeIds) { + return [...new Set(nodeIds)]; }