package app import ( "crypto/md5" "encoding/hex" "encoding/json" "errors" "fmt" "image" _ "image/jpeg" _ "image/png" "os" "regexp" "strings" "time" "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" "git.echol.cn/loser/st/server/utils" "github.com/lib/pq" "go.uber.org/zap" "gorm.io/datatypes" "gorm.io/gorm" ) type CharacterService struct{} // GetPublicCharacterList 获取公开角色卡列表(无需鉴权) func (cs *CharacterService) GetPublicCharacterList(req request.CharacterListRequest, userID *uint) (response.CharacterListResponse, error) { db := global.GVA_DB.Model(&app.AICharacter{}) // 只查询公开的角色卡 db = db.Where("is_public = ?", true) // 关键词搜索 if req.Keyword != "" { keyword := "%" + req.Keyword + "%" db = db.Where("name ILIKE ? OR description ILIKE ? OR creator_name ILIKE ?", keyword, keyword, keyword) } // 标签筛选 if len(req.Tags) > 0 { for _, tag := range req.Tags { db = db.Where("tags @> ?", fmt.Sprintf(`["%s"]`, tag)) } } // 排序 switch req.SortBy { case "popular": db = db.Order("usage_count DESC, created_at DESC") case "mostChats": db = db.Order("total_chats DESC, created_at DESC") case "mostLikes": db = db.Order("total_likes DESC, created_at DESC") case "newest": fallthrough default: db = db.Order("created_at DESC") } // 分页 var total int64 db.Count(&total) var characters []app.AICharacter offset := (req.Page - 1) * req.PageSize err := db.Offset(offset).Limit(req.PageSize).Find(&characters).Error if err != nil { return response.CharacterListResponse{}, err } // 查询当前用户的收藏状态 favoriteMap := make(map[uint]bool) if userID != nil { var favorites []app.AppUserFavoriteCharacter global.GVA_DB.Where("user_id = ?", *userID).Find(&favorites) for _, fav := range favorites { favoriteMap[fav.CharacterID] = true } } // 转换为响应 list := make([]response.CharacterResponse, len(characters)) for i, char := range characters { list[i] = response.ToCharacterResponse(&char, favoriteMap[char.ID]) } return response.CharacterListResponse{ List: list, Total: total, Page: req.Page, PageSize: req.PageSize, }, nil } // GetMyCharacterList 获取我的角色卡列表(需要鉴权) func (cs *CharacterService) GetMyCharacterList(req request.CharacterListRequest, userID uint) (response.CharacterListResponse, error) { db := global.GVA_DB.Model(&app.AICharacter{}) // 只查询当前用户创建的角色卡 db = db.Where("creator_id = ?", userID) // 关键词搜索 if req.Keyword != "" { keyword := "%" + req.Keyword + "%" db = db.Where("name ILIKE ? OR description ILIKE ?", keyword, keyword) } // 标签筛选 if len(req.Tags) > 0 { for _, tag := range req.Tags { db = db.Where("tags @> ?", fmt.Sprintf(`["%s"]`, tag)) } } // 排序 switch req.SortBy { case "popular": db = db.Order("usage_count DESC, created_at DESC") case "mostChats": db = db.Order("total_chats DESC, created_at DESC") case "mostLikes": db = db.Order("total_likes DESC, created_at DESC") case "newest": fallthrough default: db = db.Order("created_at DESC") } // 分页 var total int64 db.Count(&total) var characters []app.AICharacter offset := (req.Page - 1) * req.PageSize err := db.Offset(offset).Limit(req.PageSize).Find(&characters).Error if err != nil { return response.CharacterListResponse{}, err } // 查询收藏状态 favoriteMap := make(map[uint]bool) var favorites []app.AppUserFavoriteCharacter global.GVA_DB.Where("user_id = ?", userID).Find(&favorites) for _, fav := range favorites { favoriteMap[fav.CharacterID] = true } // 转换为响应 list := make([]response.CharacterResponse, len(characters)) for i, char := range characters { list[i] = response.ToCharacterResponse(&char, favoriteMap[char.ID]) } return response.CharacterListResponse{ List: list, Total: total, Page: req.Page, PageSize: req.PageSize, }, nil } // GetCharacterDetail 获取角色卡详情 func (cs *CharacterService) GetCharacterDetail(characterID uint, userID *uint) (response.CharacterResponse, error) { var character app.AICharacter err := global.GVA_DB.Where("id = ?", characterID).First(&character).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return response.CharacterResponse{}, errors.New("角色卡不存在") } return response.CharacterResponse{}, err } // 检查访问权限 if !character.IsPublic { if userID == nil { return response.CharacterResponse{}, errors.New("无权访问") } if character.CreatorID == nil || *character.CreatorID != *userID { return response.CharacterResponse{}, errors.New("无权访问") } } // 查询是否收藏 isFavorited := false if userID != nil { var count int64 global.GVA_DB.Model(&app.AppUserFavoriteCharacter{}). Where("user_id = ? AND character_id = ?", *userID, characterID). Count(&count) isFavorited = count > 0 } return response.ToCharacterResponse(&character, isFavorited), nil } // CreateCharacter 创建角色卡 func (cs *CharacterService) CreateCharacter(req request.CreateCharacterRequest, userID uint) (response.CharacterResponse, error) { // 构建 CardData(包含完整 V2 数据) cardData := map[string]interface{}{ "name": req.Name, "description": req.Description, "personality": req.Personality, "scenario": req.Scenario, "first_message": req.FirstMessage, "example_messages": req.ExampleMessages, "creator_name": req.CreatorName, "creator_notes": req.CreatorNotes, "system_prompt": req.SystemPrompt, "post_history_instructions": req.PostHistoryInstructions, "alternate_greetings": req.AlternateGreetings, "character_book": req.CharacterBook, "extensions": req.Extensions, } cardDataJSON, _ := json.Marshal(cardData) // 处理标签和示例消息 tags := req.Tags if tags == nil { tags = []string{} } exampleMessages := req.ExampleMessages if exampleMessages == nil { exampleMessages = []string{} } alternateGreetings := req.AlternateGreetings if alternateGreetings == nil { alternateGreetings = []string{} } // 序列化 JSON 字段 var characterBookJSON, extensionsJSON datatypes.JSON if req.CharacterBook != nil { characterBookJSON, _ = json.Marshal(req.CharacterBook) } if req.Extensions != nil { extensionsJSON, _ = json.Marshal(req.Extensions) } character := app.AICharacter{ Name: req.Name, Description: req.Description, Personality: req.Personality, Scenario: req.Scenario, Avatar: req.Avatar, CreatorID: &userID, CreatorName: req.CreatorName, CreatorNotes: req.CreatorNotes, CardData: datatypes.JSON(cardDataJSON), Tags: tags, IsPublic: req.IsPublic, FirstMessage: req.FirstMessage, ExampleMessages: exampleMessages, SystemPrompt: req.SystemPrompt, PostHistoryInstructions: req.PostHistoryInstructions, AlternateGreetings: alternateGreetings, CharacterBook: characterBookJSON, Extensions: extensionsJSON, TokenCount: calculateTokenCount(req), } err := global.GVA_DB.Create(&character).Error if err != nil { return response.CharacterResponse{}, err } return response.ToCharacterResponse(&character, false), nil } // UpdateCharacter 更新角色卡 func (cs *CharacterService) UpdateCharacter(req request.UpdateCharacterRequest, userID uint) (response.CharacterResponse, error) { var character app.AICharacter err := global.GVA_DB.Where("id = ?", req.ID).First(&character).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return response.CharacterResponse{}, errors.New("角色卡不存在") } return response.CharacterResponse{}, err } // 检查权限 if character.CreatorID == nil || *character.CreatorID != userID { return response.CharacterResponse{}, errors.New("无权修改") } // 构建 CardData(包含完整 V2 数据) cardData := map[string]interface{}{ "name": req.Name, "description": req.Description, "personality": req.Personality, "scenario": req.Scenario, "first_message": req.FirstMessage, "example_messages": req.ExampleMessages, "creator_name": req.CreatorName, "creator_notes": req.CreatorNotes, "system_prompt": req.SystemPrompt, "post_history_instructions": req.PostHistoryInstructions, "alternate_greetings": req.AlternateGreetings, "character_book": req.CharacterBook, "extensions": req.Extensions, } cardDataJSON, _ := json.Marshal(cardData) // 处理标签和示例消息,转换为 pq.StringArray tags := req.Tags if tags == nil { tags = []string{} } exampleMessages := req.ExampleMessages if exampleMessages == nil { exampleMessages = []string{} } alternateGreetings := req.AlternateGreetings if alternateGreetings == nil { alternateGreetings = []string{} } // 序列化 JSON 字段 var characterBookJSON, extensionsJSON datatypes.JSON if req.CharacterBook != nil { characterBookJSON, _ = json.Marshal(req.CharacterBook) } if req.Extensions != nil { extensionsJSON, _ = json.Marshal(req.Extensions) } // 更新字段 - 直接更新到结构体以避免类型转换问题 character.Name = req.Name character.Description = req.Description character.Personality = req.Personality character.Scenario = req.Scenario character.Avatar = req.Avatar character.CreatorName = req.CreatorName character.CreatorNotes = req.CreatorNotes character.CardData = cardDataJSON character.Tags = tags character.IsPublic = req.IsPublic character.FirstMessage = req.FirstMessage character.ExampleMessages = exampleMessages character.SystemPrompt = req.SystemPrompt character.PostHistoryInstructions = req.PostHistoryInstructions character.AlternateGreetings = alternateGreetings character.CharacterBook = characterBookJSON character.Extensions = extensionsJSON character.TokenCount = calculateTokenCount(req) character.Version = character.Version + 1 err = global.GVA_DB.Save(&character).Error if err != nil { return response.CharacterResponse{}, err } // 重新查询 err = global.GVA_DB.Where("id = ?", req.ID).First(&character).Error if err != nil { return response.CharacterResponse{}, err } // 查询是否收藏 var count int64 global.GVA_DB.Model(&app.AppUserFavoriteCharacter{}). Where("user_id = ? AND character_id = ?", userID, character.ID). Count(&count) return response.ToCharacterResponse(&character, count > 0), nil } // DeleteCharacter 删除角色卡 func (cs *CharacterService) DeleteCharacter(characterID uint, userID uint) error { var character app.AICharacter err := global.GVA_DB.Where("id = ?", characterID).First(&character).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("角色卡不存在") } return err } // 检查权限 if character.CreatorID == nil || *character.CreatorID != userID { return errors.New("无权删除") } // 删除相关的收藏记录 global.GVA_DB.Where("character_id = ?", characterID).Delete(&app.AppUserFavoriteCharacter{}) // 删除角色卡 return global.GVA_DB.Delete(&character).Error } // ToggleFavorite 切换收藏状态 func (cs *CharacterService) ToggleFavorite(characterID uint, userID uint) (bool, error) { // 检查角色卡是否存在 var character app.AICharacter err := global.GVA_DB.Where("id = ?", characterID).First(&character).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return false, errors.New("角色卡不存在") } return false, err } // 检查是否已收藏 var favorite app.AppUserFavoriteCharacter err = global.GVA_DB.Where("user_id = ? AND character_id = ?", userID, characterID).First(&favorite).Error if errors.Is(err, gorm.ErrRecordNotFound) { // 未收藏,添加收藏 favorite = app.AppUserFavoriteCharacter{ UserID: userID, CharacterID: characterID, } err = global.GVA_DB.Create(&favorite).Error if err != nil { return false, err } // 增加收藏数 global.GVA_DB.Model(&character).UpdateColumn("favorite_count", gorm.Expr("favorite_count + ?", 1)) return true, nil } else if err != nil { return false, err } else { // 已收藏,取消收藏 err = global.GVA_DB.Delete(&favorite).Error if err != nil { return false, err } // 减少收藏数 global.GVA_DB.Model(&character).UpdateColumn("favorite_count", gorm.Expr("favorite_count - ?", 1)) return false, nil } } // LikeCharacter 点赞角色卡 func (cs *CharacterService) LikeCharacter(characterID uint) error { var character app.AICharacter err := global.GVA_DB.Where("id = ?", characterID).First(&character).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("角色卡不存在") } return err } // 增加点赞数 return global.GVA_DB.Model(&character).UpdateColumn("total_likes", gorm.Expr("total_likes + ?", 1)).Error } // ExportCharacter 导出角色卡为 JSON func (cs *CharacterService) ExportCharacter(characterID uint, userID *uint) (map[string]interface{}, error) { var character app.AICharacter err := global.GVA_DB.Where("id = ?", characterID).First(&character).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("角色卡不存在") } return nil, err } // 检查访问权限 if !character.IsPublic { if userID == nil { return nil, errors.New("无权访问") } if character.CreatorID == nil || *character.CreatorID != *userID { return nil, errors.New("无权访问") } } // 处理 tags 和 exampleMessages tags := []string{} if character.Tags != nil { tags = character.Tags } exampleMessages := []string{} if character.ExampleMessages != nil { exampleMessages = character.ExampleMessages } alternateGreetings := []string{} if character.AlternateGreetings != nil { alternateGreetings = character.AlternateGreetings } // 解析或构建 character_book JSON var characterBook map[string]interface{} if len(character.CharacterBook) > 0 { json.Unmarshal(character.CharacterBook, &characterBook) } // 如果角色没有内嵌的 CharacterBook,尝试从世界书表中查找关联的世界书 if characterBook == nil { characterBook = cs.exportLinkedWorldBook(character.ID) } // 解析或构建 extensions JSON extensions := map[string]interface{}{} if len(character.Extensions) > 0 { json.Unmarshal(character.Extensions, &extensions) } // 导出关联的正则脚本到 extensions if regexScripts := cs.exportRegexScripts(character.ID); regexScripts != nil && len(regexScripts) > 0 { extensions["regex_scripts"] = regexScripts } // 构建导出数据(兼容 SillyTavern 格式) data := map[string]interface{}{ "name": character.Name, "description": character.Description, "personality": character.Personality, "scenario": character.Scenario, "first_mes": character.FirstMessage, "mes_example": strings.Join(exampleMessages, "\n\n"), "creator_notes": character.CreatorNotes, "system_prompt": character.SystemPrompt, "post_history_instructions": character.PostHistoryInstructions, "tags": tags, "creator": character.CreatorName, "character_version": character.Version, "alternate_greetings": alternateGreetings, "extensions": extensions, } // 仅在存在时添加 character_book(现在包含关联的世界书) if characterBook != nil { data["character_book"] = characterBook } exportData := map[string]interface{}{ "spec": "chara_card_v2", "spec_version": "2.0", "data": data, } return exportData, nil } // ImportCharacter 导入角色卡(支持 PNG 和 JSON) func (cs *CharacterService) ImportCharacter(fileData []byte, filename string, userID uint, isPublic bool) (response.CharacterResponse, error) { // 添加 defer 捕获 panic defer func() { if r := recover(); r != nil { global.GVA_LOG.Error("导入角色卡时发生 panic", zap.Any("panic", r), zap.String("filename", filename)) } }() global.GVA_LOG.Info("开始导入角色卡", zap.String("filename", filename), zap.Int("fileSize", len(fileData)), zap.Uint("userID", userID)) var card *utils.CharacterCardV2 var err error var avatarData []byte // 判断文件类型 if strings.HasSuffix(strings.ToLower(filename), ".png") { global.GVA_LOG.Info("检测到 PNG 格式,开始提取角色卡数据") // PNG 格式:提取角色卡数据和头像 card, err = utils.ExtractCharacterFromPNG(fileData) if err != nil { global.GVA_LOG.Error("解析 PNG 角色卡失败", zap.Error(err)) return response.CharacterResponse{}, errors.New("解析 PNG 角色卡失败: " + err.Error()) } global.GVA_LOG.Info("PNG 角色卡解析成功", zap.String("characterName", card.Data.Name)) avatarData = fileData } else if strings.HasSuffix(strings.ToLower(filename), ".json") { global.GVA_LOG.Info("检测到 JSON 格式,开始解析") // JSON 格式:只有数据,没有头像 card, err = utils.ParseCharacterCardJSON(fileData) if err != nil { global.GVA_LOG.Error("解析 JSON 角色卡失败", zap.Error(err)) return response.CharacterResponse{}, errors.New("解析 JSON 角色卡失败: " + err.Error()) } global.GVA_LOG.Info("JSON 角色卡解析成功", zap.String("characterName", card.Data.Name)) } else { return response.CharacterResponse{}, errors.New("不支持的文件格式,请上传 PNG 或 JSON 文件") } // 转换为创建请求 global.GVA_LOG.Info("转换角色卡数据为创建请求") createReq := convertCardToCreateRequest(card, avatarData, isPublic) global.GVA_LOG.Info("开始创建角色卡到数据库", zap.String("name", createReq.Name), zap.Bool("isPublic", createReq.IsPublic)) // 创建角色卡 result, err := cs.CreateCharacter(createReq, userID) if err != nil { global.GVA_LOG.Error("创建角色卡到数据库失败", zap.Error(err)) return response.CharacterResponse{}, err } // 处理角色卡中的世界书数据(CharacterBook) if card.Data.CharacterBook != nil && len(card.Data.CharacterBook) > 0 { global.GVA_LOG.Info("检测到角色卡包含世界书数据,开始导入世界书", zap.Uint("characterID", result.ID)) if err := cs.importCharacterBook(userID, result.ID, card.Data.CharacterBook); err != nil { global.GVA_LOG.Warn("导入世界书失败(不影响角色卡导入)", zap.Error(err), zap.Uint("characterID", result.ID)) } else { global.GVA_LOG.Info("世界书导入成功", zap.Uint("characterID", result.ID)) } } // 处理角色卡中的扩展数据(Extensions) if card.Data.Extensions != nil && len(card.Data.Extensions) > 0 { global.GVA_LOG.Info("检测到角色卡包含扩展数据,开始处理扩展", zap.Uint("characterID", result.ID)) // 处理 Regex 脚本 if regexScripts, ok := card.Data.Extensions["regex_scripts"]; ok { if err := cs.importRegexScripts(userID, result.ID, regexScripts); err != nil { global.GVA_LOG.Warn("导入正则脚本失败(不影响角色卡导入)", zap.Error(err), zap.Uint("characterID", result.ID)) } else { global.GVA_LOG.Info("正则脚本导入成功", zap.Uint("characterID", result.ID)) } } // 其他扩展数据已经存储在 Extensions 字段中,无需额外处理 } global.GVA_LOG.Info("角色卡导入完成", zap.Uint("characterID", result.ID)) return result, nil } // ExportCharacterAsPNG 导出角色卡为 PNG 格式 func (cs *CharacterService) ExportCharacterAsPNG(characterID uint, userID *uint) ([]byte, error) { var character app.AICharacter err := global.GVA_DB.Where("id = ?", characterID).First(&character).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("角色卡不存在") } return nil, err } // 检查访问权限 if !character.IsPublic { if userID == nil { return nil, errors.New("无权访问") } if character.CreatorID == nil || *character.CreatorID != *userID { return nil, errors.New("无权访问") } } // 构建角色卡数据 card := cs.convertCharacterToCard(&character) // 获取角色头像 var img image.Image var loadErr error if character.Avatar != "" { // 尝试从文件系统或 URL 加载头像 img, loadErr = loadAvatarImage(character.Avatar) if loadErr != nil { global.GVA_LOG.Warn("加载角色头像失败,使用默认头像", zap.String("avatar", character.Avatar), zap.Error(loadErr)) img = createDefaultAvatar() } } else { img = createDefaultAvatar() } // 将角色卡数据嵌入到 PNG pngData, err := utils.EmbedCharacterToPNG(img, card) if err != nil { global.GVA_LOG.Error("生成 PNG 失败", zap.Error(err)) return nil, errors.New("生成 PNG 失败: " + err.Error()) } global.GVA_LOG.Info("PNG 导出成功", zap.Uint("characterID", characterID), zap.Int("size", len(pngData))) return pngData, nil } // createCharacterFromRequest 从请求创建角色卡对象(用于事务) func createCharacterFromRequest(req request.CreateCharacterRequest, userID uint) app.AICharacter { // 处理标签和示例消息 tags := req.Tags if tags == nil { tags = []string{} } exampleMessages := req.ExampleMessages if exampleMessages == nil { exampleMessages = []string{} } alternateGreetings := req.AlternateGreetings if alternateGreetings == nil { alternateGreetings = []string{} } // 构建 CardData cardData := map[string]interface{}{ "name": req.Name, "description": req.Description, "personality": req.Personality, "scenario": req.Scenario, "first_message": req.FirstMessage, "example_messages": req.ExampleMessages, "creator_name": req.CreatorName, "creator_notes": req.CreatorNotes, "system_prompt": req.SystemPrompt, "post_history_instructions": req.PostHistoryInstructions, "alternate_greetings": req.AlternateGreetings, "character_book": req.CharacterBook, "extensions": req.Extensions, } cardDataJSON, _ := json.Marshal(cardData) // 序列化 JSON 字段 var characterBookJSON, extensionsJSON datatypes.JSON if req.CharacterBook != nil { characterBookJSON, _ = json.Marshal(req.CharacterBook) } if req.Extensions != nil { extensionsJSON, _ = json.Marshal(req.Extensions) } return app.AICharacter{ Name: req.Name, Description: req.Description, Personality: req.Personality, Scenario: req.Scenario, Avatar: req.Avatar, CreatorID: &userID, CreatorName: req.CreatorName, CreatorNotes: req.CreatorNotes, CardData: datatypes.JSON(cardDataJSON), Tags: tags, IsPublic: req.IsPublic, FirstMessage: req.FirstMessage, ExampleMessages: exampleMessages, SystemPrompt: req.SystemPrompt, PostHistoryInstructions: req.PostHistoryInstructions, AlternateGreetings: alternateGreetings, CharacterBook: characterBookJSON, Extensions: extensionsJSON, TokenCount: calculateTokenCount(req), } } // importCharacterBookWithTx 在事务中导入角色卡中的世界书数据 func (cs *CharacterService) importCharacterBookWithTx(tx *gorm.DB, userID, characterID uint, characterBook map[string]interface{}) error { // 解析世界书名称 bookName := "" if name, ok := characterBook["name"].(string); ok && name != "" { bookName = name } // 如果没有名称,使用角色名称 if bookName == "" { var character app.AICharacter if err := tx.Where("id = ?", characterID).First(&character).Error; err == nil { bookName = character.Name + " 的世界书" } else { bookName = "角色世界书" } } // 解析世界书条目 entries := []app.AIWorldInfoEntry{} if entriesData, ok := characterBook["entries"].([]interface{}); ok { for i, entryData := range entriesData { if entryMap, ok := entryData.(map[string]interface{}); ok { entry := convertToWorldInfoEntry(entryMap, i) entries = append(entries, entry) } } } if len(entries) == 0 { global.GVA_LOG.Warn("角色卡中的世界书没有有效条目,跳过导入") return nil // 没有条目时不报错,只是跳过 } // 序列化条目 entriesJSON, err := json.Marshal(entries) if err != nil { return errors.New("序列化世界书条目失败: " + err.Error()) } // 创建世界书记录 worldBook := &app.AIWorldInfo{ UserID: userID, BookName: bookName, IsGlobal: false, Entries: datatypes.JSON(entriesJSON), LinkedChars: pq.StringArray{fmt.Sprintf("%d", characterID)}, } if err := tx.Create(worldBook).Error; err != nil { return errors.New("创建世界书记录失败: " + err.Error()) } global.GVA_LOG.Info("成功从角色卡导入世界书", zap.Uint("worldBookID", worldBook.ID), zap.String("bookName", bookName), zap.Int("entriesCount", len(entries))) return nil } // importRegexScripts 导入角色卡中的正则脚本 func (cs *CharacterService) importRegexScripts(userID, characterID uint, regexScriptsData interface{}) error { scriptsArray, ok := regexScriptsData.([]interface{}) if !ok { return errors.New("正则脚本数据格式错误") } if len(scriptsArray) == 0 { global.GVA_LOG.Info("角色卡中没有正则脚本数据") return nil } characterIDStr := fmt.Sprintf("%d", characterID) imported := 0 for i, scriptData := range scriptsArray { scriptMap, ok := scriptData.(map[string]interface{}) if !ok { global.GVA_LOG.Warn("跳过无效的正则脚本数据", zap.Int("index", i)) continue } // 解析正则脚本 script := convertMapToRegexScript(scriptMap, characterIDStr) script.UserID = userID // 验证正则表达式 if _, err := regexp.Compile(script.FindRegex); err != nil { global.GVA_LOG.Warn("跳过无效的正则表达式", zap.Int("index", i), zap.String("regex", script.FindRegex), zap.Error(err)) continue } // 检查是否已存在同名脚本 var existingCount int64 global.GVA_DB.Model(&app.AIRegexScript{}). Where("user_id = ? AND script_name = ?", userID, script.ScriptName). Count(&existingCount) if existingCount > 0 { script.ScriptName = script.ScriptName + fmt.Sprintf(" (角色-%d)", characterID) } // 创建脚本 if err := global.GVA_DB.Create(&script).Error; err != nil { global.GVA_LOG.Warn("创建正则脚本失败", zap.Int("index", i), zap.Error(err)) continue } imported++ } global.GVA_LOG.Info("成功导入正则脚本", zap.Uint("characterID", characterID), zap.Int("imported", imported)) return nil } // convertMapToRegexScript 将 map 转换为 RegexScript func convertMapToRegexScript(scriptMap map[string]interface{}, characterIDStr string) app.AIRegexScript { script := app.AIRegexScript{ ScriptName: getStringValue(scriptMap, "scriptName", "未命名脚本"), Description: getStringValue(scriptMap, "description", ""), FindRegex: getStringValue(scriptMap, "findRegex", ""), ReplaceString: getStringValue(scriptMap, "replaceString", ""), Enabled: getBoolValue(scriptMap, "enabled", true), IsGlobal: false, // 从角色卡导入的脚本默认不是全局脚本 TrimStrings: getBoolValue(scriptMap, "trimStrings", false), OnlyFormat: getBoolValue(scriptMap, "onlyFormat", false), RunOnEdit: getBoolValue(scriptMap, "runOnEdit", false), SubstituteRegex: getBoolValue(scriptMap, "substituteRegex", false), Placement: getStringValue(scriptMap, "placement", ""), LinkedChars: pq.StringArray{characterIDStr}, } // 处理可选的数字字段 if val, ok := scriptMap["minDepth"]; ok { if intVal := getIntValue(scriptMap, "minDepth", 0); intVal != 0 { script.MinDepth = &intVal } else if val != nil { intVal := 0 script.MinDepth = &intVal } } if val, ok := scriptMap["maxDepth"]; ok { if intVal := getIntValue(scriptMap, "maxDepth", 0); intVal != 0 { script.MaxDepth = &intVal } else if val != nil { intVal := 0 script.MaxDepth = &intVal } } if val, ok := scriptMap["affectMinDepth"]; ok { if intVal := getIntValue(scriptMap, "affectMinDepth", 0); intVal != 0 { script.AffectMinDepth = &intVal } else if val != nil { intVal := 0 script.AffectMinDepth = &intVal } } if val, ok := scriptMap["affectMaxDepth"]; ok { if intVal := getIntValue(scriptMap, "affectMaxDepth", 0); intVal != 0 { script.AffectMaxDepth = &intVal } else if val != nil { intVal := 0 script.AffectMaxDepth = &intVal } } // 处理 ScriptData if scriptData, ok := scriptMap["scriptData"].(map[string]interface{}); ok && scriptData != nil { if data, err := datatypes.NewJSONType(scriptData).MarshalJSON(); err == nil { script.ScriptData = data } } return script } // exportRegexScripts 导出角色关联的正则脚本 func (cs *CharacterService) exportRegexScripts(characterID uint) []map[string]interface{} { // 查找关联的正则脚本 var scripts []app.AIRegexScript err := global.GVA_DB. Where("? = ANY(linked_chars)", fmt.Sprintf("%d", characterID)). Find(&scripts).Error if err != nil || len(scripts) == 0 { return nil } // 转换为 map 格式 scriptsData := make([]map[string]interface{}, 0, len(scripts)) for _, script := range scripts { scriptMap := map[string]interface{}{ "scriptName": script.ScriptName, "description": script.Description, "findRegex": script.FindRegex, "replaceString": script.ReplaceString, "enabled": script.Enabled, "trimStrings": script.TrimStrings, "onlyFormat": script.OnlyFormat, "runOnEdit": script.RunOnEdit, "substituteRegex": script.SubstituteRegex, "placement": script.Placement, } // 添加可选字段 if script.MinDepth != nil { scriptMap["minDepth"] = *script.MinDepth } if script.MaxDepth != nil { scriptMap["maxDepth"] = *script.MaxDepth } if script.AffectMinDepth != nil { scriptMap["affectMinDepth"] = *script.AffectMinDepth } if script.AffectMaxDepth != nil { scriptMap["affectMaxDepth"] = *script.AffectMaxDepth } // 添加 ScriptData if len(script.ScriptData) > 0 { var scriptData map[string]interface{} if err := json.Unmarshal([]byte(script.ScriptData), &scriptData); err == nil { scriptMap["scriptData"] = scriptData } } scriptsData = append(scriptsData, scriptMap) } return scriptsData } // importCharacterBook 导入角色卡中的世界书数据(已废弃,使用 importCharacterBookWithTx) func (cs *CharacterService) importCharacterBook(userID, characterID uint, characterBook map[string]interface{}) error { // 解析世界书名称 bookName := "角色世界书" if name, ok := characterBook["name"].(string); ok && name != "" { bookName = name } else { // 获取角色名称作为世界书名称 var character app.AICharacter if err := global.GVA_DB.Where("id = ?", characterID).First(&character).Error; err == nil { bookName = character.Name + " 的世界书" } } // 解析世界书条目 entries := []app.AIWorldInfoEntry{} if entriesData, ok := characterBook["entries"].([]interface{}); ok { for i, entryData := range entriesData { if entryMap, ok := entryData.(map[string]interface{}); ok { entry := convertToWorldInfoEntry(entryMap, i) entries = append(entries, entry) } } } if len(entries) == 0 { return errors.New("世界书中没有有效的条目") } // 序列化条目 entriesJSON, err := json.Marshal(entries) if err != nil { return errors.New("序列化世界书条目失败: " + err.Error()) } // 创建世界书记录 worldBook := &app.AIWorldInfo{ UserID: userID, BookName: bookName, IsGlobal: false, Entries: datatypes.JSON(entriesJSON), LinkedChars: pq.StringArray{fmt.Sprintf("%d", characterID)}, } if err := global.GVA_DB.Create(worldBook).Error; err != nil { return errors.New("创建世界书记录失败: " + err.Error()) } global.GVA_LOG.Info("成功从角色卡导入世界书", zap.Uint("worldBookID", worldBook.ID), zap.String("bookName", bookName), zap.Int("entriesCount", len(entries))) return nil } // convertToWorldInfoEntry 将角色卡中的世界书条目转换为标准格式 func convertToWorldInfoEntry(entryMap map[string]interface{}, index int) app.AIWorldInfoEntry { entry := app.AIWorldInfoEntry{ UID: getStringValue(entryMap, "uid", fmt.Sprintf("entry_%d", index)), Enabled: getBoolValue(entryMap, "enabled", true), Order: getIntValue(entryMap, "insertion_order", index), Content: getStringValue(entryMap, "content", ""), Comment: getStringValue(entryMap, "comment", ""), } // 解析关键词 if keys, ok := entryMap["keys"].([]interface{}); ok { entry.Keys = convertToStringArray(keys) } if secondaryKeys, ok := entryMap["secondary_keys"].([]interface{}); ok { entry.SecondaryKeys = convertToStringArray(secondaryKeys) } // 高级选项 entry.Constant = getBoolValue(entryMap, "constant", false) entry.Selective = getBoolValue(entryMap, "selective", false) entry.Position = getStringValue(entryMap, "position", "before_char") if depth, ok := entryMap["depth"].(float64); ok { entry.Depth = int(depth) } // 概率设置 entry.UseProbability = getBoolValue(entryMap, "use_probability", false) if prob, ok := entryMap["probability"].(float64); ok { entry.Probability = int(prob) } // 分组设置 entry.Group = getStringValue(entryMap, "group", "") entry.GroupOverride = getBoolValue(entryMap, "group_override", false) if weight, ok := entryMap["group_weight"].(float64); ok { entry.GroupWeight = int(weight) } // 递归设置 entry.PreventRecursion = getBoolValue(entryMap, "prevent_recursion", false) entry.DelayUntilRecursion = getBoolValue(entryMap, "delay_until_recursion", false) // 扫描深度 if scanDepth, ok := entryMap["scan_depth"].(float64); ok { depth := int(scanDepth) entry.ScanDepth = &depth } // 匹配选项 if caseSensitive, ok := entryMap["case_sensitive"].(bool); ok { entry.CaseSensitive = &caseSensitive } if matchWholeWords, ok := entryMap["match_whole_words"].(bool); ok { entry.MatchWholeWords = &matchWholeWords } if useRegex, ok := entryMap["use_regex"].(bool); ok { entry.UseRegex = &useRegex } // 其他字段 entry.Automation = getStringValue(entryMap, "automation_id", "") entry.Role = getStringValue(entryMap, "role", "") entry.VectorizedContent = getStringValue(entryMap, "vectorized", "") // 扩展数据 if extensions, ok := entryMap["extensions"].(map[string]interface{}); ok { entry.Extensions = extensions } return entry } // 辅助函数:从 map 中安全获取字符串值 func getStringValue(m map[string]interface{}, key, defaultValue string) string { if val, ok := m[key].(string); ok { return val } return defaultValue } // 辅助函数:从 map 中安全获取布尔值 func getBoolValue(m map[string]interface{}, key string, defaultValue bool) bool { if val, ok := m[key].(bool); ok { return val } return defaultValue } // 辅助函数:从 map 中安全获取整数值 func getIntValue(m map[string]interface{}, key string, defaultValue int) int { if val, ok := m[key].(float64); ok { return int(val) } return defaultValue } // 辅助函数:将 []interface{} 转换为 []string func convertToStringArray(arr []interface{}) []string { result := make([]string, 0, len(arr)) for _, item := range arr { if str, ok := item.(string); ok { result = append(result, str) } } return result } // convertCardToCreateRequest 将角色卡转换为创建请求 func convertCardToCreateRequest(card *utils.CharacterCardV2, avatarData []byte, isPublic bool) request.CreateCharacterRequest { // 处理示例消息 exampleMessages := []string{} if card.Data.MesExample != "" { // 按 分割 parts := strings.Split(card.Data.MesExample, "") for _, msg := range parts { msg = strings.TrimSpace(msg) if msg != "" { exampleMessages = append(exampleMessages, msg) } } } // 备用问候语独立存储,不再合并到 exampleMessages alternateGreetings := card.Data.AlternateGreetings if alternateGreetings == nil { alternateGreetings = []string{} } // 保存头像到本地文件 avatar := "" if avatarData != nil { savedPath, err := saveAvatarFromBytes(avatarData, card.Data.Name, ".png") if err != nil { global.GVA_LOG.Warn("保存角色卡头像失败", zap.Error(err)) } else { avatar = savedPath } } return request.CreateCharacterRequest{ Name: card.Data.Name, Description: card.Data.Description, Personality: card.Data.Personality, Scenario: card.Data.Scenario, Avatar: avatar, CreatorName: card.Data.Creator, CreatorNotes: card.Data.CreatorNotes, FirstMessage: card.Data.FirstMes, ExampleMessages: exampleMessages, Tags: card.Data.Tags, IsPublic: isPublic, SystemPrompt: card.Data.SystemPrompt, PostHistoryInstructions: card.Data.PostHistoryInstructions, AlternateGreetings: alternateGreetings, CharacterBook: card.Data.CharacterBook, Extensions: card.Data.Extensions, } } // exportLinkedWorldBook 导出角色关联的世界书数据 func (cs *CharacterService) exportLinkedWorldBook(characterID uint) map[string]interface{} { // 查找关联的世界书 var worldBooks []app.AIWorldInfo err := global.GVA_DB. Where("? = ANY(linked_chars)", fmt.Sprintf("%d", characterID)). Find(&worldBooks).Error if err != nil || len(worldBooks) == 0 { return nil } // 合并所有世界书的条目 var allEntries []app.AIWorldInfoEntry var bookName string for _, book := range worldBooks { var entries []app.AIWorldInfoEntry if err := json.Unmarshal([]byte(book.Entries), &entries); err != nil { continue } allEntries = append(allEntries, entries...) if bookName == "" { bookName = book.BookName } } if len(allEntries) == 0 { return nil } // 转换为 CharacterBook 格式 entriesData := make([]map[string]interface{}, 0, len(allEntries)) for _, entry := range allEntries { entryMap := map[string]interface{}{ "uid": entry.UID, "keys": entry.Keys, "secondary_keys": entry.SecondaryKeys, "content": entry.Content, "comment": entry.Comment, "enabled": entry.Enabled, "constant": entry.Constant, "selective": entry.Selective, "insertion_order": entry.Order, "position": entry.Position, "depth": entry.Depth, "use_probability": entry.UseProbability, "probability": entry.Probability, "group": entry.Group, "group_override": entry.GroupOverride, "group_weight": entry.GroupWeight, "prevent_recursion": entry.PreventRecursion, "delay_until_recursion": entry.DelayUntilRecursion, "scan_depth": entry.ScanDepth, "case_sensitive": entry.CaseSensitive, "match_whole_words": entry.MatchWholeWords, "use_regex": entry.UseRegex, "automation_id": entry.Automation, "role": entry.Role, "vectorized": entry.VectorizedContent, } if entry.Extensions != nil { entryMap["extensions"] = entry.Extensions } entriesData = append(entriesData, entryMap) } return map[string]interface{}{ "name": bookName, "entries": entriesData, } } // convertCharacterToCard 将角色卡转换为 CharacterCardV2 func (cs *CharacterService) convertCharacterToCard(character *app.AICharacter) *utils.CharacterCardV2 { tags := []string{} if character.Tags != nil { tags = character.Tags } exampleMessages := []string{} if character.ExampleMessages != nil { exampleMessages = character.ExampleMessages } alternateGreetings := []string{} if character.AlternateGreetings != nil { alternateGreetings = character.AlternateGreetings } // 解析或构建 character_book JSON var characterBook map[string]interface{} if len(character.CharacterBook) > 0 { json.Unmarshal(character.CharacterBook, &characterBook) } // 如果角色没有内嵌的 CharacterBook,尝试从世界书表中查找关联的世界书 if characterBook == nil { characterBook = cs.exportLinkedWorldBook(character.ID) } // 解析或构建 extensions JSON extensions := map[string]interface{}{} if len(character.Extensions) > 0 { json.Unmarshal(character.Extensions, &extensions) } // 导出关联的正则脚本到 extensions if regexScripts := cs.exportRegexScripts(character.ID); regexScripts != nil && len(regexScripts) > 0 { extensions["regex_scripts"] = regexScripts } return &utils.CharacterCardV2{ Spec: "chara_card_v2", SpecVersion: "2.0", Data: utils.CharacterCardV2Data{ Name: character.Name, Description: character.Description, Personality: character.Personality, Scenario: character.Scenario, FirstMes: character.FirstMessage, MesExample: strings.Join(exampleMessages, "\n\n"), CreatorNotes: character.CreatorNotes, SystemPrompt: character.SystemPrompt, PostHistoryInstructions: character.PostHistoryInstructions, Tags: tags, Creator: character.CreatorName, CharacterVersion: fmt.Sprintf("%d", character.Version), AlternateGreetings: alternateGreetings, CharacterBook: characterBook, Extensions: extensions, }, } } // loadAvatarImage 从文件系统或 URL 加载头像 func loadAvatarImage(avatarPath string) (image.Image, error) { // 如果是 URL,暂时不支持 if strings.HasPrefix(avatarPath, "http://") || strings.HasPrefix(avatarPath, "https://") { return nil, errors.New("暂不支持从 URL 加载头像") } // 从文件系统加载 file, err := os.Open(avatarPath) if err != nil { return nil, fmt.Errorf("打开头像文件失败: %w", err) } defer file.Close() // 解码图片(自动检测格式) img, _, err := image.Decode(file) if err != nil { return nil, fmt.Errorf("解码头像图片失败: %w", err) } return img, nil } // createDefaultAvatar 创建默认头像 func createDefaultAvatar() image.Image { // 创建一个 400x533 的默认图片(3:4 比例) width, height := 400, 533 img := image.NewRGBA(image.Rect(0, 0, width, height)) // 填充渐变色(从紫色到蓝色) for y := 0; y < height; y++ { for x := 0; x < width; x++ { // 计算渐变颜色 r := uint8(102 + y*155/height) g := uint8(126 + y*138/height) b := uint8(234 - y*72/height) // 直接设置像素颜色 pix := img.Pix[y*img.Stride+x*4:] pix[0] = r pix[1] = g pix[2] = b pix[3] = 255 // 完全不透明 } } return img } // saveAvatarFromBytes 将头像原始字节数据保存到本地文件系统 func saveAvatarFromBytes(data []byte, name string, ext string) (string, error) { // 生成唯一文件名:MD5(name) + 时间戳 hash := md5.Sum([]byte(name)) hashStr := hex.EncodeToString(hash[:]) filename := hashStr + "_" + time.Now().Format("20060102150405") + ext storePath := global.GVA_CONFIG.Local.StorePath if err := os.MkdirAll(storePath, os.ModePerm); err != nil { return "", fmt.Errorf("创建存储目录失败: %w", err) } filePath := storePath + "/" + filename if err := os.WriteFile(filePath, data, 0644); err != nil { return "", fmt.Errorf("写入文件失败: %w", err) } // 返回访问路径 return global.GVA_CONFIG.Local.Path + "/" + filename, nil } // calculateTokenCount 计算角色卡的 Token 数量(简单估算) func calculateTokenCount(req interface{}) int { var text string switch v := req.(type) { case request.CreateCharacterRequest: text = v.Name + v.Description + v.Personality + v.Scenario + v.FirstMessage + v.SystemPrompt + v.PostHistoryInstructions for _, msg := range v.ExampleMessages { text += msg } case request.UpdateCharacterRequest: text = v.Name + v.Description + v.Personality + v.Scenario + v.FirstMessage + v.SystemPrompt + v.PostHistoryInstructions for _, msg := range v.ExampleMessages { text += msg } } // 简单估算:中文约 1.5 token/字,英文约 0.25 token/词 return len([]rune(text)) }