// ST-BME: 操控面板交互逻辑 import { renderTemplateAsync } from "../../../templates.js"; import { GraphRenderer } from "./graph-renderer.js"; import { getNodeDisplayName } from "./node-labels.js"; import { cloneTaskProfile, createBuiltinPromptBlock, createCustomPromptBlock, createLocalRegexRule, DEFAULT_TASK_BLOCKS, ensureTaskProfiles, exportTaskProfile as serializeTaskProfile, getBuiltinBlockDefinitions, getLegacyPromptFieldForTask, getTaskTypeOptions, importTaskProfile as parseImportedTaskProfile, restoreDefaultTaskProfile, setActiveTaskProfileId, upsertTaskProfile, } from "./prompt-profiles.js"; import { getNodeColors } from "./themes.js"; import { getSuggestedBackendModel, getVectorIndexStats, } from "./vector-index.js"; // 从 DEFAULT_TASK_BLOCKS 派生的合并文本(用于旧版兼容回退) const DEFAULT_PROMPTS = Object.fromEntries( Object.entries(DEFAULT_TASK_BLOCKS).map(([key, { role, format, rules }]) => [ key, [role, format, rules].filter(Boolean).join("\n\n"), ]), ); const TASK_PROFILE_TABS = [ { id: "generation", label: "生成参数" }, { id: "prompt", label: "Prompt 编排" }, { id: "regex", 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 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: 0.7 }, { 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: "对发送给 LLM 的 prompt 执行正则替换。" }, { key: "output", label: "输出阶段", desc: "对 LLM 返回的结果执行正则替换。" }, ]; 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 = []; // 由 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 _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; } /** * 初始化面板(由 index.js 调用一次) */ export async function initPanel({ getGraph, getSettings, getLastExtract, getLastRecall, getRuntimeStatus, getLastExtractionStatus, getLastVectorStatus, getLastRecallStatus, getLastInjection, updateSettings, actions, }) { _getGraph = getGraph; _getSettings = getSettings; _getLastExtract = getLastExtract; _getLastRecall = getLastRecall; _getRuntimeStatus = getRuntimeStatus; _getLastExtractionStatus = getLastExtractionStatus; _getLastVectorStatus = getLastVectorStatus; _getLastRecallStatus = getLastRecallStatus; _getLastInjection = getLastInjection; _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"); $("body").append(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", ); } } _bindTabs(); _bindClose(); _bindResizeHandle(); _bindPanelResize(); _bindGraphControls(); _bindActions(); _bindConfigControls(); currentTabId = panelEl?.querySelector(".bme-tab-btn.active")?.dataset.tab || "dashboard"; _applyWorkspaceMode(); _syncConfigSectionState(); _refreshRuntimeStatus(); } /** * 打开面板 */ export function openPanel() { if (!overlayEl) return; overlayEl.classList.add("active"); _restorePanelSize(); const isMobile = _isMobile(); const settings = _getSettings?.() || {}; const themeName = settings.panelTheme || "crimson"; const canvas = document.getElementById("bme-graph-canvas"); if (canvas && !graphRenderer && !isMobile) { graphRenderer = new GraphRenderer(canvas, themeName); graphRenderer.onNodeSelect = (node) => _showNodeDetail(node); } const mobileCanvas = document.getElementById("bme-mobile-graph-canvas"); if (mobileCanvas && !mobileGraphRenderer && isMobile) { mobileGraphRenderer = new GraphRenderer(mobileCanvas, themeName); 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; } _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}`); }); _applyWorkspaceMode(); switch (currentTabId) { case "dashboard": _refreshDashboard(); break; case "memory": _refreshMemoryBrowser(); break; case "injection": void _refreshInjectionPreview(); break; case "config": _refreshConfigTab(); break; default: break; } } function _applyWorkspaceMode() { if (!panelEl) return; const isConfig = currentTabId === "config"; panelEl.classList.toggle("config-mode", isConfig); } function _switchConfigSection(sectionId) { currentConfigSectionId = sectionId || "api"; _syncConfigSectionState(); } 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?.(); if (!graph) 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 = 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?.() || {}; _setText("bme-status-chat-id", chatId); _setText( "bme-status-history", 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) { listEl.innerHTML = '
  • 暂无数据
  • '; return; } listEl.innerHTML = items .map((item) => { const secondary = item.meta || item.time || ""; return `
  • ${_typeLabel(item.type)}
    ${_escHtml(item.name || "—")}
    ${_escHtml(secondary)}
  • `; }) .join(""); } // ==================== 记忆浏览器 ==================== 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"); if (!listEl) return; const query = String(searchInput?.value || "") .trim() .toLowerCase(); const filter = filterSelect?.value || "all"; let nodes = graph.nodes.filter((node) => !node.archived); if (filter !== "all") { nodes = nodes.filter((node) => node.type === 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); }); } 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((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(""); 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); }); 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) { 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 (error) { container.innerHTML = `
    预览生成失败: ${_escHtml(error.message)}
    `; if (tokenEl) tokenEl.textContent = ""; } } // ==================== 图谱 ==================== function _refreshGraph() { const graph = _getGraph?.(); if (!graph) return; graphRenderer?.loadGraph(graph); mobileGraphRenderer?.loadGraph(graph); } function _buildLegend() { const legendEl = document.getElementById("bme-graph-legend"); if (!legendEl) return; const settings = _getSettings?.() || {}; 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: "reflection", label: "反思" }, ]; 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()); } // ==================== 节点详情 ==================== 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); 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 }, ]; 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(", ") }); } for (const [key, value] of Object.entries(fields)) { items.push({ label: key, value: typeof value === "object" ? JSON.stringify(value, null, 2) : value, }); } 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"); }); 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; 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-vector-rebuild": "rebuildVectorIndex", "bme-act-vector-reembed": "reembedDirect", }; const actionLabels = { extract: "手动提取", compress: "手动压缩", sleep: "执行遗忘", synopsis: "更新概要", export: "导出图谱", import: "导入图谱", rebuild: "重建图谱", evolve: "强制进化", 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 { 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(); } toastr.success(`${label} 完成`, "ST-BME"); } catch (error) { console.error(`[ST-BME] Action ${actionKey} failed:`, error); toastr.error(`${label} 失败: ${error?.message || error}`, "ST-BME"); } finally { btn.disabled = false; btn.style.opacity = ""; } }); } 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.disabled = false; btn.style.opacity = ""; } } }); // 重新提取 (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.disabled = false; btn.style.opacity = ""; } } }); } function _refreshConfigTab() { const settings = _getSettings?.() || {}; _setCheckboxValue("bme-setting-enabled", settings.enabled ?? 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-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-prob-recall-enabled", settings.enableProbRecall ?? false, ); _setCheckboxValue( "bme-setting-reflection-enabled", settings.enableReflection ?? false, ); _setInputValue("bme-setting-extract-every", settings.extractEvery ?? 1); _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-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-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 || "backend", ); _toggleEmbedFields(settings.embeddingTransportMode || "backend"); _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 || DEFAULT_PROMPTS.extract, ); _setInputValue( "bme-setting-recall-prompt", settings.recallPrompt || DEFAULT_PROMPTS.recall, ); _setInputValue( "bme-setting-consolidation-prompt", settings.consolidationPrompt || DEFAULT_PROMPTS.consolidation, ); _setInputValue( "bme-setting-compress-prompt", settings.compressPrompt || DEFAULT_PROMPTS.compress, ); _setInputValue( "bme-setting-synopsis-prompt", settings.synopsisPrompt || DEFAULT_PROMPTS.synopsis, ); _setInputValue( "bme-setting-reflection-prompt", settings.reflectionPrompt || DEFAULT_PROMPTS.reflection, ); _refreshFetchedModelSelects(settings); _refreshGuardedConfigStates(settings); _refreshStageCardStates(settings); _refreshPromptCardStates(settings); _refreshTaskProfileWorkspace(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-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-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-prob-recall-enabled", (checked) => { _patchSettings({ enableProbRecall: checked }); _refreshGuardedConfigStates(); }); bindCheckbox("bme-setting-reflection-enabled", (checked) => { _patchSettings({ enableReflection: checked }); _refreshGuardedConfigStates(); }); bindNumber("bme-setting-extract-every", 1, 1, 50, (value) => _patchSettings({ extractEvery: 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-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-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, DEFAULT_PROMPTS[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-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, DEFAULT_PROMPTS[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"; } } 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(); 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(), }; } function _refreshTaskProfileWorkspace(settings = _getSettings?.() || {}) { const workspace = document.getElementById("bme-task-profile-workspace"); if (!workspace) return; const state = _getTaskProfileWorkspaceState(settings); workspace.innerHTML = _renderTaskProfileWorkspace(state); } function _patchTaskProfiles(taskProfiles, extraPatch = {}, options = {}) { return _patchSettings( { taskProfilesVersion: 1, taskProfiles, ...extraPatch, }, { refreshTaskWorkspace: options.refresh !== false, }, ); } async function _handleTaskProfileWorkspaceClick(event) { const actionEl = event.target.closest("[data-task-action]"); if (!actionEl) return; const action = actionEl.dataset.taskAction || ""; const state = _getTaskProfileWorkspaceState(); const selectedProfile = state.profile; if (!selectedProfile && action !== "switch-task-type") return; switch (action) { case "switch-task-type": currentTaskProfileTaskType = actionEl.dataset.taskType || currentTaskProfileTaskType; currentTaskProfileBlockId = ""; currentTaskProfileRuleId = ""; _refreshTaskProfileWorkspace(); return; case "switch-task-tab": currentTaskProfileTabId = actionEl.dataset.taskTab || currentTaskProfileTabId; _refreshTaskProfileWorkspace(); return; case "select-block": currentTaskProfileBlockId = actionEl.dataset.blockId || ""; _refreshTaskProfileWorkspace(); return; case "select-regex-rule": currentTaskProfileRuleId = actionEl.dataset.ruleId || ""; _refreshTaskProfileWorkspace(); return; case "add-custom-block": _updateCurrentTaskProfile((draft, context) => { const nextBlock = createCustomPromptBlock(context.taskType, { name: `自定义块 ${draft.blocks.length + 1}`, order: draft.blocks.length, }); draft.blocks.push(nextBlock); return { selectBlockId: nextBlock.id }; }); return; case "add-builtin-block": { const select = document.getElementById("bme-task-builtin-select"); const sourceKey = String(select?.value || "").trim(); if (!sourceKey) { toastr.info("先选择一个内置块来源", "ST-BME"); return; } _updateCurrentTaskProfile((draft, context) => { const nextBlock = createBuiltinPromptBlock(context.taskType, sourceKey, { order: draft.blocks.length, }); draft.blocks.push(nextBlock); return { selectBlockId: nextBlock.id }; }); return; } case "move-block-up": _moveTaskBlock(actionEl.dataset.blockId, -1); return; case "move-block-down": _moveTaskBlock(actionEl.dataset.blockId, 1); return; case "toggle-block-enabled": _updateCurrentTaskProfile((draft) => { const blocks = _sortTaskBlocks(draft.blocks); const block = blocks.find((item) => item.id === actionEl.dataset.blockId); if (!block) return null; block.enabled = block.enabled === false; draft.blocks = _normalizeTaskBlocks(blocks); return { selectBlockId: block.id }; }); return; case "delete-block": _deleteTaskBlock(actionEl.dataset.blockId); return; case "save-profile": _patchTaskProfiles(state.taskProfiles, {}, { refresh: true }); toastr.success("当前预设已保存", "ST-BME"); return; case "rename-profile": { const nameInput = document.getElementById("bme-task-profile-name"); const nextName = String(nameInput?.value || "").trim(); if (!nextName) { toastr.info("预设名称不能为空", "ST-BME"); return; } _updateCurrentTaskProfile((draft) => { draft.name = nextName; }); toastr.success("预设名称已更新", "ST-BME"); return; } case "save-as-profile": { const suggestedName = `${selectedProfile.name || "预设"} 副本`; const nextName = window.prompt("请输入新预设名称", suggestedName); if (nextName == null) return; const trimmedName = String(nextName).trim(); if (!trimmedName) { toastr.info("预设名称不能为空", "ST-BME"); return; } const nextProfile = cloneTaskProfile(selectedProfile, { taskType: currentTaskProfileTaskType, name: trimmedName, }); currentTaskProfileBlockId = nextProfile.blocks?.[0]?.id || ""; currentTaskProfileRuleId = nextProfile.regex?.localRules?.[0]?.id || ""; const nextTaskProfiles = upsertTaskProfile( state.taskProfiles, currentTaskProfileTaskType, nextProfile, { setActive: true }, ); _patchTaskProfiles(nextTaskProfiles); toastr.success("已另存为新预设", "ST-BME"); return; } case "export-profile": _downloadTaskProfile(state.taskProfiles, currentTaskProfileTaskType, selectedProfile); return; case "import-profile": document.getElementById("bme-task-profile-import")?.click(); return; case "restore-default-profile": { const confirmed = window.confirm( "这会重建当前任务的默认预设,并切换到默认预设。是否继续?", ); if (!confirmed) return; const nextTaskProfiles = restoreDefaultTaskProfile( state.taskProfiles, currentTaskProfileTaskType, ); const legacyField = getLegacyPromptFieldForTask(currentTaskProfileTaskType); currentTaskProfileBlockId = ""; currentTaskProfileRuleId = ""; _patchTaskProfiles( nextTaskProfiles, legacyField ? { [legacyField]: "" } : {}, ); toastr.success("默认预设已恢复", "ST-BME"); return; } case "add-regex-rule": _updateCurrentTaskProfile((draft, context) => { const localRules = Array.isArray(draft.regex?.localRules) ? draft.regex.localRules : []; const nextRule = createLocalRegexRule(context.taskType, { script_name: `本地规则 ${localRules.length + 1}`, }); draft.regex = { ...(draft.regex || {}), localRules: [...localRules, nextRule], }; return { selectRuleId: nextRule.id }; }); return; case "delete-regex-rule": _deleteRegexRule(actionEl.dataset.ruleId); return; default: return; } } function _renderTaskProfileWorkspace(state) { if (!state.profile) { return `
    任务预设不可用
    当前没有可编辑的任务预设数据。
    `; } const taskMeta = state.taskTypeOptions.find((item) => item.id === state.taskType) || state.taskTypeOptions[0]; const profileUpdatedAt = _formatTaskProfileTime(state.profile.updatedAt); return `
    ${state.taskTypeOptions .map( (item) => ` `, ) .join("")}
    ${_escHtml(taskMeta?.label || state.taskType)} 任务预设
    ${_escHtml(taskMeta?.description || "")}
    ${state.profile.builtin ? "内置" : "自定义"} 更新于 ${_escHtml(profileUpdatedAt)}
    ${TASK_PROFILE_TABS.map( (tab) => ` `, ).join("")}
    ${ state.taskTabId === "generation" ? _renderTaskGenerationTab(state) : state.taskTabId === "regex" ? _renderTaskRegexTab(state) : _renderTaskPromptTab(state) }
    `; } function _renderTaskPromptTab(state) { return `
    Prompt 块列表
    通过顺序、启停与角色控制最终请求的编排方式。
    ${state.blocks.length} 个块
    ${state.blocks.length ? state.blocks .map((block, index) => _renderTaskBlockListItem(block, index, state)) .join("") : `
    当前预设还没有块。可以先新增一个自定义块或内置块。
    `}
    ${_renderTaskBlockEditor(state)}
    `; } function _renderTaskGenerationTab(state) { return `
    ${TASK_PROFILE_GENERATION_GROUPS.map( (group) => `
    ${_escHtml(group.title)}
    留空表示不强制下发,由模型或 provider 默认值决定。
    ${group.fields .map((field) => _renderGenerationField(field, state.profile.generation?.[field.key]), ) .join("")}
    `, ).join("")}
    运行时说明 — 这里配置的是完整版 generation options。实际请求发送前,仍会根据模型能力做过滤,避免把不支持的字段直接下发给 provider。
    `; } function _renderTaskRegexTab(state) { const regex = state.profile.regex || {}; return `
    复用与阶段
    任务预设可复用酒馆正则,并叠加当前任务自己的附加规则。
    ${[ ["global", "全局"], ["preset", "当前预设"], ["character", "角色卡"], ] .map( ([key, label]) => ` `, ) .join("")}
    ${TASK_PROFILE_REGEX_STAGES.map( (stage) => ` `, ).join("")}
    本地附加规则
    本地规则只作用于当前任务预设,不会污染宿主酒馆配置。
    ${state.regexRules.length ? state.regexRules .map((rule, index) => _renderRegexRuleListItem(rule, index, state)) .join("") : `
    当前预设还没有本地正则规则。
    `}
    ${_renderRegexRuleEditor(state)}
    `; } function _renderTaskBlockListItem(block, index, state) { const isSelected = block.id === state.selectedBlock?.id; return `
    `; } function _renderTaskBlockEditor(state) { const block = state.selectedBlock; if (!block) { return `
    块详情
    从左侧列表选择一个块进行编辑。
    `; } const builtinOptions = state.builtinBlockDefinitions .map( (item) => ` `, ) .join(""); const legacyField = getLegacyPromptFieldForTask(state.taskType); const legacyValue = legacyField && block.type === "legacyPrompt" ? state.settings?.[legacyField] || block.content || DEFAULT_PROMPTS[state.taskType] || "" : block.content || ""; return `
    块详情
    当前块会直接写回到任务预设中。
    ${_escHtml(_getTaskBlockTypeLabel(block.type))} ${block.type === "builtin" ? _helpTip( (state.builtinBlockDefinitions.find((d) => d.sourceKey === block.sourceKey) || {}).description || "" ) : ""}
    ${ block.type === "builtin" ? (() => { const externalSourceMap = { charDescription: "角色卡描述", userPersona: "用户 Persona 设定", worldInfoBefore: "World Info (↑ Char)", worldInfoAfter: "World Info (↓ Char)", }; const externalLabel = externalSourceMap[block.sourceKey]; return `
    ${externalLabel ? `
    此提示词的内容是从其他地方提取的,无法在此处进行编辑。
    来源:${externalLabel}
    ` : `
    ` }`; })() : block.type === "legacyPrompt" ? `
    当前块与旧版 prompt 字段保持兼容。留空时运行时会回退到内置默认 prompt。
    ` : `
    ` } `; } function _renderGenerationField(field, value) { const effectiveValue = (value != null && value !== "") ? value : field.defaultValue; if (field.type === "tri_bool") { const currentValue = effectiveValue === true ? "true" : effectiveValue === false ? "false" : ""; return `
    `; } if (field.type === "enum") { return `
    `; } if (field.type === "range") { const numValue = effectiveValue != null && effectiveValue !== "" ? Number(effectiveValue) : ""; const displayValue = numValue !== "" ? numValue : field.min ?? 0; return `
    `; } return `
    `; } function _renderRegexRuleListItem(rule, index, state) { const isSelected = rule.id === state.selectedRule?.id; return `
    `; } function _renderRegexRuleEditor(state) { const rule = state.selectedRule; if (!rule) { return `
    规则详情
    从左侧规则列表选择一条规则进行编辑。
    `; } const trimStrings = Array.isArray(rule.trim_strings) ? rule.trim_strings.join("\n") : String(rule.trim_strings || ""); return `
    规则详情
    字段尽量与 Tavern 正则结构保持对齐,方便后续导入导出与对照。
    ${rule.enabled ? "启用中" : "已停用"}
    数据来源
    作用目标
    `; } function _moveTaskBlock(blockId, direction) { if (!blockId || !Number.isFinite(direction) || direction === 0) return; _updateCurrentTaskProfile((draft) => { const blocks = _sortTaskBlocks(draft.blocks); const index = blocks.findIndex((item) => item.id === blockId); const targetIndex = index + direction; if (index < 0 || targetIndex < 0 || targetIndex >= blocks.length) { return null; } [blocks[index], blocks[targetIndex]] = [blocks[targetIndex], blocks[index]]; // 直接重新编号,不要再 sort(否则会按旧 order 排回去) draft.blocks = blocks.map((block, i) => ({ ...block, order: i })); return { selectBlockId: blockId }; }); } function _deleteTaskBlock(blockId) { if (!blockId) return; _updateCurrentTaskProfile((draft) => { const blocks = _sortTaskBlocks(draft.blocks); const index = blocks.findIndex((item) => item.id === blockId); if (index < 0) return null; const block = blocks[index]; blocks.splice(index, 1); draft.blocks = _normalizeTaskBlocks(blocks); return { selectBlockId: blocks[Math.max(0, index - 1)]?.id || blocks[0]?.id || "", }; }); } function _deleteRegexRule(ruleId) { if (!ruleId) return; _updateCurrentTaskProfile((draft) => { const localRules = Array.isArray(draft.regex?.localRules) ? [...draft.regex.localRules] : []; const index = localRules.findIndex((item) => item.id === ruleId); if (index < 0) return null; localRules.splice(index, 1); draft.regex = { ...(draft.regex || {}), localRules, }; return { selectRuleId: localRules[Math.max(0, index - 1)]?.id || localRules[0]?.id || "", }; }); } function _persistSelectedBlockField(target, refresh) { const field = target.dataset.blockField; if (!field) return; _updateCurrentTaskProfile( (draft, context) => { const blocks = _sortTaskBlocks(draft.blocks); const block = blocks.find((item) => item.id === currentTaskProfileBlockId); if (!block) return null; const rawValue = target instanceof HTMLInputElement && target.type === "checkbox" ? Boolean(target.checked) : target.value; let extraSettingsPatch = {}; if (field === "enabled") { block.enabled = Boolean(rawValue); } else if (field === "content" && block.type === "legacyPrompt") { block.content = String(rawValue || ""); const legacyField = getLegacyPromptFieldForTask(context.taskType); if (legacyField) { extraSettingsPatch[legacyField] = block.content; } } else { block[field] = String(rawValue || ""); } draft.blocks = _normalizeTaskBlocks(blocks); return { extraSettingsPatch, selectBlockId: block.id, }; }, { refresh }, ); } function _persistGenerationField(target, refresh) { const key = target.dataset.generationKey; const valueType = target.dataset.valueType || "text"; if (!key) return; _updateCurrentTaskProfile( (draft) => { draft.generation = { ...(draft.generation || {}), [key]: _parseTaskWorkspaceValue(target, valueType), }; }, { refresh }, ); } function _persistRegexConfigField(target, refresh) { const key = target.dataset.regexField; if (!key) return; _updateCurrentTaskProfile( (draft) => { draft.regex = { ...(draft.regex || {}), [key]: target instanceof HTMLInputElement && target.type === "checkbox" ? Boolean(target.checked) : target.value, }; }, { refresh }, ); } function _persistRegexSourceField(target, refresh) { const sourceKey = target.dataset.regexSource; if (!sourceKey) return; _updateCurrentTaskProfile( (draft) => { draft.regex = { ...(draft.regex || {}), sources: { ...(draft.regex?.sources || {}), [sourceKey]: Boolean(target.checked), }, }; }, { refresh }, ); } function _persistRegexStageField(target, refresh) { const stageKey = target.dataset.regexStage; if (!stageKey) return; _updateCurrentTaskProfile( (draft) => { draft.regex = { ...(draft.regex || {}), stages: { ...(draft.regex?.stages || {}), [stageKey]: Boolean(target.checked), }, }; }, { refresh }, ); } function _persistSelectedRegexRuleField(target, refresh) { _updateCurrentTaskProfile( (draft) => { const localRules = Array.isArray(draft.regex?.localRules) ? [...draft.regex.localRules] : []; const rule = localRules.find((item) => item.id === currentTaskProfileRuleId); if (!rule) return null; if (target.dataset.regexRuleField) { const field = target.dataset.regexRuleField; if (target instanceof HTMLInputElement && target.type === "checkbox") { rule[field] = Boolean(target.checked); } else if (["min_depth", "max_depth"].includes(field)) { const parsed = Number.parseInt(String(target.value || "").trim(), 10); rule[field] = Number.isFinite(parsed) ? parsed : 0; } else if (field === "trim_strings") { rule[field] = String(target.value || ""); } else { rule[field] = String(target.value || ""); } } if (target.dataset.regexRuleSource) { const sourceKey = target.dataset.regexRuleSource; rule.source = { ...(rule.source || {}), [sourceKey]: Boolean(target.checked), }; } if (target.dataset.regexRuleDestination) { const destinationKey = target.dataset.regexRuleDestination; rule.destination = { ...(rule.destination || {}), [destinationKey]: Boolean(target.checked), }; } draft.regex = { ...(draft.regex || {}), localRules, }; return { selectRuleId: rule.id }; }, { refresh }, ); } function _updateCurrentTaskProfile(mutator, options = {}) { const settings = _getSettings?.() || {}; const taskProfiles = ensureTaskProfiles(settings); const taskType = currentTaskProfileTaskType; const bucket = taskProfiles[taskType]; const activeProfile = bucket?.profiles?.find((item) => item.id === bucket.activeProfileId) || bucket?.profiles?.[0]; if (!activeProfile) return null; const draft = _normalizeTaskProfileDraft(_cloneJson(activeProfile)); const mutationResult = mutator?.(draft, { settings, taskProfiles, taskType, bucket, activeProfile, }); if (mutationResult === null) return null; const result = mutationResult || {}; const nextProfile = _normalizeTaskProfileDraft(result.profile || draft); const nextTaskProfiles = upsertTaskProfile(taskProfiles, taskType, nextProfile, { setActive: true, }); if (Object.prototype.hasOwnProperty.call(result, "selectBlockId")) { currentTaskProfileBlockId = result.selectBlockId || ""; } if (Object.prototype.hasOwnProperty.call(result, "selectRuleId")) { currentTaskProfileRuleId = result.selectRuleId || ""; } return _patchTaskProfiles( nextTaskProfiles, result.extraSettingsPatch || {}, { refresh: result.refresh === undefined ? options.refresh !== false : result.refresh, }, ); } function _normalizeTaskProfileDraft(profile = {}) { const draft = profile || {}; draft.blocks = _normalizeTaskBlocks(draft.blocks); draft.regex = { enabled: true, inheritStRegex: true, sources: { global: true, preset: true, character: true, }, stages: { input: true, output: true, }, localRules: [], ...(draft.regex || {}), sources: { global: true, preset: true, character: true, ...(draft.regex?.sources || {}), }, stages: { input: true, output: true, ...(draft.regex?.stages || {}), }, localRules: Array.isArray(draft.regex?.localRules) ? draft.regex.localRules.map((rule) => ({ ...rule, source: { user_input: true, ai_output: true, ...(rule?.source || {}), }, destination: { prompt: true, display: false, ...(rule?.destination || {}), }, })) : [], }; return draft; } function _normalizeTaskBlocks(blocks = []) { return _sortTaskBlocks(blocks).map((block, index) => ({ ...block, order: index, })); } function _sortTaskBlocks(blocks = []) { return [...(Array.isArray(blocks) ? blocks : [])].sort((a, b) => { const orderA = Number.isFinite(Number(a?.order)) ? Number(a.order) : 0; const orderB = Number.isFinite(Number(b?.order)) ? Number(b.order) : 0; return orderA - orderB; }); } function _parseTaskWorkspaceValue(target, valueType = "text") { if (valueType === "tri_bool") { if (target.value === "true") return true; if (target.value === "false") return false; return null; } if (valueType === "number") { const raw = String(target.value || "").trim(); if (!raw) return null; const parsed = Number(raw); return Number.isFinite(parsed) ? parsed : null; } return String(target.value || "").trim(); } function _downloadTaskProfile(taskProfiles, taskType, profile) { try { const payload = serializeTaskProfile(taskProfiles, taskType, profile?.id || ""); const fileName = _sanitizeFileName( `st-bme-${taskType}-${profile?.name || "profile"}.json`, ); const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(url); toastr.success("预设导出成功", "ST-BME"); } catch (error) { console.error("[ST-BME] 导出任务预设失败:", error); toastr.error(`预设导出失败: ${error?.message || error}`, "ST-BME"); } } function _sanitizeFileName(fileName = "profile.json") { return String(fileName || "profile.json").replace(/[<>:"/\\|?*\x00-\x1f]/g, "-"); } function _cloneJson(value) { return JSON.parse(JSON.stringify(value ?? null)); } function _helpTip(text) { if (!text) return ""; return `${_escHtml(text)}`; } function _getTaskBlockTypeLabel(type) { const typeMap = { custom: "自定义块", builtin: "内置块", legacyPrompt: "兼容块", }; return typeMap[type] || type || "块"; } function _formatTaskProfileTime(raw) { if (!raw) return "刚刚"; try { const date = new Date(raw); if (Number.isNaN(date.getTime())) return "刚刚"; return date.toLocaleString("zh-CN", { hour12: false, month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); } catch { return "刚刚"; } } // ==================== 工具函数 ==================== function _setText(id, text) { const el = document.getElementById(id); if (el) el.textContent = String(text); } function _refreshRuntimeStatus() { const runtimeStatus = _getRuntimeStatus?.() || {}; const text = runtimeStatus.text || "待命"; const meta = runtimeStatus.meta || "准备就绪"; _setText("bme-status-text", text); _setText("bme-status-meta", meta); _setText("bme-panel-status", text); } function _patchSettings(patch = {}, options = {}) { const settings = _updateSettings?.(patch) || _getSettings?.() || {}; if (options.refreshGuards) _refreshGuardedConfigStates(settings); if (options.refreshPrompts) _refreshPromptCardStates(settings); if (options.refreshTaskWorkspace) _refreshTaskProfileWorkspace(settings); if (options.refreshTheme) _highlightThemeChoice(settings.panelTheme || "crimson"); return settings; } function _highlightThemeChoice(themeName) { if (!panelEl) return; panelEl.querySelectorAll(".bme-theme-option").forEach((opt) => { opt.classList.toggle("active", opt.dataset.theme === themeName); }); panelEl.querySelectorAll(".bme-theme-card").forEach((card) => { card.classList.toggle("active", card.dataset.theme === themeName); }); } function _refreshGuardedConfigStates(settings = _getSettings?.() || {}) { if (!panelEl) return; panelEl.querySelectorAll(".bme-guarded-card").forEach((card) => { const guardKeys = String(card.dataset.guardSettings || "") .split(",") .map((key) => key.trim()) .filter(Boolean); const enabled = guardKeys.every((key) => Boolean(settings[key])); card.classList.toggle("is-disabled", !enabled); const note = card.querySelector(".bme-config-guard-note"); note?.classList.toggle("visible", !enabled); card .querySelectorAll("input, select, textarea, button") .forEach((element) => { element.disabled = !enabled; }); }); } function _refreshStageCardStates(settings = _getSettings?.() || {}) { if (!panelEl) return; panelEl.querySelectorAll(".bme-stage-card").forEach((card) => { const toggleId = card.dataset.stageToggleId; const toggle = toggleId ? document.getElementById(toggleId) : null; const cardDisabled = card.classList.contains("is-disabled"); const stageEnabled = toggleId === "bme-setting-recall-llm" ? (settings.recallEnableLLM ?? true) : toggle ? Boolean(toggle.checked) : true; card.classList.toggle("stage-disabled", !cardDisabled && !stageEnabled); card.querySelectorAll(".bme-stage-param").forEach((section) => { section .querySelectorAll("input, select, textarea, button") .forEach((element) => { element.disabled = cardDisabled || !stageEnabled; }); }); }); } function _refreshFetchedModelSelects(settings = _getSettings?.() || {}) { _renderFetchedModelOptions( "bme-select-llm-model", fetchedMemoryLLMModels, settings.llmModel || "", ); _renderFetchedModelOptions( "bme-select-embed-backend-model", fetchedBackendEmbeddingModels, settings.embeddingBackendModel || "", ); _renderFetchedModelOptions( "bme-select-embed-direct-model", fetchedDirectEmbeddingModels, settings.embeddingModel || "", ); } function _renderFetchedModelOptions(selectId, models, currentValue = "") { const select = document.getElementById(selectId); if (!select) return; const normalized = Array.isArray(models) ? models : []; select.innerHTML = ""; const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = normalized.length ? "从拉取结果中选择模型" : "暂无已拉取模型"; select.appendChild(placeholder); normalized.forEach((model) => { const option = document.createElement("option"); option.value = String(model?.id || ""); option.textContent = String(model?.label || model?.id || ""); select.appendChild(option); }); if ( currentValue && normalized.some((model) => String(model?.id || "") === String(currentValue)) ) { select.value = String(currentValue); } else { select.value = ""; } select.style.display = normalized.length > 0 ? "" : "none"; } function _refreshPromptCardStates(settings = _getSettings?.() || {}) { if (!panelEl) return; panelEl.querySelectorAll(".bme-prompt-card").forEach((card) => { const settingKey = card.dataset.settingKey; const statusEl = card.querySelector(".bme-prompt-status"); const resetButton = card.querySelector(".bme-prompt-reset"); const isCustom = Boolean(String(settings?.[settingKey] || "").trim()); card.classList.toggle("is-custom", isCustom); if (statusEl) { statusEl.textContent = isCustom ? "已自定义" : "默认"; statusEl.classList.toggle("is-custom", isCustom); } if (resetButton) { resetButton.disabled = !isCustom; } }); } function _toggleEmbedFields(mode) { const backendEl = document.getElementById("bme-embed-backend-fields"); const directEl = document.getElementById("bme-embed-direct-fields"); if (backendEl) backendEl.style.display = mode === "backend" ? "" : "none"; if (directEl) directEl.style.display = mode === "direct" ? "" : "none"; } function _setInputValue(id, value) { const el = document.getElementById(id); if (el && el.value !== String(value ?? "")) { el.value = String(value ?? ""); } } function _setCheckboxValue(id, checked) { const el = document.getElementById(id); if (el) { el.checked = Boolean(checked); } } function _parseOptionalInt(value) { const parsed = Number.parseInt(String(value ?? "").trim(), 10); return Number.isFinite(parsed) ? parsed : null; } function _escHtml(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: "反思", }; return map[type] || type || "—"; } function _getNodeSnippet(node) { 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 "无补充字段"; } function _isMobile() { return window.innerWidth <= 768; }