464 lines
14 KiB
Go
464 lines
14 KiB
Go
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
|
||
|
||
// markdownOnly=true 脚本只在前端显示层执行,后端不应用
|
||
db := global.GVA_DB.Where("user_id = ? AND placement = ? AND disabled = ? AND markdown_only = ?", userID, placement, false, 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
|
||
}
|