499 lines
15 KiB
Go
499 lines
15 KiB
Go
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(®exScripts)
|
||
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,
|
||
}
|
||
|
||
// 提取字段 - 兼容 SillyTavern 的字段名
|
||
// 脚本名称:优先使用 scriptName,其次 name
|
||
if scriptName, ok := scriptData["scriptName"].(string); ok {
|
||
script.Name = scriptName
|
||
} else if name, ok := scriptData["name"].(string); ok {
|
||
script.Name = name
|
||
}
|
||
|
||
// 查找正则表达式
|
||
if findRegex, ok := scriptData["findRegex"].(string); ok {
|
||
script.FindRegex = findRegex
|
||
}
|
||
|
||
// 替换字符串:优先使用 replaceString,其次 replaceWith
|
||
if replaceString, ok := scriptData["replaceString"].(string); ok {
|
||
script.ReplaceWith = replaceString
|
||
} else 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())
|
||
}
|
||
}
|
||
}
|