🎨 优化角色卡功能模块,后续待优化图像上传&前端流畅性待优化
This commit is contained in:
@@ -164,20 +164,34 @@ oracle:
|
||||
max-open-conns: 100
|
||||
singular: false
|
||||
log-zap: false
|
||||
#pgsql:
|
||||
# prefix: ""
|
||||
# port: "5432"
|
||||
# config: sslmode=disable TimeZone=Asia/Shanghai
|
||||
# db-name: st_dev
|
||||
# username: postgres
|
||||
# password: loser765911.
|
||||
# path: 149.88.74.188
|
||||
# engine: ""
|
||||
# log-mode: error
|
||||
# max-idle-conns: 10
|
||||
# max-open-conns: 100
|
||||
# singular: false
|
||||
# log-zap: false
|
||||
pgsql:
|
||||
prefix: ""
|
||||
port: "5432"
|
||||
config: sslmode=disable TimeZone=Asia/Shanghai
|
||||
db-name: st_dev
|
||||
username: postgres
|
||||
password: loser765911.
|
||||
path: 149.88.74.188
|
||||
engine: ""
|
||||
log-mode: error
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
singular: false
|
||||
log-zap: false
|
||||
prefix: ""
|
||||
port: "5432"
|
||||
config: sslmode=disable TimeZone=Asia/Shanghai
|
||||
db-name: st_dev
|
||||
username: loser
|
||||
password: loser765911.
|
||||
path: pg.echol.top
|
||||
engine: ""
|
||||
log-mode: error
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
singular: false
|
||||
log-zap: false
|
||||
qiniu:
|
||||
zone: ZoneHuaDong
|
||||
bucket: ""
|
||||
|
||||
@@ -100,6 +100,9 @@ func RegisterTables() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 修复 PostgreSQL 序列(确保序列值与现有数据一致)
|
||||
fixPostgresSequences()
|
||||
|
||||
// 创建向量索引(必须在 AutoMigrate 之后)
|
||||
CreateVectorIndexes()
|
||||
|
||||
@@ -111,3 +114,27 @@ func RegisterTables() {
|
||||
}
|
||||
global.GVA_LOG.Info("register table success")
|
||||
}
|
||||
|
||||
// fixPostgresSequences 修复 PostgreSQL 自增序列与现有数据不一致的问题
|
||||
// AutoMigrate 后序列可能落后于已有数据的 max(id),导致插入时主键冲突
|
||||
func fixPostgresSequences() {
|
||||
if global.GVA_CONFIG.System.DbType != "pgsql" {
|
||||
return
|
||||
}
|
||||
|
||||
// 需要修复序列的表名列表
|
||||
tables := []string{
|
||||
"ai_characters",
|
||||
"app_users",
|
||||
"app_user_sessions",
|
||||
"app_user_favorite_characters",
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
seqName := table + "_id_seq"
|
||||
sql := "SELECT setval('" + seqName + "', COALESCE((SELECT MAX(id) FROM " + table + "), 0) + 1, false)"
|
||||
if err := global.GVA_DB.Exec(sql).Error; err != nil {
|
||||
global.GVA_LOG.Warn("fix sequence failed", zap.String("table", table), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,26 +9,31 @@ import (
|
||||
// AICharacter AI 角色表
|
||||
type AICharacter struct {
|
||||
global.GVA_MODEL
|
||||
Name string `json:"name" gorm:"type:varchar(500);not null;comment:角色名称"`
|
||||
Description string `json:"description" gorm:"type:text;comment:角色描述"`
|
||||
Personality string `json:"personality" gorm:"type:text;comment:角色性格"`
|
||||
Scenario string `json:"scenario" gorm:"type:text;comment:角色场景"`
|
||||
Avatar string `json:"avatar" gorm:"type:varchar(1024);comment:角色头像"`
|
||||
CreatorID *uint `json:"creatorId" gorm:"index;comment:创建者ID"`
|
||||
Creator *AppUser `json:"creator" gorm:"foreignKey:CreatorID"`
|
||||
CreatorName string `json:"creatorName" gorm:"type:varchar(200);comment:创建者名称"`
|
||||
CreatorNotes string `json:"creatorNotes" gorm:"type:text;comment:创建者备注"`
|
||||
CardData datatypes.JSON `json:"cardData" gorm:"type:jsonb;not null;comment:角色卡片数据"`
|
||||
Tags pq.StringArray `json:"tags" gorm:"type:text[];comment:角色标签"`
|
||||
IsPublic bool `json:"isPublic" gorm:"default:false;index;comment:是否公开"`
|
||||
Version int `json:"version" gorm:"default:1;comment:角色版本"`
|
||||
FirstMessage string `json:"firstMessage" gorm:"type:text;comment:第一条消息"`
|
||||
ExampleMessages pq.StringArray `json:"exampleMessages" gorm:"type:text[];comment:消息示例"`
|
||||
TotalChats int `json:"totalChats" gorm:"default:0;comment:对话总数"`
|
||||
TotalLikes int `json:"totalLikes" gorm:"default:0;comment:点赞总数"`
|
||||
UsageCount int `json:"usageCount" gorm:"default:0;comment:使用次数"`
|
||||
FavoriteCount int `json:"favoriteCount" gorm:"default:0;comment:收藏次数"`
|
||||
TokenCount int `json:"tokenCount" gorm:"default:0;comment:Token数量"`
|
||||
Name string `json:"name" gorm:"type:varchar(500);not null;comment:角色名称"`
|
||||
Description string `json:"description" gorm:"type:text;comment:角色描述"`
|
||||
Personality string `json:"personality" gorm:"type:text;comment:角色性格"`
|
||||
Scenario string `json:"scenario" gorm:"type:text;comment:角色场景"`
|
||||
Avatar string `json:"avatar" gorm:"type:varchar(1024);comment:角色头像"`
|
||||
CreatorID *uint `json:"creatorId" gorm:"index;comment:创建者ID"`
|
||||
Creator *AppUser `json:"creator" gorm:"foreignKey:CreatorID"`
|
||||
CreatorName string `json:"creatorName" gorm:"type:varchar(200);comment:创建者名称"`
|
||||
CreatorNotes string `json:"creatorNotes" gorm:"type:text;comment:创建者备注"`
|
||||
CardData datatypes.JSON `json:"cardData" gorm:"type:jsonb;not null;comment:角色卡片数据"`
|
||||
Tags pq.StringArray `json:"tags" gorm:"type:text[];comment:角色标签"`
|
||||
IsPublic bool `json:"isPublic" gorm:"default:false;index;comment:是否公开"`
|
||||
Version int `json:"version" gorm:"default:1;comment:角色版本"`
|
||||
FirstMessage string `json:"firstMessage" gorm:"type:text;comment:第一条消息"`
|
||||
ExampleMessages pq.StringArray `json:"exampleMessages" gorm:"type:text[];comment:消息示例"`
|
||||
SystemPrompt string `json:"systemPrompt" gorm:"type:text;comment:系统提示词"`
|
||||
PostHistoryInstructions string `json:"postHistoryInstructions" gorm:"type:text;comment:后置历史指令"`
|
||||
AlternateGreetings pq.StringArray `json:"alternateGreetings" gorm:"type:text[];comment:备用问候语"`
|
||||
CharacterBook datatypes.JSON `json:"characterBook" gorm:"type:jsonb;comment:角色书/世界信息"`
|
||||
Extensions datatypes.JSON `json:"extensions" gorm:"type:jsonb;comment:扩展数据(depth_prompt等)"`
|
||||
TotalChats int `json:"totalChats" gorm:"default:0;comment:对话总数"`
|
||||
TotalLikes int `json:"totalLikes" gorm:"default:0;comment:点赞总数"`
|
||||
UsageCount int `json:"usageCount" gorm:"default:0;comment:使用次数"`
|
||||
FavoriteCount int `json:"favoriteCount" gorm:"default:0;comment:收藏次数"`
|
||||
TokenCount int `json:"tokenCount" gorm:"default:0;comment:Token数量"`
|
||||
}
|
||||
|
||||
func (AICharacter) TableName() string {
|
||||
|
||||
@@ -4,33 +4,43 @@ import "mime/multipart"
|
||||
|
||||
// CreateCharacterRequest 创建角色卡请求
|
||||
type CreateCharacterRequest struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=500"`
|
||||
Description string `json:"description"`
|
||||
Personality string `json:"personality"`
|
||||
Scenario string `json:"scenario"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatorName string `json:"creatorName"`
|
||||
CreatorNotes string `json:"creatorNotes"`
|
||||
FirstMessage string `json:"firstMessage"`
|
||||
ExampleMessages []string `json:"exampleMessages"`
|
||||
Tags []string `json:"tags"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
Name string `json:"name" binding:"required,min=1,max=500"`
|
||||
Description string `json:"description"`
|
||||
Personality string `json:"personality"`
|
||||
Scenario string `json:"scenario"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatorName string `json:"creatorName"`
|
||||
CreatorNotes string `json:"creatorNotes"`
|
||||
FirstMessage string `json:"firstMessage"`
|
||||
ExampleMessages []string `json:"exampleMessages"`
|
||||
Tags []string `json:"tags"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
SystemPrompt string `json:"systemPrompt"`
|
||||
PostHistoryInstructions string `json:"postHistoryInstructions"`
|
||||
AlternateGreetings []string `json:"alternateGreetings"`
|
||||
CharacterBook map[string]interface{} `json:"characterBook"`
|
||||
Extensions map[string]interface{} `json:"extensions"`
|
||||
}
|
||||
|
||||
// UpdateCharacterRequest 更新角色卡请求
|
||||
type UpdateCharacterRequest struct {
|
||||
ID uint `json:"id" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=500"`
|
||||
Description string `json:"description"`
|
||||
Personality string `json:"personality"`
|
||||
Scenario string `json:"scenario"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatorName string `json:"creatorName"`
|
||||
CreatorNotes string `json:"creatorNotes"`
|
||||
FirstMessage string `json:"firstMessage"`
|
||||
ExampleMessages []string `json:"exampleMessages"`
|
||||
Tags []string `json:"tags"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
ID uint `json:"id" binding:"required"`
|
||||
Name string `json:"name" binding:"required,min=1,max=500"`
|
||||
Description string `json:"description"`
|
||||
Personality string `json:"personality"`
|
||||
Scenario string `json:"scenario"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatorName string `json:"creatorName"`
|
||||
CreatorNotes string `json:"creatorNotes"`
|
||||
FirstMessage string `json:"firstMessage"`
|
||||
ExampleMessages []string `json:"exampleMessages"`
|
||||
Tags []string `json:"tags"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
SystemPrompt string `json:"systemPrompt"`
|
||||
PostHistoryInstructions string `json:"postHistoryInstructions"`
|
||||
AlternateGreetings []string `json:"alternateGreetings"`
|
||||
CharacterBook map[string]interface{} `json:"characterBook"`
|
||||
Extensions map[string]interface{} `json:"extensions"`
|
||||
}
|
||||
|
||||
// CharacterListRequest 角色卡列表请求
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.echol.cn/loser/st/server/model/app"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CharacterResponse 角色卡响应
|
||||
type CharacterResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Personality string `json:"personality"`
|
||||
Scenario string `json:"scenario"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatorID *uint `json:"creatorId"`
|
||||
CreatorName string `json:"creatorName"`
|
||||
CreatorNotes string `json:"creatorNotes"`
|
||||
Tags []string `json:"tags"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
Version int `json:"version"`
|
||||
FirstMessage string `json:"firstMessage"`
|
||||
ExampleMessages []string `json:"exampleMessages"`
|
||||
TotalChats int `json:"totalChats"`
|
||||
TotalLikes int `json:"totalLikes"`
|
||||
UsageCount int `json:"usageCount"`
|
||||
FavoriteCount int `json:"favoriteCount"`
|
||||
TokenCount int `json:"tokenCount"`
|
||||
IsFavorited bool `json:"isFavorited"` // 当前用户是否收藏
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Personality string `json:"personality"`
|
||||
Scenario string `json:"scenario"`
|
||||
Avatar string `json:"avatar"`
|
||||
CreatorID *uint `json:"creatorId"`
|
||||
CreatorName string `json:"creatorName"`
|
||||
CreatorNotes string `json:"creatorNotes"`
|
||||
Tags []string `json:"tags"`
|
||||
IsPublic bool `json:"isPublic"`
|
||||
Version int `json:"version"`
|
||||
FirstMessage string `json:"firstMessage"`
|
||||
ExampleMessages []string `json:"exampleMessages"`
|
||||
SystemPrompt string `json:"systemPrompt"`
|
||||
PostHistoryInstructions string `json:"postHistoryInstructions"`
|
||||
AlternateGreetings []string `json:"alternateGreetings"`
|
||||
CharacterBook json.RawMessage `json:"characterBook"`
|
||||
Extensions json.RawMessage `json:"extensions"`
|
||||
TotalChats int `json:"totalChats"`
|
||||
TotalLikes int `json:"totalLikes"`
|
||||
UsageCount int `json:"usageCount"`
|
||||
FavoriteCount int `json:"favoriteCount"`
|
||||
TokenCount int `json:"tokenCount"`
|
||||
IsFavorited bool `json:"isFavorited"` // 当前用户是否收藏
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// CharacterListResponse 角色卡列表响应
|
||||
@@ -52,28 +58,49 @@ func ToCharacterResponse(character *app.AICharacter, isFavorited bool) Character
|
||||
exampleMessages = character.ExampleMessages
|
||||
}
|
||||
|
||||
alternateGreetings := []string{}
|
||||
if character.AlternateGreetings != nil {
|
||||
alternateGreetings = character.AlternateGreetings
|
||||
}
|
||||
|
||||
// 处理 JSON 字段,确保返回有效 JSON(nil 时返回 null)
|
||||
characterBook := json.RawMessage(character.CharacterBook)
|
||||
if len(characterBook) == 0 {
|
||||
characterBook = json.RawMessage("null")
|
||||
}
|
||||
|
||||
extensions := json.RawMessage(character.Extensions)
|
||||
if len(extensions) == 0 {
|
||||
extensions = json.RawMessage("null")
|
||||
}
|
||||
|
||||
return CharacterResponse{
|
||||
ID: character.ID,
|
||||
Name: character.Name,
|
||||
Description: character.Description,
|
||||
Personality: character.Personality,
|
||||
Scenario: character.Scenario,
|
||||
Avatar: character.Avatar,
|
||||
CreatorID: character.CreatorID,
|
||||
CreatorName: character.CreatorName,
|
||||
CreatorNotes: character.CreatorNotes,
|
||||
Tags: tags,
|
||||
IsPublic: character.IsPublic,
|
||||
Version: character.Version,
|
||||
FirstMessage: character.FirstMessage,
|
||||
ExampleMessages: exampleMessages,
|
||||
TotalChats: character.TotalChats,
|
||||
TotalLikes: character.TotalLikes,
|
||||
UsageCount: character.UsageCount,
|
||||
FavoriteCount: character.FavoriteCount,
|
||||
TokenCount: character.TokenCount,
|
||||
IsFavorited: isFavorited,
|
||||
CreatedAt: character.CreatedAt,
|
||||
UpdatedAt: character.UpdatedAt,
|
||||
ID: character.ID,
|
||||
Name: character.Name,
|
||||
Description: character.Description,
|
||||
Personality: character.Personality,
|
||||
Scenario: character.Scenario,
|
||||
Avatar: character.Avatar,
|
||||
CreatorID: character.CreatorID,
|
||||
CreatorName: character.CreatorName,
|
||||
CreatorNotes: character.CreatorNotes,
|
||||
Tags: tags,
|
||||
IsPublic: character.IsPublic,
|
||||
Version: character.Version,
|
||||
FirstMessage: character.FirstMessage,
|
||||
ExampleMessages: exampleMessages,
|
||||
SystemPrompt: character.SystemPrompt,
|
||||
PostHistoryInstructions: character.PostHistoryInstructions,
|
||||
AlternateGreetings: alternateGreetings,
|
||||
CharacterBook: characterBook,
|
||||
Extensions: extensions,
|
||||
TotalChats: character.TotalChats,
|
||||
TotalLikes: character.TotalLikes,
|
||||
UsageCount: character.UsageCount,
|
||||
FavoriteCount: character.FavoriteCount,
|
||||
TokenCount: character.TokenCount,
|
||||
IsFavorited: isFavorited,
|
||||
CreatedAt: character.CreatedAt,
|
||||
UpdatedAt: character.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"git.echol.cn/loser/st/server/model/app"
|
||||
@@ -192,16 +196,21 @@ func (cs *CharacterService) GetCharacterDetail(characterID uint, userID *uint) (
|
||||
|
||||
// CreateCharacter 创建角色卡
|
||||
func (cs *CharacterService) CreateCharacter(req request.CreateCharacterRequest, userID uint) (response.CharacterResponse, error) {
|
||||
// 构建 CardData
|
||||
// 构建 CardData(包含完整 V2 数据)
|
||||
cardData := map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"description": req.Description,
|
||||
"personality": req.Personality,
|
||||
"scenario": req.Scenario,
|
||||
"first_message": req.FirstMessage,
|
||||
"example_messages": req.ExampleMessages,
|
||||
"creator_name": req.CreatorName,
|
||||
"creator_notes": req.CreatorNotes,
|
||||
"name": req.Name,
|
||||
"description": req.Description,
|
||||
"personality": req.Personality,
|
||||
"scenario": req.Scenario,
|
||||
"first_message": req.FirstMessage,
|
||||
"example_messages": req.ExampleMessages,
|
||||
"creator_name": req.CreatorName,
|
||||
"creator_notes": req.CreatorNotes,
|
||||
"system_prompt": req.SystemPrompt,
|
||||
"post_history_instructions": req.PostHistoryInstructions,
|
||||
"alternate_greetings": req.AlternateGreetings,
|
||||
"character_book": req.CharacterBook,
|
||||
"extensions": req.Extensions,
|
||||
}
|
||||
cardDataJSON, _ := json.Marshal(cardData)
|
||||
|
||||
@@ -216,21 +225,40 @@ func (cs *CharacterService) CreateCharacter(req request.CreateCharacterRequest,
|
||||
exampleMessages = []string{}
|
||||
}
|
||||
|
||||
alternateGreetings := req.AlternateGreetings
|
||||
if alternateGreetings == nil {
|
||||
alternateGreetings = []string{}
|
||||
}
|
||||
|
||||
// 序列化 JSON 字段
|
||||
var characterBookJSON, extensionsJSON datatypes.JSON
|
||||
if req.CharacterBook != nil {
|
||||
characterBookJSON, _ = json.Marshal(req.CharacterBook)
|
||||
}
|
||||
if req.Extensions != nil {
|
||||
extensionsJSON, _ = json.Marshal(req.Extensions)
|
||||
}
|
||||
|
||||
character := app.AICharacter{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Personality: req.Personality,
|
||||
Scenario: req.Scenario,
|
||||
Avatar: req.Avatar,
|
||||
CreatorID: &userID,
|
||||
CreatorName: req.CreatorName,
|
||||
CreatorNotes: req.CreatorNotes,
|
||||
CardData: datatypes.JSON(cardDataJSON),
|
||||
Tags: tags,
|
||||
IsPublic: req.IsPublic,
|
||||
FirstMessage: req.FirstMessage,
|
||||
ExampleMessages: exampleMessages,
|
||||
TokenCount: calculateTokenCount(req),
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Personality: req.Personality,
|
||||
Scenario: req.Scenario,
|
||||
Avatar: req.Avatar,
|
||||
CreatorID: &userID,
|
||||
CreatorName: req.CreatorName,
|
||||
CreatorNotes: req.CreatorNotes,
|
||||
CardData: datatypes.JSON(cardDataJSON),
|
||||
Tags: tags,
|
||||
IsPublic: req.IsPublic,
|
||||
FirstMessage: req.FirstMessage,
|
||||
ExampleMessages: exampleMessages,
|
||||
SystemPrompt: req.SystemPrompt,
|
||||
PostHistoryInstructions: req.PostHistoryInstructions,
|
||||
AlternateGreetings: alternateGreetings,
|
||||
CharacterBook: characterBookJSON,
|
||||
Extensions: extensionsJSON,
|
||||
TokenCount: calculateTokenCount(req),
|
||||
}
|
||||
|
||||
err := global.GVA_DB.Create(&character).Error
|
||||
@@ -257,20 +285,25 @@ func (cs *CharacterService) UpdateCharacter(req request.UpdateCharacterRequest,
|
||||
return response.CharacterResponse{}, errors.New("无权修改")
|
||||
}
|
||||
|
||||
// 构建 CardData
|
||||
// 构建 CardData(包含完整 V2 数据)
|
||||
cardData := map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"description": req.Description,
|
||||
"personality": req.Personality,
|
||||
"scenario": req.Scenario,
|
||||
"first_message": req.FirstMessage,
|
||||
"example_messages": req.ExampleMessages,
|
||||
"creator_name": req.CreatorName,
|
||||
"creator_notes": req.CreatorNotes,
|
||||
"name": req.Name,
|
||||
"description": req.Description,
|
||||
"personality": req.Personality,
|
||||
"scenario": req.Scenario,
|
||||
"first_message": req.FirstMessage,
|
||||
"example_messages": req.ExampleMessages,
|
||||
"creator_name": req.CreatorName,
|
||||
"creator_notes": req.CreatorNotes,
|
||||
"system_prompt": req.SystemPrompt,
|
||||
"post_history_instructions": req.PostHistoryInstructions,
|
||||
"alternate_greetings": req.AlternateGreetings,
|
||||
"character_book": req.CharacterBook,
|
||||
"extensions": req.Extensions,
|
||||
}
|
||||
cardDataJSON, _ := json.Marshal(cardData)
|
||||
|
||||
// 处理标签和示例消息
|
||||
// 处理标签和示例消息,转换为 pq.StringArray
|
||||
tags := req.Tags
|
||||
if tags == nil {
|
||||
tags = []string{}
|
||||
@@ -281,25 +314,42 @@ func (cs *CharacterService) UpdateCharacter(req request.UpdateCharacterRequest,
|
||||
exampleMessages = []string{}
|
||||
}
|
||||
|
||||
// 更新
|
||||
updates := map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"description": req.Description,
|
||||
"personality": req.Personality,
|
||||
"scenario": req.Scenario,
|
||||
"avatar": req.Avatar,
|
||||
"creator_name": req.CreatorName,
|
||||
"creator_notes": req.CreatorNotes,
|
||||
"card_data": cardDataJSON,
|
||||
"tags": tags,
|
||||
"is_public": req.IsPublic,
|
||||
"first_message": req.FirstMessage,
|
||||
"example_messages": exampleMessages,
|
||||
"token_count": calculateTokenCount(req),
|
||||
"version": character.Version + 1,
|
||||
alternateGreetings := req.AlternateGreetings
|
||||
if alternateGreetings == nil {
|
||||
alternateGreetings = []string{}
|
||||
}
|
||||
|
||||
err = global.GVA_DB.Model(&character).Updates(updates).Error
|
||||
// 序列化 JSON 字段
|
||||
var characterBookJSON, extensionsJSON datatypes.JSON
|
||||
if req.CharacterBook != nil {
|
||||
characterBookJSON, _ = json.Marshal(req.CharacterBook)
|
||||
}
|
||||
if req.Extensions != nil {
|
||||
extensionsJSON, _ = json.Marshal(req.Extensions)
|
||||
}
|
||||
|
||||
// 更新字段 - 直接更新到结构体以避免类型转换问题
|
||||
character.Name = req.Name
|
||||
character.Description = req.Description
|
||||
character.Personality = req.Personality
|
||||
character.Scenario = req.Scenario
|
||||
character.Avatar = req.Avatar
|
||||
character.CreatorName = req.CreatorName
|
||||
character.CreatorNotes = req.CreatorNotes
|
||||
character.CardData = cardDataJSON
|
||||
character.Tags = tags
|
||||
character.IsPublic = req.IsPublic
|
||||
character.FirstMessage = req.FirstMessage
|
||||
character.ExampleMessages = exampleMessages
|
||||
character.SystemPrompt = req.SystemPrompt
|
||||
character.PostHistoryInstructions = req.PostHistoryInstructions
|
||||
character.AlternateGreetings = alternateGreetings
|
||||
character.CharacterBook = characterBookJSON
|
||||
character.Extensions = extensionsJSON
|
||||
character.TokenCount = calculateTokenCount(req)
|
||||
character.Version = character.Version + 1
|
||||
|
||||
err = global.GVA_DB.Save(&character).Error
|
||||
if err != nil {
|
||||
return response.CharacterResponse{}, err
|
||||
}
|
||||
@@ -436,25 +486,50 @@ func (cs *CharacterService) ExportCharacter(characterID uint, userID *uint) (map
|
||||
exampleMessages = character.ExampleMessages
|
||||
}
|
||||
|
||||
alternateGreetings := []string{}
|
||||
if character.AlternateGreetings != nil {
|
||||
alternateGreetings = character.AlternateGreetings
|
||||
}
|
||||
|
||||
// 解析 character_book JSON
|
||||
var characterBook map[string]interface{}
|
||||
if len(character.CharacterBook) > 0 {
|
||||
json.Unmarshal(character.CharacterBook, &characterBook)
|
||||
}
|
||||
|
||||
// 解析 extensions JSON
|
||||
extensions := map[string]interface{}{}
|
||||
if len(character.Extensions) > 0 {
|
||||
json.Unmarshal(character.Extensions, &extensions)
|
||||
}
|
||||
|
||||
// 构建导出数据(兼容 SillyTavern 格式)
|
||||
data := map[string]interface{}{
|
||||
"name": character.Name,
|
||||
"description": character.Description,
|
||||
"personality": character.Personality,
|
||||
"scenario": character.Scenario,
|
||||
"first_mes": character.FirstMessage,
|
||||
"mes_example": strings.Join(exampleMessages, "\n<START>\n"),
|
||||
"creator_notes": character.CreatorNotes,
|
||||
"system_prompt": character.SystemPrompt,
|
||||
"post_history_instructions": character.PostHistoryInstructions,
|
||||
"tags": tags,
|
||||
"creator": character.CreatorName,
|
||||
"character_version": character.Version,
|
||||
"alternate_greetings": alternateGreetings,
|
||||
"extensions": extensions,
|
||||
}
|
||||
|
||||
// 仅在存在时添加 character_book
|
||||
if characterBook != nil {
|
||||
data["character_book"] = characterBook
|
||||
}
|
||||
|
||||
exportData := map[string]interface{}{
|
||||
"spec": "chara_card_v2",
|
||||
"spec_version": "2.0",
|
||||
"data": map[string]interface{}{
|
||||
"name": character.Name,
|
||||
"description": character.Description,
|
||||
"personality": character.Personality,
|
||||
"scenario": character.Scenario,
|
||||
"first_mes": character.FirstMessage,
|
||||
"mes_example": strings.Join(exampleMessages, "\n<START>\n"),
|
||||
"creator_notes": character.CreatorNotes,
|
||||
"system_prompt": "",
|
||||
"post_history_instructions": "",
|
||||
"tags": tags,
|
||||
"creator": character.CreatorName,
|
||||
"character_version": character.Version,
|
||||
"extensions": map[string]interface{}{},
|
||||
},
|
||||
"data": data,
|
||||
}
|
||||
|
||||
return exportData, nil
|
||||
@@ -572,42 +647,49 @@ func convertCardToCreateRequest(card *utils.CharacterCardV2, avatarData []byte,
|
||||
exampleMessages := []string{}
|
||||
if card.Data.MesExample != "" {
|
||||
// 按 <START> 分割
|
||||
exampleMessages = strings.Split(card.Data.MesExample, "<START>")
|
||||
// 清理空白
|
||||
cleaned := []string{}
|
||||
for _, msg := range exampleMessages {
|
||||
parts := strings.Split(card.Data.MesExample, "<START>")
|
||||
for _, msg := range parts {
|
||||
msg = strings.TrimSpace(msg)
|
||||
if msg != "" {
|
||||
cleaned = append(cleaned, msg)
|
||||
exampleMessages = append(exampleMessages, msg)
|
||||
}
|
||||
}
|
||||
exampleMessages = cleaned
|
||||
}
|
||||
|
||||
// 合并备用问候语
|
||||
if len(card.Data.AlternateGreetings) > 0 {
|
||||
exampleMessages = append(exampleMessages, card.Data.AlternateGreetings...)
|
||||
// 备用问候语独立存储,不再合并到 exampleMessages
|
||||
alternateGreetings := card.Data.AlternateGreetings
|
||||
if alternateGreetings == nil {
|
||||
alternateGreetings = []string{}
|
||||
}
|
||||
|
||||
// TODO: 处理头像数据,上传到文件服务器
|
||||
// 保存头像到本地文件
|
||||
avatar := ""
|
||||
if avatarData != nil {
|
||||
// 这里应该将头像上传到文件服务器并获取 URL
|
||||
// avatar = uploadAvatar(avatarData)
|
||||
savedPath, err := saveAvatarFromBytes(avatarData, card.Data.Name, ".png")
|
||||
if err != nil {
|
||||
global.GVA_LOG.Warn("保存角色卡头像失败", zap.Error(err))
|
||||
} else {
|
||||
avatar = savedPath
|
||||
}
|
||||
}
|
||||
|
||||
return request.CreateCharacterRequest{
|
||||
Name: card.Data.Name,
|
||||
Description: card.Data.Description,
|
||||
Personality: card.Data.Personality,
|
||||
Scenario: card.Data.Scenario,
|
||||
Avatar: avatar,
|
||||
CreatorName: card.Data.Creator,
|
||||
CreatorNotes: card.Data.CreatorNotes,
|
||||
FirstMessage: card.Data.FirstMes,
|
||||
ExampleMessages: exampleMessages,
|
||||
Tags: card.Data.Tags,
|
||||
IsPublic: isPublic,
|
||||
Name: card.Data.Name,
|
||||
Description: card.Data.Description,
|
||||
Personality: card.Data.Personality,
|
||||
Scenario: card.Data.Scenario,
|
||||
Avatar: avatar,
|
||||
CreatorName: card.Data.Creator,
|
||||
CreatorNotes: card.Data.CreatorNotes,
|
||||
FirstMessage: card.Data.FirstMes,
|
||||
ExampleMessages: exampleMessages,
|
||||
Tags: card.Data.Tags,
|
||||
IsPublic: isPublic,
|
||||
SystemPrompt: card.Data.SystemPrompt,
|
||||
PostHistoryInstructions: card.Data.PostHistoryInstructions,
|
||||
AlternateGreetings: alternateGreetings,
|
||||
CharacterBook: card.Data.CharacterBook,
|
||||
Extensions: card.Data.Extensions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,6 +705,23 @@ func convertCharacterToCard(character *app.AICharacter) *utils.CharacterCardV2 {
|
||||
exampleMessages = character.ExampleMessages
|
||||
}
|
||||
|
||||
alternateGreetings := []string{}
|
||||
if character.AlternateGreetings != nil {
|
||||
alternateGreetings = character.AlternateGreetings
|
||||
}
|
||||
|
||||
// 解析 character_book JSON
|
||||
var characterBook map[string]interface{}
|
||||
if len(character.CharacterBook) > 0 {
|
||||
json.Unmarshal(character.CharacterBook, &characterBook)
|
||||
}
|
||||
|
||||
// 解析 extensions JSON
|
||||
extensions := map[string]interface{}{}
|
||||
if len(character.Extensions) > 0 {
|
||||
json.Unmarshal(character.Extensions, &extensions)
|
||||
}
|
||||
|
||||
return &utils.CharacterCardV2{
|
||||
Spec: "chara_card_v2",
|
||||
SpecVersion: "2.0",
|
||||
@@ -634,13 +733,14 @@ func convertCharacterToCard(character *app.AICharacter) *utils.CharacterCardV2 {
|
||||
FirstMes: character.FirstMessage,
|
||||
MesExample: strings.Join(exampleMessages, "\n<START>\n"),
|
||||
CreatorNotes: character.CreatorNotes,
|
||||
SystemPrompt: "",
|
||||
PostHistoryInstructions: "",
|
||||
SystemPrompt: character.SystemPrompt,
|
||||
PostHistoryInstructions: character.PostHistoryInstructions,
|
||||
Tags: tags,
|
||||
Creator: character.CreatorName,
|
||||
CharacterVersion: fmt.Sprintf("%d", character.Version),
|
||||
AlternateGreetings: []string{},
|
||||
Extensions: map[string]interface{}{},
|
||||
AlternateGreetings: alternateGreetings,
|
||||
CharacterBook: characterBook,
|
||||
Extensions: extensions,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -673,17 +773,38 @@ func createDefaultAvatar() image.Image {
|
||||
return img
|
||||
}
|
||||
|
||||
// saveAvatarFromBytes 将头像原始字节数据保存到本地文件系统
|
||||
func saveAvatarFromBytes(data []byte, name string, ext string) (string, error) {
|
||||
// 生成唯一文件名:MD5(name) + 时间戳
|
||||
hash := md5.Sum([]byte(name))
|
||||
hashStr := hex.EncodeToString(hash[:])
|
||||
filename := hashStr + "_" + time.Now().Format("20060102150405") + ext
|
||||
|
||||
storePath := global.GVA_CONFIG.Local.StorePath
|
||||
if err := os.MkdirAll(storePath, os.ModePerm); err != nil {
|
||||
return "", fmt.Errorf("创建存储目录失败: %w", err)
|
||||
}
|
||||
|
||||
filePath := storePath + "/" + filename
|
||||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return "", fmt.Errorf("写入文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 返回访问路径
|
||||
return global.GVA_CONFIG.Local.Path + "/" + filename, nil
|
||||
}
|
||||
|
||||
// calculateTokenCount 计算角色卡的 Token 数量(简单估算)
|
||||
func calculateTokenCount(req interface{}) int {
|
||||
var text string
|
||||
switch v := req.(type) {
|
||||
case request.CreateCharacterRequest:
|
||||
text = v.Name + v.Description + v.Personality + v.Scenario + v.FirstMessage
|
||||
text = v.Name + v.Description + v.Personality + v.Scenario + v.FirstMessage + v.SystemPrompt + v.PostHistoryInstructions
|
||||
for _, msg := range v.ExampleMessages {
|
||||
text += msg
|
||||
}
|
||||
case request.UpdateCharacterRequest:
|
||||
text = v.Name + v.Description + v.Personality + v.Scenario + v.FirstMessage
|
||||
text = v.Name + v.Description + v.Personality + v.Scenario + v.FirstMessage + v.SystemPrompt + v.PostHistoryInstructions
|
||||
for _, msg := range v.ExampleMessages {
|
||||
text += msg
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ type CharacterCardV2Data struct {
|
||||
Creator string `json:"creator"`
|
||||
CharacterVersion string `json:"character_version"`
|
||||
AlternateGreetings []string `json:"alternate_greetings"`
|
||||
CharacterBook map[string]interface{} `json:"character_book,omitempty"`
|
||||
Extensions map[string]interface{} `json:"extensions"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user