From e7ecf4e4360edc5e3f64ecac1764d3ee1f179127 Mon Sep 17 00:00:00 2001
From: Wx-2025 <351320169@qq.com>
Date: Tue, 6 Jan 2026 10:38:44 +0800
Subject: [PATCH] Update table-bindings.js
---
ui/table-bindings.js | 2865 +++++++++++++++++++++++++++++++++---------
1 file changed, 2256 insertions(+), 609 deletions(-)
diff --git a/ui/table-bindings.js b/ui/table-bindings.js
index 75bd1ba..2af54f8 100644
--- a/ui/table-bindings.js
+++ b/ui/table-bindings.js
@@ -1,654 +1,2301 @@
-import { getMemoryState, getHighlights } from '../core/table-system/manager.js';
-import { extension_settings } from '/scripts/extensions.js';
+import * as TableManager from '../core/table-system/manager.js';
+import { log } from '../core/table-system/logger.js';
+import { extension_settings, getContext } from '/scripts/extensions.js';
import { extensionName } from '../utils/settings.js';
-import { getContext } from '/scripts/extensions.js';
+import { updateOrInsertTableInChat } from './message-table-renderer.js';
+import { saveSettingsDebounced } from '/script.js';
+import { startBatchFilling } from '../core/table-system/batch-filler.js';
+import { showHtmlModal } from './page-window.js';
+import { DEFAULT_AI_RULE_TEMPLATE, DEFAULT_AI_FLOW_TEMPLATE } from '../core/table-system/settings.js';
+import { world_names, loadWorldInfo } from '/scripts/world-info.js';
+import { safeCharLorebooks, safeLorebookEntries } from '../core/tavernhelper-compatibility.js';
+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 TABLE_CONTAINER_ID = 'amily2-chat-table-container';
const isTouchDevice = () => window.matchMedia('(pointer: coarse)').matches;
+const getAllTablesContainer = () => document.getElementById('all-tables-container');
-// 【V155.3】注入真正的游戏UI样式 (侧边栏+内容区)
-function injectChatTableStyles() {
- if (document.getElementById('amily2-chat-table-styles')) return;
- const style = document.createElement('style');
- style.id = 'amily2-chat-table-styles';
- style.textContent = `
- /* 主容器:游戏面板风格 */
- #amily2-chat-table-container {
- display: flex !important; /* 强制 Flex 布局 */
- flex-direction: row !important; /* 强制横向排列 */
- min-height: 300px;
- max-height: 80vh;
- background: rgba(12, 14, 20, 0.95);
- border: 2px solid #3a4a5e;
- border-radius: 8px;
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.8), inset 0 0 30px rgba(0, 0, 0, 0.5);
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- color: #c0c0c0;
- margin-top: 15px;
- overflow: hidden;
- position: relative;
- resize: vertical;
- }
+let isResizing = false;
+let activeTableIndex = 0; // 【V155.0】当前激活的表格索引
- /* 装饰性角落 */
- #amily2-chat-table-container::before {
- content: '';
- position: absolute;
- top: 0; left: 0;
- width: 20px; height: 20px;
- border-top: 2px solid #00bfff;
- border-left: 2px solid #00bfff;
- border-radius: 6px 0 0 0;
- z-index: 2;
- }
- #amily2-chat-table-container::after {
- content: '';
- position: absolute;
- bottom: 0; right: 0;
- width: 20px; height: 20px;
- border-bottom: 2px solid #00bfff;
- border-right: 2px solid #00bfff;
- border-radius: 0 0 6px 0;
- z-index: 2;
- }
- /* 侧边栏:导航菜单 */
- .amily2-game-sidebar {
- width: 140px; /* 加宽以显示文字 */
- background: rgba(20, 25, 35, 0.9);
- border-right: 1px solid #3a4a5e;
- display: flex;
- flex-direction: column;
- align-items: stretch; /* 拉伸以填满宽度 */
- padding: 10px;
- gap: 8px;
- overflow-y: auto;
- flex-shrink: 0;
- }
+function toggleRowContextMenu(event) {
+ event.preventDefault();
+ event.stopPropagation();
- .amily2-game-tab {
- height: 40px;
- border-radius: 6px;
- display: flex;
- justify-content: flex-start; /* 左对齐 */
- align-items: center;
- padding: 0 10px;
- cursor: pointer;
- color: #7a8a9e;
- transition: all 0.2s ease;
- position: relative;
- border: 1px solid transparent;
- font-size: 0.9em;
- font-weight: 600;
- }
+ const targetTd = event.target.closest('td.index-col');
+ if (!targetTd) return;
- .amily2-game-tab i {
- width: 24px;
- text-align: center;
- margin-right: 8px;
- }
+ const tableWrapper = targetTd.closest('.amily2-table-wrapper');
+ if (!tableWrapper) return;
- .amily2-game-tab:hover {
- color: #e0e0e0;
- background: rgba(255, 255, 255, 0.05);
- }
-
- .amily2-game-tab.active {
- color: #fff;
- background: linear-gradient(90deg, rgba(0, 191, 255, 0.25), transparent);
- border-left: 3px solid #00bfff;
- text-shadow: 0 0 8px rgba(0, 191, 255, 0.8);
- box-shadow: inset 5px 0 10px -5px rgba(0, 191, 255, 0.3);
- }
-
- .amily2-game-tab.active::after {
- display: none; /* 移除原来的三角形指示器 */
- }
-
- /* 内容区 */
- .amily2-game-content {
- position: absolute;
- left: 140px; top: 0; bottom: 0; right: 0;
- overflow: hidden;
- background: transparent;
- display: flex;
- flex-direction: column;
- z-index: 10;
- }
-
- /* 扫描线效果 */
- .amily2-game-content::before {
- content: '';
- position: absolute;
- top: 0; left: 0; right: 0; bottom: 0;
- background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%);
- background-size: 100% 4px;
- pointer-events: none;
- z-index: 1;
- opacity: 0.3;
- }
-
- .amily2-game-panel {
- display: none;
- width: 100%;
- height: 100%;
- padding: 20px;
- overflow-y: auto;
- box-sizing: border-box;
- position: relative;
- z-index: 10; /* 确保最高层级 */
- }
-
- .amily2-game-panel.active {
- display: block !important;
- animation: amily2-panel-fade 0.3s ease-out;
- }
-
- @keyframes amily2-panel-fade {
- from { opacity: 0; transform: translateY(5px); }
- to { opacity: 1; transform: translateY(0); }
- }
-
- /* 面板标题 */
- .amily2-panel-title {
- font-size: 1.2em;
- font-weight: bold;
- color: #00bfff;
- margin-bottom: 15px;
- padding-bottom: 8px;
- border-bottom: 2px solid rgba(0, 191, 255, 0.3);
- text-transform: uppercase;
- letter-spacing: 1px;
- display: flex;
- align-items: center;
- }
-
- .amily2-panel-title i {
- margin-right: 10px;
- }
-
- /* 卡片式布局 (RPG风格) */
- .amily2-game-cards-container {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
-
- .amily2-game-card {
- background: rgba(30, 35, 45, 0.6);
- border: 1px solid rgba(100, 149, 237, 0.15);
- border-radius: 6px;
- padding: 12px;
- position: relative;
- transition: all 0.2s ease;
- }
-
- .amily2-game-card:hover {
- background: rgba(40, 50, 70, 0.8);
- border-color: rgba(0, 191, 255, 0.4);
- box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
- transform: translateX(2px);
- }
-
- .amily2-game-card::before {
- content: '';
- position: absolute;
- left: 0; top: 10px; bottom: 10px;
- width: 3px;
- background: #00bfff;
- border-radius: 0 2px 2px 0;
- opacity: 0.5;
- }
-
- .amily2-card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 8px;
- padding-bottom: 5px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
- }
-
- .amily2-card-title {
- font-size: 1.1em;
- font-weight: bold;
- color: #00bfff;
- text-shadow: 0 0 5px rgba(0, 191, 255, 0.3);
- }
-
- .amily2-card-body {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
- gap: 8px 15px;
- }
-
- .amily2-card-attr {
- display: flex;
- flex-direction: column;
- font-size: 0.9em;
- }
-
- .amily2-card-label {
- color: #5a6a7e;
- font-size: 0.8em;
- text-transform: uppercase;
- margin-bottom: 2px;
- }
-
- .amily2-card-value {
- color: #e0e0e0;
- }
-
- /* 滚动条 */
- .amily2-game-sidebar::-webkit-scrollbar,
- .amily2-game-panel::-webkit-scrollbar {
- width: 4px;
- }
- .amily2-game-sidebar::-webkit-scrollbar-track,
- .amily2-game-panel::-webkit-scrollbar-track {
- background: rgba(0, 0, 0, 0.2);
- }
- .amily2-game-sidebar::-webkit-scrollbar-thumb,
- .amily2-game-panel::-webkit-scrollbar-thumb {
- background: #3a4a5e;
- border-radius: 2px;
- }
-
- /* 移动端适配 */
- @media (max-width: 768px) {
- #amily2-chat-table-container {
- flex-direction: column;
- height: auto;
- min-height: 400px;
- }
- .amily2-game-sidebar {
- width: 100% !important;
- height: 50px !important;
- flex-direction: row;
- padding: 0 10px;
- border-right: none;
- border-bottom: 1px solid #3a4a5e;
- overflow-x: auto;
- top: 30px !important;
- bottom: auto !important;
- }
- .amily2-game-content {
- left: 0 !important;
- top: 80px !important;
- }
- .amily2-game-tab {
- flex-shrink: 0;
- }
- .amily2-game-tab.active::after {
- right: auto;
- bottom: -8px;
- top: auto;
- left: 50%;
- transform: translateX(-50%) rotate(90deg);
+ const isActive = targetTd.classList.contains('amily2-menu-open');
+ document.querySelectorAll('.amily2-menu-open').forEach(openEl => {
+ if (openEl !== targetTd) {
+ openEl.classList.remove('amily2-menu-open');
+ openEl.style.zIndex = '';
+ openEl.style.position = '';
+ const otherWrapper = openEl.closest('.amily2-table-wrapper');
+ if (otherWrapper) {
+ otherWrapper.style.overflowX = 'auto';
+ otherWrapper.style.zIndex = '';
+ otherWrapper.style.position = '';
}
}
-
- /* 折叠功能样式 */
- #amily2-chat-table-container.collapsed {
- min-height: 30px !important;
- height: 30px !important;
- resize: none !important;
- overflow: hidden !important;
- border-bottom: none;
- }
-
- #amily2-chat-table-container.collapsed .amily2-game-sidebar,
- #amily2-chat-table-container.collapsed .amily2-game-content {
- display: none !important;
- }
-
- .amily2-table-toggle {
- position: absolute;
- top: 0; left: 0; right: 0;
- height: 30px;
- background: rgba(20, 25, 35, 0.95);
- border-bottom: 1px solid #3a4a5e;
- color: #00bfff;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- z-index: 100;
- font-size: 0.85em;
- font-weight: bold;
- text-transform: uppercase;
- letter-spacing: 1px;
- transition: all 0.2s;
- }
-
- .amily2-table-toggle:hover {
- background: rgba(40, 50, 70, 1);
- color: #fff;
- }
-
- .amily2-table-toggle i {
- margin-right: 8px;
- transition: transform 0.3s;
- }
-
- #amily2-chat-table-container:not(.collapsed) .amily2-table-toggle i {
- transform: rotate(180deg);
- }
- `;
- document.head.appendChild(style);
-}
-
-function getTableIcon(tableName) {
- const lowerName = tableName.toLowerCase();
- if (lowerName.includes('时空') || lowerName.includes('时间') || lowerName.includes('time') || lowerName.includes('clock')) return 'fa-clock';
- if (lowerName.includes('角色') || lowerName.includes('人物') || lowerName.includes('char') || lowerName.includes('person')) return 'fa-user';
- if (lowerName.includes('关系') || lowerName.includes('relation') || lowerName.includes('social')) return 'fa-users';
- if (lowerName.includes('任务') || lowerName.includes('目标') || lowerName.includes('quest') || lowerName.includes('mission')) return 'fa-tasks';
- if (lowerName.includes('物品') || lowerName.includes('道具') || lowerName.includes('item') || lowerName.includes('inventory')) return 'fa-box-open';
- if (lowerName.includes('技能') || lowerName.includes('能力') || lowerName.includes('skill') || lowerName.includes('ability')) return 'fa-bolt';
- if (lowerName.includes('设定') || lowerName.includes('世界') || lowerName.includes('setting') || lowerName.includes('world')) return 'fa-book';
- if (lowerName.includes('总结') || lowerName.includes('大纲') || lowerName.includes('summary') || lowerName.includes('outline')) return 'fa-file-alt';
- if (lowerName.includes('日志') || lowerName.includes('log') || lowerName.includes('record')) return 'fa-clipboard-list';
- return 'fa-table';
-}
-
-function renderTablesToHtml(tables, highlights) {
- if (!tables || tables.length === 0) {
- return '';
- }
-
- // 过滤掉空表格
- const activeTables = tables.map((t, i) => ({...t, originalIndex: i})).filter(t => t.rows && t.rows.length > 0);
- if (activeTables.length === 0) return '';
-
- // Toggle 按钮
- const toggleHtml = `
-
-
- 表格面板 / Data Panel
-
- `;
-
- // 使用绝对定位强制布局,这是最稳妥的方式,不受 Flex 环境影响
- // top: 30px 留给 toggle 按钮
- let sidebarHtml = '';
+ targetTd.classList.toggle('amily2-menu-open');
- return `${toggleHtml}${sidebarHtml}${contentHtml}
`;
-}
+ if (targetTd.classList.contains('amily2-menu-open')) {
+ tableWrapper.style.overflowX = 'visible';
+ tableWrapper.style.position = 'relative';
+ tableWrapper.style.zIndex = '10';
+ targetTd.style.position = 'relative';
+ targetTd.style.zIndex = '100';
+ } else {
+ tableWrapper.style.overflowX = 'auto';
+ tableWrapper.style.position = '';
+ tableWrapper.style.zIndex = '';
+ targetTd.style.position = '';
+ targetTd.style.zIndex = '';
+ }
-function removeTableContainer() {
- const existingContainer = document.getElementById(TABLE_CONTAINER_ID);
- if (existingContainer) {
- existingContainer.remove();
+ const closeMenu = (e) => {
+ if (!targetTd.contains(e.target)) {
+ targetTd.classList.remove('amily2-menu-open');
+ targetTd.style.position = '';
+ targetTd.style.zIndex = '';
+ tableWrapper.style.overflowX = 'auto';
+ tableWrapper.style.position = '';
+ tableWrapper.style.zIndex = '';
+ document.removeEventListener('click', closeMenu, true);
+ }
+ };
+
+ if (targetTd.classList.contains('amily2-menu-open')) {
+ setTimeout(() => {
+ document.addEventListener('click', closeMenu, true);
+ }, 0);
}
}
-function bindSwipePreventer(container) {
- if (!isTouchDevice()) {
+
+function toggleColumnContextMenu(event) {
+ if (isResizing || event.target.classList.contains('amily2-resizer')) {
return;
}
+ event.preventDefault();
+ event.stopPropagation();
- let touchstartX = 0;
- let touchstartY = 0;
+ const targetTh = event.target.closest('th');
+ if (!targetTh) return;
- container.addEventListener('touchstart', function(event) {
- touchstartX = event.changedTouches[0].screenX;
- touchstartY = event.changedTouches[0].screenY;
- }, { passive: true });
+ const tableWrapper = targetTh.closest('.amily2-table-wrapper');
+ if (!tableWrapper) return;
- container.addEventListener('touchmove', function(event) {
- const touchendX = event.changedTouches[0].screenX;
- const touchendY = event.changedTouches[0].screenY;
+ const isActive = targetTh.classList.contains('amily2-menu-open');
- const deltaX = Math.abs(touchendX - touchstartX);
- const deltaY = Math.abs(touchendY - touchstartY);
-
- if (deltaX > deltaY) {
- event.stopPropagation();
+ document.querySelectorAll('th.amily2-menu-open').forEach(openTh => {
+ if (openTh !== targetTh) {
+ openTh.classList.remove('amily2-menu-open');
+ const otherWrapper = openTh.closest('.amily2-table-wrapper');
+ if (otherWrapper) {
+ otherWrapper.style.overflowX = 'auto';
+ otherWrapper.style.zIndex = '';
+ otherWrapper.style.position = '';
+ }
}
- }, { passive: false });
+ });
+
+ targetTh.classList.toggle('amily2-menu-open');
+
+ if (targetTh.classList.contains('amily2-menu-open')) {
+ tableWrapper.style.overflowX = 'visible';
+ tableWrapper.style.position = 'relative';
+ tableWrapper.style.zIndex = '10';
+ } else {
+ tableWrapper.style.overflowX = 'auto';
+ tableWrapper.style.position = '';
+ tableWrapper.style.zIndex = '';
+ }
+
+ const closeMenu = (e) => {
+ if (!targetTh.contains(e.target)) {
+ targetTh.classList.remove('amily2-menu-open');
+ tableWrapper.style.overflowX = 'auto';
+ tableWrapper.style.position = '';
+ tableWrapper.style.zIndex = '';
+ document.removeEventListener('click', closeMenu, true);
+ }
+ };
+
+ if (targetTh.classList.contains('amily2-menu-open')) {
+ setTimeout(() => {
+ document.addEventListener('click', closeMenu, true);
+ }, 0);
+ }
}
-export function updateOrInsertTableInChat() {
- injectChatTableStyles(); // 确保样式已注入
+
+function toggleHeaderIndexContextMenu(event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const targetTh = event.target.closest('th.index-col');
+ if (!targetTh) return;
+
+ const menu = targetTh.querySelector('.amily2-context-menu');
+ if (!menu) return;
+
+ const isActive = menu.classList.contains('amily2-menu-active');
+
+ document.querySelectorAll('.amily2-context-menu.amily2-menu-active').forEach(activeMenu => {
+ activeMenu.classList.remove('amily2-menu-active');
+ });
+
+ if (!isActive) {
+ menu.classList.add('amily2-menu-active');
+ }
+
+ const closeMenu = (e) => {
+ if (!menu.contains(e.target)) {
+ menu.classList.remove('amily2-menu-active');
+ document.removeEventListener('click', closeMenu, true);
+ }
+ };
setTimeout(() => {
- const context = getContext();
- if (!context || !context.chat || context.chat.length < 2) {
- removeTableContainer();
- return;
- }
-
- const settings = extension_settings[extensionName];
- removeTableContainer();
-
- if (!settings || !settings.show_table_in_chat) {
- return;
- }
-
- const tables = getMemoryState();
-
- if (!tables || tables.every(t => !t.rows || t.rows.length === 0)) {
- return;
- }
-
- const highlights = getHighlights();
- const htmlContent = renderTablesToHtml(tables, highlights);
-
- if (!htmlContent) {
- return;
- }
-
- const lastMessage = document.querySelector('.last_mes .mes_text');
- if (lastMessage) {
- const container = document.createElement('div');
- container.id = TABLE_CONTAINER_ID;
-
- // 强制内联样式,使用相对定位作为绝对定位子元素的锚点
- container.style.cssText = `
- display: block !important; /* 不再依赖 Flex */
- min-height: 300px;
- max-height: 80vh;
- background: rgba(12, 14, 20, 0.95);
- border: 2px solid #3a4a5e;
- border-radius: 8px;
- margin-top: 15px;
- overflow: hidden;
- position: relative; /* 关键:作为定位锚点 */
- resize: vertical;
- width: 100%;
- `;
-
- container.innerHTML = htmlContent;
- container.classList.add('collapsed'); // 默认折叠
-
- // On mobile devices, add a specific class to enable horizontal scrolling via CSS
- if (isTouchDevice()) {
- container.classList.add('mobile-table-view');
- container.style.flexDirection = 'column'; // 移动端保持垂直
- }
-
- lastMessage.appendChild(container);
- bindSwipePreventer(container);
-
- // 绑定折叠按钮事件
- const toggleBtn = container.querySelector('.amily2-table-toggle');
- if (toggleBtn) {
- toggleBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- container.classList.toggle('collapsed');
- });
- }
-
- // 【V155.3】绑定游戏UI的交互事件
- const tabs = container.querySelectorAll('.amily2-game-tab');
- const panels = container.querySelectorAll('.amily2-game-panel');
-
- tabs.forEach(tab => {
- tab.addEventListener('click', (e) => {
- e.stopPropagation(); // 防止触发消息点击
-
- // 移除所有激活状态
- tabs.forEach(t => t.classList.remove('active'));
- panels.forEach(p => p.classList.remove('active'));
-
- // 激活当前
- tab.classList.add('active');
- const targetId = tab.dataset.target;
- const targetPanel = container.querySelector(`#${targetId}`);
- if (targetPanel) targetPanel.classList.add('active');
- });
- });
-
- } else {
- console.warn('[Amily2] 未找到最后一条消息的容器,无法插入表格。');
+ if (menu.classList.contains('amily2-menu-active')) {
+ document.addEventListener('click', closeMenu, true);
}
}, 0);
}
-function debounce(func, wait) {
- let timeout;
- return function executedFunction(...args) {
- const later = () => {
- clearTimeout(timeout);
- func(...args);
- };
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
+
+function showInputDialog({ title, label, currentValue, placeholder, onSave }) {
+ const dialogHtml = `
+ `;
+
+ const dialogElement = $(dialogHtml).appendTo('body');
+ const input = dialogElement.find('#generic-input');
+
+ const closeDialog = () => {
+ dialogElement[0].close();
+ dialogElement.remove();
};
+
+ const save = () => {
+ const newValue = input.val().trim();
+ if (newValue && newValue !== currentValue) {
+ onSave(newValue);
+ } else if (!newValue) {
+ toastr.warning('名称不能为空!');
+ input.focus();
+ return;
+ }
+ closeDialog();
+ };
+
+ dialogElement.find('.popup-button-ok').on('click', save);
+ dialogElement.find('.popup-button-cancel').on('click', closeDialog);
+ input.on('keypress', (e) => { if (e.which === 13) save(); });
+ input.on('keydown', (e) => { if (e.which === 27) closeDialog(); });
+
+ dialogElement[0].showModal();
+ input.focus().select();
}
-let chatObserver = null;
-const debouncedUpdate = debounce(updateOrInsertTableInChat, 100);
-export function startContinuousRendering() {
- if (chatObserver) {
- console.log('[Amily2] Continuous rendering is already active.');
- return;
- }
-
- const chatContainer = document.getElementById('chat');
- if (!chatContainer) {
- console.error('[Amily2] Could not find chat container to observe.');
- setTimeout(startContinuousRendering, 500);
- return;
- }
-
- const observerConfig = { childList: true };
-
- chatObserver = new MutationObserver((mutationsList, observer) => {
- for (const mutation of mutationsList) {
- if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
- let messageAdded = false;
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === 1 && node.classList.contains('mes')) {
- messageAdded = true;
- }
- });
-
- if (messageAdded) {
- debouncedUpdate();
- return;
- }
- }
+function showColumnNameEditor(tableIndex, colIndex, currentName) {
+ showInputDialog({
+ title: '编辑列名',
+ label: '列名:',
+ currentValue: currentName,
+ placeholder: '请输入列名...',
+ onSave: (newName) => {
+ TableManager.updateHeader(tableIndex, colIndex, newName);
+ renderTables();
+ toastr.success(`列名已更新为 "${newName}"`);
}
});
+}
+
+
+function showTableNameEditor(tableIndex, currentName) {
+ showInputDialog({
+ title: '编辑表名',
+ label: '表名:',
+ currentValue: currentName,
+ placeholder: '请输入表名...',
+ onSave: (newName) => {
+ TableManager.renameTable(tableIndex, newName);
+ renderTables();
+ toastr.success(`表名已更新为 "${newName}"`);
+ }
+ });
+}
+
+
+function positionContextMenu(menu, trigger) {
+ menu.style.position = 'absolute';
+ menu.style.zIndex = '10000';
+ menu.style.left = '0';
+ menu.style.right = 'auto';
+ menu.style.marginTop = '';
+ menu.style.marginBottom = '';
+ menu.style.maxHeight = '';
+ menu.style.overflowY = '';
+
+ const viewportHeight = window.innerHeight;
+ const triggerRect = trigger.getBoundingClientRect();
+ const menuHeight = 200;
+ const scrollContainer = trigger.closest('.hly-scroll');
+ const containerRect = scrollContainer ? scrollContainer.getBoundingClientRect() : { top: 0, bottom: viewportHeight };
+
+ const spaceBelow = Math.min(viewportHeight, containerRect.bottom) - triggerRect.bottom;
+ const spaceAbove = triggerRect.top - Math.max(0, containerRect.top);
+
+ if (spaceBelow < menuHeight && spaceAbove > spaceBelow) {
+ menu.style.top = 'auto';
+ menu.style.bottom = '100%';
+ menu.style.marginBottom = '2px';
+ } else {
+ menu.style.top = '100%';
+ menu.style.bottom = 'auto';
+ menu.style.marginTop = '2px';
+ }
+
+ const menuWidth = 160;
+ const table = trigger.closest('table');
+ const tableWrapper = table ? table.closest('div[style*="overflowX"]') : null;
+
+ if (tableWrapper) {
+ const wrapperRect = tableWrapper.getBoundingClientRect();
+ const triggerLeftInWrapper = triggerRect.left - wrapperRect.left;
+
+ if (triggerLeftInWrapper + menuWidth > wrapperRect.width - 20) {
+ menu.style.left = 'auto';
+ menu.style.right = '0';
+ }
+ }
+}
+
+
+export function renderTables() {
+ let tables = TableManager.getMemoryState();
+ if (!tables) {
+ log('内存状态为空,从聊天记录加载作为后备。', 'warn');
+ tables = TableManager.loadTables();
+ }
+
+ const container = getAllTablesContainer();
+
+ if (!tables || !container) {
+ console.error('[内存储司-工部] 缺少表格数据或容器,无法渲染。');
+ return;
+ }
+
+ // 【V155.0】验证 activeTableIndex
+ if (activeTableIndex >= tables.length) {
+ activeTableIndex = Math.max(0, tables.length - 1);
+ }
+
+ const highlights = TableManager.getHighlights();
+ const updatedTables = TableManager.getUpdatedTables();
+ const fragment = document.createDocumentFragment();
+
+ // 【V155.1 移动端适配】注入样式
+ if (!document.getElementById('amily2-table-tabs-style')) {
+ const style = document.createElement('style');
+ style.id = 'amily2-table-tabs-style';
+ style.textContent = `
+ .amily2-table-tabs {
+ display: flex;
+ overflow-x: auto;
+ gap: 8px;
+ margin-bottom: 10px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+ align-items: center;
+ -webkit-overflow-scrolling: touch; /* iOS 平滑滚动 */
+ scrollbar-width: none; /* Firefox 隐藏滚动条 */
+ }
+ .amily2-table-tabs::-webkit-scrollbar {
+ display: none; /* Chrome/Safari 隐藏滚动条 */
+ }
+ .amily2-table-tabs .menu_button {
+ flex-shrink: 0; /* 防止标签被压缩 */
+ white-space: nowrap;
+ }
+ /* 移动端表头适配 */
+ @media (max-width: 768px) {
+ .amily2-table-header-container {
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+ .amily2-table-header-container h3 {
+ width: 100%;
+ margin-bottom: 5px;
+ }
+ .amily2-table-header-container .table-controls {
+ width: 100%;
+ justify-content: space-between;
+ }
+ .amily2-table-header-container .table-controls .menu_button {
+ flex: 1;
+ justify-content: center;
+ }
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ // 1. 渲染标签页 (Tabs)
+ const tabsContainer = document.createElement('div');
+ tabsContainer.className = 'amily2-table-tabs';
+ // 移除内联样式,改用上方注入的 CSS 类
+ // tabsContainer.style.cssText = ...
+
+ tables.forEach((table, index) => {
+ const tab = document.createElement('button');
+ tab.className = `menu_button small_button ${index === activeTableIndex ? 'active' : ''}`;
+ tab.style.whiteSpace = 'nowrap';
+
+ // 高亮当前标签
+ if (index === activeTableIndex) {
+ tab.style.backgroundColor = 'rgba(158, 138, 255, 0.4)';
+ tab.style.borderColor = 'rgba(158, 138, 255, 0.8)';
+ tab.style.boxShadow = '0 0 8px rgba(158, 138, 255, 0.3)';
+ }
+
+ // 如果表格有更新,添加标记
+ if (updatedTables.has(index)) {
+ tab.innerHTML = `${escapeHTML(table.name)} •`;
+ } else {
+ tab.textContent = table.name;
+ }
+
+ tab.onclick = () => {
+ activeTableIndex = index;
+ renderTables();
+ };
+ tabsContainer.appendChild(tab);
+ });
+
+ // 添加“新建表格”按钮到标签栏
+ const addBtn = document.createElement('button');
+ addBtn.className = 'menu_button small_button';
+ addBtn.innerHTML = '';
+ addBtn.title = '新建表格';
+ addBtn.style.marginLeft = '5px';
+ addBtn.onclick = () => {
+ const newName = prompt('请输入新表格的名称:', '新表格');
+ if (newName && newName.trim()) {
+ TableManager.addTable(newName.trim());
+ // 切换到新创建的表格
+ const newTables = TableManager.getMemoryState();
+ activeTableIndex = newTables.length - 1;
+ renderTables();
+ }
+ };
+ tabsContainer.appendChild(addBtn);
+
+ fragment.appendChild(tabsContainer);
+
+ // 2. 渲染当前激活的表格 (Active Table)
+ if (tables.length > 0 && tables[activeTableIndex]) {
+ const tableIndex = activeTableIndex;
+ const tableData = tables[tableIndex];
+
+ const header = document.createElement('div');
+ header.className = 'amily2-table-header-container';
+ const title = document.createElement('h3');
+ if (updatedTables.has(tableIndex)) {
+ title.classList.add('table-updated');
+ }
+ title.innerHTML = ` ${escapeHTML(tableData.name)}`;
+ const controls = document.createElement('div');
+ controls.className = 'table-controls';
+
+ // 左右移动表格(原上下移动)
+ // 【移动端优化】增加按钮的触摸区域和间距
+ const moveLeftBtn = tableIndex > 0 ? `` : '';
+ const moveRightBtn = tableIndex < tables.length - 1 ? `` : '';
+
+ controls.innerHTML = `
+ ${moveLeftBtn}
+ ${moveRightBtn}
+
+
+ `;
+ header.appendChild(title);
+ header.appendChild(controls);
+ fragment.appendChild(header);
+
+ const tableWrapper = document.createElement('div');
+ tableWrapper.className = 'amily2-table-wrapper';
+
+ const tableElement = document.createElement('table');
+ tableElement.id = `amily2-table-${tableIndex}`;
+ tableElement.dataset.tableIndex = tableIndex;
+
+ const colgroup = document.createElement('colgroup');
+ const indexCol = document.createElement('col');
+ indexCol.style.width = '40px';
+ colgroup.appendChild(indexCol);
+
+ if (tableData.headers) {
+ tableData.headers.forEach((_, colIndex) => {
+ const col = document.createElement('col');
+ const colWidth = (tableData.columnWidths && tableData.columnWidths[colIndex]) ? tableData.columnWidths[colIndex] : 90;
+ col.style.width = `${colWidth}px`;
+ colgroup.appendChild(col);
+ });
+ }
+ tableElement.appendChild(colgroup);
+
+ let totalWidth = 0;
+ const cols = colgroup.querySelectorAll('col');
+ cols.forEach(col => {
+ totalWidth += parseInt(col.style.width, 10);
+ });
+ tableElement.style.minWidth = '100%';
+ if (totalWidth > 0) {
+ tableElement.style.width = `${Math.max(totalWidth, 0)}px`;
+ tableElement.style.minWidth = `${totalWidth}px`;
+ tableElement.style.width = '100%';
+ }
+
+ const thead = tableElement.createTHead();
+ const headerRow = thead.insertRow();
+
+ const indexTh = document.createElement('th');
+ indexTh.className = 'index-col';
+ 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';
+ headerMenu.style.display = 'none'; // 默认隐藏
+
+ const addRowButton = document.createElement('button');
+ addRowButton.innerHTML = ' 创建第一行';
+ addRowButton.className = 'menu_button small_button';
+ addRowButton.addEventListener('click', (e) => {
+ e.stopPropagation();
+ TableManager.addRow(tableIndex);
+ 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('已添加第一行');
+ });
+ }
+
+ headerRow.appendChild(indexTh);
+
+ tableData.headers.forEach((headerText, colIndex) => {
+ const th = document.createElement('th');
+ th.dataset.colIndex = colIndex;
+ th.style.cursor = 'pointer';
+
+ const headerContent = document.createElement('span');
+ headerContent.className = 'amily2-header-text';
+ headerContent.textContent = headerText; // textContent is safe
+ th.appendChild(headerContent);
+
+ const menu = document.createElement('div');
+ menu.className = 'amily2-context-menu';
+
+ const actions = [
+ { label: '向左移动', action: 'move-left', icon: 'fa-arrow-left' },
+ { label: '向右移动', action: 'move-right', icon: 'fa-arrow-right' },
+ { label: '在左加列', action: 'add-left', icon: 'fa-plus-circle' },
+ { label: '在右加列', action: 'add-right', icon: 'fa-plus-circle' },
+ { label: '编辑列名', action: 'rename', icon: 'fa-pen' },
+ { label: '删除该列', action: 'delete', icon: 'fa-trash-alt', isDanger: true }
+ ];
+
+ actions.forEach(({ label, action, icon, isDanger }) => {
+ const button = document.createElement('button');
+ button.textContent = label;
+ button.className = 'menu_button small_button';
+ if (isDanger) button.classList.add('danger');
+
+ button.addEventListener('click', (e) => {
+ e.stopPropagation();
+ switch (action) {
+ case 'move-left':
+ TableManager.moveColumn(tableIndex, colIndex, 'left');
+ break;
+ case 'move-right':
+ TableManager.moveColumn(tableIndex, colIndex, 'right');
+ break;
+ case 'add-left':
+ TableManager.insertColumn(tableIndex, colIndex, 'left');
+ break;
+ case 'add-right':
+ TableManager.insertColumn(tableIndex, colIndex, 'right');
+ break;
+ case 'rename':
+ showColumnNameEditor(tableIndex, colIndex, headerText);
+ break;
+ case 'delete':
+ if (confirm(`您确定要删除 “${headerText}” 列吗?`)) {
+ TableManager.deleteColumn(tableIndex, colIndex);
+ }
+ break;
+ }
+ renderTables();
+ });
+ menu.appendChild(button);
+ });
+
+ th.appendChild(menu);
+
+ const resizer = document.createElement('div');
+ resizer.className = 'amily2-resizer';
+ th.appendChild(resizer);
+
+ const startResize = (startEvent) => {
+ startEvent.preventDefault();
+ startEvent.stopPropagation();
+
+ isResizing = true;
+
+ const table = startEvent.target.closest('table');
+ const th = startEvent.target.parentElement;
+ const col = table.querySelector(`colgroup > col:nth-child(${th.cellIndex + 1})`);
+
+ const isTouchEvent = startEvent.type.startsWith('touch');
+ const startX = isTouchEvent ? startEvent.touches[0].clientX : startEvent.clientX;
+ const startWidth = th.offsetWidth;
+
+ const onMove = (moveEvent) => {
+ const currentX = isTouchEvent ? moveEvent.touches[0].clientX : moveEvent.clientX;
+ const newWidth = startWidth + (currentX - startX);
+ if (newWidth > 50) {
+ col.style.width = `${newWidth}px`;
+ }
+ };
+
+ const onEnd = () => {
+ document.removeEventListener('mousemove', onMove);
+ document.removeEventListener('mouseup', onEnd);
+ document.removeEventListener('touchmove', onMove);
+ document.removeEventListener('touchend', onEnd);
+
+ const finalWidth = parseInt(col.style.width, 10);
+ TableManager.updateColumnWidth(tableIndex, colIndex, finalWidth);
+
+ setTimeout(() => { isResizing = false; }, 0);
+ };
+
+ if (isTouchEvent) {
+ document.addEventListener('touchmove', onMove, { passive: false });
+ document.addEventListener('touchend', onEnd);
+ } else {
+ document.addEventListener('mousemove', onMove);
+ document.addEventListener('mouseup', onEnd);
+ }
+ };
+
+ resizer.addEventListener('mousedown', startResize);
+ resizer.addEventListener('touchstart', startResize, { passive: false });
+
+ headerRow.appendChild(th);
+ });
+
+ const tbody = tableElement.createTBody();
+ if (tableData.rows && tableData.rows.length > 0) {
+ tableData.rows.forEach((rowData, rowIndex) => {
+ const row = tbody.insertRow();
+ row.dataset.rowIndex = rowIndex;
+
+ // 【延迟删除】根据行状态添加样式
+ const rowStatus = tableData.rowStatuses ? tableData.rowStatuses[rowIndex] : 'normal';
+ if (rowStatus === 'pending-deletion') {
+ row.classList.add('pending-deletion-row');
+ }
+
+ const indexCell = row.insertCell();
+ indexCell.className = 'index-col';
+
+ const rowIndexSpan = document.createElement('span');
+ rowIndexSpan.textContent = rowIndex + 1;
+ indexCell.appendChild(rowIndexSpan);
+
+ const menu = document.createElement('div');
+ menu.className = 'amily2-context-menu amily2-row-context-menu';
+
+ let actions;
+
+ if (rowStatus === 'pending-deletion') {
+ actions = [
+ { label: '恢复该行', action: 'restore-row', icon: 'fa-undo', isSuccess: true, btnClass: 'restore-row-btn' }
+ ];
+ } else {
+ actions = [
+ { label: '向上移动', action: 'move-up', icon: 'fa-arrow-up', btnClass: 'move-row-up-btn' },
+ { label: '向下移动', action: 'move-down', icon: 'fa-arrow-down', btnClass: 'move-row-down-btn' },
+ { label: '在上加行', action: 'add-above', icon: 'fa-plus-circle', btnClass: 'add-row-above-btn' },
+ { label: '在下加行', action: 'add-below', icon: 'fa-plus-circle', btnClass: 'add-row-below-btn' },
+ { label: '删除该行', action: 'delete-row', icon: 'fa-trash-alt', isDanger: true, btnClass: 'delete-row-btn' }
+ ];
+ }
+
+ actions.forEach(({ label, action, icon, isDanger, isSuccess }) => {
+ const button = document.createElement('button');
+ button.innerHTML = ` ${label}`;
+ button.className = 'menu_button small_button';
+ if (isDanger) button.classList.add('danger');
+ if (isSuccess) button.classList.add('success'); // Use a success style for restore
+
+ button.addEventListener('click', (e) => {
+ e.stopPropagation();
+
+ switch (action) {
+ case 'move-up':
+ TableManager.moveRow(tableIndex, rowIndex, 'up');
+ break;
+ case 'move-down':
+ TableManager.moveRow(tableIndex, rowIndex, 'down');
+ break;
+ case 'add-above':
+ TableManager.insertRow(tableIndex, rowIndex, 'above');
+ break;
+ case 'add-below':
+ TableManager.insertRow(tableIndex, rowIndex, 'below');
+ break;
+ case 'delete-row':
+ TableManager.deleteRow(tableIndex, rowIndex);
+ break;
+ case 'restore-row':
+ TableManager.restoreRow(tableIndex, rowIndex);
+ break;
+ }
+ if (action === 'delete-row' || action === 'restore-row') {
+ } else {
+ renderTables();
+ }
+ });
+ menu.appendChild(button);
+ });
+ indexCell.appendChild(menu);
+
+ rowData.forEach((cellData, colIndex) => {
+ const cell = row.insertCell();
+
+ const cellContent = document.createElement('div');
+ cellContent.className = 'amily2-cell-content';
+ cellContent.textContent = cellData;
+ cell.appendChild(cellContent);
+
+ if (rowStatus !== 'pending-deletion' && !isTouchDevice()) {
+ cell.setAttribute('contenteditable', 'true');
+ }
+ cell.dataset.colIndex = colIndex;
+ cell.dataset.label = tableData.headers[colIndex] || '';
+
+ const highlightKey = `${tableIndex}-${rowIndex}-${colIndex}`;
+ if (highlights.has(highlightKey)) {
+ cell.classList.add('cell-highlight');
+ }
+ });
+ });
+ }
+ tableWrapper.appendChild(tableElement);
+ fragment.appendChild(tableWrapper);
+ } else {
+ // 如果没有表格,显示占位符
+ const placeholder = document.createElement('div');
+ placeholder.id = 'add-table-placeholder';
+ placeholder.innerHTML = '';
+ placeholder.title = '点击创建第一个表格';
+ placeholder.addEventListener('click', () => {
+ const newName = prompt('请输入新表格的名称:', '新表格');
+ if (newName && newName.trim()) {
+ TableManager.addTable(newName.trim());
+ renderTables();
+ }
+ });
+ fragment.appendChild(placeholder);
+ }
+
+ container.innerHTML = '';
+ container.appendChild(fragment);
- chatObserver.observe(chatContainer, observerConfig);
- console.log('[Amily2] Started continuous table rendering.');
updateOrInsertTableInChat();
}
-export function stopContinuousRendering() {
- if (chatObserver) {
- chatObserver.disconnect();
- chatObserver = null;
- removeTableContainer();
- console.log('[Amily2] Stopped continuous table rendering.');
+
+function openTableRuleEditor() {
+ const settings = extension_settings[extensionName];
+ const tags = settings.table_tags_to_extract || '';
+ const exclusionRules = settings.table_exclusion_rules || [];
+
+ const rulesHtml = exclusionRules.map((rule, index) => `
+
+
+ -
+
+
+
+ `).join('');
+
+ const modalHtml = `
+
+
+
+
+ 仅提取指定XML标签的内容,例如填“content”,即提取...中的内容。
+
+
+
+
${rulesHtml}
+
+
移除所有被起始和结束标记包裹的内容(例如 OOC 部分)。
+
+
+ `;
+
+ const dialog = showHtmlModal('配置独立提取规则', modalHtml, {
+ onOk: () => {
+ const newTags = document.getElementById('table-tags-input').value;
+ updateAndSaveTableSetting('table_tags_to_extract', newTags);
+
+ const newExclusionRules = [];
+ document.querySelectorAll('#exclusion-rules-list .exclusion-rule-item').forEach(item => {
+ const start = item.querySelector('.rule-start').value.trim();
+ const end = item.querySelector('.rule-end').value.trim();
+ if (start && end) {
+ newExclusionRules.push({ start, end });
+ }
+ });
+ updateAndSaveTableSetting('table_exclusion_rules', newExclusionRules);
+ toastr.success('独立提取规则已保存。');
+ },
+ onShow: (dialogElement) => {
+ const rulesList = dialogElement.find('#exclusion-rules-list');
+
+ dialogElement.find('#add-exclusion-rule-btn').on('click', () => {
+ const newIndex = rulesList.children().length;
+ const newItemHtml = `
+
+
+ -
+
+
+
`;
+ rulesList.append(newItemHtml);
+ });
+
+ rulesList.on('click', '.remove-rule-btn', function() {
+ $(this).closest('.exclusion-rule-item').remove();
+ });
+ }
+ });
+}
+
+function openRuleEditor(tableIndex) {
+ const tables = TableManager.getMemoryState();
+ if (!tables || !tables[tableIndex]) return;
+ const table = tables[tableIndex];
+
+ if (table.charLimitRule && !table.charLimitRules) {
+ table.charLimitRules = {};
+ if (table.charLimitRule.columnIndex !== -1) {
+ table.charLimitRules[table.charLimitRule.columnIndex] = table.charLimitRule.limit;
+ }
+ }
+ const charLimitRules = table.charLimitRules || {};
+
+ const renderCharLimitRules = (rules) => {
+ return Object.entries(rules).map(([colIndex, limit]) => {
+ const header = table.headers[colIndex] || `未知列 (${colIndex})`;
+ return `
+
+ ${escapeHTML(header)}: 不超过 ${limit} 字
+
+
+ `;
+ }).join('');
+ };
+
+ const getColumnOptions = (rules) => {
+ return table.headers.map((header, index) => {
+ if (rules[index]) return '';
+ return ``;
+ }).join('');
+ };
+
+ const dialogHtml = `
+ `;
+
+ const dialogElement = $(dialogHtml).appendTo('body');
+
+ const closeDialog = () => {
+ dialogElement[0].close();
+ dialogElement.remove();
+ };
+
+ const refreshRuleUI = () => {
+ const currentRules = JSON.parse(dialogElement.find('#current-char-limit-rules').attr('data-rules') || '{}');
+ dialogElement.find('#current-char-limit-rules').html(renderCharLimitRules(currentRules));
+ dialogElement.find('#new-rule-column-select').html(`${getColumnOptions(currentRules)}`);
+ };
+
+ dialogElement.find('#current-char-limit-rules').attr('data-rules', JSON.stringify(charLimitRules));
+
+ dialogElement.on('click', '#add-char-limit-rule-btn', () => {
+ const selectedColumn = parseInt(dialogElement.find('#new-rule-column-select').val(), 10);
+ const limitValue = parseInt(dialogElement.find('#new-rule-limit-input').val(), 10);
+
+ if (selectedColumn === -1) {
+ toastr.warning('请选择一个列。');
+ return;
+ }
+
+ if (isNaN(limitValue) || limitValue < 0) {
+ toastr.warning('请输入一个有效的字数限制(大于等于0)。');
+ return;
+ }
+
+ const currentRules = JSON.parse(dialogElement.find('#current-char-limit-rules').attr('data-rules') || '{}');
+
+ if (limitValue > 0) {
+ currentRules[selectedColumn] = limitValue;
+ dialogElement.find('#current-char-limit-rules').attr('data-rules', JSON.stringify(currentRules));
+ refreshRuleUI();
+ } else {
+ toastr.info('字数限制为0表示不设置规则。');
+ }
+ });
+
+ dialogElement.on('click', '.remove-char-limit-rule-btn', function() {
+ const colIndexToRemove = $(this).data('col-index');
+ const currentRules = JSON.parse(dialogElement.find('#current-char-limit-rules').attr('data-rules') || '{}');
+ delete currentRules[colIndexToRemove];
+ dialogElement.find('#current-char-limit-rules').attr('data-rules', JSON.stringify(currentRules));
+ refreshRuleUI();
+ });
+
+ dialogElement.find('.popup-button-ok').on('click', () => {
+ const newCharLimitRules = JSON.parse(dialogElement.find('#current-char-limit-rules').attr('data-rules') || '{}');
+ const rowLimitValue = parseInt(dialogElement.find('#rule-row-limit-value').val(), 10);
+ const simplifyThresholdValue = parseInt(dialogElement.find('#rule-simplify-threshold').val(), 10);
+
+ const newRules = {
+ note: dialogElement.find('#rule-note').val(),
+ rule_add: dialogElement.find('#rule-add').val(),
+ rule_delete: dialogElement.find('#rule-delete').val(),
+ rule_update: dialogElement.find('#rule-update').val(),
+ charLimitRules: newCharLimitRules,
+ rowLimitRule: rowLimitValue,
+ simplifyRowThreshold: simplifyThresholdValue, // 保存新设置
+ };
+ TableManager.updateTableRules(tableIndex, newRules);
+ closeDialog();
+ });
+
+ dialogElement.find('.popup-button-cancel').on('click', closeDialog);
+ dialogElement[0].showModal();
+}
+
+
+function bindInjectionSettings() {
+ const settings = extension_settings[extensionName];
+
+ const masterSwitchCheckbox = document.getElementById('table-system-master-switch');
+ const enabledCheckbox = document.getElementById('table-injection-enabled');
+ const optimizationCheckbox = document.getElementById('context-optimization-enabled'); // 【V144.0】
+ const positionSelect = document.getElementById('table-injection-position');
+ const depthInput = document.getElementById('table-injection-depth');
+ const roleRadioGroup = document.querySelectorAll('input[name="table-injection-role"]');
+
+ if (!masterSwitchCheckbox || !enabledCheckbox || !positionSelect || !depthInput || !roleRadioGroup.length) {
+ return;
+ }
+
+ const updateInjectionUI = () => {
+ const position = positionSelect.value;
+ const masterEnabled = masterSwitchCheckbox.checked;
+
+ const isChatInjection = position === '1';
+
+ enabledCheckbox.disabled = !masterEnabled;
+ positionSelect.disabled = !masterEnabled;
+ depthInput.disabled = !masterEnabled || !isChatInjection;
+ roleRadioGroup.forEach(radio => radio.disabled = !masterEnabled || !isChatInjection);
+
+ const enabledOpacity = masterEnabled ? '1' : '0.5';
+ enabledCheckbox.style.opacity = enabledOpacity;
+ if (enabledCheckbox.closest('.control-block-with-switch')) {
+ enabledCheckbox.closest('.control-block-with-switch').style.opacity = enabledOpacity;
+ }
+
+ positionSelect.style.opacity = enabledOpacity;
+ if (positionSelect.previousElementSibling) {
+ positionSelect.previousElementSibling.style.opacity = enabledOpacity;
+ }
+
+ const depthOpacity = masterEnabled && isChatInjection ? '1' : '0.5';
+ depthInput.style.opacity = depthOpacity;
+ if (depthInput.previousElementSibling) {
+ depthInput.previousElementSibling.style.opacity = depthOpacity;
+ }
+
+ const roleOpacity = masterEnabled && isChatInjection ? '1' : '0.5';
+ const roleGroupContainer = document.getElementById('table-role-system')?.closest('.radio-group');
+ if (roleGroupContainer) {
+ roleGroupContainer.style.opacity = roleOpacity;
+ if (roleGroupContainer.previousElementSibling) {
+ roleGroupContainer.previousElementSibling.style.opacity = roleOpacity;
+ }
+ }
+
+ const fillingModeRadios = document.querySelectorAll('input[name="filling-mode"]');
+ fillingModeRadios.forEach(radio => {
+ radio.disabled = !masterEnabled;
+ const label = radio.closest('label');
+ if (label) {
+ label.style.opacity = masterEnabled ? '1' : '0.5';
+ }
+ });
+
+ const fillButton = document.getElementById('fill-table-now-btn');
+ if (fillButton) {
+ fillButton.disabled = !masterEnabled;
+ fillButton.style.opacity = masterEnabled ? '1' : '0.5';
+ }
+ };
+
+ masterSwitchCheckbox.checked = settings.table_system_enabled !== false;
+ enabledCheckbox.checked = settings.table_injection_enabled;
+ if (optimizationCheckbox) { // 【V144.0】
+ optimizationCheckbox.checked = settings.context_optimization_enabled !== false;
+ }
+ positionSelect.value = settings.injection.position;
+ depthInput.value = settings.injection.depth;
+ roleRadioGroup.forEach(radio => {
+ if (parseInt(radio.value, 10) === settings.injection.role) {
+ radio.checked = true;
+ }
+ });
+
+ updateInjectionUI();
+
+ if (masterSwitchCheckbox.dataset.eventsBound) return;
+
+ masterSwitchCheckbox.addEventListener('change', () => {
+ settings.table_system_enabled = masterSwitchCheckbox.checked;
+ saveSettingsDebounced();
+ updateInjectionUI();
+
+ const statusText = masterSwitchCheckbox.checked ? '已启用' : '已禁用';
+ toastr.info(`表格系统总开关${statusText}。`);
+ log(`表格系统总开关${statusText}。`, 'info');
+ });
+
+ enabledCheckbox.addEventListener('change', () => {
+ settings.table_injection_enabled = enabledCheckbox.checked;
+ saveSettingsDebounced();
+ });
+
+ // 【V144.0】
+ if (optimizationCheckbox) {
+ optimizationCheckbox.addEventListener('change', () => {
+ settings.context_optimization_enabled = optimizationCheckbox.checked;
+ saveSettingsDebounced();
+ toastr.info(`上下文优化(世界书合并)已${optimizationCheckbox.checked ? '启用' : '禁用'}。`);
+ });
+ }
+
+ positionSelect.addEventListener('change', () => {
+ settings.injection.position = parseInt(positionSelect.value, 10);
+ saveSettingsDebounced();
+
+ updateInjectionUI();
+ });
+
+ depthInput.addEventListener('input', () => {
+ settings.injection.depth = parseInt(depthInput.value, 10);
+ saveSettingsDebounced();
+ });
+
+ roleRadioGroup.forEach(radio => {
+ radio.addEventListener('change', () => {
+ if (radio.checked) {
+ settings.injection.role = parseInt(radio.value, 10);
+ saveSettingsDebounced();
+ }
+ });
+ });
+
+ masterSwitchCheckbox.dataset.eventsBound = 'true';
+ log('表格注入设置已成功绑定。', 'success');
+}
+
+
+function updateAndSaveTableSetting(key, value) {
+ if (!extension_settings[extensionName]) {
+ extension_settings[extensionName] = {};
+ }
+ extension_settings[extensionName][key] = value;
+ saveSettingsDebounced();
+}
+
+function bindWorldBookSettings() {
+ const settings = extension_settings[extensionName];
+
+ if (settings.table_worldbook_enabled === undefined) settings.table_worldbook_enabled = false;
+ if (settings.table_worldbook_char_limit === undefined) settings.table_worldbook_char_limit = 30000;
+ if (settings.table_worldbook_source === undefined) settings.table_worldbook_source = 'character';
+ if (settings.table_selected_worldbooks === undefined) settings.table_selected_worldbooks = [];
+ if (settings.table_selected_entries === undefined) settings.table_selected_entries = {};
+
+ const enabledCheckbox = document.getElementById('table_worldbook_enabled');
+ const limitSlider = document.getElementById('table_worldbook_char_limit');
+ const limitValueSpan = document.getElementById('table_worldbook_char_limit_value');
+ const sourceRadios = document.querySelectorAll('input[name="table_worldbook_source"]');
+ const manualSelectWrapper = document.getElementById('table_worldbook_select_wrapper');
+ const refreshButton = document.getElementById('table_refresh_worldbooks');
+ const bookListContainer = document.getElementById('table_worldbook_checkbox_list');
+ const entryListContainer = document.getElementById('table_worldbook_entry_list');
+
+ if (!enabledCheckbox || !limitSlider || !limitValueSpan || !sourceRadios.length || !manualSelectWrapper || !refreshButton || !bookListContainer || !entryListContainer) {
+ log('无法找到世界书设置的相关UI元素,绑定失败。', 'warn');
+ return;
+ }
+
+ const saveSelectedEntries = () => {
+ const selected = {};
+ entryListContainer.querySelectorAll('input[type="checkbox"]:checked').forEach(cb => {
+ const book = cb.dataset.book;
+ const uid = cb.dataset.uid;
+ if (!selected[book]) {
+ selected[book] = [];
+ }
+ selected[book].push(uid);
+ });
+ settings.table_selected_entries = selected;
+ saveSettingsDebounced();
+ };
+
+ const renderWorldBookEntries = async () => {
+ entryListContainer.innerHTML = '加载条目中...
';
+ const source = settings.table_worldbook_source || 'character';
+ let bookNames = [];
+
+ if (source === 'manual') {
+ bookNames = settings.table_selected_worldbooks || [];
+ } else {
+ if (this_chid !== undefined && this_chid >= 0 && characters[this_chid]) {
+ try {
+ const charLorebooks = await safeCharLorebooks({ type: 'all' });
+ if (charLorebooks.primary) bookNames.push(charLorebooks.primary);
+ if (charLorebooks.additional?.length) bookNames.push(...charLorebooks.additional);
+ } catch (error) {
+ console.error(`[内存储司] 获取角色世界书失败:`, error);
+ entryListContainer.innerHTML = '获取角色世界书失败。
';
+ return;
+ }
+ } else {
+ entryListContainer.innerHTML = '请先加载一个角色。
';
+ return;
+ }
+ }
+
+ if (bookNames.length === 0) {
+ entryListContainer.innerHTML = '未选择或绑定世界书。
';
+ return;
+ }
+
+ try {
+ const allEntries = [];
+ for (const bookName of bookNames) {
+ const entries = await safeLorebookEntries(bookName);
+ entries.forEach(entry => allEntries.push({ ...entry, bookName }));
+ }
+
+ entryListContainer.innerHTML = '';
+ if (allEntries.length === 0) {
+ entryListContainer.innerHTML = '所选世界书中没有条目。
';
+ return;
+ }
+
+ allEntries.forEach(entry => {
+ const div = document.createElement('div');
+ div.className = 'checkbox-item';
+ div.title = `世界书: ${entry.bookName}\nUID: ${entry.uid}`;
+
+ const checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.id = `wb-entry-check-${entry.bookName}-${entry.uid}`;
+ checkbox.dataset.book = entry.bookName;
+ checkbox.dataset.uid = entry.uid;
+
+ const isChecked = settings.table_selected_entries[entry.bookName]?.includes(String(entry.uid));
+ checkbox.checked = !!isChecked;
+
+ const label = document.createElement('label');
+ label.htmlFor = checkbox.id;
+ label.textContent = entry.comment || '无标题条目'; // textContent is safe
+
+ div.appendChild(checkbox);
+ div.appendChild(label);
+ entryListContainer.appendChild(div);
+ });
+ } catch (error) {
+ console.error(`[内存储司] 加载世界书条目失败:`, error);
+ entryListContainer.innerHTML = '加载条目失败。
';
+ }
+ };
+
+ const renderWorldBookList = () => {
+ const worldBooks = world_names.map(name => ({ name: name.replace('.json', ''), file_name: name }));
+ bookListContainer.innerHTML = '';
+ if (worldBooks && worldBooks.length > 0) {
+ worldBooks.forEach(book => {
+ const div = document.createElement('div');
+ div.className = 'checkbox-item';
+ div.title = book.name;
+
+ const checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.id = `wb-check-${book.file_name}`;
+ checkbox.value = book.file_name;
+ checkbox.checked = settings.table_selected_worldbooks.includes(book.file_name);
+
+ checkbox.addEventListener('change', () => {
+ if (checkbox.checked) {
+ if (!settings.table_selected_worldbooks.includes(book.file_name)) {
+ settings.table_selected_worldbooks.push(book.file_name);
+ }
+ } else {
+ settings.table_selected_worldbooks = settings.table_selected_worldbooks.filter(name => name !== book.file_name);
+ }
+ saveSettingsDebounced();
+ renderWorldBookEntries();
+ });
+
+ const label = document.createElement('label');
+ label.htmlFor = `wb-check-${book.file_name}`;
+ label.textContent = book.name; // textContent is safe
+
+ div.appendChild(checkbox);
+ div.appendChild(label);
+ bookListContainer.appendChild(div);
+ });
+ } else {
+ bookListContainer.innerHTML = '没有找到世界书。
';
+ }
+ renderWorldBookEntries();
+ };
+
+ const updateManualSelectVisibility = () => {
+ const isManual = settings.table_worldbook_source === 'manual';
+ manualSelectWrapper.style.display = isManual ? 'block' : 'none';
+ renderWorldBookEntries();
+ if (isManual) {
+ renderWorldBookList();
+ }
+ };
+
+ enabledCheckbox.checked = settings.table_worldbook_enabled;
+ limitSlider.value = settings.table_worldbook_char_limit;
+ limitValueSpan.textContent = settings.table_worldbook_char_limit;
+ sourceRadios.forEach(radio => {
+ radio.checked = radio.value === settings.table_worldbook_source;
+ });
+
+ updateManualSelectVisibility();
+
+ if (enabledCheckbox.dataset.eventsBound) return;
+
+ enabledCheckbox.addEventListener('change', () => {
+ settings.table_worldbook_enabled = enabledCheckbox.checked;
+ saveSettingsDebounced();
+ });
+
+ limitSlider.addEventListener('input', () => { limitValueSpan.textContent = limitSlider.value; });
+ limitSlider.addEventListener('change', () => {
+ settings.table_worldbook_char_limit = parseInt(limitSlider.value, 10);
+ saveSettingsDebounced();
+ });
+
+ sourceRadios.forEach(radio => {
+ radio.addEventListener('change', () => {
+ if (radio.checked) {
+ settings.table_worldbook_source = radio.value;
+ updateManualSelectVisibility();
+ saveSettingsDebounced();
+ }
+ });
+ });
+
+ refreshButton.addEventListener('click', renderWorldBookList);
+ entryListContainer.addEventListener('change', (event) => {
+ if (event.target.type === 'checkbox') {
+ saveSelectedEntries();
+ }
+ });
+
+ enabledCheckbox.dataset.eventsBound = 'true';
+ log('世界书设置已成功绑定。', 'success');
+}
+
+export function bindTableEvents() {
+ const panel = document.getElementById('amily2_memorisation_forms_panel');
+ if (!panel || panel.dataset.eventsBound) {
+ return;
+ }
+ 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');
+
+ const independentRulesContainer = document.getElementById('table-independent-rules-container');
+ const independentRulesToggle = document.getElementById('table-independent-rules-enabled');
+ const configureRulesBtn = document.getElementById('table-configure-rules-btn');
+
+ const updateFillingModeUI = () => {
+ const currentMode = extension_settings[extensionName]?.filling_mode || 'main-api';
+ fillingModeRadios.forEach(radio => {
+ radio.checked = (radio.value === currentMode);
+ });
+
+ const isSecondaryMode = currentMode === 'secondary-api';
+
+ if (secondaryFillerControls) {
+ secondaryFillerControls.style.display = isSecondaryMode ? 'block' : 'none';
+ }
+
+ if (independentRulesContainer) {
+ independentRulesContainer.style.display = 'flex';
+ }
+
+ if (independentRulesToggle && configureRulesBtn) {
+ configureRulesBtn.style.display = independentRulesToggle.checked ? 'block' : 'none';
+ }
+ };
+
+ fillingModeRadios.forEach(radio => {
+ radio.addEventListener('change', function() {
+ const selectedMode = this.value;
+ updateAndSaveTableSetting('filling_mode', selectedMode);
+
+ let modeName = '原始填表';
+ if (selectedMode === 'secondary-api') modeName = '分步填表';
+ if (selectedMode === 'optimized') modeName = '优化中填表';
+
+ toastr.info(`填表模式已切换为 ${modeName}。`);
+ updateFillingModeUI();
+ });
+ });
+
+ if (contextSlider) {
+ const value = extension_settings[extensionName]?.secondary_filler_context || 2;
+ contextSlider.value = value;
+
+ contextSlider.addEventListener('change', function() {
+ updateAndSaveTableSetting('secondary_filler_context', parseInt(this.value, 10));
+ toastr.info(`上下文深度已设置为 ${this.value}。`);
+ });
+ }
+
+ if (batchSlider) {
+ const value = extension_settings[extensionName]?.secondary_filler_batch || 0;
+ batchSlider.value = value;
+
+ batchSlider.addEventListener('change', function() {
+ updateAndSaveTableSetting('secondary_filler_batch', parseInt(this.value, 10));
+ toastr.info(`填表批次已设置为 ${this.value}。`);
+ });
+ }
+
+ if (bufferSlider) {
+ const value = extension_settings[extensionName]?.secondary_filler_buffer || 0;
+ bufferSlider.value = value;
+
+ bufferSlider.addEventListener('change', function() {
+ updateAndSaveTableSetting('secondary_filler_buffer', parseInt(this.value, 10));
+ toastr.info(`保留楼层已设置为 ${this.value}。`);
+ });
+ }
+
+ if (independentRulesToggle) {
+ independentRulesToggle.checked = extension_settings[extensionName]?.table_independent_rules_enabled ?? false;
+ independentRulesToggle.addEventListener('change', () => {
+ updateAndSaveTableSetting('table_independent_rules_enabled', independentRulesToggle.checked);
+ updateFillingModeUI();
+ });
+ }
+
+ updateFillingModeUI();
+
+ if (configureRulesBtn) {
+ configureRulesBtn.addEventListener('click', openTableRuleEditor);
+ }
+
+ const renderAll = () => {
+ renderTables();
+ bindInjectionSettings();
+ bindTemplateEditors();
+ };
+
+ renderAll();
+ bindWorldBookSettings();
+ bindBatchFillButton(); // 【新增】绑定批量填表按钮
+ bindFloorFillButtons(); // 【新增】绑定楼层填表按钮
+ bindReorganizeButton(); // 【新增】绑定重新整理按钮
+ bindClearRecordsButton(); // 【新增】绑定清除记录按钮
+ bindNccsApiEvents(); // 【新增】绑定Nccs API系统事件
+ bindChatTableDisplaySetting(); // 【新增】绑定聊天内表格显示开关
+
+ const navDeck = document.querySelector('#amily2_memorisation_forms_panel .sinan-navigation-deck');
+ if (navDeck) {
+ navDeck.addEventListener('click', (event) => {
+ const target = event.target.closest('.sinan-nav-item');
+ if (!target) return;
+
+ const tabName = target.dataset.tab;
+ if (!tabName) return;
+
+ const container = target.closest('.settings-group');
+ if (!container) return;
+
+ container.querySelectorAll('.sinan-nav-item').forEach(btn => btn.classList.remove('active'));
+ target.classList.add('active');
+ container.querySelectorAll('.sinan-tab-pane').forEach(pane => pane.classList.remove('active'));
+ const activePane = container.querySelector(`#sinan-${tabName}-tab`);
+ if (activePane) {
+ activePane.classList.add('active');
+ }
+ });
+ }
+
+ const openGraphBtn = document.getElementById('amily2-open-relationship-graph-btn');
+ const exportBtn = document.getElementById('amily2-export-preset-btn');
+ const exportFullBtn = document.getElementById('amily2-export-preset-full-btn');
+ const importBtn = document.getElementById('amily2-import-preset-btn');
+ const importGlobalBtn = document.getElementById('amily2-import-global-preset-btn');
+ const clearGlobalBtn = document.getElementById('amily2-clear-global-preset-btn');
+
+ if (openGraphBtn) {
+ openGraphBtn.addEventListener('click', () => {
+ showGraphVisualization();
+ });
+ }
+
+ if (exportBtn) {
+ exportBtn.addEventListener('click', () => TableManager.exportPreset());
+ }
+ if (exportFullBtn) {
+ exportFullBtn.addEventListener('click', () => TableManager.exportPresetFull());
+ }
+ if (importBtn) {
+ importBtn.addEventListener('click', () => TableManager.importPreset(renderAll));
+ }
+ if (importGlobalBtn) {
+ importGlobalBtn.addEventListener('click', () => {
+
+ const isEmpty = TableManager.isCurrentTablesEmpty();
+ TableManager.importGlobalPreset(() => {
+ if (isEmpty) {
+ TableManager.loadTables();
+ renderAll();
+ }
+ });
+ });
+ }
+ if (clearGlobalBtn) {
+ clearGlobalBtn.addEventListener('click', () => {
+ const isEmpty = TableManager.isCurrentTablesEmpty();
+ TableManager.clearGlobalPreset();
+ if (isEmpty) {
+ TableManager.loadTables();
+ renderAll();
+ }
+ });
+ }
+
+ const clearAllBtn = document.getElementById('amily2-clear-all-tables-btn');
+ if (clearAllBtn) {
+ clearAllBtn.addEventListener('click', () => {
+ if (confirm('【确认】您确定要清空所有表格的剧情内容吗?此操作将保留表格结构,但会删除所有已填写的行。')) {
+ TableManager.clearAllTables();
+ renderAll();
+ }
+ });
+ }
+
+
+ // 【V155.0】移除旧的 addTablePlaceholder 绑定,因为现在它在 renderTables 内部动态生成
+ // const addTablePlaceholder = document.getElementById('add-table-placeholder');
+ // if (addTablePlaceholder) { ... }
+
+
+ const allTablesContainer = getAllTablesContainer();
+ if (allTablesContainer) {
+ allTablesContainer.addEventListener('click', (event) => {
+ const th = event.target.closest('th');
+ if (th && th.classList.contains('index-col')) {
+ toggleHeaderIndexContextMenu(event);
+ return;
+ }
+ if (th && !th.classList.contains('index-col')) {
+ toggleColumnContextMenu(event);
+ return;
+ }
+
+ const td = event.target.closest('td.index-col');
+ if (td) {
+ toggleRowContextMenu(event);
+ return;
+ }
+
+ const renameIcon = event.target.closest('.table-rename-icon');
+ if (renameIcon) {
+ const tableIndex = parseInt(renameIcon.dataset.tableIndex, 10);
+ const tables = TableManager.getMemoryState();
+ const currentName = tables[tableIndex]?.name || '';
+ showTableNameEditor(tableIndex, currentName);
+ return;
+ }
+
+ const target = event.target.closest('button');
+ if (!target) return;
+
+ const tableIndex = parseInt(target.dataset.tableIndex, 10);
+
+ if (target.matches('.add-row-btn')) {
+ TableManager.addRow(tableIndex);
+ renderAll();
+ } else if (target.matches('.add-col-btn')) {
+ TableManager.addColumn(tableIndex);
+ renderAll();
+ } else if (target.matches('.move-table-up-btn') || target.matches('.move-table-down-btn')) {
+ const direction = target.classList.contains('move-table-up-btn') ? 'up' : 'down';
+ TableManager.moveTable(tableIndex, direction);
+ // 【V155.0】移动表格后,activeTableIndex 需要跟随移动
+ if (direction === 'up' && activeTableIndex > 0) {
+ activeTableIndex--;
+ } else if (direction === 'down' && activeTableIndex < TableManager.getMemoryState().length - 1) {
+ activeTableIndex++;
+ }
+ renderAll();
+ } else if (target.matches('.edit-rules-btn')) {
+ openRuleEditor(tableIndex);
+ } else if (target.matches('.delete-table-btn')) {
+ const tables = TableManager.getMemoryState();
+ const tableName = tables[tableIndex]?.name || '未知表格';
+ if (confirm(`【最终警告】您确定要永久废黜表格 “[${tableName}]” 吗?此操作不可逆!`)) {
+ TableManager.deleteTable(tableIndex);
+ renderAll();
+ }
+ }
+ });
+
+ if (isTouchDevice()) {
+ let lastTap = 0;
+ let lastTapTarget = null;
+ allTablesContainer.addEventListener('touchstart', (event) => {
+ const target = event.target.closest('td');
+ if (!target || target.dataset.colIndex === undefined) return;
+
+ const currentTime = new Date().getTime();
+ const tapLength = currentTime - lastTap;
+ if (tapLength < 300 && tapLength > 0 && lastTapTarget === target) {
+ event.preventDefault();
+ if (target.getAttribute('contenteditable') !== 'true') {
+ target.setAttribute('contenteditable', 'true');
+ setTimeout(() => target.focus(), 0);
+ }
+ }
+ lastTap = currentTime;
+ lastTapTarget = target;
+ });
+ }
+
+ allTablesContainer.addEventListener('blur', (event) => {
+ const target = event.target;
+ if (target.tagName !== 'TD' || target.getAttribute('contenteditable') !== 'true') return;
+
+ if (isTouchDevice()) {
+ target.setAttribute('contenteditable', 'false');
+ }
+
+ const tableElement = target.closest('table');
+ if (!tableElement) return;
+
+ const tableIndex = parseInt(tableElement.dataset.tableIndex, 10);
+ const rowIndex = parseInt(target.closest('tr').dataset.rowIndex, 10);
+ const colIndex = parseInt(target.dataset.colIndex, 10);
+ const newValue = target.textContent;
+
+ // Correctly save scroll positions before re-rendering
+ const tableWrapper = tableElement.closest('.amily2-table-wrapper');
+ const hScroll = tableWrapper ? tableWrapper.scrollLeft : 0;
+ const vScroll = allTablesContainer.scrollTop;
+
+ TableManager.addHighlight(tableIndex, rowIndex, colIndex);
+ const dataToUpdate = { [colIndex]: newValue };
+ TableManager.updateRow(tableIndex, rowIndex, dataToUpdate);
+
+ renderAll();
+
+ // Correctly restore scroll positions after re-rendering
+ const newTableWrapper = document.getElementById(`amily2-table-${tableIndex}`)?.closest('.amily2-table-wrapper');
+ if (newTableWrapper) {
+ newTableWrapper.scrollLeft = hScroll;
+ }
+ allTablesContainer.scrollTop = vScroll;
+
+ }, true);
+ }
+
+ panel.dataset.eventsBound = 'true';
+ log('表格视图交互事件已成功绑定。', 'success');
+
+ eventSource.on(event_types.CHAT_CHANGED, () => {
+ console.log(`[${extensionName}] 检测到角色/聊天切换,正在刷新表格系统UI和世界书设置...`);
+ renderAll();
+
+ setTimeout(() => {
+ const settings = extension_settings[extensionName];
+ if (settings && settings.table_worldbook_enabled) {
+ try {
+ bindWorldBookSettings();
+ console.log(`[${extensionName}] 世界书设置已刷新`);
+ } catch (error) {
+ console.error(`[${extensionName}] 刷新世界书设置时出错:`, error);
+ }
+ }
+ }, 100);
+ });
+}
+
+function bindBatchFillButton() {
+ const fillButton = document.getElementById('fill-table-now-btn');
+ if (fillButton) {
+ if (fillButton.dataset.batchEventBound) return;
+
+ fillButton.addEventListener('click', (event) => {
+ const settings = extension_settings[extensionName];
+ const tableSystemEnabled = settings.table_system_enabled !== false;
+
+ if (!tableSystemEnabled) {
+ event.preventDefault();
+ toastr.warning('表格系统总开关已关闭,请先启用总开关。');
+ return;
+ }
+
+ startBatchFilling();
+ });
+
+ fillButton.dataset.batchEventBound = 'true';
+ log('"立即填表"按钮已成功绑定。', 'success');
}
}
+
+function bindReorganizeButton() {
+ const reorganizeBtn = document.getElementById('reorganize-table-btn');
+
+ if (reorganizeBtn) {
+ if (reorganizeBtn.dataset.reorganizeEventBound) return;
+
+ reorganizeBtn.addEventListener('click', async (event) => {
+ const settings = extension_settings[extensionName];
+ const tableSystemEnabled = settings.table_system_enabled !== false;
+
+ if (!tableSystemEnabled) {
+ event.preventDefault();
+ toastr.warning('表格系统总开关已关闭,请先启用总开关。');
+ return;
+ }
+
+ const tables = TableManager.getMemoryState();
+ if (!tables || tables.length === 0) {
+ toastr.warning('当前没有表格可供整理。');
+ return;
+ }
+
+ // 构建表格选择列表 HTML
+ const tableListHtml = tables.map((table, index) => `
+
+
+
+
+ `).join('');
+
+ const modalHtml = `
+
+
建议:最好一次只选择一个表格,或少数几个相关联的表格进行整理。一次性处理过多表格可能会导致AI混淆或遗漏信息。
+
请勾选需要AI重新整理和去重的表格:
+
+ ${tableListHtml}
+
+
+
+
+
+
+ `;
+
+ showHtmlModal('选择要整理的表格', modalHtml, {
+ onOk: async (dialogElement) => {
+ const selectedIndices = [];
+ dialogElement.find('input[type="checkbox"]:checked').each(function() {
+ selectedIndices.push(parseInt($(this).val(), 10));
+ });
+
+ if (selectedIndices.length === 0) {
+ toastr.warning('请至少选择一个表格。');
+ return false; // 阻止关闭弹窗
+ }
+
+ try {
+ const { reorganizeTableContent } = await import('../core/table-system/reorganizer.js');
+ await reorganizeTableContent(selectedIndices);
+ } catch (error) {
+ console.error('[内存储司] 重新整理功能导入失败:', error);
+ toastr.error('重新整理功能启动失败,请检查系统状态。');
+ }
+ },
+ onShow: (dialogElement) => {
+ dialogElement.find('#reorg-select-all').on('click', () => {
+ dialogElement.find('input[type="checkbox"]').prop('checked', true);
+ });
+ dialogElement.find('#reorg-deselect-all').on('click', () => {
+ dialogElement.find('input[type="checkbox"]').prop('checked', false);
+ });
+ }
+ });
+ });
+
+ reorganizeBtn.dataset.reorganizeEventBound = 'true';
+ log('"重新整理"按钮已成功绑定。', 'success');
+ }
+}
+
+function bindClearRecordsButton() {
+ const clearBtn = document.getElementById('clear-records-btn');
+ const floorInput = document.getElementById('clear-records-before-floor');
+
+ if (clearBtn && floorInput) {
+ if (clearBtn.dataset.clearEventBound) return;
+
+ clearBtn.addEventListener('click', async () => {
+ const floorIndex = parseInt(floorInput.value, 10);
+ if (isNaN(floorIndex) || floorIndex < 0) {
+ toastr.warning('请输入有效的楼层号。');
+ return;
+ }
+
+ if (confirm(`【警告】您确定要清除第 ${floorIndex} 楼之前的所有表格记录吗?\n\n此操作将永久删除这些消息中存储的表格快照,无法恢复。当前最新的表格状态不会受影响。`)) {
+ try {
+ const { clearTableRecordsBefore } = await import('../core/table-system/cleaner.js');
+ const count = await clearTableRecordsBefore(floorIndex);
+ toastr.success(`已成功清除 ${count} 条消息中的表格记录。`);
+ } catch (error) {
+ console.error('[内存储司] 清除记录失败:', error);
+ toastr.error('清除记录失败,请检查控制台日志。');
+ }
+ }
+ });
+
+ clearBtn.dataset.clearEventBound = 'true';
+ log('"清除记录"按钮已成功绑定。', 'success');
+ }
+}
+
+
+function bindFloorFillButtons() {
+ const selectedFloorsBtn = document.getElementById('fill-selected-floors-btn');
+ const currentFloorBtn = document.getElementById('fill-current-floor-btn');
+ const rollbackBtn = document.getElementById('rollback-and-refill-btn');
+
+ if (selectedFloorsBtn) {
+
+ if (selectedFloorsBtn.dataset.floorEventBound) return;
+
+ selectedFloorsBtn.addEventListener('click', (event) => {
+ const settings = extension_settings[extensionName];
+ const tableSystemEnabled = settings.table_system_enabled !== false;
+
+ if (!tableSystemEnabled) {
+ event.preventDefault();
+ toastr.warning('表格系统总开关已关闭,请先启用总开关。');
+ return;
+ }
+
+ const startFloorInput = document.getElementById('floor-start-input');
+ const endFloorInput = document.getElementById('floor-end-input');
+
+ const startFloor = parseInt(startFloorInput.value, 10);
+ const endFloor = parseInt(endFloorInput.value, 10);
+
+ if (!startFloor || !endFloor) {
+ toastr.warning('请输入有效的起始楼层和结束楼层。');
+ return;
+ }
+
+ if (startFloor > endFloor) {
+ toastr.warning('起始楼层不能大于结束楼层。');
+ return;
+ }
+
+ if (startFloor < 1) {
+ toastr.warning('楼层不能小于1。');
+ return;
+ }
+
+ import('../core/table-system/batch-filler.js').then(module => {
+ module.startFloorRangeFilling(startFloor, endFloor);
+ });
+ });
+
+ selectedFloorsBtn.dataset.floorEventBound = 'true';
+ log('"选定楼层填表"按钮已成功绑定。', 'success');
+ }
+
+ if (currentFloorBtn) {
+ if (currentFloorBtn.dataset.currentEventBound) return;
+
+ currentFloorBtn.addEventListener('click', (event) => {
+ const settings = extension_settings[extensionName];
+ const tableSystemEnabled = settings.table_system_enabled !== false;
+
+ if (!tableSystemEnabled) {
+ event.preventDefault();
+ toastr.warning('表格系统总开关已关闭,请先启用总开关。');
+ return;
+ }
+
+ import('../core/table-system/batch-filler.js').then(module => {
+ module.startCurrentFloorFilling();
+ });
+ });
+
+ currentFloorBtn.dataset.currentEventBound = 'true';
+ log('"填当前楼层"按钮已成功绑定。', 'success');
+ }
+
+ if (rollbackBtn) {
+ if (rollbackBtn.dataset.rollbackEventBound) return;
+
+ rollbackBtn.addEventListener('click', async (event) => {
+ const settings = extension_settings[extensionName];
+ const tableSystemEnabled = settings.table_system_enabled !== false;
+
+ if (!tableSystemEnabled) {
+ event.preventDefault();
+ toastr.warning('表格系统总开关已关闭,请先启用总开关。');
+ return;
+ }
+
+ if (confirm('您确定要将表格状态回退到上一楼,并使用最新消息重新填表吗?')) {
+ try {
+ await TableManager.rollbackAndRefill();
+ } catch (error) {
+ console.error('[内存储司] 回退重填功能失败:', error);
+ toastr.error('回退重填失败,请检查系统状态。');
+ }
+ }
+ });
+
+ rollbackBtn.dataset.rollbackEventBound = 'true';
+ log('"回退重填"按钮已成功绑定。', 'success');
+ }
+}
+
+function bindTemplateEditors() {
+ const ruleEditor = document.getElementById('ai-rule-template-editor');
+ const ruleSaveBtn = document.getElementById('ai-rule-template-save-btn');
+ const ruleRestoreBtn = document.getElementById('ai-rule-template-restore-btn');
+
+ const flowEditor = document.getElementById('ai-flow-template-editor');
+ const flowSaveBtn = document.getElementById('ai-flow-template-save-btn');
+ const flowRestoreBtn = document.getElementById('ai-flow-template-restore-btn');
+
+ if (!ruleEditor || !flowEditor || !ruleSaveBtn || !flowSaveBtn) {
+ log('无法找到指令模板编辑器或其按钮,绑定失败。', 'warn');
+ return;
+ }
+
+ if (ruleSaveBtn.dataset.templateEventsBound) {
+ return;
+ }
+
+ ruleEditor.value = TableManager.getBatchFillerRuleTemplate();
+ flowEditor.value = TableManager.getBatchFillerFlowTemplate();
+
+ ruleSaveBtn.addEventListener('click', () => {
+ TableManager.saveBatchFillerRuleTemplate(ruleEditor.value);
+ toastr.success('规则提示词已保存。');
+ log('批量填表-规则提示词已保存。', 'success');
+ });
+
+ flowSaveBtn.addEventListener('click', () => {
+ TableManager.saveBatchFillerFlowTemplate(flowEditor.value);
+ toastr.success('流程提示词已保存。');
+ log('批量填表-流程提示词已保存。', 'success');
+ });
+
+ ruleRestoreBtn.addEventListener('click', () => {
+ if (confirm('您确定要将规则提示词恢复为默认设置吗?')) {
+ ruleEditor.value = DEFAULT_AI_RULE_TEMPLATE;
+ TableManager.saveBatchFillerRuleTemplate(ruleEditor.value);
+ toastr.info('规则提示词已恢复为默认。');
+ log('批量填表-规则提示词已恢复默认。', 'info');
+ }
+ });
+
+ flowRestoreBtn.addEventListener('click', () => {
+ if (confirm('您确定要将流程提示词恢复为默认设置吗?')) {
+ flowEditor.value = DEFAULT_AI_FLOW_TEMPLATE;
+ TableManager.saveBatchFillerFlowTemplate(flowEditor.value);
+ toastr.info('流程提示词已恢复为默认。');
+ log('批量填表-流程提示词已恢复默认。', 'info');
+ }
+ });
+
+ ruleSaveBtn.dataset.templateEventsBound = 'true';
+ flowSaveBtn.dataset.templateEventsBound = 'true';
+ log('指令模板编辑器已成功绑定。', 'success');
+}
+
+function bindNccsApiEvents() {
+ const settings = extension_settings[extensionName];
+
+ if (settings.nccsEnabled === undefined) settings.nccsEnabled = false;
+ if (settings.nccsApiMode === undefined) settings.nccsApiMode = 'openai_test';
+ if (settings.nccsApiUrl === undefined) settings.nccsApiUrl = 'https://api.openai.com/v1';
+ if (settings.nccsApiKey === undefined) settings.nccsApiKey = '';
+ if (settings.nccsModel === undefined) settings.nccsModel = '';
+ if (settings.nccsMaxTokens === undefined) settings.nccsMaxTokens = 2000;
+ if (settings.nccsTemperature === undefined) settings.nccsTemperature = 0.7;
+ if (settings.nccsTavernProfile === undefined) settings.nccsTavernProfile = '';
+
+ const enabledToggle = document.getElementById('nccs-api-enabled');
+ const configDiv = document.getElementById('nccs-api-config');
+ const modeSelect = document.getElementById('nccs-api-mode');
+ const urlInput = document.getElementById('nccs-api-url');
+ const keyInput = document.getElementById('nccs-api-key');
+ const modelInput = document.getElementById('nccs-api-model');
+ const maxTokensSlider = document.getElementById('nccs-max-tokens');
+ const maxTokensValue = document.getElementById('nccs-max-tokens-value');
+ const temperatureSlider = document.getElementById('nccs-temperature');
+ const temperatureValue = document.getElementById('nccs-temperature-value');
+ const presetSelect = document.getElementById('nccs-sillytavern-preset');
+ const testButton = document.getElementById('nccs-test-connection');
+ const fetchModelsButton = document.getElementById('nccs-fetch-models');
+
+ if (!enabledToggle || !configDiv) return;
+
+ enabledToggle.checked = settings.nccsEnabled;
+ if (modeSelect) modeSelect.value = settings.nccsApiMode;
+ if (urlInput) urlInput.value = settings.nccsApiUrl;
+ if (keyInput) keyInput.value = settings.nccsApiKey;
+ if (modelInput) modelInput.value = settings.nccsModel;
+ if (maxTokensSlider) {
+ maxTokensSlider.value = settings.nccsMaxTokens;
+ if (maxTokensValue) maxTokensValue.textContent = settings.nccsMaxTokens;
+ }
+ if (temperatureSlider) {
+ temperatureSlider.value = settings.nccsTemperature;
+ if (temperatureValue) temperatureValue.textContent = settings.nccsTemperature;
+ }
+ if (presetSelect) presetSelect.value = settings.nccsTavernProfile || '';
+
+ const updateConfigVisibility = () => {
+ configDiv.style.display = enabledToggle.checked ? 'block' : 'none';
+ };
+ updateConfigVisibility();
+
+ const updateModeBasedVisibility = () => {
+ if (!modeSelect) return;
+ const isSillyTavernMode = modeSelect.value === 'sillytavern_preset';
+ const isOpenAIMode = modeSelect.value === 'openai_test';
+
+ const presetContainer = presetSelect?.closest('.amily2_opt_settings_block');
+ if (presetContainer) {
+ presetContainer.style.display = isSillyTavernMode ? 'block' : 'none';
+ }
+
+ const fieldsToHideInPresetMode = [
+ { element: urlInput, containerId: null },
+ { element: keyInput, containerId: null },
+ { element: modelInput, containerId: null },
+ { element: maxTokensSlider, containerId: null },
+ { element: temperatureSlider, containerId: null }
+ ];
+
+ fieldsToHideInPresetMode.forEach(({ element }) => {
+ if (element) {
+ const container = element.closest('.amily2_opt_settings_block');
+ if (container) {
+ container.style.display = isSillyTavernMode ? 'none' : 'block';
+ }
+ }
+ });
+
+ const buttonsContainer = testButton?.closest('.nccs-button-row');
+ if (buttonsContainer) {
+ buttonsContainer.style.display = 'flex';
+ }
+ };
+ updateModeBasedVisibility();
+
+ enabledToggle.addEventListener('change', () => {
+ settings.nccsEnabled = enabledToggle.checked;
+ saveSettingsDebounced();
+ updateConfigVisibility();
+ log(`Nccs API ${enabledToggle.checked ? '已启用' : '已禁用'}`, 'info');
+ });
+
+ if (modeSelect) {
+ modeSelect.addEventListener('change', () => {
+ settings.nccsApiMode = modeSelect.value;
+ saveSettingsDebounced();
+ updateModeBasedVisibility();
+ log(`Nccs API模式已切换为: ${modeSelect.value}`, 'info');
+ });
+ }
+
+ if (urlInput) {
+ const saveUrl = () => {
+ settings.nccsApiUrl = urlInput.value;
+ saveSettingsDebounced();
+ };
+
+ urlInput.addEventListener('blur', saveUrl);
+ }
+
+ if (keyInput) {
+ const saveKey = () => {
+ settings.nccsApiKey = keyInput.value;
+ saveSettingsDebounced();
+ };
+
+ keyInput.addEventListener('blur', saveKey);
+ }
+
+ if (modelInput) {
+ const saveModel = () => {
+ settings.nccsModel = modelInput.value;
+ saveSettingsDebounced();
+ };
+
+ modelInput.addEventListener('blur', saveModel);
+ modelInput.addEventListener('input', saveModel);
+ }
+
+ if (maxTokensSlider && maxTokensValue) {
+ maxTokensSlider.addEventListener('input', () => {
+ maxTokensValue.textContent = maxTokensSlider.value;
+ });
+ maxTokensSlider.addEventListener('change', () => {
+ settings.nccsMaxTokens = parseInt(maxTokensSlider.value);
+ saveSettingsDebounced();
+ });
+ }
+
+ if (temperatureSlider && temperatureValue) {
+ temperatureSlider.addEventListener('input', () => {
+ temperatureValue.textContent = temperatureSlider.value;
+ });
+ temperatureSlider.addEventListener('change', () => {
+ settings.nccsTemperature = parseFloat(temperatureSlider.value);
+ saveSettingsDebounced();
+ });
+ }
+
+ if (presetSelect) {
+ presetSelect.addEventListener('change', () => {
+ settings.nccsTavernProfile = presetSelect.value;
+ saveSettingsDebounced();
+ });
+ }
+
+ if (testButton) {
+ testButton.addEventListener('click', async () => {
+ testButton.disabled = true;
+ testButton.innerHTML = ' 测试中...';
+
+ try {
+ const success = await testNccsApiConnection();
+ if (success) {
+ toastr.success('Nccs API连接测试成功!');
+ log('Nccs API连接测试成功', 'success');
+ } else {
+ toastr.error('Nccs API连接测试失败,请检查配置');
+ log('Nccs API连接测试失败', 'error');
+ }
+ } catch (error) {
+ toastr.error('Nccs API连接测试出错:' + error.message);
+ log('Nccs API连接测试出错:' + error.message, 'error');
+ } finally {
+ testButton.disabled = false;
+ testButton.innerHTML = ' 测试连接';
+ }
+ });
+ }
+
+ if (fetchModelsButton) {
+ fetchModelsButton.addEventListener('click', async () => {
+ fetchModelsButton.disabled = true;
+ fetchModelsButton.innerHTML = ' 获取中...';
+
+ if (urlInput) {
+ settings.nccsApiUrl = urlInput.value;
+ }
+ if (keyInput) {
+ settings.nccsApiKey = keyInput.value;
+ }
+ saveSettingsDebounced();
+
+ try {
+ const models = await fetchNccsModels();
+ if (models && models.length > 0) {
+ let modelSelect = document.getElementById('nccs-api-model-select');
+ if (!modelSelect) {
+ modelSelect = document.createElement('select');
+ modelSelect.id = 'nccs-api-model-select';
+ modelSelect.className = 'text_pole';
+ modelInput.parentNode.insertBefore(modelSelect, modelInput.nextSibling);
+ }
+
+ modelSelect.innerHTML = '';
+ models.forEach(model => {
+ const option = document.createElement('option');
+ option.value = model.id || model.name;
+ option.textContent = model.name || model.id;
+ if ((model.id || model.name) === settings.nccsModel) {
+ option.selected = true;
+ }
+ modelSelect.appendChild(option);
+ });
+
+ modelInput.style.display = 'none';
+ modelSelect.style.display = 'block';
+
+ modelSelect.addEventListener('change', () => {
+ const selectedModel = modelSelect.value;
+ settings.nccsModel = selectedModel;
+ modelInput.value = selectedModel;
+ saveSettingsDebounced();
+ });
+
+ toastr.success(`成功获取 ${models.length} 个模型`);
+ log(`Nccs API获取到 ${models.length} 个模型`, 'success');
+ } else {
+ toastr.warning('未获取到可用模型');
+ log('Nccs API未获取到可用模型', 'warn');
+ }
+ } catch (error) {
+ toastr.error('获取模型失败:' + error.message);
+ log('Nccs API获取模型失败:' + error.message, 'error');
+ } finally {
+ fetchModelsButton.disabled = false;
+ fetchModelsButton.innerHTML = ' 获取模型';
+ }
+ });
+ }
+
+ const loadSillyTavernPresets = async () => {
+ if (!presetSelect) return;
+ try {
+ const context = getContext();
+ if (!context?.extensionSettings?.connectionManager?.profiles) {
+ throw new Error('无法获取SillyTavern配置文件列表');
+ }
+
+ const profiles = context.extensionSettings.connectionManager.profiles;
+
+ const currentProfileId = settings.nccsTavernProfile;
+
+ presetSelect.innerHTML = '';
+ presetSelect.appendChild(new Option('选择预设', '', false, false));
+
+ if (profiles && profiles.length > 0) {
+ profiles.forEach(profile => {
+ const isSelected = profile.id === currentProfileId;
+ const option = new Option(profile.name, profile.id, isSelected, isSelected);
+ presetSelect.appendChild(option);
+ });
+ log(`成功加载 ${profiles.length} 个SillyTavern配置文件`, 'success');
+ } else {
+ log('未找到可用的SillyTavern配置文件', 'warn');
+ }
+ } catch (error) {
+ log('加载SillyTavern预设失败:' + error.message, 'error');
+ }
+ };
+
+ if (modeSelect && presetSelect) {
+ modeSelect.addEventListener('change', () => {
+ if (modeSelect.value === 'sillytavern_preset') {
+ loadSillyTavernPresets();
+ }
+ });
+
+ if (settings.nccsApiMode === 'sillytavern_preset') {
+ loadSillyTavernPresets();
+ }
+ }
+
+ log('Nccs API事件绑定完成', 'success');
+}
+
+function bindChatTableDisplaySetting() {
+ const settings = extension_settings[extensionName];
+ const showInChatToggle = document.getElementById('show-table-in-chat-toggle');
+ const continuousRenderToggle = document.getElementById('render-on-every-message-toggle');
+
+ if (!showInChatToggle || !continuousRenderToggle) {
+ log('找不到聊天内表格相关的开关,绑定失败。', 'warn');
+ return;
+ }
+
+ showInChatToggle.checked = settings.show_table_in_chat === true;
+ continuousRenderToggle.checked = settings.render_on_every_message === true;
+
+ const updateContinuousRenderState = () => {
+ if (showInChatToggle.checked) {
+ continuousRenderToggle.disabled = false;
+ continuousRenderToggle.closest('.control-block-with-switch').style.opacity = '1';
+ } else {
+ continuousRenderToggle.disabled = true;
+ continuousRenderToggle.closest('.control-block-with-switch').style.opacity = '0.5';
+ }
+ };
+
+ updateContinuousRenderState();
+
+ showInChatToggle.addEventListener('change', () => {
+ settings.show_table_in_chat = showInChatToggle.checked;
+ saveSettingsDebounced();
+ toastr.info(`聊天内表格显示已${showInChatToggle.checked ? '开启' : '关闭'}。`);
+ updateContinuousRenderState();
+ });
+
+ continuousRenderToggle.addEventListener('change', () => {
+ settings.render_on_every_message = continuousRenderToggle.checked;
+ saveSettingsDebounced();
+ toastr.info(`持续渲染最新消息功能已${continuousRenderToggle.checked ? '开启' : '关闭'}。请切换聊天以应用更改。`);
+ });
+
+ log('聊天内表格显示设置及其依赖关系已成功绑定。', 'success');
+}