mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-13 09:15:50 +00:00
### 新功能 - **翰林院向量化质量升级**: - **边界感知切块**:替换四个来源(聊天记录/小说/世界书/手动)的纯字符硬切——优先在段落边界断开,其次句末标点(含中文引号闭合),极端长串才硬切;句子/对话不再被拦腰截断,embedding 质量同步受益。仅影响新录入,已有向量无需重建 - **注入时序重排**:检索结果注入提示词前按时序重排(聊天记录按楼层、小说按卷/章/节——中文数字章节号可解析),rerank 只决定"选哪些块",不再决定呈现顺序;修复"不打不相识的剧情之后紧跟关系亲密"这类因按相关度排序导致的认知时间错乱 - **断层提示**:聊天记录相邻块楼层跳跃时自动插入"与上文相隔约 N 楼,并非连续发生"提示行,消除中间剧情缺失造成的割裂感 - **时间标识**:新录入的聊天记录块在来源标识中带上消息发送时间(ST 向量存储不持久化元数据,时间必须写入块文本才能在检索后取回;旧格式块兼容解析) - **记忆块工作流(memory-blocks)**:剧情优化新增"自定义记忆块"体系——占位符驱动的并发工作流框架 - 在剧情优化面板「匹配替换 (sulv)」下方可增删自定义块:每个块定义一个占位符,执行剧情优化时主/拦截提示词中的占位符会被块的产出替换 - **静态块**:直接输出固定内容;**AI 调用块**:用所选 API 功能槽独立请求一次,把回复(或其中指定 `<标签>` 的内容)作为替换值 - 原有 sulv1-4 速率占位符迁入同一框架,行为与旧版逐字节一致 - 块定义为纯 JSON、随设置持久化,为后续导入导出与战斗系统接入预留扩展点 - 框架层新增**顺序拼接式 Chain**(`composeChain`):与占位符替换并列的第二种组合范式——同链的块并发执行后按 `order` 排序、以 `separator` 拼接并可选 `header/footer` 包裹,产出一个完整注入块;为记忆注入合成块与战斗系统"底部战报块"预留的承载结构,本版本暂无 UI 入口 - **API 连接配置**: - 角色世界书(cwb)与一键生卡(autoCharCard)纳入旧配置自动迁移:老用户首次加载会把旧 URL / Key / 模型自动迁移为连接配置并分配槽位(一键生卡仅在规划者与执行者配置一致或规划者为空时迁移,避免悄悄改变行为) - **profile 已分配时参数控件 informational 化**:主面板 / 并发剧情优化 / 角色世界书 / 术语表的温度、maxTokens 控件在槽位分配 profile 后自动禁用并显示"由连接配置控制"提示,消除"改了没效果"的用户陷阱 - **profile 状态卡新增"本设备无 Key"警示**:API Key 仅保存在最初填写它的设备/浏览器上(安全设计,不随云端设置同步),换设备后状态卡会直接亮出警示徽标,不必等到调用报错才发现 ### 修复 - **独立聊天记忆从摆设变真功能(原作遗留坑)**:此前向量数据"随卡不随聊天"——开启"独立聊天记忆"后录入仍存进角色库、查询却去查一个从未被写入过的聊天集合、计数恒为 0,整体静默失效。现已重构为聊天级分桶: - 独立模式下,聊天记录类向量按当前聊天隔离存储与检索,同一张卡开多个聊天(不同剧情线)的记忆互不污染 - 小说 / 世界书 / 手动录入属于"知识",仍随角色卡跨聊天共享;全局库不受影响 - 知识管理列表为聊天专属库显示"聊天级"徽标;聊天级库禁止移动到全局 - 统一模式(默认关闭独立记忆)的存量数据与行为完全不变 - 已知限制:聊天专属记忆跟随聊天文件,重命名聊天文件会使其失联(与 ST 官方向量扩展同等限制) - **超级排序截断顺序修正**:开启"超级排序"时,时序重排发生在 top_n 截断之前,导致保留的是"时序最早"而非"最相关"的块,检索结果长期偏向最旧的聊天记录。现改为先按相关度截取 top_n、再做时序排序 - **翰林院向量化失败("向量化块数量不识别"反馈)**: - 一次性清洗 profile-sync 历史污染:`retrieval/rerank.apiKey` 中的掩码占位符在持久层根治(此前仅读取侧防御);`apiEndpoint` / `rerank.apiMode` 的非法值(如被旧版写入的空字符串)归一化为 `custom` - 修复 `apiEndpoint` 为空/非法时请求被硬定向到 `api.openai.com`、无视用户自定义 URL 的问题(CSP 拦截 / 401 的元凶) - 修复**本地代理(LM Studio/Ollama)模式**自始就缺少 URL 分支、同样被错误定向到 openai.com 的问题 - API 模式下拉补全 `OpenAI 官方` / `Azure` 选项;默认 API 模式改为 `custom`(与默认 URL 配套),新用户不再因选项缺失导致首次保存写入空值 - profile-sync 给下拉框赋不存在选项值的污染源头修复(影响所有模块面板,不止翰林院) - **Rerank "API Key 未提供"报错升级**:当原因是"连接配置在本设备没有可用 Key"时,报错会直接说明 Key 的设备本地性并指引到 API 连接配置重新填写(向量化 Google 直连、获取模型列表同步处理) - **旧配置迁移**:一键生卡迁移时排除掩码占位符,避免把历史污染的假 Key 迁入新连接配置 - **超级记忆稳定性专项**(针对"工作不大稳定"反馈,4 处根因一次修复): - **切聊天竞态污染**:CHAT_CHANGED 时超级记忆立即全量同步,而表格系统延迟 100ms 才加载新聊天的表格,导致【旧聊天】的表格内容被写进【新角色】的记忆世界书;两边表名不同时旧表条目无 GC 兜底会**永久残留**("记忆串台"元凶)。现 CHAT_CHANGED 只确保世界书存在,新状态同步交由 `loadTables()` 完成后的自动推送,单次且时序正确 - **死代码双轨存储拆除**:`saveStateToMetadata` / `tryRestoreStateFromMetadata` 把表格状态写到 `msg.metadata`——该字段非 ST 持久化位(同 v2.2.5 二次填表修过的坑),写入即蒸发、恢复永远为空,且每次同步还白调一次 `saveChat()`。整条链路删除,表格状态唯一信源为表格系统的 `msg.extra.amily2_tables_data` - **`awaitSync()` 穿透**:同步队列正忙时 `pushUpdate` 会用一个立即 resolve 的空 Promise 覆盖 `_syncPromise`,Pipeline Stage 4 等待形同虚设、后续阶段在同步未完成时被放行。现忙时不覆盖,正在运行的 drain 循环自然吃掉新入队项 - **开关打开不生效**:启动时若总开关为关,初始化早退且不注册监听器;此后在 UI 勾选开关只写设置,超级记忆直到刷新页面前都是死的。现勾选即触发初始化(幂等) - 附带:`forceSyncAll` 的表格角色推断改为复用 `events-schema.inferTableRole`,消除两处重复逻辑漂移风险;每次切聊天的双倍全量同步(restore 路径一次 + 显式一次)随死代码移除归一 ### 重构 - 表格核心 `manager.js` 瘦身(约 1050 → 600 行):19 个 UI 突变操作拆分至 `actions/ui-mutations.js`,SuperMemory 事件分发拆分至 `events-dispatch.js`;全部经 re-export 保持兼容,外部调用路径零改动 - 角色世界书最后 2 处散乱的厂商 URL 判断迁移至 `detectVendor` 统一入口,业务路径上不再有硬编码的 URL substring 判断
1487 lines
64 KiB
JavaScript
1487 lines
64 KiB
JavaScript
/**
|
||
* plot-opt-bindings.js — 剧情优化 + JQYH 面板的所有 UI 事件绑定
|
||
*
|
||
* 从 bindings.js 中拆分而来,由 PlotOptModule.mount() 调用入口函数
|
||
* initializePlotOptimizationBindings()。
|
||
*/
|
||
|
||
import { extension_settings, getContext } from "/scripts/extensions.js";
|
||
import { characters, this_chid, getRequestHeaders, saveSettingsDebounced, eventSource, event_types } from "/script.js";
|
||
import { defaultSettings, extensionName } from "../utils/settings.js";
|
||
import { testConcurrentApiConnection, fetchConcurrentModels } from '../core/api/ConcurrentApi.js';
|
||
import { safeLorebooks, safeCharLorebooks, safeLorebookEntries } from "../core/tavernhelper-compatibility.js";
|
||
import { createDrawer } from '../ui/drawer.js';
|
||
import { pluginAuthStatus } from "../utils/auth.js";
|
||
import { configManager } from '../utils/config/ConfigManager.js';
|
||
import { SENSITIVE_KEYS } from '../utils/config/sensitive-keys.js';
|
||
import { showHtmlModal } from './page-window.js';
|
||
import { escapeHTML } from '../utils/utils.js';
|
||
import { SLOTS } from '../utils/config/ApiProfileManager.js';
|
||
import {
|
||
listCustomBlocks,
|
||
getCustomBlock,
|
||
addCustomBlock,
|
||
updateCustomBlock,
|
||
deleteCustomBlock,
|
||
syncCustomBlocksFromSettings,
|
||
} from '../core/memory-blocks/index.js';
|
||
import { watchProfileSliderGuard } from './profile-slider-guard.js';
|
||
|
||
// ========== Prompt Cache (module-level state) ==========
|
||
|
||
const promptCache = {
|
||
main: '',
|
||
system: '',
|
||
final_system: ''
|
||
};
|
||
|
||
// ========== 导出函数 ==========
|
||
|
||
export function opt_saveAllSettings() {
|
||
const panel = $('#amily2_plot_optimization_panel');
|
||
if (panel.length === 0) return;
|
||
|
||
console.log(`[${extensionName}] 手动触发所有剧情优化设置的保存...`);
|
||
panel.find('input[type="checkbox"], input[type="radio"], input[type="text"], input[type="password"], textarea, select').trigger('change.amily2_opt');
|
||
|
||
panel.find('input[type="range"]').trigger('change.amily2_opt');
|
||
|
||
opt_saveEnabledEntries();
|
||
|
||
toastr.info('剧情优化设置已自动保存。');
|
||
}
|
||
|
||
|
||
function opt_toCamelCase(str) {
|
||
return str.replace(/[-_]([a-z])/g, (g) => g[1].toUpperCase());
|
||
}
|
||
|
||
function opt_updateWorldbookSourceVisibility(panel, source) {
|
||
const manualSelectionWrapper = panel.find('#amily2_opt_worldbook_select_wrapper');
|
||
if (source === 'manual') {
|
||
manualSelectionWrapper.show();
|
||
const selectBox = manualSelectionWrapper.find('#amily2_opt_selected_worldbooks');
|
||
selectBox.css({
|
||
'height': 'auto',
|
||
'background-color': 'var(--bg1)',
|
||
'appearance': 'none',
|
||
'-webkit-appearance': 'none'
|
||
});
|
||
} else {
|
||
manualSelectionWrapper.hide();
|
||
}
|
||
}
|
||
|
||
|
||
const opt_characterSpecificSettings = [
|
||
'plotOpt_worldbookSource',
|
||
'plotOpt_selectedWorldbooks',
|
||
'plotOpt_autoSelectWorldbooks',
|
||
'plotOpt_enabledWorldbookEntries'
|
||
];
|
||
|
||
|
||
async function opt_saveSetting(key, value) {
|
||
if (opt_characterSpecificSettings.includes(key)) {
|
||
const character = characters[this_chid];
|
||
if (!character) return;
|
||
|
||
if (!character.data.extensions) character.data.extensions = {};
|
||
if (!character.data.extensions[extensionName]) character.data.extensions[extensionName] = {};
|
||
|
||
character.data.extensions[extensionName][key] = value;
|
||
|
||
try {
|
||
const response = await fetch('/api/characters/merge-attributes', {
|
||
method: 'POST',
|
||
headers: getRequestHeaders(),
|
||
body: JSON.stringify({
|
||
avatar: character.avatar,
|
||
data: { extensions: { [extensionName]: character.data.extensions[extensionName] } }
|
||
})
|
||
});
|
||
|
||
if (!response.ok) throw new Error(`API call failed with status: ${response.status}`);
|
||
console.log(`[${extensionName}] 角色卡设置已更新: ${key} ->`, value);
|
||
} catch (error) {
|
||
console.error(`[${extensionName}] 保存角色数据失败:`, error);
|
||
toastr.error('无法保存角色卡设置,请检查控制台。');
|
||
}
|
||
} else if (SENSITIVE_KEYS.has(key)) {
|
||
// 敏感字段(API Key)经 configManager 写入 localStorage
|
||
configManager.set(key, value);
|
||
} else {
|
||
if (!extension_settings[extensionName]) {
|
||
extension_settings[extensionName] = {};
|
||
}
|
||
extension_settings[extensionName][key] = value;
|
||
saveSettingsDebounced();
|
||
}
|
||
}
|
||
|
||
|
||
function opt_getMergedSettings() {
|
||
const character = characters[this_chid];
|
||
const globalSettings = extension_settings[extensionName] || defaultSettings;
|
||
const characterSettings = character?.data?.extensions?.[extensionName] || {};
|
||
|
||
return { ...globalSettings, ...characterSettings };
|
||
}
|
||
|
||
function bindInputLikeSave(element, handler) {
|
||
if (!element) return;
|
||
element.oninput = handler;
|
||
element.onchange = handler;
|
||
}
|
||
|
||
function syncModelMirror(inputElement, selectElement) {
|
||
if (!inputElement || !selectElement) return;
|
||
const value = inputElement.value || '';
|
||
if (!value) return;
|
||
|
||
let option = Array.from(selectElement.options || []).find(item => item.value === value);
|
||
if (!option) {
|
||
option = new Option(value, value, true, true);
|
||
selectElement.add(option);
|
||
}
|
||
selectElement.value = value;
|
||
}
|
||
|
||
|
||
|
||
function opt_bindSlider(panel, sliderId, displayId) {
|
||
const slider = panel.find(sliderId);
|
||
const display = panel.find(displayId);
|
||
|
||
display.text(slider.val());
|
||
|
||
slider.on('input', function() {
|
||
display.text($(this).val());
|
||
});
|
||
}
|
||
|
||
async function opt_loadWorldbooks(panel) {
|
||
const container = panel.find('#amily2_opt_worldbook_checkbox_list');
|
||
const settings = opt_getMergedSettings();
|
||
const currentSelection = settings.plotOpt_selectedWorldbooks || [];
|
||
container.empty();
|
||
|
||
// 移除旧的搜索框以防重复
|
||
panel.find('#amily2_opt_worldbook_search').remove();
|
||
const searchBox = $(`<input type="text" id="amily2_opt_worldbook_search" class="text_pole" placeholder="搜索世界书..." style="width: 100%; margin-bottom: 10px;">`);
|
||
container.before(searchBox);
|
||
|
||
searchBox.on('input', function() {
|
||
const searchTerm = $(this).val().toLowerCase();
|
||
container.find('.amily2_opt_worldbook_list_item').each(function() {
|
||
const bookName = $(this).find('label').text().toLowerCase();
|
||
if (bookName.includes(searchTerm)) {
|
||
$(this).show();
|
||
} else {
|
||
$(this).hide();
|
||
}
|
||
});
|
||
});
|
||
|
||
try {
|
||
const lorebooks = await safeLorebooks();
|
||
if (!lorebooks || lorebooks.length === 0) {
|
||
container.html('<p class="notes">未找到世界书。</p>');
|
||
return;
|
||
}
|
||
|
||
lorebooks.forEach(name => {
|
||
const bookId = `amily2-opt-wb-check-${name.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
||
const isChecked = currentSelection.includes(name);
|
||
|
||
// Auto Select Logic
|
||
const autoId = `amily2-opt-wb-auto-${name.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
||
const isAuto = (settings.plotOpt_autoSelectWorldbooks || []).includes(name);
|
||
|
||
const item = $(`
|
||
<div class="amily2_opt_worldbook_list_item" style="display: flex; align-items: center; justify-content: space-between; padding-right: 5px;">
|
||
<div style="display: flex; align-items: center;">
|
||
<input type="checkbox" id="${bookId}" value="${name}" ${isChecked ? 'checked' : ''} style="margin-right: 5px;">
|
||
<label for="${bookId}" style="margin-bottom: 0;">${name}</label>
|
||
</div>
|
||
<div style="display: flex; align-items: center;" title="开启后自动加载该世界书所有条目(包括新增)">
|
||
<input type="checkbox" class="amily2_opt_wb_auto_check" id="${autoId}" data-book="${name}" ${isAuto ? 'checked' : ''} style="margin-right: 5px;">
|
||
<label for="${autoId}" style="margin-bottom: 0; font-size: 0.9em; opacity: 0.8; cursor: pointer;">全选</label>
|
||
</div>
|
||
</div>
|
||
`);
|
||
container.append(item);
|
||
});
|
||
} catch (error) {
|
||
console.error(`[${extensionName}] 加载世界书失败:`, error);
|
||
container.html('<p class="notes" style="color:red;">加载世界书列表失败。</p>');
|
||
toastr.error('无法加载世界书列表,请查看控制台。');
|
||
}
|
||
}
|
||
|
||
async function opt_loadWorldbookEntries(panel) {
|
||
const container = panel.find('#amily2_opt_worldbook_entry_list_container');
|
||
const countDisplay = panel.find('#amily2_opt_worldbook_entry_count');
|
||
container.html('<p>加载条目中...</p>');
|
||
countDisplay.text('');
|
||
|
||
// 移除旧的搜索框以防重复
|
||
panel.find('#amily2_opt_worldbook_entry_search').remove();
|
||
const searchBox = $(`<input type="text" id="amily2_opt_worldbook_entry_search" class="text_pole" placeholder="搜索条目..." style="width: 100%; margin-bottom: 10px;">`);
|
||
container.before(searchBox);
|
||
|
||
searchBox.on('input', function() {
|
||
const searchTerm = $(this).val().toLowerCase();
|
||
let visibleCount = 0;
|
||
container.find('.amily2_opt_worldbook_entry_item').each(function() {
|
||
const entryName = $(this).find('label').text().toLowerCase();
|
||
if (entryName.includes(searchTerm)) {
|
||
$(this).show();
|
||
visibleCount++;
|
||
} else {
|
||
$(this).hide();
|
||
}
|
||
});
|
||
const totalEntries = container.find('.amily2_opt_worldbook_entry_item').length;
|
||
countDisplay.text(`显示 ${visibleCount} / ${totalEntries} 条目.`);
|
||
});
|
||
|
||
const settings = opt_getMergedSettings();
|
||
const currentSource = settings.plotOpt_worldbookSource || 'character';
|
||
let bookNames = [];
|
||
|
||
if (currentSource === 'manual') {
|
||
bookNames = settings.plotOpt_selectedWorldbooks || [];
|
||
} else {
|
||
|
||
if (this_chid === -1 || !characters[this_chid]) {
|
||
container.html('<p class="notes">未选择角色。</p>');
|
||
countDisplay.text('');
|
||
return;
|
||
}
|
||
try {
|
||
const charLorebooks = await safeCharLorebooks({ type: 'all' });
|
||
if (charLorebooks.primary) bookNames.push(charLorebooks.primary);
|
||
if (charLorebooks.additional?.length) bookNames.push(...charLorebooks.additional);
|
||
} catch (error) {
|
||
|
||
console.error(`[${extensionName}] 获取角色世界书失败:`, error);
|
||
toastr.error('获取角色世界书失败。');
|
||
container.html('<p class="notes" style="color:red;">获取角色世界书失败。</p>');
|
||
return;
|
||
}
|
||
}
|
||
|
||
const selectedBooks = bookNames;
|
||
let enabledEntries = settings.plotOpt_enabledWorldbookEntries || {};
|
||
let totalEntries = 0;
|
||
let visibleEntries = 0;
|
||
|
||
if (selectedBooks.length === 0) {
|
||
container.html('<p class="notes">请选择一个或多个世界书以查看其条目。</p>');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const allEntries = [];
|
||
for (const bookName of selectedBooks) {
|
||
const entries = await safeLorebookEntries(bookName);
|
||
entries.forEach(entry => {
|
||
allEntries.push({ ...entry, bookName });
|
||
});
|
||
}
|
||
|
||
// 根据用户要求,只显示默认启用的条目
|
||
const enabledOnlyEntries = allEntries.filter(entry => entry.enabled);
|
||
|
||
container.empty();
|
||
|
||
totalEntries = enabledOnlyEntries.length;
|
||
|
||
if (totalEntries === 0) {
|
||
container.html('<p class="notes">所选世界书没有(已启用的)条目。</p>');
|
||
countDisplay.text('0 条目.');
|
||
return;
|
||
}
|
||
|
||
enabledOnlyEntries.sort((a, b) => (a.comment || '').localeCompare(b.comment || '')).forEach(entry => {
|
||
const entryId = `amily2-opt-entry-${entry.bookName.replace(/[^a-zA-Z0-9]/g, '-')}-${entry.uid}`;
|
||
|
||
const isAuto = (settings.plotOpt_autoSelectWorldbooks || []).includes(entry.bookName);
|
||
// If auto is enabled, the entry is forced enabled in logic, so show checked and disabled
|
||
const isChecked = isAuto || (enabledEntries[entry.bookName]?.includes(entry.uid) ?? true);
|
||
const isDisabled = isAuto;
|
||
|
||
const item = $(`
|
||
<div class="amily2_opt_worldbook_entry_item" style="display: flex; align-items: center;">
|
||
<input type="checkbox" id="${entryId}" data-book="${entry.bookName}" data-uid="${entry.uid}" ${isChecked ? 'checked' : ''} ${isDisabled ? 'disabled' : ''} style="margin-right: 5px;">
|
||
<label for="${entryId}" title="世界书: ${entry.bookName}\nUID: ${entry.uid}" style="margin-bottom: 0; ${isDisabled ? 'opacity:0.7;' : ''}">${entry.comment || '无标题条目'} ${isAuto ? '<span style="font-size:0.8em; opacity:0.6;">(全选生效中)</span>' : ''}</label>
|
||
</div>
|
||
`);
|
||
container.append(item);
|
||
});
|
||
|
||
visibleEntries = container.children().length;
|
||
countDisplay.text(`显示 ${visibleEntries} / ${totalEntries} 条目.`);
|
||
|
||
} catch (error) {
|
||
console.error(`[${extensionName}] 加载世界书条目失败:`, error);
|
||
container.html('<p class="notes" style="color:red;">加载条目失败。</p>');
|
||
}
|
||
}
|
||
|
||
|
||
function opt_saveEnabledEntries() {
|
||
const panel = $('#amily2_plot_optimization_panel');
|
||
let enabledEntries = {};
|
||
|
||
panel.find('#amily2_opt_worldbook_entry_list_container input[type="checkbox"]').each(function() {
|
||
const bookName = $(this).data('book');
|
||
const uid = parseInt($(this).data('uid'));
|
||
|
||
if (!enabledEntries[bookName]) {
|
||
enabledEntries[bookName] = [];
|
||
}
|
||
|
||
if ($(this).is(':checked')) {
|
||
enabledEntries[bookName].push(uid);
|
||
}
|
||
});
|
||
|
||
const settings = opt_getMergedSettings();
|
||
|
||
if (settings.plotOpt_worldbookSource === 'manual') {
|
||
const selectedBooks = settings.plotOpt_selectedWorldbooks || [];
|
||
Object.keys(enabledEntries).forEach(bookName => {
|
||
if (!selectedBooks.includes(bookName)) {
|
||
delete enabledEntries[bookName];
|
||
}
|
||
});
|
||
}
|
||
|
||
opt_saveSetting('plotOpt_enabledWorldbookEntries', enabledEntries);
|
||
}
|
||
|
||
|
||
function opt_loadPromptPresets(panel) {
|
||
const presets = extension_settings[extensionName]?.promptPresets || [];
|
||
const select = panel.find('#amily2_opt_prompt_preset_select');
|
||
const settings = opt_getMergedSettings();
|
||
const lastUsedPresetName = settings.plotOpt_lastUsedPresetName;
|
||
|
||
select.empty().append(new Option('-- 选择一个预设 --', ''));
|
||
|
||
presets.forEach(preset => {
|
||
const option = new Option(preset.name, preset.name);
|
||
if (preset.name === lastUsedPresetName) {
|
||
option.selected = true;
|
||
}
|
||
select.append(option);
|
||
});
|
||
}
|
||
|
||
|
||
function opt_saveCurrentPromptsAsPreset(panel) {
|
||
const selectedPresetName = panel.find('#amily2_opt_prompt_preset_select').val();
|
||
let presetName;
|
||
let isOverwriting = false;
|
||
|
||
if (selectedPresetName) {
|
||
if (confirm(`您确定要用当前编辑的提示词覆盖预设 "${selectedPresetName}" 吗?`)) {
|
||
presetName = selectedPresetName;
|
||
isOverwriting = true;
|
||
} else {
|
||
toastr.info('保存操作已取消。');
|
||
return;
|
||
}
|
||
} else {
|
||
presetName = prompt("您正在创建一个新的预设,请输入预设名称:");
|
||
if (!presetName) {
|
||
toastr.info('保存操作已取消。');
|
||
return;
|
||
}
|
||
}
|
||
|
||
const presets = extension_settings[extensionName]?.promptPresets || [];
|
||
const existingPresetIndex = presets.findIndex(p => p.name === presetName);
|
||
|
||
// Ensure the cache is up-to-date before saving
|
||
const currentEditorPromptKey = panel.find('#amily2_opt_prompt_selector').val();
|
||
promptCache[currentEditorPromptKey] = panel.find('#amily2_opt_prompt_editor').val();
|
||
|
||
const currentSettings = extension_settings[extensionName] || {};
|
||
const newPresetData = {
|
||
name: presetName,
|
||
mainPrompt: promptCache.main,
|
||
systemPrompt: promptCache.system,
|
||
finalSystemDirective: promptCache.final_system,
|
||
concurrentMainPrompt: currentSettings.plotOpt_concurrentMainPrompt || '',
|
||
concurrentSystemPrompt: currentSettings.plotOpt_concurrentSystemPrompt || '',
|
||
rateMain: parseFloat(panel.find('#amily2_opt_rate_main').val()),
|
||
ratePersonal: parseFloat(panel.find('#amily2_opt_rate_personal').val()),
|
||
rateErotic: parseFloat(panel.find('#amily2_opt_rate_erotic').val()),
|
||
rateCuckold: parseFloat(panel.find('#amily2_opt_rate_cuckold').val())
|
||
};
|
||
|
||
if (existingPresetIndex !== -1) {
|
||
presets[existingPresetIndex] = newPresetData;
|
||
toastr.success(`预设 "${presetName}" 已成功覆盖。`);
|
||
} else {
|
||
presets.push(newPresetData);
|
||
toastr.success(`新预设 "${presetName}" 已成功创建。`);
|
||
}
|
||
opt_saveSetting('promptPresets', presets);
|
||
|
||
opt_loadPromptPresets(panel);
|
||
setTimeout(() => {
|
||
panel.find('#amily2_opt_prompt_preset_select').val(presetName).trigger('change', { isAutomatic: false });
|
||
}, 0);
|
||
}
|
||
|
||
function opt_deleteSelectedPreset(panel) {
|
||
const select = panel.find('#amily2_opt_prompt_preset_select');
|
||
const selectedName = select.val();
|
||
|
||
if (!selectedName) {
|
||
toastr.warning('没有选择任何预设。');
|
||
return;
|
||
}
|
||
|
||
if (!confirm(`确定要删除预设 "${selectedName}" 吗?`)) {
|
||
return;
|
||
}
|
||
|
||
const presets = extension_settings[extensionName]?.promptPresets || [];
|
||
const indexToDelete = presets.findIndex(p => p.name === selectedName);
|
||
|
||
if (indexToDelete > -1) {
|
||
presets.splice(indexToDelete, 1);
|
||
opt_saveSetting('promptPresets', presets);
|
||
toastr.success(`预设 "${selectedName}" 已被删除。`);
|
||
} else {
|
||
toastr.error('找不到要删除的预设,操作可能已过期。');
|
||
}
|
||
|
||
opt_loadPromptPresets(panel);
|
||
select.trigger('change');
|
||
}
|
||
|
||
function opt_exportPromptPresets() {
|
||
const select = $('#amily2_opt_prompt_preset_select');
|
||
const selectedName = select.val();
|
||
|
||
if (!selectedName) {
|
||
toastr.info('请先从下拉菜单中选择一个要导出的预设。');
|
||
return;
|
||
}
|
||
|
||
const presets = extension_settings[extensionName]?.promptPresets || [];
|
||
const selectedPreset = presets.find(p => p.name === selectedName);
|
||
|
||
if (!selectedPreset) {
|
||
toastr.error('找不到选中的预设,请刷新页面后重试。');
|
||
return;
|
||
}
|
||
|
||
const dataToExport = [selectedPreset];
|
||
const dataStr = JSON.stringify(dataToExport, null, 2);
|
||
const blob = new Blob([dataStr], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `amily2_opt_preset_${selectedName.replace(/[^a-z0-9]/gi, '_')}.json`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
toastr.success(`预设 "${selectedName}" 已成功导出。`);
|
||
}
|
||
|
||
|
||
function opt_importPromptPresets(file, panel) {
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
try {
|
||
const importedPresets = JSON.parse(e.target.result);
|
||
|
||
if (!Array.isArray(importedPresets)) {
|
||
throw new Error('JSON文件格式不正确,根节点必须是一个数组。');
|
||
}
|
||
|
||
let currentPresets = extension_settings[extensionName]?.promptPresets || [];
|
||
let importedCount = 0;
|
||
let overwrittenCount = 0;
|
||
|
||
importedPresets.forEach(preset => {
|
||
if (preset && typeof preset.name === 'string' && preset.name.length > 0) {
|
||
const presetData = {
|
||
name: preset.name,
|
||
mainPrompt: preset.mainPrompt || '',
|
||
systemPrompt: preset.systemPrompt || '',
|
||
finalSystemDirective: preset.finalSystemDirective || '',
|
||
concurrentMainPrompt: preset.concurrentMainPrompt || '',
|
||
concurrentSystemPrompt: preset.concurrentSystemPrompt || '',
|
||
rateMain: preset.rateMain ?? 1.0,
|
||
ratePersonal: preset.ratePersonal ?? 1.0,
|
||
rateErotic: preset.rateErotic ?? 1.0,
|
||
rateCuckold: preset.rateCuckold ?? 1.0
|
||
};
|
||
|
||
const existingIndex = currentPresets.findIndex(p => p.name === preset.name);
|
||
|
||
if (existingIndex !== -1) {
|
||
currentPresets[existingIndex] = presetData;
|
||
overwrittenCount++;
|
||
} else {
|
||
currentPresets.push(presetData);
|
||
importedCount++;
|
||
}
|
||
}
|
||
});
|
||
|
||
if (importedCount > 0 || overwrittenCount > 0) {
|
||
const selectedPresetBeforeImport = panel.find('#amily2_opt_prompt_preset_select').val();
|
||
|
||
opt_saveSetting('promptPresets', currentPresets);
|
||
opt_loadPromptPresets(panel);
|
||
panel.find('#amily2_opt_prompt_preset_select').val(selectedPresetBeforeImport);
|
||
panel.find('#amily2_opt_prompt_preset_select').trigger('change');
|
||
|
||
let messages = [];
|
||
if (importedCount > 0) messages.push(`成功导入 ${importedCount} 个新预设。`);
|
||
if (overwrittenCount > 0) messages.push(`成功覆盖 ${overwrittenCount} 个同名预设。`);
|
||
toastr.success(messages.join(' '));
|
||
} else {
|
||
toastr.warning('未找到可导入的有效预设。');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error(`[${extensionName}] 导入预设失败:`, error);
|
||
toastr.error(`导入失败: ${error.message}`, '错误');
|
||
} finally {
|
||
panel.find('#amily2_opt_preset_file_input').val('');
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
}
|
||
|
||
function opt_loadSettings(panel) {
|
||
const settings = opt_getMergedSettings();
|
||
|
||
panel.find('#amily2_opt_enabled').prop('checked', settings.plotOpt_enabled);
|
||
|
||
// Handle table enabled setting which can be boolean (legacy) or string
|
||
let tableEnabledValue = settings.plotOpt_tableEnabled;
|
||
if (tableEnabledValue === true) {
|
||
tableEnabledValue = 'main';
|
||
} else if (tableEnabledValue === false || tableEnabledValue === undefined) {
|
||
tableEnabledValue = 'disabled';
|
||
}
|
||
panel.find('#amily2_opt_table_enabled').val(tableEnabledValue);
|
||
|
||
panel.find('#amily2_opt_ejs_enabled').prop('checked', settings.plotOpt_ejsEnabled);
|
||
panel.find(`input[name="amily2_opt_worldbook_source"][value="${settings.plotOpt_worldbookSource || 'character'}"]`).prop('checked', true);
|
||
panel.find('#amily2_opt_worldbook_enabled').prop('checked', settings.plotOpt_worldbookEnabled);
|
||
panel.find('#amily2_opt_new_memory_logic_enabled').prop('checked', settings.plotOpt_newMemoryLogicEnabled);
|
||
panel.find('#amily2_opt_top_p').val(settings.plotOpt_top_p);
|
||
panel.find('#amily2_opt_presence_penalty').val(settings.plotOpt_presence_penalty);
|
||
panel.find('#amily2_opt_frequency_penalty').val(settings.plotOpt_frequency_penalty);
|
||
const contextLimit = settings.plotOpt_contextLimit ?? settings.plotOpt_contextTurnCount ?? defaultSettings.plotOpt_contextLimit;
|
||
panel.find('#amily2_opt_worldbook_char_limit').val(settings.plotOpt_worldbookCharLimit);
|
||
panel.find('#amily2_opt_context_limit').val(contextLimit);
|
||
|
||
panel.find('#amily2_opt_rate_main').val(settings.plotOpt_rateMain);
|
||
panel.find('#amily2_opt_rate_personal').val(settings.plotOpt_ratePersonal);
|
||
panel.find('#amily2_opt_rate_erotic').val(settings.plotOpt_rateErotic);
|
||
panel.find('#amily2_opt_rate_cuckold').val(settings.plotOpt_rateCuckold);
|
||
|
||
opt_loadPromptPresets(panel);
|
||
|
||
const lastUsedPresetName = settings.plotOpt_lastUsedPresetName;
|
||
|
||
const initFunc = panel.data('initAmily2PromptEditor');
|
||
if (initFunc) {
|
||
initFunc();
|
||
}
|
||
|
||
// After loading presets and initializing the editor, trigger a "light" change event
|
||
// to update UI elements like the delete button, without reloading all the data.
|
||
if (lastUsedPresetName && panel.find('#amily2_opt_prompt_preset_select').val() === lastUsedPresetName) {
|
||
setTimeout(() => {
|
||
panel.find('#amily2_opt_prompt_preset_select').trigger('change', { isAutomatic: true, noLoad: true });
|
||
}, 0);
|
||
}
|
||
|
||
opt_updateWorldbookSourceVisibility(panel, settings.plotOpt_worldbookSource || 'character');
|
||
|
||
opt_bindSlider(panel, '#amily2_opt_top_p', '#amily2_opt_top_p_value');
|
||
opt_bindSlider(panel, '#amily2_opt_presence_penalty', '#amily2_opt_presence_penalty_value');
|
||
opt_bindSlider(panel, '#amily2_opt_frequency_penalty', '#amily2_opt_frequency_penalty_value');
|
||
opt_bindSlider(panel, '#amily2_opt_worldbook_char_limit', '#amily2_opt_worldbook_char_limit_value');
|
||
opt_bindSlider(panel, '#amily2_opt_context_limit', '#amily2_opt_context_limit_value');
|
||
|
||
opt_loadWorldbooks(panel).then(() => {
|
||
opt_loadWorldbookEntries(panel);
|
||
});
|
||
|
||
}
|
||
|
||
|
||
function bindConcurrentApiEvents() {
|
||
const concurrentToggle = document.getElementById('amily2_plotOpt_concurrentEnabled');
|
||
const concurrentContent = document.getElementById('amily2_concurrent_content');
|
||
|
||
if (!concurrentToggle || !concurrentContent) return;
|
||
|
||
// plotOptConc 槽分配 profile 后,maxTokens 由 profile 权威控制(T-006 informational 化)
|
||
watchProfileSliderGuard('plotOptConc', ['#amily2_plotOpt_concurrentMaxTokens']);
|
||
|
||
const settings = extension_settings[extensionName] || {};
|
||
|
||
// Initial Load
|
||
concurrentToggle.checked = settings.plotOpt_concurrentEnabled ?? false;
|
||
concurrentContent.style.display = concurrentToggle.checked ? 'grid' : 'none';
|
||
|
||
const fields = [
|
||
{ id: 'amily2_plotOpt_concurrentApiProvider', key: 'plotOpt_concurrentApiProvider' },
|
||
{ id: 'amily2_plotOpt_concurrentApiUrl', key: 'plotOpt_concurrentApiUrl' },
|
||
{ id: 'amily2_plotOpt_concurrentApiKey', key: 'plotOpt_concurrentApiKey', sensitive: true },
|
||
{ id: 'amily2_plotOpt_concurrentModel', key: 'plotOpt_concurrentModel' }
|
||
];
|
||
|
||
fields.forEach(field => {
|
||
const element = document.getElementById(field.id);
|
||
if (element) {
|
||
// 敏感字段(API Key)从 configManager(localStorage)读取
|
||
element.value = field.sensitive
|
||
? (configManager.get(field.key) || '')
|
||
: (settings[field.key] || '');
|
||
}
|
||
});
|
||
|
||
// Button Listeners
|
||
const testButton = document.getElementById('amily2_plotOpt_concurrent_test_connection');
|
||
if (testButton) {
|
||
testButton.addEventListener('click', async () => {
|
||
const button = $(testButton);
|
||
const originalHtml = button.html();
|
||
button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 测试中');
|
||
try {
|
||
await testConcurrentApiConnection();
|
||
} finally {
|
||
button.prop('disabled', false).html(originalHtml);
|
||
}
|
||
});
|
||
}
|
||
|
||
const fetchButton = document.getElementById('amily2_plotOpt_concurrent_fetch_models');
|
||
const modelInput = document.getElementById('amily2_plotOpt_concurrentModel');
|
||
const modelSelect = document.getElementById('amily2_plotOpt_concurrentModel_select');
|
||
|
||
if (fetchButton && modelInput && modelSelect) {
|
||
fetchButton.addEventListener('click', async () => {
|
||
const button = $(fetchButton);
|
||
const originalHtml = button.html();
|
||
button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> 获取中');
|
||
try {
|
||
const models = await fetchConcurrentModels();
|
||
if (models && models.length > 0) {
|
||
modelSelect.innerHTML = '<option value="">-- 选择一个模型 --</option>';
|
||
models.forEach(model => {
|
||
const option = document.createElement('option');
|
||
option.value = model.id;
|
||
option.textContent = model.name;
|
||
if (model.id === modelInput.value) {
|
||
option.selected = true;
|
||
}
|
||
modelSelect.appendChild(option);
|
||
});
|
||
modelSelect.style.display = 'block';
|
||
modelInput.style.display = 'none';
|
||
toastr.success(`成功获取 ${models.length} 个并发模型`, '获取模型成功');
|
||
} else {
|
||
toastr.warning('未获取到任何并发模型。', '获取模型');
|
||
}
|
||
} catch (error) {
|
||
toastr.error(`获取并发模型失败: ${error.message}`, '获取模型失败');
|
||
} finally {
|
||
button.prop('disabled', false).html(originalHtml);
|
||
}
|
||
});
|
||
|
||
modelSelect.addEventListener('change', function() {
|
||
const selectedModel = this.value;
|
||
if (selectedModel) {
|
||
modelInput.value = selectedModel;
|
||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||
extension_settings[extensionName].plotOpt_concurrentModel = selectedModel;
|
||
saveSettingsDebounced();
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
// Event Listeners
|
||
concurrentToggle.addEventListener('change', function() {
|
||
const isEnabled = this.checked;
|
||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||
extension_settings[extensionName].plotOpt_concurrentEnabled = isEnabled;
|
||
saveSettingsDebounced();
|
||
concurrentContent.style.display = isEnabled ? 'grid' : 'none';
|
||
});
|
||
|
||
fields.forEach(field => {
|
||
const element = document.getElementById(field.id);
|
||
if (element) {
|
||
const saveField = function() {
|
||
if (field.sensitive) {
|
||
configManager.set(field.key, this.value);
|
||
} else {
|
||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||
extension_settings[extensionName][field.key] = this.value;
|
||
saveSettingsDebounced();
|
||
if (field.key === 'plotOpt_concurrentModel') {
|
||
syncModelMirror(
|
||
document.getElementById('amily2_plotOpt_concurrentModel'),
|
||
document.getElementById('amily2_plotOpt_concurrentModel_select')
|
||
);
|
||
}
|
||
}
|
||
};
|
||
bindInputLikeSave(element, saveField);
|
||
}
|
||
});
|
||
|
||
// Slider Bindings
|
||
const sliderFields = [
|
||
{ id: 'amily2_plotOpt_concurrentMaxTokens', key: 'plotOpt_concurrentMaxTokens', defaultValue: 8100 }
|
||
];
|
||
|
||
sliderFields.forEach(field => {
|
||
const slider = document.getElementById(field.id);
|
||
const display = document.getElementById(field.id + '_value');
|
||
if (slider && display) {
|
||
const value = settings[field.key] || field.defaultValue;
|
||
slider.value = value;
|
||
display.textContent = value;
|
||
|
||
slider.addEventListener('input', function() {
|
||
const newValue = parseInt(this.value, 10);
|
||
display.textContent = newValue;
|
||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||
extension_settings[extensionName][field.key] = newValue;
|
||
saveSettingsDebounced();
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
function bindConcurrentPromptEvents() {
|
||
const panel = $('#sinan-prompt-settings-tab');
|
||
if (panel.length === 0) return;
|
||
|
||
const selector = panel.find('#amily2_concurrent_prompt_selector');
|
||
const editor = panel.find('#amily2_concurrent_prompt_editor');
|
||
const resetButton = panel.find('#amily2_opt_reset_concurrent_prompt');
|
||
|
||
const promptMap = {
|
||
main: 'plotOpt_concurrentMainPrompt',
|
||
system: 'plotOpt_concurrentSystemPrompt'
|
||
};
|
||
|
||
function updateConcurrentEditor() {
|
||
const settings = extension_settings[extensionName] || {};
|
||
const selectedKey = selector.val();
|
||
const settingKey = promptMap[selectedKey];
|
||
editor.val(settings[settingKey] || '');
|
||
}
|
||
|
||
// Initial load
|
||
updateConcurrentEditor();
|
||
|
||
// Event Listeners
|
||
selector.on('change', updateConcurrentEditor);
|
||
|
||
editor.on('input', function() {
|
||
const selectedKey = selector.val();
|
||
const settingKey = promptMap[selectedKey];
|
||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||
extension_settings[extensionName][settingKey] = $(this).val();
|
||
saveSettingsDebounced();
|
||
});
|
||
|
||
resetButton.on('click', function() {
|
||
const selectedKey = selector.val();
|
||
const settingKey = promptMap[selectedKey];
|
||
const defaultValue = defaultSettings[settingKey] || '';
|
||
|
||
if (confirm(`您确定要将 "${selector.find('option:selected').text()}" 恢复为默认值吗?`)) {
|
||
editor.val(defaultValue);
|
||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||
extension_settings[extensionName][settingKey] = defaultValue;
|
||
saveSettingsDebounced();
|
||
toastr.success('并发提示词已成功恢复为默认值。');
|
||
}
|
||
});
|
||
}
|
||
|
||
function opt_loadConcurrentWorldbookSettings() {
|
||
const panel = $('#amily2_plot_optimization_panel');
|
||
if (panel.length === 0) return;
|
||
|
||
const settings = extension_settings[extensionName] || {};
|
||
const enabledCheckbox = panel.find('#amily2_plotOpt_concurrentWorldbookEnabled');
|
||
const sourceRadios = panel.find('input[name="amily2_plotOpt_concurrentWorldbook_source"]');
|
||
const charLimitSlider = panel.find('#amily2_plotOpt_concurrentWorldbookCharLimit');
|
||
const charLimitValue = panel.find('#amily2_plotOpt_concurrentWorldbookCharLimit_value');
|
||
|
||
enabledCheckbox.prop('checked', settings.plotOpt_concurrentWorldbookEnabled ?? true);
|
||
const currentSource = settings.plotOpt_concurrentWorldbookSource || 'character';
|
||
panel.find(`input[name="amily2_plotOpt_concurrentWorldbook_source"][value="${currentSource}"]`).prop('checked', true);
|
||
charLimitSlider.val(settings.plotOpt_concurrentWorldbookCharLimit || 60000);
|
||
charLimitValue.text(charLimitSlider.val());
|
||
|
||
// This will also trigger the visibility update
|
||
enabledCheckbox.trigger('change');
|
||
}
|
||
|
||
function bindConcurrentWorldbookEvents() {
|
||
const panel = $('#amily2_plot_optimization_panel');
|
||
if (panel.length === 0) return;
|
||
|
||
const settings = extension_settings[extensionName] || {};
|
||
const enabledCheckbox = panel.find('#amily2_plotOpt_concurrentWorldbookEnabled');
|
||
const contentDiv = panel.find('#amily2_concurrent_worldbook_content');
|
||
const sourceRadios = panel.find('input[name="amily2_plotOpt_concurrentWorldbook_source"]');
|
||
const manualSelectWrapper = panel.find('#amily2_plotOpt_concurrent_worldbook_select_wrapper');
|
||
const refreshButton = panel.find('#amily2_plotOpt_concurrent_refresh_worldbooks');
|
||
const bookListContainer = panel.find('#amily2_plotOpt_concurrent_worldbook_checkbox_list');
|
||
const charLimitSlider = panel.find('#amily2_plotOpt_concurrentWorldbookCharLimit');
|
||
const charLimitValue = panel.find('#amily2_plotOpt_concurrentWorldbookCharLimit_value');
|
||
|
||
function updateVisibility() {
|
||
const isEnabled = enabledCheckbox.is(':checked');
|
||
contentDiv.css('display', isEnabled ? 'block' : 'none');
|
||
if (isEnabled) {
|
||
const source = panel.find('input[name="amily2_plotOpt_concurrentWorldbook_source"]:checked').val();
|
||
manualSelectWrapper.css('display', source === 'manual' ? 'block' : 'none');
|
||
}
|
||
}
|
||
|
||
async function loadConcurrentWorldbooks() {
|
||
bookListContainer.html('<p class="notes">加载中...</p>');
|
||
try {
|
||
const lorebooks = await safeLorebooks();
|
||
bookListContainer.empty();
|
||
if (!lorebooks || lorebooks.length === 0) {
|
||
bookListContainer.html('<p class="notes">未找到世界书。</p>');
|
||
return;
|
||
}
|
||
const selectedBooks = settings.plotOpt_concurrentSelectedWorldbooks || [];
|
||
const autoSelectedBooks = settings.plotOpt_concurrentAutoSelectWorldbooks || [];
|
||
lorebooks.forEach(name => {
|
||
const bookId = `amily2-opt-concurrent-wb-check-${name.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
||
const autoId = `amily2-opt-concurrent-wb-auto-${name.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
||
const isChecked = selectedBooks.includes(name);
|
||
const isAuto = autoSelectedBooks.includes(name);
|
||
const item = $(`
|
||
<div class="amily2_opt_worldbook_list_item" style="display: flex; align-items: center; justify-content: space-between; padding-right: 5px;">
|
||
<div style="display: flex; align-items: center;">
|
||
<input type="checkbox" id="${bookId}" value="${name}" ${isChecked ? 'checked' : ''} style="margin-right: 5px;">
|
||
<label for="${bookId}" style="margin-bottom: 0;">${name}</label>
|
||
</div>
|
||
<div style="display: flex; align-items: center;" title="开启后自动加载该世界书所有条目(包括新增)">
|
||
<input type="checkbox" class="amily2_opt_concurrent_wb_auto_check" id="${autoId}" data-book="${name}" ${isAuto ? 'checked' : ''} style="margin-right: 5px;">
|
||
<label for="${autoId}" style="margin-bottom: 0; font-size: 0.9em; opacity: 0.8; cursor: pointer;">全选</label>
|
||
</div>
|
||
</div>
|
||
`);
|
||
bookListContainer.append(item);
|
||
});
|
||
} catch (error) {
|
||
console.error(`[${extensionName}] 加载并发世界书失败:`, error);
|
||
bookListContainer.html('<p class="notes" style="color:red;">加载世界书列表失败。</p>');
|
||
}
|
||
}
|
||
|
||
// Initial State is now handled by opt_loadConcurrentWorldbookSettings
|
||
updateVisibility();
|
||
if (panel.find('input[name="amily2_plotOpt_concurrentWorldbook_source"]:checked').val() === 'manual') {
|
||
loadConcurrentWorldbooks();
|
||
}
|
||
|
||
// Event Listeners
|
||
enabledCheckbox.on('change', function() {
|
||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||
extension_settings[extensionName].plotOpt_concurrentWorldbookEnabled = this.checked;
|
||
saveSettingsDebounced();
|
||
updateVisibility();
|
||
});
|
||
|
||
sourceRadios.on('change', function() {
|
||
if (this.checked) {
|
||
const source = $(this).val();
|
||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||
extension_settings[extensionName].plotOpt_concurrentWorldbookSource = source;
|
||
saveSettingsDebounced();
|
||
updateVisibility();
|
||
if (source === 'manual') {
|
||
loadConcurrentWorldbooks();
|
||
}
|
||
}
|
||
});
|
||
|
||
refreshButton.on('click', loadConcurrentWorldbooks);
|
||
|
||
bookListContainer.on('change', 'input[type="checkbox"]:not(.amily2_opt_concurrent_wb_auto_check)', function() {
|
||
const selected = [];
|
||
bookListContainer.find('input[type="checkbox"]:not(.amily2_opt_concurrent_wb_auto_check):checked').each(function() {
|
||
selected.push($(this).val());
|
||
});
|
||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||
extension_settings[extensionName].plotOpt_concurrentSelectedWorldbooks = selected;
|
||
saveSettingsDebounced();
|
||
});
|
||
|
||
bookListContainer.on('change', '.amily2_opt_concurrent_wb_auto_check', function() {
|
||
const autoSelected = [];
|
||
bookListContainer.find('.amily2_opt_concurrent_wb_auto_check:checked').each(function() {
|
||
autoSelected.push($(this).data('book'));
|
||
});
|
||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||
extension_settings[extensionName].plotOpt_concurrentAutoSelectWorldbooks = autoSelected;
|
||
saveSettingsDebounced();
|
||
});
|
||
|
||
charLimitSlider.on('input', function() {
|
||
const value = $(this).val();
|
||
charLimitValue.text(value);
|
||
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
|
||
extension_settings[extensionName].plotOpt_concurrentWorldbookCharLimit = parseInt(value, 10);
|
||
saveSettingsDebounced();
|
||
});
|
||
}
|
||
|
||
function opt_purgeGarbageKeys() {
|
||
const store = extension_settings[extensionName];
|
||
if (!store) return;
|
||
let removed = 0;
|
||
for (const key of Object.keys(store)) {
|
||
// 历史 bug 造成的污染 key:handleSettingChange 误把世界书/条目复选框当作设置项,
|
||
// 生成形如 plotOpt_amily2-opt-wb-*、plotOpt_amily2-opt-entry-*、plotOpt_amily2-opt-concurrent-wb-* 的键
|
||
if (/^plotOpt_amily2-opt-/.test(key)) {
|
||
delete store[key];
|
||
removed++;
|
||
}
|
||
}
|
||
if (removed > 0) {
|
||
console.log(`[${extensionName}] 清理残留的 ${removed} 条无效 plotOpt_* 设置键。`);
|
||
saveSettingsDebounced();
|
||
}
|
||
}
|
||
|
||
// ========== 自定义记忆块(memory-blocks Phase 2) ==========
|
||
|
||
// 该面板管理的块固定挂在剧情优化流水线;其他 context(如战斗系统)由各自模块注册
|
||
const MEMORY_BLOCK_CONTEXT = 'plotOptimization';
|
||
|
||
function opt_renderCustomBlocks(panel) {
|
||
const list = panel.find('#amily2_opt_custom_blocks_list');
|
||
if (list.length === 0) return;
|
||
|
||
const blocks = listCustomBlocks(MEMORY_BLOCK_CONTEXT);
|
||
if (blocks.length === 0) {
|
||
list.html('<div style="opacity: 0.6; font-style: italic; padding: 4px 0;">尚无自定义块。</div>');
|
||
return;
|
||
}
|
||
|
||
const rows = blocks.map(b => {
|
||
const typeBadge = b.generator?.type === 'ai_call'
|
||
? '<span style="font-size: 0.8em; padding: 1px 6px; border-radius: 3px; background: rgba(88,166,255,0.25);">AI 调用</span>'
|
||
: '<span style="font-size: 0.8em; padding: 1px 6px; border-radius: 3px; background: rgba(120,200,120,0.25);">静态</span>';
|
||
return `
|
||
<div class="amily2-custom-block-row" data-id="${escapeHTML(b.id)}" style="display: flex; align-items: center; gap: 8px; padding: 6px 8px; margin-bottom: 4px; background: rgba(0,0,0,0.15); border-radius: 4px;">
|
||
<input type="checkbox" class="mb-enabled" ${b.enabled !== false ? 'checked' : ''} title="启用/停用此块">
|
||
<span style="flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||
<b>${escapeHTML(b.name || '(未命名)')}</b>
|
||
<code style="margin-left: 6px;">${escapeHTML(b.placeholder)}</code>
|
||
</span>
|
||
${typeBadge}
|
||
<button class="menu_button mb-edit" title="编辑" style="padding: 2px 8px;"><i class="fa-solid fa-pen"></i></button>
|
||
<button class="menu_button mb-delete" title="删除" style="padding: 2px 8px;"><i class="fa-solid fa-trash-alt"></i></button>
|
||
</div>`;
|
||
});
|
||
list.html(rows.join(''));
|
||
}
|
||
|
||
function opt_showCustomBlockModal(panel, blockId) {
|
||
const existing = blockId ? getCustomBlock(blockId) : null;
|
||
const gen = existing?.generator || {};
|
||
const isAiCall = gen.type === 'ai_call';
|
||
|
||
// API 槽下拉:仅列出 chat 类型功能槽
|
||
const slotOptions = Object.entries(SLOTS)
|
||
.filter(([, def]) => def.type === 'chat')
|
||
.map(([key, def]) => `<option value="${key}" ${key === (gen.apiSlot || 'main') ? 'selected' : ''}>${escapeHTML(def.label)} (${key})</option>`)
|
||
.join('');
|
||
|
||
const formHtml = `
|
||
<div class="amily2-mb-form" style="display: flex; flex-direction: column; gap: 12px;">
|
||
<label>显示名称
|
||
<input id="mb_name" class="text_pole" type="text" value="${escapeHTML(existing?.name || '')}" placeholder="例如:战况摘要">
|
||
</label>
|
||
<label>占位符(在主/拦截提示词中按字面量匹配替换)
|
||
<input id="mb_placeholder" class="text_pole" type="text" value="${escapeHTML(existing?.placeholder || '')}" placeholder="例如:{{combat_state}} 或 myBlock1">
|
||
</label>
|
||
<label>生成方式
|
||
<select id="mb_type" class="text_pole">
|
||
<option value="static" ${!isAiCall ? 'selected' : ''}>静态内容</option>
|
||
<option value="ai_call" ${isAiCall ? 'selected' : ''}>AI 调用</option>
|
||
</select>
|
||
</label>
|
||
<div id="mb_static_fields" style="${isAiCall ? 'display: none;' : ''}">
|
||
<label>静态内容
|
||
<textarea id="mb_static_value" class="text_pole" rows="4">${escapeHTML(gen.value !== undefined ? String(gen.value) : '')}</textarea>
|
||
</label>
|
||
</div>
|
||
<div id="mb_ai_fields" style="display: ${isAiCall ? 'flex' : 'none'}; flex-direction: column; gap: 12px;">
|
||
<label>API 槽(使用该功能槽的连接配置独立请求一次)
|
||
<select id="mb_api_slot" class="text_pole">${slotOptions}</select>
|
||
</label>
|
||
<label>系统提示词(可选)
|
||
<textarea id="mb_system_prompt" class="text_pole" rows="3">${escapeHTML(gen.systemPrompt || '')}</textarea>
|
||
</label>
|
||
<label>用户提示词(必填)
|
||
<textarea id="mb_prompt_template" class="text_pole" rows="5">${escapeHTML(gen.promptTemplate || '')}</textarea>
|
||
</label>
|
||
<label>提取标签(可选,只取回复中 <标签>...</标签> 的内容;标签缺失时回退完整回复)
|
||
<input id="mb_extract_tag" class="text_pole" type="text" value="${escapeHTML(gen.extractTag || '')}" placeholder="例如:result">
|
||
</label>
|
||
</div>
|
||
</div>`;
|
||
|
||
showHtmlModal(existing ? '编辑记忆块' : '新增记忆块', formHtml, {
|
||
okText: '保存',
|
||
onShow: (dialog) => {
|
||
dialog.find('#mb_type').on('change', function () {
|
||
const aiMode = $(this).val() === 'ai_call';
|
||
dialog.find('#mb_static_fields').toggle(!aiMode);
|
||
dialog.find('#mb_ai_fields').css('display', aiMode ? 'flex' : 'none');
|
||
});
|
||
},
|
||
onOk: (dialog) => {
|
||
const placeholder = String(dialog.find('#mb_placeholder').val() || '').trim();
|
||
if (!placeholder) {
|
||
toastr.warning('占位符不能为空。');
|
||
return false;
|
||
}
|
||
const conflict = listCustomBlocks(MEMORY_BLOCK_CONTEXT)
|
||
.find(b => b.placeholder === placeholder && b.id !== blockId);
|
||
if (conflict) {
|
||
toastr.warning(`占位符 "${placeholder}" 已被块 "${conflict.name || conflict.id}" 占用。`);
|
||
return false;
|
||
}
|
||
|
||
const type = dialog.find('#mb_type').val();
|
||
let generator;
|
||
if (type === 'ai_call') {
|
||
const promptTemplate = String(dialog.find('#mb_prompt_template').val() || '');
|
||
if (!promptTemplate.trim()) {
|
||
toastr.warning('AI 调用块的用户提示词不能为空。');
|
||
return false;
|
||
}
|
||
generator = {
|
||
type: 'ai_call',
|
||
apiSlot: dialog.find('#mb_api_slot').val() || 'main',
|
||
promptTemplate,
|
||
};
|
||
const systemPrompt = String(dialog.find('#mb_system_prompt').val() || '');
|
||
if (systemPrompt.trim()) generator.systemPrompt = systemPrompt;
|
||
const extractTag = String(dialog.find('#mb_extract_tag').val() || '').trim();
|
||
if (extractTag) generator.extractTag = extractTag;
|
||
} else {
|
||
generator = { type: 'static', value: String(dialog.find('#mb_static_value').val() || '') };
|
||
}
|
||
|
||
const patch = {
|
||
name: String(dialog.find('#mb_name').val() || '').trim(),
|
||
placeholder,
|
||
context: MEMORY_BLOCK_CONTEXT,
|
||
generator,
|
||
};
|
||
|
||
try {
|
||
if (existing) {
|
||
updateCustomBlock(blockId, patch);
|
||
toastr.success('记忆块已更新。');
|
||
} else {
|
||
addCustomBlock(patch);
|
||
toastr.success('记忆块已创建。');
|
||
}
|
||
} catch (error) {
|
||
toastr.error(`保存失败: ${error.message}`);
|
||
return false;
|
||
}
|
||
opt_renderCustomBlocks(panel);
|
||
},
|
||
});
|
||
}
|
||
|
||
function bindCustomBlockEvents(panel) {
|
||
// settings → registry 重放一次,确保面板与执行器看到同一份块清单
|
||
syncCustomBlocksFromSettings();
|
||
opt_renderCustomBlocks(panel);
|
||
|
||
panel.on('click', '#amily2_opt_add_custom_block', () => opt_showCustomBlockModal(panel, null));
|
||
|
||
panel.on('click', '.amily2-custom-block-row .mb-edit', function () {
|
||
opt_showCustomBlockModal(panel, $(this).closest('[data-id]').attr('data-id'));
|
||
});
|
||
|
||
panel.on('click', '.amily2-custom-block-row .mb-delete', function () {
|
||
const row = $(this).closest('[data-id]');
|
||
const id = row.attr('data-id');
|
||
const block = getCustomBlock(id);
|
||
if (!confirm(`确定删除记忆块 "${block?.name || id}"?`)) return;
|
||
deleteCustomBlock(id);
|
||
opt_renderCustomBlocks(panel);
|
||
toastr.success('记忆块已删除。');
|
||
});
|
||
|
||
panel.on('change', '.amily2-custom-block-row .mb-enabled', function () {
|
||
const id = $(this).closest('[data-id]').attr('data-id');
|
||
updateCustomBlock(id, { enabled: this.checked });
|
||
});
|
||
}
|
||
|
||
export function initializePlotOptimizationBindings() {
|
||
const panel = $('#amily2_plot_optimization_panel');
|
||
if (panel.length === 0 || panel.data('events-bound')) {
|
||
return;
|
||
}
|
||
|
||
opt_purgeGarbageKeys();
|
||
|
||
// Tab switching logic
|
||
panel.find('.sinan-navigation-deck').on('click', '.sinan-nav-item', function() {
|
||
const tabButton = $(this);
|
||
const tabName = tabButton.data('tab');
|
||
const contentWrapper = panel.find('.sinan-content-wrapper');
|
||
|
||
// Deactivate all tabs and panes
|
||
panel.find('.sinan-nav-item').removeClass('active');
|
||
contentWrapper.find('.sinan-tab-pane').removeClass('active');
|
||
|
||
// Activate the clicked tab and corresponding pane
|
||
tabButton.addClass('active');
|
||
contentWrapper.find(`#sinan-${tabName}-tab`).addClass('active');
|
||
});
|
||
|
||
// Unified prompt editor logic
|
||
function updateEditorFromCache() {
|
||
const selectedPrompt = panel.find('#amily2_opt_prompt_selector').val();
|
||
if (selectedPrompt) {
|
||
panel.find('#amily2_opt_prompt_editor').val(promptCache[selectedPrompt]);
|
||
}
|
||
}
|
||
|
||
// Make it available for opt_loadSettings
|
||
panel.data('initAmily2PromptEditor', function() {
|
||
const settings = opt_getMergedSettings();
|
||
const lastUsedPresetName = settings.plotOpt_lastUsedPresetName;
|
||
const presets = settings.promptPresets || [];
|
||
const lastUsedPreset = presets.find(p => p.name === lastUsedPresetName);
|
||
|
||
if (lastUsedPreset) {
|
||
// If a valid preset was last used, load its data into the cache
|
||
promptCache.main = lastUsedPreset.mainPrompt || defaultSettings.plotOpt_mainPrompt;
|
||
promptCache.system = lastUsedPreset.systemPrompt || defaultSettings.plotOpt_systemPrompt;
|
||
promptCache.final_system = lastUsedPreset.finalSystemDirective || defaultSettings.plotOpt_finalSystemDirective;
|
||
} else {
|
||
// Otherwise, load from the base settings (non-preset values)
|
||
promptCache.main = settings.plotOpt_mainPrompt || defaultSettings.plotOpt_mainPrompt;
|
||
promptCache.system = settings.plotOpt_systemPrompt || defaultSettings.plotOpt_systemPrompt;
|
||
promptCache.final_system = settings.plotOpt_finalSystemDirective || defaultSettings.plotOpt_finalSystemDirective;
|
||
}
|
||
|
||
updateEditorFromCache();
|
||
panel.find('#amily2_opt_prompt_editor').data('current-prompt', panel.find('#amily2_opt_prompt_selector').val());
|
||
});
|
||
|
||
panel.on('change', '#amily2_opt_prompt_selector', function() {
|
||
const previousPromptKey = panel.find('#amily2_opt_prompt_editor').data('current-prompt');
|
||
if (previousPromptKey) {
|
||
const previousValue = panel.find('#amily2_opt_prompt_editor').val();
|
||
promptCache[previousPromptKey] = previousValue;
|
||
const keyMap = {
|
||
main: 'plotOpt_mainPrompt',
|
||
system: 'plotOpt_systemPrompt',
|
||
final_system: 'plotOpt_finalSystemDirective'
|
||
};
|
||
opt_saveSetting(keyMap[previousPromptKey], previousValue);
|
||
}
|
||
|
||
const selectedPrompt = $(this).val();
|
||
panel.find('#amily2_opt_prompt_editor').val(promptCache[selectedPrompt]);
|
||
panel.find('#amily2_opt_prompt_editor').data('current-prompt', selectedPrompt);
|
||
});
|
||
|
||
panel.on('input', '#amily2_opt_prompt_editor', function() {
|
||
const currentPrompt = panel.find('#amily2_opt_prompt_selector').val();
|
||
const currentValue = $(this).val();
|
||
promptCache[currentPrompt] = currentValue;
|
||
|
||
const keyMap = {
|
||
main: 'plotOpt_mainPrompt',
|
||
system: 'plotOpt_systemPrompt',
|
||
final_system: 'plotOpt_finalSystemDirective'
|
||
};
|
||
opt_saveSetting(keyMap[currentPrompt], currentValue);
|
||
});
|
||
|
||
panel.on('click', '#amily2_opt_reset_main_prompt', function() {
|
||
const defaultValue = defaultSettings.plotOpt_mainPrompt;
|
||
promptCache.main = defaultValue;
|
||
updateEditorFromCache();
|
||
opt_saveSetting('plotOpt_mainPrompt', defaultValue);
|
||
toastr.info('主提示词已恢复为默认值。');
|
||
});
|
||
|
||
panel.on('click', '#amily2_opt_reset_system_prompt', function() {
|
||
const defaultValue = defaultSettings.plotOpt_systemPrompt;
|
||
promptCache.system = defaultValue;
|
||
updateEditorFromCache();
|
||
opt_saveSetting('plotOpt_systemPrompt', defaultValue);
|
||
toastr.info('拦截任务指令已恢复为默认值。');
|
||
});
|
||
|
||
panel.on('click', '#amily2_opt_reset_final_system_directive', function() {
|
||
const defaultValue = defaultSettings.plotOpt_finalSystemDirective;
|
||
promptCache.final_system = defaultValue;
|
||
updateEditorFromCache();
|
||
opt_saveSetting('plotOpt_finalSystemDirective', defaultValue);
|
||
toastr.info('最终注入指令已恢复为默认值。');
|
||
});
|
||
|
||
opt_loadSettings(panel);
|
||
bindCustomBlockEvents(panel);
|
||
bindJqyhApiEvents();
|
||
bindConcurrentApiEvents();
|
||
bindConcurrentPromptEvents();
|
||
opt_loadConcurrentWorldbookSettings(); // Load settings
|
||
bindConcurrentWorldbookEvents(); // Then bind events
|
||
|
||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||
console.log(`[${extensionName}] 检测到角色/聊天切换,正在刷新剧情优化设置UI...`);
|
||
opt_loadSettings(panel);
|
||
});
|
||
|
||
const refreshWorldbookUI = () => {
|
||
if (panel.is(':visible')) {
|
||
console.log(`[${extensionName}] 检测到世界书变更,正在刷新列表...`);
|
||
opt_loadWorldbooks(panel).then(() => {
|
||
opt_loadWorldbookEntries(panel);
|
||
});
|
||
}
|
||
};
|
||
|
||
eventSource.on(event_types.WORLDINFO_UPDATED, refreshWorldbookUI);
|
||
// 尝试监听更多可能的世界书事件,确保第一时间更新
|
||
if (event_types.WORLDINFO_ENTRY_UPDATED) eventSource.on(event_types.WORLDINFO_ENTRY_UPDATED, refreshWorldbookUI);
|
||
if (event_types.WORLDINFO_ENTRY_CREATED) eventSource.on(event_types.WORLDINFO_ENTRY_CREATED, refreshWorldbookUI);
|
||
if (event_types.WORLDINFO_ENTRY_DELETED) eventSource.on(event_types.WORLDINFO_ENTRY_DELETED, refreshWorldbookUI);
|
||
|
||
const handleSettingChange = function(element) {
|
||
const el = $(element);
|
||
const rawName = element.name || element.id || '';
|
||
// 仅处理下划线前缀的真实设置项;动态生成的世界书/条目复选框用连字符命名(amily2-opt-wb-*、amily2-opt-entry-*),
|
||
// 它们有自己的专属 handler,若被此处捕获会生成 plotOpt_amily2-opt-... 的垃圾 key 污染 settings
|
||
if (!rawName.startsWith('amily2_opt_')) return;
|
||
const key_part = rawName.replace('amily2_opt_', '');
|
||
const key = 'plotOpt_' + key_part.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
|
||
|
||
let value = element.type === 'checkbox' ? element.checked : el.val();
|
||
|
||
if (key === 'plotOpt_selected_worldbooks' && !Array.isArray(value)) {
|
||
value = el.val() || [];
|
||
}
|
||
|
||
const floatKeys = ['plotOpt_temperature', 'plotOpt_top_p', 'plotOpt_presence_penalty', 'plotOpt_frequency_penalty', 'plotOpt_rateMain', 'plotOpt_ratePersonal', 'plotOpt_rateErotic', 'plotOpt_rateCuckold'];
|
||
if (floatKeys.includes(key) && value !== '') {
|
||
value = parseFloat(value);
|
||
} else if (element.type === 'range' || element.type === 'number') {
|
||
if (value !== '') value = parseInt(value, 10);
|
||
}
|
||
|
||
if (value !== '' || element.type === 'checkbox') {
|
||
opt_saveSetting(key, value);
|
||
}
|
||
|
||
if (element.name === 'amily2_opt_worldbook_source') {
|
||
opt_updateWorldbookSourceVisibility(panel, value);
|
||
opt_loadWorldbookEntries(panel);
|
||
}
|
||
};
|
||
const allInputSelectors = [
|
||
'input[type="checkbox"]', 'input[type="radio"]', 'select',
|
||
'input[type="text"]', 'input[type="password"]', 'textarea',
|
||
'input[type="range"]', 'input[type="number"]'
|
||
].join(', ');
|
||
|
||
panel.on('input.amily2_opt change.amily2_opt', allInputSelectors, function() {
|
||
handleSettingChange(this);
|
||
});
|
||
|
||
|
||
panel.find('#amily2_opt_import_prompt_presets').on('click', () => panel.find('#amily2_opt_preset_file_input').click());
|
||
panel.find('#amily2_opt_export_prompt_presets').on('click', () => opt_exportPromptPresets());
|
||
panel.find('#amily2_opt_save_prompt_preset').on('click', () => opt_saveCurrentPromptsAsPreset(panel));
|
||
panel.find('#amily2_opt_delete_prompt_preset').on('click', () => opt_deleteSelectedPreset(panel));
|
||
|
||
panel.on('change.amily2_opt', '#amily2_opt_preset_file_input', function(e) {
|
||
opt_importPromptPresets(e.target.files[0], panel);
|
||
});
|
||
|
||
panel.on('change.amily2_opt', '#amily2_opt_prompt_preset_select', function(event, data) {
|
||
const selectedName = $(this).val();
|
||
const deleteBtn = panel.find('#amily2_opt_delete_prompt_preset');
|
||
const isAutomatic = data && data.isAutomatic;
|
||
const noLoad = data && data.noLoad;
|
||
|
||
console.log('[Amily2-Debug] Preset select changed:', selectedName, 'isAutomatic:', isAutomatic, 'noLoad:', noLoad);
|
||
opt_saveSetting('plotOpt_lastUsedPresetName', selectedName);
|
||
console.log('[Amily2-Debug] After saving, extension_settings contains:', extension_settings[extensionName]?.plotOpt_lastUsedPresetName);
|
||
|
||
// On initial load, we might not need to reload all the data, just update the UI state.
|
||
if (noLoad) {
|
||
if (selectedName) deleteBtn.show();
|
||
else deleteBtn.hide();
|
||
return;
|
||
}
|
||
|
||
if (!selectedName) {
|
||
deleteBtn.hide();
|
||
opt_saveSetting('lastUsedPresetName', '');
|
||
return;
|
||
}
|
||
|
||
const presets = extension_settings[extensionName]?.promptPresets || [];
|
||
const selectedPreset = presets.find(p => p.name === selectedName);
|
||
|
||
if (selectedPreset) {
|
||
// Update cache with preset values
|
||
promptCache.main = selectedPreset.mainPrompt || defaultSettings.plotOpt_mainPrompt;
|
||
promptCache.system = selectedPreset.systemPrompt || defaultSettings.plotOpt_systemPrompt;
|
||
promptCache.final_system = selectedPreset.finalSystemDirective || defaultSettings.plotOpt_finalSystemDirective;
|
||
|
||
// Update the editor to show the content of the currently selected prompt type
|
||
const initFunc = panel.data('initAmily2PromptEditor');
|
||
if (initFunc) {
|
||
initFunc();
|
||
}
|
||
|
||
// Save the new prompt values to the main settings
|
||
opt_saveSetting('plotOpt_mainPrompt', promptCache.main);
|
||
opt_saveSetting('plotOpt_systemPrompt', promptCache.system);
|
||
opt_saveSetting('plotOpt_finalSystemDirective', promptCache.final_system);
|
||
|
||
// Also load and save concurrent prompts
|
||
const concurrentMain = selectedPreset.concurrentMainPrompt || defaultSettings.plotOpt_concurrentMainPrompt;
|
||
const concurrentSystem = selectedPreset.concurrentSystemPrompt || defaultSettings.plotOpt_concurrentSystemPrompt;
|
||
opt_saveSetting('plotOpt_concurrentMainPrompt', concurrentMain);
|
||
opt_saveSetting('plotOpt_concurrentSystemPrompt', concurrentSystem);
|
||
|
||
// Trigger UI update for concurrent editor
|
||
const concurrentEditor = panel.find('#amily2_concurrent_prompt_editor');
|
||
const concurrentSelector = panel.find('#amily2_concurrent_prompt_selector');
|
||
if (concurrentSelector.val() === 'main') {
|
||
concurrentEditor.val(concurrentMain);
|
||
} else {
|
||
concurrentEditor.val(concurrentSystem);
|
||
}
|
||
|
||
panel.find('#amily2_opt_rate_main').val(selectedPreset.rateMain ?? 1.0).trigger('change');
|
||
panel.find('#amily2_opt_rate_personal').val(selectedPreset.ratePersonal ?? 1.0).trigger('change');
|
||
panel.find('#amily2_opt_rate_erotic').val(selectedPreset.rateErotic ?? 1.0).trigger('change');
|
||
panel.find('#amily2_opt_rate_cuckold').val(selectedPreset.rateCuckold ?? 1.0).trigger('change');
|
||
|
||
if (!isAutomatic) {
|
||
toastr.success(`已加载预设 "${selectedName}"。`);
|
||
}
|
||
deleteBtn.show();
|
||
} else {
|
||
deleteBtn.hide();
|
||
}
|
||
});
|
||
|
||
panel.data('events-bound', true);
|
||
console.log(`[${extensionName}] 剧情优化UI事件已成功绑定,自动保存已激活。`);
|
||
|
||
panel.on('click.amily2_opt', '#amily2_opt_refresh_worldbooks', () => {
|
||
opt_loadWorldbooks(panel).then(() => {
|
||
opt_loadWorldbookEntries(panel);
|
||
});
|
||
});
|
||
|
||
|
||
// Manual Selection Change
|
||
panel.on('change.amily2_opt', '#amily2_opt_worldbook_checkbox_list input[type="checkbox"]:not(.amily2_opt_wb_auto_check)', async function() {
|
||
const selected = [];
|
||
panel.find('#amily2_opt_worldbook_checkbox_list input[type="checkbox"]:not(.amily2_opt_wb_auto_check):checked').each(function() {
|
||
selected.push($(this).val());
|
||
});
|
||
|
||
await opt_saveSetting('plotOpt_selectedWorldbooks', selected);
|
||
await opt_loadWorldbookEntries(panel);
|
||
});
|
||
|
||
// Auto Selection Change
|
||
panel.on('change.amily2_opt', '#amily2_opt_worldbook_checkbox_list input.amily2_opt_wb_auto_check', async function() {
|
||
const autoSelected = [];
|
||
panel.find('#amily2_opt_worldbook_checkbox_list input.amily2_opt_wb_auto_check:checked').each(function() {
|
||
autoSelected.push($(this).data('book'));
|
||
});
|
||
|
||
await opt_saveSetting('plotOpt_autoSelectWorldbooks', autoSelected);
|
||
await opt_loadWorldbookEntries(panel);
|
||
});
|
||
|
||
panel.on('change.amily2_opt', '#amily2_opt_worldbook_entry_list_container input[type="checkbox"]', () => {
|
||
opt_saveEnabledEntries();
|
||
});
|
||
|
||
panel.on('click.amily2_opt', '#amily2_opt_worldbook_entry_select_all', () => {
|
||
panel.find('#amily2_opt_worldbook_entry_list_container input[type="checkbox"]').prop('checked', true);
|
||
opt_saveEnabledEntries();
|
||
});
|
||
|
||
panel.on('click.amily2_opt', '#amily2_opt_worldbook_entry_deselect_all', () => {
|
||
panel.find('#amily2_opt_worldbook_entry_list_container input[type="checkbox"]').prop('checked', false);
|
||
opt_saveEnabledEntries();
|
||
});
|
||
}
|
||
|
||
// ========== Jqyh API 事件绑定函数(已迁移至 plotOpt 槽位,此处仅保留空壳) ==========
|
||
function bindJqyhApiEvents() {
|
||
// Jqyh 直连配置已移除,剧情优化统一走 ApiProfile plotOpt 槽位
|
||
}
|
||
|
||
// ========== 图标位置切换(跨模块通用事件) ==========
|
||
$(document).on('change', 'input[name="amily2_icon_location"]', function() {
|
||
if (!pluginAuthStatus.authorized) return;
|
||
const newLocation = $(this).val();
|
||
extension_settings[extensionName]['iconLocation'] = newLocation;
|
||
saveSettingsDebounced();
|
||
console.log(`[Amily-禁卫军] 收到迁都指令 -> ${newLocation}。圣意已存档。`);
|
||
toastr.info(`正在将帝国徽记迁往 [${newLocation === 'topbar' ? '顶栏' : '扩展区'}]...`, "迁都令", { timeOut: 2000 });
|
||
$('#amily2_main_drawer').remove();
|
||
$(document).off("mousedown.amily2Drawer");
|
||
$('#amily2_extension_frame').remove();
|
||
|
||
setTimeout(createDrawer, 50);
|
||
});
|