mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 05:45:51 +00:00
### 新功能 - Function Call 填表开关下方新增公益站风险提示横幅:部分公益站会屏蔽 tools 参数,请确认支持情况避免被意外封禁 ### 修复 - **Function Call 填表**: - 修复 ST 代理以 HTTP 200 + error body 形式返回错误、导致降级重试机制从未触发的问题 - 修复思考模式模型(如 DeepSeek v4-flash)因 tool_choice 不兼容返回 Bad Request 后正确降级并重试 - 重试时自动追加强制调用指令,防止思考模型绕过工具直接输出文本造成无效二次开销 - **超级记忆 / 翰林院**: - 修复 `getRagSettings()` 读写顶层路径而非嵌套路径,导致打开超级记忆面板后向量化、归档等开关在重载时被全默认值覆盖的问题 - 修复自动归档失效问题 - 修复归档管理器在同一事件中被三次触发的回归问题 - 修复翰林院设置旧版迁移逻辑异常
1297 lines
55 KiB
JavaScript
1297 lines
55 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';
|
||
|
||
// ========== 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;
|
||
|
||
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();
|
||
}
|
||
}
|
||
|
||
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);
|
||
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);
|
||
});
|