Files
ST-Amily2-Chat-Optimisation/core/table-system/secondary-filler.js

394 lines
16 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 settings = extension_settings[extensionName];
// 总开关关闭时,分步填表同样禁用
if (settings.table_system_enabled === false) {
log('【分步填表】表格系统总开关已关闭,跳过。', 'info');
return;
}
const context = getContext();
if (context.chat.length <= 1) {
console.log("[Amily2-副API] 聊天刚开始,跳过本次自动填表。");
return;
}
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;
}
try {
const bufferSize = parseInt(settings.secondary_filler_buffer || 0, 10);
const batchSize = parseInt(settings.secondary_filler_batch || 0, 10);
const contextLimit = parseInt(settings.secondary_filler_context || 2, 10);
// 【V1.7.7 修复】限制最大回溯深度,防止更新后无限填补旧历史
// 响应用户反馈:扫描深度 = 上下文 + 填表批次 + 保留楼层 + 冗余量(10)
// redundancy (冗余量): 额外扫描 10 层作为安全缓冲,防止因消息索引计算偏差导致漏掉边缘消息
const redundancy = 10;
const maxScanDepth = contextLimit + batchSize + bufferSize + redundancy;
const chat = context.chat;
const totalMessages = chat.length;
const validEndIndex = totalMessages - 1 - bufferSize;
// 计算扫描的起始索引不小于0
const scanStartIndex = Math.max(0, validEndIndex - maxScanDepth);
if (validEndIndex < 0) {
console.log(`[Amily2-副API] 消息数量不足以超出保留区(${bufferSize}),跳过。`);
return;
}
let targetMessages = [];
let needsProcessing = false;
const getContentHash = (content) => {
let hash = 0, i, chr;
if (content.length === 0) return hash;
for (i = 0; i < content.length; i++) {
chr = content.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return hash;
};
// 【修复】改为正向扫描,优先处理最老的未处理消息,防止遗留消息被挤出扫描区
for (let i = scanStartIndex; i <= validEndIndex; i++) {
const msg = chat[i];
if (msg.is_user) continue;
const currentHash = getContentHash(msg.mes);
const savedHash = msg.metadata?.Amily2_Process_Hash;
const isUnprocessed = !savedHash;
const isChanged = savedHash && savedHash !== currentHash;
if (isUnprocessed || isChanged) {
targetMessages.push({ index: i, msg: msg, hash: currentHash });
if (batchSize > 0 && targetMessages.length >= batchSize) {
needsProcessing = true;
break;
}
}
}
if (targetMessages.length === 0) {
console.log("[Amily2-副API] 没有发现需要处理的消息。");
return;
}
if (batchSize > 0) {
if (targetMessages.length < batchSize) {
console.log(`[Amily2-副API] 批量模式: 当前累积 ${targetMessages.length}/${batchSize} 条未处理消息,暂不触发。`);
return;
}
} else {
targetMessages = [targetMessages[targetMessages.length - 1]];
}
console.log(`[Amily2-副API] 触发填表: 处理 ${targetMessages.length} 条消息。索引范围: ${targetMessages[0].index} - ${targetMessages[targetMessages.length-1].index}`);
toastr.info(`分步填表正在执行,正在填写 ${targetMessages[0].index + 1} 楼至 ${targetMessages[targetMessages.length-1].index + 1} 楼的内容`, "Amily2-分步填表");
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 || [];
}
let coreContentText = "";
const userName = context.name1 || '用户';
const characterName = context.name2 || '角色';
for (const target of targetMessages) {
let textToProcess = target.msg.mes;
if (tagsToExtract.length > 0) {
const blocks = extractBlocksByTags(textToProcess, tagsToExtract);
textToProcess = blocks.join('\n\n');
}
textToProcess = applyExclusionRules(textToProcess, exclusionRules);
if (!textToProcess.trim()) continue;
coreContentText += `\n【第 ${target.index + 1} 楼】${characterName}AI消息\n${textToProcess}\n`;
}
if (!coreContentText.trim()) {
console.log("[Amily2-副API] 目标内容处理后为空,跳过。");
return;
}
const historyEndIndex = targetMessages[0].index - 1;
let historyContextStr = "";
if (contextLimit > 0 && historyEndIndex >= 0) {
historyContextStr = await getHistoryContext(contextLimit, historyEndIndex, tagsToExtract, exclusionRules) || "";
}
const currentInteractionContent = (historyContextStr ? `${historyContextStr}\n\n` : '') +
`<核心填表内容>\n${coreContentText}\n</核心填表内容>`;
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':
if (historyContextStr) {
messages.push({ role: "system", content: historyContextStr });
}
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${coreContentText}\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);
// 【修复】检查 AI 是否返回了有效的指令块,防止 AI 偷懒或格式错误被误判为成功
if (!rawContent.includes('<Amily2Edit>')) {
throw new Error('AI未返回有效的 <Amily2Edit> 指令块,可能格式错误或未产生实质性变更。');
}
updateTableFromText(rawContent);
const memoryState = getMemoryState();
const lastProcessedMsg = targetMessages[targetMessages.length - 1].msg;
for (const target of targetMessages) {
if (!target.msg.metadata) target.msg.metadata = {};
target.msg.metadata.Amily2_Process_Hash = target.hash;
}
if (saveStateToMessage(memoryState, lastProcessedMsg)) {
renderTables();
updateOrInsertTableInChat();
}
saveChat();
toastr.success("分步填表执行完毕。", "Amily2-分步填表");
} catch (error) {
console.error(`[Amily2-副API] 发生严重错误:`, error);
// 【新增】自定义重试逻辑
const maxRetries = parseInt(settings.secondary_filler_max_retries || 0, 10);
const currentRetryCount = latestMessage?.metadata?.Amily2_Retry_Count || 0;
if (currentRetryCount < maxRetries) {
const nextRetryCount = currentRetryCount + 1;
console.log(`[Amily2-副API] 准备进行第 ${nextRetryCount}/${maxRetries} 次重试...`);
toastr.warning(`副API填表失败: ${error.message}。将在3秒后进行第 ${nextRetryCount} 次重试...`, "自动重试");
// 记录重试次数到最新消息的 metadata 中,以便跨调用传递状态
if (latestMessage) {
if (!latestMessage.metadata) latestMessage.metadata = {};
latestMessage.metadata.Amily2_Retry_Count = nextRetryCount;
}
setTimeout(() => {
fillWithSecondaryApi(latestMessage, forceRun);
}, 3000);
} else {
console.log(`[Amily2-副API] 已达到最大重试次数 (${maxRetries}),放弃本次填表。`);
toastr.error(`副API填表失败: ${error.message}。已达到最大重试次数,任务终止。`, "严重错误");
// 清除重试计数器
if (latestMessage && latestMessage.metadata) {
delete latestMessage.metadata.Amily2_Retry_Count;
}
}
}
}
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);
// 【修复】slice 的 end 索引是不包含的,为了包含 historyUntilend 必须 +1
const sliceEnd = historyUntil + 1;
const messagesToExtract = Math.min(messagesToFetch, sliceEnd);
const sliceStart = Math.max(0, sliceEnd - messagesToExtract);
const historySlice = chat.slice(sliceStart, sliceEnd);
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: sliceStart + 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</对话记录>`;
}