diff --git a/README.md b/README.md index 5b4bfcc..f98e0a0 100644 --- a/README.md +++ b/README.md @@ -478,6 +478,7 @@ ST-BME/ │ ├── graph-renderer.js # Canvas 力导向图谱渲染器 │ ├── graph-renderer-utils.js # 渲染工具函数 │ ├── panel-graph-refresh-utils.js # 面板图谱刷新工具 +│ ├── panel-ena-sections.js # ENA Planner 原生配置区绑定 │ ├── recall-message-ui.js # 消息级召回卡片 UI(子图渲染 + 侧边栏编辑) │ ├── hide-engine.js # 旧消息隐藏引擎(使用酒馆原生 /hide /unhide) │ ├── notice.js # 通知系统 @@ -487,8 +488,7 @@ ST-BME/ │ ├── ena-planner.js # Planner 主逻辑 │ ├── ena-planner-storage.js # Planner 存储 │ ├── ena-planner-presets.js # Planner 预设 -│ ├── ena-planner.html # Planner UI -│ └── ena-planner.css # Planner 样式 +│ └── (UI 已并入主面板配置页) │ ├── vendor/ # 第三方依赖 │ └── js-yaml.mjs # YAML 解析器 diff --git a/ena-planner/ena-planner.css b/ena-planner/ena-planner.css deleted file mode 100644 index 52dec9c..0000000 --- a/ena-planner/ena-planner.css +++ /dev/null @@ -1,888 +0,0 @@ -/* ═══════════════════════════════════════════════════════════════════════════ - Ena Planner — Settings UI - ═══════════════════════════════════════════════════════════════════════════ */ - -*, -*::before, -*::after { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -:root { - --bg: #121212; - --bg2: #1e1e1e; - --bg3: #2a2a2a; - --txt: #e0e0e0; - --txt2: #b0b0b0; - --txt3: #808080; - --bdr: #3a3a3a; - --bdr2: #333; - --acc: #e0e0e0; - --hl: #e8928a; - --hl2: #d87a7a; - --hl-soft: rgba(232, 146, 138, .1); - --inv: #1e1e1e; - --success: #4caf50; - --warn: #ffb74d; - --error: #ef5350; - --code-bg: #0d0d0d; - --code-txt: #d4d4d4; - --radius: 4px; -} - -html, -body { - height: auto; - overflow-y: auto; - -webkit-overflow-scrolling: touch; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif; - background: var(--bg); - color: var(--txt); - font-size: 14px; - line-height: 1.6; - min-height: 100vh; -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Layout - ═══════════════════════════════════════════════════════════════════════════ */ - -.container { - display: flex; - flex-direction: column; - min-height: 100vh; - padding: 24px 40px; - max-width: 860px; - margin: 0 auto; -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Header - ═══════════════════════════════════════════════════════════════════════════ */ - -header { - display: flex; - justify-content: space-between; - align-items: flex-start; - padding-bottom: 24px; - border-bottom: 1px solid var(--bdr); - margin-bottom: 24px; -} - -.header-left h1 { - font-size: 2rem; - font-weight: 300; - letter-spacing: -.02em; - margin-bottom: 4px; - color: var(--txt); -} - -.header-left h1 span { - font-weight: 600; -} - -.subtitle { - font-size: .75rem; - color: var(--txt3); - letter-spacing: .08em; - text-transform: uppercase; -} - -.stats { - display: flex; - gap: 40px; - align-items: center; - text-align: right; -} - -.stat-val { - font-size: 1.125rem; - font-weight: 500; - line-height: 1.2; - color: var(--txt); -} - -.stat-val .hl { - color: var(--hl); -} - -.stat-lbl { - font-size: .6875rem; - color: var(--txt3); - text-transform: uppercase; - letter-spacing: .1em; - margin-top: 4px; -} - -.modal-close { - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - background: transparent; - border: 1px solid var(--bdr); - border-radius: var(--radius); - cursor: pointer; - transition: border-color .2s; - margin-left: 16px; -} - -.modal-close:hover { - border-color: var(--txt2); -} - -.modal-close svg { - width: 16px; - height: 16px; - color: var(--txt2); -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Nav Tabs (desktop) - ═══════════════════════════════════════════ */ - -.nav-tabs { - display: flex; - gap: 24px; - border-bottom: 1px solid var(--bdr); - margin-bottom: 24px; -} - -.nav-item { - font-size: .8125rem; - font-weight: 500; - color: var(--txt3); - text-transform: uppercase; - letter-spacing: .08em; - padding-bottom: 12px; - border-bottom: 2px solid transparent; - margin-bottom: -1px; - cursor: pointer; - transition: color .2s, border-color .2s; - user-select: none; -} - -.nav-item:hover { - color: var(--txt2); -} - -.nav-item.active { - color: var(--hl); - border-bottom-color: var(--hl); -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Mobile Nav (bottom) - ═══════════════════════════════════════════════════════════════════════════ */ - -.mobile-nav { - display: none; - position: fixed; - bottom: 0; - left: 0; - right: 0; - height: 56px; - background: var(--bg2); - border-top: 1px solid var(--bdr); - z-index: 100; -} - -.mobile-nav-inner { - display: flex; - height: 100%; -} - -.mobile-nav-item { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 2px; - color: var(--txt3); - font-size: .625rem; - text-transform: uppercase; - letter-spacing: .05em; - cursor: pointer; - user-select: none; - transition: color .2s; -} - -.mobile-nav-item span { - line-height: 1; -} - -.mobile-nav-item .nav-dot { - width: 4px; - height: 4px; - border-radius: 50%; - background: transparent; - transition: background .2s; - margin-bottom: 2px; -} - -.mobile-nav-item.active { - color: var(--hl); -} - -.mobile-nav-item.active .nav-dot { - background: var(--hl); -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Views - ═══════════════════════════════════════════════════════════════════════════ */ - -.view { - display: none; -} - -.view.active { - display: block; - animation: fadeIn .25s ease; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(4px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Cards - ═══════════════════════════════════════════════════════════════════════════ */ - -.card { - background: var(--bg2); - border: 1px solid var(--bdr); - border-radius: var(--radius); - padding: 24px; - margin-bottom: 20px; -} - -.card-title { - font-size: .75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: .12em; - color: var(--txt2); - margin-bottom: 20px; - padding-bottom: 10px; - border-bottom: 1px dashed var(--bdr2); -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Forms - ═══════════════════════════════════════════════════════════════════════════ */ - -.form-row { - display: flex; - gap: 16px; - flex-wrap: wrap; -} - -.form-group { - display: flex; - flex-direction: column; - gap: 6px; - flex: 1; - min-width: 180px; - margin-bottom: 16px; -} - -.form-row .form-group { - margin-bottom: 0; -} - -.form-row+.form-row { - margin-top: 16px; -} - -.form-label { - font-size: .6875rem; - color: var(--txt3); - text-transform: uppercase; - letter-spacing: .06em; -} - -.form-hint { - font-size: .75rem; - color: var(--txt3); - line-height: 1.5; - margin-top: 4px; -} - -.input { - width: 100%; - padding: 9px 12px; - background: var(--bg3); - border: 1px solid var(--bdr); - border-radius: var(--radius); - font-size: .8125rem; - color: var(--txt); - font-family: inherit; - outline: none; - transition: border-color .2s; -} - -.input:focus { - border-color: var(--txt2); -} - -.input::placeholder { - color: var(--txt3); -} - -select.input { - appearance: none; - -webkit-appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' fill='none' stroke='%23808080' stroke-width='2'%3E%3Cpolyline points='2 3.5 5 6.5 8 3.5'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 10px center; - padding-right: 28px; - cursor: pointer; -} - -textarea.input { - min-height: 80px; - resize: vertical; -} - -.input-row { - display: flex; - gap: 8px; -} - -.input-row .input { - flex: 1; - min-width: 0; -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Buttons - ═══════════════════════════════════════════ */ - -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 9px 18px; - background: var(--bg2); - color: var(--txt); - border: 1px solid var(--bdr); - border-radius: var(--radius); - font-size: .8125rem; - font-weight: 500; - font-family: inherit; - cursor: pointer; - transition: border-color .2s, background .2s; - white-space: nowrap; -} - -.btn:hover { - border-color: var(--txt3); - background: var(--bg3); -} - -.btn:disabled { - opacity: .35; - cursor: not-allowed; -} - -.btn-p { - background: var(--acc); - color: var(--inv); - border-color: var(--acc); -} - -.btn-p:hover { - background: var(--txt2); - border-color: var(--txt2); -} - -.btn-del { - color: var(--hl); - border-color: rgba(232, 146, 138, .3); -} - -.btn-del:hover { - background: var(--hl-soft); - border-color: var(--hl); -} - -.btn-sm { - padding: 5px 12px; - font-size: .75rem; -} - -.btn-group { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Tip Box - ═══════════════════════════════════════════════════════════════════════════ */ - -.tip-box { - display: flex; - gap: 12px; - align-items: flex-start; - padding: 14px 16px; - background: var(--hl-soft); - border: 1px solid var(--bdr); - border-left: 3px solid var(--hl); - border-radius: var(--radius); - margin-bottom: 20px; -} - -.tip-icon { - flex-shrink: 0; - font-size: .875rem; - line-height: 1.6; -} - -.tip-text { - font-size: .8125rem; - color: var(--txt2); - line-height: 1.6; -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Prompt Blocks - ═══════════════════════════════════════════════════════════════════════════ */ - -.prompt-block { - background: var(--bg3); - border: 1px solid var(--bdr); - border-radius: var(--radius); - padding: 16px; - margin-bottom: 10px; -} - -.prompt-head { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 10px; - margin-bottom: 10px; - flex-wrap: wrap; -} - -.prompt-head-left { - display: flex; - gap: 8px; - flex: 1; - min-width: 200px; -} - -.prompt-head-right { - display: flex; - gap: 6px; -} - -.prompt-block textarea.input { - min-height: 120px; - font-family: 'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace; - font-size: .75rem; - line-height: 1.5; -} - -.prompt-empty { - text-align: center; - padding: 36px 20px; - color: var(--txt3); - font-size: .8125rem; - border: 1px dashed var(--bdr); - border-radius: var(--radius); -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Undo Bar - ═══════════════════════════════════════════════════════════════════════════ */ - -.undo-bar { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 14px; - margin-top: 12px; - background: var(--hl-soft); - border: 1px solid var(--bdr); - border-radius: var(--radius); - font-size: .8125rem; - color: var(--txt2); -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Status Text - ═══════════════════════════════════════════ */ - -.status-text { - font-size: .75rem; - color: var(--txt3); - margin-top: 10px; - min-height: 1em; -} - -.status-text.success { - color: var(--success); -} - -.status-text.error { - color: var(--error); -} - -.status-text.loading { - color: var(--warn); -} - -/* ═══════════════════════════════════════════ - Logs - ═══════════════════════════════════════════════════════════════════════════ */ - -.log-list { - max-height: 60vh; - overflow-y: auto; - border: 1px solid var(--bdr); - border-radius: var(--radius); - background: var(--bg3); -} - -.log-item { - padding: 14px 16px; - border-bottom: 1px solid var(--bdr2); -} - -.log-item:last-child { - border-bottom: none; -} - -.log-meta { - display: flex; - justify-content: space-between; - font-size: .6875rem; - color: var(--txt3); - text-transform: uppercase; - letter-spacing: .04em; - margin-bottom: 8px; -} - -.log-meta .success { - color: var(--success); -} - -.log-meta .error { - color: var(--error); -} - -.log-error { - color: var(--error); - font-size: .8125rem; - margin-bottom: 8px; - white-space: pre-wrap; -} - -.log-pre { - background: var(--code-bg); - color: var(--code-txt); - padding: 12px; - border-radius: var(--radius); - font-family: 'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace; - font-size: .6875rem; - line-height: 1.5; - white-space: pre-wrap; - word-break: break-word; - max-height: 280px; - overflow-y: auto; - margin-top: 6px; -} - -.log-empty { - text-align: center; - padding: 36px 20px; - color: var(--txt3); - font-size: .8125rem; -} - -/* Message cards inside log */ -.msg-list { - display: flex; - flex-direction: column; - gap: 8px; - margin-top: 8px; -} - -.msg-card { - border-radius: var(--radius); - border-left: 3px solid var(--bdr); - background: var(--code-bg); - padding: 8px 12px; -} - -.msg-card.msg-system { border-left-color: #6b8afd; } -.msg-card.msg-user { border-left-color: #4ecdc4; } -.msg-card.msg-assistant { border-left-color: #f7a046; } - -.msg-role { - font-size: .6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: .04em; - margin-bottom: 4px; - color: var(--txt3); -} - -.msg-system .msg-role { color: #6b8afd; } -.msg-user .msg-role { color: #4ecdc4; } -.msg-assistant .msg-role { color: #f7a046; } - -.msg-content { - font-family: 'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace; - font-size: .6875rem; - line-height: 1.6; - white-space: pre-wrap; - word-break: break-word; - color: var(--code-txt); - margin: 0; - max-height: 300px; - overflow-y: auto; -} - -details { - margin-bottom: 6px; -} - -details:last-child { - margin-bottom: 0; -} - -details summary { - cursor: pointer; - font-size: .75rem; - font-weight: 500; - color: var(--txt3); - user-select: none; - padding: 4px 0; - transition: color .15s; -} - -details summary:hover { - color: var(--txt); -} - -/* ═══════════════════════════════════════════════════════════════════════════ - Debug Output - ═══════════════════════════════════════════════════════════════════════════ */ - -.debug-output { - background: var(--code-bg); - color: var(--code-txt); - padding: 14px; - border-radius: var(--radius); - font-family: 'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace; - font-size: .6875rem; - line-height: 1.6; - margin-top: 16px; - max-height: 400px; - overflow-y: auto; - white-space: pre-wrap; - word-break: break-word; - display: none; -} - -.debug-output.visible { - display: block; -} - -/* ═══════════════════════════════════════════ - Utilities - ═══════════════════════════════════════════════════════════════════════════ */ - -.hidden { - display: none !important; -} - -::-webkit-scrollbar { - width: 5px; - height: 5px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: var(--bdr); - border-radius: 3px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--txt3); -} - -/* ═══════════════════════════════════════════ - Responsive — Tablet - ═══════════════════════════════════════════ */ - -@media (max-width: 768px) { - .container { - padding: 16px; - } - - header { - flex-direction: column; - gap: 16px; - } - - .header-left h1 { - font-size: 1.5rem; - } - - .stats { - width: 100%; - justify-content: flex-start; - gap: 24px; - } - - .modal-close { - position: absolute; - top: 16px; - right: 16px; - margin-left: 0; - } - - .nav-tabs { - display: none; - } - - .mobile-nav { - display: block; - } - - .container { - padding-bottom: 72px; - } - - .form-row { - flex-direction: column; - gap: 0; - } - - .card { - padding: 16px; - } - - .prompt-head { - flex-direction: column; - } - - .prompt-head-left { - min-width: 0; - flex-direction: column; - } -} - -/* ═══════════════════════════════════════════ - Responsive — Small phone - ═══════════════════════════════════════════════════════════════════════════ */ - -@media (max-width: 480px) { - .container { - padding: 12px; - padding-bottom: 68px; - } - - header { - gap: 12px; - padding-bottom: 16px; - margin-bottom: 16px; - } - - .header-left h1 { - font-size: 1.25rem; - } - - .subtitle { - font-size: .625rem; - } - - .stats { - gap: 16px; - } - - .stat-val { - font-size: 1rem; - } - - .card { - padding: 14px; - margin-bottom: 14px; - } - - .btn-group { - flex-direction: column; - } - - .btn-group .btn { - width: 100%; - } - - .mobile-nav { - height: 52px; - } - - .mobile-nav-item { - font-size: .5625rem; - } -} - -/* ═══════════════════════════════════════════ - Touch devices — 44px minimum target - ═══════════════════════════════════════════════════════════════════════════ */ - -@media (hover: none) and (pointer: coarse) { - .btn { - min-height: 44px; - padding: 10px 18px; - } - - .btn-sm { - min-height: 40px; - } - - .input { - min-height: 44px; - padding: 10px 12px; - } - - .nav-item { - padding-bottom: 14px; - } - - .mobile-nav-item { - min-height: 44px; - } - - .modal-close { - width: 44px; - height: 44px; - } - - details summary { - padding: 8px 0; - } -} diff --git a/ena-planner/ena-planner.html b/ena-planner/ena-planner.html deleted file mode 100644 index e42e37a..0000000 --- a/ena-planner/ena-planner.html +++ /dev/null @@ -1,993 +0,0 @@ - - - - - - - - - Ena Planner - - - - -
- -
-
-

EnaPlanner

-
Story Planning · LLM Integration —— Created by Hao19911125
-
-
-
-
未启用
-
状态
-
-
-
就绪
-
保存
-
- -
-
- - - - -
- - -
-
-
-
- 工作流程:点击发送 → 拦截 → 收集上下文(角色卡、世界书、BME 记忆、历史 plot、最近 AI 回复)→ 发给规划 LLM → 提取 <plot> 和 - <note> → 追加到你的输入 → 放行发送 -
-
- -
-
基本设置
-
-
- - -
-
- - -
-
-

输入中已有 <plot> 标签时跳过自动规划。

-
- -
-
快速测试
-
- - -
-
- -
-
-
-
- - -
-
-
连接设置
-
-
- - -
-
- - -
-
-
- - -
- -
-
- -
- - -
-
-
- - -
-
- -
- - -
-
-
- -
-
生成参数
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
- - -
-
-
💡
-
- 系统会自动在提示词之后注入:角色卡、世界书、BME 结构化记忆、聊天历史和历史 plot 等上下文。你只需专注编写"规划指令"。 -
-
- -
-
模板管理
-
-
- -
-
-
- - - -
-
-
- -
- -
-
提示词块
-
- -
- - -
-
-
- - -
-
-
世界书
-
-
- - -
-
- - -
-
-
- - -
-
- -
-
聊天与历史
-
- - -

仅支持英文标签(如 plot, note, memory)。留空表示不按标签过滤(仅去除 think)。无效标签会自动忽略。

-
-
- - -
-
- - -
-
-
- - -
-
-
诊断工具
-
- - - -
-

-        
- -
-
日志
-
-
- - -
-
- - -
-
-
- - - -
-
-
暂无日志
-
-
-
- -
- - - - -
- - - - - diff --git a/ena-planner/ena-planner.js b/ena-planner/ena-planner.js index 981a947..016841c 100644 --- a/ena-planner/ena-planner.js +++ b/ena-planner/ena-planner.js @@ -6,10 +6,8 @@ import { debugLog } from '../runtime/debug-logging.js'; import jsyaml from '../vendor/js-yaml.mjs'; const EXT_NAME = 'ena-planner'; -const OVERLAY_ID = 'xiaobaix-ena-planner-overlay'; const VECTOR_RECALL_TIMEOUT_MS = 30000; const PLANNER_REQUEST_TIMEOUT_MS = 90000; -const _currentModuleUrl = import.meta.url; let _bmeRuntime = null; @@ -27,36 +25,6 @@ function getPlannerRequestTimeoutMs() { : PLANNER_REQUEST_TIMEOUT_MS; } -function getTrustedOrigin() { return window.location.origin; } - -function postToIframe(iframe, payload) { - if (!iframe?.contentWindow) return false; - iframe.contentWindow.postMessage(payload, getTrustedOrigin()); - return true; -} - -function isTrustedIframeEvent(event, iframe) { - return !!iframe && event.origin === getTrustedOrigin() - && event.source === iframe.contentWindow; -} - -function getPluginBasePath() { - try { - const url = new URL(_currentModuleUrl); - const parts = url.pathname.split('/'); - const idx = parts.lastIndexOf('ena-planner'); - if (idx > 0) { - return parts.slice(0, idx).join('/'); - } - } catch { } - return _bmeRuntime?.getExtensionPath?.() - || 'scripts/extensions/third-party/ST-Bionic-Memory-Ecology-main'; -} - -function getHtmlPath() { - return `${getPluginBasePath()}/ena-planner/ena-planner.html`; -} - /** * ------------------------- * Default settings @@ -128,12 +96,24 @@ const state = { }; let config = null; -let overlay = null; -let iframeMessageBound = false; let sendListenersInstalled = false; let sendClickHandler = null; let sendKeydownHandler = null; +/** + * Native UI subscribers (replaces the iframe postMessage channel). + * Callbacks receive `(kind, payload)` where kind is 'config' or 'logs'. + */ +const nativeSubscribers = new Set(); + +function notifyNativeChange(kind, payload) { + if (!nativeSubscribers.size) return; + for (const cb of nativeSubscribers) { + try { cb(kind, payload); } + catch (err) { console.warn('[Ena] native subscriber error:', err); } + } +} + /** * ------------------------- * Helpers @@ -228,9 +208,11 @@ function clampLogs() { function persistLogsMaybe() { const s = ensureSettings(); - if (!s.logsPersist) return; - state.logs = state.logs.slice(0, s.logsMax); - EnaPlannerStorage.set('logs', state.logs).catch(() => {}); + if (s.logsPersist) { + state.logs = state.logs.slice(0, s.logsMax); + EnaPlannerStorage.set('logs', state.logs).catch(() => {}); + } + try { notifyNativeChange('logs', getPlannerLogsSnapshot()); } catch {} } function loadPersistedLogsMaybe() { @@ -1137,6 +1119,101 @@ function debugCharForUi() { ].join('\n'); } +/** + * ------------------------- + * Native UI API (consumed by ui/panel-ena-sections.js) + * These replace the iframe postMessage channel with direct function calls. + * -------------------------- + */ +function getPlannerConfigSnapshot() { + return structuredClone(ensureSettings()); +} + +function getPlannerLogsSnapshot() { + return Array.isArray(state.logs) ? structuredClone(state.logs) : []; +} + +function subscribePlannerChanges(cb) { + if (typeof cb !== 'function') return () => {}; + nativeSubscribers.add(cb); + return () => nativeSubscribers.delete(cb); +} + +async function patchPlannerConfig(patch) { + if (!patch || typeof patch !== 'object') { + return { ok: false, error: '无效的补丁' }; + } + const s = ensureSettings(); + for (const key of Object.keys(patch)) { + if (patch[key] && typeof patch[key] === 'object' && !Array.isArray(patch[key])) { + s[key] = { ...(s[key] || {}), ...patch[key] }; + } else { + s[key] = patch[key]; + } + } + const ok = await saveConfigNow(); + if (ok) { + notifyNativeChange('config', getPlannerConfigSnapshot()); + return { ok: true, config: getPlannerConfigSnapshot() }; + } + return { ok: false, error: '保存失败' }; +} + +async function resetPlannerPromptToDefault() { + const s = ensureSettings(); + s.promptBlocks = getDefaultSettings().promptBlocks; + const ok = await saveConfigNow(); + if (ok) { + notifyNativeChange('config', getPlannerConfigSnapshot()); + return { ok: true, config: getPlannerConfigSnapshot() }; + } + return { ok: false, error: '重置失败' }; +} + +async function runPlannerTestFromUi(text) { + const fake = String(text || '').trim() || '(测试输入)我想让你帮我规划下一步剧情。'; + try { + await runPlanningOnce(fake, true); + notifyNativeChange('logs', getPlannerLogsSnapshot()); + return { ok: true }; + } catch (err) { + notifyNativeChange('logs', getPlannerLogsSnapshot()); + return { ok: false, error: String(err?.message ?? err) }; + } +} + +async function fetchPlannerModelsFromUi() { + try { + const models = await fetchModelsForUi(); + return { ok: true, models }; + } catch (err) { + return { ok: false, error: String(err?.message ?? err) }; + } +} + +async function debugPlannerWorldbookFromUi() { + try { + return { ok: true, output: await debugWorldbookForUi() }; + } catch (err) { + return { ok: false, output: String(err?.message ?? err) }; + } +} + +function debugPlannerCharFromUi() { + try { + return { ok: true, output: debugCharForUi() }; + } catch (err) { + return { ok: false, output: String(err?.message ?? err) }; + } +} + +async function clearPlannerLogs() { + state.logs = []; + const ok = await saveConfigNow(); + notifyNativeChange('logs', getPlannerLogsSnapshot()); + return { ok }; +} + /** * ------------------------- * Build planner messages @@ -1413,183 +1490,29 @@ function uninstallSendInterceptors() { sendListenersInstalled = false; } -function getIframeConfigPayload() { - const s = ensureSettings(); - return { - ...s, - logs: state.logs, - }; -} - -function openSettings() { - if (document.getElementById(OVERLAY_ID)) return; - - overlay = document.createElement('div'); - overlay.id = OVERLAY_ID; - overlay.style.cssText = ` - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: ${window.innerHeight}px; - background: rgba(0,0,0,0.5); - z-index: 99999; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - `; - - const iframe = document.createElement('iframe'); - iframe.src = getHtmlPath(); - iframe.style.cssText = ` - width: min(1200px, 96vw); - height: min(980px, 94vh); - max-height: calc(100% - 24px); - border: none; - border-radius: 12px; - background: #1a1a1a; - `; - - overlay.appendChild(iframe); - document.body.appendChild(overlay); - - if (!iframeMessageBound) { - // Guarded by isTrustedIframeEvent (origin + source). - // eslint-disable-next-line no-restricted-syntax - window.addEventListener('message', handleIframeMessage); - iframeMessageBound = true; - } -} - -function closeSettings() { - const overlayEl = document.getElementById(OVERLAY_ID); - if (overlayEl) overlayEl.remove(); - overlay = null; -} - -async function handleIframeMessage(ev) { - const iframe = overlay?.querySelector('iframe'); - if (!isTrustedIframeEvent(ev, iframe)) return; - if (!ev.data?.type?.startsWith('xb-ena:')) return; - - const { type, payload } = ev.data; - switch (type) { - case 'xb-ena:ready': - postToIframe(iframe, { type: 'xb-ena:config', payload: getIframeConfigPayload() }); - break; - case 'xb-ena:close': - closeSettings(); - break; - case 'xb-ena:save-config': { - const requestId = payload?.requestId || ''; - const patch = (payload && typeof payload.patch === 'object') ? payload.patch : payload; - Object.assign(ensureSettings(), patch || {}); - const ok = await saveConfigNow(); - if (ok) { - postToIframe(iframe, { - type: 'xb-ena:config-saved', - payload: { - ...getIframeConfigPayload(), - requestId - } - }); - } else { - postToIframe(iframe, { - type: 'xb-ena:config-save-error', - payload: { - message: '保存失败', - requestId - } - }); - } - break; - } - case 'xb-ena:reset-prompt-default': { - const requestId = payload?.requestId || ''; - const s = ensureSettings(); - s.promptBlocks = getDefaultSettings().promptBlocks; - const ok = await saveConfigNow(); - if (ok) { - postToIframe(iframe, { - type: 'xb-ena:config-saved', - payload: { - ...getIframeConfigPayload(), - requestId - } - }); - } else { - postToIframe(iframe, { - type: 'xb-ena:config-save-error', - payload: { - message: '重置失败', - requestId - } - }); - } - break; - } - case 'xb-ena:run-test': { - try { - const fake = payload?.text || '(测试输入)我想让你帮我规划下一步剧情。'; - await runPlanningOnce(fake, true); - postToIframe(iframe, { type: 'xb-ena:test-done' }); - postToIframe(iframe, { type: 'xb-ena:logs', payload: { logs: state.logs } }); - } catch (err) { - postToIframe(iframe, { type: 'xb-ena:test-error', payload: { message: String(err?.message ?? err) } }); - } - break; - } - case 'xb-ena:logs-request': - postToIframe(iframe, { type: 'xb-ena:logs', payload: { logs: state.logs } }); - break; - case 'xb-ena:logs-clear': - state.logs = []; - await saveConfigNow(); - postToIframe(iframe, { type: 'xb-ena:logs', payload: { logs: state.logs } }); - break; - case 'xb-ena:fetch-models': { - try { - const models = await fetchModelsForUi(); - postToIframe(iframe, { type: 'xb-ena:models', payload: { models } }); - } catch (err) { - postToIframe(iframe, { type: 'xb-ena:models-error', payload: { message: String(err?.message ?? err) } }); - } - break; - } - case 'xb-ena:debug-worldbook': { - try { - const output = await debugWorldbookForUi(); - postToIframe(iframe, { type: 'xb-ena:debug-output', payload: { output } }); - } catch (err) { - postToIframe(iframe, { type: 'xb-ena:debug-output', payload: { output: String(err?.message ?? err) } }); - } - break; - } - case 'xb-ena:debug-char': { - const output = debugCharForUi(); - postToIframe(iframe, { type: 'xb-ena:debug-output', payload: { output } }); - break; - } - } -} - export async function initEnaPlanner(bmeRuntime) { _bmeRuntime = bmeRuntime || null; await migrateFromLWBIfNeeded(); await loadConfig(); loadPersistedLogsMaybe(); installSendInterceptors(); - window.stBmeEnaPlanner = { openSettings, closeSettings }; + window.stBmeEnaPlanner = { + getConfig: getPlannerConfigSnapshot, + getLogs: getPlannerLogsSnapshot, + subscribe: subscribePlannerChanges, + patchConfig: patchPlannerConfig, + resetPromptToDefault: resetPlannerPromptToDefault, + runTest: runPlannerTestFromUi, + fetchModels: fetchPlannerModelsFromUi, + debugWorldbook: debugPlannerWorldbookFromUi, + debugChar: debugPlannerCharFromUi, + clearLogs: clearPlannerLogs, + }; } export function cleanupEnaPlanner() { uninstallSendInterceptors(); - closeSettings(); - if (iframeMessageBound) { - window.removeEventListener('message', handleIframeMessage); - iframeMessageBound = false; - } + nativeSubscribers.clear(); delete window.stBmeEnaPlanner; _bmeRuntime = null; } diff --git a/style.css b/style.css index d2915f9..70de74f 100644 --- a/style.css +++ b/style.css @@ -7475,3 +7475,368 @@ display: none; } } + +/* ═══════════════════════════════════════════════════════════ + ENA Planner (native panel section) + ═══════════════════════════════════════════════════════════ */ + +.bme-config-danger-btn { + color: var(--bme-error, #d47380); +} + +.bme-config-danger-btn:hover { + border-color: var(--bme-error, #d47380); + background: rgba(212, 115, 128, 0.14); + color: var(--bme-error, #d47380); +} + +.bme-planner-status-strip { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 10px; +} + +.bme-planner-status-chip { + display: inline-flex; + align-items: center; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.02em; + border: 1px solid var(--bme-border); + background: var(--bme-surface-container); + color: var(--bme-on-surface-dim); +} + +.bme-planner-status-chip[data-tone="active"] { + border-color: var(--bme-primary); + background: var(--bme-primary-dim); + color: var(--bme-primary); +} + +.bme-planner-status-chip[data-tone="success"] { + border-color: rgba(53, 179, 119, 0.4); + background: rgba(53, 179, 119, 0.14); + color: var(--bme-success, #35b377); +} + +.bme-planner-status-chip[data-tone="error"] { + border-color: rgba(212, 115, 128, 0.4); + background: rgba(212, 115, 128, 0.14); + color: var(--bme-error, #d47380); +} + +.bme-planner-status-chip[data-tone="loading"] { + border-color: rgba(234, 181, 67, 0.4); + background: rgba(234, 181, 67, 0.12); + color: var(--bme-warning, #eab543); +} + +.bme-planner-status-text { + font-size: 12px; + line-height: 1.5; + color: var(--bme-on-surface-dim); + margin-top: 8px; + min-height: 1em; +} + +.bme-planner-status-text[data-tone="loading"] { + color: var(--bme-warning, #eab543); +} + +.bme-planner-status-text[data-tone="success"] { + color: var(--bme-success, #35b377); +} + +.bme-planner-status-text[data-tone="error"] { + color: var(--bme-error, #d47380); +} + +.bme-planner-textarea { + resize: vertical; + min-height: 72px; + font-family: inherit; + line-height: 1.5; +} + +.bme-planner-inline-row { + display: flex; + gap: 8px; + align-items: stretch; +} + +.bme-planner-inline-row .bme-config-input { + flex: 1; +} + +.bme-planner-param-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 10px; +} + +.bme-planner-undo-bar { + margin-top: 10px; + padding: 10px 14px; + border-radius: 10px; + border: 1px solid var(--bme-warning, #eab543); + background: rgba(234, 181, 67, 0.1); + color: var(--bme-on-surface); + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + font-size: 12px; +} + +.bme-planner-undo-bar[hidden] { + display: none; +} + +.bme-planner-prompt-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.bme-planner-prompt-empty { + padding: 18px; + text-align: center; + font-size: 12px; + color: var(--bme-on-surface-dim); + border: 1px dashed var(--bme-border); + border-radius: 10px; +} + +.bme-planner-prompt-empty[hidden] { + display: none; +} + +.bme-planner-prompt-block { + border: 1px solid var(--bme-border); + border-radius: 10px; + padding: 10px 12px; + background: var(--bme-surface-container); + display: flex; + flex-direction: column; + gap: 10px; +} + +.bme-planner-prompt-head { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + justify-content: space-between; +} + +.bme-planner-prompt-head-left { + display: flex; + gap: 8px; + flex: 1; + min-width: 0; +} + +.bme-planner-prompt-head-left .bme-config-input { + flex: 1; + min-width: 100px; +} + +.bme-planner-prompt-head-left select.bme-config-input { + flex: 0 0 auto; + width: 110px; +} + +.bme-planner-prompt-head-right { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +.bme-planner-icon-btn { + width: 32px !important; + padding: 0 !important; + flex: 0 0 auto; +} + +.bme-planner-icon-btn i { + font-size: 12px; +} + +.bme-planner-debug-output { + margin-top: 12px; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid var(--bme-border); + background: var(--bme-surface-low); + font-family: "Cascadia Code", "Fira Code", monospace; + font-size: 11px; + line-height: 1.45; + white-space: pre-wrap; + word-break: break-word; + max-height: 240px; + overflow-y: auto; + color: var(--bme-on-surface); +} + +.bme-planner-debug-output[hidden] { + display: none; +} + +.bme-planner-log-list { + display: flex; + flex-direction: column; + gap: 10px; + max-height: 420px; + overflow-y: auto; + padding-right: 2px; +} + +.bme-planner-log-empty { + padding: 16px; + text-align: center; + font-size: 12px; + color: var(--bme-on-surface-dim); + border: 1px dashed var(--bme-border); + border-radius: 10px; +} + +.bme-planner-log-item { + border: 1px solid var(--bme-border); + border-radius: 10px; + padding: 10px 12px; + background: var(--bme-surface-container); + display: flex; + flex-direction: column; + gap: 8px; +} + +.bme-planner-log-meta { + display: flex; + justify-content: space-between; + gap: 8px; + font-size: 11px; + color: var(--bme-on-surface-dim); + flex-wrap: wrap; +} + +.bme-planner-log-meta .success { + color: var(--bme-success, #35b377); + font-weight: 600; +} + +.bme-planner-log-meta .error { + color: var(--bme-error, #d47380); + font-weight: 600; +} + +.bme-planner-log-error { + padding: 6px 10px; + border-radius: 8px; + background: rgba(212, 115, 128, 0.12); + color: var(--bme-error, #d47380); + font-size: 11px; + word-break: break-word; +} + +.bme-planner-log-item details > summary { + cursor: pointer; + font-size: 11px; + color: var(--bme-on-surface-dim); + padding: 4px 0; + list-style: none; +} + +.bme-planner-log-item details > summary::-webkit-details-marker { + display: none; +} + +.bme-planner-log-item details[open] > summary { + color: var(--bme-primary); +} + +.bme-planner-log-pre { + margin: 4px 0 0; + padding: 8px 10px; + border-radius: 8px; + background: var(--bme-surface-low); + color: var(--bme-on-surface); + font-family: "Cascadia Code", "Fira Code", monospace; + font-size: 11px; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + max-height: 200px; + overflow-y: auto; +} + +.bme-planner-msg-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.bme-planner-msg-card { + border-left: 3px solid var(--bme-border); + padding: 6px 10px; + background: var(--bme-surface-low); + border-radius: 0 8px 8px 0; +} + +.bme-planner-msg-card.msg-system { + border-left-color: var(--bme-primary); +} + +.bme-planner-msg-card.msg-user { + border-left-color: var(--bme-success, #35b377); +} + +.bme-planner-msg-card.msg-assistant { + border-left-color: var(--bme-warning, #eab543); +} + +.bme-planner-msg-role { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--bme-on-surface-dim); + margin-bottom: 4px; +} + +.bme-planner-msg-content { + margin: 0; + font-family: "Cascadia Code", "Fira Code", monospace; + font-size: 11px; + line-height: 1.5; + color: var(--bme-on-surface); + white-space: pre-wrap; + word-break: break-word; + max-height: 180px; + overflow-y: auto; +} + +@media (max-width: 768px) { + .bme-planner-prompt-head-left { + flex-wrap: wrap; + } + + .bme-planner-prompt-head-left select.bme-config-input { + width: 100%; + } + + .bme-planner-icon-btn { + width: 44px !important; + min-height: 44px; + } + + .bme-planner-inline-row { + flex-wrap: wrap; + } + + .bme-planner-param-grid { + grid-template-columns: 1fr 1fr; + } +} diff --git a/ui/panel-ena-sections.js b/ui/panel-ena-sections.js new file mode 100644 index 0000000..4fbc571 --- /dev/null +++ b/ui/panel-ena-sections.js @@ -0,0 +1,754 @@ +/** + * ENA Planner - native BME panel integration + * + * This module binds the planner config section inside `ui/panel.html` to the + * runtime API exposed by `ena-planner/ena-planner.js` (via `window.stBmeEnaPlanner`). + * + * Replaces the previous iframe + postMessage bridge with direct function calls, + * so the planner configuration lives inside the main panel's DOM and inherits + * BME theming automatically. + */ + +const SECTION_SELECTOR = '[data-config-section="planner"]'; +const AUTOSAVE_DELAY_MS = 600; + +let bound = false; +let unsubscribePlanner = null; +let autoSaveTimer = null; +let cfgCache = null; +let logsCache = []; +let fetchedModels = []; +let undoState = null; +let fieldChangeHandler = null; +let autosaveInProgress = false; + +/* ── DOM helpers ────────────────────────────────────────────────────────── */ + +function $(id) { return document.getElementById(id); } + +function getPlannerApi() { + return globalThis?.stBmeEnaPlanner || null; +} + +function setHidden(el, hidden) { + if (!el) return; + if (hidden) el.setAttribute('hidden', ''); + else el.removeAttribute('hidden'); +} + +function setStatusChip(id, text, tone) { + const el = $(id); + if (!el) return; + el.textContent = text ?? ''; + el.dataset.tone = tone || 'idle'; +} + +function setLocalStatus(id, text, tone) { + const el = $(id); + if (!el) return; + el.textContent = text ?? ''; + el.dataset.tone = tone || ''; +} + +function escapeHtml(str) { + return String(str ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +/* ── Type coercion ──────────────────────────────────────────────────────── */ + +function toBool(v, fallback = false) { + if (v === true || v === false) return v; + if (v === 'true') return true; + if (v === 'false') return false; + return fallback; +} + +function toNum(v, fallback = 0) { + const n = Number(v); + return Number.isFinite(n) ? n : fallback; +} + +function arrToCsv(arr) { + return Array.isArray(arr) ? arr.join(', ') : ''; +} + +function csvToArr(text) { + return String(text || '') + .split(/[,,]/) + .map((x) => x.trim()) + .filter(Boolean); +} + +function normalizeKeepTagsInput(text) { + const src = csvToArr(text); + const out = []; + for (const item of src) { + const tag = String(item || '').replace(/^<+|>+$/g, '').toLowerCase(); + if (!/^[a-z][a-z0-9_-]*$/.test(tag)) continue; + if (!out.includes(tag)) out.push(tag); + } + return out; +} + +function genId() { + try { return crypto.randomUUID(); } + catch { return `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } +} + +/* ── Prompt block editor ────────────────────────────────────────────────── */ + +function createPromptBlockElement(block, idx, total) { + const wrap = document.createElement('div'); + wrap.className = 'bme-planner-prompt-block'; + + const head = document.createElement('div'); + head.className = 'bme-planner-prompt-head'; + + const left = document.createElement('div'); + left.className = 'bme-planner-prompt-head-left'; + + const nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.className = 'bme-config-input'; + nameInput.placeholder = '块名称'; + nameInput.value = block.name || ''; + nameInput.addEventListener('change', () => { + block.name = nameInput.value; + scheduleSave(); + }); + + const roleSelect = document.createElement('select'); + roleSelect.className = 'bme-config-input'; + for (const r of ['system', 'user', 'assistant']) { + const opt = document.createElement('option'); + opt.value = r; + opt.textContent = r; + opt.selected = (block.role || 'system') === r; + roleSelect.appendChild(opt); + } + roleSelect.addEventListener('change', () => { + block.role = roleSelect.value; + scheduleSave(); + }); + + left.append(nameInput, roleSelect); + + const right = document.createElement('div'); + right.className = 'bme-planner-prompt-head-right'; + + const upBtn = document.createElement('button'); + upBtn.type = 'button'; + upBtn.className = 'bme-config-secondary-btn bme-planner-icon-btn'; + upBtn.innerHTML = ''; + upBtn.title = '上移'; + upBtn.disabled = idx === 0; + upBtn.addEventListener('click', (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + if (!cfgCache?.promptBlocks || idx === 0) return; + const blocks = cfgCache.promptBlocks; + [blocks[idx - 1], blocks[idx]] = [blocks[idx], blocks[idx - 1]]; + renderPromptList(); + scheduleSave(); + }); + + const downBtn = document.createElement('button'); + downBtn.type = 'button'; + downBtn.className = 'bme-config-secondary-btn bme-planner-icon-btn'; + downBtn.innerHTML = ''; + downBtn.title = '下移'; + downBtn.disabled = idx === total - 1; + downBtn.addEventListener('click', (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + if (!cfgCache?.promptBlocks || idx >= total - 1) return; + const blocks = cfgCache.promptBlocks; + [blocks[idx], blocks[idx + 1]] = [blocks[idx + 1], blocks[idx]]; + renderPromptList(); + scheduleSave(); + }); + + const delBtn = document.createElement('button'); + delBtn.type = 'button'; + delBtn.className = 'bme-config-secondary-btn bme-config-danger-btn bme-planner-icon-btn'; + delBtn.innerHTML = ''; + delBtn.title = '删除块'; + delBtn.addEventListener('click', (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + if (!cfgCache?.promptBlocks) return; + cfgCache.promptBlocks.splice(idx, 1); + renderPromptList(); + scheduleSave(); + }); + + right.append(upBtn, downBtn, delBtn); + + const content = document.createElement('textarea'); + content.className = 'bme-config-input bme-planner-textarea'; + content.placeholder = '提示词内容...'; + content.rows = 4; + content.value = block.content || ''; + content.addEventListener('change', () => { + block.content = content.value; + scheduleSave(); + }); + + head.append(left, right); + wrap.append(head, content); + return wrap; +} + +function renderPromptList() { + const list = $('bme-planner-prompt-list'); + const empty = $('bme-planner-prompt-empty'); + if (!list || !empty) return; + const blocks = cfgCache?.promptBlocks || []; + list.innerHTML = ''; + if (!blocks.length) { + setHidden(empty, false); + return; + } + setHidden(empty, true); + blocks.forEach((block, idx) => { + list.appendChild(createPromptBlockElement(block, idx, blocks.length)); + }); +} + +function renderTemplateSelect(selected = '') { + const sel = $('bme-planner-tpl-select'); + if (!sel) return; + sel.innerHTML = ''; + const names = Object.keys(cfgCache?.promptTemplates || {}); + const selectedName = names.includes(selected) ? selected : ''; + for (const name of names) { + const opt = document.createElement('option'); + opt.value = name; + opt.textContent = name; + opt.selected = name === selectedName; + sel.appendChild(opt); + } +} + +/* ── Undo for template delete ───────────────────────────────────────────── */ + +function clearUndo() { + if (undoState?.timer) clearTimeout(undoState.timer); + undoState = null; + const bar = $('bme-planner-tpl-undo'); + setHidden(bar, true); +} + +function showUndoBar(name, blocks) { + clearUndo(); + undoState = { + name, + blocks, + timer: setTimeout(() => { + undoState = null; + setHidden($('bme-planner-tpl-undo'), true); + }, 5000), + }; + const nameEl = $('bme-planner-tpl-undo-name'); + if (nameEl) nameEl.textContent = name; + setHidden($('bme-planner-tpl-undo'), false); +} + +/* ── Logs rendering ─────────────────────────────────────────────────────── */ + +function renderLogs() { + const body = $('bme-planner-log-body'); + if (!body) return; + const list = Array.isArray(logsCache) ? logsCache : []; + if (!list.length) { + body.innerHTML = '
暂无日志
'; + return; + } + body.innerHTML = list + .map((item) => { + const time = item.time ? new Date(item.time).toLocaleString() : '-'; + const cls = item.ok ? 'success' : 'error'; + const label = item.ok ? '成功' : '失败'; + let msgHtml = ''; + if (Array.isArray(item.requestMessages) && item.requestMessages.length) { + msgHtml = item.requestMessages + .map((m, i) => { + const role = escapeHtml(m.role || 'unknown'); + const roleClass = + role === 'system' + ? 'msg-system' + : role === 'user' + ? 'msg-user' + : 'msg-assistant'; + const content = escapeHtml(m.content || ''); + return `
+
[${i + 1}] ${role}
+
${content}
+
`; + }) + .join(''); + } else { + msgHtml = '
无消息
'; + } + return ` +
+
+ ${escapeHtml(time)} · ${label} + ${escapeHtml(item.model || '-')} +
+ ${item.error ? `
${escapeHtml(item.error)}
` : ''} +
请求消息 (${(item.requestMessages || []).length} 条) +
${msgHtml}
+
+
原始回复 +
${escapeHtml(item.rawReply || '')}
+
+
过滤后回复 +
${escapeHtml(item.filteredReply || '')}
+
+
`; + }) + .join(''); +} + +/* ── Apply / collect ────────────────────────────────────────────────────── */ + +function applyConfigToFields(cfg) { + cfgCache = cfg || {}; + const api = cfgCache.api || {}; + + const setVal = (id, value) => { + const el = $(id); + if (el) el.value = value; + }; + + setVal('bme-planner-enabled', String(toBool(cfgCache.enabled, false))); + setVal('bme-planner-skip-plot', String(toBool(cfgCache.skipIfPlotPresent, true))); + + setVal('bme-planner-api-channel', api.channel || 'openai'); + setVal('bme-planner-prefix-mode', api.prefixMode || 'auto'); + setVal('bme-planner-api-base', api.baseUrl || ''); + setVal('bme-planner-prefix-custom', api.customPrefix || ''); + setVal('bme-planner-api-key', api.apiKey || ''); + setVal('bme-planner-model', api.model || ''); + setVal('bme-planner-stream', String(toBool(api.stream, false))); + setVal('bme-planner-temp', String(toNum(api.temperature, 1))); + setVal('bme-planner-top-p', String(toNum(api.top_p, 1))); + setVal('bme-planner-top-k', String(toNum(api.top_k, 0))); + setVal('bme-planner-pp', api.presence_penalty ?? ''); + setVal('bme-planner-fp', api.frequency_penalty ?? ''); + setVal('bme-planner-mt', api.max_tokens ?? ''); + + setVal('bme-planner-include-global-wb', String(toBool(cfgCache.includeGlobalWorldbooks, false))); + setVal('bme-planner-wb-pos4', String(toBool(cfgCache.excludeWorldbookPosition4, true))); + setVal('bme-planner-wb-exclude-names', arrToCsv(cfgCache.worldbookExcludeNames)); + setVal('bme-planner-plot-n', String(toNum(cfgCache.plotCount, 2))); + setVal( + 'bme-planner-keep-tags', + arrToCsv( + cfgCache.responseKeepTags || ['plot', 'note', 'plot-log', 'state'], + ), + ); + setVal('bme-planner-exclude-tags', arrToCsv(cfgCache.chatExcludeTags)); + + setVal('bme-planner-logs-persist', String(toBool(cfgCache.logsPersist, true))); + setVal('bme-planner-logs-max', String(toNum(cfgCache.logsMax, 20))); + + setStatusChip( + 'bme-planner-state-chip', + toBool(cfgCache.enabled, false) ? '已启用' : '未启用', + toBool(cfgCache.enabled, false) ? 'active' : 'idle', + ); + updatePrefixModeUI(); + + const keepSelected = cfgCache.activePromptTemplate || $('bme-planner-tpl-select')?.value || ''; + renderTemplateSelect(keepSelected); + renderPromptList(); +} + +function collectPatch() { + const getVal = (id) => $(id)?.value ?? ''; + + return { + enabled: toBool(getVal('bme-planner-enabled'), false), + skipIfPlotPresent: toBool(getVal('bme-planner-skip-plot'), true), + api: { + channel: getVal('bme-planner-api-channel'), + prefixMode: getVal('bme-planner-prefix-mode'), + baseUrl: getVal('bme-planner-api-base').trim(), + customPrefix: getVal('bme-planner-prefix-custom').trim(), + apiKey: getVal('bme-planner-api-key'), + model: getVal('bme-planner-model').trim(), + stream: toBool(getVal('bme-planner-stream'), false), + temperature: toNum(getVal('bme-planner-temp'), 1), + top_p: toNum(getVal('bme-planner-top-p'), 1), + top_k: Math.floor(toNum(getVal('bme-planner-top-k'), 0)), + presence_penalty: getVal('bme-planner-pp').trim(), + frequency_penalty: getVal('bme-planner-fp').trim(), + max_tokens: getVal('bme-planner-mt').trim(), + }, + includeGlobalWorldbooks: toBool(getVal('bme-planner-include-global-wb'), false), + excludeWorldbookPosition4: toBool(getVal('bme-planner-wb-pos4'), true), + worldbookExcludeNames: csvToArr(getVal('bme-planner-wb-exclude-names')), + plotCount: Math.max(0, Math.floor(toNum(getVal('bme-planner-plot-n'), 2))), + responseKeepTags: normalizeKeepTagsInput(getVal('bme-planner-keep-tags')), + chatExcludeTags: csvToArr(getVal('bme-planner-exclude-tags')), + logsPersist: toBool(getVal('bme-planner-logs-persist'), true), + logsMax: Math.max(1, Math.min(200, Math.floor(toNum(getVal('bme-planner-logs-max'), 20)))), + promptBlocks: cfgCache?.promptBlocks || [], + promptTemplates: cfgCache?.promptTemplates || {}, + activePromptTemplate: $('bme-planner-tpl-select')?.value || '', + }; +} + +function updatePrefixModeUI() { + const mode = $('bme-planner-prefix-mode')?.value || 'auto'; + setHidden($('bme-planner-prefix-custom-row'), mode !== 'custom'); +} + +/* ── Save flow ──────────────────────────────────────────────────────────── */ + +function scheduleSave() { + if (autoSaveTimer) clearTimeout(autoSaveTimer); + autoSaveTimer = setTimeout(doSave, AUTOSAVE_DELAY_MS); +} + +async function doSave() { + if (autosaveInProgress) return; + const api = getPlannerApi(); + if (!api?.patchConfig) { + setStatusChip('bme-planner-save-chip', 'API 未就绪', 'error'); + return; + } + autosaveInProgress = true; + setStatusChip('bme-planner-save-chip', '保存中…', 'loading'); + try { + const patch = collectPatch(); + const res = await api.patchConfig(patch); + if (res?.ok) { + setStatusChip('bme-planner-save-chip', '已保存', 'success'); + setTimeout(() => { + if ($('bme-planner-save-chip')?.dataset?.tone === 'success') { + setStatusChip('bme-planner-save-chip', '就绪', 'idle'); + } + }, 2000); + } else { + setStatusChip('bme-planner-save-chip', res?.error || '保存失败', 'error'); + } + } catch (err) { + setStatusChip('bme-planner-save-chip', String(err?.message ?? err), 'error'); + } finally { + autosaveInProgress = false; + } +} + +/* ── Event wiring ───────────────────────────────────────────────────────── */ + +function onKeepTagsBlur() { + const el = $('bme-planner-keep-tags'); + if (!el) return; + const normalized = normalizeKeepTagsInput(el.value); + el.value = normalized.join(', '); +} + +function bindOnce(section) { + if (bound) return; + bound = true; + + const api = getPlannerApi(); + + /* Basic settings */ + $('bme-planner-enabled')?.addEventListener('change', () => { + setStatusChip( + 'bme-planner-state-chip', + toBool($('bme-planner-enabled').value, false) ? '已启用' : '未启用', + toBool($('bme-planner-enabled').value, false) ? 'active' : 'idle', + ); + }); + + $('bme-planner-run-test')?.addEventListener('click', async () => { + const textEl = $('bme-planner-test-input'); + const text = (textEl?.value || '').trim(); + setLocalStatus('bme-planner-test-status', '测试中…', 'loading'); + const res = await api?.runTest?.(text); + if (res?.ok) setLocalStatus('bme-planner-test-status', '规划测试完成', 'success'); + else setLocalStatus('bme-planner-test-status', res?.error || '规划测试失败', 'error'); + }); + + /* API connection */ + $('bme-planner-toggle-key')?.addEventListener('click', () => { + const input = $('bme-planner-api-key'); + const btn = $('bme-planner-toggle-key'); + if (!input || !btn) return; + if (input.type === 'password') { + input.type = 'text'; + btn.querySelector('span').textContent = '隐藏'; + } else { + input.type = 'password'; + btn.querySelector('span').textContent = '显示'; + } + }); + + $('bme-planner-prefix-mode')?.addEventListener('change', updatePrefixModeUI); + + const handleFetchModels = async (statusText) => { + setLocalStatus('bme-planner-api-status', statusText, 'loading'); + const res = await api?.fetchModels?.(); + if (!res) { + setLocalStatus('bme-planner-api-status', 'API 未就绪', 'error'); + return; + } + if (!res.ok) { + setLocalStatus('bme-planner-api-status', res.error || '拉取失败', 'error'); + return; + } + const models = Array.isArray(res.models) ? res.models : []; + if (!models.length) { + setLocalStatus('bme-planner-api-status', '未获取到模型', 'error'); + const sel = $('bme-planner-model-select'); + if (sel) sel.style.display = 'none'; + return; + } + fetchedModels = models; + const sel = $('bme-planner-model-select'); + if (sel) { + sel.innerHTML = ''; + const cur = ($('bme-planner-model')?.value || '').trim(); + for (const m of models) { + const opt = document.createElement('option'); + opt.value = m; + opt.textContent = m; + opt.selected = m === cur; + sel.appendChild(opt); + } + sel.style.display = ''; + } + setLocalStatus('bme-planner-api-status', `获取到 ${models.length} 个模型`, 'success'); + }; + + $('bme-planner-fetch-models')?.addEventListener('click', () => handleFetchModels('拉取中…')); + $('bme-planner-test-conn')?.addEventListener('click', () => handleFetchModels('测试中…')); + + $('bme-planner-model-select')?.addEventListener('change', () => { + const sel = $('bme-planner-model-select'); + const val = sel?.value; + if (!val) return; + const modelInput = $('bme-planner-model'); + if (modelInput) modelInput.value = val; + scheduleSave(); + }); + + /* Prompts + templates */ + $('bme-planner-keep-tags')?.addEventListener('change', onKeepTagsBlur); + + $('bme-planner-add-prompt')?.addEventListener('click', () => { + cfgCache = cfgCache || {}; + cfgCache.promptBlocks = cfgCache.promptBlocks || []; + cfgCache.promptBlocks.push({ id: genId(), role: 'system', name: '新块', content: '' }); + renderPromptList(); + scheduleSave(); + }); + + $('bme-planner-reset-prompt')?.addEventListener('click', async () => { + if (!confirm('确定恢复默认提示词块?当前提示词块将被覆盖。')) return; + setStatusChip('bme-planner-save-chip', '重置中…', 'loading'); + const res = await api?.resetPromptToDefault?.(); + if (res?.ok && res.config) { + applyConfigToFields(res.config); + setStatusChip('bme-planner-save-chip', '已恢复默认', 'success'); + } else { + setStatusChip('bme-planner-save-chip', res?.error || '重置失败', 'error'); + } + }); + + $('bme-planner-tpl-select')?.addEventListener('change', () => { + const name = $('bme-planner-tpl-select').value; + if (!cfgCache) return; + cfgCache.activePromptTemplate = name; + if (!name) return; + const blocks = cfgCache.promptTemplates?.[name]; + if (!Array.isArray(blocks)) return; + cfgCache.promptBlocks = structuredClone(blocks); + renderPromptList(); + scheduleSave(); + }); + + $('bme-planner-tpl-save')?.addEventListener('click', () => { + const name = $('bme-planner-tpl-select').value; + if (!name) { + setStatusChip('bme-planner-save-chip', '请先选择或新建模板', 'error'); + return; + } + cfgCache.promptTemplates = cfgCache.promptTemplates || {}; + cfgCache.promptTemplates[name] = structuredClone(cfgCache.promptBlocks || []); + cfgCache.activePromptTemplate = name; + renderTemplateSelect(name); + scheduleSave(); + }); + + $('bme-planner-tpl-saveas')?.addEventListener('click', () => { + const name = prompt('新模板名称'); + if (!name) return; + cfgCache.promptTemplates = cfgCache.promptTemplates || {}; + cfgCache.promptTemplates[name] = structuredClone(cfgCache.promptBlocks || []); + cfgCache.activePromptTemplate = name; + renderTemplateSelect(name); + scheduleSave(); + }); + + $('bme-planner-tpl-delete')?.addEventListener('click', () => { + const name = $('bme-planner-tpl-select').value; + if (!name) return; + cfgCache.promptTemplates = cfgCache.promptTemplates || {}; + const backup = structuredClone(cfgCache.promptTemplates[name]); + delete cfgCache.promptTemplates[name]; + cfgCache.activePromptTemplate = ''; + renderTemplateSelect(''); + showUndoBar(name, backup); + scheduleSave(); + }); + + $('bme-planner-tpl-undo-btn')?.addEventListener('click', () => { + if (!undoState) return; + cfgCache.promptTemplates = cfgCache.promptTemplates || {}; + cfgCache.promptTemplates[undoState.name] = undoState.blocks; + cfgCache.activePromptTemplate = undoState.name; + renderTemplateSelect(undoState.name); + clearUndo(); + scheduleSave(); + }); + + /* Debug tools */ + $('bme-planner-debug-wb')?.addEventListener('click', async () => { + const out = $('bme-planner-debug-output'); + if (out) { + setHidden(out, false); + out.textContent = '诊断中…'; + } + const res = await api?.debugWorldbook?.(); + if (out) out.textContent = res?.output ?? '诊断失败'; + }); + + $('bme-planner-debug-char')?.addEventListener('click', async () => { + const out = $('bme-planner-debug-output'); + if (out) { + setHidden(out, false); + out.textContent = '诊断中…'; + } + const res = await api?.debugChar?.(); + if (out) out.textContent = res?.output ?? '诊断失败'; + }); + + /* Logs */ + $('bme-planner-logs-refresh')?.addEventListener('click', () => { + if (!api?.getLogs) return; + logsCache = api.getLogs(); + renderLogs(); + }); + + $('bme-planner-logs-clear')?.addEventListener('click', async () => { + if (!confirm('确定清空所有日志?')) return; + const res = await api?.clearLogs?.(); + if (res?.ok !== false) { + logsCache = []; + renderLogs(); + } + }); + + $('bme-planner-logs-export')?.addEventListener('click', () => { + const blob = new Blob([JSON.stringify(logsCache || [], null, 2)], { + type: 'application/json', + }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `ena-planner-logs-${Date.now()}.json`; + a.click(); + URL.revokeObjectURL(url); + }); + + /* Generic field auto-save: every `.bme-config-input` inside this section + except the test-input textarea and prompt block inputs saves on change. */ + fieldChangeHandler = (ev) => { + const target = ev.target; + if (!target) return; + if (target.closest('.bme-planner-prompt-block')) return; + if (target.id === 'bme-planner-test-input') return; + if (!target.classList?.contains('bme-config-input')) return; + scheduleSave(); + }; + section.addEventListener('change', fieldChangeHandler); +} + +/* ── Public controller ──────────────────────────────────────────────────── */ + +export function initPlannerSections(rootEl) { + const root = rootEl || document; + const section = root.querySelector(SECTION_SELECTOR); + if (!section) return; + bindOnce(section); + + const api = getPlannerApi(); + if (!api) { + setStatusChip('bme-planner-state-chip', '模块未加载', 'error'); + setStatusChip('bme-planner-save-chip', '不可用', 'error'); + return; + } + + if (!unsubscribePlanner && typeof api.subscribe === 'function') { + unsubscribePlanner = api.subscribe((kind, payload) => { + if (kind === 'config') { + applyConfigToFields(payload || {}); + } else if (kind === 'logs') { + logsCache = Array.isArray(payload) ? payload : []; + renderLogs(); + } + }); + } + + const cfg = typeof api.getConfig === 'function' ? api.getConfig() : null; + if (cfg) applyConfigToFields(cfg); + + if (typeof api.getLogs === 'function') { + logsCache = api.getLogs() || []; + renderLogs(); + } +} + +export function refreshPlannerSections() { + const api = getPlannerApi(); + if (!api) { + setStatusChip('bme-planner-state-chip', '模块未加载', 'error'); + return; + } + if (typeof api.getConfig === 'function') applyConfigToFields(api.getConfig()); + if (typeof api.getLogs === 'function') { + logsCache = api.getLogs() || []; + renderLogs(); + } +} + +export function cleanupPlannerSections() { + if (autoSaveTimer) { + clearTimeout(autoSaveTimer); + autoSaveTimer = null; + } + if (typeof unsubscribePlanner === 'function') { + try { unsubscribePlanner(); } catch {} + } + unsubscribePlanner = null; + if (fieldChangeHandler) { + const section = document.querySelector(SECTION_SELECTOR); + section?.removeEventListener('change', fieldChangeHandler); + fieldChangeHandler = null; + } + bound = false; + cfgCache = null; + logsCache = []; + fetchedModels = []; + clearUndo(); +} diff --git a/ui/panel.html b/ui/panel.html index b1f4cca..ef7c406 100644 --- a/ui/panel.html +++ b/ui/panel.html @@ -124,6 +124,14 @@ 任务预设 + -
- 检测中... -
- +
@@ -763,6 +763,14 @@ 任务预设 + +
+
+ + +
+
+
+
规划 LLM · 连接
+
+ 独立的规划 LLM 通道,与 BME 记忆 LLM 相互隔离。支持 OpenAI / Gemini / Claude 兼容协议。 +
+
+
+
+ + +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+
+
+
规划 LLM · 生成参数
+
+ 流式输出用于实时预览,数值留空表示不覆盖渠道默认。 +
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+
提示词 · 模板
+
+ 模板保存的是当前提示词块列表;切换模板会覆盖当前编辑中的块。 +
+
+
+
+ + +
+
+ + + +
+ +
+ +
+
+
+
提示词 · 块编排
+
+ 每个块会作为一条独立消息发送给规划 LLM。系统会在块之后自动追加:角色卡、世界书、BME 结构化记忆、聊天历史和历史 plot。 +
+
+
+
+ +
+ + +
+
+ +
+
+
+
上下文 · 世界书
+
+ 默认读取角色卡绑定的世界书;可选择是否附加全局世界书。 +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
上下文 · 聊天与历史
+
+ 控制从历史消息中提取的 plot 数量,以及过滤 AI 回复里的干扰标签。 +
+
+
+
+ + +
+
+ 仅支持英文标签(如 plot, note, memory)。留空表示不按标签过滤(仅去除 + <think>)。无效标签会自动忽略。 +
+
+ + +
+
+ + +
+
+ +
+
+
+
调试 · 诊断
+
+ 直接诊断世界书/角色卡读取是否正常,定位上下文拼装问题。 +
+
+
+
+ + +
+ +
+ +
+
+
+
调试 · 日志
+
+ 保留最近的规划调用,便于查看请求消息、原始回复与过滤结果。 +
+
+
+
+
+ + +
+
+ + +
+
+
+ + + +
+
+
暂无日志
+
+
+ + +
{ - const plannerApi = _getPlannerApi(); - if (typeof plannerApi?.openSettings === "function") { - plannerApi.openSettings(); - } - _refreshPlannerLauncher(); - }); - - button.dataset.bmeBound = "true"; - _refreshPlannerLauncher(); } function _applyWorkspaceMode() { @@ -3576,6 +3558,8 @@ function _switchConfigSection(sectionId) { _refreshTaskProfileWorkspace(); } else if (currentConfigSectionId === "trace") { _refreshMessageTraceWorkspace(); + } else if (currentConfigSectionId === "planner") { + _refreshPlannerLauncher(); } }