新增正则和扩展模块

This commit is contained in:
2026-02-11 23:44:09 +08:00
parent 2bca8e2788
commit 4e611d3a5e
47 changed files with 10058 additions and 49 deletions

View File

@@ -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",