From a86f91991db967f813d2dd20bd5f76cd8929902d Mon Sep 17 00:00:00 2001
From: Youzini-afk <13153778771cx@gmail.com>
Date: Thu, 23 Apr 2026 15:24:54 +0800
Subject: [PATCH] feat: integrate ena planner into native bme panel
---
README.md | 4 +-
ena-planner/ena-planner.css | 888 -------------------------------
ena-planner/ena-planner.html | 993 -----------------------------------
ena-planner/ena-planner.js | 331 +++++-------
style.css | 365 +++++++++++++
ui/panel-ena-sections.js | 754 ++++++++++++++++++++++++++
ui/panel.html | 546 ++++++++++++++++++-
ui/panel.js | 46 +-
8 files changed, 1800 insertions(+), 2127 deletions(-)
delete mode 100644 ena-planner/ena-planner.css
delete mode 100644 ena-planner/ena-planner.html
create mode 100644 ui/panel-ena-sections.js
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
-
-
-
-
-
-
-
-
-
-
-
快速开始
-
API 配置
-
提示词
-
上下文
-
调试
-
-
-
-
-
-
-
-
ℹ
-
- 工作流程:点击发送 → 拦截 → 收集上下文(角色卡、世界书、BME 记忆、历史 plot、最近 AI 回复)→ 发给规划 LLM → 提取 <plot> 和
- <note> → 追加到你的输入 → 放行发送
-
-
-
-
- 基本设置
-
- 输入中已有 <plot> 标签时跳过自动规划。
-
-
-
- 快速测试
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 连接设置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
💡
-
- 系统会自动在提示词之后注入:角色卡、世界书、BME 结构化记忆、聊天历史和历史 plot 等上下文。你只需专注编写"规划指令"。
-
-
-
-
- 模板管理
-
-
- 模板 已删除
-
-
-
-
-
- 提示词块
-
- 暂无提示词块
-
-
-
-
-
-
-
-
-
-
-
-
-
- 诊断工具
-
-
-
-
-
-
-
-
-
- 日志
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 @@
任务预设
+