mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 11:15:50 +00:00
Compare commits
17 Commits
f1f2da1ee4
...
1.7.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67c9708d68 | ||
| b52738248d | |||
| 7a0227cbed | |||
| 74381da9a8 | |||
| 27fcd146ff | |||
| d2e6edb2ac | |||
| 4f0f067a5a | |||
| 20aaad4f7e | |||
| 44d0a46d74 | |||
| 5380bbddcf | |||
| fdcc95ba09 | |||
| c64b35ccf5 | |||
| 833b197ece | |||
| 9736b8abb4 | |||
| 4e6614f9fc | |||
| 90f9feb6de | |||
| ff697236e1 |
@@ -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 { safeLorebooks, safeLorebookEntries, safeUpdateLorebookEntries, compatibleWriteToLorebook } from '../core/tavernhelper-compatibility.js';
|
||||
import { amilyHelper } from '../core/tavern-helper/main.js';
|
||||
import { escapeHTML } from '../utils/utils.js';
|
||||
const { SillyTavern } = window;
|
||||
|
||||
class WorldEditor {
|
||||
@@ -159,7 +160,7 @@ class WorldEditor {
|
||||
row.dataset.bookName = book.name;
|
||||
row.innerHTML = `
|
||||
<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">
|
||||
<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>
|
||||
@@ -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="状态" 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="条目"><input type="text" class="inline-edit" data-field="comment" data-uid="${entry.uid}" value="${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="条目"><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="${escapeHTML(entry.content || '')}">${escapeHTML(entry.content || '')}</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="order" data-uid="${entry.uid}" value="${entry.order}"></div>
|
||||
@@ -541,7 +542,7 @@ class WorldEditor {
|
||||
<div class="copy-dialog">
|
||||
<label for="target-worldbook">选择目标世界书:</label>
|
||||
<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>
|
||||
<div class="info">
|
||||
将复制 ${this.selectedEntries.size} 个条目到目标世界书
|
||||
|
||||
21
core/api.js
21
core/api.js
@@ -47,6 +47,8 @@ const UPDATE_CHECK_URL =
|
||||
|
||||
const MESSAGE_BOARD_URL =
|
||||
"https://amilyservice.amily49.cc/amily2_message_board.json";
|
||||
|
||||
let lastMessageId = null;
|
||||
|
||||
export async function fetchMessageBoardContent() {
|
||||
if (!MESSAGE_BOARD_URL) {
|
||||
@@ -54,11 +56,28 @@ export async function fetchMessageBoardContent() {
|
||||
return null;
|
||||
}
|
||||
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) {
|
||||
throw new Error(`服务器响应异常: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.id) {
|
||||
lastMessageId = data.id;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (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
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
world_names,
|
||||
loadWorldInfo,
|
||||
saveWorldInfo,
|
||||
createNewWorldInfo,
|
||||
import {
|
||||
world_names,
|
||||
loadWorldInfo,
|
||||
saveWorldInfo,
|
||||
createNewWorldInfo,
|
||||
createWorldInfoEntry
|
||||
} from "/scripts/world-info.js";
|
||||
|
||||
@@ -20,9 +20,9 @@ let reloadEditor = () => {
|
||||
console.warn("[Amily助手] 动态导入 reloadEditor 失败,将使用空函数。错误信息:", error.message);
|
||||
}
|
||||
})();
|
||||
import {
|
||||
characters,
|
||||
eventSource,
|
||||
import {
|
||||
characters,
|
||||
eventSource,
|
||||
event_types,
|
||||
chat,
|
||||
reloadCurrentChat,
|
||||
@@ -46,14 +46,14 @@ class AmilyHelper {
|
||||
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;
|
||||
@@ -186,7 +186,7 @@ class AmilyHelper {
|
||||
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}`);
|
||||
}
|
||||
@@ -279,7 +279,7 @@ class AmilyHelper {
|
||||
const should_update_swipe = add_swipes_if_required();
|
||||
update_chat_message();
|
||||
await saveChatConditional();
|
||||
|
||||
|
||||
if (refresh == 'all') {
|
||||
await reloadCurrentChat();
|
||||
} else {
|
||||
@@ -378,12 +378,12 @@ class AmilyHelper {
|
||||
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'
|
||||
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),
|
||||
@@ -393,7 +393,7 @@ class AmilyHelper {
|
||||
keys: entry.key || [],
|
||||
enabled: !entry.disable,
|
||||
constant: entry.constant || false,
|
||||
position: positionMap[entry.position] || 'at_depth_as_system',
|
||||
position: positionMap[entry.position] || 'at_depth_as_system',
|
||||
depth: entry.depth || 998,
|
||||
}));
|
||||
} catch (error) {
|
||||
@@ -421,13 +421,13 @@ class AmilyHelper {
|
||||
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
|
||||
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;
|
||||
}
|
||||
@@ -462,13 +462,13 @@ class AmilyHelper {
|
||||
|
||||
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
|
||||
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 || '新条目',
|
||||
@@ -499,7 +499,7 @@ class AmilyHelper {
|
||||
if (!bookData || !bookData.entries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
let deletedCount = 0;
|
||||
for (const uid of uids) {
|
||||
if (bookData.entries[uid]) {
|
||||
@@ -507,7 +507,7 @@ class AmilyHelper {
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (deletedCount > 0) {
|
||||
await saveWorldInfo(bookName, bookData, true);
|
||||
reloadEditor(bookName);
|
||||
@@ -583,7 +583,7 @@ class AmilyHelper {
|
||||
const char = characters[this_chid];
|
||||
if (!char.data) char.data = {};
|
||||
if (!char.data.extensions) char.data.extensions = {};
|
||||
|
||||
|
||||
// 确保 world 字段是数组
|
||||
let worlds = char.data.extensions.world;
|
||||
if (!Array.isArray(worlds)) {
|
||||
@@ -594,7 +594,7 @@ class AmilyHelper {
|
||||
worlds.push(bookName);
|
||||
char.data.extensions.world = worlds;
|
||||
console.log(`[Amily助手] 已将世界书《${bookName}》绑定到角色 ${char.name}`);
|
||||
|
||||
|
||||
if (typeof saveCharacterDebounced === 'function') {
|
||||
saveCharacterDebounced();
|
||||
return true;
|
||||
@@ -652,12 +652,13 @@ export function makeRequest(request, data) {
|
||||
reject(new Error(`请求 '${request}' 超时 (30秒)`));
|
||||
}, 30000);
|
||||
|
||||
const targetOrigin = window.location.origin === 'null' ? '*' : window.location.origin;
|
||||
window.parent.postMessage({
|
||||
source: 'amily2-iframe-request',
|
||||
request: request,
|
||||
uid: uid,
|
||||
data: data
|
||||
}, window.location.origin);
|
||||
}, targetOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -676,9 +677,9 @@ export function registerApiHandler(request, handler) {
|
||||
|
||||
export function initializeApiListener() {
|
||||
window.addEventListener('message', async (event) => {
|
||||
// 安全修复:严格验证消息来源,防止跨源消息伪造
|
||||
// 'null' 是 srcdoc 或 blob URL iframe 的 origin
|
||||
if (event.origin !== window.location.origin && event.origin !== 'null') {
|
||||
|
||||
if (window.location.origin !== 'null' && event.origin !== window.location.origin) {
|
||||
console.warn(`[Amily2-IframeAPI] 拒绝来自未知来源的请求: ${event.origin}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -696,6 +697,7 @@ export function initializeApiListener() {
|
||||
|
||||
const handler = apiHandlers.get(data.request);
|
||||
const callbackRequest = `${data.request}_callback`;
|
||||
const targetOrigin = event.origin === 'null' ? '*' : event.origin;
|
||||
|
||||
if (!handler) {
|
||||
console.error(`[Amily2-IframeAPI] 收到未知请求: ${data.request}`);
|
||||
@@ -703,7 +705,7 @@ export function initializeApiListener() {
|
||||
request: callbackRequest,
|
||||
uid: data.uid,
|
||||
error: `未注册请求 '${data.request}' 的处理器`
|
||||
}, event.origin); // 安全修复:回复到确切的来源,而不是 '*'
|
||||
}, targetOrigin);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -713,15 +715,15 @@ export function initializeApiListener() {
|
||||
request: callbackRequest,
|
||||
uid: data.uid,
|
||||
result: result
|
||||
}, event.origin); // 安全修复:回复到确切的来源
|
||||
}, targetOrigin);
|
||||
} catch (error) {
|
||||
console.error(`[Amily2-IframeAPI] 执行处理器 '${data.request}' 时出错:`, error);
|
||||
event.source.postMessage({
|
||||
request: callbackRequest,
|
||||
uid: data.uid,
|
||||
error: error.message || String(error)
|
||||
}, event.origin); // 安全修复:回复到确切的来源
|
||||
}, targetOrigin);
|
||||
}
|
||||
});
|
||||
console.log('[Amily2-IframeAPI] 主窗口监听器已初始化');
|
||||
console.log('[Amily2-IframeAPI] 主窗口监听器已初始化 (已启用安全验证)');
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { testSybdApiConnection, fetchSybdModels } from '../core/api/SybdApi.js';
|
||||
import { handleFileUpload, processNovel } from './index.js';
|
||||
import { reorganizeEntriesByHeadings, loadDatabaseFiles } from './executor.js';
|
||||
import { SETTINGS_KEY as PRESET_SETTINGS_KEY } from '../PresetSettings/config.js';
|
||||
import { escapeHTML } from '../utils/utils.js';
|
||||
|
||||
const moduleState = {
|
||||
selectedWorldBook: '',
|
||||
@@ -267,12 +268,12 @@ async function renderWorldBookEntries() {
|
||||
}
|
||||
|
||||
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>`;
|
||||
} catch {
|
||||
return `<pre>${content}</pre>`;
|
||||
return `<pre>${escapeHTML(content)}</pre>`;
|
||||
}
|
||||
}
|
||||
if (trimmedContent.includes('|') && trimmedContent.includes('\n')) {
|
||||
@@ -283,7 +284,7 @@ async function renderWorldBookEntries() {
|
||||
let isHeaderRow = true;
|
||||
rows.forEach(rowStr => {
|
||||
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) {
|
||||
header += `<tr>${cells.replace(/<td>/g, '<th>').replace(/<\/td>/g, '</th>')}</tr>`;
|
||||
isHeaderRow = false;
|
||||
@@ -293,15 +294,15 @@ async function renderWorldBookEntries() {
|
||||
});
|
||||
return `<table class="table-render"><thead>${header}</thead><tbody>${body}</tbody></table>`;
|
||||
} catch {
|
||||
return `<pre>${content}</pre>`;
|
||||
return `<pre>${escapeHTML(content)}</pre>`;
|
||||
}
|
||||
}
|
||||
return `<pre>${content}</pre>`;
|
||||
return `<pre>${escapeHTML(content)}</pre>`;
|
||||
};
|
||||
|
||||
entryElement.innerHTML = `
|
||||
<div class="entry-header">
|
||||
<strong class="entry-title">${title}</strong>
|
||||
<strong class="entry-title">${escapeHTML(title)}</strong>
|
||||
<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 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;
|
||||
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('');
|
||||
|
||||
resetProcessing();
|
||||
@@ -563,7 +564,7 @@ function bindNovelProcessEvents() {
|
||||
fileInput.addEventListener('change', (event) => {
|
||||
const file = event.target.files[0];
|
||||
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) => {
|
||||
fileContent = content;
|
||||
updateChunks();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getPresetPrompts, getMixedOrder } from '../PresetSettings/index.js';
|
||||
import { generateRandomSeed } from '../core/api.js';
|
||||
import { safeLorebookEntries, safeUpdateLorebookEntries, compatibleWriteToLorebook } from '../core/tavernhelper-compatibility.js';
|
||||
import { loadWorldInfo, saveWorldInfo, createWorldInfoEntry } from "/scripts/world-info.js";
|
||||
import { escapeHTML } from '../utils/utils.js';
|
||||
|
||||
function buildContextFromEntries(entries) {
|
||||
if (!entries || entries.length === 0) {
|
||||
@@ -310,7 +311,7 @@ export async function loadDatabaseFiles() {
|
||||
document.dispatchEvent(event);
|
||||
|
||||
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) {
|
||||
console.error(`Error processing file ${file.name}:`, error);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Amily2号聊天优化助手",
|
||||
"display_name": "Amily2号助手",
|
||||
"version": "1.7.5",
|
||||
"version": "1.7.6",
|
||||
"author": "Wx-2025",
|
||||
"description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
|
||||
"minSillyTavernVersion": "1.10.0",
|
||||
@@ -46,5 +46,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { safeCharLorebooks, safeLorebookEntries } from '../core/tavernhelper-com
|
||||
import { characters, this_chid, eventSource, event_types } from "/script.js";
|
||||
import { fetchNccsModels, testNccsApiConnection } from '../core/api/NccsApi.js';
|
||||
import { showGraphVisualization } from '../core/relationship-graph/visualizer.js';
|
||||
import { escapeHTML } from '../utils/utils.js';
|
||||
|
||||
const isTouchDevice = () => window.matchMedia('(pointer: coarse)').matches;
|
||||
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')) {
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', closeMenu, true);
|
||||
@@ -178,15 +178,15 @@ function showInputDialog({ title, label, currentValue, placeholder, onSave }) {
|
||||
<dialog class="popup custom-input-dialog">
|
||||
<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;">
|
||||
<i class="fas fa-edit" style="color: #9e8aff;"></i> ${title}
|
||||
<i class="fas fa-edit" style="color: #9e8aff;"></i> ${escapeHTML(title)}
|
||||
</h4>
|
||||
<div class="popup-content" style="padding: 20px 10px;">
|
||||
<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"
|
||||
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;"
|
||||
placeholder="${placeholder}">
|
||||
placeholder="${escapeHTML(placeholder)}">
|
||||
<small style="color: #aaa; font-style: italic;">提示:输入内容将用于更新项目。</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -321,7 +321,7 @@ export function renderTables() {
|
||||
}
|
||||
|
||||
const highlights = TableManager.getHighlights();
|
||||
const updatedTables = TableManager.getUpdatedTables(); // 【V15.2 新增】获取被更新的表格
|
||||
const updatedTables = TableManager.getUpdatedTables();
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
const placeholder = document.getElementById('add-table-placeholder');
|
||||
@@ -334,9 +334,9 @@ export function renderTables() {
|
||||
header.className = 'amily2-table-header-container';
|
||||
const title = document.createElement('h3');
|
||||
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');
|
||||
controls.className = 'table-controls';
|
||||
|
||||
@@ -368,7 +368,6 @@ export function renderTables() {
|
||||
if (tableData.headers) {
|
||||
tableData.headers.forEach((_, colIndex) => {
|
||||
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;
|
||||
col.style.width = `${colWidth}px`;
|
||||
colgroup.appendChild(col);
|
||||
@@ -376,21 +375,14 @@ export function renderTables() {
|
||||
}
|
||||
tableElement.appendChild(colgroup);
|
||||
|
||||
// Explicitly calculate and set the total table width to override CSS conflicts
|
||||
let totalWidth = 0;
|
||||
const cols = colgroup.querySelectorAll('col');
|
||||
cols.forEach(col => {
|
||||
totalWidth += parseInt(col.style.width, 10);
|
||||
});
|
||||
// Set min-width instead of fixed width to allow expansion
|
||||
tableElement.style.minWidth = '100%';
|
||||
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`;
|
||||
// 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.width = '100%';
|
||||
}
|
||||
@@ -403,8 +395,7 @@ export function renderTables() {
|
||||
indexTh.textContent = '#';
|
||||
indexTh.style.cursor = 'pointer';
|
||||
indexTh.title = '点击添加第一行';
|
||||
|
||||
// 为表头的 # 号添加特殊的上下文菜单(仅在表格为空时显示)
|
||||
|
||||
if (!tableData.rows || tableData.rows.length === 0) {
|
||||
const headerMenu = document.createElement('div');
|
||||
headerMenu.className = 'amily2-context-menu amily2-header-menu';
|
||||
@@ -422,13 +413,11 @@ export function renderTables() {
|
||||
headerMenu.appendChild(addRowButton);
|
||||
indexTh.appendChild(headerMenu);
|
||||
|
||||
// 为表头添加直接的点击事件监听器
|
||||
indexTh.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('Header # clicked for table', tableIndex);
|
||||
|
||||
// 直接执行添加行操作
|
||||
|
||||
TableManager.addRow(tableIndex);
|
||||
renderTables();
|
||||
toastr.success('已添加第一行');
|
||||
@@ -444,7 +433,7 @@ export function renderTables() {
|
||||
|
||||
const headerContent = document.createElement('span');
|
||||
headerContent.className = 'amily2-header-text';
|
||||
headerContent.textContent = headerText;
|
||||
headerContent.textContent = headerText; // textContent is safe
|
||||
th.appendChild(headerContent);
|
||||
|
||||
const menu = document.createElement('div');
|
||||
@@ -611,16 +600,13 @@ export function renderTables() {
|
||||
TableManager.insertRow(tableIndex, rowIndex, 'below');
|
||||
break;
|
||||
case 'delete-row':
|
||||
// 【延迟删除】不再需要确认,因为操作是可逆的
|
||||
TableManager.deleteRow(tableIndex, rowIndex);
|
||||
break;
|
||||
case 'restore-row':
|
||||
TableManager.restoreRow(tableIndex, rowIndex);
|
||||
break;
|
||||
}
|
||||
// For instant feedback, re-render is needed for delete/restore
|
||||
if (action === 'delete-row' || action === 'restore-row') {
|
||||
// The manager functions now handle their own re-rendering
|
||||
} else {
|
||||
renderTables();
|
||||
}
|
||||
@@ -634,10 +620,9 @@ export function renderTables() {
|
||||
|
||||
const cellContent = document.createElement('div');
|
||||
cellContent.className = 'amily2-cell-content';
|
||||
cellContent.textContent = cellData;
|
||||
cellContent.textContent = cellData;
|
||||
cell.appendChild(cellContent);
|
||||
|
||||
// 【延迟删除】如果行正在待删除,则禁止编辑
|
||||
if (rowStatus !== 'pending-deletion' && !isTouchDevice()) {
|
||||
cell.setAttribute('contenteditable', 'true');
|
||||
}
|
||||
@@ -662,7 +647,6 @@ export function renderTables() {
|
||||
container.appendChild(placeholder);
|
||||
}
|
||||
|
||||
// Also update the in-chat table whenever the main tables are re-rendered
|
||||
updateOrInsertTableInChat();
|
||||
}
|
||||
|
||||
@@ -740,7 +724,6 @@ function openRuleEditor(tableIndex) {
|
||||
if (!tables || !tables[tableIndex]) return;
|
||||
const table = tables[tableIndex];
|
||||
|
||||
// 兼容旧数据结构
|
||||
if (table.charLimitRule && !table.charLimitRules) {
|
||||
table.charLimitRules = {};
|
||||
if (table.charLimitRule.columnIndex !== -1) {
|
||||
@@ -754,7 +737,7 @@ function openRuleEditor(tableIndex) {
|
||||
const header = table.headers[colIndex] || `未知列 (${colIndex})`;
|
||||
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;">
|
||||
<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="删除此规则">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
@@ -765,9 +748,8 @@ function openRuleEditor(tableIndex) {
|
||||
|
||||
const getColumnOptions = (rules) => {
|
||||
return table.headers.map((header, index) => {
|
||||
// 如果该列已存在规则,则不应出现在下拉菜单中
|
||||
if (rules[index]) return '';
|
||||
return `<option value="${index}">${header}</option>`;
|
||||
return `<option value="${index}">${escapeHTML(header)}</option>`;
|
||||
}).join('');
|
||||
};
|
||||
|
||||
@@ -775,7 +757,7 @@ function openRuleEditor(tableIndex) {
|
||||
<dialog class="popup wide_dialogue_popup large_dialogue_popup">
|
||||
<div class="popup-body">
|
||||
<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>
|
||||
<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;">
|
||||
@@ -864,8 +846,7 @@ function openRuleEditor(tableIndex) {
|
||||
toastr.warning('请选择一个列。');
|
||||
return;
|
||||
}
|
||||
|
||||
// 允许输入0,但0意味着“无限制”,所以我们不添加规则。
|
||||
|
||||
if (isNaN(limitValue) || limitValue < 0) {
|
||||
toastr.warning('请输入一个有效的字数限制(大于等于0)。');
|
||||
return;
|
||||
@@ -874,12 +855,10 @@ function openRuleEditor(tableIndex) {
|
||||
const currentRules = JSON.parse(dialogElement.find('#current-char-limit-rules').attr('data-rules') || '{}');
|
||||
|
||||
if (limitValue > 0) {
|
||||
// 只有当限制大于0时,才添加或更新规则
|
||||
currentRules[selectedColumn] = limitValue;
|
||||
dialogElement.find('#current-char-limit-rules').attr('data-rules', JSON.stringify(currentRules));
|
||||
refreshRuleUI();
|
||||
} else {
|
||||
// 如果用户输入0,则视为不设置规则
|
||||
toastr.info('字数限制为0表示不设置规则。');
|
||||
}
|
||||
});
|
||||
@@ -1152,7 +1131,7 @@ function bindWorldBookSettings() {
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = checkbox.id;
|
||||
label.textContent = entry.comment || '无标题条目';
|
||||
label.textContent = entry.comment || '无标题条目'; // textContent is safe
|
||||
|
||||
div.appendChild(checkbox);
|
||||
div.appendChild(label);
|
||||
@@ -1193,7 +1172,7 @@ function bindWorldBookSettings() {
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `wb-check-${book.file_name}`;
|
||||
label.textContent = book.name;
|
||||
label.textContent = book.name; // textContent is safe
|
||||
|
||||
div.appendChild(checkbox);
|
||||
div.appendChild(label);
|
||||
@@ -1265,11 +1244,8 @@ export function bindTableEvents() {
|
||||
log('开始为表格视图绑定交互事件...', 'info');
|
||||
|
||||
const fillingModeRadios = panel.querySelectorAll('input[name="filling-mode"]');
|
||||
|
||||
// 获取新的分步填表控制容器
|
||||
const secondaryFillerControls = document.getElementById('secondary-filler-controls');
|
||||
|
||||
// 获取新的滑块元素
|
||||
|
||||
const contextSlider = document.getElementById('secondary-filler-context');
|
||||
const batchSlider = document.getElementById('secondary-filler-batch');
|
||||
const bufferSlider = document.getElementById('secondary-filler-buffer');
|
||||
@@ -1309,11 +1285,10 @@ export function bindTableEvents() {
|
||||
if (selectedMode === 'optimized') modeName = '优化中填表';
|
||||
|
||||
toastr.info(`填表模式已切换为 ${modeName}。`);
|
||||
updateFillingModeUI(); // 更新UI以确保状态同步
|
||||
updateFillingModeUI();
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定上下文深度输入框
|
||||
if (contextSlider) {
|
||||
const value = extension_settings[extensionName]?.secondary_filler_context || 2;
|
||||
contextSlider.value = value;
|
||||
@@ -1324,7 +1299,6 @@ export function bindTableEvents() {
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定填表批次输入框
|
||||
if (batchSlider) {
|
||||
const value = extension_settings[extensionName]?.secondary_filler_batch || 0;
|
||||
batchSlider.value = value;
|
||||
@@ -1335,7 +1309,6 @@ export function bindTableEvents() {
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定保留楼层输入框
|
||||
if (bufferSlider) {
|
||||
const value = extension_settings[extensionName]?.secondary_filler_buffer || 0;
|
||||
bufferSlider.value = value;
|
||||
@@ -1470,7 +1443,6 @@ export function bindTableEvents() {
|
||||
allTablesContainer.addEventListener('click', (event) => {
|
||||
const th = event.target.closest('th');
|
||||
if (th && th.classList.contains('index-col')) {
|
||||
// 处理表头 # 号的点击(用于空表格添加首行)
|
||||
toggleHeaderIndexContextMenu(event);
|
||||
return;
|
||||
}
|
||||
@@ -1649,7 +1621,7 @@ function bindReorganizeButton() {
|
||||
const tableListHtml = tables.map((table, index) => `
|
||||
<div class="checkbox-item" style="margin-bottom: 8px; display: flex; align-items: center;">
|
||||
<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>
|
||||
`).join('');
|
||||
|
||||
@@ -2204,6 +2176,7 @@ function bindChatTableDisplaySetting() {
|
||||
toastr.info(`聊天内表格显示已${showInChatToggle.checked ? '开启' : '关闭'}。`);
|
||||
updateContinuousRenderState();
|
||||
});
|
||||
|
||||
continuousRenderToggle.addEventListener('change', () => {
|
||||
settings.render_on_every_message = continuousRenderToggle.checked;
|
||||
saveSettingsDebounced();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user