package app import ( "encoding/json" "errors" "fmt" "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 WorldbookService struct{} // CreateWorldbook 创建世界书 func (s *WorldbookService) CreateWorldbook(userID uint, req *request.CreateWorldbookRequest) (*response.WorldbookResponse, error) { wb := &app.Worldbook{ UserID: userID, Name: req.Name, Description: req.Description, IsPublic: req.IsPublic, } if err := global.GVA_DB.Create(wb).Error; err != nil { global.GVA_LOG.Error("创建世界书失败", zap.Error(err)) return nil, err } resp := response.ToWorldbookResponse(wb) return &resp, nil } // GetWorldbookList 获取世界书列表(自己的 + 公开的) func (s *WorldbookService) GetWorldbookList(userID uint, req *request.GetWorldbookListRequest) ([]response.WorldbookResponse, int64, error) { var worldbooks []app.Worldbook var total int64 db := global.GVA_DB.Model(&app.Worldbook{}).Where("user_id = ? OR is_public = ?", userID, true) if req.Keyword != "" { db = db.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%") } if err := db.Count(&total).Error; err != nil { return nil, 0, err } offset := (req.Page - 1) * req.PageSize if err := db.Order("updated_at DESC").Offset(offset).Limit(req.PageSize).Find(&worldbooks).Error; err != nil { global.GVA_LOG.Error("获取世界书列表失败", zap.Error(err)) return nil, 0, err } var list []response.WorldbookResponse for i := range worldbooks { list = append(list, response.ToWorldbookResponse(&worldbooks[i])) } return list, total, nil } // GetWorldbookByID 获取世界书详情 func (s *WorldbookService) GetWorldbookByID(userID uint, id uint) (*response.WorldbookResponse, error) { var wb app.Worldbook if err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", id, userID, true). First(&wb).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("世界书不存在或无权访问") } return nil, err } resp := response.ToWorldbookResponse(&wb) return &resp, nil } // UpdateWorldbook 更新世界书 func (s *WorldbookService) UpdateWorldbook(userID uint, id uint, req *request.UpdateWorldbookRequest) error { var wb app.Worldbook if err := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).First(&wb).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.Description != nil { updates["description"] = *req.Description } if req.IsPublic != nil { updates["is_public"] = *req.IsPublic } return global.GVA_DB.Model(&wb).Updates(updates).Error } // DeleteWorldbook 删除世界书(级联删除条目) func (s *WorldbookService) DeleteWorldbook(userID uint, id uint) error { return global.GVA_DB.Transaction(func(tx *gorm.DB) error { // 删除所有条目 if err := tx.Where("worldbook_id = ?", id).Delete(&app.WorldbookEntry{}).Error; err != nil { return err } // 删除世界书 result := tx.Where("id = ? AND user_id = ?", id, userID).Delete(&app.Worldbook{}) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return errors.New("世界书不存在或无权删除") } return nil }) } // CreateEntry 创建世界书条目 func (s *WorldbookService) CreateEntry(userID uint, worldbookID uint, req *request.CreateEntryRequest) (*response.EntryResponse, error) { // 验证世界书归属 var wb app.Worldbook if err := global.GVA_DB.Where("id = ? AND user_id = ?", worldbookID, userID).First(&wb).Error; err != nil { return nil, errors.New("世界书不存在或无权操作") } keysJSON, _ := json.Marshal(req.Keys) secKeysJSON, _ := json.Marshal(req.SecondaryKeys) enabled := true if req.Enabled != nil { enabled = *req.Enabled } probability := req.Probability if probability == 0 { probability = 100 } order := req.Order if order == 0 { order = 100 } scanDepth := req.ScanDepth if scanDepth == 0 { scanDepth = 2 } entry := &app.WorldbookEntry{ WorldbookID: worldbookID, Comment: req.Comment, Content: req.Content, Keys: datatypes.JSON(keysJSON), SecondaryKeys: datatypes.JSON(secKeysJSON), Constant: req.Constant, Enabled: enabled, UseRegex: req.UseRegex, CaseSensitive: req.CaseSensitive, MatchWholeWords: req.MatchWholeWords, Selective: req.Selective, SelectiveLogic: req.SelectiveLogic, Position: req.Position, Depth: req.Depth, Order: order, Probability: probability, ScanDepth: scanDepth, GroupID: req.GroupID, } if err := global.GVA_DB.Create(entry).Error; err != nil { global.GVA_LOG.Error("创建世界书条目失败", zap.Error(err)) return nil, err } // 更新世界书条目计数 global.GVA_DB.Model(&wb).Update("entry_count", gorm.Expr("entry_count + ?", 1)) resp := response.ToEntryResponse(entry) return &resp, nil } // GetEntryList 获取世界书条目列表 func (s *WorldbookService) GetEntryList(userID uint, worldbookID uint) ([]response.EntryResponse, int64, error) { // 验证访问权限 var wb app.Worldbook if err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", worldbookID, userID, true). First(&wb).Error; err != nil { return nil, 0, errors.New("世界书不存在或无权访问") } var entries []app.WorldbookEntry var total int64 db := global.GVA_DB.Model(&app.WorldbookEntry{}).Where("worldbook_id = ?", worldbookID) db.Count(&total) if err := db.Order("`order` ASC, created_at ASC").Find(&entries).Error; err != nil { return nil, 0, err } var list []response.EntryResponse for i := range entries { list = append(list, response.ToEntryResponse(&entries[i])) } return list, total, nil } // UpdateEntry 更新世界书条目 func (s *WorldbookService) UpdateEntry(userID uint, entryID uint, req *request.UpdateEntryRequest) error { // 查找条目并验证归属 var entry app.WorldbookEntry if err := global.GVA_DB.First(&entry, entryID).Error; err != nil { return errors.New("条目不存在") } var wb app.Worldbook if err := global.GVA_DB.Where("id = ? AND user_id = ?", entry.WorldbookID, userID).First(&wb).Error; err != nil { return errors.New("无权修改此条目") } updates := make(map[string]interface{}) if req.Comment != nil { updates["comment"] = *req.Comment } if req.Content != nil { updates["content"] = *req.Content } if req.Keys != nil { keysJSON, _ := json.Marshal(req.Keys) updates["keys"] = datatypes.JSON(keysJSON) } if req.SecondaryKeys != nil { secKeysJSON, _ := json.Marshal(req.SecondaryKeys) updates["secondary_keys"] = datatypes.JSON(secKeysJSON) } if req.Constant != nil { updates["constant"] = *req.Constant } if req.Enabled != nil { updates["enabled"] = *req.Enabled } if req.UseRegex != nil { updates["use_regex"] = *req.UseRegex } if req.CaseSensitive != nil { updates["case_sensitive"] = *req.CaseSensitive } if req.MatchWholeWords != nil { updates["match_whole_words"] = *req.MatchWholeWords } if req.Selective != nil { updates["selective"] = *req.Selective } if req.SelectiveLogic != nil { updates["selective_logic"] = *req.SelectiveLogic } if req.Position != nil { updates["position"] = *req.Position } if req.Depth != nil { updates["depth"] = *req.Depth } if req.Order != nil { updates["order"] = *req.Order } if req.Probability != nil { updates["probability"] = *req.Probability } if req.ScanDepth != nil { updates["scan_depth"] = *req.ScanDepth } if req.GroupID != nil { updates["group_id"] = *req.GroupID } return global.GVA_DB.Model(&entry).Updates(updates).Error } // DeleteEntry 删除世界书条目 func (s *WorldbookService) DeleteEntry(userID uint, entryID uint) error { var entry app.WorldbookEntry if err := global.GVA_DB.First(&entry, entryID).Error; err != nil { return errors.New("条目不存在") } var wb app.Worldbook if err := global.GVA_DB.Where("id = ? AND user_id = ?", entry.WorldbookID, userID).First(&wb).Error; err != nil { return errors.New("无权删除此条目") } if err := global.GVA_DB.Delete(&entry).Error; err != nil { return err } // 更新条目计数 global.GVA_DB.Model(&wb).Update("entry_count", gorm.Expr("GREATEST(entry_count - 1, 0)")) return nil } // ImportFromJSON 从 JSON 文件导入世界书(兼容 SillyTavern 格式) func (s *WorldbookService) ImportFromJSON(userID uint, jsonData []byte, filename string) (*response.WorldbookResponse, error) { // 尝试解析 SillyTavern 世界书格式 var stFormat map[string]interface{} if err := json.Unmarshal(jsonData, &stFormat); err != nil { return nil, fmt.Errorf("JSON 格式错误: %v", err) } // 提取世界书名称 name := filename if n, ok := stFormat["name"].(string); ok && n != "" { name = n } description := "" if d, ok := stFormat["description"].(string); ok { description = d } wb := &app.Worldbook{ UserID: userID, Name: name, Description: description, } if err := global.GVA_DB.Create(wb).Error; err != nil { return nil, err } // 解析条目(SillyTavern entries 格式:map[string]entry 或 []entry) var entryCount int if entriesRaw, ok := stFormat["entries"]; ok { switch entries := entriesRaw.(type) { case map[string]interface{}: // SillyTavern 格式:键值对 for _, v := range entries { if entryMap, ok := v.(map[string]interface{}); ok { s.importEntry(wb.ID, entryMap) entryCount++ } } case []interface{}: // 数组格式 for _, v := range entries { if entryMap, ok := v.(map[string]interface{}); ok { s.importEntry(wb.ID, entryMap) entryCount++ } } } } // 更新条目计数 global.GVA_DB.Model(wb).Update("entry_count", entryCount) resp := response.ToWorldbookResponse(wb) resp.EntryCount = entryCount return &resp, nil } // importEntry 辅助方法:从 SillyTavern 格式导入单条条目 func (s *WorldbookService) importEntry(worldbookID uint, entryMap map[string]interface{}) { content := "" if c, ok := entryMap["content"].(string); ok { content = c } if content == "" { return } comment := "" if c, ok := entryMap["comment"].(string); ok { comment = c } // 解析 keys(SillyTavern 存为 []string 或 []interface{}) var keys []string if k, ok := entryMap["key"].([]interface{}); ok { for _, kk := range k { if ks, ok := kk.(string); ok { keys = append(keys, ks) } } } else if k, ok := entryMap["keys"].([]interface{}); ok { for _, kk := range k { if ks, ok := kk.(string); ok { keys = append(keys, ks) } } } keysJSON, _ := json.Marshal(keys) var secKeys []string if k, ok := entryMap["secondary_key"].([]interface{}); ok { for _, kk := range k { if ks, ok := kk.(string); ok { secKeys = append(secKeys, ks) } } } else if k, ok := entryMap["secondaryKeys"].([]interface{}); ok { for _, kk := range k { if ks, ok := kk.(string); ok { secKeys = append(secKeys, ks) } } } secKeysJSON, _ := json.Marshal(secKeys) constant := false if c, ok := entryMap["constant"].(bool); ok { constant = c } enabled := true if e, ok := entryMap["enabled"].(bool); ok { enabled = e } else if d, ok := entryMap["disable"].(bool); ok { enabled = !d } useRegex := false if r, ok := entryMap["use_regex"].(bool); ok { useRegex = r } position := 1 if p, ok := entryMap["position"].(float64); ok { position = int(p) } order := 100 if o, ok := entryMap["insertion_order"].(float64); ok { order = int(o) } else if o, ok := entryMap["order"].(float64); ok { order = int(o) } probability := 100 if p, ok := entryMap["probability"].(float64); ok { probability = int(p) } entry := &app.WorldbookEntry{ WorldbookID: worldbookID, Comment: comment, Content: content, Keys: datatypes.JSON(keysJSON), SecondaryKeys: datatypes.JSON(secKeysJSON), Constant: constant, Enabled: enabled, UseRegex: useRegex, Position: position, Order: order, Probability: probability, ScanDepth: 2, } global.GVA_DB.Create(entry) } // ExportToJSON 导出世界书为 JSON(兼容 SillyTavern 格式) func (s *WorldbookService) ExportToJSON(userID uint, worldbookID uint) ([]byte, string, error) { var wb app.Worldbook if err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", worldbookID, userID, true). First(&wb).Error; err != nil { return nil, "", errors.New("世界书不存在或无权访问") } var entries []app.WorldbookEntry global.GVA_DB.Where("worldbook_id = ?", worldbookID).Order("`order` ASC").Find(&entries) // 构建 SillyTavern 兼容格式 entriesMap := make(map[string]interface{}) for i, entry := range entries { var keys []string json.Unmarshal(entry.Keys, &keys) var secKeys []string json.Unmarshal(entry.SecondaryKeys, &secKeys) entriesMap[fmt.Sprintf("%d", i)] = map[string]interface{}{ "uid": entry.ID, "key": keys, "secondary_key": secKeys, "comment": entry.Comment, "content": entry.Content, "constant": entry.Constant, "enabled": entry.Enabled, "use_regex": entry.UseRegex, "case_sensitive": entry.CaseSensitive, "match_whole_words": entry.MatchWholeWords, "selective": entry.Selective, "selectiveLogic": entry.SelectiveLogic, "position": entry.Position, "depth": entry.Depth, "insertion_order": entry.Order, "probability": entry.Probability, "scanDepth": entry.ScanDepth, "group": entry.GroupID, } } exportData := map[string]interface{}{ "name": wb.Name, "description": wb.Description, "entries": entriesMap, } data, err := json.MarshalIndent(exportData, "", " ") return data, wb.Name + ".json", err }