Files
st/server/service/app/character.go

695 lines
20 KiB
Go
Raw 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"
"image"
_ "image/jpeg"
"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"
"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
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,
}
cardDataJSON, _ := json.Marshal(cardData)
// 处理标签和示例消息
tags := req.Tags
if tags == nil {
tags = []string{}
}
exampleMessages := req.ExampleMessages
if exampleMessages == nil {
exampleMessages = []string{}
}
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,
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
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,
}
cardDataJSON, _ := json.Marshal(cardData)
// 处理标签和示例消息
tags := req.Tags
if tags == nil {
tags = []string{}
}
exampleMessages := req.ExampleMessages
if exampleMessages == nil {
exampleMessages = []string{}
}
// 更新
updates := map[string]interface{}{
"name": req.Name,
"description": req.Description,
"personality": req.Personality,
"scenario": req.Scenario,
"avatar": req.Avatar,
"creator_name": req.CreatorName,
"creator_notes": req.CreatorNotes,
"card_data": cardDataJSON,
"tags": tags,
"is_public": req.IsPublic,
"first_message": req.FirstMessage,
"example_messages": exampleMessages,
"token_count": calculateTokenCount(req),
"version": character.Version + 1,
}
err = global.GVA_DB.Model(&character).Updates(updates).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
}
// 构建导出数据(兼容 SillyTavern 格式)
exportData := map[string]interface{}{
"spec": "chara_card_v2",
"spec_version": "2.0",
"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<START>\n"),
"creator_notes": character.CreatorNotes,
"system_prompt": "",
"post_history_instructions": "",
"tags": tags,
"creator": character.CreatorName,
"character_version": character.Version,
"extensions": map[string]interface{}{},
},
}
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 != "" {
// 按 <START> 分割
exampleMessages = strings.Split(card.Data.MesExample, "<START>")
// 清理空白
cleaned := []string{}
for _, msg := range exampleMessages {
msg = strings.TrimSpace(msg)
if msg != "" {
cleaned = append(cleaned, msg)
}
}
exampleMessages = cleaned
}
// 合并备用问候语
if len(card.Data.AlternateGreetings) > 0 {
exampleMessages = append(exampleMessages, card.Data.AlternateGreetings...)
}
// TODO: 处理头像数据,上传到文件服务器
avatar := ""
if avatarData != nil {
// 这里应该将头像上传到文件服务器并获取 URL
// avatar = uploadAvatar(avatarData)
}
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,
}
}
// 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
}
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<START>\n"),
CreatorNotes: character.CreatorNotes,
SystemPrompt: "",
PostHistoryInstructions: "",
Tags: tags,
Creator: character.CreatorName,
CharacterVersion: fmt.Sprintf("%d", character.Version),
AlternateGreetings: []string{},
Extensions: map[string]interface{}{},
},
}
}
// 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
}
// 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
for _, msg := range v.ExampleMessages {
text += msg
}
case request.UpdateCharacterRequest:
text = v.Name + v.Description + v.Personality + v.Scenario + v.FirstMessage
for _, msg := range v.ExampleMessages {
text += msg
}
}
// 简单估算:中文约 1.5 token/字,英文约 0.25 token/词
return len([]rune(text))
}