package app import ( "crypto/md5" "encoding/hex" "encoding/json" "errors" "fmt" "image" _ "image/jpeg" "os" "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" "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) } // 解析 extensions JSON extensions := map[string]interface{}{} if len(character.Extensions) > 0 { json.Unmarshal(character.Extensions, &extensions) } // 构建导出数据(兼容 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 } 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 := convertCharacterToCard(&character) // 获取角色头像 var img image.Image if character.Avatar != "" { // TODO: 从 URL 或文件系统加载头像 // 这里暂时创建一个默认图片 img = createDefaultAvatar() } else { img = createDefaultAvatar() } // 将角色卡数据嵌入到 PNG pngData, err := utils.EmbedCharacterToPNG(img, card) if err != nil { return nil, errors.New("生成 PNG 失败: " + err.Error()) } return pngData, nil } // 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, } } // convertCharacterToCard 将角色卡转换为 CharacterCardV2 func 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) } // 解析 extensions JSON extensions := map[string]interface{}{} if len(character.Extensions) > 0 { json.Unmarshal(character.Extensions, &extensions) } 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, }, } } // 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) img.Set(x, y, image.NewRGBA(image.Rect(0, 0, 1, 1)).At(0, 0)) img.SetRGBA(x, y, image.NewRGBA(image.Rect(0, 0, 1, 1)).RGBAAt(0, 0)) // 设置颜色 img.SetRGBA(x, y, image.NewRGBA(image.Rect(0, 0, 1, 1)).RGBAAt(0, 0)) 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)) }