From a6234e7bb0795dabe0df29906675b275a4be5736 Mon Sep 17 00:00:00 2001 From: Echo <1711788888@qq.com> Date: Sat, 28 Feb 2026 15:09:53 +0800 Subject: [PATCH] =?UTF-8?q?:art:=20=E4=BC=98=E5=8C=96=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E5=8D=A1=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Echo <1711788888@qq.com> --- server/api/v1/app/enter.go | 1 + server/initialize/gorm.go | 1 + server/initialize/router.go | 1 + server/router/app/enter.go | 1 + server/service/app/character.go | 127 ++++++++- server/service/app/enter.go | 1 + web-app/src/components/SettingsPanel.tsx | 87 +++++- web-app/src/pages/CharacterManagePage.tsx | 320 +++++++++++++++++++++- 8 files changed, 530 insertions(+), 9 deletions(-) diff --git a/server/api/v1/app/enter.go b/server/api/v1/app/enter.go index a4941fa..f858ac1 100644 --- a/server/api/v1/app/enter.go +++ b/server/api/v1/app/enter.go @@ -8,4 +8,5 @@ type ApiGroup struct { PresetApi UploadApi WorldbookApi + RegexScriptApi } diff --git a/server/initialize/gorm.go b/server/initialize/gorm.go index a934ca2..a635391 100644 --- a/server/initialize/gorm.go +++ b/server/initialize/gorm.go @@ -88,6 +88,7 @@ func RegisterTables() { app.AIPreset{}, app.Worldbook{}, app.WorldbookEntry{}, + app.RegexScript{}, ) if err != nil { global.GVA_LOG.Error("register table failed", zap.Error(err)) diff --git a/server/initialize/router.go b/server/initialize/router.go index b459b89..8e5a7b5 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -154,6 +154,7 @@ func Routers() *gin.Engine { appRouter.InitPresetRouter(appGroup) // 预设路由:/app/preset/* appRouter.InitUploadRouter(appGroup) // 上传路由:/app/upload/* appRouter.InitWorldbookRouter(appGroup) // 世界书路由:/app/worldbook/* + appRouter.InitRegexScriptRouter(appGroup) // 正则脚本路由:/app/regex/* } //插件路由安装 diff --git a/server/router/app/enter.go b/server/router/app/enter.go index 9cda855..cdff5c2 100644 --- a/server/router/app/enter.go +++ b/server/router/app/enter.go @@ -8,4 +8,5 @@ type RouterGroup struct { PresetRouter UploadRouter WorldbookRouter + RegexScriptRouter } diff --git a/server/service/app/character.go b/server/service/app/character.go index 9b58e99..ed33f9b 100644 --- a/server/service/app/character.go +++ b/server/service/app/character.go @@ -267,7 +267,18 @@ func (s *CharacterService) ImportCharacterFromPNG(userID uint, file *multipart.F IsPublic: false, } - return s.CreateCharacter(userID, req) + // 创建角色卡 + resp, err := s.CreateCharacter(userID, req) + if err != nil { + return nil, err + } + + // 处理扩展数据中的正则脚本 + if card.Data.Extensions != nil { + s.processRegexScriptsFromExtensions(userID, resp.ID, card.Data.Extensions) + } + + return resp, nil } // ImportCharacterFromJSON 从 JSON 文件导入角色卡 @@ -312,7 +323,18 @@ func (s *CharacterService) ImportCharacterFromJSON(userID uint, file *multipart. IsPublic: false, } - return s.CreateCharacter(userID, req) + // 创建角色卡 + resp, err := s.CreateCharacter(userID, req) + if err != nil { + return nil, err + } + + // 处理扩展数据中的正则脚本 + if card.Data.Extensions != nil { + s.processRegexScriptsFromExtensions(userID, resp.ID, card.Data.Extensions) + } + + return resp, nil } // ExportCharacterToJSON 导出角色卡为 JSON @@ -339,6 +361,16 @@ func (s *CharacterService) ExportCharacterToJSON(userID, characterID uint) (*uti json.Unmarshal(character.CharacterBook, &characterBook) json.Unmarshal(character.Extensions, &extensions) + // 查询角色关联的正则脚本并添加到 extensions + var regexScripts []app.RegexScript + global.GVA_DB.Where("owner_char_id = ? AND scope = 1", characterID).Find(®exScripts) + if len(regexScripts) > 0 { + if extensions == nil { + extensions = make(map[string]interface{}) + } + extensions["regex_scripts"] = regexScripts + } + // 构建 V2 格式 card := &utils.CharacterCardV2{ Spec: character.Spec, @@ -364,3 +396,94 @@ func (s *CharacterService) ExportCharacterToJSON(userID, characterID uint) (*uti return card, nil } + +// processRegexScriptsFromExtensions 从扩展数据中提取并创建正则脚本 +func (s *CharacterService) processRegexScriptsFromExtensions(userID, characterID uint, extensions map[string]interface{}) { + // 检查是否包含正则脚本 + regexScriptsData, ok := extensions["regex_scripts"] + if !ok { + return + } + + // 转换为 JSON 以便解析 + scriptsJSON, err := json.Marshal(regexScriptsData) + if err != nil { + global.GVA_LOG.Error("序列化正则脚本失败: " + err.Error()) + return + } + + // 解析正则脚本数组 + var scripts []map[string]interface{} + if err := json.Unmarshal(scriptsJSON, &scripts); err != nil { + global.GVA_LOG.Error("解析正则脚本失败: " + err.Error()) + return + } + + // 创建正则脚本记录 + for _, scriptData := range scripts { + script := app.RegexScript{ + UserID: userID, + Scope: 1, // 角色作用域 + OwnerCharID: &characterID, + } + + // 提取字段 + if name, ok := scriptData["name"].(string); ok { + script.Name = name + } + if findRegex, ok := scriptData["findRegex"].(string); ok { + script.FindRegex = findRegex + } + if replaceWith, ok := scriptData["replaceWith"].(string); ok { + script.ReplaceWith = replaceWith + } + if placement, ok := scriptData["placement"].(float64); ok { + script.Placement = int(placement) + } + if disabled, ok := scriptData["disabled"].(bool); ok { + script.Disabled = disabled + } + if markdownOnly, ok := scriptData["markdownOnly"].(bool); ok { + script.MarkdownOnly = markdownOnly + } + if runOnEdit, ok := scriptData["runOnEdit"].(bool); ok { + script.RunOnEdit = runOnEdit + } + if promptOnly, ok := scriptData["promptOnly"].(bool); ok { + script.PromptOnly = promptOnly + } + if substituteRegex, ok := scriptData["substituteRegex"].(bool); ok { + script.SubstituteRegex = substituteRegex + } + if order, ok := scriptData["order"].(float64); ok { + script.Order = int(order) + } + + // 处理可选的整数字段 + if minDepth, ok := scriptData["minDepth"].(float64); ok { + depth := int(minDepth) + script.MinDepth = &depth + } + if maxDepth, ok := scriptData["maxDepth"].(float64); ok { + depth := int(maxDepth) + script.MaxDepth = &depth + } + + // 处理 trimStrings 数组 + if trimStrings, ok := scriptData["trimStrings"].([]interface{}); ok { + trimStringsJSON, _ := json.Marshal(trimStrings) + script.TrimStrings = datatypes.JSON(trimStringsJSON) + } + + // 处理扩展字段 + if scriptExtensions, ok := scriptData["extensions"].(map[string]interface{}); ok { + extensionsJSON, _ := json.Marshal(scriptExtensions) + script.Extensions = datatypes.JSON(extensionsJSON) + } + + // 创建记录 + if err := global.GVA_DB.Create(&script).Error; err != nil { + global.GVA_LOG.Error("创建正则脚本失败: " + err.Error()) + } + } +} diff --git a/server/service/app/enter.go b/server/service/app/enter.go index bb83796..10356be 100644 --- a/server/service/app/enter.go +++ b/server/service/app/enter.go @@ -8,4 +8,5 @@ type AppServiceGroup struct { PresetService UploadService WorldbookService + RegexScriptService } diff --git a/web-app/src/components/SettingsPanel.tsx b/web-app/src/components/SettingsPanel.tsx index 32a6b70..2b766c6 100644 --- a/web-app/src/components/SettingsPanel.tsx +++ b/web-app/src/components/SettingsPanel.tsx @@ -1,6 +1,7 @@ -import {Image, Palette, RotateCcw, Save, Upload, X} from 'lucide-react' +import {Image, Palette, RotateCcw, Save, Upload, X, BookOpen} from 'lucide-react' import {useEffect, useRef, useState} from 'react' import {type Conversation, conversationApi} from '../api/conversation' +import {type Worldbook, getWorldbookList} from '../api/worldbook' interface SettingsPanelProps { conversation: Conversation @@ -11,6 +12,8 @@ interface SettingsPanelProps { interface ConversationSettings { themeColor: string backgroundImage?: string + worldbookId?: number + worldbookEnabled: boolean } const THEME_COLORS = [ @@ -25,13 +28,16 @@ const THEME_COLORS = [ export default function SettingsPanel({ conversation, onClose, onUpdate }: SettingsPanelProps) { const [settings, setSettings] = useState({ themeColor: '#7C3AED', + worldbookEnabled: false, }) const [hasChanges, setHasChanges] = useState(false) const [backgroundPreview, setBackgroundPreview] = useState() + const [worldbooks, setWorldbooks] = useState([]) const fileInputRef = useRef(null) useEffect(() => { loadSettings() + loadWorldbooks() }, []) const loadSettings = () => { @@ -44,6 +50,8 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti // 合并默认设置和保存的设置 const mergedSettings = { themeColor: '#7C3AED', + worldbookEnabled: conversation.worldbookEnabled || false, + worldbookId: conversation.worldbookId, ...parsed } @@ -60,6 +68,22 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti } catch (e) { console.error('解析设置失败:', e) } + } else { + // 如果没有 settings,从 conversation 直接读取世界书配置 + setSettings(prev => ({ + ...prev, + worldbookEnabled: conversation.worldbookEnabled || false, + worldbookId: conversation.worldbookId, + })) + } + } + + const loadWorldbooks = async () => { + try { + const response = await getWorldbookList({ page: 1, pageSize: 100 }) + setWorldbooks(response.data.list || []) + } catch (error) { + console.error('加载世界书列表失败:', error) } } @@ -99,7 +123,11 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti const handleSave = async () => { try { // 保存到后端 - await conversationApi.updateConversationSettings(conversation.id, settings) + await conversationApi.updateConversationSettings(conversation.id, { + settings, + worldbookId: settings.worldbookId, + worldbookEnabled: settings.worldbookEnabled, + }) // 应用主题色到根元素 if (settings.themeColor) { @@ -119,6 +147,7 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti const handleReset = () => { setSettings({ themeColor: '#7C3AED', + worldbookEnabled: false, }) setBackgroundPreview(undefined) setHasChanges(false) @@ -214,10 +243,64 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti +
+
+
+ +
+

世界书设置

+
+ +
+
+
+ +

根据关键词自动注入背景信息

+
+ +
+ + {settings.worldbookEnabled && ( +
+ + + {worldbooks.length === 0 && ( +

+ 还没有世界书,请先在世界书管理页面创建 +

+ )} +
+ )} +
+
+

💡 主题颜色会应用到整个界面

💡 背景图片仅在对话页面显示

+

💡 世界书会根据对话内容自动触发相关条目

💡 AI 模型可在对话界面顶部切换

diff --git a/web-app/src/pages/CharacterManagePage.tsx b/web-app/src/pages/CharacterManagePage.tsx index 1e665ec..e6cfe0b 100644 --- a/web-app/src/pages/CharacterManagePage.tsx +++ b/web-app/src/pages/CharacterManagePage.tsx @@ -1,8 +1,9 @@ import {useEffect, useState} from 'react' import {useNavigate, useSearchParams} from 'react-router-dom' import Navbar from '../components/Navbar' -import {Book, Download, Edit, FileJson, Image as ImageIcon, Plus, Search, Trash2, X} from 'lucide-react' +import {Book, Code2, Download, Edit, FileJson, Image as ImageIcon, Plus, Search, Trash2, X} from 'lucide-react' import {type Character, characterApi} from '../api/character' +import {type RegexScript, regexScriptApi} from '../api/regex' interface WorldBookEntry { keys: string[] @@ -32,6 +33,9 @@ export default function CharacterManagePage() { const [total, setTotal] = useState(0) const [showWorldBookEditor, setShowWorldBookEditor] = useState(false) const [worldBookEntries, setWorldBookEntries] = useState([]) + const [showRegexScriptEditor, setShowRegexScriptEditor] = useState(false) + const [regexScripts, setRegexScripts] = useState([]) + const [editingTab, setEditingTab] = useState<'basic' | 'worldbook' | 'regex'>('basic') const navigate = useNavigate() const [searchParams] = useSearchParams() @@ -47,6 +51,7 @@ export default function CharacterManagePage() { setSelectedCharacter(char) setShowEditModal(true) loadWorldBook(char) + loadRegexScripts(char.id) } } }, [searchParams, characters]) @@ -66,6 +71,35 @@ export default function CharacterManagePage() { } } + const loadRegexScripts = async (characterId: number) => { + try { + const response = await regexScriptApi.getRegexScriptList({ + scope: 1, + pageSize: 100, + }) + console.log('正则脚本 API 响应:', response) + + // apiClient 拦截器返回 response.data,所以这里的 response 就是后端的完整响应 + // 后端返回: {code: 0, data: {list: [...], total: 10, ...}, msg: '获取成功'} + let list = [] + if (response.data && response.data.list && Array.isArray(response.data.list)) { + // response.data.list 是正确的路径 + list = response.data.list + } else { + console.warn('正则脚本列表数据格式不正确:', response) + setRegexScripts([]) + return + } + + const charScripts = list.filter((s: any) => s.ownerCharId === characterId) + console.log(`找到 ${charScripts.length} 个角色关联的正则脚本`) + setRegexScripts(charScripts) + } catch (err) { + console.error('加载正则脚本失败:', err) + setRegexScripts([]) + } + } + const loadCharacters = async () => { try { setLoading(true) @@ -173,6 +207,7 @@ export default function CharacterManagePage() { setWorldBookEntries([...worldBookEntries, { keys: [], content: '', + comment: '', enabled: true, insertion_order: worldBookEntries.length, position: 'after_char' @@ -308,6 +343,8 @@ export default function CharacterManagePage() { onClick={() => { setSelectedCharacter(char) setShowEditModal(true) + loadWorldBook(char) + loadRegexScripts(char.id) }} className="flex-1 px-4 py-2 glass-hover rounded-lg text-sm cursor-pointer flex items-center justify-center gap-2" > @@ -513,14 +550,22 @@ export default function CharacterManagePage() {

公开后其他用户可以在角色广场看到并使用此角色卡

-
+
+
@@ -572,7 +617,14 @@ export default function CharacterManagePage() { worldBookEntries.map((entry, index) => (
- 条目 #{index + 1} +
+ + {entry.comment ? entry.comment : `条目 #${index + 1}`} + + {entry.comment && ( + #{index + 1} + )} +