Files
ST-Amily2-Chat-Optimisation/ui/hanlinyuan-bindings.js

1841 lines
75 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { syncSlot } from './profile-sync.js';
import {
filterWorldbooks,
filterWorldbookEntries,
highlightSearchMatch,
debounce
} from '../core/rag-processor.js';
'use strict';
function escapeTextareaContent(text) {
return String(text ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function escapeAttribute(text) {
return String(text ?? '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function _populateHlyRuleProfileSelect(select, slot, detail) {
const profiles = detail?.profiles ?? ruleProfileManager.listProfiles();
const assigned = detail?.assignments?.[slot] ?? ruleProfileManager.getAssignment(slot) ?? '';
select.innerHTML = [
'<option value="">— 未分配 —</option>',
...profiles.map(p =>
`<option value="${p.id}" ${p.id === assigned ? 'selected' : ''}>${escapeTextareaContent(p.name || p.id)}</option>`
),
].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();
syncSlot('ragEmbed');
syncSlot('ragRerank');
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;
}
}
export 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;
}
// 注hly-tag-extraction-toggle / hly-tag-input / hly-tag-input-container 已从 HTML 移除,
// 标签提取规则改由 RuleProfileManager 管理。此处保留兼容性 null 检查,避免抛错吞掉后续段落加载。
const tagExtractionToggle = document.getElementById('hly-tag-extraction-toggle');
const tagInput = document.getElementById('hly-tag-input');
const tagInputContainer = document.getElementById('hly-tag-input-container');
if (tagExtractionToggle) tagExtractionToggle.checked = settings.condensation.tagExtractionEnabled;
if (tagInput) tagInput.value = settings.condensation.tags;
if (tagInputContainer && tagExtractionToggle) {
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 = `<p class="hly-record-hint"><i>上次已从第 ${record.start} 楼凝识至第 ${record.end} 楼。</i></p>`;
} else {
recordEl.innerHTML = `<p class="hly-record-hint">可在此预览凝识结果。</p>`;
}
}
// 最后,渲染知识库列表
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 = `<p class="hly-notes log-error"><i>加载失败: ${escapeTextareaContent(error.message)}</i></p>`;
globalContainer.innerHTML = `<p class="hly-notes log-error"><i>加载失败: ${escapeTextareaContent(error.message)}</i></p>`;
}
}
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 = `
<details class="hly-kb-group-details">
<summary class="hly-kb-group-summary">
<span class="hly-kb-group-title"><i class="fas fa-folder"></i> 自动凝识记录 (${autoCondenseGroup.length}个片段, 共${totalVectors}条)</span>
</summary>
<div class="hly-kb-group-content">
<!-- 子项目将在这里渲染 -->
</div>
</details>
`;
groupItem.innerHTML = groupHtml;
container.appendChild(groupItem);
const groupContent = groupItem.querySelector('.hly-kb-group-content');
for (const kb of autoCondenseGroup) {
const item = _createKbItemElement(kb.id, kb, scope, kb.vectorCount);
groupContent.appendChild(item);
}
}
// 渲染其他普通知识库
for (const kb of otherKbs) {
const vectorCount = await HanlinyuanCore.getVectorCount(kb.id, scope);
const item = _createKbItemElement(kb.id, kb, scope, vectorCount);
container.appendChild(item);
}
}
function _createKbItemElement(id, kb, scope, vectorCount) {
const item = document.createElement('div');
item.className = 'hly-kb-list-item';
item.dataset.kbId = id;
item.dataset.kbScope = scope;
const moveButtonHtml = scope === 'local'
? `<button class="hly-kb-move-btn" title="上移到全局"><i class="fas fa-arrow-up"></i></button>`
: `<button class="hly-kb-move-btn" title="下移到局部"><i class="fas fa-arrow-down"></i></button>`;
item.innerHTML = `
<div class="hly-kb-name-container">
<input type="checkbox" class="hly-kb-item-checkbox" data-kb-id="${escapeAttribute(id)}">
<span class="hly-kb-name" title="ID: ${escapeAttribute(id)}">${escapeTextareaContent(kb.name || '')} (${Number(vectorCount) || 0}条)</span>
</div>
<div class="hly-kb-actions">
${moveButtonHtml}
<button class="hly-kb-rename-btn" title="重命名"><i class="fas fa-pen-to-square"></i></button>
<label class="hly-toggle-switch" title="启用/禁用此知识库">
<input type="checkbox" class="hly-kb-toggle" ${kb.enabled ? 'checked' : ''}>
<span class="hly-toggle-slider"></span>
</label>
<button class="hly-kb-delete-btn" title="删除此知识库">&times;</button>
</div>
`;
return item;
}
async function handleKbAction(event) {
const target = event.target;
const listItem = target.closest('.hly-kb-list-item');
if (!listItem) return;
const kbId = listItem.dataset.kbId;
const scope = listItem.dataset.kbScope;
const kbName = listItem.querySelector('.hly-kb-name').textContent.split(' (')[0];
// 重命名操作
if (target.closest('.hly-kb-rename-btn')) {
const currentName = listItem.querySelector('.hly-kb-name').textContent.split(' (')[0];
const newName = prompt(`请输入知识库的新名称:`, currentName);
if (newName && newName.trim() && newName.trim() !== currentName) {
try {
await HanlinyuanCore.renameKnowledgeBase(kbId, newName, scope);
// 重命名成功后直接刷新整个面板
await updatePanelStatus();
} catch (error) {
log(`重命名知识库 ${currentName} 失败: ${error.message}`, 'error');
toastr.error(`重命名失败: ${error.message}`);
}
}
return; // 处理完重命名后退出,避免触发其他逻辑
}
// 删除操作
if (target.classList.contains('hly-kb-delete-btn')) {
if (confirm(`您确定要永久删除知识库【${kbName}】吗?此操作无法恢复!`)) {
try {
await HanlinyuanCore.removeKnowledgeBase(kbId, scope);
log(`知识库 ${kbName} (ID: ${kbId}) 已被删除`, 'success');
toastr.success(`知识库【${kbName}】已删除。`);
await updatePanelStatus();
} catch (error) {
log(`删除知识库 ${kbName} 失败: ${error.message}`, 'error');
toastr.error(`删除失败: ${error.message}`);
}
}
}
// 移动操作
if (target.closest('.hly-kb-move-btn')) {
const direction = scope === 'local' ? '全局' : '局部';
if (confirm(`您确定要将知识库【${kbName}】移动到【${direction}】吗?`)) {
try {
await HanlinyuanCore.moveKnowledgeBase(kbId, scope);
await updatePanelStatus();
} catch (error) {
log(`移动知识库 ${kbName} 失败: ${error.message}`, 'error');
toastr.error(`移动失败: ${error.message}`);
}
}
}
// 启用/禁用操作
if (target.classList.contains('hly-kb-toggle') && event.type === 'change') {
try {
await HanlinyuanCore.toggleKnowledgeBase(kbId, scope);
log(`知识库 ${kbName} 的状态已切换`, 'success');
// 只更新UI不完全重载避免勾选状态丢失
// await updatePanelStatus();
} catch (error) {
log(`切换知识库 ${kbName} 状态失败: ${error.message}`, 'error');
toastr.error(`切换状态失败: ${error.message}`);
// 切换失败时恢复UI状态
target.checked = !target.checked;
}
}
// 多选框勾选操作
if (target.classList.contains('hly-kb-item-checkbox') && event.type === 'change') {
updateBulkActionUI(scope);
}
}
function handleSelectAll(event, scope) {
const isChecked = event.target.checked;
const container = document.getElementById(`hly-kb-list-${scope}`);
const itemCheckboxes = container.querySelectorAll('.hly-kb-item-checkbox');
itemCheckboxes.forEach(cb => cb.checked = isChecked);
updateBulkActionUI(scope);
}
function updateBulkActionUI(scope) {
const container = document.getElementById(`hly-kb-list-${scope}`);
const bulkActions = document.getElementById(`hly-kb-bulk-actions-${scope}`);
const selectAllCheckbox = document.getElementById(`hly-kb-select-all-${scope}`);
const itemCheckboxes = container.querySelectorAll('.hly-kb-item-checkbox');
const selectedCheckboxes = container.querySelectorAll('.hly-kb-item-checkbox:checked');
const selectedCount = selectedCheckboxes.length;
const totalCount = itemCheckboxes.length;
if (selectedCount > 0) {
bulkActions.style.display = 'flex';
} else {
bulkActions.style.display = 'none';
}
if (totalCount === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (selectedCount === totalCount) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else if (selectedCount > 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
}
async function handleBulkAction(event, scope) {
const action = event.target.dataset.action;
if (!action) return;
const container = document.getElementById(`hly-kb-list-${scope}`);
const selectedCheckboxes = container.querySelectorAll('.hly-kb-item-checkbox:checked');
const selectedIds = Array.from(selectedCheckboxes).map(cb => cb.dataset.kbId);
if (selectedIds.length === 0) {
toastr.warning('请至少选择一个知识库进行操作。', '圣谕');
return;
}
let confirmMessage = '';
let actionFunction;
let successMessage = '';
switch (action) {
case 'delete':
confirmMessage = `您确定要永久删除选中的 ${selectedIds.length} 个知识库吗?此操作无法恢复!`;
actionFunction = (id) => HanlinyuanCore.removeKnowledgeBase(id, scope);
successMessage = `成功删除了 ${selectedIds.length} 个知识库。`;
break;
case 'move':
const direction = scope === 'local' ? '全局' : '局部';
confirmMessage = `您确定要将选中的 ${selectedIds.length} 个知识库移动到【${direction}】吗?`;
actionFunction = (id) => HanlinyuanCore.moveKnowledgeBase(id, scope);
successMessage = `成功移动了 ${selectedIds.length} 个知识库。`;
break;
case 'toggle':
confirmMessage = `您确定要切换选中的 ${selectedIds.length} 个知识库的启用状态吗?`;
actionFunction = (id) => HanlinyuanCore.toggleKnowledgeBase(id, scope);
successMessage = `成功切换了 ${selectedIds.length} 个知识库的状态。`;
break;
default:
return;
}
if (!confirm(confirmMessage)) {
return;
}
toastr.info(`正在对 ${selectedIds.length} 个知识库执行批量操作...`, '圣旨');
log(`开始对 ${selectedIds.length} 个知识库 (范围: ${scope}) 执行批量 ${action} 操作...`, 'info');
try {
const promises = selectedIds.map(id => actionFunction(id));
await Promise.all(promises);
toastr.success(successMessage, '大功告成');
log(`批量 ${action} 操作成功。`, 'success');
} catch (error) {
toastr.error(`批量操作失败: ${error.message}`, '警报');
log(`批量 ${action} 操作失败: ${error.message}`, 'error');
} finally {
await updatePanelStatus(); // 刷新整个面板以显示最新状态
}
}
async function testApi() {
toastr.info('正在测试神力连接...', '圣旨');
try {
await HanlinyuanCore.testApiConnection();
toastr.success('神力连接通畅!', '圣意');
} catch (error) {
toastr.error(`神力连接失败: ${error.message}`, '警报');
}
}
async function fetchHLYEmbeddingModels() {
const modelSelect = document.getElementById('hly-embedding-model');
const currentModel = modelSelect.value; // 保存当前选中的模型
modelSelect.innerHTML = '<option>正在获取...</option>';
modelSelect.disabled = true;
try {
log('开始获取模型列表...', 'info');
const models = await HanlinyuanCore.fetchEmbeddingModels();
modelSelect.innerHTML = ''; // 清空
if (models.length === 0) {
modelSelect.innerHTML = '<option>未找到模型</option>';
toastr.warn('未能获取到任何模型。', '翰林院启奏');
log('未能获取到任何模型。', 'warn');
return;
}
models.forEach(modelId => {
const option = new Option(modelId, modelId);
modelSelect.add(option);
});
// 尝试恢复之前的选择
if (models.includes(currentModel)) {
modelSelect.value = currentModel;
} else {
// 如果之前的模型不在新列表中,则默认选中第一个
modelSelect.selectedIndex = 0;
}
toastr.success(`成功获取 ${models.length} 个模型。`, '圣意');
log(`成功获取 ${models.length} 个模型。`, 'success');
} catch (error) {
console.error('[翰林院-枢纽] 获取模型列表失败:', error);
toastr.error(`获取模型失败: ${error.message}`, '严重错误');
log(`获取模型失败: ${error.message}`, 'error');
modelSelect.innerHTML = `<option>获取失败</option>`;
} finally {
modelSelect.disabled = false;
}
}
/**
* 新增获取并填充Rerank模型列表
*/
async function fetchHLYRerankModels() {
const modelSelect = document.getElementById('hly-rerank-model');
const currentModel = modelSelect.value;
modelSelect.innerHTML = '<option>正在获取...</option>';
modelSelect.disabled = true;
try {
log('开始获取Rerank模型列表...', 'info');
const models = await HanlinyuanCore.fetchRerankModels();
modelSelect.innerHTML = '';
if (models.length === 0) {
modelSelect.innerHTML = '<option>未找到模型</option>';
toastr.warn('未能获取到任何Rerank模型。', '翰林院启奏');
log('未能获取到任何Rerank模型。', 'warn');
return;
}
models.forEach(modelId => {
const option = new Option(modelId, modelId);
modelSelect.add(option);
});
if (models.includes(currentModel)) {
modelSelect.value = currentModel;
} else {
modelSelect.selectedIndex = 0;
}
toastr.success(`成功获取 ${models.length} 个Rerank模型。`, '圣意');
log(`成功获取 ${models.length} 个Rerank模型。`, 'success');
} catch (error) {
console.error('[翰林院-枢纽] 获取Rerank模型列表失败:', error);
toastr.error(`获取Rerank模型失败: ${error.message}`, '严重错误');
log(`获取Rerank模型失败: ${error.message}`, 'error');
modelSelect.innerHTML = `<option>获取失败</option>`;
} finally {
modelSelect.disabled = false;
}
}
async function purgeStorage() {
if (confirm('此操作将彻底清空当前角色的所有忆识(向量),且无法恢复。您确定要继续吗?')) {
toastr.info('正在清空宝库...', '圣旨');
const success = await HanlinyuanCore.purgeStorage();
if (success) {
toastr.success('宝库已清空。', '圣意');
} else {
toastr.error('清空宝库失败。', '警报');
}
await updatePanelStatus();
}
}
async function startCondensation() {
const resultsEl = document.getElementById('hly-condensation-results');
const preprocessedMessagesJSON = resultsEl.dataset.finalMessages;
const layerStart = document.getElementById('hly-layer-start').value;
const layerEnd = document.getElementById('hly-layer-end').value;
const range = { start: parseInt(layerStart), end: parseInt(layerEnd) };
try {
let messagesToProcess;
// 【V6 重构】路径判断:是处理预览后的消息对象数组,还是重新采集
if (preprocessedMessagesJSON) {
log('检测到预览后待处理的消息对象,开始精确凝识...', 'info');
toastr.info('正在处理您确认后的文书...', '圣旨');
messagesToProcess = JSON.parse(preprocessedMessagesJSON);
delete resultsEl.dataset.finalMessages; // 清理暂存数据
} else {
log('未检测到预览文本,按标准流程采集消息...', 'info');
toastr.info('正在准备凝识...', '圣旨');
messagesToProcess = HanlinyuanCore.getMessagesForCondensation();
}
if (!messagesToProcess || messagesToProcess.length === 0) {
toastr.warning('未找到符合条件的消息可供凝识。', '翰林院启奏');
resultsEl.textContent = '未找到符合条件的消息。';
return;
}
resultsEl.textContent = `已采集 ${messagesToProcess.length} 条消息,开始凝识...`;
toastr.info(`已采集 ${messagesToProcess.length} 条消息,开始凝识...`, '翰林院启奏');
// 统一调用 processCondensation它现在能处理任何符合格式的消息数组
const result = await HanlinyuanCore.processCondensation(messagesToProcess, log, range);
if (result.success) {
toastr.success(`凝识完成!新增 ${result.count} 条忆识。`, '大功告成');
const finalEnd = range.end === 0 ? getContext().chat.length : range.end;
resultsEl.textContent = `聊天记录从第 ${range.start} 楼到第 ${finalEnd} 楼已成功凝识,新增 ${result.count} 条忆识。`;
} else {
throw new Error(result.error || '未知错误');
}
} catch (error) {
console.error('[翰林院-枢纽] 凝识过程发生错误:', error);
toastr.error(`凝识失败: ${error.message}`, '严重错误');
resultsEl.textContent = `凝识失败: ${error.message}`;
} finally {
await updatePanelStatus();
}
}
async function loadWorldbookList() {
const selectEl = document.getElementById('hly-hist-select-library');
const searchInput = document.getElementById('hly-worldbook-search');
if (!selectEl) return;
try {
log('正在获取可用书库列表...', 'info');
const books = await Historiographer.getAvailableWorldbooks();
// 存储所有书库数据以供搜索使用
window.allWorldbooks = books;
// 初始化世界书选项
updateWorldbookOptions(selectEl, '', books);
// 绑定搜索事件
if (searchInput) {
const debouncedSearch = debounce((query) => {
updateWorldbookOptions(selectEl, query, books);
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
}
log(`成功加载 ${books.length} 个书库。`, 'success');
} catch (error) {
console.error('[翰林院-枢纽] 加载书库列表失败:', error);
log(`加载书库列表失败: ${error.message}`, 'error');
if (selectEl) {
selectEl.innerHTML = '<option value="">加载失败</option>';
}
}
}
function updateWorldbookOptions(selectElement, query, allBooks) {
const filteredBooks = filterWorldbooks(query, allBooks);
const currentValue = selectElement.value;
// 清空并重新填充
selectElement.innerHTML = '<option value="">请选择一个书库...</option>';
if (filteredBooks.length === 0) {
selectElement.innerHTML = query.trim() ?
'<option value="">未找到匹配的书库</option>' :
'<option value="">未找到任何书库</option>';
return;
}
filteredBooks.forEach(bookName => {
const option = document.createElement('option');
option.value = bookName;
option.textContent = bookName;
selectElement.appendChild(option);
});
// 恢复选择
if (currentValue && filteredBooks.includes(currentValue)) {
selectElement.value = currentValue;
}
}
async function handleWorldbookSelectionChange() {
const librarySelect = document.getElementById('hly-hist-select-library');
const multiSelectBtn = document.getElementById('hly-hist-entry-multiselect-btn');
const optionsContainer = document.getElementById('hly-hist-entry-multiselect-options');
const entrySearchInput = document.getElementById('hly-entry-search');
const selectedBook = librarySelect.value;
// 重置状态
multiSelectBtn.disabled = true;
multiSelectBtn.querySelector('span').textContent = '正在加载条目...';
optionsContainer.innerHTML = '';
optionsContainer.style.display = 'none';
if (entrySearchInput) {
entrySearchInput.value = '';
}
if (!selectedBook) {
multiSelectBtn.querySelector('span').textContent = '请先选择书库';
return;
}
try {
log(`正在为《${selectedBook}》获取条目列表...`, 'info');
const entries = await Historiographer.getLoresForWorldbook(selectedBook);
if (entries.length === 0) {
multiSelectBtn.querySelector('span').textContent = '此书库为空';
return;
}
// 存储所有条目以供搜索使用
window.allEntries = entries;
// 初始化条目选项
updateEntryOptions('', entries);
// 绑定条目搜索事件
if (entrySearchInput) {
// 移除之前的事件监听器(如果有)
entrySearchInput.removeEventListener('input', entrySearchInput._searchHandler);
const debouncedEntrySearch = debounce((query) => {
updateEntryOptions(query, entries);
}, 300);
entrySearchInput._searchHandler = (e) => {
debouncedEntrySearch(e.target.value);
};
entrySearchInput.addEventListener('input', entrySearchInput._searchHandler);
}
log(`成功加载 ${entries.length} 个条目。`, 'success');
} catch (error) {
console.error(`[翰林院-枢纽] 加载《${selectedBook}》的条目失败:`, error);
log(`加载条目失败: ${error.message}`, 'error');
multiSelectBtn.querySelector('span').textContent = '加载失败';
} finally {
multiSelectBtn.disabled = false;
}
}
function updateEntryOptions(query, allEntries) {
const optionsContainer = document.getElementById('hly-hist-entry-multiselect-options');
const multiSelectBtn = document.getElementById('hly-hist-entry-multiselect-btn');
const filteredEntries = filterWorldbookEntries(query, allEntries);
// 清空容器
optionsContainer.innerHTML = '';
// 添加全选选项
const selectAllHtml = `
<label class="hly-multiselect-option">
<input type="checkbox" id="hly-hist-select-all-entries">
<strong>全选/全不选</strong>
</label>`;
optionsContainer.insertAdjacentHTML('beforeend', selectAllHtml);
if (filteredEntries.length === 0) {
const noResultsHtml = `<div class="hly-no-results">未找到匹配的条目</div>`;
optionsContainer.insertAdjacentHTML('beforeend', noResultsHtml);
multiSelectBtn.querySelector('span').textContent = `未找到匹配的条目`;
return;
}
// 添加过滤后的条目
filteredEntries.forEach(entry => {
const displayText = query ?
highlightSearchMatch(entry.comment, query) :
escapeTextareaContent(entry.comment);
const optionHtml = `
<label class="hly-multiselect-option" title="${escapeAttribute(entry.comment)} (Key: ${escapeAttribute(entry.key)})">
<input type="checkbox" class="hly-hist-entry-checkbox" value="${escapeAttribute(entry.key)}">
<span>${displayText}</span>
</label>`;
optionsContainer.insertAdjacentHTML('beforeend', optionHtml);
});
// 更新按钮文本
multiSelectBtn.querySelector('span').textContent = `已选择 0 / ${filteredEntries.length} 个条目`;
}
/**
* 【V9.1 重构】开始书库编纂的核心函数,支持多选
*/
async function startHistoriography() {
const library = document.getElementById('hly-hist-select-library').value;
const optionsContainer = document.getElementById('hly-hist-entry-multiselect-options');
const resultsEl = document.getElementById('hly-historiography-results');
const selectedEntries = Array.from(optionsContainer.querySelectorAll('.hly-hist-entry-checkbox:checked')).map(cb => cb.value);
if (!library || selectedEntries.length === 0) {
toastr.warning('请先选择一个书库并至少选择一个要编纂的条目。', '圣谕不明');
return;
}
resultsEl.textContent = `准备对《${library}》中的 ${selectedEntries.length} 个条目进行批量编纂...`;
toastr.info('批量编纂任务已开始...', '圣旨');
log(`开始对《${library}》中的 ${selectedEntries.length} 个条目进行编纂...`, 'info');
try {
const result = await Historiographer.executeCompilation(library, selectedEntries);
resultsEl.textContent = result.content; // 显示来自后端的详细报告
if (result.success) {
toastr.success('批量编纂任务已完成。', '大功告成');
} else {
toastr.warning('批量编纂任务已完成,但有部分错误。', '圣谕');
}
log(`对《${library}》的批量编纂任务已完成。成功: ${result.totalSuccess}, 向量: ${result.totalVectors}`, 'success');
} catch (error) {
console.error('[翰林院-枢纽] 编纂过程发生严重错误:', error);
toastr.error(`编纂失败: ${error.message}`, '严重错误');
resultsEl.textContent = `编纂失败: ${error.message}`;
} finally {
await updatePanelStatus();
}
}
async function showStats() {
try {
log('用户请求查看宝库状态。', 'info');
toastr.info('正在查询宝库状态...', '圣旨');
const count = await HanlinyuanCore.getVectorCount();
const collectionId = await HanlinyuanCore.getCollectionId();
const settings = HanlinyuanCore.getSettings();
// 使用 pre 标签来保持格式
const statsText = `
<pre>
翰林院宝库状态
--------------------
集合ID: ${collectionId}
忆识总数: ${count}
--------------------
API端点: ${settings.retrieval.apiEndpoint}
所用模型: ${settings.retrieval.embeddingModel}
</pre>
`;
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) => `
<div class="hly-preview-item-v2" id="${item.id}">
<details class="hly-preview-details">
<summary class="hly-preview-summary">
${item.floor} 楼: [${item.name}]
</summary>
<div class="hly-preview-content">
<textarea class="hly-preview-textarea"
data-floor="${item.floor}"
data-is-user="${item.is_user}"
data-send-date="${item.send_date}">${escapeTextareaContent(item.content)}</textarea>
</div>
</details>
<button class="hly-preview-delete-btn-v2" data-target="${item.id}" title="删除此条">&times;</button>
</div>
`).join('');
// 5. 显示模态窗口
showHtmlModal('预览并编辑凝识内容', `<div class="hly-preview-container-v2">${editorHtml}</div>`, {
okText: '确认并更新预览',
onOk: (dialogElement) => {
const finalMessages = [];
dialogElement.find('.hly-preview-item-v2').each(function () {
const textarea = $(this).find('.hly-preview-textarea');
const text = textarea.val();
if (text.trim()) {
// 【V6 重构】收集包含完整元数据的消息对象
finalMessages.push({
mes: text,
is_user: textarea.data('is-user'),
send_date: textarea.data('send-date'),
floor: textarea.data('floor'), // 【V6.2 修复】把楼层号也一并传过去!
});
}
});
// 将对象数组字符串化后存入dataset
resultsEl.dataset.finalMessages = JSON.stringify(finalMessages);
// 更新预览区UI
const layerStart = document.getElementById('hly-layer-start').value;
const layerEnd = document.getElementById('hly-layer-end').value;
resultsEl.textContent = `已选择 ${layerStart} 楼到 ${layerEnd} 楼的内容(共 ${finalMessages.length} 条有效条目),请点击“开始凝识”进入自动向量化流程。`;
toastr.success('预览内容已更新,可随时开始凝识。', '圣旨已达');
}
});
// 6. 为新生成的删除按钮绑定事件 (V2)
$('.hly-preview-delete-btn-v2').on('click', function (e) {
e.preventDefault();
const targetId = $(this).data('target');
$(`#${targetId}`).remove();
});
} catch (error) {
console.error('[翰林院-枢纽] 预览过程发生错误:', error);
resultsEl.textContent = `预览失败: ${error.message}`;
toastr.error(`预览失败: ${error.message}`, '严重错误');
}
}
function log(message, type = 'info') {
const logOutput = document.getElementById('hly-log-output');
if (!logOutput) return;
const p = document.createElement('p');
const timestamp = new Date().toLocaleTimeString();
let icon = 'fa-circle-info';
let colorClass = 'log-info';
switch (type) {
case 'success':
icon = 'fa-check-circle';
colorClass = 'log-success';
break;
case 'error':
icon = 'fa-times-circle';
colorClass = 'log-error';
break;
case 'warn':
icon = 'fa-exclamation-triangle';
colorClass = 'log-warn';
break;
}
p.className = `hly-log-entry ${colorClass}`;
p.innerHTML = `<i class="fa-solid ${escapeAttribute(icon)}"></i> [${escapeTextareaContent(timestamp)}] ${escapeTextareaContent(message)}`;
// 移除初始的占位符
const placeholder = logOutput.querySelector('.hly-log-placeholder');
if (placeholder) {
placeholder.remove();
}
logOutput.appendChild(p);
logOutput.scrollTop = logOutput.scrollHeight; // 自动滚动到底部
}
async function ingestManualText() {
const textArea = document.getElementById('hly-manual-text');
const text = textArea.value.trim();
if (!text) {
toastr.warning('录入内容不能为空。', '翰林院启奏');
log('用户尝试录入空文本。', 'warn');
return;
}
log(`收到手动录入请求,文本长度: ${text.length}`, 'info');
toastr.info('正在处理您提交的文书...', '圣旨');
try {
const result = await HanlinyuanCore.ingestTextToHanlinyuan(text, 'manual', { sourceName: '手动录入' });
if (result.success) {
toastr.success(`文书已成功录入宝库,新增 ${result.count} 条忆识。`, '大功告成');
log(`手动录入成功,新增 ${result.count} 条忆识。`, 'success');
textArea.value = ''; // 清空文本域
} else {
throw new Error(result.error || '未知错误');
}
} catch (error) {
console.error('[翰林院-枢纽] 手动录入过程发生错误:', error);
toastr.error(`文书录入失败: ${error.message}`, '严重错误');
log(`手动录入失败: ${error.message}`, 'error');
} finally {
await updatePanelStatus();
}
}