新增正则和扩展模块

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

View File

@@ -4,4 +4,6 @@ type AppServiceGroup struct {
AuthService
CharacterService
WorldInfoService
ExtensionService
RegexScriptService
}

View File

@@ -0,0 +1,803 @@
package app
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/app"
"git.echol.cn/loser/st/server/model/app/request"
"git.echol.cn/loser/st/server/model/app/response"
"go.uber.org/zap"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type ExtensionService struct{}
// CreateExtension 创建/安装扩展
func (es *ExtensionService) CreateExtension(userID uint, req *request.CreateExtensionRequest) (*app.AIExtension, error) {
// 检查扩展是否已存在
var existing app.AIExtension
err := global.GVA_DB.Where("user_id = ? AND name = ?", userID, req.Name).First(&existing).Error
if err == nil {
return nil, errors.New("扩展已存在")
}
if err != gorm.ErrRecordNotFound {
return nil, err
}
// 序列化 JSON 字段
tagsJSON, _ := json.Marshal(req.Tags)
dependenciesJSON, _ := json.Marshal(req.Dependencies)
conflictsJSON, _ := json.Marshal(req.Conflicts)
manifestJSON, _ := json.Marshal(req.ManifestData)
assetsJSON, _ := json.Marshal(req.AssetsPaths)
settingsJSON, _ := json.Marshal(req.Settings)
optionsJSON, _ := json.Marshal(req.Options)
metadataJSON, _ := json.Marshal(req.Metadata)
extension := &app.AIExtension{
UserID: userID,
Name: req.Name,
DisplayName: req.DisplayName,
Version: req.Version,
Author: req.Author,
Description: req.Description,
Homepage: req.Homepage,
Repository: req.Repository,
License: req.License,
Tags: datatypes.JSON(tagsJSON),
ExtensionType: req.ExtensionType,
Category: req.Category,
Dependencies: datatypes.JSON(dependenciesJSON),
Conflicts: datatypes.JSON(conflictsJSON),
ManifestData: datatypes.JSON(manifestJSON),
ScriptPath: req.ScriptPath,
StylePath: req.StylePath,
AssetsPaths: datatypes.JSON(assetsJSON),
Settings: datatypes.JSON(settingsJSON),
Options: datatypes.JSON(optionsJSON),
IsEnabled: false,
IsInstalled: true,
IsSystemExt: false,
InstallSource: req.InstallSource,
SourceURL: req.SourceURL,
Branch: req.Branch,
AutoUpdate: req.AutoUpdate,
InstallDate: time.Now(),
Metadata: datatypes.JSON(metadataJSON),
}
if err := global.GVA_DB.Create(extension).Error; err != nil {
return nil, err
}
global.GVA_LOG.Info("扩展安装成功", zap.Uint("extensionID", extension.ID), zap.String("name", extension.Name))
return extension, nil
}
// UpdateExtension 更新扩展
func (es *ExtensionService) UpdateExtension(userID, extensionID uint, req *request.UpdateExtensionRequest) error {
var extension app.AIExtension
if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil {
return errors.New("扩展不存在")
}
// 系统内置扩展不允许修改
if extension.IsSystemExt {
return errors.New("系统内置扩展不允许修改")
}
updates := map[string]interface{}{}
if req.DisplayName != "" {
updates["display_name"] = req.DisplayName
}
if req.Description != "" {
updates["description"] = req.Description
}
if req.Settings != nil {
settingsJSON, _ := json.Marshal(req.Settings)
updates["settings"] = datatypes.JSON(settingsJSON)
}
if req.Options != nil {
optionsJSON, _ := json.Marshal(req.Options)
updates["options"] = datatypes.JSON(optionsJSON)
}
if req.Metadata != nil {
metadataJSON, _ := json.Marshal(req.Metadata)
updates["metadata"] = datatypes.JSON(metadataJSON)
}
if err := global.GVA_DB.Model(&extension).Updates(updates).Error; err != nil {
return err
}
return nil
}
// DeleteExtension 删除/卸载扩展
func (es *ExtensionService) DeleteExtension(userID, extensionID uint, deleteFiles bool) error {
var extension app.AIExtension
if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil {
return errors.New("扩展不存在")
}
// 系统内置扩展不允许删除
if extension.IsSystemExt {
return errors.New("系统内置扩展不允许删除")
}
// TODO: 如果 deleteFiles=true删除扩展文件
// 这需要文件系统支持
// 删除扩展(配置已经在扩展记录的 Settings 字段中,无需单独删除)
if err := global.GVA_DB.Delete(&extension).Error; err != nil {
return err
}
global.GVA_LOG.Info("扩展卸载成功", zap.Uint("extensionID", extensionID))
return nil
}
// GetExtension 获取扩展详情
func (es *ExtensionService) GetExtension(userID, extensionID uint) (*app.AIExtension, error) {
var extension app.AIExtension
if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil {
return nil, errors.New("扩展不存在")
}
return &extension, nil
}
// GetExtensionList 获取扩展列表
func (es *ExtensionService) GetExtensionList(userID uint, req *request.ExtensionListRequest) (*response.ExtensionListResponse, error) {
var extensions []app.AIExtension
var total int64
db := global.GVA_DB.Model(&app.AIExtension{}).Where("user_id = ?", userID)
// 过滤条件
if req.Name != "" {
db = db.Where("name ILIKE ? OR display_name ILIKE ?", "%"+req.Name+"%", "%"+req.Name+"%")
}
if req.ExtensionType != "" {
db = db.Where("extension_type = ?", req.ExtensionType)
}
if req.Category != "" {
db = db.Where("category = ?", req.Category)
}
if req.IsEnabled != nil {
db = db.Where("is_enabled = ?", *req.IsEnabled)
}
if req.IsInstalled != nil {
db = db.Where("is_installed = ?", *req.IsInstalled)
}
if req.Tag != "" {
db = db.Where("tags @> ?", fmt.Sprintf(`["%s"]`, req.Tag))
}
// 统计总数
if err := db.Count(&total).Error; err != nil {
return nil, err
}
// 分页查询
if err := db.Scopes(req.Paginate()).Order("created_at DESC").Find(&extensions).Error; err != nil {
return nil, err
}
// 转换响应
result := make([]response.ExtensionResponse, 0, len(extensions))
for i := range extensions {
result = append(result, response.ToExtensionResponse(&extensions[i]))
}
return &response.ExtensionListResponse{
List: result,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// ToggleExtension 启用/禁用扩展
func (es *ExtensionService) ToggleExtension(userID, extensionID uint, isEnabled bool) error {
var extension app.AIExtension
if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil {
return errors.New("扩展不存在")
}
// 检查依赖
if isEnabled {
if err := es.checkDependencies(userID, &extension); err != nil {
return err
}
}
// 检查冲突
if isEnabled {
if err := es.checkConflicts(userID, &extension); err != nil {
return err
}
}
updates := map[string]interface{}{
"is_enabled": isEnabled,
}
if isEnabled {
updates["last_enabled"] = time.Now()
}
if err := global.GVA_DB.Model(&extension).Updates(updates).Error; err != nil {
return err
}
global.GVA_LOG.Info("扩展状态更新", zap.Uint("extensionID", extensionID), zap.Bool("enabled", isEnabled))
return nil
}
// UpdateExtensionSettings 更新扩展配置
func (es *ExtensionService) UpdateExtensionSettings(userID, extensionID uint, settings map[string]interface{}) error {
var extension app.AIExtension
if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil {
return errors.New("扩展不存在")
}
settingsJSON, err := json.Marshal(settings)
if err != nil {
return errors.New("序列化配置失败")
}
// 直接更新扩展表的 settings 字段
return global.GVA_DB.Model(&extension).Update("settings", datatypes.JSON(settingsJSON)).Error
}
// GetExtensionSettings 获取扩展配置
func (es *ExtensionService) GetExtensionSettings(userID, extensionID uint) (map[string]interface{}, error) {
// 获取扩展信息
var extension app.AIExtension
if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil {
return nil, errors.New("扩展不存在")
}
// 从扩展的 Settings 字段读取用户配置
var settings map[string]interface{}
if len(extension.Settings) > 0 {
if err := json.Unmarshal([]byte(extension.Settings), &settings); err != nil {
return nil, errors.New("解析配置失败: " + err.Error())
}
}
// 如果 ManifestData 中有默认配置,合并进来
if len(extension.ManifestData) > 0 {
var manifest map[string]interface{}
if err := json.Unmarshal([]byte(extension.ManifestData), &manifest); err == nil {
if manifestSettings, ok := manifest["settings"].(map[string]interface{}); ok && manifestSettings != nil {
// 只添加用户未设置的默认值
if settings == nil {
settings = make(map[string]interface{})
}
for k, v := range manifestSettings {
if _, exists := settings[k]; !exists {
settings[k] = v
}
}
}
}
}
if settings == nil {
settings = make(map[string]interface{})
}
return settings, nil
}
// UpdateExtensionStats 更新扩展统计
func (es *ExtensionService) UpdateExtensionStats(userID, extensionID uint, action string, value int) error {
var extension app.AIExtension
if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil {
return errors.New("扩展不存在")
}
updates := map[string]interface{}{}
switch action {
case "usage":
updates["usage_count"] = gorm.Expr("usage_count + ?", value)
case "error":
updates["error_count"] = gorm.Expr("error_count + ?", value)
case "load":
// 计算平均加载时间
newAvg := (extension.LoadTime*extension.UsageCount + value) / (extension.UsageCount + 1)
updates["load_time"] = newAvg
default:
return errors.New("未知的统计类型")
}
return global.GVA_DB.Model(&extension).Updates(updates).Error
}
// GetExtensionManifest 获取扩展 manifest
func (es *ExtensionService) GetExtensionManifest(userID, extensionID uint) (*response.ExtensionManifestResponse, error) {
var extension app.AIExtension
if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil {
return nil, errors.New("扩展不存在")
}
var manifestData map[string]interface{}
if extension.ManifestData != nil {
_ = json.Unmarshal([]byte(extension.ManifestData), &manifestData)
}
// 从 manifestData 构建响应
manifest := &response.ExtensionManifestResponse{
Name: extension.Name,
DisplayName: extension.DisplayName,
Version: extension.Version,
Description: extension.Description,
Author: extension.Author,
Homepage: extension.Homepage,
Repository: extension.Repository,
License: extension.License,
Type: extension.ExtensionType,
Category: extension.Category,
Entry: extension.ScriptPath,
Style: extension.StylePath,
}
// 解析数组和对象
if extension.Tags != nil {
_ = json.Unmarshal([]byte(extension.Tags), &manifest.Tags)
}
if extension.Dependencies != nil {
_ = json.Unmarshal([]byte(extension.Dependencies), &manifest.Dependencies)
}
if extension.Conflicts != nil {
_ = json.Unmarshal([]byte(extension.Conflicts), &manifest.Conflicts)
}
if extension.AssetsPaths != nil {
_ = json.Unmarshal([]byte(extension.AssetsPaths), &manifest.Assets)
}
if extension.Settings != nil {
_ = json.Unmarshal([]byte(extension.Settings), &manifest.Settings)
}
if extension.Options != nil {
_ = json.Unmarshal([]byte(extension.Options), &manifest.Options)
}
if extension.Metadata != nil {
_ = json.Unmarshal([]byte(extension.Metadata), &manifest.Metadata)
}
return manifest, nil
}
// ImportExtension 导入扩展(从文件)
func (es *ExtensionService) ImportExtension(userID uint, manifestData []byte) (*app.AIExtension, error) {
// 解析 manifest.json
var manifest app.AIExtensionManifest
if err := json.Unmarshal(manifestData, &manifest); err != nil {
return nil, errors.New("无效的 manifest.json 格式")
}
// 验证必填字段
if manifest.Name == "" || manifest.Version == "" {
return nil, errors.New("manifest 缺少必填字段")
}
// 构建创建请求
req := &request.CreateExtensionRequest{
Name: manifest.Name,
DisplayName: manifest.DisplayName,
Version: manifest.Version,
Author: manifest.Author,
Description: manifest.Description,
Homepage: manifest.Homepage,
Repository: manifest.Repository,
License: manifest.License,
Tags: manifest.Tags,
ExtensionType: manifest.Type,
Category: manifest.Category,
Dependencies: manifest.Dependencies,
Conflicts: manifest.Conflicts,
ScriptPath: manifest.Entry,
StylePath: manifest.Style,
AssetsPaths: manifest.Assets,
Settings: manifest.Settings,
Options: manifest.Options,
InstallSource: "file",
Metadata: manifest.Metadata,
}
// 将 manifest 原始数据也保存
var manifestMap map[string]interface{}
_ = json.Unmarshal(manifestData, &manifestMap)
req.ManifestData = manifestMap
return es.CreateExtension(userID, req)
}
// ExportExtension 导出扩展
func (es *ExtensionService) ExportExtension(userID, extensionID uint) ([]byte, error) {
manifest, err := es.GetExtensionManifest(userID, extensionID)
if err != nil {
return nil, err
}
return json.MarshalIndent(manifest, "", " ")
}
// checkDependencies 检查扩展依赖
func (es *ExtensionService) checkDependencies(userID uint, extension *app.AIExtension) error {
if extension.Dependencies == nil || len(extension.Dependencies) == 0 {
return nil
}
var dependencies map[string]string
_ = json.Unmarshal([]byte(extension.Dependencies), &dependencies)
for depName := range dependencies {
var depExt app.AIExtension
err := global.GVA_DB.Where("user_id = ? AND name = ? AND is_enabled = true", userID, depName).First(&depExt).Error
if err != nil {
return fmt.Errorf("缺少依赖扩展: %s", depName)
}
// TODO: 检查版本号是否满足要求
}
return nil
}
// checkConflicts 检查扩展冲突
func (es *ExtensionService) checkConflicts(userID uint, extension *app.AIExtension) error {
if extension.Conflicts == nil || len(extension.Conflicts) == 0 {
return nil
}
var conflicts []string
_ = json.Unmarshal([]byte(extension.Conflicts), &conflicts)
for _, conflictName := range conflicts {
var conflictExt app.AIExtension
err := global.GVA_DB.Where("user_id = ? AND name = ? AND is_enabled = true", userID, conflictName).First(&conflictExt).Error
if err == nil {
return fmt.Errorf("扩展 %s 与 %s 冲突", extension.Name, conflictName)
}
}
return nil
}
// GetEnabledExtensions 获取用户启用的所有扩展(用于前端加载)
func (es *ExtensionService) GetEnabledExtensions(userID uint) ([]response.ExtensionResponse, error) {
var extensions []app.AIExtension
if err := global.GVA_DB.Where("user_id = ? AND is_enabled = true AND is_installed = true", userID).
Order("created_at ASC").Find(&extensions).Error; err != nil {
return nil, err
}
result := make([]response.ExtensionResponse, 0, len(extensions))
for i := range extensions {
result = append(result, response.ToExtensionResponse(&extensions[i]))
}
return result, nil
}
// InstallExtensionFromURL 智能安装扩展(自动识别 Git URL 或 Manifest URL
func (es *ExtensionService) InstallExtensionFromURL(userID uint, url string, branch string) (*app.AIExtension, error) {
global.GVA_LOG.Info("开始从 URL 安装扩展", zap.String("url", url), zap.String("branch", branch))
// 智能识别 URL 类型
if isGitURL(url) {
global.GVA_LOG.Info("检测到 Git 仓库 URL使用 Git 安装")
if branch == "" {
branch = "main"
}
return es.InstallExtensionFromGit(userID, url, branch)
}
// 否则作为 manifest.json URL 处理
global.GVA_LOG.Info("作为 Manifest URL 处理")
return es.downloadAndInstallFromManifestURL(userID, url)
}
// isGitURL 判断是否为 Git 仓库 URL
func isGitURL(url string) bool {
// Git 仓库特征:
// 1. 包含 .git 后缀
// 2. 包含常见的 Git 托管平台域名github.com, gitlab.com, gitee.com 等)
// 3. 不以 /manifest.json 或 .json 结尾
url = strings.ToLower(url)
// 如果明确以 .json 结尾,不是 Git URL
if strings.HasSuffix(url, ".json") {
return false
}
// 如果包含 .git 后缀,是 Git URL
if strings.HasSuffix(url, ".git") {
return true
}
// 检查是否包含 Git 托管平台域名
gitHosts := []string{
"github.com",
"gitlab.com",
"gitee.com",
"bitbucket.org",
"gitea.io",
"codeberg.org",
}
for _, host := range gitHosts {
if strings.Contains(url, host) {
// 如果包含 Git 平台且不是 raw 文件 URL则认为是 Git 仓库
if !strings.Contains(url, "/raw/") && !strings.Contains(url, "/blob/") {
return true
}
}
}
return false
}
// downloadAndInstallFromManifestURL 从 Manifest URL 下载并安装
func (es *ExtensionService) downloadAndInstallFromManifestURL(userID uint, manifestURL string) (*app.AIExtension, error) {
// 创建 HTTP 客户端
client := &http.Client{
Timeout: 30 * time.Second,
}
// 下载 manifest.json
resp, err := client.Get(manifestURL)
if err != nil {
return nil, fmt.Errorf("下载 manifest.json 失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("下载 manifest.json 失败: HTTP %d", resp.StatusCode)
}
// 读取响应内容
manifestData, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取 manifest.json 失败: %w", err)
}
// 解析 manifest
var manifest app.AIExtensionManifest
if err := json.Unmarshal(manifestData, &manifest); err != nil {
return nil, fmt.Errorf("解析 manifest.json 失败: %w", err)
}
// 验证必填字段
if manifest.Name == "" {
return nil, errors.New("manifest.json 缺少 name 字段")
}
// 检查扩展是否已存在
var existing app.AIExtension
err = global.GVA_DB.Where("user_id = ? AND name = ?", userID, manifest.Name).First(&existing).Error
if err == nil {
return nil, fmt.Errorf("扩展 %s 已安装", manifest.Name)
}
if err != gorm.ErrRecordNotFound {
return nil, err
}
// 将 manifest 转换为 map[string]interface{}
var manifestMap map[string]interface{}
if err := json.Unmarshal(manifestData, &manifestMap); err != nil {
return nil, fmt.Errorf("转换 manifest 失败: %w", err)
}
// 构建创建请求
createReq := &request.CreateExtensionRequest{
Name: manifest.Name,
DisplayName: manifest.DisplayName,
Version: manifest.Version,
Author: manifest.Author,
Description: manifest.Description,
Homepage: manifest.Homepage,
Repository: manifest.Repository, // 使用 manifest 中的 repository
License: manifest.License,
Tags: manifest.Tags,
ExtensionType: manifest.Type,
Category: manifest.Category,
Dependencies: manifest.Dependencies,
Conflicts: manifest.Conflicts,
ManifestData: manifestMap,
ScriptPath: manifest.Entry,
StylePath: manifest.Style,
AssetsPaths: manifest.Assets,
Settings: manifest.Settings,
Options: manifest.Options,
InstallSource: "url",
SourceURL: manifestURL, // 记录原始 URL 用于更新
AutoUpdate: manifest.AutoUpdate,
Metadata: nil,
}
// 确保扩展类型有效
if createReq.ExtensionType == "" {
createReq.ExtensionType = "ui"
}
// 创建扩展
extension, err := es.CreateExtension(userID, createReq)
if err != nil {
return nil, fmt.Errorf("创建扩展失败: %w", err)
}
global.GVA_LOG.Info("从 URL 安装扩展成功",
zap.Uint("extensionID", extension.ID),
zap.String("name", extension.Name),
zap.String("url", manifestURL))
return extension, nil
}
// UpgradeExtension 升级扩展版本(根据安装来源自动选择更新方式)
func (es *ExtensionService) UpgradeExtension(userID, extensionID uint, force bool) (*app.AIExtension, error) {
// 获取扩展信息
var extension app.AIExtension
if err := global.GVA_DB.Where("id = ? AND user_id = ?", extensionID, userID).First(&extension).Error; err != nil {
return nil, errors.New("扩展不存在")
}
global.GVA_LOG.Info("开始升级扩展",
zap.Uint("extensionID", extensionID),
zap.String("name", extension.Name),
zap.String("installSource", extension.InstallSource),
zap.String("sourceUrl", extension.SourceURL))
// 根据安装来源选择更新方式
switch extension.InstallSource {
case "git":
return es.updateExtensionFromGit(userID, &extension, force)
case "url":
return es.updateExtensionFromURL(userID, &extension)
default:
return nil, fmt.Errorf("不支持的安装来源: %s", extension.InstallSource)
}
}
// updateExtensionFromGit 从 Git 仓库更新扩展
func (es *ExtensionService) updateExtensionFromGit(userID uint, extension *app.AIExtension, force bool) (*app.AIExtension, error) {
if extension.SourceURL == "" {
return nil, errors.New("缺少 Git 仓库 URL")
}
global.GVA_LOG.Info("从 Git 更新扩展",
zap.String("name", extension.Name),
zap.String("sourceUrl", extension.SourceURL),
zap.String("branch", extension.Branch))
// 重新克隆(简单方式,避免处理本地修改)
return es.InstallExtensionFromGit(userID, extension.SourceURL, extension.Branch)
}
// updateExtensionFromURL 从 URL 更新扩展(重新下载 manifest.json
func (es *ExtensionService) updateExtensionFromURL(userID uint, extension *app.AIExtension) (*app.AIExtension, error) {
if extension.SourceURL == "" {
return nil, errors.New("缺少 Manifest URL")
}
global.GVA_LOG.Info("从 URL 更新扩展",
zap.String("name", extension.Name),
zap.String("sourceUrl", extension.SourceURL))
// 重新下载并安装
return es.downloadAndInstallFromManifestURL(userID, extension.SourceURL)
}
// InstallExtensionFromGit 从 Git URL 安装扩展
func (es *ExtensionService) InstallExtensionFromGit(userID uint, gitUrl, branch string) (*app.AIExtension, error) {
// 验证 Git URL
if !strings.Contains(gitUrl, "://") && !strings.HasSuffix(gitUrl, ".git") {
return nil, errors.New("无效的 Git URL")
}
// 创建临时目录
tempDir, err := os.MkdirTemp("", "extension-*")
if err != nil {
return nil, fmt.Errorf("创建临时目录失败: %w", err)
}
defer os.RemoveAll(tempDir) // 确保清理临时目录
global.GVA_LOG.Info("开始从 Git 克隆扩展",
zap.String("gitUrl", gitUrl),
zap.String("branch", branch),
zap.String("tempDir", tempDir))
// 执行 git clone
cmd := exec.Command("git", "clone", "--depth=1", "--branch="+branch, gitUrl, tempDir)
output, err := cmd.CombinedOutput()
if err != nil {
global.GVA_LOG.Error("Git clone 失败",
zap.String("gitUrl", gitUrl),
zap.String("output", string(output)),
zap.Error(err))
return nil, fmt.Errorf("Git clone 失败: %s", string(output))
}
// 读取 manifest.json
manifestPath := filepath.Join(tempDir, "manifest.json")
manifestData, err := os.ReadFile(manifestPath)
if err != nil {
return nil, fmt.Errorf("读取 manifest.json 失败: %w", err)
}
// 解析 manifest
var manifest app.AIExtensionManifest
if err := json.Unmarshal(manifestData, &manifest); err != nil {
return nil, fmt.Errorf("解析 manifest.json 失败: %w", err)
}
// 检查扩展是否已存在
var existing app.AIExtension
err = global.GVA_DB.Where("user_id = ? AND name = ?", userID, manifest.Name).First(&existing).Error
if err == nil {
return nil, fmt.Errorf("扩展 %s 已安装", manifest.Name)
}
if err != gorm.ErrRecordNotFound {
return nil, err
}
// 将 manifest 转换为 map[string]interface{}
var manifestMap map[string]interface{}
if err := json.Unmarshal(manifestData, &manifestMap); err != nil {
return nil, fmt.Errorf("转换 manifest 失败: %w", err)
}
// 构建创建请求
createReq := &request.CreateExtensionRequest{
Name: manifest.Name,
DisplayName: manifest.DisplayName,
Version: manifest.Version,
Author: manifest.Author,
Description: manifest.Description,
Homepage: manifest.Homepage,
Repository: manifest.Repository, // 使用 manifest 中的 repository
License: manifest.License,
Tags: manifest.Tags,
ExtensionType: manifest.Type,
Category: manifest.Category,
Dependencies: manifest.Dependencies,
Conflicts: manifest.Conflicts,
ManifestData: manifestMap,
ScriptPath: manifest.Entry,
StylePath: manifest.Style,
AssetsPaths: manifest.Assets,
Settings: manifest.Settings,
Options: manifest.Options,
InstallSource: "git",
SourceURL: gitUrl, // 记录 Git URL 用于更新
Branch: branch, // 记录分支
AutoUpdate: manifest.AutoUpdate,
Metadata: manifest.Metadata,
}
// 创建扩展记录
extension, err := es.CreateExtension(userID, createReq)
if err != nil {
return nil, fmt.Errorf("创建扩展记录失败: %w", err)
}
global.GVA_LOG.Info("从 Git 安装扩展成功",
zap.Uint("extensionID", extension.ID),
zap.String("name", extension.Name),
zap.String("version", extension.Version))
return extension, nil
}

View File

@@ -0,0 +1,476 @@
package app
import (
"errors"
"fmt"
"regexp"
"strings"
"time"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/app"
"git.echol.cn/loser/st/server/model/app/request"
"git.echol.cn/loser/st/server/model/app/response"
"github.com/lib/pq"
"go.uber.org/zap"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type RegexScriptService struct{}
// CreateRegexScript 创建正则脚本
func (rs *RegexScriptService) CreateRegexScript(userID uint, req *request.CreateRegexScriptRequest) (*app.AIRegexScript, error) {
// 验证正则表达式
if _, err := regexp.Compile(req.FindRegex); err != nil {
return nil, errors.New("无效的正则表达式: " + err.Error())
}
// 序列化 ScriptData
var scriptDataJSON datatypes.JSON
if req.ScriptData != nil {
data, err := datatypes.NewJSONType(req.ScriptData).MarshalJSON()
if err != nil {
return nil, errors.New("序列化脚本数据失败: " + err.Error())
}
scriptDataJSON = data
}
linkedChars := pq.StringArray{}
if req.LinkedChars != nil {
linkedChars = req.LinkedChars
}
script := &app.AIRegexScript{
UserID: userID,
ScriptName: req.ScriptName,
Description: req.Description,
FindRegex: req.FindRegex,
ReplaceString: req.ReplaceString,
Enabled: req.Enabled,
IsGlobal: req.IsGlobal,
TrimStrings: req.TrimStrings,
OnlyFormat: req.OnlyFormat,
RunOnEdit: req.RunOnEdit,
SubstituteRegex: req.SubstituteRegex,
MinDepth: req.MinDepth,
MaxDepth: req.MaxDepth,
Placement: req.Placement,
AffectMinDepth: req.AffectMinDepth,
AffectMaxDepth: req.AffectMaxDepth,
LinkedChars: linkedChars,
ScriptData: scriptDataJSON,
}
if err := global.GVA_DB.Create(script).Error; err != nil {
return nil, err
}
return script, nil
}
// UpdateRegexScript 更新正则脚本
func (rs *RegexScriptService) UpdateRegexScript(userID, scriptID uint, req *request.UpdateRegexScriptRequest) error {
// 查询脚本
var script app.AIRegexScript
if err := global.GVA_DB.Where("id = ? AND user_id = ?", scriptID, userID).First(&script).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("脚本不存在")
}
return err
}
// 验证正则表达式
if req.FindRegex != "" {
if _, err := regexp.Compile(req.FindRegex); err != nil {
return errors.New("无效的正则表达式: " + err.Error())
}
script.FindRegex = req.FindRegex
}
// 更新字段
if req.ScriptName != "" {
script.ScriptName = req.ScriptName
}
if req.Description != "" {
script.Description = req.Description
}
if req.ReplaceString != "" {
script.ReplaceString = req.ReplaceString
}
if req.Enabled != nil {
script.Enabled = *req.Enabled
}
if req.IsGlobal != nil {
script.IsGlobal = *req.IsGlobal
}
if req.TrimStrings != nil {
script.TrimStrings = *req.TrimStrings
}
if req.OnlyFormat != nil {
script.OnlyFormat = *req.OnlyFormat
}
if req.RunOnEdit != nil {
script.RunOnEdit = *req.RunOnEdit
}
if req.SubstituteRegex != nil {
script.SubstituteRegex = *req.SubstituteRegex
}
if req.MinDepth != nil {
script.MinDepth = req.MinDepth
}
if req.MaxDepth != nil {
script.MaxDepth = req.MaxDepth
}
if req.Placement != "" {
script.Placement = req.Placement
}
if req.AffectMinDepth != nil {
script.AffectMinDepth = req.AffectMinDepth
}
if req.AffectMaxDepth != nil {
script.AffectMaxDepth = req.AffectMaxDepth
}
if req.LinkedChars != nil {
script.LinkedChars = req.LinkedChars
}
if req.ScriptData != nil {
data, err := datatypes.NewJSONType(req.ScriptData).MarshalJSON()
if err != nil {
return errors.New("序列化脚本数据失败: " + err.Error())
}
script.ScriptData = data
}
return global.GVA_DB.Save(&script).Error
}
// DeleteRegexScript 删除正则脚本
func (rs *RegexScriptService) DeleteRegexScript(userID, scriptID uint) error {
result := global.GVA_DB.Where("id = ? AND user_id = ?", scriptID, userID).Delete(&app.AIRegexScript{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("脚本不存在")
}
return nil
}
// GetRegexScript 获取正则脚本详情
func (rs *RegexScriptService) GetRegexScript(userID, scriptID uint) (*app.AIRegexScript, error) {
var script app.AIRegexScript
if err := global.GVA_DB.Where("id = ? AND user_id = ?", scriptID, userID).First(&script).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("脚本不存在")
}
return nil, err
}
return &script, nil
}
// GetRegexScriptList 获取正则脚本列表
func (rs *RegexScriptService) GetRegexScriptList(userID uint, req *request.RegexScriptListRequest) ([]app.AIRegexScript, int64, error) {
db := global.GVA_DB.Where("user_id = ?", userID)
// 条件筛选
if req.ScriptName != "" {
db = db.Where("script_name ILIKE ?", "%"+req.ScriptName+"%")
}
if req.IsGlobal != nil {
db = db.Where("is_global = ?", *req.IsGlobal)
}
if req.Enabled != nil {
db = db.Where("enabled = ?", *req.Enabled)
}
if req.CharacterID != nil {
db = db.Where("? = ANY(linked_chars)", fmt.Sprintf("%d", *req.CharacterID))
}
// 查询总数
var total int64
if err := db.Model(&app.AIRegexScript{}).Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
var scripts []app.AIRegexScript
offset := (req.Page - 1) * req.PageSize
if err := db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&scripts).Error; err != nil {
return nil, 0, err
}
return scripts, total, nil
}
// LinkCharactersToRegex 关联角色到正则脚本
func (rs *RegexScriptService) LinkCharactersToRegex(userID, scriptID uint, characterIDs []uint) error {
// 查询脚本
var script app.AIRegexScript
if err := global.GVA_DB.Where("id = ? AND user_id = ?", scriptID, userID).First(&script).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("脚本不存在")
}
return err
}
// 转换为字符串数组
linkedChars := make([]string, len(characterIDs))
for i, id := range characterIDs {
linkedChars[i] = fmt.Sprintf("%d", id)
}
// 更新 LinkedChars
script.LinkedChars = linkedChars
return global.GVA_DB.Save(&script).Error
}
// GetCharacterRegexScripts 获取角色关联的正则脚本列表
func (rs *RegexScriptService) GetCharacterRegexScripts(userID, characterID uint) ([]app.AIRegexScript, error) {
var scripts []app.AIRegexScript
err := global.GVA_DB.
Where("user_id = ? AND (is_global = true OR ? = ANY(linked_chars))", userID, fmt.Sprintf("%d", characterID)).
Where("enabled = true").
Order("created_at ASC").
Find(&scripts).Error
return scripts, err
}
// DuplicateRegexScript 复制正则脚本
func (rs *RegexScriptService) DuplicateRegexScript(userID, scriptID uint) (*app.AIRegexScript, error) {
// 获取原脚本
original, err := rs.GetRegexScript(userID, scriptID)
if err != nil {
return nil, err
}
// 创建副本
newScript := &app.AIRegexScript{
UserID: userID,
ScriptName: original.ScriptName + " (副本)",
Description: original.Description,
FindRegex: original.FindRegex,
ReplaceString: original.ReplaceString,
Enabled: original.Enabled,
IsGlobal: false, // 副本默认非全局
TrimStrings: original.TrimStrings,
OnlyFormat: original.OnlyFormat,
RunOnEdit: original.RunOnEdit,
SubstituteRegex: original.SubstituteRegex,
MinDepth: original.MinDepth,
MaxDepth: original.MaxDepth,
Placement: original.Placement,
AffectMinDepth: original.AffectMinDepth,
AffectMaxDepth: original.AffectMaxDepth,
LinkedChars: original.LinkedChars,
ScriptData: original.ScriptData,
}
if err := global.GVA_DB.Create(newScript).Error; err != nil {
return nil, err
}
return newScript, nil
}
// TestRegexScript 测试正则脚本
func (rs *RegexScriptService) TestRegexScript(req *request.TestRegexScriptRequest) (*response.TestRegexScriptResponse, error) {
// 编译正则表达式
re, err := regexp.Compile(req.FindRegex)
if err != nil {
return &response.TestRegexScriptResponse{
Success: false,
Input: req.TestInput,
Output: req.TestInput,
Error: "无效的正则表达式: " + err.Error(),
}, nil
}
// 应用替换
output := req.TestInput
if req.TrimStrings {
output = strings.TrimSpace(output)
}
// 查找所有匹配
matches := re.FindAllString(output, -1)
// 执行替换
if req.SubstituteRegex {
// 使用正则替换
output = re.ReplaceAllString(output, req.ReplaceString)
} else {
// 简单字符串替换
output = re.ReplaceAllLiteralString(output, req.ReplaceString)
}
return &response.TestRegexScriptResponse{
Success: true,
Input: req.TestInput,
Output: output,
MatchedCount: len(matches),
Matches: matches,
}, nil
}
// ApplyRegexScripts 应用正则脚本
func (rs *RegexScriptService) ApplyRegexScripts(userID uint, req *request.ApplyRegexScriptsRequest) (*response.ApplyRegexScriptsResponse, error) {
var scripts []app.AIRegexScript
// 收集要应用的脚本
db := global.GVA_DB.Where("user_id = ? AND enabled = true", userID)
if len(req.RegexIDs) > 0 {
// 应用指定的脚本
db = db.Where("id IN ?", req.RegexIDs)
} else {
// 根据条件自动选择脚本
conditions := []string{}
if req.UseGlobal {
conditions = append(conditions, "is_global = true")
}
if req.CharacterID != nil {
conditions = append(conditions, fmt.Sprintf("'%d' = ANY(linked_chars)", *req.CharacterID))
}
if len(conditions) > 0 {
db = db.Where(strings.Join(conditions, " OR "))
}
// 筛选位置
if req.Placement != "" {
db = db.Where("(placement = '' OR placement = ?)", req.Placement)
}
}
if err := db.Order("created_at ASC").Find(&scripts).Error; err != nil {
return nil, err
}
// 应用脚本
processedText := req.Text
appliedScripts := []uint{}
for _, script := range scripts {
// 检查深度限制
if req.MinDepth != nil && script.MinDepth != nil && *req.MinDepth < *script.MinDepth {
continue
}
if req.MaxDepth != nil && script.MaxDepth != nil && *req.MaxDepth > *script.MaxDepth {
continue
}
// 编译正则表达式
re, err := regexp.Compile(script.FindRegex)
if err != nil {
global.GVA_LOG.Warn("正则表达式编译失败",
zap.Uint("scriptID", script.ID),
zap.String("regex", script.FindRegex),
zap.Error(err))
continue
}
// 应用替换
beforeText := processedText
if script.TrimStrings {
processedText = strings.TrimSpace(processedText)
}
if script.SubstituteRegex {
processedText = re.ReplaceAllString(processedText, script.ReplaceString)
} else {
processedText = re.ReplaceAllLiteralString(processedText, script.ReplaceString)
}
// 记录成功应用的脚本
if beforeText != processedText {
appliedScripts = append(appliedScripts, script.ID)
// 更新使用统计
now := time.Now().Unix()
global.GVA_DB.Model(&script).Updates(map[string]interface{}{
"usage_count": gorm.Expr("usage_count + 1"),
"last_used_at": now,
})
}
}
return &response.ApplyRegexScriptsResponse{
OriginalText: req.Text,
ProcessedText: processedText,
AppliedCount: len(appliedScripts),
AppliedScripts: appliedScripts,
}, nil
}
// ImportRegexScripts 导入正则脚本
func (rs *RegexScriptService) ImportRegexScripts(userID uint, scripts []app.AIRegexScript, overwriteMode string) (int, error) {
imported := 0
for _, script := range scripts {
script.UserID = userID
script.ID = 0 // 重置 ID
// 检查是否存在同名脚本
var existing app.AIRegexScript
err := global.GVA_DB.Where("user_id = ? AND script_name = ?", userID, script.ScriptName).First(&existing).Error
if err == nil {
// 脚本已存在
switch overwriteMode {
case "skip":
continue
case "overwrite":
script.ID = existing.ID
if err := global.GVA_DB.Save(&script).Error; err != nil {
global.GVA_LOG.Warn("覆盖脚本失败", zap.Error(err))
continue
}
case "merge":
script.ScriptName = script.ScriptName + " (导入)"
if err := global.GVA_DB.Create(&script).Error; err != nil {
global.GVA_LOG.Warn("合并导入脚本失败", zap.Error(err))
continue
}
default:
continue
}
} else {
// 新脚本
if err := global.GVA_DB.Create(&script).Error; err != nil {
global.GVA_LOG.Warn("创建脚本失败", zap.Error(err))
continue
}
}
imported++
}
return imported, nil
}
// ExportRegexScripts 导出正则脚本
func (rs *RegexScriptService) ExportRegexScripts(userID uint, scriptIDs []uint) (*response.RegexScriptExportData, error) {
var scripts []app.AIRegexScript
db := global.GVA_DB.Where("user_id = ?", userID)
if len(scriptIDs) > 0 {
db = db.Where("id IN ?", scriptIDs)
}
if err := db.Find(&scripts).Error; err != nil {
return nil, err
}
// 转换为响应格式
responses := make([]response.RegexScriptResponse, len(scripts))
for i, script := range scripts {
responses[i] = response.ToRegexScriptResponse(&script)
}
return &response.RegexScriptExportData{
Version: "1.0",
Scripts: responses,
ExportedAt: time.Now().Unix(),
}, nil
}