diff --git a/graph-renderer.js b/graph-renderer.js index 166fa88..55316bf 100644 --- a/graph-renderer.js +++ b/graph-renderer.js @@ -86,7 +86,7 @@ export class GraphRenderer { const node = { id: n.id, type: n.type || 'event', - name: n.content?.name || n.content?.title || n.id.slice(0, 8), + name: getNodeDisplayName(n), importance: n.importance || 5, x: this.canvas.width / 2 + r * Math.cos(angle) + (Math.random() - 0.5) * 40, y: this.canvas.height / 2 + r * Math.sin(angle) + (Math.random() - 0.5) * 40, @@ -101,7 +101,7 @@ export class GraphRenderer { // 转换边 this.edges = graph.edges - .filter(e => this.nodeMap.has(e.fromId) && this.nodeMap.has(e.toId)) + .filter(e => !e.invalidAt && !e.expiredAt && this.nodeMap.has(e.fromId) && this.nodeMap.has(e.toId)) .map(e => ({ from: this.nodeMap.get(e.fromId), to: this.nodeMap.get(e.toId), @@ -459,3 +459,14 @@ export class GraphRenderer { this._resizeObserver?.disconnect(); } } + +function getNodeDisplayName(node) { + return ( + node?.fields?.name || + node?.fields?.title || + node?.fields?.summary || + node?.fields?.insight || + node?.id?.slice(0, 8) || + '—' + ); +} diff --git a/graph.js b/graph.js index fc5840a..1100d73 100644 --- a/graph.js +++ b/graph.js @@ -90,15 +90,6 @@ export function addNode(graph, node) { return node; } -/** - * 获取所有活跃(未归档)节点 - * @param {GraphState} graph - * @returns {Array} - */ -export function getActiveNodes(graph) { - return graph.nodes.filter((n) => !n.archived); -} - /** * 根据 ID 获取节点 * @param {GraphState} graph diff --git a/index.js b/index.js index b3c3fa6..e979813 100644 --- a/index.js +++ b/index.js @@ -5,13 +5,13 @@ import { eventSource, event_types, saveSettingsDebounced, -} from "../../../script.js"; +} from "../../../../script.js"; import { extension_settings, getContext, renderExtensionTemplateAsync, saveMetadataDebounced, -} from "../../extensions.js"; +} from "../../../extensions.js"; import { compressAll, sleepCycle } from "./compressor.js"; import { testConnection as testEmbeddingConnection } from "./embedding.js"; @@ -27,6 +27,7 @@ import { exportGraph, getGraphStats, importGraph, + getNode, } from "./graph.js"; import { estimateTokens, formatInjection } from "./injector.js"; import { retrieve } from "./retriever.js"; @@ -38,6 +39,7 @@ let _themesModule = null; const MODULE_NAME = "st_bme"; const GRAPH_METADATA_KEY = "st_bme_graph"; +const TEMPLATE_PATH = "third-party/ST-BME"; // ==================== 默认设置 ==================== @@ -127,6 +129,65 @@ let lastExtractedItems = []; // 最近提取的节点(面板展示用) let lastRecalledItems = []; // 最近召回的节点(面板展示用) let extractionCount = 0; // v2: 提取次数计数器(定期触发概要/遗忘/反思) +function getNodeDisplayName(node) { + return ( + node?.fields?.name || + node?.fields?.title || + node?.fields?.summary || + node?.fields?.insight || + node?.id || + "—" + ); +} + +function toPanelNodeItem(node, meta = "") { + return { + id: node.id, + type: node.type, + name: getNodeDisplayName(node), + meta, + }; +} + +function updateLastExtractedItems(nodeIds = []) { + if (!currentGraph || !Array.isArray(nodeIds)) { + lastExtractedItems = []; + return; + } + + lastExtractedItems = nodeIds + .map((id) => getNode(currentGraph, id)) + .filter(Boolean) + .slice(-5) + .reverse() + .map((node) => + toPanelNodeItem( + node, + `seq ${node.seqRange?.[1] ?? node.seq ?? 0} · ${new Date( + node.createdTime || Date.now(), + ).toLocaleTimeString()}`, + ), + ); +} + +function updateLastRecalledItems(nodeIds = []) { + if (!currentGraph || !Array.isArray(nodeIds)) { + lastRecalledItems = []; + return; + } + + lastRecalledItems = nodeIds + .map((id) => getNode(currentGraph, id)) + .filter(Boolean) + .slice(0, 8) + .map((node) => + toPanelNodeItem( + node, + `imp ${node.importance ?? 5} · seq ${node.seqRange?.[1] ?? node.seq ?? 0}`, + ), + ); +} + // ==================== 设置管理 ==================== function getSettings() { @@ -164,6 +225,9 @@ function loadGraphFromChat() { const context = getContext(); if (!context.chatMetadata) { currentGraph = createEmptyGraph(); + lastExtractedItems = []; + lastRecalledItems = []; + lastInjectionContent = ""; return; } @@ -174,6 +238,10 @@ function loadGraphFromChat() { } else { currentGraph = createEmptyGraph(); } + + lastExtractedItems = []; + updateLastRecalledItems(currentGraph.lastRecallResult || []); + lastInjectionContent = ""; } function saveGraphToChat() { @@ -296,6 +364,69 @@ function clampFloat(value, fallback, min = 0, max = 1) { return Math.min(max, Math.max(min, num)); } +function getCurrentChatSeq(context = getContext()) { + const chat = context?.chat; + if (Array.isArray(chat) && chat.length > 0) { + return chat.length - 1; + } + return currentGraph?.lastProcessedSeq ?? 0; +} + +async function handleExtractionSuccess(result, endIdx, settings) { + extractionCount++; + updateLastExtractedItems(result.newNodeIds || []); + + 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); + } + } + + if (settings.enableSynopsis && extractionCount % settings.synopsisEveryN === 0) { + try { + await generateSynopsis({ + graph: currentGraph, + schema: getSchema(), + currentSeq: endIdx, + }); + } catch (e) { + console.error("[ST-BME] 概要生成失败:", e); + } + } + + if ( + settings.enableReflection && + extractionCount % settings.reflectEveryN === 0 + ) { + try { + await generateReflection({ + graph: currentGraph, + currentSeq: endIdx, + }); + } catch (e) { + console.error("[ST-BME] 反思生成失败:", e); + } + } + + if (settings.enableSleepCycle && extractionCount % settings.sleepEveryN === 0) { + try { + sleepCycle(currentGraph, settings); + } catch (e) { + console.error("[ST-BME] 主动遗忘失败:", e); + } + } + + await compressAll(currentGraph, getSchema(), getEmbeddingConfig()); + saveGraphToChat(); +} + /** * 提取管线:处理未提取的对话楼层 */ @@ -383,68 +514,7 @@ async function runExtraction() { }); 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.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(); + await handleExtractionSuccess(result, endIdx, settings); } } catch (e) { console.error("[ST-BME] 提取失败:", e); @@ -534,6 +604,7 @@ async function runRecall() { // 保存召回结果和访问强化 currentGraph.lastRecallResult = result.selectedNodeIds; + updateLastRecalledItems(result.selectedNodeIds || []); saveGraphToChat(); } catch (e) { console.error("[ST-BME] 召回失败:", e); @@ -591,6 +662,9 @@ async function onRebuild() { if (!confirm("确定要从当前聊天重建图谱?这将清除现有图谱数据。")) return; currentGraph = createEmptyGraph(); + lastExtractedItems = []; + lastRecalledItems = []; + lastInjectionContent = ""; saveGraphToChat(); toastr.info("图谱已重置,将在下次生成时重新提取"); @@ -636,6 +710,9 @@ async function onImportGraph() { try { const text = await file.text(); currentGraph = importGraph(text); + lastExtractedItems = []; + updateLastRecalledItems(currentGraph.lastRecallResult || []); + lastInjectionContent = ""; saveGraphToChat(); toastr.success("图谱已导入"); } catch (err) { @@ -684,6 +761,123 @@ async function onTestEmbedding() { } } +async function onManualExtract() { + if (isExtracting) return; + if (!currentGraph) currentGraph = createEmptyGraph(); + + const context = getContext(); + const chat = context.chat; + if (!Array.isArray(chat) || chat.length === 0) { + toastr.info("当前聊天为空,暂无可提取内容"); + return; + } + + 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 = Number.isFinite(currentGraph.lastProcessedSeq) + ? currentGraph.lastProcessedSeq + : -1; + const pendingAssistantTurns = assistantTurns.filter((i) => i > lastProcessed); + if (pendingAssistantTurns.length === 0) { + toastr.info("没有待提取的新回复"); + return; + } + + const startIdx = pendingAssistantTurns[0]; + const endIdx = pendingAssistantTurns[pendingAssistantTurns.length - 1]; + const settings = getSettings(); + const contextTurns = clampInt(settings.extractContextTurns, 2, 0, 20); + const contextStart = Math.max(0, startIdx - contextTurns * 2); + const messages = []; + + for (let i = contextStart; i <= endIdx && i < chat.length; i++) { + const msg = chat[i]; + if (msg.is_system) continue; + messages.push({ + seq: i, + role: msg.is_user ? "user" : "assistant", + content: msg.mes || "", + }); + } + + isExtracting = true; + try { + const result = await extractMemories({ + graph: currentGraph, + messages, + startSeq: startIdx, + endSeq: endIdx, + lastProcessedSeq: lastProcessed, + schema: getSchema(), + embeddingConfig: getEmbeddingConfig(), + extractPrompt: settings.extractPrompt || undefined, + v2Options: { + enablePreciseConflict: settings.enablePreciseConflict, + conflictThreshold: settings.conflictThreshold, + }, + }); + + if (!result.success) { + toastr.warning("手动提取未返回有效结果"); + return; + } + + await handleExtractionSuccess(result, endIdx, settings); + toastr.success( + `提取完成:新建 ${result.newNodes},更新 ${result.updatedNodes},新边 ${result.newEdges}`, + ); + } catch (e) { + console.error("[ST-BME] 手动提取失败:", e); + toastr.error(`手动提取失败: ${e.message || e}`); + } finally { + isExtracting = false; + } +} + +async function onManualSleep() { + if (!currentGraph) return; + const result = sleepCycle(currentGraph, getSettings()); + saveGraphToChat(); + toastr.info(`执行完成:归档 ${result.forgotten} 个节点`); +} + +async function onManualSynopsis() { + if (!currentGraph) return; + await generateSynopsis({ + graph: currentGraph, + schema: getSchema(), + currentSeq: getCurrentChatSeq(), + }); + saveGraphToChat(); + toastr.success("概要生成完成"); +} + +async function onManualEvolve() { + if (!currentGraph) return; + + const candidateIds = lastExtractedItems.map((item) => item.id).filter(Boolean); + if (candidateIds.length === 0) { + toastr.info("暂无最近提取节点可用于进化"); + return; + } + + const result = await evolveMemories({ + graph: currentGraph, + newNodeIds: candidateIds, + embeddingConfig: getEmbeddingConfig(), + options: { neighborCount: getSettings().evoNeighborCount }, + }); + saveGraphToChat(); + toastr.success( + `进化完成:${result.evolved} 次进化,${result.connections} 条链接,${result.updates} 个回溯更新`, + ); +} + // ==================== 设置 UI ==================== function bindSettingsUI() { @@ -935,15 +1129,16 @@ function bindSettingsUI() { // ==================== 初始化 ==================== (async function init() { - // 加载设置面板 HTML - const settingsHtml = await renderExtensionTemplateAsync( - "third-party/st-bme", - "settings", - ); - $("#extensions_settings2").append(settingsHtml); - - // 绑定 UI - bindSettingsUI(); + try { + const settingsHtml = await renderExtensionTemplateAsync( + TEMPLATE_PATH, + "settings", + ); + $("#extensions_settings2").append(settingsHtml); + bindSettingsUI(); + } catch (settingsError) { + console.error("[ST-BME] 设置面板加载失败:", settingsError); + } // 注册事件钩子 eventSource.on(event_types.CHAT_CHANGED, onChatChanged); @@ -964,12 +1159,12 @@ function bindSettingsUI() { try { // 动态加载面板模块 - _panelModule = await import('./panel.js'); - _themesModule = await import('./themes.js'); + _panelModule = await import("./panel.js"); + _themesModule = await import("./themes.js"); // 应用主题 const settings = getSettings(); - _themesModule.applyTheme(settings.panelTheme || 'crimson'); + _themesModule.applyTheme(settings.panelTheme || "crimson"); // 初始化操控面板 await _panelModule.initPanel({ @@ -977,64 +1172,16 @@ function bindSettingsUI() { getSettings: () => getSettings(), getLastExtract: () => lastExtractedItems, getLastRecall: () => lastRecalledItems, + getLastInjection: () => lastInjectionContent, actions: { - extract: async () => { - const context = getContext(); - const chat = context.chat; - if (!chat || !chat.length) return; - const s = getSettings(); - const result = await extractMemories( - currentGraph, chat, chat.length - 1, s.extractContextTurns, - getSchema(), getEmbeddingConfig(), s, - ); - if (result?.newNodes?.length) { - lastExtractedItems = result.newNodes.map(n => ({ - type: n.type, name: n.content?.name || '', time: new Date().toLocaleTimeString(), - })).slice(0, 5); - } - saveGraphToChat(); - }, - compress: async () => { - await compressAll(currentGraph, getSettings()); - saveGraphToChat(); - }, - sleep: async () => { - await sleepCycle(currentGraph, getSettings()); - saveGraphToChat(); - }, - synopsis: async () => { - await generateSynopsis(currentGraph, getSettings()); - saveGraphToChat(); - }, - export: () => { - 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.json'; a.click(); - URL.revokeObjectURL(url); - }, - import: () => { - const input = document.createElement('input'); - input.type = 'file'; input.accept = '.json'; - input.addEventListener('change', async (e) => { - const file = e.target.files?.[0]; - if (!file) return; - const text = await file.text(); - currentGraph = importGraph(text); - saveGraphToChat(); - }); - input.click(); - }, - rebuild: async () => { - if (!confirm('确定要重建图谱吗?这将清除所有现有数据。')) return; - currentGraph = createEmptyGraph(); - saveGraphToChat(); - }, - evolve: async () => { - await evolveMemories(currentGraph, getEmbeddingConfig(), getSettings()); - saveGraphToChat(); - }, + extract: onManualExtract, + compress: onManualCompress, + sleep: onManualSleep, + synopsis: onManualSynopsis, + export: onExportGraph, + import: onImportGraph, + rebuild: onRebuild, + evolve: onManualEvolve, }, }); diff --git a/llm.js b/llm.js index 8287cc3..2344118 100644 --- a/llm.js +++ b/llm.js @@ -1,7 +1,7 @@ // ST-BME: LLM 调用封装 // 包装 ST 的 sendOpenAIRequest,提供结构化 JSON 输出和重试机制 -import { sendOpenAIRequest } from '../../openai.js'; +import { sendOpenAIRequest } from "../../../openai.js"; /** * 调用 LLM 并期望返回结构化 JSON diff --git a/panel.js b/panel.js index b131b5a..5970a14 100644 --- a/panel.js +++ b/panel.js @@ -1,38 +1,49 @@ // ST-BME: 操控面板交互逻辑 -import { renderExtensionTemplateAsync } from '../../extensions.js'; -import { GraphRenderer } from './graph-renderer.js'; -import { getNodeColors } from './themes.js'; +import { renderExtensionTemplateAsync } from "../../../extensions.js"; +import { GraphRenderer } from "./graph-renderer.js"; +import { getNodeColors } from "./themes.js"; let panelEl = null; let overlayEl = null; let graphRenderer = null; let mobileGraphRenderer = null; -let isOpen = false; // 由 index.js 注入的引用 let _getGraph = null; let _getSettings = null; let _getLastExtract = null; let _getLastRecall = null; +let _getLastInjection = null; let _actionHandlers = {}; /** * 初始化面板(由 index.js 调用一次) */ -export async function initPanel({ getGraph, getSettings, getLastExtract, getLastRecall, actions }) { +export async function initPanel({ + getGraph, + getSettings, + getLastExtract, + getLastRecall, + getLastInjection, + actions, +}) { _getGraph = getGraph; _getSettings = getSettings; _getLastExtract = getLastExtract; _getLastRecall = getLastRecall; + _getLastInjection = getLastInjection; _actionHandlers = actions || {}; - // 加载 HTML 模板 - const html = await renderExtensionTemplateAsync('third-party/st-bme', 'panel'); - $('body').append(html); + overlayEl = document.getElementById("st-bme-panel-overlay"); + panelEl = document.getElementById("st-bme-panel"); - overlayEl = document.getElementById('st-bme-panel-overlay'); - panelEl = document.getElementById('st-bme-panel'); + if (!overlayEl || !panelEl) { + const html = await renderExtensionTemplateAsync("third-party/ST-BME", "panel"); + $("body").append(html); + overlayEl = document.getElementById("st-bme-panel-overlay"); + panelEl = document.getElementById("st-bme-panel"); + } _bindTabs(); _bindClose(); @@ -45,24 +56,21 @@ export async function initPanel({ getGraph, getSettings, getLastExtract, getLast */ export function openPanel() { if (!overlayEl) return; - overlayEl.classList.add('active'); - isOpen = true; + overlayEl.classList.add("active"); const isMobile = _isMobile(); + const settings = _getSettings?.() || {}; + const themeName = settings.panelTheme || "crimson"; - // 初始化桌面端图谱渲染器 - const canvas = document.getElementById('bme-graph-canvas'); + const canvas = document.getElementById("bme-graph-canvas"); if (canvas && !graphRenderer && !isMobile) { - const settings = _getSettings?.() || {}; - graphRenderer = new GraphRenderer(canvas, settings.panelTheme || 'crimson'); + graphRenderer = new GraphRenderer(canvas, themeName); graphRenderer.onNodeSelect = (node) => _showNodeDetail(node); } - // 初始化移动端 mini 图谱渲染器 - const mobileCanvas = document.getElementById('bme-mobile-graph-canvas'); + const mobileCanvas = document.getElementById("bme-mobile-graph-canvas"); if (mobileCanvas && !mobileGraphRenderer && isMobile) { - const settings = _getSettings?.() || {}; - mobileGraphRenderer = new GraphRenderer(mobileCanvas, settings.panelTheme || 'crimson'); + mobileGraphRenderer = new GraphRenderer(mobileCanvas, themeName); mobileGraphRenderer.onNodeSelect = (node) => _showNodeDetail(node); } @@ -76,24 +84,23 @@ export function openPanel() { */ export function closePanel() { if (!overlayEl) return; - overlayEl.classList.remove('active'); - isOpen = false; + overlayEl.classList.remove("active"); } /** * 更新主题 */ export function updatePanelTheme(themeName) { - if (graphRenderer) graphRenderer.setTheme(themeName); - if (mobileGraphRenderer) mobileGraphRenderer.setTheme(themeName); + graphRenderer?.setTheme(themeName); + mobileGraphRenderer?.setTheme(themeName); + _buildLegend(); } // ==================== Tab 切换 ==================== function _bindTabs() { - // 桌面端 sidebar tabs + 手机端 bottom tabs - document.querySelectorAll('.bme-tab-btn').forEach(btn => { - btn.addEventListener('click', () => { + panelEl?.querySelectorAll(".bme-tab-btn").forEach((btn) => { + btn.addEventListener("click", () => { const tabId = btn.dataset.tab; _switchTab(tabId); }); @@ -101,21 +108,26 @@ function _bindTabs() { } function _switchTab(tabId) { - // 更新所有 tab 按钮状态 - document.querySelectorAll('.bme-tab-btn').forEach(b => { - b.classList.toggle('active', b.dataset.tab === tabId); + panelEl?.querySelectorAll(".bme-tab-btn").forEach((btn) => { + btn.classList.toggle("active", btn.dataset.tab === tabId); }); - // 更新 pane 显示 - document.querySelectorAll('.bme-tab-pane').forEach(p => { - p.classList.toggle('active', p.id === `bme-pane-${tabId}`); + panelEl?.querySelectorAll(".bme-tab-pane").forEach((pane) => { + pane.classList.toggle("active", pane.id === `bme-pane-${tabId}`); }); - // 按需刷新内容 switch (tabId) { - case 'dashboard': _refreshDashboard(); break; - case 'memory': _refreshMemoryBrowser(); break; - case 'injection': _refreshInjectionPreview(); break; + case "dashboard": + _refreshDashboard(); + break; + case "memory": + _refreshMemoryBrowser(); + break; + case "injection": + void _refreshInjectionPreview(); + break; + default: + break; } } @@ -125,46 +137,46 @@ function _refreshDashboard() { const graph = _getGraph?.(); if (!graph) return; - const activeNodes = graph.nodes.filter(n => !n.archived); - const archived = graph.nodes.filter(n => n.archived).length; - const total = graph.nodes.length; - const fragRate = total > 0 ? Math.round((archived / total) * 100) : 0; + const activeNodes = graph.nodes.filter((node) => !node.archived); + const archivedCount = graph.nodes.filter((node) => node.archived).length; + const totalNodes = graph.nodes.length; + const fragRate = totalNodes > 0 ? Math.round((archivedCount / totalNodes) * 100) : 0; - _setText('bme-stat-nodes', activeNodes.length); - _setText('bme-stat-edges', graph.edges.length); - _setText('bme-stat-archived', archived); - _setText('bme-stat-frag', fragRate + '%'); - _setText('bme-status-meta', `NODES: ${activeNodes.length} | EDGES: ${graph.edges.length}`); + _setText("bme-stat-nodes", activeNodes.length); + _setText("bme-stat-edges", graph.edges.length); + _setText("bme-stat-archived", archivedCount); + _setText("bme-stat-frag", `${fragRate}%`); + _setText( + "bme-status-meta", + `NODES: ${activeNodes.length} | EDGES: ${graph.edges.length}`, + ); - // 最近提取 - const extractList = document.getElementById('bme-recent-extract'); - if (extractList) { - const items = _getLastExtract?.() || []; - extractList.innerHTML = items.length ? items.map(item => - `
  • - ${_typeLabel(item.type)} -
    -
    ${_escHtml(item.name || item.content?.name || '—')}
    -
    ${item.time || ''}
    -
    -
  • ` - ).join('') : '
  • 暂无数据
  • '; + _renderRecentList("bme-recent-extract", _getLastExtract?.() || []); + _renderRecentList("bme-recent-recall", _getLastRecall?.() || []); +} + +function _renderRecentList(elementId, items) { + const listEl = document.getElementById(elementId); + if (!listEl) return; + + if (!items.length) { + listEl.innerHTML = + '
  • 暂无数据
  • '; + return; } - // 最近召回 - const recallList = document.getElementById('bme-recent-recall'); - if (recallList) { - const items = _getLastRecall?.() || []; - recallList.innerHTML = items.length ? items.map(item => - `
  • + listEl.innerHTML = items + .map((item) => { + const secondary = item.meta || item.time || ""; + return `
  • ${_typeLabel(item.type)}
    -
    ${_escHtml(item.name || '—')}
    -
    score: ${(item.score || 0).toFixed(2)}
    +
    ${_escHtml(item.name || "—")}
    +
    ${_escHtml(secondary)}
    -
  • ` - ).join('') : '
  • 暂无数据
  • '; - } + `; + }) + .join(""); } // ==================== 记忆浏览器 ==================== @@ -173,64 +185,69 @@ function _refreshMemoryBrowser() { const graph = _getGraph?.(); if (!graph) return; - const searchInput = document.getElementById('bme-memory-search'); - const filterSelect = document.getElementById('bme-memory-filter'); - const listEl = document.getElementById('bme-memory-list'); + const searchInput = document.getElementById("bme-memory-search"); + const filterSelect = document.getElementById("bme-memory-filter"); + const listEl = document.getElementById("bme-memory-list"); if (!listEl) return; - const query = (searchInput?.value || '').toLowerCase(); - const filter = filterSelect?.value || 'all'; + const query = String(searchInput?.value || "").trim().toLowerCase(); + const filter = filterSelect?.value || "all"; - let nodes = graph.nodes.filter(n => !n.archived); - if (filter !== 'all') { - nodes = nodes.filter(n => n.type === filter); + let nodes = graph.nodes.filter((node) => !node.archived); + if (filter !== "all") { + nodes = nodes.filter((node) => node.type === filter); } if (query) { - nodes = nodes.filter(n => { - const name = (n.content?.name || n.content?.title || '').toLowerCase(); - const text = JSON.stringify(n.content || {}).toLowerCase(); + nodes = nodes.filter((node) => { + const name = getNodeDisplayName(node).toLowerCase(); + const text = JSON.stringify(node.fields || {}).toLowerCase(); return name.includes(query) || text.includes(query); }); } - // 按 importance 降序 - nodes.sort((a, b) => (b.importance || 5) - (a.importance || 5)); + nodes.sort((a, b) => { + const importanceDiff = (b.importance || 5) - (a.importance || 5); + if (importanceDiff !== 0) return importanceDiff; + return (b.seqRange?.[1] ?? b.seq ?? 0) - (a.seqRange?.[1] ?? a.seq ?? 0); + }); - listEl.innerHTML = nodes.slice(0, 100).map(n => { - const name = n.content?.name || n.content?.title || n.id.slice(0, 8); - const snippet = _getNodeSnippet(n); - return `
  • - ${_typeLabel(n.type)} -
    -
    ${_escHtml(name)}
    -
    ${_escHtml(snippet)}
    -
    - imp: ${n.importance || 5} - acc: ${n.accessCount || 0} - seq: ${n.seq || 0} + listEl.innerHTML = nodes + .slice(0, 100) + .map((node) => { + const name = getNodeDisplayName(node); + const snippet = _getNodeSnippet(node); + return `
  • + ${_typeLabel(node.type)} +
    +
    ${_escHtml(name)}
    +
    ${_escHtml(snippet)}
    +
    + imp: ${node.importance || 5} + acc: ${node.accessCount || 0} + seq: ${node.seqRange?.[1] ?? node.seq ?? 0} +
    - -
  • `; - }).join(''); + `; + }) + .join(""); - // 点击事件 - listEl.querySelectorAll('.bme-memory-item').forEach(el => { - el.addEventListener('click', () => { + listEl.querySelectorAll(".bme-memory-item").forEach((el) => { + el.addEventListener("click", () => { const nodeId = el.dataset.nodeId; - if (graphRenderer) graphRenderer.highlightNode(nodeId); - const node = graph.nodes.find(n => n.id === nodeId); - if (node) _showNodeDetail({ raw: node, type: node.type, name: node.content?.name || '' }); + graphRenderer?.highlightNode(nodeId); + mobileGraphRenderer?.highlightNode(nodeId); + const node = graph.nodes.find((candidate) => candidate.id === nodeId); + if (node) _showNodeDetail(node); }); }); - // 搜索绑定(防抖) - if (!searchInput._bmeBound) { - let timer; - searchInput.addEventListener('input', () => { + if (searchInput && !searchInput._bmeBound) { + let timer = null; + searchInput.addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(() => _refreshMemoryBrowser(), 200); }); - filterSelect?.addEventListener('change', () => _refreshMemoryBrowser()); + filterSelect?.addEventListener("change", () => _refreshMemoryBrowser()); searchInput._bmeBound = true; } } @@ -238,24 +255,26 @@ function _refreshMemoryBrowser() { // ==================== 注入预览 ==================== async function _refreshInjectionPreview() { - const graph = _getGraph?.(); - const settings = _getSettings?.(); - if (!graph || !settings) return; - - const container = document.getElementById('bme-injection-content'); - const tokenEl = document.getElementById('bme-injection-tokens'); + const container = document.getElementById("bme-injection-content"); + const tokenEl = document.getElementById("bme-injection-tokens"); if (!container) return; - try { - // 动态导入注入器模块 - const { estimateTokens, formatInjection } = await import('./injector.js'); - const injection = formatInjection(graph, settings.nodeSchema || []); - const totalTokens = estimateTokens(injection); + const injection = String(_getLastInjection?.() || "").trim(); + if (!injection) { + container.innerHTML = + '
    暂无注入内容。先完成一次召回或正常生成后再查看。
    '; + if (tokenEl) tokenEl.textContent = ""; + return; + } + try { + const { estimateTokens } = await import("./injector.js"); + const totalTokens = estimateTokens(injection); container.innerHTML = `
    ${_escHtml(injection)}
    `; if (tokenEl) tokenEl.textContent = `≈ ${totalTokens} tokens`; - } catch (e) { - container.innerHTML = `
    预览生成失败: ${_escHtml(e.message)}
    `; + } catch (error) { + container.innerHTML = `
    预览生成失败: ${_escHtml(error.message)}
    `; + if (tokenEl) tokenEl.textContent = ""; } } @@ -264,85 +283,101 @@ async function _refreshInjectionPreview() { function _refreshGraph() { const graph = _getGraph?.(); if (!graph) return; - if (graphRenderer) graphRenderer.loadGraph(graph); - if (mobileGraphRenderer) mobileGraphRenderer.loadGraph(graph); + graphRenderer?.loadGraph(graph); + mobileGraphRenderer?.loadGraph(graph); } function _buildLegend() { - const legendEl = document.getElementById('bme-graph-legend'); + const legendEl = document.getElementById("bme-graph-legend"); if (!legendEl) return; const settings = _getSettings?.() || {}; - const colors = getNodeColors(settings.panelTheme || 'crimson'); + const colors = getNodeColors(settings.panelTheme || "crimson"); const types = [ - { key: 'character', label: '角色' }, - { key: 'event', label: '事件' }, - { key: 'location', label: '地点' }, - { key: 'thread', label: '线索' }, - { key: 'rule', label: '规则' }, - { key: 'synopsis', label: '概要' }, + { key: "character", label: "角色" }, + { key: "event", label: "事件" }, + { key: "location", label: "地点" }, + { key: "thread", label: "主线" }, + { key: "rule", label: "规则" }, + { key: "synopsis", label: "概要" }, + { key: "reflection", label: "反思" }, ]; - legendEl.innerHTML = types.map(t => - ` - - ${t.label} - ` - ).join(''); + legendEl.innerHTML = types + .map( + (type) => ` + + ${type.label} + `, + ) + .join(""); } function _bindGraphControls() { - document.getElementById('bme-graph-zoom-in')?.addEventListener('click', () => graphRenderer?.zoomIn()); - document.getElementById('bme-graph-zoom-out')?.addEventListener('click', () => graphRenderer?.zoomOut()); - document.getElementById('bme-graph-reset')?.addEventListener('click', () => graphRenderer?.resetView()); + document + .getElementById("bme-graph-zoom-in") + ?.addEventListener("click", () => graphRenderer?.zoomIn()); + document + .getElementById("bme-graph-zoom-out") + ?.addEventListener("click", () => graphRenderer?.zoomOut()); + document + .getElementById("bme-graph-reset") + ?.addEventListener("click", () => graphRenderer?.resetView()); } // ==================== 节点详情 ==================== function _showNodeDetail(node) { - const detailEl = document.getElementById('bme-node-detail'); - const titleEl = document.getElementById('bme-detail-title'); - const bodyEl = document.getElementById('bme-detail-body'); + const detailEl = document.getElementById("bme-node-detail"); + const titleEl = document.getElementById("bme-detail-title"); + const bodyEl = document.getElementById("bme-detail-body"); if (!detailEl || !titleEl || !bodyEl) return; const raw = node.raw || node; - const name = raw.content?.name || raw.content?.title || raw.id?.slice(0, 8) || '—'; - titleEl.textContent = name; + const fields = raw.fields || {}; + titleEl.textContent = getNodeDisplayName(raw); - const fields = [ - { label: '类型', value: _typeLabel(raw.type) }, - { label: 'ID', value: raw.id?.slice(0, 12) + '...' }, - { label: '重要度', value: raw.importance || 5 }, - { label: '访问次数', value: raw.accessCount || 0 }, - { label: '序列号', value: raw.seq || 0 }, + const items = [ + { label: "类型", value: _typeLabel(raw.type) }, + { label: "ID", value: raw.id || "—" }, + { label: "重要度", value: raw.importance || 5 }, + { label: "访问次数", value: raw.accessCount || 0 }, + { label: "序列号", value: raw.seqRange?.[1] ?? raw.seq ?? 0 }, ]; - // 展示 content 字段 - if (raw.content) { - for (const [k, v] of Object.entries(raw.content)) { - if (k === 'embedding') continue; - fields.push({ label: k, value: typeof v === 'object' ? JSON.stringify(v, null, 2) : v }); - } + if (Array.isArray(raw.seqRange)) { + items.push({ label: "序列范围", value: `${raw.seqRange[0]} ~ ${raw.seqRange[1]}` }); + } + if (Array.isArray(raw.clusters) && raw.clusters.length > 0) { + items.push({ label: "聚类标签", value: raw.clusters.join(", ") }); } - bodyEl.innerHTML = fields.map(f => - `
    - -
    ${_escHtml(String(f.value))}
    -
    ` - ).join(''); + for (const [key, value] of Object.entries(fields)) { + items.push({ + label: key, + value: typeof value === "object" ? JSON.stringify(value, null, 2) : value, + }); + } - detailEl.classList.add('open'); + bodyEl.innerHTML = items + .map( + (item) => `
    + +
    ${_escHtml(String(item.value ?? "—"))}
    +
    `, + ) + .join(""); + + detailEl.classList.add("open"); } function _bindClose() { - document.getElementById('bme-panel-close')?.addEventListener('click', closePanel); - document.getElementById('bme-detail-close')?.addEventListener('click', () => { - document.getElementById('bme-node-detail')?.classList.remove('open'); + document.getElementById("bme-panel-close")?.addEventListener("click", closePanel); + document.getElementById("bme-detail-close")?.addEventListener("click", () => { + document.getElementById("bme-node-detail")?.classList.remove("open"); }); - // 点击遮罩关闭 - overlayEl?.addEventListener('click', (e) => { - if (e.target === overlayEl) closePanel(); + overlayEl?.addEventListener("click", (event) => { + if (event.target === overlayEl) closePanel(); }); } @@ -350,28 +385,33 @@ function _bindClose() { function _bindActions() { const bindings = { - 'bme-act-extract': 'extract', - 'bme-act-compress': 'compress', - 'bme-act-sleep': 'sleep', - 'bme-act-synopsis': 'synopsis', - 'bme-act-export': 'export', - 'bme-act-import': 'import', - 'bme-act-rebuild': 'rebuild', - 'bme-act-evolve': 'evolve', + "bme-act-extract": "extract", + "bme-act-compress": "compress", + "bme-act-sleep": "sleep", + "bme-act-synopsis": "synopsis", + "bme-act-export": "export", + "bme-act-import": "import", + "bme-act-rebuild": "rebuild", + "bme-act-evolve": "evolve", }; - for (const [elId, actionKey] of Object.entries(bindings)) { - document.getElementById(elId)?.addEventListener('click', async () => { + for (const [elementId, actionKey] of Object.entries(bindings)) { + document.getElementById(elementId)?.addEventListener("click", async () => { const handler = _actionHandlers[actionKey]; - if (handler) { - try { - await handler(); - // 刷新面板 - _refreshDashboard(); - _refreshGraph(); - } catch (e) { - console.error(`[ST-BME] Action ${actionKey} failed:`, e); + if (!handler) return; + + try { + await handler(); + _refreshDashboard(); + _refreshGraph(); + if (document.getElementById("bme-pane-memory")?.classList.contains("active")) { + _refreshMemoryBrowser(); } + if (document.getElementById("bme-pane-injection")?.classList.contains("active")) { + await _refreshInjectionPreview(); + } + } catch (error) { + console.error(`[ST-BME] Action ${actionKey} failed:`, error); } }); } @@ -385,29 +425,53 @@ function _setText(id, text) { } function _escHtml(str) { - const div = document.createElement('div'); - div.textContent = str; + const div = document.createElement("div"); + div.textContent = String(str ?? ""); return div.innerHTML; } function _typeLabel(type) { const map = { - character: '角色', event: '事件', location: '地点', - thread: '线索', rule: '规则', synopsis: '概要', reflection: '反思', + character: "角色", + event: "事件", + location: "地点", + thread: "主线", + rule: "规则", + synopsis: "概要", + reflection: "反思", }; - return map[type] || type || '—'; + return map[type] || type || "—"; } function _getNodeSnippet(node) { - const c = node.content || {}; - if (c.description) return c.description; - if (c.summary) return c.summary; - if (c.what) return c.what; - const entries = Object.entries(c).filter(([k]) => k !== 'name' && k !== 'title' && k !== 'embedding'); - if (entries.length) { - return entries.slice(0, 2).map(([k, v]) => `${k}: ${v}`).join('; '); + const fields = node.fields || {}; + if (fields.summary) return fields.summary; + if (fields.state) return fields.state; + if (fields.constraint) return fields.constraint; + if (fields.insight) return fields.insight; + if (fields.traits) return fields.traits; + + const entries = Object.entries(fields).filter( + ([key]) => !["name", "title", "summary", "embedding"].includes(key), + ); + if (entries.length > 0) { + return entries + .slice(0, 2) + .map(([key, value]) => `${key}: ${value}`) + .join("; "); } - return ''; + return "无补充字段"; +} + +function getNodeDisplayName(node) { + return ( + node?.fields?.name || + node?.fields?.title || + node?.fields?.summary || + node?.fields?.insight || + node?.id?.slice(0, 8) || + "—" + ); } function _isMobile() {