🎨 重构用户端前端为vue开发,完善基础类和角色相关接口

This commit is contained in:
2026-02-10 21:55:45 +08:00
parent db934ebed7
commit 56e821b222
92 changed files with 18377 additions and 21 deletions

View File

@@ -0,0 +1,694 @@
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))
}