diff --git a/glossary/executor.js b/glossary/executor.js index b4d2b22..a6c4995 100644 --- a/glossary/executor.js +++ b/glossary/executor.js @@ -1,8 +1,10 @@ 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"; function buildContextFromEntries(entries) { if (!entries || entries.length === 0) { @@ -38,7 +40,7 @@ function parseStructuredResponse(responseText) { export async function executeNovelProcessing(processingState, updateStatusCallback) { - const { chunks: recognizedChapters, batchSize, forceNew, selectedWorldBook } = processingState; + const { chunks: recognizedChapters, batchSize, selectedWorldBook } = processingState; if (recognizedChapters.length === 0) { updateStatusCallback('没有可处理的章节。', 'error'); @@ -49,17 +51,22 @@ export async function executeNovelProcessing(processingState, updateStatusCallba try { const bookName = selectedWorldBook; - if (!bookName) { - throw new Error('请先在设置中选择一个目标世界书。'); - } + if (!bookName) throw new Error('请先在设置中选择一个目标世界书。'); - const allEntries = (await safeLorebookEntries(bookName)) || []; - const managedEntries = allEntries.filter(e => e.comment?.startsWith('[Amily2小说处理]') || e.comment?.startsWith('[Amily2-Glossary]')); - const localManagedEntries = [...managedEntries]; + let previousBatchAIResponse = ''; - let existingEntriesContent = '当前世界书为空。'; - if (!forceNew) { - existingEntriesContent = buildContextFromEntries(localManagedEntries); + 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) { @@ -69,9 +76,10 @@ export async function executeNovelProcessing(processingState, updateStatusCallba } processingState.currentIndex = i; + const currentBatchNumber = i + 1; const batch = recognizedChapters.slice(i, i + batchSize); - const progress = `(${i + batch.length}/${recognizedChapters.length})`; - updateStatusCallback(`正在处理批次 ${Math.floor(i / batchSize) + 1}... ${progress}`, 'info'); + 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') || []; @@ -87,9 +95,10 @@ export async function executeNovelProcessing(processingState, updateStatusCallba } } else if (item.type === 'conditional') { if (item.id === 'existingLore') { - messages.push({ role: 'user', content: `# 已有世界书条目\n\n${existingEntriesContent}` }); + 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请根据以上信息,分析并输出需要新增或更新的世界书条目。` }); + messages.push({ role: 'user', content: `# 最新章节内容\n\n${chapterContent}\n\n请根据以上信息,分析并输出当前章节的剧情发展概要。` }); } } } @@ -97,82 +106,34 @@ export async function executeNovelProcessing(processingState, updateStatusCallba if (messages.length <= 1) throw new Error('未能根据预设构建有效的API请求。'); const response = await callSybdAI(messages); - if (!response) { - throw new Error(`API调用失败,批次 ${Math.floor(i / batchSize) + 1} 未收到响应。`); - } - if (response.trim() === '无需更新') { - updateStatusCallback(`批次 ${Math.floor(i / batchSize) + 1} 无需更新。`, 'info'); - continue; - } - - const structuredData = parseStructuredResponse(response); - if (structuredData.length === 0) { - throw new Error(`未能从API响应中提取有效信息,批次 ${Math.floor(i / batchSize) + 1}。`); + if (!response || response.trim().length === 0) { + throw new Error(`API调用失败,批次 ${currentBatchNumber} 未收到有效响应。`); } - const entriesToUpdate = []; - const entriesToCreate = []; - const fixedNovelEntries = ['世界观设定', '时间线', '角色关系网', '角色总览']; + 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', + }; - let maxPart = 0; - localManagedEntries.forEach(entry => { - const match = entry.comment.match(/章节内容概述-第(\d+)部分/); - if (match && parseInt(match[1], 10) > maxPart) maxPart = parseInt(match[1], 10); + await compatibleWriteToLorebook(bookName, newEntryData.comment, () => newEntryData.content, { + keys: newEntryData.keys, + isConstant: false, + insertion_position: newEntryData.position, + order: newEntryData.order, }); - let nextPart = maxPart + 1; - - for (const entry of structuredData) { - const { title, content } = entry; - let comment; - let keys; - - if (title === '章节内容概述') { - comment = `[Amily2小说处理] ${title}-第${nextPart}部分`; - keys = [`小说处理`, title, `第${nextPart}部分`]; - const newEntryData = { keys, content, comment, enabled: true, order: 100, position: 'before_char' }; - entriesToCreate.push(newEntryData); - localManagedEntries.push({ uid: -1, ...newEntryData, keys: keys }); - nextPart++; - continue; - } - - if (fixedNovelEntries.includes(title)) { - comment = `[Amily2小说处理] ${title}`; - keys = [`小说处理`, title]; - } else { - comment = `[Amily2-Glossary] ${title}`; - keys = [`自定义条目`, title]; - } - - const existingEntry = localManagedEntries.find(e => e.comment === comment); - const loreData = { keys, content, comment, enabled: true, order: 100, position: 'before_char' }; - - if (existingEntry) { - entriesToUpdate.push({ uid: existingEntry.uid, ...loreData }); - Object.assign(existingEntry, { ...loreData, keys: keys }); - } else { - entriesToCreate.push(loreData); - localManagedEntries.push({ uid: -1, ...loreData, keys: keys }); - } - } - - if (entriesToUpdate.length > 0) { - await safeUpdateLorebookEntries(bookName, entriesToUpdate); - updateStatusCallback(`更新了 ${entriesToUpdate.length} 个世界书条目。`, 'info'); - } - if (entriesToCreate.length > 0) { - for (const entry of entriesToCreate) { - await compatibleWriteToLorebook(bookName, entry.comment, () => entry.content, { - keys: entry.keys, - isConstant: false, - insertion_position: 'before_char', - depth: 100, - }); - } - updateStatusCallback(`创建了 ${entriesToCreate.length} 个新世界书条目。`, 'success'); - } - - existingEntriesContent = buildContextFromEntries(localManagedEntries); + + updateStatusCallback(`批次 ${currentBatchNumber} 处理完成,已创建新条目。`, 'success'); + previousBatchAIResponse = aiContent; } updateStatusCallback('小说处理完成!', 'success'); @@ -183,3 +144,181 @@ export async function executeNovelProcessing(processingState, updateStatusCallba 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 = ` 已选择: ${file.name}`; + + } catch (error) { + console.error(`Error processing file ${file.name}:`, error); + toastr.error(`处理文件 ${file.name} 失败。`); + } + }); + container.appendChild(fileElement); + }); + + container.style.display = 'block'; +}