Files
st/server/service/app/world_info.go
2026-02-11 14:55:41 +08:00

714 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package app
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"regexp"
"sort"
"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"
"github.com/google/uuid"
"github.com/lib/pq"
"go.uber.org/zap"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type WorldInfoService struct{}
// CreateWorldBook 创建世界书
func (s *WorldInfoService) CreateWorldBook(userID uint, req *request.CreateWorldBookRequest) (*app.AIWorldInfo, error) {
// 验证条目 UID 唯一性
if err := s.validateEntries(req.Entries); err != nil {
return nil, err
}
entriesJSON, err := json.Marshal(req.Entries)
if err != nil {
return nil, errors.New("条目数据序列化失败")
}
worldBook := &app.AIWorldInfo{
UserID: userID,
BookName: req.BookName,
IsGlobal: req.IsGlobal,
Entries: datatypes.JSON(entriesJSON),
LinkedChars: req.LinkedChars,
}
if err := global.GVA_DB.Create(worldBook).Error; err != nil {
return nil, err
}
return worldBook, nil
}
// UpdateWorldBook 更新世界书
func (s *WorldInfoService) UpdateWorldBook(userID, bookID uint, req *request.UpdateWorldBookRequest) error {
// 验证条目 UID 唯一性
if err := s.validateEntries(req.Entries); err != nil {
return err
}
entriesJSON, err := json.Marshal(req.Entries)
if err != nil {
return errors.New("条目数据序列化失败")
}
result := global.GVA_DB.Model(&app.AIWorldInfo{}).
Where("id = ? AND user_id = ?", bookID, userID).
Updates(map[string]interface{}{
"book_name": req.BookName,
"is_global": req.IsGlobal,
"entries": datatypes.JSON(entriesJSON),
"linked_chars": pq.StringArray(req.LinkedChars),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("世界书不存在或无权限")
}
return nil
}
// DeleteWorldBook 删除世界书
func (s *WorldInfoService) DeleteWorldBook(userID, bookID uint) error {
result := global.GVA_DB.
Where("id = ? AND user_id = ?", bookID, userID).
Delete(&app.AIWorldInfo{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("世界书不存在或无权限")
}
return nil
}
// GetWorldBook 获取世界书详情
func (s *WorldInfoService) GetWorldBook(userID, bookID uint) (*app.AIWorldInfo, error) {
var book app.AIWorldInfo
err := global.GVA_DB.
Where("id = ? AND user_id = ?", bookID, userID).
First(&book).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("世界书不存在")
}
return nil, err
}
return &book, nil
}
// GetWorldBookList 获取世界书列表
func (s *WorldInfoService) GetWorldBookList(userID uint, req *request.WorldBookListRequest) (*response.WorldBookListResponse, error) {
var books []app.AIWorldInfo
var total int64
db := global.GVA_DB.Model(&app.AIWorldInfo{}).Where("user_id = ?", userID)
// 条件过滤
if req.BookName != "" {
db = db.Where("book_name ILIKE ?", "%"+req.BookName+"%")
}
if req.IsGlobal != nil {
db = db.Where("is_global = ?", *req.IsGlobal)
}
if req.CharacterID != nil {
db = db.Where("? = ANY(linked_chars)", fmt.Sprintf("%d", *req.CharacterID))
}
// 总数
if err := db.Count(&total).Error; err != nil {
return nil, err
}
// 分页查询
offset := (req.Page - 1) * req.PageSize
if err := db.Offset(offset).Limit(req.PageSize).Order("updated_at DESC").Find(&books).Error; err != nil {
return nil, err
}
// 转换响应
list := make([]response.WorldBookResponse, 0, len(books))
for i := range books {
list = append(list, response.ToWorldBookResponse(&books[i]))
}
return &response.WorldBookListResponse{
List: list,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// CreateWorldEntry 创建世界书条目
func (s *WorldInfoService) CreateWorldEntry(userID, bookID uint, entry *app.AIWorldInfoEntry) error {
book, err := s.GetWorldBook(userID, bookID)
if err != nil {
return err
}
var entries []app.AIWorldInfoEntry
if err := json.Unmarshal([]byte(book.Entries), &entries); err != nil {
return errors.New("条目数据解析失败")
}
// 生成唯一 UID
if entry.UID == "" {
entry.UID = uuid.New().String()
}
// 检查 UID 是否重复
for _, e := range entries {
if e.UID == entry.UID {
return errors.New("条目 UID 已存在")
}
}
entries = append(entries, *entry)
entriesJSON, err := json.Marshal(entries)
if err != nil {
return errors.New("条目数据序列化失败")
}
return global.GVA_DB.Model(&app.AIWorldInfo{}).
Where("id = ? AND user_id = ?", bookID, userID).
Update("entries", datatypes.JSON(entriesJSON)).Error
}
// UpdateWorldEntry 更新世界书条目
func (s *WorldInfoService) UpdateWorldEntry(userID, bookID uint, entry *app.AIWorldInfoEntry) error {
book, err := s.GetWorldBook(userID, bookID)
if err != nil {
return err
}
var entries []app.AIWorldInfoEntry
if err := json.Unmarshal([]byte(book.Entries), &entries); err != nil {
return errors.New("条目数据解析失败")
}
found := false
for i := range entries {
if entries[i].UID == entry.UID {
entries[i] = *entry
found = true
break
}
}
if !found {
return errors.New("条目不存在")
}
entriesJSON, err := json.Marshal(entries)
if err != nil {
return errors.New("条目数据序列化失败")
}
return global.GVA_DB.Model(&app.AIWorldInfo{}).
Where("id = ? AND user_id = ?", bookID, userID).
Update("entries", datatypes.JSON(entriesJSON)).Error
}
// DeleteWorldEntry 删除世界书条目
func (s *WorldInfoService) DeleteWorldEntry(userID, bookID uint, entryID string) error {
book, err := s.GetWorldBook(userID, bookID)
if err != nil {
return err
}
var entries []app.AIWorldInfoEntry
if err := json.Unmarshal([]byte(book.Entries), &entries); err != nil {
return errors.New("条目数据解析失败")
}
newEntries := make([]app.AIWorldInfoEntry, 0)
found := false
for _, e := range entries {
if e.UID != entryID {
newEntries = append(newEntries, e)
} else {
found = true
}
}
if !found {
return errors.New("条目不存在")
}
entriesJSON, err := json.Marshal(newEntries)
if err != nil {
return errors.New("条目数据序列化失败")
}
return global.GVA_DB.Model(&app.AIWorldInfo{}).
Where("id = ? AND user_id = ?", bookID, userID).
Update("entries", datatypes.JSON(entriesJSON)).Error
}
// LinkCharactersToWorldBook 关联角色到世界书
func (s *WorldInfoService) LinkCharactersToWorldBook(userID, bookID uint, characterIDs []uint) error {
// 验证世界书是否存在
_, err := s.GetWorldBook(userID, bookID)
if err != nil {
return err
}
// 验证角色是否属于该用户
var count int64
err = global.GVA_DB.Model(&app.AICharacter{}).
Where("user_id = ? AND id IN ?", userID, characterIDs).
Count(&count).Error
if err != nil {
return err
}
if int(count) != len(characterIDs) {
return errors.New("部分角色不存在或无权限")
}
// 转换为字符串数组
linkedChars := make([]string, len(characterIDs))
for i, id := range characterIDs {
linkedChars[i] = fmt.Sprintf("%d", id)
}
return global.GVA_DB.Model(&app.AIWorldInfo{}).
Where("id = ? AND user_id = ?", bookID, userID).
Update("linked_chars", pq.StringArray(linkedChars)).Error
}
// ImportWorldBook 导入世界书
func (s *WorldInfoService) ImportWorldBook(userID uint, bookName string, data []byte, format string) (*app.AIWorldInfo, error) {
var entries []app.AIWorldInfoEntry
switch format {
case "json", "lorebook":
var importData struct {
Name string `json:"name"`
Entries []app.AIWorldInfoEntry `json:"entries"`
}
if err := json.Unmarshal(data, &importData); err != nil {
return nil, errors.New("JSON 格式解析失败")
}
entries = importData.Entries
if bookName == "" && importData.Name != "" {
bookName = importData.Name
}
default:
return nil, errors.New("不支持的导入格式")
}
if bookName == "" {
bookName = "导入的世界书"
}
// 为没有 UID 的条目生成 UID
for i := range entries {
if entries[i].UID == "" {
entries[i].UID = uuid.New().String()
}
}
// 验证条目 UID 唯一性
if err := s.validateEntries(entries); err != nil {
return nil, err
}
entriesJSON, err := json.Marshal(entries)
if err != nil {
return nil, errors.New("条目数据序列化失败")
}
worldBook := &app.AIWorldInfo{
UserID: userID,
BookName: bookName,
IsGlobal: false,
Entries: datatypes.JSON(entriesJSON),
LinkedChars: []string{},
}
if err := global.GVA_DB.Create(worldBook).Error; err != nil {
return nil, err
}
return worldBook, nil
}
// ExportWorldBook 导出世界书
func (s *WorldInfoService) ExportWorldBook(userID, bookID uint) (*response.WorldBookExportData, error) {
book, err := s.GetWorldBook(userID, bookID)
if err != nil {
return nil, err
}
var entries []app.AIWorldInfoEntry
if err := json.Unmarshal([]byte(book.Entries), &entries); err != nil {
return nil, errors.New("条目数据解析失败")
}
return &response.WorldBookExportData{
Name: book.BookName,
Entries: entries,
}, nil
}
// MatchWorldInfo 匹配世界书条目(用于聊天时激活)
func (s *WorldInfoService) MatchWorldInfo(userID uint, req *request.MatchWorldInfoRequest) (*response.MatchWorldInfoResponse, error) {
// 获取角色关联的世界书
var books []app.AIWorldInfo
err := global.GVA_DB.
Where("user_id = ? AND (is_global = true OR ? = ANY(linked_chars))", userID, fmt.Sprintf("%d", req.CharacterID)).
Find(&books).Error
if err != nil {
return nil, err
}
// 合并所有世界书的条目
var allEntries []struct {
Entry app.AIWorldInfoEntry
Source string
}
for _, book := range books {
var entries []app.AIWorldInfoEntry
if err := json.Unmarshal([]byte(book.Entries), &entries); err != nil {
global.GVA_LOG.Warn("世界书条目解析失败",
zap.Uint("bookID", book.ID),
zap.String("bookName", book.BookName),
zap.Error(err))
continue
}
for _, entry := range entries {
if entry.Enabled {
allEntries = append(allEntries, struct {
Entry app.AIWorldInfoEntry
Source string
}{Entry: entry, Source: book.BookName})
}
}
}
// 匹配条目
matched := s.matchEntries(allEntries, req.Messages, req.ScanDepth, req.MaxTokens)
// 计算总 Token 数(简单估算:每 4 个字符 = 1 token
totalTokens := 0
for _, m := range matched {
totalTokens += len(m.Content) / 4
}
return &response.MatchWorldInfoResponse{
Entries: matched,
TotalTokens: totalTokens,
}, nil
}
// matchEntries 核心匹配引擎
func (s *WorldInfoService) matchEntries(
allEntries []struct {
Entry app.AIWorldInfoEntry
Source string
},
messages []string,
scanDepth int,
maxTokens int,
) []response.MatchedWorldInfoEntry {
// 获取扫描范围内的消息
scanMessages := messages
if scanDepth > 0 && scanDepth < len(messages) {
scanMessages = messages[len(messages)-scanDepth:]
}
// 合并扫描文本
scanText := strings.Join(scanMessages, "\n")
// 第一轮匹配常驻条目Constant
var matched []matchedEntry
for _, item := range allEntries {
if item.Entry.Constant {
matched = append(matched, matchedEntry{
entry: item.Entry,
source: item.Source,
depth: 0,
})
}
}
// 第二轮:正常匹配
for _, item := range allEntries {
if item.Entry.Constant {
continue // 常驻条目已处理
}
// 检查概率
if item.Entry.UseProbability && item.Entry.Probability < 100 {
if rand.Intn(100) >= item.Entry.Probability {
continue
}
}
// 匹配关键词
isMatch := s.matchKeys(scanText, item.Entry.Keys, item.Entry)
// 如果是选择性激活,还需要匹配次要关键词
if isMatch && item.Entry.Selective && len(item.Entry.SecondaryKeys) > 0 {
secondaryMatch := s.matchKeys(scanText, item.Entry.SecondaryKeys, item.Entry)
if !secondaryMatch {
isMatch = false
}
}
if isMatch {
matched = append(matched, matchedEntry{
entry: item.Entry,
source: item.Source,
depth: 0,
})
}
}
// 第三轮:递归激活
recursiveDepth := 1
for recursiveDepth <= 3 { // 最多递归 3 层
newMatches := []matchedEntry{}
for _, item := range allEntries {
// 跳过已匹配的条目
alreadyMatched := false
for _, m := range matched {
if m.entry.UID == item.Entry.UID {
alreadyMatched = true
break
}
}
if alreadyMatched {
continue
}
// 检查是否设置了递归延迟
if item.Entry.DelayUntilRecursion && recursiveDepth == 1 {
continue
}
// 防止递归
if item.Entry.PreventRecursion {
continue
}
// 在已匹配的内容中查找
for _, m := range matched {
if m.depth == recursiveDepth-1 {
isMatch := s.matchKeys(m.entry.Content, item.Entry.Keys, item.Entry)
if isMatch && item.Entry.Selective && len(item.Entry.SecondaryKeys) > 0 {
secondaryMatch := s.matchKeys(m.entry.Content, item.Entry.SecondaryKeys, item.Entry)
if !secondaryMatch {
isMatch = false
}
}
if isMatch {
newMatches = append(newMatches, matchedEntry{
entry: item.Entry,
source: item.Source,
depth: recursiveDepth,
})
break
}
}
}
}
if len(newMatches) == 0 {
break // 没有新匹配,停止递归
}
matched = append(matched, newMatches...)
recursiveDepth++
}
// 分组处理(同组只保留一个)
matched = s.applyGroupFilters(matched)
// 排序(按 Order 排序)
sort.Slice(matched, func(i, j int) bool {
return matched[i].entry.Order < matched[j].entry.Order
})
// Token 限制
result := []response.MatchedWorldInfoEntry{}
currentTokens := 0
for _, m := range matched {
entryTokens := len(m.entry.Content) / 4
if currentTokens+entryTokens > maxTokens {
break
}
result = append(result, response.MatchedWorldInfoEntry{
Content: m.entry.Content,
Position: m.entry.Position,
Order: m.entry.Order,
Source: m.source,
})
currentTokens += entryTokens
}
return result
}
// matchedEntry 内部匹配结果
type matchedEntry struct {
entry app.AIWorldInfoEntry
source string
depth int // 递归深度
}
// matchKeys 关键词匹配
func (s *WorldInfoService) matchKeys(text string, keys []string, entry app.AIWorldInfoEntry) bool {
// 确定大小写敏感性
caseSensitive := false
if entry.CaseSensitive != nil {
caseSensitive = *entry.CaseSensitive
}
// 确定是否使用正则
useRegex := false
if entry.UseRegex != nil {
useRegex = *entry.UseRegex
}
// 确定是否匹配整词
matchWholeWords := false
if entry.MatchWholeWords != nil {
matchWholeWords = *entry.MatchWholeWords
}
searchText := text
if !caseSensitive {
searchText = strings.ToLower(text)
}
for _, key := range keys {
if key == "" {
continue
}
searchKey := key
if !caseSensitive {
searchKey = strings.ToLower(key)
}
if useRegex {
// 正则匹配
pattern := searchKey
if !caseSensitive {
pattern = "(?i)" + pattern
}
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 if matchWholeWords {
// 整词匹配
pattern := `\b` + regexp.QuoteMeta(searchKey) + `\b`
if !caseSensitive {
pattern = "(?i)" + pattern
}
matched, err := regexp.MatchString(pattern, searchText)
if err != nil {
global.GVA_LOG.Warn("整词匹配失败",
zap.String("pattern", pattern),
zap.Error(err))
continue
}
if matched {
return true
}
} else {
// 普通子串匹配
if strings.Contains(searchText, searchKey) {
return true
}
}
}
return false
}
// applyGroupFilters 应用分组过滤
func (s *WorldInfoService) applyGroupFilters(matched []matchedEntry) []matchedEntry {
// 按分组分类
groups := make(map[string][]matchedEntry)
noGroup := []matchedEntry{}
for _, m := range matched {
if m.entry.Group == "" {
noGroup = append(noGroup, m)
} else {
groups[m.entry.Group] = append(groups[m.entry.Group], m)
}
}
// 每个分组只保留一个(权重最高的)
result := noGroup
for _, group := range groups {
if len(group) == 0 {
continue
}
// 按权重排序
sort.Slice(group, func(i, j int) bool {
if group[i].entry.GroupWeight != group[j].entry.GroupWeight {
return group[i].entry.GroupWeight > group[j].entry.GroupWeight
}
return group[i].entry.Order < group[j].entry.Order
})
result = append(result, group[0])
}
return result
}
// validateEntries 验证条目 UID 唯一性
func (s *WorldInfoService) validateEntries(entries []app.AIWorldInfoEntry) error {
uidMap := make(map[string]bool)
for _, entry := range entries {
if entry.UID == "" {
return errors.New("条目 UID 不能为空")
}
if uidMap[entry.UID] {
return errors.New("条目 UID 重复: " + entry.UID)
}
uidMap[entry.UID] = true
}
return nil
}