Compare commits

...

17 Commits

Author SHA1 Message Date
SilenceLurker
67c9708d68 Merge branch 'Wx-2025:main' into main 2025-12-28 19:10:21 +08:00
b52738248d Update executor.js 2025-12-28 11:35:03 +08:00
7a0227cbed Update Security Dependency 2025-12-28 01:28:03 +08:00
74381da9a8 Merge remote-tracking branch 'upstream/HEAD' 2025-12-27 17:44:24 +08:00
27fcd146ff Update manifest.json 2025-12-27 13:05:03 +08:00
d2e6edb2ac Update fmt.Println message from 'Hello' to 'Goodbye' 2025-12-27 13:00:24 +08:00
4f0f067a5a Update auth.js 2025-12-27 12:59:19 +08:00
20aaad4f7e Update rag-processor.js 2025-12-27 12:11:45 +08:00
44d0a46d74 Refactor utils.js with improved debounce and escapeHTML
Refactor utils.js to improve readability and functionality.
2025-12-27 12:02:03 +08:00
5380bbddcf Update fmt.Println message from 'Hello' to 'Goodbye' 2025-12-27 12:00:59 +08:00
fdcc95ba09 Update table-bindings.js 2025-12-27 11:59:33 +08:00
c64b35ccf5 Update fmt.Println to output 'Goodbye World' 2025-12-27 11:50:28 +08:00
833b197ece Refactor executor.js to improve structure and imports 2025-12-27 11:48:17 +08:00
9736b8abb4 Refactor table executor functions for clarity 2025-12-27 11:46:41 +08:00
4e6614f9fc Enhance fetchMessageBoardContent to use lastMessageId
Add support for fetching message board content with last message ID.
2025-12-27 11:44:31 +08:00
90f9feb6de Update print statement from 'Hello' to 'Goodbye' 2025-12-27 11:43:40 +08:00
ff697236e1 Update index.js 2025-12-27 11:42:03 +08:00
12 changed files with 3277 additions and 114 deletions

View File

@@ -4,6 +4,7 @@ import { eventSource, event_types } from '/script.js';
import { showHtmlModal } from '/scripts/extensions/third-party/ST-Amily2-Chat-Optimisation/ui/page-window.js'; import { showHtmlModal } from '/scripts/extensions/third-party/ST-Amily2-Chat-Optimisation/ui/page-window.js';
import { safeLorebooks, safeLorebookEntries, safeUpdateLorebookEntries, compatibleWriteToLorebook } from '../core/tavernhelper-compatibility.js'; import { safeLorebooks, safeLorebookEntries, safeUpdateLorebookEntries, compatibleWriteToLorebook } from '../core/tavernhelper-compatibility.js';
import { amilyHelper } from '../core/tavern-helper/main.js'; import { amilyHelper } from '../core/tavern-helper/main.js';
import { escapeHTML } from '../utils/utils.js';
const { SillyTavern } = window; const { SillyTavern } = window;
class WorldEditor { class WorldEditor {
@@ -159,7 +160,7 @@ class WorldEditor {
row.dataset.bookName = book.name; row.dataset.bookName = book.name;
row.innerHTML = ` row.innerHTML = `
<input type="checkbox" class="world-book-checkbox" ${isSelected ? 'checked' : ''}> <input type="checkbox" class="world-book-checkbox" ${isSelected ? 'checked' : ''}>
<span class="world-book-name">${book.name}</span> <span class="world-book-name">${escapeHTML(book.name)}</span>
<div class="world-book-actions"> <div class="world-book-actions">
<button class="world-editor-btn small-btn" data-action="edit"><i class="fas fa-pencil-alt"></i> 编辑</button> <button class="world-editor-btn small-btn" data-action="edit"><i class="fas fa-pencil-alt"></i> 编辑</button>
<button class="world-editor-btn small-btn" data-action="rename"><i class="fas fa-i-cursor"></i> 重命名</button> <button class="world-editor-btn small-btn" data-action="rename"><i class="fas fa-i-cursor"></i> 重命名</button>
@@ -400,8 +401,8 @@ class WorldEditor {
<div data-label="选择"><input type="checkbox" class="world-editor-entry-checkbox" ${this.selectedEntries.has(entry.uid) ? 'checked' : ''}></div> <div data-label="选择"><input type="checkbox" class="world-editor-entry-checkbox" ${this.selectedEntries.has(entry.uid) ? 'checked' : ''}></div>
<div data-label="状态" class="inline-toggle" data-field="enabled" data-uid="${entry.uid}"><i class="fas ${entry.enabled ? 'fa-toggle-on' : 'fa-toggle-off'}"></i></div> <div data-label="状态" class="inline-toggle" data-field="enabled" data-uid="${entry.uid}"><i class="fas ${entry.enabled ? 'fa-toggle-on' : 'fa-toggle-off'}"></i></div>
<div data-label="灯色" class="inline-toggle" data-field="type" data-uid="${entry.uid}">${entry.type === 'constant' ? '🔵' : '🟢'}</div> <div data-label="灯色" class="inline-toggle" data-field="type" data-uid="${entry.uid}">${entry.type === 'constant' ? '🔵' : '🟢'}</div>
<div data-label="条目"><input type="text" class="inline-edit" data-field="comment" data-uid="${entry.uid}" value="${entry.comment || ''}" placeholder="点击填写条目名"></div> <div data-label="条目"><input type="text" class="inline-edit" data-field="comment" data-uid="${entry.uid}" value="${escapeHTML(entry.comment || '')}" placeholder="点击填写条目名"></div>
<div data-label="内容" class="world-editor-entry-content" data-action="open-editor" data-uid="${entry.uid}" title="${entry.content || ''}">${entry.content || ''}</div> <div data-label="内容" class="world-editor-entry-content" data-action="open-editor" data-uid="${entry.uid}" title="${escapeHTML(entry.content || '')}">${escapeHTML(entry.content || '')}</div>
<div data-label="位置">${positionSelect}</div> <div data-label="位置">${positionSelect}</div>
<div data-label="深度"><input type="number" class="inline-edit" data-field="depth" data-uid="${entry.uid}" value="${entry.depth != null ? entry.depth : ''}" ${!String(entry.position)?.startsWith('at_depth') ? 'disabled' : ''}></div> <div data-label="深度"><input type="number" class="inline-edit" data-field="depth" data-uid="${entry.uid}" value="${entry.depth != null ? entry.depth : ''}" ${!String(entry.position)?.startsWith('at_depth') ? 'disabled' : ''}></div>
<div data-label="顺序"><input type="number" class="inline-edit" data-field="order" data-uid="${entry.uid}" value="${entry.order}"></div> <div data-label="顺序"><input type="number" class="inline-edit" data-field="order" data-uid="${entry.uid}" value="${entry.order}"></div>
@@ -541,7 +542,7 @@ class WorldEditor {
<div class="copy-dialog"> <div class="copy-dialog">
<label for="target-worldbook">选择目标世界书:</label> <label for="target-worldbook">选择目标世界书:</label>
<select id="target-worldbook" class="form-control"> <select id="target-worldbook" class="form-control">
${availableBooks.map(name => `<option value="${name}" ${name === this.currentWorldBook ? 'selected' : ''}>${name}${name === this.currentWorldBook ? ' (当前)' : ''}</option>`).join('')} ${availableBooks.map(name => `<option value="${escapeHTML(name)}" ${name === this.currentWorldBook ? 'selected' : ''}>${escapeHTML(name)}${name === this.currentWorldBook ? ' (当前)' : ''}</option>`).join('')}
</select> </select>
<div class="info"> <div class="info">
将复制 ${this.selectedEntries.size} 个条目到目标世界书 将复制 ${this.selectedEntries.size} 个条目到目标世界书

View File

@@ -47,6 +47,8 @@ const UPDATE_CHECK_URL =
const MESSAGE_BOARD_URL = const MESSAGE_BOARD_URL =
"https://amilyservice.amily49.cc/amily2_message_board.json"; "https://amilyservice.amily49.cc/amily2_message_board.json";
let lastMessageId = null;
export async function fetchMessageBoardContent() { export async function fetchMessageBoardContent() {
if (!MESSAGE_BOARD_URL) { if (!MESSAGE_BOARD_URL) {
@@ -54,11 +56,28 @@ export async function fetchMessageBoardContent() {
return null; return null;
} }
try { try {
const response = await fetch(MESSAGE_BOARD_URL, { cache: 'no-store' }); let url = MESSAGE_BOARD_URL;
if (lastMessageId) {
const separator = url.includes('?') ? '&' : '?';
url += `${separator}nowId=${encodeURIComponent(lastMessageId)}`;
}
const response = await fetch(url, { cache: 'no-store' });
if (response.status === 304) {
console.log('[Amily2号-内务府] 留言板内容未变更 (304)。');
return null;
}
if (!response.ok) { if (!response.ok) {
throw new Error(`服务器响应异常: ${response.status}`); throw new Error(`服务器响应异常: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
if (data && data.id) {
lastMessageId = data.id;
}
return data; return data;
} catch (error) { } catch (error) {
console.error('[Amily2号-内务府] 获取留言板内容失败:', error); console.error('[Amily2号-内务府] 获取留言板内容失败:', error);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,8 @@
import { import {
world_names, world_names,
loadWorldInfo, loadWorldInfo,
saveWorldInfo, saveWorldInfo,
createNewWorldInfo, createNewWorldInfo,
createWorldInfoEntry createWorldInfoEntry
} from "/scripts/world-info.js"; } from "/scripts/world-info.js";
@@ -20,9 +20,9 @@ let reloadEditor = () => {
console.warn("[Amily助手] 动态导入 reloadEditor 失败,将使用空函数。错误信息:", error.message); console.warn("[Amily助手] 动态导入 reloadEditor 失败,将使用空函数。错误信息:", error.message);
} }
})(); })();
import { import {
characters, characters,
eventSource, eventSource,
event_types, event_types,
chat, chat,
reloadCurrentChat, reloadCurrentChat,
@@ -46,14 +46,14 @@ class AmilyHelper {
getChatMessages(range, options = {}) { getChatMessages(range, options = {}) {
const { role = 'all', hide_state = 'all', include_swipes = false, include_swipe = false } = options; const { role = 'all', hide_state = 'all', include_swipes = false, include_swipe = false } = options;
const includeSwipes = include_swipes || include_swipe; const includeSwipes = include_swipes || include_swipe;
if (!chat || !Array.isArray(chat)) { if (!chat || !Array.isArray(chat)) {
throw new Error('聊天数组不可用'); throw new Error('聊天数组不可用');
} }
let start, end; let start, end;
const rangeStr = String(range); const rangeStr = String(range);
if (rangeStr.match(/^(-?\d+)$/)) { if (rangeStr.match(/^(-?\d+)$/)) {
const value = Number(rangeStr); const value = Number(rangeStr);
start = end = value < 0 ? chat.length + value : value; start = end = value < 0 ? chat.length + value : value;
@@ -186,7 +186,7 @@ class AmilyHelper {
refresh = 'display_and_render_current' refresh = 'display_and_render_current'
} = {}) { } = {}) {
field_values = typeof field_values === 'string' ? { message: field_values } : field_values; field_values = typeof field_values === 'string' ? { message: field_values } : field_values;
if (typeof swipe_id !== 'number' && swipe_id !== 'current') { if (typeof swipe_id !== 'number' && swipe_id !== 'current') {
throw new Error(`提供的 swipe_id 无效, 请提供 'current' 或序号, 你提供的是: ${swipe_id}`); throw new Error(`提供的 swipe_id 无效, 请提供 'current' 或序号, 你提供的是: ${swipe_id}`);
} }
@@ -279,7 +279,7 @@ class AmilyHelper {
const should_update_swipe = add_swipes_if_required(); const should_update_swipe = add_swipes_if_required();
update_chat_message(); update_chat_message();
await saveChatConditional(); await saveChatConditional();
if (refresh == 'all') { if (refresh == 'all') {
await reloadCurrentChat(); await reloadCurrentChat();
} else { } else {
@@ -378,12 +378,12 @@ class AmilyHelper {
if (!bookData || !bookData.entries) { if (!bookData || !bookData.entries) {
return []; return [];
} }
const positionMap = { const positionMap = {
0: 'before_character_definition', 0: 'before_character_definition',
1: 'after_character_definition', 1: 'after_character_definition',
2: 'before_author_note', 2: 'before_author_note',
3: 'after_author_note', 3: 'after_author_note',
4: 'at_depth_as_system' 4: 'at_depth_as_system'
}; };
return Object.entries(bookData.entries).map(([uid, entry]) => ({ return Object.entries(bookData.entries).map(([uid, entry]) => ({
uid: parseInt(uid), uid: parseInt(uid),
@@ -393,7 +393,7 @@ class AmilyHelper {
keys: entry.key || [], keys: entry.key || [],
enabled: !entry.disable, enabled: !entry.disable,
constant: entry.constant || false, constant: entry.constant || false,
position: positionMap[entry.position] || 'at_depth_as_system', position: positionMap[entry.position] || 'at_depth_as_system',
depth: entry.depth || 998, depth: entry.depth || 998,
})); }));
} catch (error) { } catch (error) {
@@ -421,13 +421,13 @@ class AmilyHelper {
if (entryUpdate.type === 'constant') existingEntry.constant = true; if (entryUpdate.type === 'constant') existingEntry.constant = true;
if (entryUpdate.type === 'selective') existingEntry.constant = false; if (entryUpdate.type === 'selective') existingEntry.constant = false;
if (entryUpdate.position !== undefined) { if (entryUpdate.position !== undefined) {
const positionMap = { const positionMap = {
'before_character_definition': 0, 'before_character_definition': 0,
'after_character_definition': 1, 'after_character_definition': 1,
'before_author_note': 2, 'before_author_note': 2,
'after_author_note': 3, 'after_author_note': 3,
'at_depth': 4, 'at_depth': 4,
'at_depth_as_system': 4 'at_depth_as_system': 4
}; };
existingEntry.position = positionMap[entryUpdate.position] ?? 4; existingEntry.position = positionMap[entryUpdate.position] ?? 4;
} }
@@ -462,13 +462,13 @@ class AmilyHelper {
for (const newEntryData of entries) { for (const newEntryData of entries) {
const newEntry = createWorldInfoEntry(bookName, bookData); const newEntry = createWorldInfoEntry(bookName, bookData);
const positionMap = { const positionMap = {
'before_character_definition': 0, 'before_character_definition': 0,
'after_character_definition': 1, 'after_character_definition': 1,
'before_author_note': 2, 'before_author_note': 2,
'after_author_note': 3, 'after_author_note': 3,
'at_depth': 4, 'at_depth': 4,
'at_depth_as_system': 4 'at_depth_as_system': 4
}; };
Object.assign(newEntry, { Object.assign(newEntry, {
comment: newEntryData.comment || '新条目', comment: newEntryData.comment || '新条目',
@@ -499,7 +499,7 @@ class AmilyHelper {
if (!bookData || !bookData.entries) { if (!bookData || !bookData.entries) {
return false; return false;
} }
let deletedCount = 0; let deletedCount = 0;
for (const uid of uids) { for (const uid of uids) {
if (bookData.entries[uid]) { if (bookData.entries[uid]) {
@@ -507,7 +507,7 @@ class AmilyHelper {
deletedCount++; deletedCount++;
} }
} }
if (deletedCount > 0) { if (deletedCount > 0) {
await saveWorldInfo(bookName, bookData, true); await saveWorldInfo(bookName, bookData, true);
reloadEditor(bookName); reloadEditor(bookName);
@@ -583,7 +583,7 @@ class AmilyHelper {
const char = characters[this_chid]; const char = characters[this_chid];
if (!char.data) char.data = {}; if (!char.data) char.data = {};
if (!char.data.extensions) char.data.extensions = {}; if (!char.data.extensions) char.data.extensions = {};
// 确保 world 字段是数组 // 确保 world 字段是数组
let worlds = char.data.extensions.world; let worlds = char.data.extensions.world;
if (!Array.isArray(worlds)) { if (!Array.isArray(worlds)) {
@@ -594,7 +594,7 @@ class AmilyHelper {
worlds.push(bookName); worlds.push(bookName);
char.data.extensions.world = worlds; char.data.extensions.world = worlds;
console.log(`[Amily助手] 已将世界书《${bookName}》绑定到角色 ${char.name}`); console.log(`[Amily助手] 已将世界书《${bookName}》绑定到角色 ${char.name}`);
if (typeof saveCharacterDebounced === 'function') { if (typeof saveCharacterDebounced === 'function') {
saveCharacterDebounced(); saveCharacterDebounced();
return true; return true;
@@ -652,12 +652,13 @@ export function makeRequest(request, data) {
reject(new Error(`请求 '${request}' 超时 (30秒)`)); reject(new Error(`请求 '${request}' 超时 (30秒)`));
}, 30000); }, 30000);
const targetOrigin = window.location.origin === 'null' ? '*' : window.location.origin;
window.parent.postMessage({ window.parent.postMessage({
source: 'amily2-iframe-request', source: 'amily2-iframe-request',
request: request, request: request,
uid: uid, uid: uid,
data: data data: data
}, window.location.origin); }, targetOrigin);
}); });
} }
@@ -676,9 +677,9 @@ export function registerApiHandler(request, handler) {
export function initializeApiListener() { export function initializeApiListener() {
window.addEventListener('message', async (event) => { window.addEventListener('message', async (event) => {
// 安全修复:严格验证消息来源,防止跨源消息伪造
// 'null' 是 srcdoc 或 blob URL iframe 的 origin if (window.location.origin !== 'null' && event.origin !== window.location.origin) {
if (event.origin !== window.location.origin && event.origin !== 'null') { console.warn(`[Amily2-IframeAPI] 拒绝来自未知来源的请求: ${event.origin}`);
return; return;
} }
@@ -696,6 +697,7 @@ export function initializeApiListener() {
const handler = apiHandlers.get(data.request); const handler = apiHandlers.get(data.request);
const callbackRequest = `${data.request}_callback`; const callbackRequest = `${data.request}_callback`;
const targetOrigin = event.origin === 'null' ? '*' : event.origin;
if (!handler) { if (!handler) {
console.error(`[Amily2-IframeAPI] 收到未知请求: ${data.request}`); console.error(`[Amily2-IframeAPI] 收到未知请求: ${data.request}`);
@@ -703,7 +705,7 @@ export function initializeApiListener() {
request: callbackRequest, request: callbackRequest,
uid: data.uid, uid: data.uid,
error: `未注册请求 '${data.request}' 的处理器` error: `未注册请求 '${data.request}' 的处理器`
}, event.origin); // 安全修复:回复到确切的来源,而不是 '*' }, targetOrigin);
return; return;
} }
@@ -713,15 +715,15 @@ export function initializeApiListener() {
request: callbackRequest, request: callbackRequest,
uid: data.uid, uid: data.uid,
result: result result: result
}, event.origin); // 安全修复:回复到确切的来源 }, targetOrigin);
} catch (error) { } catch (error) {
console.error(`[Amily2-IframeAPI] 执行处理器 '${data.request}' 时出错:`, error); console.error(`[Amily2-IframeAPI] 执行处理器 '${data.request}' 时出错:`, error);
event.source.postMessage({ event.source.postMessage({
request: callbackRequest, request: callbackRequest,
uid: data.uid, uid: data.uid,
error: error.message || String(error) error: error.message || String(error)
}, event.origin); // 安全修复:回复到确切的来源 }, targetOrigin);
} }
}); });
console.log('[Amily2-IframeAPI] 主窗口监听器已初始化'); console.log('[Amily2-IframeAPI] 主窗口监听器已初始化 (已启用安全验证)');
} }

View File

@@ -6,6 +6,7 @@ import { testSybdApiConnection, fetchSybdModels } from '../core/api/SybdApi.js';
import { handleFileUpload, processNovel } from './index.js'; import { handleFileUpload, processNovel } from './index.js';
import { reorganizeEntriesByHeadings, loadDatabaseFiles } from './executor.js'; import { reorganizeEntriesByHeadings, loadDatabaseFiles } from './executor.js';
import { SETTINGS_KEY as PRESET_SETTINGS_KEY } from '../PresetSettings/config.js'; import { SETTINGS_KEY as PRESET_SETTINGS_KEY } from '../PresetSettings/config.js';
import { escapeHTML } from '../utils/utils.js';
const moduleState = { const moduleState = {
selectedWorldBook: '', selectedWorldBook: '',
@@ -267,12 +268,12 @@ async function renderWorldBookEntries() {
} }
if (source && target) { if (source && target) {
body += `<tr><td>${source.trim()}</td><td>${rel.trim()}</td><td>${target.trim().replace(';','')}</td></tr>`; body += `<tr><td>${escapeHTML(source.trim())}</td><td>${escapeHTML(rel.trim())}</td><td>${escapeHTML(target.trim().replace(';',''))}</td></tr>`;
} }
}); });
return `<table class="table-render"><thead><tr><th>源头</th><th>关系</th><th>目标</th></tr></thead><tbody>${body}</tbody></table>`; return `<table class="table-render"><thead><tr><th>源头</th><th>关系</th><th>目标</th></tr></thead><tbody>${body}</tbody></table>`;
} catch { } catch {
return `<pre>${content}</pre>`; return `<pre>${escapeHTML(content)}</pre>`;
} }
} }
if (trimmedContent.includes('|') && trimmedContent.includes('\n')) { if (trimmedContent.includes('|') && trimmedContent.includes('\n')) {
@@ -283,7 +284,7 @@ async function renderWorldBookEntries() {
let isHeaderRow = true; let isHeaderRow = true;
rows.forEach(rowStr => { rows.forEach(rowStr => {
if (rowStr.includes('---')) return; if (rowStr.includes('---')) return;
const cells = rowStr.split('|').filter(c => c.trim()).map(cell => `<td>${cell.trim()}</td>`).join(''); const cells = rowStr.split('|').filter(c => c.trim()).map(cell => `<td>${escapeHTML(cell.trim())}</td>`).join('');
if (isHeaderRow) { if (isHeaderRow) {
header += `<tr>${cells.replace(/<td>/g, '<th>').replace(/<\/td>/g, '</th>')}</tr>`; header += `<tr>${cells.replace(/<td>/g, '<th>').replace(/<\/td>/g, '</th>')}</tr>`;
isHeaderRow = false; isHeaderRow = false;
@@ -293,15 +294,15 @@ async function renderWorldBookEntries() {
}); });
return `<table class="table-render"><thead>${header}</thead><tbody>${body}</tbody></table>`; return `<table class="table-render"><thead>${header}</thead><tbody>${body}</tbody></table>`;
} catch { } catch {
return `<pre>${content}</pre>`; return `<pre>${escapeHTML(content)}</pre>`;
} }
} }
return `<pre>${content}</pre>`; return `<pre>${escapeHTML(content)}</pre>`;
}; };
entryElement.innerHTML = ` entryElement.innerHTML = `
<div class="entry-header"> <div class="entry-header">
<strong class="entry-title">${title}</strong> <strong class="entry-title">${escapeHTML(title)}</strong>
<div class="entry-actions"> <div class="entry-actions">
<button class="menu_button primary small_button save-entry-btn" style="display: none;"><i class="fas fa-save"></i> 保存</button> <button class="menu_button primary small_button save-entry-btn" style="display: none;"><i class="fas fa-save"></i> 保存</button>
<button class="menu_button danger small_button cancel-entry-btn" style="display: none;"><i class="fas fa-times"></i> 取消</button> <button class="menu_button danger small_button cancel-entry-btn" style="display: none;"><i class="fas fa-times"></i> 取消</button>
@@ -484,7 +485,7 @@ function bindNovelProcessEvents() {
chunkCountEl.textContent = newChunks.length; chunkCountEl.textContent = newChunks.length;
chunkPreviewEl.innerHTML = newChunks.map((chunk, index) => chunkPreviewEl.innerHTML = newChunks.map((chunk, index) =>
`<div class="chunk-preview-item"><b>块 ${index + 1}:</b> ${chunk.content.substring(0, 100)}...</div>` `<div class="chunk-preview-item"><b>块 ${index + 1}:</b> ${escapeHTML(chunk.content.substring(0, 100))}...</div>`
).join(''); ).join('');
resetProcessing(); resetProcessing();
@@ -563,7 +564,7 @@ function bindNovelProcessEvents() {
fileInput.addEventListener('change', (event) => { fileInput.addEventListener('change', (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
fileLabel.innerHTML = `<i class="fas fa-check"></i> 已选择: ${file.name}`; fileLabel.innerHTML = `<i class="fas fa-check"></i> 已选择: ${escapeHTML(file.name)}`;
handleFileUpload(file, (content) => { handleFileUpload(file, (content) => {
fileContent = content; fileContent = content;
updateChunks(); updateChunks();

View File

@@ -5,6 +5,7 @@ import { getPresetPrompts, getMixedOrder } from '../PresetSettings/index.js';
import { generateRandomSeed } from '../core/api.js'; import { generateRandomSeed } from '../core/api.js';
import { safeLorebookEntries, safeUpdateLorebookEntries, compatibleWriteToLorebook } from '../core/tavernhelper-compatibility.js'; import { safeLorebookEntries, safeUpdateLorebookEntries, compatibleWriteToLorebook } from '../core/tavernhelper-compatibility.js';
import { loadWorldInfo, saveWorldInfo, createWorldInfoEntry } from "/scripts/world-info.js"; import { loadWorldInfo, saveWorldInfo, createWorldInfoEntry } from "/scripts/world-info.js";
import { escapeHTML } from '../utils/utils.js';
function buildContextFromEntries(entries) { function buildContextFromEntries(entries) {
if (!entries || entries.length === 0) { if (!entries || entries.length === 0) {
@@ -310,7 +311,7 @@ export async function loadDatabaseFiles() {
document.dispatchEvent(event); document.dispatchEvent(event);
container.style.display = 'none'; container.style.display = 'none';
document.getElementById('select-from-database-button').innerHTML = `<i class="fas fa-check"></i> 已选择: ${file.name}`; document.getElementById('select-from-database-button').innerHTML = `<i class="fas fa-check"></i> 已选择: ${escapeHTML(file.name)}`;
} catch (error) { } catch (error) {
console.error(`Error processing file ${file.name}:`, error); console.error(`Error processing file ${file.name}:`, error);

1148
index.js

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{ {
"name": "Amily2号聊天优化助手", "name": "Amily2号聊天优化助手",
"display_name": "Amily2号助手", "display_name": "Amily2号助手",
"version": "1.7.5", "version": "1.7.6",
"author": "Wx-2025", "author": "Wx-2025",
"description": "一个拥有独立UI的智能引擎正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。", "description": "一个拥有独立UI的智能引擎正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
"minSillyTavernVersion": "1.10.0", "minSillyTavernVersion": "1.10.0",
@@ -46,5 +46,6 @@

View File

@@ -12,6 +12,7 @@ import { safeCharLorebooks, safeLorebookEntries } from '../core/tavernhelper-com
import { characters, this_chid, eventSource, event_types } from "/script.js"; import { characters, this_chid, eventSource, event_types } from "/script.js";
import { fetchNccsModels, testNccsApiConnection } from '../core/api/NccsApi.js'; import { fetchNccsModels, testNccsApiConnection } from '../core/api/NccsApi.js';
import { showGraphVisualization } from '../core/relationship-graph/visualizer.js'; import { showGraphVisualization } from '../core/relationship-graph/visualizer.js';
import { escapeHTML } from '../utils/utils.js';
const isTouchDevice = () => window.matchMedia('(pointer: coarse)').matches; const isTouchDevice = () => window.matchMedia('(pointer: coarse)').matches;
const getAllTablesContainer = () => document.getElementById('all-tables-container'); const getAllTablesContainer = () => document.getElementById('all-tables-container');
@@ -129,7 +130,6 @@ function toggleColumnContextMenu(event) {
} }
}; };
// If the menu was opened, set up the listener to close it
if (targetTh.classList.contains('amily2-menu-open')) { if (targetTh.classList.contains('amily2-menu-open')) {
setTimeout(() => { setTimeout(() => {
document.addEventListener('click', closeMenu, true); document.addEventListener('click', closeMenu, true);
@@ -178,15 +178,15 @@ function showInputDialog({ title, label, currentValue, placeholder, onSave }) {
<dialog class="popup custom-input-dialog"> <dialog class="popup custom-input-dialog">
<div class="popup-body"> <div class="popup-body">
<h4 style="margin-top:0; color: #e0e0e0; border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 10px; display: flex; align-items: center; gap: 8px;"> <h4 style="margin-top:0; color: #e0e0e0; border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 10px; display: flex; align-items: center; gap: 8px;">
<i class="fas fa-edit" style="color: #9e8aff;"></i> ${title} <i class="fas fa-edit" style="color: #9e8aff;"></i> ${escapeHTML(title)}
</h4> </h4>
<div class="popup-content" style="padding: 20px 10px;"> <div class="popup-content" style="padding: 20px 10px;">
<div style="display: flex; flex-direction: column; gap: 12px;"> <div style="display: flex; flex-direction: column; gap: 12px;">
<label style="color: #ccc; font-weight: bold;">${label}</label> <label style="color: #ccc; font-weight: bold;">${escapeHTML(label)}</label>
<input type="text" id="generic-input" class="text_pole" <input type="text" id="generic-input" class="text_pole"
value="${currentValue}" value="${escapeHTML(currentValue)}"
style="padding: 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.3); background: rgba(0,0,0,0.2); color: #fff; font-size: 1em;" style="padding: 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.3); background: rgba(0,0,0,0.2); color: #fff; font-size: 1em;"
placeholder="${placeholder}"> placeholder="${escapeHTML(placeholder)}">
<small style="color: #aaa; font-style: italic;">提示:输入内容将用于更新项目。</small> <small style="color: #aaa; font-style: italic;">提示:输入内容将用于更新项目。</small>
</div> </div>
</div> </div>
@@ -321,7 +321,7 @@ export function renderTables() {
} }
const highlights = TableManager.getHighlights(); const highlights = TableManager.getHighlights();
const updatedTables = TableManager.getUpdatedTables(); // 【V15.2 新增】获取被更新的表格 const updatedTables = TableManager.getUpdatedTables();
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
const placeholder = document.getElementById('add-table-placeholder'); const placeholder = document.getElementById('add-table-placeholder');
@@ -334,9 +334,9 @@ export function renderTables() {
header.className = 'amily2-table-header-container'; header.className = 'amily2-table-header-container';
const title = document.createElement('h3'); const title = document.createElement('h3');
if (updatedTables.has(tableIndex)) { if (updatedTables.has(tableIndex)) {
title.classList.add('table-updated'); // 【V15.2 新增】为更新的表格添加高亮 title.classList.add('table-updated');
} }
title.innerHTML = `<i class="fas fa-table table-rename-icon" data-table-index="${tableIndex}" title="重命名"></i> ${tableData.name}`; title.innerHTML = `<i class="fas fa-table table-rename-icon" data-table-index="${tableIndex}" title="重命名"></i> ${escapeHTML(tableData.name)}`;
const controls = document.createElement('div'); const controls = document.createElement('div');
controls.className = 'table-controls'; controls.className = 'table-controls';
@@ -368,7 +368,6 @@ export function renderTables() {
if (tableData.headers) { if (tableData.headers) {
tableData.headers.forEach((_, colIndex) => { tableData.headers.forEach((_, colIndex) => {
const col = document.createElement('col'); const col = document.createElement('col');
// Assign a default width of 120px if none is specified
const colWidth = (tableData.columnWidths && tableData.columnWidths[colIndex]) ? tableData.columnWidths[colIndex] : 90; const colWidth = (tableData.columnWidths && tableData.columnWidths[colIndex]) ? tableData.columnWidths[colIndex] : 90;
col.style.width = `${colWidth}px`; col.style.width = `${colWidth}px`;
colgroup.appendChild(col); colgroup.appendChild(col);
@@ -376,21 +375,14 @@ export function renderTables() {
} }
tableElement.appendChild(colgroup); tableElement.appendChild(colgroup);
// Explicitly calculate and set the total table width to override CSS conflicts
let totalWidth = 0; let totalWidth = 0;
const cols = colgroup.querySelectorAll('col'); const cols = colgroup.querySelectorAll('col');
cols.forEach(col => { cols.forEach(col => {
totalWidth += parseInt(col.style.width, 10); totalWidth += parseInt(col.style.width, 10);
}); });
// Set min-width instead of fixed width to allow expansion
tableElement.style.minWidth = '100%'; tableElement.style.minWidth = '100%';
if (totalWidth > 0) { if (totalWidth > 0) {
// Only set explicit width if it exceeds the container (handled by min-width: 100% usually,
// but here we set it as a base to ensure columns don't shrink below their defined width)
tableElement.style.width = `${Math.max(totalWidth, 0)}px`; tableElement.style.width = `${Math.max(totalWidth, 0)}px`;
// Actually, to allow full width expansion, we should just use min-width and let CSS handle the rest
// unless we want to force scrolling.
// Let's try setting min-width to the calculated total, and width to 100%.
tableElement.style.minWidth = `${totalWidth}px`; tableElement.style.minWidth = `${totalWidth}px`;
tableElement.style.width = '100%'; tableElement.style.width = '100%';
} }
@@ -403,8 +395,7 @@ export function renderTables() {
indexTh.textContent = '#'; indexTh.textContent = '#';
indexTh.style.cursor = 'pointer'; indexTh.style.cursor = 'pointer';
indexTh.title = '点击添加第一行'; indexTh.title = '点击添加第一行';
// 为表头的 # 号添加特殊的上下文菜单(仅在表格为空时显示)
if (!tableData.rows || tableData.rows.length === 0) { if (!tableData.rows || tableData.rows.length === 0) {
const headerMenu = document.createElement('div'); const headerMenu = document.createElement('div');
headerMenu.className = 'amily2-context-menu amily2-header-menu'; headerMenu.className = 'amily2-context-menu amily2-header-menu';
@@ -422,13 +413,11 @@ export function renderTables() {
headerMenu.appendChild(addRowButton); headerMenu.appendChild(addRowButton);
indexTh.appendChild(headerMenu); indexTh.appendChild(headerMenu);
// 为表头添加直接的点击事件监听器
indexTh.addEventListener('click', (e) => { indexTh.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
console.log('Header # clicked for table', tableIndex); console.log('Header # clicked for table', tableIndex);
// 直接执行添加行操作
TableManager.addRow(tableIndex); TableManager.addRow(tableIndex);
renderTables(); renderTables();
toastr.success('已添加第一行'); toastr.success('已添加第一行');
@@ -444,7 +433,7 @@ export function renderTables() {
const headerContent = document.createElement('span'); const headerContent = document.createElement('span');
headerContent.className = 'amily2-header-text'; headerContent.className = 'amily2-header-text';
headerContent.textContent = headerText; headerContent.textContent = headerText; // textContent is safe
th.appendChild(headerContent); th.appendChild(headerContent);
const menu = document.createElement('div'); const menu = document.createElement('div');
@@ -611,16 +600,13 @@ export function renderTables() {
TableManager.insertRow(tableIndex, rowIndex, 'below'); TableManager.insertRow(tableIndex, rowIndex, 'below');
break; break;
case 'delete-row': case 'delete-row':
// 【延迟删除】不再需要确认,因为操作是可逆的
TableManager.deleteRow(tableIndex, rowIndex); TableManager.deleteRow(tableIndex, rowIndex);
break; break;
case 'restore-row': case 'restore-row':
TableManager.restoreRow(tableIndex, rowIndex); TableManager.restoreRow(tableIndex, rowIndex);
break; break;
} }
// For instant feedback, re-render is needed for delete/restore
if (action === 'delete-row' || action === 'restore-row') { if (action === 'delete-row' || action === 'restore-row') {
// The manager functions now handle their own re-rendering
} else { } else {
renderTables(); renderTables();
} }
@@ -634,10 +620,9 @@ export function renderTables() {
const cellContent = document.createElement('div'); const cellContent = document.createElement('div');
cellContent.className = 'amily2-cell-content'; cellContent.className = 'amily2-cell-content';
cellContent.textContent = cellData; cellContent.textContent = cellData;
cell.appendChild(cellContent); cell.appendChild(cellContent);
// 【延迟删除】如果行正在待删除,则禁止编辑
if (rowStatus !== 'pending-deletion' && !isTouchDevice()) { if (rowStatus !== 'pending-deletion' && !isTouchDevice()) {
cell.setAttribute('contenteditable', 'true'); cell.setAttribute('contenteditable', 'true');
} }
@@ -662,7 +647,6 @@ export function renderTables() {
container.appendChild(placeholder); container.appendChild(placeholder);
} }
// Also update the in-chat table whenever the main tables are re-rendered
updateOrInsertTableInChat(); updateOrInsertTableInChat();
} }
@@ -740,7 +724,6 @@ function openRuleEditor(tableIndex) {
if (!tables || !tables[tableIndex]) return; if (!tables || !tables[tableIndex]) return;
const table = tables[tableIndex]; const table = tables[tableIndex];
// 兼容旧数据结构
if (table.charLimitRule && !table.charLimitRules) { if (table.charLimitRule && !table.charLimitRules) {
table.charLimitRules = {}; table.charLimitRules = {};
if (table.charLimitRule.columnIndex !== -1) { if (table.charLimitRule.columnIndex !== -1) {
@@ -754,7 +737,7 @@ function openRuleEditor(tableIndex) {
const header = table.headers[colIndex] || `未知列 (${colIndex})`; const header = table.headers[colIndex] || `未知列 (${colIndex})`;
return ` return `
<div class="char-limit-rule-item" style="display: flex; justify-content: space-between; align-items: center; padding: 8px; background: rgba(0,0,0,0.1); border-radius: 4px;"> <div class="char-limit-rule-item" style="display: flex; justify-content: space-between; align-items: center; padding: 8px; background: rgba(0,0,0,0.1); border-radius: 4px;">
<span><i class="fas fa-file-alt" style="margin-right: 8px; color: #9e8aff;"></i><b>${header}</b>: 不超过 ${limit} 字</span> <span><i class="fas fa-file-alt" style="margin-right: 8px; color: #9e8aff;"></i><b>${escapeHTML(header)}</b>: 不超过 ${limit} 字</span>
<button class="menu_button danger small_button remove-char-limit-rule-btn" data-col-index="${colIndex}" title="删除此规则"> <button class="menu_button danger small_button remove-char-limit-rule-btn" data-col-index="${colIndex}" title="删除此规则">
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
</button> </button>
@@ -765,9 +748,8 @@ function openRuleEditor(tableIndex) {
const getColumnOptions = (rules) => { const getColumnOptions = (rules) => {
return table.headers.map((header, index) => { return table.headers.map((header, index) => {
// 如果该列已存在规则,则不应出现在下拉菜单中
if (rules[index]) return ''; if (rules[index]) return '';
return `<option value="${index}">${header}</option>`; return `<option value="${index}">${escapeHTML(header)}</option>`;
}).join(''); }).join('');
}; };
@@ -775,7 +757,7 @@ function openRuleEditor(tableIndex) {
<dialog class="popup wide_dialogue_popup large_dialogue_popup"> <dialog class="popup wide_dialogue_popup large_dialogue_popup">
<div class="popup-body"> <div class="popup-body">
<h4 style="margin-top:0; color: #eee; border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 10px;"> <h4 style="margin-top:0; color: #eee; border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 10px;">
<i class="fa-solid fa-scroll"></i> 编辑 “${table.name}” 的规则 <i class="fa-solid fa-scroll"></i> 编辑 “${escapeHTML(table.name)}” 的规则
</h4> </h4>
<div class="popup-content" style="height: 70vh; overflow-y: auto;"> <div class="popup-content" style="height: 70vh; overflow-y: auto;">
<div class="rule-editor-form" style="display: flex; flex-direction: column; gap: 15px; padding: 10px;"> <div class="rule-editor-form" style="display: flex; flex-direction: column; gap: 15px; padding: 10px;">
@@ -864,8 +846,7 @@ function openRuleEditor(tableIndex) {
toastr.warning('请选择一个列。'); toastr.warning('请选择一个列。');
return; return;
} }
// 允许输入0但0意味着“无限制”所以我们不添加规则。
if (isNaN(limitValue) || limitValue < 0) { if (isNaN(limitValue) || limitValue < 0) {
toastr.warning('请输入一个有效的字数限制大于等于0。'); toastr.warning('请输入一个有效的字数限制大于等于0。');
return; return;
@@ -874,12 +855,10 @@ function openRuleEditor(tableIndex) {
const currentRules = JSON.parse(dialogElement.find('#current-char-limit-rules').attr('data-rules') || '{}'); const currentRules = JSON.parse(dialogElement.find('#current-char-limit-rules').attr('data-rules') || '{}');
if (limitValue > 0) { if (limitValue > 0) {
// 只有当限制大于0时才添加或更新规则
currentRules[selectedColumn] = limitValue; currentRules[selectedColumn] = limitValue;
dialogElement.find('#current-char-limit-rules').attr('data-rules', JSON.stringify(currentRules)); dialogElement.find('#current-char-limit-rules').attr('data-rules', JSON.stringify(currentRules));
refreshRuleUI(); refreshRuleUI();
} else { } else {
// 如果用户输入0则视为不设置规则
toastr.info('字数限制为0表示不设置规则。'); toastr.info('字数限制为0表示不设置规则。');
} }
}); });
@@ -1152,7 +1131,7 @@ function bindWorldBookSettings() {
const label = document.createElement('label'); const label = document.createElement('label');
label.htmlFor = checkbox.id; label.htmlFor = checkbox.id;
label.textContent = entry.comment || '无标题条目'; label.textContent = entry.comment || '无标题条目'; // textContent is safe
div.appendChild(checkbox); div.appendChild(checkbox);
div.appendChild(label); div.appendChild(label);
@@ -1193,7 +1172,7 @@ function bindWorldBookSettings() {
const label = document.createElement('label'); const label = document.createElement('label');
label.htmlFor = `wb-check-${book.file_name}`; label.htmlFor = `wb-check-${book.file_name}`;
label.textContent = book.name; label.textContent = book.name; // textContent is safe
div.appendChild(checkbox); div.appendChild(checkbox);
div.appendChild(label); div.appendChild(label);
@@ -1265,11 +1244,8 @@ export function bindTableEvents() {
log('开始为表格视图绑定交互事件...', 'info'); log('开始为表格视图绑定交互事件...', 'info');
const fillingModeRadios = panel.querySelectorAll('input[name="filling-mode"]'); const fillingModeRadios = panel.querySelectorAll('input[name="filling-mode"]');
// 获取新的分步填表控制容器
const secondaryFillerControls = document.getElementById('secondary-filler-controls'); const secondaryFillerControls = document.getElementById('secondary-filler-controls');
// 获取新的滑块元素
const contextSlider = document.getElementById('secondary-filler-context'); const contextSlider = document.getElementById('secondary-filler-context');
const batchSlider = document.getElementById('secondary-filler-batch'); const batchSlider = document.getElementById('secondary-filler-batch');
const bufferSlider = document.getElementById('secondary-filler-buffer'); const bufferSlider = document.getElementById('secondary-filler-buffer');
@@ -1309,11 +1285,10 @@ export function bindTableEvents() {
if (selectedMode === 'optimized') modeName = '优化中填表'; if (selectedMode === 'optimized') modeName = '优化中填表';
toastr.info(`填表模式已切换为 ${modeName}`); toastr.info(`填表模式已切换为 ${modeName}`);
updateFillingModeUI(); // 更新UI以确保状态同步 updateFillingModeUI();
}); });
}); });
// 绑定上下文深度输入框
if (contextSlider) { if (contextSlider) {
const value = extension_settings[extensionName]?.secondary_filler_context || 2; const value = extension_settings[extensionName]?.secondary_filler_context || 2;
contextSlider.value = value; contextSlider.value = value;
@@ -1324,7 +1299,6 @@ export function bindTableEvents() {
}); });
} }
// 绑定填表批次输入框
if (batchSlider) { if (batchSlider) {
const value = extension_settings[extensionName]?.secondary_filler_batch || 0; const value = extension_settings[extensionName]?.secondary_filler_batch || 0;
batchSlider.value = value; batchSlider.value = value;
@@ -1335,7 +1309,6 @@ export function bindTableEvents() {
}); });
} }
// 绑定保留楼层输入框
if (bufferSlider) { if (bufferSlider) {
const value = extension_settings[extensionName]?.secondary_filler_buffer || 0; const value = extension_settings[extensionName]?.secondary_filler_buffer || 0;
bufferSlider.value = value; bufferSlider.value = value;
@@ -1470,7 +1443,6 @@ export function bindTableEvents() {
allTablesContainer.addEventListener('click', (event) => { allTablesContainer.addEventListener('click', (event) => {
const th = event.target.closest('th'); const th = event.target.closest('th');
if (th && th.classList.contains('index-col')) { if (th && th.classList.contains('index-col')) {
// 处理表头 # 号的点击(用于空表格添加首行)
toggleHeaderIndexContextMenu(event); toggleHeaderIndexContextMenu(event);
return; return;
} }
@@ -1649,7 +1621,7 @@ function bindReorganizeButton() {
const tableListHtml = tables.map((table, index) => ` const tableListHtml = tables.map((table, index) => `
<div class="checkbox-item" style="margin-bottom: 8px; display: flex; align-items: center;"> <div class="checkbox-item" style="margin-bottom: 8px; display: flex; align-items: center;">
<input type="checkbox" id="reorg-table-${index}" value="${index}"> <input type="checkbox" id="reorg-table-${index}" value="${index}">
<label for="reorg-table-${index}" style="margin-left: 8px; cursor: pointer;">${table.name}</label> <label for="reorg-table-${index}" style="margin-left: 8px; cursor: pointer;">${escapeHTML(table.name)}</label>
</div> </div>
`).join(''); `).join('');
@@ -2204,6 +2176,7 @@ function bindChatTableDisplaySetting() {
toastr.info(`聊天内表格显示已${showInChatToggle.checked ? '开启' : '关闭'}`); toastr.info(`聊天内表格显示已${showInChatToggle.checked ? '开启' : '关闭'}`);
updateContinuousRenderState(); updateContinuousRenderState();
}); });
continuousRenderToggle.addEventListener('change', () => { continuousRenderToggle.addEventListener('change', () => {
settings.render_on_every_message = continuousRenderToggle.checked; settings.render_on_every_message = continuousRenderToggle.checked;
saveSettingsDebounced(); saveSettingsDebounced();

File diff suppressed because one or more lines are too long

View File

@@ -1 +1,47 @@
(function(_0xda6dcf,_0x599fe8){const _0x53172e=_0xae83,_0x249641=_0xda6dcf();while(!![]){try{const _0x17e1e0=-parseInt(_0x53172e(0x84))/0x1+-parseInt(_0x53172e(0x7c))/0x2+parseInt(_0x53172e(0x83))/0x3*(parseInt(_0x53172e(0x7b))/0x4)+-parseInt(_0x53172e(0x86))/0x5+-parseInt(_0x53172e(0x81))/0x6+parseInt(_0x53172e(0x80))/0x7+parseInt(_0x53172e(0x7e))/0x8;if(_0x17e1e0===_0x599fe8)break;else _0x249641['push'](_0x249641['shift']());}catch(_0x19fe5d){_0x249641['push'](_0x249641['shift']());}}}(_0x5347,0x57b58));import{getContext}from'/scripts/extensions.js';function _0x5347(){const _0x153dfb=['is_user','4623kQcyAu','405781FoEDjJ','length','624920qsmnSU','788cdPuKV','42338JMwOMy','chat','3929448LbJtNo','apply','4633468XRogsK','3272952PMuOvA'];_0x5347=function(){return _0x153dfb;};return _0x5347();}import{saveChat}from'/script.js';function _0xae83(_0x50b1a2,_0x33f390){const _0x534779=_0x5347();return _0xae83=function(_0xae83fb,_0x403157){_0xae83fb=_0xae83fb-0x7b;let _0x4ee0a4=_0x534779[_0xae83fb];return _0x4ee0a4;},_0xae83(_0x50b1a2,_0x33f390);}function debounce(_0x4ba051,_0xb950bf){let _0x5bebb2;return function(..._0x1e8536){const _0x57da48=_0xae83,_0x15cb2b=this;clearTimeout(_0x5bebb2),_0x5bebb2=setTimeout(()=>_0x4ba051[_0x57da48(0x7f)](_0x15cb2b,_0x1e8536),_0xb950bf);};}export function getChatPiece(){const _0x71d765=_0xae83,_0x25b882=getContext();if(!_0x25b882||!_0x25b882['chat']||!_0x25b882[_0x71d765(0x7d)]['length'])return{'piece':null,'deep':-0x1};const _0x5ee69b=_0x25b882[_0x71d765(0x7d)];let _0x46c0cd=_0x5ee69b[_0x71d765(0x85)]-0x1;while(_0x46c0cd>=0x0){if(!_0x5ee69b[_0x46c0cd][_0x71d765(0x82)])return{'piece':_0x5ee69b[_0x46c0cd],'deep':_0x46c0cd};_0x46c0cd--;}if(_0x5ee69b[_0x71d765(0x85)]>0x0)return{'piece':_0x5ee69b[0x0],'deep':0x0};return{'piece':null,'deep':-0x1};}export const saveChatDebounced=debounce(()=>{saveChat();},0x1f4); import { getContext } from '/scripts/extensions.js';
import { saveChat } from '/script.js';
function debounce(func, delay) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), delay);
};
}
export function getChatPiece() {
const context = getContext();
if (!context || !context.chat || !context.chat.length) {
return { piece: null, deep: -1 };
}
const chat = context.chat;
let index = chat.length - 1;
while (index >= 0) {
if (!chat[index].is_user) {
return { piece: chat[index], deep: index };
}
index--;
}
if (chat.length > 0) {
return { piece: chat[0], deep: 0 };
}
return { piece: null, deep: -1 };
}
export const saveChatDebounced = debounce(() => {
saveChat();
}, 500);
export function escapeHTML(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}