✨ 新增正则和扩展模块
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"git.echol.cn/loser/st/server/model/app/request"
|
||||
"git.echol.cn/loser/st/server/model/app/response"
|
||||
"git.echol.cn/loser/st/server/utils"
|
||||
"github.com/lib/pq"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
@@ -492,18 +494,28 @@ func (cs *CharacterService) ExportCharacter(characterID uint, userID *uint) (map
|
||||
alternateGreetings = character.AlternateGreetings
|
||||
}
|
||||
|
||||
// 解析 character_book JSON
|
||||
// 解析或构建 character_book JSON
|
||||
var characterBook map[string]interface{}
|
||||
if len(character.CharacterBook) > 0 {
|
||||
json.Unmarshal(character.CharacterBook, &characterBook)
|
||||
}
|
||||
|
||||
// 解析 extensions JSON
|
||||
// 如果角色没有内嵌的 CharacterBook,尝试从世界书表中查找关联的世界书
|
||||
if characterBook == nil {
|
||||
characterBook = cs.exportLinkedWorldBook(character.ID)
|
||||
}
|
||||
|
||||
// 解析或构建 extensions JSON
|
||||
extensions := map[string]interface{}{}
|
||||
if len(character.Extensions) > 0 {
|
||||
json.Unmarshal(character.Extensions, &extensions)
|
||||
}
|
||||
|
||||
// 导出关联的正则脚本到 extensions
|
||||
if regexScripts := cs.exportRegexScripts(character.ID); regexScripts != nil && len(regexScripts) > 0 {
|
||||
extensions["regex_scripts"] = regexScripts
|
||||
}
|
||||
|
||||
// 构建导出数据(兼容 SillyTavern 格式)
|
||||
data := map[string]interface{}{
|
||||
"name": character.Name,
|
||||
@@ -522,7 +534,7 @@ func (cs *CharacterService) ExportCharacter(characterID uint, userID *uint) (map
|
||||
"extensions": extensions,
|
||||
}
|
||||
|
||||
// 仅在存在时添加 character_book
|
||||
// 仅在存在时添加 character_book(现在包含关联的世界书)
|
||||
if characterBook != nil {
|
||||
data["character_book"] = characterBook
|
||||
}
|
||||
@@ -595,6 +607,39 @@ func (cs *CharacterService) ImportCharacter(fileData []byte, filename string, us
|
||||
return response.CharacterResponse{}, err
|
||||
}
|
||||
|
||||
// 处理角色卡中的世界书数据(CharacterBook)
|
||||
if card.Data.CharacterBook != nil && len(card.Data.CharacterBook) > 0 {
|
||||
global.GVA_LOG.Info("检测到角色卡包含世界书数据,开始导入世界书",
|
||||
zap.Uint("characterID", result.ID))
|
||||
|
||||
if err := cs.importCharacterBook(userID, result.ID, card.Data.CharacterBook); err != nil {
|
||||
global.GVA_LOG.Warn("导入世界书失败(不影响角色卡导入)",
|
||||
zap.Error(err),
|
||||
zap.Uint("characterID", result.ID))
|
||||
} else {
|
||||
global.GVA_LOG.Info("世界书导入成功", zap.Uint("characterID", result.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理角色卡中的扩展数据(Extensions)
|
||||
if card.Data.Extensions != nil && len(card.Data.Extensions) > 0 {
|
||||
global.GVA_LOG.Info("检测到角色卡包含扩展数据,开始处理扩展",
|
||||
zap.Uint("characterID", result.ID))
|
||||
|
||||
// 处理 Regex 脚本
|
||||
if regexScripts, ok := card.Data.Extensions["regex_scripts"]; ok {
|
||||
if err := cs.importRegexScripts(userID, result.ID, regexScripts); err != nil {
|
||||
global.GVA_LOG.Warn("导入正则脚本失败(不影响角色卡导入)",
|
||||
zap.Error(err),
|
||||
zap.Uint("characterID", result.ID))
|
||||
} else {
|
||||
global.GVA_LOG.Info("正则脚本导入成功", zap.Uint("characterID", result.ID))
|
||||
}
|
||||
}
|
||||
|
||||
// 其他扩展数据已经存储在 Extensions 字段中,无需额外处理
|
||||
}
|
||||
|
||||
global.GVA_LOG.Info("角色卡导入完成", zap.Uint("characterID", result.ID))
|
||||
return result, nil
|
||||
}
|
||||
@@ -621,7 +666,7 @@ func (cs *CharacterService) ExportCharacterAsPNG(characterID uint, userID *uint)
|
||||
}
|
||||
|
||||
// 构建角色卡数据
|
||||
card := convertCharacterToCard(&character)
|
||||
card := cs.convertCharacterToCard(&character)
|
||||
|
||||
// 获取角色头像
|
||||
var img image.Image
|
||||
@@ -654,6 +699,481 @@ func (cs *CharacterService) ExportCharacterAsPNG(characterID uint, userID *uint)
|
||||
return pngData, nil
|
||||
}
|
||||
|
||||
// createCharacterFromRequest 从请求创建角色卡对象(用于事务)
|
||||
func createCharacterFromRequest(req request.CreateCharacterRequest, userID uint) app.AICharacter {
|
||||
// 处理标签和示例消息
|
||||
tags := req.Tags
|
||||
if tags == nil {
|
||||
tags = []string{}
|
||||
}
|
||||
|
||||
exampleMessages := req.ExampleMessages
|
||||
if exampleMessages == nil {
|
||||
exampleMessages = []string{}
|
||||
}
|
||||
|
||||
alternateGreetings := req.AlternateGreetings
|
||||
if alternateGreetings == nil {
|
||||
alternateGreetings = []string{}
|
||||
}
|
||||
|
||||
// 构建 CardData
|
||||
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,
|
||||
"system_prompt": req.SystemPrompt,
|
||||
"post_history_instructions": req.PostHistoryInstructions,
|
||||
"alternate_greetings": req.AlternateGreetings,
|
||||
"character_book": req.CharacterBook,
|
||||
"extensions": req.Extensions,
|
||||
}
|
||||
cardDataJSON, _ := json.Marshal(cardData)
|
||||
|
||||
// 序列化 JSON 字段
|
||||
var characterBookJSON, extensionsJSON datatypes.JSON
|
||||
if req.CharacterBook != nil {
|
||||
characterBookJSON, _ = json.Marshal(req.CharacterBook)
|
||||
}
|
||||
if req.Extensions != nil {
|
||||
extensionsJSON, _ = json.Marshal(req.Extensions)
|
||||
}
|
||||
|
||||
return 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,
|
||||
SystemPrompt: req.SystemPrompt,
|
||||
PostHistoryInstructions: req.PostHistoryInstructions,
|
||||
AlternateGreetings: alternateGreetings,
|
||||
CharacterBook: characterBookJSON,
|
||||
Extensions: extensionsJSON,
|
||||
TokenCount: calculateTokenCount(req),
|
||||
}
|
||||
}
|
||||
|
||||
// importCharacterBookWithTx 在事务中导入角色卡中的世界书数据
|
||||
func (cs *CharacterService) importCharacterBookWithTx(tx *gorm.DB, userID, characterID uint, characterBook map[string]interface{}) error {
|
||||
// 解析世界书名称
|
||||
bookName := ""
|
||||
if name, ok := characterBook["name"].(string); ok && name != "" {
|
||||
bookName = name
|
||||
}
|
||||
|
||||
// 如果没有名称,使用角色名称
|
||||
if bookName == "" {
|
||||
var character app.AICharacter
|
||||
if err := tx.Where("id = ?", characterID).First(&character).Error; err == nil {
|
||||
bookName = character.Name + " 的世界书"
|
||||
} else {
|
||||
bookName = "角色世界书"
|
||||
}
|
||||
}
|
||||
|
||||
// 解析世界书条目
|
||||
entries := []app.AIWorldInfoEntry{}
|
||||
if entriesData, ok := characterBook["entries"].([]interface{}); ok {
|
||||
for i, entryData := range entriesData {
|
||||
if entryMap, ok := entryData.(map[string]interface{}); ok {
|
||||
entry := convertToWorldInfoEntry(entryMap, i)
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
global.GVA_LOG.Warn("角色卡中的世界书没有有效条目,跳过导入")
|
||||
return nil // 没有条目时不报错,只是跳过
|
||||
}
|
||||
|
||||
// 序列化条目
|
||||
entriesJSON, err := json.Marshal(entries)
|
||||
if err != nil {
|
||||
return errors.New("序列化世界书条目失败: " + err.Error())
|
||||
}
|
||||
|
||||
// 创建世界书记录
|
||||
worldBook := &app.AIWorldInfo{
|
||||
UserID: userID,
|
||||
BookName: bookName,
|
||||
IsGlobal: false,
|
||||
Entries: datatypes.JSON(entriesJSON),
|
||||
LinkedChars: pq.StringArray{fmt.Sprintf("%d", characterID)},
|
||||
}
|
||||
|
||||
if err := tx.Create(worldBook).Error; err != nil {
|
||||
return errors.New("创建世界书记录失败: " + err.Error())
|
||||
}
|
||||
|
||||
global.GVA_LOG.Info("成功从角色卡导入世界书",
|
||||
zap.Uint("worldBookID", worldBook.ID),
|
||||
zap.String("bookName", bookName),
|
||||
zap.Int("entriesCount", len(entries)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// importRegexScripts 导入角色卡中的正则脚本
|
||||
func (cs *CharacterService) importRegexScripts(userID, characterID uint, regexScriptsData interface{}) error {
|
||||
scriptsArray, ok := regexScriptsData.([]interface{})
|
||||
if !ok {
|
||||
return errors.New("正则脚本数据格式错误")
|
||||
}
|
||||
|
||||
if len(scriptsArray) == 0 {
|
||||
global.GVA_LOG.Info("角色卡中没有正则脚本数据")
|
||||
return nil
|
||||
}
|
||||
|
||||
characterIDStr := fmt.Sprintf("%d", characterID)
|
||||
imported := 0
|
||||
|
||||
for i, scriptData := range scriptsArray {
|
||||
scriptMap, ok := scriptData.(map[string]interface{})
|
||||
if !ok {
|
||||
global.GVA_LOG.Warn("跳过无效的正则脚本数据", zap.Int("index", i))
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析正则脚本
|
||||
script := convertMapToRegexScript(scriptMap, characterIDStr)
|
||||
script.UserID = userID
|
||||
|
||||
// 验证正则表达式
|
||||
if _, err := regexp.Compile(script.FindRegex); err != nil {
|
||||
global.GVA_LOG.Warn("跳过无效的正则表达式",
|
||||
zap.Int("index", i),
|
||||
zap.String("regex", script.FindRegex),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否已存在同名脚本
|
||||
var existingCount int64
|
||||
global.GVA_DB.Model(&app.AIRegexScript{}).
|
||||
Where("user_id = ? AND script_name = ?", userID, script.ScriptName).
|
||||
Count(&existingCount)
|
||||
|
||||
if existingCount > 0 {
|
||||
script.ScriptName = script.ScriptName + fmt.Sprintf(" (角色-%d)", characterID)
|
||||
}
|
||||
|
||||
// 创建脚本
|
||||
if err := global.GVA_DB.Create(&script).Error; err != nil {
|
||||
global.GVA_LOG.Warn("创建正则脚本失败",
|
||||
zap.Int("index", i),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
imported++
|
||||
}
|
||||
|
||||
global.GVA_LOG.Info("成功导入正则脚本",
|
||||
zap.Uint("characterID", characterID),
|
||||
zap.Int("imported", imported))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertMapToRegexScript 将 map 转换为 RegexScript
|
||||
func convertMapToRegexScript(scriptMap map[string]interface{}, characterIDStr string) app.AIRegexScript {
|
||||
script := app.AIRegexScript{
|
||||
ScriptName: getStringValue(scriptMap, "scriptName", "未命名脚本"),
|
||||
Description: getStringValue(scriptMap, "description", ""),
|
||||
FindRegex: getStringValue(scriptMap, "findRegex", ""),
|
||||
ReplaceString: getStringValue(scriptMap, "replaceString", ""),
|
||||
Enabled: getBoolValue(scriptMap, "enabled", true),
|
||||
IsGlobal: false, // 从角色卡导入的脚本默认不是全局脚本
|
||||
TrimStrings: getBoolValue(scriptMap, "trimStrings", false),
|
||||
OnlyFormat: getBoolValue(scriptMap, "onlyFormat", false),
|
||||
RunOnEdit: getBoolValue(scriptMap, "runOnEdit", false),
|
||||
SubstituteRegex: getBoolValue(scriptMap, "substituteRegex", false),
|
||||
Placement: getStringValue(scriptMap, "placement", ""),
|
||||
LinkedChars: pq.StringArray{characterIDStr},
|
||||
}
|
||||
|
||||
// 处理可选的数字字段
|
||||
if val, ok := scriptMap["minDepth"]; ok {
|
||||
if intVal := getIntValue(scriptMap, "minDepth", 0); intVal != 0 {
|
||||
script.MinDepth = &intVal
|
||||
} else if val != nil {
|
||||
intVal := 0
|
||||
script.MinDepth = &intVal
|
||||
}
|
||||
}
|
||||
if val, ok := scriptMap["maxDepth"]; ok {
|
||||
if intVal := getIntValue(scriptMap, "maxDepth", 0); intVal != 0 {
|
||||
script.MaxDepth = &intVal
|
||||
} else if val != nil {
|
||||
intVal := 0
|
||||
script.MaxDepth = &intVal
|
||||
}
|
||||
}
|
||||
if val, ok := scriptMap["affectMinDepth"]; ok {
|
||||
if intVal := getIntValue(scriptMap, "affectMinDepth", 0); intVal != 0 {
|
||||
script.AffectMinDepth = &intVal
|
||||
} else if val != nil {
|
||||
intVal := 0
|
||||
script.AffectMinDepth = &intVal
|
||||
}
|
||||
}
|
||||
if val, ok := scriptMap["affectMaxDepth"]; ok {
|
||||
if intVal := getIntValue(scriptMap, "affectMaxDepth", 0); intVal != 0 {
|
||||
script.AffectMaxDepth = &intVal
|
||||
} else if val != nil {
|
||||
intVal := 0
|
||||
script.AffectMaxDepth = &intVal
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 ScriptData
|
||||
if scriptData, ok := scriptMap["scriptData"].(map[string]interface{}); ok && scriptData != nil {
|
||||
if data, err := datatypes.NewJSONType(scriptData).MarshalJSON(); err == nil {
|
||||
script.ScriptData = data
|
||||
}
|
||||
}
|
||||
|
||||
return script
|
||||
}
|
||||
|
||||
// exportRegexScripts 导出角色关联的正则脚本
|
||||
func (cs *CharacterService) exportRegexScripts(characterID uint) []map[string]interface{} {
|
||||
// 查找关联的正则脚本
|
||||
var scripts []app.AIRegexScript
|
||||
err := global.GVA_DB.
|
||||
Where("? = ANY(linked_chars)", fmt.Sprintf("%d", characterID)).
|
||||
Find(&scripts).Error
|
||||
|
||||
if err != nil || len(scripts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换为 map 格式
|
||||
scriptsData := make([]map[string]interface{}, 0, len(scripts))
|
||||
for _, script := range scripts {
|
||||
scriptMap := map[string]interface{}{
|
||||
"scriptName": script.ScriptName,
|
||||
"description": script.Description,
|
||||
"findRegex": script.FindRegex,
|
||||
"replaceString": script.ReplaceString,
|
||||
"enabled": script.Enabled,
|
||||
"trimStrings": script.TrimStrings,
|
||||
"onlyFormat": script.OnlyFormat,
|
||||
"runOnEdit": script.RunOnEdit,
|
||||
"substituteRegex": script.SubstituteRegex,
|
||||
"placement": script.Placement,
|
||||
}
|
||||
|
||||
// 添加可选字段
|
||||
if script.MinDepth != nil {
|
||||
scriptMap["minDepth"] = *script.MinDepth
|
||||
}
|
||||
if script.MaxDepth != nil {
|
||||
scriptMap["maxDepth"] = *script.MaxDepth
|
||||
}
|
||||
if script.AffectMinDepth != nil {
|
||||
scriptMap["affectMinDepth"] = *script.AffectMinDepth
|
||||
}
|
||||
if script.AffectMaxDepth != nil {
|
||||
scriptMap["affectMaxDepth"] = *script.AffectMaxDepth
|
||||
}
|
||||
|
||||
// 添加 ScriptData
|
||||
if len(script.ScriptData) > 0 {
|
||||
var scriptData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(script.ScriptData), &scriptData); err == nil {
|
||||
scriptMap["scriptData"] = scriptData
|
||||
}
|
||||
}
|
||||
|
||||
scriptsData = append(scriptsData, scriptMap)
|
||||
}
|
||||
|
||||
return scriptsData
|
||||
}
|
||||
|
||||
// importCharacterBook 导入角色卡中的世界书数据(已废弃,使用 importCharacterBookWithTx)
|
||||
func (cs *CharacterService) importCharacterBook(userID, characterID uint, characterBook map[string]interface{}) error {
|
||||
// 解析世界书名称
|
||||
bookName := "角色世界书"
|
||||
if name, ok := characterBook["name"].(string); ok && name != "" {
|
||||
bookName = name
|
||||
} else {
|
||||
// 获取角色名称作为世界书名称
|
||||
var character app.AICharacter
|
||||
if err := global.GVA_DB.Where("id = ?", characterID).First(&character).Error; err == nil {
|
||||
bookName = character.Name + " 的世界书"
|
||||
}
|
||||
}
|
||||
|
||||
// 解析世界书条目
|
||||
entries := []app.AIWorldInfoEntry{}
|
||||
if entriesData, ok := characterBook["entries"].([]interface{}); ok {
|
||||
for i, entryData := range entriesData {
|
||||
if entryMap, ok := entryData.(map[string]interface{}); ok {
|
||||
entry := convertToWorldInfoEntry(entryMap, i)
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
return errors.New("世界书中没有有效的条目")
|
||||
}
|
||||
|
||||
// 序列化条目
|
||||
entriesJSON, err := json.Marshal(entries)
|
||||
if err != nil {
|
||||
return errors.New("序列化世界书条目失败: " + err.Error())
|
||||
}
|
||||
|
||||
// 创建世界书记录
|
||||
worldBook := &app.AIWorldInfo{
|
||||
UserID: userID,
|
||||
BookName: bookName,
|
||||
IsGlobal: false,
|
||||
Entries: datatypes.JSON(entriesJSON),
|
||||
LinkedChars: pq.StringArray{fmt.Sprintf("%d", characterID)},
|
||||
}
|
||||
|
||||
if err := global.GVA_DB.Create(worldBook).Error; err != nil {
|
||||
return errors.New("创建世界书记录失败: " + err.Error())
|
||||
}
|
||||
|
||||
global.GVA_LOG.Info("成功从角色卡导入世界书",
|
||||
zap.Uint("worldBookID", worldBook.ID),
|
||||
zap.String("bookName", bookName),
|
||||
zap.Int("entriesCount", len(entries)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToWorldInfoEntry 将角色卡中的世界书条目转换为标准格式
|
||||
func convertToWorldInfoEntry(entryMap map[string]interface{}, index int) app.AIWorldInfoEntry {
|
||||
entry := app.AIWorldInfoEntry{
|
||||
UID: getStringValue(entryMap, "uid", fmt.Sprintf("entry_%d", index)),
|
||||
Enabled: getBoolValue(entryMap, "enabled", true),
|
||||
Order: getIntValue(entryMap, "insertion_order", index),
|
||||
Content: getStringValue(entryMap, "content", ""),
|
||||
Comment: getStringValue(entryMap, "comment", ""),
|
||||
}
|
||||
|
||||
// 解析关键词
|
||||
if keys, ok := entryMap["keys"].([]interface{}); ok {
|
||||
entry.Keys = convertToStringArray(keys)
|
||||
}
|
||||
|
||||
if secondaryKeys, ok := entryMap["secondary_keys"].([]interface{}); ok {
|
||||
entry.SecondaryKeys = convertToStringArray(secondaryKeys)
|
||||
}
|
||||
|
||||
// 高级选项
|
||||
entry.Constant = getBoolValue(entryMap, "constant", false)
|
||||
entry.Selective = getBoolValue(entryMap, "selective", false)
|
||||
entry.Position = getStringValue(entryMap, "position", "before_char")
|
||||
|
||||
if depth, ok := entryMap["depth"].(float64); ok {
|
||||
entry.Depth = int(depth)
|
||||
}
|
||||
|
||||
// 概率设置
|
||||
entry.UseProbability = getBoolValue(entryMap, "use_probability", false)
|
||||
if prob, ok := entryMap["probability"].(float64); ok {
|
||||
entry.Probability = int(prob)
|
||||
}
|
||||
|
||||
// 分组设置
|
||||
entry.Group = getStringValue(entryMap, "group", "")
|
||||
entry.GroupOverride = getBoolValue(entryMap, "group_override", false)
|
||||
if weight, ok := entryMap["group_weight"].(float64); ok {
|
||||
entry.GroupWeight = int(weight)
|
||||
}
|
||||
|
||||
// 递归设置
|
||||
entry.PreventRecursion = getBoolValue(entryMap, "prevent_recursion", false)
|
||||
entry.DelayUntilRecursion = getBoolValue(entryMap, "delay_until_recursion", false)
|
||||
|
||||
// 扫描深度
|
||||
if scanDepth, ok := entryMap["scan_depth"].(float64); ok {
|
||||
depth := int(scanDepth)
|
||||
entry.ScanDepth = &depth
|
||||
}
|
||||
|
||||
// 匹配选项
|
||||
if caseSensitive, ok := entryMap["case_sensitive"].(bool); ok {
|
||||
entry.CaseSensitive = &caseSensitive
|
||||
}
|
||||
if matchWholeWords, ok := entryMap["match_whole_words"].(bool); ok {
|
||||
entry.MatchWholeWords = &matchWholeWords
|
||||
}
|
||||
if useRegex, ok := entryMap["use_regex"].(bool); ok {
|
||||
entry.UseRegex = &useRegex
|
||||
}
|
||||
|
||||
// 其他字段
|
||||
entry.Automation = getStringValue(entryMap, "automation_id", "")
|
||||
entry.Role = getStringValue(entryMap, "role", "")
|
||||
entry.VectorizedContent = getStringValue(entryMap, "vectorized", "")
|
||||
|
||||
// 扩展数据
|
||||
if extensions, ok := entryMap["extensions"].(map[string]interface{}); ok {
|
||||
entry.Extensions = extensions
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
// 辅助函数:从 map 中安全获取字符串值
|
||||
func getStringValue(m map[string]interface{}, key, defaultValue string) string {
|
||||
if val, ok := m[key].(string); ok {
|
||||
return val
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 辅助函数:从 map 中安全获取布尔值
|
||||
func getBoolValue(m map[string]interface{}, key string, defaultValue bool) bool {
|
||||
if val, ok := m[key].(bool); ok {
|
||||
return val
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 辅助函数:从 map 中安全获取整数值
|
||||
func getIntValue(m map[string]interface{}, key string, defaultValue int) int {
|
||||
if val, ok := m[key].(float64); ok {
|
||||
return int(val)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 辅助函数:将 []interface{} 转换为 []string
|
||||
func convertToStringArray(arr []interface{}) []string {
|
||||
result := make([]string, 0, len(arr))
|
||||
for _, item := range arr {
|
||||
if str, ok := item.(string); ok {
|
||||
result = append(result, str)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// convertCardToCreateRequest 将角色卡转换为创建请求
|
||||
func convertCardToCreateRequest(card *utils.CharacterCardV2, avatarData []byte, isPublic bool) request.CreateCharacterRequest {
|
||||
// 处理示例消息
|
||||
@@ -706,8 +1226,83 @@ func convertCardToCreateRequest(card *utils.CharacterCardV2, avatarData []byte,
|
||||
}
|
||||
}
|
||||
|
||||
// exportLinkedWorldBook 导出角色关联的世界书数据
|
||||
func (cs *CharacterService) exportLinkedWorldBook(characterID uint) map[string]interface{} {
|
||||
// 查找关联的世界书
|
||||
var worldBooks []app.AIWorldInfo
|
||||
err := global.GVA_DB.
|
||||
Where("? = ANY(linked_chars)", fmt.Sprintf("%d", characterID)).
|
||||
Find(&worldBooks).Error
|
||||
|
||||
if err != nil || len(worldBooks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 合并所有世界书的条目
|
||||
var allEntries []app.AIWorldInfoEntry
|
||||
var bookName string
|
||||
|
||||
for _, book := range worldBooks {
|
||||
var entries []app.AIWorldInfoEntry
|
||||
if err := json.Unmarshal([]byte(book.Entries), &entries); err != nil {
|
||||
continue
|
||||
}
|
||||
allEntries = append(allEntries, entries...)
|
||||
if bookName == "" {
|
||||
bookName = book.BookName
|
||||
}
|
||||
}
|
||||
|
||||
if len(allEntries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换为 CharacterBook 格式
|
||||
entriesData := make([]map[string]interface{}, 0, len(allEntries))
|
||||
for _, entry := range allEntries {
|
||||
entryMap := map[string]interface{}{
|
||||
"uid": entry.UID,
|
||||
"keys": entry.Keys,
|
||||
"secondary_keys": entry.SecondaryKeys,
|
||||
"content": entry.Content,
|
||||
"comment": entry.Comment,
|
||||
"enabled": entry.Enabled,
|
||||
"constant": entry.Constant,
|
||||
"selective": entry.Selective,
|
||||
"insertion_order": entry.Order,
|
||||
"position": entry.Position,
|
||||
"depth": entry.Depth,
|
||||
"use_probability": entry.UseProbability,
|
||||
"probability": entry.Probability,
|
||||
"group": entry.Group,
|
||||
"group_override": entry.GroupOverride,
|
||||
"group_weight": entry.GroupWeight,
|
||||
"prevent_recursion": entry.PreventRecursion,
|
||||
"delay_until_recursion": entry.DelayUntilRecursion,
|
||||
"scan_depth": entry.ScanDepth,
|
||||
"case_sensitive": entry.CaseSensitive,
|
||||
"match_whole_words": entry.MatchWholeWords,
|
||||
"use_regex": entry.UseRegex,
|
||||
"automation_id": entry.Automation,
|
||||
"role": entry.Role,
|
||||
"vectorized": entry.VectorizedContent,
|
||||
}
|
||||
|
||||
if entry.Extensions != nil {
|
||||
entryMap["extensions"] = entry.Extensions
|
||||
}
|
||||
|
||||
entriesData = append(entriesData, entryMap)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"name": bookName,
|
||||
"entries": entriesData,
|
||||
}
|
||||
}
|
||||
|
||||
// convertCharacterToCard 将角色卡转换为 CharacterCardV2
|
||||
func convertCharacterToCard(character *app.AICharacter) *utils.CharacterCardV2 {
|
||||
func (cs *CharacterService) convertCharacterToCard(character *app.AICharacter) *utils.CharacterCardV2 {
|
||||
tags := []string{}
|
||||
if character.Tags != nil {
|
||||
tags = character.Tags
|
||||
@@ -723,18 +1318,28 @@ func convertCharacterToCard(character *app.AICharacter) *utils.CharacterCardV2 {
|
||||
alternateGreetings = character.AlternateGreetings
|
||||
}
|
||||
|
||||
// 解析 character_book JSON
|
||||
// 解析或构建 character_book JSON
|
||||
var characterBook map[string]interface{}
|
||||
if len(character.CharacterBook) > 0 {
|
||||
json.Unmarshal(character.CharacterBook, &characterBook)
|
||||
}
|
||||
|
||||
// 解析 extensions JSON
|
||||
// 如果角色没有内嵌的 CharacterBook,尝试从世界书表中查找关联的世界书
|
||||
if characterBook == nil {
|
||||
characterBook = cs.exportLinkedWorldBook(character.ID)
|
||||
}
|
||||
|
||||
// 解析或构建 extensions JSON
|
||||
extensions := map[string]interface{}{}
|
||||
if len(character.Extensions) > 0 {
|
||||
json.Unmarshal(character.Extensions, &extensions)
|
||||
}
|
||||
|
||||
// 导出关联的正则脚本到 extensions
|
||||
if regexScripts := cs.exportRegexScripts(character.ID); regexScripts != nil && len(regexScripts) > 0 {
|
||||
extensions["regex_scripts"] = regexScripts
|
||||
}
|
||||
|
||||
return &utils.CharacterCardV2{
|
||||
Spec: "chara_card_v2",
|
||||
SpecVersion: "2.0",
|
||||
|
||||
Reference in New Issue
Block a user