🎨 优化角色卡功能模块,后续待优化图像上传&前端流畅性待优化

This commit is contained in:
2026-02-11 05:33:38 +08:00
parent 56e821b222
commit cf3197929e
12 changed files with 576 additions and 198 deletions

View File

@@ -1,12 +1,16 @@
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"
@@ -192,16 +196,21 @@ func (cs *CharacterService) GetCharacterDetail(characterID uint, userID *uint) (
// CreateCharacter 创建角色卡
func (cs *CharacterService) CreateCharacter(req request.CreateCharacterRequest, userID uint) (response.CharacterResponse, error) {
// 构建 CardData
// 构建 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,
"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)
@@ -216,21 +225,40 @@ func (cs *CharacterService) CreateCharacter(req request.CreateCharacterRequest,
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,
TokenCount: calculateTokenCount(req),
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
@@ -257,20 +285,25 @@ func (cs *CharacterService) UpdateCharacter(req request.UpdateCharacterRequest,
return response.CharacterResponse{}, errors.New("无权修改")
}
// 构建 CardData
// 构建 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,
"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{}
@@ -281,25 +314,42 @@ func (cs *CharacterService) UpdateCharacter(req request.UpdateCharacterRequest,
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,
alternateGreetings := req.AlternateGreetings
if alternateGreetings == nil {
alternateGreetings = []string{}
}
err = global.GVA_DB.Model(&character).Updates(updates).Error
// 序列化 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
}
@@ -436,25 +486,50 @@ func (cs *CharacterService) ExportCharacter(characterID uint, userID *uint) (map
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<START>\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": 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{}{},
},
"data": data,
}
return exportData, nil
@@ -572,42 +647,49 @@ func convertCardToCreateRequest(card *utils.CharacterCardV2, avatarData []byte,
exampleMessages := []string{}
if card.Data.MesExample != "" {
// 按 <START> 分割
exampleMessages = strings.Split(card.Data.MesExample, "<START>")
// 清理空白
cleaned := []string{}
for _, msg := range exampleMessages {
parts := strings.Split(card.Data.MesExample, "<START>")
for _, msg := range parts {
msg = strings.TrimSpace(msg)
if msg != "" {
cleaned = append(cleaned, msg)
exampleMessages = append(exampleMessages, msg)
}
}
exampleMessages = cleaned
}
// 合并备用问候语
if len(card.Data.AlternateGreetings) > 0 {
exampleMessages = append(exampleMessages, card.Data.AlternateGreetings...)
// 备用问候语独立存储,不再合并到 exampleMessages
alternateGreetings := card.Data.AlternateGreetings
if alternateGreetings == nil {
alternateGreetings = []string{}
}
// TODO: 处理头像数据,上传到文件服务器
// 保存头像到本地文件
avatar := ""
if avatarData != nil {
// 这里应该将头像上传到文件服务器并获取 URL
// avatar = uploadAvatar(avatarData)
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,
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,
}
}
@@ -623,6 +705,23 @@ func convertCharacterToCard(character *app.AICharacter) *utils.CharacterCardV2 {
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",
@@ -634,13 +733,14 @@ func convertCharacterToCard(character *app.AICharacter) *utils.CharacterCardV2 {
FirstMes: character.FirstMessage,
MesExample: strings.Join(exampleMessages, "\n<START>\n"),
CreatorNotes: character.CreatorNotes,
SystemPrompt: "",
PostHistoryInstructions: "",
SystemPrompt: character.SystemPrompt,
PostHistoryInstructions: character.PostHistoryInstructions,
Tags: tags,
Creator: character.CreatorName,
CharacterVersion: fmt.Sprintf("%d", character.Version),
AlternateGreetings: []string{},
Extensions: map[string]interface{}{},
AlternateGreetings: alternateGreetings,
CharacterBook: characterBook,
Extensions: extensions,
},
}
}
@@ -673,17 +773,38 @@ func createDefaultAvatar() image.Image {
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
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
text = v.Name + v.Description + v.Personality + v.Scenario + v.FirstMessage + v.SystemPrompt + v.PostHistoryInstructions
for _, msg := range v.ExampleMessages {
text += msg
}