From cae9c61aff3e6825cf39c3797f6d7a0eade2e642 Mon Sep 17 00:00:00 2001 From: Wx-2025 <351320169@qq.com> Date: Sun, 12 Oct 2025 16:18:11 +0800 Subject: [PATCH] Add files via upload --- glossary/GT_bindings.js | 289 ++++++++++++++++++++++++++++++++++++++++ glossary/executor.js | 105 +++++++++++++++ glossary/index.js | 126 ++++++++++++++++++ 3 files changed, 520 insertions(+) create mode 100644 glossary/GT_bindings.js create mode 100644 glossary/executor.js create mode 100644 glossary/index.js diff --git a/glossary/GT_bindings.js b/glossary/GT_bindings.js new file mode 100644 index 0000000..025e73d --- /dev/null +++ b/glossary/GT_bindings.js @@ -0,0 +1,289 @@ +import { extension_settings, getContext } from "/scripts/extensions.js"; +import { saveSettingsDebounced } from "/script.js"; +import { extensionName } from "../utils/settings.js"; +import { testSybdApiConnection, fetchSybdModels } from '../core/api/SybdApi.js'; +import { handleFileUpload, recognizeChapters, processNovel } from './index.js'; +import { SETTINGS_KEY as PRESET_SETTINGS_KEY } from '../PresetSettings/config.js'; + +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; + const value = settings[key]; + + if (value === undefined) { + 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 sybdToggle = document.getElementById('amily2_sybd_enabled'); + const sybdContent = document.getElementById('amily2_sybd_content'); + if (sybdToggle && sybdContent) { + sybdContent.classList.toggle('amily2-content-hidden', !sybdToggle.checked); + } + + 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; + } + + updateAndSaveSetting(key, value); + + if (key === 'sybdEnabled') { + document.getElementById('amily2_sybd_content').classList.toggle('amily2-content-hidden', !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 = ''; + + try { + const context = getContext(); + const tavernProfiles = context.extensionSettings?.connectionManager?.profiles || []; + + select.innerHTML = ''; + + if (tavernProfiles.length > 0) { + tavernProfiles.forEach(profile => { + if (profile.api && profile.preset) { + const option = new Option(profile.name || profile.id, profile.id); + select.add(option); + } + }); + select.value = currentValue; + } else { + select.innerHTML = ''; + } + } catch (error) { + console.error('[Amily2-术语表] 加载SillyTavern预设失败:', error); + select.innerHTML = ''; + } +} + +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 = ' 测试中'; + 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 = ' 获取中'; + + try { + const models = await fetchSybdModels(); + if (models && models.length > 0) { + modelSelect.innerHTML = ''; + 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 })); + } + }); + } +} + +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'); + } + }); + + }); + }); +} + +function bindNovelProcessEvents() { + const fileInput = document.getElementById('novel-file-input'); + const fileLabel = document.querySelector('label[for="novel-file-input"]'); + const recognizeBtn = document.getElementById('novel-recognize-chapters'); + const processBtn = document.getElementById('novel-confirm-and-process'); + + if (fileLabel && fileInput) { + fileLabel.addEventListener('click', (event) => { + event.preventDefault(); + fileInput.click(); + }); + fileInput.addEventListener('change', (event) => { + handleFileUpload(event.target.files[0]); + }); + } + + if (recognizeBtn) { + recognizeBtn.addEventListener('click', async () => { + const originalHtml = recognizeBtn.innerHTML; + recognizeBtn.disabled = true; + recognizeBtn.innerHTML = ' 识别中...'; + + await recognizeChapters(); + + recognizeBtn.disabled = false; + recognizeBtn.innerHTML = originalHtml; + }); + } + + if (processBtn) { + processBtn.addEventListener('click', async () => { + const originalHtml = processBtn.innerHTML; + processBtn.disabled = true; + processBtn.innerHTML = ' 处理中...'; + + await processNovel(); + + processBtn.disabled = false; + processBtn.innerHTML = originalHtml; + }); + } +} + +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(); + + panel.dataset.eventsBound = 'true'; + console.log('[Amily2-术语表] UI事件绑定完成 (最终重构版)。'); +} diff --git a/glossary/executor.js b/glossary/executor.js new file mode 100644 index 0000000..a2e0988 --- /dev/null +++ b/glossary/executor.js @@ -0,0 +1,105 @@ +import { callSybdAI } from '../core/api/SybdApi.js'; +import { getTargetWorldBook, syncNovelLorebookEntries } from '../CharacterWorldBook/src/cwb_lorebookManager.js'; +import { getPresetPrompts, getMixedOrder } from '../PresetSettings/index.js'; +import { generateRandomSeed } from '../core/api.js'; + +const { TavernHelper } = window; + +function parseStructuredResponse(responseText) { + const entries = []; + const entryRegex = /【(.*?)】.*?\[START_TABLE\]([\s\S]*?)\[END_TABLE\]/g; + let match; + + while ((match = entryRegex.exec(responseText)) !== null) { + const title = match[1].trim(); + const content = match[2].trim(); + if (title && content) { + entries.push({ title, content }); + } + } + + return entries; +} + + +export async function executeNovelProcessing(recognizedChapters, batchSize, forceNew, updateStatusCallback) { + if (recognizedChapters.length === 0) { + updateStatusCallback('没有可处理的章节。', 'error'); + return; + } + + updateStatusCallback('开始处理小说...', 'info'); + + try { + const bookName = await getTargetWorldBook(); + if (!bookName) throw new Error('无法确定目标世界书。'); + let existingEntriesContent = '当前世界书为空。'; + if (!forceNew) { + const allEntries = (await TavernHelper.getLorebookEntries(bookName)) || []; + const managedEntries = allEntries.filter(e => e.comment?.startsWith(`[Amily2小说处理]`)); + if (managedEntries.length > 0) { + existingEntriesContent = managedEntries.map(entry => { + return `【${entry.keyword}】\n[START_TABLE]\n${entry.content}\n[END_TABLE]`; + }).join('\n\n'); + } + } + + for (let i = 0; i < recognizedChapters.length; i += batchSize) { + const batch = recognizedChapters.slice(i, i + batchSize); + const progress = `(${i + batch.length}/${recognizedChapters.length})`; + updateStatusCallback(`正在处理批次 ${Math.floor(i / batchSize) + 1}... ${progress}`, 'info'); + + const chapterContent = batch.map(c => `## ${c.title}\n${c.content}`).join('\n\n---\n\n'); + + const order = getMixedOrder('novel_processor') || []; + const presetPrompts = await getPresetPrompts('novel_processor'); + + const messages = [ + { role: 'system', content: generateRandomSeed() } + ]; + + let promptCounter = 0; + for (const item of order) { + if (item.type === 'prompt') { + if (presetPrompts && presetPrompts[promptCounter]) { + messages.push(presetPrompts[promptCounter]); + promptCounter++; + } + } else if (item.type === 'conditional') { + switch (item.id) { + case 'existingLore': + messages.push({ role: 'user', content: `# 已有世界书条目\n\n${existingEntriesContent}` }); + break; + case 'chapterContent': + messages.push({ role: 'user', content: `# 最新章节内容\n\n${chapterContent}\n\n请根据以上信息,分析并输出需要新增或更新的世界书条目。` }); + break; + } + } + } + + if (messages.length <= 1) { + throw new Error('未能根据预设构建有效的API请求。'); + } + + const response = await callSybdAI(messages); + if (!response || response.trim() === '无需更新') { + updateStatusCallback(`批次 ${Math.floor(i / batchSize) + 1} 无需更新。`, 'info'); + continue; + } + + const structuredData = parseStructuredResponse(response); + if (structuredData.length === 0) { + updateStatusCallback(`批次 ${Math.floor(i / batchSize) + 1} 未提取到有效信息。`, 'info'); + continue; + } + + await syncNovelLorebookEntries(bookName, structuredData); + existingEntriesContent = response; + } + + updateStatusCallback('小说处理完成!', 'success'); + } catch (error) { + console.error('处理小说时发生严重错误:', error); + updateStatusCallback(`处理失败: ${error.message}`, 'error'); + } +} diff --git a/glossary/index.js b/glossary/index.js new file mode 100644 index 0000000..bf61eb3 --- /dev/null +++ b/glossary/index.js @@ -0,0 +1,126 @@ +import { executeNovelProcessing } from './executor.js'; + +let novelText = null; +let recognizedChaptersList = []; + +const getNovelFileInput = () => document.getElementById('novel-file-input'); +const getChapterRegexInput = () => document.getElementById('novel-chapter-regex'); +const getRecognizeBtn = () => document.getElementById('novel-recognize-chapters'); +const getProcessBtn = () => document.getElementById('novel-confirm-and-process'); +const getChapterPreview = () => document.getElementById('novel-chapter-preview'); +const getChapterCount = () => document.getElementById('novel-chapter-count'); +const getStatusDisplay = () => document.getElementById('novel-process-status'); +const getPresetSelect = () => document.getElementById('novel-preset-select'); +const getBatchSizeInput = () => document.getElementById('novel-batch-size'); +const getForceNewCheckbox = () => document.getElementById('novel-force-new'); + +export function updateStatus(message, type = 'info') { + const statusDisplay = getStatusDisplay(); + if (statusDisplay) { + statusDisplay.textContent = message; + statusDisplay.style.color = type === 'error' ? '#ff8a8a' : (type === 'success' ? '#8aff8a' : ''); + } +} + +function resetChapterUI() { + const preview = getChapterPreview(); + const count = getChapterCount(); + const processBtn = getProcessBtn(); + if (preview) preview.innerHTML = '请先上传文件并识别章节...'; + if (count) count.textContent = '0'; + if (processBtn) processBtn.disabled = true; + recognizedChaptersList = []; +} + +export function handleFileUpload(file) { + if (!file || !file.type.startsWith('text/')) { + updateStatus('请选择一个有效的 .txt 文件。', 'error'); + return; + } + const reader = new FileReader(); + reader.onload = (event) => { + novelText = event.target.result; + updateStatus(`文件 "${file.name}" 已成功加载。请点击“识别章节”。`, 'success'); + resetChapterUI(); + }; + reader.onerror = () => { + updateStatus(`读取文件 "${file.name}" 时发生错误。`, 'error'); + novelText = null; + }; + reader.readAsText(file); +} + +export function recognizeChapters() { + if (!novelText) { + updateStatus('请先上传一个小说文件。', 'error'); + return; + } + const regexInput = getChapterRegexInput(); + const customRegex = regexInput.value.trim(); + const defaultRegex = '(^\\s*(?:(?:第|卷)\\s*[一二三四五六七八九十百千万零〇\\d]+\\s*[章回节部篇]|Chapter\\s+\\d+|\\d+\\s*[.、]|序章|楔子|引子|序幕|尾声|终章|后记|番外)\\s*.*)'; + let finalRegex; + try { + finalRegex = new RegExp(customRegex || defaultRegex, 'gm'); + } catch (e) { + updateStatus('无效的正则表达式。', 'error'); + return; + } + + updateStatus('正在识别章节...', 'info'); + recognizedChaptersList = []; + const matches = [...novelText.matchAll(finalRegex)]; + + if (matches.length > 0) { + for (let i = 0; i < matches.length; i++) { + const currentMatch = matches[i]; + const nextMatch = matches[i + 1]; + + const title = currentMatch[0].trim(); + const startIndex = currentMatch.index + currentMatch[0].length; + const endIndex = nextMatch ? nextMatch.index : novelText.length; + + const content = novelText.substring(startIndex, endIndex).trim(); + + if (title) { + recognizedChaptersList.push({ title, content }); + } + } + } + + const preview = getChapterPreview(); + const count = getChapterCount(); + const processBtn = getProcessBtn(); + + if (preview) { + preview.innerHTML = recognizedChaptersList.map((chap, index) => `