🎨 优化预设正则解析

This commit is contained in:
2026-03-03 20:33:46 +08:00
parent f8306be916
commit f4ff763b78
12 changed files with 678 additions and 29 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"` // 错误信息
}

View File

@@ -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 查询模型

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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