import { getContext } from '/scripts/extensions.js'; import { extensionBasePath } from '../utils/settings.js'; import * as HanlinyuanCore from '../core/rag-processor.js'; import * as Historiographer from '../core/historiographer.js'; import * as ContextUtils from '../core/utils/context-utils.js'; import * as IngestionManager from '../core/ingestion-manager.js'; import { showContentModal, showHtmlModal } from './page-window.js'; import { extractBlocksByTags, applyExclusionRules } from '../core/utils/rag-tag-extractor.js'; import { ruleProfileManager, resolveCondensationRuleConfig } from '../utils/config/RuleProfileManager.js'; import { filterWorldbooks, filterWorldbookEntries, highlightSearchMatch, debounce } from '../core/rag-processor.js'; 'use strict'; function escapeTextareaContent(text) { return String(text ?? '') .replace(/&/g, '&') .replace(//g, '>'); } function escapeAttribute(text) { return String(text ?? '') .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); } function _populateHlyRuleProfileSelect(select, slot, detail) { const profiles = detail?.profiles ?? ruleProfileManager.listProfiles(); const assigned = detail?.assignments?.[slot] ?? ruleProfileManager.getAssignment(slot) ?? ''; select.innerHTML = [ '', ...profiles.map(p => `` ), ].join(''); } function setupGlobalEventHandlers() { window.saveHLYSettings = () => saveSettingsFromUI(false); // false表示非自动保存 window.resetHLYSettings = resetSettingsToUI; window.testHLYApi = testApi; window.fetchHLYEmbeddingModels = fetchHLYEmbeddingModels; window.fetchHLYRerankModels = fetchHLYRerankModels; // 新增 window.updateHLYMemoryCount = updatePanelStatus; window.purgeHLYStorage = purgeStorage; window.startHLYCondensation = startCondensation; window.previewHLYCondensation = previewCondensation; window.ingestHLYManualText = ingestManualText; window.hlyLog = log; window.showHLYStats = showStats; // 【新增】书库编纂相关 window.startHLYHistoriography = startHistoriography; } function updateAndSaveSetting(key, value) { const settings = HanlinyuanCore.getSettings(); if (!settings) return; const keys = key.split('.'); let current = settings; for (let i = 0; i < keys.length - 1; i++) { current = current[keys[i]] = current[keys[i]] || {}; } current[keys[keys.length - 1]] = value; HanlinyuanCore.saveSettings(); if (key === 'condensation.tagExtractionEnabled') { syncHanlinLinkedRuleProfile('condensation', { tagExtractionEnabled: value }); } else if (key === 'condensation.tags') { syncHanlinLinkedRuleProfile('condensation', { tags: value }); } else if (key === 'queryPreprocessing.tagExtractionEnabled') { syncHanlinLinkedRuleProfile('queryPreprocessing', { tagExtractionEnabled: value }); } else if (key === 'queryPreprocessing.tags') { syncHanlinLinkedRuleProfile('queryPreprocessing', { tags: value }); } log(`[自动保存] 设置项 '${key}' 已更新为: ${JSON.stringify(value)}`, 'success'); } function bindAutoSaveEvents() { const container = document.getElementById('hly-modal-container'); if (!container) return; container.addEventListener('change', (event) => { const target = event.target; const key = target.dataset.settingKey; if (!key) return; let value; const type = target.dataset.type || 'string'; if (target.type === 'checkbox') { value = target.checked; } else if (target.type === 'radio') { if (target.checked) { const radioGroup = container.querySelectorAll(`input[name="${target.name}"]`); const checkedRadio = Array.from(radioGroup).find(r => r.checked); value = checkedRadio.value; } else { return; // 如果不是选中的那个radio,则不处理 } } else { value = target.value; } // 类型转换 switch (type) { case 'integer': value = parseInt(value, 10); break; case 'float': value = parseFloat(value); break; case 'boolean': // Checkbox value is already a boolean if (typeof value !== 'boolean') { value = value === 'true'; } break; } // 对于radio按钮,我们需要确保只处理一次 if (target.type === 'radio' && !target.checked) return; updateAndSaveSetting(key, value); // 如果更改了影响面板状态的设置(如独立聊天记忆开关),则立即刷新 if (key === 'retrieval.independentChatMemoryEnabled') { updatePanelStatus(); } }); } export function bindHanlinyuanEvents() { const context = getContext(); if (!context) { console.error('[翰林院-枢纽] 未能获取SillyTavern上下文,绑定失败。'); return; } setupGlobalEventHandlers(); bindPanelToggleEvents(); bindInternalUIEvents(); bindTutorialEvents(); // 【新增】绑定教程按钮事件 bindAutoSaveEvents(); // 【新增】激活自动保存机制 bindSessionLockEvent(); // 【新增】绑定会话锁定事件 initializeUnifiedInjectionEditor(); // 初始化统一注入编辑器 // 确保核心已经初始化 if (HanlinyuanCore.initialize) { try { HanlinyuanCore.initialize(); } catch (e) { console.error('[翰林院-枢纽] 核心初始化抛出异常:', e); } } else { console.error('[翰林院-枢纽] 核心法典未能提供初始化圣旨!'); return; } try { loadSettingsToUI(); } catch (e) { console.error('[翰林院-枢纽] loadSettingsToUI 抛出异常:', e); } try { loadWorldbookList(); } catch (e) { console.error('[翰林院-枢纽] loadWorldbookList 抛出异常:', e); } log('[翰林院-枢纽] 已成功连接各部,政令畅通。', 'info'); const fileInput = document.getElementById('hanlinyuan-ingest-novel-file-input'); const fileNameSpan = document.getElementById('hanlinyuan-ingest-novel-file-name'); const startBtn = document.getElementById('hanlinyuan-ingest-novel-start'); const abortBtn = document.getElementById('hanlinyuan-ingest-abort'); const progressContainer = document.getElementById('hanlinyuan-ingest-progress-container'); const progressBar = document.getElementById('hanlinyuan-ingest-progress-bar'); const statusText = document.getElementById('hanlinyuan-ingest-status'); const controlsContainer = document.getElementById('hanlinyuan-ingest-novel-controls'); let selectedFile = null; let abortController = null; fileInput.addEventListener('change', (event) => { selectedFile = event.target.files[0]; if (selectedFile) { fileNameSpan.textContent = selectedFile.name; fileNameSpan.title = selectedFile.name; } else { fileNameSpan.textContent = '未选择文件'; } }); startBtn.addEventListener('click', async () => { if (!selectedFile) { toastr.warning('请先选择一个 .txt 文件'); return; } let resumeFromIndex = 0; const jobId = IngestionManager.generateJobId(selectedFile); const savedState = IngestionManager.loadProgress(jobId); if (savedState) { const progressPercentage = ((savedState.processedChunks / savedState.totalChunks) * 100).toFixed(1); const userChoice = confirm(`启禀大人,发现此书上次录入已完成 ${progressPercentage}%。是否从上次中断之处继续?`); if (userChoice) { resumeFromIndex = savedState.processedChunks; toastr.info(`遵命,将从第 ${resumeFromIndex + 1} 块继续录入。`, '圣旨已达'); log(`[断点续传] 用户选择继续任务 ${jobId},从第 ${resumeFromIndex} 块开始。`, 'info'); } else { IngestionManager.clearJob(jobId); toastr.info('遵命,将从头开始录入此书。', '圣旨已达'); log(`[断点续传] 用户选择放弃旧任务 ${jobId},重新开始。`, 'warn'); } } abortController = new AbortController(); const signal = abortController.signal; controlsContainer.style.display = 'none'; progressContainer.style.display = 'block'; statusText.textContent = '正在读取文件...'; progressBar.value = 0; try { const text = await selectedFile.text(); const progressCallback = (progress) => { statusText.textContent = `处理中: ${progress.message} (${progress.processed}/${progress.total})`; progressBar.value = (progress.processed / progress.total) * 100; }; const batchCompleteCallback = () => { updatePanelStatus(); log('[实时刷新] 批次完成,忆识总数已更新。', 'info'); }; const result = await HanlinyuanCore.ingestTextToHanlinyuan( text, 'novel', { sourceName: selectedFile.name }, progressCallback, signal, log, batchCompleteCallback, jobId, resumeFromIndex ); if (result.success) { toastr.success(`成功录入 ${result.count} 个知识块`); statusText.textContent = `任务完成!成功录入 ${result.count} 个知识块。`; progressBar.value = 100; updatePanelStatus(); } else { throw new Error(result.error || '未知错误'); } } catch (error) { if (error.name === 'AbortError') { toastr.info('任务已由用户中止。进度已保存,可随时继续。'); statusText.textContent = '任务已中止。'; } else { toastr.error(`录入失败: ${error.message}。进度已保存,可稍后重试。`); statusText.textContent = `错误: ${error.message}`; } } finally { setTimeout(() => { controlsContainer.style.display = 'flex'; progressContainer.style.display = 'none'; fileInput.value = ''; selectedFile = null; fileNameSpan.textContent = '未选择文件'; }, 3000); } }); abortBtn.addEventListener('click', () => { if (abortController) { abortController.abort(); } }); } function bindSessionLockEvent() { const lockButton = document.getElementById('hly-session-lock-btn'); if (!lockButton) return; lockButton.addEventListener('click', async () => { const isNowLocked = await HanlinyuanCore.toggleSessionLock(); updateSessionLockUI(isNowLocked); if (isNowLocked) { const lockedInfo = HanlinyuanCore.getLockedSessionInfo(); if (lockedInfo) { toastr.success(`会话已锁定到: ${lockedInfo.id}`, '圣旨已下'); log(`会话已锁定到宝库: ${lockedInfo.id}`, 'success'); } } else { toastr.info('会话已解锁,将跟随当前角色。', '诏曰'); log('会话已解锁。', 'info'); } // 锁定/解锁后,立即刷新状态面板以反映正确的ID和数量 updatePanelStatus(); }); // 初始化时也更新一次UI updateSessionLockUI(HanlinyuanCore.isSessionLocked()); } function updateSessionLockUI(isLocked) { const lockButton = document.getElementById('hly-session-lock-btn'); if (!lockButton) return; const icon = lockButton.querySelector('i'); const text = lockButton.querySelector('span'); if (isLocked) { lockButton.classList.add('active'); icon.className = 'fas fa-lock'; text.textContent = '解锁会话'; lockButton.title = '点击以解锁,让翰林院跟随当前角色'; } else { lockButton.classList.remove('active'); icon.className = 'fas fa-lock-open'; text.textContent = '锁定会话'; lockButton.title = '点击以锁定,让翰林院固定操作当前角色的宝库'; } } function bindPanelToggleEvents() { // “返回主殿”按钮的逻辑已由 ui/bindings.js 中的中央导航系统统一处理。 // 我们只需处理“打开翰林院”的按钮即可。 const openButton = document.getElementById('amily2_open_rag_palace'); if (openButton) { // 这个按钮的逻辑依然由中央导航系统处理,我们无需在此添加监听器。 // 保留此函数结构以备将来可能的扩展,但目前它无需执行任何操作。 } // 我们自己的返回按钮 (hly-back-to-main) 已被赋予新的ID,并由中央接管。 // 此处不再需要为它绑定任何事件。 } function bindTutorialEvents() { const tutorialButton = document.getElementById('amily2_open_hanlin_tutorial'); if (tutorialButton) { tutorialButton.addEventListener('click', () => { showContentModal("翰林院使用教程", `${extensionBasePath}/HanLin.md`); }); } } function bindInternalUIEvents() { const tabs = document.querySelectorAll('.hly-nav-item'); tabs.forEach(tab => { tab.addEventListener('click', () => { const targetTabId = tab.dataset.tab; // 修正选择器以匹配新的 'historiography' ID const targetPaneId = `hly-${targetTabId}-tab`; document.querySelectorAll('.hly-tab-pane').forEach(pane => { pane.classList.toggle('active', pane.id === targetPaneId); }); tabs.forEach(t => t.classList.toggle('active', t === tab)); }); }); const apiEndpointSelect = document.getElementById('hly-api-endpoint'); if (apiEndpointSelect) { // 现在这个函数将处理所有模式的UI变化 apiEndpointSelect.addEventListener('change', handleApiModeChange); } // 注入设置的UI逻辑已由 initializeUnifiedInjectionEditor 函数统一处理。 // 【新增】为“标签提取”复选框绑定事件 const tagExtractionToggle = document.getElementById('hly-tag-extraction-toggle'); const tagInputContainer = document.getElementById('hly-tag-input-container'); if (tagExtractionToggle && tagInputContainer) { tagExtractionToggle.addEventListener('change', () => { tagInputContainer.style.display = tagExtractionToggle.checked ? 'block' : 'none'; }); } // 为“书库选择”下拉框绑定联动事件 const librarySelect = document.getElementById('hly-hist-select-library'); if (librarySelect) { librarySelect.addEventListener('change', handleWorldbookSelectionChange); } // 浓缩 — 提取规则下拉选单 const condensationRuleSelect = document.getElementById('hly-condensation-rule-profile-select'); if (condensationRuleSelect) { _populateHlyRuleProfileSelect(condensationRuleSelect, 'condensation'); condensationRuleSelect.addEventListener('change', () => { ruleProfileManager.setAssignment('condensation', condensationRuleSelect.value || null); const name = condensationRuleSelect.selectedOptions[0]?.textContent || ''; toastr.info(condensationRuleSelect.value ? `浓缩提取规则已切换为「${name}」` : '浓缩提取规则已取消分配'); }); } // 查询预处理 — 提取规则下拉选单 const queryPrepRuleSelect = document.getElementById('hly-query-preprocessing-rule-profile-select'); if (queryPrepRuleSelect) { _populateHlyRuleProfileSelect(queryPrepRuleSelect, 'queryPreprocessing'); queryPrepRuleSelect.addEventListener('change', () => { ruleProfileManager.setAssignment('queryPreprocessing', queryPrepRuleSelect.value || null); const name = queryPrepRuleSelect.selectedOptions[0]?.textContent || ''; toastr.info(queryPrepRuleSelect.value ? `查询预处理规则已切换为「${name}」` : '查询预处理规则已取消分配'); }); } // 规则配置中心保存/删除后自动刷新翰林院下拉选单 document.addEventListener('amily2:ruleProfilesChanged', (e) => { if (condensationRuleSelect) _populateHlyRuleProfileSelect(condensationRuleSelect, 'condensation', e.detail); if (queryPrepRuleSelect) _populateHlyRuleProfileSelect(queryPrepRuleSelect, 'queryPreprocessing', e.detail); }); // 为自定义多选下拉框绑定事件 const multiSelectBtn = document.getElementById('hly-hist-entry-multiselect-btn'); const optionsContainer = document.getElementById('hly-hist-entry-multiselect-options'); if (multiSelectBtn && optionsContainer) { multiSelectBtn.addEventListener('click', (event) => { event.stopPropagation(); const isVisible = optionsContainer.style.display === 'block'; optionsContainer.style.display = isVisible ? 'none' : 'block'; }); optionsContainer.addEventListener('change', (event) => { const target = event.target; if (target.type !== 'checkbox') return; const allEntryCheckboxes = optionsContainer.querySelectorAll('.hly-hist-entry-checkbox'); const selectAllCheckbox = document.getElementById('hly-hist-select-all-entries'); if (target.id === 'hly-hist-select-all-entries') { // 处理“全选”逻辑 allEntryCheckboxes.forEach(cb => cb.checked = target.checked); } else { // 更新“全选”复选框的状态 const allChecked = Array.from(allEntryCheckboxes).every(cb => cb.checked); selectAllCheckbox.checked = allChecked; } // 更新按钮上的计数 const selectedCount = optionsContainer.querySelectorAll('.hly-hist-entry-checkbox:checked').length; const totalCount = allEntryCheckboxes.length; multiSelectBtn.querySelector('span').textContent = `已选择 ${selectedCount} / ${totalCount} 个条目`; }); // 点击外部关闭下拉框 document.addEventListener('click', (event) => { if (!multiSelectBtn.contains(event.target) && !optionsContainer.contains(event.target)) { optionsContainer.style.display = 'none'; } }); } // 为“一键删除”按钮绑定事件 const deleteAllBtn = document.getElementById('hly-kb-delete-local-btn'); if (deleteAllBtn) { deleteAllBtn.addEventListener('click', deleteAllLocalKnowledgeBases); } // 为“一键移动”按钮绑定事件 const moveAllToLocalBtn = document.getElementById('hly-kb-move-all-to-local'); if (moveAllToLocalBtn) { moveAllToLocalBtn.addEventListener('click', () => moveAllKnowledgeBases('globalToLocal')); } const moveAllToGlobalBtn = document.getElementById('hly-kb-move-all-to-global'); if (moveAllToGlobalBtn) { moveAllToGlobalBtn.addEventListener('click', () => moveAllKnowledgeBases('localToGlobal')); } // 为知识库列表容器绑定事件委托,避免重复绑定 const kbContainers = ['hly-kb-list-local', 'hly-kb-list-global']; kbContainers.forEach(id => { const container = document.getElementById(id); if (container) { container.addEventListener('click', handleKbAction); container.addEventListener('change', handleKbAction); } }); // 为多选工具栏绑定事件 document.getElementById('hly-kb-select-all-global').addEventListener('change', (e) => handleSelectAll(e, 'global')); document.getElementById('hly-kb-select-all-local').addEventListener('change', (e) => handleSelectAll(e, 'local')); document.getElementById('hly-kb-bulk-actions-global').addEventListener('click', (e) => handleBulkAction(e, 'global')); document.getElementById('hly-kb-bulk-actions-local').addEventListener('click', (e) => handleBulkAction(e, 'local')); } function initializeUnifiedInjectionEditor() { const sourceSelector = document.getElementById('hly-injection-source-selector'); const templateEditor = document.getElementById('hly-unified-template-editor'); const templateNotes = document.getElementById('hly-unified-template-notes'); const positionRadios = document.querySelectorAll('input[name="hly-unified-injection-position"]'); const depthInput = document.getElementById('hly-unified-injection-depth'); const roleSelect = document.getElementById('hly-unified-injection-role'); if (!sourceSelector) return; // 如果关键元素不存在,则中止 const placeholderMap = { novel: '{{novel_text}}', chat: '{{chat_text}}', lorebook: '{{lorebook_text}}', manual: '{{manual_text}}' }; function updateView() { const source = sourceSelector.value; const settings = HanlinyuanCore.getSettings(); const sourceSettings = settings[`injection_${source}`] || {}; // 从设置加载值,如果未定义则提供默认值 templateEditor.value = sourceSettings.template || ''; templateNotes.textContent = `以 ${placeholderMap[source] || '{{text}}'} 为占位符。`; const position = sourceSettings.position !== undefined ? String(sourceSettings.position) : '2'; positionRadios.forEach(radio => radio.checked = radio.value === position); depthInput.value = sourceSettings.depth || 0; roleSelect.value = sourceSettings.depth_role !== undefined ? String(sourceSettings.depth_role) : '0'; // 更新深度/角色控件的可用状态 const isChatMode = position === '1'; depthInput.disabled = !isChatMode; roleSelect.disabled = !isChatMode; } function saveSettings() { const source = sourceSelector.value; updateAndSaveSetting(`injection_${source}.template`, templateEditor.value); const selectedPosition = document.querySelector('input[name="hly-unified-injection-position"]:checked'); if (selectedPosition) { updateAndSaveSetting(`injection_${source}.position`, parseInt(selectedPosition.value, 10)); } updateAndSaveSetting(`injection_${source}.depth`, parseInt(depthInput.value, 10)); updateAndSaveSetting(`injection_${source}.depth_role`, parseInt(roleSelect.value, 10)); } // 绑定事件监听器 sourceSelector.addEventListener('change', updateView); // 使用 debounce 避免过于频繁的保存操作 const debouncedSave = debounce(saveSettings, 300); templateEditor.addEventListener('input', debouncedSave); depthInput.addEventListener('change', saveSettings); roleSelect.addEventListener('change', saveSettings); positionRadios.forEach(radio => radio.addEventListener('change', () => { saveSettings(); // 立即更新UI状态以获得即时反馈 const isChatMode = radio.value === '1' && radio.checked; depthInput.disabled = !isChatMode; roleSelect.disabled = !isChatMode; })); // 初始加载时更新视图 updateView(); } function handleApiModeChange() { const endpoint = document.getElementById('hly-api-endpoint').value; const urlDocket = document.getElementById('hly-custom-endpoint-docket'); const keyDocket = document.getElementById('hly-api-key-group'); const modelSelect = document.getElementById('hly-embedding-model'); const modelLabel = modelSelect.previousElementSibling; if (!urlDocket || !keyDocket) return; // 默认都显示 urlDocket.style.display = 'block'; keyDocket.style.display = 'block'; // 根据模式调整 switch (endpoint) { case 'google_direct': // Google模式下,URL是固定的,所以隐藏URL输入框 urlDocket.style.display = 'none'; keyDocket.querySelector('label').textContent = 'Google API Key:'; keyDocket.querySelector('input').placeholder = '请输入您的Google API Key'; break; case 'local_proxy': urlDocket.querySelector('label').textContent = '本地代理地址:'; urlDocket.querySelector('input').placeholder = '例如 http://127.0.0.1:8000/v1'; // 本地代理通常不需要key keyDocket.style.display = 'none'; break; case 'custom': default: urlDocket.querySelector('label').textContent = '自定义路径:'; urlDocket.querySelector('input').placeholder = '输入兼容OpenAI的embeddings端点'; keyDocket.querySelector('label').textContent = '通行令牌 (API Key):'; break; } } function loadSettingsToUI() { const settings = HanlinyuanCore.getSettings(); if (!settings) return; // 检索设置 document.getElementById('hly-retrieval-enabled').checked = settings.retrieval.enabled; document.getElementById('hly-api-endpoint').value = settings.retrieval.apiEndpoint; document.getElementById('hly-custom-api-url').value = settings.retrieval.customApiUrl; document.getElementById('hly-api-key').value = settings.retrieval.apiKey; // 对于下拉框,我们只设置初始值,但不清空列表 const modelSelect = document.getElementById('hly-embedding-model'); if (modelSelect.options.length === 0) { const currentModel = settings.retrieval.embeddingModel; const option = new Option(currentModel, currentModel, true, true); modelSelect.add(option); } modelSelect.value = settings.retrieval.embeddingModel; document.getElementById('hly-retrieval-notify').checked = settings.retrieval.notify; // 高级设定 document.getElementById('hly-chunk-size').value = settings.advanced.chunkSize; document.getElementById('hly-overlap-size').value = settings.advanced.overlap; document.getElementById('hly-match-threshold').value = settings.advanced.matchThreshold; document.getElementById('hly-query-message-count').value = settings.advanced.queryMessageCount; document.getElementById('hly-max-results').value = settings.advanced.maxResults; document.getElementById('hly-batch-size').value = settings.retrieval.batchSize; // 注入设定的加载已由 initializeUnifiedInjectionEditor 函数处理 handleApiModeChange(); // 根据加载的API模式更新UI // 凝识设置 document.getElementById('hly-condensation-enabled').checked = settings.condensation.enabled; document.getElementById('hly-auto-condense-toggle').checked = settings.condensation.autoCondense; document.getElementById('hly-preserve-floors').value = settings.condensation.preserveFloors; document.getElementById('hly-layer-start').value = settings.condensation.layerStart; document.getElementById('hly-layer-end').value = settings.condensation.layerEnd; document.getElementById('hly-include-user').checked = settings.condensation.messageTypes.user; document.getElementById('hly-include-ai').checked = settings.condensation.messageTypes.ai; // 史官设置 const histMaxRetriesEl = document.getElementById('historiography_max_retries'); if (histMaxRetriesEl) { histMaxRetriesEl.value = settings.historiographyMaxRetries ?? 2; } // 新增:加载标签提取设置 const tagExtractionToggle = document.getElementById('hly-tag-extraction-toggle'); const tagInput = document.getElementById('hly-tag-input'); const tagInputContainer = document.getElementById('hly-tag-input-container'); tagExtractionToggle.checked = settings.condensation.tagExtractionEnabled; tagInput.value = settings.condensation.tags; // 直接使用从核心获取的值 tagInputContainer.style.display = tagExtractionToggle.checked ? 'block' : 'none'; // Rerank 设置 document.getElementById('hly-rerank-enabled').checked = settings.rerank.enabled; document.getElementById('hly-rerank-url').value = settings.rerank.url; document.getElementById('hly-rerank-api-key').value = settings.rerank.apiKey; const rerankModelSelect = document.getElementById('hly-rerank-model'); if (rerankModelSelect.options.length === 0) { const currentModel = settings.rerank.model; if (currentModel) { const option = new Option(currentModel, currentModel, true, true); rerankModelSelect.add(option); } } rerankModelSelect.value = settings.rerank.model; document.getElementById('hly-rerank-top-n').value = settings.rerank.top_n; document.getElementById('hly-rerank-hybrid-alpha').value = settings.rerank.hybrid_alpha; document.getElementById('hly-rerank-notify').checked = settings.rerank.notify; document.getElementById('hly-super-sort-enabled').checked = settings.rerank.superSortEnabled; // 新增:加载优先检索设置 const prioritySettings = settings.rerank.priorityRetrieval; if (prioritySettings) { document.getElementById('hly-priority-retrieval-enabled').checked = prioritySettings.enabled; const sources = ['novel', 'chat_history', 'lorebook', 'manual']; sources.forEach(source => { const sourceSettings = prioritySettings.sources[source]; if (sourceSettings) { const enabledCheckbox = document.querySelector(`[data-setting-key="rerank.priorityRetrieval.sources.${source}.enabled"]`); const countInput = document.querySelector(`[data-setting-key="rerank.priorityRetrieval.sources.${source}.count"]`); if (enabledCheckbox) enabledCheckbox.checked = sourceSettings.enabled; if (countInput) countInput.value = sourceSettings.count; } }); } // 新增:加载检索预处理设置 if (settings.queryPreprocessing) { document.getElementById('hly-query-preprocessing-enabled').checked = settings.queryPreprocessing.enabled; } // 新增:加载独立聊天记忆开关状态 if (settings.retrieval.independentChatMemoryEnabled !== undefined) { document.getElementById('hly-independent-chat-memory-enabled').checked = settings.retrieval.independentChatMemoryEnabled; } } function saveSettingsFromUI(isAutoSave = true) { const container = document.getElementById('hly-modal-container'); if (!container) return; const inputs = container.querySelectorAll('[data-setting-key]'); inputs.forEach(target => { const key = target.dataset.settingKey; if (!key) return; let value; const type = target.dataset.type || 'string'; if (target.type === 'checkbox') { value = target.checked; } else if (target.type === 'radio') { if (!target.checked) return; // 只处理选中的radio value = target.value; } else { value = target.value; } // 类型转换 switch (type) { case 'integer': value = parseInt(value, 10); break; case 'float': value = parseFloat(value); break; case 'boolean': if (typeof value !== 'boolean') value = (value === 'true'); break; } // 特殊处理史官设置 if (key === 'historiographyMaxRetries') { const extSettings = extension_settings[extensionName] || {}; extSettings.historiographyMaxRetries = value; saveSettingsDebounced(); return; } // 直接调用核心更新函数,但不在这里重复记录日志 const settings = HanlinyuanCore.getSettings(); const keys = key.split('.'); let current = settings; for (let i = 0; i < keys.length - 1; i++) { current = current[keys[i]] = current[keys[i]] || {}; } current[keys[keys.length - 1]] = value; }); HanlinyuanCore.saveSettings(); if (!isAutoSave) { log('【手动存档】所有设定已存档封印。', 'success'); toastr.success('翰林院设定已存档封印。', '圣旨已达'); } // 自动保存的日志已在 updateAndSaveSetting 中处理,此处不再重复 } function resetSettingsToUI() { if (confirm('您确定要将所有设定恢复为出厂默认值吗?')) { HanlinyuanCore.resetSettings(); loadSettingsToUI(); toastr.info('翰林院设定已重置为初始状态。', '诏曰'); } } async function updatePanelStatus() { // 根据锁定状态更新显示 const isLocked = HanlinyuanCore.isSessionLocked(); const charNameEl = document.getElementById('hly-current-character-name'); const chatIdEl = document.getElementById('hly-current-chat-id'); if (isLocked) { const lockedInfo = HanlinyuanCore.getLockedSessionInfo(); if (lockedInfo) { charNameEl.textContent = '会话已锁定'; chatIdEl.textContent = lockedInfo.id; chatIdEl.title = `当前所有操作都将指向这个锁定的宝库:${lockedInfo.id}`; charNameEl.classList.add('hly-locked-status'); chatIdEl.classList.add('hly-locked-status'); } } else { charNameEl.textContent = ContextUtils.getCharacterName(); chatIdEl.textContent = ContextUtils.getChatId() || '无'; chatIdEl.title = ''; charNameEl.classList.remove('hly-locked-status'); chatIdEl.classList.remove('hly-locked-status'); } const countEl = document.getElementById('hly-current-vector-count'); countEl.textContent = '...'; try { const count = await HanlinyuanCore.getVectorCount(); countEl.textContent = count; } catch (error) { console.error('[翰林院-枢纽] 更新忆识数量失败:', error); countEl.textContent = 'N/A'; countEl.title = `无法获取总数: ${error.message}`; } // 显示上次凝识记录 const recordEl = document.getElementById('hly-condensation-results'); // 只有在没有进行中的预览时才更新记录 if (recordEl && !recordEl.dataset.finalText) { const settings = HanlinyuanCore.getSettings(); const collectionId = await HanlinyuanCore.getCollectionId(); if (settings.condensationHistory && settings.condensationHistory[collectionId]) { const record = settings.condensationHistory[collectionId]; // V5.4 - record.end is now always a number, so the text is simpler. recordEl.innerHTML = `
上次已从第 ${record.start} 楼凝识至第 ${record.end} 楼。
`; } else { recordEl.innerHTML = `可在此预览凝识结果。
`; } } // 最后,渲染知识库列表 renderKnowledgeBases(); } async function moveAllKnowledgeBases(direction) { const isMovingToLocal = direction === 'globalToLocal'; const sourceScope = isMovingToLocal ? 'global' : 'local'; const targetScope = isMovingToLocal ? '局部' : '全局'; const sourceKbs = isMovingToLocal ? HanlinyuanCore.getGlobalKnowledgeBases() : HanlinyuanCore.getLocalKnowledgeBases(); const kbIds = Object.keys(sourceKbs); if (kbIds.length === 0) { toastr.info(`源区域(${isMovingToLocal ? '全局' : '局部'})没有任何知识库可供移动。`, '圣谕'); return; } if (!confirm(`您确定要将 ${kbIds.length} 个知识库从【${isMovingToLocal ? '全局' : '局部'}】移动到【${targetScope}】吗?`)) { return; } log(`开始将 ${kbIds.length} 个知识库从 ${sourceScope} 移动到 ${isMovingToLocal ? 'local' : 'global'}...`, 'info'); const movePromises = kbIds.map(kbId => HanlinyuanCore.moveKnowledgeBase(kbId, sourceScope)); try { await Promise.all(movePromises); toastr.success(`所有 ${kbIds.length} 个知识库均已成功移动。`, '大功告成'); log(`批量移动完成。`, 'success'); } catch (error) { toastr.error(`批量移动过程中发生错误: ${error.message}`, '警报'); log(`批量移动失败: ${error.message}`, 'error'); } finally { await updatePanelStatus(); } } async function deleteAllLocalKnowledgeBases() { const localKbs = HanlinyuanCore.getLocalKnowledgeBases(); const kbIds = Object.keys(localKbs); if (kbIds.length === 0) { toastr.info('当前角色没有任何局部知识库可供删除。', '圣谕'); return; } if (!confirm(`您确定要永久删除【当前角色】的全部 ${kbIds.length} 个局部知识库吗?此操作无法恢复!`)) { return; } toastr.info(`正在删除 ${kbIds.length} 个局部知识库...`, '圣旨'); log(`开始批量删除 ${kbIds.length} 个局部知识库...`, 'warn'); let successCount = 0; let errorCount = 0; for (const kbId of kbIds) { try { // 明确指定 scope 为 'local' await HanlinyuanCore.removeKnowledgeBase(kbId, 'local'); successCount++; } catch (error) { errorCount++; log(`删除局部知识库 ${kbId} 失败: ${error.message}`, 'error'); } } if (errorCount > 0) { toastr.error(`操作完成,但有 ${errorCount} 个知识库删除失败。`, '警报'); } else { toastr.success(`所有 ${successCount} 个局部知识库均已成功删除。`, '大功告成'); } log(`局部知识库批量删除完成。成功: ${successCount}, 失败: ${errorCount}`, 'info'); await updatePanelStatus(); } async function renderKnowledgeBases() { const localContainer = document.getElementById('hly-kb-list-local'); const globalContainer = document.getElementById('hly-kb-list-global'); const localCharNameEl = document.getElementById('hly-local-kb-char-name'); if (!localContainer || !globalContainer || !localCharNameEl) return; // 更新局部知识库标题中的角色名 localCharNameEl.textContent = ContextUtils.getCharacterName() || '当前角色'; try { const localKbs = HanlinyuanCore.getLocalKnowledgeBases(); const globalKbs = HanlinyuanCore.getGlobalKnowledgeBases(); // 渲染局部知识库 await _renderKbList(localKbs, localContainer, 'local', 'hly-kb-list-local-placeholder'); // 渲染全局知识库 await _renderKbList(globalKbs, globalContainer, 'global', 'hly-kb-list-global-placeholder'); } catch (error) { console.error('[翰林院-枢纽] 渲染知识库列表失败:', error); localContainer.innerHTML = `加载失败: ${escapeTextareaContent(error.message)}
`; globalContainer.innerHTML = `加载失败: ${escapeTextareaContent(error.message)}
`; } } async function _renderKbList(kbs, container, scope, placeholderId) { const placeholder = document.getElementById(placeholderId); container.innerHTML = ''; // 清空 container.appendChild(placeholder); // 先把占位符加回去 if (Object.keys(kbs).length === 0) { placeholder.style.display = 'block'; return; } placeholder.style.display = 'none'; // 分组逻辑:找出自动凝识的记录 const autoCondenseGroup = []; const otherKbs = []; for (const [id, kb] of Object.entries(kbs)) { if (kb.name && kb.name.includes(': 自动凝识 (')) { autoCondenseGroup.push({ id, ...kb }); } else { otherKbs.push({ id, ...kb }); } } // 渲染自动凝识分组(如果有) if (autoCondenseGroup.length > 0) { const groupItem = document.createElement('div'); groupItem.className = 'hly-kb-group-item'; // 计算组内总向量数和启用状态 let totalVectors = 0; let allEnabled = true; // 预先获取所有向量数(并行) const countPromises = autoCondenseGroup.map(kb => HanlinyuanCore.getVectorCount(kb.id, scope)); const counts = await Promise.all(countPromises); autoCondenseGroup.forEach((kb, index) => { kb.vectorCount = counts[index]; totalVectors += counts[index]; if (!kb.enabled) allEnabled = false; }); // 排序:按楼层顺序 (假设名字里有数字) autoCondenseGroup.sort((a, b) => { const matchA = a.name.match(/\((\d+)-/); const matchB = b.name.match(/\((\d+)-/); if (matchA && matchB) { return parseInt(matchA[1]) - parseInt(matchB[1]); } return a.name.localeCompare(b.name); }); const groupHtml = `
翰林院宝库状态
--------------------
集合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 previewCondensation() {
const resultsEl = document.getElementById('hly-condensation-results');
try {
// 1. 获取UI设置和新规则
const settings = HanlinyuanCore.getSettings();
const condensationRuleConfig = resolveCondensationRuleConfig(settings);
const exclusionRules = condensationRuleConfig.exclusionRules || [];
const overrideMessageTypes = {
user: document.getElementById('hly-include-user').checked,
ai: document.getElementById('hly-include-ai').checked,
};
const useTagExtraction = condensationRuleConfig.tagExtractionEnabled;
const tagsToExtract = useTagExtraction
? (condensationRuleConfig.tags || '').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) => `