🎉 初始化项目

Signed-off-by: Echo <1711788888@qq.com>
This commit is contained in:
2026-02-27 21:52:00 +08:00
commit f4e166c5ee
482 changed files with 55079 additions and 0 deletions

View File

@@ -0,0 +1,366 @@
package app
import (
"encoding/base64"
"encoding/json"
"errors"
"mime/multipart"
"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"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type CharacterService struct{}
// CreateCharacter 创建角色卡
func (s *CharacterService) CreateCharacter(userID uint, req *request.CreateCharacterRequest) (*response.CharacterResponse, error) {
// 序列化 JSON 字段
tagsJSON, _ := json.Marshal(req.Tags)
alternateGreetingsJSON, _ := json.Marshal(req.AlternateGreetings)
characterBookJSON, _ := json.Marshal(req.CharacterBook)
extensionsJSON, _ := json.Marshal(req.Extensions)
character := app.AICharacter{
UserID: userID,
Name: req.Name,
Avatar: req.Avatar,
Creator: req.Creator,
Version: req.Version,
Description: req.Description,
Personality: req.Personality,
Scenario: req.Scenario,
FirstMes: req.FirstMes,
MesExample: req.MesExample,
CreatorNotes: req.CreatorNotes,
SystemPrompt: req.SystemPrompt,
PostHistoryInstructions: req.PostHistoryInstructions,
Tags: datatypes.JSON(tagsJSON),
AlternateGreetings: datatypes.JSON(alternateGreetingsJSON),
CharacterBook: datatypes.JSON(characterBookJSON),
Extensions: datatypes.JSON(extensionsJSON),
IsPublic: req.IsPublic,
Spec: "chara_card_v2",
SpecVersion: "2.0",
}
err := global.GVA_DB.Create(&character).Error
if err != nil {
return nil, err
}
resp := response.ToCharacterResponse(&character)
return &resp, nil
}
// GetCharacterList 获取角色卡列表
func (s *CharacterService) GetCharacterList(userID uint, req *request.GetCharacterListRequest) (*response.CharacterListResponse, error) {
var characters []app.AICharacter
var total int64
db := global.GVA_DB.Model(&app.AICharacter{})
// 筛选条件
if req.IsPublic != nil {
if *req.IsPublic {
db = db.Where("is_public = ?", true)
} else {
db = db.Where("user_id = ?", userID)
}
} else {
db = db.Where("user_id = ? OR is_public = ?", userID, true)
}
// 关键词搜索
if req.Keyword != "" {
db = db.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 标签筛选
if req.Tag != "" {
db = db.Where("tags @> ?", datatypes.JSON(`["`+req.Tag+`"]`))
}
// 统计总数
err := db.Count(&total).Error
if err != nil {
return nil, err
}
// 分页查询
offset := (req.Page - 1) * req.PageSize
err = db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&characters).Error
if err != nil {
return nil, err
}
// 转换响应
list := make([]response.CharacterResponse, len(characters))
for i, char := range characters {
list[i] = response.ToCharacterResponse(&char)
}
return &response.CharacterListResponse{
List: list,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// GetCharacterByID 获取角色卡详情
func (s *CharacterService) GetCharacterByID(userID, characterID uint) (*response.CharacterResponse, error) {
var character app.AICharacter
err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", characterID, userID, true).
First(&character).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("角色卡不存在或无权访问")
}
return nil, err
}
resp := response.ToCharacterResponse(&character)
return &resp, nil
}
// UpdateCharacter 更新角色卡
func (s *CharacterService) UpdateCharacter(userID, characterID uint, req *request.UpdateCharacterRequest) error {
var character app.AICharacter
// 检查权限
err := global.GVA_DB.Where("id = ? AND user_id = ?", characterID, userID).First(&character).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("角色卡不存在或无权修改")
}
return err
}
// 更新字段
updates := map[string]interface{}{}
if req.Name != "" {
updates["name"] = req.Name
}
if req.Avatar != "" {
updates["avatar"] = req.Avatar
}
if req.Creator != "" {
updates["creator"] = req.Creator
}
if req.Version != "" {
updates["version"] = req.Version
}
if req.Description != "" {
updates["description"] = req.Description
}
if req.Personality != "" {
updates["personality"] = req.Personality
}
if req.Scenario != "" {
updates["scenario"] = req.Scenario
}
if req.FirstMes != "" {
updates["first_mes"] = req.FirstMes
}
if req.MesExample != "" {
updates["mes_example"] = req.MesExample
}
if req.CreatorNotes != "" {
updates["creator_notes"] = req.CreatorNotes
}
if req.SystemPrompt != "" {
updates["system_prompt"] = req.SystemPrompt
}
if req.PostHistoryInstructions != "" {
updates["post_history_instructions"] = req.PostHistoryInstructions
}
if req.Tags != nil {
tagsJSON, _ := json.Marshal(req.Tags)
updates["tags"] = datatypes.JSON(tagsJSON)
}
if req.AlternateGreetings != nil {
alternateGreetingsJSON, _ := json.Marshal(req.AlternateGreetings)
updates["alternate_greetings"] = datatypes.JSON(alternateGreetingsJSON)
}
if req.CharacterBook != nil {
characterBookJSON, _ := json.Marshal(req.CharacterBook)
updates["character_book"] = datatypes.JSON(characterBookJSON)
}
if req.Extensions != nil {
extensionsJSON, _ := json.Marshal(req.Extensions)
updates["extensions"] = datatypes.JSON(extensionsJSON)
}
updates["is_public"] = req.IsPublic
return global.GVA_DB.Model(&character).Updates(updates).Error
}
// DeleteCharacter 删除角色卡
func (s *CharacterService) DeleteCharacter(userID, characterID uint) error {
result := global.GVA_DB.Where("id = ? AND user_id = ?", characterID, userID).Delete(&app.AICharacter{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("角色卡不存在或无权删除")
}
return nil
}
// ImportCharacterFromPNG 从 PNG 文件导入角色卡
func (s *CharacterService) ImportCharacterFromPNG(userID uint, file *multipart.FileHeader) (*response.CharacterResponse, error) {
// 读取文件内容
src, err := file.Open()
if err != nil {
return nil, errors.New("打开文件失败")
}
defer src.Close()
// 读取文件数据
fileData := make([]byte, file.Size)
_, err = src.Read(fileData)
if err != nil {
return nil, errors.New("读取文件失败")
}
// 提取角色卡数据
card, err := utils.ExtractCharacterFromPNG(fileData)
if err != nil {
return nil, err
}
// 上传 PNG 图片到 OSS替代 Base64
var uploadService UploadService
avatarURL, err := uploadService.UploadImage(file)
if err != nil {
// 如果上传失败,回退到 Base64向后兼容
avatarURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(fileData)
}
// 转换为创建请求
req := &request.CreateCharacterRequest{
Name: card.Data.Name,
Avatar: avatarURL,
Creator: card.Data.Creator,
Version: card.Data.CharacterVersion,
Description: card.Data.Description,
Personality: card.Data.Personality,
Scenario: card.Data.Scenario,
FirstMes: card.Data.FirstMes,
MesExample: card.Data.MesExample,
CreatorNotes: card.Data.CreatorNotes,
SystemPrompt: card.Data.SystemPrompt,
PostHistoryInstructions: card.Data.PostHistoryInstructions,
Tags: card.Data.Tags,
AlternateGreetings: card.Data.AlternateGreetings,
CharacterBook: card.Data.CharacterBook,
Extensions: card.Data.Extensions,
IsPublic: false,
}
return s.CreateCharacter(userID, req)
}
// ImportCharacterFromJSON 从 JSON 文件导入角色卡
func (s *CharacterService) ImportCharacterFromJSON(userID uint, file *multipart.FileHeader) (*response.CharacterResponse, error) {
// 读取文件内容
src, err := file.Open()
if err != nil {
return nil, errors.New("打开文件失败")
}
defer src.Close()
// 读取文件数据
fileData := make([]byte, file.Size)
_, err = src.Read(fileData)
if err != nil {
return nil, errors.New("读取文件失败")
}
// 解析 JSON
card, err := utils.ParseCharacterCardJSON(fileData)
if err != nil {
return nil, err
}
// 转换为创建请求
req := &request.CreateCharacterRequest{
Name: card.Data.Name,
Creator: card.Data.Creator,
Version: card.Data.CharacterVersion,
Description: card.Data.Description,
Personality: card.Data.Personality,
Scenario: card.Data.Scenario,
FirstMes: card.Data.FirstMes,
MesExample: card.Data.MesExample,
CreatorNotes: card.Data.CreatorNotes,
SystemPrompt: card.Data.SystemPrompt,
PostHistoryInstructions: card.Data.PostHistoryInstructions,
Tags: card.Data.Tags,
AlternateGreetings: card.Data.AlternateGreetings,
CharacterBook: card.Data.CharacterBook,
Extensions: card.Data.Extensions,
IsPublic: false,
}
return s.CreateCharacter(userID, req)
}
// ExportCharacterToJSON 导出角色卡为 JSON
func (s *CharacterService) ExportCharacterToJSON(userID, characterID uint) (*utils.CharacterCardV2, error) {
var character app.AICharacter
err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", characterID, userID, true).
First(&character).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("角色卡不存在或无权访问")
}
return nil, err
}
// 解析 JSON 字段
var tags []string
var alternateGreetings []string
var characterBook map[string]interface{}
var extensions map[string]interface{}
json.Unmarshal(character.Tags, &tags)
json.Unmarshal(character.AlternateGreetings, &alternateGreetings)
json.Unmarshal(character.CharacterBook, &characterBook)
json.Unmarshal(character.Extensions, &extensions)
// 构建 V2 格式
card := &utils.CharacterCardV2{
Spec: character.Spec,
SpecVersion: character.SpecVersion,
Data: utils.CharacterCardV2Data{
Name: character.Name,
Description: character.Description,
Personality: character.Personality,
Scenario: character.Scenario,
FirstMes: character.FirstMes,
MesExample: character.MesExample,
CreatorNotes: character.CreatorNotes,
SystemPrompt: character.SystemPrompt,
PostHistoryInstructions: character.PostHistoryInstructions,
Tags: tags,
Creator: character.Creator,
CharacterVersion: character.Version,
AlternateGreetings: alternateGreetings,
CharacterBook: characterBook,
Extensions: extensions,
},
}
return card, nil
}