From 7903180c4f09bae6f65a65594e6b2ea75c448194 Mon Sep 17 00:00:00 2001
From: Wx-2025 <351320169@qq.com>
Date: Sat, 25 Oct 2025 15:11:34 +0800
Subject: [PATCH] Update cwb_lorebookManager.js
---
CharacterWorldBook/src/cwb_lorebookManager.js | 924 ++++++++++++------
1 file changed, 650 insertions(+), 274 deletions(-)
diff --git a/CharacterWorldBook/src/cwb_lorebookManager.js b/CharacterWorldBook/src/cwb_lorebookManager.js
index 9385e59..b8f0143 100644
--- a/CharacterWorldBook/src/cwb_lorebookManager.js
+++ b/CharacterWorldBook/src/cwb_lorebookManager.js
@@ -1,320 +1,696 @@
-import { state } from './cwb_state.js';
-import { logError, logDebug, showToastr, parseCustomFormat } from './cwb_utils.js';
-import {
- safeLorebooks,
- safeCharLorebooks,
- safeLorebookEntries,
- safeUpdateLorebookEntries,
- compatibleWriteToLorebook,
-} from '../../core/tavernhelper-compatibility.js';
-import { amilyHelper } from '../../core/tavern-helper/main.js';
+import { SCRIPT_ID_PREFIX, CHAR_CARD_VIEWER_BUTTON_ID, CHAR_CARD_VIEWER_POPUP_ID, state } from './cwb_state.js';
+import { logDebug, logError, showToastr, escapeHtml, parseCustomFormat, buildCustomFormat, isCwbEnabled } from './cwb_utils.js';
+import { deleteLorebookEntries, getTargetWorldBook } from './cwb_lorebookManager.js';
+import { manualUpdateLogic } from './cwb_core.js';
+import { testCwbConnection, fetchCwbModels } from './cwb_apiService.js';
+import { extensionName } from '../../utils/settings.js';
+import { extension_settings } from '/scripts/extensions.js';
+import { saveSettingsDebounced } from '/script.js';
-const { SillyTavern } = window;
+const { jQuery: $, SillyTavern, TavernHelper } = window;
-export async function getTargetWorldBook() {
- logDebug('[CWB-DIAGNOSTIC] getTargetWorldBook called. Current state:', {
- target: state.worldbookTarget,
- book: state.customWorldBook
- });
- if (state.worldbookTarget === 'custom' && state.customWorldBook) {
- return state.customWorldBook;
- }
- try {
- const charLorebooks = await safeCharLorebooks();
- const primaryBook = charLorebooks.primary;
- if (!primaryBook) {
- showToastr('error', '当前角色未设置主世界书。');
- return null;
+function createCharCardViewerPopupHtml(displayItems) {
+ const pathToLabelMap = {
+ 'narrative_essence.core_traits.name': '特质名称',
+ 'narrative_essence.key_relationships.name': '关系人姓名',
+ };
+ const keyToLabelMap = {
+ 'name': '姓名',
+ 'archetype': '身份原型',
+ 'gender': '性别',
+ 'age': '年龄',
+ 'race': '种族',
+ 'current_status': '当前状态',
+
+ 'first_impression': '第一印象',
+ 'key_features': '显著特征',
+ 'attire': '衣着风格',
+ 'mannerisms': '习惯举止',
+ 'voice': '声音特征',
+
+ 'tags': '性格标签',
+ 'description': '性格详述',
+ 'motivation': '内在驱动',
+ 'values': '价值观',
+ 'inner_conflict': '内心挣扎',
+
+ 'interaction_style': '互动风格',
+ 'skills': '技能能力',
+ 'reputation': '他人声望',
+
+ 'core_traits': '核心特质',
+ 'verbal_patterns': '语言范式',
+ 'key_relationships': '关键关系',
+ 'definition': '特质定义',
+ 'evidence': '具体事例',
+ 'style_summary': '风格总结',
+ 'quotes': '代表性引言',
+ 'summary': '关系概述',
+ };
+ const getLabel = (key, path) => {
+ const pathKey = path.replace(/\.\d+\./g, '.');
+ if (pathToLabelMap[pathKey]) {
+ return pathToLabelMap[pathKey];
}
- return primaryBook;
- } catch (error) {
- logError('获取主世界书时出错:', error);
- return null;
- }
-}
+ return keyToLabelMap[key] || key.replace(/_/g, ' ');
+ };
-export async function deleteLorebookEntries(uids) {
- if (!Array.isArray(uids) || uids.length === 0) return;
+ const renderField = (label, path, value, isTextarea = false, isArray = false) => {
+ const escapedLabel = escapeHtml(label);
+ const escapedValue = escapeHtml(isArray ? value.join('\n') : value || '');
- try {
- const context = SillyTavern.getContext();
- if (!context || !context.characterId) {
- throw new Error('没有选择角色,无法删除。');
- }
- const book = await getTargetWorldBook();
- if (!book) throw new Error('未找到目标世界书。');
+ const isLongContent = (value && String(value).length > 50) || (Array.isArray(value) && value.length > 1);
+ const rows = isArray ? Math.max(3, value.length) : (isLongContent ? 4 : 2);
- const bookData = await amilyHelper.loadWorldInfo(book);
- if (!bookData) throw new Error(`World book "${book}" not found.`);
- uids.forEach(uid => {
- delete bookData.entries[uid];
- });
- await amilyHelper.saveWorldInfo(book, bookData, true);
- } catch (error) {
- logError('删除世界书条目失败:', error);
- showToastr('error', `删除失败: ${error.message}`);
- }
-}
+ const inputElement = ``;
-export async function saveDescriptionToLorebook(characterName, newDescription, startFloor, endFloor) {
- if (!characterName?.trim()) return false;
+ return `
+
+ ${inputElement}
+
`;
+ };
- try {
- const context = SillyTavern.getContext();
- if (!context || !context.characterId) {
- showToastr('error', '没有选择角色,无法保存到世界书。');
- return false;
- }
- let chatIdentifier = state.currentChatFileIdentifier || '未知聊天';
- chatIdentifier = chatIdentifier.replace(/ imported/g, '');
-
- const safeCharName = characterName.replace(/[^a-zA-Z0-9\u4e00-\u9fa5·""“”_-]/g, ',');
- const floorRange = `${startFloor + 1}-${endFloor + 1}`;
-
- const newComment = `${safeCharName}-${chatIdentifier}`;
-
- let bookName = await getTargetWorldBook();
-
- if (!bookName) {
- showToastr('error', '未能确定要写入的世界书。请检查主世界书或自定义世界书设置。');
- return false;
- }
-
- const entries = (await safeLorebookEntries(bookName)) || [];
- let existing = entries.find(e =>
- Array.isArray(e.keys) &&
- e.keys.includes(chatIdentifier) &&
- e.keys.includes(safeCharName) &&
- !e.keys.includes('Amily2角色总集')
- );
-
- const entryData = {
- comment: newComment,
- content: newDescription,
- keys: [chatIdentifier, safeCharName, floorRange],
- enabled: true,
- type: 'selective',
- };
-
- if (existing) {
- await safeUpdateLorebookEntries(bookName, [{ uid: existing.uid, ...entryData }]);
- } else {
- const cwbEntries = entries.filter(e =>
- Array.isArray(e.keys) &&
- e.keys.includes(chatIdentifier) &&
- !e.keys.includes('Amily2角色总集')
- );
- let maxDepth = 7000;
- cwbEntries.forEach(entry => {
- if (entry.position === 'at_depth_as_system' && typeof entry.depth === 'number') {
- if (entry.depth >= 7001 && entry.depth > maxDepth) {
- maxDepth = entry.depth;
+ const renderCard = (title, data, pathPrefix) => {
+ if (!data || typeof data !== 'object' || Object.keys(data).length === 0) return '';
+ let cardHtml = `${escapeHtml(title)}
`;
+ for (const [key, value] of Object.entries(data)) {
+ const currentPath = pathPrefix ? `${pathPrefix}.${key}` : key;
+ const label = getLabel(key, currentPath);
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
+ cardHtml += renderCard(label, value, currentPath); // Recursive call for nested objects
+ } else if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object') {
+ cardHtml += `
${escapeHtml(label)}
`;
+ value.forEach((item, itemIndex) => {
+ cardHtml += `
`;
+ for (const [itemKey, itemValue] of Object.entries(item)) {
+ const itemPath = `${currentPath}.${itemIndex}.${itemKey}`;
+ cardHtml += renderField(getLabel(itemKey, itemPath), itemPath, itemValue, false, Array.isArray(itemValue));
}
- }
- });
-
- const newDepth = maxDepth + 1;
- let maxOrder = 7000;
- if (cwbEntries.length > 0) {
- maxOrder = cwbEntries.reduce((max, entry) => {
- const order = Number(entry.order);
- return !isNaN(order) && order > max ? order : max;
- }, 7000);
+ cardHtml += `
`;
+ });
+ cardHtml += `
`;
+ } else {
+ cardHtml += renderField(label, currentPath, value, false, Array.isArray(value));
}
-
- const newEntryData = {
- ...entryData,
- order: 100,
- position: 'at_depth_as_system',
- depth: newDepth,
- };
-
- logDebug(`创建新角色条目:${safeCharName}`, {
- position: newEntryData.position,
- depth: newEntryData.depth,
- order: newEntryData.order
- });
-
- await amilyHelper.createLorebookEntries(bookName, [newEntryData]);
}
- showToastr('success', `角色 ${safeCharName} 的描述已保存到世界书。`);
- return true;
- } catch (error) {
- logError(`保存世界书失败 for ${characterName}:`, error);
- showToastr('error', `保存角色 ${safeCharName} 到世界书失败。`);
- return false;
+ cardHtml += `
`;
+ return cardHtml;
+ };
+
+ let html = ``;
+ return html;
}
+
+ html += ``;
+ html += `
`;
+ displayItems.forEach((item, index) => {
+ const itemName = item.isRoster ? '人物总览' : (item.parsed?.name || `未知实体 ${index + 1}`);
+ const wrapperClass = index === 0 ? 'cwb-cyber-tab active' : 'cwb-cyber-tab';
+ html += `
+
+
+
`;
+ });
+ html += `
`;
+
+ html += `
`;
+ displayItems.forEach((item, index) => {
+ html += `
`;
+ if (item.isRoster) {
+ html += `
`;
+ } else {
+ const charData = item.parsed;
+ if (charData) {
+ const charName = charData.name || `角色 ${index + 1}`;
+ if (charData.name) html += renderCard('姓名', { name: charData.name }, '');
+ if (charData.core_identity) html += renderCard('核心认同', charData.core_identity, 'core_identity');
+ if (charData.physical_imprint) html += renderCard('物理印记', charData.physical_imprint, 'physical_imprint');
+ if (charData.psyche_profile) html += renderCard('心智侧写', charData.psyche_profile, 'psyche_profile');
+ if (charData.social_matrix) html += renderCard('社交矩阵', charData.social_matrix, 'social_matrix');
+ if (charData.narrative_essence) html += renderCard('叙事精粹', charData.narrative_essence, 'narrative_essence');
+
+ html += `
+
注入设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+
+ html += ``;
+ }
+ }
+ html += `
`;
+ });
+ html += `
`;
+ return html;
}
-export async function updateCharacterRosterLorebookEntry(processedCharacterNames, startFloor, endFloor) {
- if (!Array.isArray(processedCharacterNames)) return true;
-
- try {
- const context = SillyTavern.getContext();
- if (!context || !context.characterId) {
- logDebug('未选择角色,无法更新角色名册。');
- return false;
+function bindCharCardViewerPopupEvents($popup) {
+ $popup.on('change', '.cwb-insertion-position', function() {
+ const $this = $(this);
+ const $depthContainer = $this.closest('.cwb-insertion-settings-content').find('.cwb-insertion-depth-container');
+ if ($this.val() === 'at_depth') {
+ $depthContainer.show();
+ } else {
+ $depthContainer.hide();
}
- let chatIdentifier = state.currentChatFileIdentifier || '未知聊天';
- if (chatIdentifier === '未知聊天') return false;
-
- const cleanChatId = chatIdentifier.replace(/ imported/g, '');
- const rosterEntryComment = `Amily2角色总集-${cleanChatId}-角色总览`;
+ });
- let characterCardName = '未识别到该角色卡名称';
- try {
- const currentChar = context.characters[context.characterId];
- if (currentChar && currentChar.name) {
- characterCardName = currentChar.name.trim();
+ $popup.on('click', '.cwb-viewer-popup-close-button', closeCharCardViewerPopup);
+ $popup.find('#cwb-viewer-refresh').on('click', () => {
+ showToastr('info', '正在刷新角色数据...');
+ showCharCardViewerPopup();
+ });
+
+ $popup.find('#cwb-manual-update-btn').on('click', async function() {
+ const $button = $(this);
+ $button.prop('disabled', true).html(' 更新中...');
+ await manualUpdateLogic();
+ showToastr('info', '更新完成,正在刷新查看器...');
+ showCharCardViewerPopup();
+ });
+
+ $popup.find('.cwb-cyber-tab__button').on('click', function () {
+ const $this = $(this);
+ const targetUid = $this.data('char-uid');
+ $popup.find('.cwb-cyber-tab').removeClass('active');
+ $this.closest('.cwb-cyber-tab').addClass('active');
+ $popup.find('.cwb-cyber-content-pane').removeClass('active');
+ $popup.find(`#cwb-char-content-${targetUid}`).addClass('active');
+ });
+
+ $popup.find('.cwb-cyber-tab__delete').on('click', async function(e) {
+ e.stopPropagation();
+ if (confirm('您确定要删除这个角色条目吗?此操作不可撤销。')) {
+ const uidToDelete = $(this).data('char-uid');
+ await deleteLorebookEntries([uidToDelete]);
+ const $wrapper = $(this).closest('.cwb-cyber-tab');
+ const $pane = $popup.find(`#cwb-char-content-${uidToDelete}`);
+ const wasActive = $wrapper.hasClass('active');
+ $wrapper.remove();
+ $pane.remove();
+ if (wasActive && $popup.find('.cwb-cyber-tab').length > 0) {
+ $popup.find('.cwb-cyber-tab').first().find('.cwb-cyber-tab__button').trigger('click');
+ } else if ($popup.find('.cwb-cyber-tab').length === 0) {
+ showCharCardViewerPopup();
}
- } catch (e) {
- logDebug('[CWB] 无法获取角色名称,使用默认值');
}
-
- const initialContentPrefix = `此为当前角色卡【${characterCardName}】中登场的角色,AI需要根据剧情让以下角色在合适的时机登场:\n\n`;
-
- let bookName = await getTargetWorldBook();
+ });
- if (!bookName) {
- showToastr('error', '未能确定要写入的世界书。请检查主世界书或自定义世界书设置。');
- return false;
+ $popup.find('#cwb-viewer-delete-all').on('click', async function() {
+ if (confirm('您确定要清除当前聊天中的所有角色卡和总览吗?此操作将删除所有相关条目,且不可撤销。')) {
+ const allUids = $popup.find('.cwb-cyber-tab__button').map(function() {
+ return $(this).data('char-uid');
+ }).get();
+ if (allUids.length > 0) {
+ await deleteLorebookEntries(allUids);
+ }
+ showCharCardViewerPopup();
}
+ });
- let entries = (await safeLorebookEntries(bookName)) || [];
- let existingRosterEntry = entries.find(entry =>
- entry.comment === rosterEntryComment ||
- entry.comment === `Amily2角色总集-${chatIdentifier}-角色总览`
- );
-
- let existingNames = new Set();
- let oldStartFloor = 1;
- let oldEndFloor = 0;
-
- if (existingRosterEntry) {
- if (existingRosterEntry.content) {
- let contentToParse = existingRosterEntry.content.replace(initialContentPrefix, '');
-
- const floorMatch = contentToParse.match(/【前(\d+)楼角色世界书已更新完成】/);
- if (floorMatch && floorMatch[1]) {
- oldEndFloor = parseInt(floorMatch[1], 10);
- }
-
- contentToParse.split('\n').forEach(line => {
- if (line.trim().startsWith('[')) {
- const nameMatch = line.match(/\[(.*?):/);
- if (nameMatch && nameMatch[1]) {
- existingNames.add(nameMatch[1].trim());
+ $popup.find('.cwb-save-button').on('click', async function () {
+ const $button = $(this);
+ const targetUid = $button.data('uid');
+ $button.prop('disabled', true).html(' 保存中...');
+ try {
+ const book = await getTargetWorldBook();
+ if (!book) throw new Error('未找到目标世界书。');
+ const $activePane = $popup.find(`#cwb-char-content-${targetUid}`);
+ const collectedData = {};
+ const setNestedValue = (obj, path, value) => {
+ const keys = path.split('.');
+ let current = obj;
+ keys.forEach((key, index) => {
+ if (index === keys.length - 1) {
+ current[key] = value === '' ? null : value;
+ } else {
+ const nextKeyIsNumber = /^\d+$/.test(keys[index + 1]);
+ if (!current[key]) {
+ current[key] = nextKeyIsNumber ? [] : {};
}
+ current = current[key];
}
});
- }
- if (Array.isArray(existingRosterEntry.keys)) {
- const floorRangeKey = existingRosterEntry.keys.find(k => /^\d+-\d+$/.test(k));
- if (floorRangeKey) {
- [oldStartFloor] = floorRangeKey.split('-').map(Number);
+ };
+ $activePane.find('.cwb-cyber-field__input').each(function () {
+ const $field = $(this);
+ const path = $field.data('path');
+ let value = $field.val();
+ if ($field.data('is-array')) {
+ value = value.split('\n').map(l => l.trim()).filter(Boolean);
}
+ if(path){
+ setNestedValue(collectedData, path, value);
+ }
+ });
+ const finalContentToSave = buildCustomFormat(collectedData);
+ const allEntries = await TavernHelper.getLorebookEntries(book);
+ const entryToUpdate = allEntries.find(e => e.uid === targetUid);
+ if (!entryToUpdate) throw new Error('无法在世界书中找到原始条目。');
+
+ const insertionPosition = $activePane.find('.cwb-insertion-position').val();
+ const insertionDepth = parseInt($activePane.find('.cwb-insertion-depth').val(), 10);
+ const insertionOrder = parseInt($activePane.find('.cwb-insertion-order').val(), 10);
+
+ logDebug(`[DEBUG] 界面收集值 UID:${targetUid}`, {
+ insertionPosition: insertionPosition,
+ insertionDepth: insertionDepth,
+ insertionOrder: insertionOrder
+ });
+
+ const positionMap = {
+ 'before_char': 'before_character_definition',
+ 'after_char': 'after_character_definition',
+ 'before_an': 'before_author_note',
+ 'after_an': 'after_author_note',
+ 'at_depth': 'at_depth_as_system'
+ };
+
+ const finalEntryData = { ...entryToUpdate };
+
+ finalEntryData.content = finalContentToSave;
+ finalEntryData.uid = targetUid;
+
+ const newPosition = positionMap[insertionPosition];
+ finalEntryData.position = newPosition || 'before_character_definition';
+ if (insertionPosition === 'at_depth') {
+ finalEntryData.depth = isNaN(insertionDepth) ? 0 : insertionDepth;
+ } else {
+ finalEntryData.depth = null;
}
+
+ finalEntryData.order = isNaN(insertionOrder) ? 7001 : insertionOrder;
+
+ logDebug(`[DEBUG] 最终保存数据 UID:${targetUid}`, {
+ position: finalEntryData.position,
+ depth: finalEntryData.depth,
+ order: finalEntryData.order,
+ hasDepthField: 'depth' in finalEntryData
+ });
+
+ await TavernHelper.setLorebookEntries(book, [finalEntryData]);
+ showToastr('success', '角色卡已成功保存!');
+ } catch (error) {
+ logError('保存角色卡失败:', error);
+ showToastr('error', `保存失败: ${error.message}`);
+ } finally {
+ $button.prop('disabled', false).text(`保存修改`);
}
-
- processedCharacterNames.forEach(name => existingNames.add(name.trim()));
-
- const newStartFloor = Math.min(oldStartFloor, startFloor + 1);
- const newEndFloor = Math.max(oldEndFloor, endFloor + 1);
-
- const newContent =
- initialContentPrefix +
- [...existingNames]
- .sort()
- .map(name => `[${name}: (详细查看绿灯角色条目)]`)
- .join('\n') + `\n\n{{// 本条勿动,【前${newEndFloor}楼角色世界书已更新完成】否则后续更新无法完成。}}`;
-
- const newFloorRange = `${newStartFloor}-${newEndFloor}`;
-
- const baseKeys = [`Amily2角色总集`, cleanChatId, `角色总览`];
- const newKeys = [...baseKeys, newFloorRange];
-
- const entryData = {
- content: newContent,
- keys: newKeys,
- type: 'constant',
- position: 'before_character_definition',
- depth: null,
- enabled: true,
- order: 9999,
- prevent_recursion: true,
- };
-
- if (existingRosterEntry) {
- await safeUpdateLorebookEntries(bookName, [
- { uid: existingRosterEntry.uid, comment: rosterEntryComment, ...entryData },
- ]);
- } else {
- await amilyHelper.createLorebookEntries(bookName, [
- { comment: rosterEntryComment, ...entryData },
- ]);
- }
- return true;
- } catch (error) {
- logError('更新角色名册条目时出错:', error);
- return false;
- }
+ });
}
+function closeCharCardViewerPopup() {
+ $(`#${CHAR_CARD_VIEWER_POPUP_ID}`).remove();
+}
-export async function manageAutoCardUpdateLorebookEntry() {
+export async function showCharCardViewerPopup() {
+ if (!isCwbEnabled()) return;
+ closeCharCardViewerPopup();
try {
- if (state.worldbookTarget === 'custom' && state.customWorldBook) {
- logDebug('[CWB] 使用自定义世界书模式,跳过角色总览条目的自动管理');
+ const book = await getTargetWorldBook();
+ if (!book) {
+ showToastr('warning', '当前角色未设置主世界书或自定义世界书。');
+ $('body').append(createCharCardViewerPopupHtml([]));
+ bindCharCardViewerPopupEvents($(`#${CHAR_CARD_VIEWER_POPUP_ID}`));
return;
}
+ const allEntries = await TavernHelper.getLorebookEntries(book);
+ let currentChatId = state.currentChatFileIdentifier;
- const context = SillyTavern.getContext();
- if (!context || !context.characterId) {
- logDebug('未选择角色,跳过世界书管理。');
- return;
- }
- const bookName = await getTargetWorldBook();
- if (!bookName) return;
-
- const entries = (await safeLorebookEntries(bookName)) || [];
-
- const currentChatId = state.currentChatFileIdentifier;
if (!currentChatId || currentChatId.startsWith('unknown_chat')) {
- logError(`无效的聊天标识符 "${currentChatId}"。正在中止世界书管理。`);
+ logError(`Invalid chat identifier "${currentChatId}" for viewer.`);
+ $('body').append(createCharCardViewerPopupHtml([]));
+ bindCharCardViewerPopupEvents($(`#${CHAR_CARD_VIEWER_POPUP_ID}`));
return;
}
+
const cleanChatId = currentChatId.replace(/ imported/g, '');
+ let displayItems = [];
- let currentChatRosterExists = false;
- const entriesToUpdate = [];
-
- for (const entry of entries) {
- if (Array.isArray(entry.keys) && (entry.keys.includes('Amily2角色总集') || entry.keys.includes(cleanChatId) || entry.keys.includes(currentChatId))) {
+ let relevantEntries;
+ if (state.worldbookTarget === 'custom' && state.customWorldBook) {
+ relevantEntries = allEntries.filter(entry => {
+ if (!entry.enabled || !Array.isArray(entry.keys)) return false;
+ if (entry.keys.includes('Amily2角色总集') || entry.keys.includes('角色总览')) return true;
+ if (entry.content) {
+ try {
+ const parsed = parseCustomFormat(entry.content);
+ return parsed && Object.keys(parsed).length > 0;
+ } catch (e) {
+ return false;
+ }
+ }
- const isForCurrentChat = entry.keys.includes(cleanChatId) || entry.keys.includes(currentChatId);
- let shouldBeEnabled = isForCurrentChat;
+ return false;
+ });
+ } else {
+ relevantEntries = allEntries.filter(entry =>
+ entry.enabled &&
+ Array.isArray(entry.keys) &&
+ entry.keys.includes(cleanChatId)
+ );
+ }
- if (isForCurrentChat && entry.keys.includes('角色总览')) {
- currentChatRosterExists = true;
+ const rosterEntries = relevantEntries.filter(entry =>
+ entry.keys.includes('Amily2角色总集') && entry.keys.includes('角色总览')
+ );
+
+ rosterEntries.forEach((entry, index) => {
+ displayItems.push({
+ uid: entry.uid,
+ isRoster: true,
+ comment: entry.comment,
+ content: entry.content,
+ rosterIndex: index
+ });
+ });
+
+ const characterEntries = relevantEntries
+ .filter(entry => !entry.keys.includes('Amily2角色总集'))
+ .map(entry => {
+ try {
+ logDebug(`[DEBUG] 原始条目数据 UID:${entry.uid}`, {
+ position: entry.position,
+ depth: entry.depth,
+ order: entry.order,
+ comment: entry.comment
+ });
+
+ const positionStringMap = {
+ 0: 'before_char',
+ 1: 'after_char',
+ 2: 'before_an',
+ 3: 'after_an',
+ 4: 'at_depth',
+ 'before_character_definition': 'before_char',
+ 'after_character_definition': 'after_char',
+ 'before_author_note': 'before_an',
+ 'after_author_note': 'after_an',
+ 'at_depth_as_system': 'at_depth'
+ };
+
+ const position = entry.position;
+ const mappedPosition = positionStringMap[position] || 'at_depth';
+ const finalDepth = (position === 4 || position === 'at_depth_as_system') ? (entry.depth ?? 0) : 0;
+ logDebug(`[DEBUG] 映射结果 UID:${entry.uid}`, {
+ originalPosition: position,
+ mappedPosition: mappedPosition,
+ finalDepth: finalDepth
+ });
+
+ return {
+ uid: entry.uid,
+ isRoster: false,
+ comment: entry.comment,
+ content: entry.content,
+ parsed: parseCustomFormat(entry.content),
+ insertionPosition: mappedPosition,
+ insertionDepth: finalDepth,
+ insertionOrder: entry.order ?? 7001,
+ };
+ } catch (e) {
+ logError(`解析角色条目失败 (UID: ${entry.uid}),已跳过。`, e);
+ return null;
}
+ })
+ .filter(c => c && c.parsed && Object.keys(c.parsed).length > 0);
+
+ displayItems = displayItems.concat(characterEntries);
- if (entry.enabled !== shouldBeEnabled) {
- entriesToUpdate.push({ uid: entry.uid, enabled: shouldBeEnabled });
- }
- }
- }
-
- if (entriesToUpdate.length > 0) {
- await safeUpdateLorebookEntries(bookName, entriesToUpdate);
- logDebug(`已为聊天: ${cleanChatId} 管理了 ${entriesToUpdate.length} 个世界书条目的状态。`);
- }
-
- if (!currentChatRosterExists) {
- logDebug(`未找到聊天 "${cleanChatId}" 的名册。正在触发创建。`);
- await updateCharacterRosterLorebookEntry([]);
- }
-
+ const popupHtml = createCharCardViewerPopupHtml(displayItems);
+ $('body').append(popupHtml);
+ const $popup = $(`#${CHAR_CARD_VIEWER_POPUP_ID}`);
+ bindCharCardViewerPopupEvents($popup);
} catch (error) {
- logError('管理世界书条目时出错:', error);
+ logError('无法显示角色卡查看器:', error);
+ showToastr('error', '加载角色卡数据时出错。');
}
}
+
+function toggleCharCardViewerPopup() {
+ if ($(`#${CHAR_CARD_VIEWER_POPUP_ID}`).length > 0) {
+ closeCharCardViewerPopup();
+ } else {
+ showCharCardViewerPopup();
+ }
+}
+
+function keepButtonInBounds($element, savePosition = false) {
+ if (!$element || !$element.length) return;
+ const windowWidth = $(window).width();
+ const windowHeight = $(window).height();
+ const buttonWidth = $element.outerWidth();
+ const buttonHeight = $element.outerHeight();
+ let currentPos = $element.offset();
+ let newTop = Math.max(0, Math.min(currentPos.top, windowHeight - buttonHeight));
+ let newLeft = Math.max(0, Math.min(currentPos.left, windowWidth - buttonWidth));
+ $element.css({ top: `${newTop}px`, left: `${newLeft}px` });
+ if (savePosition) {
+ localStorage.setItem(state.STORAGE_KEY_VIEWER_BUTTON_POS, JSON.stringify({ top: $element.css('top'), left: $element.css('left') }));
+ }
+}
+
+function makeButtonDraggable($button) {
+ let isDragging = false, wasDragged = false, offset = { x: 0, y: 0 }, startPos = { x: 0, y: 0 };
+ const DRAG_THRESHOLD = 5; // 5 pixels threshold
+
+ const getCoords = (e) => e.touches && e.touches.length ? e.touches[0] : e;
+
+ const dragStart = function (e) {
+ if (e.type === 'touchstart') e.preventDefault();
+ isDragging = true;
+ wasDragged = false;
+ const coords = getCoords(e);
+ startPos.x = coords.clientX;
+ startPos.y = coords.clientY;
+ offset.x = coords.clientX - $button.offset().left;
+ offset.y = coords.clientY - $button.offset().top;
+ $button.css('cursor', 'grabbing');
+ $('body').css({ 'user-select': 'none', '-webkit-user-select': 'none' });
+ };
+
+ const dragMove = function (e) {
+ if (!isDragging) return;
+ const coords = getCoords(e);
+ const dx = coords.clientX - startPos.x;
+ const dy = coords.clientY - startPos.y;
+
+ if (!wasDragged && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
+ wasDragged = true;
+ }
+
+ if (wasDragged) {
+ if (e.type === 'touchmove') e.preventDefault();
+ let newX = coords.clientX - offset.x;
+ let newY = coords.clientY - offset.y;
+ newX = Math.max(0, Math.min(newX, window.innerWidth - $button.outerWidth()));
+ newY = Math.max(0, Math.min(newY, window.innerHeight - $button.outerHeight()));
+ $button.css({ top: newY + 'px', left: newX + 'px', right: '', bottom: '' });
+ }
+ };
+
+ const dragEnd = function (e) {
+ if (!isDragging) return;
+ isDragging = false;
+ $button.css('cursor', 'grab');
+ $('body').css({ 'user-select': 'auto', '-webkit-user-select': 'auto' });
+ if (wasDragged) {
+ keepButtonInBounds($button, true);
+ } else if (e.type === 'touchend') {
+ e.preventDefault();
+ toggleCharCardViewerPopup();
+ }
+ };
+
+ $button.on('mousedown', dragStart);
+ $(document).on('mousemove.cwbViewer', dragMove).on('mouseup.cwbViewer', dragEnd);
+ $button.on('touchstart', dragStart);
+ $(document).on('touchmove.cwbViewer', dragMove).on('touchend.cwbViewer', dragEnd);
+
+ $button.on('click', function (e) {
+ if (wasDragged) {
+ e.preventDefault();
+ e.stopPropagation();
+ return;
+ }
+ toggleCharCardViewerPopup();
+ });
+}
+
+export function initializeCharCardViewer() {
+ const $existingButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`);
+
+ if ($existingButton.length > 0) {
+ console.log('[CWB] Char card viewer button already exists');
+ setTimeout(() => {
+ const shouldShow = isCwbEnabled() && state.viewerEnabled;
+ $existingButton.toggle(shouldShow);
+ console.log(`[CWB] Force updated existing button visibility: ${shouldShow}`);
+ }, 100);
+ return;
+ }
+
+ const buttonHtml = ``;
+ $('body').append(buttonHtml);
+ const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`);
+ makeButtonDraggable($viewerButton);
+
+ const savedPosition = JSON.parse(localStorage.getItem(state.STORAGE_KEY_VIEWER_BUTTON_POS) || 'null');
+ if (savedPosition) {
+ $viewerButton.css({ top: savedPosition.top, left: savedPosition.left });
+ } else {
+ $viewerButton.css({ top: '120px', right: '10px', left: 'auto' });
+ }
+
+ setTimeout(() => {
+ const shouldShow = isCwbEnabled() && state.viewerEnabled;
+ $viewerButton.toggle(shouldShow);
+ console.log(`[CWB] New button created with visibility: ${shouldShow}`);
+ }, 100);
+
+ console.log('[CWB] Char card viewer button initialized');
+
+ let resizeTimeout;
+ $(window).on('resize.cwbViewer', function () {
+ clearTimeout(resizeTimeout);
+ resizeTimeout = setTimeout(() => keepButtonInBounds($(`#${CHAR_CARD_VIEWER_BUTTON_ID}`), true), 150);
+ });
+}
+
+export function updateViewerButtonVisibility() {
+ const $button = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`);
+ const shouldShow = isCwbEnabled() && state.viewerEnabled;
+
+ console.log(`[CWB] Updating viewer button visibility: ${shouldShow} (master: ${isCwbEnabled()}, viewer: ${state.viewerEnabled})`);
+
+ if ($button.length > 0) {
+ $button.toggle(shouldShow);
+ console.log(`[CWB] Viewer button visibility set to: ${shouldShow}`);
+ } else {
+ console.log('[CWB] Viewer button not found, will initialize when DOM is ready');
+ // Try to initialize if button doesn't exist yet
+ setTimeout(() => {
+ initializeCharCardViewer();
+ }, 500);
+ }
+
+ logDebug('悬浮窗按钮显示状态更新:', {
+ masterEnabled: isCwbEnabled(),
+ viewerEnabled: state.viewerEnabled,
+ shouldShow: shouldShow
+ });
+}
+
+export function bindCwbApiEvents() {
+ console.log('[CWB] Binding API events');
+
+ $('#cwb-api-url').off('input').on('input', function() {
+ const value = $(this).val();
+ extension_settings[extensionName].cwb_api_url = value;
+ saveSettingsDebounced();
+ });
+
+ $('#cwb-api-key').off('input').on('input', function() {
+ const value = $(this).val();
+ extension_settings[extensionName].cwb_api_key = value;
+ saveSettingsDebounced();
+ });
+
+ $('#cwb-model').off('input').on('input', function() {
+ const value = $(this).val();
+ extension_settings[extensionName].cwb_model = value;
+ saveSettingsDebounced();
+ });
+
+ $('#cwb-temperature').off('input').on('input', function() {
+ const value = parseFloat($(this).val());
+ $('#cwb-temperature-value').text(value);
+ extension_settings[extensionName].cwb_temperature = value;
+ saveSettingsDebounced();
+ });
+
+ $('#cwb-max-tokens').off('input').on('input', function() {
+ const value = parseInt($(this).val());
+ $('#cwb-max-tokens-value').text(value);
+ extension_settings[extensionName].cwb_max_tokens = value;
+ saveSettingsDebounced();
+ });
+
+ $('#cwb-test-connection').off('click').on('click', async function() {
+ const $button = $(this);
+ $button.prop('disabled', true).html(' 测试中...');
+
+ try {
+ await testCwbConnection();
+ } catch (error) {
+ console.error('[CWB] 测试连接失败:', error);
+ } finally {
+ $button.prop('disabled', false).html(' 测试连接');
+ }
+ });
+
+ $('#cwb-fetch-models').off('click').on('click', async function() {
+ const $button = $(this);
+ $button.prop('disabled', true).html(' 获取中...');
+
+ try {
+ const models = await fetchCwbModels();
+ const $modelSelect = $('#cwb-model');
+ $modelSelect.empty();
+
+ if (models && models.length > 0) {
+ models.forEach(model => {
+ $modelSelect.append(new Option(model.name, model.id));
+ });
+ showToastr('success', `已获取到 ${models.length} 个模型`);
+ } else {
+ $modelSelect.append(new Option('无可用模型', ''));
+ showToastr('warning', '未获取到可用模型');
+ }
+ } catch (error) {
+ console.error('[CWB] 获取模型失败:', error);
+ $('#cwb-model').empty().append(new Option('获取失败', ''));
+ } finally {
+ $button.prop('disabled', false).html(' 获取模型');
+ }
+ });
+}