Files
st-react/server/service/app/character.go
2026-02-28 15:09:53 +08:00

490 lines
14 KiB
Go
Raw 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 (
"encoding/base64"
"encoding/json"
"errors"
"mime/multipart"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/app"
"git.echol.cn/loser/st/server/model/app/request"
"git.echol.cn/loser/st/server/model/app/response"
"git.echol.cn/loser/st/server/utils"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type CharacterService struct{}
// CreateCharacter 创建角色卡
func (s *CharacterService) CreateCharacter(userID uint, req *request.CreateCharacterRequest) (*response.CharacterResponse, error) {
// 序列化 JSON 字段
tagsJSON, _ := json.Marshal(req.Tags)
alternateGreetingsJSON, _ := json.Marshal(req.AlternateGreetings)
characterBookJSON, _ := json.Marshal(req.CharacterBook)
extensionsJSON, _ := json.Marshal(req.Extensions)
character := app.AICharacter{
UserID: userID,
Name: req.Name,
Avatar: req.Avatar,
Creator: req.Creator,
Version: req.Version,
Description: req.Description,
Personality: req.Personality,
Scenario: req.Scenario,
FirstMes: req.FirstMes,
MesExample: req.MesExample,
CreatorNotes: req.CreatorNotes,
SystemPrompt: req.SystemPrompt,
PostHistoryInstructions: req.PostHistoryInstructions,
Tags: datatypes.JSON(tagsJSON),
AlternateGreetings: datatypes.JSON(alternateGreetingsJSON),
CharacterBook: datatypes.JSON(characterBookJSON),
Extensions: datatypes.JSON(extensionsJSON),
IsPublic: req.IsPublic,
Spec: "chara_card_v2",
SpecVersion: "2.0",
}
err := global.GVA_DB.Create(&character).Error
if err != nil {
return nil, err
}
resp := response.ToCharacterResponse(&character)
return &resp, nil
}
// GetCharacterList 获取角色卡列表
func (s *CharacterService) GetCharacterList(userID uint, req *request.GetCharacterListRequest) (*response.CharacterListResponse, error) {
var characters []app.AICharacter
var total int64
db := global.GVA_DB.Model(&app.AICharacter{})
// 筛选条件
if req.IsPublic != nil {
if *req.IsPublic {
db = db.Where("is_public = ?", true)
} else {
db = db.Where("user_id = ?", userID)
}
} else {
db = db.Where("user_id = ? OR is_public = ?", userID, true)
}
// 关键词搜索
if req.Keyword != "" {
db = db.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 标签筛选
if req.Tag != "" {
db = db.Where("tags @> ?", datatypes.JSON(`["`+req.Tag+`"]`))
}
// 统计总数
err := db.Count(&total).Error
if err != nil {
return nil, err
}
// 分页查询
offset := (req.Page - 1) * req.PageSize
err = db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&characters).Error
if err != nil {
return nil, err
}
// 转换响应
list := make([]response.CharacterResponse, len(characters))
for i, char := range characters {
list[i] = response.ToCharacterResponse(&char)
}
return &response.CharacterListResponse{
List: list,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// GetCharacterByID 获取角色卡详情
func (s *CharacterService) GetCharacterByID(userID, characterID uint) (*response.CharacterResponse, error) {
var character app.AICharacter
err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", characterID, userID, true).
First(&character).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("角色卡不存在或无权访问")
}
return nil, err
}
resp := response.ToCharacterResponse(&character)
return &resp, nil
}
// UpdateCharacter 更新角色卡
func (s *CharacterService) UpdateCharacter(userID, characterID uint, req *request.UpdateCharacterRequest) error {
var character app.AICharacter
// 检查权限
err := global.GVA_DB.Where("id = ? AND user_id = ?", characterID, userID).First(&character).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("角色卡不存在或无权修改")
}
return err
}
// 更新字段
updates := map[string]interface{}{}
if req.Name != "" {
updates["name"] = req.Name
}
if req.Avatar != "" {
updates["avatar"] = req.Avatar
}
if req.Creator != "" {
updates["creator"] = req.Creator
}
if req.Version != "" {
updates["version"] = req.Version
}
if req.Description != "" {
updates["description"] = req.Description
}
if req.Personality != "" {
updates["personality"] = req.Personality
}
if req.Scenario != "" {
updates["scenario"] = req.Scenario
}
if req.FirstMes != "" {
updates["first_mes"] = req.FirstMes
}
if req.MesExample != "" {
updates["mes_example"] = req.MesExample
}
if req.CreatorNotes != "" {
updates["creator_notes"] = req.CreatorNotes
}
if req.SystemPrompt != "" {
updates["system_prompt"] = req.SystemPrompt
}
if req.PostHistoryInstructions != "" {
updates["post_history_instructions"] = req.PostHistoryInstructions
}
if req.Tags != nil {
tagsJSON, _ := json.Marshal(req.Tags)
updates["tags"] = datatypes.JSON(tagsJSON)
}
if req.AlternateGreetings != nil {
alternateGreetingsJSON, _ := json.Marshal(req.AlternateGreetings)
updates["alternate_greetings"] = datatypes.JSON(alternateGreetingsJSON)
}
if req.CharacterBook != nil {
characterBookJSON, _ := json.Marshal(req.CharacterBook)
updates["character_book"] = datatypes.JSON(characterBookJSON)
}
if req.Extensions != nil {
extensionsJSON, _ := json.Marshal(req.Extensions)
updates["extensions"] = datatypes.JSON(extensionsJSON)
}
updates["is_public"] = req.IsPublic
return global.GVA_DB.Model(&character).Updates(updates).Error
}
// DeleteCharacter 删除角色卡
func (s *CharacterService) DeleteCharacter(userID, characterID uint) error {
result := global.GVA_DB.Where("id = ? AND user_id = ?", characterID, userID).Delete(&app.AICharacter{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("角色卡不存在或无权删除")
}
return nil
}
// ImportCharacterFromPNG 从 PNG 文件导入角色卡
func (s *CharacterService) ImportCharacterFromPNG(userID uint, file *multipart.FileHeader) (*response.CharacterResponse, error) {
// 读取文件内容
src, err := file.Open()
if err != nil {
return nil, errors.New("打开文件失败")
}
defer src.Close()
// 读取文件数据
fileData := make([]byte, file.Size)
_, err = src.Read(fileData)
if err != nil {
return nil, errors.New("读取文件失败")
}
// 提取角色卡数据
card, err := utils.ExtractCharacterFromPNG(fileData)
if err != nil {
return nil, err
}
// 上传 PNG 图片到 OSS替代 Base64
var uploadService UploadService
avatarURL, err := uploadService.UploadImage(file)
if err != nil {
// 如果上传失败,回退到 Base64向后兼容
avatarURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(fileData)
}
// 转换为创建请求
req := &request.CreateCharacterRequest{
Name: card.Data.Name,
Avatar: avatarURL,
Creator: card.Data.Creator,
Version: card.Data.CharacterVersion,
Description: card.Data.Description,
Personality: card.Data.Personality,
Scenario: card.Data.Scenario,
FirstMes: card.Data.FirstMes,
MesExample: card.Data.MesExample,
CreatorNotes: card.Data.CreatorNotes,
SystemPrompt: card.Data.SystemPrompt,
PostHistoryInstructions: card.Data.PostHistoryInstructions,
Tags: card.Data.Tags,
AlternateGreetings: card.Data.AlternateGreetings,
CharacterBook: card.Data.CharacterBook,
Extensions: card.Data.Extensions,
IsPublic: false,
}
// 创建角色卡
resp, err := s.CreateCharacter(userID, req)
if err != nil {
return nil, err
}
// 处理扩展数据中的正则脚本
if card.Data.Extensions != nil {
s.processRegexScriptsFromExtensions(userID, resp.ID, card.Data.Extensions)
}
return resp, nil
}
// ImportCharacterFromJSON 从 JSON 文件导入角色卡
func (s *CharacterService) ImportCharacterFromJSON(userID uint, file *multipart.FileHeader) (*response.CharacterResponse, error) {
// 读取文件内容
src, err := file.Open()
if err != nil {
return nil, errors.New("打开文件失败")
}
defer src.Close()
// 读取文件数据
fileData := make([]byte, file.Size)
_, err = src.Read(fileData)
if err != nil {
return nil, errors.New("读取文件失败")
}
// 解析 JSON
card, err := utils.ParseCharacterCardJSON(fileData)
if err != nil {
return nil, err
}
// 转换为创建请求
req := &request.CreateCharacterRequest{
Name: card.Data.Name,
Creator: card.Data.Creator,
Version: card.Data.CharacterVersion,
Description: card.Data.Description,
Personality: card.Data.Personality,
Scenario: card.Data.Scenario,
FirstMes: card.Data.FirstMes,
MesExample: card.Data.MesExample,
CreatorNotes: card.Data.CreatorNotes,
SystemPrompt: card.Data.SystemPrompt,
PostHistoryInstructions: card.Data.PostHistoryInstructions,
Tags: card.Data.Tags,
AlternateGreetings: card.Data.AlternateGreetings,
CharacterBook: card.Data.CharacterBook,
Extensions: card.Data.Extensions,
IsPublic: false,
}
// 创建角色卡
resp, err := s.CreateCharacter(userID, req)
if err != nil {
return nil, err
}
// 处理扩展数据中的正则脚本
if card.Data.Extensions != nil {
s.processRegexScriptsFromExtensions(userID, resp.ID, card.Data.Extensions)
}
return resp, nil
}
// ExportCharacterToJSON 导出角色卡为 JSON
func (s *CharacterService) ExportCharacterToJSON(userID, characterID uint) (*utils.CharacterCardV2, error) {
var character app.AICharacter
err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", characterID, userID, true).
First(&character).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("角色卡不存在或无权访问")
}
return nil, err
}
// 解析 JSON 字段
var tags []string
var alternateGreetings []string
var characterBook map[string]interface{}
var extensions map[string]interface{}
json.Unmarshal(character.Tags, &tags)
json.Unmarshal(character.AlternateGreetings, &alternateGreetings)
json.Unmarshal(character.CharacterBook, &characterBook)
json.Unmarshal(character.Extensions, &extensions)
// 查询角色关联的正则脚本并添加到 extensions
var regexScripts []app.RegexScript
global.GVA_DB.Where("owner_char_id = ? AND scope = 1", characterID).Find(&regexScripts)
if len(regexScripts) > 0 {
if extensions == nil {
extensions = make(map[string]interface{})
}
extensions["regex_scripts"] = regexScripts
}
// 构建 V2 格式
card := &utils.CharacterCardV2{
Spec: character.Spec,
SpecVersion: character.SpecVersion,
Data: utils.CharacterCardV2Data{
Name: character.Name,
Description: character.Description,
Personality: character.Personality,
Scenario: character.Scenario,
FirstMes: character.FirstMes,
MesExample: character.MesExample,
CreatorNotes: character.CreatorNotes,
SystemPrompt: character.SystemPrompt,
PostHistoryInstructions: character.PostHistoryInstructions,
Tags: tags,
Creator: character.Creator,
CharacterVersion: character.Version,
AlternateGreetings: alternateGreetings,
CharacterBook: characterBook,
Extensions: extensions,
},
}
return card, nil
}
// processRegexScriptsFromExtensions 从扩展数据中提取并创建正则脚本
func (s *CharacterService) processRegexScriptsFromExtensions(userID, characterID uint, extensions map[string]interface{}) {
// 检查是否包含正则脚本
regexScriptsData, ok := extensions["regex_scripts"]
if !ok {
return
}
// 转换为 JSON 以便解析
scriptsJSON, err := json.Marshal(regexScriptsData)
if err != nil {
global.GVA_LOG.Error("序列化正则脚本失败: " + err.Error())
return
}
// 解析正则脚本数组
var scripts []map[string]interface{}
if err := json.Unmarshal(scriptsJSON, &scripts); err != nil {
global.GVA_LOG.Error("解析正则脚本失败: " + err.Error())
return
}
// 创建正则脚本记录
for _, scriptData := range scripts {
script := app.RegexScript{
UserID: userID,
Scope: 1, // 角色作用域
OwnerCharID: &characterID,
}
// 提取字段
if name, ok := scriptData["name"].(string); ok {
script.Name = name
}
if findRegex, ok := scriptData["findRegex"].(string); ok {
script.FindRegex = findRegex
}
if replaceWith, ok := scriptData["replaceWith"].(string); ok {
script.ReplaceWith = replaceWith
}
if placement, ok := scriptData["placement"].(float64); ok {
script.Placement = int(placement)
}
if disabled, ok := scriptData["disabled"].(bool); ok {
script.Disabled = disabled
}
if markdownOnly, ok := scriptData["markdownOnly"].(bool); ok {
script.MarkdownOnly = markdownOnly
}
if runOnEdit, ok := scriptData["runOnEdit"].(bool); ok {
script.RunOnEdit = runOnEdit
}
if promptOnly, ok := scriptData["promptOnly"].(bool); ok {
script.PromptOnly = promptOnly
}
if substituteRegex, ok := scriptData["substituteRegex"].(bool); ok {
script.SubstituteRegex = substituteRegex
}
if order, ok := scriptData["order"].(float64); ok {
script.Order = int(order)
}
// 处理可选的整数字段
if minDepth, ok := scriptData["minDepth"].(float64); ok {
depth := int(minDepth)
script.MinDepth = &depth
}
if maxDepth, ok := scriptData["maxDepth"].(float64); ok {
depth := int(maxDepth)
script.MaxDepth = &depth
}
// 处理 trimStrings 数组
if trimStrings, ok := scriptData["trimStrings"].([]interface{}); ok {
trimStringsJSON, _ := json.Marshal(trimStrings)
script.TrimStrings = datatypes.JSON(trimStringsJSON)
}
// 处理扩展字段
if scriptExtensions, ok := scriptData["extensions"].(map[string]interface{}); ok {
extensionsJSON, _ := json.Marshal(scriptExtensions)
script.Extensions = datatypes.JSON(extensionsJSON)
}
// 创建记录
if err := global.GVA_DB.Create(&script).Error; err != nil {
global.GVA_LOG.Error("创建正则脚本失败: " + err.Error())
}
}
}