Files
ST-Amily2-Chat-Optimisation/core/lore.js

559 lines
24 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 { extension_settings, getContext } from "/scripts/extensions.js";
import { characters, eventSource, event_types } from "/script.js";
import { loadWorldInfo, createNewWorldInfo, createWorldInfoEntry, saveWorldInfo, world_names, updateWorldInfoList } from "/scripts/world-info.js";
import { compatibleWriteToLorebook, safeLorebooks, safeCharLorebooks, safeLorebookEntries } from "./tavernhelper-compatibility.js";
import { extensionName } from "../utils/settings.js";
document.addEventListener('amily-lorebook-created', (event) => {
if (event.detail && event.detail.bookName) {
console.log(`[Amily2-国史馆] 监听到史书《${event.detail.bookName}》变更,即刻通报工部刷新宫殿。`);
refreshWorldbookListOnly(event.detail.bookName);
}
});
export const LOREBOOK_PREFIX = "Amily2档案-";
export const DEDICATED_LOREBOOK_NAME = "Amily2号-国史馆";
export const INTRODUCTORY_TEXT =
"【Amily2号自动档案】\n此卷宗由Amily2号优化助手自动生成并维护记录核心事件脉络。\n---\n";
export async function getChatIdentifier() {
let attempts = 0;
const maxAttempts = 50;
const interval = 100;
while (attempts < maxAttempts) {
try {
const context = getContext();
if (context && context.characterId) {
const character = characters[context.characterId];
if (character && character.avatar) {
return `char-${character.avatar.replace(/\.(png|webp|jpg|jpeg|gif)$/, "")}`;
}
return `char-${context.characterId}`;
}
if (context && context.chat_filename) {
const fileName = context.chat_filename.split(/[\\/]/).pop();
return fileName.replace(/\.jsonl?$/, "");
}
} catch (error) {
console.warn(
`[Amily2-户籍管理处] 等待上下文时发生轻微错误 (尝试次数 ${attempts + 1}):`,
error.message,
);
}
await new Promise((resolve) => setTimeout(resolve, interval));
attempts++;
}
console.error("[Amily2-国史馆] 户籍管理处在长时间等待后,仍无法确定户籍。");
toastr.warning(
"Amily2号无法确定当前聊天身份世界书功能将受影响。",
"上下文错误",
);
return "unknown_chat_timeout";
}
export async function findLatestSummaryLore(lorebookName, chatIdentifier) {
try {
const bookData = await loadWorldInfo(lorebookName);
if (!bookData || !bookData.entries) {
return null;
}
const entriesArray = Object.values(bookData.entries);
const uniqueLoreName = `${LOREBOOK_PREFIX}${chatIdentifier}`;
return (
entriesArray.find(
(entry) => entry.comment === uniqueLoreName && !entry.disable,
) || null
);
} catch (error) {
console.error(
`[Amily2-国史馆] 钦差大臣在 '${lorebookName}' 检索时发生错误:`,
error,
);
return null;
}
}
export async function getCombinedWorldbookContent(lorebookName) {
if (!lorebookName) return "";
try {
const bookData = await loadWorldInfo(lorebookName);
if (!bookData || !bookData.entries) {
return "";
}
const activeContents = Object.values(bookData.entries)
.filter((entry) => !entry.disable)
.map((entry) => `[条目: ${entry.comment || "无标题"}]\n${entry.content}`);
return activeContents.join("\n\n---\n\n");
} catch (error) {
console.error(
`[Amily2-国史馆] 钦差大臣在整合 '${lorebookName}' 时发生错误:`,
error,
);
toastr.error(`读取世界书 '${lorebookName}' 失败!`, "档案整合错误");
return "";
}
}
export async function refreshWorldbookListOnly(newBookName = null) {
console.log("[Amily2号-工部-v2.0] 执行SillyTavern核心UI刷新...");
try {
await updateWorldInfoList();
console.log("[Amily2号-工部] SillyTavern核心刷新函数 (updateWorldInfoList) 调用成功。");
} catch (error) {
console.error("[Amily2号-工部] 调用核心刷新函数时出错:", error);
toastr.error("Amily2号调用核心UI刷新函数时失败。", "核心刷新失败");
}
}
export async function writeSummaryToLorebook(pendingData) {
if (!pendingData || !pendingData.summary || !pendingData.sourceAiMessageTimestamp || !pendingData.settings) {
console.warn("[Amily助手-国史馆] 接到一份残缺的待办文书,写入任务已中止。", pendingData);
return;
}
const context = getContext();
const chat = context.chat;
let isSourceMessageValid = false;
let sourceMessageCandidate = null;
// 寻找最新的 AI 消息以进行时间戳验证
for (let i = chat.length - 1; i >= 0; i--) {
if (!chat[i].is_user) {
sourceMessageCandidate = chat[i];
break;
}
}
if (sourceMessageCandidate && sourceMessageCandidate.send_date === pendingData.sourceAiMessageTimestamp) {
isSourceMessageValid = true;
}
if (!isSourceMessageValid) {
console.log("[Amily助手-逆时寻踪] 裁决: 源消息已被修改或删除,遵旨废黜过时总结。");
return;
}
const { summary: summaryToCommit, settings } = pendingData;
console.groupCollapsed(`[Amily助手-存档任务] ${new Date().toLocaleTimeString()}`);
console.time("总结写入总耗时");
try {
const chatIdentifier = await getChatIdentifier();
const character = characters[context.characterId];
let targetLorebookName = null;
switch (settings.target) {
case "character_main":
targetLorebookName = character?.data?.extensions?.world;
if (!targetLorebookName) {
toastr.warning("角色未绑定主世界书,总结写入任务已中止。", "Amily助手");
console.groupEnd();
return;
}
break;
case "dedicated":
targetLorebookName = `${DEDICATED_LOREBOOK_NAME}-${chatIdentifier}`;
break;
default:
toastr.error(`收到未知的写入指令: "${settings.target}"`, "Amily助手");
console.groupEnd();
return;
}
const uniqueLoreName = `${LOREBOOK_PREFIX}${chatIdentifier}`;
// 定义内容更新的回调函数
const contentUpdateCallback = (existingContent) => {
if (existingContent) {
// 如果条目已存在,追加内容
const cleanedContent = existingContent.replace(INTRODUCTORY_TEXT, "").trim();
const lines = cleanedContent ? cleanedContent.split("\n") : [];
const nextNumber = lines.length + 1;
return `${existingContent}\n${nextNumber}. ${summaryToCommit}`;
} else {
// 如果条目不存在,创建新内容
return `${INTRODUCTORY_TEXT}1. ${summaryToCommit}`;
}
};
// 定义写入选项
const options = {
keys: settings.keywords.split(',').map(k => k.trim()).filter(Boolean),
isConstant: settings.activationMode === 'always',
insertion_position: settings.insertionPosition,
depth: settings.depth,
};
// 使用统一的兼容性写入函数
const success = await compatibleWriteToLorebook(targetLorebookName, uniqueLoreName, contentUpdateCallback, options);
if (success) {
toastr.success(`总结已成功写入《${targetLorebookName}》!`, "Amily助手");
} else {
toastr.error(`总结写入《${targetLorebookName}》时失败。`, "Amily助手");
}
} catch (error) {
console.error("[Amily助手-写入失败] 写入流程发生意外错误:", error);
toastr.error("后台写入总结时发生错误。", "Amily助手");
} finally {
console.timeEnd("总结写入总耗时");
console.groupEnd();
}
}
export async function getOptimizationWorldbookContent() {
const settings = extension_settings[extensionName];
if (!settings || !settings.modal_wbEnabled) {
return '';
}
try {
let bookNames = [];
if (settings.modal_wbSource === 'manual') {
bookNames = settings.modal_amily2_wb_selected_worldbooks || [];
} else { // 'character' source
const charLorebooks = await safeCharLorebooks({ type: 'all' });
if (charLorebooks.primary) bookNames.push(charLorebooks.primary);
if (charLorebooks.additional?.length) bookNames.push(...charLorebooks.additional);
}
if (bookNames.length === 0) {
console.log('[Amily2-正文优化] No world books selected or linked for optimization.');
return '';
}
let allEntries = [];
for (const bookName of bookNames) {
if (bookName) {
const entries = await safeLorebookEntries(bookName);
if (entries?.length) {
entries.forEach(entry => allEntries.push({ ...entry, bookName }));
}
}
}
const selectedEntriesConfig = settings.modal_amily2_wb_selected_entries || {};
const userEnabledEntries = allEntries.filter(entry => {
// Entry must be enabled in the lorebook itself
if (!entry.enabled) return false;
// Check against our UI selection
const bookConfig = selectedEntriesConfig[entry.bookName];
return bookConfig ? bookConfig.includes(String(entry.uid)) : false;
});
if (userEnabledEntries.length === 0) {
console.log('[Amily2-正文优化] No entries are selected for optimization in the chosen world books.');
return '';
}
const finalContent = userEnabledEntries.map(entry => entry.content).filter(Boolean);
const combinedContent = finalContent.join('\n\n---\n\n');
console.log(`[Amily2-正文优化] Loaded ${userEnabledEntries.length} world book entries, total length: ${combinedContent.length}`);
return combinedContent;
} catch (error) {
console.error(`[Amily2-正文优化] Processing world book content failed:`, error);
return '';
}
}
export async function getPlotOptimizedWorldbookContent(context, apiSettings, isConcurrent = false) {
const panel = $('#amily2_plot_optimization_panel');
let liveSettings = {};
const isPanelReady = panel.length > 0 && panel.find('#amily2_opt_worldbook_entry_list_container input[type="checkbox"]').length > 0;
if (isConcurrent) {
// This is a concurrent call, force use of passed apiSettings
console.log('[剧情优化大师] 检测到并发调用,强制使用传入的并发世界书设置。');
liveSettings = {
worldbookEnabled: apiSettings.plotOpt_worldbook_enabled,
worldbookSource: apiSettings.plotOpt_worldbook_source || 'character',
selectedWorldbooks: apiSettings.plotOpt_selectedWorldbooks || [],
autoSelectWorldbooks: apiSettings.plotOpt_autoSelectWorldbooks || [],
worldbookCharLimit: apiSettings.plotOpt_worldbookCharLimit,
enabledWorldbookEntries: null, // Let the logic below handle it based on selected books.
};
} else if (isPanelReady) {
// This is a main call and the panel is ready, read from UI.
liveSettings.worldbookEnabled = panel.find('#amily2_opt_worldbook_enabled').is(':checked');
liveSettings.newMemoryLogicEnabled = panel.find('#amily2_opt_new_memory_logic_enabled').is(':checked');
liveSettings.worldbookSource = panel.find('input[name="amily2_opt_worldbook_source"]:checked').val() || 'character';
liveSettings.selectedWorldbooks = [];
if (liveSettings.worldbookSource === 'manual') {
panel.find('#amily2_opt_worldbook_checkbox_list input[type="checkbox"]:not(.amily2_opt_wb_auto_check):checked').each(function() {
liveSettings.selectedWorldbooks.push($(this).val());
});
}
liveSettings.autoSelectWorldbooks = [];
panel.find('#amily2_opt_worldbook_checkbox_list input.amily2_opt_wb_auto_check:checked').each(function() {
liveSettings.autoSelectWorldbooks.push($(this).data('book'));
});
liveSettings.worldbookCharLimit = parseInt(panel.find('#amily2_opt_worldbook_char_limit').val(), 10) || 60000;
liveSettings.contextLimit = parseInt(panel.find('#amily2_opt_context_limit').val(), 10) || 5;
let enabledEntries = {};
panel.find('#amily2_opt_worldbook_entry_list_container input[type="checkbox"]:checked').each(function() {
const bookName = $(this).data('book');
const uid = parseInt($(this).data('uid'));
if (!enabledEntries[bookName]) {
enabledEntries[bookName] = [];
}
enabledEntries[bookName].push(uid);
});
liveSettings.enabledWorldbookEntries = enabledEntries;
} else {
// Fallback for main call when panel is not ready.
if (panel.length > 0) {
console.warn('[剧情优化大师] 检测到UI面板但内容未完全加载回退到使用已保存的设置。');
} else {
console.warn('[剧情优化大师] 未找到设置面板,世界书功能将使用已保存的设置。');
}
liveSettings = {
worldbookEnabled: apiSettings.plotOpt_worldbookEnabled,
newMemoryLogicEnabled: apiSettings.plotOpt_newMemoryLogicEnabled,
worldbookSource: apiSettings.plotOpt_worldbookSource || 'character',
selectedWorldbooks: apiSettings.plotOpt_selectedWorldbooks,
autoSelectWorldbooks: apiSettings.plotOpt_autoSelectWorldbooks || [],
worldbookCharLimit: apiSettings.plotOpt_worldbookCharLimit,
contextLimit: apiSettings.plotOpt_contextLimit ?? apiSettings.plotOpt_contextTurnCount ?? 5,
enabledWorldbookEntries: apiSettings.plotOpt_enabledWorldbookEntries,
};
}
if (!liveSettings.worldbookEnabled) {
return '';
}
if (!context) {
console.warn('[剧情优化大师] context 未提供,无法获取世界书内容。');
return '';
}
try {
let bookNames = [];
if (liveSettings.worldbookSource === 'manual') {
bookNames = liveSettings.selectedWorldbooks;
if (bookNames.length === 0) return '';
} else {
const charLorebooks = await safeCharLorebooks({ type: 'all' });
if (charLorebooks.primary) bookNames.push(charLorebooks.primary);
if (charLorebooks.additional?.length) bookNames.push(...charLorebooks.additional);
if (bookNames.length === 0) return '';
}
let allEntries = [];
for (const bookName of bookNames) {
if (bookName) {
const entries = await safeLorebookEntries(bookName);
if (entries?.length) {
entries.forEach(entry => allEntries.push({ ...entry, bookName }));
}
}
}
if (allEntries.length === 0) return '';
const enabledEntriesMap = liveSettings.enabledWorldbookEntries; // Can be null for concurrent
const autoSelectedBooks = liveSettings.autoSelectWorldbooks || [];
const userEnabledEntries = allEntries.filter(entry => {
if (!entry.enabled) return false;
// New Memory Logic
if (liveSettings.newMemoryLogicEnabled) {
const character = characters[context.characterId];
const charName = character ? (character.data?.name || character.name) : null;
if (charName && entry.bookName === `Amily2_Memory_${charName}`) {
const keywords = [...new Set([...(entry.key || []), ...(entry.keys || [])])];
if (keywords.some(k => k.includes('索引'))) {
entry.constant = true; // Blue Light (Constant)
entry.prevent_recursion = true; // Prevent Index from triggering other entries
} else {
// Ensure it's not constant unless it was already constant in ST (which we might want to respect, or override?)
// The requirement says: "其余的绿灯条目则依照SittlyTavern原本的绿灯关键词的触发逻辑"
// This implies we should treat them as potential Green Lights.
// In my logic, if entry.constant is false, it becomes a Green Light candidate.
// However, ST entries have a `constant` property. `safeLorebookEntries` returns it.
// If the entry was originally constant in ST, should we keep it constant?
// The requirement says "原逻辑转换...".
// "只要关键词里面包含索引,则将所有索引条目发送给我们的模型。"
// "而其余的绿灯条目..."
// This implies we are redefining what is constant/triggered based on this logic.
// So I will force constant=false if it doesn't have "索引".
entry.constant = false;
}
return true; // Always include as candidate
}
}
// For concurrent calls where enabledWorldbookEntries is null, or for books marked as "auto-select",
// we consider all enabled entries within that book as selected.
const isAuto = autoSelectedBooks.includes(entry.bookName);
if (isConcurrent || isAuto) {
entry.constant = true; // Force as constant if auto-selected or concurrent
return true;
}
// For main calls with manual entry selection
if (enabledEntriesMap) {
const bookConfig = enabledEntriesMap[entry.bookName];
const isChecked = (bookConfig ? (bookConfig.includes(entry.uid) || bookConfig.includes(String(entry.uid))) : false);
if (isChecked) {
entry.constant = true; // Force as constant if checked in UI
}
// If not checked, it relies on its own constant/green-light status.
return true;
}
// Default case if something goes wrong (should not be reached)
return false;
});
if (userEnabledEntries.length === 0) return '';
let messagesToScan = context.chat;
if (liveSettings.contextLimit > 0) {
messagesToScan = context.chat.slice(-liveSettings.contextLimit);
}
const chatHistory = messagesToScan.map(message => message.mes).join('\n').toLowerCase();
const getEntryKeywords = (entry) => [...new Set([...(entry.key || []), ...(entry.keys || [])])]
.filter(k => k && k.trim().length > 0)
.map(k => k.toLowerCase());
const blueLightEntries = userEnabledEntries.filter(entry => entry.constant);
let pendingGreenLights = userEnabledEntries.filter(entry => !entry.constant);
const triggeredEntries = new Set([...blueLightEntries]);
// 禁用递归扫描,防止总结/索引条目触发所有内容。
// 仅扫描聊天记录。
for (const entry of pendingGreenLights) {
const keywords = getEntryKeywords(entry);
const secondaryKeys = (entry.secondary_keys || []).filter(k => k && k.trim().length > 0).map(k => k.toLowerCase());
const selectiveKeys = (entry.selective || []).filter(k => k && k.trim().length > 0).map(k => k.toLowerCase());
// 仅检查聊天记录,忽略其他条目的内容(防止递归触发)
const checkText = chatHistory;
const hasPrimary = keywords.length > 0 && keywords.some(k => checkText.includes(k));
const hasSecondary = secondaryKeys.length === 0 || secondaryKeys.some(k => checkText.includes(k));
const hasSelective = selectiveKeys.length > 0 && selectiveKeys.some(k => checkText.includes(k));
let isTriggered = hasPrimary && hasSecondary && !hasSelective;
if (isTriggered) {
triggeredEntries.add(entry);
}
}
const finalEntries = Array.from(triggeredEntries);
// 排序:索引内容(常驻且防递归) > 触发的条目 > 其他常驻
finalEntries.sort((a, b) => {
const isIndex = (e) => e.constant && e.prevent_recursion;
const isTriggered = (e) => !e.constant;
const getPriority = (e) => {
if (isIndex(e)) return 1;
if (isTriggered(e)) return 2;
return 3; // 其他常驻
};
return getPriority(a) - getPriority(b);
});
const finalContent = finalEntries.map(entry => {
const keys = [...new Set([...(entry.key || []), ...(entry.keys || [])])].filter(Boolean).join('、');
const displayName = entry.comment || `Entry ${entry.uid}`;
return `【世界书条目:${displayName}。绿灯触发关键词:${keys}\n内容:${entry.content}`;
}).filter(Boolean);
if (finalContent.length === 0) return '';
const combinedContent = finalContent.join('\n\n---\n\n');
const limit = liveSettings.worldbookCharLimit;
if (combinedContent.length > limit) {
console.log(`[剧情优化大师] 世界书内容 (${combinedContent.length} chars) 超出限制 (${limit} chars),将被截断。`);
return combinedContent.substring(0, limit);
}
return combinedContent;
} catch (error) {
console.error(`[剧情优化大师] 处理世界书逻辑时出错:`, error);
return '';
}
}
export async function manageLorebookEntriesForChat() {
try {
const chatIdentifier = await getChatIdentifier();
if (!chatIdentifier || chatIdentifier.startsWith("unknown_chat")) {
console.error(`[Amily2-国史馆] 无法获取有效的聊天标识符,中止条目状态管理。`);
return;
}
const context = getContext();
if (!context || !context.characterId) {
console.log("[Amily2-国史馆] 未选择任何角色,跳过世界书管理。");
return;
}
const charLorebooks = await safeCharLorebooks({ type: 'all' });
const bookNames = [];
if (charLorebooks.primary) bookNames.push(charLorebooks.primary);
if (charLorebooks.additional?.length) bookNames.push(...charLorebooks.additional);
const dedicatedBookName = `${DEDICATED_LOREBOOK_NAME}-${chatIdentifier}`;
if (!bookNames.includes(dedicatedBookName)) {
bookNames.push(dedicatedBookName);
}
for (const bookName of bookNames) {
if (!world_names.includes(bookName)) continue;
const entries = await safeLorebookEntries(bookName);
const entriesToUpdate = [];
for (const entry of entries) {
if (entry.comment && entry.comment.startsWith(LOREBOOK_PREFIX)) {
const isForCurrentChat = entry.comment.includes(chatIdentifier);
if (isForCurrentChat && entry.disable) {
entriesToUpdate.push({ uid: entry.uid, enabled: true });
} else if (!isForCurrentChat && !entry.disable) {
entriesToUpdate.push({ uid: entry.uid, enabled: false });
}
}
}
if (entriesToUpdate.length > 0) {
const success = await safeUpdateLorebookEntries(bookName, entriesToUpdate);
if (success) {
console.log(`[Amily2-国史馆] 已为《${bookName}》更新了 ${entriesToUpdate.length} 个条目的状态以匹配当前聊天: ${chatIdentifier}`);
}
}
}
} catch (error) {
console.error("[Amily2-国史馆] 管理世界书条目状态时发生错误:", error);
}
}