// ST-BME: 操控面板交互逻辑 import { callGenericPopup, POPUP_TYPE } from "../../../popup.js"; import { getContext } from "../../../extensions.js"; import { renderTemplateAsync } from "../../../templates.js"; import { GraphRenderer } from "./graph-renderer.js"; import { getNodeDisplayName } from "./node-labels.js"; import { buildRegionLine, buildScopeBadgeText, normalizeMemoryScope, } from "./memory-scope.js"; import { cloneTaskProfile, createBuiltinPromptBlock, createCustomPromptBlock, createLocalRegexRule, DEFAULT_TASK_BLOCKS, ensureTaskProfiles, exportTaskProfile as serializeTaskProfile, getBuiltinBlockDefinitions, getLegacyPromptFieldForTask, getTaskTypeOptions, importTaskProfile as parseImportedTaskProfile, isTaskRegexStageEnabled, normalizeTaskRegexStages, restoreDefaultTaskProfile, setActiveTaskProfileId, upsertTaskProfile, } from "./prompt-profiles.js"; import { getNodeColors } from "./themes.js"; import { getSuggestedBackendModel, getVectorIndexStats, } from "./vector-index.js"; let defaultPromptCache = null; function getDefaultPrompts() { if (defaultPromptCache) { return defaultPromptCache; } const prompts = {}; for (const [key, block] of Object.entries(DEFAULT_TASK_BLOCKS || {})) { prompts[key] = [block?.role, block?.format, block?.rules] .filter(Boolean) .join("\n\n"); } defaultPromptCache = prompts; return prompts; } function getDefaultPromptText(taskType = "") { return getDefaultPrompts()[taskType] || ""; } const TASK_PROFILE_TABS = [ { id: "generation", label: "生成参数" }, { id: "prompt", label: "Prompt 编排" }, { id: "regex", label: "正则" }, { id: "debug", label: "调试预览" }, ]; const TASK_PROFILE_ROLE_OPTIONS = [ { value: "system", label: "system" }, { value: "user", label: "user" }, { value: "assistant", label: "assistant" }, ]; const TASK_PROFILE_INJECTION_OPTIONS = [ { value: "append", label: "追加" }, { value: "prepend", label: "前置" }, { value: "relative", label: "相对" }, ]; const TASK_PROFILE_BOOLEAN_OPTIONS = [ { value: "", label: "跟随默认" }, { value: "true", label: "开启" }, { value: "false", label: "关闭" }, ]; const GRAPH_WRITE_ACTION_IDS = [ "bme-act-extract", "bme-act-compress", "bme-act-sleep", "bme-act-synopsis", "bme-act-evolve", "bme-act-undo-maintenance", "bme-act-import", "bme-act-rebuild", "bme-act-vector-rebuild", "bme-act-vector-range", "bme-act-vector-reembed", "bme-act-reroll", "bme-detail-delete", "bme-detail-save", ]; const TASK_PROFILE_GENERATION_GROUPS = [ { title: "基础生成参数", fields: [ { key: "max_context_tokens", label: "最大上下文 Tokens", type: "number", defaultValue: "" }, { key: "max_completion_tokens", label: "最大补全 Tokens", type: "number", defaultValue: "" }, { key: "reply_count", label: "回复次数", type: "number", defaultValue: 1 }, { key: "stream", label: "流式输出", type: "tri_bool", defaultValue: false }, { key: "temperature", label: "温度 (Temperature)", type: "range", min: 0, max: 2, step: 0.01, defaultValue: 1 }, { key: "top_p", label: "Top P", type: "range", min: 0, max: 1, step: 0.01, defaultValue: 1 }, { key: "top_k", label: "Top K", type: "number", defaultValue: 0 }, { key: "top_a", label: "Top A", type: "range", min: 0, max: 1, step: 0.01, defaultValue: 0 }, { key: "min_p", label: "Min P", type: "range", min: 0, max: 1, step: 0.01, defaultValue: 0 }, { key: "seed", label: "随机种子 (Seed)", type: "number", defaultValue: "" }, ], }, { title: "惩罚参数", fields: [ { key: "frequency_penalty", label: "频率惩罚", type: "range", min: -2, max: 2, step: 0.01, defaultValue: 0 }, { key: "presence_penalty", label: "存在惩罚", type: "range", min: -2, max: 2, step: 0.01, defaultValue: 0 }, { key: "repetition_penalty", label: "重复惩罚", type: "range", min: 0, max: 3, step: 0.01, defaultValue: 1 }, ], }, { title: "行为参数", fields: [ { key: "squash_system_messages", label: "合并系统消息", type: "tri_bool", defaultValue: false }, { key: "reasoning_effort", label: "推理强度", type: "enum", options: [ { value: "", label: "跟随默认" }, { value: "minimal", label: "最低" }, { value: "low", label: "低" }, { value: "medium", label: "中" }, { value: "high", label: "高" }, ], defaultValue: "", }, { key: "request_thoughts", label: "请求思考过程", type: "tri_bool", defaultValue: false }, { key: "enable_function_calling", label: "函数调用", type: "tri_bool", defaultValue: false }, { key: "enable_web_search", label: "网页搜索", type: "tri_bool", defaultValue: false }, { key: "character_name_prefix", label: "角色名前缀", type: "text", defaultValue: "" }, { key: "wrap_user_messages_in_quotes", label: "用户消息加引号", type: "tri_bool", defaultValue: false }, ], }, ]; const TASK_PROFILE_REGEX_STAGES = [ { key: "input", label: "输入总开关", desc: "控制全部输入阶段;未单独覆写的细分阶段会跟随它。", }, { key: "input.userMessage", label: "输入: 用户消息", desc: "处理当前 userMessage。", }, { key: "input.recentMessages", label: "输入: 最近上下文", desc: "处理 recentMessages、chatMessages、dialogueText。", }, { key: "input.candidateText", label: "输入: 候选与摘要", desc: "处理 candidateText、candidateNodes、nodeContent 和各类摘要。", }, { key: "input.finalPrompt", label: "输入: 发送前最终消息", desc: "在最终 messages 全部组装完成、真正发送给 LLM 前统一清洗。", }, { key: "output", label: "输出总开关", desc: "控制全部输出阶段;未单独覆写的细分阶段会跟随它。", }, { key: "output.rawResponse", label: "输出: 原始响应", desc: "LLM 原始文本到手后先清洗一次。", }, { key: "output.beforeParse", label: "输出: 解析前", desc: "在 JSON 提取/解析前再清洗一次。", }, ]; let panelEl = null; let overlayEl = null; let graphRenderer = null; let mobileGraphRenderer = null; let currentTabId = "dashboard"; let currentConfigSectionId = "api"; let currentTaskProfileTaskType = "extract"; let currentTaskProfileTabId = "generation"; let currentTaskProfileBlockId = ""; let currentTaskProfileRuleId = ""; let fetchedMemoryLLMModels = []; let fetchedBackendEmbeddingModels = []; let fetchedDirectEmbeddingModels = []; let viewportSyncBound = false; // 由 index.js 注入的引用 let _getGraph = null; let _getSettings = null; let _getLastExtract = null; let _getLastRecall = null; let _getRuntimeStatus = null; let _getLastExtractionStatus = null; let _getLastVectorStatus = null; let _getLastRecallStatus = null; let _getLastInjection = null; let _getRuntimeDebugSnapshot = null; let _getGraphPersistenceState = null; let _updateSettings = null; let _actionHandlers = {}; async function loadLocalTemplate(templateName) { const templatePath = new URL(`./${templateName}.html`, import.meta.url) .pathname; const html = await renderTemplateAsync(templatePath, {}, true, true, true); if (typeof html !== "string" || html.trim().length === 0) { throw new Error(`Template render returned empty content: ${templatePath}`); } return html; } function mountPanelHtml(html) { const markup = String(html || "").trim(); if (!markup) { throw new Error("Panel template markup is empty"); } if (document.body?.insertAdjacentHTML) { document.body.insertAdjacentHTML("beforebegin", markup); return; } const template = document.createElement("template"); template.innerHTML = markup; const fragment = template.content.cloneNode(true); document.documentElement?.appendChild(fragment); } function ensureNodeMountedAtRoot(node, { beforeBody = false } = {}) { if (!node) return; const root = document.documentElement; const body = document.body; if (!root) return; if (beforeBody && body?.parentElement === root) { if (node.parentElement === root && node.nextElementSibling === body) { return; } root.insertBefore(node, body); return; } if (node.parentElement === root) { return; } root.appendChild(node); } function ensureOverlayMountedAtRoot() { ensureNodeMountedAtRoot(overlayEl, { beforeBody: true }); } function ensureFabMountedAtRoot() { ensureNodeMountedAtRoot(_fabEl); } function getViewportMetrics() { const viewport = window.visualViewport; return { width: Math.max( 1, Math.round(viewport?.width || window.innerWidth || 0), ), height: Math.max( 1, Math.round(viewport?.height || window.innerHeight || 0), ), }; } function syncViewportCssVars() { const rootStyle = document.documentElement?.style; if (!rootStyle) return; const { width, height } = getViewportMetrics(); rootStyle.setProperty("--bme-viewport-width", `${width}px`); rootStyle.setProperty("--bme-viewport-height", `${height}px`); } function getFabFallbackSize() { return _isMobile() ? 54 : 46; } function getFabSize(fab = _fabEl) { if (fab) { const rect = fab.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { return { width: rect.width, height: rect.height, }; } } const fallback = getFabFallbackSize(); return { width: fallback, height: fallback, }; } function getDefaultFabPosition(fab = _fabEl) { const { width: viewportWidth, height: viewportHeight } = getViewportMetrics(); const { width, height } = getFabSize(fab); const sideGap = _isMobile() ? 14 : 16; const bottomGap = _isMobile() ? 96 : 80; return { x: Math.max(sideGap, viewportWidth - width - sideGap), y: Math.max(sideGap, viewportHeight - height - bottomGap), }; } function clampFabPosition(position = {}, fab = _fabEl) { const { width: viewportWidth, height: viewportHeight } = getViewportMetrics(); const { width, height } = getFabSize(fab); const margin = _isMobile() ? 10 : 8; const maxX = Math.max(margin, viewportWidth - width - margin); const maxY = Math.max(margin, viewportHeight - height - margin); const x = Number.isFinite(position?.x) ? position.x : maxX; const y = Number.isFinite(position?.y) ? position.y : maxY; return { x: Math.min(Math.max(margin, Math.round(x)), Math.round(maxX)), y: Math.min(Math.max(margin, Math.round(y)), Math.round(maxY)), }; } function applyFabPosition(position = {}, fab = _fabEl) { if (!fab) return; const clamped = clampFabPosition(position, fab); fab.style.left = `${clamped.x}px`; fab.style.top = `${clamped.y}px`; fab.style.right = "auto"; fab.style.bottom = "auto"; } function syncFabPosition() { if (!_fabEl) return; ensureFabMountedAtRoot(); const mode = _fabEl.dataset.positionMode || "default"; if (mode === "saved") { const currentX = Number.parseFloat(_fabEl.style.left); const currentY = Number.parseFloat(_fabEl.style.top); const fallback = _loadFabPosition() || getDefaultFabPosition(_fabEl); const next = clampFabPosition( { x: Number.isFinite(currentX) ? currentX : fallback.x, y: Number.isFinite(currentY) ? currentY : fallback.y, }, _fabEl, ); applyFabPosition(next, _fabEl); _saveFabPosition(next.x, next.y); return; } applyFabPosition(getDefaultFabPosition(_fabEl), _fabEl); } function bindViewportSync() { if (viewportSyncBound) return; viewportSyncBound = true; const update = () => { syncViewportCssVars(); syncFabPosition(); }; window.addEventListener("resize", update); window.addEventListener("orientationchange", update); window.visualViewport?.addEventListener("resize", update); window.visualViewport?.addEventListener("scroll", update); } /** * 初始化面板(由 index.js 调用一次) */ export async function initPanel({ getGraph, getSettings, getLastExtract, getLastRecall, getRuntimeStatus, getLastExtractionStatus, getLastVectorStatus, getLastRecallStatus, getLastInjection, getRuntimeDebugSnapshot, getGraphPersistenceState, updateSettings, actions, }) { _getGraph = getGraph; _getSettings = getSettings; _getLastExtract = getLastExtract; _getLastRecall = getLastRecall; _getRuntimeStatus = getRuntimeStatus; _getLastExtractionStatus = getLastExtractionStatus; _getLastVectorStatus = getLastVectorStatus; _getLastRecallStatus = getLastRecallStatus; _getLastInjection = getLastInjection; _getRuntimeDebugSnapshot = getRuntimeDebugSnapshot; _getGraphPersistenceState = getGraphPersistenceState; _updateSettings = updateSettings; _actionHandlers = actions || {}; overlayEl = document.getElementById("st-bme-panel-overlay"); panelEl = document.getElementById("st-bme-panel"); if (!overlayEl || !panelEl) { const html = await loadLocalTemplate("panel"); mountPanelHtml(html); overlayEl = document.getElementById("st-bme-panel-overlay"); panelEl = document.getElementById("st-bme-panel"); if (!overlayEl || !panelEl) { throw new Error( "Panel template rendered but required DOM nodes were not found", ); } } ensureOverlayMountedAtRoot(); bindViewportSync(); syncViewportCssVars(); _bindTabs(); _bindClose(); _bindNodeDetailPanel(); _bindResizeHandle(); _bindPanelResize(); _bindGraphControls(); _bindActions(); _bindConfigControls(); _bindPlannerLauncher(); currentTabId = panelEl?.querySelector(".bme-tab-btn.active")?.dataset.tab || "dashboard"; _applyWorkspaceMode(); _syncConfigSectionState(); _refreshRuntimeStatus(); _initFloatingBall(); _bindFabToggle(); } // ==================== 悬浮球 ==================== const FAB_STORAGE_KEY = "bme-fab-position"; const FAB_VISIBLE_KEY = "bme-fab-visible"; let _fabEl = null; function _getFabVisible() { try { const val = localStorage.getItem(FAB_VISIBLE_KEY); return val === null ? true : val === "true"; } catch { return true; } } function _setFabVisible(visible) { try { localStorage.setItem(FAB_VISIBLE_KEY, String(visible)); } catch {} if (_fabEl) { ensureFabMountedAtRoot(); _fabEl.style.display = visible ? "flex" : "none"; if (visible) { syncFabPosition(); } } const btn = panelEl?.querySelector("#bme-fab-toggle-btn"); if (btn) btn.setAttribute("data-active", String(visible)); } function _bindFabToggle() { const btn = panelEl?.querySelector("#bme-fab-toggle-btn"); if (!btn) return; btn.setAttribute("data-active", String(_getFabVisible())); btn.addEventListener("click", () => { const next = !_getFabVisible(); _setFabVisible(next); }); } function _initFloatingBall() { const existing = document.getElementById("bme-floating-ball"); if (existing) { _fabEl = existing; ensureFabMountedAtRoot(); syncFabPosition(); return; } const fab = document.createElement("div"); fab.id = "bme-floating-ball"; fab.setAttribute("data-status", "idle"); fab.innerHTML = ` BME 记忆图谱 `; _fabEl = fab; ensureFabMountedAtRoot(); // 应用可见性 if (!_getFabVisible()) fab.style.display = "none"; // 恢复位置 const saved = _loadFabPosition(); if (saved) { fab.dataset.positionMode = "saved"; applyFabPosition(saved, fab); } else { fab.dataset.positionMode = "default"; syncFabPosition(); } // 拖拽 + 点击逻辑 let isDragging = false; let hasMoved = false; let startX = 0, startY = 0; let fabStartX = 0, fabStartY = 0; let clickTimer = null; const DRAG_THRESHOLD = 5; const DBLCLICK_DELAY = 280; function onPointerDown(e) { isDragging = true; hasMoved = false; startX = e.clientX; startY = e.clientY; const rect = fab.getBoundingClientRect(); fabStartX = rect.left; fabStartY = rect.top; fab.setPointerCapture(e.pointerId); e.preventDefault(); } function onPointerMove(e) { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (!hasMoved && Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) return; hasMoved = true; applyFabPosition( { x: fabStartX + dx, y: fabStartY + dy, }, fab, ); } function onPointerUp(e) { if (!isDragging) return; isDragging = false; fab.releasePointerCapture(e.pointerId); if (hasMoved) { // 拖拽结束 → 保存位置 fab.dataset.positionMode = "saved"; _saveFabPosition( Number.parseInt(fab.style.left, 10), Number.parseInt(fab.style.top, 10), ); return; } // 非拖拽 → 处理单击/双击 if (clickTimer) { // 第二次点击 → 双击 → 重 Roll clearTimeout(clickTimer); clickTimer = null; _onFabDoubleClick(); } else { // 第一次点击 → 等待双击 clickTimer = setTimeout(() => { clickTimer = null; _onFabSingleClick(); }, DBLCLICK_DELAY); } } fab.addEventListener("pointerdown", onPointerDown); document.addEventListener("pointermove", onPointerMove); document.addEventListener("pointerup", onPointerUp); } function _onFabSingleClick() { openPanel(); } async function _onFabDoubleClick() { if (!_actionHandlers.reroll) return; try { _fabEl?.setAttribute("data-status", "running"); await _actionHandlers.reroll({}); _fabEl?.setAttribute("data-status", "success"); _refreshDashboard(); _refreshGraph(); setTimeout(() => { const status = _getRuntimeStatus?.() || {}; _fabEl?.setAttribute("data-status", status.status || "idle"); }, 3000); } catch (err) { console.error("[ST-BME] FAB reroll failed:", err); _fabEl?.setAttribute("data-status", "error"); } } function _loadFabPosition() { try { const raw = localStorage.getItem(FAB_STORAGE_KEY); if (!raw) return null; const pos = JSON.parse(raw); if (Number.isFinite(pos.x) && Number.isFinite(pos.y)) return pos; } catch {} return null; } function _saveFabPosition(x, y) { try { localStorage.setItem(FAB_STORAGE_KEY, JSON.stringify({ x, y })); } catch {} } export function updateFloatingBallStatus(status = "idle", tooltipText = "") { if (!_fabEl) return; _fabEl.setAttribute("data-status", status); if (tooltipText) { const tip = _fabEl.querySelector(".bme-fab-tooltip"); if (tip) tip.textContent = tooltipText; } } /** * 打开面板 */ export function openPanel() { if (!overlayEl) return; ensureOverlayMountedAtRoot(); syncViewportCssVars(); _actionHandlers.syncGraphLoad?.(); overlayEl.classList.add("active"); _restorePanelSize(); const isMobile = _isMobile(); const settings = _getSettings?.() || {}; const themeName = settings.panelTheme || "crimson"; const graphOpts = { theme: themeName, userPovAliases: _hostUserPovAliasHintsForGraph(), }; const canvas = document.getElementById("bme-graph-canvas"); if (canvas && !graphRenderer && !isMobile) { graphRenderer = new GraphRenderer(canvas, graphOpts); graphRenderer.onNodeSelect = (node) => _showNodeDetail(node); } const mobileCanvas = document.getElementById("bme-mobile-graph-canvas"); if (mobileCanvas && !mobileGraphRenderer && isMobile) { mobileGraphRenderer = new GraphRenderer(mobileCanvas, graphOpts); mobileGraphRenderer.onNodeSelect = (node) => _showNodeDetail(node); } const activeTabId = panelEl?.querySelector(".bme-tab-btn.active")?.dataset.tab || currentTabId; _switchTab(activeTabId); _refreshRuntimeStatus(); _refreshGraph(); _buildLegend(); } /** * 关闭面板 */ export function closePanel() { if (!overlayEl) return; overlayEl.classList.remove("active"); } /** * 更新主题 */ export function updatePanelTheme(themeName) { graphRenderer?.setTheme(themeName); mobileGraphRenderer?.setTheme(themeName); _buildLegend(); _highlightThemeChoice(themeName); } export function refreshLiveState() { if (!overlayEl?.classList.contains("active")) return; _refreshRuntimeStatus(); switch (currentTabId) { case "dashboard": _refreshDashboard(); break; case "memory": _refreshMemoryBrowser(); break; case "injection": void _refreshInjectionPreview(); break; default: break; } if ( currentTabId === "config" && currentConfigSectionId === "prompts" && currentTaskProfileTabId === "debug" ) { _refreshTaskProfileWorkspace(); } if (currentTabId === "config" && currentConfigSectionId === "trace") { _refreshMessageTraceWorkspace(); } _refreshGraph(); } // ==================== Tab 切换 ==================== function _bindTabs() { panelEl?.querySelectorAll(".bme-tab-btn").forEach((btn) => { btn.addEventListener("click", () => { const tabId = btn.dataset.tab; _switchTab(tabId); }); }); } function _switchTab(tabId) { currentTabId = tabId || "dashboard"; panelEl?.querySelectorAll(".bme-tab-btn").forEach((btn) => { btn.classList.toggle("active", btn.dataset.tab === currentTabId); }); panelEl?.querySelectorAll(".bme-tab-pane").forEach((pane) => { pane.classList.toggle("active", pane.id === `bme-pane-${currentTabId}`); }); // ⑥ 移动端图谱 tab 全屏覆盖 const mainEl = panelEl?.querySelector(".bme-panel-main"); if (mainEl) { mainEl.classList.toggle("mobile-visible", currentTabId === "graph"); } _applyWorkspaceMode(); switch (currentTabId) { case "dashboard": _refreshDashboard(); break; case "memory": _refreshMemoryBrowser(); break; case "injection": void _refreshInjectionPreview(); break; case "config": _refreshConfigTab(); break; default: break; } } function _getPlannerApi() { return globalThis?.stBmeEnaPlanner || null; } function _refreshPlannerLauncher() { const button = document.getElementById("bme-open-ena-planner"); const hint = document.getElementById("bme-open-ena-planner-hint"); if (!button || !hint) return; const plannerApi = _getPlannerApi(); const ready = typeof plannerApi?.openSettings === "function"; button.disabled = !ready; button.classList.toggle("is-runtime-disabled", !ready); hint.textContent = ready ? "已加载,可打开独立的 Ena Planner 设置页。" : "未检测到 Ena Planner 模块,请重载 ST-BME 后再试。"; } function _bindPlannerLauncher() { const button = document.getElementById("bme-open-ena-planner"); if (!button || button.dataset.bmeBound === "true") { _refreshPlannerLauncher(); return; } button.addEventListener("click", () => { const plannerApi = _getPlannerApi(); if (typeof plannerApi?.openSettings === "function") { plannerApi.openSettings(); } _refreshPlannerLauncher(); }); button.dataset.bmeBound = "true"; _refreshPlannerLauncher(); } function _applyWorkspaceMode() { if (!panelEl) return; const isConfig = currentTabId === "config"; panelEl.classList.toggle("config-mode", isConfig); } function _switchConfigSection(sectionId) { currentConfigSectionId = sectionId || "api"; _syncConfigSectionState(); if (currentConfigSectionId === "prompts") { _refreshTaskProfileWorkspace(); } else if (currentConfigSectionId === "trace") { _refreshMessageTraceWorkspace(); } } function _syncConfigSectionState() { if (!panelEl) return; panelEl.querySelectorAll(".bme-config-nav-btn").forEach((btn) => { btn.classList.toggle( "active", btn.dataset.configSection === currentConfigSectionId, ); }); panelEl.querySelectorAll(".bme-config-section").forEach((section) => { section.classList.toggle( "active", section.dataset.configSection === currentConfigSectionId, ); }); } // ==================== 总览 Tab ==================== function _refreshDashboard() { const graph = _getGraph?.(); const loadInfo = _getGraphPersistenceSnapshot(); if (!graph) return; if (!_canRenderGraphData(loadInfo) && loadInfo.loadState !== "empty-confirmed") { _setText("bme-stat-nodes", "—"); _setText("bme-stat-edges", "—"); _setText("bme-stat-archived", "—"); _setText("bme-stat-frag", "—"); _setText("bme-status-chat-id", loadInfo.chatId || "—"); _setText("bme-status-history", _getGraphLoadLabel(loadInfo.loadState)); _setText("bme-status-vector", "等待聊天图谱元数据加载"); _setText("bme-status-recovery", "等待聊天图谱元数据加载"); _setText("bme-status-last-extract", "等待聊天图谱元数据加载"); _setText("bme-status-last-vector", "等待聊天图谱元数据加载"); _setText("bme-status-last-recall", "等待聊天图谱元数据加载"); _renderStatefulListPlaceholder( document.getElementById("bme-recent-extract"), _getGraphLoadLabel(loadInfo.loadState), ); _renderStatefulListPlaceholder( document.getElementById("bme-recent-recall"), _getGraphLoadLabel(loadInfo.loadState), ); return; } 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", archivedCount); _setText("bme-stat-frag", `${fragRate}%`); const chatId = loadInfo.chatId || graph?.historyState?.chatId || "—"; const lastProcessed = graph?.historyState?.lastProcessedAssistantFloor ?? -1; const dirtyFrom = graph?.historyState?.historyDirtyFrom; const vectorStats = getVectorIndexStats(graph); const vectorMode = graph?.vectorIndexState?.mode || "—"; const vectorSource = graph?.vectorIndexState?.source || "—"; const recovery = graph?.historyState?.lastRecoveryResult; const extractionStatus = _getLastExtractionStatus?.() || {}; const vectorStatus = _getLastVectorStatus?.() || {}; const recallStatus = _getLastRecallStatus?.() || {}; const historyPrefix = loadInfo.loadState === "shadow-restored" ? "临时恢复 · " : loadInfo.loadState === "blocked" && loadInfo.shadowSnapshotUsed ? "保护模式 · " : ""; _setText("bme-status-chat-id", chatId); _setText( "bme-status-history", `${historyPrefix}${ Number.isFinite(dirtyFrom) ? `脏区从楼层 ${dirtyFrom} 开始,已处理到 ${lastProcessed}` : `干净,已处理到楼层 ${lastProcessed}` }`, ); _setText( "bme-status-vector", `${vectorMode}/${vectorSource} · total ${vectorStats.total} · indexed ${vectorStats.indexed} · stale ${vectorStats.stale} · pending ${vectorStats.pending}`, ); _setText( "bme-status-recovery", recovery ? [ recovery.status || "—", recovery.path ? `path ${recovery.path}` : "", recovery.detectionSource ? `src ${recovery.detectionSource}` : "", recovery.fromFloor != null ? `from ${recovery.fromFloor}` : "", recovery.affectedBatchCount != null ? `affected ${recovery.affectedBatchCount}` : "", recovery.replayedBatchCount != null ? `replayed ${recovery.replayedBatchCount}` : "", recovery.reason || "", ] .filter(Boolean) .join(" · ") : "暂无恢复记录", ); _setText("bme-status-last-extract", extractionStatus.meta || "尚未执行提取"); _setText("bme-status-last-vector", vectorStatus.meta || "尚未执行向量任务"); _setText("bme-status-last-recall", recallStatus.meta || "尚未执行召回"); _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) { const li = document.createElement("li"); li.className = "bme-recent-item"; const text = document.createElement("div"); text.className = "bme-recent-text"; text.style.color = "var(--bme-on-surface-dim)"; text.textContent = "暂无数据"; li.appendChild(text); listEl.replaceChildren(li); return; } const fragment = document.createDocumentFragment(); items.forEach((item) => { const secondary = item.meta || item.time || ""; const li = document.createElement("li"); li.className = "bme-recent-item"; const badge = document.createElement("span"); badge.className = `bme-type-badge ${_safeCssToken(item.type)}`; badge.textContent = _typeLabel(item.type); li.appendChild(badge); const content = document.createElement("div"); const title = document.createElement("div"); title.className = "bme-recent-text"; title.textContent = item.name || "—"; const meta = document.createElement("div"); meta.className = "bme-recent-meta"; meta.textContent = secondary; content.append(title, meta); li.appendChild(content); fragment.appendChild(li); }); listEl.replaceChildren(fragment); } // ==================== 记忆浏览器 ==================== function _refreshMemoryBrowser() { const graph = _getGraph?.(); const loadInfo = _getGraphPersistenceSnapshot(); if (!graph) return; const searchInput = document.getElementById("bme-memory-search"); const regionInput = document.getElementById("bme-memory-region-filter"); const filterSelect = document.getElementById("bme-memory-filter"); const listEl = document.getElementById("bme-memory-list"); if (!listEl) return; const canRenderGraph = _canRenderGraphData(loadInfo); if (searchInput) searchInput.disabled = !canRenderGraph; if (regionInput) regionInput.disabled = !canRenderGraph; if (filterSelect) filterSelect.disabled = !canRenderGraph; if (!canRenderGraph && loadInfo.loadState !== "empty-confirmed") { _renderStatefulListPlaceholder(listEl, _getGraphLoadLabel(loadInfo.loadState)); return; } const query = String(searchInput?.value || "") .trim() .toLowerCase(); const regionQuery = String(regionInput?.value || "") .trim() .toLowerCase(); const filter = filterSelect?.value || "all"; let nodes = graph.nodes.filter((node) => !node.archived); if (filter !== "all") { nodes = nodes.filter((node) => _matchesMemoryFilter(node, filter)); } if (query) { nodes = nodes.filter((node) => { const name = getNodeDisplayName(node).toLowerCase(); const text = JSON.stringify(node.fields || {}).toLowerCase(); return name.includes(query) || text.includes(query); }); } if (regionQuery) { nodes = nodes.filter((node) => { const scope = normalizeMemoryScope(node.scope); const regionText = [ scope.regionPrimary, ...(scope.regionPath || []), ...(scope.regionSecondary || []), ] .join(" ") .toLowerCase(); return regionText.includes(regionQuery); }); } 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); }); if (!nodes.length && loadInfo.loadState === "empty-confirmed") { _renderStatefulListPlaceholder(listEl, "当前聊天还没有图谱"); return; } const fragment = document.createDocumentFragment(); nodes.slice(0, 100).forEach((node) => { const name = getNodeDisplayName(node); const snippetText = _getNodeSnippet(node); const li = document.createElement("li"); li.className = "bme-memory-item"; li.dataset.nodeId = String(node.id || ""); const card = document.createElement("div"); card.className = "bme-memory-card"; const head = document.createElement("div"); head.className = "bme-memory-card-head"; const badge = document.createElement("span"); badge.className = `bme-type-badge ${_safeCssToken(node.type)}`; badge.textContent = _typeLabel(node.type); const scopeChip = document.createElement("span"); scopeChip.className = "bme-memory-scope-chip"; scopeChip.textContent = buildScopeBadgeText(node.scope); head.append(badge, scopeChip); const titleEl = document.createElement("div"); titleEl.className = "bme-memory-name"; titleEl.textContent = name; const snippetEl = document.createElement("div"); snippetEl.className = "bme-memory-content"; snippetEl.textContent = snippetText; const foot = document.createElement("div"); foot.className = "bme-memory-foot"; const stats = document.createElement("div"); stats.className = "bme-memory-stats"; const impSpan = document.createElement("span"); impSpan.className = "bme-memory-stat-pill"; impSpan.textContent = `重要度 ${_formatMemoryMetricNumber(node.importance, { fallback: 5, maxFrac: 2, })}`; const accSpan = document.createElement("span"); accSpan.className = "bme-memory-stat-pill"; accSpan.textContent = `访问 ${_formatMemoryInt(node.accessCount, 0)}`; const seqSpan = document.createElement("span"); seqSpan.className = "bme-memory-stat-pill"; seqSpan.textContent = `序列 ${_formatMemoryInt( node.seqRange?.[1] ?? node.seq, 0, )}`; stats.append(impSpan, accSpan, seqSpan); foot.appendChild(stats); const regionMeta = _buildScopeMetaText(node); if (regionMeta) { const regionEl = document.createElement("div"); regionEl.className = "bme-memory-region"; regionEl.textContent = regionMeta; foot.appendChild(regionEl); } card.append(head, titleEl, snippetEl, foot); li.appendChild(card); fragment.appendChild(li); }); listEl.replaceChildren(fragment); listEl.querySelectorAll(".bme-memory-item").forEach((el) => { el.addEventListener("click", () => { const nodeId = el.dataset.nodeId; graphRenderer?.highlightNode(nodeId); mobileGraphRenderer?.highlightNode(nodeId); const node = graph.nodes.find((candidate) => candidate.id === nodeId); if (node) _showNodeDetail(node); }); }); if (searchInput && !searchInput._bmeBound) { let timer = null; searchInput.addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(() => _refreshMemoryBrowser(), 200); }); regionInput?.addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(() => _refreshMemoryBrowser(), 200); }); filterSelect?.addEventListener("change", () => _refreshMemoryBrowser()); searchInput._bmeBound = true; } } // ==================== 注入预览 ==================== async function _refreshInjectionPreview() { const container = document.getElementById("bme-injection-content"); const tokenEl = document.getElementById("bme-injection-tokens"); if (!container) return; const injection = String(_getLastInjection?.() || "").trim(); if (!injection) { const empty = document.createElement("div"); empty.className = "bme-injection-preview"; empty.style.color = "var(--bme-on-surface-dim)"; empty.textContent = "暂无注入内容。先完成一次召回或正常生成后再查看。"; container.replaceChildren(empty); if (tokenEl) tokenEl.textContent = ""; return; } try { const { estimateTokens } = await import("./injector.js"); const totalTokens = estimateTokens(injection); const preview = document.createElement("div"); preview.className = "bme-injection-preview"; preview.textContent = injection; container.replaceChildren(preview); if (tokenEl) tokenEl.textContent = `≈ ${totalTokens} tokens`; } catch (error) { const failure = document.createElement("div"); failure.className = "bme-injection-preview"; failure.style.color = "var(--bme-accent3)"; failure.textContent = `预览生成失败: ${error.message}`; container.replaceChildren(failure); if (tokenEl) tokenEl.textContent = ""; } } // ==================== 图谱 ==================== /** SillyTavern 用户显示名(name1),用于图谱分区:误标为角色的用户 POV 强制归用户区 */ function _hostUserPovAliasHintsForGraph() { try { const ctx = typeof getContext === "function" ? getContext() : null; const out = []; if (ctx?.name1 && String(ctx.name1).trim()) { out.push(String(ctx.name1).trim()); } return out; } catch { return []; } } function _refreshGraph() { const graph = _getGraph?.(); if (!graph) return; const hints = { userPovAliases: _hostUserPovAliasHintsForGraph() }; graphRenderer?.loadGraph(graph, hints); mobileGraphRenderer?.loadGraph(graph, hints); } function _buildLegend() { const legendEl = document.getElementById("bme-graph-legend"); if (!legendEl) return; const settings = _getSettings?.() || {}; const colors = getNodeColors(settings.panelTheme || "crimson"); const scopeColors = { objective: "#57c7ff", characterPov: "#ffb347", userPov: "#7dff9b", }; const layers = [ { key: "objective", label: "客观层" }, { key: "characterPov", label: "角色 POV" }, { key: "userPov", label: "用户 POV" }, ]; const types = [ { key: "character", label: "角色" }, { key: "event", label: "事件" }, { key: "location", label: "地点" }, { key: "thread", label: "主线" }, { key: "rule", label: "规则" }, { key: "synopsis", label: "概要" }, { key: "reflection", label: "反思" }, { key: "pov_memory", label: "主观记忆" }, ]; const fragment = document.createDocumentFragment(); layers.forEach((type) => { const item = document.createElement("span"); item.className = "bme-legend-item"; const dot = document.createElement("span"); dot.className = "bme-legend-dot"; dot.style.background = scopeColors[type.key] || ""; item.appendChild(dot); item.append(document.createTextNode(type.label)); fragment.appendChild(item); }); types.forEach((type) => { const item = document.createElement("span"); item.className = "bme-legend-item"; const dot = document.createElement("span"); dot.className = "bme-legend-dot"; dot.style.background = colors[type.key] || ""; item.appendChild(dot); item.append(document.createTextNode(type.label)); fragment.appendChild(item); }); legendEl.replaceChildren(fragment); } function _getActiveGraphRenderer() { return mobileGraphRenderer || graphRenderer; } function _bindGraphControls() { document .getElementById("bme-graph-zoom-in") ?.addEventListener("click", () => _getActiveGraphRenderer()?.zoomIn()); document .getElementById("bme-graph-zoom-out") ?.addEventListener("click", () => _getActiveGraphRenderer()?.zoomOut()); document .getElementById("bme-graph-reset") ?.addEventListener("click", () => _getActiveGraphRenderer()?.resetView()); } // ==================== 节点详情 ==================== function _appendNodeDetailReadOnly(container, labelText, valueText) { const row = document.createElement("div"); row.className = "bme-node-detail-field"; const label = document.createElement("label"); label.textContent = labelText; const value = document.createElement("div"); value.className = "value"; value.textContent = String(valueText ?? "—"); row.append(label, value); container.appendChild(row); } function _appendNodeDetailNumberInput( container, labelText, inputId, value, { min, max, step } = {}, ) { const row = document.createElement("div"); row.className = "bme-node-detail-field"; const label = document.createElement("label"); label.setAttribute("for", inputId); label.textContent = labelText; const input = document.createElement("input"); input.type = "number"; input.id = inputId; input.className = "bme-node-detail-input"; if (min != null) input.min = String(min); if (max != null) input.max = String(max); if (step != null) input.step = String(step); input.value = value === undefined || value === null ? "" : String(Number(value)); row.append(label, input); container.appendChild(row); } function _appendNodeDetailTextInput(container, labelText, inputId, value) { const row = document.createElement("div"); row.className = "bme-node-detail-field"; const label = document.createElement("label"); label.setAttribute("for", inputId); label.textContent = labelText; const input = document.createElement("input"); input.type = "text"; input.id = inputId; input.className = "bme-node-detail-input"; input.value = String(value ?? ""); row.append(label, input); container.appendChild(row); } function _appendNodeDetailTextareaField( container, labelText, fieldKey, fieldType, text, ) { const row = document.createElement("div"); row.className = "bme-node-detail-field"; const label = document.createElement("label"); label.textContent = labelText; const ta = document.createElement("textarea"); ta.className = "bme-node-detail-textarea"; ta.dataset.bmeFieldKey = fieldKey; ta.dataset.bmeFieldType = fieldType; ta.rows = String(text || "").length > 160 ? 6 : 3; ta.value = text; row.append(label, ta); container.appendChild(row); } function _showNodeDetail(node) { 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 fields = raw.fields || {}; titleEl.textContent = getNodeDisplayName(raw); detailEl.dataset.editNodeId = raw.id || ""; const fragment = document.createDocumentFragment(); _appendNodeDetailReadOnly(fragment, "类型", _typeLabel(raw.type)); _appendNodeDetailReadOnly( fragment, "作用域", buildScopeBadgeText(raw.scope), ); _appendNodeDetailReadOnly(fragment, "ID", raw.id || "—"); _appendNodeDetailReadOnly( fragment, "序列号", raw.seqRange?.[1] ?? raw.seq ?? 0, ); const scope = normalizeMemoryScope(raw.scope); if (scope.layer === "pov") { _appendNodeDetailReadOnly( fragment, "POV 归属", `${scope.ownerType || "unknown"} / ${scope.ownerName || scope.ownerId || "—"}`, ); } const regionLine = buildRegionLine(scope); if (regionLine) { _appendNodeDetailReadOnly(fragment, "地区", regionLine); } if (Array.isArray(raw.seqRange)) { _appendNodeDetailReadOnly( fragment, "序列范围", `${raw.seqRange[0]} ~ ${raw.seqRange[1]}`, ); } _appendNodeDetailNumberInput( fragment, "重要度 (0–10)", "bme-detail-importance", raw.importance ?? 5, { min: 0, max: 10, step: 0.1 }, ); _appendNodeDetailNumberInput( fragment, "访问次数", "bme-detail-accesscount", raw.accessCount ?? 0, { min: 0, step: 1 }, ); const clustersStr = Array.isArray(raw.clusters) ? raw.clusters.join(", ") : ""; _appendNodeDetailTextInput( fragment, "聚类标签 (逗号分隔)", "bme-detail-clusters", clustersStr, ); const section = document.createElement("div"); section.className = "bme-node-detail-section"; section.textContent = "记忆字段"; fragment.appendChild(section); for (const [key, value] of Object.entries(fields)) { const isJson = typeof value === "object" && value !== null; const displayVal = isJson ? JSON.stringify(value, null, 2) : String(value ?? ""); _appendNodeDetailTextareaField( fragment, key, key, isJson ? "json" : "string", displayVal, ); } bodyEl.replaceChildren(fragment); detailEl.classList.add("open"); } function _saveNodeDetail() { const detailEl = document.getElementById("bme-node-detail"); const bodyEl = document.getElementById("bme-detail-body"); const nodeId = detailEl?.dataset?.editNodeId; if (!nodeId || !bodyEl) return; if (_isGraphWriteBlocked()) { toastr.error("当前图谱不可写入,请稍后再试", "ST-BME"); return; } const updates = { fields: {} }; const impEl = document.getElementById("bme-detail-importance"); if (impEl && impEl.value !== "") { const imp = Number.parseFloat(impEl.value); if (Number.isFinite(imp)) { updates.importance = Math.max(0, Math.min(10, imp)); } } const accessEl = document.getElementById("bme-detail-accesscount"); if (accessEl && accessEl.value !== "") { const ac = Number.parseInt(accessEl.value, 10); if (Number.isFinite(ac)) { updates.accessCount = Math.max(0, ac); } } const clustersEl = document.getElementById("bme-detail-clusters"); if (clustersEl) { updates.clusters = clustersEl.value .split(/[,,]/) .map((s) => s.trim()) .filter(Boolean); } const fieldEls = bodyEl.querySelectorAll("[data-bme-field-key]"); for (const el of fieldEls) { const key = el.dataset.bmeFieldKey; const type = el.dataset.bmeFieldType || "string"; const rawVal = el.value; if (type === "json") { try { updates.fields[key] = JSON.parse(rawVal || "null"); } catch { toastr.error(`字段「${key}」须为合法 JSON`, "ST-BME"); return; } } else { updates.fields[key] = rawVal; } } const result = _actionHandlers.saveGraphNode?.({ nodeId, updates, }); if (!result?.ok) { toastr.error( result?.error === "node-not-found" ? "节点已不存在,请关闭后重试" : "保存失败", "ST-BME", ); return; } if (result.persistBlocked) { toastr.warning( "内容已更新,但写回聊天元数据可能被拦截,请查看图谱状态", "ST-BME", ); } else { toastr.success("节点已保存", "ST-BME"); } const r = _getActiveGraphRenderer(); const sel = r?.selectedNode; if (sel?.id === nodeId && sel.raw) { _showNodeDetail(sel); } else { const g = _getGraph?.(); const rawN = g?.nodes?.find((n) => n.id === nodeId); if (rawN) { _showNodeDetail({ raw: rawN, id: rawN.id }); } } refreshLiveState(); } function _bindNodeDetailPanel() { const saveBtn = document.getElementById("bme-detail-save"); if (saveBtn && saveBtn.dataset.bmeBound !== "true") { saveBtn.addEventListener("click", () => _saveNodeDetail()); saveBtn.dataset.bmeBound = "true"; } const deleteBtn = document.getElementById("bme-detail-delete"); if (deleteBtn && deleteBtn.dataset.bmeBound !== "true") { deleteBtn.addEventListener("click", () => _deleteNodeDetail()); deleteBtn.dataset.bmeBound = "true"; } } function _deleteNodeDetail() { const detailEl = document.getElementById("bme-node-detail"); const nodeId = detailEl?.dataset?.editNodeId; if (!nodeId) return; if (_isGraphWriteBlocked()) { toastr.error("当前图谱不可写入,请稍后再试", "ST-BME"); return; } const g = _getGraph?.(); const node = g?.nodes?.find((n) => n.id === nodeId); const label = node ? getNodeDisplayName(node) : nodeId; if ( !confirm( `确定删除节点「${label}」?\n\n若该节点有层级子节点,将一并删除。此操作不可在本面板内撤销。`, ) ) { return; } const result = _actionHandlers.deleteGraphNode?.({ nodeId }); if (!result?.ok) { toastr.error( result?.error === "node-not-found" ? "节点已不存在" : "删除失败", "ST-BME", ); return; } if (result.persistBlocked) { toastr.warning( "节点已从图中移除,但写回可能被拦截,请查看图谱状态", "ST-BME", ); } else { toastr.success("节点已删除", "ST-BME"); } detailEl?.classList.remove("open"); if (detailEl) delete detailEl.dataset.editNodeId; graphRenderer?.highlightNode?.("__cleared__"); mobileGraphRenderer?.highlightNode?.("__cleared__"); refreshLiveState(); } 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"); }); overlayEl?.addEventListener("click", (event) => { if (event.target === overlayEl) closePanel(); }); } function _bindResizeHandle() { const handle = document.getElementById("bme-resize-handle"); const sidebar = panelEl?.querySelector(".bme-panel-sidebar"); if (!handle || !sidebar) return; let dragging = false; let startX = 0; let startWidth = 0; handle.addEventListener("mousedown", (e) => { e.preventDefault(); dragging = true; startX = e.clientX; startWidth = sidebar.offsetWidth; handle.classList.add("dragging"); document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }); document.addEventListener("mousemove", (e) => { if (!dragging) return; const delta = e.clientX - startX; const newWidth = Math.max(180, Math.min(600, startWidth + delta)); sidebar.style.width = newWidth + "px"; sidebar.style.minWidth = newWidth + "px"; }); document.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; handle.classList.remove("dragging"); document.body.style.cursor = ""; document.body.style.userSelect = ""; }); } const PANEL_SIZE_KEY = "st-bme-panel-size"; let _panelResizeTimer = null; function _bindPanelResize() { if (!panelEl || typeof ResizeObserver === "undefined") return; const observer = new ResizeObserver(() => { clearTimeout(_panelResizeTimer); _panelResizeTimer = setTimeout(() => { if (!overlayEl?.classList.contains("active")) return; const w = panelEl.offsetWidth; const h = panelEl.offsetHeight; if (w > 0 && h > 0) { try { localStorage.setItem(PANEL_SIZE_KEY, JSON.stringify({ w, h })); } catch { /* ignore */ } } }, 300); }); observer.observe(panelEl); } function _restorePanelSize() { if (!panelEl) return; if (_isMobile()) { panelEl.style.width = ""; panelEl.style.height = ""; return; } try { const raw = localStorage.getItem(PANEL_SIZE_KEY); if (!raw) return; const { w, h } = JSON.parse(raw); if (Number.isFinite(w) && Number.isFinite(h) && w > 200 && h > 200) { panelEl.style.width = w + "px"; panelEl.style.height = h + "px"; } } catch { /* ignore */ } } // ==================== 操作绑定 ==================== 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-undo-maintenance": "undoMaintenance", "bme-act-vector-rebuild": "rebuildVectorIndex", "bme-act-vector-reembed": "reembedDirect", }; const actionLabels = { extract: "手动提取", compress: "手动压缩", sleep: "执行遗忘", synopsis: "更新概要", export: "导出图谱", import: "导入图谱", rebuild: "重建图谱", evolve: "强制进化", undoMaintenance: "撤销最近维护", rebuildVectorIndex: "重建向量", reembedDirect: "直连重嵌", }; for (const [elementId, actionKey] of Object.entries(bindings)) { const btn = document.getElementById(elementId); if (!btn) continue; btn.addEventListener("click", async () => { const handler = _actionHandlers[actionKey]; if (!handler) return; const label = actionLabels[actionKey] || actionKey; // 防止重复点击 if (btn.disabled) return; btn.disabled = true; btn.style.opacity = "0.5"; toastr.info(`${label} 进行中…`, "ST-BME", { timeOut: 2000 }); try { const result = await handler(); if (result?.cancelled) { return; } _refreshDashboard(); _refreshGraph(); if ( document .getElementById("bme-pane-memory") ?.classList.contains("active") ) { _refreshMemoryBrowser(); } if ( document .getElementById("bme-pane-injection") ?.classList.contains("active") ) { await _refreshInjectionPreview(); } if (!result?.handledToast) { toastr.success(`${label} 完成`, "ST-BME"); } } catch (error) { console.error(`[ST-BME] Action ${actionKey} failed:`, error); if (!error?._stBmeToastHandled) { toastr.error(`${label} 失败: ${error?.message || error}`, "ST-BME"); } } finally { btn.style.opacity = ""; _refreshRuntimeStatus(); _refreshGraphAvailabilityState(); } }); } document .getElementById("bme-act-vector-range") ?.addEventListener("click", async () => { const btn = document.getElementById("bme-act-vector-range"); if (btn?.disabled) return; if (btn) { btn.disabled = true; btn.style.opacity = "0.5"; } toastr.info("范围重建 进行中…", "ST-BME", { timeOut: 2000 }); try { const start = _parseOptionalInt( document.getElementById("bme-range-start")?.value, ); const end = _parseOptionalInt( document.getElementById("bme-range-end")?.value, ); await _actionHandlers.rebuildVectorRange?.( Number.isFinite(start) && Number.isFinite(end) ? { start, end } : null, ); _refreshDashboard(); _refreshGraph(); toastr.success("范围重建 完成", "ST-BME"); } catch (error) { console.error("[ST-BME] Action rebuildVectorRange failed:", error); toastr.error(`范围重建 失败: ${error?.message || error}`, "ST-BME"); } finally { if (btn) { btn.style.opacity = ""; } _refreshRuntimeStatus(); _refreshGraphAvailabilityState(); } }); // 重新提取 (reroll) 绑定 document .getElementById("bme-act-reroll") ?.addEventListener("click", async () => { const btn = document.getElementById("bme-act-reroll"); if (btn?.disabled) return; const floorStr = document.getElementById("bme-reroll-floor")?.value; const fromFloor = _parseOptionalInt(floorStr); const desc = Number.isFinite(fromFloor) ? `从楼层 ${fromFloor} 开始回滚并重新提取` : "回滚最新 AI 楼并重新提取"; if (!confirm(`确认要重新提取吗?\n\n${desc}\n\n已提取的记忆节点将被回滚。`)) { return; } if (btn) { btn.disabled = true; btn.style.opacity = "0.5"; } try { await _actionHandlers.reroll?.({ fromFloor: Number.isFinite(fromFloor) ? fromFloor : undefined, }); _refreshDashboard(); _refreshGraph(); if ( document .getElementById("bme-pane-memory") ?.classList.contains("active") ) { _refreshMemoryBrowser(); } } catch (error) { console.error("[ST-BME] Action reroll failed:", error); toastr.error(`重新提取失败: ${error?.message || error}`, "ST-BME"); } finally { if (btn) { btn.style.opacity = ""; } _refreshRuntimeStatus(); _refreshGraphAvailabilityState(); } }); } function _refreshConfigTab() { const settings = _getSettings?.() || {}; _refreshPlannerLauncher(); _setCheckboxValue("bme-setting-enabled", settings.enabled ?? true); _setCheckboxValue( "bme-setting-debug-logging-enabled", settings.debugLoggingEnabled ?? false, ); _setCheckboxValue( "bme-setting-hide-old-messages-enabled", settings.hideOldMessagesEnabled ?? false, ); _setCheckboxValue( "bme-setting-recall-enabled", settings.recallEnabled ?? true, ); _setCheckboxValue("bme-setting-recall-llm", settings.recallEnableLLM ?? true); _setCheckboxValue( "bme-setting-recall-vector-prefilter-enabled", settings.recallEnableVectorPrefilter ?? true, ); _setCheckboxValue( "bme-setting-recall-graph-diffusion-enabled", settings.recallEnableGraphDiffusion ?? true, ); _setCheckboxValue( "bme-setting-recall-multi-intent-enabled", settings.recallEnableMultiIntent ?? true, ); _setCheckboxValue( "bme-setting-recall-context-query-blend-enabled", settings.recallEnableContextQueryBlend ?? true, ); _setCheckboxValue( "bme-setting-recall-lexical-boost-enabled", settings.recallEnableLexicalBoost ?? true, ); _setCheckboxValue( "bme-setting-recall-temporal-links-enabled", settings.recallEnableTemporalLinks ?? true, ); _setCheckboxValue( "bme-setting-recall-diversity-enabled", settings.recallEnableDiversitySampling ?? true, ); _setCheckboxValue( "bme-setting-recall-cooccurrence-enabled", settings.recallEnableCooccurrenceBoost ?? false, ); _setCheckboxValue( "bme-setting-recall-residual-enabled", settings.recallEnableResidualRecall ?? false, ); _setCheckboxValue( "bme-setting-scoped-memory-enabled", settings.enableScopedMemory ?? true, ); _setCheckboxValue( "bme-setting-pov-memory-enabled", settings.enablePovMemory ?? true, ); _setCheckboxValue( "bme-setting-region-scoped-objective-enabled", settings.enableRegionScopedObjective ?? true, ); _setCheckboxValue( "bme-setting-inject-user-pov-memory", settings.injectUserPovMemory ?? true, ); _setCheckboxValue( "bme-setting-inject-objective-global-memory", settings.injectObjectiveGlobalMemory ?? true, ); _setCheckboxValue( "bme-setting-consolidation-enabled", settings.enableConsolidation ?? true, ); _setCheckboxValue( "bme-setting-synopsis-enabled", settings.enableSynopsis ?? true, ); _setCheckboxValue( "bme-setting-visibility-enabled", settings.enableVisibility ?? false, ); _setCheckboxValue( "bme-setting-cross-recall-enabled", settings.enableCrossRecall ?? false, ); _setCheckboxValue( "bme-setting-smart-trigger-enabled", settings.enableSmartTrigger ?? false, ); _setCheckboxValue( "bme-setting-sleep-cycle-enabled", settings.enableSleepCycle ?? false, ); _setCheckboxValue( "bme-setting-auto-compression-enabled", settings.enableAutoCompression ?? true, ); _setCheckboxValue( "bme-setting-prob-recall-enabled", settings.enableProbRecall ?? false, ); _setCheckboxValue( "bme-setting-reflection-enabled", settings.enableReflection ?? false, ); _setInputValue( "bme-setting-recall-card-user-input-display-mode", settings.recallCardUserInputDisplayMode ?? "beautify_only", ); _setInputValue( "bme-setting-notice-display-mode", settings.noticeDisplayMode ?? "normal", ); _setInputValue("bme-setting-extract-every", settings.extractEvery ?? 1); _setInputValue( "bme-setting-hide-old-messages-keep-last-n", settings.hideOldMessagesKeepLastN ?? 12, ); _setInputValue( "bme-setting-extract-context-turns", settings.extractContextTurns ?? 2, ); _setInputValue("bme-setting-recall-top-k", settings.recallTopK ?? 20); _setInputValue("bme-setting-recall-max-nodes", settings.recallMaxNodes ?? 8); _setInputValue( "bme-setting-recall-diffusion-top-k", settings.recallDiffusionTopK ?? 100, ); _setInputValue( "bme-setting-recall-llm-candidate-pool", settings.recallLlmCandidatePool ?? 30, ); _setInputValue( "bme-setting-recall-llm-context-messages", settings.recallLlmContextMessages ?? 4, ); _setInputValue( "bme-setting-recall-multi-intent-max-segments", settings.recallMultiIntentMaxSegments ?? 4, ); _setInputValue( "bme-setting-recall-context-assistant-weight", settings.recallContextAssistantWeight ?? 0.2, ); _setInputValue( "bme-setting-recall-context-previous-user-weight", settings.recallContextPreviousUserWeight ?? 0.1, ); _setInputValue( "bme-setting-recall-lexical-weight", settings.recallLexicalWeight ?? 0.18, ); _setInputValue( "bme-setting-recall-teleport-alpha", settings.recallTeleportAlpha ?? 0.15, ); _setInputValue( "bme-setting-recall-temporal-link-strength", settings.recallTemporalLinkStrength ?? 0.2, ); _setInputValue( "bme-setting-recall-dpp-candidate-multiplier", settings.recallDppCandidateMultiplier ?? 3, ); _setInputValue( "bme-setting-recall-dpp-quality-weight", settings.recallDppQualityWeight ?? 1.0, ); _setInputValue( "bme-setting-recall-cooccurrence-scale", settings.recallCooccurrenceScale ?? 0.1, ); _setInputValue( "bme-setting-recall-cooccurrence-max-neighbors", settings.recallCooccurrenceMaxNeighbors ?? 10, ); _setInputValue( "bme-setting-recall-residual-basis-max-nodes", settings.recallResidualBasisMaxNodes ?? 24, ); _setInputValue( "bme-setting-recall-nmf-topics", settings.recallNmfTopics ?? 15, ); _setInputValue( "bme-setting-recall-nmf-novelty-threshold", settings.recallNmfNoveltyThreshold ?? 0.4, ); _setInputValue( "bme-setting-recall-residual-threshold", settings.recallResidualThreshold ?? 0.3, ); _setInputValue( "bme-setting-recall-residual-top-k", settings.recallResidualTopK ?? 5, ); _setInputValue( "bme-setting-recall-character-pov-weight", settings.recallCharacterPovWeight ?? 1.25, ); _setInputValue( "bme-setting-recall-user-pov-weight", settings.recallUserPovWeight ?? 1.05, ); _setInputValue( "bme-setting-recall-objective-current-region-weight", settings.recallObjectiveCurrentRegionWeight ?? 1.15, ); _setInputValue( "bme-setting-recall-objective-adjacent-region-weight", settings.recallObjectiveAdjacentRegionWeight ?? 0.9, ); _setInputValue( "bme-setting-recall-objective-global-weight", settings.recallObjectiveGlobalWeight ?? 0.75, ); _setInputValue("bme-setting-inject-depth", settings.injectDepth ?? 9999); _setInputValue("bme-setting-graph-weight", settings.graphWeight ?? 0.6); _setInputValue("bme-setting-vector-weight", settings.vectorWeight ?? 0.3); _setInputValue( "bme-setting-importance-weight", settings.importanceWeight ?? 0.1, ); _setInputValue( "bme-setting-consolidation-neighbor-count", settings.consolidationNeighborCount ?? 5, ); _setInputValue( "bme-setting-consolidation-threshold", settings.consolidationThreshold ?? 0.85, ); _setInputValue("bme-setting-synopsis-every", settings.synopsisEveryN ?? 5); _setInputValue( "bme-setting-trigger-patterns", settings.triggerPatterns || "", ); _setInputValue( "bme-setting-smart-trigger-threshold", settings.smartTriggerThreshold ?? 2, ); _setInputValue( "bme-setting-forget-threshold", settings.forgetThreshold ?? 0.5, ); _setInputValue( "bme-setting-consolidation-auto-min-new-nodes", settings.consolidationAutoMinNewNodes ?? 2, ); _setInputValue( "bme-setting-compression-every", settings.compressionEveryN ?? 10, ); _setInputValue("bme-setting-sleep-every", settings.sleepEveryN ?? 10); _setInputValue( "bme-setting-prob-recall-chance", settings.probRecallChance ?? 0.15, ); _setInputValue("bme-setting-reflect-every", settings.reflectEveryN ?? 10); _setInputValue("bme-setting-llm-url", settings.llmApiUrl || ""); _setInputValue("bme-setting-llm-key", settings.llmApiKey || ""); _setInputValue("bme-setting-llm-model", settings.llmModel || ""); _setInputValue("bme-setting-timeout-ms", settings.timeoutMs ?? 300000); _setInputValue("bme-setting-embed-url", settings.embeddingApiUrl || ""); _setInputValue("bme-setting-embed-key", settings.embeddingApiKey || ""); _setInputValue( "bme-setting-embed-model", settings.embeddingModel || "text-embedding-3-small", ); _setInputValue( "bme-setting-embed-mode", settings.embeddingTransportMode || "direct", ); _toggleEmbedFields(settings.embeddingTransportMode || "direct"); _setInputValue( "bme-setting-embed-backend-source", settings.embeddingBackendSource || "openai", ); _setInputValue( "bme-setting-embed-backend-model", settings.embeddingBackendModel || getSuggestedBackendModel(settings.embeddingBackendSource || "openai"), ); _setInputValue( "bme-setting-embed-backend-url", settings.embeddingBackendApiUrl || "", ); _setCheckboxValue( "bme-setting-embed-auto-suffix", settings.embeddingAutoSuffix !== false, ); _setInputValue( "bme-setting-extract-prompt", settings.extractPrompt || getDefaultPromptText("extract"), ); _setInputValue( "bme-setting-recall-prompt", settings.recallPrompt || getDefaultPromptText("recall"), ); _setInputValue( "bme-setting-consolidation-prompt", settings.consolidationPrompt || getDefaultPromptText("consolidation"), ); _setInputValue( "bme-setting-compress-prompt", settings.compressPrompt || getDefaultPromptText("compress"), ); _setInputValue( "bme-setting-synopsis-prompt", settings.synopsisPrompt || getDefaultPromptText("synopsis"), ); _setInputValue( "bme-setting-reflection-prompt", settings.reflectionPrompt || getDefaultPromptText("reflection"), ); _refreshFetchedModelSelects(settings); _refreshGuardedConfigStates(settings); _refreshStageCardStates(settings); _refreshPromptCardStates(settings); _refreshTaskProfileWorkspace(settings); _refreshMessageTraceWorkspace(settings); _highlightThemeChoice(settings.panelTheme || "crimson"); _syncConfigSectionState(); } function _bindConfigControls() { if (!panelEl || panelEl.dataset.bmeConfigBound === "true") return; panelEl.querySelectorAll(".bme-config-nav-btn").forEach((btn) => { if (btn.dataset.bmeBound === "true") return; btn.addEventListener("click", () => { _switchConfigSection(btn.dataset.configSection || "api"); }); btn.dataset.bmeBound = "true"; }); bindCheckbox("bme-setting-enabled", (checked) => { _patchSettings({ enabled: checked }); _refreshGuardedConfigStates(); }); bindCheckbox("bme-setting-debug-logging-enabled", (checked) => { _patchSettings({ debugLoggingEnabled: checked }); }); bindCheckbox("bme-setting-hide-old-messages-enabled", (checked) => { _patchSettings({ hideOldMessagesEnabled: checked }); }); bindCheckbox("bme-setting-recall-enabled", (checked) => { _patchSettings({ recallEnabled: checked }); _refreshGuardedConfigStates(); _refreshStageCardStates(); }); bindCheckbox("bme-setting-recall-llm", (checked) => { _patchSettings({ recallEnableLLM: checked }); _refreshGuardedConfigStates(); _refreshStageCardStates(); }); bindCheckbox("bme-setting-recall-vector-prefilter-enabled", (checked) => { _patchSettings({ recallEnableVectorPrefilter: checked }); _refreshStageCardStates(); }); bindCheckbox("bme-setting-recall-graph-diffusion-enabled", (checked) => { _patchSettings({ recallEnableGraphDiffusion: checked }); _refreshStageCardStates(); }); bindCheckbox("bme-setting-recall-multi-intent-enabled", (checked) => { _patchSettings({ recallEnableMultiIntent: checked }); }); bindCheckbox("bme-setting-recall-context-query-blend-enabled", (checked) => { _patchSettings({ recallEnableContextQueryBlend: checked }); }); bindCheckbox("bme-setting-recall-lexical-boost-enabled", (checked) => { _patchSettings({ recallEnableLexicalBoost: checked }); }); bindCheckbox("bme-setting-recall-temporal-links-enabled", (checked) => { _patchSettings({ recallEnableTemporalLinks: checked }); }); bindCheckbox("bme-setting-recall-diversity-enabled", (checked) => { _patchSettings({ recallEnableDiversitySampling: checked }); }); bindCheckbox("bme-setting-recall-cooccurrence-enabled", (checked) => { _patchSettings({ recallEnableCooccurrenceBoost: checked }); }); bindCheckbox("bme-setting-recall-residual-enabled", (checked) => { _patchSettings({ recallEnableResidualRecall: checked }); }); bindCheckbox("bme-setting-scoped-memory-enabled", (checked) => { _patchSettings({ enableScopedMemory: checked }); }); bindCheckbox("bme-setting-pov-memory-enabled", (checked) => { _patchSettings({ enablePovMemory: checked }); }); bindCheckbox( "bme-setting-region-scoped-objective-enabled", (checked) => { _patchSettings({ enableRegionScopedObjective: checked }); }, ); bindCheckbox("bme-setting-inject-user-pov-memory", (checked) => { _patchSettings({ injectUserPovMemory: checked }); }); bindCheckbox("bme-setting-inject-objective-global-memory", (checked) => { _patchSettings({ injectObjectiveGlobalMemory: checked }); }); bindCheckbox("bme-setting-consolidation-enabled", (checked) => { _patchSettings({ enableConsolidation: checked }); _refreshGuardedConfigStates(); }); bindCheckbox("bme-setting-synopsis-enabled", (checked) => { _patchSettings({ enableSynopsis: checked }); _refreshGuardedConfigStates(); }); bindCheckbox("bme-setting-visibility-enabled", (checked) => _patchSettings({ enableVisibility: checked }), ); bindCheckbox("bme-setting-cross-recall-enabled", (checked) => _patchSettings({ enableCrossRecall: checked }), ); bindCheckbox("bme-setting-smart-trigger-enabled", (checked) => { _patchSettings({ enableSmartTrigger: checked }); _refreshGuardedConfigStates(); }); bindCheckbox("bme-setting-sleep-cycle-enabled", (checked) => { _patchSettings({ enableSleepCycle: checked }); _refreshGuardedConfigStates(); }); bindCheckbox("bme-setting-auto-compression-enabled", (checked) => { _patchSettings({ enableAutoCompression: checked }); _refreshGuardedConfigStates(); }); bindCheckbox("bme-setting-prob-recall-enabled", (checked) => { _patchSettings({ enableProbRecall: checked }); _refreshGuardedConfigStates(); }); bindCheckbox("bme-setting-reflection-enabled", (checked) => { _patchSettings({ enableReflection: checked }); _refreshGuardedConfigStates(); }); const recallCardUserInputDisplayModeEl = document.getElementById( "bme-setting-recall-card-user-input-display-mode", ); if ( recallCardUserInputDisplayModeEl && recallCardUserInputDisplayModeEl.dataset.bmeBound !== "true" ) { recallCardUserInputDisplayModeEl.addEventListener("change", () => { _patchSettings({ recallCardUserInputDisplayMode: recallCardUserInputDisplayModeEl.value || "beautify_only", }); }); recallCardUserInputDisplayModeEl.dataset.bmeBound = "true"; } const noticeDisplayModeEl = document.getElementById( "bme-setting-notice-display-mode", ); if (noticeDisplayModeEl && noticeDisplayModeEl.dataset.bmeBound !== "true") { noticeDisplayModeEl.addEventListener("change", () => { _patchSettings({ noticeDisplayMode: noticeDisplayModeEl.value || "normal", }); }); noticeDisplayModeEl.dataset.bmeBound = "true"; } bindNumber("bme-setting-extract-every", 1, 1, 50, (value) => _patchSettings({ extractEvery: value }), ); bindNumber( "bme-setting-hide-old-messages-keep-last-n", 12, 0, 200, (value) => _patchSettings({ hideOldMessagesKeepLastN: value }), ); bindNumber("bme-setting-extract-context-turns", 2, 0, 20, (value) => _patchSettings({ extractContextTurns: value }), ); bindNumber("bme-setting-recall-top-k", 20, 1, 100, (value) => _patchSettings({ recallTopK: value }), ); bindNumber("bme-setting-recall-max-nodes", 8, 1, 50, (value) => _patchSettings({ recallMaxNodes: value }), ); bindNumber("bme-setting-recall-diffusion-top-k", 100, 1, 300, (value) => _patchSettings({ recallDiffusionTopK: value }), ); bindNumber("bme-setting-recall-llm-candidate-pool", 30, 1, 100, (value) => _patchSettings({ recallLlmCandidatePool: value }), ); bindNumber("bme-setting-recall-llm-context-messages", 4, 0, 20, (value) => _patchSettings({ recallLlmContextMessages: value }), ); bindNumber( "bme-setting-recall-multi-intent-max-segments", 4, 1, 8, (value) => _patchSettings({ recallMultiIntentMaxSegments: value }), ); bindFloat( "bme-setting-recall-context-assistant-weight", 0.2, 0, 1, (value) => _patchSettings({ recallContextAssistantWeight: value }), ); bindFloat( "bme-setting-recall-context-previous-user-weight", 0.1, 0, 1, (value) => _patchSettings({ recallContextPreviousUserWeight: value }), ); bindFloat("bme-setting-recall-lexical-weight", 0.18, 0, 1, (value) => _patchSettings({ recallLexicalWeight: value }), ); bindFloat("bme-setting-recall-teleport-alpha", 0.15, 0, 1, (value) => _patchSettings({ recallTeleportAlpha: value }), ); bindFloat( "bme-setting-recall-temporal-link-strength", 0.2, 0, 1, (value) => _patchSettings({ recallTemporalLinkStrength: value }), ); bindNumber( "bme-setting-recall-dpp-candidate-multiplier", 3, 1, 10, (value) => _patchSettings({ recallDppCandidateMultiplier: value }), ); bindFloat("bme-setting-recall-dpp-quality-weight", 1.0, 0, 10, (value) => _patchSettings({ recallDppQualityWeight: value }), ); bindFloat("bme-setting-recall-cooccurrence-scale", 0.1, 0, 10, (value) => _patchSettings({ recallCooccurrenceScale: value }), ); bindNumber( "bme-setting-recall-cooccurrence-max-neighbors", 10, 1, 50, (value) => _patchSettings({ recallCooccurrenceMaxNeighbors: value }), ); bindNumber( "bme-setting-recall-residual-basis-max-nodes", 24, 2, 64, (value) => _patchSettings({ recallResidualBasisMaxNodes: value }), ); bindNumber("bme-setting-recall-nmf-topics", 15, 2, 64, (value) => _patchSettings({ recallNmfTopics: value }), ); bindFloat( "bme-setting-recall-nmf-novelty-threshold", 0.4, 0, 1, (value) => _patchSettings({ recallNmfNoveltyThreshold: value }), ); bindFloat("bme-setting-recall-residual-threshold", 0.3, 0, 10, (value) => _patchSettings({ recallResidualThreshold: value }), ); bindNumber("bme-setting-recall-residual-top-k", 5, 1, 20, (value) => _patchSettings({ recallResidualTopK: value }), ); bindFloat("bme-setting-recall-character-pov-weight", 1.25, 0, 3, (value) => _patchSettings({ recallCharacterPovWeight: value }), ); bindFloat("bme-setting-recall-user-pov-weight", 1.05, 0, 3, (value) => _patchSettings({ recallUserPovWeight: value }), ); bindFloat( "bme-setting-recall-objective-current-region-weight", 1.15, 0, 3, (value) => _patchSettings({ recallObjectiveCurrentRegionWeight: value }), ); bindFloat( "bme-setting-recall-objective-adjacent-region-weight", 0.9, 0, 3, (value) => _patchSettings({ recallObjectiveAdjacentRegionWeight: value }), ); bindFloat( "bme-setting-recall-objective-global-weight", 0.75, 0, 3, (value) => _patchSettings({ recallObjectiveGlobalWeight: value }), ); bindNumber("bme-setting-inject-depth", 9999, 0, 9999, (value) => _patchSettings({ injectDepth: value }), ); bindFloat("bme-setting-graph-weight", 0.6, 0, 1, (value) => _patchSettings({ graphWeight: value }), ); bindFloat("bme-setting-vector-weight", 0.3, 0, 1, (value) => _patchSettings({ vectorWeight: value }), ); bindFloat("bme-setting-importance-weight", 0.1, 0, 1, (value) => _patchSettings({ importanceWeight: value }), ); bindNumber("bme-setting-consolidation-neighbor-count", 5, 1, 20, (value) => _patchSettings({ consolidationNeighborCount: value }), ); bindFloat("bme-setting-consolidation-threshold", 0.85, 0.5, 0.99, (value) => _patchSettings({ consolidationThreshold: value }), ); bindNumber("bme-setting-synopsis-every", 5, 1, 100, (value) => _patchSettings({ synopsisEveryN: value }), ); bindText("bme-setting-trigger-patterns", (value) => _patchSettings({ triggerPatterns: value }), ); bindNumber("bme-setting-smart-trigger-threshold", 2, 1, 10, (value) => _patchSettings({ smartTriggerThreshold: value }), ); bindFloat("bme-setting-forget-threshold", 0.5, 0.1, 1, (value) => _patchSettings({ forgetThreshold: value }), ); bindNumber( "bme-setting-consolidation-auto-min-new-nodes", 2, 1, 50, (value) => _patchSettings({ consolidationAutoMinNewNodes: value }), ); bindNumber( "bme-setting-compression-every", 10, 0, 500, (value) => _patchSettings({ compressionEveryN: value }), ); bindNumber("bme-setting-sleep-every", 10, 1, 200, (value) => _patchSettings({ sleepEveryN: value }), ); bindFloat("bme-setting-prob-recall-chance", 0.15, 0.01, 0.5, (value) => _patchSettings({ probRecallChance: value }), ); bindNumber("bme-setting-reflect-every", 10, 1, 200, (value) => _patchSettings({ reflectEveryN: value }), ); bindText("bme-setting-llm-url", (value) => _patchSettings({ llmApiUrl: value.trim() }), ); bindText("bme-setting-llm-key", (value) => _patchSettings({ llmApiKey: value.trim() }), ); bindText("bme-setting-llm-model", (value) => _patchSettings({ llmModel: value.trim() }), ); bindNumber("bme-setting-timeout-ms", 300000, 1000, 3600000, (value) => _patchSettings({ timeoutMs: value }), ); bindText("bme-setting-embed-url", (value) => _patchSettings({ embeddingApiUrl: value.trim() }), ); bindText("bme-setting-embed-key", (value) => _patchSettings({ embeddingApiKey: value.trim() }), ); bindText("bme-setting-embed-model", (value) => _patchSettings({ embeddingModel: value.trim() }), ); bindText("bme-setting-embed-mode", (value) => { _patchSettings({ embeddingTransportMode: value }); _toggleEmbedFields(value); }); bindText("bme-setting-embed-backend-source", (value) => { const settings = _getSettings?.() || {}; const patch = { embeddingBackendSource: value }; const suggestedModel = getSuggestedBackendModel(value); if ( !settings.embeddingBackendModel || settings.embeddingBackendModel === getSuggestedBackendModel(settings.embeddingBackendSource || "openai") ) { patch.embeddingBackendModel = suggestedModel; } _patchSettings(patch); _setInputValue( "bme-setting-embed-backend-model", patch.embeddingBackendModel || settings.embeddingBackendModel || "", ); }); bindText("bme-setting-embed-backend-model", (value) => _patchSettings({ embeddingBackendModel: value.trim() }), ); bindText("bme-setting-embed-backend-url", (value) => _patchSettings({ embeddingBackendApiUrl: value.trim() }), ); bindCheckbox("bme-setting-embed-auto-suffix", (checked) => _patchSettings({ embeddingAutoSuffix: checked }), ); bindPromptText("bme-setting-extract-prompt", "extractPrompt", "extract"); bindPromptText("bme-setting-recall-prompt", "recallPrompt", "recall"); bindPromptText( "bme-setting-consolidation-prompt", "consolidationPrompt", "consolidation", ); bindPromptText("bme-setting-compress-prompt", "compressPrompt", "compress"); bindPromptText("bme-setting-synopsis-prompt", "synopsisPrompt", "synopsis"); bindPromptText( "bme-setting-reflection-prompt", "reflectionPrompt", "reflection", ); _bindTaskProfileWorkspace(); panelEl.querySelectorAll(".bme-prompt-reset").forEach((button) => { if (button.dataset.bmeBound === "true") return; button.addEventListener("click", () => { const settingKey = button.dataset.settingKey; const promptKey = button.dataset.defaultPrompt; const targetId = button.dataset.targetId; if (!settingKey || !promptKey || !targetId) return; _patchSettings({ [settingKey]: "" }, { refreshPrompts: true }); _setInputValue(targetId, getDefaultPromptText(promptKey)); _refreshPromptCardStates(); }); button.dataset.bmeBound = "true"; }); const pickerBtn = document.getElementById("bme-theme-picker-btn"); const dropdown = document.getElementById("bme-theme-dropdown"); if (pickerBtn && dropdown) { pickerBtn.addEventListener("click", (e) => { e.stopPropagation(); dropdown.classList.toggle("open"); }); dropdown.querySelectorAll(".bme-theme-option").forEach((opt) => { opt.addEventListener("click", () => { const theme = opt.dataset.theme; if (!theme) return; _patchSettings({ panelTheme: theme }, { refreshTheme: true }); dropdown.classList.remove("open"); }); }); document.addEventListener("click", () => { dropdown.classList.remove("open"); }); dropdown.addEventListener("click", (e) => e.stopPropagation()); } panelEl.querySelectorAll(".bme-theme-card").forEach((card) => { if (card.dataset.bmeBound === "true") return; card.addEventListener("click", () => { const theme = card.dataset.theme; if (!theme) return; _patchSettings({ panelTheme: theme }, { refreshTheme: true }); }); card.dataset.bmeBound = "true"; }); document .getElementById("bme-apply-hide-settings") ?.addEventListener("click", async () => { const result = await _actionHandlers.applyCurrentHide?.(); if (result?.error) { toastr.error(result.error, "ST-BME"); return; } toastr.success("当前聊天的隐藏设置已重新应用", "ST-BME"); }); document .getElementById("bme-clear-hide-settings") ?.addEventListener("click", async () => { const result = await _actionHandlers.clearCurrentHide?.(); if (result?.error) { toastr.error(result.error, "ST-BME"); return; } toastr.info("已取消当前聊天里由 ST-BME 应用的隐藏", "ST-BME"); }); document .getElementById("bme-test-llm") ?.addEventListener("click", async () => { await _actionHandlers.testMemoryLLM?.(); }); document .getElementById("bme-test-embedding") ?.addEventListener("click", async () => { await _actionHandlers.testEmbedding?.(); }); document .getElementById("bme-fetch-llm-models") ?.addEventListener("click", async () => { const result = await _actionHandlers.fetchMemoryLLMModels?.(); if (!result?.success) return; fetchedMemoryLLMModels = result.models || []; _renderFetchedModelOptions( "bme-select-llm-model", fetchedMemoryLLMModels, (_getSettings?.() || {}).llmModel || "", ); }); document .getElementById("bme-fetch-embed-backend-models") ?.addEventListener("click", async () => { const result = await _actionHandlers.fetchEmbeddingModels?.("backend"); if (!result?.success) return; fetchedBackendEmbeddingModels = result.models || []; _renderFetchedModelOptions( "bme-select-embed-backend-model", fetchedBackendEmbeddingModels, (_getSettings?.() || {}).embeddingBackendModel || "", ); }); document .getElementById("bme-fetch-embed-direct-models") ?.addEventListener("click", async () => { const result = await _actionHandlers.fetchEmbeddingModels?.("direct"); if (!result?.success) return; fetchedDirectEmbeddingModels = result.models || []; _renderFetchedModelOptions( "bme-select-embed-direct-model", fetchedDirectEmbeddingModels, (_getSettings?.() || {}).embeddingModel || "", ); }); bindSelectModel("bme-select-llm-model", "bme-setting-llm-model", "llmModel"); bindSelectModel( "bme-select-embed-backend-model", "bme-setting-embed-backend-model", "embeddingBackendModel", ); bindSelectModel( "bme-select-embed-direct-model", "bme-setting-embed-model", "embeddingModel", ); panelEl.dataset.bmeConfigBound = "true"; } function bindText(id, onChange) { const element = document.getElementById(id); if (!element || element.dataset.bmeBound === "true") return; element.addEventListener("input", () => onChange(element.value)); element.addEventListener("change", () => onChange(element.value)); element.dataset.bmeBound = "true"; } function bindCheckbox(id, onChange) { const element = document.getElementById(id); if (!element || element.dataset.bmeBound === "true") return; element.addEventListener("change", () => onChange(Boolean(element.checked))); element.dataset.bmeBound = "true"; } function bindNumber(id, fallback, min, max, onChange) { const element = document.getElementById(id); if (!element || element.dataset.bmeBound === "true") return; element.addEventListener("input", () => { let value = Number.parseInt(element.value, 10); if (!Number.isFinite(value)) value = fallback; value = Math.min(max, Math.max(min, value)); onChange(value); }); element.dataset.bmeBound = "true"; } function bindFloat(id, fallback, min, max, onChange) { const element = document.getElementById(id); if (!element || element.dataset.bmeBound === "true") return; element.addEventListener("input", () => { let value = Number.parseFloat(element.value); if (!Number.isFinite(value)) value = fallback; value = Math.min(max, Math.max(min, value)); onChange(value); }); element.dataset.bmeBound = "true"; } function bindPromptText(id, settingKey, promptKey) { const element = document.getElementById(id); if (!element || element.dataset.bmeBound === "true") return; const update = () => { _patchSettings({ [settingKey]: element.value }, { refreshPrompts: true }); }; element.addEventListener("input", update); element.addEventListener("change", update); element.addEventListener("blur", () => { if (!String(element.value || "").trim()) { _setInputValue(id, getDefaultPromptText(promptKey)); } }); element.dataset.bmeBound = "true"; } function bindSelectModel(selectId, inputId, settingKey) { const element = document.getElementById(selectId); if (!element || element.dataset.bmeBound === "true") return; element.addEventListener("change", () => { if (!element.value) return; _setInputValue(inputId, element.value); _patchSettings({ [settingKey]: element.value }); }); element.dataset.bmeBound = "true"; } function _bindTaskProfileWorkspace() { const workspace = document.getElementById("bme-task-profile-workspace"); const importInput = document.getElementById("bme-task-profile-import"); if (!workspace) return; if (workspace.dataset.bmeBound !== "true") { workspace.addEventListener("click", (event) => { void _handleTaskProfileWorkspaceClick(event); }); workspace.addEventListener("input", (event) => { _handleTaskProfileWorkspaceInput(event); }); workspace.addEventListener("change", (event) => { _handleTaskProfileWorkspaceChange(event); }); workspace.dataset.bmeBound = "true"; } if (importInput && importInput.dataset.bmeBound !== "true") { importInput.addEventListener("change", async () => { const file = importInput.files?.[0]; if (!file) return; try { const text = await file.text(); const settings = _getSettings?.() || {}; const imported = parseImportedTaskProfile( settings.taskProfiles || {}, text, ); currentTaskProfileTaskType = imported.taskType || currentTaskProfileTaskType; currentTaskProfileBlockId = imported.profile?.blocks?.[0]?.id || ""; currentTaskProfileRuleId = imported.profile?.regex?.localRules?.[0]?.id || ""; _patchTaskProfiles(imported.taskProfiles); toastr.success("预设导入成功", "ST-BME"); } catch (error) { console.error("[ST-BME] 导入任务预设失败:", error); toastr.error(`预设导入失败: ${error?.message || error}`, "ST-BME"); } finally { importInput.value = ""; } }); importInput.dataset.bmeBound = "true"; } const importAllInput = document.getElementById("bme-task-profile-import-all"); if (importAllInput && importAllInput.dataset.bmeBound !== "true") { importAllInput.addEventListener("change", async () => { const file = importAllInput.files?.[0]; if (!file) return; try { const text = await file.text(); const parsed = JSON.parse(text); if (parsed?.format !== "st-bme-all-task-profiles" || !parsed?.profiles) { throw new Error("文件格式不正确,请选择「导出全部」生成的文件"); } const settings = _getSettings?.() || {}; let mergedProfiles = settings.taskProfiles || {}; let importedCount = 0; for (const [taskType, entry] of Object.entries(parsed.profiles)) { try { const imported = parseImportedTaskProfile( mergedProfiles, entry, taskType, ); mergedProfiles = imported.taskProfiles; importedCount++; } catch (innerError) { console.warn(`[ST-BME] 跳过导入任务 ${taskType}:`, innerError); } } if (importedCount === 0) { toastr.warning("没有成功导入任何预设", "ST-BME"); return; } _patchTaskProfiles(mergedProfiles); toastr.success(`已导入 ${importedCount} 个任务预设`, "ST-BME"); } catch (error) { console.error("[ST-BME] 导入全部预设失败:", error); toastr.error(`导入全部预设失败: ${error?.message || error}`, "ST-BME"); } finally { importAllInput.value = ""; } }); importAllInput.dataset.bmeBound = "true"; } } function _handleTaskProfileWorkspaceInput(event) { const target = event.target; if (!(target instanceof HTMLElement)) return; if (target.id === "bme-task-profile-name") { _updateCurrentTaskProfile( (draft) => { draft.name = String(target.value || "").trim() || draft.name; }, { refresh: false }, ); return; } if (target.matches("[data-block-field]")) { _persistSelectedBlockField(target, false); return; } if (target.matches("[data-generation-key]")) { // 滑动条 ↔ 数字输入 同步 const group = target.closest(".bme-range-group"); if (group) { const key = target.dataset.generationKey; const sibling = group.querySelector( target.type === "range" ? `.bme-range-number` : `.bme-range-input`, ); if (sibling) sibling.value = target.value; // 更新 label 上的值显示 const row = target.closest(".bme-config-row"); const badge = row?.querySelector(".bme-range-value"); if (badge) badge.textContent = target.value || "默认"; } _persistGenerationField(target, false); return; } if ( target.matches("[data-regex-rule-field]") || target.matches("[data-regex-rule-source]") || target.matches("[data-regex-rule-destination]") ) { _persistSelectedRegexRuleField(target, false); } } function _handleTaskProfileWorkspaceChange(event) { const target = event.target; if (!(target instanceof HTMLElement)) return; if (target.id === "bme-task-profile-select") { const settings = _getSettings?.() || {}; const nextTaskProfiles = setActiveTaskProfileId( settings.taskProfiles || {}, currentTaskProfileTaskType, target.value, ); currentTaskProfileBlockId = ""; currentTaskProfileRuleId = ""; _patchTaskProfiles(nextTaskProfiles); return; } if (target.matches("[data-block-field]")) { _persistSelectedBlockField(target, true); return; } if (target.matches("[data-generation-key]")) { _persistGenerationField(target, true); return; } if (target.matches("[data-regex-field]")) { _persistRegexConfigField(target, false); return; } if (target.matches("[data-regex-source]")) { _persistRegexSourceField(target, false); return; } if (target.matches("[data-regex-stage]")) { _persistRegexStageField(target, false); return; } if ( target.matches("[data-regex-rule-field]") || target.matches("[data-regex-rule-source]") || target.matches("[data-regex-rule-destination]") ) { _persistSelectedRegexRuleField(target, true); } } function _getTaskProfileWorkspaceState(settings = _getSettings?.() || {}) { const taskProfiles = ensureTaskProfiles(settings); const taskTypeOptions = getTaskTypeOptions(); const runtimeDebug = _getRuntimeDebugSnapshot?.() || { hostCapabilities: null, runtimeDebug: null, }; if (!taskTypeOptions.some((item) => item.id === currentTaskProfileTaskType)) { currentTaskProfileTaskType = taskTypeOptions[0]?.id || "extract"; } if (!TASK_PROFILE_TABS.some((item) => item.id === currentTaskProfileTabId)) { currentTaskProfileTabId = TASK_PROFILE_TABS[0]?.id || "generation"; } const bucket = taskProfiles[currentTaskProfileTaskType] || { activeProfileId: "default", profiles: [], }; const profile = bucket.profiles.find((item) => item.id === bucket.activeProfileId) || bucket.profiles[0] || null; const blocks = _sortTaskBlocks(profile?.blocks || []); const regexRules = Array.isArray(profile?.regex?.localRules) ? profile.regex.localRules : []; if (!blocks.some((block) => block.id === currentTaskProfileBlockId)) { currentTaskProfileBlockId = blocks[0]?.id || ""; } if (!regexRules.some((rule) => rule.id === currentTaskProfileRuleId)) { currentTaskProfileRuleId = regexRules[0]?.id || ""; } return { settings, taskProfiles, taskTypeOptions, taskType: currentTaskProfileTaskType, taskTabId: currentTaskProfileTabId, bucket, profile, blocks, selectedBlock: blocks.find((block) => block.id === currentTaskProfileBlockId) || null, regexRules, selectedRule: regexRules.find((rule) => rule.id === currentTaskProfileRuleId) || null, builtinBlockDefinitions: getBuiltinBlockDefinitions(), runtimeDebug, }; } function _refreshTaskProfileWorkspace(settings = _getSettings?.() || {}) { const workspace = document.getElementById("bme-task-profile-workspace"); if (!workspace) return; const state = _getTaskProfileWorkspaceState(settings); workspace.innerHTML = _renderTaskProfileWorkspace(state); } function _getMessageTraceWorkspaceState(settings = _getSettings?.() || {}) { const panelDebug = _getRuntimeDebugSnapshot?.() || { hostCapabilities: null, runtimeDebug: null, }; const runtimeDebug = panelDebug.runtimeDebug || {}; return { settings, panelDebug, runtimeDebug, recallInjection: runtimeDebug?.injections?.recall || null, messageTrace: runtimeDebug?.messageTrace || null, recallLlmRequest: runtimeDebug?.taskLlmRequests?.recall || null, recallPromptBuild: runtimeDebug?.taskPromptBuilds?.recall || null, extractLlmRequest: runtimeDebug?.taskLlmRequests?.extract || null, extractPromptBuild: runtimeDebug?.taskPromptBuilds?.extract || null, }; } function _refreshMessageTraceWorkspace(settings = _getSettings?.() || {}) { const workspace = document.getElementById("bme-message-trace-workspace"); if (!workspace) return; const state = _getMessageTraceWorkspaceState(settings); workspace.innerHTML = _renderMessageTraceWorkspace(state); } function _renderMessageTraceWorkspace(state) { const updatedCandidates = [ state.recallInjection?.updatedAt, state.recallLlmRequest?.updatedAt, state.extractLlmRequest?.updatedAt, state.extractPromptBuild?.updatedAt, ] .map((value) => Date.parse(String(value || ""))) .filter((value) => Number.isFinite(value)); const updatedAt = updatedCandidates.length ? new Date(Math.max(...updatedCandidates)).toISOString() : ""; return `
${_escHtml(normalized)}`
: `${_escHtml(rule.findRegex || "(空 findRegex)")}
${rule.replaceString ? ` -> ${_escHtml(rule.replaceString)}` : ""}
${flags.length ? `${_escHtml(_stringifyDebugValue(value))}`
}