Files
ST-Amily2-Chat-Optimisation/glossary/GT_bindings.js

717 lines
29 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 { extension_settings, getContext } from "/scripts/extensions.js";
import { saveSettingsDebounced, eventSource, event_types } from "/script.js";
import { extensionName } from "../utils/settings.js";
import { configManager } from '../utils/config/ConfigManager.js';
import { SENSITIVE_KEYS } from '../utils/config/sensitive-keys.js';
import { safeLorebooks, safeLorebookEntries, safeUpdateLorebookEntries } from '../core/tavernhelper-compatibility.js';
import { testSybdApiConnection, fetchSybdModels } from '../core/api/SybdApi.js';
import { handleFileUpload, processNovel } from './index.js';
import { reorganizeEntriesByHeadings, loadDatabaseFiles } from './executor.js';
import { SETTINGS_KEY as PRESET_SETTINGS_KEY } from '../PresetSettings/config.js';
import { escapeHTML } from '../utils/utils.js';
const moduleState = {
selectedWorldBook: '',
};
function updateAndSaveSetting(key, value) {
if (!extension_settings[extensionName]) {
extension_settings[extensionName] = {};
}
extension_settings[extensionName][key] = value;
saveSettingsDebounced();
console.log(`[Amily2-术语表] 设置项 '${key}' 已更新为: ${JSON.stringify(value)}`);
}
function loadSettingsToUI() {
const settings = extension_settings[extensionName] || {};
const container = document.getElementById('amily2_glossary_panel');
if (!container) return;
const inputs = container.querySelectorAll('[data-setting-key]');
inputs.forEach(target => {
const key = target.dataset.settingKey;
// 敏感字段从 configManagerlocalStorage读取其余从 extension_settings 读取
const value = SENSITIVE_KEYS.has(key) ? configManager.get(key) : settings[key];
if (value === undefined || value === null || value === '') {
if (!SENSITIVE_KEYS.has(key)) {
let defaultValue;
if (target.type === 'checkbox') {
defaultValue = target.checked;
} else if (target.type === 'range') {
defaultValue = target.dataset.type === 'float' ? parseFloat(target.value) : parseInt(target.value, 10);
} else {
defaultValue = target.value;
}
updateAndSaveSetting(key, defaultValue);
}
return;
};
if (target.type === 'checkbox') {
target.checked = value;
} else if (target.type === 'range') {
target.value = value;
const valueDisplay = document.getElementById(`${target.id}_value`);
if (valueDisplay) valueDisplay.textContent = value;
}
else {
target.value = value;
}
});
const sybdContent = document.getElementById('amily2_sybd_content');
if (sybdContent) {
sybdContent.classList.remove('amily2-content-hidden');
}
const apiModeSelect = document.getElementById('amily2_sybd_api_mode');
if (apiModeSelect) {
updateConfigVisibility(apiModeSelect.value);
}
}
function bindAutoSaveEvents() {
const container = document.getElementById('amily2_glossary_panel');
if (!container) return;
const handler = (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 {
value = target.value;
}
switch (type) {
case 'integer': value = parseInt(value, 10); break;
case 'float': value = parseFloat(value); break;
case 'boolean': value = (typeof value === 'boolean') ? value : (value === 'true'); break;
}
// 敏感字段API Key经 configManager 写入 localStorage
if (SENSITIVE_KEYS.has(key)) {
configManager.set(key, value);
} else {
updateAndSaveSetting(key, value);
}
if (key === 'sybdApiMode') {
updateConfigVisibility(value);
}
if (target.type === 'range') {
document.getElementById(`${target.id}_value`).textContent = value;
}
};
container.addEventListener('change', handler);
container.addEventListener('input', (event) => {
if (event.target.type === 'range') handler(event);
});
}
function updateConfigVisibility(mode) {
const compatibleConfig = document.getElementById('amily2_sybd_compatible_config');
const presetConfig = document.getElementById('amily2_sybd_preset_config');
if (mode === 'sillytavern_preset') {
compatibleConfig.style.display = 'none';
presetConfig.style.display = 'block';
loadTavernPresets();
} else {
compatibleConfig.style.display = 'block';
presetConfig.style.display = 'none';
}
}
async function loadTavernPresets() {
const select = document.getElementById('amily2_sybd_tavern_profile');
if (!select) return;
const currentValue = extension_settings[extensionName]?.sybdTavernProfile || '';
select.innerHTML = '<option value="">-- 加载中 --</option>';
try {
const context = getContext();
const tavernProfiles = context.extensionSettings?.connectionManager?.profiles || [];
select.innerHTML = '<option value="">-- 请选择预设 --</option>';
if (tavernProfiles.length > 0) {
tavernProfiles.forEach(profile => {
if (profile.api && profile.preset) {
const option = new Option(profile.name || profile.id, profile.id);
select.add(option);
}
});
select.value = currentValue;
} else {
select.innerHTML = '<option value="">未找到可用预设</option>';
}
} catch (error) {
console.error('[Amily2-术语表] 加载SillyTavern预设失败:', error);
select.innerHTML = '<option value="">加载失败</option>';
}
}
function bindManualActionEvents() {
const testBtn = document.getElementById('amily2_sybd_test_connection');
if (testBtn) {
testBtn.addEventListener('click', async () => {
const originalHtml = testBtn.innerHTML;
testBtn.disabled = true;
testBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 测试中';
await testSybdApiConnection();
testBtn.disabled = false;
testBtn.innerHTML = originalHtml;
});
}
const fetchBtn = document.getElementById('amily2_sybd_fetch_models');
const modelSelect = document.getElementById('amily2_sybd_model_select');
const modelInput = document.getElementById('amily2_sybd_model');
if (fetchBtn && modelSelect && modelInput) {
fetchBtn.addEventListener('click', async () => {
const originalHtml = fetchBtn.innerHTML;
fetchBtn.disabled = true;
fetchBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 获取中';
try {
const models = await fetchSybdModels();
if (models && models.length > 0) {
modelSelect.innerHTML = '<option value="">-- 请选择模型 --</option>';
models.forEach(model => {
const option = new Option(model.name || model.id, model.id);
modelSelect.add(option);
});
modelSelect.style.display = 'block';
modelInput.style.display = 'none';
toastr.success(`成功获取 ${models.length} 个模型`);
} else {
toastr.warning('未获取到任何模型');
}
} catch (error) {
toastr.error(`获取模型失败: ${error.message}`);
} finally {
fetchBtn.disabled = false;
fetchBtn.innerHTML = originalHtml;
}
});
modelSelect.addEventListener('change', () => {
const selectedModel = modelSelect.value;
if (selectedModel) {
modelInput.value = selectedModel;
modelInput.dispatchEvent(new Event('change', { bubbles: true }));
}
});
}
}
async function renderWorldBookEntries() {
const container = document.getElementById('world-book-entries-display');
if (!container) return;
const selectedBook = moduleState.selectedWorldBook;
if (!selectedBook) {
container.innerHTML = '<p style="text-align:center;">请先在“小说处理”标签页中选择一个世界书。</p>';
return;
}
container.innerHTML = '<p style="text-align:center;"><i class="fas fa-spinner fa-spin"></i> 正在加载条目...</p>';
try {
const allEntries = await safeLorebookEntries(selectedBook);
let managedEntries = allEntries.filter(e => e.comment?.startsWith('[Amily2小说处理]'));
if (managedEntries.length === 0) {
container.innerHTML = '<p style="text-align:center;">未找到由小说处理功能生成的条目。</p>';
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 = '<i>(直接关联)</i>';
}
}
}
if (source && target) {
body += `<tr><td>${escapeHTML(source.trim())}</td><td>${escapeHTML(rel.trim())}</td><td>${escapeHTML(target.trim().replace(';',''))}</td></tr>`;
}
});
return `<table class="table-render"><thead><tr><th>源头</th><th>关系</th><th>目标</th></tr></thead><tbody>${body}</tbody></table>`;
} catch {
return `<pre>${escapeHTML(content)}</pre>`;
}
}
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 => `<td>${escapeHTML(cell.trim())}</td>`).join('');
if (isHeaderRow) {
header += `<tr>${cells.replace(/<td>/g, '<th>').replace(/<\/td>/g, '</th>')}</tr>`;
isHeaderRow = false;
} else {
body += `<tr>${cells}</tr>`;
}
});
return `<table class="table-render"><thead>${header}</thead><tbody>${body}</tbody></table>`;
} catch {
return `<pre>${escapeHTML(content)}</pre>`;
}
}
return `<pre>${escapeHTML(content)}</pre>`;
};
entryElement.innerHTML = `
<div class="entry-header">
<strong class="entry-title">${escapeHTML(title)}</strong>
<div class="entry-actions">
<button class="menu_button primary small_button save-entry-btn" style="display: none;"><i class="fas fa-save"></i> 保存</button>
<button class="menu_button danger small_button cancel-entry-btn" style="display: none;"><i class="fas fa-times"></i> 取消</button>
<button class="menu_button secondary small_button edit-entry-btn"><i class="fas fa-edit"></i> 编辑</button>
</div>
</div>
<div class="entry-content-display">${renderContent(entry.content)}</div>
<div class="entry-content-editor" style="display: none;">
<textarea class="text_pole" style="width: 98%; min-height: 150px;">${entry.content}</textarea>
</div>
`;
const editBtn = entryElement.querySelector('.edit-entry-btn');
const saveBtn = entryElement.querySelector('.save-entry-btn');
const cancelBtn = entryElement.querySelector('.cancel-entry-btn');
const displayDiv = entryElement.querySelector('.entry-content-display');
const editorDiv = entryElement.querySelector('.entry-content-editor');
const textarea = editorDiv.querySelector('textarea');
const originalContent = entry.content;
editBtn.addEventListener('click', () => {
displayDiv.style.display = 'none';
editorDiv.style.display = 'block';
saveBtn.style.display = 'inline-block';
cancelBtn.style.display = 'inline-block';
editBtn.style.display = 'none';
});
const hideEditor = () => {
displayDiv.style.display = 'block';
editorDiv.style.display = 'none';
saveBtn.style.display = 'none';
cancelBtn.style.display = 'none';
editBtn.style.display = 'inline-block';
};
cancelBtn.addEventListener('click', () => {
textarea.value = originalContent;
hideEditor();
});
saveBtn.addEventListener('click', async () => {
const newContent = textarea.value;
displayDiv.innerHTML = renderContent(newContent);
hideEditor();
try {
const entryToUpdate = { uid: entry.uid, content: newContent };
await safeUpdateLorebookEntries(selectedBook, [entryToUpdate]);
toastr.success(`条目 "${title}" 已保存。`);
entry.content = newContent;
} catch (error) {
displayDiv.innerHTML = renderContent(originalContent);
console.error('保存世界书条目失败:', error);
toastr.error(`保存失败: ${error.message}`);
}
});
container.appendChild(entryElement);
});
} catch (error) {
console.error('加载世界书条目失败:', error);
container.innerHTML = `<p style="text-align:center; color: #ff8a8a;">加载失败: ${error.message}</p>`;
}
}
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 = '<i class="fas fa-spinner fa-spin"></i> 正在重组...';
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) =>
`<div class="chunk-preview-item"><b>块 ${index + 1}:</b> ${escapeHTML(chunk.content.substring(0, 100))}...</div>`
).join('');
resetProcessing();
}
function resetProcessing() {
processingState.currentIndex = 0;
processingState.isAborted = false;
processingState.isRunning = false;
processingState.lastStatus = 'idle';
updateButtonUI();
}
function updateButtonUI() {
if (processingState.isRunning) {
processBtn.disabled = false;
processBtn.innerHTML = '<i class="fas fa-stop-circle"></i> 请求中止';
processBtn.classList.add('danger');
} else {
processBtn.classList.remove('danger');
switch (processingState.lastStatus) {
case 'paused':
processBtn.innerHTML = '<i class="fas fa-play"></i> 继续处理';
processBtn.disabled = false;
break;
case 'failed':
processBtn.innerHTML = '<i class="fas fa-redo"></i> 重试处理';
processBtn.disabled = false;
break;
case 'success':
processBtn.innerHTML = '<i class="fas fa-check"></i> 处理完成';
processBtn.disabled = true;
break;
case 'idle':
default:
processBtn.innerHTML = '确认并开始处理';
processBtn.disabled = processingState.chunks.length === 0;
break;
}
}
}
async function startOrResumeProcessing() {
if (processingState.isRunning) return;
processingState.isRunning = true;
processingState.isAborted = false;
updateButtonUI();
processingState.forceNew = document.getElementById('novel-force-new').checked;
processingState.batchSize = 1;
processingState.selectedWorldBook = moduleState.selectedWorldBook;
try {
const result = await processNovel(processingState);
if (result === 'paused') {
processingState.lastStatus = 'paused';
} else if (result === 'success') {
processingState.lastStatus = 'success';
processingState.currentIndex = 0;
}
} catch (error) {
processingState.lastStatus = 'failed';
processingState.isAborted = true;
} finally {
processingState.isRunning = false;
updateButtonUI();
}
}
if (fileLabel && fileInput) {
fileLabel.addEventListener('click', (event) => {
event.preventDefault();
fileInput.click();
});
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
fileLabel.innerHTML = `<i class="fas fa-check"></i> 已选择: ${escapeHTML(file.name)}`;
handleFileUpload(file, (content) => {
fileContent = content;
updateChunks();
});
});
}
if (dbSelectBtn) {
dbSelectBtn.addEventListener('click', () => {
loadDatabaseFiles();
});
}
document.addEventListener('novel-file-loaded', (event) => {
const { content, fileName } = event.detail;
fileContent = content;
updateChunks();
if (fileLabel) {
fileLabel.innerHTML = `<i class="fas fa-upload"></i> 2a. 上传本地文件 (.txt)`;
}
});
if (chunkSizeInput) {
chunkSizeInput.addEventListener('input', updateChunks);
}
if (processBtn) {
processBtn.addEventListener('click', async () => {
if (processingState.isRunning) {
processingState.isAborted = true;
processBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 正在中止...';
processBtn.disabled = true;
} else {
if (processingState.lastStatus !== 'paused') {
const startBatchInput = document.getElementById('novel-start-batch-index');
let startBatch = parseInt(startBatchInput.value, 10);
if (isNaN(startBatch) || startBatch < 1) {
startBatch = 1;
if (startBatchInput) startBatchInput.value = 1;
}
processingState.currentIndex = (startBatch - 1);
}
startOrResumeProcessing();
}
});
}
}
async function loadWorldBooks() {
const select = document.getElementById('novel-world-book-select');
if (!select) return;
const savedBook = extension_settings[extensionName]?.selectedWorldBook;
moduleState.selectedWorldBook = savedBook || '';
try {
const allBooks = await safeLorebooks();
select.innerHTML = '<option value="">-- 请选择世界书 --</option>';
if (allBooks && allBooks.length > 0) {
allBooks.forEach(bookName => {
const option = new Option(bookName, bookName);
select.add(option);
});
if (savedBook && allBooks.includes(savedBook)) {
select.value = savedBook;
}
} else {
select.innerHTML = '<option value="">未找到世界书</option>';
}
} catch (error) {
console.error('[Amily2-术语表] 加载世界书失败:', error);
select.innerHTML = '<option value="">加载失败</option>';
}
}
export function bindGlossaryEvents() {
const panel = document.getElementById('amily2_glossary_panel');
if (!panel || panel.dataset.eventsBound) {
return;
}
console.log('[Amily2-术语表] 开始绑定UI事件 (最终重构版)...');
loadSettingsToUI();
bindAutoSaveEvents();
bindManualActionEvents();
bindTabEvents();
bindNovelProcessEvents();
bindReorganizeEvents();
loadWorldBooks();
// 监听我们自己的世界书创建事件,而不是监听全局的角色加载事件,避免冲突
document.addEventListener('amily-lorebook-created', (event) => {
console.log(`[Amily2-术语表] 检测到新世界书《${event.detail.bookName}》创建,重新加载列表以确保同步。`);
loadWorldBooks();
});
const worldBookSelect = document.getElementById('novel-world-book-select');
if (worldBookSelect) {
const updateOnBookSelect = (selectedValue) => {
updateAndSaveSetting('selectedWorldBook', selectedValue);
moduleState.selectedWorldBook = selectedValue;
const contextTab = document.querySelector('.glossary-tab[data-tab="context"]');
if (contextTab && contextTab.classList.contains('active')) {
renderWorldBookEntries();
}
const toolsTab = document.querySelector('.glossary-tab[data-tab="tools"]');
if (toolsTab && toolsTab.classList.contains('active')) {
const statusEl = document.getElementById('reorganize-status');
if (statusEl) {
if (selectedValue) {
statusEl.textContent = `当前已选择世界书: "${selectedValue}"。可以开始重组。`;
statusEl.style.color = '';
} else {
statusEl.textContent = '请先在“小说处理”标签页中选择一个世界书。';
statusEl.style.color = '#ffdb58';
}
}
}
};
worldBookSelect.addEventListener('change', () => {
updateOnBookSelect(worldBookSelect.value);
});
if (moduleState.selectedWorldBook) {
updateOnBookSelect(moduleState.selectedWorldBook);
}
}
panel.dataset.eventsBound = 'true';
console.log('[Amily2-术语表] UI事件绑定完成 (最终重构版)。');
}