import { world_names, loadWorldInfo, saveWorldInfo, deleteWorldInfo, updateWorldInfoList } from "/scripts/world-info.js"; 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 } from '../core/tavernhelper-compatibility.js'; import { writeToLorebookWithTavernHelper } from '../core/lore.js'; const { SillyTavern, TavernHelper } = window; class WorldEditor { constructor() { // 通用状态 this.isLoading = false; // 世界书视图状态 this.allWorldBooks = []; this.filteredWorldBooks = []; this.selectedWorldBooks = new Set(); // 条目视图状态 this.currentWorldBook = null; this.entries = []; this.selectedEntries = new Set(); this.filteredEntries = []; this.currentEditingEntry = null; this.sortState = { key: 'order', asc: true }; this.init(); } init() { if (!this.initializeComponents()) { console.error('[世界书编辑器] 组件初始化失败,5秒后重试...'); setTimeout(() => this.init(), 5000); return; } this.bindEvents(); this.loadAvailableWorldBooks(); this.bindExternalEvents(); } initializeComponents() { const ids = [ // 主视图 'world-book-selection-view', 'world-editor-entry-view', // 顶部按钮 'world-editor-refresh-btn', 'world-editor-create-book-btn', 'world-editor-create-entry-btn', // 世界书视图 'world-book-search-box', 'world-book-search-btn', 'world-book-count', 'world-book-batch-actions', 'world-book-selected-count', 'world-book-clone-btn', 'world-book-delete-btn', 'world-book-list-container', // 条目视图 'world-editor-current-book-title', 'world-editor-back-to-list-btn', 'world-editor-search-type', 'world-editor-search-box', 'world-editor-search-btn', 'world-editor-entry-count', 'world-editor-select-all', 'world-editor-selected-count', 'world-editor-batch-actions', 'world-editor-entries-container', 'world-editor-enable-selected-btn', 'world-editor-disable-selected-btn', 'world-editor-set-blue-btn', 'world-editor-set-green-btn', 'world-editor-delete-selected-btn', 'world-editor-set-disable-recursion-btn', 'world-editor-set-prevent-recursion-btn' ]; this.elements = {}; let missing = false; for (const id of ids) { const camelCaseId = id.replace(/-(\w)/g, (_, c) => c.toUpperCase()); this.elements[camelCaseId] = document.getElementById(id); if (!this.elements[camelCaseId]) { console.warn(`[世界书编辑器] UI元素缺失: ${id}`); if (id.endsWith('container') || id.endsWith('view')) { missing = true; // 关键元素缺失 } } } return !missing; } bindEvents() { // 视图切换 this.elements.worldEditorBackToListBtn.addEventListener('click', () => this.switchToBookListView()); // 顶部按钮 this.elements.worldEditorRefreshBtn.addEventListener('click', () => this.loadAvailableWorldBooks()); this.elements.worldEditorCreateBookBtn.addEventListener('click', () => this.createNewWorldBook()); this.elements.worldEditorCreateEntryBtn.addEventListener('click', () => this.openCreateModal()); // 世界书视图事件 this.elements.worldBookSearchBox.addEventListener('input', () => this.filterWorldBooks()); this.elements.worldBookSearchBtn.addEventListener('click', () => this.filterWorldBooks()); this.elements.worldBookCloneBtn.addEventListener('click', () => this.cloneSelectedBooks()); this.elements.worldBookDeleteBtn.addEventListener('click', () => this.deleteSelectedBooks()); // 条目视图事件 document.querySelector('#world-editor-entry-view .world-editor-entries-header').addEventListener('click', (e) => { if (e.target.dataset.sort) this.sortEntries(e.target.dataset.sort); }); this.elements.worldEditorSearchBox.addEventListener('input', () => this.filterEntries()); this.elements.worldEditorSearchBtn.addEventListener('click', () => this.filterEntries()); this.elements.worldEditorSelectAll.addEventListener('change', (e) => this.toggleSelectAll(e.target.checked)); this.elements.worldEditorEnableSelectedBtn.addEventListener('click', () => this.batchUpdateEntries({ enabled: true })); this.elements.worldEditorDisableSelectedBtn.addEventListener('click', () => this.batchUpdateEntries({ enabled: false })); this.elements.worldEditorSetBlueBtn.addEventListener('click', () => this.batchUpdateEntries({ type: 'constant' })); this.elements.worldEditorSetGreenBtn.addEventListener('click', () => this.batchUpdateEntries({ type: 'selective' })); this.elements.worldEditorDeleteSelectedBtn.addEventListener('click', () => this.batchDeleteEntries()); this.elements.worldEditorSetDisableRecursionBtn.addEventListener('click', () => this.toggleBatchRecursion('exclude_recursion', '不可递归')); this.elements.worldEditorSetPreventRecursionBtn.addEventListener('click', () => this.toggleBatchRecursion('prevent_recursion', '防止递归')); } // 视图管理 switchToBookListView() { this.elements.worldBookSelectionView.style.display = 'block'; this.elements.worldEditorEntryView.style.display = 'none'; this.elements.worldEditorCreateEntryBtn.disabled = true; this.currentWorldBook = null; } switchToEntryView(bookName) { this.elements.worldBookSelectionView.style.display = 'none'; this.elements.worldEditorEntryView.style.display = 'block'; this.elements.worldEditorCreateEntryBtn.disabled = false; this.elements.worldEditorCurrentBookTitle.textContent = `当前编辑:${bookName}`; this.loadWorldBookEntries(bookName); } // 世界书数据处理 async loadAvailableWorldBooks() { this.setLoading(true); try { const books = await this.getAllWorldBooks(); this.allWorldBooks = books.sort((a, b) => a.name.localeCompare(b.name)); this.filterWorldBooks(); // 这会渲染列表 } catch (error) { this.showError('加载世界书列表失败: ' + error.message); } finally { this.setLoading(false); } } async getAllWorldBooks() { const books = await safeLorebooks(); return books.map(name => ({ name })); } filterWorldBooks() { const term = this.elements.worldBookSearchBox.value.toLowerCase(); this.filteredWorldBooks = this.allWorldBooks.filter(book => book.name.toLowerCase().includes(term)); this.renderWorldBookList(); this.updateWorldBookCount(); } renderWorldBookList() { const container = this.elements.worldBookListContainer; container.innerHTML = ''; // 清空 if (this.filteredWorldBooks.length === 0) { container.innerHTML = '

没有找到世界书

'; return; } const fragment = document.createDocumentFragment(); this.filteredWorldBooks.forEach(book => { const isSelected = this.selectedWorldBooks.has(book.name); const row = document.createElement('div'); row.className = `world-book-row ${isSelected ? 'selected' : ''}`; row.dataset.bookName = book.name; row.innerHTML = ` ${book.name}
`; fragment.appendChild(row); }); container.appendChild(fragment); this.bindWorldBookListEvents(); } bindWorldBookListEvents() { this.elements.worldBookListContainer.querySelectorAll('.world-book-row').forEach(row => { const bookName = row.dataset.bookName; // 复选框事件 row.querySelector('.world-book-checkbox').addEventListener('change', (e) => { if (e.target.checked) { this.selectedWorldBooks.add(bookName); } else { this.selectedWorldBooks.delete(bookName); } row.classList.toggle('selected', e.target.checked); this.updateWorldBookSelectionUI(); }); // 按钮事件 row.querySelector('[data-action="edit"]').addEventListener('click', (e) => { e.stopPropagation(); this.switchToEntryView(bookName); }); row.querySelector('[data-action="rename"]').addEventListener('click', (e) => { e.stopPropagation(); this.renameWorldBook(bookName); }); }); } async createNewWorldBook() { const bookName = prompt("请输入新的世界书名称:"); if (bookName && bookName.trim()) { const trimmedBookName = bookName.trim(); try { await writeToLorebookWithTavernHelper(trimmedBookName, '新条目', () => '这是一个新条目', {}); if (window.toastr) window.toastr.success(`世界书 "${trimmedBookName}" 创建成功!`); this.loadAvailableWorldBooks(); } catch (error) { this.showError(`创建失败: ${error.message}`); } } } async renameWorldBook(oldName) { const newName = prompt(`重命名世界书 "${oldName}":`, oldName); if (newName && newName.trim() && newName !== oldName) { const trimmedNewName = newName.trim(); try { const bookData = await loadWorldInfo(oldName); await saveWorldInfo(trimmedNewName, bookData); await deleteWorldInfo(oldName); if (window.toastr) window.toastr.success('重命名成功!'); await updateWorldInfoList(); eventSource.emit(event_types.CHARACTER_PAGE_LOADED); this.loadAvailableWorldBooks(); } catch (error) { this.showError(`重命名失败: ${error.message}`); } } } async cloneSelectedBooks() { if (this.selectedWorldBooks.size === 0) return; if (!confirm(`确定要为 ${this.selectedWorldBooks.size} 个世界书创建备份吗?`)) return; this.setLoading(true); try { for (const bookName of this.selectedWorldBooks) { const newName = `${bookName}_备份_${Date.now()}`; const bookData = await loadWorldInfo(bookName); await saveWorldInfo(newName, bookData); } if (window.toastr) window.toastr.success('备份创建成功!'); await updateWorldInfoList(); eventSource.emit(event_types.CHARACTER_PAGE_LOADED); this.loadAvailableWorldBooks(); } catch (error) { this.showError(`备份失败: ${error.message}`); } finally { this.setLoading(false); } } async deleteSelectedBooks() { if (this.selectedWorldBooks.size === 0) return; if (!confirm(`警告:这将永久删除 ${this.selectedWorldBooks.size} 个世界书及其所有内容!确定要继续吗?`)) return; this.setLoading(true); try { for (const bookName of this.selectedWorldBooks) { await deleteWorldInfo(bookName); } if (window.toastr) window.toastr.success('批量删除成功!'); await updateWorldInfoList(); eventSource.emit(event_types.CHARACTER_PAGE_LOADED); this.loadAvailableWorldBooks(); } catch (error) { this.showError(`删除失败: ${error.message}`); } finally { this.setLoading(false); } } updateWorldBookCount() { this.elements.worldBookCount.textContent = `世界书:${this.allWorldBooks.length}`; } updateWorldBookSelectionUI() { const count = this.selectedWorldBooks.size; this.elements.worldBookSelectedCount.textContent = `已选择 ${count} 项`; this.elements.worldBookBatchActions.classList.toggle('active', count > 0); } // 条目数据处理 (大部分逻辑与旧版相似) async loadWorldBookEntries(worldBookName) { if (!worldBookName) { this.entries = []; this.filteredEntries = []; this.selectedEntries.clear(); this.renderEntries(); this.updateEntryCount(); this.updateSelectionUI(); return; } this.setLoading(true); this.currentWorldBook = worldBookName; try { const rawEntries = await safeLorebookEntries(worldBookName); this.entries = (rawEntries || []).map(e => ({ uid: e.uid, enabled: e.enabled, type: e.type || (e.constant ? 'constant' : 'selective'), keys: e.keys || [], content: e.content || '', position: e.position || 'before_character_definition', depth: (e.position?.startsWith('at_depth')) ? e.depth : null, order: e.order || 100, comment: e.comment || '', exclude_recursion: e.exclude_recursion, prevent_recursion: e.prevent_recursion })); this.filteredEntries = [...this.entries]; this.renderEntries(); this.updateEntryCount(); } catch (error) { this.showError(`加载条目失败: ${error.message}`); this.entries = []; this.filteredEntries = []; } finally { this.selectedEntries.clear(); this.updateSelectionUI(); this.setLoading(false); } } convertPositionFromNative(pos) { const map = { 0: 'before_character_definition', 1: 'after_character_definition', 2: 'before_author_note', 3: 'after_author_note', 4: 'at_depth' }; return map[pos] || 'at_depth'; } renderEntries() { const container = this.elements.worldEditorEntriesContainer; const header = container.querySelector('.world-editor-entries-header'); while (header && header.nextSibling) { container.removeChild(header.nextSibling); } this.sortFilteredEntries(); if (this.filteredEntries.length === 0) { const emptyState = document.createElement('div'); emptyState.className = 'world-editor-empty-state'; emptyState.innerHTML = '

没有条目

'; container.appendChild(emptyState); return; } const fragment = document.createDocumentFragment(); this.filteredEntries.forEach(e => { const rowHTML = this.renderEntryRow(e).trim(); const tempDiv = document.createElement('div'); tempDiv.innerHTML = rowHTML; const rowElement = tempDiv.firstChild; const contentCell = rowElement.querySelector('.world-editor-entry-content'); if (contentCell) { contentCell.textContent = e.content || ''; } fragment.appendChild(rowElement); }); container.appendChild(fragment); this.bindEntryEvents(); } renderEntryRow(entry) { const positionOptions = { 'before_character_definition': '角色前', 'after_character_definition': '角色后', 'before_author_note': '注释前', 'after_author_note': '注释后', 'at_depth': '@D深度', 'at_depth_as_system': '@D深度' }; const positionSelect = ``; return `
${entry.type === 'constant' ? '🔵' : '🟢'}
${entry.content || ''}
${positionSelect}
`; } bindEntryEvents() { this.elements.worldEditorEntriesContainer.querySelectorAll('.world-editor-entry-row').forEach(row => { const uid = parseInt(row.dataset.uid); const checkbox = row.querySelector('.world-editor-entry-checkbox'); checkbox.addEventListener('change', (e) => { if (e.target.checked) this.selectedEntries.add(uid); else this.selectedEntries.delete(uid); row.classList.toggle('selected', e.target.checked); this.updateSelectionUI(); }); row.querySelector('[data-action="open-editor"]').addEventListener('click', () => this.openEditModal(uid)); row.querySelectorAll('.inline-toggle').forEach(toggle => { toggle.addEventListener('click', (e) => { e.stopPropagation(); const field = toggle.dataset.field; const entry = this.entries.find(e => e.uid === uid); let newValue; if (field === 'enabled') newValue = !entry.enabled; if (field === 'type') newValue = entry.type === 'constant' ? 'selective' : 'constant'; this.updateSingleEntry(uid, { [field]: newValue }); }); }); row.querySelectorAll('.inline-edit').forEach(input => { input.addEventListener('change', (e) => { e.stopPropagation(); const field = input.dataset.field; let value = input.value; if (input.type === 'number') value = parseInt(value, 10); const updates = { [field]: value }; if (field === 'position') { const depthInput = row.querySelector('[data-field="depth"]'); if (depthInput) depthInput.disabled = !value.startsWith('at_depth'); } this.updateSingleEntry(uid, updates); }); input.addEventListener('click', e => e.stopPropagation()); }); }); } /** * 使用原生 saveWorldInfo 更新条目,避免界面跳转 * @param {Array} entriesToUpdate - 需要更新的条目对象数组 */ async updateEntriesWithNativeMethod(entriesToUpdate) { try { const bookData = await loadWorldInfo(this.currentWorldBook); if (!bookData || !bookData.entries) { throw new Error("无法加载世界书数据。"); } const uidsToUpdate = new Set(entriesToUpdate.map(e => e.uid)); const updatedUIDs = new Set(); // 更新 bookData.entries for (const entry of entriesToUpdate) { if (bookData.entries[entry.uid]) { const nativeEntry = bookData.entries[entry.uid]; nativeEntry.comment = entry.comment; nativeEntry.content = entry.content; nativeEntry.key = entry.keys; nativeEntry.disable = !entry.enabled; nativeEntry.constant = entry.type === 'constant'; nativeEntry.position = this.convertPositionToNative(entry.position); nativeEntry.depth = entry.depth; nativeEntry.order = entry.order; nativeEntry.exclude_recursion = entry.exclude_recursion; nativeEntry.prevent_recursion = entry.prevent_recursion; updatedUIDs.add(entry.uid); } } if (updatedUIDs.size !== uidsToUpdate.size) { console.warn("[世界书编辑器] 部分条目更新失败,UID可能不存在。"); } await saveWorldInfo(this.currentWorldBook, bookData, true); // true 表示静默保存 // Optimistic UI update in local state for (const updatedEntry of entriesToUpdate) { const localEntry = this.entries.find(e => e.uid === updatedEntry.uid); if (localEntry) { Object.assign(localEntry, updatedEntry); } } this.renderEntries(); } catch (error) { this.showError(`更新失败: ${error.message}`); this.loadWorldBookEntries(this.currentWorldBook); // On error, re-sync with truth } } // Helper function to convert string position to native number format convertPositionToNative(posStr) { const map = { 'before_character_definition': 0, 'after_character_definition': 1, 'before_author_note': 2, 'after_author_note': 3, 'at_depth': 4, 'at_depth_as_system': 4 }; return map[posStr] !== undefined ? map[posStr] : 4; } async updateSingleEntry(uid, updates) { const entry = this.entries.find(e => e.uid === uid); if (!entry) return; const updatedEntry = { ...entry, ...updates }; await this.updateEntriesWithNativeMethod([updatedEntry]); } async batchUpdateEntries(updates, confirmation = null) { if (this.selectedEntries.size === 0) return; if (confirmation && !confirm(confirmation)) return; const entriesToUpdate = this.entries .filter(e => this.selectedEntries.has(e.uid)) .map(e => ({ ...e, ...updates })); await this.updateEntriesWithNativeMethod(entriesToUpdate); if (window.toastr) window.toastr.success('批量更新成功!'); } toggleBatchRecursion(field, fieldName) { if (this.selectedEntries.size === 0) return; const selected = this.entries.filter(e => this.selectedEntries.has(e.uid)); const enabledCount = selected.filter(e => e[field]).length; const shouldEnable = enabledCount <= selected.length / 2; const action = shouldEnable ? '启用' : '禁用'; const confirmation = `确定为 ${this.selectedEntries.size} 个条目 ${action} "${fieldName}" 吗?`; this.batchUpdateEntries({ [field]: shouldEnable }, confirmation); } async batchDeleteEntries() { if (this.selectedEntries.size === 0 || !confirm(`删除 ${this.selectedEntries.size} 个条目?`)) return; try { await TavernHelper.deleteLorebookEntries(this.currentWorldBook, Array.from(this.selectedEntries)); this.loadWorldBookEntries(this.currentWorldBook); } catch (error) { this.showError(`删除失败: ${error.message}`); } } toggleSelectAll(checked) { this.selectedEntries.clear(); if (checked) this.filteredEntries.forEach(e => this.selectedEntries.add(e.uid)); this.renderEntries(); this.updateSelectionUI(); } updateSelectionUI() { const count = this.selectedEntries.size; this.elements.worldEditorSelectedCount.textContent = `已选择 ${count} 项`; this.elements.worldEditorBatchActions.classList.toggle('active', count > 0); this.elements.worldEditorSelectAll.checked = count > 0 && count === this.filteredEntries.length; this.elements.worldEditorSelectAll.indeterminate = count > 0 && count < this.filteredEntries.length; } updateEntryCount() { this.elements.worldEditorEntryCount.textContent = `条目:${this.entries.length}`; } filterEntries() { const term = this.elements.worldEditorSearchBox.value.toLowerCase(); const searchType = this.elements.worldEditorSearchType.value; this.filteredEntries = !term ? [...this.entries] : this.entries.filter(e => (e[searchType] || '').toLowerCase().includes(term)); this.renderEntries(); } openCreateModal() { this.currentEditingEntry = null; const entry = { enabled: true, type: 'selective', keys: [], content: '', position: 'at_depth', depth: 4, order: 100, comment: '' }; this.showEditModal('创建新条目', entry); } openEditModal(uid) { const entry = this.entries.find(e => e.uid === uid); if (!entry) return; this.currentEditingEntry = entry; this.showEditModal('编辑条目', entry); } showEditModal(title, entry) { const formHtml = this.getEditFormHtml(entry); showHtmlModal(title, formHtml, { onOk: (d) => { this.saveEntry(d); return true; } }); } getEditFormHtml(entry) { return `
`; } async saveEntry(dialog) { const formData = this.getFormDataFromModal(dialog); try { if (this.currentEditingEntry) { // 使用改造后的原生方法更新 await this.updateEntriesWithNativeMethod([{ ...this.currentEditingEntry, ...formData }]); } else { // 创建条目仍然可以使用TavernHelper,因为它通常不会触发跳转 await TavernHelper.createLorebookEntries(this.currentWorldBook, [formData]); } // 刷新当前视图 this.loadWorldBookEntries(this.currentWorldBook); } catch (error) { this.showError(`保存失败: ${error.message}`); } } getFormDataFromModal(dialog) { return { enabled: dialog.find('#world-editor-entry-enabled').is(':checked'), type: dialog.find('#world-editor-entry-type').val(), keys: dialog.find('#world-editor-entry-keys').val().split('\n').map(k => k.trim()).filter(Boolean), content: dialog.find('#world-editor-entry-content').val(), position: dialog.find('#world-editor-entry-position').val(), depth: parseInt(dialog.find('#world-editor-entry-depth').val()), order: parseInt(dialog.find('#world-editor-entry-order').val()), comment: dialog.find('#world-editor-entry-comment').val(), exclude_recursion: dialog.find('#world-editor-entry-disable-recursion').is(':checked'), prevent_recursion: dialog.find('#world-editor-entry-prevent-recursion').is(':checked') }; } setLoading(loading) { this.isLoading = loading; document.getElementById('world-editor-container').classList.toggle('loading', loading); } showError(msg) { if (window.toastr) window.toastr.error(msg); console.error(msg); } sortEntries(key) { if (this.sortState.key === key) this.sortState.asc = !this.sortState.asc; else { this.sortState.key = key; this.sortState.asc = true; } this.renderEntries(); } sortFilteredEntries() { const { key, asc } = this.sortState; this.filteredEntries.sort((a, b) => { let valA = a[key], valB = b[key]; if (typeof valA === 'string') valA = valA.toLowerCase(); if (typeof valB === 'string') valB = valB.toLowerCase(); if (valA < valB) return asc ? -1 : 1; if (valA > valB) return asc ? 1 : -1; return 0; }); } bindExternalEvents() { eventSource.on(event_types.CHAT_CHANGED, () => { console.log('[世界书编辑器] 检测到聊天变更,将自动刷新。'); if (this.currentWorldBook) { this.loadWorldBookEntries(this.currentWorldBook); } else { this.loadAvailableWorldBooks(); } }); console.log('[世界书编辑器] 已成功绑定外部事件监听器。'); } } function initializeWorldEditor() { // 确保面板存在 if (!document.getElementById('amily2_world_editor_panel')) { console.error('[WorldEditor] Panel not found, initialization aborted.'); return; } // 防止重复初始化 if (!window.worldEditorInstance) { console.log('[WorldEditor] Initializing WorldEditor instance.'); window.worldEditorInstance = new WorldEditor(); } } // 确保在DOM加载完毕后执行 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeWorldEditor); } else { initializeWorldEditor(); }