From f4ff763b789b96f4c77b8076f7cad3cd8c471951 Mon Sep 17 00:00:00 2001 From: Eg <1711788888@qq.com> Date: Tue, 3 Mar 2026 20:33:46 +0800 Subject: [PATCH] =?UTF-8?q?:art:=20=E4=BC=98=E5=8C=96=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E6=AD=A3=E5=88=99=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api/v1/app/ai_preset.go | 10 +- server/config.docker.yaml | 10 +- server/config.yaml | 2 +- server/model/app/response/ai_proxy.go | 18 + server/service/app/ai_model.go | 3 +- server/service/app/ai_preset.go | 23 +- server/service/app/ai_preset_injector.go | 79 ++- server/service/app/ai_proxy.go | 14 +- web/src/pathInfo.json | 1 + .../view/ai/preset/components/RegexEditor.vue | 526 ++++++++++++++++++ web/src/view/ai/preset/index.vue | 16 +- web/vite.config.js | 5 +- 12 files changed, 678 insertions(+), 29 deletions(-) create mode 100644 web/src/view/ai/preset/components/RegexEditor.vue diff --git a/server/api/v1/app/ai_preset.go b/server/api/v1/app/ai_preset.go index 5f44fb2..7467784 100644 --- a/server/api/v1/app/ai_preset.go +++ b/server/api/v1/app/ai_preset.go @@ -170,7 +170,7 @@ func (a *AiPresetApi) ImportAiPreset(c *gin.Context) { return } - preset, err := aiPresetService.ParseImportedPreset(rawData) + preset, err := aiPresetService.ParseImportedPreset(rawData, "") if err != nil { response.FailWithMessage("解析预设失败:"+err.Error(), c) return @@ -222,7 +222,13 @@ func (a *AiPresetApi) ImportAiPresetFile(c *gin.Context) { return } - preset, err := aiPresetService.ParseImportedPreset(rawData) + // 从文件名提取预设名称(去掉 .json 后缀) + fileName := file.Filename + if strings.HasSuffix(fileName, ".json") { + fileName = fileName[:len(fileName)-5] + } + + preset, err := aiPresetService.ParseImportedPreset(rawData, fileName) if err != nil { response.FailWithMessage("解析预设失败:"+err.Error(), c) return diff --git a/server/config.docker.yaml b/server/config.docker.yaml index 873ed67..5334858 100644 --- a/server/config.docker.yaml +++ b/server/config.docker.yaml @@ -72,11 +72,11 @@ email: # system configuration system: - env: local # 修改为public可以关闭路由日志输出 - addr: 8888 - db-type: mysql + env: public # 修改为public可以关闭路由日志输出 + addr: 8989 + db-type: pgsql oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置 - use-redis: false # 使用redis + use-redis: true # 使用redis use-mongo: false # 使用mongo use-multipoint: false # IP限制次数 一个小时15000次 @@ -84,7 +84,7 @@ system: # IP限制一个小时 iplimit-time: 3600 # 路由全局前缀 - router-prefix: "" + router-prefix: /api # 严格角色模式 打开后权限将会存在上下级关系 use-strict-auth: false diff --git a/server/config.yaml b/server/config.yaml index bcc0124..d52764b 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -238,7 +238,7 @@ sqlite: system: db-type: pgsql oss-type: aliyun-oss - router-prefix: "" + router-prefix: "/api" addr: 8989 iplimit-count: 15000 iplimit-time: 3600 diff --git a/server/model/app/response/ai_proxy.go b/server/model/app/response/ai_proxy.go index 0223611..cbc9035 100644 --- a/server/model/app/response/ai_proxy.go +++ b/server/model/app/response/ai_proxy.go @@ -8,6 +8,8 @@ type ChatCompletionResponse struct { Model string `json:"model"` Choices []ChatCompletionChoice `json:"choices"` Usage ChatCompletionUsage `json:"usage"` + // 扩展字段:正则脚本执行日志 + RegexLogs *RegexExecutionLogs `json:"regex_logs,omitempty"` } type ChatCompletionChoice struct { @@ -46,3 +48,19 @@ type ChatMessageDelta struct { Role string `json:"role,omitempty"` Content string `json:"content,omitempty"` } + +// RegexExecutionLogs 正则脚本执行日志 +type RegexExecutionLogs struct { + InputScripts []RegexScriptLog `json:"input_scripts,omitempty"` // 输入前执行的脚本 + OutputScripts []RegexScriptLog `json:"output_scripts,omitempty"` // 输出后执行的脚本 + TotalMatches int `json:"total_matches"` // 总匹配次数 +} + +// RegexScriptLog 单个正则脚本的执行日志 +type RegexScriptLog struct { + ScriptName string `json:"script_name"` // 脚本名称 + ScriptID string `json:"script_id"` // 脚本ID + Executed bool `json:"executed"` // 是否执行 + MatchCount int `json:"match_count"` // 匹配次数 + ErrorMessage string `json:"error_message,omitempty"` // 错误信息 +} diff --git a/server/service/app/ai_model.go b/server/service/app/ai_model.go index aff43a8..18eb171 100644 --- a/server/service/app/ai_model.go +++ b/server/service/app/ai_model.go @@ -26,7 +26,8 @@ func (s *AiModelService) DeleteAiModel(id uint, userID uint) error { // UpdateAiModel 更新模型 func (s *AiModelService) UpdateAiModel(model *app.AiModel, userID uint) error { - return global.GVA_DB.Where("user_id = ?", userID).Updates(model).Error + // 使用 Select("*") 来更新所有字段,包括零值字段(如 enabled=false) + return global.GVA_DB.Model(&app.AiModel{}).Where("id = ? AND user_id = ?", model.ID, userID).Select("*").Updates(model).Error } // GetAiModel 查询模型 diff --git a/server/service/app/ai_preset.go b/server/service/app/ai_preset.go index b9e897e..931beae 100644 --- a/server/service/app/ai_preset.go +++ b/server/service/app/ai_preset.go @@ -46,7 +46,8 @@ func (s *AiPresetService) GetAiPresetList(info request.PageInfo, userID uint) (l } // ParseImportedPreset 解析导入的预设,支持 SillyTavern 格式 -func (s *AiPresetService) ParseImportedPreset(rawData map[string]interface{}) (*app.AiPreset, error) { +// defaultName: 当 JSON 中没有名称时使用的默认名称(通常是文件名) +func (s *AiPresetService) ParseImportedPreset(rawData map[string]interface{}, defaultName string) (*app.AiPreset, error) { preset := &app.AiPreset{ Enabled: true, } @@ -58,6 +59,9 @@ func (s *AiPresetService) ParseImportedPreset(rawData map[string]interface{}) (* preset.Name = name } else if name, ok := rawData["presetName"].(string); ok && name != "" { preset.Name = name + } else if defaultName != "" { + // 使用默认名称(文件名) + preset.Name = defaultName } else { return nil, fmt.Errorf("预设名称不能为空") } @@ -110,7 +114,8 @@ func (s *AiPresetService) ParseImportedPreset(rawData map[string]interface{}) (* json.Unmarshal(orderData, &preset.PromptOrder) } - // 处理正则脚本 + // 处理正则脚本 - 支持两种格式 + // 格式1: 顶层 regex_scripts if regexScripts, ok := rawData["regex_scripts"].([]interface{}); ok { scriptsData, _ := json.Marshal(regexScripts) json.Unmarshal(scriptsData, &preset.RegexScripts) @@ -118,6 +123,20 @@ func (s *AiPresetService) ParseImportedPreset(rawData map[string]interface{}) (* // 处理扩展配置 if extensions, ok := rawData["extensions"].(map[string]interface{}); ok { + // 格式2: extensions.regex_scripts (SillyTavern 格式) + if regexScripts, ok := extensions["regex_scripts"].([]interface{}); ok { + scriptsData, _ := json.Marshal(regexScripts) + // 同时填充到 RegexScripts 和 Extensions.RegexBinding.Regexes + json.Unmarshal(scriptsData, &preset.RegexScripts) + + // 确保 Extensions.RegexBinding 被初始化 + if preset.Extensions.RegexBinding == nil { + preset.Extensions.RegexBinding = &app.RegexBindingConfig{} + } + json.Unmarshal(scriptsData, &preset.Extensions.RegexBinding.Regexes) + } + + // 解析其他扩展配置 extData, _ := json.Marshal(extensions) json.Unmarshal(extData, &preset.Extensions) } diff --git a/server/service/app/ai_preset_injector.go b/server/service/app/ai_preset_injector.go index b21fe3f..5a3e554 100644 --- a/server/service/app/ai_preset_injector.go +++ b/server/service/app/ai_preset_injector.go @@ -8,16 +8,29 @@ import ( "git.echol.cn/loser/ai_proxy/server/model/app" "git.echol.cn/loser/ai_proxy/server/model/app/request" + "git.echol.cn/loser/ai_proxy/server/model/app/response" ) // PresetInjector 预设注入器 type PresetInjector struct { - preset *app.AiPreset + preset *app.AiPreset + regexLogs *response.RegexExecutionLogs } // NewPresetInjector 创建预设注入器 func NewPresetInjector(preset *app.AiPreset) *PresetInjector { - return &PresetInjector{preset: preset} + return &PresetInjector{ + preset: preset, + regexLogs: &response.RegexExecutionLogs{ + InputScripts: []response.RegexScriptLog{}, + OutputScripts: []response.RegexScriptLog{}, + }, + } +} + +// GetRegexLogs 获取正则脚本执行日志 +func (p *PresetInjector) GetRegexLogs() *response.RegexExecutionLogs { + return p.regexLogs } // InjectMessages 注入预设到消息列表 @@ -196,8 +209,8 @@ func (p *PresetInjector) applyRegexScripts(messages []request.ChatMessage, place // 检查 placement hasPlacement := false - for _, p := range script.Placement { - if p == placement { + for _, pl := range script.Placement { + if pl == placement { hasPlacement = true break } @@ -206,15 +219,34 @@ func (p *PresetInjector) applyRegexScripts(messages []request.ChatMessage, place continue } - // 应用正则替换 - messages = p.applyRegexScript(messages, script) + // 应用正则替换并记录日志 + var matchCount int + var err error + messages, matchCount, err = p.applyRegexScriptWithLog(messages, script) + + // 记录执行日志 + log := response.RegexScriptLog{ + ScriptName: script.ScriptName, + ScriptID: script.ID, + Executed: true, + MatchCount: matchCount, + } + if err != nil { + log.ErrorMessage = err.Error() + } + + // 根据 placement 添加到对应的日志列表 + if placement == 1 { + p.regexLogs.InputScripts = append(p.regexLogs.InputScripts, log) + } + p.regexLogs.TotalMatches += matchCount } return messages } -// applyRegexScript 应用单个正则脚本 -func (p *PresetInjector) applyRegexScript(messages []request.ChatMessage, script app.RegexScript) []request.ChatMessage { +// applyRegexScriptWithLog 应用单个正则脚本并返回匹配次数 +func (p *PresetInjector) applyRegexScriptWithLog(messages []request.ChatMessage, script app.RegexScript) ([]request.ChatMessage, int, error) { // 解析正则表达式 pattern := script.FindRegex // 移除正则标志(如 /pattern/g) @@ -229,19 +261,25 @@ func (p *PresetInjector) applyRegexScript(messages []request.ChatMessage, script re, err := regexp.Compile(pattern) if err != nil { - return messages + return messages, 0, fmt.Errorf("正则编译失败: %v", err) } + matchCount := 0 // 对每条消息应用替换 for i := range messages { if script.PromptOnly && messages[i].Role != "user" { continue } + // 统计匹配次数 + matches := re.FindAllString(messages[i].Content, -1) + matchCount += len(matches) + + // 执行替换 messages[i].Content = re.ReplaceAllString(messages[i].Content, script.ReplaceString) } - return messages + return messages, matchCount, nil } // ProcessResponse 处理AI响应(应用输出后的正则) @@ -280,10 +318,31 @@ func (p *PresetInjector) ProcessResponse(content string) string { re, err := regexp.Compile(pattern) if err != nil { + // 记录错误日志 + p.regexLogs.OutputScripts = append(p.regexLogs.OutputScripts, response.RegexScriptLog{ + ScriptName: script.ScriptName, + ScriptID: script.ID, + Executed: false, + ErrorMessage: fmt.Sprintf("正则编译失败: %v", err), + }) continue } + // 统计匹配次数 + matches := re.FindAllString(content, -1) + matchCount := len(matches) + + // 执行替换 content = re.ReplaceAllString(content, script.ReplaceString) + + // 记录执行日志 + p.regexLogs.OutputScripts = append(p.regexLogs.OutputScripts, response.RegexScriptLog{ + ScriptName: script.ScriptName, + ScriptID: script.ID, + Executed: true, + MatchCount: matchCount, + }) + p.regexLogs.TotalMatches += matchCount } return content diff --git a/server/service/app/ai_proxy.go b/server/service/app/ai_proxy.go index 4b17d0d..226580d 100644 --- a/server/service/app/ai_proxy.go +++ b/server/service/app/ai_proxy.go @@ -34,8 +34,9 @@ func (s *AiProxyService) ProcessChatCompletion(ctx context.Context, req *request } // 2. 注入预设 + var injector *PresetInjector if preset != nil { - injector := NewPresetInjector(preset) + injector = NewPresetInjector(preset) req.Messages = injector.InjectMessages(req.Messages) injector.ApplyPresetParameters(req) } @@ -46,10 +47,15 @@ func (s *AiProxyService) ProcessChatCompletion(ctx context.Context, req *request return nil, err } - // 4. 处理响应 - if preset != nil && len(resp.Choices) > 0 { - injector := NewPresetInjector(preset) + // 4. 处理响应并收集正则日志 + if preset != nil && injector != nil && len(resp.Choices) > 0 { resp.Choices[0].Message.Content = injector.ProcessResponse(resp.Choices[0].Message.Content) + + // 添加正则执行日志到响应 + regexLogs := injector.GetRegexLogs() + if regexLogs.TotalMatches > 0 || len(regexLogs.InputScripts) > 0 || len(regexLogs.OutputScripts) > 0 { + resp.RegexLogs = regexLogs + } } return resp, nil diff --git a/web/src/pathInfo.json b/web/src/pathInfo.json index 3feca9a..ce7ca97 100644 --- a/web/src/pathInfo.json +++ b/web/src/pathInfo.json @@ -4,6 +4,7 @@ "/src/view/ai/binding/index.vue": "Index", "/src/view/ai/model/index.vue": "Index", "/src/view/ai/preset/components/PromptEditor.vue": "PromptEditor", + "/src/view/ai/preset/components/RegexEditor.vue": "RegexEditor", "/src/view/ai/preset/index.vue": "Index", "/src/view/ai/provider/index.vue": "Index", "/src/view/dashboard/components/banner.vue": "Banner", diff --git a/web/src/view/ai/preset/components/RegexEditor.vue b/web/src/view/ai/preset/components/RegexEditor.vue new file mode 100644 index 0000000..2c574a7 --- /dev/null +++ b/web/src/view/ai/preset/components/RegexEditor.vue @@ -0,0 +1,526 @@ + + + + + diff --git a/web/src/view/ai/preset/index.vue b/web/src/view/ai/preset/index.vue index 12df9bb..7feace6 100644 --- a/web/src/view/ai/preset/index.vue +++ b/web/src/view/ai/preset/index.vue @@ -185,6 +185,13 @@ @save="handlePromptSave" /> + + + @@ -253,6 +260,7 @@ import { importAiPresetFile } from '@/api/aiPreset' import PromptEditor from './components/PromptEditor.vue' +import RegexEditor from './components/RegexEditor.vue' const page = ref(1) const pageSize = ref(10) @@ -271,6 +279,7 @@ const importTabActive = ref('file') const uploadRef = ref(null) const uploadFile = ref(null) const promptEditorVisible = ref(false) +const regexEditorVisible = ref(false) const formData = ref({ name: '', @@ -468,7 +477,12 @@ const handlePromptSave = (prompts) => { } const openRegexEditor = () => { - ElMessage.info('正则脚本编辑器功能开发中...') + regexEditorVisible.value = true +} + +const handleRegexSave = (regexScripts) => { + formData.value.regex_scripts = regexScripts + ElMessage.success('正则脚本已更新') } getTableData() diff --git a/web/vite.config.js b/web/vite.config.js index f887440..97ef6fa 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -76,9 +76,8 @@ export default ({ mode }) => { [process.env.VITE_BASE_API]: { // 需要代理的路径 例如 '/api' target: `${process.env.VITE_BASE_PATH}:${process.env.VITE_SERVER_PORT}/`, // 代理到 目标路径 - changeOrigin: true, - rewrite: (path) => - path.replace(new RegExp('^' + process.env.VITE_BASE_API), '') + changeOrigin: true + // 不需要 rewrite,保留 /api 前缀 }, "/plugin": { // 需要代理的路径 例如 '/api'