From 14ae17d997a326bb52c17786a900b3469ef56ca9 Mon Sep 17 00:00:00 2001
From: Wx-2025 <351320169@qq.com>
Date: Sun, 12 Oct 2025 16:07:27 +0800
Subject: [PATCH] Update WorldEditor.js
---
WorldEditor/WorldEditor.js | 601 +++++++++++++++++++++++--------------
1 file changed, 377 insertions(+), 224 deletions(-)
diff --git a/WorldEditor/WorldEditor.js b/WorldEditor/WorldEditor.js
index 659b8b1..bd1b878 100644
--- a/WorldEditor/WorldEditor.js
+++ b/WorldEditor/WorldEditor.js
@@ -1,20 +1,29 @@
-/**
- * 世界书编辑器 - 最终稳定版
- */
-import { world_names, loadWorldInfo, saveWorldInfo } from "/scripts/world-info.js";
+
+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.isLoading = false;
this.currentEditingEntry = null;
this.sortState = { key: 'order', asc: true };
+
this.init();
}
@@ -26,13 +35,22 @@ class WorldEditor {
}
this.bindEvents();
this.loadAvailableWorldBooks();
- this.bindExternalEvents(); // 绑定外部事件监听
+ this.bindExternalEvents();
}
initializeComponents() {
const ids = [
- 'world-editor-world-select', 'world-editor-refresh-btn', 'world-editor-create-entry-btn',
- 'world-editor-search-box', 'world-editor-search-btn', 'world-editor-entry-count',
+ // 主视图
+ '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',
@@ -44,23 +62,35 @@ class WorldEditor {
for (const id of ids) {
const camelCaseId = id.replace(/-(\w)/g, (_, c) => c.toUpperCase());
this.elements[camelCaseId] = document.getElementById(id);
- if (!this.elements[camelCaseId] && id.endsWith('container')) { // Only container is critical
- console.error(`[世界书编辑器] 关键元素缺失: ${id}`);
- missing = true;
+ if (!this.elements[camelCaseId]) {
+ console.warn(`[世界书编辑器] UI元素缺失: ${id}`);
+ if (id.endsWith('container') || id.endsWith('view')) {
+ missing = true; // 关键元素缺失
+ }
}
}
return !missing;
}
bindEvents() {
- this.elements.worldEditorWorldSelect.addEventListener('change', (e) => this.loadWorldBookEntries(e.target.value));
+ // 视图切换
+ this.elements.worldEditorBackToListBtn.addEventListener('click', () => this.switchToBookListView());
+
+ // 顶部按钮
this.elements.worldEditorRefreshBtn.addEventListener('click', () => this.loadAvailableWorldBooks());
- document.querySelector('#world-editor-container .world-editor-entries-header').addEventListener('click', (e) => {
- if (e.target.dataset.sort) {
- this.sortEntries(e.target.dataset.sort);
- }
- });
+ 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));
@@ -73,19 +103,29 @@ class WorldEditor {
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();
- const select = this.elements.worldEditorWorldSelect;
- select.innerHTML = '';
- books.forEach(book => {
- const option = document.createElement('option');
- option.value = book.name;
- option.textContent = book.name;
- select.appendChild(option);
- });
- await this.selectCurrentCharacterWorldBook();
+ this.allWorldBooks = books.sort((a, b) => a.name.localeCompare(b.name));
+ this.filterWorldBooks(); // 这会渲染列表
} catch (error) {
this.showError('加载世界书列表失败: ' + error.message);
} finally {
@@ -94,39 +134,174 @@ class WorldEditor {
}
async getAllWorldBooks() {
- if (TavernHelper?.getLorebooks) {
- const books = await TavernHelper.getLorebooks();
- if (Array.isArray(books) && books.length > 0) return books.map(name => ({ name }));
- }
- return (world_names || []).map(name => ({ name }));
+ const books = await safeLorebooks();
+ return books.map(name => ({ name }));
}
- async selectCurrentCharacterWorldBook() {
- if (TavernHelper?.getCurrentCharPrimaryLorebook) {
- const primaryBook = await TavernHelper.getCurrentCharPrimaryLorebook();
- if (primaryBook) {
- this.elements.worldEditorWorldSelect.value = primaryBook;
- await this.loadWorldBookEntries(primaryBook);
+ 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.renderEntries(); return; }
+ if (!worldBookName) {
+ this.entries = [];
+ this.filteredEntries = [];
+ this.selectedEntries.clear();
+ this.renderEntries();
+ this.updateEntryCount();
+ this.updateSelectionUI();
+ return;
+ }
this.setLoading(true);
this.currentWorldBook = worldBookName;
try {
- let rawEntries = await TavernHelper?.getLorebookEntries?.(worldBookName);
- if (!rawEntries || rawEntries.length === 0) {
- const bookData = await loadWorldInfo(worldBookName);
- if (bookData?.entries) {
- rawEntries = Object.entries(bookData.entries).map(([uid, entry]) => ({
- uid: parseInt(uid), enabled: !entry.disable, type: entry.constant ? 'constant' : 'selective',
- keys: entry.key || [], content: entry.content || '', position: this.convertPositionFromNative(entry.position),
- depth: entry.depth, order: entry.order, comment: entry.comment || ''
- }));
- }
- }
+ 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',
@@ -138,8 +313,11 @@ class WorldEditor {
this.updateEntryCount();
} catch (error) {
this.showError(`加载条目失败: ${error.message}`);
- this.entries = []; this.renderEntries();
+ this.entries = [];
+ this.filteredEntries = [];
} finally {
+ this.selectedEntries.clear();
+ this.updateSelectionUI();
this.setLoading(false);
}
}
@@ -153,10 +331,6 @@ class WorldEditor {
const container = this.elements.worldEditorEntriesContainer;
const header = container.querySelector('.world-editor-entries-header');
- // Clear only the entry rows, not the header
- while (container.firstChild && container.firstChild !== header) {
- container.removeChild(container.firstChild);
- }
while (header && header.nextSibling) {
container.removeChild(header.nextSibling);
}
@@ -178,7 +352,6 @@ class WorldEditor {
tempDiv.innerHTML = rowHTML;
const rowElement = tempDiv.firstChild;
- // Safely set the content to prevent HTML rendering
const contentCell = rowElement.querySelector('.world-editor-entry-content');
if (contentCell) {
contentCell.textContent = e.content || '';
@@ -216,19 +389,13 @@ class WorldEditor {
bindEntryEvents() {
this.elements.worldEditorEntriesContainer.querySelectorAll('.world-editor-entry-row').forEach(row => {
const uid = parseInt(row.dataset.uid);
-
- // Checkbox
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();
});
-
- // Content click to open modal (for longer edits)
row.querySelector('[data-action="open-editor"]').addEventListener('click', () => this.openEditModal(uid));
-
- // Inline toggles (enabled, type)
row.querySelectorAll('.inline-toggle').forEach(toggle => {
toggle.addEventListener('click', (e) => {
e.stopPropagation();
@@ -240,54 +407,106 @@ class WorldEditor {
this.updateSingleEntry(uid, { [field]: newValue });
});
});
-
- // Inline edits (inputs, selects)
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);
- // The 'keys' field is no longer inline editable, so that specific logic can be removed.
-
const updates = { [field]: value };
- // If position changes, re-evaluate depth disable state
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()); // Prevent row selection when clicking input
+ input.addEventListener('click', e => e.stopPropagation());
});
});
}
+ /**
+ * 使用原生 saveWorldInfo 更新条目,避免界面跳转
+ * @param {Array