Files
ST-Amily2-Chat-Optimisation…/WorldEditor/WorldEditor.js
2025-10-12 16:07:27 +08:00

688 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = '<p class="world-editor-empty-state">没有找到世界书</p>';
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 = `
<input type="checkbox" class="world-book-checkbox" ${isSelected ? 'checked' : ''}>
<span class="world-book-name">${book.name}</span>
<div class="world-book-actions">
<button class="world-editor-btn small-btn" data-action="edit"><i class="fas fa-pencil-alt"></i> 编辑</button>
<button class="world-editor-btn small-btn" data-action="rename"><i class="fas fa-i-cursor"></i> 重命名</button>
</div>
`;
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 = '<p>没有条目</p>';
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 = `<select class="inline-edit" data-field="position" data-uid="${entry.uid}">
${Object.entries(positionOptions).map(([value, text]) => `<option value="${value}" ${entry.position === value ? 'selected' : ''}>${text}</option>`).join('')}
</select>`;
return `
<div class="world-editor-entry-row ${this.selectedEntries.has(entry.uid) ? 'selected' : ''}" data-uid="${entry.uid}">
<div data-label="选择"><input type="checkbox" class="world-editor-entry-checkbox" ${this.selectedEntries.has(entry.uid) ? 'checked' : ''}></div>
<div data-label="状态" class="inline-toggle" data-field="enabled" data-uid="${entry.uid}"><i class="fas ${entry.enabled ? 'fa-toggle-on' : 'fa-toggle-off'}"></i></div>
<div data-label="灯色" class="inline-toggle" data-field="type" data-uid="${entry.uid}">${entry.type === 'constant' ? '🔵' : '🟢'}</div>
<div data-label="条目"><input type="text" class="inline-edit" data-field="comment" data-uid="${entry.uid}" value="${entry.comment || ''}" placeholder="点击填写条目名"></div>
<div data-label="内容" class="world-editor-entry-content" data-action="open-editor" data-uid="${entry.uid}" title="${entry.content || ''}">${entry.content || ''}</div>
<div data-label="位置">${positionSelect}</div>
<div data-label="深度"><input type="number" class="inline-edit" data-field="depth" data-uid="${entry.uid}" value="${entry.depth != null ? entry.depth : ''}" ${!entry.position?.startsWith('at_depth') ? 'disabled' : ''}></div>
<div data-label="顺序"><input type="number" class="inline-edit" data-field="order" data-uid="${entry.uid}" value="${entry.order}"></div>
</div>`;
}
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<object>} 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 `
<style>
.world-editor-form-grid { display: grid; grid-template-columns: 120px 1fr; gap: 15px; align-items: center; }
.world-editor-form-grid label { text-align: right; color: #ccc; }
.world-editor-form-grid .form-control { width: 100%; padding: 8px; background-color: #404040; color: white; border: 1px solid #555; border-radius: 4px; box-sizing: border-box; }
.world-editor-form-grid textarea.form-control { min-height: 100px; resize: vertical; }
.world-editor-form-grid .full-width { grid-column: 1 / -1; }
.world-editor-form-grid .checkbox-group { grid-column: 2 / -1; display: flex; align-items: center; gap: 8px; }
</style>
<form id="world-editor-edit-form" class="world-editor-form-grid">
<div class="checkbox-group"><input type="checkbox" id="world-editor-entry-enabled" ${entry.enabled ? 'checked' : ''}><label for="world-editor-entry-enabled">启用条目</label></div>
<label for="world-editor-entry-type">激活模式:</label><select id="world-editor-entry-type" class="form-control"><option value="selective" ${entry.type === 'selective' ? 'selected' : ''}>🟢 绿灯 (关键词触发)</option><option value="constant" ${entry.type === 'constant' ? 'selected' : ''}>🔵 蓝灯 (始终激活)</option></select>
<label for="world-editor-entry-keys" class="full-width" style="text-align: left; grid-column: 1 / -1;">关键词 (每行一个)</label><textarea id="world-editor-entry-keys" class="form-control full-width" placeholder="输入关键词,每行一个">${(entry.keys || []).join('\n')}</textarea>
<label for="world-editor-entry-content" class="full-width" style="text-align: left; grid-column: 1 / -1;">内容:</label><textarea id="world-editor-entry-content" class="form-control full-width" placeholder="输入条目内容">${entry.content || ''}</textarea>
<label for="world-editor-entry-position">插入位置:</label><select id="world-editor-entry-position" class="form-control"><option value="before_character_definition" ${entry.position === 'before_character_definition' ? 'selected' : ''}>角色定义之前</option><option value="after_character_definition" ${entry.position === 'after_character_definition' ? 'selected' : ''}>角色定义之后</option><option value="before_author_note" ${entry.position === 'before_author_note' ? 'selected' : ''}>作者注释之前</option><option value="after_author_note" ${entry.position === 'after_author_note' ? 'selected' : ''}>作者注释之后</option><option value="at_depth" ${entry.position === 'at_depth' ? 'selected' : ''}>@D 注入指定深度</option></select>
<label for="world-editor-entry-depth">深度:</label><input type="number" id="world-editor-entry-depth" class="form-control" min="0" max="9999" value="${entry.depth || 4}">
<label for="world-editor-entry-order">顺序:</label><input type="number" id="world-editor-entry-order" class="form-control" min="0" max="9999" value="${entry.order || 100}">
<label for="world-editor-entry-comment">备注:</label><input type="text" id="world-editor-entry-comment" class="form-control" placeholder="可选的备注信息" value="${entry.comment || ''}">
<div class="checkbox-group"><input type="checkbox" id="world-editor-entry-disable-recursion" ${entry.exclude_recursion ? 'checked' : ''}><label for="world-editor-entry-disable-recursion">不可递归 (不会被其他条目激活)</label></div>
<div class="checkbox-group"><input type="checkbox" id="world-editor-entry-prevent-recursion" ${entry.prevent_recursion ? 'checked' : ''}><label for="world-editor-entry-prevent-recursion">防止进一步递归 (本条目将不会激活其他条目)</label></div>
</form>
`;
}
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();
}