From 8ef7ef59d951cffb1d0d7d5adba7747ed3992765 Mon Sep 17 00:00:00 2001
From: Wx-2025 <351320169@qq.com>
Date: Thu, 23 Oct 2025 00:18:02 +0800
Subject: [PATCH] Update executor.js
---
glossary/executor.js | 311 +++++++++++++++++++++++++++++++------------
1 file changed, 225 insertions(+), 86 deletions(-)
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';
+}