mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 11:15:50 +00:00
Add files via upload
This commit is contained in:
222
core/commands.js
222
core/commands.js
File diff suppressed because one or more lines are too long
36
core/tavern-helper/Wrapperiframe.js
Normal file
36
core/tavern-helper/Wrapperiframe.js
Normal file
@@ -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);
|
||||
};
|
||||
}
|
||||
})();
|
||||
31
core/tavern-helper/iframe_client.js
Normal file
31
core/tavern-helper/iframe_client.js
Normal file
@@ -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();
|
||||
}
|
||||
@@ -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] 主窗口监听器已初始化');
|
||||
}
|
||||
|
||||
51
core/tavern-helper/renderer-bindings.js
Normal file
51
core/tavern-helper/renderer-bindings.js
Normal file
@@ -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.");
|
||||
}
|
||||
21
core/tavern-helper/renderer.html
Normal file
21
core/tavern-helper/renderer.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<div class="flex-container">
|
||||
<button id="amily2_renderer_back_button" class="menu_button wide_button"><i class="fas fa-arrow-left"></i> 返回主殿</button>
|
||||
</div>
|
||||
<div class="extension-content-item">
|
||||
<div class="name">启用前端渲染</div>
|
||||
<div class="description">在聊天消息中渲染HTML内容。</div>
|
||||
<input id="render-enable-toggle" type="checkbox" class="slider">
|
||||
</div>
|
||||
<div class="extension-content-item">
|
||||
<div class="name">渲染深度</div>
|
||||
<div class="description">设置要渲染的最新消息的数量。0表示无限制。</div>
|
||||
<input id="render-depth" type="number" class="text_pole" value="5">
|
||||
</div>
|
||||
<div class="amily2-renderer-info-container">
|
||||
<p class="emo-statement">“想给温柔的人奏响一段温柔的小插曲。”</p>
|
||||
<p class="description-text">
|
||||
当开启Amily前端渲染后,务必关闭酒馆助手的前端渲染,借鉴了酒馆助手的渲染和交互逻辑,实现了更加轻量级,渲染更快,降低卡顿。
|
||||
<br><br>
|
||||
与酒馆助手的脚本、变量等功能,完全无冲突,可并存使用。
|
||||
</p>
|
||||
</div>
|
||||
601
core/tavern-helper/renderer.js
Normal file
601
core/tavern-helper/renderer.js
Normal file
@@ -0,0 +1,601 @@
|
||||
import { eventSource, event_types } from '/script.js';
|
||||
import { extension_settings } from '/scripts/extensions.js';
|
||||
import { extensionName } from '../../utils/settings.js';
|
||||
|
||||
const settings = {
|
||||
sandboxMode: false,
|
||||
useBlob: false,
|
||||
wrapperIframe: true,
|
||||
renderEnabled: true
|
||||
};
|
||||
|
||||
const winMap = new Map();
|
||||
let lastHeights = new WeakMap();
|
||||
const blobUrls = new WeakMap();
|
||||
const hashToBlobUrl = new Map();
|
||||
const blobLRU = [];
|
||||
const BLOB_CACHE_LIMIT = 32;
|
||||
|
||||
function generateUniqueId() {
|
||||
return `amily2-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
}
|
||||
|
||||
function shouldRenderContentByBlock(codeBlock) {
|
||||
if (!codeBlock) return false;
|
||||
const content = (codeBlock.textContent || '').trim();
|
||||
if (!content) return false;
|
||||
return /^\s*<!doctype html/i.test(content) || /^\s*<html/i.test(content) || /<script/i.test(content);
|
||||
}
|
||||
|
||||
function djb2(str) {
|
||||
let h = 5381;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
h = ((h << 5) + h) ^ str.charCodeAt(i);
|
||||
}
|
||||
return (h >>> 0).toString(16);
|
||||
}
|
||||
|
||||
function buildResourceHints(html) {
|
||||
const urls = Array.from(new Set((html.match(/https?:\/\/[^"'()\s]+/gi) || []).map(u => { try { return new URL(u).origin } catch { return null } }).filter(Boolean)));
|
||||
let hints = "";
|
||||
const maxHosts = 6;
|
||||
for (let i = 0; i < Math.min(urls.length, maxHosts); i++) {
|
||||
const origin = urls[i];
|
||||
hints += `<link rel="dns-prefetch" href="${origin}">`;
|
||||
hints += `<link rel="preconnect" href="${origin}" crossorigin>`;
|
||||
}
|
||||
let preload = "";
|
||||
const font = (html.match(/https?:\/\/[^"'()\s]+\.(?:woff2|woff|ttf|otf)/i) || [])[0];
|
||||
if (font) {
|
||||
const type = font.endsWith(".woff2") ? "font/woff2" : font.endsWith(".woff") ? "font/woff" : font.endsWith(".ttf") ? "font/ttf" : "font/otf";
|
||||
preload += `<link rel="preload" as="font" href="${font}" type="${type}" crossorigin fetchpriority="high">`;
|
||||
}
|
||||
const css = (html.match(/https?:\/\/[^"'()\s]+\.css/i) || [])[0];
|
||||
if (css) {
|
||||
preload += `<link rel="preload" as="style" href="${css}" crossorigin fetchpriority="high">`;
|
||||
}
|
||||
const img = (html.match(/https?:\/\/[^"'()\s]+\.(?:png|jpg|jpeg|webp|gif|svg)/i) || [])[0];
|
||||
if (img) {
|
||||
preload += `<link rel="preload" as="image" href="${img}" crossorigin fetchpriority="high">`;
|
||||
}
|
||||
return hints + preload;
|
||||
}
|
||||
|
||||
function iframeClientScript() {
|
||||
return `
|
||||
(function(){
|
||||
function measureVisibleHeight(){
|
||||
try{
|
||||
var doc = document;
|
||||
var target = doc.querySelector('.calendar-wrapper') || doc.body;
|
||||
if(!target) return 0;
|
||||
var minTop = Infinity, maxBottom = 0;
|
||||
var addRect = function(el){
|
||||
try{
|
||||
var r = el.getBoundingClientRect();
|
||||
if(r && r.height > 0){
|
||||
if(minTop > r.top) minTop = r.top;
|
||||
if(maxBottom < r.bottom) maxBottom = r.bottom;
|
||||
}
|
||||
}catch(e){}
|
||||
};
|
||||
addRect(target);
|
||||
var children = target.children || [];
|
||||
for(var i=0;i<children.length;i++){
|
||||
var child = children[i];
|
||||
if(!child) continue;
|
||||
try{
|
||||
var s = window.getComputedStyle(child);
|
||||
if(s.display === 'none' || s.visibility === 'hidden') continue;
|
||||
if(!child.offsetParent && s.position !== 'fixed') continue;
|
||||
}catch(e){}
|
||||
addRect(child);
|
||||
}
|
||||
return maxBottom > 0 ? Math.ceil(maxBottom - Math.min(minTop, 0)) : (target.scrollHeight || 0);
|
||||
}catch(e){
|
||||
return (document.body && document.body.scrollHeight) || 0;
|
||||
}
|
||||
} function post(m){ try{ parent.postMessage(m,'*') }catch(e){} }
|
||||
var rafPending=false, lastH=0;
|
||||
var HYSTERESIS = 2;
|
||||
function send(force){
|
||||
if(rafPending && !force) return;
|
||||
rafPending = true;
|
||||
requestAnimationFrame(function(){
|
||||
rafPending = false;
|
||||
var h = measureVisibleHeight();
|
||||
if(force || Math.abs(h - lastH) >= HYSTERESIS){
|
||||
lastH = h;
|
||||
post({height:h, force:!!force});
|
||||
}
|
||||
});
|
||||
}
|
||||
try{ send(true) }catch(e){}
|
||||
document.addEventListener('DOMContentLoaded', function(){ send(true) }, {once:true});
|
||||
window.addEventListener('load', function(){ send(true) }, {once:true});
|
||||
try{
|
||||
if(document.fonts){
|
||||
document.fonts.ready.then(function(){ send(true) }).catch(function(){});
|
||||
if(document.fonts.addEventListener){
|
||||
document.fonts.addEventListener('loadingdone', function(){ send(true) });
|
||||
document.fonts.addEventListener('loadingerror', function(){ send(true) });
|
||||
}
|
||||
}
|
||||
}catch(e){}
|
||||
['transitionend','animationend'].forEach(function(evt){
|
||||
document.addEventListener(evt, function(){ send(false) }, {passive:true, capture:true});
|
||||
});
|
||||
try{
|
||||
var root = document.querySelector('.calendar-wrapper') || document.body || document.documentElement;
|
||||
var ro = new ResizeObserver(function(){ send(false) });
|
||||
ro.observe(root);
|
||||
}catch(e){
|
||||
try{
|
||||
var rootMO = document.querySelector('.calendar-wrapper') || document.body || document.documentElement;
|
||||
new MutationObserver(function(){ send(false) })
|
||||
.observe(rootMO, {childList:true, subtree:true, attributes:true, characterData:true});
|
||||
}catch(e){}
|
||||
window.addEventListener('resize', function(){ send(false) }, {passive:true});
|
||||
}
|
||||
window.addEventListener('message', function(e){
|
||||
var d = e && e.data || {};
|
||||
if(d && d.type === 'probe') setTimeout(function(){ send(true) }, 10);
|
||||
});
|
||||
})();`;
|
||||
}
|
||||
|
||||
function buildWrappedHtml(html) {
|
||||
const origin = (typeof location !== 'undefined' && location.origin) ? location.origin : '';
|
||||
const baseTag = settings && settings.useBlob ? `<base href="${origin}/">` : "";
|
||||
const headHints = buildResourceHints(html);
|
||||
const vhFix = `<style>html,body{height:auto!important;min-height:0!important;max-height:none!important}.profile-container,[style*="100vh"]{height:auto!important;min-height:600px!important}[style*="height:100%"]{height:auto!important;min-height:100%!important}</style>`;
|
||||
|
||||
const apiScript = `
|
||||
<script>
|
||||
window.makeRequest = function(request, data) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var uid = Date.now() + Math.random();
|
||||
var callbackRequest = request + '_callback';
|
||||
|
||||
function handleMessage(event) {
|
||||
var 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(function() {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
reject(new Error('请求 "' + request + '" 超时 (30秒)'));
|
||||
}, 30000);
|
||||
|
||||
window.parent.postMessage({
|
||||
source: 'amily2-iframe-request',
|
||||
request: request,
|
||||
uid: uid,
|
||||
data: data
|
||||
}, '*');
|
||||
});
|
||||
};
|
||||
|
||||
window.AmilyHelper = {
|
||||
getChatMessages: function(range, options) {
|
||||
return makeRequest('getChatMessages', { range: range, options: options });
|
||||
},
|
||||
setChatMessages: function(messages, options) {
|
||||
return makeRequest('setChatMessages', { messages: messages, options: options });
|
||||
},
|
||||
setChatMessage: function(index, content) {
|
||||
return makeRequest('setChatMessage', { index: index, content: content });
|
||||
},
|
||||
createChatMessages: function(messages, options) {
|
||||
return makeRequest('createChatMessages', { messages: messages, options: options });
|
||||
},
|
||||
deleteChatMessages: function(ids, options) {
|
||||
return makeRequest('deleteChatMessages', { ids: ids, options: options });
|
||||
},
|
||||
getLorebooks: function() {
|
||||
return makeRequest('getLorebooks', {});
|
||||
},
|
||||
getCharLorebooks: function(options) {
|
||||
return makeRequest('getCharLorebooks', { options: options });
|
||||
},
|
||||
getLorebookEntries: function(bookName) {
|
||||
return makeRequest('getLorebookEntries', { bookName: bookName });
|
||||
},
|
||||
setLorebookEntries: function(bookName, entries) {
|
||||
return makeRequest('setLorebookEntries', { bookName: bookName, entries: entries });
|
||||
},
|
||||
createLorebookEntries: function(bookName, entries) {
|
||||
return makeRequest('createLorebookEntries', { bookName: bookName, entries: entries });
|
||||
},
|
||||
createLorebook: function(bookName) {
|
||||
return makeRequest('createLorebook', { bookName: bookName });
|
||||
},
|
||||
triggerSlash: function(command) {
|
||||
return makeRequest('triggerSlash', { command: command });
|
||||
},
|
||||
getLastMessageId: function() {
|
||||
return makeRequest('getLastMessageId', {});
|
||||
},
|
||||
toastr: function(type, message, title) {
|
||||
return makeRequest('toastr', { type: type, message: message, title: title });
|
||||
}
|
||||
};
|
||||
|
||||
if (!window.TavernHelper) {
|
||||
window.TavernHelper = window.AmilyHelper;
|
||||
console.log('[Amily2-Iframe] TavernHelper 别名已创建');
|
||||
} else {
|
||||
console.log('[Amily2-Iframe] 检测到已存在的 TavernHelper,保持原有实现');
|
||||
}
|
||||
|
||||
window.triggerSlash = function(command) {
|
||||
return makeRequest('triggerSlash', { command: command });
|
||||
};
|
||||
|
||||
window.getChatMessages = function(range, options) {
|
||||
return makeRequest('getChatMessages', { range: range, options: options });
|
||||
};
|
||||
|
||||
window.setChatMessages = function(messages, options) {
|
||||
return makeRequest('setChatMessages', { messages: messages, options: options });
|
||||
};
|
||||
|
||||
window.setChatMessage = function(field_values, message_id, options) {
|
||||
return makeRequest('setChatMessage', {
|
||||
field_values: field_values,
|
||||
message_id: message_id,
|
||||
options: options || {}
|
||||
});
|
||||
};
|
||||
|
||||
window.switchSwipe = function(messageIndex, swipeIndex) {
|
||||
return makeRequest('switchSwipe', { messageIndex: messageIndex, swipeIndex: swipeIndex });
|
||||
};
|
||||
|
||||
window.createChatMessages = function(messages, options) {
|
||||
return makeRequest('createChatMessages', { messages: messages, options: options });
|
||||
};
|
||||
|
||||
window.deleteChatMessages = function(ids, options) {
|
||||
return makeRequest('deleteChatMessages', { ids: ids, options: options });
|
||||
};
|
||||
|
||||
window.getLorebooks = function() {
|
||||
return makeRequest('getLorebooks', {});
|
||||
};
|
||||
|
||||
window.getCharLorebooks = function(options) {
|
||||
return makeRequest('getCharLorebooks', { options: options });
|
||||
};
|
||||
|
||||
window.getLorebookEntries = function(bookName) {
|
||||
return makeRequest('getLorebookEntries', { bookName: bookName });
|
||||
};
|
||||
|
||||
window.setLorebookEntries = function(bookName, entries) {
|
||||
return makeRequest('setLorebookEntries', { bookName: bookName, entries: entries });
|
||||
};
|
||||
|
||||
window.createLorebookEntries = function(bookName, entries) {
|
||||
return makeRequest('createLorebookEntries', { bookName: bookName, entries: entries });
|
||||
};
|
||||
|
||||
window.createLorebook = function(bookName) {
|
||||
return makeRequest('createLorebook', { bookName: bookName });
|
||||
};
|
||||
|
||||
window.getLastMessageId = function() {
|
||||
return makeRequest('getLastMessageId', {});
|
||||
};
|
||||
|
||||
window.getVariables = function(options) {
|
||||
return makeRequest('getVariables', { options: options });
|
||||
};
|
||||
|
||||
window.setVariables = function(variables, options) {
|
||||
return makeRequest('setVariables', { variables: variables, options: options });
|
||||
};
|
||||
|
||||
window.deleteVariable = function(variablePath, options) {
|
||||
return makeRequest('deleteVariable', { variablePath: variablePath, options: options });
|
||||
};
|
||||
|
||||
window.getCharData = function(name) {
|
||||
return makeRequest('getCharData', { name: name });
|
||||
};
|
||||
|
||||
window.getCharAvatarPath = function(name) {
|
||||
return makeRequest('getCharAvatarPath', { name: name });
|
||||
};
|
||||
|
||||
window.getLorebookSettings = function() {
|
||||
return makeRequest('getLorebookSettings', {});
|
||||
};
|
||||
|
||||
window.setLorebookSettings = function(settings) {
|
||||
return makeRequest('setLorebookSettings', { settings: settings });
|
||||
};
|
||||
|
||||
window.getChatLorebook = function() {
|
||||
return makeRequest('getChatLorebook', {});
|
||||
};
|
||||
|
||||
window.setChatLorebook = function(lorebook) {
|
||||
return makeRequest('setChatLorebook', { lorebook: lorebook });
|
||||
};
|
||||
|
||||
window.substitudeMacros = function(text) {
|
||||
return makeRequest('substitudeMacros', { text: text });
|
||||
};
|
||||
|
||||
window.toastr = {
|
||||
success: function(message, title) {
|
||||
return makeRequest('toastr', { type: 'success', message: message, title: title });
|
||||
},
|
||||
info: function(message, title) {
|
||||
return makeRequest('toastr', { type: 'info', message: message, title: title });
|
||||
},
|
||||
warning: function(message, title) {
|
||||
return makeRequest('toastr', { type: 'warning', message: message, title: title });
|
||||
},
|
||||
warn: function(message, title) {
|
||||
return makeRequest('toastr', { type: 'warning', message: message, title: title });
|
||||
},
|
||||
error: function(message, title) {
|
||||
return makeRequest('toastr', { type: 'error', message: message, title: title });
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[Amily2-Iframe] 完整的 API 已加载到全局作用域');
|
||||
console.log('[Amily2-Iframe] 可用的全局对象: AmilyHelper, TavernHelper');
|
||||
console.log('[Amily2-Iframe] 可用的全局函数: triggerSlash, getChatMessages, setChatMessage, toastr, 等');
|
||||
</script>
|
||||
<script type="module" src="/scripts/extensions/third-party/${extensionName}/core/tavern-helper/iframe_client.js"></script>
|
||||
`;
|
||||
|
||||
const injectionBlock = `
|
||||
${baseTag}
|
||||
<script>${iframeClientScript()}</script>
|
||||
${headHints}
|
||||
${vhFix}
|
||||
${apiScript}
|
||||
`;
|
||||
|
||||
const isFullHtml = /<html/i.test(html) && /<\/html>/i.test(html);
|
||||
|
||||
if (isFullHtml) {
|
||||
if (html.includes('</head>')) {
|
||||
return html.replace('</head>', `${injectionBlock}</head>`);
|
||||
} else if (html.includes('<body')) {
|
||||
return html.replace('<body', `<head>${injectionBlock}</head><body`);
|
||||
}
|
||||
return `<!DOCTYPE html>${injectionBlock}${html}`;
|
||||
}
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>html,body{margin:0;padding:0;background:transparent;font-family:inherit;color:inherit}</style>
|
||||
${injectionBlock}
|
||||
</head>
|
||||
<body>${html}</body></html>`;
|
||||
}
|
||||
|
||||
|
||||
function getOrCreateWrapper(preEl) {
|
||||
let wrapper = preEl.previousElementSibling;
|
||||
if (!wrapper || !wrapper.classList.contains('amily2-iframe-wrapper')) {
|
||||
wrapper = document.createElement('div');
|
||||
wrapper.className = 'amily2-iframe-wrapper';
|
||||
wrapper.style.cssText = 'margin:0;';
|
||||
preEl.parentNode.insertBefore(wrapper, preEl);
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function registerIframeMapping(iframe, wrapper) {
|
||||
const tryMap = () => {
|
||||
try {
|
||||
if (iframe && iframe.contentWindow) {
|
||||
winMap.set(iframe.contentWindow, { iframe, wrapper });
|
||||
return true;
|
||||
}
|
||||
} catch (e) { }
|
||||
return false;
|
||||
};
|
||||
if (tryMap()) return;
|
||||
let tries = 0;
|
||||
const t = setInterval(() => {
|
||||
tries++;
|
||||
if (tryMap() || tries > 20) clearInterval(t);
|
||||
}, 25);
|
||||
}
|
||||
|
||||
function handleIframeMessage(event) {
|
||||
const data = event.data || {};
|
||||
let rec = winMap.get(event.source);
|
||||
if (!rec || !rec.iframe) {
|
||||
const iframes = document.querySelectorAll('iframe.amily2-iframe');
|
||||
for (const iframe of iframes) {
|
||||
if (iframe.contentWindow === event.source) {
|
||||
rec = { iframe, wrapper: iframe.parentElement };
|
||||
winMap.set(event.source, rec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rec && rec.iframe && typeof data.height === 'number') {
|
||||
const next = Math.max(0, Number(data.height) || 0);
|
||||
if (next < 1) return;
|
||||
const prev = lastHeights.get(rec.iframe) || 0;
|
||||
if (!data.force && Math.abs(next - prev) < 1) return;
|
||||
lastHeights.set(rec.iframe, next);
|
||||
requestAnimationFrame(() => { rec.iframe.style.height = `${next}px`; });
|
||||
}
|
||||
}
|
||||
|
||||
function setIframeBlobHTML(iframe, fullHTML, codeHash) {
|
||||
const existing = hashToBlobUrl.get(codeHash);
|
||||
if (existing) {
|
||||
iframe.src = existing;
|
||||
blobUrls.set(iframe, existing);
|
||||
return;
|
||||
}
|
||||
const blob = new Blob([fullHTML], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
iframe.src = url;
|
||||
blobUrls.set(iframe, url);
|
||||
hashToBlobUrl.set(codeHash, url);
|
||||
blobLRU.push(codeHash);
|
||||
while (blobLRU.length > BLOB_CACHE_LIMIT) {
|
||||
const old = blobLRU.shift();
|
||||
const u = hashToBlobUrl.get(old);
|
||||
hashToBlobUrl.delete(old);
|
||||
try { URL.revokeObjectURL(u) } catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
function releaseIframeBlob(iframe) {
|
||||
try {
|
||||
const url = blobUrls.get(iframe);
|
||||
if (url) URL.revokeObjectURL(url);
|
||||
blobUrls.delete(iframe);
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
function renderHtmlInIframe(htmlContent, container, preElement) {
|
||||
try {
|
||||
const originalHash = djb2(htmlContent);
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = generateUniqueId();
|
||||
iframe.className = 'amily2-iframe';
|
||||
iframe.style.cssText = 'width:100%;border:none;background:transparent;overflow:hidden;height:0;margin:0;padding:0;display:block;contain:layout paint style;will-change:height;min-height:50px';
|
||||
iframe.setAttribute('frameborder', '0');
|
||||
iframe.setAttribute('scrolling', 'no');
|
||||
iframe.loading = 'eager';
|
||||
if (settings.sandboxMode) {
|
||||
iframe.setAttribute('sandbox', 'allow-scripts allow-modals');
|
||||
} else {
|
||||
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-modals allow-popups');
|
||||
}
|
||||
const wrapper = getOrCreateWrapper(preElement);
|
||||
wrapper.querySelectorAll('.amily2-iframe').forEach(old => {
|
||||
try { old.src = 'about:blank'; } catch (e) { }
|
||||
releaseIframeBlob(old);
|
||||
old.remove();
|
||||
});
|
||||
const codeHash = djb2(htmlContent);
|
||||
const full = buildWrappedHtml(htmlContent);
|
||||
if (settings.useBlob) {
|
||||
setIframeBlobHTML(iframe, full, codeHash);
|
||||
} else {
|
||||
iframe.srcdoc = full;
|
||||
}
|
||||
wrapper.appendChild(iframe);
|
||||
preElement.classList.remove('xb-show');
|
||||
preElement.style.display = 'none';
|
||||
registerIframeMapping(iframe, wrapper);
|
||||
try { iframe.contentWindow?.postMessage({ type: 'probe' }, '*'); } catch (e) { }
|
||||
preElement.dataset.xbFinal = 'true';
|
||||
preElement.dataset.xbHash = originalHash;
|
||||
return iframe;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function processCodeBlocks(messageElement) {
|
||||
if (extension_settings[extensionName].render_enabled === false) return;
|
||||
try {
|
||||
const codeBlocks = messageElement.querySelectorAll('pre > code');
|
||||
codeBlocks.forEach(codeBlock => {
|
||||
const preElement = codeBlock.parentElement;
|
||||
const should = shouldRenderContentByBlock(codeBlock);
|
||||
const html = codeBlock.textContent || '';
|
||||
const hash = djb2(html);
|
||||
const isFinal = preElement.dataset.xbFinal === 'true';
|
||||
const same = preElement.dataset.xbHash === hash;
|
||||
if (isFinal && same) return;
|
||||
if (should) {
|
||||
renderHtmlInIframe(html, preElement.parentNode, preElement);
|
||||
} else {
|
||||
preElement.classList.add('xb-show');
|
||||
preElement.removeAttribute('data-xbfinal');
|
||||
preElement.removeAttribute('data-xbhash');
|
||||
preElement.style.display = '';
|
||||
}
|
||||
preElement.dataset.xiaobaixBound = 'true';
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Amily2-Renderer] Error during processCodeBlocks:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function processMessageById(messageId) {
|
||||
const messageElement = document.querySelector(`div.mes[mesid="${messageId}"] .mes_text`);
|
||||
if (!messageElement) return;
|
||||
processCodeBlocks(messageElement);
|
||||
}
|
||||
|
||||
export function initializeRenderer() {
|
||||
const handleMessage = (data) => {
|
||||
const messageId = typeof data === 'object' ? data.messageId : data;
|
||||
if (messageId == null) return;
|
||||
console.log('[Amily2-Renderer] 处理消息渲染:', messageId);
|
||||
setTimeout(() => processMessageById(messageId), 50);
|
||||
};
|
||||
|
||||
eventSource.on(event_types.MESSAGE_RECEIVED, handleMessage);
|
||||
eventSource.on(event_types.MESSAGE_UPDATED, handleMessage);
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, handleMessage);
|
||||
eventSource.on(event_types.MESSAGE_EDITED, handleMessage);
|
||||
eventSource.on(event_types.USER_MESSAGE_RENDERED, handleMessage);
|
||||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, handleMessage);
|
||||
eventSource.on(event_types.IMPERSONATE_READY, handleMessage);
|
||||
|
||||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||
console.log('[Amily2-Renderer] 聊天已切换,重新渲染所有 iframe');
|
||||
setTimeout(renderAllIframes, 100);
|
||||
});
|
||||
|
||||
window.addEventListener('message', handleIframeMessage);
|
||||
|
||||
console.log('[Amily2-Renderer] 渲染器已初始化,监听事件: MESSAGE_RECEIVED, MESSAGE_UPDATED, MESSAGE_SWIPED, MESSAGE_EDITED, USER_MESSAGE_RENDERED, CHARACTER_MESSAGE_RENDERED, IMPERSONATE_READY');
|
||||
}
|
||||
|
||||
export function renderAllIframes() {
|
||||
const messages = document.querySelectorAll('.mes');
|
||||
messages.forEach(message => {
|
||||
const messageId = message.getAttribute('mesid');
|
||||
if (messageId) {
|
||||
processMessageById(messageId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function clearAllIframes() {
|
||||
const iframes = document.querySelectorAll('.amily2-iframe');
|
||||
iframes.forEach(iframe => {
|
||||
const wrapper = iframe.parentElement;
|
||||
if (wrapper && wrapper.classList.contains('amily2-iframe-wrapper')) {
|
||||
const preElement = wrapper.nextElementSibling;
|
||||
if (preElement && preElement.tagName === 'PRE') {
|
||||
preElement.classList.add('xb-show');
|
||||
preElement.style.display = '';
|
||||
}
|
||||
wrapper.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user