🎨 添加文档 && 新增正则脚本模块

Signed-off-by: Echo <1711788888@qq.com>
This commit is contained in:
2026-02-28 15:11:12 +08:00
parent a6234e7bb0
commit 3bfa59cf3e
17 changed files with 26980 additions and 1 deletions

View File

@@ -0,0 +1,167 @@
package app
import (
"net/http"
"strconv"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/app/request"
"git.echol.cn/loser/st/server/model/common"
commonResponse "git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type RegexScriptApi struct{}
// CreateRegexScript
// @Tags AppRegexScript
// @Summary 创建正则脚本
// @Router /app/regex [post]
// @Security ApiKeyAuth
func (a *RegexScriptApi) CreateRegexScript(c *gin.Context) {
userID := common.GetAppUserID(c)
var req request.CreateRegexScriptRequest
if err := c.ShouldBindJSON(&req); err != nil {
commonResponse.FailWithMessage(err.Error(), c)
return
}
resp, err := service.ServiceGroupApp.AppServiceGroup.RegexScriptService.CreateRegexScript(userID, &req)
if err != nil {
global.GVA_LOG.Error("创建正则脚本失败", zap.Error(err))
commonResponse.FailWithMessage(err.Error(), c)
return
}
commonResponse.OkWithData(resp, c)
}
// GetRegexScriptList
// @Tags AppRegexScript
// @Summary 获取正则脚本列表
// @Router /app/regex [get]
// @Security ApiKeyAuth
func (a *RegexScriptApi) GetRegexScriptList(c *gin.Context) {
userID := common.GetAppUserID(c)
var req request.GetRegexScriptListRequest
req.Page, _ = strconv.Atoi(c.DefaultQuery("page", "1"))
req.PageSize, _ = strconv.Atoi(c.DefaultQuery("pageSize", "20"))
req.Keyword = c.Query("keyword")
if scope := c.Query("scope"); scope != "" {
scopeInt, _ := strconv.Atoi(scope)
req.Scope = &scopeInt
}
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 20
}
list, total, err := service.ServiceGroupApp.AppServiceGroup.RegexScriptService.GetRegexScriptList(userID, &req)
if err != nil {
global.GVA_LOG.Error("获取正则脚本列表失败", zap.Error(err))
commonResponse.FailWithMessage(err.Error(), c)
return
}
commonResponse.OkWithDetailed(commonResponse.PageResult{
List: list,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, "获取成功", c)
}
// GetRegexScriptByID
// @Tags AppRegexScript
// @Summary 获取正则脚本详情
// @Router /app/regex/:id [get]
// @Security ApiKeyAuth
func (a *RegexScriptApi) GetRegexScriptByID(c *gin.Context) {
userID := common.GetAppUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
commonResponse.FailWithMessage("无效的脚本ID", c)
return
}
resp, err := service.ServiceGroupApp.AppServiceGroup.RegexScriptService.GetRegexScriptByID(userID, uint(id))
if err != nil {
global.GVA_LOG.Error("获取正则脚本失败", zap.Error(err))
commonResponse.FailWithMessage(err.Error(), c)
return
}
commonResponse.OkWithData(resp, c)
}
// UpdateRegexScript
// @Tags AppRegexScript
// @Summary 更新正则脚本
// @Router /app/regex/:id [put]
// @Security ApiKeyAuth
func (a *RegexScriptApi) UpdateRegexScript(c *gin.Context) {
userID := common.GetAppUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
commonResponse.FailWithMessage("无效的脚本ID", c)
return
}
var req request.UpdateRegexScriptRequest
if err := c.ShouldBindJSON(&req); err != nil {
commonResponse.FailWithMessage(err.Error(), c)
return
}
if err := service.ServiceGroupApp.AppServiceGroup.RegexScriptService.UpdateRegexScript(userID, uint(id), &req); err != nil {
global.GVA_LOG.Error("更新正则脚本失败", zap.Error(err))
commonResponse.FailWithMessage(err.Error(), c)
return
}
commonResponse.OkWithMessage("更新成功", c)
}
// DeleteRegexScript
// @Tags AppRegexScript
// @Summary 删除正则脚本
// @Router /app/regex/:id [delete]
// @Security ApiKeyAuth
func (a *RegexScriptApi) DeleteRegexScript(c *gin.Context) {
userID := common.GetAppUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
commonResponse.FailWithMessage("无效的脚本ID", c)
return
}
if err := service.ServiceGroupApp.AppServiceGroup.RegexScriptService.DeleteRegexScript(userID, uint(id)); err != nil {
global.GVA_LOG.Error("删除正则脚本失败", zap.Error(err))
commonResponse.FailWithMessage(err.Error(), c)
return
}
commonResponse.OkWithMessage("删除成功", c)
}
// TestRegexScript
// @Tags AppRegexScript
// @Summary 测试正则脚本
// @Router /app/regex/:id/test [post]
// @Security ApiKeyAuth
func (a *RegexScriptApi) TestRegexScript(c *gin.Context) {
userID := common.GetAppUserID(c)
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
commonResponse.FailWithMessage("无效的脚本ID", c)
return
}
var req request.TestRegexScriptRequest
if err := c.ShouldBindJSON(&req); err != nil {
commonResponse.FailWithMessage(err.Error(), c)
return
}
resp, err := service.ServiceGroupApp.AppServiceGroup.RegexScriptService.TestRegexScript(userID, uint(id), req.TestString)
if err != nil {
global.GVA_LOG.Error("测试正则脚本失败", zap.Error(err))
commonResponse.FailWithMessage(err.Error(), c)
return
}
c.JSON(http.StatusOK, resp)
}

9314
server/docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

9286
server/docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

5677
server/docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
package app
import (
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
)
// RegexScript 正则脚本模型(完全兼容 SillyTavern 格式)
type RegexScript struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
UserID uint `gorm:"index;not null" json:"userId"`
Name string `gorm:"type:varchar(100);not null" json:"name"` // 脚本名称
// 正则表达式
FindRegex string `gorm:"type:text;not null" json:"findRegex"` // 查找的正则表达式
ReplaceWith string `gorm:"type:text" json:"replaceWith"` // 替换的内容
TrimStrings datatypes.JSON `gorm:"type:jsonb" json:"trimStrings"` // 要修剪的字符串列表 []string
// 执行阶段
// 0=输入(input), 1=输出(output), 2=世界书(world_info), 3=推理(display)
Placement int `gorm:"default:1" json:"placement"`
// 执行选项
Disabled bool `gorm:"default:false" json:"disabled"` // 是否禁用
MarkdownOnly bool `gorm:"default:false" json:"markdownOnly"` // 仅在 Markdown 模式下执行
RunOnEdit bool `gorm:"default:false" json:"runOnEdit"` // 编辑消息时执行
PromptOnly bool `gorm:"default:false" json:"promptOnly"` // 仅在 prompt 中执行
// 宏替换
SubstituteRegex bool `gorm:"default:true" json:"substituteRegex"` // 是否替换宏变量 {{user}}/{{char}}
// 深度控制
MinDepth *int `gorm:"type:int" json:"minDepth"` // 最小深度null 表示不限制)
MaxDepth *int `gorm:"type:int" json:"maxDepth"` // 最大深度null 表示不限制)
// 作用域
// 0=全局(global), 1=角色(character), 2=预设(preset)
Scope int `gorm:"default:0" json:"scope"`
OwnerCharID *uint `gorm:"type:int" json:"ownerCharId"` // 角色IDscope=1时有效
OwnerPresetID *uint `gorm:"type:int" json:"ownerPresetId"` // 预设IDscope=2时有效
// 执行顺序
Order int `gorm:"default:100" json:"order"` // 执行顺序,数字越小越先执行
// 扩展字段
Extensions datatypes.JSON `gorm:"type:jsonb" json:"extensions"`
}
func (RegexScript) TableName() string {
return "regex_scripts"
}

View File

@@ -0,0 +1,54 @@
package request
// CreateRegexScriptRequest 创建正则脚本请求
type CreateRegexScriptRequest struct {
Name string `json:"name" binding:"required,max=100"`
FindRegex string `json:"findRegex" binding:"required"`
ReplaceWith string `json:"replaceWith"`
TrimStrings []string `json:"trimStrings"`
Placement int `json:"placement"`
Disabled bool `json:"disabled"`
MarkdownOnly bool `json:"markdownOnly"`
RunOnEdit bool `json:"runOnEdit"`
PromptOnly bool `json:"promptOnly"`
SubstituteRegex bool `json:"substituteRegex"`
MinDepth *int `json:"minDepth"`
MaxDepth *int `json:"maxDepth"`
Scope int `json:"scope"`
OwnerCharID *uint `json:"ownerCharId"`
OwnerPresetID *uint `json:"ownerPresetId"`
Order int `json:"order"`
}
// UpdateRegexScriptRequest 更新正则脚本请求
type UpdateRegexScriptRequest struct {
Name *string `json:"name" binding:"omitempty,max=100"`
FindRegex *string `json:"findRegex"`
ReplaceWith *string `json:"replaceWith"`
TrimStrings []string `json:"trimStrings"`
Placement *int `json:"placement"`
Disabled *bool `json:"disabled"`
MarkdownOnly *bool `json:"markdownOnly"`
RunOnEdit *bool `json:"runOnEdit"`
PromptOnly *bool `json:"promptOnly"`
SubstituteRegex *bool `json:"substituteRegex"`
MinDepth *int `json:"minDepth"`
MaxDepth *int `json:"maxDepth"`
Scope *int `json:"scope"`
OwnerCharID *uint `json:"ownerCharId"`
OwnerPresetID *uint `json:"ownerPresetId"`
Order *int `json:"order"`
}
// GetRegexScriptListRequest 获取正则脚本列表请求
type GetRegexScriptListRequest struct {
Page int `json:"page"`
PageSize int `json:"pageSize"`
Keyword string `json:"keyword"`
Scope *int `json:"scope"`
}
// TestRegexScriptRequest 测试正则脚本请求
type TestRegexScriptRequest struct {
TestString string `json:"testString" binding:"required"`
}

View File

@@ -0,0 +1,78 @@
package response
import (
"encoding/json"
"time"
"git.echol.cn/loser/st/server/model/app"
)
// RegexScriptResponse 正则脚本响应
type RegexScriptResponse struct {
ID uint `json:"id"`
UserID uint `json:"userId"`
Name string `json:"name"`
FindRegex string `json:"findRegex"`
ReplaceWith string `json:"replaceWith"`
TrimStrings []string `json:"trimStrings"`
Placement int `json:"placement"`
Disabled bool `json:"disabled"`
MarkdownOnly bool `json:"markdownOnly"`
RunOnEdit bool `json:"runOnEdit"`
PromptOnly bool `json:"promptOnly"`
SubstituteRegex bool `json:"substituteRegex"`
MinDepth *int `json:"minDepth"`
MaxDepth *int `json:"maxDepth"`
Scope int `json:"scope"`
OwnerCharID *uint `json:"ownerCharId"`
OwnerPresetID *uint `json:"ownerPresetId"`
Order int `json:"order"`
Extensions map[string]interface{} `json:"extensions"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// TestRegexScriptResponse 测试正则脚本响应
type TestRegexScriptResponse struct {
Original string `json:"original"`
Result string `json:"result"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// ToRegexScriptResponse 转换为正则脚本响应结构
func ToRegexScriptResponse(script *app.RegexScript) RegexScriptResponse {
var trimStrings []string
if len(script.TrimStrings) > 0 {
json.Unmarshal(script.TrimStrings, &trimStrings)
}
var extensions map[string]interface{}
if len(script.Extensions) > 0 {
json.Unmarshal(script.Extensions, &extensions)
}
return RegexScriptResponse{
ID: script.ID,
UserID: script.UserID,
Name: script.Name,
FindRegex: script.FindRegex,
ReplaceWith: script.ReplaceWith,
TrimStrings: trimStrings,
Placement: script.Placement,
Disabled: script.Disabled,
MarkdownOnly: script.MarkdownOnly,
RunOnEdit: script.RunOnEdit,
PromptOnly: script.PromptOnly,
SubstituteRegex: script.SubstituteRegex,
MinDepth: script.MinDepth,
MaxDepth: script.MaxDepth,
Scope: script.Scope,
OwnerCharID: script.OwnerCharID,
OwnerPresetID: script.OwnerPresetID,
Order: script.Order,
Extensions: extensions,
CreatedAt: script.CreatedAt,
UpdatedAt: script.UpdatedAt,
}
}

View File

@@ -0,0 +1,24 @@
package app
import (
v1 "git.echol.cn/loser/st/server/api/v1"
"git.echol.cn/loser/st/server/middleware"
"github.com/gin-gonic/gin"
)
type RegexScriptRouter struct{}
// InitRegexScriptRouter 初始化正则脚本路由
func (r *RegexScriptRouter) InitRegexScriptRouter(Router *gin.RouterGroup) {
regexRouter := Router.Group("regex").Use(middleware.AppJWTAuth())
regexApi := v1.ApiGroupApp.AppApiGroup.RegexScriptApi
{
regexRouter.POST("", regexApi.CreateRegexScript) // 创建正则脚本
regexRouter.GET("", regexApi.GetRegexScriptList) // 获取正则脚本列表
regexRouter.GET(":id", regexApi.GetRegexScriptByID) // 获取正则脚本详情
regexRouter.PUT(":id", regexApi.UpdateRegexScript) // 更新正则脚本
regexRouter.DELETE(":id", regexApi.DeleteRegexScript) // 删除正则脚本
regexRouter.POST(":id/test", regexApi.TestRegexScript) // 测试正则脚本
}
}

View 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
}

View 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()
}