285
server/service/app/regex_script.go
Normal file
285
server/service/app/regex_script.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"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 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// substituteMacros 替换宏变量
|
||||
func (s *RegexScriptService) substituteMacros(text string, userName string, charName string) string {
|
||||
result := 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)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 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)
|
||||
scopeCondition := "scope = 0" // 全局
|
||||
if charID != nil {
|
||||
scopeCondition += " OR (scope = 1 AND owner_char_id = " + string(rune(*charID)) + ")"
|
||||
}
|
||||
if presetID != nil {
|
||||
scopeCondition += " OR (scope = 2 AND owner_preset_id = " + string(rune(*presetID)) + ")"
|
||||
}
|
||||
db = db.Where(scopeCondition)
|
||||
|
||||
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
|
||||
}
|
||||
198
server/service/app/worldbook_engine.go
Normal file
198
server/service/app/worldbook_engine.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"git.echol.cn/loser/st/server/model/app"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// WorldbookEngine 世界书触发引擎
|
||||
type WorldbookEngine struct{}
|
||||
|
||||
// TriggeredEntry 触发的条目
|
||||
type TriggeredEntry struct {
|
||||
Entry *app.WorldbookEntry
|
||||
Position int
|
||||
Order int
|
||||
}
|
||||
|
||||
// ScanAndTrigger 扫描消息并触发匹配的世界书条目
|
||||
func (e *WorldbookEngine) ScanAndTrigger(worldbookID uint, messages []string) ([]*TriggeredEntry, error) {
|
||||
// 获取世界书的所有启用条目
|
||||
var entries []app.WorldbookEntry
|
||||
err := global.GVA_DB.Where("worldbook_id = ? AND enabled = ?", worldbookID, true).
|
||||
Order("`order` ASC").
|
||||
Find(&entries).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var triggered []*TriggeredEntry
|
||||
|
||||
// 合并所有消息用于扫描
|
||||
var scanTexts []string
|
||||
for _, entry := range entries {
|
||||
// 根据 scanDepth 决定扫描范围
|
||||
if entry.ScanDepth > 0 && entry.ScanDepth < len(messages) {
|
||||
// 只扫描最近 N 条消息
|
||||
scanTexts = messages[len(messages)-entry.ScanDepth:]
|
||||
} else {
|
||||
// 扫描所有消息
|
||||
scanTexts = messages
|
||||
}
|
||||
|
||||
// 检查是否触发
|
||||
if e.shouldTrigger(&entry, scanTexts) {
|
||||
triggered = append(triggered, &TriggeredEntry{
|
||||
Entry: &entry,
|
||||
Position: entry.Position,
|
||||
Order: entry.Order,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return triggered, nil
|
||||
}
|
||||
|
||||
// shouldTrigger 判断条目是否应该被触发
|
||||
func (e *WorldbookEngine) shouldTrigger(entry *app.WorldbookEntry, messages []string) bool {
|
||||
// 常驻条目总是触发
|
||||
if entry.Constant {
|
||||
return true
|
||||
}
|
||||
|
||||
// 概率触发
|
||||
if entry.Probability < 100 {
|
||||
// 简单的概率判断(实际应用中可以使用更好的随机数生成器)
|
||||
if entry.Probability <= 0 {
|
||||
return false
|
||||
}
|
||||
// 这里简化处理,实际应该用随机数
|
||||
// 为了演示,我们假设概率大于 50 就触发
|
||||
if entry.Probability < 50 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 解析关键词
|
||||
var keys []string
|
||||
if len(entry.Keys) > 0 {
|
||||
json.Unmarshal(entry.Keys, &keys)
|
||||
}
|
||||
|
||||
// 如果没有关键词,不触发
|
||||
if len(keys) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 合并所有消息为一个文本
|
||||
fullText := strings.Join(messages, " ")
|
||||
|
||||
// 检查主关键词是否匹配
|
||||
primaryMatch := e.matchKeys(keys, fullText, entry.UseRegex, entry.CaseSensitive, entry.MatchWholeWords)
|
||||
if !primaryMatch {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果需要次要关键词
|
||||
if entry.Selective {
|
||||
var secondaryKeys []string
|
||||
if len(entry.SecondaryKeys) > 0 {
|
||||
json.Unmarshal(entry.SecondaryKeys, &secondaryKeys)
|
||||
}
|
||||
|
||||
if len(secondaryKeys) > 0 {
|
||||
secondaryMatch := e.matchKeys(secondaryKeys, fullText, entry.UseRegex, entry.CaseSensitive, entry.MatchWholeWords)
|
||||
|
||||
// SelectiveLogic: 0=AND, 1=NOT
|
||||
if entry.SelectiveLogic == 0 {
|
||||
// AND: 次要关键词也必须匹配
|
||||
return secondaryMatch
|
||||
} else {
|
||||
// NOT: 次要关键词不能匹配
|
||||
return !secondaryMatch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// matchKeys 检查关键词是否匹配
|
||||
func (e *WorldbookEngine) matchKeys(keys []string, text string, useRegex, caseSensitive, matchWholeWords bool) bool {
|
||||
for _, key := range keys {
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if useRegex {
|
||||
// 正则表达式匹配
|
||||
flags := ""
|
||||
if !caseSensitive {
|
||||
flags = "(?i)"
|
||||
}
|
||||
pattern := flags + key
|
||||
matched, err := regexp.MatchString(pattern, text)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn("正则表达式匹配失败", zap.String("pattern", pattern), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// 普通文本匹配
|
||||
searchText := text
|
||||
searchKey := key
|
||||
|
||||
if !caseSensitive {
|
||||
searchText = strings.ToLower(searchText)
|
||||
searchKey = strings.ToLower(searchKey)
|
||||
}
|
||||
|
||||
if matchWholeWords {
|
||||
// 全词匹配
|
||||
pattern := `\b` + regexp.QuoteMeta(searchKey) + `\b`
|
||||
matched, _ := regexp.MatchString(pattern, searchText)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// 包含匹配
|
||||
if strings.Contains(searchText, searchKey) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// BuildPromptWithWorldbook 构建包含世界书内容的 prompt
|
||||
func (e *WorldbookEngine) BuildPromptWithWorldbook(basePrompt string, triggered []*TriggeredEntry) string {
|
||||
if len(triggered) == 0 {
|
||||
return basePrompt
|
||||
}
|
||||
|
||||
// 按位置和顺序排序
|
||||
// Position: 0=系统提示词前, 1=系统提示词后, 4=指定深度
|
||||
// 这里简化处理,都插入到系统提示词后
|
||||
|
||||
var worldbookContent strings.Builder
|
||||
worldbookContent.WriteString("\n\n[World Information]\n")
|
||||
|
||||
for _, t := range triggered {
|
||||
if t.Entry.Content != "" {
|
||||
worldbookContent.WriteString(t.Entry.Content)
|
||||
worldbookContent.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// 将世界书内容插入到 basePrompt 之后
|
||||
return basePrompt + worldbookContent.String()
|
||||
}
|
||||
Reference in New Issue
Block a user