Files
ST-Amily2-Chat-Optimisation…/glossary/executor.js

326 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
}