From b5a1d401eacd1fd834c6805f2ce5249f1333ac61 Mon Sep 17 00:00:00 2001
From: Wx-2025 <351320169@qq.com>
Date: Sun, 23 Nov 2025 22:14:53 +0800
Subject: [PATCH] Update cwb_uiManager.js
---
CharacterWorldBook/src/cwb_uiManager.js | 1356 ++++++++++++-----------
1 file changed, 702 insertions(+), 654 deletions(-)
diff --git a/CharacterWorldBook/src/cwb_uiManager.js b/CharacterWorldBook/src/cwb_uiManager.js
index 450cf7b..9447dc4 100644
--- a/CharacterWorldBook/src/cwb_uiManager.js
+++ b/CharacterWorldBook/src/cwb_uiManager.js
@@ -1,692 +1,740 @@
-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';
-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';
+ import { amilyHelper } from '../../core/tavern-helper/main.js';
-const { jQuery: $, SillyTavern } = window;
+ const { jQuery: $, SillyTavern } = window;
-function createCharCardViewerPopupHtml(displayItems) {
- const pathToLabelMap = {
- 'narrative_essence.core_traits.name': '特质名称',
- 'narrative_essence.key_relationships.name': '关系人姓名',
- };
- const keyToLabelMap = {
- 'name': '姓名',
- 'archetype': '身份原型',
- 'gender': '性别',
- 'age': '年龄',
- 'race': '种族',
- 'current_status': '当前状态',
+ function createCharCardViewerPopupHtml(displayItems) {
+ const pathToLabelMap = {
+ 'narrative_essence.core_traits.name': '特质名称',
+ 'narrative_essence.key_relationships.name': '关系人姓名',
+ 'NE.trait.name': '特质名称',
+ 'NE.rel.name': '关系人姓名',
+ };
+ const keyToLabelMap = {
+ 'name': '姓名',
+ // Old keys
+ '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': '关系概述',
- 'first_impression': '第一印象',
- 'key_features': '显著特征',
- 'attire': '衣着风格',
- 'mannerisms': '习惯举止',
- 'voice': '声音特征',
+ // New short keys
+ 'CI': '核心认同',
+ 'PI': '物理印记',
+ 'PP': '心智侧写',
+ 'SM': '社交矩阵',
+ 'NE': '叙事精粹',
+
+ 'arch': '身份原型',
+ 'gen': '性别',
+ // age is same
+ // race is same
+ 'status': '当前状态',
- 'tags': '性格标签',
- 'description': '性格详述',
- 'motivation': '内在驱动',
- 'values': '价值观',
- 'inner_conflict': '内心挣扎',
+ 'first': '第一印象',
+ 'feat': '显著特征',
+ // attire is same
+ 'manner': '习惯举止',
+ // voice is same
- 'interaction_style': '互动风格',
- 'skills': '技能能力',
- 'reputation': '他人声望',
+ // tags is same
+ 'desc': '性格详述',
+ 'mot': '内在驱动',
+ 'val': '价值观',
+ 'conf': '内心挣扎',
- '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 keyToLabelMap[key] || key.replace(/_/g, ' ');
- };
+ 'style': '互动风格/风格总结', // Shared by SM.style and NE.verb.style
+ 'skill': '技能能力',
+ 'rep': '他人声望',
- const renderField = (label, path, value, isTextarea = false, isArray = false) => {
- const escapedLabel = escapeHtml(label);
- const escapedValue = escapeHtml(isArray ? value.join('\n') : value || '');
+ 'trait': '核心特质',
+ 'verb': '语言范式',
+ 'rel': '关键关系',
- 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 inputElement = ``;
-
- return `
- ${escapedLabel}
- ${inputElement}
-
`;
- };
-
- 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));
- }
- cardHtml += `
`;
- });
- cardHtml += `
`;
- } else {
- cardHtml += renderField(label, currentPath, value, false, Array.isArray(value));
+ 'def': '特质定义',
+ 'evid': '具体事例',
+ 'quote': '代表性引言',
+ 'sum': '关系概述',
+ };
+ const getLabel = (key, path) => {
+ const pathKey = path.replace(/\.\d+\./g, '.');
+ if (pathToLabelMap[pathKey]) {
+ return pathToLabelMap[pathKey];
}
+ return keyToLabelMap[key] || key.replace(/_/g, ' ');
+ };
+
+ const renderField = (label, path, value, isTextarea = false, isArray = false) => {
+ const escapedLabel = escapeHtml(label);
+ const escapedValue = escapeHtml(isArray ? value.join('\n') : value || '');
+
+ 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 inputElement = `
`;
+
+ return `
+ ${escapedLabel}
+ ${inputElement}
+
`;
+ };
+
+ 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));
+ }
+ cardHtml += `
`;
+ });
+ cardHtml += `
`;
+ } else {
+ cardHtml += renderField(label, currentPath, value, false, Array.isArray(value));
+ }
+ }
+ cardHtml += `
`;
+ return cardHtml;
+ };
+
+ let html = ``;
+ return html;
}
- 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 += `
- ${escapeHtml(itemName)}
-
-
`;
- });
- 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 += `
-
注入设置
-
-
- 注入位置
-
- 角色定义之前
- 角色定义之后
- 作者注释之前
- 作者注释之后
- @D 注入指定深度
-
-
-
- 注入深度
-
-
-
- 注入顺序
-
-
-
-
`;
-
- html += ``;
- }
- }
- html += `
`;
- });
- html += `
`;
- return html;
-}
-
-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();
- }
- });
-
- $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();
- }
- }
- });
-
- $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();
- }
- });
-
- $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];
- }
- });
- };
- $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 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 = {
- uid: targetUid,
- content: finalContentToSave,
- position: positionMap[insertionPosition] || 'before_character_definition',
- order: isNaN(insertionOrder) ? 7001 : insertionOrder,
- };
-
- if (insertionPosition === 'at_depth') {
- finalEntryData.depth = isNaN(insertionDepth) ? 0 : insertionDepth;
+ 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 {
- finalEntryData.depth = null;
+ $depthContainer.hide();
}
-
- logDebug(`[DEBUG] 最终保存数据 UID:${targetUid}`, {
- position: finalEntryData.position,
- depth: finalEntryData.depth,
- order: finalEntryData.order,
- hasDepthField: 'depth' in finalEntryData
- });
-
- await amilyHelper.setLorebookEntries(book, [finalEntryData]);
- showToastr('success', '角色卡已成功保存!');
- } catch (error) {
- logError('保存角色卡失败:', error);
- showToastr('error', `保存失败: ${error.message}`);
- } finally {
- $button.prop('disabled', false).text(`保存修改`);
- }
- });
-}
-
-function closeCharCardViewerPopup() {
- $(`#${CHAR_CARD_VIEWER_POPUP_ID}`).remove();
-}
-
-export async function showCharCardViewerPopup() {
- if (!isCwbEnabled()) return;
- closeCharCardViewerPopup();
- try {
- const book = await getTargetWorldBook();
- if (!book) {
- showToastr('warning', '当前角色未设置主世界书或自定义世界书。');
- $('body').append(createCharCardViewerPopupHtml([]));
- bindCharCardViewerPopupEvents($(`#${CHAR_CARD_VIEWER_POPUP_ID}`));
- return;
- }
- const allEntries = await amilyHelper.getLorebookEntries(book);
- let currentChatId = state.currentChatFileIdentifier;
-
- if (!currentChatId || currentChatId.startsWith('unknown_chat')) {
- 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 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;
- }
- }
-
- return false;
- });
- } else {
- relevantEntries = allEntries.filter(entry =>
- entry.enabled &&
- Array.isArray(entry.keys) &&
- entry.keys.includes(cleanChatId)
- );
- }
-
- 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
- });
+ $popup.on('click', '.cwb-viewer-popup-close-button', closeCharCardViewerPopup);
+ $popup.find('#cwb-viewer-refresh').on('click', () => {
+ showToastr('info', '正在刷新角色数据...');
+ showCharCardViewerPopup();
+ });
- 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'
- };
+ $popup.find('#cwb-manual-update-btn').on('click', async function() {
+ const $button = $(this);
+ $button.prop('disabled', true).html(' 更新中...');
+ await manualUpdateLogic();
+ showToastr('info', '更新完成,正在刷新查看器...');
+ showCharCardViewerPopup();
+ });
- 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
- });
+ $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');
+ });
- 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);
-
- const popupHtml = createCharCardViewerPopupHtml(displayItems);
- $('body').append(popupHtml);
- const $popup = $(`#${CHAR_CARD_VIEWER_POPUP_ID}`);
- bindCharCardViewerPopupEvents($popup);
- } catch (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();
+ $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();
+ }
+ }
+ });
+
+ $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();
+ }
+ });
+
+ $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];
+ }
+ });
+ };
+ $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 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 = {
+ uid: targetUid,
+ content: finalContentToSave,
+ position: positionMap[insertionPosition] || 'before_character_definition',
+ order: isNaN(insertionOrder) ? 7001 : insertionOrder,
+ };
+
+ if (insertionPosition === 'at_depth') {
+ finalEntryData.depth = isNaN(insertionDepth) ? 0 : insertionDepth;
+ } else {
+ finalEntryData.depth = null;
+ }
+
+ logDebug(`[DEBUG] 最终保存数据 UID:${targetUid}`, {
+ position: finalEntryData.position,
+ depth: finalEntryData.depth,
+ order: finalEntryData.order,
+ hasDepthField: 'depth' in finalEntryData
+ });
+
+ await amilyHelper.setLorebookEntries(book, [finalEntryData]);
+ showToastr('success', '角色卡已成功保存!');
+ } catch (error) {
+ logError('保存角色卡失败:', error);
+ showToastr('error', `保存失败: ${error.message}`);
+ } finally {
+ $button.prop('disabled', false).text(`保存修改`);
+ }
+ });
+ }
+
+ function closeCharCardViewerPopup() {
+ $(`#${CHAR_CARD_VIEWER_POPUP_ID}`).remove();
+ }
+
+ export async function showCharCardViewerPopup() {
+ if (!isCwbEnabled()) return;
+ closeCharCardViewerPopup();
+ try {
+ const book = await getTargetWorldBook();
+ if (!book) {
+ showToastr('warning', '当前角色未设置主世界书或自定义世界书。');
+ $('body').append(createCharCardViewerPopupHtml([]));
+ bindCharCardViewerPopupEvents($(`#${CHAR_CARD_VIEWER_POPUP_ID}`));
+ return;
+ }
+ const allEntries = await amilyHelper.getLorebookEntries(book);
+ let currentChatId = state.currentChatFileIdentifier;
+
+ if (!currentChatId || currentChatId.startsWith('unknown_chat')) {
+ 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 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;
+ }
+ }
+
+ return false;
+ });
+ } else {
+ relevantEntries = allEntries.filter(entry =>
+ entry.enabled &&
+ Array.isArray(entry.keys) &&
+ entry.keys.includes(cleanChatId)
+ );
+ }
+
+ 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);
+
+ const popupHtml = createCharCardViewerPopupHtml(displayItems);
+ $('body').append(popupHtml);
+ const $popup = $(`#${CHAR_CARD_VIEWER_POPUP_ID}`);
+ bindCharCardViewerPopupEvents($popup);
+ } catch (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;
}
- toggleCharCardViewerPopup();
- });
-}
+
+ 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' });
+ }
-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}`);
+ $viewerButton.toggle(shouldShow);
+ console.log(`[CWB] New button created with 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' });
+
+ 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);
+ });
}
- setTimeout(() => {
+ export function updateViewerButtonVisibility() {
+ const $button = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`);
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);
+
+ 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
+ });
}
-
- 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(' 测试中...');
+ export function bindCwbApiEvents() {
+ console.log('[CWB] Binding API events');
- try {
- await testCwbConnection();
- } catch (error) {
- console.error('[CWB] 测试连接失败:', error);
- } finally {
- $button.prop('disabled', false).html(' 测试连接');
- }
- });
+ $('#cwb-api-url').off('input').on('input', function() {
+ const value = $(this).val();
+ extension_settings[extensionName].cwb_api_url = value;
+ saveSettingsDebounced();
+ });
- $('#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();
+ $('#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(' 测试中...');
- 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', '未获取到可用模型');
+ try {
+ await testCwbConnection();
+ } catch (error) {
+ console.error('[CWB] 测试连接失败:', error);
+ } finally {
+ $button.prop('disabled', false).html(' 测试连接');
}
- } catch (error) {
- console.error('[CWB] 获取模型失败:', error);
- $('#cwb-model').empty().append(new Option('获取失败', ''));
- } 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(' 获取模型');
+ }
+ });
+ }