✨ 新增正则和扩展模块
This commit is contained in:
@@ -6,6 +6,8 @@ type ApiGroup struct {
|
||||
AuthApi
|
||||
CharacterApi
|
||||
WorldInfoApi
|
||||
ExtensionApi
|
||||
RegexScriptApi
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
565
server/api/v1/app/extension.go
Normal file
565
server/api/v1/app/extension.go
Normal file
@@ -0,0 +1,565 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"git.echol.cn/loser/st/server/middleware"
|
||||
"git.echol.cn/loser/st/server/model/app/request"
|
||||
"git.echol.cn/loser/st/server/model/app/response"
|
||||
sysResponse "git.echol.cn/loser/st/server/model/common/response"
|
||||
"git.echol.cn/loser/st/server/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ExtensionApi struct{}
|
||||
|
||||
var extensionService = service.ServiceGroupApp.AppServiceGroup.ExtensionService
|
||||
|
||||
// CreateExtension 创建/安装扩展
|
||||
// @Summary 创建/安装扩展
|
||||
// @Description 创建一个新的扩展或安装扩展
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body request.CreateExtensionRequest true "扩展信息"
|
||||
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
||||
// @Router /app/extension [post]
|
||||
func (a *ExtensionApi) CreateExtension(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
var req request.CreateExtensionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
extension, err := extensionService.CreateExtension(userID, &req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("创建扩展失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("创建失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
||||
}
|
||||
|
||||
// UpdateExtension 更新扩展
|
||||
// @Summary 更新扩展
|
||||
// @Description 更新扩展信息
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "扩展ID"
|
||||
// @Param data body request.UpdateExtensionRequest true "扩展信息"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /app/extension/:id [put]
|
||||
func (a *ExtensionApi) UpdateExtension(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||
return
|
||||
}
|
||||
extensionID := uint(id)
|
||||
|
||||
var req request.UpdateExtensionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := extensionService.UpdateExtension(userID, extensionID, &req); err != nil {
|
||||
global.GVA_LOG.Error("更新扩展失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithMessage("更新成功", c)
|
||||
}
|
||||
|
||||
// DeleteExtension 删除/卸载扩展
|
||||
// @Summary 删除/卸载扩展
|
||||
// @Description 删除扩展
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "扩展ID"
|
||||
// @Param deleteFiles query bool false "是否删除文件"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /app/extension/:id [delete]
|
||||
func (a *ExtensionApi) DeleteExtension(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||
return
|
||||
}
|
||||
extensionID := uint(id)
|
||||
deleteFiles := c.Query("deleteFiles") == "true"
|
||||
|
||||
if err := extensionService.DeleteExtension(userID, extensionID, deleteFiles); err != nil {
|
||||
global.GVA_LOG.Error("删除扩展失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("删除失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
// GetExtension 获取扩展详情
|
||||
// @Summary 获取扩展详情
|
||||
// @Description 获取扩展详细信息
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "扩展ID"
|
||||
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
||||
// @Router /app/extension/:id [get]
|
||||
func (a *ExtensionApi) GetExtension(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||
return
|
||||
}
|
||||
extensionID := uint(id)
|
||||
|
||||
extension, err := extensionService.GetExtension(userID, extensionID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取扩展失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
||||
}
|
||||
|
||||
// GetExtensionList 获取扩展列表
|
||||
// @Summary 获取扩展列表
|
||||
// @Description 获取扩展列表
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data query request.ExtensionListRequest true "查询参数"
|
||||
// @Success 200 {object} response.Response{data=response.ExtensionListResponse}
|
||||
// @Router /app/extension/list [get]
|
||||
func (a *ExtensionApi) GetExtensionList(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
var req request.ExtensionListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 默认分页
|
||||
if req.Page < 1 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize < 1 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
result, err := extensionService.GetExtensionList(userID, &req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取扩展列表失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(result, c)
|
||||
}
|
||||
|
||||
// ToggleExtension 启用/禁用扩展
|
||||
// @Summary 启用/禁用扩展
|
||||
// @Description 切换扩展的启用状态
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "扩展ID"
|
||||
// @Param data body request.ToggleExtensionRequest true "启用状态"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /app/extension/:id/toggle [post]
|
||||
func (a *ExtensionApi) ToggleExtension(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||
return
|
||||
}
|
||||
extensionID := uint(id)
|
||||
|
||||
var req request.ToggleExtensionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage("请求参数错误", c)
|
||||
return
|
||||
}
|
||||
|
||||
if req.IsEnabled == nil {
|
||||
sysResponse.FailWithMessage("isEnabled 参数不能为空", c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := extensionService.ToggleExtension(userID, extensionID, *req.IsEnabled); err != nil {
|
||||
global.GVA_LOG.Error("切换扩展状态失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("操作失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithMessage("操作成功", c)
|
||||
}
|
||||
|
||||
// UpdateExtensionSettings 更新扩展配置
|
||||
// @Summary 更新扩展配置
|
||||
// @Description 更新扩展的用户配置
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "扩展ID"
|
||||
// @Param data body request.UpdateExtensionSettingsRequest true "配置信息"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /app/extension/:id/settings [put]
|
||||
func (a *ExtensionApi) UpdateExtensionSettings(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||
return
|
||||
}
|
||||
extensionID := uint(id)
|
||||
|
||||
var req request.UpdateExtensionSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := extensionService.UpdateExtensionSettings(userID, extensionID, req.Settings); err != nil {
|
||||
global.GVA_LOG.Error("更新扩展配置失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithMessage("更新成功", c)
|
||||
}
|
||||
|
||||
// GetExtensionSettings 获取扩展配置
|
||||
// @Summary 获取扩展配置
|
||||
// @Description 获取扩展的用户配置
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "扩展ID"
|
||||
// @Success 200 {object} response.Response{data=map[string]interface{}}
|
||||
// @Router /app/extension/:id/settings [get]
|
||||
func (a *ExtensionApi) GetExtensionSettings(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||
return
|
||||
}
|
||||
extensionID := uint(id)
|
||||
|
||||
settings, err := extensionService.GetExtensionSettings(userID, extensionID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取扩展配置失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(settings, c)
|
||||
}
|
||||
|
||||
// GetExtensionManifest 获取扩展 manifest
|
||||
// @Summary 获取扩展 manifest
|
||||
// @Description 获取扩展的 manifest.json
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "扩展ID"
|
||||
// @Success 200 {object} response.Response{data=response.ExtensionManifestResponse}
|
||||
// @Router /app/extension/:id/manifest [get]
|
||||
func (a *ExtensionApi) GetExtensionManifest(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||
return
|
||||
}
|
||||
extensionID := uint(id)
|
||||
|
||||
manifest, err := extensionService.GetExtensionManifest(userID, extensionID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取扩展 manifest 失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(manifest, c)
|
||||
}
|
||||
|
||||
// ImportExtension 导入扩展
|
||||
// @Summary 导入扩展
|
||||
// @Description 从文件导入扩展
|
||||
// @Tags 扩展管理
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param file formData file true "扩展文件(manifest.json)"
|
||||
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
||||
// @Router /app/extension/import [post]
|
||||
func (a *ExtensionApi) ImportExtension(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 获取文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("请上传扩展文件", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 文件大小限制(5MB)
|
||||
if file.Size > 5<<20 {
|
||||
sysResponse.FailWithMessage("文件大小不能超过 5MB", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("打开文件失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("文件读取失败", c)
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
fileData, err := io.ReadAll(src)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("读取文件内容失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("文件读取失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 导入扩展
|
||||
extension, err := extensionService.ImportExtension(userID, fileData)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("导入扩展失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("导入失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
||||
}
|
||||
|
||||
// ExportExtension 导出扩展
|
||||
// @Summary 导出扩展
|
||||
// @Description 导出扩展为 manifest.json 文件
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce application/json
|
||||
// @Param id path int true "扩展ID"
|
||||
// @Success 200 {object} response.ExtensionManifestResponse
|
||||
// @Router /app/extension/:id/export [get]
|
||||
func (a *ExtensionApi) ExportExtension(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||
return
|
||||
}
|
||||
extensionID := uint(id)
|
||||
|
||||
exportData, err := extensionService.ExportExtension(userID, extensionID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("导出扩展失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("导出失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
filename := fmt.Sprintf("extension_%d_manifest.json", extensionID)
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
|
||||
// 直接返回 JSON 数据
|
||||
c.Data(http.StatusOK, "application/json", exportData)
|
||||
}
|
||||
|
||||
// UpdateExtensionStats 更新扩展统计
|
||||
// @Summary 更新扩展统计
|
||||
// @Description 更新扩展的使用统计
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body request.ExtensionStatsRequest true "统计信息"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /app/extension/stats [post]
|
||||
func (a *ExtensionApi) UpdateExtensionStats(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
var req request.ExtensionStatsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := extensionService.UpdateExtensionStats(userID, req.ExtensionID, req.Action, req.Value); err != nil {
|
||||
global.GVA_LOG.Error("更新扩展统计失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithMessage("更新成功", c)
|
||||
}
|
||||
|
||||
// GetEnabledExtensions 获取启用的扩展列表
|
||||
// @Summary 获取启用的扩展列表
|
||||
// @Description 获取用户启用的所有扩展(用于前端加载)
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response{data=[]response.ExtensionResponse}
|
||||
// @Router /app/extension/enabled [get]
|
||||
func (a *ExtensionApi) GetEnabledExtensions(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
extensions, err := extensionService.GetEnabledExtensions(userID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取启用扩展列表失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(extensions, c)
|
||||
}
|
||||
|
||||
// InstallExtensionFromURL 智能安装扩展(自动识别 Git URL 或 Manifest URL)
|
||||
// @Summary 智能安装扩展
|
||||
// @Description 自动识别 Git 仓库 URL 或 Manifest.json URL 并安装扩展(兼容 SillyTavern)
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body request.InstallExtensionFromURLRequest true "安装 URL 信息"
|
||||
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
||||
// @Router /app/extension/install/url [post]
|
||||
func (a *ExtensionApi) InstallExtensionFromURL(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
var req request.InstallExtensionFromURLRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage("请求参数错误: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认分支
|
||||
if req.Branch == "" {
|
||||
req.Branch = "main"
|
||||
}
|
||||
|
||||
extension, err := extensionService.InstallExtensionFromURL(userID, req.URL, req.Branch)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("从 URL 安装扩展失败", zap.Error(err), zap.String("url", req.URL))
|
||||
sysResponse.FailWithMessage("安装失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
||||
}
|
||||
|
||||
// UpgradeExtension 升级扩展版本
|
||||
// @Summary 升级扩展版本
|
||||
// @Description 根据扩展的安装来源自动选择更新方式(Git pull 或重新下载)
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "扩展ID"
|
||||
// @Param data body request.UpdateExtensionRequest false "更新选项"
|
||||
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
||||
// @Router /app/extension/:id/update [post]
|
||||
func (a *ExtensionApi) UpgradeExtension(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||
return
|
||||
}
|
||||
extensionID := uint(id)
|
||||
|
||||
var req request.UpdateExtensionRequest
|
||||
// 允许不传 body(使用默认值)
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
|
||||
extension, err := extensionService.UpgradeExtension(userID, extensionID, req.Force)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("升级扩展失败", zap.Error(err), zap.Uint("extensionID", extensionID))
|
||||
sysResponse.FailWithMessage("升级失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
||||
}
|
||||
|
||||
// InstallExtensionFromGit 从 Git URL 安装扩展
|
||||
// @Summary 从 Git URL 安装扩展
|
||||
// @Description 从 Git 仓库 URL 克隆并安装扩展
|
||||
// @Tags 扩展管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body request.InstallExtensionFromGitRequest true "Git URL 信息"
|
||||
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
||||
// @Router /app/extension/install/git [post]
|
||||
func (a *ExtensionApi) InstallExtensionFromGit(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
var req request.InstallExtensionFromGitRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认分支
|
||||
if req.Branch == "" {
|
||||
req.Branch = "main"
|
||||
}
|
||||
|
||||
extension, err := extensionService.InstallExtensionFromGit(userID, req.GitUrl, req.Branch)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("从 Git 安装扩展失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("安装失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
||||
}
|
||||
479
server/api/v1/app/regex_script.go
Normal file
479
server/api/v1/app/regex_script.go
Normal file
@@ -0,0 +1,479 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"git.echol.cn/loser/st/server/middleware"
|
||||
"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"
|
||||
sysResponse "git.echol.cn/loser/st/server/model/common/response"
|
||||
"git.echol.cn/loser/st/server/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type RegexScriptApi struct{}
|
||||
|
||||
var regexScriptService = service.ServiceGroupApp.AppServiceGroup.RegexScriptService
|
||||
|
||||
// CreateRegexScript 创建正则脚本
|
||||
// @Summary 创建正则脚本
|
||||
// @Description 创建一个新的正则表达式脚本
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body request.CreateRegexScriptRequest true "脚本信息"
|
||||
// @Success 200 {object} response.Response{data=app.AIRegexScript}
|
||||
// @Router /app/regex [post]
|
||||
func (a *RegexScriptApi) CreateRegexScript(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
var req request.CreateRegexScriptRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
script, err := regexScriptService.CreateRegexScript(userID, &req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("创建正则脚本失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("创建失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(response.ToRegexScriptResponse(script), c)
|
||||
}
|
||||
|
||||
// UpdateRegexScript 更新正则脚本
|
||||
// @Summary 更新正则脚本
|
||||
// @Description 更新正则脚本信息
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "脚本ID"
|
||||
// @Param data body request.UpdateRegexScriptRequest true "脚本信息"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /app/regex/:id [put]
|
||||
func (a *RegexScriptApi) UpdateRegexScript(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的脚本ID", c)
|
||||
return
|
||||
}
|
||||
scriptID := uint(id)
|
||||
|
||||
var req request.UpdateRegexScriptRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := regexScriptService.UpdateRegexScript(userID, scriptID, &req); err != nil {
|
||||
global.GVA_LOG.Error("更新正则脚本失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithMessage("更新成功", c)
|
||||
}
|
||||
|
||||
// DeleteRegexScript 删除正则脚本
|
||||
// @Summary 删除正则脚本
|
||||
// @Description 删除正则脚本
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "脚本ID"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /app/regex/:id [delete]
|
||||
func (a *RegexScriptApi) DeleteRegexScript(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的脚本ID", c)
|
||||
return
|
||||
}
|
||||
scriptID := uint(id)
|
||||
|
||||
if err := regexScriptService.DeleteRegexScript(userID, scriptID); err != nil {
|
||||
global.GVA_LOG.Error("删除正则脚本失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("删除失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
// GetRegexScript 获取正则脚本详情
|
||||
// @Summary 获取正则脚本详情
|
||||
// @Description 获取正则脚本详细信息
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "脚本ID"
|
||||
// @Success 200 {object} response.Response{data=response.RegexScriptResponse}
|
||||
// @Router /app/regex/:id [get]
|
||||
func (a *RegexScriptApi) GetRegexScript(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的脚本ID", c)
|
||||
return
|
||||
}
|
||||
scriptID := uint(id)
|
||||
|
||||
script, err := regexScriptService.GetRegexScript(userID, scriptID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取正则脚本失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(response.ToRegexScriptResponse(script), c)
|
||||
}
|
||||
|
||||
// GetRegexScriptList 获取正则脚本列表
|
||||
// @Summary 获取正则脚本列表
|
||||
// @Description 获取正则脚本列表
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param scriptName query string false "脚本名称"
|
||||
// @Param isGlobal query boolean false "是否全局"
|
||||
// @Param enabled query boolean false "是否启用"
|
||||
// @Param characterId query int false "关联角色ID"
|
||||
// @Param page query int false "页码"
|
||||
// @Param pageSize query int false "每页大小"
|
||||
// @Success 200 {object} response.Response{data=response.RegexScriptListResponse}
|
||||
// @Router /app/regex [get]
|
||||
func (a *RegexScriptApi) GetRegexScriptList(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
var req request.RegexScriptListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
|
||||
scripts, total, err := regexScriptService.GetRegexScriptList(userID, &req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取正则脚本列表失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
responses := make([]response.RegexScriptResponse, len(scripts))
|
||||
for i, script := range scripts {
|
||||
responses[i] = response.ToRegexScriptResponse(&script)
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(response.RegexScriptListResponse{
|
||||
List: responses,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}, c)
|
||||
}
|
||||
|
||||
// LinkCharactersToRegex 关联角色到正则脚本
|
||||
// @Summary 关联角色到正则脚本
|
||||
// @Description 将角色关联到指定的正则脚本
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "脚本ID"
|
||||
// @Param data body request.LinkCharacterToRegexRequest true "角色ID列表"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /app/regex/:id/link [post]
|
||||
func (a *RegexScriptApi) LinkCharactersToRegex(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的脚本ID", c)
|
||||
return
|
||||
}
|
||||
scriptID := uint(id)
|
||||
|
||||
var req request.LinkCharacterToRegexRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
if err := regexScriptService.LinkCharactersToRegex(userID, scriptID, req.CharacterIDs); err != nil {
|
||||
global.GVA_LOG.Error("关联角色失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("关联失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithMessage("关联成功", c)
|
||||
}
|
||||
|
||||
// GetCharacterRegexScripts 获取角色关联的正则脚本
|
||||
// @Summary 获取角色关联的正则脚本
|
||||
// @Description 获取特定角色关联的所有正则脚本
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param characterId path int true "角色ID"
|
||||
// @Success 200 {object} response.Response{data=[]response.RegexScriptResponse}
|
||||
// @Router /app/regex/character/:characterId [get]
|
||||
func (a *RegexScriptApi) GetCharacterRegexScripts(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("characterId")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的角色ID", c)
|
||||
return
|
||||
}
|
||||
characterID := uint(id)
|
||||
|
||||
scripts, err := regexScriptService.GetCharacterRegexScripts(userID, characterID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取角色正则脚本失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为响应格式
|
||||
responses := make([]response.RegexScriptResponse, len(scripts))
|
||||
for i, script := range scripts {
|
||||
responses[i] = response.ToRegexScriptResponse(&script)
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(responses, c)
|
||||
}
|
||||
|
||||
// DuplicateRegexScript 复制正则脚本
|
||||
// @Summary 复制正则脚本
|
||||
// @Description 创建正则脚本的副本
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "脚本ID"
|
||||
// @Success 200 {object} response.Response{data=app.AIRegexScript}
|
||||
// @Router /app/regex/:id/duplicate [post]
|
||||
func (a *RegexScriptApi) DuplicateRegexScript(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的脚本ID", c)
|
||||
return
|
||||
}
|
||||
scriptID := uint(id)
|
||||
|
||||
newScript, err := regexScriptService.DuplicateRegexScript(userID, scriptID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("复制正则脚本失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("复制失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(response.ToRegexScriptResponse(newScript), c)
|
||||
}
|
||||
|
||||
// TestRegexScript 测试正则脚本
|
||||
// @Summary 测试正则脚本
|
||||
// @Description 测试正则表达式的匹配和替换效果
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body request.TestRegexScriptRequest true "测试数据"
|
||||
// @Success 200 {object} response.Response{data=response.TestRegexScriptResponse}
|
||||
// @Router /app/regex/test [post]
|
||||
func (a *RegexScriptApi) TestRegexScript(c *gin.Context) {
|
||||
var req request.TestRegexScriptRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := regexScriptService.TestRegexScript(&req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("测试正则脚本失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("测试失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(result, c)
|
||||
}
|
||||
|
||||
// ApplyRegexScripts 应用正则脚本
|
||||
// @Summary 应用正则脚本
|
||||
// @Description 对文本应用正则脚本进行处理
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body request.ApplyRegexScriptsRequest true "应用参数"
|
||||
// @Success 200 {object} response.Response{data=response.ApplyRegexScriptsResponse}
|
||||
// @Router /app/regex/apply [post]
|
||||
func (a *RegexScriptApi) ApplyRegexScripts(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
var req request.ApplyRegexScriptsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
sysResponse.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := regexScriptService.ApplyRegexScripts(userID, &req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("应用正则脚本失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("应用失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithData(result, c)
|
||||
}
|
||||
|
||||
// ImportRegexScripts 导入正则脚本
|
||||
// @Summary 导入正则脚本
|
||||
// @Description 从 JSON 文件导入正则脚本
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param file formData file true "JSON 文件"
|
||||
// @Param overwriteMode formData string false "覆盖模式: skip, overwrite, merge"
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /app/regex/import [post]
|
||||
func (a *RegexScriptApi) ImportRegexScripts(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 获取文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("获取文件失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取覆盖模式
|
||||
overwriteMode := c.DefaultPostForm("overwriteMode", "skip")
|
||||
|
||||
// 读取文件内容
|
||||
openedFile, err := file.Open()
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("打开文件失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
defer openedFile.Close()
|
||||
|
||||
content, err := io.ReadAll(openedFile)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("读取文件失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 JSON
|
||||
var exportData response.RegexScriptExportData
|
||||
if err := json.Unmarshal(content, &exportData); err != nil {
|
||||
sysResponse.FailWithMessage("解析JSON失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为 AIRegexScript
|
||||
scripts := make([]app.AIRegexScript, len(exportData.Scripts))
|
||||
for i, resp := range exportData.Scripts {
|
||||
scripts[i] = app.AIRegexScript{
|
||||
ScriptName: resp.ScriptName,
|
||||
Description: resp.Description,
|
||||
FindRegex: resp.FindRegex,
|
||||
ReplaceString: resp.ReplaceString,
|
||||
Enabled: resp.Enabled,
|
||||
IsGlobal: resp.IsGlobal,
|
||||
TrimStrings: resp.TrimStrings,
|
||||
OnlyFormat: resp.OnlyFormat,
|
||||
RunOnEdit: resp.RunOnEdit,
|
||||
SubstituteRegex: resp.SubstituteRegex,
|
||||
MinDepth: resp.MinDepth,
|
||||
MaxDepth: resp.MaxDepth,
|
||||
Placement: resp.Placement,
|
||||
AffectMinDepth: resp.AffectMinDepth,
|
||||
AffectMaxDepth: resp.AffectMaxDepth,
|
||||
LinkedChars: resp.LinkedChars,
|
||||
}
|
||||
}
|
||||
|
||||
// 导入
|
||||
imported, err := regexScriptService.ImportRegexScripts(userID, scripts, overwriteMode)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("导入正则脚本失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("导入失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
sysResponse.OkWithMessage(fmt.Sprintf("成功导入 %d 个脚本", imported), c)
|
||||
}
|
||||
|
||||
// ExportRegexScripts 导出正则脚本
|
||||
// @Summary 导出正则脚本
|
||||
// @Description 导出正则脚本为 JSON 文件
|
||||
// @Tags 正则脚本管理
|
||||
// @Accept json
|
||||
// @Produce application/json
|
||||
// @Param scriptIds query string false "脚本ID列表(逗号分隔)"
|
||||
// @Success 200 {object} response.RegexScriptExportData
|
||||
// @Router /app/regex/export [get]
|
||||
func (a *RegexScriptApi) ExportRegexScripts(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
|
||||
// 获取脚本ID列表
|
||||
scriptIDsStr := c.Query("scriptIds")
|
||||
var scriptIDs []uint
|
||||
if scriptIDsStr != "" {
|
||||
var ids []uint
|
||||
for _, idStr := range strings.Split(scriptIDsStr, ",") {
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err == nil {
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
}
|
||||
scriptIDs = ids
|
||||
}
|
||||
|
||||
exportData, err := regexScriptService.ExportRegexScripts(userID, scriptIDs)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("导出正则脚本失败", zap.Error(err))
|
||||
sysResponse.FailWithMessage("导出失败: "+err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置下载响应头
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.Header("Content-Disposition", "attachment; filename=regex_scripts_export.json")
|
||||
|
||||
c.JSON(http.StatusOK, exportData)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"git.echol.cn/loser/st/server/middleware"
|
||||
@@ -61,7 +62,15 @@ func (a *WorldInfoApi) CreateWorldBook(c *gin.Context) {
|
||||
// @Router /app/worldbook/:id [put]
|
||||
func (a *WorldInfoApi) UpdateWorldBook(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
bookID := c.GetUint("id")
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的世界书ID", c)
|
||||
return
|
||||
}
|
||||
bookID := uint(id)
|
||||
|
||||
var req request.UpdateWorldBookRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -89,7 +98,15 @@ func (a *WorldInfoApi) UpdateWorldBook(c *gin.Context) {
|
||||
// @Router /app/worldbook/:id [delete]
|
||||
func (a *WorldInfoApi) DeleteWorldBook(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
bookID := c.GetUint("id")
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的世界书ID", c)
|
||||
return
|
||||
}
|
||||
bookID := uint(id)
|
||||
|
||||
if err := worldInfoService.DeleteWorldBook(userID, bookID); err != nil {
|
||||
global.GVA_LOG.Error("删除世界书失败", zap.Error(err))
|
||||
@@ -111,7 +128,15 @@ func (a *WorldInfoApi) DeleteWorldBook(c *gin.Context) {
|
||||
// @Router /app/worldbook/:id [get]
|
||||
func (a *WorldInfoApi) GetWorldBook(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
bookID := c.GetUint("id")
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的世界书ID", c)
|
||||
return
|
||||
}
|
||||
bookID := uint(id)
|
||||
|
||||
book, err := worldInfoService.GetWorldBook(userID, bookID)
|
||||
if err != nil {
|
||||
@@ -334,7 +359,15 @@ func (a *WorldInfoApi) ImportWorldBook(c *gin.Context) {
|
||||
// @Router /app/worldbook/:id/export [get]
|
||||
func (a *WorldInfoApi) ExportWorldBook(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
bookID := c.GetUint("id")
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的世界书ID", c)
|
||||
return
|
||||
}
|
||||
bookID := uint(id)
|
||||
|
||||
exportData, err := worldInfoService.ExportWorldBook(userID, bookID)
|
||||
if err != nil {
|
||||
@@ -399,10 +432,18 @@ func (a *WorldInfoApi) MatchWorldInfo(c *gin.Context) {
|
||||
// @Router /app/worldbook/character/:characterId [get]
|
||||
func (a *WorldInfoApi) GetCharacterWorldBooks(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
characterID := c.GetUint("characterId")
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("characterId")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的角色ID", c)
|
||||
return
|
||||
}
|
||||
characterID := uint(id)
|
||||
|
||||
var books []app.AIWorldInfo
|
||||
err := global.GVA_DB.
|
||||
err = global.GVA_DB.
|
||||
Where("user_id = ? AND (is_global = true OR ? = ANY(linked_chars))", userID, fmt.Sprintf("%d", characterID)).
|
||||
Find(&books).Error
|
||||
|
||||
@@ -431,7 +472,15 @@ func (a *WorldInfoApi) GetCharacterWorldBooks(c *gin.Context) {
|
||||
// @Router /app/worldbook/:id/duplicate [post]
|
||||
func (a *WorldInfoApi) DuplicateWorldBook(c *gin.Context) {
|
||||
userID := middleware.GetAppUserID(c)
|
||||
bookID := c.GetUint("id")
|
||||
|
||||
// 从路径参数获取 ID
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
sysResponse.FailWithMessage("无效的世界书ID", c)
|
||||
return
|
||||
}
|
||||
bookID := uint(id)
|
||||
|
||||
// 获取原世界书
|
||||
book, err := worldInfoService.GetWorldBook(userID, bookID)
|
||||
|
||||
@@ -182,10 +182,10 @@ pgsql:
|
||||
prefix: ""
|
||||
port: "5432"
|
||||
config: sslmode=disable TimeZone=Asia/Shanghai
|
||||
db-name: st_dev
|
||||
username: loser
|
||||
password: loser765911.
|
||||
path: pg.echol.top
|
||||
db-name: st
|
||||
username: postgres
|
||||
password: e5zse3Adrja7PNfA
|
||||
path: 219.152.55.29
|
||||
engine: ""
|
||||
log-mode: error
|
||||
max-idle-conns: 10
|
||||
|
||||
9
server/initialize/fix_world_info_table.sql
Normal file
9
server/initialize/fix_world_info_table.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- 修复 ai_world_info 表结构
|
||||
-- 如果表存在旧的 name 字段,需要删除并重新创建
|
||||
|
||||
-- 删除旧表(如果存在)
|
||||
DROP TABLE IF EXISTS ai_character_world_info CASCADE;
|
||||
DROP TABLE IF EXISTS ai_world_info CASCADE;
|
||||
|
||||
-- 表将由 Gorm AutoMigrate 自动创建
|
||||
-- 重启服务器即可
|
||||
@@ -94,6 +94,9 @@ func RegisterTables() {
|
||||
app.AIPreset{},
|
||||
app.AIWorldInfo{},
|
||||
app.AIUsageStat{},
|
||||
app.AIExtension{},
|
||||
app.AIRegexScript{},
|
||||
app.AICharacterRegexScript{},
|
||||
)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("register table failed", zap.Error(err))
|
||||
|
||||
@@ -143,10 +143,12 @@ func Routers() *gin.Engine {
|
||||
|
||||
// 前台应用路由(新增)
|
||||
{
|
||||
appGroup := PublicGroup.Group("app") // 统一使用 /app 前缀
|
||||
appRouter.InitAuthRouter(appGroup) // 认证路由:/app/auth/* 和 /app/user/*
|
||||
appRouter.InitCharacterRouter(appGroup) // 角色卡路由:/app/character/*
|
||||
appRouter.InitWorldInfoRouter(appGroup) // 世界书路由:/app/worldbook/*
|
||||
appGroup := PublicGroup.Group("app") // 统一使用 /app 前缀
|
||||
appRouter.InitAuthRouter(appGroup) // 认证路由:/app/auth/* 和 /app/user/*
|
||||
appRouter.InitCharacterRouter(appGroup) // 角色卡路由:/app/character/*
|
||||
appRouter.InitWorldInfoRouter(appGroup) // 世界书路由:/app/worldbook/*
|
||||
appRouter.InitExtensionRouter(appGroup) // 扩展路由:/app/extension/*
|
||||
appRouter.InitRegexScriptRouter(appGroup) // 正则脚本路由:/app/regex/*
|
||||
}
|
||||
|
||||
//插件路由安装
|
||||
|
||||
108
server/model/app/ai_extension.go
Normal file
108
server/model/app/ai_extension.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"gorm.io/datatypes"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AIExtension 扩展表 (兼容 SillyTavern Extension 规范)
|
||||
type AIExtension struct {
|
||||
global.GVA_MODEL
|
||||
UserID uint `json:"userId" gorm:"not null;index;comment:所属用户ID"`
|
||||
User *AppUser `json:"user" gorm:"foreignKey:UserID"`
|
||||
Name string `json:"name" gorm:"type:varchar(500);not null;index;comment:扩展名称"`
|
||||
DisplayName string `json:"displayName" gorm:"type:varchar(500);comment:显示名称"`
|
||||
Version string `json:"version" gorm:"type:varchar(50);comment:版本号"`
|
||||
Author string `json:"author" gorm:"type:varchar(200);comment:作者"`
|
||||
Description string `json:"description" gorm:"type:text;comment:扩展描述"`
|
||||
Homepage string `json:"homepage" gorm:"type:varchar(1024);comment:主页地址"`
|
||||
Repository string `json:"repository" gorm:"type:varchar(1024);comment:仓库地址"`
|
||||
License string `json:"license" gorm:"type:varchar(100);comment:许可证"`
|
||||
Tags datatypes.JSON `json:"tags" gorm:"type:jsonb;comment:标签列表"`
|
||||
|
||||
// 扩展类型和功能
|
||||
ExtensionType string `json:"extensionType" gorm:"type:varchar(50);default:'ui';comment:扩展类型(ui/server/hybrid)"` // ui, server, hybrid
|
||||
Category string `json:"category" gorm:"type:varchar(100);comment:分类(utilities/themes/integrations/tools)"`
|
||||
|
||||
// 依赖关系
|
||||
Dependencies datatypes.JSON `json:"dependencies" gorm:"type:jsonb;comment:依赖的其他扩展"`
|
||||
Conflicts datatypes.JSON `json:"conflicts" gorm:"type:jsonb;comment:冲突的扩展列表"`
|
||||
|
||||
// 扩展文件
|
||||
ManifestData datatypes.JSON `json:"manifestData" gorm:"type:jsonb;not null;comment:manifest.json 完整内容"`
|
||||
ScriptPath string `json:"scriptPath" gorm:"type:varchar(1024);comment:主脚本文件路径"`
|
||||
StylePath string `json:"stylePath" gorm:"type:varchar(1024);comment:样式文件路径"`
|
||||
AssetsPaths datatypes.JSON `json:"assetsPaths" gorm:"type:jsonb;comment:资源文件路径列表"`
|
||||
|
||||
// 扩展配置
|
||||
Settings datatypes.JSON `json:"settings" gorm:"type:jsonb;comment:扩展配置项"`
|
||||
Options datatypes.JSON `json:"options" gorm:"type:jsonb;comment:扩展选项"`
|
||||
|
||||
// 状态
|
||||
IsEnabled bool `json:"isEnabled" gorm:"default:false;index;comment:是否启用"`
|
||||
IsInstalled bool `json:"isInstalled" gorm:"default:true;index;comment:是否已安装"`
|
||||
IsSystemExt bool `json:"isSystemExt" gorm:"default:false;comment:是否系统内置扩展"`
|
||||
InstallSource string `json:"installSource" gorm:"type:varchar(500);comment:安装来源(url/git/file/marketplace)"`
|
||||
SourceURL string `json:"sourceUrl" gorm:"type:varchar(1000);comment:原始安装 URL(用于更新)"`
|
||||
Branch string `json:"branch" gorm:"type:varchar(100);comment:Git 分支名称"`
|
||||
InstallDate time.Time `json:"installDate" gorm:"comment:安装日期"`
|
||||
LastEnabled time.Time `json:"lastEnabled" gorm:"comment:最后启用时间"`
|
||||
|
||||
// 更新相关
|
||||
AutoUpdate bool `json:"autoUpdate" gorm:"default:false;comment:是否自动更新"`
|
||||
LastUpdateCheck *time.Time `json:"lastUpdateCheck" gorm:"comment:最后检查更新时间"`
|
||||
AvailableVersion string `json:"availableVersion" gorm:"type:varchar(50);comment:可用的新版本"`
|
||||
|
||||
// 统计
|
||||
UsageCount int `json:"usageCount" gorm:"default:0;comment:使用次数"`
|
||||
ErrorCount int `json:"errorCount" gorm:"default:0;comment:错误次数"`
|
||||
LoadTime int `json:"loadTime" gorm:"default:0;comment:平均加载时间(ms)"`
|
||||
|
||||
// 元数据
|
||||
Metadata datatypes.JSON `json:"metadata" gorm:"type:jsonb;comment:扩展元数据"`
|
||||
}
|
||||
|
||||
func (AIExtension) TableName() string {
|
||||
return "ai_extensions"
|
||||
}
|
||||
|
||||
// AIExtensionManifest 扩展清单结构 (对应 manifest.json)
|
||||
type AIExtensionManifest struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Type string `json:"type,omitempty"` // ui, server, hybrid
|
||||
Category string `json:"category,omitempty"` // utilities, themes, integrations, tools
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"` // {"extension-name": ">=1.0.0"}
|
||||
Conflicts []string `json:"conflicts,omitempty"`
|
||||
Entry string `json:"entry,omitempty"` // 主入口文件
|
||||
Style string `json:"style,omitempty"` // 样式文件
|
||||
Assets []string `json:"assets,omitempty"` // 资源文件列表
|
||||
Settings map[string]interface{} `json:"settings,omitempty"` // 默认设置
|
||||
Options map[string]interface{} `json:"options,omitempty"` // 扩展选项
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"` // 扩展元数据
|
||||
AutoUpdate bool `json:"auto_update,omitempty"` // 是否自动更新(SillyTavern 兼容)
|
||||
InlineScript string `json:"inline_script,omitempty"` // 内联脚本(SillyTavern 兼容)
|
||||
}
|
||||
|
||||
// AIExtensionSettings 用户的扩展配置(已废弃,配置现在直接存储在 AIExtension.Settings 中)
|
||||
// type AIExtensionSettings struct {
|
||||
// global.GVA_MODEL
|
||||
// UserID uint `json:"userId" gorm:"not null;index:idx_user_ext,unique;comment:用户ID"`
|
||||
// User *AppUser `json:"user" gorm:"foreignKey:UserID"`
|
||||
// ExtensionID uint `json:"extensionId" gorm:"not null;index:idx_user_ext,unique;comment:扩展ID"`
|
||||
// Extension *AIExtension `json:"extension" gorm:"foreignKey:ExtensionID"`
|
||||
// Settings datatypes.JSON `json:"settings" gorm:"type:jsonb;not null;comment:用户配置"`
|
||||
// IsEnabled bool `json:"isEnabled" gorm:"default:true;comment:用户是否启用"`
|
||||
// }
|
||||
//
|
||||
// func (AIExtensionSettings) TableName() string {
|
||||
// return "ai_extension_settings"
|
||||
// }
|
||||
65
server/model/app/ai_regex_script.go
Normal file
65
server/model/app/ai_regex_script.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"github.com/lib/pq"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
// AIRegexScript 正则表达式脚本
|
||||
type AIRegexScript struct {
|
||||
global.GVA_MODEL
|
||||
UserID uint `json:"userId" gorm:"not null;index;comment:用户ID"`
|
||||
User *AppUser `json:"user" gorm:"foreignKey:UserID"`
|
||||
|
||||
// 基础信息
|
||||
ScriptName string `json:"scriptName" gorm:"type:varchar(500);not null;comment:脚本名称"`
|
||||
Description string `json:"description" gorm:"type:text;comment:脚本描述"`
|
||||
|
||||
// 脚本内容
|
||||
FindRegex string `json:"findRegex" gorm:"type:text;not null;comment:查找正则表达式"`
|
||||
ReplaceString string `json:"replaceString" gorm:"type:text;comment:替换字符串"`
|
||||
|
||||
// 脚本配置
|
||||
Enabled bool `json:"enabled" gorm:"default:true;comment:是否启用"`
|
||||
IsGlobal bool `json:"isGlobal" gorm:"default:false;index;comment:是否全局脚本"`
|
||||
TrimStrings bool `json:"trimStrings" gorm:"default:false;comment:是否去除首尾空格"`
|
||||
OnlyFormat bool `json:"onlyFormat" gorm:"default:false;comment:仅格式化消息"`
|
||||
RunOnEdit bool `json:"runOnEdit" gorm:"default:false;comment:编辑时运行"`
|
||||
SubstituteRegex bool `json:"substituteRegex" gorm:"default:false;comment:替换正则"`
|
||||
MinDepth *int `json:"minDepth" gorm:"comment:最小深度"`
|
||||
MaxDepth *int `json:"maxDepth" gorm:"comment:最大深度"`
|
||||
|
||||
// 应用范围
|
||||
Placement string `json:"placement" gorm:"type:varchar(50);comment:应用位置:user,ai,sys,slash"` // user, ai, sys, slash
|
||||
AffectMinDepth *int `json:"affectMinDepth" gorm:"comment:影响最小深度"`
|
||||
AffectMaxDepth *int `json:"affectMaxDepth" gorm:"comment:影响最大深度"`
|
||||
|
||||
// 关联数据
|
||||
LinkedChars pq.StringArray `json:"linkedChars" gorm:"type:text[];comment:关联的角色ID列表"`
|
||||
|
||||
// 扩展数据(用于存储额外配置)
|
||||
ScriptData datatypes.JSON `json:"scriptData" gorm:"type:jsonb;comment:脚本附加数据"`
|
||||
|
||||
// 统计信息
|
||||
UsageCount int `json:"usageCount" gorm:"default:0;comment:使用次数"`
|
||||
LastUsedAt *int64 `json:"lastUsedAt" gorm:"comment:最后使用时间戳"`
|
||||
}
|
||||
|
||||
func (AIRegexScript) TableName() string {
|
||||
return "ai_regex_scripts"
|
||||
}
|
||||
|
||||
// AICharacterRegexScript 角色与正则脚本关联表
|
||||
type AICharacterRegexScript struct {
|
||||
global.GVA_MODEL
|
||||
CharacterID uint `json:"characterId" gorm:"not null;index:idx_char_regex,unique;comment:角色ID"`
|
||||
Character *AICharacter `json:"character" gorm:"foreignKey:CharacterID"`
|
||||
RegexID uint `json:"regexId" gorm:"not null;index:idx_char_regex,unique;comment:正则脚本ID"`
|
||||
Regex *AIRegexScript `json:"regex" gorm:"foreignKey:RegexID"`
|
||||
Order int `json:"order" gorm:"default:0;comment:执行顺序"`
|
||||
}
|
||||
|
||||
func (AICharacterRegexScript) TableName() string {
|
||||
return "ai_character_regex_scripts"
|
||||
}
|
||||
107
server/model/app/request/extension.go
Normal file
107
server/model/app/request/extension.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
common "git.echol.cn/loser/st/server/model/common/request"
|
||||
)
|
||||
|
||||
// CreateExtensionRequest 创建/安装扩展请求
|
||||
type CreateExtensionRequest struct {
|
||||
Name string `json:"name" binding:"required,min=1,max=500"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Version string `json:"version"`
|
||||
Author string `json:"author"`
|
||||
Description string `json:"description"`
|
||||
Homepage string `json:"homepage"`
|
||||
Repository string `json:"repository"`
|
||||
License string `json:"license"`
|
||||
Tags []string `json:"tags"`
|
||||
ExtensionType string `json:"extensionType" binding:"required,oneof=ui server hybrid"`
|
||||
Category string `json:"category"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
Conflicts []string `json:"conflicts"`
|
||||
ManifestData map[string]interface{} `json:"manifestData" binding:"required"`
|
||||
ScriptPath string `json:"scriptPath"`
|
||||
StylePath string `json:"stylePath"`
|
||||
AssetsPaths []string `json:"assetsPaths"`
|
||||
Settings map[string]interface{} `json:"settings"`
|
||||
Options map[string]interface{} `json:"options"`
|
||||
InstallSource string `json:"installSource"`
|
||||
SourceURL string `json:"sourceUrl"` // 原始安装 URL(用于更新)
|
||||
Branch string `json:"branch"` // Git 分支
|
||||
AutoUpdate bool `json:"autoUpdate"` // 是否自动更新
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// ExtensionListRequest 扩展列表查询请求
|
||||
type ExtensionListRequest struct {
|
||||
common.PageInfo
|
||||
Name string `json:"name" form:"name"` // 扩展名称(模糊搜索)
|
||||
ExtensionType string `json:"extensionType" form:"extensionType"` // 扩展类型
|
||||
Category string `json:"category" form:"category"` // 分类
|
||||
IsEnabled *bool `json:"isEnabled" form:"isEnabled"` // 是否启用
|
||||
IsInstalled *bool `json:"isInstalled" form:"isInstalled"` // 是否已安装
|
||||
Tag string `json:"tag" form:"tag"` // 标签过滤
|
||||
}
|
||||
|
||||
// InstallExtensionRequest 安装扩展请求
|
||||
type InstallExtensionRequest struct {
|
||||
Source string `json:"source" binding:"required,oneof=url file marketplace"` // 安装来源
|
||||
URL string `json:"url"` // URL 安装
|
||||
ManifestData []byte `json:"manifestData"` // 文件安装
|
||||
MarketplaceID string `json:"marketplaceId"` // 市场安装
|
||||
}
|
||||
|
||||
// UninstallExtensionRequest 卸载扩展请求
|
||||
type UninstallExtensionRequest struct {
|
||||
DeleteFiles bool `json:"deleteFiles"` // 是否删除文件
|
||||
}
|
||||
|
||||
// ToggleExtensionRequest 启用/禁用扩展请求
|
||||
type ToggleExtensionRequest struct {
|
||||
IsEnabled *bool `json:"isEnabled"` // 使用指针类型,允许 false 值
|
||||
}
|
||||
|
||||
// UpdateExtensionSettingsRequest 更新扩展配置请求
|
||||
type UpdateExtensionSettingsRequest struct {
|
||||
Settings map[string]interface{} `json:"settings" binding:"required"`
|
||||
}
|
||||
|
||||
// ImportExtensionRequest 导入扩展请求
|
||||
type ImportExtensionRequest struct {
|
||||
Format string `json:"format" binding:"required,oneof=zip folder"`
|
||||
}
|
||||
|
||||
// ExportExtensionRequest 导出扩展请求
|
||||
type ExportExtensionRequest struct {
|
||||
Format string `json:"format" binding:"required,oneof=zip folder"`
|
||||
IncludeAssets bool `json:"includeAssets"` // 是否包含资源文件
|
||||
}
|
||||
|
||||
// ExtensionStatsRequest 扩展统计请求
|
||||
type ExtensionStatsRequest struct {
|
||||
ExtensionID uint `json:"extensionId" binding:"required"`
|
||||
Action string `json:"action" binding:"required,oneof=usage error load"` // 统计类型
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
// InstallExtensionFromGitRequest 从 Git URL 安装扩展请求
|
||||
type InstallExtensionFromGitRequest struct {
|
||||
GitUrl string `json:"gitUrl" binding:"required"` // Git 仓库 URL
|
||||
Branch string `json:"branch" binding:"omitempty,max=100"` // 分支名称(可选,默认 main)
|
||||
}
|
||||
|
||||
// InstallExtensionFromURLRequest 从 URL 安装扩展请求(智能识别 Git URL 或 Manifest URL)
|
||||
type InstallExtensionFromURLRequest struct {
|
||||
URL string `json:"url" binding:"required"` // Git 仓库 URL 或 Manifest.json URL
|
||||
Branch string `json:"branch"` // Git 分支名称(可选,默认 main)
|
||||
}
|
||||
|
||||
// UpdateExtensionRequest 更新扩展请求
|
||||
type UpdateExtensionRequest struct {
|
||||
Force bool `json:"force"` // 是否强制更新(忽略本地修改)
|
||||
DisplayName string `json:"displayName"`
|
||||
Description string `json:"description"`
|
||||
Settings map[string]interface{} `json:"settings"`
|
||||
Options map[string]interface{} `json:"options"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
88
server/model/app/request/regex_script.go
Normal file
88
server/model/app/request/regex_script.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/st/server/model/app"
|
||||
common "git.echol.cn/loser/st/server/model/common/request"
|
||||
)
|
||||
|
||||
// CreateRegexScriptRequest 创建正则脚本请求
|
||||
type CreateRegexScriptRequest struct {
|
||||
ScriptName string `json:"scriptName" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
FindRegex string `json:"findRegex" binding:"required"`
|
||||
ReplaceString string `json:"replaceString"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IsGlobal bool `json:"isGlobal"`
|
||||
TrimStrings bool `json:"trimStrings"`
|
||||
OnlyFormat bool `json:"onlyFormat"`
|
||||
RunOnEdit bool `json:"runOnEdit"`
|
||||
SubstituteRegex bool `json:"substituteRegex"`
|
||||
MinDepth *int `json:"minDepth"`
|
||||
MaxDepth *int `json:"maxDepth"`
|
||||
Placement string `json:"placement"`
|
||||
AffectMinDepth *int `json:"affectMinDepth"`
|
||||
AffectMaxDepth *int `json:"affectMaxDepth"`
|
||||
LinkedChars []string `json:"linkedChars"`
|
||||
ScriptData map[string]interface{} `json:"scriptData"`
|
||||
}
|
||||
|
||||
// UpdateRegexScriptRequest 更新正则脚本请求
|
||||
type UpdateRegexScriptRequest struct {
|
||||
ScriptName string `json:"scriptName"`
|
||||
Description string `json:"description"`
|
||||
FindRegex string `json:"findRegex"`
|
||||
ReplaceString string `json:"replaceString"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
IsGlobal *bool `json:"isGlobal"`
|
||||
TrimStrings *bool `json:"trimStrings"`
|
||||
OnlyFormat *bool `json:"onlyFormat"`
|
||||
RunOnEdit *bool `json:"runOnEdit"`
|
||||
SubstituteRegex *bool `json:"substituteRegex"`
|
||||
MinDepth *int `json:"minDepth"`
|
||||
MaxDepth *int `json:"maxDepth"`
|
||||
Placement string `json:"placement"`
|
||||
AffectMinDepth *int `json:"affectMinDepth"`
|
||||
AffectMaxDepth *int `json:"affectMaxDepth"`
|
||||
LinkedChars []string `json:"linkedChars"`
|
||||
ScriptData map[string]interface{} `json:"scriptData"`
|
||||
}
|
||||
|
||||
// RegexScriptListRequest 正则脚本列表查询请求
|
||||
type RegexScriptListRequest struct {
|
||||
common.PageInfo
|
||||
ScriptName string `json:"scriptName" form:"scriptName"` // 脚本名称(模糊搜索)
|
||||
IsGlobal *bool `json:"isGlobal" form:"isGlobal"` // 是否全局
|
||||
Enabled *bool `json:"enabled" form:"enabled"` // 是否启用
|
||||
CharacterID *uint `json:"characterId" form:"characterId"` // 关联角色ID
|
||||
}
|
||||
|
||||
// LinkCharacterToRegexRequest 关联角色到正则脚本请求
|
||||
type LinkCharacterToRegexRequest struct {
|
||||
CharacterIDs []uint `json:"characterIds" binding:"required"`
|
||||
}
|
||||
|
||||
// TestRegexScriptRequest 测试正则脚本请求
|
||||
type TestRegexScriptRequest struct {
|
||||
FindRegex string `json:"findRegex" binding:"required"`
|
||||
ReplaceString string `json:"replaceString"`
|
||||
TestInput string `json:"testInput" binding:"required"`
|
||||
TrimStrings bool `json:"trimStrings"`
|
||||
SubstituteRegex bool `json:"substituteRegex"`
|
||||
}
|
||||
|
||||
// ApplyRegexScriptsRequest 应用正则脚本请求
|
||||
type ApplyRegexScriptsRequest struct {
|
||||
Text string `json:"text" binding:"required"`
|
||||
RegexIDs []uint `json:"regexIds"` // 指定要应用的脚本ID列表
|
||||
CharacterID *uint `json:"characterId"` // 角色ID(自动应用关联的脚本)
|
||||
Placement string `json:"placement"` // 应用位置
|
||||
MinDepth *int `json:"minDepth"` // 最小深度
|
||||
MaxDepth *int `json:"maxDepth"` // 最大深度
|
||||
UseGlobal bool `json:"useGlobal"` // 是否应用全局脚本
|
||||
}
|
||||
|
||||
// ImportRegexScriptsRequest 导入正则脚本请求
|
||||
type ImportRegexScriptsRequest struct {
|
||||
Scripts []app.AIRegexScript `json:"scripts" binding:"required"`
|
||||
OverwriteMode string `json:"overwriteMode"` // skip, overwrite, merge
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package request
|
||||
|
||||
import "git.echol.cn/loser/st/server/model/app"
|
||||
import (
|
||||
"git.echol.cn/loser/st/server/model/app"
|
||||
common "git.echol.cn/loser/st/server/model/common/request"
|
||||
)
|
||||
|
||||
// CreateWorldBookRequest 创建世界书请求
|
||||
type CreateWorldBookRequest struct {
|
||||
@@ -20,7 +23,7 @@ type UpdateWorldBookRequest struct {
|
||||
|
||||
// WorldBookListRequest 世界书列表查询请求
|
||||
type WorldBookListRequest struct {
|
||||
PageInfo
|
||||
common.PageInfo
|
||||
BookName string `json:"bookName" form:"bookName"` // 世界书名称(模糊搜索)
|
||||
IsGlobal *bool `json:"isGlobal" form:"isGlobal"` // 是否全局
|
||||
CharacterID *uint `json:"characterId" form:"characterId"` // 关联角色ID
|
||||
|
||||
188
server/model/app/response/extension.go
Normal file
188
server/model/app/response/extension.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.echol.cn/loser/st/server/model/app"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExtensionResponse 扩展响应
|
||||
type ExtensionResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"userId"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Version string `json:"version"`
|
||||
Author string `json:"author"`
|
||||
Description string `json:"description"`
|
||||
Homepage string `json:"homepage"`
|
||||
Repository string `json:"repository"`
|
||||
License string `json:"license"`
|
||||
Tags []string `json:"tags"`
|
||||
ExtensionType string `json:"extensionType"`
|
||||
Category string `json:"category"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
Conflicts []string `json:"conflicts"`
|
||||
ManifestData map[string]interface{} `json:"manifestData"`
|
||||
ScriptPath string `json:"scriptPath"`
|
||||
StylePath string `json:"stylePath"`
|
||||
AssetsPaths []string `json:"assetsPaths"`
|
||||
Settings map[string]interface{} `json:"settings"`
|
||||
Options map[string]interface{} `json:"options"`
|
||||
IsEnabled bool `json:"isEnabled"`
|
||||
IsInstalled bool `json:"isInstalled"`
|
||||
IsSystemExt bool `json:"isSystemExt"`
|
||||
InstallSource string `json:"installSource"`
|
||||
InstallDate time.Time `json:"installDate"`
|
||||
LastEnabled time.Time `json:"lastEnabled"`
|
||||
UsageCount int `json:"usageCount"`
|
||||
ErrorCount int `json:"errorCount"`
|
||||
LoadTime int `json:"loadTime"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// ExtensionListResponse 扩展列表响应
|
||||
type ExtensionListResponse struct {
|
||||
List []ExtensionResponse `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
|
||||
// ExtensionManifestResponse manifest.json 响应
|
||||
type ExtensionManifestResponse struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author"`
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
Dependencies map[string]string `json:"dependencies,omitempty"`
|
||||
Conflicts []string `json:"conflicts,omitempty"`
|
||||
Entry string `json:"entry,omitempty"`
|
||||
Style string `json:"style,omitempty"`
|
||||
Assets []string `json:"assets,omitempty"`
|
||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
||||
Options map[string]interface{} `json:"options,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ExtensionStatsResponse 扩展统计响应
|
||||
type ExtensionStatsResponse struct {
|
||||
ExtensionID uint `json:"extensionId"`
|
||||
ExtensionName string `json:"extensionName"`
|
||||
UsageCount int `json:"usageCount"`
|
||||
ErrorCount int `json:"errorCount"`
|
||||
LoadTime int `json:"loadTime"`
|
||||
LastUsed time.Time `json:"lastUsed"`
|
||||
}
|
||||
|
||||
// ToExtensionResponse 转换为扩展响应
|
||||
func ToExtensionResponse(ext *app.AIExtension) ExtensionResponse {
|
||||
var tags []string
|
||||
if ext.Tags != nil {
|
||||
_ = json.Unmarshal([]byte(ext.Tags), &tags)
|
||||
}
|
||||
if tags == nil {
|
||||
tags = []string{}
|
||||
}
|
||||
|
||||
var dependencies map[string]string
|
||||
if ext.Dependencies != nil {
|
||||
_ = json.Unmarshal([]byte(ext.Dependencies), &dependencies)
|
||||
}
|
||||
if dependencies == nil {
|
||||
dependencies = map[string]string{}
|
||||
}
|
||||
|
||||
var conflicts []string
|
||||
if ext.Conflicts != nil {
|
||||
_ = json.Unmarshal([]byte(ext.Conflicts), &conflicts)
|
||||
}
|
||||
if conflicts == nil {
|
||||
conflicts = []string{}
|
||||
}
|
||||
|
||||
var manifestData map[string]interface{}
|
||||
if ext.ManifestData != nil {
|
||||
_ = json.Unmarshal([]byte(ext.ManifestData), &manifestData)
|
||||
}
|
||||
if manifestData == nil {
|
||||
manifestData = map[string]interface{}{}
|
||||
}
|
||||
|
||||
var assetsPaths []string
|
||||
if ext.AssetsPaths != nil {
|
||||
_ = json.Unmarshal([]byte(ext.AssetsPaths), &assetsPaths)
|
||||
}
|
||||
if assetsPaths == nil {
|
||||
assetsPaths = []string{}
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
if ext.Settings != nil {
|
||||
_ = json.Unmarshal([]byte(ext.Settings), &settings)
|
||||
}
|
||||
if settings == nil {
|
||||
settings = map[string]interface{}{}
|
||||
}
|
||||
|
||||
var options map[string]interface{}
|
||||
if ext.Options != nil {
|
||||
_ = json.Unmarshal([]byte(ext.Options), &options)
|
||||
}
|
||||
if options == nil {
|
||||
options = map[string]interface{}{}
|
||||
}
|
||||
|
||||
var metadata map[string]interface{}
|
||||
if ext.Metadata != nil {
|
||||
_ = json.Unmarshal([]byte(ext.Metadata), &metadata)
|
||||
}
|
||||
if metadata == nil {
|
||||
metadata = map[string]interface{}{}
|
||||
}
|
||||
|
||||
return ExtensionResponse{
|
||||
ID: ext.ID,
|
||||
UserID: ext.UserID,
|
||||
Name: ext.Name,
|
||||
DisplayName: ext.DisplayName,
|
||||
Version: ext.Version,
|
||||
Author: ext.Author,
|
||||
Description: ext.Description,
|
||||
Homepage: ext.Homepage,
|
||||
Repository: ext.Repository,
|
||||
License: ext.License,
|
||||
Tags: tags,
|
||||
ExtensionType: ext.ExtensionType,
|
||||
Category: ext.Category,
|
||||
Dependencies: dependencies,
|
||||
Conflicts: conflicts,
|
||||
ManifestData: manifestData,
|
||||
ScriptPath: ext.ScriptPath,
|
||||
StylePath: ext.StylePath,
|
||||
AssetsPaths: assetsPaths,
|
||||
Settings: settings,
|
||||
Options: options,
|
||||
IsEnabled: ext.IsEnabled,
|
||||
IsInstalled: ext.IsInstalled,
|
||||
IsSystemExt: ext.IsSystemExt,
|
||||
InstallSource: ext.InstallSource,
|
||||
InstallDate: ext.InstallDate,
|
||||
LastEnabled: ext.LastEnabled,
|
||||
UsageCount: ext.UsageCount,
|
||||
ErrorCount: ext.ErrorCount,
|
||||
LoadTime: ext.LoadTime,
|
||||
Metadata: metadata,
|
||||
CreatedAt: ext.CreatedAt,
|
||||
UpdatedAt: ext.UpdatedAt,
|
||||
}
|
||||
}
|
||||
106
server/model/app/response/regex_script.go
Normal file
106
server/model/app/response/regex_script.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.echol.cn/loser/st/server/model/app"
|
||||
)
|
||||
|
||||
// RegexScriptResponse 正则脚本响应
|
||||
type RegexScriptResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"userId"`
|
||||
ScriptName string `json:"scriptName"`
|
||||
Description string `json:"description"`
|
||||
FindRegex string `json:"findRegex"`
|
||||
ReplaceString string `json:"replaceString"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IsGlobal bool `json:"isGlobal"`
|
||||
TrimStrings bool `json:"trimStrings"`
|
||||
OnlyFormat bool `json:"onlyFormat"`
|
||||
RunOnEdit bool `json:"runOnEdit"`
|
||||
SubstituteRegex bool `json:"substituteRegex"`
|
||||
MinDepth *int `json:"minDepth"`
|
||||
MaxDepth *int `json:"maxDepth"`
|
||||
Placement string `json:"placement"`
|
||||
AffectMinDepth *int `json:"affectMinDepth"`
|
||||
AffectMaxDepth *int `json:"affectMaxDepth"`
|
||||
LinkedChars []string `json:"linkedChars"`
|
||||
ScriptData map[string]interface{} `json:"scriptData"`
|
||||
UsageCount int `json:"usageCount"`
|
||||
LastUsedAt *int64 `json:"lastUsedAt"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// RegexScriptListResponse 正则脚本列表响应
|
||||
type RegexScriptListResponse struct {
|
||||
List []RegexScriptResponse `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
|
||||
// TestRegexScriptResponse 测试正则脚本响应
|
||||
type TestRegexScriptResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Input string `json:"input"`
|
||||
Output string `json:"output"`
|
||||
MatchedCount int `json:"matchedCount"`
|
||||
Matches []string `json:"matches"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ApplyRegexScriptsResponse 应用正则脚本响应
|
||||
type ApplyRegexScriptsResponse struct {
|
||||
OriginalText string `json:"originalText"`
|
||||
ProcessedText string `json:"processedText"`
|
||||
AppliedCount int `json:"appliedCount"`
|
||||
AppliedScripts []uint `json:"appliedScripts"` // 应用的脚本ID列表
|
||||
}
|
||||
|
||||
// RegexScriptExportData 正则脚本导出数据
|
||||
type RegexScriptExportData struct {
|
||||
Version string `json:"version"` // 导出格式版本
|
||||
Scripts []RegexScriptResponse `json:"scripts"`
|
||||
ExportedAt int64 `json:"exportedAt"`
|
||||
}
|
||||
|
||||
// ToRegexScriptResponse 将 AIRegexScript 转换为 RegexScriptResponse
|
||||
func ToRegexScriptResponse(script *app.AIRegexScript) RegexScriptResponse {
|
||||
var scriptData map[string]interface{}
|
||||
if len(script.ScriptData) > 0 {
|
||||
_ = json.Unmarshal(script.ScriptData, &scriptData)
|
||||
}
|
||||
|
||||
linkedChars := []string{}
|
||||
if script.LinkedChars != nil {
|
||||
linkedChars = script.LinkedChars
|
||||
}
|
||||
|
||||
return RegexScriptResponse{
|
||||
ID: script.ID,
|
||||
UserID: script.UserID,
|
||||
ScriptName: script.ScriptName,
|
||||
Description: script.Description,
|
||||
FindRegex: script.FindRegex,
|
||||
ReplaceString: script.ReplaceString,
|
||||
Enabled: script.Enabled,
|
||||
IsGlobal: script.IsGlobal,
|
||||
TrimStrings: script.TrimStrings,
|
||||
OnlyFormat: script.OnlyFormat,
|
||||
RunOnEdit: script.RunOnEdit,
|
||||
SubstituteRegex: script.SubstituteRegex,
|
||||
MinDepth: script.MinDepth,
|
||||
MaxDepth: script.MaxDepth,
|
||||
Placement: script.Placement,
|
||||
AffectMinDepth: script.AffectMinDepth,
|
||||
AffectMaxDepth: script.AffectMaxDepth,
|
||||
LinkedChars: linkedChars,
|
||||
ScriptData: scriptData,
|
||||
UsageCount: script.UsageCount,
|
||||
LastUsedAt: script.LastUsedAt,
|
||||
CreatedAt: script.CreatedAt.Unix(),
|
||||
UpdatedAt: script.UpdatedAt.Unix(),
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
|
||||
// PageInfo Paging common input parameter structure
|
||||
type PageInfo struct {
|
||||
Page int `json:"page" form:"page"` // 页码
|
||||
PageSize int `json:"pageSize" form:"pageSize"` // 每页大小
|
||||
Keyword string `json:"keyword" form:"keyword"` // 关键字
|
||||
Page int `json:"page" form:"page,default=1"` // 页码
|
||||
PageSize int `json:"pageSize" form:"pageSize,default=20"` // 每页大小
|
||||
Keyword string `json:"keyword" form:"keyword"` // 关键字
|
||||
}
|
||||
|
||||
func (r *PageInfo) Paginate() func(db *gorm.DB) *gorm.DB {
|
||||
|
||||
@@ -4,4 +4,6 @@ type RouterGroup struct {
|
||||
AuthRouter
|
||||
CharacterRouter
|
||||
WorldInfoRouter
|
||||
ExtensionRouter
|
||||
RegexScriptRouter
|
||||
}
|
||||
|
||||
46
server/router/app/extension.go
Normal file
46
server/router/app/extension.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/st/server/api/v1"
|
||||
"git.echol.cn/loser/st/server/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ExtensionRouter struct{}
|
||||
|
||||
func (r *ExtensionRouter) InitExtensionRouter(Router *gin.RouterGroup) {
|
||||
extensionRouter := Router.Group("extension").Use(middleware.AppJWTAuth())
|
||||
extensionApi := v1.ApiGroupApp.AppApiGroup.ExtensionApi
|
||||
|
||||
{
|
||||
// 扩展管理
|
||||
extensionRouter.POST("", extensionApi.CreateExtension) // 创建/安装扩展
|
||||
extensionRouter.PUT("/:id", extensionApi.UpdateExtension) // 更新扩展
|
||||
extensionRouter.DELETE("/:id", extensionApi.DeleteExtension) // 删除/卸载扩展
|
||||
extensionRouter.GET("/:id", extensionApi.GetExtension) // 获取扩展详情
|
||||
extensionRouter.GET("/list", extensionApi.GetExtensionList) // 获取扩展列表
|
||||
extensionRouter.GET("/enabled", extensionApi.GetEnabledExtensions) // 获取启用的扩展列表
|
||||
|
||||
// 扩展操作
|
||||
extensionRouter.POST("/:id/toggle", extensionApi.ToggleExtension) // 启用/禁用扩展
|
||||
extensionRouter.POST("/:id/update", extensionApi.UpgradeExtension) // 升级扩展版本
|
||||
|
||||
// 扩展配置
|
||||
extensionRouter.GET("/:id/settings", extensionApi.GetExtensionSettings) // 获取扩展配置
|
||||
extensionRouter.PUT("/:id/settings", extensionApi.UpdateExtensionSettings) // 更新扩展配置
|
||||
|
||||
// 扩展元数据
|
||||
extensionRouter.GET("/:id/manifest", extensionApi.GetExtensionManifest) // 获取 manifest.json
|
||||
|
||||
// 导入导出
|
||||
extensionRouter.POST("/import", extensionApi.ImportExtension) // 导入扩展
|
||||
extensionRouter.GET("/:id/export", extensionApi.ExportExtension) // 导出扩展
|
||||
|
||||
// 安装方式
|
||||
extensionRouter.POST("/install/url", extensionApi.InstallExtensionFromURL) // 从 URL 安装扩展(后端代理)
|
||||
extensionRouter.POST("/install/git", extensionApi.InstallExtensionFromGit) // 从 Git URL 安装扩展
|
||||
|
||||
// 统计
|
||||
extensionRouter.POST("/stats", extensionApi.UpdateExtensionStats) // 更新扩展统计
|
||||
}
|
||||
}
|
||||
29
server/router/app/regex_script.go
Normal file
29
server/router/app/regex_script.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"git.echol.cn/loser/st/server/api/v1"
|
||||
"git.echol.cn/loser/st/server/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RegexScriptRouter struct{}
|
||||
|
||||
// InitRegexScriptRouter 初始化正则脚本路由
|
||||
func (r *RegexScriptRouter) InitRegexScriptRouter(Router *gin.RouterGroup) {
|
||||
regexRouter := Router.Group("regex").Use(middleware.AppJWTAuth())
|
||||
regexApi := v1.ApiGroupApp.AppApiGroup.RegexScriptApi
|
||||
{
|
||||
regexRouter.POST("", regexApi.CreateRegexScript) // 创建正则脚本
|
||||
regexRouter.PUT(":id", regexApi.UpdateRegexScript) // 更新正则脚本
|
||||
regexRouter.DELETE(":id", regexApi.DeleteRegexScript) // 删除正则脚本
|
||||
regexRouter.GET(":id", regexApi.GetRegexScript) // 获取正则脚本详情
|
||||
regexRouter.GET("", regexApi.GetRegexScriptList) // 获取正则脚本列表
|
||||
regexRouter.POST(":id/link", regexApi.LinkCharactersToRegex) // 关联角色到脚本
|
||||
regexRouter.GET("character/:characterId", regexApi.GetCharacterRegexScripts) // 获取角色的脚本
|
||||
regexRouter.POST(":id/duplicate", regexApi.DuplicateRegexScript) // 复制脚本
|
||||
regexRouter.POST("test", regexApi.TestRegexScript) // 测试正则脚本
|
||||
regexRouter.POST("apply", regexApi.ApplyRegexScripts) // 应用正则脚本
|
||||
regexRouter.POST("import", regexApi.ImportRegexScripts) // 导入正则脚本
|
||||
regexRouter.GET("export", regexApi.ExportRegexScripts) // 导出正则脚本
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -4,4 +4,6 @@ type AppServiceGroup struct {
|
||||
AuthService
|
||||
CharacterService
|
||||
WorldInfoService
|
||||
ExtensionService
|
||||
RegexScriptService
|
||||
}
|
||||
|
||||
803
server/service/app/extension.go
Normal file
803
server/service/app/extension.go
Normal 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
|
||||
}
|
||||
476
server/service/app/regex_script.go
Normal file
476
server/service/app/regex_script.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user