From d4ec0aa201d00b5ef7a06cea2e1a5fb0d2be1aad Mon Sep 17 00:00:00 2001 From: Wx-2025 <351320169@qq.com> Date: Wed, 29 Oct 2025 21:22:18 +0800 Subject: [PATCH] Add files via upload --- core/commands.js | 222 ++++++- core/tavern-helper/Wrapperiframe.js | 36 ++ core/tavern-helper/iframe_client.js | 31 + core/tavern-helper/main.js | 792 ++++++++++++++++++------ core/tavern-helper/renderer-bindings.js | 51 ++ core/tavern-helper/renderer.html | 21 + core/tavern-helper/renderer.js | 601 ++++++++++++++++++ 7 files changed, 1580 insertions(+), 174 deletions(-) create mode 100644 core/tavern-helper/Wrapperiframe.js create mode 100644 core/tavern-helper/iframe_client.js create mode 100644 core/tavern-helper/renderer-bindings.js create mode 100644 core/tavern-helper/renderer.html create mode 100644 core/tavern-helper/renderer.js diff --git a/core/commands.js b/core/commands.js index 73b77ef..3f01098 100644 --- a/core/commands.js +++ b/core/commands.js @@ -1 +1,221 @@ -(function(_0x18689,_0x75b104){const _0x22eef5=_0x1e93,_0x211589=_0x18689();while(!![]){try{const _0x1fc4aa=parseInt(_0x22eef5(0x87))/0x1+-parseInt(_0x22eef5(0x9a))/0x2+parseInt(_0x22eef5(0xad))/0x3+parseInt(_0x22eef5(0x92))/0x4*(parseInt(_0x22eef5(0x95))/0x5)+-parseInt(_0x22eef5(0xa8))/0x6+parseInt(_0x22eef5(0xb3))/0x7*(-parseInt(_0x22eef5(0x8c))/0x8)+parseInt(_0x22eef5(0x9f))/0x9;if(_0x1fc4aa===_0x75b104)break;else _0x211589['push'](_0x211589['shift']());}catch(_0x155ea3){_0x211589['push'](_0x211589['shift']());}}}(_0x5a0e,0xd1ebd));import{getContext,extension_settings}from'/scripts/extensions.js';import{saveChatConditional,reloadCurrentChat}from'/script.js';import{extensionName}from'../utils/settings.js';import{SlashCommand}from'/scripts/slash-commands/SlashCommand.js';function _0x5a0e(){const _0x4d9310=['error','info','apiUrl','检测到问题,建议使用修复功能','需要至少2条消息才能测试','63libeyI','正在使用API检查回复...','正在使用API测试检测功能...','修复最新的AI回复中的问题','[Amily2]\x20致命错误:SlashCommand\x20或\x20SlashCommandParser\x20模块未能加载。','未检测到问题','mes','[Amily2-命令检查器]\x20已获取上下文消息:','正在检查并修复回复...','fix-reply','请先配置API\x20URL','测试结果:API未检测到问题,请检查API配置或提示词','592774fVyiKg','optimizedContent','...','最新消息是用户消息,无需检查','previousMessages','1233928GYQwMk','检查最新的AI回复是否有问题','[Amily2-新诏]\x20/fix-reply\x20命令已成功颁布。','[Amily2-命令检查器]\x20目标为用户消息,跳过。','contextMessages','[Amily2-命令检查器]\x20没有聊天记录。','180GJJBhk','[Amily2]\x20命令注册过程中发生意外错误:','message','184430ijBqSz','fromProps','命令检查器','addCommandObject','length','3144032mSqFRc','log','warning','未检测到需要修复的问题','check-reply','19929132pvOOTj','test-reply-checker','slice','success','chat','is_user','undefined','[Amily2-新诏]\x20/test-reply-checker\x20命令已成功颁布。','[Amily2-新诏]\x20/check-reply\x20命令已成功颁布。','5894136ufrcWW','没有找到可用于测试的AI消息','max','没有可修复的消息','测试聊天回复检查器功能','1006158NMrHEv'];_0x5a0e=function(){return _0x4d9310;};return _0x5a0e();}function _0x1e93(_0x594217,_0x4e8ad1){const _0x5a0e36=_0x5a0e();return _0x1e93=function(_0x1e9306,_0x1385fb){_0x1e9306=_0x1e9306-0x84;let _0x11a0cb=_0x5a0e36[_0x1e9306];return _0x11a0cb;},_0x1e93(_0x594217,_0x4e8ad1);}import{SlashCommandParser}from'/scripts/slash-commands/SlashCommandParser.js';import{checkAndFixWithAPI}from'./api.js';async function checkLatestMessage(){const _0x5426a2=_0x1e93,_0xd34af3=getContext(),_0x134d56=_0xd34af3[_0x5426a2(0xa3)]||[];if(!_0x134d56||_0x134d56['length']===0x0)return console[_0x5426a2(0x9b)](_0x5426a2(0x91)),{'message':null,'previousMessages':[]};const _0x6f9c91=_0x134d56[_0x134d56[_0x5426a2(0x99)]-0x1];console[_0x5426a2(0x9b)]('[Amily2-命令检查器]\x20正在侦测消息:',{'isUser':_0x6f9c91['is_user'],'messagePreview':_0x6f9c91['mes']?.['substring'](0x0,0x32)+_0x5426a2(0x89)});if(_0x6f9c91[_0x5426a2(0xa4)])return console[_0x5426a2(0x9b)](_0x5426a2(0x8f)),{'message':_0x6f9c91,'previousMessages':[]};const _0x2a6209=extension_settings[extensionName],_0xe8211a=_0x2a6209[_0x5426a2(0x90)]||0x2,_0x581bd8=Math[_0x5426a2(0xaa)](0x0,_0x134d56[_0x5426a2(0x99)]-_0xe8211a-0x1),_0x45c0ea=_0x134d56[_0x5426a2(0xa1)](_0x581bd8,_0x134d56[_0x5426a2(0x99)]-0x1);return console[_0x5426a2(0x9b)](_0x5426a2(0xba),{'count':_0x45c0ea[_0x5426a2(0x99)]}),{'message':_0x6f9c91,'previousMessages':_0x45c0ea};}async function checkCommand(){const _0xb1f058=_0x1e93,_0x391057=extension_settings[extensionName];if(!_0x391057['apiUrl'])return toastr[_0xb1f058(0xae)](_0xb1f058(0x85),'命令检查器'),'';const _0x52f2a0=await checkLatestMessage();if(!_0x52f2a0[_0xb1f058(0x94)]||_0x52f2a0[_0xb1f058(0x94)][_0xb1f058(0xa4)])return toastr[_0xb1f058(0xaf)](_0xb1f058(0x8a),_0xb1f058(0x97)),'';toastr['info'](_0xb1f058(0xb4),'命令检查器');const _0x50e800=await checkAndFixWithAPI(_0x52f2a0[_0xb1f058(0x94)],_0x52f2a0[_0xb1f058(0x8b)]);return _0x50e800&&_0x50e800[_0xb1f058(0x88)]&&_0x50e800[_0xb1f058(0x88)]!==_0x52f2a0['message'][_0xb1f058(0xb9)]?toastr[_0xb1f058(0x9c)](_0xb1f058(0xb1),_0xb1f058(0x97)):toastr[_0xb1f058(0xa2)](_0xb1f058(0xb8),_0xb1f058(0x97)),'';}export async function fixCommand(){const _0x34f052=_0x1e93,_0x2e8506=extension_settings[extensionName];if(!_0x2e8506[_0x34f052(0xb0)])return toastr[_0x34f052(0xae)](_0x34f052(0x85),_0x34f052(0x97)),'';const _0x419437=getContext(),_0x21071a=_0x419437[_0x34f052(0xa3)];if(!_0x21071a||_0x21071a[_0x34f052(0x99)]===0x0)return toastr[_0x34f052(0xaf)](_0x34f052(0xab),'命令检查器'),'';const _0x31d855=_0x21071a[_0x21071a['length']-0x1];if(_0x31d855[_0x34f052(0xa4)])return toastr[_0x34f052(0xaf)]('最新消息是用户消息,无需修复','命令检查器'),'';const _0x56bf1c=_0x2e8506['contextMessages']||0x2,_0x739a3a=Math[_0x34f052(0xaa)](0x0,_0x21071a[_0x34f052(0x99)]-0x1-_0x56bf1c),_0x5b8caa=_0x21071a[_0x34f052(0xa1)](_0x739a3a,_0x21071a[_0x34f052(0x99)]-0x1);toastr[_0x34f052(0xaf)](_0x34f052(0xbb),_0x34f052(0x97));const _0x2575e8=await checkAndFixWithAPI(_0x31d855,_0x5b8caa);return _0x2575e8&&_0x2575e8[_0x34f052(0x88)]&&_0x2575e8[_0x34f052(0x88)]!==_0x31d855[_0x34f052(0xb9)]?(_0x31d855[_0x34f052(0xb9)]=_0x2575e8[_0x34f052(0x88)],await saveChatConditional(),await reloadCurrentChat(),toastr[_0x34f052(0xa2)]('回复已修复',_0x34f052(0x97))):toastr[_0x34f052(0xaf)](_0x34f052(0x9d),'命令检查器'),'';}export async function testReplyChecker(){const _0x337447=_0x1e93,_0x4e0dc1=extension_settings[extensionName];if(!_0x4e0dc1[_0x337447(0xb0)])return toastr[_0x337447(0xae)]('请先配置API\x20URL',_0x337447(0x97)),'';const _0x55f607=getContext(),_0x83fffd=_0x55f607['chat'];if(!_0x83fffd||_0x83fffd[_0x337447(0x99)]<0x2)return toastr[_0x337447(0x9c)](_0x337447(0xb2),_0x337447(0x97)),'';let _0x53f0e1=null;for(let _0x129cbc=_0x83fffd[_0x337447(0x99)]-0x2;_0x129cbc>=0x0;_0x129cbc--){if(!_0x83fffd[_0x129cbc][_0x337447(0xa4)]){_0x53f0e1=_0x83fffd[_0x129cbc][_0x337447(0xb9)];break;}}if(!_0x53f0e1)return toastr['warning'](_0x337447(0xa9),_0x337447(0x97)),'';const _0x5c5890=_0x83fffd[_0x83fffd[_0x337447(0x99)]-0x1];if(_0x5c5890[_0x337447(0xa4)])return toastr[_0x337447(0x9c)]('最后一条消息是用户消息,无法测试',_0x337447(0x97)),'';const _0x492d95=_0x5c5890[_0x337447(0xb9)];_0x5c5890[_0x337447(0xb9)]=_0x53f0e1+'\x0a\x0a'+_0x53f0e1,toastr['info'](_0x337447(0xb5),'命令检查器');const _0x355121=_0x4e0dc1[_0x337447(0x90)]||0x2,_0x47f2a8=Math['max'](0x0,_0x83fffd[_0x337447(0x99)]-_0x355121-0x1),_0x57c82b=_0x83fffd[_0x337447(0xa1)](_0x47f2a8,_0x83fffd[_0x337447(0x99)]-0x1),_0x37e83d=await checkAndFixWithAPI(_0x5c5890,_0x57c82b);return _0x5c5890[_0x337447(0xb9)]=_0x492d95,_0x37e83d&&_0x37e83d[_0x337447(0x88)]&&_0x37e83d['optimizedContent']!==_0x53f0e1+'\x0a\x0a'+_0x53f0e1?toastr[_0x337447(0xa2)]('测试成功!API检测到重复内容并提供了修复建议','命令检查器'):toastr['warning'](_0x337447(0x86),_0x337447(0x97)),'';}export async function registerSlashCommands(){const _0x52e940=_0x1e93;try{if(typeof SlashCommand==='undefined'||typeof SlashCommandParser===_0x52e940(0xa5)){console[_0x52e940(0xae)](_0x52e940(0xb7));return;}SlashCommandParser[_0x52e940(0x98)](SlashCommand[_0x52e940(0x96)]({'name':_0x52e940(0x9e),'callback':checkCommand,'helpString':_0x52e940(0x8d)})),console['log'](_0x52e940(0xa7)),SlashCommandParser[_0x52e940(0x98)](SlashCommand[_0x52e940(0x96)]({'name':_0x52e940(0x84),'callback':fixCommand,'helpString':_0x52e940(0xb6)})),console[_0x52e940(0x9b)](_0x52e940(0x8e)),SlashCommandParser[_0x52e940(0x98)](SlashCommand[_0x52e940(0x96)]({'name':_0x52e940(0xa0),'callback':testReplyChecker,'helpString':_0x52e940(0xac)})),console[_0x52e940(0x9b)](_0x52e940(0xa6));}catch(_0x212a74){console[_0x52e940(0xae)](_0x52e940(0x93),_0x212a74);}} \ No newline at end of file +import { getContext, extension_settings } from "/scripts/extensions.js"; +import { saveChatConditional, reloadCurrentChat } from "/script.js"; +import { extensionName } from "../utils/settings.js"; +import { SlashCommand } from "/scripts/slash-commands/SlashCommand.js"; +import { SlashCommandParser } from "/scripts/slash-commands/SlashCommandParser.js"; +import { checkAndFixWithAPI } from "./api.js"; + +async function checkLatestMessage() { + const context = getContext(); + const chat = context.chat || []; + + if (!chat || chat.length === 0) { + console.log("[Amily2-命令检查器] 没有聊天记录。"); + return { message: null, previousMessages: [] }; + } + + const latestMessage = chat[chat.length - 1]; + + console.log("[Amily2-命令检查器] 正在侦测消息:", { + isUser: latestMessage.is_user, + messagePreview: latestMessage.mes?.substring(0, 50) + "...", + }); + + if (latestMessage.is_user) { + console.log("[Amily2-命令检查器] 目标为用户消息,跳过。"); + return { message: latestMessage, previousMessages: [] }; + } + + const settings = extension_settings[extensionName]; + const contextCount = settings.contextMessages || 2; + const startIndex = Math.max(0, chat.length - contextCount - 1); + const previousMessages = chat.slice(startIndex, chat.length - 1); + + console.log("[Amily2-命令检查器] 已获取上下文消息:", { + count: previousMessages.length, + }); + + return { message: latestMessage, previousMessages }; +} + +async function checkCommand() { + const settings = extension_settings[extensionName]; + if (!settings.apiUrl) { + toastr.error("请先配置API URL", "命令检查器"); + return ""; + } + const checkResult = await checkLatestMessage(); + if (!checkResult.message || checkResult.message.is_user) { + toastr.info("最新消息是用户消息,无需检查", "命令检查器"); + return ""; + } + toastr.info("正在使用API检查回复...", "命令检查器"); + const result = await checkAndFixWithAPI( + checkResult.message, + checkResult.previousMessages, + ); + if ( + result && + result.optimizedContent && + result.optimizedContent !== checkResult.message.mes + ) { + toastr.warning("检测到问题,建议使用修复功能", "命令检查器"); + } else { + toastr.success("未检测到问题", "命令检查器"); + } + return ""; +} + + +export async function fixCommand() { + const settings = extension_settings[extensionName]; + if (!settings.apiUrl) { + toastr.error("请先配置API URL", "命令检查器"); + return ""; + } + const context = getContext(); + const chat = context.chat; + if (!chat || chat.length === 0) { + toastr.info("没有可修复的消息", "命令检查器"); + return ""; + } + const latestMessage = chat[chat.length - 1]; + if (latestMessage.is_user) { + toastr.info("最新消息是用户消息,无需修复", "命令检查器"); + return ""; + } + const contextCount = settings.contextMessages || 2; + const startIndex = Math.max(0, chat.length - 1 - contextCount); + const previousMessages = chat.slice(startIndex, chat.length - 1); + toastr.info("正在检查并修复回复...", "命令检查器"); + const result = await checkAndFixWithAPI(latestMessage, previousMessages); + if ( + result && + result.optimizedContent && + result.optimizedContent !== latestMessage.mes + ) { + latestMessage.mes = result.optimizedContent; + await saveChatConditional(); + await reloadCurrentChat(); + toastr.success("回复已修复", "命令检查器"); + } else { + toastr.info("未检测到需要修复的问题", "命令检查器"); + } + return ""; +} + +export async function testReplyChecker() { + const settings = extension_settings[extensionName]; + if (!settings.apiUrl) { + toastr.error("请先配置API URL", "命令检查器"); + return ""; + } + const context = getContext(); + const chat = context.chat; + if (!chat || chat.length < 2) { + toastr.warning("需要至少2条消息才能测试", "命令检查器"); + return ""; + } + let testMessage = null; + for (let i = chat.length - 2; i >= 0; i--) { + if (!chat[i].is_user) { + testMessage = chat[i].mes; + break; + } + } + if (!testMessage) { + toastr.warning("没有找到可用于测试的AI消息", "命令检查器"); + return ""; + } + const lastMessage = chat[chat.length - 1]; + if (lastMessage.is_user) { + toastr.warning("最后一条消息是用户消息,无法测试", "命令检查器"); + return ""; + } + const originalMessage = lastMessage.mes; + lastMessage.mes = testMessage + "\n\n" + testMessage; + toastr.info("正在使用API测试检测功能...", "命令检查器"); + const contextCount = settings.contextMessages || 2; + const startIndex = Math.max(0, chat.length - contextCount - 1); + const previousMessages = chat.slice(startIndex, chat.length - 1); + const result = await checkAndFixWithAPI(lastMessage, previousMessages); + lastMessage.mes = originalMessage; + if ( + result && + result.optimizedContent && + result.optimizedContent !== testMessage + "\n\n" + testMessage + ) { + toastr.success("测试成功!API检测到重复内容并提供了修复建议", "命令检查器"); + } else { + toastr.warning( + "测试结果:API未检测到问题,请检查API配置或提示词", + "命令检查器", + ); + } + return ""; +} + +async function triggerSendButton() { + // 模拟点击发送按钮 + const sendButton = document.getElementById('send_but'); + if (sendButton) { + sendButton.click(); + console.log("[Amily2-触发器] 已触发发送按钮"); + return ""; + } else { + console.warn("[Amily2-触发器] 未找到发送按钮"); + toastr.warning("未找到发送按钮", "触发器"); + return ""; + } +} + +export async function registerSlashCommands() { + try { + if ( + typeof SlashCommand === "undefined" || + typeof SlashCommandParser === "undefined" + ) { + console.error( + "[Amily2] 致命错误:SlashCommand 或 SlashCommandParser 模块未能加载。", + ); + return; + } + SlashCommandParser.addCommandObject( + SlashCommand.fromProps({ + name: "check-reply", + callback: checkCommand, + helpString: "检查最新的AI回复是否有问题", + }), + ); + console.log("[Amily2-新诏] /check-reply 命令已成功颁布。"); + + SlashCommandParser.addCommandObject( + SlashCommand.fromProps({ + name: "fix-reply", + callback: fixCommand, + helpString: "修复最新的AI回复中的问题", + }), + ); + console.log("[Amily2-新诏] /fix-reply 命令已成功颁布。"); + + SlashCommandParser.addCommandObject( + SlashCommand.fromProps({ + name: "test-reply-checker", + callback: testReplyChecker, + helpString: "测试聊天回复检查器功能", + }), + ); + console.log("[Amily2-新诏] /test-reply-checker 命令已成功颁布。"); + + SlashCommandParser.addCommandObject( + SlashCommand.fromProps({ + name: "trigger", + callback: triggerSendButton, + helpString: "触发发送按钮 (用于自动发送消息)", + }), + ); + console.log("[Amily2-新诏] /trigger 命令已成功颁布。"); + } catch (e) { + console.error("[Amily2] 命令注册过程中发生意外错误:", e); + } +} diff --git a/core/tavern-helper/Wrapperiframe.js b/core/tavern-helper/Wrapperiframe.js new file mode 100644 index 0000000..bffad56 --- /dev/null +++ b/core/tavern-helper/Wrapperiframe.js @@ -0,0 +1,36 @@ +(function(){ + if (window.frameElement) { + window.frameElement.style.height = 'auto'; + } + function getGlobal() { + if (typeof self !== 'undefined') { return self; } + if (typeof window !== 'undefined') { return window; } + if (typeof global !== 'undefined') { return global; } + throw new Error('unable to locate global object'); + } + const globalScope = getGlobal(); + if (globalScope.generate_send_button_onclick) { + globalScope.generate_send_button_onclick_old = globalScope.generate_send_button_onclick; + globalScope.generate_send_button_onclick = function(event) { + try { + const textarea = document.getElementById('send_textarea'); + if (textarea && textarea.value) { + const customEvent = new CustomEvent('xb-send-message', { + detail: { + message: textarea.value, + event: event + }, + bubbles: true, + cancelable: true + }); + if (!window.dispatchEvent(customEvent)) { + return; + } + } + } catch (e) { + console.error('Error in xb-send-message event dispatch:', e); + } + globalScope.generate_send_button_onclick_old(event); + }; + } +})(); diff --git a/core/tavern-helper/iframe_client.js b/core/tavern-helper/iframe_client.js new file mode 100644 index 0000000..0fbca7d --- /dev/null +++ b/core/tavern-helper/iframe_client.js @@ -0,0 +1,31 @@ + +function initializeAmilyClient() { + console.log('[Amily2-IframeClient] 正在初始化...'); + + document.body.addEventListener('click', function(event) { + const target = event.target.closest('[data-amily-action]'); + + if (target) { + const action = target.dataset.amilyAction; + const detail = { ...target.dataset }; + + delete detail.amilyAction; + + console.log(`[Amily2-IframeClient] 触发动作: ${action}`, detail); + + if (window.AmilySimpleAPI && typeof window.AmilySimpleAPI.post === 'function') { + window.AmilySimpleAPI.post(action, detail); + } else { + console.error('[Amily2-IframeClient] AmilySimpleAPI 不可用。'); + } + } + }); + + console.log('[Amily2-IframeClient] 客户端脚本已加载并就绪。'); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeAmilyClient); +} else { + initializeAmilyClient(); +} diff --git a/core/tavern-helper/main.js b/core/tavern-helper/main.js index 22f5caf..a342b26 100644 --- a/core/tavern-helper/main.js +++ b/core/tavern-helper/main.js @@ -1,173 +1,619 @@ -import { - world_names, - loadWorldInfo, - saveWorldInfo, - createNewWorldInfo, - createWorldInfoEntry, - reloadEditor -} from "/scripts/world-info.js"; -import { characters, eventSource, event_types } from "/script.js"; -import { getContext } from "/scripts/extensions.js"; -import { executeSlashCommandsWithOptions } from '/scripts/slash-commands.js'; - - -class AmilyHelper { - - async getLorebooks() { - return [...world_names]; - } - - async getCharLorebooks(options = { type: 'all' }) { - try { - const context = getContext(); - if (!context || !context.characterId) { - console.warn('[Amily助手] 无法获取当前角色上下文。'); - return { primary: null, additional: [] }; - } - const character = characters[context.characterId]; - const primary = character?.data?.extensions?.world; - return { primary: primary || null, additional: [] }; - } catch (error) { - console.error('[Amily助手] 获取角色世界书时出错:', error); - return { primary: null, additional: [] }; - } - } - - async getLorebookEntries(bookName) { - try { - const bookData = await loadWorldInfo(bookName); - if (!bookData || !bookData.entries) { - return []; - } - const positionMap = { 0: 'before_character_definition', 1: 'after_character_definition', 2: 'before_author_note', 3: 'after_author_note', 4: 'at_depth_as_system' }; - return Object.entries(bookData.entries).map(([uid, entry]) => ({ - uid: parseInt(uid), - comment: entry.comment || '无标题条目', - content: entry.content || '', - key: entry.key || [], - keys: entry.key || [], - enabled: !entry.disable, - constant: entry.constant || false, - position: positionMap[entry.position] || 'at_depth_as_system', - depth: entry.depth || 998, - })); - } catch (error) { - console.error(`[Amily助手] 获取世界书《${bookName}》条目时出错:`, error); - return []; - } - } - - async setLorebookEntries(bookName, entries) { - try { - const bookData = await loadWorldInfo(bookName); - if (!bookData) { - console.error(`[Amily助手] 更新失败:找不到世界书《${bookName}》。`); - return false; - } - for (const entryUpdate of entries) { - const existingEntry = bookData.entries[entryUpdate.uid]; - if (existingEntry) { - if (entryUpdate.content !== undefined) existingEntry.content = entryUpdate.content; - if (entryUpdate.enabled !== undefined) existingEntry.disable = !entryUpdate.enabled; - if (entryUpdate.comment !== undefined) existingEntry.comment = entryUpdate.comment; - if (entryUpdate.key !== undefined) existingEntry.key = entryUpdate.key; - if (entryUpdate.keys !== undefined) existingEntry.key = entryUpdate.keys; - if (entryUpdate.constant !== undefined) existingEntry.constant = entryUpdate.constant; - if (entryUpdate.type === 'constant') existingEntry.constant = true; - if (entryUpdate.type === 'selective') existingEntry.constant = false; - if (entryUpdate.position !== undefined) { - const positionMap = { 'before_character_definition': 0, 'after_character_definition': 1, 'before_author_note': 2, 'after_author_note': 3, 'at_depth': 4, 'at_depth_as_system': 4 }; - existingEntry.position = positionMap[entryUpdate.position] ?? 4; - } - if (entryUpdate.depth !== undefined) existingEntry.depth = entryUpdate.depth; - } - } - await saveWorldInfo(bookName, bookData, true); - reloadEditor(bookName); // 刷新编辑器 - eventSource.emit(event_types.WORLD_INFO_UPDATED, bookName); - return true; - } catch (error) { - console.error(`[Amily助手] 更新世界书《${bookName}》条目时出错:`, error); - return false; - } - } - - async createLorebookEntries(bookName, entries) { - try { - let bookData = await loadWorldInfo(bookName); - if (!bookData) { - console.warn(`[Amily助手] 世界书《${bookName}》不存在,将自动创建。`); - await this.createLorebook(bookName); - bookData = await loadWorldInfo(bookName); - if (!bookData) { - throw new Error(`创建并加载世界书《${bookName}》失败。`); - } - } - - for (const newEntryData of entries) { - const newEntry = createWorldInfoEntry(bookName, bookData); - const positionMap = { 'before_character_definition': 0, 'after_character_definition': 1, 'before_author_note': 2, 'after_author_note': 3, 'at_depth': 4, 'at_depth_as_system': 4 }; - Object.assign(newEntry, { - comment: newEntryData.comment || '新条目', - content: newEntryData.content || '', - key: newEntryData.keys || newEntryData.key || [], - constant: newEntryData.type === 'constant' ? true : (newEntryData.constant || false), - position: typeof newEntryData.position === 'string' ? (positionMap[newEntryData.position] ?? 4) : (newEntryData.position ?? 4), - depth: newEntryData.depth ?? 998, - disable: !(newEntryData.enabled ?? true), - }); - if (newEntryData.type === 'selective') newEntry.constant = false; - } - await saveWorldInfo(bookName, bookData, true); - reloadEditor(bookName); // 刷新编辑器 - return true; - } catch (error) { - console.error(`[Amily助手] 在世界书《${bookName}》中创建新条目时出错:`, error); - return false; - } - } - - async createLorebook(bookName) { - try { - if (world_names.includes(bookName)) { - console.warn(`[Amily助手] 创建失败:世界书《${bookName}》已存在。`); - return false; - } - await createNewWorldInfo(bookName); - if (!world_names.includes(bookName)) { - world_names.push(bookName); - world_names.sort(); - } - // 派发一个自定义事件,通知UI更新 - document.dispatchEvent(new CustomEvent('amily-lorebook-created', { detail: { bookName } })); - return true; - } catch (error) { - console.error(`[Amily助手] 创建世界书《${bookName}》时出错:`, error); - return false; - } - } - - async triggerSlash(command) { - try { - console.log(`[Amily助手] 正在执行斜杠命令: ${command}`); - const result = await executeSlashCommandsWithOptions(command); - if (result.isError) { - throw new Error(result.errorMessage); - } - return result.pipe; - } catch (error) { - console.error(`[Amily助手] 执行斜杠命令 '${command}' 时出错:`, error); - throw error; - } - } - - async loadWorldInfo(bookName) { - return await loadWorldInfo(bookName); - } - - async saveWorldInfo(bookName, data, isWorldInfo) { - await saveWorldInfo(bookName, data, isWorldInfo); - } -} - -export const amilyHelper = new AmilyHelper(); +import { + world_names, + loadWorldInfo, + saveWorldInfo, + createNewWorldInfo, + createWorldInfoEntry, + reloadEditor +} from "/scripts/world-info.js"; +import { + characters, + eventSource, + event_types, + chat, + reloadCurrentChat, + saveChatConditional, + name1, + name2, + addOneMessage, + messageFormatting, + substituteParamsExtended +} from "/script.js"; +import { getContext } from "/scripts/extensions.js"; +import { executeSlashCommandsWithOptions } from '/scripts/slash-commands.js'; + + +class AmilyHelper { + + // ==================== Chat Message 相关方法 ==================== + + getChatMessages(range, options = {}) { + const { role = 'all', hide_state = 'all', include_swipes = false, include_swipe = false } = options; + const includeSwipes = include_swipes || include_swipe; + + if (!chat || !Array.isArray(chat)) { + throw new Error('聊天数组不可用'); + } + + let start, end; + const rangeStr = String(range); + + if (rangeStr.match(/^(-?\d+)$/)) { + const value = Number(rangeStr); + start = end = value < 0 ? chat.length + value : value; + } else { + const match = rangeStr.match(/^(-?\d+)-(-?\d+)$/); + if (!match) { + throw new Error(`无效的消息范围: ${range}`); + } + const [, s, e] = match; + const startVal = Number(s) < 0 ? chat.length + Number(s) : Number(s); + const endVal = Number(e) < 0 ? chat.length + Number(e) : Number(e); + start = Math.min(startVal, endVal); + end = Math.max(startVal, endVal); + } + + if (start < 0 || end >= chat.length || start > end) { + throw new Error(`消息范围超出界限: ${range}`); + } + + const getRole = (msg) => { + if (msg.is_system) return 'system'; + return msg.is_user ? 'user' : 'assistant'; + }; + + const messages = []; + for (let i = start; i <= end; i++) { + const msg = chat[i]; + if (!msg) continue; + + const msgRole = getRole(msg); + + if (role !== 'all' && msgRole !== role) continue; + + if (hide_state !== 'all') { + if ((hide_state === 'hidden') !== msg.is_system) continue; + } + + const swipe_id = msg.swipe_id ?? 0; + const swipes = msg.swipes ?? [msg.mes]; + const swipes_data = msg.variables ?? [{}]; + const swipes_info = msg.swipes_info ?? [msg.extra ?? {}]; + + if (includeSwipes) { + messages.push({ + message_id: i, + name: msg.name, + role: msgRole, + is_hidden: msg.is_system, + swipe_id: swipe_id, + swipes: swipes, + swipes_data: swipes_data, + swipes_info: swipes_info + }); + } else { + messages.push({ + message_id: i, + name: msg.name, + role: msgRole, + is_hidden: msg.is_system, + message: msg.mes, + data: swipes_data[swipe_id], + extra: swipes_info[swipe_id] + }); + } + } + + return messages; + } + + async setChatMessages(chat_messages, options = {}) { + const { refresh = 'affected' } = options; + + if (!Array.isArray(chat_messages)) { + throw new Error('chat_messages 必须是数组'); + } + + for (const chatMsg of chat_messages) { + const msg = chat[chatMsg.message_id]; + if (!msg) continue; + + if (chatMsg.name !== undefined) msg.name = chatMsg.name; + if (chatMsg.role !== undefined) msg.is_user = chatMsg.role === 'user'; + if (chatMsg.is_hidden !== undefined) msg.is_system = chatMsg.is_hidden; + + if (chatMsg.message !== undefined) { + msg.mes = chatMsg.message; + if (msg.swipes) { + msg.swipes[msg.swipe_id ?? 0] = chatMsg.message; + } + } + + if (chatMsg.data !== undefined) { + if (!msg.variables) { + msg.variables = Array(msg.swipes?.length ?? 1).fill({}); + } + msg.variables[msg.swipe_id ?? 0] = chatMsg.data; + } + + if (chatMsg.extra !== undefined) { + if (!msg.swipes_info) { + msg.swipes_info = Array(msg.swipes?.length ?? 1).fill({}); + } + msg.extra = chatMsg.extra; + msg.swipes_info[msg.swipe_id ?? 0] = chatMsg.extra; + } + } + + await saveChatConditional(); + + if (refresh === 'all') { + await reloadCurrentChat(); + } else if (refresh === 'affected') { + for (const chatMsg of chat_messages) { + const $mes = $(`div.mes[mesid="${chatMsg.message_id}"]`); + if ($mes.length) { + const msg = chat[chatMsg.message_id]; + $mes.find('.mes_text').empty().append( + messageFormatting(msg.mes, msg.name, msg.is_system, msg.is_user, chatMsg.message_id) + ); + } + } + } + + console.log(`[Amily助手] 已修改消息: ${chat_messages.map(m => m.message_id).join(', ')}`); + } + + + async setChatMessage(field_values, message_id, { + swipe_id = 'current', + refresh = 'display_and_render_current' + } = {}) { + field_values = typeof field_values === 'string' ? { message: field_values } : field_values; + + if (typeof swipe_id !== 'number' && swipe_id !== 'current') { + throw new Error(`提供的 swipe_id 无效, 请提供 'current' 或序号, 你提供的是: ${swipe_id}`); + } + if (!['none', 'display_current', 'display_and_render_current', 'all'].includes(refresh)) { + throw new Error( + `提供的 refresh 无效, 请提供 'none', 'display_current', 'display_and_render_current' 或 'all', 你提供的是: ${refresh}` + ); + } + + const chat_message = chat[message_id]; + if (!chat_message) { + console.warn(`[Amily助手] 未找到第 ${message_id} 楼的消息`); + return; + } + + const add_swipes_if_required = () => { + if (swipe_id === 'current') { + return false; + } + + if (swipe_id == 0 || (chat_message.swipes && swipe_id < chat_message.swipes.length)) { + return true; + } + + if (!chat_message.swipes) { + chat_message.swipe_id = 0; + chat_message.swipes = [chat_message.mes]; + chat_message.variables = [{}]; + } + for (let i = chat_message.swipes.length; i <= swipe_id; ++i) { + chat_message.swipes.push(''); + chat_message.variables.push({}); + } + return true; + }; + + const swipe_id_previous_index = chat_message.swipe_id ?? 0; + const swipe_id_to_set_index = swipe_id == 'current' ? swipe_id_previous_index : swipe_id; + const swipe_id_to_use_index = refresh != 'none' ? swipe_id_to_set_index : swipe_id_previous_index; + const message = field_values.message ?? + (chat_message.swipes ? chat_message.swipes[swipe_id_to_set_index] : undefined) ?? + chat_message.mes; + + const update_chat_message = () => { + const message_demacroed = substituteParamsExtended(message); + + if (field_values.data) { + if (!chat_message.variables) { + chat_message.variables = []; + } + chat_message.variables[swipe_id_to_set_index] = field_values.data; + } + + if (chat_message.swipes) { + chat_message.swipes[swipe_id_to_set_index] = message_demacroed; + chat_message.swipe_id = swipe_id_to_use_index; + } + + if (swipe_id_to_use_index === swipe_id_to_set_index) { + chat_message.mes = message_demacroed; + } + }; + + const update_partial_html = async (should_update_swipe) => { + const mes_html = $(`div.mes[mesid="${message_id}"]`); + if (!mes_html.length) { + return; + } + + if (should_update_swipe) { + mes_html.find('.swipes-counter').text(`${swipe_id_to_use_index + 1}\u200b/\u200b${chat_message.swipes.length}`); + } + + if (refresh != 'none') { + mes_html + .find('.mes_text') + .empty() + .append( + messageFormatting(message, chat_message.name, chat_message.is_system, chat_message.is_user, message_id) + ); + if (refresh === 'display_and_render_current') { + await eventSource.emit( + chat_message.is_user ? event_types.USER_MESSAGE_RENDERED : event_types.CHARACTER_MESSAGE_RENDERED, + message_id + ); + } + } + }; + + const should_update_swipe = add_swipes_if_required(); + update_chat_message(); + await saveChatConditional(); + + if (refresh == 'all') { + await reloadCurrentChat(); + } else { + await update_partial_html(should_update_swipe); + } + + console.log( + `[Amily助手] 设置第 ${message_id} 楼消息, 选项: ${JSON.stringify({ + swipe_id, + refresh, + })}, 设置前使用的消息页: ${swipe_id_previous_index}, 设置的消息页: ${swipe_id_to_set_index}, 现在使用的消息页: ${swipe_id_to_use_index}` + ); + } + + + async createChatMessages(chat_messages, options = {}) { + const { insert_at = 'end', refresh = 'all' } = options; + + let insertIndex = insert_at; + if (insert_at !== 'end') { + insertIndex = insert_at < 0 ? chat.length + insert_at : insert_at; + if (insertIndex < 0 || insertIndex > chat.length) { + throw new Error(`无效的插入位置: ${insert_at}`); + } + } + + const newMessages = chat_messages.map(msg => ({ + name: msg.name ?? (msg.role === 'user' ? name1 : name2), + is_user: msg.role === 'user', + is_system: msg.is_hidden ?? false, + mes: msg.message, + variables: [msg.data ?? {}] + })); + + if (insertIndex === 'end') { + chat.push(...newMessages); + } else { + chat.splice(insertIndex, 0, ...newMessages); + } + + await saveChatConditional(); + + if (refresh === 'affected' && insertIndex === 'end') { + newMessages.forEach(msg => addOneMessage(msg)); + } else if (refresh === 'all') { + await reloadCurrentChat(); + } + + console.log(`[Amily助手] 已创建 ${chat_messages.length} 条消息`); + } + + async deleteChatMessages(message_ids, options = {}) { + const { refresh = 'all' } = options; + + const validIds = message_ids + .map(id => id < 0 ? chat.length + id : id) + .filter(id => id >= 0 && id < chat.length) + .sort((a, b) => b - a); // 从后往前删除 + + for (const id of validIds) { + chat.splice(id, 1); + } + + await saveChatConditional(); + + if (refresh === 'all') { + await reloadCurrentChat(); + } + + console.log(`[Amily助手] 已删除消息: ${validIds.join(', ')}`); + } + + async getLorebooks() { + return [...world_names]; + } + + async getCharLorebooks(options = { type: 'all' }) { + try { + const context = getContext(); + if (!context || context.characterId === undefined) { + console.warn('[Amily助手] 无法获取当前角色上下文'); + return { primary: null, additional: [] }; + } + const character = characters[context.characterId]; + const primary = character?.data?.extensions?.world; + return { primary: primary || null, additional: [] }; + } catch (error) { + console.error('[Amily助手] 获取角色世界书时出错:', error); + return { primary: null, additional: [] }; + } + } + + async getLorebookEntries(bookName) { + try { + const bookData = await loadWorldInfo(bookName); + if (!bookData || !bookData.entries) { + return []; + } + const positionMap = { + 0: 'before_character_definition', + 1: 'after_character_definition', + 2: 'before_author_note', + 3: 'after_author_note', + 4: 'at_depth_as_system' + }; + return Object.entries(bookData.entries).map(([uid, entry]) => ({ + uid: parseInt(uid), + comment: entry.comment || '无标题条目', + content: entry.content || '', + key: entry.key || [], + keys: entry.key || [], + enabled: !entry.disable, + constant: entry.constant || false, + position: positionMap[entry.position] || 'at_depth_as_system', + depth: entry.depth || 998, + })); + } catch (error) { + console.error(`[Amily助手] 获取世界书《${bookName}》条目时出错:`, error); + return []; + } + } + + async setLorebookEntries(bookName, entries) { + try { + const bookData = await loadWorldInfo(bookName); + if (!bookData) { + console.error(`[Amily助手] 更新失败:找不到世界书《${bookName}》`); + return false; + } + for (const entryUpdate of entries) { + const existingEntry = bookData.entries[entryUpdate.uid]; + if (existingEntry) { + if (entryUpdate.content !== undefined) existingEntry.content = entryUpdate.content; + if (entryUpdate.enabled !== undefined) existingEntry.disable = !entryUpdate.enabled; + if (entryUpdate.comment !== undefined) existingEntry.comment = entryUpdate.comment; + if (entryUpdate.key !== undefined) existingEntry.key = entryUpdate.key; + if (entryUpdate.keys !== undefined) existingEntry.key = entryUpdate.keys; + if (entryUpdate.constant !== undefined) existingEntry.constant = entryUpdate.constant; + if (entryUpdate.type === 'constant') existingEntry.constant = true; + if (entryUpdate.type === 'selective') existingEntry.constant = false; + if (entryUpdate.position !== undefined) { + const positionMap = { + 'before_character_definition': 0, + 'after_character_definition': 1, + 'before_author_note': 2, + 'after_author_note': 3, + 'at_depth': 4, + 'at_depth_as_system': 4 + }; + existingEntry.position = positionMap[entryUpdate.position] ?? 4; + } + if (entryUpdate.depth !== undefined) existingEntry.depth = entryUpdate.depth; + } + } + await saveWorldInfo(bookName, bookData, true); + reloadEditor(bookName); + eventSource.emit(event_types.WORLD_INFO_UPDATED, bookName); + return true; + } catch (error) { + console.error(`[Amily助手] 更新世界书《${bookName}》条目时出错:`, error); + return false; + } + } + + async createLorebookEntries(bookName, entries) { + try { + let bookData = await loadWorldInfo(bookName); + if (!bookData) { + console.warn(`[Amily助手] 世界书《${bookName}》不存在,将自动创建`); + await this.createLorebook(bookName); + bookData = await loadWorldInfo(bookName); + if (!bookData) { + throw new Error(`创建并加载世界书《${bookName}》失败`); + } + } + + for (const newEntryData of entries) { + const newEntry = createWorldInfoEntry(bookName, bookData); + const positionMap = { + 'before_character_definition': 0, + 'after_character_definition': 1, + 'before_author_note': 2, + 'after_author_note': 3, + 'at_depth': 4, + 'at_depth_as_system': 4 + }; + Object.assign(newEntry, { + comment: newEntryData.comment || '新条目', + content: newEntryData.content || '', + key: newEntryData.keys || newEntryData.key || [], + constant: newEntryData.type === 'constant' ? true : (newEntryData.constant || false), + position: typeof newEntryData.position === 'string' ? (positionMap[newEntryData.position] ?? 4) : (newEntryData.position ?? 4), + depth: newEntryData.depth ?? 998, + disable: !(newEntryData.enabled ?? true), + }); + if (newEntryData.type === 'selective') newEntry.constant = false; + } + await saveWorldInfo(bookName, bookData, true); + reloadEditor(bookName); + return true; + } catch (error) { + console.error(`[Amily助手] 在世界书《${bookName}》中创建新条目时出错:`, error); + return false; + } + } + + async createLorebook(bookName) { + try { + if (world_names.includes(bookName)) { + console.warn(`[Amily助手] 创建失败:世界书《${bookName}》已存在`); + return false; + } + await createNewWorldInfo(bookName); + if (!world_names.includes(bookName)) { + world_names.push(bookName); + world_names.sort(); + } + document.dispatchEvent(new CustomEvent('amily-lorebook-created', { detail: { bookName } })); + return true; + } catch (error) { + console.error(`[Amily助手] 创建世界书《${bookName}》时出错:`, error); + return false; + } + } + + // ==================== 斜杠命令相关 ==================== + + async triggerSlash(command) { + try { + console.log(`[Amily助手] 正在执行斜杠命令: ${command}`); + const result = await executeSlashCommandsWithOptions(command); + if (result.isError) { + throw new Error(result.errorMessage); + } + return result.pipe; + } catch (error) { + console.error(`[Amily助手] 执行斜杠命令 '${command}' 时出错:`, error); + throw error; + } + } + + // ==================== 工具方法 ==================== + + async loadWorldInfo(bookName) { + return await loadWorldInfo(bookName); + } + + async saveWorldInfo(bookName, data, isWorldInfo) { + await saveWorldInfo(bookName, data, isWorldInfo); + } + + getLastMessageId() { + return chat.length - 1; + } +} + +export const amilyHelper = new AmilyHelper(); + + +export function initializeAmilyHelper() { + if (!window.AmilyHelper) { + window.AmilyHelper = amilyHelper; + console.log('[Amily2] AmilyHelper 已成功初始化并附加到 window 对象'); + } +} + +// ==================== iframe 通信 API ==================== + + +export function makeRequest(request, data) { + return new Promise((resolve, reject) => { + const uid = Date.now() + Math.random(); + const callbackRequest = `${request}_callback`; + + function handleMessage(event) { + const msgData = event.data || {}; + if (msgData.request === callbackRequest && msgData.uid === uid) { + window.removeEventListener('message', handleMessage); + if (msgData.error) { + reject(new Error(msgData.error)); + } else { + resolve(msgData.result); + } + } + } + + window.addEventListener('message', handleMessage); + + setTimeout(() => { + window.removeEventListener('message', handleMessage); + reject(new Error(`请求 '${request}' 超时 (30秒)`)); + }, 30000); + + window.parent.postMessage({ + source: 'amily2-iframe-request', + request: request, + uid: uid, + data: data + }, '*'); + }); +} + +// ==================== 主窗口 API ==================== + +const apiHandlers = new Map(); + + +export function registerApiHandler(request, handler) { + if (apiHandlers.has(request)) { + console.warn(`[Amily2-IframeAPI] 覆盖请求处理器: ${request}`); + } + apiHandlers.set(request, handler); +} + + +export function initializeApiListener() { + window.addEventListener('message', async (event) => { + const data = event.data || {}; + if (data.source !== 'amily2-iframe-request' || !data.request || data.uid === undefined) { + return; + } + + const handler = apiHandlers.get(data.request); + const callbackRequest = `${data.request}_callback`; + + if (!handler) { + console.error(`[Amily2-IframeAPI] 收到未知请求: ${data.request}`); + event.source.postMessage({ + request: callbackRequest, + uid: data.uid, + error: `未注册请求 '${data.request}' 的处理器` + }, '*'); + return; + } + + try { + const result = await handler(data.data, event); + event.source.postMessage({ + request: callbackRequest, + uid: data.uid, + result: result + }, '*'); + } catch (error) { + console.error(`[Amily2-IframeAPI] 执行处理器 '${data.request}' 时出错:`, error); + event.source.postMessage({ + request: callbackRequest, + uid: data.uid, + error: error.message || String(error) + }, '*'); + } + }); + console.log('[Amily2-IframeAPI] 主窗口监听器已初始化'); +} diff --git a/core/tavern-helper/renderer-bindings.js b/core/tavern-helper/renderer-bindings.js new file mode 100644 index 0000000..02b1e50 --- /dev/null +++ b/core/tavern-helper/renderer-bindings.js @@ -0,0 +1,51 @@ +import { renderAllIframes, clearAllIframes, initializeRenderer } from './renderer.js'; +import { extension_settings } from "/scripts/extensions.js"; +import { extensionName } from "../../utils/settings.js"; +import { saveSettingsDebounced } from "/script.js"; + +let isRendererInitialized = false; + +export function initializeRendererBindings() { + const container = $("#amily2_drawer_content").length + ? $("#amily2_drawer_content") + : $("#amily2_chat_optimiser"); + + if (!container.length) { + console.warn("[Amily2-Renderer] Could not find the settings container."); + return; + } + container.on('change', '#render-enable-toggle', function() { + const isChecked = this.checked; + + if (!extension_settings[extensionName]) { + extension_settings[extensionName] = {}; + } + extension_settings[extensionName].render_enabled = isChecked; + saveSettingsDebounced(); + + if (isChecked && !isRendererInitialized) { + initializeRenderer(); + isRendererInitialized = true; + console.log("[Amily2-Renderer] Renderer has been initialized on-demand."); + } + + if (isChecked) { + renderAllIframes(); + } else { + clearAllIframes(); + } + }); + + container.on('change', '#render-depth', function() { + const depth = parseInt(this.value, 10); + if (!extension_settings[extensionName]) { + extension_settings[extensionName] = {}; + } + extension_settings[extensionName].render_depth = depth; + saveSettingsDebounced(); + + toastr.success(`渲染深度已保存为: ${depth}`); + }); + + console.log("[Amily2-Renderer] Renderer UI events have been successfully bound."); +} diff --git a/core/tavern-helper/renderer.html b/core/tavern-helper/renderer.html new file mode 100644 index 0000000..96540fa --- /dev/null +++ b/core/tavern-helper/renderer.html @@ -0,0 +1,21 @@ +
“想给温柔的人奏响一段温柔的小插曲。”
+
+ 当开启Amily前端渲染后,务必关闭酒馆助手的前端渲染,借鉴了酒馆助手的渲染和交互逻辑,实现了更加轻量级,渲染更快,降低卡顿。
+
+ 与酒馆助手的脚本、变量等功能,完全无冲突,可并存使用。
+