From cf3197929ed37d7ad885e95b208d0ebeba8f2839 Mon Sep 17 00:00:00 2001 From: Echo <1711788888@qq.com> Date: Wed, 11 Feb 2026 05:33:38 +0800 Subject: [PATCH] =?UTF-8?q?:art:=20=E4=BC=98=E5=8C=96=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E5=8D=A1=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=90=8E?= =?UTF-8?q?=E7=BB=AD=E5=BE=85=E4=BC=98=E5=8C=96=E5=9B=BE=E5=83=8F=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0&=E5=89=8D=E7=AB=AF=E6=B5=81=E7=95=85=E6=80=A7?= =?UTF-8?q?=E5=BE=85=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + server/config.yaml | 40 ++- server/initialize/gorm.go | 27 ++ server/model/app/ai_character.go | 45 +-- server/model/app/request/character.go | 56 ++-- server/model/app/response/character.go | 115 +++++--- server/service/app/character.go | 309 ++++++++++++++------- server/utils/character_card.go | 1 + web-app-vue/src/components.d.ts | 4 + web-app-vue/src/types/character.d.ts | 12 + web-app-vue/src/views/character/Detail.vue | 40 ++- web-app-vue/src/views/character/Edit.vue | 124 ++++++++- 12 files changed, 576 insertions(+), 198 deletions(-) diff --git a/.gitignore b/.gitignore index f9a3985..e2fd613 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,4 @@ dist .yarn/install-state.gz .pnp.* +.claude \ No newline at end of file diff --git a/server/config.yaml b/server/config.yaml index 913ec7c..6eed06e 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -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: "" diff --git a/server/initialize/gorm.go b/server/initialize/gorm.go index 017598b..85a4900 100644 --- a/server/initialize/gorm.go +++ b/server/initialize/gorm.go @@ -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)) + } + } +} diff --git a/server/model/app/ai_character.go b/server/model/app/ai_character.go index b4c2e09..bbe8f15 100644 --- a/server/model/app/ai_character.go +++ b/server/model/app/ai_character.go @@ -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 { diff --git a/server/model/app/request/character.go b/server/model/app/request/character.go index eea6a96..2c844e4 100644 --- a/server/model/app/request/character.go +++ b/server/model/app/request/character.go @@ -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 角色卡列表请求 diff --git a/server/model/app/response/character.go b/server/model/app/response/character.go index 07beb36..e1c733a 100644 --- a/server/model/app/response/character.go +++ b/server/model/app/response/character.go @@ -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, } } diff --git a/server/service/app/character.go b/server/service/app/character.go index e007fae..f7fe941 100644 --- a/server/service/app/character.go +++ b/server/service/app/character.go @@ -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\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\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 != "" { // 按 分割 - exampleMessages = strings.Split(card.Data.MesExample, "") - // 清理空白 - cleaned := []string{} - for _, msg := range exampleMessages { + parts := strings.Split(card.Data.MesExample, "") + 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\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 } diff --git a/server/utils/character_card.go b/server/utils/character_card.go index 2a071be..c124773 100644 --- a/server/utils/character_card.go +++ b/server/utils/character_card.go @@ -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"` } diff --git a/web-app-vue/src/components.d.ts b/web-app-vue/src/components.d.ts index 44c7e2e..011a8ac 100644 --- a/web-app-vue/src/components.d.ts +++ b/web-app-vue/src/components.d.ts @@ -16,6 +16,8 @@ declare module 'vue' { ElCard: typeof import('element-plus/es')['ElCard'] ElCol: typeof import('element-plus/es')['ElCol'] ElContainer: typeof import('element-plus/es')['ElContainer'] + ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] + ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] @@ -34,6 +36,8 @@ declare module 'vue' { ElSelect: typeof import('element-plus/es')['ElSelect'] ElStatistic: typeof import('element-plus/es')['ElStatistic'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTabPane: typeof import('element-plus/es')['ElTabPane'] + ElTabs: typeof import('element-plus/es')['ElTabs'] ElTag: typeof import('element-plus/es')['ElTag'] ElUpload: typeof import('element-plus/es')['ElUpload'] HelloWorld: typeof import('./components/HelloWorld.vue')['default'] diff --git a/web-app-vue/src/types/character.d.ts b/web-app-vue/src/types/character.d.ts index 9794761..a478607 100644 --- a/web-app-vue/src/types/character.d.ts +++ b/web-app-vue/src/types/character.d.ts @@ -18,6 +18,11 @@ export interface Character { version: number firstMessage: string exampleMessages: string[] + systemPrompt: string + postHistoryInstructions: string + alternateGreetings: string[] + characterBook: any + extensions: Record totalChats: number totalLikes: number usageCount: number @@ -58,6 +63,11 @@ export interface CreateCharacterRequest { exampleMessages?: string[] tags?: string[] isPublic: boolean + systemPrompt?: string + postHistoryInstructions?: string + alternateGreetings?: string[] + characterBook?: any + extensions?: Record } // 更新角色卡请求 @@ -82,6 +92,8 @@ export interface CharacterExportData { tags: string[] creator: string character_version: number + alternate_greetings: string[] + character_book?: any extensions: Record } } diff --git a/web-app-vue/src/views/character/Detail.vue b/web-app-vue/src/views/character/Detail.vue index 2ef268f..d777111 100644 --- a/web-app-vue/src/views/character/Detail.vue +++ b/web-app-vue/src/views/character/Detail.vue @@ -41,7 +41,7 @@ {{ character.isFavorited ? '已收藏' : '收藏' }} 点赞 {{ character.totalLikes }} @@ -107,6 +107,16 @@

场景设定

{{ character.scenario || '暂无场景设定' }}

+ +
+

系统提示词

+
{{ character.systemPrompt }}
+
+ +
+

后置历史指令

+
{{ character.postHistoryInstructions }}
+
@@ -129,6 +139,19 @@ + +
+
+
问候语 {{ index + 1 }}
+ {{ greeting }} +
+
+
+ @@ -155,6 +178,12 @@ {{ formatDateTime(character.updatedAt) }} + + {{ Object.keys(character.extensions).join(', ') }} + + + 已配置 + @@ -181,7 +210,7 @@ import { ChatLineSquare, Star, StarFilled, - Like, + Top, Download, Edit, Delete, @@ -422,5 +451,12 @@ onMounted(async () => { &:last-child { margin-bottom: 0; } + + .greeting-label { + font-size: 12px; + color: #909399; + margin-bottom: 8px; + font-weight: 600; + } } diff --git a/web-app-vue/src/views/character/Edit.vue b/web-app-vue/src/views/character/Edit.vue index 4a31ba3..b4f6e08 100644 --- a/web-app-vue/src/views/character/Edit.vue +++ b/web-app-vue/src/views/character/Edit.vue @@ -131,6 +131,82 @@
+ + + + + + + + + + +
+
+ + +
+ + 添加备用问候语 + +
+
+ + + + + + + + 角色书数据为 JSON 格式,通常通过导入角色卡自动填入 + + + + + 扩展数据为只读字段,保留原版酒馆的扩展信息 + @@ -199,7 +275,36 @@ const formData = reactive({ firstMessage: '', exampleMessages: [], tags: [], - isPublic: false + isPublic: false, + systemPrompt: '', + postHistoryInstructions: '', + alternateGreetings: [], + characterBook: null, + extensions: null +}) + +// 角色书和扩展数据的文本表示(用于 JSON 编辑) +const characterBookText = computed({ + get: () => { + if (!formData.characterBook) return '' + return JSON.stringify(formData.characterBook, null, 2) + }, + set: (val: string) => { + if (!val.trim()) { + formData.characterBook = null + return + } + try { + formData.characterBook = JSON.parse(val) + } catch { + // 解析失败时不更新,保留原值 + } + } +}) + +const extensionsText = computed(() => { + if (!formData.extensions) return '' + return JSON.stringify(formData.extensions, null, 2) }) // 常用标签 @@ -238,6 +343,16 @@ function removeExampleMessage(index: number) { formData.exampleMessages?.splice(index, 1) } +// 添加备用问候语 +function addAlternateGreeting() { + formData.alternateGreetings?.push('') +} + +// 删除备用问候语 +function removeAlternateGreeting(index: number) { + formData.alternateGreetings?.splice(index, 1) +} + // 返回 function goBack() { router.back() @@ -296,7 +411,12 @@ onMounted(async () => { firstMessage: character.firstMessage, exampleMessages: [...(character.exampleMessages || [])], tags: [...(character.tags || [])], - isPublic: character.isPublic + isPublic: character.isPublic, + systemPrompt: character.systemPrompt || '', + postHistoryInstructions: character.postHistoryInstructions || '', + alternateGreetings: [...(character.alternateGreetings || [])], + characterBook: character.characterBook || null, + extensions: character.extensions || null }) } }