From 0ebe197cc1ee3154a16a01187fe58443f21f48d3 Mon Sep 17 00:00:00 2001 From: Echo <1711788888@qq.com> Date: Tue, 24 Feb 2026 13:38:29 +0800 Subject: [PATCH] =?UTF-8?q?:art:=20=E7=A7=BB=E9=99=A4=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api/v1/app/enter.go | 2 - server/api/v1/app/extension.go | 577 ----------- server/config.yaml | 2 + server/config/system.go | 1 + server/initialize/gorm.go | 1 - server/initialize/router.go | 7 +- server/model/app/ai_extension.go | 67 -- server/model/app/request/extension.go | 83 -- server/model/app/response/extension.go | 113 --- server/router/app/enter.go | 1 - server/router/app/extension.go | 33 - server/service/app/enter.go | 1 - server/service/app/extension.go | 410 -------- server/service/app/extension_installer.go | 764 --------------- web-app-vue/package-lock.json | 26 + web-app-vue/package.json | 2 + web-app-vue/src/api/extension.ts | 174 ---- web-app-vue/src/components.d.ts | 1 - .../src/components/ExtensionDrawer.vue | 904 ------------------ web-app-vue/src/layouts/DefaultLayout.vue | 31 +- web-app-vue/src/main.ts | 6 - web-app-vue/src/router/index.ts | 12 - web-app-vue/src/stores/extension.ts | 368 ------- web-app-vue/src/types/extension.d.ts | 129 --- web-app-vue/src/utils/compatibility.ts | 12 +- web-app-vue/src/utils/extensionRuntime.ts | 576 ----------- .../src/views/extension/ExtensionListNew.vue | 441 --------- .../src/views/extension/ExtensionSettings.vue | 291 ------ 28 files changed, 39 insertions(+), 4996 deletions(-) delete mode 100644 server/api/v1/app/extension.go delete mode 100644 server/model/app/ai_extension.go delete mode 100644 server/model/app/request/extension.go delete mode 100644 server/model/app/response/extension.go delete mode 100644 server/router/app/extension.go delete mode 100644 server/service/app/extension.go delete mode 100644 server/service/app/extension_installer.go delete mode 100644 web-app-vue/src/api/extension.ts delete mode 100644 web-app-vue/src/components/ExtensionDrawer.vue delete mode 100644 web-app-vue/src/stores/extension.ts delete mode 100644 web-app-vue/src/types/extension.d.ts delete mode 100644 web-app-vue/src/utils/extensionRuntime.ts delete mode 100644 web-app-vue/src/views/extension/ExtensionListNew.vue delete mode 100644 web-app-vue/src/views/extension/ExtensionSettings.vue diff --git a/server/api/v1/app/enter.go b/server/api/v1/app/enter.go index 6355f24..e1d0a0f 100644 --- a/server/api/v1/app/enter.go +++ b/server/api/v1/app/enter.go @@ -6,7 +6,6 @@ type ApiGroup struct { AuthApi CharacterApi WorldInfoApi - ExtensionApi RegexScriptApi ProviderApi ChatApi @@ -17,5 +16,4 @@ var ( characterService = service.ServiceGroupApp.AppServiceGroup.CharacterService providerService = service.ServiceGroupApp.AppServiceGroup.ProviderService chatService = service.ServiceGroupApp.AppServiceGroup.ChatService - // extensionService 已在 extension.go 中声明 ) diff --git a/server/api/v1/app/extension.go b/server/api/v1/app/extension.go deleted file mode 100644 index 2fe3d9e..0000000 --- a/server/api/v1/app/extension.go +++ /dev/null @@ -1,577 +0,0 @@ -package app - -import ( - "io" - "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 - } - - ext, 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(ext), 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) - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - sysResponse.FailWithMessage("无效的扩展ID", c) - return - } - extID := uint(id) - - var req request.UpdateExtensionRequest - if err := c.ShouldBindJSON(&req); err != nil { - sysResponse.FailWithMessage(err.Error(), c) - return - } - - if err := extensionService.UpdateExtension(userID, extID, &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" -// @Success 200 {object} response.Response -// @Router /app/extension/:id [delete] -func (a *ExtensionApi) DeleteExtension(c *gin.Context) { - userID := middleware.GetAppUserID(c) - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - sysResponse.FailWithMessage("无效的扩展ID", c) - return - } - extID := uint(id) - - if err := extensionService.DeleteExtension(userID, extID); 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) - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - sysResponse.FailWithMessage("无效的扩展ID", c) - return - } - extID := uint(id) - - ext, err := extensionService.GetExtension(userID, extID) - if err != nil { - global.GVA_LOG.Error("获取扩展失败", zap.Error(err)) - sysResponse.FailWithMessage("获取失败: "+err.Error(), c) - return - } - - sysResponse.OkWithData(response.ToExtensionResponse(ext), c) -} - -// GetExtensionList 获取扩展列表 -// @Summary 获取扩展列表 -// @Description 获取扩展列表 -// @Tags 扩展管理 -// @Accept json -// @Produce json -// @Param keyword query string false "关键词" -// @Param extensionType query string false "扩展类型" -// @Param category query string false "分类" -// @Param isEnabled query bool false "是否启用" -// @Param page query int false "页码" -// @Param pageSize query int false "每页大小" -// @Success 200 {object} response.Response{data=response.ExtensionListResponse} -// @Router /app/extension [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 - } - - extensions, total, err := extensionService.GetExtensionList(userID, &req) - if err != nil { - global.GVA_LOG.Error("获取扩展列表失败", zap.Error(err)) - sysResponse.FailWithMessage("获取失败: "+err.Error(), c) - return - } - - list := make([]response.ExtensionResponse, 0, len(extensions)) - for i := range extensions { - list = append(list, response.ToExtensionResponse(&extensions[i])) - } - - sysResponse.OkWithData(response.ExtensionListResponse{ - List: list, - Total: total, - Page: req.Page, - PageSize: req.PageSize, - }, 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 - } - - list := make([]response.ExtensionResponse, 0, len(extensions)) - for i := range extensions { - list = append(list, response.ToExtensionResponse(&extensions[i])) - } - - sysResponse.OkWithData(list, 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) - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - sysResponse.FailWithMessage("无效的扩展ID", c) - return - } - extID := uint(id) - - var req request.ToggleExtensionRequest - if err := c.ShouldBindJSON(&req); err != nil { - sysResponse.FailWithMessage(err.Error(), c) - return - } - - if err := extensionService.ToggleExtension(userID, extID, req.IsEnabled); err != nil { - global.GVA_LOG.Error("切换扩展状态失败", zap.Error(err)) - sysResponse.FailWithMessage("操作失败: "+err.Error(), c) - return - } - - msg := "已禁用" - if req.IsEnabled { - msg = "已启用" - } - sysResponse.OkWithMessage(msg, c) -} - -// GetExtensionSettings 获取扩展设置 -// @Summary 获取扩展设置 -// @Description 获取扩展的个性化设置 -// @Tags 扩展管理 -// @Accept json -// @Produce json -// @Param id path int true "扩展ID" -// @Success 200 {object} response.Response -// @Router /app/extension/:id/settings [get] -func (a *ExtensionApi) GetExtensionSettings(c *gin.Context) { - userID := middleware.GetAppUserID(c) - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - sysResponse.FailWithMessage("无效的扩展ID", c) - return - } - extID := uint(id) - - settings, err := extensionService.GetExtensionSettings(userID, extID) - if err != nil { - global.GVA_LOG.Error("获取扩展设置失败", zap.Error(err)) - sysResponse.FailWithMessage("获取失败: "+err.Error(), c) - return - } - - sysResponse.OkWithData(settings, 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) - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - sysResponse.FailWithMessage("无效的扩展ID", c) - return - } - extID := uint(id) - - var req request.UpdateExtensionSettingsRequest - if err := c.ShouldBindJSON(&req); err != nil { - sysResponse.FailWithMessage(err.Error(), c) - return - } - - if err := extensionService.UpdateExtensionSettings(userID, extID, req.Settings); err != nil { - global.GVA_LOG.Error("更新扩展设置失败", zap.Error(err)) - sysResponse.FailWithMessage("更新失败: "+err.Error(), c) - return - } - - sysResponse.OkWithMessage("设置已保存", c) -} - -// GetExtensionManifest 获取扩展 manifest -// @Summary 获取扩展 manifest -// @Description 获取扩展的 manifest 数据 -// @Tags 扩展管理 -// @Accept json -// @Produce json -// @Param id path int true "扩展ID" -// @Success 200 {object} response.Response -// @Router /app/extension/:id/manifest [get] -func (a *ExtensionApi) GetExtensionManifest(c *gin.Context) { - userID := middleware.GetAppUserID(c) - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - sysResponse.FailWithMessage("无效的扩展ID", c) - return - } - extID := uint(id) - - manifest, err := extensionService.GetExtensionManifest(userID, extID) - if err != nil { - global.GVA_LOG.Error("获取扩展 manifest 失败", zap.Error(err)) - sysResponse.FailWithMessage("获取失败: "+err.Error(), c) - return - } - - sysResponse.OkWithData(manifest, c) -} - -// ImportExtension 导入扩展 -// @Summary 导入扩展 -// @Description 从 ZIP 压缩包或 JSON 文件导入扩展 -// @Tags 扩展管理 -// @Accept multipart/form-data -// @Produce json -// @Param file formData file true "扩展文件(zip/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("请上传扩展文件(支持 .zip 或 .json)", c) - return - } - - // 文件大小限制(100MB,zip 包可能较大) - if file.Size > 100<<20 { - sysResponse.FailWithMessage("文件大小不能超过 100MB", 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 - } - - filename := file.Filename - ext, err := extensionService.ImportExtension(userID, filename, fileData) - if err != nil { - global.GVA_LOG.Error("导入扩展失败", zap.Error(err)) - sysResponse.FailWithMessage("导入失败: "+err.Error(), c) - return - } - - sysResponse.OkWithData(response.ToExtensionResponse(ext), c) -} - -// ExportExtension 导出扩展 -// @Summary 导出扩展 -// @Description 导出扩展数据为 JSON -// @Tags 扩展管理 -// @Accept json -// @Produce json -// @Param id path int true "扩展ID" -// @Success 200 {object} response.ExtensionResponse -// @Router /app/extension/:id/export [get] -func (a *ExtensionApi) ExportExtension(c *gin.Context) { - userID := middleware.GetAppUserID(c) - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - sysResponse.FailWithMessage("无效的扩展ID", c) - return - } - extID := uint(id) - - exportData, err := extensionService.ExportExtension(userID, extID) - if err != nil { - global.GVA_LOG.Error("导出扩展失败", zap.Error(err)) - sysResponse.FailWithMessage("导出失败: "+err.Error(), c) - return - } - - sysResponse.OkWithData(exportData, c) -} - -// InstallFromUrl 从 URL 安装扩展 -// @Summary 从 URL 安装扩展 -// @Description 智能识别 Git URL 或 Manifest URL 安装扩展 -// @Tags 扩展管理 -// @Accept json -// @Produce json -// @Param data body request.InstallExtensionRequest true "安装参数" -// @Success 200 {object} response.Response -// @Router /app/extension/install/url [post] -func (a *ExtensionApi) InstallFromUrl(c *gin.Context) { - userID := middleware.GetAppUserID(c) - - var req request.InstallExtensionRequest - if err := c.ShouldBindJSON(&req); err != nil { - sysResponse.FailWithMessage(err.Error(), c) - return - } - - branch := req.Branch - if branch == "" { - branch = "main" - } - - // 智能识别 URL 类型并安装 - ext, err := extensionService.InstallExtensionFromURL(userID, req.URL, branch) - if err != nil { - global.GVA_LOG.Error("从 URL 安装扩展失败", zap.Error(err)) - sysResponse.FailWithMessage("安装失败: "+err.Error(), c) - return - } - - sysResponse.OkWithData(response.ToExtensionResponse(ext), c) -} - -// InstallFromGit 从 Git URL 安装扩展 -// @Summary 从 Git URL 安装扩展 -// @Description 从 Git 仓库克隆安装扩展 -// @Tags 扩展管理 -// @Accept json -// @Produce json -// @Param data body request.InstallExtensionRequest true "安装参数" -// @Success 200 {object} response.Response{data=response.ExtensionResponse} -// @Router /app/extension/install/git [post] -func (a *ExtensionApi) InstallFromGit(c *gin.Context) { - userID := middleware.GetAppUserID(c) - - var req request.InstallExtensionRequest - if err := c.ShouldBindJSON(&req); err != nil { - sysResponse.FailWithMessage(err.Error(), c) - return - } - - branch := req.Branch - if branch == "" { - branch = "main" - } - - // 执行 git clone 安装 - ext, err := extensionService.InstallExtensionFromGit(userID, req.URL, branch) - if err != nil { - global.GVA_LOG.Error("从 Git 安装扩展失败", zap.Error(err)) - sysResponse.FailWithMessage("安装失败: "+err.Error(), c) - return - } - - sysResponse.OkWithData(response.ToExtensionResponse(ext), c) -} - -// UpgradeExtension 升级扩展 -// @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/upgrade [post] -func (a *ExtensionApi) UpgradeExtension(c *gin.Context) { - userID := middleware.GetAppUserID(c) - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - sysResponse.FailWithMessage("无效的扩展ID", c) - return - } - extID := uint(id) - - ext, err := extensionService.UpgradeExtension(userID, extID) - if err != nil { - global.GVA_LOG.Error("升级扩展失败", zap.Error(err)) - sysResponse.FailWithMessage("升级失败: "+err.Error(), c) - return - } - - sysResponse.OkWithData(response.ToExtensionResponse(ext), c) -} - -// UpdateStats 更新扩展统计 -// @Summary 更新扩展统计 -// @Description 更新扩展的使用统计信息 -// @Tags 扩展管理 -// @Accept json -// @Produce json -// @Param id path int true "扩展ID" -// @Success 200 {object} response.Response -// @Router /app/extension/:id/stats [post] -func (a *ExtensionApi) UpdateStats(c *gin.Context) { - userID := middleware.GetAppUserID(c) - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - sysResponse.FailWithMessage("无效的扩展ID", c) - return - } - extID := uint(id) - - var req struct { - Action string `json:"action" binding:"required"` - Value int `json:"value"` - } - if err := c.ShouldBindJSON(&req); err != nil { - sysResponse.FailWithMessage(err.Error(), c) - return - } - - if req.Value == 0 { - req.Value = 1 - } - - if err := extensionService.UpdateExtensionStats(userID, extID, req.Action, req.Value); err != nil { - global.GVA_LOG.Error("更新统计失败", zap.Error(err)) - sysResponse.FailWithMessage("更新失败: "+err.Error(), c) - return - } - - sysResponse.OkWithMessage("统计已更新", c) -} diff --git a/server/config.yaml b/server/config.yaml index 3855218..0219a88 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -83,6 +83,7 @@ hua-wei-obs: endpoint: you-endpoint access-key: you-access-key secret-key: you-secret-key + use-ssl: false jwt: signing-key: 53d59b59-dba8-4f83-886e-e5bd1bf3cbda expires-time: 7d @@ -246,6 +247,7 @@ system: use-mongo: false use-strict-auth: false disable-auto-migrate: false + data-dir: data tencent-cos: bucket: xxxxx-10005608 region: ap-shanghai diff --git a/server/config/system.go b/server/config/system.go index 78977fa..4f09773 100644 --- a/server/config/system.go +++ b/server/config/system.go @@ -12,4 +12,5 @@ type System struct { UseMongo bool `mapstructure:"use-mongo" json:"use-mongo" yaml:"use-mongo"` // 使用mongo UseStrictAuth bool `mapstructure:"use-strict-auth" json:"use-strict-auth" yaml:"use-strict-auth"` // 使用树形角色分配模式 DisableAutoMigrate bool `mapstructure:"disable-auto-migrate" json:"disable-auto-migrate" yaml:"disable-auto-migrate"` // 自动迁移数据库表结构,生产环境建议设为false,手动迁移 + DataDir string `mapstructure:"data-dir" json:"data-dir" yaml:"data-dir"` // 数据目录 } diff --git a/server/initialize/gorm.go b/server/initialize/gorm.go index 675927f..644f900 100644 --- a/server/initialize/gorm.go +++ b/server/initialize/gorm.go @@ -94,7 +94,6 @@ func RegisterTables() { app.AIPreset{}, app.AIWorldInfo{}, app.AIUsageStat{}, - app.AIExtension{}, app.AIRegexScript{}, app.AICharacterRegexScript{}, ) diff --git a/server/initialize/router.go b/server/initialize/router.go index 2fd8d45..0672f0e 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -69,22 +69,20 @@ func Routers() *gin.Engine { appRouter := router.RouterGroupApp.App // 前台应用路由 // SillyTavern 核心脚本静态文件服务 - // 扩展通过 ES module 相对路径 import 引用这些核心模块(如 ../../../../../script.js → /script.js) // 所有核心文件存储在 data/st-core-scripts/ 下,完全独立于 web-app/ 目录 - // 扩展文件存储在 data/st-core-scripts/scripts/extensions/third-party/{name}/ 下 stCorePath := "data/st-core-scripts" if _, err := os.Stat(stCorePath); err == nil { Router.Static("/scripts", stCorePath+"/scripts") Router.Static("/css", stCorePath+"/css") Router.Static("/img", stCorePath+"/img") Router.Static("/webfonts", stCorePath+"/webfonts") - Router.Static("/lib", stCorePath+"/lib") // SillyTavern 扩展依赖的第三方库 + Router.Static("/lib", stCorePath+"/lib") // SillyTavern 依赖的第三方库 Router.Static("/locales", stCorePath+"/locales") // 国际化文件 Router.StaticFile("/script.js", stCorePath+"/script.js") // SillyTavern 主入口 Router.StaticFile("/lib.js", stCorePath+"/lib.js") // Webpack 编译后的 lib.js global.GVA_LOG.Info("SillyTavern 核心脚本服务已启动: " + stCorePath) } else { - global.GVA_LOG.Warn("SillyTavern 核心脚本目录不存在: " + stCorePath + ",扩展功能将不可用") + global.GVA_LOG.Warn("SillyTavern 核心脚本目录不存在: " + stCorePath) } // 管理后台前端静态文件(web) @@ -152,7 +150,6 @@ func Routers() *gin.Engine { 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/* appRouter.InitProviderRouter(appGroup) // AI提供商路由:/app/provider/* appRouter.InitChatRouter(appGroup) // 对话路由:/app/chat/* diff --git a/server/model/app/ai_extension.go b/server/model/app/ai_extension.go deleted file mode 100644 index 9e4694f..0000000 --- a/server/model/app/ai_extension.go +++ /dev/null @@ -1,67 +0,0 @@ -package app - -import ( - "git.echol.cn/loser/st/server/global" - "gorm.io/datatypes" -) - -// AIExtension 扩展(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(200);not null;index;comment:扩展名称(唯一标识)"` - DisplayName string `json:"displayName" gorm:"type:varchar(200);comment:扩展显示名称"` - Version string `json:"version" gorm:"type:varchar(50);default:'1.0.0';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(500);comment:主页链接"` - Repository string `json:"repository" gorm:"type:varchar(500);comment:仓库地址"` - License string `json:"license" gorm:"type:varchar(100);comment:许可证"` - - // 分类与标签 - ExtensionType string `json:"extensionType" gorm:"type:varchar(20);default:'ui';comment:扩展类型:ui,server,hybrid"` - Category string `json:"category" gorm:"type:varchar(50);comment:分类:utilities,themes,integrations,tools"` - Tags datatypes.JSON `json:"tags" gorm:"type:jsonb;comment:标签列表"` - - // 依赖管理 - Dependencies datatypes.JSON `json:"dependencies" gorm:"type:jsonb;comment:依赖扩展"` - Conflicts datatypes.JSON `json:"conflicts" gorm:"type:jsonb;comment:冲突扩展"` - - // 文件路径 - ScriptPath string `json:"scriptPath" gorm:"type:varchar(500);comment:脚本文件路径"` - StylePath string `json:"stylePath" gorm:"type:varchar(500);comment:样式文件路径"` - AssetPaths datatypes.JSON `json:"assetPaths" gorm:"type:jsonb;comment:资源文件路径列表"` - - // 配置与元数据 - ManifestData datatypes.JSON `json:"manifestData" gorm:"type:jsonb;comment:manifest 元数据"` - Settings datatypes.JSON `json:"settings" gorm:"type:jsonb;comment:扩展配置"` - Options datatypes.JSON `json:"options" gorm:"type:jsonb;comment:扩展选项"` - Metadata datatypes.JSON `json:"metadata" gorm:"type:jsonb;comment:额外元数据"` - - // 状态管理 - IsEnabled bool `json:"isEnabled" gorm:"default:false;comment:是否启用"` - IsInstalled bool `json:"isInstalled" gorm:"default:true;comment:是否已安装"` - IsSystemExt bool `json:"isSystemExt" gorm:"default:false;comment:是否系统扩展"` - - // 安装信息 - InstallSource string `json:"installSource" gorm:"type:varchar(50);comment:安装来源:url,git,file,marketplace"` - SourceURL string `json:"sourceUrl" gorm:"type:varchar(500);comment:源地址"` - Branch string `json:"branch" gorm:"type:varchar(100);default:'main';comment:Git 分支"` - AutoUpdate bool `json:"autoUpdate" gorm:"default:false;comment:是否自动更新"` - LastUpdateCheck *int64 `json:"lastUpdateCheck" gorm:"comment:最后检查更新时间戳"` - AvailableVersion string `json:"availableVersion" gorm:"type:varchar(50);comment:可用的新版本"` - InstallDate *int64 `json:"installDate" gorm:"comment:安装日期时间戳"` - LastEnabled *int64 `json:"lastEnabled" gorm:"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)"` -} - -func (AIExtension) TableName() string { - return "ai_extensions" -} diff --git a/server/model/app/request/extension.go b/server/model/app/request/extension.go deleted file mode 100644 index b99a0fa..0000000 --- a/server/model/app/request/extension.go +++ /dev/null @@ -1,83 +0,0 @@ -package request - -import ( - common "git.echol.cn/loser/st/server/model/common/request" -) - -// CreateExtensionRequest 创建扩展请求 -type CreateExtensionRequest struct { - Name string `json:"name" binding:"required"` - 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"` - 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"` - AssetPaths []string `json:"assetPaths"` - Settings map[string]interface{} `json:"settings"` - Options map[string]interface{} `json:"options"` - InstallSource string `json:"installSource"` - SourceURL string `json:"sourceUrl"` - Branch string `json:"branch"` - Metadata map[string]interface{} `json:"metadata"` -} - -// UpdateExtensionRequest 更新扩展请求 -type UpdateExtensionRequest struct { - DisplayName string `json:"displayName"` - Description string `json:"description"` - Version string `json:"version"` - Author string `json:"author"` - 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"` - AssetPaths []string `json:"assetPaths"` - Settings map[string]interface{} `json:"settings"` - Options map[string]interface{} `json:"options"` - Metadata map[string]interface{} `json:"metadata"` -} - -// ExtensionListRequest 扩展列表查询请求 -type ExtensionListRequest struct { - common.PageInfo - Keyword string `json:"keyword" form:"keyword"` // 搜索关键词 - 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"` // 标签 -} - -// ToggleExtensionRequest 启用/禁用扩展请求 -type ToggleExtensionRequest struct { - IsEnabled bool `json:"isEnabled"` -} - -// UpdateExtensionSettingsRequest 更新扩展设置请求 -type UpdateExtensionSettingsRequest struct { - Settings map[string]interface{} `json:"settings" binding:"required"` -} - -// InstallExtensionRequest 从URL安装扩展请求 -type InstallExtensionRequest struct { - URL string `json:"url" binding:"required"` - Branch string `json:"branch"` -} diff --git a/server/model/app/response/extension.go b/server/model/app/response/extension.go deleted file mode 100644 index 5d91b26..0000000 --- a/server/model/app/response/extension.go +++ /dev/null @@ -1,113 +0,0 @@ -package response - -import ( - "encoding/json" - - "git.echol.cn/loser/st/server/model/app" -) - -// 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"` - ScriptPath string `json:"scriptPath"` - StylePath string `json:"stylePath"` - AssetPaths []string `json:"assetPaths"` - ManifestData map[string]interface{} `json:"manifestData"` - Settings map[string]interface{} `json:"settings"` - Options map[string]interface{} `json:"options"` - Metadata map[string]interface{} `json:"metadata"` - IsEnabled bool `json:"isEnabled"` - IsInstalled bool `json:"isInstalled"` - IsSystemExt bool `json:"isSystemExt"` - InstallSource string `json:"installSource"` - SourceURL string `json:"sourceUrl"` - Branch string `json:"branch"` - AutoUpdate bool `json:"autoUpdate"` - LastUpdateCheck *int64 `json:"lastUpdateCheck"` - AvailableVersion string `json:"availableVersion"` - InstallDate *int64 `json:"installDate"` - LastEnabled *int64 `json:"lastEnabled"` - UsageCount int `json:"usageCount"` - ErrorCount int `json:"errorCount"` - LoadTime int `json:"loadTime"` - CreatedAt int64 `json:"createdAt"` - UpdatedAt int64 `json:"updatedAt"` -} - -// ExtensionListResponse 扩展列表响应 -type ExtensionListResponse struct { - List []ExtensionResponse `json:"list"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"pageSize"` -} - -// unmarshalJSONB 通用 JSONB 反序列化辅助函数 -func unmarshalJSONB[T any](data []byte, fallback T) T { - if len(data) == 0 { - return fallback - } - var result T - if err := json.Unmarshal(data, &result); err != nil { - return fallback - } - return result -} - -// ToExtensionResponse 将 AIExtension 转换为 ExtensionResponse -func ToExtensionResponse(ext *app.AIExtension) ExtensionResponse { - 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: unmarshalJSONB(ext.Tags, []string{}), - ExtensionType: ext.ExtensionType, - Category: ext.Category, - Dependencies: unmarshalJSONB(ext.Dependencies, map[string]string{}), - Conflicts: unmarshalJSONB(ext.Conflicts, []string{}), - ScriptPath: ext.ScriptPath, - StylePath: ext.StylePath, - AssetPaths: unmarshalJSONB(ext.AssetPaths, []string{}), - ManifestData: unmarshalJSONB(ext.ManifestData, map[string]interface{}{}), - Settings: unmarshalJSONB(ext.Settings, map[string]interface{}{}), - Options: unmarshalJSONB(ext.Options, map[string]interface{}{}), - Metadata: unmarshalJSONB(ext.Metadata, map[string]interface{}{}), - IsEnabled: ext.IsEnabled, - IsInstalled: ext.IsInstalled, - IsSystemExt: ext.IsSystemExt, - InstallSource: ext.InstallSource, - SourceURL: ext.SourceURL, - Branch: ext.Branch, - AutoUpdate: ext.AutoUpdate, - LastUpdateCheck: ext.LastUpdateCheck, - AvailableVersion: ext.AvailableVersion, - InstallDate: ext.InstallDate, - LastEnabled: ext.LastEnabled, - UsageCount: ext.UsageCount, - ErrorCount: ext.ErrorCount, - LoadTime: ext.LoadTime, - CreatedAt: ext.CreatedAt.Unix(), - UpdatedAt: ext.UpdatedAt.Unix(), - } -} diff --git a/server/router/app/enter.go b/server/router/app/enter.go index 310d834..007b1e7 100644 --- a/server/router/app/enter.go +++ b/server/router/app/enter.go @@ -4,7 +4,6 @@ type RouterGroup struct { AuthRouter CharacterRouter WorldInfoRouter - ExtensionRouter RegexScriptRouter ProviderRouter ChatRouter diff --git a/server/router/app/extension.go b/server/router/app/extension.go deleted file mode 100644 index 57863cf..0000000 --- a/server/router/app/extension.go +++ /dev/null @@ -1,33 +0,0 @@ -package app - -import ( - v1 "git.echol.cn/loser/st/server/api/v1" - "git.echol.cn/loser/st/server/middleware" - "github.com/gin-gonic/gin" -) - -type ExtensionRouter struct{} - -// InitExtensionRouter 初始化扩展路由 -func (r *ExtensionRouter) InitExtensionRouter(Router *gin.RouterGroup) { - extRouter := Router.Group("extension").Use(middleware.AppJWTAuth()) - extApi := v1.ApiGroupApp.AppApiGroup.ExtensionApi - { - extRouter.POST("", extApi.CreateExtension) // 创建扩展 - extRouter.PUT(":id", extApi.UpdateExtension) // 更新扩展 - extRouter.DELETE(":id", extApi.DeleteExtension) // 删除扩展 - extRouter.GET(":id", extApi.GetExtension) // 获取扩展详情 - extRouter.GET("", extApi.GetExtensionList) // 获取扩展列表 - extRouter.GET("enabled", extApi.GetEnabledExtensions) // 获取启用的扩展 - extRouter.POST(":id/toggle", extApi.ToggleExtension) // 启用/禁用扩展 - extRouter.GET(":id/settings", extApi.GetExtensionSettings) // 获取扩展设置 - extRouter.PUT(":id/settings", extApi.UpdateExtensionSettings) // 更新扩展设置 - extRouter.GET(":id/manifest", extApi.GetExtensionManifest) // 获取 manifest - extRouter.POST("install/url", extApi.InstallFromUrl) // 从 URL 安装 - extRouter.POST("install/git", extApi.InstallFromGit) // 从 Git URL 安装 - extRouter.POST(":id/upgrade", extApi.UpgradeExtension) // 升级扩展 - extRouter.POST("import", extApi.ImportExtension) // 文件导入(zip/文件夹) - extRouter.GET(":id/export", extApi.ExportExtension) // 导出扩展 - extRouter.POST(":id/stats", extApi.UpdateStats) // 更新统计 - } -} diff --git a/server/service/app/enter.go b/server/service/app/enter.go index a685eae..6ac734a 100644 --- a/server/service/app/enter.go +++ b/server/service/app/enter.go @@ -4,7 +4,6 @@ type AppServiceGroup struct { AuthService CharacterService WorldInfoService - ExtensionService RegexScriptService ProviderService ChatService diff --git a/server/service/app/extension.go b/server/service/app/extension.go deleted file mode 100644 index 9fe8046..0000000 --- a/server/service/app/extension.go +++ /dev/null @@ -1,410 +0,0 @@ -package app - -import ( - "encoding/json" - "errors" - "path" - "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 (s *ExtensionService) CreateExtension(userID uint, req *request.CreateExtensionRequest) (*app.AIExtension, error) { - // 检查扩展名是否重复 - var count int64 - global.GVA_DB.Model(&app.AIExtension{}).Where("user_id = ? AND name = ?", userID, req.Name).Count(&count) - if count > 0 { - return nil, errors.New("同名扩展已存在") - } - - now := time.Now().Unix() - ext := 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, - ExtensionType: req.ExtensionType, - Category: req.Category, - ScriptPath: req.ScriptPath, - StylePath: req.StylePath, - InstallSource: req.InstallSource, - SourceURL: req.SourceURL, - Branch: req.Branch, - IsInstalled: true, - IsEnabled: false, - InstallDate: &now, - } - - // 设置默认值 - if ext.Version == "" { - ext.Version = "1.0.0" - } - if ext.Branch == "" { - ext.Branch = "main" - } - - // 序列化 JSON 字段 - if req.Tags != nil { - ext.Tags = mustMarshal(req.Tags) - } - if req.Dependencies != nil { - ext.Dependencies = mustMarshal(req.Dependencies) - } - if req.Conflicts != nil { - ext.Conflicts = mustMarshal(req.Conflicts) - } - if req.AssetPaths != nil { - ext.AssetPaths = mustMarshal(req.AssetPaths) - } - if req.ManifestData != nil { - ext.ManifestData = mustMarshal(req.ManifestData) - } - if req.Settings != nil { - ext.Settings = mustMarshal(req.Settings) - } - if req.Options != nil { - ext.Options = mustMarshal(req.Options) - } - if req.Metadata != nil { - ext.Metadata = mustMarshal(req.Metadata) - } - - if err := global.GVA_DB.Create(&ext).Error; err != nil { - global.GVA_LOG.Error("创建扩展失败", zap.Error(err)) - return nil, err - } - - return &ext, nil -} - -// UpdateExtension 更新扩展 -func (s *ExtensionService) UpdateExtension(userID, extID uint, req *request.UpdateExtensionRequest) error { - var ext app.AIExtension - if err := global.GVA_DB.Where("id = ? AND user_id = ?", extID, userID).First(&ext).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("扩展不存在") - } - return err - } - - updates := make(map[string]interface{}) - - if req.DisplayName != "" { - updates["display_name"] = req.DisplayName - } - if req.Description != "" { - updates["description"] = req.Description - } - if req.Version != "" { - updates["version"] = req.Version - } - if req.Author != "" { - updates["author"] = req.Author - } - if req.Homepage != "" { - updates["homepage"] = req.Homepage - } - if req.Repository != "" { - updates["repository"] = req.Repository - } - if req.License != "" { - updates["license"] = req.License - } - if req.ExtensionType != "" { - updates["extension_type"] = req.ExtensionType - } - if req.Category != "" { - updates["category"] = req.Category - } - if req.ScriptPath != "" { - updates["script_path"] = req.ScriptPath - } - if req.StylePath != "" { - updates["style_path"] = req.StylePath - } - if req.Tags != nil { - updates["tags"] = datatypes.JSON(mustMarshal(req.Tags)) - } - if req.Dependencies != nil { - updates["dependencies"] = datatypes.JSON(mustMarshal(req.Dependencies)) - } - if req.Conflicts != nil { - updates["conflicts"] = datatypes.JSON(mustMarshal(req.Conflicts)) - } - if req.AssetPaths != nil { - updates["asset_paths"] = datatypes.JSON(mustMarshal(req.AssetPaths)) - } - if req.ManifestData != nil { - updates["manifest_data"] = datatypes.JSON(mustMarshal(req.ManifestData)) - } - if req.Settings != nil { - updates["settings"] = datatypes.JSON(mustMarshal(req.Settings)) - } - if req.Options != nil { - updates["options"] = datatypes.JSON(mustMarshal(req.Options)) - } - if req.Metadata != nil { - updates["metadata"] = datatypes.JSON(mustMarshal(req.Metadata)) - } - - if len(updates) == 0 { - return nil - } - - return global.GVA_DB.Model(&app.AIExtension{}).Where("id = ? AND user_id = ?", extID, userID).Updates(updates).Error -} - -// DeleteExtension 删除扩展 -func (s *ExtensionService) DeleteExtension(userID, extID uint) error { - result := global.GVA_DB.Where("id = ? AND user_id = ?", extID, userID).Delete(&app.AIExtension{}) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return errors.New("扩展不存在或无权删除") - } - return nil -} - -// GetExtension 获取扩展详情 -func (s *ExtensionService) GetExtension(userID, extID uint) (*app.AIExtension, error) { - var ext app.AIExtension - if err := global.GVA_DB.Where("id = ? AND user_id = ?", extID, userID).First(&ext).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("扩展不存在") - } - return nil, err - } - return &ext, nil -} - -// GetExtensionList 获取扩展列表 -func (s *ExtensionService) GetExtensionList(userID uint, req *request.ExtensionListRequest) ([]app.AIExtension, int64, error) { - var extensions []app.AIExtension - var total int64 - - db := global.GVA_DB.Model(&app.AIExtension{}).Where("user_id = ?", userID) - - // 关键词搜索 - if req.Keyword != "" { - keyword := "%" + req.Keyword + "%" - db = db.Where("(name ILIKE ? OR display_name ILIKE ? OR description ILIKE ?)", keyword, keyword, keyword) - } - - // 名称过滤 - if req.Name != "" { - db = db.Where("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 @> ?", datatypes.JSON(mustMarshal([]string{req.Tag}))) - } - - // 获取总数 - if err := db.Count(&total).Error; err != nil { - return nil, 0, err - } - - // 分页查询 - offset := (req.Page - 1) * req.PageSize - if err := db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&extensions).Error; err != nil { - return nil, 0, err - } - - return extensions, total, nil -} - -// GetEnabledExtensions 获取启用的扩展列表 -func (s *ExtensionService) GetEnabledExtensions(userID uint) ([]app.AIExtension, error) { - var extensions []app.AIExtension - if err := global.GVA_DB.Where("user_id = ? AND is_enabled = ? AND is_installed = ?", userID, true, true). - Order("created_at ASC").Find(&extensions).Error; err != nil { - return nil, err - } - return extensions, nil -} - -// ToggleExtension 启用/禁用扩展 -func (s *ExtensionService) ToggleExtension(userID, extID uint, isEnabled bool) error { - updates := map[string]interface{}{ - "is_enabled": isEnabled, - } - if isEnabled { - now := time.Now().Unix() - updates["last_enabled"] = &now - } - - result := global.GVA_DB.Model(&app.AIExtension{}).Where("id = ? AND user_id = ?", extID, userID).Updates(updates) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return errors.New("扩展不存在") - } - return nil -} - -// GetExtensionSettings 获取扩展设置 -func (s *ExtensionService) GetExtensionSettings(userID, extID uint) (map[string]interface{}, error) { - var ext app.AIExtension - if err := global.GVA_DB.Select("settings").Where("id = ? AND user_id = ?", extID, userID).First(&ext).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("扩展不存在") - } - return nil, err - } - - var settings map[string]interface{} - if len(ext.Settings) > 0 { - _ = json.Unmarshal(ext.Settings, &settings) - } - if settings == nil { - settings = make(map[string]interface{}) - } - return settings, nil -} - -// UpdateExtensionSettings 更新扩展设置 -func (s *ExtensionService) UpdateExtensionSettings(userID, extID uint, settings map[string]interface{}) error { - result := global.GVA_DB.Model(&app.AIExtension{}). - Where("id = ? AND user_id = ?", extID, userID). - Update("settings", datatypes.JSON(mustMarshal(settings))) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return errors.New("扩展不存在") - } - return nil -} - -// GetExtensionManifest 获取扩展 manifest -func (s *ExtensionService) GetExtensionManifest(userID, extID uint) (map[string]interface{}, error) { - var ext app.AIExtension - if err := global.GVA_DB.Select("manifest_data").Where("id = ? AND user_id = ?", extID, userID).First(&ext).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("扩展不存在") - } - return nil, err - } - - var manifest map[string]interface{} - if len(ext.ManifestData) > 0 { - _ = json.Unmarshal(ext.ManifestData, &manifest) - } - if manifest == nil { - manifest = make(map[string]interface{}) - } - return manifest, nil -} - -// UpdateExtensionStats 更新扩展统计信息 -func (s *ExtensionService) UpdateExtensionStats(userID, extID uint, action string, value int) error { - var updateExpr string - switch action { - case "usage": - updateExpr = "usage_count" - case "error": - updateExpr = "error_count" - case "load": - // load 直接设置加载时间(ms),不累加 - return global.GVA_DB.Model(&app.AIExtension{}). - Where("id = ? AND user_id = ?", extID, userID). - Update("load_time", value).Error - default: - return errors.New("无效的统计类型") - } - - return global.GVA_DB.Model(&app.AIExtension{}). - Where("id = ? AND user_id = ?", extID, userID). - Update(updateExpr, gorm.Expr(updateExpr+" + ?", value)).Error -} - -// ExportExtension 导出扩展数据 -func (s *ExtensionService) ExportExtension(userID, extID uint) (*response.ExtensionResponse, error) { - ext, err := s.GetExtension(userID, extID) - if err != nil { - return nil, err - } - resp := response.ToExtensionResponse(ext) - return &resp, nil -} - -// ImportExtension 从文件导入扩展(支持 zip 和 json) -func (s *ExtensionService) ImportExtension(userID uint, filename string, fileData []byte) (*app.AIExtension, error) { - ext := strings.ToLower(path.Ext(filename)) - - switch ext { - case ".json": - // JSON 文件:直接解析为 CreateExtensionRequest - var req request.CreateExtensionRequest - if err := json.Unmarshal(fileData, &req); err != nil { - return nil, errors.New("JSON 文件格式错误: " + err.Error()) - } - if req.Name == "" { - req.Name = strings.TrimSuffix(filename, path.Ext(filename)) - } - if req.ExtensionType == "" { - req.ExtensionType = "ui" - } - req.InstallSource = "file" - return s.CreateExtension(userID, &req) - - case ".zip": - // ZIP 文件:解压到扩展目录,解析 manifest.json,创建数据库记录 - return s.ImportExtensionFromZip(userID, filename, fileData) - - default: - return nil, errors.New("不支持的文件格式,请上传 .zip 或 .json 文件") - } -} - -// UpgradeExtension 升级扩展(从源地址重新安装) -func (s *ExtensionService) UpgradeExtension(userID, extID uint) (*app.AIExtension, error) { - return s.UpgradeExtensionFromSource(userID, extID) -} - -// mustMarshal JSON 序列化辅助函数 -func mustMarshal(v interface{}) []byte { - data, err := json.Marshal(v) - if err != nil { - return []byte("{}") - } - return data -} diff --git a/server/service/app/extension_installer.go b/server/service/app/extension_installer.go deleted file mode 100644 index a2d1ac1..0000000 --- a/server/service/app/extension_installer.go +++ /dev/null @@ -1,764 +0,0 @@ -package app - -import ( - "archive/zip" - "bytes" - "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" - "go.uber.org/zap" - "gorm.io/datatypes" -) - -// extensionsBaseDir 扩展文件存放根目录(与 router.go 中的静态服务路径一致) -const extensionsBaseDir = "data/st-core-scripts/scripts/extensions/third-party" - -// STManifest SillyTavern 扩展 manifest.json 结构 -type STManifest struct { - DisplayName string `json:"display_name"` - Loading string `json:"loading_order"` // 加载顺序 - Requires []string `json:"requires"` - Optional []string `json:"optional"` - Js string `json:"js"` // 入口 JS 文件 - Css string `json:"css"` // 入口 CSS 文件 - Author string `json:"author"` - Version string `json:"version"` - Homepages string `json:"homepages"` - Repository string `json:"repository"` - AutoUpdate bool `json:"auto_update"` - Description string `json:"description"` - Tags []string `json:"tags"` - Settings map[string]interface{} `json:"settings"` - Raw map[string]interface{} `json:"-"` // 原始 JSON 数据 -} - -// getExtensionDir 获取指定扩展的文件系统目录 -func getExtensionDir(extName string) string { - return filepath.Join(extensionsBaseDir, extName) -} - -// ensureExtensionsBaseDir 确保扩展基础目录存在 -func ensureExtensionsBaseDir() error { - return os.MkdirAll(extensionsBaseDir, 0755) -} - -// parseManifestFile 从扩展目录中读取并解析 manifest.json -func parseManifestFile(dir string) (*STManifest, error) { - manifestPath := filepath.Join(dir, "manifest.json") - data, err := os.ReadFile(manifestPath) - if err != nil { - return nil, fmt.Errorf("无法读取 manifest.json: %w", err) - } - - var manifest STManifest - if err := json.Unmarshal(data, &manifest); err != nil { - return nil, fmt.Errorf("解析 manifest.json 失败: %w", err) - } - - // 保留原始 JSON 用于存储到数据库 - var raw map[string]interface{} - if err := json.Unmarshal(data, &raw); err == nil { - manifest.Raw = raw - } - - return &manifest, nil -} - -// parseManifestBytes 从字节数组解析 manifest.json -func parseManifestBytes(data []byte) (*STManifest, error) { - var manifest STManifest - if err := json.Unmarshal(data, &manifest); err != nil { - return nil, fmt.Errorf("解析 manifest.json 失败: %w", err) - } - - var raw map[string]interface{} - if err := json.Unmarshal(data, &raw); err == nil { - manifest.Raw = raw - } - - return &manifest, nil -} - -// InstallExtensionFromGit 从 Git 仓库安装扩展 -func (s *ExtensionService) InstallExtensionFromGit(userID uint, gitURL string, branch string) (*app.AIExtension, error) { - if branch == "" { - branch = "main" - } - - if err := ensureExtensionsBaseDir(); err != nil { - return nil, fmt.Errorf("创建扩展目录失败: %w", err) - } - - // 从 URL 提取扩展名 - extName := extractRepoName(gitURL) - if extName == "" { - return nil, errors.New("无法从 URL 中提取扩展名") - } - - extDir := getExtensionDir(extName) - - // 检查目录是否已存在 - if _, err := os.Stat(extDir); err == nil { - return nil, fmt.Errorf("扩展 '%s' 已存在,请先删除或使用升级功能", extName) - } - - global.GVA_LOG.Info("从 Git 安装扩展", - zap.String("url", gitURL), - zap.String("branch", branch), - zap.String("dir", extDir), - ) - - // 执行 git clone - cmd := exec.Command("git", "clone", "--depth", "1", "--branch", branch, gitURL, extDir) - output, err := cmd.CombinedOutput() - if err != nil { - global.GVA_LOG.Error("git clone 失败", - zap.String("output", string(output)), - zap.Error(err), - ) - // 清理失败的目录 - _ = os.RemoveAll(extDir) - return nil, fmt.Errorf("git clone 失败: %s", strings.TrimSpace(string(output))) - } - - global.GVA_LOG.Info("git clone 成功", zap.String("name", extName)) - - // 如果扩展需要构建(有 package.json 的 build 脚本且 dist 不存在),执行构建 - if err := buildExtensionIfNeeded(extDir); err != nil { - global.GVA_LOG.Warn("扩展构建失败(不影响安装)", - zap.String("name", extName), - zap.Error(err), - ) - } - - // 创建数据库记录 - return s.createExtensionFromDir(userID, extDir, extName, "git", gitURL, branch) -} - -// InstallExtensionFromManifestURL 从 manifest URL 安装扩展 -func (s *ExtensionService) InstallExtensionFromManifestURL(userID uint, manifestURL string, branch string) (*app.AIExtension, error) { - if err := ensureExtensionsBaseDir(); err != nil { - return nil, fmt.Errorf("创建扩展目录失败: %w", err) - } - - global.GVA_LOG.Info("从 Manifest URL 安装扩展", zap.String("url", manifestURL)) - - // 下载 manifest.json - manifestData, err := httpGet(manifestURL) - if err != nil { - return nil, fmt.Errorf("下载 manifest.json 失败: %w", err) - } - - manifest, err := parseManifestBytes(manifestData) - if err != nil { - return nil, err - } - - // 确定扩展名 - extName := sanitizeName(manifest.DisplayName) - if extName == "" { - extName = extractNameFromURL(manifestURL) - } - if extName == "" { - return nil, errors.New("无法确定扩展名,manifest 中缺少 display_name") - } - - extDir := getExtensionDir(extName) - if _, err := os.Stat(extDir); err == nil { - return nil, fmt.Errorf("扩展 '%s' 已存在,请先删除或使用升级功能", extName) - } - - if err := os.MkdirAll(extDir, 0755); err != nil { - return nil, fmt.Errorf("创建扩展目录失败: %w", err) - } - - // 保存 manifest.json - if err := os.WriteFile(filepath.Join(extDir, "manifest.json"), manifestData, 0644); err != nil { - _ = os.RemoveAll(extDir) - return nil, fmt.Errorf("保存 manifest.json 失败: %w", err) - } - - // 获取 manifest URL 的基础路径 - baseURL := manifestURL[:strings.LastIndex(manifestURL, "/")+1] - - // 下载 JS 入口文件 - if manifest.Js != "" { - jsURL := baseURL + manifest.Js - jsData, err := httpGet(jsURL) - if err != nil { - global.GVA_LOG.Warn("下载 JS 文件失败", zap.String("url", jsURL), zap.Error(err)) - } else { - if err := os.WriteFile(filepath.Join(extDir, manifest.Js), jsData, 0644); err != nil { - global.GVA_LOG.Warn("保存 JS 文件失败", zap.Error(err)) - } - } - } - - // 下载 CSS 文件 - if manifest.Css != "" { - cssURL := baseURL + manifest.Css - cssData, err := httpGet(cssURL) - if err != nil { - global.GVA_LOG.Warn("下载 CSS 文件失败", zap.String("url", cssURL), zap.Error(err)) - } else { - if err := os.WriteFile(filepath.Join(extDir, manifest.Css), cssData, 0644); err != nil { - global.GVA_LOG.Warn("保存 CSS 文件失败", zap.Error(err)) - } - } - } - - // 创建数据库记录 - return s.createExtensionFromDir(userID, extDir, extName, "url", manifestURL, branch) -} - -// ImportExtensionFromZip 从 zip 文件导入扩展 -func (s *ExtensionService) ImportExtensionFromZip(userID uint, filename string, zipData []byte) (*app.AIExtension, error) { - if err := ensureExtensionsBaseDir(); err != nil { - return nil, fmt.Errorf("创建扩展目录失败: %w", err) - } - - // 先解压到临时目录 - tmpDir, err := os.MkdirTemp("", "ext-import-*") - if err != nil { - return nil, fmt.Errorf("创建临时目录失败: %w", err) - } - defer os.RemoveAll(tmpDir) - - // 解压 zip - if err := extractZip(zipData, tmpDir); err != nil { - return nil, fmt.Errorf("解压 zip 失败: %w", err) - } - - // 找到 manifest.json 所在目录(可能在根目录或子目录) - manifestDir, err := findManifestDir(tmpDir) - if err != nil { - return nil, err - } - - // 解析 manifest - manifest, err := parseManifestFile(manifestDir) - if err != nil { - return nil, err - } - - // 确定扩展名 - extName := sanitizeName(manifest.DisplayName) - if extName == "" { - extName = strings.TrimSuffix(filename, filepath.Ext(filename)) - } - - extDir := getExtensionDir(extName) - if _, err := os.Stat(extDir); err == nil { - return nil, fmt.Errorf("扩展 '%s' 已存在,请先删除或使用升级功能", extName) - } - - // 移动文件到目标目录 - if err := os.Rename(manifestDir, extDir); err != nil { - // 如果跨分区移动失败,回退为复制 - if err := copyDir(manifestDir, extDir); err != nil { - return nil, fmt.Errorf("移动扩展文件失败: %w", err) - } - } - - global.GVA_LOG.Info("ZIP 扩展导入成功", - zap.String("name", extName), - zap.String("dir", extDir), - ) - - return s.createExtensionFromDir(userID, extDir, extName, "file", "", "") -} - -// UpgradeExtensionFromSource 从源地址升级扩展 -func (s *ExtensionService) UpgradeExtensionFromSource(userID, extID uint) (*app.AIExtension, error) { - ext, err := s.GetExtension(userID, extID) - if err != nil { - return nil, err - } - - if ext.SourceURL == "" { - return nil, errors.New("该扩展没有源地址,无法升级") - } - - extDir := getExtensionDir(ext.Name) - - switch ext.InstallSource { - case "git": - // Git 扩展:执行 git pull - global.GVA_LOG.Info("从 Git 升级扩展", - zap.String("name", ext.Name), - zap.String("dir", extDir), - ) - - cmd := exec.Command("git", "-C", extDir, "pull", "--ff-only") - output, err := cmd.CombinedOutput() - if err != nil { - global.GVA_LOG.Error("git pull 失败", - zap.String("output", string(output)), - zap.Error(err), - ) - return nil, fmt.Errorf("git pull 失败: %s", strings.TrimSpace(string(output))) - } - - global.GVA_LOG.Info("git pull 成功", zap.String("output", string(output))) - - // 如果扩展需要构建,执行构建 - if err := buildExtensionIfNeeded(extDir); err != nil { - global.GVA_LOG.Warn("升级后扩展构建失败", - zap.String("name", ext.Name), - zap.Error(err), - ) - } - - case "url": - // URL 扩展:重新下载 manifest 和文件 - manifestData, err := httpGet(ext.SourceURL) - if err != nil { - return nil, fmt.Errorf("下载 manifest.json 失败: %w", err) - } - - manifest, err := parseManifestBytes(manifestData) - if err != nil { - return nil, err - } - - // 覆盖写入 manifest.json - if err := os.WriteFile(filepath.Join(extDir, "manifest.json"), manifestData, 0644); err != nil { - return nil, fmt.Errorf("保存 manifest.json 失败: %w", err) - } - - baseURL := ext.SourceURL[:strings.LastIndex(ext.SourceURL, "/")+1] - - // 重新下载 JS - if manifest.Js != "" { - if jsData, err := httpGet(baseURL + manifest.Js); err == nil { - _ = os.WriteFile(filepath.Join(extDir, manifest.Js), jsData, 0644) - } - } - // 重新下载 CSS - if manifest.Css != "" { - if cssData, err := httpGet(baseURL + manifest.Css); err == nil { - _ = os.WriteFile(filepath.Join(extDir, manifest.Css), cssData, 0644) - } - } - - default: - return nil, errors.New("该扩展的安装来源不支持升级") - } - - // 重新解析 manifest 并更新数据库 - manifest, _ := parseManifestFile(extDir) - if manifest != nil { - now := time.Now().Unix() - updates := map[string]interface{}{ - "last_update_check": &now, - } - if manifest.Version != "" { - updates["version"] = manifest.Version - } - if manifest.Description != "" { - updates["description"] = manifest.Description - } - if manifest.Author != "" { - updates["author"] = manifest.Author - } - if manifest.Js != "" { - updates["script_path"] = manifest.Js - } - if manifest.Css != "" { - updates["style_path"] = manifest.Css - } - if manifest.Raw != nil { - if raw, err := json.Marshal(manifest.Raw); err == nil { - updates["manifest_data"] = datatypes.JSON(raw) - } - } - global.GVA_DB.Model(&app.AIExtension{}).Where("id = ? AND user_id = ?", extID, userID).Updates(updates) - } - - return s.GetExtension(userID, extID) -} - -// InstallExtensionFromURL 智能安装:根据 URL 判断是 Git 仓库还是 Manifest URL -func (s *ExtensionService) InstallExtensionFromURL(userID uint, url string, branch string) (*app.AIExtension, error) { - if isGitURL(url) { - return s.InstallExtensionFromGit(userID, url, branch) - } - return s.InstallExtensionFromManifestURL(userID, url, branch) -} - -// --------------------- -// 辅助函数 -// --------------------- - -// createExtensionFromDir 从扩展目录创建数据库记录 -func (s *ExtensionService) createExtensionFromDir(userID uint, extDir, extName, installSource, sourceURL, branch string) (*app.AIExtension, error) { - manifest, err := parseManifestFile(extDir) - if err != nil { - // manifest 解析失败不阻止安装,使用基本信息 - global.GVA_LOG.Warn("解析 manifest 失败,使用基本信息", zap.Error(err)) - manifest = &STManifest{} - } - - now := time.Now().Unix() - - displayName := manifest.DisplayName - if displayName == "" { - displayName = extName - } - version := manifest.Version - if version == "" { - version = "1.0.0" - } - - ext := app.AIExtension{ - UserID: userID, - Name: extName, - DisplayName: displayName, - Version: version, - Author: manifest.Author, - Description: manifest.Description, - Homepage: manifest.Homepages, - Repository: manifest.Repository, - ExtensionType: "ui", - ScriptPath: manifest.Js, - StylePath: manifest.Css, - InstallSource: installSource, - SourceURL: sourceURL, - Branch: branch, - IsInstalled: true, - IsEnabled: false, - InstallDate: &now, - AutoUpdate: manifest.AutoUpdate, - } - - // 存储 manifest 原始数据 - if manifest.Raw != nil { - if raw, err := json.Marshal(manifest.Raw); err == nil { - ext.ManifestData = datatypes.JSON(raw) - } - } - - // 存储标签 - if len(manifest.Tags) > 0 { - if tags, err := json.Marshal(manifest.Tags); err == nil { - ext.Tags = datatypes.JSON(tags) - } - } - - // 存储默认设置 - if manifest.Settings != nil { - if settings, err := json.Marshal(manifest.Settings); err == nil { - ext.Settings = datatypes.JSON(settings) - } - } - - // 存储依赖 - if len(manifest.Requires) > 0 { - deps := make(map[string]string) - for _, r := range manifest.Requires { - deps[r] = "*" - } - if depsJSON, err := json.Marshal(deps); err == nil { - ext.Dependencies = datatypes.JSON(depsJSON) - } - } - - if err := global.GVA_DB.Create(&ext).Error; err != nil { - return nil, fmt.Errorf("创建扩展记录失败: %w", err) - } - - global.GVA_LOG.Info("扩展安装成功", - zap.String("name", extName), - zap.String("source", installSource), - zap.String("version", version), - ) - - return &ext, nil -} - -// buildExtensionIfNeeded 如果扩展目录中有 package.json 且包含 build 脚本, -// 而 manifest 中指定的入口 JS 文件不存在,则自动执行 npm/pnpm install && build -func buildExtensionIfNeeded(extDir string) error { - // 读取 manifest 获取入口文件路径 - manifest, err := parseManifestFile(extDir) - if err != nil || manifest.Js == "" { - return nil // 无 manifest 或无 JS 入口,不需要构建 - } - - // 检查入口 JS 文件是否存在 - jsPath := filepath.Join(extDir, manifest.Js) - if _, err := os.Stat(jsPath); err == nil { - return nil // 入口文件已存在,无需构建 - } - - // 检查 package.json 是否存在 - pkgPath := filepath.Join(extDir, "package.json") - pkgData, err := os.ReadFile(pkgPath) - if err != nil { - return nil // 无 package.json,不是需要构建的扩展 - } - - // 检查是否有 build 脚本 - var pkg struct { - Scripts map[string]string `json:"scripts"` - } - if err := json.Unmarshal(pkgData, &pkg); err != nil { - return nil - } - if _, hasBuild := pkg.Scripts["build"]; !hasBuild { - return nil // 没有 build 脚本 - } - - global.GVA_LOG.Info("扩展需要构建,开始安装依赖及构建", - zap.String("dir", extDir), - zap.String("entry", manifest.Js), - ) - - // 判断使用 pnpm 还是 npm - var pkgManager string - if _, err := os.Stat(filepath.Join(extDir, "pnpm-lock.yaml")); err == nil { - pkgManager = "pnpm" - } else if _, err := os.Stat(filepath.Join(extDir, "pnpm-workspace.yaml")); err == nil { - pkgManager = "pnpm" - } else { - pkgManager = "npm" - } - - // 确认包管理器可用 - if _, err := exec.LookPath(pkgManager); err != nil { - // 回退到 npm - pkgManager = "npm" - if _, err := exec.LookPath("npm"); err != nil { - return fmt.Errorf("未找到 npm 或 pnpm,无法构建扩展") - } - } - - global.GVA_LOG.Info("使用包管理器", zap.String("manager", pkgManager)) - - // 第一步:安装依赖 - installCmd := exec.Command(pkgManager, "install") - installCmd.Dir = extDir - installOutput, err := installCmd.CombinedOutput() - if err != nil { - global.GVA_LOG.Error("依赖安装失败", - zap.String("output", string(installOutput)), - zap.Error(err), - ) - return fmt.Errorf("%s install 失败: %s", pkgManager, strings.TrimSpace(string(installOutput))) - } - global.GVA_LOG.Info("依赖安装成功", zap.String("manager", pkgManager)) - - // 第二步:执行构建 - buildCmd := exec.Command(pkgManager, "run", "build") - buildCmd.Dir = extDir - buildOutput, err := buildCmd.CombinedOutput() - if err != nil { - global.GVA_LOG.Error("构建失败", - zap.String("output", string(buildOutput)), - zap.Error(err), - ) - return fmt.Errorf("%s run build 失败: %s", pkgManager, strings.TrimSpace(string(buildOutput))) - } - global.GVA_LOG.Info("扩展构建成功", zap.String("dir", extDir)) - - // 验证入口文件是否已生成 - if _, err := os.Stat(jsPath); err != nil { - return fmt.Errorf("构建完成但入口文件仍不存在: %s", manifest.Js) - } - - return nil -} - -// isGitURL 判断 URL 是否为 Git 仓库 -func isGitURL(url string) bool { - url = strings.ToLower(url) - if strings.HasSuffix(url, ".git") { - return true - } - if strings.Contains(url, "github.com/") || - strings.Contains(url, "gitlab.com/") || - strings.Contains(url, "gitee.com/") || - strings.Contains(url, "bitbucket.org/") { - // 排除以 .json 结尾的 URL - if strings.HasSuffix(url, ".json") { - return false - } - return true - } - return false -} - -// extractRepoName 从 Git URL 提取仓库名 -func extractRepoName(gitURL string) string { - gitURL = strings.TrimSuffix(gitURL, ".git") - gitURL = strings.TrimRight(gitURL, "/") - parts := strings.Split(gitURL, "/") - if len(parts) == 0 { - return "" - } - return parts[len(parts)-1] -} - -// extractNameFromURL 从 URL 路径中提取名称 -func extractNameFromURL(url string) string { - // 对于 manifest URL:https://example.com/extensions/my-ext/manifest.json - // 提取上一级目录名 - url = strings.TrimRight(url, "/") - parts := strings.Split(url, "/") - if len(parts) >= 2 { - filename := parts[len(parts)-1] - if strings.Contains(filename, "manifest") { - return parts[len(parts)-2] - } - } - return "" -} - -// sanitizeName 清理扩展名(移除不安全字符) -func sanitizeName(name string) string { - name = strings.TrimSpace(name) - // 将空格替换为连字符 - name = strings.ReplaceAll(name, " ", "-") - // 只保留字母、数字、连字符、下划线 - var result strings.Builder - for _, c := range name { - if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' { - result.WriteRune(c) - } - } - return result.String() -} - -// httpGet 发送 HTTP GET 请求 -func httpGet(url string) ([]byte, error) { - client := &http.Client{ - Timeout: 60 * time.Second, - } - resp, err := client.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status) - } - - return io.ReadAll(resp.Body) -} - -// extractZip 解压 zip 文件到指定目录 -func extractZip(zipData []byte, destDir string) error { - reader, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData))) - if err != nil { - return fmt.Errorf("打开 zip 文件失败: %w", err) - } - - for _, file := range reader.File { - // 安全检查:防止 zip slip 攻击 - destPath := filepath.Join(destDir, file.Name) - if !strings.HasPrefix(filepath.Clean(destPath), filepath.Clean(destDir)+string(os.PathSeparator)) { - return fmt.Errorf("非法的 zip 文件路径: %s", file.Name) - } - - if file.FileInfo().IsDir() { - if err := os.MkdirAll(destPath, 0755); err != nil { - return err - } - continue - } - - // 确保父目录存在 - if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { - return err - } - - rc, err := file.Open() - if err != nil { - return err - } - - outFile, err := os.Create(destPath) - if err != nil { - rc.Close() - return err - } - - _, err = io.Copy(outFile, rc) - outFile.Close() - rc.Close() - if err != nil { - return err - } - } - - return nil -} - -// findManifestDir 在解压的目录中查找 manifest.json 所在目录 -func findManifestDir(rootDir string) (string, error) { - // 先检查根目录 - if _, err := os.Stat(filepath.Join(rootDir, "manifest.json")); err == nil { - return rootDir, nil - } - - // 检查一级子目录(常见的 zip 结构是 zip 内包含一个项目文件夹) - entries, err := os.ReadDir(rootDir) - if err != nil { - return "", fmt.Errorf("读取目录失败: %w", err) - } - - for _, entry := range entries { - if entry.IsDir() { - subDir := filepath.Join(rootDir, entry.Name()) - if _, err := os.Stat(filepath.Join(subDir, "manifest.json")); err == nil { - return subDir, nil - } - } - } - - return "", errors.New("未找到 manifest.json,请确保 zip 文件包含有效的 SillyTavern 扩展") -} - -// copyDir 递归复制目录 -func copyDir(src, dst string) error { - if err := os.MkdirAll(dst, 0755); err != nil { - return err - } - - entries, err := os.ReadDir(src) - if err != nil { - return err - } - - for _, entry := range entries { - srcPath := filepath.Join(src, entry.Name()) - dstPath := filepath.Join(dst, entry.Name()) - - if entry.IsDir() { - if err := copyDir(srcPath, dstPath); err != nil { - return err - } - } else { - data, err := os.ReadFile(srcPath) - if err != nil { - return err - } - if err := os.WriteFile(dstPath, data, 0644); err != nil { - return err - } - } - } - - return nil -} diff --git a/web-app-vue/package-lock.json b/web-app-vue/package-lock.json index 915be3b..8ff0abf 100644 --- a/web-app-vue/package-lock.json +++ b/web-app-vue/package-lock.json @@ -8,10 +8,12 @@ "name": "web-app-vue", "version": "0.0.0", "dependencies": { + "@types/dompurify": "^3.0.5", "@types/lodash": "^4.17.23", "@types/uuid": "^10.0.0", "@vueuse/core": "^14.2.1", "axios": "^1.13.5", + "dompurify": "^3.3.1", "element-plus": "^2.13.2", "jquery": "^4.0.0", "lodash": "^4.17.23", @@ -1291,6 +1293,15 @@ "win32" ] }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1340,6 +1351,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -1751,6 +1768,15 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/web-app-vue/package.json b/web-app-vue/package.json index d060308..f773df8 100644 --- a/web-app-vue/package.json +++ b/web-app-vue/package.json @@ -9,10 +9,12 @@ "preview": "vite preview" }, "dependencies": { + "@types/dompurify": "^3.0.5", "@types/lodash": "^4.17.23", "@types/uuid": "^10.0.0", "@vueuse/core": "^14.2.1", "axios": "^1.13.5", + "dompurify": "^3.3.1", "element-plus": "^2.13.2", "jquery": "^4.0.0", "lodash": "^4.17.23", diff --git a/web-app-vue/src/api/extension.ts b/web-app-vue/src/api/extension.ts deleted file mode 100644 index 09cc36f..0000000 --- a/web-app-vue/src/api/extension.ts +++ /dev/null @@ -1,174 +0,0 @@ -import request from '@/utils/request' -import type { - Extension, - ExtensionListParams, - ExtensionListResponse, - CreateExtensionRequest, - UpdateExtensionRequest, - ToggleExtensionRequest, - UpdateExtensionSettingsRequest, - ExtensionStatsRequest, -} from '@/types/extension' - -// 创建扩展 -export function createExtension(data: CreateExtensionRequest) { - return request({ - url: '/app/extension', - method: 'post', - data, - }) -} - -// 更新扩展 -export function updateExtension(id: number, data: UpdateExtensionRequest) { - return request({ - url: `/app/extension/${id}`, - method: 'put', - data, - }) -} - -// 删除扩展 -export function deleteExtension(id: number, deleteFiles = false) { - return request({ - url: `/app/extension/${id}`, - method: 'delete', - params: deleteFiles ? { deleteFiles: true } : undefined, - }) -} - -// 获取扩展详情 -export function getExtension(id: number) { - return request({ - url: `/app/extension/${id}`, - method: 'get', - }) -} - -// 获取扩展列表 -export function getExtensionList(params?: ExtensionListParams) { - return request({ - url: '/app/extension', - method: 'get', - params, - }) -} - -// 获取启用的扩展列表 -export function getEnabledExtensions() { - return request({ - url: '/app/extension/enabled', - method: 'get', - }) -} - -// 启用/禁用扩展 -export function toggleExtension(id: number, data: ToggleExtensionRequest) { - return request({ - url: `/app/extension/${id}/toggle`, - method: 'post', - data, - }) -} - -// 获取扩展设置 -export function getExtensionSettings(id: number) { - return request>({ - url: `/app/extension/${id}/settings`, - method: 'get', - }) -} - -// 更新扩展设置 -export function updateExtensionSettings(id: number, data: UpdateExtensionSettingsRequest) { - return request({ - url: `/app/extension/${id}/settings`, - method: 'put', - data, - }) -} - -// 获取扩展 manifest -export function getExtensionManifest(id: number) { - return request>({ - url: `/app/extension/${id}/manifest`, - method: 'get', - }) -} - -// 从 URL 安装扩展(智能识别 Git URL 或 Manifest URL) -export function installExtensionFromUrl(url: string, branch = 'main') { - return request({ - url: '/app/extension/install/url', - method: 'post', - data: { url, branch }, - }) -} - -// 从 Git URL 安装扩展 -export function installExtensionFromGit(gitUrl: string, branch = 'main') { - return request({ - url: '/app/extension/install/git', - method: 'post', - data: { url: gitUrl, branch }, - }) -} - -// 升级扩展 -export function upgradeExtension(id: number, force = false) { - return request({ - url: `/app/extension/${id}/upgrade`, - method: 'post', - data: { force }, - }) -} - -// 导入扩展(上传 zip 压缩包或文件夹) -export function importExtension(file: File) { - const formData = new FormData() - formData.append('file', file) - - return request({ - url: '/app/extension/import', - method: 'post', - data: formData, - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) -} - -// 导出扩展 -export function exportExtension(id: number) { - return request({ - url: `/app/extension/${id}/export`, - method: 'get', - }) -} - -// 更新扩展统计 -export function updateExtensionStats(data: ExtensionStatsRequest) { - return request({ - url: `/app/extension/${data.extensionId}/stats`, - method: 'post', - data: { - action: data.action, - value: data.value || 1, - }, - }) -} - -// 下载扩展 JSON 文件 -export function downloadExtensionJSON(id: number, name: string) { - return exportExtension(id).then((data) => { - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }) - const url = URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = url - link.download = `${name || 'extension'}_${Date.now()}.json` - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(url) - }) -} diff --git a/web-app-vue/src/components.d.ts b/web-app-vue/src/components.d.ts index 6b2e3ce..2588bd1 100644 --- a/web-app-vue/src/components.d.ts +++ b/web-app-vue/src/components.d.ts @@ -59,7 +59,6 @@ declare module 'vue' { ElTag: typeof import('element-plus/es')['ElTag'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElUpload: typeof import('element-plus/es')['ElUpload'] - ExtensionDrawer: typeof import('./components/ExtensionDrawer.vue')['default'] HelloWorld: typeof import('./components/HelloWorld.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/web-app-vue/src/components/ExtensionDrawer.vue b/web-app-vue/src/components/ExtensionDrawer.vue deleted file mode 100644 index 3c39e87..0000000 --- a/web-app-vue/src/components/ExtensionDrawer.vue +++ /dev/null @@ -1,904 +0,0 @@ - - - - - diff --git a/web-app-vue/src/layouts/DefaultLayout.vue b/web-app-vue/src/layouts/DefaultLayout.vue index b4934d7..027663c 100644 --- a/web-app-vue/src/layouts/DefaultLayout.vue +++ b/web-app-vue/src/layouts/DefaultLayout.vue @@ -38,17 +38,6 @@ AI 配置 - - - - - 扩展 -
@@ -85,26 +74,19 @@ - - - - - diff --git a/web-app-vue/src/views/extension/ExtensionSettings.vue b/web-app-vue/src/views/extension/ExtensionSettings.vue deleted file mode 100644 index db439e8..0000000 --- a/web-app-vue/src/views/extension/ExtensionSettings.vue +++ /dev/null @@ -1,291 +0,0 @@ - - - - -