import { world_names, loadWorldInfo, saveWorldInfo, createNewWorldInfo, createWorldInfoEntry } from "/scripts/world-info.js"; let reloadEditor = () => { console.warn("[Amily助手] reloadEditor 函数不可用,可能是旧版本。已使用空函数代替。"); }; (async () => { try { const { reloadEditor: importedReloadEditor } = await import("/scripts/world-info.js"); if (importedReloadEditor) { reloadEditor = importedReloadEditor; console.log("[Amily助手] 已成功动态导入 reloadEditor。"); } } catch (error) { console.warn("[Amily助手] 动态导入 reloadEditor 失败,将使用空函数。错误信息:", error.message); } })(); import { characters, eventSource, event_types, chat, reloadCurrentChat, saveChatConditional, name1, name2, addOneMessage, messageFormatting, substituteParamsExtended, saveCharacterDebounced, this_chid } 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; if (entryUpdate.scanDepth !== undefined) existingEntry.scanDepth = entryUpdate.scanDepth; if (entryUpdate.order !== undefined) existingEntry.order = entryUpdate.order; if (entryUpdate.exclude_recursion !== undefined) existingEntry.excludeRecursion = entryUpdate.exclude_recursion; if (entryUpdate.prevent_recursion !== undefined) existingEntry.preventRecursion = entryUpdate.prevent_recursion; } } 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, scanDepth: newEntryData.scanDepth ?? null, disable: !(newEntryData.enabled ?? true), excludeRecursion: newEntryData.excludeRecursion ?? newEntryData.exclude_recursion ?? false, preventRecursion: newEntryData.preventRecursion ?? newEntryData.prevent_recursion ?? false, }); 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 deleteLorebookEntries(bookName, uids) { try { const bookData = await loadWorldInfo(bookName); if (!bookData || !bookData.entries) { return false; } let deletedCount = 0; for (const uid of uids) { if (bookData.entries[uid]) { delete bookData.entries[uid]; deletedCount++; } } if (deletedCount > 0) { await saveWorldInfo(bookName, bookData, true); reloadEditor(bookName); console.log(`[Amily助手] 已从世界书《${bookName}》删除 ${deletedCount} 个条目`); return true; } return false; } 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; } /** * 将指定世界书绑定到当前角色 * @param {string} bookName 世界书名称 */ async bindLorebookToCharacter(bookName) { if (this_chid === undefined || !characters[this_chid]) { console.warn('[Amily助手] 无法绑定世界书:未选中角色'); return false; } const char = characters[this_chid]; if (!char.data) char.data = {}; if (!char.data.extensions) char.data.extensions = {}; // 确保 world 字段是数组 let worlds = char.data.extensions.world; if (!Array.isArray(worlds)) { worlds = worlds ? [worlds] : []; } if (!worlds.includes(bookName)) { worlds.push(bookName); char.data.extensions.world = worlds; console.log(`[Amily助手] 已将世界书《${bookName}》绑定到角色 ${char.name}`); if (typeof saveCharacterDebounced === 'function') { saveCharacterDebounced(); return true; } else { console.warn('[Amily助手] 无法保存角色数据:saveCharacterDebounced 不可用'); return false; } } return true; // 已经绑定 } } 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); const targetOrigin = window.location.origin === 'null' ? '*' : window.location.origin; window.parent.postMessage({ source: 'amily2-iframe-request', request: request, uid: uid, data: data }, targetOrigin); }); } // ==================== 主窗口 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) => { if (window.location.origin !== 'null' && event.origin !== window.location.origin) { console.warn(`[Amily2-IframeAPI] 拒绝来自未知来源的请求: ${event.origin}`); return; } 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`; const targetOrigin = event.origin === 'null' ? '*' : event.origin; if (!handler) { console.error(`[Amily2-IframeAPI] 收到未知请求: ${data.request}`); event.source.postMessage({ request: callbackRequest, uid: data.uid, error: `未注册请求 '${data.request}' 的处理器` }, targetOrigin); return; } try { const result = await handler(data.data, event); event.source.postMessage({ request: callbackRequest, uid: data.uid, result: result }, targetOrigin); } catch (error) { console.error(`[Amily2-IframeAPI] 执行处理器 '${data.request}' 时出错:`, error); event.source.postMessage({ request: callbackRequest, uid: data.uid, error: error.message || String(error) }, targetOrigin); } }); console.log('[Amily2-IframeAPI] 主窗口监听器已初始化 (已启用安全验证)'); } // ── Bus 注册 ────────────────────────────────────────────────────────────── // 注册名:'TavernHelper' // 暴露 amilyHelper 的全部公开方法,供其他模块通过 Bus query 访问, // 替代各处的直接 import { amilyHelper } from '...tavern-helper/main.js'。 setTimeout(() => { try { const _ctx = window.Amily2Bus?.register('TavernHelper'); if (!_ctx) { console.warn('[TavernHelper] Amily2Bus 尚未就绪,服务注册跳过。'); return; } _ctx.expose({ // Chat 消息操作 getChatMessages: (...a) => amilyHelper.getChatMessages(...a), setChatMessages: (...a) => amilyHelper.setChatMessages(...a), setChatMessage: (...a) => amilyHelper.setChatMessage(...a), createChatMessages: (...a) => amilyHelper.createChatMessages(...a), deleteChatMessages: (...a) => amilyHelper.deleteChatMessages(...a), getLastMessageId: (...a) => amilyHelper.getLastMessageId(...a), // 世界书 / Lorebook 操作 getLorebooks: (...a) => amilyHelper.getLorebooks(...a), getCharLorebooks: (...a) => amilyHelper.getCharLorebooks(...a), getLorebookEntries: (...a) => amilyHelper.getLorebookEntries(...a), setLorebookEntries: (...a) => amilyHelper.setLorebookEntries(...a), createLorebookEntries: (...a) => amilyHelper.createLorebookEntries(...a), deleteLorebookEntries: (...a) => amilyHelper.deleteLorebookEntries(...a), createLorebook: (...a) => amilyHelper.createLorebook(...a), loadWorldInfo: (...a) => amilyHelper.loadWorldInfo(...a), saveWorldInfo: (...a) => amilyHelper.saveWorldInfo(...a), bindLorebookToCharacter: (...a) => amilyHelper.bindLorebookToCharacter(...a), // 其他 triggerSlash: (...a) => amilyHelper.triggerSlash(...a), }); _ctx.log('TavernHelper', 'info', 'TavernHelper 服务已注册到 Bus。'); } catch (e) { console.error('[TavernHelper] Bus 注册失败:', e); } }, 0);