🎨 优化角色卡功能模块,后续待优化图像上传&前端流畅性待优化

This commit is contained in:
2026-02-11 05:33:38 +08:00
parent 56e821b222
commit cf3197929e
12 changed files with 576 additions and 198 deletions

1
.gitignore vendored
View File

@@ -174,3 +174,4 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
.claude

View File

@@ -164,14 +164,28 @@ oracle:
max-open-conns: 100 max-open-conns: 100
singular: false singular: false
log-zap: 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: pgsql:
prefix: "" prefix: ""
port: "5432" port: "5432"
config: sslmode=disable TimeZone=Asia/Shanghai config: sslmode=disable TimeZone=Asia/Shanghai
db-name: st_dev db-name: st_dev
username: postgres username: loser
password: loser765911. password: loser765911.
path: 149.88.74.188 path: pg.echol.top
engine: "" engine: ""
log-mode: error log-mode: error
max-idle-conns: 10 max-idle-conns: 10

View File

@@ -100,6 +100,9 @@ func RegisterTables() {
os.Exit(0) os.Exit(0)
} }
// 修复 PostgreSQL 序列(确保序列值与现有数据一致)
fixPostgresSequences()
// 创建向量索引(必须在 AutoMigrate 之后) // 创建向量索引(必须在 AutoMigrate 之后)
CreateVectorIndexes() CreateVectorIndexes()
@@ -111,3 +114,27 @@ func RegisterTables() {
} }
global.GVA_LOG.Info("register table success") 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))
}
}
}

View File

@@ -24,6 +24,11 @@ type AICharacter struct {
Version int `json:"version" gorm:"default:1;comment:角色版本"` Version int `json:"version" gorm:"default:1;comment:角色版本"`
FirstMessage string `json:"firstMessage" gorm:"type:text;comment:第一条消息"` FirstMessage string `json:"firstMessage" gorm:"type:text;comment:第一条消息"`
ExampleMessages pq.StringArray `json:"exampleMessages" 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:对话总数"` TotalChats int `json:"totalChats" gorm:"default:0;comment:对话总数"`
TotalLikes int `json:"totalLikes" gorm:"default:0;comment:点赞总数"` TotalLikes int `json:"totalLikes" gorm:"default:0;comment:点赞总数"`
UsageCount int `json:"usageCount" gorm:"default:0;comment:使用次数"` UsageCount int `json:"usageCount" gorm:"default:0;comment:使用次数"`

View File

@@ -15,6 +15,11 @@ type CreateCharacterRequest struct {
ExampleMessages []string `json:"exampleMessages"` ExampleMessages []string `json:"exampleMessages"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
IsPublic bool `json:"isPublic"` 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 更新角色卡请求 // UpdateCharacterRequest 更新角色卡请求
@@ -31,6 +36,11 @@ type UpdateCharacterRequest struct {
ExampleMessages []string `json:"exampleMessages"` ExampleMessages []string `json:"exampleMessages"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
IsPublic bool `json:"isPublic"` 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 角色卡列表请求 // CharacterListRequest 角色卡列表请求

View File

@@ -1,6 +1,7 @@
package response package response
import ( import (
"encoding/json"
"git.echol.cn/loser/st/server/model/app" "git.echol.cn/loser/st/server/model/app"
"time" "time"
) )
@@ -21,6 +22,11 @@ type CharacterResponse struct {
Version int `json:"version"` Version int `json:"version"`
FirstMessage string `json:"firstMessage"` FirstMessage string `json:"firstMessage"`
ExampleMessages []string `json:"exampleMessages"` 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"` TotalChats int `json:"totalChats"`
TotalLikes int `json:"totalLikes"` TotalLikes int `json:"totalLikes"`
UsageCount int `json:"usageCount"` UsageCount int `json:"usageCount"`
@@ -52,6 +58,22 @@ func ToCharacterResponse(character *app.AICharacter, isFavorited bool) Character
exampleMessages = character.ExampleMessages exampleMessages = character.ExampleMessages
} }
alternateGreetings := []string{}
if character.AlternateGreetings != nil {
alternateGreetings = character.AlternateGreetings
}
// 处理 JSON 字段,确保返回有效 JSONnil 时返回 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{ return CharacterResponse{
ID: character.ID, ID: character.ID,
Name: character.Name, Name: character.Name,
@@ -67,6 +89,11 @@ func ToCharacterResponse(character *app.AICharacter, isFavorited bool) Character
Version: character.Version, Version: character.Version,
FirstMessage: character.FirstMessage, FirstMessage: character.FirstMessage,
ExampleMessages: exampleMessages, ExampleMessages: exampleMessages,
SystemPrompt: character.SystemPrompt,
PostHistoryInstructions: character.PostHistoryInstructions,
AlternateGreetings: alternateGreetings,
CharacterBook: characterBook,
Extensions: extensions,
TotalChats: character.TotalChats, TotalChats: character.TotalChats,
TotalLikes: character.TotalLikes, TotalLikes: character.TotalLikes,
UsageCount: character.UsageCount, UsageCount: character.UsageCount,

View File

@@ -1,12 +1,16 @@
package app package app
import ( import (
"crypto/md5"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"image" "image"
_ "image/jpeg" _ "image/jpeg"
"os"
"strings" "strings"
"time"
"git.echol.cn/loser/st/server/global" "git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/app" "git.echol.cn/loser/st/server/model/app"
@@ -192,7 +196,7 @@ func (cs *CharacterService) GetCharacterDetail(characterID uint, userID *uint) (
// CreateCharacter 创建角色卡 // CreateCharacter 创建角色卡
func (cs *CharacterService) CreateCharacter(req request.CreateCharacterRequest, userID uint) (response.CharacterResponse, error) { func (cs *CharacterService) CreateCharacter(req request.CreateCharacterRequest, userID uint) (response.CharacterResponse, error) {
// 构建 CardData // 构建 CardData(包含完整 V2 数据)
cardData := map[string]interface{}{ cardData := map[string]interface{}{
"name": req.Name, "name": req.Name,
"description": req.Description, "description": req.Description,
@@ -202,6 +206,11 @@ func (cs *CharacterService) CreateCharacter(req request.CreateCharacterRequest,
"example_messages": req.ExampleMessages, "example_messages": req.ExampleMessages,
"creator_name": req.CreatorName, "creator_name": req.CreatorName,
"creator_notes": req.CreatorNotes, "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) cardDataJSON, _ := json.Marshal(cardData)
@@ -216,6 +225,20 @@ func (cs *CharacterService) CreateCharacter(req request.CreateCharacterRequest,
exampleMessages = []string{} 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{ character := app.AICharacter{
Name: req.Name, Name: req.Name,
Description: req.Description, Description: req.Description,
@@ -230,6 +253,11 @@ func (cs *CharacterService) CreateCharacter(req request.CreateCharacterRequest,
IsPublic: req.IsPublic, IsPublic: req.IsPublic,
FirstMessage: req.FirstMessage, FirstMessage: req.FirstMessage,
ExampleMessages: exampleMessages, ExampleMessages: exampleMessages,
SystemPrompt: req.SystemPrompt,
PostHistoryInstructions: req.PostHistoryInstructions,
AlternateGreetings: alternateGreetings,
CharacterBook: characterBookJSON,
Extensions: extensionsJSON,
TokenCount: calculateTokenCount(req), TokenCount: calculateTokenCount(req),
} }
@@ -257,7 +285,7 @@ func (cs *CharacterService) UpdateCharacter(req request.UpdateCharacterRequest,
return response.CharacterResponse{}, errors.New("无权修改") return response.CharacterResponse{}, errors.New("无权修改")
} }
// 构建 CardData // 构建 CardData(包含完整 V2 数据)
cardData := map[string]interface{}{ cardData := map[string]interface{}{
"name": req.Name, "name": req.Name,
"description": req.Description, "description": req.Description,
@@ -267,10 +295,15 @@ func (cs *CharacterService) UpdateCharacter(req request.UpdateCharacterRequest,
"example_messages": req.ExampleMessages, "example_messages": req.ExampleMessages,
"creator_name": req.CreatorName, "creator_name": req.CreatorName,
"creator_notes": req.CreatorNotes, "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) cardDataJSON, _ := json.Marshal(cardData)
// 处理标签和示例消息 // 处理标签和示例消息,转换为 pq.StringArray
tags := req.Tags tags := req.Tags
if tags == nil { if tags == nil {
tags = []string{} tags = []string{}
@@ -281,25 +314,42 @@ func (cs *CharacterService) UpdateCharacter(req request.UpdateCharacterRequest,
exampleMessages = []string{} exampleMessages = []string{}
} }
// 更新 alternateGreetings := req.AlternateGreetings
updates := map[string]interface{}{ if alternateGreetings == nil {
"name": req.Name, alternateGreetings = []string{}
"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,
} }
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 { if err != nil {
return response.CharacterResponse{}, err return response.CharacterResponse{}, err
} }
@@ -436,11 +486,25 @@ func (cs *CharacterService) ExportCharacter(characterID uint, userID *uint) (map
exampleMessages = character.ExampleMessages 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 格式) // 构建导出数据(兼容 SillyTavern 格式)
exportData := map[string]interface{}{ data := map[string]interface{}{
"spec": "chara_card_v2",
"spec_version": "2.0",
"data": map[string]interface{}{
"name": character.Name, "name": character.Name,
"description": character.Description, "description": character.Description,
"personality": character.Personality, "personality": character.Personality,
@@ -448,13 +512,24 @@ func (cs *CharacterService) ExportCharacter(characterID uint, userID *uint) (map
"first_mes": character.FirstMessage, "first_mes": character.FirstMessage,
"mes_example": strings.Join(exampleMessages, "\n<START>\n"), "mes_example": strings.Join(exampleMessages, "\n<START>\n"),
"creator_notes": character.CreatorNotes, "creator_notes": character.CreatorNotes,
"system_prompt": "", "system_prompt": character.SystemPrompt,
"post_history_instructions": "", "post_history_instructions": character.PostHistoryInstructions,
"tags": tags, "tags": tags,
"creator": character.CreatorName, "creator": character.CreatorName,
"character_version": character.Version, "character_version": character.Version,
"extensions": map[string]interface{}{}, "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 return exportData, nil
@@ -572,28 +647,30 @@ func convertCardToCreateRequest(card *utils.CharacterCardV2, avatarData []byte,
exampleMessages := []string{} exampleMessages := []string{}
if card.Data.MesExample != "" { if card.Data.MesExample != "" {
// 按 <START> 分割 // 按 <START> 分割
exampleMessages = strings.Split(card.Data.MesExample, "<START>") parts := strings.Split(card.Data.MesExample, "<START>")
// 清理空白 for _, msg := range parts {
cleaned := []string{}
for _, msg := range exampleMessages {
msg = strings.TrimSpace(msg) msg = strings.TrimSpace(msg)
if msg != "" { if msg != "" {
cleaned = append(cleaned, msg) exampleMessages = append(exampleMessages, msg)
} }
} }
exampleMessages = cleaned
} }
// 合并备用问候语 // 备用问候语独立存储,不再合并到 exampleMessages
if len(card.Data.AlternateGreetings) > 0 { alternateGreetings := card.Data.AlternateGreetings
exampleMessages = append(exampleMessages, card.Data.AlternateGreetings...) if alternateGreetings == nil {
alternateGreetings = []string{}
} }
// TODO: 处理头像数据,上传到文件服务器 // 保存头像到本地文件
avatar := "" avatar := ""
if avatarData != nil { if avatarData != nil {
// 这里应该将头像上传到文件服务器并获取 URL savedPath, err := saveAvatarFromBytes(avatarData, card.Data.Name, ".png")
// avatar = uploadAvatar(avatarData) if err != nil {
global.GVA_LOG.Warn("保存角色卡头像失败", zap.Error(err))
} else {
avatar = savedPath
}
} }
return request.CreateCharacterRequest{ return request.CreateCharacterRequest{
@@ -608,6 +685,11 @@ func convertCardToCreateRequest(card *utils.CharacterCardV2, avatarData []byte,
ExampleMessages: exampleMessages, ExampleMessages: exampleMessages,
Tags: card.Data.Tags, Tags: card.Data.Tags,
IsPublic: isPublic, 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 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{ return &utils.CharacterCardV2{
Spec: "chara_card_v2", Spec: "chara_card_v2",
SpecVersion: "2.0", SpecVersion: "2.0",
@@ -634,13 +733,14 @@ func convertCharacterToCard(character *app.AICharacter) *utils.CharacterCardV2 {
FirstMes: character.FirstMessage, FirstMes: character.FirstMessage,
MesExample: strings.Join(exampleMessages, "\n<START>\n"), MesExample: strings.Join(exampleMessages, "\n<START>\n"),
CreatorNotes: character.CreatorNotes, CreatorNotes: character.CreatorNotes,
SystemPrompt: "", SystemPrompt: character.SystemPrompt,
PostHistoryInstructions: "", PostHistoryInstructions: character.PostHistoryInstructions,
Tags: tags, Tags: tags,
Creator: character.CreatorName, Creator: character.CreatorName,
CharacterVersion: fmt.Sprintf("%d", character.Version), CharacterVersion: fmt.Sprintf("%d", character.Version),
AlternateGreetings: []string{}, AlternateGreetings: alternateGreetings,
Extensions: map[string]interface{}{}, CharacterBook: characterBook,
Extensions: extensions,
}, },
} }
} }
@@ -673,17 +773,38 @@ func createDefaultAvatar() image.Image {
return img 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 数量(简单估算) // calculateTokenCount 计算角色卡的 Token 数量(简单估算)
func calculateTokenCount(req interface{}) int { func calculateTokenCount(req interface{}) int {
var text string var text string
switch v := req.(type) { switch v := req.(type) {
case request.CreateCharacterRequest: 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 { for _, msg := range v.ExampleMessages {
text += msg text += msg
} }
case request.UpdateCharacterRequest: 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 { for _, msg := range v.ExampleMessages {
text += msg text += msg
} }

View File

@@ -31,6 +31,7 @@ type CharacterCardV2Data struct {
Creator string `json:"creator"` Creator string `json:"creator"`
CharacterVersion string `json:"character_version"` CharacterVersion string `json:"character_version"`
AlternateGreetings []string `json:"alternate_greetings"` AlternateGreetings []string `json:"alternate_greetings"`
CharacterBook map[string]interface{} `json:"character_book,omitempty"`
Extensions map[string]interface{} `json:"extensions"` Extensions map[string]interface{} `json:"extensions"`
} }

View File

@@ -16,6 +16,8 @@ declare module 'vue' {
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElContainer: typeof import('element-plus/es')['ElContainer'] 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'] ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@@ -34,6 +36,8 @@ declare module 'vue' {
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElStatistic: typeof import('element-plus/es')['ElStatistic'] ElStatistic: typeof import('element-plus/es')['ElStatistic']
ElSwitch: typeof import('element-plus/es')['ElSwitch'] 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'] ElTag: typeof import('element-plus/es')['ElTag']
ElUpload: typeof import('element-plus/es')['ElUpload'] ElUpload: typeof import('element-plus/es')['ElUpload']
HelloWorld: typeof import('./components/HelloWorld.vue')['default'] HelloWorld: typeof import('./components/HelloWorld.vue')['default']

View File

@@ -18,6 +18,11 @@ export interface Character {
version: number version: number
firstMessage: string firstMessage: string
exampleMessages: string[] exampleMessages: string[]
systemPrompt: string
postHistoryInstructions: string
alternateGreetings: string[]
characterBook: any
extensions: Record<string, any>
totalChats: number totalChats: number
totalLikes: number totalLikes: number
usageCount: number usageCount: number
@@ -58,6 +63,11 @@ export interface CreateCharacterRequest {
exampleMessages?: string[] exampleMessages?: string[]
tags?: string[] tags?: string[]
isPublic: boolean isPublic: boolean
systemPrompt?: string
postHistoryInstructions?: string
alternateGreetings?: string[]
characterBook?: any
extensions?: Record<string, any>
} }
// 更新角色卡请求 // 更新角色卡请求
@@ -82,6 +92,8 @@ export interface CharacterExportData {
tags: string[] tags: string[]
creator: string creator: string
character_version: number character_version: number
alternate_greetings: string[]
character_book?: any
extensions: Record<string, any> extensions: Record<string, any>
} }
} }

View File

@@ -41,7 +41,7 @@
{{ character.isFavorited ? '已收藏' : '收藏' }} {{ character.isFavorited ? '已收藏' : '收藏' }}
</el-button> </el-button>
<el-button <el-button
:icon="Like" :icon="Top"
@click="handleLike" @click="handleLike"
> >
点赞 {{ character.totalLikes }} 点赞 {{ character.totalLikes }}
@@ -107,6 +107,16 @@
<h3>场景设定</h3> <h3>场景设定</h3>
<p class="detail-text">{{ character.scenario || '暂无场景设定' }}</p> <p class="detail-text">{{ character.scenario || '暂无场景设定' }}</p>
</div> </div>
<div v-if="character.systemPrompt" class="detail-section">
<h3>系统提示词</h3>
<div class="message-box">{{ character.systemPrompt }}</div>
</div>
<div v-if="character.postHistoryInstructions" class="detail-section">
<h3>后置历史指令</h3>
<div class="message-box">{{ character.postHistoryInstructions }}</div>
</div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="对话示例"> <el-tab-pane label="对话示例">
@@ -129,6 +139,19 @@
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane v-if="character.alternateGreetings && character.alternateGreetings.length > 0" label="备用问候语">
<div class="detail-section">
<div
v-for="(greeting, index) in character.alternateGreetings"
:key="index"
class="message-box"
>
<div class="greeting-label">问候语 {{ index + 1 }}</div>
{{ greeting }}
</div>
</div>
</el-tab-pane>
<el-tab-pane label="统计信息"> <el-tab-pane label="统计信息">
<el-descriptions :column="2" border> <el-descriptions :column="2" border>
<el-descriptions-item label="对话总数"> <el-descriptions-item label="对话总数">
@@ -155,6 +178,12 @@
<el-descriptions-item label="更新时间"> <el-descriptions-item label="更新时间">
{{ formatDateTime(character.updatedAt) }} {{ formatDateTime(character.updatedAt) }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item v-if="character.extensions && Object.keys(character.extensions).length > 0" label="扩展数据">
{{ Object.keys(character.extensions).join(', ') }}
</el-descriptions-item>
<el-descriptions-item v-if="character.characterBook" label="角色书">
已配置
</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-tab-pane> </el-tab-pane>
@@ -181,7 +210,7 @@ import {
ChatLineSquare, ChatLineSquare,
Star, Star,
StarFilled, StarFilled,
Like, Top,
Download, Download,
Edit, Edit,
Delete, Delete,
@@ -422,5 +451,12 @@ onMounted(async () => {
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.greeting-label {
font-size: 12px;
color: #909399;
margin-bottom: 8px;
font-weight: 600;
}
} }
</style> </style>

View File

@@ -131,6 +131,82 @@
</el-button> </el-button>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="系统提示词">
<el-input
v-model="formData.systemPrompt"
type="textarea"
:rows="4"
placeholder="系统提示词System Prompt用于设定AI的行为和规则..."
/>
</el-form-item>
<el-form-item label="后置历史指令">
<el-input
v-model="formData.postHistoryInstructions"
type="textarea"
:rows="4"
placeholder="后置历史指令Post History Instructions会被放在对话历史之后..."
/>
</el-form-item>
<el-form-item label="备用问候语">
<div class="example-messages">
<div
v-for="(msg, index) in formData.alternateGreetings"
:key="'ag-' + index"
class="example-message-item"
>
<el-input
v-model="formData.alternateGreetings[index]"
type="textarea"
:rows="3"
placeholder="输入备用问候语..."
/>
<el-button
type="danger"
:icon="Delete"
circle
@click="removeAlternateGreeting(index)"
/>
</div>
<el-button
type="primary"
:icon="Plus"
plain
@click="addAlternateGreeting"
>
添加备用问候语
</el-button>
</div>
</el-form-item>
</el-card>
<el-card class="form-section">
<template #header>
<span>高级设置</span>
</template>
<el-form-item label="角色书">
<el-input
v-model="characterBookText"
type="textarea"
:rows="6"
placeholder="角色书/世界信息JSON 格式),通常通过导入角色卡获取..."
/>
<span class="form-tip">角色书数据为 JSON 格式通常通过导入角色卡自动填入</span>
</el-form-item>
<el-form-item label="扩展数据">
<el-input
v-model="extensionsText"
type="textarea"
:rows="4"
placeholder="扩展数据JSON 格式),如 depth_prompt 等..."
disabled
/>
<span class="form-tip">扩展数据为只读字段保留原版酒馆的扩展信息</span>
</el-form-item>
</el-card> </el-card>
<el-card class="form-section"> <el-card class="form-section">
@@ -199,7 +275,36 @@ const formData = reactive<CreateCharacterRequest>({
firstMessage: '', firstMessage: '',
exampleMessages: [], exampleMessages: [],
tags: [], 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) formData.exampleMessages?.splice(index, 1)
} }
// 添加备用问候语
function addAlternateGreeting() {
formData.alternateGreetings?.push('')
}
// 删除备用问候语
function removeAlternateGreeting(index: number) {
formData.alternateGreetings?.splice(index, 1)
}
// 返回 // 返回
function goBack() { function goBack() {
router.back() router.back()
@@ -296,7 +411,12 @@ onMounted(async () => {
firstMessage: character.firstMessage, firstMessage: character.firstMessage,
exampleMessages: [...(character.exampleMessages || [])], exampleMessages: [...(character.exampleMessages || [])],
tags: [...(character.tags || [])], 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
}) })
} }
} }