Files
st/server/service/app/character.go
2026-02-11 23:44:09 +08:00

1455 lines
44 KiB
Go
Raw Permalink 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 (
"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))
}