diff --git a/CharacterWorldBook/cwb_settings.html b/CharacterWorldBook/cwb_settings.html index a09b28b..58281ae 100644 --- a/CharacterWorldBook/cwb_settings.html +++ b/CharacterWorldBook/cwb_settings.html @@ -1,214 +1,214 @@ -
+ 这是最高优先级的总开关。关闭后,CharacterWorldBook的所有功能(包括自动更新、查看器等)都将被禁用。 +
+基于已有世界书内容进行增量更新,而非完全覆盖
+ +达到消息阈值时自动触发AI更新角色卡
+ +在主界面显示可拖动的角色卡查看按钮
+没有找到世界书。
'); - } - }; - - const updateCustomSelectVisibility = () => { - const isCustom = settings.cwb_worldbook_target === 'custom'; - customSelectWrapper.toggle(isCustom); - if (isCustom) { - renderWorldBookList(); - } - }; - - $panel.find('input[name="cwb_worldbook_target"]').each(function() { - $(this).prop('checked', $(this).val() === settings.cwb_worldbook_target); - }); - updateCustomSelectVisibility(); - - $panel.off('change.cwb_worldbook_target').on('change.cwb_worldbook_target', 'input[name="cwb_worldbook_target"]', function() { - if ($(this).prop('checked')) { - settings.cwb_worldbook_target = $(this).val(); - state.worldbookTarget = $(this).val(); - updateCustomSelectVisibility(); - saveSettingsDebounced(); - } - }); - - bookListContainer.off('change.cwb_worldbook_selection').on('change.cwb_worldbook_selection', 'input[name="cwb_worldbook_selection"]', function() { - const radio = $(this); - if (radio.prop('checked')) { - settings.cwb_custom_worldbook = radio.val(); - state.customWorldBook = radio.val(); - saveSettingsDebounced(); - showToastr('info', `已选择世界书: ${radio.next('label').text()}`); - } - }); - - $panel.off('click.cwb_refresh_worldbooks').on('click.cwb_refresh_worldbooks', '#cwb_refresh_worldbooks', renderWorldBookList); - - } else if (attempt < MAX_RETRIES) { - attempt++; - console.log(`[CWB] World books not ready, retrying... (Attempt ${attempt})`); - setTimeout(tryBind, RETRY_DELAY); - } else { - console.error('[CWB] Failed to load world books after multiple retries.'); - $panel.find('#cwb_worldbook_radio_list').html('加载世界书失败,请刷新页面重试。
'); - } - } - - tryBind(); -} - -export function bindSettingsEvents($settingsPanel) { - $panel = $settingsPanel; - - bindWorldBookSettings(); - $panel.on('click', '.sinan-nav-item', function () { - const $this = $(this); - const tabId = $this.data('tab'); - - $panel.find('.sinan-nav-item').removeClass('active'); - $this.addClass('active'); - $panel.find('.sinan-tab-pane').removeClass('active'); - $panel.find(`#cwb-${tabId}-tab`).addClass('active'); - }); - $panel.on('change', '#cwb-api-mode', function() { - const selectedMode = $(this).val(); - - // 自动保存API模式设置 - getSettings().cwb_api_mode = selectedMode; - saveSettingsDebounced(); - - updateApiModeUI(selectedMode); - if (selectedMode === 'sillytavern_preset') { - loadSillyTavernPresets(true); - } - - showToastr('success', `API模式已切换为: ${selectedMode === 'sillytavern_preset' ? 'SillyTavern预设' : '全兼容'}`); - }); - $panel.on('change', '#cwb-tavern-profile', function() { - const selectedProfile = $(this).val(); - - // 自动保存SillyTavern预设选择 - getSettings().cwb_tavern_profile = selectedProfile; - saveSettingsDebounced(); - - if (selectedProfile) { - console.log(`[CWB] 选择了预设: ${selectedProfile}`); - showToastr('success', `SillyTavern预设已选择: ${selectedProfile}`); - } - - updateApiStatusDisplay($panel); - }); - // 添加API字段的实时保存 - $panel.on('input', '#cwb-api-url', function() { - const apiUrl = $(this).val().trim(); - - // 同时更新设置和状态 - getSettings().cwb_api_url = apiUrl; - state.customApiConfig.url = apiUrl; - - saveSettingsDebounced(); - updateApiStatusDisplay($panel); - - console.log('[CWB] API URL已更新 - 设置:', getSettings().cwb_api_url, ', 状态:', state.customApiConfig.url); - }); - - $panel.on('input', '#cwb-api-key', function() { - const apiKey = $(this).val(); - - // 同时更新设置和状态 - getSettings().cwb_api_key = apiKey; - state.customApiConfig.apiKey = apiKey; - - saveSettingsDebounced(); - - console.log('[CWB] API Key已更新 - 设置长度:', getSettings().cwb_api_key?.length || 0, ', 状态长度:', state.customApiConfig.apiKey?.length || 0); - }); - - $panel.on('change', '#cwb-api-model', function() { - const model = $(this).val(); - - // 同时更新设置和状态 - getSettings().cwb_api_model = model; - state.customApiConfig.model = model; - - saveSettingsDebounced(); - updateApiStatusDisplay($panel); - - console.log('[CWB] 模型已更新 - 设置:', getSettings().cwb_api_model, ', 状态:', state.customApiConfig.model); - - if (model) { - showToastr('success', `模型已选择: ${model}`); - } - }); - - $panel.on('click', '#cwb-load-models', () => fetchModelsAndConnect($panel)); - - $panel.on('click', '#cwb-save-break-armor-prompt', saveBreakArmorPrompt); - $panel.on('click', '#cwb-reset-break-armor-prompt', resetBreakArmorPrompt); - $panel.on('click', '#cwb-save-char-card-prompt', saveCharCardPrompt); - $panel.on('click', '#cwb-reset-char-card-prompt', resetCharCardPrompt); - - $panel.on('click', '#cwb-save-auto-update-threshold', saveAutoUpdateThreshold); - $panel.on('click', '#cwb-save-scan-depth', saveScanDepth); - $panel.on('click', '#cwb-manual-update-card', () => handleManualUpdateCard($panel)); - $panel.on('click', '#cwb-batch-update-card', () => startBatchUpdate($panel)); - $panel.on('click', '#cwb-floor-range-update', () => handleFloorRangeUpdate($panel)); - $panel.on('click', '#cwb-legacy-auto-update', () => handleLegacyFormatConversion($panel)); - $panel.on('click', '#cwb-check-for-updates', () => checkForUpdates(true, $panel)); - - $panel.on('click', '#cwb-auto-update-enabled', function () { - const $checkbox = $(this).find('input[type="checkbox"]'); - const isChecked = !$checkbox.prop('checked'); - $checkbox.prop('checked', isChecked); - - console.log(`[CWB] Auto-update switch clicked. New state: ${isChecked}`); - getSettings().cwb_auto_update_enabled = isChecked; - - const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}'); - overrides.cwb_auto_update_enabled = isChecked; - localStorage.setItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY, JSON.stringify(overrides)); - - saveSettingsDebounced(); - state.autoUpdateEnabled = isChecked; - showToastr('info', `角色卡自动更新已 ${isChecked ? '启用' : '禁用'}`); - }); - - $panel.on('click', '#cwb-viewer-enabled', function () { - const $checkbox = $(this).find('input[type="checkbox"]'); - const isChecked = !$checkbox.prop('checked'); - $checkbox.prop('checked', isChecked); - - console.log(`[CWB] Viewer switch clicked. New state: ${isChecked}`); - getSettings().cwb_viewer_enabled = isChecked; - - const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}'); - overrides.cwb_viewer_enabled = isChecked; - localStorage.setItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY, JSON.stringify(overrides)); - - saveSettingsDebounced(); - - state.viewerEnabled = isChecked; - - const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); - if ($viewerButton.length > 0) { - const shouldShow = isCwbEnabled() && isChecked; - $viewerButton.toggle(shouldShow); - } - - showToastr('info', `角色卡查看器已 ${isChecked ? '启用' : '禁用'}`); - }); - - $panel.on('click', '#cwb-incremental-update-enabled', function () { - const $checkbox = $(this).find('input[type="checkbox"]'); - const isChecked = !$checkbox.prop('checked'); // Manually toggle - $checkbox.prop('checked', isChecked); - - console.log(`[CWB] Incremental update switch clicked. New state: ${isChecked}`); - getSettings().cwb_incremental_update_enabled = isChecked; - - const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}'); - overrides.cwb_incremental_update_enabled = isChecked; - localStorage.setItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY, JSON.stringify(overrides)); - - saveSettingsDebounced(); - state.isIncrementalUpdateEnabled = isChecked; - showToastr('info', `增量更新模式已 ${isChecked ? '启用' : '禁用'}`); - }); - - $panel.on('click', '#cwb_master_enabled', function () { - const $checkbox = $(this).find('input[type="checkbox"]'); - const isChecked = !$checkbox.prop('checked'); - $checkbox.prop('checked', isChecked); - - console.log(`[CWB] Master switch clicked. New state: ${isChecked}`); - - getSettings().cwb_master_enabled = isChecked; - - const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}'); - overrides.cwb_master_enabled = isChecked; - localStorage.setItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY, JSON.stringify(overrides)); - - state.masterEnabled = isChecked; - - saveSettingsDebounced(); - - updateControlsLockState(); - - const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); - if ($viewerButton.length > 0) { - const shouldShow = isChecked && state.viewerEnabled; - $viewerButton.toggle(shouldShow); - } - - showToastr('info', `CharacterWorldBook 已 ${isChecked ? '启用' : '禁用'}`); - - $(document).trigger('cwb:master-switch-changed', { isEnabled: isChecked }); - }); -} - -function updateApiModeUI(mode) { - const fields = { - openai: [ - 'label[for="cwb-api-url"]', - '#cwb-api-url', - 'label[for="cwb-api-key"]', - '#cwb-api-key', - 'label[for="cwb-api-model"]', - '#cwb-api-model', - '#cwb-load-models' - ], - sillytavern: [ - 'label[for="cwb-tavern-profile"]', - '#cwb-tavern-profile' - ] - }; - - if (mode === 'sillytavern_preset') { - fields.openai.forEach(selector => $panel.find(selector).hide()); - fields.sillytavern.forEach(selector => $panel.find(selector).show()); - } else { - fields.sillytavern.forEach(selector => $panel.find(selector).hide()); - fields.openai.forEach(selector => $panel.find(selector).show()); - } - - updateApiStatusDisplay($panel); -} - -function loadSillyTavernPresets(showNotification = false) { - const $profileSelect = $panel.find('#cwb-tavern-profile'); - - try { - const context = window.SillyTavern?.getContext?.(); - if (!context?.extensionSettings?.connectionManager?.profiles) { - showToastr('warning', '无法获取SillyTavern配置文件列表'); - return; - } - - const profiles = context.extensionSettings.connectionManager.profiles; - - $profileSelect.empty(); - $profileSelect.append(''); - - profiles.forEach(profile => { - $profileSelect.append(``); - }); - const currentProfile = getSettings().cwb_tavern_profile; - if (currentProfile) { - $profileSelect.val(currentProfile); - } - - if (showNotification) { - showToastr('success', `已加载 ${profiles.length} 个SillyTavern预设`); - } - - } catch (error) { - logError('加载SillyTavern预设失败:', error); - showToastr('error', '加载SillyTavern预设失败'); - } -} - -function updateUiWithSettings() { - if (!$panel) return; - const settings = getSettings(); - - $panel.find('#cwb-api-mode').val(settings.cwb_api_mode || 'openai_test'); - - const currentMode = settings.cwb_api_mode || 'openai_test'; - updateApiModeUI(currentMode); - - if (currentMode === 'sillytavern_preset') { - loadSillyTavernPresets(); - } - - $panel.find('#cwb-api-url').val(settings.cwb_api_url); - $panel.find('#cwb-api-key').val(settings.cwb_api_key); - $panel.find('#cwb-tavern-profile').val(settings.cwb_tavern_profile); - - const $modelSelect = $panel.find('#cwb-api-model'); - if (settings.cwb_api_model) { - $modelSelect.empty().append(``); - } else { - $modelSelect.empty().append(''); - } - updateApiStatusDisplay($panel); - - $panel.find('#cwb-break-armor-prompt-textarea').val(settings.cwb_break_armor_prompt); - $panel.find('#cwb-char-card-prompt-textarea').val(settings.cwb_char_card_prompt); - - $panel.find('#cwb-temperature').val(settings.cwb_temperature); - $panel.find('#cwb-temperature-value').text(settings.cwb_temperature); - $panel.find('#cwb-max-tokens').val(settings.cwb_max_tokens); - $panel.find('#cwb-max-tokens-value').text(settings.cwb_max_tokens); - - $panel.find('#cwb-auto-update-threshold').val(settings.cwb_auto_update_threshold); - $panel.find('#cwb-scan-depth').val(settings.cwb_scan_depth); - $panel.find('#cwb_master_enabled-checkbox').prop('checked', settings.cwb_master_enabled); - $panel.find('#cwb-auto-update-enabled-checkbox').prop('checked', settings.cwb_auto_update_enabled); - $panel.find('#cwb-viewer-enabled-checkbox').prop('checked', settings.cwb_viewer_enabled); - $panel.find('#cwb-incremental-update-enabled-checkbox').prop('checked', settings.cwb_incremental_update_enabled); - - if (!$panel.find('#cwb-start-floor').val()) { - $panel.find('#cwb-start-floor').val(1); - } - if (!$panel.find('#cwb-end-floor').val()) { - $panel.find('#cwb-end-floor').val(1); - } - - $panel.find('input[name="cwb_worldbook_target"]').each(function() { - $(this).prop('checked', $(this).val() === settings.cwb_worldbook_target); - }); - if (settings.cwb_worldbook_target === 'custom') { - $panel.find('#cwb_worldbook_select_wrapper').show(); - } else { - $panel.find('#cwb_worldbook_select_wrapper').hide(); - } -} - -export function loadSettings() { - console.log('[CWB] Loading settings...'); - - const settings = getSettings(); - - // Initialize settings with defaults if not present - if (!settings) { - extension_settings[extensionName] = { ...cwbCompleteDefaultSettings }; - console.log('[CWB] Initialized default settings'); - } else { - // Ensure all default settings exist - Object.keys(cwbCompleteDefaultSettings).forEach(key => { - if (settings[key] === undefined || settings[key] === null) { - settings[key] = cwbCompleteDefaultSettings[key]; - } - }); - } - - const finalSettings = getSettings(); - - // Apply localStorage overrides - const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}'); - if (overrides.cwb_master_enabled !== undefined) { - finalSettings.cwb_master_enabled = overrides.cwb_master_enabled; - } - if (overrides.cwb_auto_update_enabled !== undefined) { - finalSettings.cwb_auto_update_enabled = overrides.cwb_auto_update_enabled; - } - if (overrides.cwb_viewer_enabled !== undefined) { - finalSettings.cwb_viewer_enabled = overrides.cwb_viewer_enabled; - } - if (overrides.cwb_incremental_update_enabled !== undefined) { - finalSettings.cwb_incremental_update_enabled = overrides.cwb_incremental_update_enabled; - } - - // Update state object with current settings - state.masterEnabled = finalSettings.cwb_master_enabled; - state.viewerEnabled = finalSettings.cwb_viewer_enabled; - state.autoUpdateEnabled = finalSettings.cwb_auto_update_enabled; - state.isIncrementalUpdateEnabled = finalSettings.cwb_incremental_update_enabled; - - state.customApiConfig.url = finalSettings.cwb_api_url || ''; - state.customApiConfig.apiKey = finalSettings.cwb_api_key || ''; - state.customApiConfig.model = finalSettings.cwb_api_model || ''; - - state.currentBreakArmorPrompt = finalSettings.cwb_break_armor_prompt; - state.currentCharCardPrompt = finalSettings.cwb_char_card_prompt; - state.currentIncrementalCharCardPrompt = finalSettings.cwb_incremental_char_card_prompt; - - state.autoUpdateThreshold = finalSettings.cwb_auto_update_threshold; - state.scanDepth = finalSettings.cwb_scan_depth; - state.worldbookTarget = finalSettings.cwb_worldbook_target; - state.customWorldBook = finalSettings.cwb_custom_worldbook; - - console.log('[CWB] State updated:', { - masterEnabled: state.masterEnabled, - viewerEnabled: state.viewerEnabled, - autoUpdateEnabled: state.autoUpdateEnabled, - worldbookTarget: state.worldbookTarget, - customWorldBook: state.customWorldBook - }); - if ($panel) { - updateUiWithSettings(); - } - - updateControlsLockState(); - setTimeout(() => { - const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); - if ($viewerButton.length > 0) { - const shouldShow = isCwbEnabled() && state.viewerEnabled; - $viewerButton.toggle(shouldShow); - console.log('[CWB] Viewer button visibility updated:', shouldShow); - } - }, 100); -} +import { extension_settings } from '/scripts/extensions.js'; +import { extensionName } from '../../utils/settings.js'; +import { saveSettingsDebounced } from '/script.js'; +import { world_names } from '/scripts/world-info.js'; +import { state } from './cwb_state.js'; +import { cwbCompleteDefaultSettings } from './cwb_config.js'; +import { logError, showToastr, escapeHtml, compareVersions, isCwbEnabled } from './cwb_utils.js'; +import { fetchModelsAndConnect, updateApiStatusDisplay } from './cwb_apiService.js'; +import { checkForUpdates } from './cwb_updater.js'; +import { handleManualUpdateCard, startBatchUpdate, handleFloorRangeUpdate, handleLegacyFormatConversion } from './cwb_core.js'; +import { initializeCharCardViewer } from './cwb_uiManager.js'; +import { CHAR_CARD_VIEWER_BUTTON_ID } from './cwb_state.js'; + +const { jQuery: $ } = window; + +const CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY = 'cwb_boolean_settings_override'; +let $panel; + +const getSettings = () => extension_settings[extensionName]; + +function updateControlsLockState() { + if (!$panel) return; + const settings = getSettings(); + const isMasterEnabled = settings.cwb_master_enabled; + + const $controlsToToggle = $panel.find('input, textarea, select, button').not('#cwb_master_enabled-checkbox, #amily2_back_to_main_from_cwb, .sinan-nav-item'); + + if (isMasterEnabled) { + $controlsToToggle.prop('disabled', false); + $panel.find('.settings-group').not('.master-control-group').css('opacity', '1'); + } else { + $controlsToToggle.prop('disabled', true); + $panel.find('.settings-group').not('.master-control-group').css('opacity', '0.5'); + } +} + +function saveApiConfig() { + const settings = getSettings(); + settings.cwb_api_mode = $panel.find('#cwb-api-mode').val(); + settings.cwb_api_url = $panel.find('#cwb-api-url').val().trim(); + settings.cwb_api_key = $panel.find('#cwb-api-key').val(); + settings.cwb_api_model = $panel.find('#cwb-api-model').val(); + settings.cwb_tavern_profile = $panel.find('#cwb-tavern-profile').val(); + + if (settings.cwb_api_mode === 'sillytavern_preset') { + if (!settings.cwb_tavern_profile) { + showToastr('warning', '请选择SillyTavern预设。'); + return; + } + showToastr('success', 'API配置已保存!'); + } else { + if (!settings.cwb_api_url) { + showToastr('warning', 'API URL 不能为空。'); + return; + } + showToastr('success', 'API配置已保存!'); + } + + saveSettingsDebounced(); + loadSettings(); +} + +function clearApiConfig() { + const settings = getSettings(); + settings.cwb_api_url = ''; + settings.cwb_api_key = ''; + settings.cwb_api_model = ''; + saveSettingsDebounced(); + state.customApiConfig.url = ''; + state.customApiConfig.apiKey = ''; + state.customApiConfig.model = ''; + updateUiWithSettings(); + updateApiStatusDisplay($panel); + showToastr('info', 'API配置已清除!'); +} + +function saveBreakArmorPrompt() { + const newPrompt = $panel.find('#cwb-break-armor-prompt-textarea').val().trim(); + if (!newPrompt) { + showToastr('warning', '破甲预设不能为空。'); + return; + } + getSettings().cwb_break_armor_prompt = newPrompt; + state.currentBreakArmorPrompt = newPrompt; + saveSettingsDebounced(); + showToastr('success', '破甲预设已保存!'); +} + +function resetBreakArmorPrompt() { + getSettings().cwb_break_armor_prompt = cwbCompleteDefaultSettings.cwb_break_armor_prompt; + state.currentBreakArmorPrompt = cwbCompleteDefaultSettings.cwb_break_armor_prompt; + saveSettingsDebounced(); + updateUiWithSettings(); + showToastr('info', '破甲预设已恢复为默认值!'); +} + +function saveCharCardPrompt() { + const newPrompt = $panel.find('#cwb-char-card-prompt-textarea').val().trim(); + if (!newPrompt) { + showToastr('warning', '角色卡预设不能为空。'); + return; + } + getSettings().cwb_char_card_prompt = newPrompt; + state.currentCharCardPrompt = newPrompt; + saveSettingsDebounced(); + showToastr('success', '角色卡预设已保存!'); +} + +function resetCharCardPrompt() { + getSettings().cwb_char_card_prompt = cwbCompleteDefaultSettings.cwb_char_card_prompt; + state.currentCharCardPrompt = cwbCompleteDefaultSettings.cwb_char_card_prompt; + saveSettingsDebounced(); + updateUiWithSettings(); + showToastr('info', '角色卡预设已恢复为默认值!'); +} + +function saveAutoUpdateThreshold() { + const valStr = $panel.find('#cwb-auto-update-threshold').val(); + const newT = parseInt(valStr, 10); + if (!isNaN(newT) && newT >= 1) { + getSettings().cwb_auto_update_threshold = newT; + state.autoUpdateThreshold = newT; + saveSettingsDebounced(); + showToastr('success', '自动更新阈值已保存!'); + } else { + showToastr('warning', `阈值 "${valStr}" 无效。`); + $panel.find('#cwb-auto-update-threshold').val(getSettings().cwb_auto_update_threshold); + } +} + +function saveScanDepth() { + const valStr = $panel.find('#cwb-scan-depth').val(); + const newT = parseInt(valStr, 10); + if (!isNaN(newT) && newT >= 1) { + getSettings().cwb_scan_depth = newT; + state.scanDepth = newT; + saveSettingsDebounced(); + showToastr('success', '扫描深度已保存!'); + } else { + showToastr('warning', `深度 "${valStr}" 无效。`); + $panel.find('#cwb-scan-depth').val(getSettings().cwb_scan_depth); + } +} + +function bindWorldBookSettings() { + const MAX_RETRIES = 10; + const RETRY_DELAY = 200; + let attempt = 0; + + function tryBind() { + if (world_names && world_names.length > 0) { + console.log('[CWB] World books loaded, binding settings...'); + const settings = getSettings(); + + if (settings.cwb_worldbook_target === undefined) settings.cwb_worldbook_target = 'primary'; + if (settings.cwb_custom_worldbook === undefined) settings.cwb_custom_worldbook = null; + + const customSelectWrapper = $panel.find('#cwb_worldbook_select_wrapper'); + const bookListContainer = $panel.find('#cwb_worldbook_radio_list'); + + const renderWorldBookList = () => { + const worldBooks = world_names.map(name => ({ name: name.replace('.json', ''), file_name: name })); + bookListContainer.empty(); + + if (worldBooks.length > 0) { + worldBooks.forEach(book => { + const div = $('').attr('title', book.name); + const radio = $('') + .attr('id', `cwb-wb-radio-${book.file_name}`) + .val(book.file_name) + .prop('checked', settings.cwb_custom_worldbook === book.file_name); + const label = $('').attr('for', `cwb-wb-radio-${book.file_name}`).text(book.name); + div.append(radio).append(label); + bookListContainer.append(div); + }); + } else { + bookListContainer.html('没有找到世界书。
'); + } + }; + + const updateCustomSelectVisibility = () => { + const isCustom = settings.cwb_worldbook_target === 'custom'; + customSelectWrapper.toggle(isCustom); + if (isCustom) { + renderWorldBookList(); + } + }; + + $panel.find('input[name="cwb_worldbook_target"]').each(function() { + $(this).prop('checked', $(this).val() === settings.cwb_worldbook_target); + }); + updateCustomSelectVisibility(); + + $panel.off('change.cwb_worldbook_target').on('change.cwb_worldbook_target', 'input[name="cwb_worldbook_target"]', function() { + if ($(this).prop('checked')) { + settings.cwb_worldbook_target = $(this).val(); + state.worldbookTarget = $(this).val(); + updateCustomSelectVisibility(); + saveSettingsDebounced(); + } + }); + + bookListContainer.off('change.cwb_worldbook_selection').on('change.cwb_worldbook_selection', 'input[name="cwb_worldbook_selection"]', function() { + const radio = $(this); + if (radio.prop('checked')) { + settings.cwb_custom_worldbook = radio.val(); + state.customWorldBook = radio.val(); + saveSettingsDebounced(); + showToastr('info', `已选择世界书: ${radio.next('label').text()}`); + } + }); + + $panel.off('click.cwb_refresh_worldbooks').on('click.cwb_refresh_worldbooks', '#cwb_refresh_worldbooks', renderWorldBookList); + + } else if (attempt < MAX_RETRIES) { + attempt++; + console.log(`[CWB] World books not ready, retrying... (Attempt ${attempt})`); + setTimeout(tryBind, RETRY_DELAY); + } else { + console.error('[CWB] Failed to load world books after multiple retries.'); + $panel.find('#cwb_worldbook_radio_list').html('加载世界书失败,请刷新页面重试。
'); + } + } + + tryBind(); +} + +export function bindSettingsEvents($settingsPanel) { + $panel = $settingsPanel; + + bindWorldBookSettings(); + $panel.on('click', '.sinan-nav-item', function () { + const $this = $(this); + const tabId = $this.data('tab'); + + $panel.find('.sinan-nav-item').removeClass('active'); + $this.addClass('active'); + $panel.find('.sinan-tab-pane').removeClass('active'); + $panel.find(`#cwb-${tabId}-tab`).addClass('active'); + }); + $panel.on('change', '#cwb-api-mode', function() { + const selectedMode = $(this).val(); + + // 自动保存API模式设置 + getSettings().cwb_api_mode = selectedMode; + saveSettingsDebounced(); + + updateApiModeUI(selectedMode); + if (selectedMode === 'sillytavern_preset') { + loadSillyTavernPresets(true); + } + + showToastr('success', `API模式已切换为: ${selectedMode === 'sillytavern_preset' ? 'SillyTavern预设' : '全兼容'}`); + }); + $panel.on('change', '#cwb-tavern-profile', function() { + const selectedProfile = $(this).val(); + + // 自动保存SillyTavern预设选择 + getSettings().cwb_tavern_profile = selectedProfile; + saveSettingsDebounced(); + + if (selectedProfile) { + console.log(`[CWB] 选择了预设: ${selectedProfile}`); + showToastr('success', `SillyTavern预设已选择: ${selectedProfile}`); + } + + updateApiStatusDisplay($panel); + }); + // 添加API字段的实时保存 + $panel.on('input', '#cwb-api-url', function() { + const apiUrl = $(this).val().trim(); + + // 同时更新设置和状态 + getSettings().cwb_api_url = apiUrl; + state.customApiConfig.url = apiUrl; + + saveSettingsDebounced(); + updateApiStatusDisplay($panel); + + console.log('[CWB] API URL已更新 - 设置:', getSettings().cwb_api_url, ', 状态:', state.customApiConfig.url); + }); + + $panel.on('input', '#cwb-api-key', function() { + const apiKey = $(this).val(); + + // 同时更新设置和状态 + getSettings().cwb_api_key = apiKey; + state.customApiConfig.apiKey = apiKey; + + saveSettingsDebounced(); + + console.log('[CWB] API Key已更新 - 设置长度:', getSettings().cwb_api_key?.length || 0, ', 状态长度:', state.customApiConfig.apiKey?.length || 0); + }); + + $panel.on('change', '#cwb-api-model', function() { + const model = $(this).val(); + + // 同时更新设置和状态 + getSettings().cwb_api_model = model; + state.customApiConfig.model = model; + + saveSettingsDebounced(); + updateApiStatusDisplay($panel); + + console.log('[CWB] 模型已更新 - 设置:', getSettings().cwb_api_model, ', 状态:', state.customApiConfig.model); + + if (model) { + showToastr('success', `模型已选择: ${model}`); + } + }); + + $panel.on('click', '#cwb-load-models', () => fetchModelsAndConnect($panel)); + + $panel.on('click', '#cwb-save-break-armor-prompt', saveBreakArmorPrompt); + $panel.on('click', '#cwb-reset-break-armor-prompt', resetBreakArmorPrompt); + $panel.on('click', '#cwb-save-char-card-prompt', saveCharCardPrompt); + $panel.on('click', '#cwb-reset-char-card-prompt', resetCharCardPrompt); + + $panel.on('click', '#cwb-save-auto-update-threshold', saveAutoUpdateThreshold); + $panel.on('click', '#cwb-save-scan-depth', saveScanDepth); + $panel.on('click', '#cwb-manual-update-card', () => handleManualUpdateCard($panel)); + $panel.on('click', '#cwb-batch-update-card', () => startBatchUpdate($panel)); + $panel.on('click', '#cwb-floor-range-update', () => handleFloorRangeUpdate($panel)); + $panel.on('click', '#cwb-legacy-auto-update', () => handleLegacyFormatConversion($panel)); + $panel.on('click', '#cwb-check-for-updates', () => checkForUpdates(true, $panel)); + + $panel.on('click', '#cwb-auto-update-enabled', function () { + const $checkbox = $(this).find('input[type="checkbox"]'); + const isChecked = !$checkbox.prop('checked'); + $checkbox.prop('checked', isChecked); + + console.log(`[CWB] Auto-update switch clicked. New state: ${isChecked}`); + getSettings().cwb_auto_update_enabled = isChecked; + + const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}'); + overrides.cwb_auto_update_enabled = isChecked; + localStorage.setItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY, JSON.stringify(overrides)); + + saveSettingsDebounced(); + state.autoUpdateEnabled = isChecked; + showToastr('info', `角色卡自动更新已 ${isChecked ? '启用' : '禁用'}`); + }); + + $panel.on('click', '#cwb-viewer-enabled', function () { + const $checkbox = $(this).find('input[type="checkbox"]'); + const isChecked = !$checkbox.prop('checked'); + $checkbox.prop('checked', isChecked); + + console.log(`[CWB] Viewer switch clicked. New state: ${isChecked}`); + getSettings().cwb_viewer_enabled = isChecked; + + const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}'); + overrides.cwb_viewer_enabled = isChecked; + localStorage.setItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY, JSON.stringify(overrides)); + + saveSettingsDebounced(); + + state.viewerEnabled = isChecked; + + const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); + if ($viewerButton.length > 0) { + const shouldShow = isCwbEnabled() && isChecked; + $viewerButton.toggle(shouldShow); + } + + showToastr('info', `角色卡查看器已 ${isChecked ? '启用' : '禁用'}`); + }); + + $panel.on('click', '#cwb-incremental-update-enabled', function () { + const $checkbox = $(this).find('input[type="checkbox"]'); + const isChecked = !$checkbox.prop('checked'); // Manually toggle + $checkbox.prop('checked', isChecked); + + console.log(`[CWB] Incremental update switch clicked. New state: ${isChecked}`); + getSettings().cwb_incremental_update_enabled = isChecked; + + const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}'); + overrides.cwb_incremental_update_enabled = isChecked; + localStorage.setItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY, JSON.stringify(overrides)); + + saveSettingsDebounced(); + state.isIncrementalUpdateEnabled = isChecked; + showToastr('info', `增量更新模式已 ${isChecked ? '启用' : '禁用'}`); + }); + + $panel.on('click', '#cwb_master_enabled', function () { + const $checkbox = $(this).find('input[type="checkbox"]'); + const isChecked = !$checkbox.prop('checked'); + $checkbox.prop('checked', isChecked); + + console.log(`[CWB] Master switch clicked. New state: ${isChecked}`); + + getSettings().cwb_master_enabled = isChecked; + + const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}'); + overrides.cwb_master_enabled = isChecked; + localStorage.setItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY, JSON.stringify(overrides)); + + state.masterEnabled = isChecked; + + saveSettingsDebounced(); + + updateControlsLockState(); + + const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); + if ($viewerButton.length > 0) { + const shouldShow = isChecked && state.viewerEnabled; + $viewerButton.toggle(shouldShow); + } + + showToastr('info', `CharacterWorldBook 已 ${isChecked ? '启用' : '禁用'}`); + + $(document).trigger('cwb:master-switch-changed', { isEnabled: isChecked }); + }); +} + +function updateApiModeUI(mode) { + const fields = { + openai: [ + 'label[for="cwb-api-url"]', + '#cwb-api-url', + 'label[for="cwb-api-key"]', + '#cwb-api-key', + 'label[for="cwb-api-model"]', + '#cwb-api-model', + '#cwb-load-models' + ], + sillytavern: [ + 'label[for="cwb-tavern-profile"]', + '#cwb-tavern-profile' + ] + }; + + if (mode === 'sillytavern_preset') { + fields.openai.forEach(selector => $panel.find(selector).hide()); + fields.sillytavern.forEach(selector => $panel.find(selector).show()); + } else { + fields.sillytavern.forEach(selector => $panel.find(selector).hide()); + fields.openai.forEach(selector => $panel.find(selector).show()); + } + + updateApiStatusDisplay($panel); +} + +function loadSillyTavernPresets(showNotification = false) { + const $profileSelect = $panel.find('#cwb-tavern-profile'); + + try { + const context = window.SillyTavern?.getContext?.(); + if (!context?.extensionSettings?.connectionManager?.profiles) { + showToastr('warning', '无法获取SillyTavern配置文件列表'); + return; + } + + const profiles = context.extensionSettings.connectionManager.profiles; + + $profileSelect.empty(); + $profileSelect.append(''); + + profiles.forEach(profile => { + $profileSelect.append(``); + }); + const currentProfile = getSettings().cwb_tavern_profile; + if (currentProfile) { + $profileSelect.val(currentProfile); + } + + if (showNotification) { + showToastr('success', `已加载 ${profiles.length} 个SillyTavern预设`); + } + + } catch (error) { + logError('加载SillyTavern预设失败:', error); + showToastr('error', '加载SillyTavern预设失败'); + } +} + +function updateUiWithSettings() { + if (!$panel) return; + const settings = getSettings(); + + $panel.find('#cwb-api-mode').val(settings.cwb_api_mode || 'openai_test'); + + const currentMode = settings.cwb_api_mode || 'openai_test'; + updateApiModeUI(currentMode); + + if (currentMode === 'sillytavern_preset') { + loadSillyTavernPresets(); + } + + $panel.find('#cwb-api-url').val(settings.cwb_api_url); + $panel.find('#cwb-api-key').val(settings.cwb_api_key); + $panel.find('#cwb-tavern-profile').val(settings.cwb_tavern_profile); + + const $modelSelect = $panel.find('#cwb-api-model'); + if (settings.cwb_api_model) { + $modelSelect.empty().append(``); + } else { + $modelSelect.empty().append(''); + } + updateApiStatusDisplay($panel); + + $panel.find('#cwb-break-armor-prompt-textarea').val(settings.cwb_break_armor_prompt); + $panel.find('#cwb-char-card-prompt-textarea').val(settings.cwb_char_card_prompt); + + $panel.find('#cwb-temperature').val(settings.cwb_temperature); + $panel.find('#cwb-temperature-value').text(settings.cwb_temperature); + $panel.find('#cwb-max-tokens').val(settings.cwb_max_tokens); + $panel.find('#cwb-max-tokens-value').text(settings.cwb_max_tokens); + + $panel.find('#cwb-auto-update-threshold').val(settings.cwb_auto_update_threshold); + $panel.find('#cwb-scan-depth').val(settings.cwb_scan_depth); + $panel.find('#cwb_master_enabled-checkbox').prop('checked', settings.cwb_master_enabled); + $panel.find('#cwb-auto-update-enabled-checkbox').prop('checked', settings.cwb_auto_update_enabled); + $panel.find('#cwb-viewer-enabled-checkbox').prop('checked', settings.cwb_viewer_enabled); + $panel.find('#cwb-incremental-update-enabled-checkbox').prop('checked', settings.cwb_incremental_update_enabled); + + if (!$panel.find('#cwb-start-floor').val()) { + $panel.find('#cwb-start-floor').val(1); + } + if (!$panel.find('#cwb-end-floor').val()) { + $panel.find('#cwb-end-floor').val(1); + } + + $panel.find('input[name="cwb_worldbook_target"]').each(function() { + $(this).prop('checked', $(this).val() === settings.cwb_worldbook_target); + }); + if (settings.cwb_worldbook_target === 'custom') { + $panel.find('#cwb_worldbook_select_wrapper').show(); + } else { + $panel.find('#cwb_worldbook_select_wrapper').hide(); + } +} + +export function loadSettings() { + console.log('[CWB] Loading settings...'); + + const settings = getSettings(); + + // Initialize settings with defaults if not present + if (!settings) { + extension_settings[extensionName] = { ...cwbCompleteDefaultSettings }; + console.log('[CWB] Initialized default settings'); + } else { + // Ensure all default settings exist + Object.keys(cwbCompleteDefaultSettings).forEach(key => { + if (settings[key] === undefined || settings[key] === null) { + settings[key] = cwbCompleteDefaultSettings[key]; + } + }); + } + + const finalSettings = getSettings(); + + // Apply localStorage overrides + const overrides = JSON.parse(localStorage.getItem(CWB_BOOLEAN_SETTINGS_OVERRIDE_KEY) || '{}'); + if (overrides.cwb_master_enabled !== undefined) { + finalSettings.cwb_master_enabled = overrides.cwb_master_enabled; + } + if (overrides.cwb_auto_update_enabled !== undefined) { + finalSettings.cwb_auto_update_enabled = overrides.cwb_auto_update_enabled; + } + if (overrides.cwb_viewer_enabled !== undefined) { + finalSettings.cwb_viewer_enabled = overrides.cwb_viewer_enabled; + } + if (overrides.cwb_incremental_update_enabled !== undefined) { + finalSettings.cwb_incremental_update_enabled = overrides.cwb_incremental_update_enabled; + } + + // Update state object with current settings + state.masterEnabled = finalSettings.cwb_master_enabled; + state.viewerEnabled = finalSettings.cwb_viewer_enabled; + state.autoUpdateEnabled = finalSettings.cwb_auto_update_enabled; + state.isIncrementalUpdateEnabled = finalSettings.cwb_incremental_update_enabled; + + state.customApiConfig.url = finalSettings.cwb_api_url || ''; + state.customApiConfig.apiKey = finalSettings.cwb_api_key || ''; + state.customApiConfig.model = finalSettings.cwb_api_model || ''; + + state.currentBreakArmorPrompt = finalSettings.cwb_break_armor_prompt; + state.currentCharCardPrompt = finalSettings.cwb_char_card_prompt; + state.currentIncrementalCharCardPrompt = finalSettings.cwb_incremental_char_card_prompt; + + state.autoUpdateThreshold = finalSettings.cwb_auto_update_threshold; + state.scanDepth = finalSettings.cwb_scan_depth; + state.worldbookTarget = finalSettings.cwb_worldbook_target; + state.customWorldBook = finalSettings.cwb_custom_worldbook; + + console.log('[CWB] State updated:', { + masterEnabled: state.masterEnabled, + viewerEnabled: state.viewerEnabled, + autoUpdateEnabled: state.autoUpdateEnabled, + worldbookTarget: state.worldbookTarget, + customWorldBook: state.customWorldBook + }); + if ($panel) { + updateUiWithSettings(); + } + + updateControlsLockState(); + setTimeout(() => { + const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); + if ($viewerButton.length > 0) { + const shouldShow = isCwbEnabled() && state.viewerEnabled; + $viewerButton.toggle(shouldShow); + console.log('[CWB] Viewer button visibility updated:', shouldShow); + } + }, 100); +} diff --git a/CharacterWorldBook/src/cwb_state.js b/CharacterWorldBook/src/cwb_state.js index b5d001a..7d26e98 100644 --- a/CharacterWorldBook/src/cwb_state.js +++ b/CharacterWorldBook/src/cwb_state.js @@ -1,34 +1,34 @@ - -export const SCRIPT_ID_PREFIX = 'cwb'; -export const CHAR_CARD_VIEWER_BUTTON_ID = `${SCRIPT_ID_PREFIX}-viewer-button`; -export const CHAR_CARD_VIEWER_POPUP_ID = `${SCRIPT_ID_PREFIX}-viewer-popup`; -export const NEW_MESSAGE_DEBOUNCE_DELAY = 4000; -export const MIN_POLLING_INTERVAL = 10000; -export const MAX_POLLING_INTERVAL = 100000; -export const POLLING_INTERVAL_STEP = 10000; - -export const state = { - masterEnabled: false, - STORAGE_KEY_VIEWER_BUTTON_POS: 'cwb_viewer_button_position', - - customApiConfig: { url: '', apiKey: '', model: '' }, - - currentBreakArmorPrompt: '', - currentCharCardPrompt: '', - currentIncrementalCharCardPrompt: '', - - autoUpdateThreshold: null, - autoUpdateEnabled: null, - - viewerEnabled: null, - isIncrementalUpdateEnabled: null, - worldbookTarget: 'primary', - customWorldBook: null, - - isAutoUpdatingCard: false, - newMessageDebounceTimer: null, - pollingTimer: null, - currentPollingInterval: MIN_POLLING_INTERVAL, - allChatMessages: [], - currentChatFileIdentifier: 'unknown_chat_init', -}; + +export const SCRIPT_ID_PREFIX = 'cwb'; +export const CHAR_CARD_VIEWER_BUTTON_ID = `${SCRIPT_ID_PREFIX}-viewer-button`; +export const CHAR_CARD_VIEWER_POPUP_ID = `${SCRIPT_ID_PREFIX}-viewer-popup`; +export const NEW_MESSAGE_DEBOUNCE_DELAY = 4000; +export const MIN_POLLING_INTERVAL = 10000; +export const MAX_POLLING_INTERVAL = 100000; +export const POLLING_INTERVAL_STEP = 10000; + +export const state = { + masterEnabled: false, + STORAGE_KEY_VIEWER_BUTTON_POS: 'cwb_viewer_button_position', + + customApiConfig: { url: '', apiKey: '', model: '' }, + + currentBreakArmorPrompt: '', + currentCharCardPrompt: '', + currentIncrementalCharCardPrompt: '', + + autoUpdateThreshold: null, + autoUpdateEnabled: null, + + viewerEnabled: null, + isIncrementalUpdateEnabled: null, + worldbookTarget: 'primary', + customWorldBook: null, + + isAutoUpdatingCard: false, + newMessageDebounceTimer: null, + pollingTimer: null, + currentPollingInterval: MIN_POLLING_INTERVAL, + allChatMessages: [], + currentChatFileIdentifier: 'unknown_chat_init', +}; diff --git a/CharacterWorldBook/src/cwb_uiManager.js b/CharacterWorldBook/src/cwb_uiManager.js index 9447dc4..a745756 100644 --- a/CharacterWorldBook/src/cwb_uiManager.js +++ b/CharacterWorldBook/src/cwb_uiManager.js @@ -1,740 +1,740 @@ - import { SCRIPT_ID_PREFIX, CHAR_CARD_VIEWER_BUTTON_ID, CHAR_CARD_VIEWER_POPUP_ID, state } from './cwb_state.js'; - import { logDebug, logError, showToastr, escapeHtml, parseCustomFormat, buildCustomFormat, isCwbEnabled } from './cwb_utils.js'; - import { deleteLorebookEntries, getTargetWorldBook } from './cwb_lorebookManager.js'; - import { manualUpdateLogic } from './cwb_core.js'; - import { testCwbConnection, fetchCwbModels } from './cwb_apiService.js'; - import { extensionName } from '../../utils/settings.js'; - import { extension_settings } from '/scripts/extensions.js'; - import { saveSettingsDebounced } from '/script.js'; - import { amilyHelper } from '../../core/tavern-helper/main.js'; - - const { jQuery: $, SillyTavern } = window; - - function createCharCardViewerPopupHtml(displayItems) { - const pathToLabelMap = { - 'narrative_essence.core_traits.name': '特质名称', - 'narrative_essence.key_relationships.name': '关系人姓名', - 'NE.trait.name': '特质名称', - 'NE.rel.name': '关系人姓名', - }; - const keyToLabelMap = { - 'name': '姓名', - // Old keys - 'archetype': '身份原型', - 'gender': '性别', - 'age': '年龄', - 'race': '种族', - 'current_status': '当前状态', - 'first_impression': '第一印象', - 'key_features': '显著特征', - 'attire': '衣着风格', - 'mannerisms': '习惯举止', - 'voice': '声音特征', - 'tags': '性格标签', - 'description': '性格详述', - 'motivation': '内在驱动', - 'values': '价值观', - 'inner_conflict': '内心挣扎', - 'interaction_style': '互动风格', - 'skills': '技能能力', - 'reputation': '他人声望', - 'core_traits': '核心特质', - 'verbal_patterns': '语言范式', - 'key_relationships': '关键关系', - 'definition': '特质定义', - 'evidence': '具体事例', - 'style_summary': '风格总结', - 'quotes': '代表性引言', - 'summary': '关系概述', - - // New short keys - 'CI': '核心认同', - 'PI': '物理印记', - 'PP': '心智侧写', - 'SM': '社交矩阵', - 'NE': '叙事精粹', - - 'arch': '身份原型', - 'gen': '性别', - // age is same - // race is same - 'status': '当前状态', - - 'first': '第一印象', - 'feat': '显著特征', - // attire is same - 'manner': '习惯举止', - // voice is same - - // tags is same - 'desc': '性格详述', - 'mot': '内在驱动', - 'val': '价值观', - 'conf': '内心挣扎', - - 'style': '互动风格/风格总结', // Shared by SM.style and NE.verb.style - 'skill': '技能能力', - 'rep': '他人声望', - - 'trait': '核心特质', - 'verb': '语言范式', - 'rel': '关键关系', - - 'def': '特质定义', - 'evid': '具体事例', - 'quote': '代表性引言', - 'sum': '关系概述', - }; - const getLabel = (key, path) => { - const pathKey = path.replace(/\.\d+\./g, '.'); - if (pathToLabelMap[pathKey]) { - return pathToLabelMap[pathKey]; - } - return keyToLabelMap[key] || key.replace(/_/g, ' '); - }; - - const renderField = (label, path, value, isTextarea = false, isArray = false) => { - const escapedLabel = escapeHtml(label); - const escapedValue = escapeHtml(isArray ? value.join('\n') : value || ''); - - const isLongContent = (value && String(value).length > 50) || (Array.isArray(value) && value.length > 1); - const rows = isArray ? Math.max(3, value.length) : (isLongContent ? 4 : 2); - - const inputElement = ``; - - return `看什么?没更新角色条目就等着我给你显示出来条目吗?想关悬浮窗就点角色世界,功能设置关掉。
看什么?没更新角色条目就等着我给你显示出来条目吗?想关悬浮窗就点角色世界,功能设置关掉。
检测到当前提示词版本为旧版本。
+为更好的体验,请点击 一键更新,会将提示词恢复成最新版本提示词链默认状态。
+或者点击 保留自定义 按钮,则保留您之前的提示词。
+${block.description}
- ${block.description}
+ 请先在“小说处理”标签页中选择一个世界书。
'; - return; - } - - container.innerHTML = '正在加载条目...
'; - - try { - const allEntries = await safeLorebookEntries(selectedBook); - let managedEntries = allEntries.filter(e => e.comment?.startsWith('[Amily2小说处理]')); - - if (managedEntries.length === 0) { - container.innerHTML = '未找到由小说处理功能生成的条目。
'; - return; - } - - container.innerHTML = ''; - - const summaryEntries = managedEntries.filter(e => e.comment.replace('[Amily2小说处理]', '').trim().startsWith('章节内容概述')); - const otherEntries = managedEntries.filter(e => !e.comment.replace('[Amily2小说处理]', '').trim().startsWith('章节内容概述')); - const sortedEntries = otherEntries.concat(summaryEntries); - - sortedEntries.forEach(entry => { - const entryElement = document.createElement('div'); - entryElement.className = 'world-book-entry-item'; - entryElement.dataset.entryId = entry.uid; - - const title = entry.comment.replace('[Amily2小说处理]', '').trim(); - - const renderContent = (content) => { - const trimmedContent = content.trim(); - if (trimmedContent.startsWith('graph') || trimmedContent.startsWith('flowchart')) { - try { - const lines = trimmedContent.split('\n').map(l => l.trim()).filter(l => l.includes('-->') || l.includes('--')); - let body = ''; - lines.forEach(line => { - if (line.startsWith('flowchart')) return; - let source = '', rel = '', target = ''; - - let match = line.match(/(.+?)\s*--\s*"(.*?)"\s*-->(.+)/); - if (match) { - [source, rel, target] = [match[1], match[2], match[3]]; - } else { - match = line.match(/(.+?)\s*-->\s*\|(.*?)\|(.+)/); - if (match) { - [source, rel, target] = [match[1], match[2], match[3]]; - } else { - match = line.match(/(.+?)\s*-->(.+)/); - if (match) { - [source, target] = [match[1], match[2]]; - rel = '(直接关联)'; - } - } - } - - if (source && target) { - body += `| 源头 | 关系 | 目标 |
|---|
${escapeHTML(content)}`;
- }
- }
- if (trimmedContent.includes('|') && trimmedContent.includes('\n')) {
- try {
- const rows = trimmedContent.split('\n').filter(row => row.trim() && row.includes('|'));
- let header = '';
- let body = '';
- let isHeaderRow = true;
- rows.forEach(rowStr => {
- if (rowStr.includes('---')) return;
- const cells = rowStr.split('|').filter(c => c.trim()).map(cell => `${escapeHTML(content)}`;
- }
- }
- return `${escapeHTML(content)}`;
- };
-
- entryElement.innerHTML = `
- 加载失败: ${error.message}
`; - } -} - - -function bindTabEvents() { - const tabs = document.querySelectorAll('.glossary-tab'); - const contents = document.querySelectorAll('.glossary-content'); - - tabs.forEach(tab => { - tab.addEventListener('click', () => { - const tabId = tab.dataset.tab; - - tabs.forEach(t => t.classList.remove('active')); - tab.classList.add('active'); - - contents.forEach(content => { - if (content.id === `glossary-content-${tabId}`) { - content.classList.add('active'); - } else { - content.classList.remove('active'); - } - }); - - if (tabId === 'context') { - renderWorldBookEntries(); - } else if (tabId === 'tools') { - const statusEl = document.getElementById('reorganize-status'); - if (statusEl) { - if (moduleState.selectedWorldBook) { - statusEl.textContent = `当前已选择世界书: "${moduleState.selectedWorldBook}"。可以开始重组。`; - statusEl.style.color = ''; - } else { - statusEl.textContent = '请先在“小说处理”标签页中选择一个世界书。'; - statusEl.style.color = '#ffdb58'; // Warning color - } - } - } - }); - }); -} - -function bindReorganizeEvents() { - const reorganizeBtn = document.getElementById('reorganize-entries-by-heading'); - const statusEl = document.getElementById('reorganize-status'); - const headingsListEl = document.getElementById('reorganize-headings-list'); - - if (!reorganizeBtn || !statusEl || !headingsListEl) return; - - const updateStatusCallback = (message, type = 'info') => { - statusEl.textContent = message; - statusEl.style.color = type === 'error' ? '#ff8a8a' : (type === 'success' ? '#8aff8a' : ''); - }; - - reorganizeBtn.addEventListener('click', async () => { - const headingsToProcess = headingsListEl.value.split('\n').map(h => h.trim()).filter(Boolean); - if (headingsToProcess.length === 0) { - updateStatusCallback('错误:请在文本框中输入至少一个要重组的标题。', 'error'); - return; - } - - const bookName = moduleState.selectedWorldBook; - if (!bookName) { - updateStatusCallback('错误:请先在“小说处理”标签页中选择一个世界书。', 'error'); - return; - } - - const originalHtml = reorganizeBtn.innerHTML; - reorganizeBtn.disabled = true; - reorganizeBtn.innerHTML = ' 正在重组...'; - - try { - await reorganizeEntriesByHeadings(bookName, headingsToProcess, updateStatusCallback); - - if (document.querySelector('.glossary-tab[data-tab="context"].active')) { - renderWorldBookEntries(); - } - } catch (error) { - console.error('An error occurred during reorganization:', error); - } finally { - reorganizeBtn.disabled = false; - reorganizeBtn.innerHTML = originalHtml; - } - }); -} - -function bindNovelProcessEvents() { - const fileInput = document.getElementById('novel-file-input'); - const fileLabel = document.querySelector('label[for="novel-file-input"]'); - const dbSelectBtn = document.getElementById('select-from-database-button'); - const processBtn = document.getElementById('novel-confirm-and-process'); - const chunkSizeInput = document.getElementById('novel-chunk-size'); - const chunkCountEl = document.getElementById('novel-chunk-count'); - const chunkPreviewEl = document.getElementById('novel-chunk-preview'); - - let fileContent = ''; - let processingState = { - chunks: [], - batchSize: 1, - forceNew: false, - selectedWorldBook: '', - currentIndex: 0, - isAborted: false, - isRunning: false, - lastStatus: 'idle', - }; - - function updateChunks() { - if (!fileContent) return; - const chunkSize = parseInt(chunkSizeInput.value, 10) || 5000; - const newChunks = []; - for (let i = 0; i < fileContent.length; i += chunkSize) { - newChunks.push({ title: `Part ${i/chunkSize + 1}`, content: fileContent.substring(i, i + chunkSize) }); - } - processingState.chunks = newChunks; - - chunkCountEl.textContent = newChunks.length; - chunkPreviewEl.innerHTML = newChunks.map((chunk, index) => - `请先在“小说处理”标签页中选择一个世界书。
'; + return; + } + + container.innerHTML = '正在加载条目...
'; + + try { + const allEntries = await safeLorebookEntries(selectedBook); + let managedEntries = allEntries.filter(e => e.comment?.startsWith('[Amily2小说处理]')); + + if (managedEntries.length === 0) { + container.innerHTML = '未找到由小说处理功能生成的条目。
'; + return; + } + + container.innerHTML = ''; + + const summaryEntries = managedEntries.filter(e => e.comment.replace('[Amily2小说处理]', '').trim().startsWith('章节内容概述')); + const otherEntries = managedEntries.filter(e => !e.comment.replace('[Amily2小说处理]', '').trim().startsWith('章节内容概述')); + const sortedEntries = otherEntries.concat(summaryEntries); + + sortedEntries.forEach(entry => { + const entryElement = document.createElement('div'); + entryElement.className = 'world-book-entry-item'; + entryElement.dataset.entryId = entry.uid; + + const title = entry.comment.replace('[Amily2小说处理]', '').trim(); + + const renderContent = (content) => { + const trimmedContent = content.trim(); + if (trimmedContent.startsWith('graph') || trimmedContent.startsWith('flowchart')) { + try { + const lines = trimmedContent.split('\n').map(l => l.trim()).filter(l => l.includes('-->') || l.includes('--')); + let body = ''; + lines.forEach(line => { + if (line.startsWith('flowchart')) return; + let source = '', rel = '', target = ''; + + let match = line.match(/(.+?)\s*--\s*"(.*?)"\s*-->(.+)/); + if (match) { + [source, rel, target] = [match[1], match[2], match[3]]; + } else { + match = line.match(/(.+?)\s*-->\s*\|(.*?)\|(.+)/); + if (match) { + [source, rel, target] = [match[1], match[2], match[3]]; + } else { + match = line.match(/(.+?)\s*-->(.+)/); + if (match) { + [source, target] = [match[1], match[2]]; + rel = '(直接关联)'; + } + } + } + + if (source && target) { + body += `| 源头 | 关系 | 目标 |
|---|
${escapeHTML(content)}`;
+ }
+ }
+ if (trimmedContent.includes('|') && trimmedContent.includes('\n')) {
+ try {
+ const rows = trimmedContent.split('\n').filter(row => row.trim() && row.includes('|'));
+ let header = '';
+ let body = '';
+ let isHeaderRow = true;
+ rows.forEach(rowStr => {
+ if (rowStr.includes('---')) return;
+ const cells = rowStr.split('|').filter(c => c.trim()).map(cell => `${escapeHTML(content)}`;
+ }
+ }
+ return `${escapeHTML(content)}`;
+ };
+
+ entryElement.innerHTML = `
+ 加载失败: ${error.message}
`; + } +} + + +function bindTabEvents() { + const tabs = document.querySelectorAll('.glossary-tab'); + const contents = document.querySelectorAll('.glossary-content'); + + tabs.forEach(tab => { + tab.addEventListener('click', () => { + const tabId = tab.dataset.tab; + + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + contents.forEach(content => { + if (content.id === `glossary-content-${tabId}`) { + content.classList.add('active'); + } else { + content.classList.remove('active'); + } + }); + + if (tabId === 'context') { + renderWorldBookEntries(); + } else if (tabId === 'tools') { + const statusEl = document.getElementById('reorganize-status'); + if (statusEl) { + if (moduleState.selectedWorldBook) { + statusEl.textContent = `当前已选择世界书: "${moduleState.selectedWorldBook}"。可以开始重组。`; + statusEl.style.color = ''; + } else { + statusEl.textContent = '请先在“小说处理”标签页中选择一个世界书。'; + statusEl.style.color = '#ffdb58'; // Warning color + } + } + } + }); + }); +} + +function bindReorganizeEvents() { + const reorganizeBtn = document.getElementById('reorganize-entries-by-heading'); + const statusEl = document.getElementById('reorganize-status'); + const headingsListEl = document.getElementById('reorganize-headings-list'); + + if (!reorganizeBtn || !statusEl || !headingsListEl) return; + + const updateStatusCallback = (message, type = 'info') => { + statusEl.textContent = message; + statusEl.style.color = type === 'error' ? '#ff8a8a' : (type === 'success' ? '#8aff8a' : ''); + }; + + reorganizeBtn.addEventListener('click', async () => { + const headingsToProcess = headingsListEl.value.split('\n').map(h => h.trim()).filter(Boolean); + if (headingsToProcess.length === 0) { + updateStatusCallback('错误:请在文本框中输入至少一个要重组的标题。', 'error'); + return; + } + + const bookName = moduleState.selectedWorldBook; + if (!bookName) { + updateStatusCallback('错误:请先在“小说处理”标签页中选择一个世界书。', 'error'); + return; + } + + const originalHtml = reorganizeBtn.innerHTML; + reorganizeBtn.disabled = true; + reorganizeBtn.innerHTML = ' 正在重组...'; + + try { + await reorganizeEntriesByHeadings(bookName, headingsToProcess, updateStatusCallback); + + if (document.querySelector('.glossary-tab[data-tab="context"].active')) { + renderWorldBookEntries(); + } + } catch (error) { + console.error('An error occurred during reorganization:', error); + } finally { + reorganizeBtn.disabled = false; + reorganizeBtn.innerHTML = originalHtml; + } + }); +} + +function bindNovelProcessEvents() { + const fileInput = document.getElementById('novel-file-input'); + const fileLabel = document.querySelector('label[for="novel-file-input"]'); + const dbSelectBtn = document.getElementById('select-from-database-button'); + const processBtn = document.getElementById('novel-confirm-and-process'); + const chunkSizeInput = document.getElementById('novel-chunk-size'); + const chunkCountEl = document.getElementById('novel-chunk-count'); + const chunkPreviewEl = document.getElementById('novel-chunk-preview'); + + let fileContent = ''; + let processingState = { + chunks: [], + batchSize: 1, + forceNew: false, + selectedWorldBook: '', + currentIndex: 0, + isAborted: false, + isRunning: false, + lastStatus: 'idle', + }; + + function updateChunks() { + if (!fileContent) return; + const chunkSize = parseInt(chunkSizeInput.value, 10) || 5000; + const newChunks = []; + for (let i = 0; i < fileContent.length; i += chunkSize) { + newChunks.push({ title: `Part ${i/chunkSize + 1}`, content: fileContent.substring(i, i + chunkSize) }); + } + processingState.chunks = newChunks; + + chunkCountEl.textContent = newChunks.length; + chunkPreviewEl.innerHTML = newChunks.map((chunk, index) => + `
+翰林院宝库状态
+--------------------
+集合ID: ${collectionId}
+忆识总数: ${count}
+--------------------
+API端点: ${settings.retrieval.apiEndpoint}
+所用模型: ${settings.retrieval.embeddingModel}
+
+ `;
+
+ toastr.info(statsText, '宝库状态', {
+ timeOut: 15000, // 延长显示时间
+ extendedTimeOut: 5000,
+ tapToDismiss: true,
+ closeButton: true,
+ });
+
+ log(`查看宝库状态成功:集合ID=${collectionId}, 忆识总数=${count}`, 'success');
+
+ } catch (error) {
+ console.error('[翰林院-枢纽] 查询宝库状态失败:', error);
+ toastr.error(`查询宝库状态失败: ${error.message}`, '严重错误');
+ log(`查询宝库状态失败: ${error.message}`, 'error');
+ }
+}
+function showRulesModal(type) {
+ const settings = HanlinyuanCore.getSettings();
+ const config = settings[type];
+ if (!config) {
+ console.error(`[翰林院-枢纽] 未找到类型为 "${type}" 的配置项。`);
+ return;
+ }
+
+ const title = type === 'condensation' ? '编辑凝识内容排除规则' : '编辑检索内容排除规则';
+ const rules = config.exclusionRules || [];
+
+ const createRuleRowHtml = (rule = { start: '', end: '' }, index) => `
+ 在这里定义需要从提取内容中排除的文本片段。例如,排除HTML注释,可以设置开始字符串为 \`\`。
+暂无规则
'}暂无规则
'); + } + }); + + // 如果是检索预处理设置,则绑定标签提取的UI事件 + if (type === 'queryPreprocessing') { + const tagToggle = dialogElement.find('#hly-modal-tag-extraction-enabled'); + const tagInputContainer = dialogElement.find('#hly-modal-tag-input-container'); + tagToggle.on('change', () => { + tagInputContainer.css('display', tagToggle.is(':checked') ? 'block' : 'none'); + }); + } + } + }); +} + +function previewCondensation() { + const resultsEl = document.getElementById('hly-condensation-results'); + try { + // 1. 获取UI设置和新规则 + const settings = HanlinyuanCore.getSettings(); + const exclusionRules = settings.condensation.exclusionRules || []; + const overrideMessageTypes = { + user: document.getElementById('hly-include-user').checked, + ai: document.getElementById('hly-include-ai').checked, + }; + const useTagExtraction = document.getElementById('hly-tag-extraction-toggle').checked; + const tagsToExtract = useTagExtraction + ? document.getElementById('hly-tag-input').value.split(',').map(t => t.trim()).filter(Boolean) + : []; + + // 2. 获取原始消息 + const messages = HanlinyuanCore.getMessagesForCondensation(overrideMessageTypes); + + if (!messages || messages.length === 0) { + resultsEl.textContent = '根据当前勾选条件,未找到符合的消息可供预览。'; + toastr.warning('未找到符合条件的消息。', '翰林院启奏'); + return; + } + + // 3. 处理消息内容 + const fullChat = getContext().chat; + const processedMessages = messages.map((msg, index) => { + let content; + + // 【V5.2 最终规则】用户消息不受标签提取和内容排除的任何影响 + if (msg.is_user) { + content = msg.mes; + } + // AI消息则遵循所有规则 + else { + if (useTagExtraction && tagsToExtract.length > 0) { + const blocks = extractBlocksByTags(msg.mes, tagsToExtract); + if (blocks.length > 0) { + // 恢复逻辑:直接连接完整的块,保留标签 + content = blocks.join('\n\n'); + } else { + content = msg.mes; // 保留原始内容 + } + } else { + content = msg.mes; + } + // 内容排除规则只对AI消息生效 + content = applyExclusionRules(content, exclusionRules); + } + + const floorIndex = fullChat.findIndex(chatMsg => chatMsg === msg); + const floor = floorIndex !== -1 ? floorIndex + 1 : -1; + + return { + id: `preview-item-${index}`, + name: msg.name, + content: content.trim(), + floor: floor, // 【V6 新增】保留绝对楼层号 + is_user: msg.is_user, // 【V6 新增】保留用户标识 + send_date: msg.send_date, // 【V6 新增】保留发送时间 + }; + }).filter(item => item.content); // 过滤掉处理后内容为空的条目 + + if (processedMessages.length === 0) { + resultsEl.textContent = '根据标签提取或内容排除条件,未找到任何有效内容。'; + toastr.warning('根据标签提取或内容排除条件,未找到任何有效内容。', '翰林院启奏'); + return; + } + + // 4. 构建编辑器HTML (V6 - 增加 data-* 属性以保留元数据) + const editorHtml = processedMessages.map((item, index) => ` +加载中...
'); + try { + const lorebooks = await safeLorebooks(); + bookListContainer.empty(); + if (!lorebooks || lorebooks.length === 0) { + bookListContainer.html('未找到世界书。
'); + 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 = $(` +加载世界书列表失败。
'); + } + } + + // 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(); + }); +} + +export function initializePlotOptimizationBindings() { + const panel = $('#amily2_plot_optimization_panel'); + if (panel.length === 0 || panel.data('events-bound')) { + return; + } + + // 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 key_part = (element.name || element.id).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 (key === 'plotOpt_api_mode') { + opt_updateApiUrlVisibility(panel, value); + } + + if (element.name === 'amily2_opt_worldbook_source') { + opt_updateWorldbookSourceVisibility(panel, value); + opt_loadWorldbookEntries(panel); + } + }; + const allInputSelectors = [ + 'input[type="checkbox"]', 'input[type="radio"]', 'select:not(#amily2_opt_model_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.on('change.amily2_opt', '#amily2_opt_model_select', function() { + const selectedModel = $(this).val(); + if (selectedModel) { + panel.find('#amily2_opt_model').val(selectedModel).trigger('change'); + } + }); + + + panel.on('click.amily2_opt', '#amily2_opt_refresh_tavern_api_profiles', () => { + opt_loadTavernApiProfiles(panel); + }); + + panel.on('change.amily2_opt', '#amily2_opt_tavern_api_profile_select', function() { + const value = $(this).val(); + opt_saveSetting('tavernProfile', value); + }); + + + 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 事件绑定函数 ========== +function bindJqyhApiEvents() { + console.log("[Amily2号-Jqyh工部] 正在绑定Jqyh API事件..."); + + const updateAndSaveSetting = (key, value) => { + console.log(`[Amily2-Jqyh令] 收到指令: 将 [${key}] 设置为 ->`, value); + if (!extension_settings[extensionName]) { + extension_settings[extensionName] = {}; + } + extension_settings[extensionName][key] = value; + saveSettingsDebounced(); + console.log(`[Amily2-Jqyh录] [${key}] 的新状态已保存。`); + }; + + // Jqyh API 开关控制 + const jqyhToggle = document.getElementById('amily2_jqyh_enabled'); + const jqyhContent = document.getElementById('amily2_jqyh_content'); + + if (jqyhToggle && jqyhContent) { + jqyhToggle.checked = extension_settings[extensionName].jqyhEnabled ?? false; + jqyhContent.style.display = jqyhToggle.checked ? 'block' : 'none'; + + jqyhToggle.addEventListener('change', function() { + const isEnabled = this.checked; + updateAndSaveSetting('jqyhEnabled', isEnabled); + jqyhContent.style.display = isEnabled ? 'block' : 'none'; + }); + } + + // API模式切换 + const apiModeSelect = document.getElementById('amily2_jqyh_api_mode'); + const compatibleConfig = document.getElementById('amily2_jqyh_compatible_config'); + const presetConfig = document.getElementById('amily2_jqyh_preset_config'); + + if (apiModeSelect && compatibleConfig && presetConfig) { + apiModeSelect.value = extension_settings[extensionName].jqyhApiMode || 'openai_test'; + + const updateConfigVisibility = (mode) => { + if (mode === 'sillytavern_preset') { + compatibleConfig.style.display = 'none'; + presetConfig.style.display = 'block'; + loadJqyhTavernPresets(); + } else { + compatibleConfig.style.display = 'block'; + presetConfig.style.display = 'none'; + } + }; + + updateConfigVisibility(apiModeSelect.value); + + apiModeSelect.addEventListener('change', function() { + updateAndSaveSetting('jqyhApiMode', this.value); + updateConfigVisibility(this.value); + }); + } + + // API配置字段绑定 + const apiFields = [ + { id: 'amily2_jqyh_api_url', key: 'jqyhApiUrl' }, + { id: 'amily2_jqyh_api_key', key: 'jqyhApiKey' }, + { id: 'amily2_jqyh_model', key: 'jqyhModel' } + ]; + + apiFields.forEach(field => { + const element = document.getElementById(field.id); + if (element) { + element.value = extension_settings[extensionName][field.key] || ''; + element.addEventListener('change', function() { + updateAndSaveSetting(field.key, this.value); + }); + } + }); + + // 滑块控件绑定 + const sliderFields = [ + { id: 'amily2_jqyh_max_tokens', key: 'jqyhMaxTokens', defaultValue: 4000 }, + { id: 'amily2_jqyh_temperature', key: 'jqyhTemperature', defaultValue: 0.7 } + ]; + + sliderFields.forEach(field => { + const slider = document.getElementById(field.id); + const display = document.getElementById(field.id + '_value'); + if (slider && display) { + const value = extension_settings[extensionName][field.key] || field.defaultValue; + slider.value = value; + display.textContent = value; + + slider.addEventListener('input', function() { + const newValue = parseFloat(this.value); + display.textContent = newValue; + updateAndSaveSetting(field.key, newValue); + }); + } + }); + + // SillyTavern预设选择器 + const tavernProfileSelect = document.getElementById('amily2_jqyh_tavern_profile'); + if (tavernProfileSelect) { + tavernProfileSelect.value = extension_settings[extensionName].jqyhTavernProfile || ''; + tavernProfileSelect.addEventListener('change', function() { + updateAndSaveSetting('jqyhTavernProfile', this.value); + }); + } + + // 测试连接按钮 + const testButton = document.getElementById('amily2_jqyh_test_connection'); + if (testButton) { + testButton.addEventListener('click', async function() { + const button = $(this); + const originalHtml = button.html(); + button.prop('disabled', true).html(' 测试中'); + + try { + await testJqyhApiConnection(); + } catch (error) { + console.error('[Amily2号-Jqyh] 测试连接失败:', error); + } finally { + button.prop('disabled', false).html(originalHtml); + } + }); + } + + const fetchModelsButton = document.getElementById('amily2_jqyh_fetch_models'); + const modelSelect = document.getElementById('amily2_jqyh_model_select'); + const modelInput = document.getElementById('amily2_jqyh_model'); + + if (fetchModelsButton && modelSelect && modelInput) { + fetchModelsButton.addEventListener('click', async function() { + const button = $(this); + const originalHtml = button.html(); + button.prop('disabled', true).html(' 获取中'); + + try { + const models = await fetchJqyhModels(); + + if (models && models.length > 0) { + modelSelect.innerHTML = ''; + models.forEach(model => { + const option = document.createElement('option'); + option.value = model.id || model.name || model; + option.textContent = model.name || model.id || model; + modelSelect.appendChild(option); + }); + modelSelect.style.display = 'block'; + modelInput.style.display = 'none'; + + modelSelect.addEventListener('change', function() { + const selectedModel = this.value; + modelInput.value = selectedModel; + updateAndSaveSetting('jqyhModel', selectedModel); + console.log(`[Amily2-Jqyh] 已选择模型: ${selectedModel}`); + }); + + toastr.success(`成功获取 ${models.length} 个模型`, 'Jqyh 模型获取'); + } else { + toastr.warning('未获取到任何模型', 'Jqyh 模型获取'); + } + + } catch (error) { + console.error('[Amily2号-Jqyh] 获取模型列表失败:', error); + toastr.error(`获取模型失败: ${error.message}`, 'Jqyh 模型获取'); + } finally { + button.prop('disabled', false).html(originalHtml); + } + }); + } +} + +async function loadJqyhTavernPresets() { + const select = document.getElementById('amily2_jqyh_tavern_profile'); + if (!select) return; + + const currentValue = select.value; + select.innerHTML = ''; + + try { + const context = getContext(); + const tavernProfiles = context.extensionSettings?.connectionManager?.profiles || []; + + select.innerHTML = ''; + + if (tavernProfiles.length > 0) { + tavernProfiles.forEach(profile => { + if (profile.api && profile.preset) { + const option = document.createElement('option'); + option.value = profile.id; + option.textContent = profile.name || profile.id; + if (profile.id === currentValue) { + option.selected = true; + } + select.appendChild(option); + } + }); + } else { + select.innerHTML = ''; + } + } catch (error) { + console.error('[Amily2号-Jqyh] 加载SillyTavern预设失败:', error); + select.innerHTML = ''; + } +} + +// ========== 图标位置切换(跨模块通用事件) ========== +$(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); +}); diff --git a/ui/profile-sync.js b/ui/profile-sync.js new file mode 100644 index 0000000..7a48170 --- /dev/null +++ b/ui/profile-sync.js @@ -0,0 +1,402 @@ +/** + * ui/profile-sync.js — API Profile → 子面板 UI 同步 + * + * 当某功能槽分配了 Profile 时: + * 1. 隐藏对应功能区的 API 连接配置字段(保留温度/Token 等生成参数) + * 2. 注入一张状态卡,显示 Profile 信息 + 测试连接 / 获取模型按钮 + * + * 当槽位未分配时:恢复旧字段显示,移除状态卡。 + * + * 用法: + * import { syncAllSlots, syncSlot } from './profile-sync.js'; + * await syncAllSlots(); // 面板初始化时全量同步 + * await syncSlot('main'); // 单个槽位分配变更时调用 + * + * 外部事件: + * document 上监听 'amily2:slotAssigned',detail = { slot } + * 由 api-config-bindings.js 在分配变更后 dispatch。 + */ + +import { apiProfileManager } from '../utils/config/ApiProfileManager.js'; +import { getRequestHeaders } from '/script.js'; +import { testApiConnection } from '../core/api.js'; +import { testConcurrentApiConnection } from '../core/api/ConcurrentApi.js'; +import { testNgmsApiConnection } from '../core/api/Ngms_api.js'; +import { testNccsApiConnection } from '../core/api/NccsApi.js'; + +// ── 常量 ────────────────────────────────────────────────────────────────────── + +// 用于通过子元素定位父 block 的选择器 +const BLOCK_SEL = '.amily2_settings_block, .control-group, .amily2_opt_settings_block'; + +const CARD_CLASS = 'amily2_profile_status_card'; +const CARD_SLOT_ATTR = 'data-card-slot'; +const HIDDEN_ATTR = 'data-profile-hidden'; + +// ── 槽位 → DOM 映射 ─────────────────────────────────────────────────────────── +// +// container : 状态卡注入的父容器(CSS 选择器或 'closest-fieldset:xxx') +// hideParentBlock: 通过子元素选择器找到其最近的 BLOCK_SEL 父元素并隐藏 +// hideDirectly : 直接隐藏的元素选择器 +// hideWithLabel : 隐藏元素(上溯到容器直接子元素)+ 前一个兄弟