mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 14:20:35 +08:00
feat: integrate ena planner into native bme panel
This commit is contained in:
754
ui/panel-ena-sections.js
Normal file
754
ui/panel-ena-sections.js
Normal file
@@ -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, '>')
|
||||
.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 = '<i class="fa-solid fa-chevron-up"></i>';
|
||||
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 = '<i class="fa-solid fa-chevron-down"></i>';
|
||||
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 = '<i class="fa-solid fa-trash-can"></i>';
|
||||
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 = '<option value="">-- 选择模板 --</option>';
|
||||
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 = '<div class="bme-planner-log-empty">暂无日志</div>';
|
||||
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 `<div class="bme-planner-msg-card ${roleClass}">
|
||||
<div class="bme-planner-msg-role">[${i + 1}] ${role}</div>
|
||||
<pre class="bme-planner-msg-content">${content}</pre>
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
} else {
|
||||
msgHtml = '<div class="bme-planner-log-empty">无消息</div>';
|
||||
}
|
||||
return `
|
||||
<div class="bme-planner-log-item">
|
||||
<div class="bme-planner-log-meta">
|
||||
<span>${escapeHtml(time)} · <span class="${cls}">${label}</span></span>
|
||||
<span>${escapeHtml(item.model || '-')}</span>
|
||||
</div>
|
||||
${item.error ? `<div class="bme-planner-log-error">${escapeHtml(item.error)}</div>` : ''}
|
||||
<details><summary>请求消息 (${(item.requestMessages || []).length} 条)</summary>
|
||||
<div class="bme-planner-msg-list">${msgHtml}</div>
|
||||
</details>
|
||||
<details><summary>原始回复</summary>
|
||||
<pre class="bme-planner-log-pre">${escapeHtml(item.rawReply || '')}</pre>
|
||||
</details>
|
||||
<details open><summary>过滤后回复</summary>
|
||||
<pre class="bme-planner-log-pre">${escapeHtml(item.filteredReply || '')}</pre>
|
||||
</details>
|
||||
</div>`;
|
||||
})
|
||||
.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 = '<option value="">-- 从列表选择 --</option>';
|
||||
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();
|
||||
}
|
||||
546
ui/panel.html
546
ui/panel.html
@@ -124,6 +124,14 @@
|
||||
<i class="fa-solid fa-scroll"></i>
|
||||
<span>任务预设</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-nav-btn"
|
||||
data-config-section="planner"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||||
<span>ENA 规划器</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-nav-btn"
|
||||
data-config-section="appearance"
|
||||
@@ -719,15 +727,7 @@
|
||||
在这里集中配置第二记忆模型、功能开关、细粒度参数、任务预设和面板主题。
|
||||
</p>
|
||||
</div>
|
||||
<div class="bme-config-workspace-actions">
|
||||
<button class="bme-config-launch-btn" id="bme-open-ena-planner" type="button">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||||
<span>Ena Planner 设置</span>
|
||||
</button>
|
||||
<div class="bme-config-launch-hint" id="bme-open-ena-planner-hint">
|
||||
检测中...
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-workspace-actions"></div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-nav bme-config-nav-mobile">
|
||||
@@ -763,6 +763,14 @@
|
||||
<i class="fa-solid fa-scroll"></i>
|
||||
<span>任务预设</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-nav-btn"
|
||||
data-config-section="planner"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||||
<span>ENA 规划器</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-nav-btn"
|
||||
data-config-section="appearance"
|
||||
@@ -2833,6 +2841,526 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="bme-config-section" data-config-section="planner">
|
||||
<div class="bme-config-section-head">
|
||||
<div class="bme-config-section-kicker">ENA 规划器</div>
|
||||
<h3 class="bme-config-section-title">剧情规划 · LLM 接入</h3>
|
||||
<p class="bme-config-section-desc">
|
||||
发送前自动拦截并调用规划 LLM,从角色卡、世界书、BME 记忆、历史 plot 中收集上下文,生成
|
||||
<code><plot></code> 和 <code><note></code> 追加到你的输入。
|
||||
</p>
|
||||
<div class="bme-planner-status-strip" id="bme-planner-status-strip">
|
||||
<span class="bme-planner-status-chip" id="bme-planner-state-chip">加载中…</span>
|
||||
<span class="bme-planner-status-chip" id="bme-planner-save-chip" data-tone="idle">就绪</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-grid">
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">基本设置</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
启用后,发送消息将先走规划 LLM;输入中已存在 <code><plot></code> 标签时自动跳过。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-enabled">启用规划器</label>
|
||||
<select id="bme-planner-enabled" class="bme-config-input">
|
||||
<option value="true">开启</option>
|
||||
<option value="false">关闭</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-skip-plot">跳过已有规划的输入</label>
|
||||
<select id="bme-planner-skip-plot" class="bme-config-input">
|
||||
<option value="true">是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-config-help">
|
||||
检测到输入里已有 <code><plot></code> 标签时跳过规划,避免重复拼接。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">快速测试</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
输入一段剧情描述,立即调用规划 LLM 试跑一次,日志会写入下方“调试 & 日志”。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-test-input">测试输入(留空使用默认)</label>
|
||||
<textarea
|
||||
id="bme-planner-test-input"
|
||||
class="bme-config-input bme-planner-textarea"
|
||||
rows="3"
|
||||
placeholder="输入一段剧情描述,测试规划器输出..."
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="bme-config-actions">
|
||||
<button
|
||||
class="bme-config-test-btn"
|
||||
id="bme-planner-run-test"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-flask"></i>
|
||||
<span>运行规划测试</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bme-planner-status-text" id="bme-planner-test-status"></div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">规划 LLM · 连接</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
独立的规划 LLM 通道,与 BME 记忆 LLM 相互隔离。支持 OpenAI / Gemini / Claude 兼容协议。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-api-channel">渠道类型</label>
|
||||
<select id="bme-planner-api-channel" class="bme-config-input">
|
||||
<option value="openai">OpenAI 兼容</option>
|
||||
<option value="gemini">Gemini 兼容</option>
|
||||
<option value="claude">Claude 兼容</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-prefix-mode">路径前缀</label>
|
||||
<select id="bme-planner-prefix-mode" class="bme-config-input">
|
||||
<option value="auto">自动(如 /v1)</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-config-row" id="bme-planner-prefix-custom-row" hidden>
|
||||
<label for="bme-planner-prefix-custom">自定义前缀</label>
|
||||
<input
|
||||
id="bme-planner-prefix-custom"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="/v1"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-api-base">API 地址</label>
|
||||
<input
|
||||
id="bme-planner-api-base"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="https://api.openai.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-api-key">API Key</label>
|
||||
<div class="bme-planner-inline-row">
|
||||
<input
|
||||
id="bme-planner-api-key"
|
||||
class="bme-config-input"
|
||||
type="password"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-toggle-key"
|
||||
type="button"
|
||||
>
|
||||
<span>显示</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-model">模型</label>
|
||||
<input
|
||||
id="bme-planner-model"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="gpt-4o / claude-3-5-sonnet / gemini-2.0-flash"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-model-fetch-block">
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-fetch-models"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
<span>拉取模型</span>
|
||||
</button>
|
||||
<select
|
||||
id="bme-planner-model-select"
|
||||
class="bme-config-input bme-model-select"
|
||||
style="display: none"
|
||||
>
|
||||
<option value="">-- 从列表选择 --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-config-actions">
|
||||
<button
|
||||
class="bme-config-test-btn"
|
||||
id="bme-planner-test-conn"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-plug"></i>
|
||||
<span>测试连接</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bme-planner-status-text" id="bme-planner-api-status"></div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">规划 LLM · 生成参数</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
流式输出用于实时预览,数值留空表示不覆盖渠道默认。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-stream">流式输出</label>
|
||||
<select id="bme-planner-stream" class="bme-config-input">
|
||||
<option value="true">开启</option>
|
||||
<option value="false">关闭</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-planner-param-grid">
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-temp">Temperature</label>
|
||||
<input
|
||||
id="bme-planner-temp"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="2"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-top-p">Top P</label>
|
||||
<input
|
||||
id="bme-planner-top-p"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
step="0.05"
|
||||
min="0"
|
||||
max="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-top-k">Top K</label>
|
||||
<input
|
||||
id="bme-planner-top-k"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-pp">Presence penalty</label>
|
||||
<input
|
||||
id="bme-planner-pp"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="-2 ~ 2"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-fp">Frequency penalty</label>
|
||||
<input
|
||||
id="bme-planner-fp"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="-2 ~ 2"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-mt">最大 Token 数</label>
|
||||
<input
|
||||
id="bme-planner-mt"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="留空则不限制"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">提示词 · 模板</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
模板保存的是当前提示词块列表;切换模板会覆盖当前编辑中的块。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-tpl-select">活动模板</label>
|
||||
<select id="bme-planner-tpl-select" class="bme-config-input">
|
||||
<option value="">-- 选择模板 --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-config-actions">
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-tpl-save"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-floppy-disk"></i>
|
||||
<span>覆盖保存</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-tpl-saveas"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-file-circle-plus"></i>
|
||||
<span>另存为</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-secondary-btn bme-config-danger-btn"
|
||||
id="bme-planner-tpl-delete"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bme-planner-undo-bar" id="bme-planner-tpl-undo" hidden>
|
||||
<span
|
||||
>模板 <strong id="bme-planner-tpl-undo-name"></strong> 已删除</span
|
||||
>
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-tpl-undo-btn"
|
||||
type="button"
|
||||
>
|
||||
撤销
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">提示词 · 块编排</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
每个块会作为一条独立消息发送给规划 LLM。系统会在块之后自动追加:角色卡、世界书、BME 结构化记忆、聊天历史和历史 plot。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bme-planner-prompt-list" class="bme-planner-prompt-list"></div>
|
||||
<div
|
||||
class="bme-planner-prompt-empty"
|
||||
id="bme-planner-prompt-empty"
|
||||
hidden
|
||||
>
|
||||
暂无提示词块
|
||||
</div>
|
||||
<div class="bme-config-actions">
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-add-prompt"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span>添加块</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-secondary-btn bme-config-danger-btn"
|
||||
id="bme-planner-reset-prompt"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-rotate-left"></i>
|
||||
<span>恢复默认</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">上下文 · 世界书</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
默认读取角色卡绑定的世界书;可选择是否附加全局世界书。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-include-global-wb">读取全局世界书</label>
|
||||
<select id="bme-planner-include-global-wb" class="bme-config-input">
|
||||
<option value="false">否</option>
|
||||
<option value="true">是</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-wb-pos4">排除 position=4 的条目</label>
|
||||
<select id="bme-planner-wb-pos4" class="bme-config-input">
|
||||
<option value="true">是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-wb-exclude-names"
|
||||
>按条目名称关键词排除(逗号分隔)</label
|
||||
>
|
||||
<input
|
||||
id="bme-planner-wb-exclude-names"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="mvu_update, system, ..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">上下文 · 聊天与历史</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
控制从历史消息中提取的 plot 数量,以及过滤 AI 回复里的干扰标签。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-keep-tags"
|
||||
>保留的规划输出标签(逗号分隔)</label
|
||||
>
|
||||
<input
|
||||
id="bme-planner-keep-tags"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="plot, note, plot-log, state"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-help">
|
||||
仅支持英文标签(如 plot, note, memory)。留空表示不按标签过滤(仅去除
|
||||
<code><think></code>)。无效标签会自动忽略。
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-exclude-tags"
|
||||
>清理 AI 回复中的干扰标签(逗号分隔)</label
|
||||
>
|
||||
<input
|
||||
id="bme-planner-exclude-tags"
|
||||
class="bme-config-input"
|
||||
type="text"
|
||||
placeholder="行动选项, UpdateVariable, StatusPlaceHolderImpl"
|
||||
/>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-plot-n">携带最近 N 条历史 plot</label>
|
||||
<input
|
||||
id="bme-planner-plot-n"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">调试 · 诊断</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
直接诊断世界书/角色卡读取是否正常,定位上下文拼装问题。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-actions">
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-debug-wb"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-book"></i>
|
||||
<span>诊断世界书</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-debug-char"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-user"></i>
|
||||
<span>诊断角色卡</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre class="bme-planner-debug-output" id="bme-planner-debug-output" hidden></pre>
|
||||
</div>
|
||||
|
||||
<div class="bme-config-card">
|
||||
<div class="bme-config-card-head">
|
||||
<div>
|
||||
<div class="bme-config-card-title">调试 · 日志</div>
|
||||
<div class="bme-config-card-subtitle">
|
||||
保留最近的规划调用,便于查看请求消息、原始回复与过滤结果。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-planner-param-grid">
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-logs-persist">持久化日志</label>
|
||||
<select id="bme-planner-logs-persist" class="bme-config-input">
|
||||
<option value="true">是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="bme-config-row">
|
||||
<label for="bme-planner-logs-max">最大日志条数</label>
|
||||
<input
|
||||
id="bme-planner-logs-max"
|
||||
class="bme-config-input"
|
||||
type="number"
|
||||
min="1"
|
||||
max="200"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bme-config-actions">
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-logs-refresh"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-secondary-btn"
|
||||
id="bme-planner-logs-export"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-file-arrow-down"></i>
|
||||
<span>导出 JSON</span>
|
||||
</button>
|
||||
<button
|
||||
class="bme-config-secondary-btn bme-config-danger-btn"
|
||||
id="bme-planner-logs-clear"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
<span>清空日志</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="bme-planner-log-body" class="bme-planner-log-list">
|
||||
<div class="bme-planner-log-empty">暂无日志</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="bme-config-section"
|
||||
data-config-section="appearance"
|
||||
|
||||
46
ui/panel.js
46
ui/panel.js
@@ -5,6 +5,10 @@ import {
|
||||
buildVisibleGraphRefreshToken,
|
||||
resolveVisibleGraphWorkspaceMode,
|
||||
} from "./panel-graph-refresh-utils.js";
|
||||
import {
|
||||
initPlannerSections,
|
||||
refreshPlannerSections,
|
||||
} from "./panel-ena-sections.js";
|
||||
import { getNodeDisplayName } from "../graph/node-labels.js";
|
||||
import {
|
||||
buildRegionLine,
|
||||
@@ -1350,42 +1354,20 @@ function _switchTab(tabId) {
|
||||
}
|
||||
}
|
||||
|
||||
function _getPlannerApi() {
|
||||
return globalThis?.stBmeEnaPlanner || null;
|
||||
}
|
||||
|
||||
function _refreshPlannerLauncher() {
|
||||
const button = document.getElementById("bme-open-ena-planner");
|
||||
const hint = document.getElementById("bme-open-ena-planner-hint");
|
||||
if (!button || !hint) return;
|
||||
|
||||
const plannerApi = _getPlannerApi();
|
||||
const ready = typeof plannerApi?.openSettings === "function";
|
||||
|
||||
button.disabled = !ready;
|
||||
button.classList.toggle("is-runtime-disabled", !ready);
|
||||
hint.textContent = ready
|
||||
? "已加载,可打开独立的 Ena Planner 设置页。"
|
||||
: "未检测到 Ena Planner 模块,请重载 ST-BME 后再试。";
|
||||
try {
|
||||
refreshPlannerSections();
|
||||
} catch (err) {
|
||||
console.warn("[ST-BME] planner section refresh failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function _bindPlannerLauncher() {
|
||||
const button = document.getElementById("bme-open-ena-planner");
|
||||
if (!button || button.dataset.bmeBound === "true") {
|
||||
_refreshPlannerLauncher();
|
||||
return;
|
||||
try {
|
||||
initPlannerSections(panelEl || document);
|
||||
} catch (err) {
|
||||
console.warn("[ST-BME] planner section init failed:", err);
|
||||
}
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user