Files
st-react/server/service/app/regex_script.go
Echo 4cecfd6589 🎨 1.优化前端渲染功能(html和对话消息格式)
2.优化流式传输,新增流式渲染功能
3.优化正则处理逻辑
4.新增context budget管理系统
5.优化对话消息失败处理逻辑
6.新增前端卡功能(待完整测试)
2026-03-13 15:58:33 +08:00

463 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package app
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"regexp"
"strconv"
"strings"
"time"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/app"
"git.echol.cn/loser/st/server/model/app/request"
"git.echol.cn/loser/st/server/model/app/response"
"go.uber.org/zap"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type RegexScriptService struct{}
// CreateRegexScript 创建正则脚本
func (s *RegexScriptService) CreateRegexScript(userID uint, req *request.CreateRegexScriptRequest) (*response.RegexScriptResponse, error) {
trimStringsJSON, _ := json.Marshal(req.TrimStrings)
script := &app.RegexScript{
UserID: userID,
Name: req.Name,
FindRegex: req.FindRegex,
ReplaceWith: req.ReplaceWith,
TrimStrings: datatypes.JSON(trimStringsJSON),
Placement: req.Placement,
Disabled: req.Disabled,
MarkdownOnly: req.MarkdownOnly,
RunOnEdit: req.RunOnEdit,
PromptOnly: req.PromptOnly,
SubstituteRegex: req.SubstituteRegex,
MinDepth: req.MinDepth,
MaxDepth: req.MaxDepth,
Scope: req.Scope,
OwnerCharID: req.OwnerCharID,
OwnerPresetID: req.OwnerPresetID,
Order: req.Order,
}
if err := global.GVA_DB.Create(script).Error; err != nil {
global.GVA_LOG.Error("创建正则脚本失败", zap.Error(err))
return nil, err
}
resp := response.ToRegexScriptResponse(script)
return &resp, nil
}
// GetRegexScriptList 获取正则脚本列表
func (s *RegexScriptService) GetRegexScriptList(userID uint, req *request.GetRegexScriptListRequest) ([]response.RegexScriptResponse, int64, error) {
var scripts []app.RegexScript
var total int64
db := global.GVA_DB.Model(&app.RegexScript{}).Where("user_id = ?", userID)
if req.Keyword != "" {
db = db.Where("name LIKE ?", "%"+req.Keyword+"%")
}
if req.Scope != nil {
db = db.Where("scope = ?", *req.Scope)
}
if req.OwnerCharID != nil {
db = db.Where("owner_char_id = ?", *req.OwnerCharID)
}
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (req.Page - 1) * req.PageSize
if err := db.Order("\"order\" ASC, created_at DESC").Offset(offset).Limit(req.PageSize).Find(&scripts).Error; err != nil {
global.GVA_LOG.Error("获取正则脚本列表失败", zap.Error(err))
return nil, 0, err
}
var list []response.RegexScriptResponse
for i := range scripts {
list = append(list, response.ToRegexScriptResponse(&scripts[i]))
}
return list, total, nil
}
// GetRegexScriptByID 获取正则脚本详情
func (s *RegexScriptService) GetRegexScriptByID(userID uint, id uint) (*response.RegexScriptResponse, error) {
var script app.RegexScript
if err := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).First(&script).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("正则脚本不存在或无权访问")
}
return nil, err
}
resp := response.ToRegexScriptResponse(&script)
return &resp, nil
}
// UpdateRegexScript 更新正则脚本
func (s *RegexScriptService) UpdateRegexScript(userID uint, id uint, req *request.UpdateRegexScriptRequest) error {
var script app.RegexScript
if err := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).First(&script).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("正则脚本不存在或无权修改")
}
return err
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.FindRegex != nil {
updates["find_regex"] = *req.FindRegex
}
if req.ReplaceWith != nil {
updates["replace_with"] = *req.ReplaceWith
}
if req.TrimStrings != nil {
trimStringsJSON, _ := json.Marshal(req.TrimStrings)
updates["trim_strings"] = datatypes.JSON(trimStringsJSON)
}
if req.Placement != nil {
updates["placement"] = *req.Placement
}
if req.Disabled != nil {
updates["disabled"] = *req.Disabled
}
if req.MarkdownOnly != nil {
updates["markdown_only"] = *req.MarkdownOnly
}
if req.RunOnEdit != nil {
updates["run_on_edit"] = *req.RunOnEdit
}
if req.PromptOnly != nil {
updates["prompt_only"] = *req.PromptOnly
}
if req.SubstituteRegex != nil {
updates["substitute_regex"] = *req.SubstituteRegex
}
if req.MinDepth != nil {
updates["min_depth"] = req.MinDepth
}
if req.MaxDepth != nil {
updates["max_depth"] = req.MaxDepth
}
if req.Scope != nil {
updates["scope"] = *req.Scope
}
if req.OwnerCharID != nil {
updates["owner_char_id"] = req.OwnerCharID
}
if req.OwnerPresetID != nil {
updates["owner_preset_id"] = req.OwnerPresetID
}
if req.Order != nil {
updates["order"] = *req.Order
}
return global.GVA_DB.Model(&script).Updates(updates).Error
}
// DeleteRegexScript 删除正则脚本
func (s *RegexScriptService) DeleteRegexScript(userID uint, id uint) error {
result := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).Delete(&app.RegexScript{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("正则脚本不存在或无权删除")
}
return nil
}
// TestRegexScript 测试正则脚本
func (s *RegexScriptService) TestRegexScript(userID uint, id uint, testString string) (*response.TestRegexScriptResponse, error) {
var script app.RegexScript
if err := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).First(&script).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("正则脚本不存在或无权访问")
}
return nil, err
}
result, err := s.ExecuteScript(&script, testString, "", "")
if err != nil {
return &response.TestRegexScriptResponse{
Original: testString,
Result: testString,
Success: false,
Error: err.Error(),
}, nil
}
return &response.TestRegexScriptResponse{
Original: testString,
Result: result,
Success: true,
}, nil
}
// ExecuteScript 执行正则脚本
func (s *RegexScriptService) ExecuteScript(script *app.RegexScript, text string, userName string, charName string) (string, error) {
if script.Disabled {
return text, nil
}
result := text
// 1. 宏替换
if script.SubstituteRegex {
result = s.substituteMacros(result, userName, charName)
}
// 2. 正则替换
if script.FindRegex != "" {
re, err := regexp.Compile(script.FindRegex)
if err != nil {
global.GVA_LOG.Warn("正则表达式编译失败", zap.String("pattern", script.FindRegex), zap.Error(err))
return text, err
}
// 特殊处理:如果正则匹配 <Status_block>,需要提取 YAML 并注入到 HTML 模板中
if strings.Contains(script.FindRegex, "Status_block") {
result = s.replaceStatusBlockWithHTML(result, script.ReplaceWith, re)
} else {
result = re.ReplaceAllString(result, script.ReplaceWith)
}
}
// 3. 修剪字符串
if len(script.TrimStrings) > 0 {
var trimStrings []string
json.Unmarshal(script.TrimStrings, &trimStrings)
for _, trimStr := range trimStrings {
result = strings.ReplaceAll(result, trimStr, "")
}
}
return result, nil
}
// replaceStatusBlockWithHTML 替换 <Status_block> 为 HTML 模板,并注入 YAML 数据
func (s *RegexScriptService) replaceStatusBlockWithHTML(text string, htmlTemplate string, statusBlockRegex *regexp.Regexp) string {
return statusBlockRegex.ReplaceAllStringFunc(text, func(match string) string {
// 提取 YAML 数据
yamlRegex := regexp.MustCompile(`<Status_block>\s*([\s\S]*?)\s*</Status_block>`)
yamlMatches := yamlRegex.FindStringSubmatch(match)
if len(yamlMatches) < 2 {
global.GVA_LOG.Warn("无法提取 Status_block 中的 YAML 数据")
return match
}
yamlData := strings.TrimSpace(yamlMatches[1])
// 在 HTML 模板中查找 <script id="yaml-data-source" type="text/yaml"></script>
// 并将 YAML 数据注入其中
injectedHTML := strings.Replace(
htmlTemplate,
`<script id="yaml-data-source" type="text/yaml"></script>`,
fmt.Sprintf(`<script id="yaml-data-source" type="text/yaml">%s</script>`, yamlData),
1,
)
global.GVA_LOG.Info(fmt.Sprintf("[正则脚本] 已将 Status_block YAML 数据注入到 HTML 模板YAML 长度: %d", len(yamlData)))
return injectedHTML
})
}
// substituteMacros 替换宏变量
func (s *RegexScriptService) substituteMacros(text string, userName string, charName string) string {
result := text
// 保存原始文本
result = strings.ReplaceAll(result, "{{original}}", text)
// 用户名变量
if userName != "" {
result = strings.ReplaceAll(result, "{{user}}", userName)
result = strings.ReplaceAll(result, "{{User}}", userName)
}
// 角色名变量
if charName != "" {
result = strings.ReplaceAll(result, "{{char}}", charName)
result = strings.ReplaceAll(result, "{{Char}}", charName)
}
// 时间变量
now := time.Now()
result = strings.ReplaceAll(result, "{{time}}", now.Format("15:04:05"))
result = strings.ReplaceAll(result, "{{date}}", now.Format("2006-01-02"))
result = strings.ReplaceAll(result, "{{datetime}}", now.Format("2006-01-02 15:04:05"))
result = strings.ReplaceAll(result, "{{timestamp}}", fmt.Sprintf("%d", now.Unix()))
result = strings.ReplaceAll(result, "{{time_12h}}", now.Format("03:04:05 PM"))
result = strings.ReplaceAll(result, "{{date_short}}", now.Format("01/02/06"))
result = strings.ReplaceAll(result, "{{weekday}}", now.Weekday().String())
result = strings.ReplaceAll(result, "{{month}}", now.Month().String())
result = strings.ReplaceAll(result, "{{year}}", fmt.Sprintf("%d", now.Year()))
// 随机数变量
result = regexp.MustCompile(`\{\{random:(\d+)-(\d+)\}\}`).ReplaceAllStringFunc(result, func(match string) string {
re := regexp.MustCompile(`\{\{random:(\d+)-(\d+)\}\}`)
matches := re.FindStringSubmatch(match)
if len(matches) == 3 {
min, _ := strconv.Atoi(matches[1])
max, _ := strconv.Atoi(matches[2])
if max > min {
return fmt.Sprintf("%d", rand.Intn(max-min+1)+min)
}
}
return match
})
// 简单随机数 {{random}}
result = regexp.MustCompile(`\{\{random\}\}`).ReplaceAllStringFunc(result, func(match string) string {
return fmt.Sprintf("%d", rand.Intn(100))
})
// 随机选择 {{pick:option1|option2|option3}}
result = regexp.MustCompile(`\{\{pick:([^}]+)\}\}`).ReplaceAllStringFunc(result, func(match string) string {
re := regexp.MustCompile(`\{\{pick:([^}]+)\}\}`)
matches := re.FindStringSubmatch(match)
if len(matches) == 2 {
options := strings.Split(matches[1], "|")
if len(options) > 0 {
return options[rand.Intn(len(options))]
}
}
return match
})
// 换行符变量
result = strings.ReplaceAll(result, "{{newline}}", "\n")
result = strings.ReplaceAll(result, "{{tab}}", "\t")
result = strings.ReplaceAll(result, "{{space}}", " ")
// 空值变量
result = strings.ReplaceAll(result, "{{empty}}", "")
return result
}
// ExtractSetVars 从文本中提取 {{setvar::key::value}} 并返回变量映射和清理后的文本
func (s *RegexScriptService) ExtractSetVars(text string) (map[string]string, string) {
vars := make(map[string]string)
// 匹配 {{setvar::key::value}}
re := regexp.MustCompile(`\{\{setvar::([^:]+)::([^}]*)\}\}`)
matches := re.FindAllStringSubmatch(text, -1)
for _, match := range matches {
if len(match) == 3 {
key := strings.TrimSpace(match[1])
value := match[2]
vars[key] = value
}
}
// 从文本中移除所有 {{setvar::}} 标记
cleanText := re.ReplaceAllString(text, "")
return vars, cleanText
}
// SubstituteGetVars 替换文本中的 {{getvar::key}} 为实际变量值
func (s *RegexScriptService) SubstituteGetVars(text string, variables map[string]string) string {
result := text
// 匹配 {{getvar::key}}
re := regexp.MustCompile(`\{\{getvar::([^}]+)\}\}`)
result = re.ReplaceAllStringFunc(result, func(match string) string {
matches := re.FindStringSubmatch(match)
if len(matches) == 2 {
key := strings.TrimSpace(matches[1])
if value, ok := variables[key]; ok {
return value
}
}
return "" // 如果变量不存在,返回空字符串
})
return result
}
// ExtractStatusBlock 提取 <Status_block> 中的 YAML 数据
func (s *RegexScriptService) ExtractStatusBlock(text string) (string, string) {
// 匹配 <Status_block>...</Status_block>
re := regexp.MustCompile(`(?s)<Status_block>\s*(.*?)\s*</Status_block>`)
matches := re.FindStringSubmatch(text)
if len(matches) == 2 {
statusBlock := strings.TrimSpace(matches[1])
cleanText := re.ReplaceAllString(text, "")
return statusBlock, cleanText
}
return "", text
}
// ExtractMaintext 提取 <maintext> 中的内容
func (s *RegexScriptService) ExtractMaintext(text string) (string, string) {
// 匹配 <maintext>...</maintext>
re := regexp.MustCompile(`(?s)<maintext>\s*(.*?)\s*</maintext>`)
matches := re.FindStringSubmatch(text)
if len(matches) == 2 {
maintext := strings.TrimSpace(matches[1])
cleanText := re.ReplaceAllString(text, "")
return maintext, cleanText
}
return "", text
}
// GetScriptsForPlacement 获取指定阶段的脚本
func (s *RegexScriptService) GetScriptsForPlacement(userID uint, placement int, charID *uint, presetID *uint) ([]app.RegexScript, error) {
var scripts []app.RegexScript
db := global.GVA_DB.Where("user_id = ? AND placement = ? AND disabled = ?", userID, placement, false)
// 作用域过滤:全局(0) 或 角色(1) 或 预设(2)
// 使用参数化查询避免 SQL 注入
if charID != nil && presetID != nil {
db = db.Where("scope = 0 OR (scope = 1 AND owner_char_id = ?) OR (scope = 2 AND owner_preset_id = ?)", *charID, *presetID)
} else if charID != nil {
db = db.Where("scope = 0 OR (scope = 1 AND owner_char_id = ?)", *charID)
} else if presetID != nil {
db = db.Where("scope = 0 OR (scope = 2 AND owner_preset_id = ?)", *presetID)
} else {
db = db.Where("scope = 0")
}
if err := db.Order("\"order\" ASC").Find(&scripts).Error; err != nil {
return nil, err
}
return scripts, nil
}
// ExecuteScripts 批量执行脚本
func (s *RegexScriptService) ExecuteScripts(scripts []app.RegexScript, text string, userName string, charName string) string {
result := text
for _, script := range scripts {
executed, err := s.ExecuteScript(&script, result, userName, charName)
if err != nil {
global.GVA_LOG.Warn("执行正则脚本失败", zap.String("name", script.Name), zap.Error(err))
continue
}
result = executed
}
return result
}