-
-
-
-
原始内容:
-
这是回复 {settings.thinkingTagPrefix}思考过程{settings.thinkingTagSuffix} 继续
+ {/* 正则规则设置 */}
+ {activeTab === 'regex' && (
+
+
+
📝 正则规则管理
+
+ 管理文本处理规则,支持 SillyTavern 格式导入
+
+
+
+
+
规则文件结构
+
-
-
处理后:
-
这是回复 继续
+
+ - 📁 global/ - 全局规则
+ - 📁 characters/ - 角色卡规则
+ - 📁 presets/ - 预设规则
+
+
+
+
+
导入导出
+
+
导入规则文件
+
+ {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const response = await fetch('/api/regex/import', {
+ method: 'POST',
+ body: formData
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ showMessage('success', data.message);
+ } else {
+ showMessage('error', '导入失败');
+ }
+ } catch (error) {
+ showMessage('error', '导入失败');
+ }
+ }}
+ />
+ 支持 SillyTavern 格式的规则文件
+
+
+
+
+
导出全局规则
+
+
+
+ )}
-
-
-
-
- )}
+ {/* 思考标签配置 */}
+ {activeTab === 'thinking' && (
+
+
+
💭 思考标签配置
+
+ 配置 AI 推理/思考内容的标记标签
+
+
+
+
+
标签设置
+
+
前缀
+
+ handleInputChange('thinkingTagPrefix', e.target.value)}
+ placeholder="<thinking>"
+ className="text-input"
+ />
+ 默认值:<thinking>
+
+
- {/* 通用设置 */}
- {activeTab === 'general' && (
-
-
通用设置
-
- 其他系统级配置
-
-
-
-
- handleInputChange('currentPresetName', e.target.value || null)}
- placeholder="未选择"
- />
- 影响全局正则规则的启用
-
+
+
后缀
+
+ handleInputChange('thinkingTagSuffix', e.target.value)}
+ placeholder="</thinking>"
+ className="text-input"
+ />
+ 默认值:</thinking>
+
+
+
-
-
+
+
效果预览
+
+
+
原始内容:
+
+ 这是回复 {settings.thinkingTagPrefix}思考过程{settings.thinkingTagSuffix} 继续
+
+
+
+
+
+
+
+
+
-
- )}
+ )}
+
+ {/* 通用设置 */}
+ {activeTab === 'general' && (
+
+
+
⚙️ 通用设置
+
+ 其他系统级配置
+
+
+
+
+
预设配置
+
+
当前预设
+
+ handleInputChange('currentPresetName', e.target.value || null)}
+ placeholder="未选择"
+ className="text-input"
+ />
+ 影响全局正则规则的启用
+
+
+
+
+
+
+
+
+ )}
+
);
diff --git a/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css b/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css
index 94b2ad6..0cd0575 100644
--- a/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css
+++ b/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.css
@@ -134,7 +134,7 @@
overflow: hidden;
margin-top: 8px; /* 与全局世界书保持间距 */
position: relative; /* 确保正常文档流 */
- z-index: 1; /* 降低层级,不遮挡其他元素 */
+ z-index: var(--z-base-content); /* ✅ 基础内容层 */
/* 最小高度 = header(33px) + actions(37px) + selector(37px) + entries最小内容(约200px) */
min-height: 307px;
display: flex;
@@ -258,17 +258,14 @@
}
.dropdown-menu {
- position: absolute;
- top: 100%;
- left: 0;
- right: 0;
+ position: fixed;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 4px;
margin-top: 4px;
max-height: 200px;
overflow-y: auto;
- z-index: 1000;
+ z-index: var(--z-dropdown-menu); /* ✅ 组件层 - 下拉菜单 */
box-shadow: var(--shadow-md);
}
@@ -295,6 +292,33 @@
font-weight: 600;
}
+/* ✅ 世界书下拉菜单项内容布局 */
+.dropdown-item-content {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+}
+
+.global-checkbox {
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.book-name {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.current-indicator {
+ color: var(--color-accent);
+ font-weight: bold;
+ font-size: 14px;
+ flex-shrink: 0;
+}
+
/* ==================== 条目列表区域 ==================== */
.entries-container {
flex: 1;
@@ -321,6 +345,41 @@
font-weight: 500;
}
+/* ✅ 页码跳转容器 */
+.page-jump-container {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+}
+
+.page-jump-input {
+ width: 50px;
+ padding: 4px 6px;
+ font-size: 11px;
+ border: 1px solid var(--color-border);
+ border-radius: 3px;
+ background: var(--color-bg-primary);
+ color: var(--color-text-primary);
+ text-align: center;
+ outline: none;
+ transition: all 0.15s ease;
+}
+
+.page-jump-input:hover {
+ border-color: var(--color-accent);
+}
+
+.page-jump-input:focus {
+ border-color: var(--color-accent);
+ box-shadow: 0 0 0 2px var(--color-accent-light);
+}
+
+.btn-jump {
+ padding: 4px 8px;
+ font-size: 11px;
+ min-width: 40px;
+}
+
.page-size-select {
padding: 3px 6px;
font-size: 11px;
@@ -634,12 +693,13 @@
left: 0;
right: 0;
bottom: 0;
- background-color: rgba(0, 0, 0, 0.5);
- backdrop-filter: blur(4px);
- z-index: 9998;
+ background-color: rgba(0, 0, 0, 0.3); /* ✅ 降低透明度,不使用模糊 */
+ /* backdrop-filter: blur(4px); */ /* ✅ 移除模糊效果,允许操作旁边区域 */
+ z-index: var(--z-edit-panel-overlay); /* ✅ 弹窗层 - 遮罩 */
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
+ pointer-events: none; /* ✅ 允许点击穿透到下层元素 */
}
.edit-panel-overlay.open {
@@ -657,7 +717,7 @@
height: 85vh;
max-height: 900px;
background: var(--color-bg-primary);
- z-index: 9999;
+ z-index: var(--z-edit-panel-content); /* ✅ 弹窗层 - 编辑面板 */
padding: 0;
overflow: hidden;
opacity: 0;
@@ -764,6 +824,7 @@
display: flex;
gap: 12px;
flex-wrap: wrap;
+ align-items: stretch; /* ✅ 确保所有子元素高度一致 */
}
.form-group {
@@ -775,22 +836,30 @@
.form-group.compact {
flex: 1;
min-width: 150px;
+ display: flex;
+ flex-direction: column;
}
-.form-label {
- font-size: 12px;
- font-weight: 500;
- color: var(--color-text-secondary);
-}
-
+/* ✅ 统一输入框和选择框的高度 */
.form-input {
- padding: 10px 12px;
+ padding: 9px 12px;
font-size: 13px;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-bg-primary);
color: var(--color-text-primary);
transition: all 0.2s ease;
+ height: 38px; /* ✅ 固定高度 */
+ line-height: 1.4;
+ box-sizing: border-box;
+}
+
+.form-label {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--color-text-secondary);
+ display: block;
+ margin-bottom: 2px;
}
.form-input:hover {
@@ -804,6 +873,16 @@
transform: translateY(-1px);
}
+/* select 特殊优化 */
+select.form-input {
+ cursor: pointer;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 10px center;
+ padding-right: 30px;
+}
+
.content-textarea {
flex: 1;
min-height: 250px;
@@ -817,6 +896,39 @@
display: flex;
gap: 12px;
flex-wrap: wrap;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ padding: 12px;
+}
+
+.trigger-selector {
+ flex: 0 0 auto;
+ min-width: 160px;
+ max-width: 200px;
+}
+
+.trigger-selector .form-label {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--color-accent);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 6px;
+ display: block;
+}
+
+.trigger-config-panel {
+ flex: 1;
+ min-width: 300px;
+ padding-left: 12px;
+ border-left: 2px solid var(--color-border-light);
+}
+
+.keyword-config {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
}
/* ✅ 编辑面板底部操作栏 */
@@ -847,20 +959,118 @@
transform: translateY(0);
}
-.trigger-selector {
- flex: 1;
- min-width: 200px;
-}
-
-.trigger-config-panel {
- flex: 2;
- min-width: 300px;
-}
-
-.keyword-config {
+/* ✅ 条件触发配置样式 */
+.condition-config {
display: flex;
flex-direction: column;
+ gap: 12px;
+}
+
+/* 条件块样式 */
+.condition-block {
+ background: var(--color-bg-primary);
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ padding: 12px;
+ transition: all 0.2s ease;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+}
+
+.condition-block:hover {
+ border-color: var(--color-accent);
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
+ transform: translateY(-1px);
+}
+
+.condition-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid var(--color-border-light);
+}
+
+.condition-label {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--color-accent);
+ text-transform: uppercase;
+ letter-spacing: 0.8px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.condition-label::before {
+ content: '';
+ display: inline-block;
+ width: 3px;
+ height: 12px;
+ background: var(--color-accent);
+ border-radius: 2px;
+}
+
+.condition-fields {
+ display: flex;
gap: 8px;
+ align-items: center;
+}
+
+.condition-fields .form-input {
+ flex: 1;
+ min-width: 0;
+ padding: 8px 10px;
+ font-size: 12px;
+ height: 36px;
+}
+
+.condition-fields select.form-input {
+ flex: 0 0 auto;
+ min-width: 95px;
+ max-width: 110px;
+}
+
+/* 条件预览 */
+.condition-preview {
+ background: linear-gradient(135deg, var(--color-accent-ultra-light) 0%, rgba(102, 126, 234, 0.05) 100%);
+ border: 1px solid var(--color-accent-light);
+ border-left: 3px solid var(--color-accent);
+ border-radius: 6px;
+ padding: 10px 14px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-top: 4px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03);
+}
+
+.preview-label {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--color-text-secondary);
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.preview-label::before {
+ content: '📋';
+ font-size: 12px;
+}
+
+.preview-code {
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+ font-size: 12px;
+ color: var(--color-accent);
+ font-weight: 600;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ background: rgba(255, 255, 255, 0.5);
+ padding: 4px 8px;
+ border-radius: 4px;
}
/* ==================== 分页控件样式 ==================== */
@@ -927,15 +1137,13 @@
/* ✅ 排序下拉菜单 - 仿照预设的操作下拉菜单 */
.sort-dropdown-menu {
- position: absolute;
- top: 100%;
- right: 0;
+ position: fixed;
margin-top: 4px;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: 6px;
box-shadow: var(--shadow-lg);
- z-index: 1100;
+ z-index: var(--z-sort-panel); /* ✅ 组件层 - 排序面板 */
min-width: 160px;
padding: 4px;
}
diff --git a/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.jsx b/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.jsx
index c0ccf9f..9b26045 100644
--- a/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.jsx
+++ b/frontend/src/components/SideBarLeft/tabs/WorldBook/WorldBook.jsx
@@ -1,4 +1,5 @@
import React, {useEffect, useState} from 'react';
+import ReactDOM from 'react-dom'; // ✅ 用于 Portal
import './WorldBook.css';
import useWorldBookStore from '../../../../Store/SideBarLeft/WorldBookSlice';
import useSideBarLeftStore from '../../../../Store/SideBarLeft/SideBarLeftSlice';
@@ -76,25 +77,34 @@ const WorldBook = () => {
// 分页状态
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20); // 默认每页20条
+ const [jumpPageInput, setJumpPageInput] = useState(''); // ✅ 页码跳转输入
// ✅ 排序状态
const [sortBy, setSortBy] = useState('order'); // 'name' | 'name_desc' | 'order'
const [activeFirst, setActiveFirst] = useState(false); // 激活条目优先
const [showSortMenu, setShowSortMenu] = useState(false); // 显示排序菜单
+ // 下拉菜单位置
+ const [sortDropdownPosition, setSortDropdownPosition] = useState({ top: 0, right: 0 });
+ const [worldbookDropdownPosition, setWorldbookDropdownPosition] = useState({ top: 0, left: 0 });
+
// ✅ 点击外部关闭排序下拉菜单
useEffect(() => {
const handleClickOutside = (event) => {
if (showSortMenu && !event.target.closest('.sort-settings-container')) {
setShowSortMenu(false);
}
+ // ✅ 点击外部关闭世界书下拉菜单
+ if (showWorldBookDropdown && !event.target.closest('.worldbook-dropdown-container')) {
+ setShowWorldBookDropdown(false);
+ }
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
- }, [showSortMenu]);
+ }, [showSortMenu, showWorldBookDropdown]);
// 获取当前激活的分页
const { activeTab } = useSideBarLeftStore();
@@ -267,6 +277,7 @@ const WorldBook = () => {
const handleEntryClick = (entry) => {
setCurrentEntry(entry);
+ setLocalEntry(entry); // ✅ 设置本地状态以在编辑面板中显示
setShowEditPanel(true);
};
@@ -287,6 +298,27 @@ const WorldBook = () => {
}
};
+ // ✅ 处理列表中条目的快速更新(不依赖 currentEntry)
+ const handleEntryQuickUpdate = async (entry, field, value) => {
+ if (!currentWorldBook) return;
+
+ const updatedEntry = { ...entry, [field]: value };
+
+ try {
+ await updateWorldBookEntry(currentWorldBook.name, entry.uid, updatedEntry);
+ // 如果当前正在编辑这个条目,也更新 currentEntry 和 localEntry
+ if (currentEntry?.uid === entry.uid) {
+ setCurrentEntry(updatedEntry);
+ setLocalEntry(updatedEntry);
+ }
+ // 更新成功后刷新当前页
+ await fetchWorldBookEntries(currentWorldBook.name, currentPage, pageSize);
+ console.log(`[WorldBook] ✅ 快速更新 ${field} 成功`);
+ } catch (err) {
+ console.error(`[WorldBook] ❌ 快速更新 ${field} 失败:`, err);
+ }
+ };
+
// 处理页码变化
const handlePageChange = async (newPage) => {
if (!currentWorldBook) return;
@@ -310,6 +342,29 @@ const WorldBook = () => {
}
};
+ // ✅ 处理页码跳转
+ const handleJumpToPage = async () => {
+ if (!currentWorldBook || !jumpPageInput) return;
+
+ const pageNum = parseInt(jumpPageInput);
+ const totalPages = entriesPagination.total_pages;
+
+ // 验证页码范围
+ if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) {
+ alert(`请输入 1 到 ${totalPages} 之间的页码`);
+ setJumpPageInput('');
+ return;
+ }
+
+ setCurrentPage(pageNum);
+ setJumpPageInput('');
+ try {
+ await fetchWorldBookEntries(currentWorldBook.name, pageNum, pageSize);
+ } catch (err) {
+ console.error('加载世界书条目失败:', err);
+ }
+ };
+
// ✅ 性能优化:防抖保存,500ms后自动保存
const debouncedSave = React.useCallback((updatedEntry) => {
if (saveTimeoutRef.current) {
@@ -710,14 +765,33 @@ const WorldBook = () => {
{/* 世界书选择区域 */}
-
+
{showWorldBookDropdown && (
-
+
{worldBooks.map(book => (
{
onClick={(e) => e.stopPropagation()}
onChange={(e) => {
e.stopPropagation();
- handleEntryUpdate('position', parseInt(e.target.value));
+ handleEntryQuickUpdate(entry, 'position', parseInt(e.target.value));
}}
>
{[0, 1, 2, 3, 4, 5].map(pos => {
@@ -813,7 +887,8 @@ const WorldBook = () => {
title={isEnabled ? '已启用' : '已禁用'}
onClick={(e) => {
e.stopPropagation();
- handleEntryUpdate('disable', !isEnabled);
+ // ✅ 直接更新条目,使用 entry 而不是 currentEntry
+ handleEntryQuickUpdate(entry, 'disable', !isEnabled);
}}
/>
@@ -871,7 +946,8 @@ const WorldBook = () => {
{/* 编辑面板遮罩层 */}
setShowEditPanel(false)}/>
+ /* ✅ 移除 onClick,允许点击穿透到下层元素(动态表格等) */
+ />
{/* 编辑面板 */}
{showEditPanel && localEntry && (
@@ -879,7 +955,11 @@ const WorldBook = () => {
{/* 头部 */}
编辑条目 - UID: {localEntry.uid} {isSaving && 💾 保存中...}
-
@@ -1183,12 +1263,12 @@ const WorldBook = () => {
{activeTriggerStrategy === 'condition' && (
-
-
-
+
+
+ >
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+ 条件 A
+
+
+ {
+ const updatedTriggerConfig = {
+ ...currentEntry.trigger_config,
+ triggers: {
+ ...currentEntry.trigger_config?.triggers,
+ condition: [
+ true,
+ {
+ ...currentEntry.trigger_config?.triggers?.condition?.[1],
+ variable_a: e.target.value
+ }
+ ]
+ }
+ };
+ handleLocalEntryChange('trigger_config', updatedTriggerConfig);
+ }}
+ />
+
+ {
+ const updatedTriggerConfig = {
+ ...currentEntry.trigger_config,
+ triggers: {
+ ...currentEntry.trigger_config?.triggers,
+ condition: [
+ true,
+ {
+ ...currentEntry.trigger_config?.triggers?.condition?.[1],
+ value_a: e.target.value
+ }
+ ]
+ }
+ };
+ handleLocalEntryChange('trigger_config', updatedTriggerConfig);
+ }}
+ />
+
+
+
+ {/* 条件 B */}
+
+
+ 条件 B
+
+
+ {
- const updatedTriggerConfig = {
- ...currentEntry.trigger_config,
- triggers: {
- ...currentEntry.trigger_config?.triggers,
- condition: [
- true,
- {
- ...currentEntry.trigger_config?.triggers?.condition?.[1],
- variable_b: e.target.value
- }
- ]
- }
- };
- handleEntryUpdate('trigger_config', updatedTriggerConfig);
- }}
- />
-
-
- )}
+ onChange={(e) => {
+ const updatedTriggerConfig = {
+ ...currentEntry.trigger_config,
+ triggers: {
+ ...currentEntry.trigger_config?.triggers,
+ condition: [
+ true,
+ {
+ ...currentEntry.trigger_config?.triggers?.condition?.[1],
+ variable_b: e.target.value
+ }
+ ]
+ }
+ };
+ handleLocalEntryChange('trigger_config', updatedTriggerConfig);
+ }}
+ />
+
+
{
+ const updatedTriggerConfig = {
+ ...currentEntry.trigger_config,
+ triggers: {
+ ...currentEntry.trigger_config?.triggers,
+ condition: [
+ true,
+ {
+ ...currentEntry.trigger_config?.triggers?.condition?.[1],
+ value_b: e.target.value
+ }
+ ]
+ }
+ };
+ handleLocalEntryChange('trigger_config', updatedTriggerConfig);
+ }}
+ />
+
+
+
+ {/* 条件预览 */}
+
+ 预览:
+
+ {(() => {
+ const config = currentEntry.trigger_config?.triggers?.condition?.[1] || {};
+ const logic = config.logic || 'AND';
+ const condA = `${config.variable_a || '?'} ${config.operator_a || '='} ${config.value_a || '?'}`;
+ const condB = `${config.variable_b || '?'} ${config.operator_b || '='} ${config.value_b || '?'}`;
+
+ const logicSymbols = {
+ 'AND': '∧',
+ 'OR': '∨',
+ 'NOT_A': '¬',
+ 'NOT_B': '¬',
+ 'XOR': '⊕'
+ };
+
+ if (logic === 'NOT_A') {
+ return `¬(${condA})`;
+ } else if (logic === 'NOT_B') {
+ return `¬(${condB})`;
+ } else {
+ return `(${condA}) ${logicSymbols[logic]} (${condB})`;
+ }
+ })()}
+
+
+
+ )}
@@ -1286,13 +1506,21 @@ const WorldBook = () => {
setShowEditPanel(false)}
+ onClick={() => {
+ setShowEditPanel(false);
+ setLocalEntry(null); // ✅ 清空本地状态
+ setCurrentEntry(null); // ✅ 清空当前条目
+ }}
>
取消
setShowEditPanel(false)}
+ onClick={() => {
+ setShowEditPanel(false);
+ setLocalEntry(null); // ✅ 清空本地状态
+ setCurrentEntry(null); // ✅ 清空当前条目
+ }}
>
保存并关闭
diff --git a/frontend/src/components/SideBarRight/tabs/Settings/Settings.jsx b/frontend/src/components/SideBarRight/tabs/Settings/Settings.jsx
new file mode 100644
index 0000000..6f8c8c4
--- /dev/null
+++ b/frontend/src/components/SideBarRight/tabs/Settings/Settings.jsx
@@ -0,0 +1,229 @@
+import React, { useState, useEffect } from 'react';
+import './Settings.css';
+import useChatBoxStore from '../../../Store/Mid/ChatBoxSlice';
+import { HistoryMode } from '../../../types/internal.types';
+
+/**
+ * 设置Tab - 显示和编辑当前聊天的总结配置
+ */
+const Settings = () => {
+ const { currentRole, currentChat } = useChatBoxStore();
+ const [settings, setSettings] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [saving, setSaving] = useState(false);
+
+ // 加载设置
+ useEffect(() => {
+ if (currentRole && currentChat) {
+ loadSettings();
+ }
+ }, [currentRole, currentChat]);
+
+ const loadSettings = async () => {
+ if (!currentRole || !currentChat) return;
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ const response = await fetch(
+ `/api/chats/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/summary-status`
+ );
+
+ if (!response.ok) {
+ throw new Error('加载设置失败');
+ }
+
+ const data = await response.json();
+ setSettings(data);
+ } catch (err) {
+ console.error('[Settings] 加载设置失败:', err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSave = async () => {
+ if (!currentRole || !currentChat || !settings) return;
+
+ setSaving(true);
+
+ try {
+ const response = await fetch(
+ `/api/chats/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/settings`,
+ {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(settings)
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error('保存设置失败');
+ }
+
+ alert('设置已保存');
+ } catch (err) {
+ console.error('[Settings] 保存设置失败:', err);
+ alert('保存失败: ' + err.message);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const updateSummaryConfig = (field, value) => {
+ setSettings(prev => ({
+ ...prev,
+ summaryConfig: {
+ ...prev.summaryConfig,
+ [field]: value
+ }
+ }));
+ };
+
+ if (!currentRole || !currentChat) {
+ return (
+
+ );
+ }
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
聊天设置
+
+ {saving ? '保存中...' : '保存设置'}
+
+
+
+
+ {/* 历史记录模式 */}
+
+
+
+ {settings?.historyMode === 'rag' && (
+
+ ⚠️ RAG模式一旦选择不可取消
+
+ )}
+
+
+ {/* 总结配置(仅在summary模式下显示) */}
+ {(settings?.historyMode === 'summary' || settings?.summaryConfig) && (
+
+
+
+
+
+ updateSummaryConfig('interval', Number(e.target.value))}
+ />
+
+
+
+
+ updateSummaryConfig('recentFloorsToKeep', Number(e.target.value))}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ 💡 总结间隔:AI回复达到此数量时触发总结
+ 保留楼层:最近X条不总结
+
+
+ )}
+
+ {/* 计数器状态 */}
+ {settings?.historyMode === 'summary' && (
+
+
+
+
+ 当前计数:
+ {settings?.summaryCounter || 0}
+
+
+ 上次总结楼层:
+ L{settings?.lastSummaryFloor || 0}
+
+
+
+ )}
+
+
+ );
+};
+
+export default Settings;
diff --git a/frontend/src/components/SideBarRight/tabs/Table/Table.css b/frontend/src/components/SideBarRight/tabs/Table/Table.css
index 66a347b..a918131 100644
--- a/frontend/src/components/SideBarRight/tabs/Table/Table.css
+++ b/frontend/src/components/SideBarRight/tabs/Table/Table.css
@@ -273,3 +273,104 @@
cursor: not-allowed;
transform: none;
}
+
+/* ==================== 动态表格样式(键值对)==================== */
+.dynamic-table {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.table-row {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ background-color: var(--color-bg-elevated);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ transition: all 0.15s ease;
+}
+
+.table-row:hover {
+ border-color: var(--color-accent);
+ box-shadow: 0 2px 4px rgba(102, 126, 234, 0.1);
+}
+
+.table-key {
+ min-width: 100px;
+ max-width: 150px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--color-text-secondary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.table-value {
+ flex: 1;
+ font-size: 0.9rem;
+ color: var(--color-text-primary);
+ padding-left: 12px;
+ border-left: 2px solid var(--color-border-light);
+ word-break: break-word;
+}
+
+/* ✅ 可编辑输入框样式 */
+.table-input {
+ width: 100%;
+ padding: 4px 8px;
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid transparent;
+ border-radius: 4px;
+ color: var(--color-text-primary);
+ font-size: 0.85rem;
+ transition: all 0.2s ease;
+}
+
+.table-input:hover {
+ background: rgba(255, 255, 255, 0.12);
+ border-color: rgba(255, 255, 255, 0.15);
+}
+
+.table-input:focus {
+ outline: none;
+ background: rgba(255, 255, 255, 0.15);
+ border-color: var(--color-accent);
+ box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
+}
+
+.table-input::placeholder {
+ color: var(--color-text-muted);
+ opacity: 0.5;
+ font-style: italic;
+}
+
+/* SillyTavern 标签区域 */
+.tags-section {
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid var(--color-border-light);
+}
+
+.tags-header {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--color-text-muted);
+ margin-bottom: 8px;
+}
+
+.tags-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+
+.tag-badge {
+ padding: 3px 8px;
+ background-color: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: 12px;
+ font-size: 0.75rem;
+ color: var(--color-text-secondary);
+}
diff --git a/frontend/src/components/SideBarRight/tabs/Table/Table.jsx b/frontend/src/components/SideBarRight/tabs/Table/Table.jsx
index 9aec6b3..40db184 100644
--- a/frontend/src/components/SideBarRight/tabs/Table/Table.jsx
+++ b/frontend/src/components/SideBarRight/tabs/Table/Table.jsx
@@ -5,66 +5,68 @@ import './Table.css';
const Table = () => {
// ✅ 从 Zustand store 获取状态和方法
const {
- tags,
+ tableHeaders,
+ tableDefaults,
isLoading,
currentRole,
currentChat,
loadTags,
- updateTags,
- addTag,
- deleteTag,
- moveLeft,
- moveRight,
- editTag,
- refresh
+ refresh,
+ updateTableData // ✅ 新增:保存方法
} = useTableStore();
- // 本地 UI 状态(不需要持久化)
- const [editingIndex, setEditingIndex] = useState(null);
- const [editValue, setEditValue] = useState('');
- const [newTagInput, setNewTagInput] = useState('');
+ // 本地编辑状态
+ const [editingValues, setEditingValues] = useState({});
+ const [isSaving, setIsSaving] = useState(false);
// 监听角色/聊天变化,自动加载数据
useEffect(() => {
if (currentRole) {
loadTags(currentRole, currentChat);
+ setEditingValues({}); // 重置编辑状态
}
}, [currentRole, currentChat, loadTags]);
- // 开始编辑标签
- const handleStartEdit = (index, value) => {
- setEditingIndex(index);
- setEditValue(value);
+ // ✅ 处理值变化
+ const handleValueChange = (key, value) => {
+ setEditingValues(prev => ({
+ ...prev,
+ [key]: value
+ }));
};
- // 保存编辑
- const handleSaveEdit = () => {
- if (editingIndex === null) return;
+ // ✅ 保存单个字段(失焦时自动保存)
+ const handleBlur = async (key) => {
+ const newValue = editingValues[key];
+ if (newValue === undefined || newValue === tableDefaults[key]) {
+ return; // 没有变化,不保存
+ }
+
+ setIsSaving(true);
- editTag(editingIndex, editValue);
- setEditingIndex(null);
- setEditValue('');
+ // 更新 tableDefaults
+ const newTableDefaults = {
+ ...tableDefaults,
+ [key]: newValue
+ };
+
+ await updateTableData({ tableDefaults: newTableDefaults });
+
+ // 清除编辑状态
+ setEditingValues(prev => {
+ const newState = { ...prev };
+ delete newState[key];
+ return newState;
+ });
+
+ setIsSaving(false);
};
- // 取消编辑
- const handleCancelEdit = () => {
- setEditingIndex(null);
- setEditValue('');
- };
-
- // 添加新标签(支持批量输入)
- const handleAddTag = () => {
- const input = newTagInput.trim();
- if (!input) return;
-
- // 智能分割:支持空格、逗号、分号作为分隔符
- const newTagsList = input
- .split(/[\s,;,;]+/)
- .filter(tag => tag.trim() !== '')
- .map(tag => tag.trim());
-
- addTag(newTagsList);
- setNewTagInput('');
+ // ✅ 按 Enter 键保存
+ const handleKeyDown = (e, key) => {
+ if (e.key === 'Enter') {
+ e.target.blur(); // 触发 blur 事件
+ }
};
// 如果没有角色,显示提示
@@ -82,116 +84,69 @@ const Table = () => {
);
}
+ // ✅ 如果有动态表格数据,显示键值对表格
+ if (tableHeaders.length > 0) {
+ return (
+
+
+
动态表格
+
+
+ {tableHeaders.length} 个字段 {isSaving && '💾'}
+
+
+ {isLoading ? '⏳' : '🔄'}
+
+
+
+
+
+ {/* ✅ 动态表格 - 可编辑的键值对展示 */}
+
+ {tableHeaders.map((header, index) => {
+ const currentValue = editingValues[header] !== undefined
+ ? editingValues[header]
+ : (tableDefaults[header] !== undefined ? tableDefaults[header] : '');
+
+ return (
+
+
+ {header}
+
+
+ handleValueChange(header, e.target.value)}
+ onBlur={() => handleBlur(header)}
+ onKeyDown={(e) => handleKeyDown(e, header)}
+ placeholder="输入值..."
+ />
+
+
+ );
+ })}
+
+
+
+ );
+ }
+
+ // ✅ 否则显示空状态
return (
动态表格
-
-
- {tags.length} 个标签
-
-
- {isLoading ? '⏳' : '🔄'}
-
-
-
-
- {/* 标签列表 */}
-
- {tags.length === 0 ? (
-
- 暂无标签,请在下方添加
-
- ) : (
- tags.map((tag, index) => (
-
- {editingIndex === index ? (
- // 编辑模式
-
- setEditValue(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === 'Enter') handleSaveEdit();
- if (e.key === 'Escape') handleCancelEdit();
- }}
- onBlur={handleSaveEdit}
- autoFocus
- />
-
- ) : (
- // 显示模式
- <>
-
handleStartEdit(index, tag)}
- title="双击编辑"
- >
- {tag}
-
-
- moveLeft(index)}
- disabled={index === 0}
- title="左移"
- >
- ◀
-
- moveRight(index)}
- disabled={index === tags.length - 1}
- title="右移"
- >
- ▶
-
- deleteTag(index)}
- title="删除"
- >
- ✕
-
-
- >
- )}
-
- ))
- )}
-
-
- {/* 添加新标签输入框 */}
-
- setNewTagInput(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === 'Enter') handleAddTag();
- }}
- />
-
- + 添加
-
-
+
);
diff --git a/frontend/src/components/TopBar/TopBar.css b/frontend/src/components/TopBar/TopBar.css
index 121e98c..a7206f9 100644
--- a/frontend/src/components/TopBar/TopBar.css
+++ b/frontend/src/components/TopBar/TopBar.css
@@ -7,7 +7,7 @@
background-color: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border-light);
box-shadow: var(--shadow-sm);
- z-index: 100; /* 降低层级,不遮挡悬浮提示 */
+ z-index: var(--z-top-bar); /* ✅ 组件层 - TopBar */
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
diff --git a/frontend/src/components/TopBar/TopBar.jsx b/frontend/src/components/TopBar/TopBar.jsx
index 74514db..9525824 100644
--- a/frontend/src/components/TopBar/TopBar.jsx
+++ b/frontend/src/components/TopBar/TopBar.jsx
@@ -244,7 +244,7 @@ const Toolbar = () => {
-
系统设置
+ 系统设置
✕
diff --git a/frontend/src/index.css b/frontend/src/index.css
index d68acd8..7fa8f5f 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,6 +1,7 @@
/* Import global styles */
@import './styles/variables.css';
@import './styles/reset.css';
+@import './styles/z-index.css'; /* ✅ Z-Index 层级规范 */
/* ==================== Main Layout ==================== */
.app {
@@ -128,12 +129,12 @@
radial-gradient(circle at 80% 70%, rgba(109, 140, 255, 0.03) 0%, transparent 50%),
linear-gradient(180deg, var(--color-bg-primary) 0%, var(--color-bg-subtle) 100%);
pointer-events: none;
- z-index: 0;
+ z-index: var(--z-background); /* ✅ 基础层 - 背景 */
}
.chat-area > * {
position: relative;
- z-index: 1;
+ z-index: var(--z-base-content); /* ✅ 基础层 - 内容 */
}
.sidebar-right {
diff --git a/frontend/src/styles/z-index.css b/frontend/src/styles/z-index.css
new file mode 100644
index 0000000..724b611
--- /dev/null
+++ b/frontend/src/styles/z-index.css
@@ -0,0 +1,39 @@
+/**
+ * Z-Index CSS 变量定义
+ * ====================
+ *
+ * 在 CSS 文件中使用方式:
+ * z-index: var(--z-dropdown-menu);
+ * z-index: var(--z-modal-overlay);
+ */
+
+:root {
+ /* ==================== 基础层 (0-99) ==================== */
+ --z-background: 0;
+ --z-base-content: 1;
+ --z-divider: 10;
+
+ /* ==================== 组件层 (100-999) ==================== */
+ --z-top-bar: 100;
+ --z-sidebar: 100;
+ --z-dropdown-menu: 1000;
+ --z-sort-panel: 1100;
+ --z-tooltip: 1200;
+ --z-chat-actions: 1000;
+ --z-character-preview: 1000;
+
+ /* ==================== 弹窗层 (10000-19999) ==================== */
+ --z-modal-overlay: 10000;
+ --z-modal-backdrop: 10000; /* TopBar 面板遮罩 */
+ --z-modal-content: 10100;
+ --z-edit-panel-overlay: 10200;
+ --z-edit-panel-content: 10300;
+
+ /* ==================== 通知层 (20000-29999) ==================== */
+ --z-toast-container: 20000;
+ --z-toast-item: 20100;
+
+ /* ==================== 系统层 (30000+) ==================== */
+ --z-loading-spinner: 30000;
+ --z-error-boundary: 30100;
+}
diff --git a/frontend/src/styles/z-index.ts b/frontend/src/styles/z-index.ts
new file mode 100644
index 0000000..b4b51ba
--- /dev/null
+++ b/frontend/src/styles/z-index.ts
@@ -0,0 +1,109 @@
+/**
+ * Z-Index 层级规范
+ * ==================
+ *
+ * 本文件定义了项目中所有 z-index 的使用规范,确保层级关系清晰、一致。
+ *
+ * 层级划分原则:
+ * - 每层之间预留足够的空间(至少 100),方便后续插入新层级
+ * - 同层级的元素使用相近的 z-index 值
+ * - 避免使用过大的数值(如 99999),保持可读性
+ */
+
+export const Z_INDEX = {
+ // ==================== 基础层 (0-99) ====================
+ // 用于页面背景、基础布局等
+
+ /** 最底层 - 背景装饰 */
+ BACKGROUND: 0,
+
+ /** 基础内容层 - 普通文本、图片等 */
+ BASE_CONTENT: 1,
+
+ /** 分割线、边框装饰 */
+ DIVIDER: 10,
+
+
+ // ==================== 组件层 (100-999) ====================
+ // 用于常规 UI 组件,如下拉菜单、悬浮提示等
+
+ /** TopBar 导航栏 */
+ TOP_BAR: 100,
+
+ /** 侧边栏容器 */
+ SIDEBAR: 100,
+
+ /** 下拉菜单 - 预设操作菜单、世界书选择菜单等 */
+ DROPDOWN_MENU: 1000,
+
+ /** 排序设置面板 */
+ SORT_PANEL: 1100,
+
+ /** 悬浮提示 Tooltip */
+ TOOLTIP: 1200,
+
+ /** 聊天消息操作按钮 */
+ CHAT_ACTIONS: 1000,
+
+ /** 角色卡预览弹窗 */
+ CHARACTER_PREVIEW: 1000,
+
+
+ // ==================== 弹窗层 (10000-19999) ====================
+ // 用于模态对话框、编辑面板等需要覆盖整个页面的元素
+
+ /** 对话框遮罩层背景 */
+ MODAL_OVERLAY: 10000,
+
+ /** TopBar 面板遮罩 */
+ MODAL_BACKDROP: 10000,
+
+ /** 对话框内容 - API 配置对话框、预设保存对话框等 */
+ MODAL_CONTENT: 10100,
+
+ /** 世界书编辑面板遮罩层 */
+ EDIT_PANEL_OVERLAY: 10200,
+
+ /** 世界书编辑面板内容 */
+ EDIT_PANEL_CONTENT: 10300,
+
+
+ // ==================== 通知层 (20000-29999) ====================
+ // 用于全局通知、Toast 提示等
+
+ /** Toast 通知容器 */
+ TOAST_CONTAINER: 20000,
+
+ /** Toast 通知项 */
+ TOAST_ITEM: 20100,
+
+
+ // ==================== 系统层 (30000+) ====================
+ // 用于系统级元素,如加载动画、错误边界等
+
+ /** 全局加载动画 */
+ LOADING_SPINNER: 30000,
+
+ /** 错误边界覆盖层 */
+ ERROR_BOUNDARY: 30100,
+} as const;
+
+/**
+ * 使用示例:
+ *
+ * import { Z_INDEX } from '../styles/z-index';
+ *
+ * .dropdown-menu {
+ * z-index: ${Z_INDEX.DROPDOWN_MENU};
+ * }
+ *
+ * .modal-overlay {
+ * z-index: ${Z_INDEX.MODAL_OVERLAY};
+ * }
+ *
+ * .edit-panel {
+ * z-index: ${Z_INDEX.EDIT_PANEL_CONTENT};
+ * }
+ */
+
+export default Z_INDEX;
diff --git a/frontend/src/utils/summaryCounter.js b/frontend/src/utils/summaryCounter.js
new file mode 100644
index 0000000..9a87108
--- /dev/null
+++ b/frontend/src/utils/summaryCounter.js
@@ -0,0 +1,159 @@
+/**
+ * 聊天总结计数器管理器
+ *
+ * 负责在LocalStorage中维护每个聊天的总结计数器
+ * 避免频繁请求后端,提升性能
+ */
+
+const STORAGE_KEY = 'chat_summary_counters';
+
+/**
+ * 计数器数据结构
+ * @typedef {Object} CounterData
+ * @property {number} counter - 当前计数器值
+ * @property {number} lastSummaryFloor - 最后一次总结的楼层
+ * @property {number} updatedAt - 最后更新时间戳
+ */
+
+/**
+ * 获取所有聊天的计数器数据
+ * @returns {Object.}
+ */
+export function getAllCounters() {
+ try {
+ const data = localStorage.getItem(STORAGE_KEY);
+ return data ? JSON.parse(data) : {};
+ } catch (error) {
+ console.error('[SummaryCounter] 读取计数器失败:', error);
+ return {};
+ }
+}
+
+/**
+ * 获取指定聊天的计数器
+ * @param {string} chatKey - 聊天键(格式:role_name/chat_name)
+ * @returns {CounterData}
+ */
+export function getCounter(chatKey) {
+ const allCounters = getAllCounters();
+ return allCounters[chatKey] || {
+ counter: 0,
+ lastSummaryFloor: 0,
+ updatedAt: Date.now()
+ };
+}
+
+/**
+ * 更新指定聊天的计数器
+ * @param {string} chatKey - 聊天键
+ * @param {Partial} updates - 更新的数据
+ */
+export function updateCounter(chatKey, updates) {
+ try {
+ const allCounters = getAllCounters();
+ const current = allCounters[chatKey] || {
+ counter: 0,
+ lastSummaryFloor: 0,
+ updatedAt: Date.now()
+ };
+
+ allCounters[chatKey] = {
+ ...current,
+ ...updates,
+ updatedAt: Date.now()
+ };
+
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(allCounters));
+ } catch (error) {
+ console.error('[SummaryCounter] 更新计数器失败:', error);
+ }
+}
+
+/**
+ * 增加计数器(当AI回复被用户确认时调用)
+ * @param {string} chatKey - 聊天键
+ * @param {number} increment - 增加的值(默认1)
+ * @returns {number} 新的计数器值
+ */
+export function incrementCounter(chatKey, increment = 1) {
+ const current = getCounter(chatKey);
+ const newCounter = current.counter + increment;
+
+ updateCounter(chatKey, { counter: newCounter });
+
+ console.log(`[SummaryCounter] ${chatKey} 计数器: ${current.counter} -> ${newCounter}`);
+
+ return newCounter;
+}
+
+/**
+ * 重置计数器(总结完成后调用)
+ * @param {string} chatKey - 聊天键
+ * @param {number} lastSummaryFloor - 最后一次总结的楼层
+ */
+export function resetCounter(chatKey, lastSummaryFloor) {
+ updateCounter(chatKey, {
+ counter: 0,
+ lastSummaryFloor
+ });
+
+ console.log(`[SummaryCounter] ${chatKey} 计数器已重置,lastSummaryFloor=${lastSummaryFloor}`);
+}
+
+/**
+ * 检查是否应该触发总结
+ * @param {string} chatKey - 聊天键
+ * @param {number} interval - 总结间隔阈值
+ * @returns {boolean}
+ */
+export function shouldTriggerSummary(chatKey, interval) {
+ const current = getCounter(chatKey);
+ return current.counter >= interval;
+}
+
+/**
+ * 清除指定聊天的计数器
+ * @param {string} chatKey - 聊天键
+ */
+export function clearCounter(chatKey) {
+ try {
+ const allCounters = getAllCounters();
+ delete allCounters[chatKey];
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(allCounters));
+ console.log(`[SummaryCounter] 已清除 ${chatKey} 的计数器`);
+ } catch (error) {
+ console.error('[SummaryCounter] 清除计数器失败:', error);
+ }
+}
+
+/**
+ * 清除所有计数器
+ */
+export function clearAllCounters() {
+ try {
+ localStorage.removeItem(STORAGE_KEY);
+ console.log('[SummaryCounter] 已清除所有计数器');
+ } catch (error) {
+ console.error('[SummaryCounter] 清除所有计数器失败:', error);
+ }
+}
+
+/**
+ * 获取存储占用大小(字节)
+ * @returns {number}
+ */
+export function getStorageSize() {
+ const data = localStorage.getItem(STORAGE_KEY);
+ return data ? new Blob([data]).size : 0;
+}
+
+/**
+ * 格式化存储大小
+ * @returns {string}
+ */
+export function getFormattedStorageSize() {
+ const bytes = getStorageSize();
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
+}
diff --git a/test_character_update.py b/test_character_update.py
new file mode 100644
index 0000000..e05c05b
--- /dev/null
+++ b/test_character_update.py
@@ -0,0 +1,99 @@
+"""
+测试角色卡更新API
+"""
+import requests
+import json
+
+# 测试配置
+BASE_URL = "http://localhost:8000/api"
+
+def test_update_character():
+ """测试更新角色卡"""
+
+ # 1. 先获取所有角色
+ print("1. 获取角色列表...")
+ response = requests.get(f"{BASE_URL}/characters/")
+ if response.status_code != 200:
+ print(f"❌ 获取角色列表失败: {response.status_code}")
+ return
+
+ characters = response.json()
+ if not characters:
+ print("❌ 没有角色卡可供测试")
+ return
+
+ # 选择第一个角色
+ test_char = characters[0]
+ char_name = test_char['name']
+ print(f"✅ 找到角色: {char_name}")
+
+ # 2. 获取角色详情
+ print(f"\n2. 获取角色 '{char_name}' 的详情...")
+ response = requests.get(f"{BASE_URL}/characters/{char_name}")
+ if response.status_code != 200:
+ print(f"❌ 获取角色详情失败: {response.status_code}")
+ return
+
+ original_data = response.json()
+ print(f"✅ 原始描述长度: {len(original_data.get('description', ''))}")
+
+ # 3. 更新角色(添加测试标记)
+ print(f"\n3. 更新角色 '{char_name}'...")
+ update_data = {
+ "description": original_data.get('description', '') + '\n\n[测试更新标记]',
+ "tags": original_data.get('tags', []) + ['test_update']
+ }
+
+ response = requests.put(
+ f"{BASE_URL}/characters/{char_name}",
+ json=update_data,
+ headers={'Content-Type': 'application/json'}
+ )
+
+ if response.status_code != 200:
+ print(f"❌ 更新失败: {response.status_code}")
+ print(f"响应内容: {response.text}")
+ return
+
+ result = response.json()
+ print(f"✅ 更新成功: {result.get('success')}")
+
+ # 4. 验证更新
+ print(f"\n4. 验证更新结果...")
+ response = requests.get(f"{BASE_URL}/characters/{char_name}")
+ updated_data = response.json()
+
+ new_desc = updated_data.get('description', '')
+ if '[测试更新标记]' in new_desc:
+ print("✅ 验证成功:描述已更新")
+ else:
+ print("❌ 验证失败:描述未更新")
+
+ if 'test_update' in updated_data.get('tags', []):
+ print("✅ 验证成功:标签已更新")
+ else:
+ print("❌ 验证失败:标签未更新")
+
+ # 5. 恢复原始数据
+ print(f"\n5. 恢复原始数据...")
+ response = requests.put(
+ f"{BASE_URL}/characters/{char_name}",
+ json={
+ "description": original_data.get('description', ''),
+ "tags": original_data.get('tags', [])
+ },
+ headers={'Content-Type': 'application/json'}
+ )
+
+ if response.status_code == 200:
+ print("✅ 数据已恢复")
+ else:
+ print(f"⚠️ 恢复失败: {response.status_code}")
+
+if __name__ == "__main__":
+ try:
+ test_update_character()
+ except Exception as e:
+ print(f"❌ 测试出错: {e}")
+ import traceback
+ traceback.print_exc()
diff --git a/test_chat_summary.py b/test_chat_summary.py
new file mode 100644
index 0000000..a4e5a4a
--- /dev/null
+++ b/test_chat_summary.py
@@ -0,0 +1,76 @@
+"""
+简化版聊天记录总结功能测试
+直接操作现有数据进行测试
+"""
+import sys
+from pathlib import Path
+
+# 添加backend目录到Python路径
+backend_root = Path(__file__).parent / "backend"
+sys.path.insert(0, str(backend_root))
+
+from services.chat_service import chat_service
+
+
+def test_simple_summary():
+ """简单测试:直接操作现有聊天数据"""
+ print("=" * 60)
+ print("聊天记录总结功能简单测试")
+ print("=" * 60)
+
+ # 列出所有现有聊天
+ print("\n现有聊天列表:")
+ all_chats = chat_service.list_all_chats()
+
+ if not all_chats:
+ print(" 没有找到任何聊天,请先创建角色和聊天")
+ return
+
+ for role_name, chats in all_chats.items():
+ print(f"\n 角色: {role_name}")
+ for chat in chats[:3]: # 只显示前3个
+ print(f" - {chat['chat_name']} ({chat['message_count']}条消息)")
+
+ # 选择第一个聊天进行测试
+ first_role = list(all_chats.keys())[0]
+ first_chat = all_chats[first_role][0]['chat_name']
+
+ print(f"\n选择测试: {first_role}/{first_chat}")
+
+ # 获取聊天记录
+ chat_data = chat_service.get_chat(first_role, first_chat)
+ messages = chat_data['messages']
+
+ print(f"总消息数: {len(messages)}")
+
+ if len(messages) < 5:
+ print("消息数量不足,无法测试总结功能")
+ return
+
+ # 显示前5条消息
+ print("\n前5条消息:")
+ for i, msg in enumerate(messages[:5], 1):
+ name = msg.get('name', 'Unknown')
+ mes = msg.get('mes', '')[:50]
+ print(f" L{i}: {name}: {mes}...")
+
+ # 测试总结功能(模拟)
+ print("\n模拟总结 L1-L3:")
+ print(" 原始消息:")
+ for i in range(3):
+ msg = messages[i]
+ print(f" L{i+1}: {msg.get('name')}: {msg.get('mes', '')[:30]}...")
+
+ print("\n 总结后效果:")
+ print(" L1: [空] (is_summarized=true)")
+ print(" L2: [空] (is_summarized=true)")
+ print(" L3: [总结文本] (is_summary=true, summary_range='L1-L3')")
+ print(" L4+: 保持不变")
+
+ print("\n" + "=" * 60)
+ print("✅ 测试完成!请在UI中验证实际效果")
+ print("=" * 60)
+
+
+if __name__ == "__main__":
+ test_simple_summary()