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 = '未找到数据库文件。'; 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 = ` 已选择: ${escapeHTML(file.name)}`; } catch (error) { console.error(`Error processing file ${file.name}:`, error); toastr.error(`处理文件 ${file.name} 失败。`); } }); container.appendChild(fileElement); }); container.style.display = 'block'; }