正则相关

This commit is contained in:
2026-05-06 00:40:18 +08:00
parent f843a74715
commit 9faccc2c03
32 changed files with 4814 additions and 346 deletions

View File

@@ -76,7 +76,7 @@ function App() {
// ✅ 应用启动时自动加载必要的配置数据
useEffect(() => {
console.log('[App] 🚀 应用启动,开始加载默认配置...');
// console.log('[App] 🚀 应用启动,开始加载默认配置...');
const startTime = Date.now();
// 获取各个 Store 的方法
@@ -91,7 +91,7 @@ function App() {
(async () => {
try {
await apiConfigStore.fetchProfiles();
console.log('[App] ✅ API 配置文件列表加载完成');
// console.log('[App] ✅ API 配置文件列表加载完成');
// ✅ 如果有持久化的配置 ID优先使用否则加载第一个
const persistedProfileId = apiConfigStore.currentProfileId;
@@ -99,7 +99,7 @@ function App() {
if (persistedProfileId && !currentProfile) {
// 有持久化 ID 但没有详情,加载详情
console.log(`[App] 🔄 恢复上次选中的配置: ${persistedProfileId}`);
// console.log(`[App] 🔄 恢复上次选中的配置: ${persistedProfileId}`);
await apiConfigStore.fetchProfile(persistedProfileId);
console.log('[App] ✅ API 配置详情加载完成');
} else if (!currentProfile) {
@@ -107,17 +107,17 @@ function App() {
const profiles = useApiConfigStore.getState().profiles;
if (profiles.length > 0) {
const firstProfile = profiles[0];
console.log(`[App] 📝 自动加载第一个配置文件: ${firstProfile.name}`);
// console.log(`[App] 📝 自动加载第一个配置文件: ${firstProfile.name}`);
await apiConfigStore.fetchProfile(firstProfile.id);
console.log('[App] ✅ API 配置详情加载完成');
// console.log('[App] ✅ API 配置详情加载完成');
} else {
console.warn('[App] ⚠️ 没有可用的 API 配置文件,请先到 API 配置页面创建');
// console.warn('[App] ⚠️ 没有可用的 API 配置文件,请先到 API 配置页面创建');
}
} else {
// 从缓存恢复
}
} catch (err) {
console.error('[App] ❌ API 配置加载失败:', err);
// console.error('[App] ❌ API 配置加载失败:', err);
}
})(),
@@ -143,11 +143,11 @@ function App() {
console.log(`[App] 📝 自动选择第一个预设: ${firstPreset.name}`);
presetStore.selectPreset(firstPreset.name);
} else {
console.warn('[App] ⚠️ 没有可用的预设');
// console.warn('[App] ⚠️ 没有可用的预设');
}
}
} catch (err) {
console.error('[App] ❌ 预设列表加载失败:', err);
// console.error('[App] ❌ 预设列表加载失败:', err);
}
})(),
@@ -155,9 +155,9 @@ function App() {
(async () => {
try {
await characterStore.fetchCharacters();
console.log('[App] ✅ 角色卡列表加载完成');
// console.log('[App] ✅ 角色卡列表加载完成');
} catch (err) {
console.error('[App] ❌ 角色卡列表加载失败:', err);
// console.error('[App] ❌ 角色卡列表加载失败:', err);
}
})(),
@@ -165,9 +165,9 @@ function App() {
(async () => {
try {
await worldBookStore.fetchWorldBooks();
console.log('[App] ✅ 世界书列表加载完成');
// console.log('[App] ✅ 世界书列表加载完成');
} catch (err) {
console.error('[App] ❌ 世界书列表加载失败:', err);
// console.error('[App] ❌ 世界书列表加载失败:', err);
}
})()
]).then((results) => {
@@ -178,12 +178,12 @@ function App() {
const successCount = results.filter(r => r.status === 'fulfilled').length;
const failCount = results.filter(r => r.status === 'rejected').length;
console.log(`[App] 🎉 配置加载完成 (${duration}s)`);
console.log(`[App] 📊 成功: ${successCount}, 失败: ${failCount}`);
// console.log(`[App] 🎉 配置加载完成 (${duration}s)`);
// console.log(`[App] 📊 成功: ${successCount}, 失败: ${failCount}`);
if (failCount > 0) {
console.warn('[App] ⚠️ 部分配置加载失败,但应用仍可正常使用');
}
// if (failCount > 0) {
// console.warn('[App] ⚠️ 部分配置加载失败,但应用仍可正常使用');
// }
});
}, []); // 仅在应用启动时执行一次

View File

@@ -110,7 +110,7 @@ const useChatBoxStore = create(
// 同时设置角色和聊天
setChatBoxRoleAndChat: (role, chat) => {
console.log('[ChatBoxStore] setChatBoxRoleAndChat called with:', { role, chat });
// console.log('[ChatBoxStore] setChatBoxRoleAndChat called with:', { role, chat });
set({
currentRole: role,
currentChat: typeof chat === 'object' && chat !== null ? chat.chat_name : chat
@@ -145,7 +145,7 @@ const useChatBoxStore = create(
// ✅ 如果没有 currentChat先创建聊天文件
let actualChat = currentChat;
if (!currentChat && currentRole) {
console.log(`[ChatBoxStore] 检测到未选择聊天,自动创建...`);
// console.log(`[ChatBoxStore] 检测到未选择聊天,自动创建...`);
try {
const chatName = '默认聊天';
const response = await fetch(`/api/chat/${encodeURIComponent(currentRole)}`, {
@@ -164,12 +164,12 @@ const useChatBoxStore = create(
actualChat = chatName;
// 更新 currentChat
set({ currentChat: chatName });
console.log(`[ChatBoxStore] ✅ 已创建/使用聊天: ${chatName}`);
// console.log(`[ChatBoxStore] ✅ 已创建/使用聊天: ${chatName}`);
} else {
throw new Error(`创建聊天失败: ${response.status}`);
}
} catch (error) {
console.error('[ChatBoxStore] ❌ 创建聊天失败:', error);
// console.error('[ChatBoxStore] ❌ 创建聊天失败:', error);
set({ error: '创建聊天失败: ' + error.message });
return;
}
@@ -194,28 +194,50 @@ const useChatBoxStore = create(
wsConnection.close();
}
// ✅ 判断是重roll还是新消息现在重roll也创建新消息
// ✅ 判断是重roll还是新消息
const isReroll = targetFloor !== null;
let userFloor, assistantFloor, nextFloor;
// ✅ 重roll模式也创建新消息而不是更新现有消息
nextFloor = get().getNextFloor(messages);
userFloor = nextFloor;
assistantFloor = nextFloor + 1;
set({
isGenerating: true,
wsConnection: null, // 重置连接
messages: [...messages, {
id: `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, // ✅ 使用唯一ID
floor: userFloor,
mes: processedContent, // 使用处理后的内容
is_user: true,
name: userName || 'User',
sendDate: new Date().toISOString()
}]
});
if (isReroll) {
// 重roll模式不创建新用户消息楼层直接使用目标楼层的上一条用户消息
// console.log('[ChatBoxStore] 🔄 重roll模式目标楼层:', targetFloor);
// 找到目标 AI 消息
const targetMessage = messages.find(m => m.floor === targetFloor);
if (!targetMessage || targetMessage.is_user) {
// console.error('[ChatBoxStore] ❌ 无效的目标楼层');
return;
}
// 助手楼层就是目标楼层
assistantFloor = targetFloor;
nextFloor = targetFloor; // ✅ 设置 nextFloor 用于后续发送
// ✅ 不添加新的用户消息,只准备更新 AI 消息
set({
isGenerating: true,
wsConnection: null, // 重置连接
});
} else {
// 正常模式:创建新的用户消息和 AI 消息
nextFloor = get().getNextFloor(messages);
userFloor = nextFloor;
assistantFloor = nextFloor + 1;
set({
isGenerating: true,
wsConnection: null, // 重置连接
messages: [...messages, {
id: `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, // ✅ 使用唯一ID
floor: userFloor,
mes: processedContent, // 使用处理后的内容
is_user: true,
name: userName || 'User',
sendDate: new Date().toISOString()
}]
});
}
try {
// 统一使用WebSocket处理流式和非流式输出
@@ -245,19 +267,34 @@ const useChatBoxStore = create(
// ✅ 根据模式处理 AI 消息
let newMessageId;
// ✅ 正常模式:添加一个空的助手消息,稍后会更新
newMessageId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // ✅ 使用唯一ID
set((state) => ({
messages: [...state.messages, {
id: newMessageId,
floor: assistantFloor,
mes: '',
is_user: false,
name: characterName || 'Assistant',
sendDate: new Date().toISOString()
}],
isGenerating: true
}));
if (isReroll) {
// 重roll模式找到目标消息并准备添加新的 swipe
const targetMessage = messages.find(m => m.floor === assistantFloor);
if (!targetMessage) {
// console.error('[ChatBoxStore] ❌ 找不到目标消息');
return;
}
newMessageId = targetMessage.id; // 使用现有的 ID
// console.log('[ChatBoxStore] 🔄 重roll模式更新消息:', newMessageId);
// ✅ 不添加新消息,只是标记为正在生成
set({ isGenerating: true });
} else {
// 正常模式:添加一个空的助手消息,稍后会更新
newMessageId = `ai_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // ✅ 使用唯一ID
set((state) => ({
messages: [...state.messages, {
id: newMessageId,
floor: assistantFloor,
mes: '',
is_user: false,
name: characterName || 'Assistant',
sendDate: new Date().toISOString()
}],
isGenerating: true
}));
}
let assistantMessage = '';
let isStreamComplete = false;
@@ -295,12 +332,17 @@ const useChatBoxStore = create(
// 处理流式数据块
assistantMessage += data.content;
// ✅ 更新新创建的消息
set((state) => ({
messages: state.messages.map((msg) =>
msg.id === newMessageId ? { ...msg, mes: assistantMessage } : msg
)
}));
if (isReroll) {
// ✅ 重roll模式不更新 mes只在内部累积 assistantMessage
// mes 保持不变,直到 complete 事件才更新
} else {
// 正常模式:更新新创建的消息
set((state) => ({
messages: state.messages.map((msg) =>
msg.id === newMessageId ? { ...msg, mes: assistantMessage } : msg
)
}));
}
} else if (data.type === 'worldbook_active') {
console.log('[WebSocket] 📚 收到世界书激活信息:', data.entries.length, '个条目');
// ✅ 更新世界书激活显示
@@ -358,6 +400,48 @@ const useChatBoxStore = create(
console.log(' - 总 Chunks:', chunkCount);
console.log(' - 消息长度:', assistantMessage.length);
// ✅ 处理重roll模式将新生成的内容添加到 swipes 数组
if (isReroll) {
// console.log('[ChatBoxStore] 🔄 重roll完成添加新的 swipe 版本');
set((state) => ({
messages: state.messages.map((msg) => {
if (msg.id === newMessageId) {
// 获取现有的 swipes 数组
const existingSwipes = msg.swipes || [];
const currentMes = msg.mes; // 当前显示的内容(旧版本)
// 构建新的 swipes 数组:包含所有旧版本 + 新版本
let updatedSwipes = [...existingSwipes];
// 如果当前 mes 不在 swipes 中,先添加它
if (!updatedSwipes.includes(currentMes)) {
updatedSwipes.push(currentMes);
}
// 添加新生成的内容
updatedSwipes.push(assistantMessage);
// console.log('[ChatBoxStore] 📊 Swipes 更新:', {
// oldCount: existingSwipes.length,
// newCount: updatedSwipes.length,
// newSwipeIndex: updatedSwipes.length - 1,
// currentMesLength: currentMes.length,
// assistantMessageLength: assistantMessage.length
// });
return {
...msg,
mes: assistantMessage, // 显示新生成的内容
swipes: updatedSwipes, // 更新 swipes 数组
swipe_id: updatedSwipes.length - 1 // 自动切换到新版本
};
}
return msg;
})
}));
}
// 完成响应
isStreamComplete = true;
ws.close();
@@ -406,21 +490,21 @@ const useChatBoxStore = create(
model: apiConfigStore.currentProfile?.apis?.mainLLM?.model || ''
};
console.log('\n' + '-'.repeat(80));
console.log('[WebSocket] 📤 发送消息:');
console.log(' - Floor:', nextFloor);
console.log(' - Mode:', isReroll ? '🔄 Reroll (新消息)' : ' New Message');
console.log(' - Role:', currentRole);
console.log(' - Chat:', actualChat);
console.log(' - Stream:', options.streamOutput);
console.log(' - Message Length:', processedContent.length);
console.log(' - API Config:', apiConfigData);
console.log(' - Current Profile:', apiConfigStore.currentProfile);
console.log('-'.repeat(80) + '\n');
// console.log('\n' + '-'.repeat(80));
// console.log('[WebSocket] 📤 发送消息:');
// console.log(' - Floor:', isReroll ? assistantFloor : nextFloor);
// console.log(' - Mode:', isReroll ? '🔄 Reroll (添加swipe)' : ' New Message');
// console.log(' - Role:', currentRole);
// console.log(' - Chat:', actualChat);
// console.log(' - Stream:', options.streamOutput);
// console.log(' - Message Length:', processedContent.length);
// console.log(' - API Config:', apiConfigData);
// console.log(' - Current Profile:', apiConfigStore.currentProfile);
// console.log('-'.repeat(80) + '\n');
// ✅ 实际发送消息
const messageData = JSON.stringify({
floor: nextFloor, // ✅ 总是使用 nextFloor因为重roll也创建新消息
floor: isReroll ? assistantFloor : nextFloor, // ✅ 重roll模式使用目标楼层
mes: processedContent, // 使用处理后的内容
is_user: true,
currentRole: currentRole,
@@ -469,19 +553,21 @@ const useChatBoxStore = create(
} : null,
// ✅ 时间戳(用于冲突解决)
timestamp: Date.now(),
stream: options.streamOutput
stream: options.streamOutput,
// ✅ 调试标志请求后端返回完整的prompt拼接内容
debugPrompt: true
});
console.log('[WebSocket] 📤 正在发送消息,数据长度:', messageData.length);
// console.log('[WebSocket] 📤 正在发送消息,数据长度:', messageData.length);
ws.send(messageData);
console.log('[WebSocket] ✅ 消息已发送');
// console.log('[WebSocket] ✅ 消息已发送');
} else if (ws.readyState === WebSocket.CONNECTING) {
// 如果正在连接,继续等待
console.log('[WebSocket] 等待连接...', { readyState: ws.readyState });
// console.log('[WebSocket] 等待连接...', { readyState: ws.readyState });
setTimeout(sendAfterConnect, 100);
} else {
// 连接失败或已关闭
console.error('[WebSocket] 连接失败', { readyState: ws.readyState });
// console.error('[WebSocket] 连接失败', { readyState: ws.readyState });
set({
error: 'WebSocket connection failed',
isGenerating: false,
@@ -502,7 +588,7 @@ const useChatBoxStore = create(
// 终止生成
stopGeneration: () => set((state) => {
if (state.wsConnection && state.wsConnection.readyState === WebSocket.OPEN) {
console.log('[ChatBoxStore] 🛑 发送终止信号...');
// console.log('[ChatBoxStore] 🛑 发送终止信号...');
// ✅ 先发送取消任务信号给后端
state.wsConnection.send(JSON.stringify({
@@ -514,7 +600,7 @@ const useChatBoxStore = create(
setTimeout(() => {
if (state.wsConnection) {
state.wsConnection.close();
console.log('[ChatBoxStore] 🔌 WebSocket 已关闭');
// console.log('[ChatBoxStore] 🔌 WebSocket 已关闭');
}
}, 100);
}
@@ -531,7 +617,7 @@ const useChatBoxStore = create(
// 如果已经在加载中,跳过
if (currentState.isLoading) {
console.log(`[ChatBoxStore] 跳过重复加载: ${roleName}/${chatName}`);
// console.log(`[ChatBoxStore] 跳过重复加载: ${roleName}/${chatName}`);
return;
}
@@ -563,11 +649,11 @@ const useChatBoxStore = create(
name: characterData.name || roleName,
sendDate: new Date().toISOString()
}];
console.log(`[ChatBoxStore] 显示角色 ${roleName} 的开场白(临时消息)`);
// console.log(`[ChatBoxStore] 显示角色 ${roleName} 的开场白(临时消息)`);
}
}
} catch (error) {
console.warn('[ChatBoxStore] 获取角色信息失败:', error);
// console.warn('[ChatBoxStore] 获取角色信息失败:', error);
}
}
@@ -579,13 +665,13 @@ const useChatBoxStore = create(
isLoading: false
});
console.log(`[ChatBoxStore] 已加载聊天: ${roleName}/${actualChatName}, 消息数: ${messages.length}`);
// console.log(`[ChatBoxStore] 已加载聊天: ${roleName}/${actualChatName}, 消息数: ${messages.length}`);
} catch (error) {
set({
error: error.message,
isLoading: false
});
console.error('[ChatBoxStore] 加载聊天失败:', error);
// console.error('[ChatBoxStore] 加载聊天失败:', error);
}
},

View File

@@ -121,7 +121,7 @@ const usePresetStore = create(
set({ presets: presetList, isLoadingPresets: false });
} catch (error) {
console.error('Failed to fetch presets:', error);
// console.error('Failed to fetch presets:', error);
set({ isLoadingPresets: false });
}
},
@@ -217,7 +217,7 @@ const usePresetStore = create(
isParametersExpanded: true // 确保参数容器展开
});
} catch (error) {
console.error('Failed to load preset:', error);
// console.error('Failed to load preset:', error);
}
},
@@ -298,7 +298,7 @@ const usePresetStore = create(
return result;
} catch (error) {
console.error('Failed to save preset:', error);
// console.error('Failed to save preset:', error);
throw error;
}
},
@@ -329,7 +329,7 @@ const usePresetStore = create(
return await response.json();
} catch (error) {
console.error('Failed to update preset name:', error);
// console.error('Failed to update preset name:', error);
throw error;
}
},
@@ -383,7 +383,7 @@ const usePresetStore = create(
saveComponentOrder: async () => {
const state = get();
if (!state.selectedPreset) {
console.warn('No preset selected, cannot save order');
// console.warn('No preset selected, cannot save order');
return;
}
@@ -406,10 +406,10 @@ const usePresetStore = create(
}
const result = await response.json();
console.log('Component order saved successfully:', result);
// console.log('Component order saved successfully:', result);
return result;
} catch (error) {
console.error('Failed to save component order:', error);
// console.error('Failed to save component order:', error);
throw error;
}
},

View File

@@ -522,13 +522,20 @@
color: var(--color-text-primary);
font-size: 0.95rem;
resize: none;
overflow-y: hidden;
overflow-y: auto;
min-height: 36px;
max-height: 300px;
transition: all 0.15s ease;
font-family: inherit;
line-height: 1.6;
display: block;
/* ✅ 业界标准:使用 field-sizing 实现自适应高度 */
/* 支持此属性的浏览器会自动根据内容调整高度 */
field-sizing: content;
/* ✅ 兼容性方案:为不支持 field-sizing 的浏览器提供回退 */
height: auto;
}
.message-input:focus {
@@ -717,6 +724,10 @@
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
position: relative; /* ✅ 为删除按钮定位 */
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.chat-option:hover {
@@ -726,6 +737,31 @@
.chat-option-content {
cursor: pointer;
flex: 1; /* ✅ 占据剩余空间 */
}
/* ✅ 删除按钮样式 */
.chat-delete-btn {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
opacity: 0; /* 默认隐藏 */
transition: all var(--transition-fast);
color: var(--color-text-muted);
}
.chat-option:hover .chat-delete-btn {
opacity: 0.6; /* hover 时显示 */
}
.chat-delete-btn:hover {
opacity: 1 !important;
background: rgba(255, 77, 77, 0.15);
color: #ff4d4d;
transform: scale(1.1);
}
.chat-option-name {

View File

@@ -120,10 +120,11 @@ const ChatBox = () => {
const handleInputHeight = (e) => {
const textarea = e.target;
// 标准方案:先重置为 auto再设置为 scrollHeight
// 这是业界公认的最佳实践,确保高度计算准确
// ✅ 业界标准方案:先重置为 auto再设置为 scrollHeight
// 这是 SillyTavern、Discord、Slack 等成熟产品使用的方案
// 确保删除内容时高度也能正确收缩
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
textarea.style.height = Math.min(textarea.scrollHeight, 300) + 'px';
};
// 处理发送或终止
@@ -133,7 +134,11 @@ const ChatBox = () => {
} else {
sendMessage(inputValue);
clearInput(); // ✅ 使用 store 方法
setInputHeight(42); // ✅ 重置为一行高度
// ✅ 重置输入框高度:直接操作 DOM 元素重置为 auto
const textarea = document.querySelector('.message-input');
if (textarea) {
textarea.style.height = 'auto';
}
}
};
@@ -228,13 +233,13 @@ const ChatBox = () => {
if (currentIndex < lastAiMessage.swipes.length - 1) {
handleSwipeChange(lastAiMessage.floor, 1);
} else {
// 已在最后一个版本触发重roll
console.log('[ChatBox] 已在最后一个版本触发重roll发送新消息');
// 已在最后一个版本触发重roll在目标消息的swipe数组中添加新版本
console.log('[ChatBox] 已在最后一个版本触发重roll添加swipe');
handleRerollMessage(lastAiMessage);
}
} else {
// 没有 swipe直接触发重roll
console.log('[ChatBox] 没有 swipe触发重roll发送新消息');
// 没有 swipe直接触发重roll在目标消息的swipe数组中添加新版本
console.log('[ChatBox] 没有 swipe触发重roll添加swipe');
handleRerollMessage(lastAiMessage);
}
}
@@ -271,7 +276,8 @@ const ChatBox = () => {
const newIndex = currentIndex + direction;
if (newIndex >= 0 && newIndex < message.swipes.length) {
setCurrentSwipeId({ [messageId]: newIndex }); // ✅ 使用 store 方法
// ✅ 合并更新,保留其他楼层的 swipe 状态
setCurrentSwipeId({ ...currentSwipeId, [messageId]: newIndex });
}
}
};
@@ -428,6 +434,47 @@ const ChatBox = () => {
}
};
// ✅ 新增:删除聊天
const handleDeleteChat = async (chatName, e) => {
e.stopPropagation(); // 阻止事件冒泡,避免触发选择聊天
if (!confirm(`确定要删除聊天 "${chatName}" 吗?此操作不可恢复!`)) {
return;
}
try {
const { currentRole } = useChatBoxStore.getState();
const response = await fetch(`/api/chat/${encodeURIComponent(currentRole)}/${encodeURIComponent(chatName)}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('删除聊天失败');
}
console.log(`[ChatBox] 已删除聊天: ${chatName}`);
// 重新获取聊天列表
const chatsResponse = await fetch(`/api/chat/${encodeURIComponent(currentRole)}`);
if (chatsResponse.ok) {
const chats = await chatsResponse.json();
setCharacterChats(chats);
}
// 如果删除的是当前聊天,清空当前聊天
const { currentChat, setChatBoxRoleAndChat } = useChatBoxStore.getState();
if (currentChat === chatName) {
setChatBoxRoleAndChat(currentRole, null);
}
alert('聊天已删除');
} catch (error) {
console.error('[ChatBox] 删除聊天失败:', error);
alert('删除聊天失败: ' + error.message);
}
};
// 新建聊天
const handleCreateNewChat = async () => {
try {
@@ -686,10 +733,6 @@ const ChatBox = () => {
handleInputHeight(e);
}}
onKeyDown={handleKeyDown}
style={{
height: `${inputHeight}px`,
overflowY: inputHeight >= 300 ? 'auto' : 'hidden'
}}
placeholder="Type your message..."
/>
</div>
@@ -751,6 +794,14 @@ const ChatBox = () => {
</div>
)}
</div>
{/* ✅ 删除按钮 */}
<button
className="chat-delete-btn"
onClick={(e) => handleDeleteChat(chat.chat_name, e)}
title="删除此聊天"
>
🗑
</button>
</div>
))}
</div>

View File

@@ -298,7 +298,9 @@ const PresetPanel = () => {
// 导入预设
const handleImportPreset = async (file) => {
console.log('[Preset Import] 开始导入文件:', file.name);
console.log('\n' + '='.repeat(80));
console.log('[预设导入] 📥 开始导入文件:', file.name);
console.log('='.repeat(80));
try {
// 读取文件内容
const importData = await new Promise((resolve, reject) => {
@@ -308,9 +310,7 @@ const PresetPanel = () => {
reader.readAsText(file);
});
console.log('[Preset Import] 文件内容读取成功');
const importedPreset = JSON.parse(importData);
console.log('[Preset Import] JSON 解析成功');
// 支持内部结构和SillyTavern结构
let presetName = '';
@@ -321,7 +321,8 @@ const PresetPanel = () => {
if (importedPreset.entries && Array.isArray(importedPreset.entries)) {
// 内部结构
presetName = importedPreset.name || file.name.replace('.json', '');
console.log('[Preset Import] 检测到内部格式, preset name:', presetName);
console.log('[预设导入] ✅ 检测到内部格式');
console.log(' - 预设名称:', presetName);
importedParameters = {
temperature: importedPreset.temperature,
top_p: importedPreset.topP,
@@ -351,7 +352,8 @@ const PresetPanel = () => {
else if (importedPreset.name || importedPreset.temperature || importedPreset.parameters || importedPreset.prompts) {
// SillyTavern 格式通常没有 name 字段,使用文件名
presetName = importedPreset.name || file.name.replace('.json', '');
console.log('[Preset Import] 检测到 SillyTavern 格式, preset name:', presetName);
console.log('[预设导入] ✅ 检测到 SillyTavern 格式');
console.log(' - 预设名称:', presetName);
// 提取参数
importedParameters = importedPreset.parameters || {
@@ -366,7 +368,7 @@ const PresetPanel = () => {
// 转换 SillyTavern 的 prompts 为内部格式
if (importedPreset.prompts && Array.isArray(importedPreset.prompts)) {
console.log('[Preset Import] 找到', importedPreset.prompts.length, '个 prompts');
console.log(' - Prompts 数量:', importedPreset.prompts.length);
// 获取 prompt_order支持多个 prompt_order优先使用最后一个或组件数最多的
let promptOrder = [];
@@ -376,19 +378,21 @@ const PresetPanel = () => {
if (importedPreset.prompt_order.length === 1) {
if (importedPreset.prompt_order[0].order) {
promptOrder = importedPreset.prompt_order[0].order;
console.log('[Preset Import] 使用唯一的 prompt_order');
console.log(' - 使用唯一的 prompt_order (组件数:', promptOrder.length, ')');
}
} else if (importedPreset.prompt_order.length > 1) {
// 有多个时,使用最后一个(通常是最完整的)
const lastOrder = importedPreset.prompt_order[importedPreset.prompt_order.length - 1];
if (lastOrder && lastOrder.order) {
promptOrder = lastOrder.order;
console.log('[Preset Import] 使用最后一个 prompt_order (character_id:', lastOrder.character_id, ', 组件数:', promptOrder.length, ')');
console.log(' - 使用最后一个 prompt_order');
console.log(' * character_id:', lastOrder.character_id);
console.log(' * 组件数:', promptOrder.length);
}
console.log('[Preset Import] 检测到', importedPreset.prompt_order.length, '个 prompt_order');
console.log(' - 检测到', importedPreset.prompt_order.length, '个 prompt_order:');
importedPreset.prompt_order.forEach((po, idx) => {
console.log(` [${idx}] character_id: ${po.character_id}, order_count: ${po.order?.length || 0}`);
console.log(` [${idx}] character_id: ${po.character_id}, order_count: ${po.order?.length || 0}`);
});
}
}
@@ -444,18 +448,62 @@ const PresetPanel = () => {
});
}
console.log('[Preset Import] 转换后组件数量:', importedComponents.length);
console.log(' - 转换后组件数量:', importedComponents.length);
} else {
console.log('[Preset Import] 未找到 prompts 字段');
console.log(' - ⚠️ 未找到 prompts 字段');
importedComponents = [];
}
}
if (presetName) {
console.log('[Preset Import] 准备应用导入的配置');
console.log('\n[预设导入] 🔄 应用导入的配置...');
// ✅ 调试:检查导入的预设结构
console.log('[预设导入] 🔍 检查预设文件结构...');
console.log('[预设导入] 📋 预设文件名:', presetName);
console.log('[预设导入] 🔑 是否有 extensions:', !!importedPreset.extensions);
console.log('[预设导入] 📜 是否有 regex_scripts:', !!importedPreset.extensions?.regex_scripts);
console.log('[预设导入] 📊 regex_scripts 数量:', importedPreset.extensions?.regex_scripts?.length || 0);
// ✅ 提取并保存正则规则(如果存在)
if (importedPreset.extensions?.regex_scripts && importedPreset.extensions.regex_scripts.length > 0) {
const regexScripts = importedPreset.extensions.regex_scripts;
console.log(`\n[预设导入] ✅ 检测到 ${regexScripts.length} 条正则规则`);
console.log('[预设导入] 📜 前3条规则:', regexScripts.slice(0, 3).map(r => r.scriptName));
try {
// 将正则规则发送到后端保存
console.log('[预设导入] 开始保存正则规则到后端...');
const response = await fetch('/api/regex/import-from-preset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rules: regexScripts,
scope: 'preset',
presetName: presetName
})
});
console.log('[预设导入] 📥 后端响应状态:', response.status);
if (response.ok) {
const result = await response.json();
console.log('[预设导入] ✅ 正则规则已保存到:', `data/regex/presets/${presetName}.json`);
console.log('[预设导入] 📊 导入结果:', result.message);
} else {
const errorText = await response.text();
console.error('[预设导入] ❌ 保存正则规则失败:', response.status, errorText);
}
} catch (error) {
console.error('[预设导入] ❌ 保存正则规则失败:', error);
}
} else {
console.warn('[预设导入] 预设中未找到 extensions.regex_scripts');
console.log('[预设导入] 💡 预设文件中的 extensions 字段:', Object.keys(importedPreset.extensions || {}));
}
// 先更新参数
console.log('[Preset Import] 更新参数:', importedParameters);
console.log(' - 参数配置:', Object.keys(importedParameters).filter(k => importedParameters[k] !== undefined).join(', '));
Object.keys(importedParameters).forEach(key => {
if (importedParameters[key] !== undefined) {
updateParameter({ name: key, value: importedParameters[key] });
@@ -464,30 +512,33 @@ const PresetPanel = () => {
// 再更新组件列表
if (importedComponents.length > 0) {
console.log('[Preset Import] 更新组件列表:', importedComponents.length, '个组件');
console.log(' - 组件列表:', importedComponents.length, '个组件');
setPromptComponents(importedComponents);
}
// 使用 requestAnimationFrame 确保状态更新完成后再保存
requestAnimationFrame(() => {
setTimeout(async () => {
console.log('[Preset Import] 准备保存预设:', presetName);
console.log('\n[预设导入] 💾 保存预设:', presetName);
try {
await saveCurrentAsPreset({ name: presetName });
console.log('[Preset Import] 预设保存成功');
console.log('[预设导入] ✅ 预设保存成功');
console.log('='.repeat(80) + '\n');
// 重新加载预设列表并重置到第一页
setCurrentPage(1);
fetchPresets();
} catch (error) {
console.error('[Preset Import] 保存预设失败:', error);
console.error('[预设导入] ❌ 保存预设失败:', error);
console.log('='.repeat(80) + '\n');
alert('保存预设失败: ' + error.message);
}
}, 50);
});
}
} catch (error) {
console.error('导入预设失败:', error);
console.error('\n[预设导入] ❌ 导入失败:', error);
console.log('='.repeat(80) + '\n');
alert('导入预设失败: ' + error.message);
}
};

View File

@@ -0,0 +1,536 @@
/* ==================== 正则面板 - 完整重写 ==================== */
/* 主容器 */
.regex-panel {
padding: 16px;
height: 100%;
overflow-y: auto;
background: var(--color-bg-primary);
color: var(--color-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
/* 头部 */
.regex-panel-header {
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--color-border);
}
.regex-panel-header h2 {
margin: 0 0 8px 0;
font-size: 22px;
font-weight: 600;
color: var(--color-text-primary);
}
.subtitle {
margin: 0;
font-size: 13px;
color: var(--color-text-muted);
}
/* ==================== 左右布局 ==================== */
.regex-layout {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
height: calc(100% - 80px);
}
.regex-left {
overflow-y: auto;
padding-right: 12px;
}
.regex-right {
overflow-y: auto;
}
/* ==================== 可折叠区域 ==================== */
.regex-section {
margin-bottom: 16px;
background: var(--color-bg-secondary);
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--color-border);
}
.regex-section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--color-bg-tertiary);
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
.regex-section-header:hover {
background: var(--color-bg-hover);
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
}
.collapse-icon {
font-size: 12px;
color: var(--color-text-muted);
transition: transform 0.2s;
}
.section-icon {
font-size: 16px;
}
.section-title h4 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
}
.rule-count {
font-size: 12px;
color: var(--color-text-muted);
}
.btn-add-rule-small {
padding: 4px 12px;
background: var(--color-accent);
border: none;
border-radius: 4px;
color: white;
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
}
.btn-add-rule-small:hover {
background: var(--color-accent-hover);
}
.regex-section-content {
padding: 12px;
background: var(--color-bg-primary);
}
.empty-rules {
text-align: center;
padding: 20px;
color: var(--color-text-muted);
font-size: 13px;
}
/* ==================== 规则列表 ==================== */
.rules-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.rule-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
background: var(--color-bg-tertiary);
border-radius: 6px;
transition: all 0.2s;
border: 1px solid transparent;
}
.rule-item:hover {
background: var(--color-bg-hover);
border-color: var(--color-border);
}
.rule-info {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.rule-name {
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rule-badges {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.rule-badge {
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
white-space: nowrap;
}
.rule-badge.disabled {
background: var(--color-error);
color: white;
}
.rule-badge.prompt {
background: var(--color-info);
color: white;
}
.rule-badge.markdown {
/* 注意Markdown 徽章使用固定紫色,作为功能标识色 */
background: #8844ff;
color: white;
}
.rule-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.icon-btn {
padding: 4px 6px;
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
border-radius: 4px;
font-size: 14px;
transition: all 0.2s;
}
.icon-btn:hover {
background: var(--color-bg-elevated);
color: var(--color-text-primary);
}
/* 开关按钮 */
.toggle-switch {
position: relative;
width: 40px;
height: 22px;
display: inline-block;
cursor: pointer;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-border);
transition: 0.3s;
border-radius: 22px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: var(--color-accent);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(18px);
}
/* ==================== 右侧信息卡片 ==================== */
.regex-info-card {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 16px;
}
.regex-info-card h4 {
margin: 0 0 12px 0;
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
}
.regex-info-card ul {
margin: 0;
padding-left: 20px;
list-style: none;
}
.regex-info-card li {
margin-bottom: 8px;
font-size: 13px;
color: var(--color-text-secondary);
line-height: 1.5;
}
.regex-info-card li strong {
color: var(--color-text-primary);
}
.quick-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.btn-quick {
padding: 10px 16px;
background: var(--color-accent);
border: none;
border-radius: 6px;
color: white;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-quick:hover {
background: var(--color-accent-hover);
}
/* ==================== 编辑器弹窗 ==================== */
.regex-editor-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.regex-editor-modal {
width: 90%;
max-width: 800px;
max-height: 90vh;
background: var(--color-bg-primary);
border-radius: 12px;
border: 1px solid var(--color-border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.regex-editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border);
}
.regex-editor-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary);
}
.close-btn {
background: transparent;
border: none;
color: var(--color-text-muted);
font-size: 24px;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.close-btn:hover {
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
.regex-editor-body {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
}
.form-group small {
display: block;
margin-top: 4px;
font-size: 11px;
color: var(--color-text-muted);
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group textarea,
.form-group select {
width: 100%;
padding: 8px 12px;
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 6px;
color: var(--color-text-primary);
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: var(--color-accent);
}
.form-group textarea {
resize: vertical;
min-height: 60px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.form-section h4 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 13px;
color: var(--color-text-secondary);
}
.checkbox-item input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.form-group.small {
margin-bottom: 0;
}
.regex-editor-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
background: var(--color-bg-secondary);
border-top: 1px solid var(--color-border);
}
.btn-cancel,
.btn-save {
padding: 8px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-cancel {
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
.btn-cancel:hover {
background: var(--color-bg-hover);
}
.btn-save {
background: var(--color-accent);
color: white;
}
.btn-save:hover {
background: var(--color-accent-hover);
}
.btn-save:disabled {
background: var(--color-border);
cursor: not-allowed;
}
/* ==================== 响应式 ==================== */
@media (max-width: 1200px) {
.regex-layout {
grid-template-columns: 1fr;
}
.regex-right {
display: none;
}
}

View File

@@ -0,0 +1,575 @@
import React, { useState, useEffect } from 'react';
import usePresetStore from '../../../../Store/SideBarLeft/PresetSlice';
import './RegexPanel.css';
const RegexPanel = () => {
// 状态管理
const [globalRulesets, setGlobalRulesets] = useState([]);
const [presetRulesets, setPresetRulesets] = useState([]);
const [editingRule, setEditingRule] = useState(null);
const [showEditor, setShowEditor] = useState(false);
const [isLoading, setIsLoading] = useState(false);
// 折叠状态
const [collapsedSections, setCollapsedSections] = useState({
global: false,
preset: false
});
// 获取当前预设
const { selectedPreset } = usePresetStore();
// 编辑器表单状态
const [formData, setFormData] = useState({
id: '',
scriptName: '',
findRegex: '',
replaceString: '',
trimStrings: [],
placement: [1], // 默认 AI输出 (注意0=System, 1=AI Output, 2=User Input)
disabled: false,
markdownOnly: false,
promptOnly: false,
runOnEdit: true,
substituteRegex: 0,
minDepth: null,
maxDepth: null
});
// 作用范围选项(修正顺序)
const placementOptions = [
{ value: 0, label: '系统提示词' },
{ value: 1, label: 'AI输出' },
{ value: 2, label: '用户输入' },
{ value: 3, label: '快捷命令' },
{ value: 4, label: '世界信息' },
{ value: 5, label: '推理' }
];
// 替换模式选项
const substituteOptions = [
{ value: 0, label: '不替换' },
{ value: 1, label: '替换首次匹配' },
{ value: 2, label: '替换所有匹配' }
];
// 加载全局规则集
useEffect(() => {
fetchGlobalRulesets();
}, []);
// 当预设变化时加载预设规则集
useEffect(() => {
if (selectedPreset) {
fetchPresetRulesets(selectedPreset);
} else {
setPresetRulesets([]);
}
}, [selectedPreset]);
const fetchGlobalRulesets = async () => {
try {
setIsLoading(true);
const response = await fetch('/api/regex/rulesets/global');
const data = await response.json();
if (data.success) {
setGlobalRulesets(data.rulesets || []);
}
} catch (error) {
console.error('加载全局规则集失败:', error);
} finally {
setIsLoading(false);
}
};
const fetchPresetRulesets = async (presetName) => {
try {
const response = await fetch(`/api/regex/rulesets/preset/${encodeURIComponent(presetName)}`);
const data = await response.json();
if (data.success && data.ruleset) {
setPresetRulesets([data.ruleset]);
} else {
setPresetRulesets([]);
}
} catch (error) {
console.error('加载预设规则集失败:', error);
setPresetRulesets([]);
}
};
// 切换折叠状态
const toggleCollapse = (section) => {
setCollapsedSections(prev => ({
...prev,
[section]: !prev[section]
}));
};
// 打开编辑器 - 新建规则
const handleNewRule = (scope = 'global') => {
setFormData({
id: crypto.randomUUID(),
scriptName: '',
findRegex: '',
replaceString: '',
trimStrings: [],
placement: [1],
disabled: false,
markdownOnly: false,
promptOnly: false,
runOnEdit: true,
substituteRegex: 0,
minDepth: null,
maxDepth: null
});
setEditingRule({ scope });
setShowEditor(true);
};
// 打开编辑器 - 编辑规则
const handleEditRule = (rule) => {
setFormData({
...rule,
placement: rule.placement || [2],
trimStrings: rule.trimStrings || []
});
setEditingRule(rule);
setShowEditor(true);
};
// 保存规则
const handleSaveRule = async () => {
try {
setIsLoading(true);
const scope = editingRule?.scope || 'global';
const presetName = scope === 'preset' ? selectedPreset : null;
const response = await fetch('/api/regex/rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rule: formData,
scope: scope,
name: presetName
})
});
const data = await response.json();
if (data.success) {
alert('规则保存成功!');
setShowEditor(false);
if (scope === 'preset') {
fetchPresetRulesets(selectedPreset);
} else {
fetchGlobalRulesets();
}
} else {
alert('保存失败: ' + data.message);
}
} catch (error) {
console.error('保存规则失败:', error);
alert('保存失败: ' + error.message);
} finally {
setIsLoading(false);
}
};
// 删除规则
const handleDeleteRule = async (ruleId, ruleName, sectionKey = 'global') => {
if (!confirm(`确定要删除规则 "${ruleName}" 吗?`)) {
return;
}
try {
setIsLoading(true);
const scope = sectionKey === 'preset' ? 'preset' : 'global';
const presetName = scope === 'preset' ? selectedPreset : null;
const url = new URL(`/api/regex/rules/${ruleId}`, window.location.origin);
url.searchParams.set('scope', scope);
if (presetName) {
url.searchParams.set('name', presetName);
}
const response = await fetch(url.toString(), {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
alert('规则已删除');
if (sectionKey === 'preset') {
fetchPresetRulesets(selectedPreset);
} else {
fetchGlobalRulesets();
}
} else {
alert('删除失败: ' + data.message);
}
} catch (error) {
console.error('删除规则失败:', error);
alert('删除失败: ' + error.message);
} finally {
setIsLoading(false);
}
};
// 切换规则启用状态
const handleToggleRule = async (rule, sectionKey = 'global') => {
try {
const updatedRule = { ...rule, disabled: !rule.disabled };
const scope = sectionKey === 'preset' ? 'preset' : 'global';
const presetName = scope === 'preset' ? selectedPreset : null;
const response = await fetch('/api/regex/rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rule: updatedRule,
scope: scope,
name: presetName
})
});
const data = await response.json();
if (data.success) {
if (sectionKey === 'preset') {
fetchPresetRulesets(selectedPreset);
} else {
fetchGlobalRulesets();
}
}
} catch (error) {
console.error('更新规则状态失败:', error);
}
};
// 更新表单字段
const updateFormField = (field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
// 切换作用范围
const togglePlacement = (value) => {
setFormData(prev => {
const current = prev.placement || [];
const updated = current.includes(value)
? current.filter(v => v !== value)
: [...current, value];
return { ...prev, placement: updated };
});
};
// 更新修剪字符串
const updateTrimStrings = (value) => {
const trimStrings = value.split('\n').filter(s => s.trim());
setFormData(prev => ({ ...prev, trimStrings }));
};
// 渲染编辑器
const renderEditor = () => {
if (!showEditor) return null;
return (
<div className="regex-editor-overlay">
<div className="regex-editor-modal">
<div className="regex-editor-header">
<h3>正则表达式编辑器</h3>
<button className="close-btn" onClick={() => setShowEditor(false)}>×</button>
</div>
<div className="regex-editor-body">
{/* 脚本名称 */}
<div className="form-group">
<label>脚本名称</label>
<input
type="text"
value={formData.scriptName}
onChange={(e) => updateFormField('scriptName', e.target.value)}
placeholder="例如隐藏thinking"
/>
</div>
{/* 查找正则表达式 */}
<div className="form-group">
<label>查找正则表达式</label>
<input
type="text"
value={formData.findRegex}
onChange={(e) => updateFormField('findRegex', e.target.value)}
placeholder="/pattern/flags"
/>
<small>SillyTavern 格式例如/&lt;thinking&gt;[\\s\\S]*?&lt;\\/thinking&gt;/gs</small>
</div>
{/* 替换为 */}
<div className="form-group">
<label>替换为</label>
<textarea
value={formData.replaceString}
onChange={(e) => updateFormField('replaceString', e.target.value)}
placeholder="使用 {{match}} 或 $1, $2 等捕获组"
rows={3}
/>
</div>
{/* 修剪掉 */}
<div className="form-group">
<label>修剪掉</label>
<textarea
value={formData.trimStrings.join('\n')}
onChange={(e) => updateTrimStrings(e.target.value)}
placeholder="每行一个字符串"
rows={2}
/>
</div>
{/* 作用范围和其他选项 */}
<div className="form-row">
<div className="form-section">
<h4>作用范围</h4>
<div className="checkbox-group">
{placementOptions.map(opt => (
<label key={opt.value} className="checkbox-item">
<input
type="checkbox"
checked={formData.placement.includes(opt.value)}
onChange={() => togglePlacement(opt.value)}
/>
<span>{opt.label}</span>
</label>
))}
</div>
</div>
<div className="form-section">
<h4>其他选项</h4>
<div className="checkbox-group">
<label className="checkbox-item">
<input
type="checkbox"
checked={formData.disabled}
onChange={(e) => updateFormField('disabled', e.target.checked)}
/>
<span>已禁用</span>
</label>
<label className="checkbox-item">
<input
type="checkbox"
checked={formData.runOnEdit}
onChange={(e) => updateFormField('runOnEdit', e.target.checked)}
/>
<span>在编辑时运行</span>
</label>
<label className="checkbox-item">
<input
type="checkbox"
checked={formData.markdownOnly}
onChange={(e) => updateFormField('markdownOnly', e.target.checked)}
/>
<span>仅格式显示</span>
</label>
<label className="checkbox-item">
<input
type="checkbox"
checked={formData.promptOnly}
onChange={(e) => updateFormField('promptOnly', e.target.checked)}
/>
<span>仅格式提示词</span>
</label>
</div>
</div>
</div>
{/* 深度设置 */}
<div className="form-row">
<div className="form-group small">
<label>最小深度</label>
<input
type="number"
value={formData.minDepth || ''}
onChange={(e) => updateFormField('minDepth', e.target.value ? parseInt(e.target.value) : null)}
placeholder="无限"
/>
</div>
<div className="form-group small">
<label>最大深度</label>
<input
type="number"
value={formData.maxDepth || ''}
onChange={(e) => updateFormField('maxDepth', e.target.value ? parseInt(e.target.value) : null)}
placeholder="无限"
/>
</div>
</div>
{/* 替换模式 */}
<div className="form-group">
<label>替换模式</label>
<select
value={formData.substituteRegex}
onChange={(e) => updateFormField('substituteRegex', parseInt(e.target.value))}
>
{substituteOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
</div>
<div className="regex-editor-footer">
<button className="btn-cancel" onClick={() => setShowEditor(false)}>
取消
</button>
<button className="btn-save" onClick={handleSaveRule} disabled={isLoading}>
{isLoading ? '保存中...' : '保存'}
</button>
</div>
</div>
</div>
);
};
// 渲染可折叠的规则列表
const renderCollapsibleRules = (title, rulesets, sectionKey, icon = '📜') => {
const allRules = rulesets.flatMap(ruleset => ruleset.rules || []);
const isCollapsed = collapsedSections[sectionKey];
return (
<div className="regex-section">
<div
className="regex-section-header"
onClick={() => toggleCollapse(sectionKey)}
>
<div className="section-title">
<span className="collapse-icon">{isCollapsed ? '▶' : '▼'}</span>
<span className="section-icon">{icon}</span>
<h4>{title}</h4>
<span className="rule-count">({allRules.length})</span>
</div>
{!isCollapsed && allRules.length > 0 && (
<button className="btn-add-rule-small" onClick={(e) => {
e.stopPropagation();
handleNewRule(sectionKey);
}}>
+ 添加
</button>
)}
</div>
{!isCollapsed && (
<div className="regex-section-content">
{allRules.length === 0 ? (
<div className="empty-rules">
<p>暂无规则</p>
</div>
) : (
<div className="rules-list">
{allRules.map((rule) => (
<div key={rule.id} className="rule-item">
<div className="rule-info">
<span className="rule-name">{rule.scriptName}</span>
<div className="rule-badges">
{rule.disabled && <span className="rule-badge disabled">已禁用</span>}
{rule.promptOnly && <span className="rule-badge prompt">仅提示词</span>}
{rule.markdownOnly && <span className="rule-badge markdown">仅格式</span>}
</div>
</div>
<div className="rule-actions">
<label className="toggle-switch" onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={!rule.disabled}
onChange={() => handleToggleRule(rule, sectionKey)}
/>
<span className="toggle-slider"></span>
</label>
<button className="icon-btn" onClick={(e) => {
e.stopPropagation();
handleEditRule(rule);
}} title="编辑">
</button>
<button className="icon-btn" onClick={(e) => {
e.stopPropagation();
handleDeleteRule(rule.id, rule.scriptName, sectionKey);
}} title="删除">
🗑
</button>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
);
};
return (
<div className="regex-panel">
<div className="regex-panel-header">
<h2>正则管理</h2>
<p className="subtitle">管理全局和预设的正则规则</p>
</div>
{/* 左右布局 */}
<div className="regex-layout">
{/* 左侧:规则列表 */}
<div className="regex-left">
{/* 全局规则 */}
{renderCollapsibleRules('全局正则脚本', globalRulesets, 'global', '🌍')}
{/* 预设规则 */}
{renderCollapsibleRules(
selectedPreset ? `预设正则脚本 (${selectedPreset})` : '预设正则脚本',
presetRulesets,
'preset',
'⚙️'
)}
</div>
{/* 右侧:说明和快速操作 */}
<div className="regex-right">
<div className="regex-info-card">
<h4>📖 正则规则说明</h4>
<ul>
<li><strong>全局规则</strong>影响所有角色和聊天</li>
<li><strong>预设规则</strong>仅影响当前预设</li>
<li><strong>仅提示词</strong>只应用于发送LLM的内容</li>
<li><strong>仅格式</strong>只应用于Markdown渲染</li>
</ul>
<h4 style={{ marginTop: '20px' }}> 快捷操作</h4>
<div className="quick-actions">
<button className="btn-quick" onClick={() => handleNewRule('global')}>
+ 新建全局规则
</button>
{selectedPreset && (
<button className="btn-quick" onClick={() => handleNewRule('preset')}>
+ 新建预设规则
</button>
)}
</div>
</div>
</div>
</div>
{/* 编辑器弹窗 */}
{renderEditor()}
</div>
);
};
export default RegexPanel;

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import './Settings.css';
import RegexPanel from '../Regex/RegexPanel';
/**
* 系统设置组件
@@ -122,77 +123,7 @@ const Settings = () => {
{/* 正则规则设置 */}
{activeTab === 'regex' && (
<div className="settings-panel">
<div className="panel-header">
<h3>📝 正则规则管理</h3>
<p className="panel-description">
管理文本处理规则支持 SillyTavern 格式导入
</p>
</div>
<div className="settings-group">
<div className="group-title">规则文件结构</div>
<div className="setting-row">
<div className="row-label">存储位置</div>
<div className="row-content">
<code className="path-code">data/regex/</code>
</div>
</div>
<ul className="file-structure-list">
<li><span className="folder-icon">📁</span> global/ - 全局规则</li>
<li><span className="folder-icon">📁</span> characters/ - 角色卡规则</li>
<li><span className="folder-icon">📁</span> presets/ - 预设规则</li>
</ul>
</div>
<div className="settings-group">
<div className="group-title">导入导出</div>
<div className="setting-row">
<div className="row-label">导入规则文件</div>
<div className="row-content">
<input
type="file"
accept=".json"
className="file-input"
onChange={async (e) => {
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', '导入失败');
}
}}
/>
<small className="hint-text">支持 SillyTavern 格式的规则文件</small>
</div>
</div>
<div className="setting-row">
<div className="row-label">导出全局规则</div>
<div className="row-content">
<button
className="btn-action"
onClick={() => window.open('/api/regex/export/global', '_blank')}
>
📥 导出 JSON
</button>
</div>
</div>
</div>
<RegexPanel />
</div>
)}

View File

@@ -487,3 +487,181 @@
font-size: 13px;
margin-bottom: 16px;
}
/* ==================== 正则规则列表样式 ==================== */
.regex-rules-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 400px;
overflow-y: auto;
}
.regex-rule-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--color-bg-tertiary);
border-radius: 8px;
transition: all 0.2s;
}
.regex-rule-item:hover {
background: var(--color-bg-hover);
}
.regex-rule-drag {
cursor: grab;
color: var(--color-text-muted);
font-size: 16px;
user-select: none;
}
.regex-rule-info {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.regex-rule-name {
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.regex-badge {
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
white-space: nowrap;
flex-shrink: 0;
}
.regex-badge.disabled {
background: var(--color-error);
color: white;
}
.regex-badge.prompt {
background: var(--color-info);
color: white;
}
.regex-badge.markdown {
/* 注意Markdown 徽章使用固定紫色,作为功能标识色 */
background: #8844ff;
color: white;
}
.regex-rule-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
/* 开关按钮 */
.toggle-switch {
position: relative;
width: 44px;
height: 24px;
display: inline-block;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-border);
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: var(--color-accent);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(20px);
}
.icon-btn-sm {
padding: 4px 6px;
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
border-radius: 4px;
font-size: 12px;
transition: all 0.2s;
}
.icon-btn-sm:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.btn-icon {
padding: 6px 10px;
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 4px;
color: var(--color-text-secondary);
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-icon:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
border-color: var(--color-text-muted);
}
.btn-add-rule {
width: 100%;
padding: 12px;
background: var(--color-bg-tertiary);
border: 2px dashed var(--color-border);
border-radius: 8px;
color: var(--color-text-secondary);
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.btn-add-rule:hover {
background: var(--color-bg-hover);
border-color: var(--color-text-muted);
color: var(--color-text-primary);
}

View File

@@ -5,6 +5,7 @@ import useUserStore from '../../Store/UserSlice'; // ✅ 新增 - 用户角色
import useWorldBookStore from '../../Store/SideBarLeft/WorldBookSlice';
import useApiConfigStore from '../../Store/SideBarLeft/ApiConfigSlice';
import ThemeToggle from './items/ThemeToggle';
import RegexPanel from '../SideBarLeft/tabs/Regex/RegexPanel'; // ✅ 新增:导入正则管理组件
import './TopBar.css';
const Toolbar = () => {
@@ -372,59 +373,13 @@ const Toolbar = () => {
</div>
<div className="setting-section" style={{ marginTop: '20px', paddingTop: '20px', borderTop: '1px solid var(--border-color)' }}>
<h4>则规则管理</h4>
<div style={{ marginBottom: '16px' }}>
<code className="settings-code">
data/regex/
</code>
<ul className="settings-file-list">
<li>📁 global/ - 全局规则</li>
<li>📁 characters/ - 角色卡规则</li>
<li>📁 presets/ - 预设规则</li>
</ul>
</div>
<h4>📝 正则管理</h4>
<p className="settings-hint" style={{ marginBottom: '16px' }}>
管理全局和预设的正则规则
</p>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: '500', color: 'var(--color-text-primary)' }}>导入规则文件</label>
<input
type="file"
accept=".json"
onChange={async (e) => {
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) {
showSettingsMessage('success', data.message);
} else {
showSettingsMessage('error', '导入失败');
}
} catch (error) {
showSettingsMessage('error', '导入失败');
}
}}
className="settings-input"
/>
<small className="settings-hint">
支持 SillyTavern 格式的规则文件
</small>
</div>
<button
className="btn-primary"
onClick={() => window.open('/api/regex/export/global', '_blank')}
>
导出全局规则
</button>
{/* ✅ 使用 RegexPanel 组件 */}
<RegexPanel />
</div>
<div style={{ marginTop: '20px' }}>