Initial commit with CC BY-NC-ND 4.0 license

This commit is contained in:
2026-02-13 09:59:19 +08:00
commit 2c31e1cbc8
140 changed files with 44625 additions and 0 deletions

706
glossary/GT_bindings.js Normal file
View File

@@ -0,0 +1,706 @@
import { extension_settings, getContext } from "/scripts/extensions.js";
import { saveSettingsDebounced, eventSource, event_types } from "/script.js";
import { extensionName } from "../utils/settings.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;
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 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;
}
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事件绑定完成 (最终重构版)。');
}

325
glossary/executor.js Normal file
View File

@@ -0,0 +1,325 @@
import { callSybdAI } from '../core/api/SybdApi.js';
import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '/scripts/chats.js';
import { extensionName } from '../utils/settings.js';
import { getPresetPrompts, getMixedOrder } from '../PresetSettings/index.js';
import { generateRandomSeed } from '../core/api.js';
import { safeLorebookEntries, safeUpdateLorebookEntries, compatibleWriteToLorebook } from '../core/tavernhelper-compatibility.js';
import { loadWorldInfo, saveWorldInfo, createWorldInfoEntry } from "/scripts/world-info.js";
import { escapeHTML } from '../utils/utils.js';
function buildContextFromEntries(entries) {
if (!entries || entries.length === 0) {
return '当前世界书为空。';
}
const mappedContent = entries.map(entry => {
if (!Array.isArray(entry.keys) || entry.keys.length < 2) {
return null;
}
const name = entry.keys[1];
return `[--START_TABLE--]\n[name]:${name}\n${entry.content}\n[--END_TABLE--]`;
}).filter(Boolean).join('\n\n');
return mappedContent || '当前世界书为空。';
}
function parseStructuredResponse(responseText) {
const entries = [];
const entryRegex = /\[--START_TABLE--\]\s*\[name\]:(.*?)\n([\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(processingState, updateStatusCallback) {
const { chunks: recognizedChapters, batchSize, selectedWorldBook } = processingState;
if (recognizedChapters.length === 0) {
updateStatusCallback('没有可处理的章节。', 'error');
throw new Error('没有可处理的章节。');
}
updateStatusCallback('开始处理小说...', 'info');
try {
const bookName = selectedWorldBook;
if (!bookName) throw new Error('请先在设置中选择一个目标世界书。');
let previousBatchAIResponse = '';
if (processingState.currentIndex > 0) {
const allEntries = (await safeLorebookEntries(bookName)) || [];
const previousBatchIndex = processingState.currentIndex;
const targetComment = `[Amily2小说处理] 链式生成-第${previousBatchIndex}部分`;
const previousEntry = allEntries.find(e => e.comment === targetComment);
if (previousEntry) {
previousBatchAIResponse = previousEntry.content;
updateStatusCallback(`已加载批次 ${previousBatchIndex} 的内容作为上下文。`, 'info');
} else {
throw new Error(`无法找到衔接批次 ${previousBatchIndex} 的世界书条目,请从 1 开始处理。`);
}
}
for (let i = processingState.currentIndex; i < recognizedChapters.length; i += batchSize) {
if (processingState.isAborted) {
updateStatusCallback(`处理已中止。当前进度: ${i}/${recognizedChapters.length}`, 'info');
return 'paused';
}
processingState.currentIndex = i;
const currentBatchNumber = i + 1;
const batch = recognizedChapters.slice(i, i + batchSize);
const progress = `(${currentBatchNumber}/${recognizedChapters.length})`;
updateStatusCallback(`正在处理批次 ${currentBatchNumber}... ${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') {
if (item.id === 'existingLore') {
const contextContent = previousBatchAIResponse ? `# 上一章节的剧情发展概要\n\n${previousBatchAIResponse}` : '这是小说的第一部分,请开始生成剧情发展概要。';
messages.push({ role: 'user', content: contextContent });
} else if (item.id === 'chapterContent') {
messages.push({ role: 'user', content: `# 最新章节内容\n\n${chapterContent}\n\n请根据以上信息,分析并输出当前章节的剧情发展概要。` });
}
}
}
if (messages.length <= 1) throw new Error('未能根据预设构建有效的API请求。');
const response = await callSybdAI(messages);
if (!response || response.trim().length === 0) {
throw new Error(`API调用失败批次 ${currentBatchNumber} 未收到有效响应。`);
}
const contentMatch = response.match(/\[--START_TABLE--\]([\s\S]*?)\[--END_TABLE--\]/);
if (!contentMatch || !contentMatch[1]) {
throw new Error(`API响应格式不正确未找到被 '[--START_TABLE--]' 和 '[--END_TABLE--]' 包裹的内容,批次 ${currentBatchNumber}`);
}
const aiContent = contentMatch[1].trim();
const newEntryData = {
comment: `[Amily2小说处理] 链式生成-第${currentBatchNumber}部分`,
content: aiContent,
keys: [`小说处理链式生成第${currentBatchNumber}部分`],
enabled: true,
order: 2000 + currentBatchNumber,
position: 'before_char',
};
await compatibleWriteToLorebook(bookName, newEntryData.comment, () => newEntryData.content, {
keys: newEntryData.keys,
isConstant: false,
insertion_position: newEntryData.position,
order: newEntryData.order,
});
updateStatusCallback(`批次 ${currentBatchNumber} 处理完成,已创建新条目。`, 'success');
previousBatchAIResponse = aiContent;
}
updateStatusCallback('小说处理完成!', 'success');
return 'success';
} catch (error) {
console.error('处理小说时发生严重错误:', error);
updateStatusCallback(`处理失败: ${error.message}`, 'error');
throw error;
}
}
export async function reorganizeEntriesByHeadings(bookName, headingsToProcess, updateStatusCallback) {
try {
updateStatusCallback('开始重组...', 'info');
const bookData = await loadWorldInfo(bookName);
if (!bookData || !bookData.entries) {
throw new Error(`无法加载世界书 "${bookName}" 的数据。`);
}
const allEntries = Object.values(bookData.entries);
updateStatusCallback(`已获取 ${allEntries.length} 个条目,正在根据您提供的 ${headingsToProcess.length} 个标题进行解析...`, 'info');
const headingsMap = new Map();
headingsToProcess.forEach(h => headingsMap.set(h, []));
const finalEntries = {};
const userTitlesSet = new Set(headingsToProcess);
for (const entry of allEntries) {
const lines = entry.content.split(/\r?\n/);
let currentCaptureTitle = null;
let currentCaptureContent = [];
const remainingLines = [];
const endCapture = () => {
if (currentCaptureTitle && currentCaptureContent.length > 0) {
headingsMap.get(currentCaptureTitle).push(currentCaptureContent.join('\n'));
}
currentCaptureTitle = null;
currentCaptureContent = [];
};
for (const line of lines) {
const trimmedLine = line.trim();
const isH1Title = trimmedLine.startsWith('#') && !trimmedLine.startsWith('##');
if (isH1Title) {
endCapture();
const potentialTitleFromFile = trimmedLine.substring(1).trim();
let matchedUserTitle = null;
for (const userTitle of userTitlesSet) {
if (potentialTitleFromFile.startsWith(userTitle)) {
matchedUserTitle = userTitle;
break;
}
}
if (matchedUserTitle) {
currentCaptureTitle = matchedUserTitle;
} else {
remainingLines.push(line);
}
} else {
if (currentCaptureTitle) {
currentCaptureContent.push(line);
} else {
remainingLines.push(line);
}
}
}
endCapture();
const remainingContent = remainingLines.join('\n').trim();
if (remainingContent) {
finalEntries[entry.uid] = { ...entry, content: remainingContent };
}
}
let foundHeadingsCount = 0;
for (const contentBlocks of headingsMap.values()) {
if (contentBlocks.length > 0) {
foundHeadingsCount++;
}
}
if (foundHeadingsCount === 0) {
updateStatusCallback('在任何条目中都未找到您指定的标题,无需操作。', 'info');
return;
}
updateStatusCallback(`解析完成,找到 ${foundHeadingsCount} 个匹配的标题类别。正在合并内容并创建新条目...`, 'info');
for (const [title, contentBlocks] of headingsMap.entries()) {
if (contentBlocks.length > 0) {
const mergedContent = contentBlocks.map((block, index) => {
return `# ${title} - 第${index + 1}部分\n${block.trim()}`;
}).join('\n\n');
const newEntry = createWorldInfoEntry(bookName, bookData);
Object.assign(newEntry, {
comment: `[Amily2重组] ${title}`,
content: mergedContent,
key: [title],
disable: false,
constant: false,
position: 0,
order: 2100,
});
finalEntries[newEntry.uid] = newEntry;
}
}
bookData.entries = finalEntries;
await saveWorldInfo(bookName, bookData, true);
updateStatusCallback(`成功!已重组 ${foundHeadingsCount} 个标题。`, 'success');
toastr.success(`世界书 "${bookName}" 已成功按标题重组。`);
} catch (error) {
console.error('重组世界书条目时发生错误:', error);
updateStatusCallback(`错误: ${error.message}`, 'error');
throw error;
}
}
export async function loadDatabaseFiles() {
const fileMap = new Map();
try {
getDataBankAttachments().forEach(file => {
if (file && file.url) fileMap.set(file.url, file);
});
getDataBankAttachmentsForSource('global').forEach(file => {
if (file && file.url) fileMap.set(file.url, file);
});
getDataBankAttachmentsForSource('character').forEach(file => {
if (file && file.url) fileMap.set(file.url, file);
});
getDataBankAttachmentsForSource('chat').forEach(file => {
if (file && file.url) fileMap.set(file.url, file);
});
} catch (error) {
console.error('Error getting database files:', error);
toastr.error('读取数据库文件失败。');
return;
}
const container = document.getElementById('database-file-list-container');
container.innerHTML = '';
if (fileMap.size === 0) {
container.innerHTML = '<small>未找到数据库文件。</small>';
container.style.display = 'block';
return;
}
const files = Array.from(fileMap.values());
files.forEach(file => {
const fileElement = document.createElement('div');
fileElement.classList.add('database-file-item', 'menu_button', 'secondary', 'interactable');
fileElement.textContent = file.name;
fileElement.dataset.url = file.url;
fileElement.addEventListener('click', async () => {
try {
const text = await getFileAttachment(file.url);
console.log(`Loaded file content from ${file.name}`);
const event = new CustomEvent('novel-file-loaded', {
detail: {
content: text,
fileName: file.name
}
});
document.dispatchEvent(event);
container.style.display = 'none';
document.getElementById('select-from-database-button').innerHTML = `<i class="fas fa-check"></i> 已选择: ${escapeHTML(file.name)}`;
} catch (error) {
console.error(`Error processing file ${file.name}:`, error);
toastr.error(`处理文件 ${file.name} 失败。`);
}
});
container.appendChild(fileElement);
});
container.style.display = 'block';
}

41
glossary/index.js Normal file
View File

@@ -0,0 +1,41 @@
import { executeNovelProcessing } from './executor.js';
const getProcessBtn = () => document.getElementById('novel-confirm-and-process');
const getStatusDisplay = () => document.getElementById('novel-process-status');
export function updateStatus(message, type = 'info') {
const statusDisplay = getStatusDisplay();
if (statusDisplay) {
statusDisplay.textContent = message;
statusDisplay.style.color = type === 'error' ? '#ff8a8a' : (type === 'success' ? '#8aff8a' : '');
}
}
export function handleFileUpload(file, callback) {
if (!file || !file.type.startsWith('text/')) {
updateStatus('请选择一个有效的 .txt 文件。', 'error');
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const content = event.target.result;
updateStatus(`文件 "${file.name}" 已成功加载。`, 'success');
if (callback) {
callback(content);
}
};
reader.onerror = () => {
updateStatus(`读取文件 "${file.name}" 时发生错误。`, 'error');
};
reader.readAsText(file);
}
export async function processNovel(processingState) {
try {
return await executeNovelProcessing(processingState, updateStatus);
} catch (error) {
console.error('处理小说时发生UI层错误:', error);
updateStatus(`处理失败: ${error.message}`, 'error');
throw error;
}
}