Files
ST-Amily2-Chat-Optimisation…/core/table-system/secondary-filler.js
2025-11-30 23:04:42 +08:00

341 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 { getContext, extension_settings } from "/scripts/extensions.js";
import { loadWorldInfo } from "/scripts/world-info.js";
import { saveChat } from "/script.js";
import { renderTables } from '../../ui/table-bindings.js';
import { updateOrInsertTableInChat } from '../../ui/message-table-renderer.js';
import { extensionName } from "../../utils/settings.js";
import { updateTableFromText, getBatchFillerRuleTemplate, getBatchFillerFlowTemplate, convertTablesToCsvString, saveStateToMessage, getMemoryState, clearHighlights } from './manager.js';
import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
import { callAI, generateRandomSeed } from '../api.js';
import { callNccsAI } from '../api/NccsApi.js';
import { extractBlocksByTags, applyExclusionRules } from '../utils/rag-tag-extractor.js';
import { safeLorebookEntries } from '../tavernhelper-compatibility.js';
async function getWorldBookContext() {
const settings = extension_settings[extensionName];
if (!settings.table_worldbook_enabled) {
return '';
}
const selectedEntriesByBook = settings.table_selected_entries || {};
const booksToInclude = Object.keys(selectedEntriesByBook);
const selectedEntryUids = new Set(Object.values(selectedEntriesByBook).flat());
if (booksToInclude.length === 0 || selectedEntryUids.size === 0) {
return '';
}
let allEntries = [];
for (const bookName of booksToInclude) {
try {
const entries = await safeLorebookEntries(bookName);
if (entries?.length) {
entries.forEach(entry => allEntries.push({ ...entry, bookName }));
}
} catch (error) {
console.error(`[Amily2-副API] Error loading entries for world book: ${bookName}`, error);
}
}
const userEnabledEntries = allEntries.filter(entry => {
return entry && selectedEntryUids.has(String(entry.uid));
});
if (userEnabledEntries.length === 0) {
return '';
}
let content = userEnabledEntries.map(entry =>
`[来源:世界书,条目名字:${entry.comment || '无标题条目'}]\n${entry.content}`
).join('\n\n');
const maxChars = settings.table_worldbook_char_limit || 30000;
if (content.length > maxChars) {
content = content.substring(0, maxChars);
const lastNewline = content.lastIndexOf('\n');
if (lastNewline !== -1) {
content = content.substring(0, lastNewline);
}
content += '\n[...内容已截断]';
}
return content.trim() ? `<世界书>\n${content.trim()}\n</世界书>` : '';
}
export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
clearHighlights();
const context = getContext();
if (context.chat.length <= 1) {
console.log("[Amily2-副API] 聊天刚开始,跳过本次自动填表。");
return;
}
const settings = extension_settings[extensionName];
const fillingMode = settings.filling_mode || 'main-api';
if (fillingMode !== 'secondary-api' && !forceRun) {
log('当前非分步填表模式,且未强制执行,跳过。', 'info');
return;
}
if (window.AMILY2_SYSTEM_PARALYZED === true) {
console.error("[Amily2-制裁] 系统完整性已受损,所有外交活动被无限期中止。");
return;
}
const { apiUrl, apiKey, model, temperature, maxTokens, forceProxyForCustomApi } = settings;
if (!apiUrl || !model) {
if (!window.secondaryApiUrlWarned) {
toastr.error("主API的URL或模型未配置分步填表功能无法启动。", "Amily2-分步填表");
window.secondaryApiUrlWarned = true;
}
return;
}
try {
// --- 延迟填表逻辑 (V151.0) ---
const delay = parseInt(settings.secondary_filler_delay || 0, 10);
const chat = context.chat;
let targetMessage;
let targetIndex;
if (delay > 0) {
// 如果有延迟,我们需要找到“延迟前”的那条消息
// chat.length - 1 是当前最新消息的索引
// 目标索引 = (chat.length - 1) - delay
targetIndex = (chat.length - 1) - delay;
if (targetIndex < 0) {
console.log(`[Amily2-副API] 延迟模式(${delay}): 历史楼层不足,跳过填表。`);
return;
}
targetMessage = chat[targetIndex];
// 检查目标消息是否是AI消息通常填表针对AI回复
// 如果目标消息是用户的消息而我们只想填AI的表这可能是一个问题。
// 但如果用户设置了延迟他们可能期望每隔几层填一次或者只填AI层。
// 现有的 `fillWithSecondaryApi` 是在 `CHAT_COMPLETION` 后调用的此时最新消息通常是AI消息。
// 如果延迟是奇数例如1目标消息可能是用户消息。
// 假设延迟是偶数例如2目标消息是上一条AI消息。
// 为了安全起见,如果目标消息是用户消息,我们可能应该跳过?或者依然填表(记录用户消息的表)?
// 目前表系统通常绑定在AI回复上。
// 如果 targetMessage.is_user我们尝试往回找最近的一条AI消息
// 不,这会乱套。严格按照楼层索引来。
console.log(`[Amily2-副API] 延迟模式生效: 当前总楼层 ${chat.length}, 延迟 ${delay}, 目标楼层索引 ${targetIndex}`);
} else {
// 无延迟,使用传入的最新消息
targetMessage = latestMessage;
targetIndex = chat.length - 1;
}
let textToProcess = targetMessage.mes;
if (!textToProcess || !textToProcess.trim()) {
console.log("[Amily2-副API] 目标消息内容为空,跳过填表任务。");
return;
}
let tagsToExtract = [];
let exclusionRules = [];
if (settings.table_independent_rules_enabled) {
tagsToExtract = (settings.table_tags_to_extract || '').split(',').map(t => t.trim()).filter(Boolean);
exclusionRules = settings.table_exclusion_rules || [];
}
if (tagsToExtract.length > 0) {
const blocks = extractBlocksByTags(textToProcess, tagsToExtract);
textToProcess = blocks.join('\n\n');
}
textToProcess = applyExclusionRules(textToProcess, exclusionRules);
if (!textToProcess.trim()) {
console.log("[Amily2-副API] 规则处理后消息内容为空,跳过填表任务。");
return;
}
const userName = context.name1 || '用户';
const characterName = context.name2 || '角色';
// 寻找目标消息之前的最后一条用户消息
let lastUserMessage = null;
let lastUserMessageIndex = -1;
// 从 targetIndex - 1 开始往前找
for (let i = targetIndex - 1; i >= 0; i--) {
if (chat[i].is_user) {
lastUserMessage = chat[i];
lastUserMessageIndex = i;
break;
}
}
const currentInteractionContent = (lastUserMessage ? `${userName}(用户)消息:${lastUserMessage.mes}\n` : '') +
`${characterName}AI消息[核心处理内容]${textToProcess}`;
let mixedOrder;
try {
const savedOrder = localStorage.getItem('amily2_prompt_presets_v2_mixed_order');
if (savedOrder) {
mixedOrder = JSON.parse(savedOrder);
}
} catch (e) {
console.error("[副API填表] 加载混合顺序失败:", e);
}
const order = getMixedOrder('secondary_filler') || [];
const presetPrompts = await getPresetPrompts('secondary_filler');
const messages = [
{ role: 'system', content: generateRandomSeed() }
];
const worldBookContext = await getWorldBookContext();
const ruleTemplate = getBatchFillerRuleTemplate();
const flowTemplate = getBatchFillerFlowTemplate();
const currentTableDataString = convertTablesToCsvString();
const finalFlowPrompt = flowTemplate.replace('{{{Amily2TableData}}}', currentTableDataString);
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') {
switch (item.id) {
case 'worldbook':
if (worldBookContext) {
messages.push({ role: "system", content: worldBookContext });
}
break;
case 'contextHistory':
const contextReadingLevel = settings.context_reading_level || 4;
const historyMessagesToGet = contextReadingLevel > 2 ? contextReadingLevel - 2 : 0;
if (historyMessagesToGet > 0) {
// 这里的 historyEndIndex 应该是我们上面计算出的 lastUserMessageIndex
// 如果没找到用户消息,则使用 targetIndex - 1
const historyEndIndex = lastUserMessageIndex !== -1 ? lastUserMessageIndex : Math.max(0, targetIndex - 1);
const historyContext = await getHistoryContext(historyMessagesToGet, historyEndIndex, tagsToExtract, exclusionRules);
if (historyContext) {
messages.push({ role: "system", content: historyContext });
}
}
break;
case 'ruleTemplate':
messages.push({ role: "system", content: ruleTemplate });
break;
case 'flowTemplate':
messages.push({ role: "system", content: finalFlowPrompt });
break;
case 'coreContent':
messages.push({ role: 'user', content: `请严格根据以下"最新消息"中的内容进行填写表格,并按照指定的格式输出,不要添加任何额外信息。\n\n<最新消息>\n${currentInteractionContent}\n</最新消息>` });
break;
}
}
}
console.groupCollapsed(`[Amily2 分步填表] 即将发送至 API 的内容`);
console.log("发送给AI的提示词: ", JSON.stringify(messages, null, 2));
console.dir(messages);
console.groupEnd();
let rawContent;
if (settings.nccsEnabled) {
console.log('[Amily2-副API] 使用 Nccs API 进行分步填表...');
rawContent = await callNccsAI(messages);
} else {
console.log('[Amily2-副API] 使用默认 API 进行分步填表...');
rawContent = await callAI(messages);
}
if (!rawContent) {
console.error('[Amily2-副API] 未能获取AI响应内容。');
return;
}
console.log("[Amily2号-副API-原始回复]:", rawContent);
updateTableFromText(rawContent);
// 保存到目标消息
if (saveStateToMessage(getMemoryState(), targetMessage)) {
// 如果目标消息不是最新消息,我们可能需要重新渲染整个聊天记录或者特定消息的表格?
// renderTables() 通常重新渲染所有可见表格
renderTables();
// updateOrInsertTableInChat 通常插入到DOM中
// 我们可能需要传递 targetIndex 给 updateOrInsertTableInChat 吗?
// 目前 updateOrInsertTableInChat 似乎是查找 .mes_text 并插入。
// 如果我们更新了历史消息的数据,我们需要确保 DOM 也更新。
// 由于 SillyTavern 的消息渲染机制,如果消息已经在屏幕上,仅仅修改数据可能不会自动更新 DOM。
// 但是 renderTables() 应该会处理这个。
updateOrInsertTableInChat();
}
saveChat();
} catch (error) {
console.error(`[Amily2-副API] 发生严重错误:`, error);
toastr.error(`副API填表失败: ${error.message}`, "严重错误");
}
}
async function getHistoryContext(messagesToFetch, historyEndIndex, tagsToExtract, exclusionRules) {
const context = getContext();
const chat = context.chat;
if (!chat || chat.length === 0 || messagesToFetch <= 0) {
return null;
}
const historyUntil = Math.max(0, historyEndIndex);
const messagesToExtract = Math.min(messagesToFetch, historyUntil);
const startIndex = Math.max(0, historyUntil - messagesToExtract);
const endIndex = historyUntil;
const historySlice = chat.slice(startIndex, endIndex);
const userName = context.name1 || '用户';
const characterName = context.name2 || '角色';
const messages = historySlice.map((msg, index) => {
let content = msg.mes;
if (!msg.is_user && tagsToExtract && tagsToExtract.length > 0) {
const blocks = extractBlocksByTags(content, tagsToExtract);
content = blocks.join('\n\n');
}
if (content && exclusionRules) {
content = applyExclusionRules(content, exclusionRules);
}
if (!content.trim()) return null;
return {
floor: startIndex + index + 1,
author: msg.is_user ? userName : characterName,
authorType: msg.is_user ? 'user' : 'char',
content: content.trim()
};
}).filter(Boolean);
if (messages.length === 0) {
return null;
}
const formattedHistory = messages.map(m => `【第 ${m.floor} 楼】 ${m.author}: ${m.content}`).join('\n');
return `<对话记录>\n${formattedHistory}\n</对话记录>`;
}