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 }