199 lines
4.7 KiB
Go
199 lines
4.7 KiB
Go
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()
|
|
}
|