feat(ena-planner): make preserved response tags configurable with UI validation
This commit is contained in:
@@ -256,6 +256,11 @@
|
|||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card-title">聊天与历史</div>
|
<div class="card-title">聊天与历史</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">保留的规划输出标签(逗号分隔)</label>
|
||||||
|
<input id="ep_keep_tags" type="text" class="input" placeholder="plot, note">
|
||||||
|
<p class="form-hint">仅支持英文标签(如 plot, note, memory)。留空表示不按标签过滤(仅去除 think)。无效标签会自动忽略。</p>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">清理 AI 回复中的干扰标签(逗号分隔)</label>
|
<label class="form-label">清理 AI 回复中的干扰标签(逗号分隔)</label>
|
||||||
<input id="ep_exclude_tags" type="text" class="input"
|
<input id="ep_exclude_tags" type="text" class="input"
|
||||||
@@ -461,6 +466,16 @@
|
|||||||
function csvToArr(text) {
|
function csvToArr(text) {
|
||||||
return String(text || '').split(/[,,]/).map(x => x.trim()).filter(Boolean);
|
return String(text || '').split(/[,,]/).map(x => x.trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
function normalizeKeepTagsInput(text) {
|
||||||
|
const src = csvToArr(text);
|
||||||
|
const out = [];
|
||||||
|
src.forEach(item => {
|
||||||
|
const tag = String(item || '').replace(/^<+|>+$/g, '').toLowerCase();
|
||||||
|
if (!/^[a-z][a-z0-9_-]*$/.test(tag)) return;
|
||||||
|
if (!out.includes(tag)) out.push(tag);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
return String(str || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
@@ -669,6 +684,7 @@
|
|||||||
$('ep_wb_pos4').value = String(toBool(cfg.excludeWorldbookPosition4, true));
|
$('ep_wb_pos4').value = String(toBool(cfg.excludeWorldbookPosition4, true));
|
||||||
$('ep_wb_exclude_names').value = arrToCsv(cfg.worldbookExcludeNames);
|
$('ep_wb_exclude_names').value = arrToCsv(cfg.worldbookExcludeNames);
|
||||||
$('ep_plot_n').value = String(toNum(cfg.plotCount, 2));
|
$('ep_plot_n').value = String(toNum(cfg.plotCount, 2));
|
||||||
|
$('ep_keep_tags').value = arrToCsv(cfg.responseKeepTags || ['plot', 'note']);
|
||||||
$('ep_exclude_tags').value = arrToCsv(cfg.chatExcludeTags);
|
$('ep_exclude_tags').value = arrToCsv(cfg.chatExcludeTags);
|
||||||
|
|
||||||
$('ep_logs_persist').value = String(toBool(cfg.logsPersist, true));
|
$('ep_logs_persist').value = String(toBool(cfg.logsPersist, true));
|
||||||
@@ -708,6 +724,7 @@
|
|||||||
p.excludeWorldbookPosition4 = toBool($('ep_wb_pos4').value, true);
|
p.excludeWorldbookPosition4 = toBool($('ep_wb_pos4').value, true);
|
||||||
p.worldbookExcludeNames = csvToArr($('ep_wb_exclude_names').value);
|
p.worldbookExcludeNames = csvToArr($('ep_wb_exclude_names').value);
|
||||||
p.plotCount = Math.max(0, Math.floor(toNum($('ep_plot_n').value, 2)));
|
p.plotCount = Math.max(0, Math.floor(toNum($('ep_plot_n').value, 2)));
|
||||||
|
p.responseKeepTags = normalizeKeepTagsInput($('ep_keep_tags').value);
|
||||||
p.chatExcludeTags = csvToArr($('ep_exclude_tags').value);
|
p.chatExcludeTags = csvToArr($('ep_exclude_tags').value);
|
||||||
|
|
||||||
p.logsPersist = toBool($('ep_logs_persist').value, true);
|
p.logsPersist = toBool($('ep_logs_persist').value, true);
|
||||||
@@ -761,6 +778,10 @@
|
|||||||
const val = $('ep_model_select').value;
|
const val = $('ep_model_select').value;
|
||||||
if (val) { $('ep_model').value = val; scheduleSave(); }
|
if (val) { $('ep_model').value = val; scheduleSave(); }
|
||||||
});
|
});
|
||||||
|
$('ep_keep_tags').addEventListener('change', () => {
|
||||||
|
const normalized = normalizeKeepTagsInput($('ep_keep_tags').value);
|
||||||
|
$('ep_keep_tags').value = normalized.join(', ');
|
||||||
|
});
|
||||||
|
|
||||||
$('ep_add_prompt').addEventListener('click', () => {
|
$('ep_add_prompt').addEventListener('click', () => {
|
||||||
cfg.promptBlocks = cfg.promptBlocks || [];
|
cfg.promptBlocks = cfg.promptBlocks || [];
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ function getDefaultSettings() {
|
|||||||
|
|
||||||
// Plot extraction
|
// Plot extraction
|
||||||
plotCount: 2,
|
plotCount: 2,
|
||||||
|
// Planner response tags to keep, in source order (empty = keep full response)
|
||||||
|
responseKeepTags: ['plot', 'note'],
|
||||||
|
|
||||||
// Planner prompts (designer)
|
// Planner prompts (designer)
|
||||||
promptBlocks: structuredClone(DEFAULT_PROMPT_BLOCKS),
|
promptBlocks: structuredClone(DEFAULT_PROMPT_BLOCKS),
|
||||||
@@ -100,6 +102,8 @@ function ensureSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
deepMerge(s, d);
|
deepMerge(s, d);
|
||||||
|
if (!Array.isArray(s.responseKeepTags)) s.responseKeepTags = structuredClone(d.responseKeepTags);
|
||||||
|
else s.responseKeepTags = normalizeResponseKeepTags(s.responseKeepTags);
|
||||||
|
|
||||||
// Migration: remove old keys that are no longer needed
|
// Migration: remove old keys that are no longer needed
|
||||||
delete s.includeCharacterLorebooks;
|
delete s.includeCharacterLorebooks;
|
||||||
@@ -114,6 +118,20 @@ function ensureSettings() {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeResponseKeepTags(tags) {
|
||||||
|
const src = Array.isArray(tags) ? tags : [];
|
||||||
|
const cleaned = [];
|
||||||
|
for (const raw of src) {
|
||||||
|
const t = String(raw || '')
|
||||||
|
.trim()
|
||||||
|
.replace(/^<+|>+$/g, '')
|
||||||
|
.toLowerCase();
|
||||||
|
if (!/^[a-z][a-z0-9_-]*$/.test(t)) continue;
|
||||||
|
if (!cleaned.includes(t)) cleaned.push(t);
|
||||||
|
}
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
const loaded = await EnaPlannerStorage.get('config', null);
|
const loaded = await EnaPlannerStorage.get('config', null);
|
||||||
config = (loaded && typeof loaded === 'object') ? loaded : getDefaultSettings();
|
config = (loaded && typeof loaded === 'object') ? loaded : getDefaultSettings();
|
||||||
@@ -878,10 +896,13 @@ function stripThinkBlocks(text) {
|
|||||||
return out.trim();
|
return out.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPlotAndNoteInOrder(text) {
|
function extractSelectedBlocksInOrder(text, tagNames) {
|
||||||
|
const names = normalizeResponseKeepTags(tagNames);
|
||||||
|
if (!Array.isArray(names) || names.length === 0) return '';
|
||||||
const src = String(text ?? '');
|
const src = String(text ?? '');
|
||||||
const blocks = [];
|
const blocks = [];
|
||||||
const re = /<(plot|note)\b[^>]*>[\s\S]*?<\/\1>/gi;
|
const escapedNames = names.map(n => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||||
|
const re = new RegExp(`<(${escapedNames.join('|')})\\b[^>]*>[\\s\\S]*?<\\/\\1>`, 'gi');
|
||||||
let m;
|
let m;
|
||||||
while ((m = re.exec(src)) !== null) {
|
while ((m = re.exec(src)) !== null) {
|
||||||
blocks.push(m[0]);
|
blocks.push(m[0]);
|
||||||
@@ -891,8 +912,9 @@ function extractPlotAndNoteInOrder(text) {
|
|||||||
|
|
||||||
function filterPlannerForInput(rawFull) {
|
function filterPlannerForInput(rawFull) {
|
||||||
const noThink = stripThinkBlocks(rawFull);
|
const noThink = stripThinkBlocks(rawFull);
|
||||||
const onlyPN = extractPlotAndNoteInOrder(noThink);
|
const tags = ensureSettings().responseKeepTags;
|
||||||
if (onlyPN) return onlyPN;
|
const selected = extractSelectedBlocksInOrder(noThink, tags);
|
||||||
|
if (selected) return selected;
|
||||||
return noThink;
|
return noThink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user