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