1455 lines
44 KiB
Go
1455 lines
44 KiB
Go
package app
|
||
|
||
import (
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"image"
|
||
_ "image/jpeg"
|
||
_ "image/png"
|
||
"os"
|
||
"regexp"
|
||
"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"
|
||
"github.com/lib/pq"
|
||
"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)
|
||
}
|
||
|
||
// 如果角色没有内嵌的 CharacterBook,尝试从世界书表中查找关联的世界书
|
||
if characterBook == nil {
|
||
characterBook = cs.exportLinkedWorldBook(character.ID)
|
||
}
|
||
|
||
// 解析或构建 extensions JSON
|
||
extensions := map[string]interface{}{}
|
||
if len(character.Extensions) > 0 {
|
||
json.Unmarshal(character.Extensions, &extensions)
|
||
}
|
||
|
||
// 导出关联的正则脚本到 extensions
|
||
if regexScripts := cs.exportRegexScripts(character.ID); regexScripts != nil && len(regexScripts) > 0 {
|
||
extensions["regex_scripts"] = regexScripts
|
||
}
|
||
|
||
// 构建导出数据(兼容 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": 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
|
||
}
|
||
|
||
// 处理角色卡中的世界书数据(CharacterBook)
|
||
if card.Data.CharacterBook != nil && len(card.Data.CharacterBook) > 0 {
|
||
global.GVA_LOG.Info("检测到角色卡包含世界书数据,开始导入世界书",
|
||
zap.Uint("characterID", result.ID))
|
||
|
||
if err := cs.importCharacterBook(userID, result.ID, card.Data.CharacterBook); err != nil {
|
||
global.GVA_LOG.Warn("导入世界书失败(不影响角色卡导入)",
|
||
zap.Error(err),
|
||
zap.Uint("characterID", result.ID))
|
||
} else {
|
||
global.GVA_LOG.Info("世界书导入成功", zap.Uint("characterID", result.ID))
|
||
}
|
||
}
|
||
|
||
// 处理角色卡中的扩展数据(Extensions)
|
||
if card.Data.Extensions != nil && len(card.Data.Extensions) > 0 {
|
||
global.GVA_LOG.Info("检测到角色卡包含扩展数据,开始处理扩展",
|
||
zap.Uint("characterID", result.ID))
|
||
|
||
// 处理 Regex 脚本
|
||
if regexScripts, ok := card.Data.Extensions["regex_scripts"]; ok {
|
||
if err := cs.importRegexScripts(userID, result.ID, regexScripts); err != nil {
|
||
global.GVA_LOG.Warn("导入正则脚本失败(不影响角色卡导入)",
|
||
zap.Error(err),
|
||
zap.Uint("characterID", result.ID))
|
||
} else {
|
||
global.GVA_LOG.Info("正则脚本导入成功", zap.Uint("characterID", result.ID))
|
||
}
|
||
}
|
||
|
||
// 其他扩展数据已经存储在 Extensions 字段中,无需额外处理
|
||
}
|
||
|
||
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 := cs.convertCharacterToCard(&character)
|
||
|
||
// 获取角色头像
|
||
var img image.Image
|
||
var loadErr error
|
||
|
||
if character.Avatar != "" {
|
||
// 尝试从文件系统或 URL 加载头像
|
||
img, loadErr = loadAvatarImage(character.Avatar)
|
||
if loadErr != nil {
|
||
global.GVA_LOG.Warn("加载角色头像失败,使用默认头像",
|
||
zap.String("avatar", character.Avatar),
|
||
zap.Error(loadErr))
|
||
img = createDefaultAvatar()
|
||
}
|
||
} else {
|
||
img = createDefaultAvatar()
|
||
}
|
||
|
||
// 将角色卡数据嵌入到 PNG
|
||
pngData, err := utils.EmbedCharacterToPNG(img, card)
|
||
if err != nil {
|
||
global.GVA_LOG.Error("生成 PNG 失败", zap.Error(err))
|
||
return nil, errors.New("生成 PNG 失败: " + err.Error())
|
||
}
|
||
|
||
global.GVA_LOG.Info("PNG 导出成功",
|
||
zap.Uint("characterID", characterID),
|
||
zap.Int("size", len(pngData)))
|
||
|
||
return pngData, nil
|
||
}
|
||
|
||
// createCharacterFromRequest 从请求创建角色卡对象(用于事务)
|
||
func createCharacterFromRequest(req request.CreateCharacterRequest, userID uint) app.AICharacter {
|
||
// 处理标签和示例消息
|
||
tags := req.Tags
|
||
if tags == nil {
|
||
tags = []string{}
|
||
}
|
||
|
||
exampleMessages := req.ExampleMessages
|
||
if exampleMessages == nil {
|
||
exampleMessages = []string{}
|
||
}
|
||
|
||
alternateGreetings := req.AlternateGreetings
|
||
if alternateGreetings == nil {
|
||
alternateGreetings = []string{}
|
||
}
|
||
|
||
// 构建 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,
|
||
"system_prompt": req.SystemPrompt,
|
||
"post_history_instructions": req.PostHistoryInstructions,
|
||
"alternate_greetings": req.AlternateGreetings,
|
||
"character_book": req.CharacterBook,
|
||
"extensions": req.Extensions,
|
||
}
|
||
cardDataJSON, _ := json.Marshal(cardData)
|
||
|
||
// 序列化 JSON 字段
|
||
var characterBookJSON, extensionsJSON datatypes.JSON
|
||
if req.CharacterBook != nil {
|
||
characterBookJSON, _ = json.Marshal(req.CharacterBook)
|
||
}
|
||
if req.Extensions != nil {
|
||
extensionsJSON, _ = json.Marshal(req.Extensions)
|
||
}
|
||
|
||
return 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),
|
||
}
|
||
}
|
||
|
||
// importCharacterBookWithTx 在事务中导入角色卡中的世界书数据
|
||
func (cs *CharacterService) importCharacterBookWithTx(tx *gorm.DB, userID, characterID uint, characterBook map[string]interface{}) error {
|
||
// 解析世界书名称
|
||
bookName := ""
|
||
if name, ok := characterBook["name"].(string); ok && name != "" {
|
||
bookName = name
|
||
}
|
||
|
||
// 如果没有名称,使用角色名称
|
||
if bookName == "" {
|
||
var character app.AICharacter
|
||
if err := tx.Where("id = ?", characterID).First(&character).Error; err == nil {
|
||
bookName = character.Name + " 的世界书"
|
||
} else {
|
||
bookName = "角色世界书"
|
||
}
|
||
}
|
||
|
||
// 解析世界书条目
|
||
entries := []app.AIWorldInfoEntry{}
|
||
if entriesData, ok := characterBook["entries"].([]interface{}); ok {
|
||
for i, entryData := range entriesData {
|
||
if entryMap, ok := entryData.(map[string]interface{}); ok {
|
||
entry := convertToWorldInfoEntry(entryMap, i)
|
||
entries = append(entries, entry)
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(entries) == 0 {
|
||
global.GVA_LOG.Warn("角色卡中的世界书没有有效条目,跳过导入")
|
||
return nil // 没有条目时不报错,只是跳过
|
||
}
|
||
|
||
// 序列化条目
|
||
entriesJSON, err := json.Marshal(entries)
|
||
if err != nil {
|
||
return errors.New("序列化世界书条目失败: " + err.Error())
|
||
}
|
||
|
||
// 创建世界书记录
|
||
worldBook := &app.AIWorldInfo{
|
||
UserID: userID,
|
||
BookName: bookName,
|
||
IsGlobal: false,
|
||
Entries: datatypes.JSON(entriesJSON),
|
||
LinkedChars: pq.StringArray{fmt.Sprintf("%d", characterID)},
|
||
}
|
||
|
||
if err := tx.Create(worldBook).Error; err != nil {
|
||
return errors.New("创建世界书记录失败: " + err.Error())
|
||
}
|
||
|
||
global.GVA_LOG.Info("成功从角色卡导入世界书",
|
||
zap.Uint("worldBookID", worldBook.ID),
|
||
zap.String("bookName", bookName),
|
||
zap.Int("entriesCount", len(entries)))
|
||
|
||
return nil
|
||
}
|
||
|
||
// importRegexScripts 导入角色卡中的正则脚本
|
||
func (cs *CharacterService) importRegexScripts(userID, characterID uint, regexScriptsData interface{}) error {
|
||
scriptsArray, ok := regexScriptsData.([]interface{})
|
||
if !ok {
|
||
return errors.New("正则脚本数据格式错误")
|
||
}
|
||
|
||
if len(scriptsArray) == 0 {
|
||
global.GVA_LOG.Info("角色卡中没有正则脚本数据")
|
||
return nil
|
||
}
|
||
|
||
characterIDStr := fmt.Sprintf("%d", characterID)
|
||
imported := 0
|
||
|
||
for i, scriptData := range scriptsArray {
|
||
scriptMap, ok := scriptData.(map[string]interface{})
|
||
if !ok {
|
||
global.GVA_LOG.Warn("跳过无效的正则脚本数据", zap.Int("index", i))
|
||
continue
|
||
}
|
||
|
||
// 解析正则脚本
|
||
script := convertMapToRegexScript(scriptMap, characterIDStr)
|
||
script.UserID = userID
|
||
|
||
// 验证正则表达式
|
||
if _, err := regexp.Compile(script.FindRegex); err != nil {
|
||
global.GVA_LOG.Warn("跳过无效的正则表达式",
|
||
zap.Int("index", i),
|
||
zap.String("regex", script.FindRegex),
|
||
zap.Error(err))
|
||
continue
|
||
}
|
||
|
||
// 检查是否已存在同名脚本
|
||
var existingCount int64
|
||
global.GVA_DB.Model(&app.AIRegexScript{}).
|
||
Where("user_id = ? AND script_name = ?", userID, script.ScriptName).
|
||
Count(&existingCount)
|
||
|
||
if existingCount > 0 {
|
||
script.ScriptName = script.ScriptName + fmt.Sprintf(" (角色-%d)", characterID)
|
||
}
|
||
|
||
// 创建脚本
|
||
if err := global.GVA_DB.Create(&script).Error; err != nil {
|
||
global.GVA_LOG.Warn("创建正则脚本失败",
|
||
zap.Int("index", i),
|
||
zap.Error(err))
|
||
continue
|
||
}
|
||
|
||
imported++
|
||
}
|
||
|
||
global.GVA_LOG.Info("成功导入正则脚本",
|
||
zap.Uint("characterID", characterID),
|
||
zap.Int("imported", imported))
|
||
|
||
return nil
|
||
}
|
||
|
||
// convertMapToRegexScript 将 map 转换为 RegexScript
|
||
func convertMapToRegexScript(scriptMap map[string]interface{}, characterIDStr string) app.AIRegexScript {
|
||
script := app.AIRegexScript{
|
||
ScriptName: getStringValue(scriptMap, "scriptName", "未命名脚本"),
|
||
Description: getStringValue(scriptMap, "description", ""),
|
||
FindRegex: getStringValue(scriptMap, "findRegex", ""),
|
||
ReplaceString: getStringValue(scriptMap, "replaceString", ""),
|
||
Enabled: getBoolValue(scriptMap, "enabled", true),
|
||
IsGlobal: false, // 从角色卡导入的脚本默认不是全局脚本
|
||
TrimStrings: getBoolValue(scriptMap, "trimStrings", false),
|
||
OnlyFormat: getBoolValue(scriptMap, "onlyFormat", false),
|
||
RunOnEdit: getBoolValue(scriptMap, "runOnEdit", false),
|
||
SubstituteRegex: getBoolValue(scriptMap, "substituteRegex", false),
|
||
Placement: getStringValue(scriptMap, "placement", ""),
|
||
LinkedChars: pq.StringArray{characterIDStr},
|
||
}
|
||
|
||
// 处理可选的数字字段
|
||
if val, ok := scriptMap["minDepth"]; ok {
|
||
if intVal := getIntValue(scriptMap, "minDepth", 0); intVal != 0 {
|
||
script.MinDepth = &intVal
|
||
} else if val != nil {
|
||
intVal := 0
|
||
script.MinDepth = &intVal
|
||
}
|
||
}
|
||
if val, ok := scriptMap["maxDepth"]; ok {
|
||
if intVal := getIntValue(scriptMap, "maxDepth", 0); intVal != 0 {
|
||
script.MaxDepth = &intVal
|
||
} else if val != nil {
|
||
intVal := 0
|
||
script.MaxDepth = &intVal
|
||
}
|
||
}
|
||
if val, ok := scriptMap["affectMinDepth"]; ok {
|
||
if intVal := getIntValue(scriptMap, "affectMinDepth", 0); intVal != 0 {
|
||
script.AffectMinDepth = &intVal
|
||
} else if val != nil {
|
||
intVal := 0
|
||
script.AffectMinDepth = &intVal
|
||
}
|
||
}
|
||
if val, ok := scriptMap["affectMaxDepth"]; ok {
|
||
if intVal := getIntValue(scriptMap, "affectMaxDepth", 0); intVal != 0 {
|
||
script.AffectMaxDepth = &intVal
|
||
} else if val != nil {
|
||
intVal := 0
|
||
script.AffectMaxDepth = &intVal
|
||
}
|
||
}
|
||
|
||
// 处理 ScriptData
|
||
if scriptData, ok := scriptMap["scriptData"].(map[string]interface{}); ok && scriptData != nil {
|
||
if data, err := datatypes.NewJSONType(scriptData).MarshalJSON(); err == nil {
|
||
script.ScriptData = data
|
||
}
|
||
}
|
||
|
||
return script
|
||
}
|
||
|
||
// exportRegexScripts 导出角色关联的正则脚本
|
||
func (cs *CharacterService) exportRegexScripts(characterID uint) []map[string]interface{} {
|
||
// 查找关联的正则脚本
|
||
var scripts []app.AIRegexScript
|
||
err := global.GVA_DB.
|
||
Where("? = ANY(linked_chars)", fmt.Sprintf("%d", characterID)).
|
||
Find(&scripts).Error
|
||
|
||
if err != nil || len(scripts) == 0 {
|
||
return nil
|
||
}
|
||
|
||
// 转换为 map 格式
|
||
scriptsData := make([]map[string]interface{}, 0, len(scripts))
|
||
for _, script := range scripts {
|
||
scriptMap := map[string]interface{}{
|
||
"scriptName": script.ScriptName,
|
||
"description": script.Description,
|
||
"findRegex": script.FindRegex,
|
||
"replaceString": script.ReplaceString,
|
||
"enabled": script.Enabled,
|
||
"trimStrings": script.TrimStrings,
|
||
"onlyFormat": script.OnlyFormat,
|
||
"runOnEdit": script.RunOnEdit,
|
||
"substituteRegex": script.SubstituteRegex,
|
||
"placement": script.Placement,
|
||
}
|
||
|
||
// 添加可选字段
|
||
if script.MinDepth != nil {
|
||
scriptMap["minDepth"] = *script.MinDepth
|
||
}
|
||
if script.MaxDepth != nil {
|
||
scriptMap["maxDepth"] = *script.MaxDepth
|
||
}
|
||
if script.AffectMinDepth != nil {
|
||
scriptMap["affectMinDepth"] = *script.AffectMinDepth
|
||
}
|
||
if script.AffectMaxDepth != nil {
|
||
scriptMap["affectMaxDepth"] = *script.AffectMaxDepth
|
||
}
|
||
|
||
// 添加 ScriptData
|
||
if len(script.ScriptData) > 0 {
|
||
var scriptData map[string]interface{}
|
||
if err := json.Unmarshal([]byte(script.ScriptData), &scriptData); err == nil {
|
||
scriptMap["scriptData"] = scriptData
|
||
}
|
||
}
|
||
|
||
scriptsData = append(scriptsData, scriptMap)
|
||
}
|
||
|
||
return scriptsData
|
||
}
|
||
|
||
// importCharacterBook 导入角色卡中的世界书数据(已废弃,使用 importCharacterBookWithTx)
|
||
func (cs *CharacterService) importCharacterBook(userID, characterID uint, characterBook map[string]interface{}) error {
|
||
// 解析世界书名称
|
||
bookName := "角色世界书"
|
||
if name, ok := characterBook["name"].(string); ok && name != "" {
|
||
bookName = name
|
||
} else {
|
||
// 获取角色名称作为世界书名称
|
||
var character app.AICharacter
|
||
if err := global.GVA_DB.Where("id = ?", characterID).First(&character).Error; err == nil {
|
||
bookName = character.Name + " 的世界书"
|
||
}
|
||
}
|
||
|
||
// 解析世界书条目
|
||
entries := []app.AIWorldInfoEntry{}
|
||
if entriesData, ok := characterBook["entries"].([]interface{}); ok {
|
||
for i, entryData := range entriesData {
|
||
if entryMap, ok := entryData.(map[string]interface{}); ok {
|
||
entry := convertToWorldInfoEntry(entryMap, i)
|
||
entries = append(entries, entry)
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(entries) == 0 {
|
||
return errors.New("世界书中没有有效的条目")
|
||
}
|
||
|
||
// 序列化条目
|
||
entriesJSON, err := json.Marshal(entries)
|
||
if err != nil {
|
||
return errors.New("序列化世界书条目失败: " + err.Error())
|
||
}
|
||
|
||
// 创建世界书记录
|
||
worldBook := &app.AIWorldInfo{
|
||
UserID: userID,
|
||
BookName: bookName,
|
||
IsGlobal: false,
|
||
Entries: datatypes.JSON(entriesJSON),
|
||
LinkedChars: pq.StringArray{fmt.Sprintf("%d", characterID)},
|
||
}
|
||
|
||
if err := global.GVA_DB.Create(worldBook).Error; err != nil {
|
||
return errors.New("创建世界书记录失败: " + err.Error())
|
||
}
|
||
|
||
global.GVA_LOG.Info("成功从角色卡导入世界书",
|
||
zap.Uint("worldBookID", worldBook.ID),
|
||
zap.String("bookName", bookName),
|
||
zap.Int("entriesCount", len(entries)))
|
||
|
||
return nil
|
||
}
|
||
|
||
// convertToWorldInfoEntry 将角色卡中的世界书条目转换为标准格式
|
||
func convertToWorldInfoEntry(entryMap map[string]interface{}, index int) app.AIWorldInfoEntry {
|
||
entry := app.AIWorldInfoEntry{
|
||
UID: getStringValue(entryMap, "uid", fmt.Sprintf("entry_%d", index)),
|
||
Enabled: getBoolValue(entryMap, "enabled", true),
|
||
Order: getIntValue(entryMap, "insertion_order", index),
|
||
Content: getStringValue(entryMap, "content", ""),
|
||
Comment: getStringValue(entryMap, "comment", ""),
|
||
}
|
||
|
||
// 解析关键词
|
||
if keys, ok := entryMap["keys"].([]interface{}); ok {
|
||
entry.Keys = convertToStringArray(keys)
|
||
}
|
||
|
||
if secondaryKeys, ok := entryMap["secondary_keys"].([]interface{}); ok {
|
||
entry.SecondaryKeys = convertToStringArray(secondaryKeys)
|
||
}
|
||
|
||
// 高级选项
|
||
entry.Constant = getBoolValue(entryMap, "constant", false)
|
||
entry.Selective = getBoolValue(entryMap, "selective", false)
|
||
entry.Position = getStringValue(entryMap, "position", "before_char")
|
||
|
||
if depth, ok := entryMap["depth"].(float64); ok {
|
||
entry.Depth = int(depth)
|
||
}
|
||
|
||
// 概率设置
|
||
entry.UseProbability = getBoolValue(entryMap, "use_probability", false)
|
||
if prob, ok := entryMap["probability"].(float64); ok {
|
||
entry.Probability = int(prob)
|
||
}
|
||
|
||
// 分组设置
|
||
entry.Group = getStringValue(entryMap, "group", "")
|
||
entry.GroupOverride = getBoolValue(entryMap, "group_override", false)
|
||
if weight, ok := entryMap["group_weight"].(float64); ok {
|
||
entry.GroupWeight = int(weight)
|
||
}
|
||
|
||
// 递归设置
|
||
entry.PreventRecursion = getBoolValue(entryMap, "prevent_recursion", false)
|
||
entry.DelayUntilRecursion = getBoolValue(entryMap, "delay_until_recursion", false)
|
||
|
||
// 扫描深度
|
||
if scanDepth, ok := entryMap["scan_depth"].(float64); ok {
|
||
depth := int(scanDepth)
|
||
entry.ScanDepth = &depth
|
||
}
|
||
|
||
// 匹配选项
|
||
if caseSensitive, ok := entryMap["case_sensitive"].(bool); ok {
|
||
entry.CaseSensitive = &caseSensitive
|
||
}
|
||
if matchWholeWords, ok := entryMap["match_whole_words"].(bool); ok {
|
||
entry.MatchWholeWords = &matchWholeWords
|
||
}
|
||
if useRegex, ok := entryMap["use_regex"].(bool); ok {
|
||
entry.UseRegex = &useRegex
|
||
}
|
||
|
||
// 其他字段
|
||
entry.Automation = getStringValue(entryMap, "automation_id", "")
|
||
entry.Role = getStringValue(entryMap, "role", "")
|
||
entry.VectorizedContent = getStringValue(entryMap, "vectorized", "")
|
||
|
||
// 扩展数据
|
||
if extensions, ok := entryMap["extensions"].(map[string]interface{}); ok {
|
||
entry.Extensions = extensions
|
||
}
|
||
|
||
return entry
|
||
}
|
||
|
||
// 辅助函数:从 map 中安全获取字符串值
|
||
func getStringValue(m map[string]interface{}, key, defaultValue string) string {
|
||
if val, ok := m[key].(string); ok {
|
||
return val
|
||
}
|
||
return defaultValue
|
||
}
|
||
|
||
// 辅助函数:从 map 中安全获取布尔值
|
||
func getBoolValue(m map[string]interface{}, key string, defaultValue bool) bool {
|
||
if val, ok := m[key].(bool); ok {
|
||
return val
|
||
}
|
||
return defaultValue
|
||
}
|
||
|
||
// 辅助函数:从 map 中安全获取整数值
|
||
func getIntValue(m map[string]interface{}, key string, defaultValue int) int {
|
||
if val, ok := m[key].(float64); ok {
|
||
return int(val)
|
||
}
|
||
return defaultValue
|
||
}
|
||
|
||
// 辅助函数:将 []interface{} 转换为 []string
|
||
func convertToStringArray(arr []interface{}) []string {
|
||
result := make([]string, 0, len(arr))
|
||
for _, item := range arr {
|
||
if str, ok := item.(string); ok {
|
||
result = append(result, str)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// convertCardToCreateRequest 将角色卡转换为创建请求
|
||
func convertCardToCreateRequest(card *utils.CharacterCardV2, avatarData []byte, isPublic bool) request.CreateCharacterRequest {
|
||
// 处理示例消息
|
||
exampleMessages := []string{}
|
||
if card.Data.MesExample != "" {
|
||
// 按 <START> 分割
|
||
parts := strings.Split(card.Data.MesExample, "<START>")
|
||
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,
|
||
}
|
||
}
|
||
|
||
// exportLinkedWorldBook 导出角色关联的世界书数据
|
||
func (cs *CharacterService) exportLinkedWorldBook(characterID uint) map[string]interface{} {
|
||
// 查找关联的世界书
|
||
var worldBooks []app.AIWorldInfo
|
||
err := global.GVA_DB.
|
||
Where("? = ANY(linked_chars)", fmt.Sprintf("%d", characterID)).
|
||
Find(&worldBooks).Error
|
||
|
||
if err != nil || len(worldBooks) == 0 {
|
||
return nil
|
||
}
|
||
|
||
// 合并所有世界书的条目
|
||
var allEntries []app.AIWorldInfoEntry
|
||
var bookName string
|
||
|
||
for _, book := range worldBooks {
|
||
var entries []app.AIWorldInfoEntry
|
||
if err := json.Unmarshal([]byte(book.Entries), &entries); err != nil {
|
||
continue
|
||
}
|
||
allEntries = append(allEntries, entries...)
|
||
if bookName == "" {
|
||
bookName = book.BookName
|
||
}
|
||
}
|
||
|
||
if len(allEntries) == 0 {
|
||
return nil
|
||
}
|
||
|
||
// 转换为 CharacterBook 格式
|
||
entriesData := make([]map[string]interface{}, 0, len(allEntries))
|
||
for _, entry := range allEntries {
|
||
entryMap := map[string]interface{}{
|
||
"uid": entry.UID,
|
||
"keys": entry.Keys,
|
||
"secondary_keys": entry.SecondaryKeys,
|
||
"content": entry.Content,
|
||
"comment": entry.Comment,
|
||
"enabled": entry.Enabled,
|
||
"constant": entry.Constant,
|
||
"selective": entry.Selective,
|
||
"insertion_order": entry.Order,
|
||
"position": entry.Position,
|
||
"depth": entry.Depth,
|
||
"use_probability": entry.UseProbability,
|
||
"probability": entry.Probability,
|
||
"group": entry.Group,
|
||
"group_override": entry.GroupOverride,
|
||
"group_weight": entry.GroupWeight,
|
||
"prevent_recursion": entry.PreventRecursion,
|
||
"delay_until_recursion": entry.DelayUntilRecursion,
|
||
"scan_depth": entry.ScanDepth,
|
||
"case_sensitive": entry.CaseSensitive,
|
||
"match_whole_words": entry.MatchWholeWords,
|
||
"use_regex": entry.UseRegex,
|
||
"automation_id": entry.Automation,
|
||
"role": entry.Role,
|
||
"vectorized": entry.VectorizedContent,
|
||
}
|
||
|
||
if entry.Extensions != nil {
|
||
entryMap["extensions"] = entry.Extensions
|
||
}
|
||
|
||
entriesData = append(entriesData, entryMap)
|
||
}
|
||
|
||
return map[string]interface{}{
|
||
"name": bookName,
|
||
"entries": entriesData,
|
||
}
|
||
}
|
||
|
||
// convertCharacterToCard 将角色卡转换为 CharacterCardV2
|
||
func (cs *CharacterService) 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)
|
||
}
|
||
|
||
// 如果角色没有内嵌的 CharacterBook,尝试从世界书表中查找关联的世界书
|
||
if characterBook == nil {
|
||
characterBook = cs.exportLinkedWorldBook(character.ID)
|
||
}
|
||
|
||
// 解析或构建 extensions JSON
|
||
extensions := map[string]interface{}{}
|
||
if len(character.Extensions) > 0 {
|
||
json.Unmarshal(character.Extensions, &extensions)
|
||
}
|
||
|
||
// 导出关联的正则脚本到 extensions
|
||
if regexScripts := cs.exportRegexScripts(character.ID); regexScripts != nil && len(regexScripts) > 0 {
|
||
extensions["regex_scripts"] = regexScripts
|
||
}
|
||
|
||
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: character.SystemPrompt,
|
||
PostHistoryInstructions: character.PostHistoryInstructions,
|
||
Tags: tags,
|
||
Creator: character.CreatorName,
|
||
CharacterVersion: fmt.Sprintf("%d", character.Version),
|
||
AlternateGreetings: alternateGreetings,
|
||
CharacterBook: characterBook,
|
||
Extensions: extensions,
|
||
},
|
||
}
|
||
}
|
||
|
||
// loadAvatarImage 从文件系统或 URL 加载头像
|
||
func loadAvatarImage(avatarPath string) (image.Image, error) {
|
||
// 如果是 URL,暂时不支持
|
||
if strings.HasPrefix(avatarPath, "http://") || strings.HasPrefix(avatarPath, "https://") {
|
||
return nil, errors.New("暂不支持从 URL 加载头像")
|
||
}
|
||
|
||
// 从文件系统加载
|
||
file, err := os.Open(avatarPath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("打开头像文件失败: %w", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
// 解码图片(自动检测格式)
|
||
img, _, err := image.Decode(file)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("解码头像图片失败: %w", err)
|
||
}
|
||
|
||
return img, nil
|
||
}
|
||
|
||
// 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)
|
||
|
||
// 直接设置像素颜色
|
||
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))
|
||
}
|