diff --git a/docs/扩展功能模块开发文档.md b/docs/扩展功能模块开发文档.md new file mode 100644 index 0000000..8ebb50c --- /dev/null +++ b/docs/扩展功能模块开发文档.md @@ -0,0 +1,95 @@ +1. 系统架构概述本系统采用前后端分离架构,旨在支持高度自定义的角色扮演体验。 +前端 (Vue 3):负责 UI 渲染、插件面板挂载、脚本沙箱执行及动态 HTML 处理。 +后端 (Go):处理业务逻辑、Prompt 组装、插件权限校验及静态资源分发。 +数据库 (PostgreSQL):存储用户数据、角色卡设定、对话历史及插件配置(JSONB)。 + +2. 插件分类定义系统支持以下两类插件,并区分 公共 (Public) 与 私人 (Private) 权限: +插件类型实现技术核心功能设定集 (Lorebook)后端注入 + 数据库检索关键词触发知识点注入 Prompt。 +功能扩展 (Extension)前端沙箱 (Iframe/JS)增加 UI 面板、导入脚本库、渲染自定义 HTML。 + +3. 数据库模型设计3.1 插件主表 (plugins) +```SQL +CREATE TABLE plugins ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL, + type VARCHAR(20) NOT NULL, -- 'lorebook' 或 'extension' + is_public BOOLEAN DEFAULT false, + owner_id UUID REFERENCES users(id), + manifest JSONB, -- 存储版本、作者、入口点等元数据 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` +3.2 设定条目表 (lore_items) +```SQL +CREATE TABLE lore_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + plugin_id UUID REFERENCES plugins(id) ON DELETE CASCADE, + keywords TEXT[], -- 触发词:["魔法", "火球"] + content TEXT NOT NULL, -- 注入内容 + priority INT DEFAULT 10 -- 插入顺序 +); +``` +3.3 用户配置表 (user_plugin_configs) +```SQL +CREATE TABLE user_plugin_configs ( + user_id UUID REFERENCES users(id), + plugin_id UUID REFERENCES plugins(id), + is_enabled BOOLEAN DEFAULT true, + settings JSONB, -- 存储插件的个性化设置 + PRIMARY KEY (user_id, plugin_id) +); +``` + +4. 后端开发:设定集注入逻辑 (Go) +4.1 Prompt 组装流程后端在向 LLM 发送请求前,需执行以下伪逻辑: +分词匹配:提取用户最新输入及其上下文,比对数据库中的 keywords。 +内容提取:获取命中条目的 content。 +模板拼接: +```Plaintext +{{System_Prompt}} +[相关设定注入区] +{{Injected_Lore_Content}} +---------------- +{{Chat_History}} +{{User_Input}} +``` + +5. 前端开发:扩展功能实现 (Vue 3) +5.1 插件面板挂载参考 src/Panel.vue 的设计,利用动态组件加载插件配置:功能:读取插件的 manifest.settings_schema。交互:通过 Vue 响应式表单实时预览设置效果,并保存至后端 JSONB 字段。 +5.2 脚本沙箱与 HTML 渲染参考你提供的 Iframe.vue 逻辑,为“酒馆助手”类插件提供运行环境:A. 沙箱初始化使用不可见的 + + +
+ 该插件没有独立设置面板。 +
+ + + + +3. 后端支持 (Go):静态资源与配置下发你的 Go 后端需要能够提供插件目录下的文件访问。Go// Go 伪代码:静态文件分发 +r.GET("/plugins/files/:plugin_id/*filepath", func(c *gin.Context) { + pluginID := c.Param("plugin_id") + filePath := c.Param("filepath") + + // 从数据库查询该插件的物理路径 + basePath := db.GetPluginPath(pluginID) + + // 返回对应的 HTML/JS/CSS + c.File(filepath.Join(basePath, filePath)) +}) +4. 针对 LittleWhiteBox 的适配建议通过分析 LittleWhiteBox 的代码,它包含了大量的模块(如 modules/tts, modules/novel-draw)。这些模块通常通过 index.js 加载。如果你要完美支持它,需要解决以下两个关键点:CSS 样式映射:LittleWhiteBox 的样式可能依赖原版酒馆的 CSS 类名(如 .menu_button, .panel)。解决方案:在你的 Vue 项目中,为插件 iframe 注入一个包含“酒馆兼容层样式”的 CSS 文件,模拟原版的 UI 环境。事件监听 (Event Bus):LittleWhiteBox 里的很多功能(如 modules/event-manager.js)是基于事件触发的。解决方案:在你的 PluginContext 中实现一个简单的事件中心,当你的 Vue 界面发生“消息发送”、“收到回复”等动作时,通过 postMessage 通知 Iframe 内的插件。5. 文档补全:插件设置渲染规范步骤开发者操作系统行为检测后端扫描 manifest.json发现 settings_file: "settings.html"。入口Vue 点击插件列表中的“设置”图标在右侧弹出 Drawer 或 Modal,挂载 PluginSettingsWrapper。握手Iframe 加载完成后执行注入将当前用户的 PgSQL 配置注入插件内存。持久化插件内点击保存插件调用 SillyTavern.saveSettings -> Vue 捕获消息 -> Go 写入 PgSQL。 \ No newline at end of file diff --git a/server/api/v1/app/enter.go b/server/api/v1/app/enter.go index a588a64..6355f24 100644 --- a/server/api/v1/app/enter.go +++ b/server/api/v1/app/enter.go @@ -17,4 +17,5 @@ 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 index fb46fa3..2fe3d9e 100644 --- a/server/api/v1/app/extension.go +++ b/server/api/v1/app/extension.go @@ -1,12 +1,7 @@ package app import ( - "fmt" "io" - "mime" - "net/http" - "os" - "path/filepath" "strconv" "git.echol.cn/loser/st/server/global" @@ -23,9 +18,9 @@ type ExtensionApi struct{} var extensionService = service.ServiceGroupApp.AppServiceGroup.ExtensionService -// CreateExtension 创建/安装扩展 -// @Summary 创建/安装扩展 -// @Description 创建一个新的扩展或安装扩展 +// CreateExtension 创建扩展 +// @Summary 创建扩展 +// @Description 创建一个新的扩展 // @Tags 扩展管理 // @Accept json // @Produce json @@ -41,14 +36,14 @@ func (a *ExtensionApi) CreateExtension(c *gin.Context) { return } - extension, err := extensionService.CreateExtension(userID, &req) + 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(extension), c) + sysResponse.OkWithData(response.ToExtensionResponse(ext), c) } // UpdateExtension 更新扩展 @@ -64,14 +59,13 @@ func (a *ExtensionApi) CreateExtension(c *gin.Context) { 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) + extID := uint(id) var req request.UpdateExtensionRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -79,7 +73,7 @@ func (a *ExtensionApi) UpdateExtension(c *gin.Context) { return } - if err := extensionService.UpdateExtension(userID, extensionID, &req); err != nil { + if err := extensionService.UpdateExtension(userID, extID, &req); err != nil { global.GVA_LOG.Error("更新扩展失败", zap.Error(err)) sysResponse.FailWithMessage("更新失败: "+err.Error(), c) return @@ -88,30 +82,27 @@ func (a *ExtensionApi) UpdateExtension(c *gin.Context) { sysResponse.OkWithMessage("更新成功", c) } -// DeleteExtension 删除/卸载扩展 -// @Summary 删除/卸载扩展 -// @Description 删除扩展 +// 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" + extID := uint(id) - if err := extensionService.DeleteExtension(userID, extensionID, deleteFiles); err != nil { + if err := extensionService.DeleteExtension(userID, extID); err != nil { global.GVA_LOG.Error("删除扩展失败", zap.Error(err)) sysResponse.FailWithMessage("删除失败: "+err.Error(), c) return @@ -132,23 +123,22 @@ func (a *ExtensionApi) DeleteExtension(c *gin.Context) { 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) + extID := uint(id) - extension, err := extensionService.GetExtension(userID, extensionID) + 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(extension), c) + sysResponse.OkWithData(response.ToExtensionResponse(ext), c) } // GetExtensionList 获取扩展列表 @@ -157,9 +147,14 @@ func (a *ExtensionApi) GetExtension(c *gin.Context) { // @Tags 扩展管理 // @Accept json // @Produce json -// @Param data query request.ExtensionListRequest true "查询参数" +// @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/list [get] +// @Router /app/extension [get] func (a *ExtensionApi) GetExtensionList(c *gin.Context) { userID := middleware.GetAppUserID(c) @@ -177,14 +172,50 @@ func (a *ExtensionApi) GetExtensionList(c *gin.Context) { req.PageSize = 20 } - result, err := extensionService.GetExtensionList(userID, &req) + extensions, total, 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) + 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 启用/禁用扩展 @@ -200,96 +231,56 @@ func (a *ExtensionApi) GetExtensionList(c *gin.Context) { 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) + extID := 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) + if err := extensionService.ToggleExtension(userID, extID, req.IsEnabled); err != nil { + global.GVA_LOG.Error("切换扩展状态失败", zap.Error(err)) + sysResponse.FailWithMessage("操作失败: "+err.Error(), c) return } - sysResponse.OkWithMessage("更新成功", c) + msg := "已禁用" + if req.IsEnabled { + msg = "已启用" + } + sysResponse.OkWithMessage(msg, c) } -// GetExtensionSettings 获取扩展配置 -// @Summary 获取扩展配置 -// @Description 获取扩展的用户配置 +// GetExtensionSettings 获取扩展设置 +// @Summary 获取扩展设置 +// @Description 获取扩展的个性化设置 // @Tags 扩展管理 // @Accept json // @Produce json // @Param id path int true "扩展ID" -// @Success 200 {object} response.Response{data=map[string]interface{}} +// @Success 200 {object} response.Response // @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) + extID := uint(id) - settings, err := extensionService.GetExtensionSettings(userID, extensionID) + settings, err := extensionService.GetExtensionSettings(userID, extID) if err != nil { - global.GVA_LOG.Error("获取扩展配置失败", zap.Error(err)) + global.GVA_LOG.Error("获取扩展设置失败", zap.Error(err)) sysResponse.FailWithMessage("获取失败: "+err.Error(), c) return } @@ -297,28 +288,63 @@ func (a *ExtensionApi) GetExtensionSettings(c *gin.Context) { sysResponse.OkWithData(settings, c) } -// GetExtensionManifest 获取扩展 manifest -// @Summary 获取扩展 manifest -// @Description 获取扩展的 manifest.json +// UpdateExtensionSettings 更新扩展设置 +// @Summary 更新扩展设置 +// @Description 更新扩展的个性化设置 // @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) { +// @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) + extID := uint(id) - manifest, err := extensionService.GetExtensionManifest(userID, extensionID) + 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) @@ -330,11 +356,11 @@ func (a *ExtensionApi) GetExtensionManifest(c *gin.Context) { // ImportExtension 导入扩展 // @Summary 导入扩展 -// @Description 从文件导入扩展 +// @Description 从 ZIP 压缩包或 JSON 文件导入扩展 // @Tags 扩展管理 // @Accept multipart/form-data // @Produce json -// @Param file formData file true "扩展文件(manifest.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) { @@ -343,13 +369,13 @@ func (a *ExtensionApi) ImportExtension(c *gin.Context) { // 获取文件 file, err := c.FormFile("file") if err != nil { - sysResponse.FailWithMessage("请上传扩展文件", c) + sysResponse.FailWithMessage("请上传扩展文件(支持 .zip 或 .json)", c) return } - // 文件大小限制(5MB) - if file.Size > 5<<20 { - sysResponse.FailWithMessage("文件大小不能超过 5MB", c) + // 文件大小限制(100MB,zip 包可能较大) + if file.Size > 100<<20 { + sysResponse.FailWithMessage("文件大小不能超过 100MB", c) return } @@ -369,269 +395,183 @@ func (a *ExtensionApi) ImportExtension(c *gin.Context) { return } - // 导入扩展 - extension, err := extensionService.ImportExtension(userID, fileData) + 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(extension), c) + sysResponse.OkWithData(response.ToExtensionResponse(ext), c) } // ExportExtension 导出扩展 // @Summary 导出扩展 -// @Description 导出扩展为 manifest.json 文件 +// @Description 导出扩展数据为 JSON // @Tags 扩展管理 // @Accept json -// @Produce application/json +// @Produce json // @Param id path int true "扩展ID" -// @Success 200 {object} response.ExtensionManifestResponse +// @Success 200 {object} response.ExtensionResponse // @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) + extID := uint(id) - exportData, err := extensionService.ExportExtension(userID, extensionID) + exportData, err := extensionService.ExportExtension(userID, extID) 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) + sysResponse.OkWithData(exportData, c) } -// UpdateExtensionStats 更新扩展统计 -// @Summary 更新扩展统计 -// @Description 更新扩展的使用统计 +// InstallFromUrl 从 URL 安装扩展 +// @Summary 从 URL 安装扩展 +// @Description 智能识别 Git URL 或 Manifest URL 安装扩展 // @Tags 扩展管理 // @Accept json // @Produce json -// @Param data body request.ExtensionStatsRequest true "统计信息" +// @Param data body request.InstallExtensionRequest true "安装参数" // @Success 200 {object} response.Response -// @Router /app/extension/stats [post] -func (a *ExtensionApi) UpdateExtensionStats(c *gin.Context) { +// @Router /app/extension/install/url [post] +func (a *ExtensionApi) InstallFromUrl(c *gin.Context) { userID := middleware.GetAppUserID(c) - var req request.ExtensionStatsRequest + var req request.InstallExtensionRequest 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 + branch := req.Branch + if branch == "" { + branch = "main" } - 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) + // 智能识别 URL 类型并安装 + ext, err := extensionService.InstallExtensionFromURL(userID, req.URL, branch) 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)) + global.GVA_LOG.Error("从 URL 安装扩展失败", zap.Error(err)) sysResponse.FailWithMessage("安装失败: "+err.Error(), c) return } - sysResponse.OkWithData(response.ToExtensionResponse(extension), c) + sysResponse.OkWithData(response.ToExtensionResponse(ext), 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 安装扩展 +// InstallFromGit 从 Git URL 安装扩展 // @Summary 从 Git URL 安装扩展 -// @Description 从 Git 仓库 URL 克隆并安装扩展 +// @Description 从 Git 仓库克隆安装扩展 // @Tags 扩展管理 // @Accept json // @Produce json -// @Param data body request.InstallExtensionFromGitRequest true "Git URL 信息" +// @Param data body request.InstallExtensionRequest true "安装参数" // @Success 200 {object} response.Response{data=response.ExtensionResponse} // @Router /app/extension/install/git [post] -func (a *ExtensionApi) InstallExtensionFromGit(c *gin.Context) { +func (a *ExtensionApi) InstallFromGit(c *gin.Context) { userID := middleware.GetAppUserID(c) - var req request.InstallExtensionFromGitRequest + var req request.InstallExtensionRequest if err := c.ShouldBindJSON(&req); err != nil { sysResponse.FailWithMessage(err.Error(), c) return } - // 设置默认分支 - if req.Branch == "" { - req.Branch = "main" + branch := req.Branch + if branch == "" { + branch = "main" } - extension, err := extensionService.InstallExtensionFromGit(userID, req.GitUrl, req.Branch) + // 执行 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(extension), c) + sysResponse.OkWithData(response.ToExtensionResponse(ext), c) } -// ProxyExtensionAsset 获取扩展资源文件(从本地文件系统读取) -// @Summary 获取扩展资源文件 -// @Description 从本地存储读取扩展的 JS/CSS 等资源文件(与原版 SillyTavern 一致,扩展文件存储在本地) +// UpgradeExtension 升级扩展 +// @Summary 升级扩展 +// @Description 从源地址重新安装以升级扩展 // @Tags 扩展管理 -// @Produce octet-stream +// @Accept json +// @Produce json // @Param id path int true "扩展ID" -// @Param path path string true "资源文件路径" -// @Success 200 {file} binary -// @Router /app/extension/:id/asset/*path [get] -func (a *ExtensionApi) ProxyExtensionAsset(c *gin.Context) { +// @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 } - extensionID := uint(id) + extID := uint(id) - // 获取资源路径(去掉前导 /) - assetPath := c.Param("path") - if len(assetPath) > 0 && assetPath[0] == '/' { - assetPath = assetPath[1:] - } - if assetPath == "" { - sysResponse.FailWithMessage("资源路径不能为空", c) - return - } - - // 通过扩展 ID 查库获取信息(公开路由,不做 userID 过滤) - extInfo, err := extensionService.GetExtensionByID(extensionID) + ext, err := extensionService.UpgradeExtension(userID, extID) if err != nil { - sysResponse.FailWithMessage("扩展不存在", c) + global.GVA_LOG.Error("升级扩展失败", zap.Error(err)) + sysResponse.FailWithMessage("升级失败: "+err.Error(), c) return } - // 从本地文件系统读取资源 - localPath, err := extensionService.GetExtensionAssetLocalPath(extInfo.Name, assetPath) - if err != nil { - global.GVA_LOG.Error("获取扩展资源失败", - zap.Error(err), - zap.String("name", extInfo.Name), - zap.String("asset", assetPath)) - sysResponse.FailWithMessage("资源不存在: "+err.Error(), c) - return - } - - // 读取文件内容 - data, err := os.ReadFile(localPath) - if err != nil { - global.GVA_LOG.Error("读取扩展资源文件失败", zap.Error(err), zap.String("path", localPath)) - sysResponse.FailWithMessage("资源读取失败", c) - return - } - - // 根据文件扩展名设置正确的 Content-Type - fileExt := filepath.Ext(assetPath) - contentType := mime.TypeByExtension(fileExt) - if contentType == "" { - contentType = "application/octet-stream" - } - - // 设置缓存和 CORS 头 - c.Header("Content-Type", contentType) - c.Header("Cache-Control", "public, max-age=3600") - c.Header("Access-Control-Allow-Origin", "*") - - c.Data(http.StatusOK, contentType, data) + 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/model/app/ai_extension.go b/server/model/app/ai_extension.go index 93fe050..9e4694f 100644 --- a/server/model/app/ai_extension.go +++ b/server/model/app/ai_extension.go @@ -3,148 +3,65 @@ package app import ( "git.echol.cn/loser/st/server/global" "gorm.io/datatypes" - "time" ) -// AIExtension 扩展表 (兼容 SillyTavern Extension 规范) +// 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(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:标签列表"` + UserID uint `json:"userId" gorm:"not null;index;comment:所属用户ID"` + User *AppUser `json:"user" gorm:"foreignKey:UserID"` - // 扩展类型和功能 - 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)"` + // 基础信息 + 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:许可证"` - // 依赖关系 - Dependencies datatypes.JSON `json:"dependencies" gorm:"type:jsonb;comment:依赖的其他扩展"` - Conflicts datatypes.JSON `json:"conflicts" gorm:"type:jsonb;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:标签列表"` - // 扩展文件 - 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:资源文件路径列表"` + // 依赖管理 + Dependencies datatypes.JSON `json:"dependencies" gorm:"type:jsonb;comment:依赖扩展"` + Conflicts datatypes.JSON `json:"conflicts" gorm:"type:jsonb;comment:冲突扩展"` - // 扩展配置 - Settings datatypes.JSON `json:"settings" gorm:"type:jsonb;comment:扩展配置项"` - Options datatypes.JSON `json:"options" 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:资源文件路径列表"` - // 状态 - 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:最后启用时间"` + // 配置与元数据 + 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:额外元数据"` - // 更新相关 - AutoUpdate bool `json:"autoUpdate" gorm:"default:false;comment:是否自动更新"` - LastUpdateCheck *time.Time `json:"lastUpdateCheck" gorm:"comment:最后检查更新时间"` - AvailableVersion string `json:"availableVersion" gorm:"type:varchar(50);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)"` - - // 元数据 - Metadata datatypes.JSON `json:"metadata" gorm:"type:jsonb;comment:扩展元数据"` + LoadTime int `json:"loadTime" gorm:"default:0;comment:加载时间(ms)"` } func (AIExtension) TableName() string { return "ai_extensions" } - -// AIExtensionManifest 扩展清单结构 (对应 manifest.json,兼容 SillyTavern 格式) -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"` - HomePage string `json:"homePage,omitempty"` // SillyTavern 兼容(驼峰命名) - 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"` // 主入口文件 - Js string `json:"js,omitempty"` // SillyTavern 兼容: JS 入口文件 - Style string `json:"style,omitempty"` // 样式文件 - Css string `json:"css,omitempty"` // SillyTavern 兼容: CSS 样式文件 - 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 兼容) - Requires []string `json:"requires,omitempty"` // SillyTavern 兼容: 依赖列表 - Optional []string `json:"optional,omitempty"` // SillyTavern 兼容: 可选依赖 - LoadingOrder int `json:"loading_order,omitempty"` // SillyTavern 兼容: 加载顺序 - I18n map[string]string `json:"i18n,omitempty"` // SillyTavern 兼容: 国际化文件 -} - -// GetEffectiveName 获取有效名称,兼容 SillyTavern manifest 没有 name 字段的情况 -func (m *AIExtensionManifest) GetEffectiveName() string { - if m.Name != "" { - return m.Name - } - if m.DisplayName != "" { - return m.DisplayName - } - return "" -} - -// GetEffectiveHomepage 获取有效主页地址 -func (m *AIExtensionManifest) GetEffectiveHomepage() string { - if m.Homepage != "" { - return m.Homepage - } - return m.HomePage -} - -// GetEffectiveEntry 获取有效的 JS 入口文件路径 -func (m *AIExtensionManifest) GetEffectiveEntry() string { - if m.Entry != "" { - return m.Entry - } - return m.Js -} - -// GetEffectiveStyle 获取有效的 CSS 样式文件路径 -func (m *AIExtensionManifest) GetEffectiveStyle() string { - if m.Style != "" { - return m.Style - } - return m.Css -} - -// 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" -// } diff --git a/server/model/app/request/extension.go b/server/model/app/request/extension.go index 04c196a..b99a0fa 100644 --- a/server/model/app/request/extension.go +++ b/server/model/app/request/extension.go @@ -4,9 +4,9 @@ import ( common "git.echol.cn/loser/st/server/model/common/request" ) -// CreateExtensionRequest 创建/安装扩展请求 +// CreateExtensionRequest 创建扩展请求 type CreateExtensionRequest struct { - Name string `json:"name" binding:"required,min=1,max=500"` + Name string `json:"name" binding:"required"` DisplayName string `json:"displayName"` Version string `json:"version"` Author string `json:"author"` @@ -15,93 +15,69 @@ type CreateExtensionRequest struct { Repository string `json:"repository"` License string `json:"license"` Tags []string `json:"tags"` - ExtensionType string `json:"extensionType" binding:"required,oneof=ui server hybrid"` + 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" binding:"required"` + ManifestData map[string]interface{} `json:"manifestData"` ScriptPath string `json:"scriptPath"` StylePath string `json:"stylePath"` - AssetsPaths []string `json:"assetsPaths"` + AssetPaths []string `json:"assetPaths"` 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"` // 是否自动更新 + 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 - Name string `json:"name" form:"name"` // 扩展名称(模糊搜索) + 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"` // 标签过滤 -} - -// 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"` // 是否删除文件 + Tag string `json:"tag" form:"tag"` // 标签 } // ToggleExtensionRequest 启用/禁用扩展请求 type ToggleExtensionRequest struct { - IsEnabled *bool `json:"isEnabled"` // 使用指针类型,允许 false 值 + IsEnabled bool `json:"isEnabled"` } -// UpdateExtensionSettingsRequest 更新扩展配置请求 +// 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"` +// 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 index 4f52969..5d91b26 100644 --- a/server/model/app/response/extension.go +++ b/server/model/app/response/extension.go @@ -2,8 +2,8 @@ package response import ( "encoding/json" + "git.echol.cn/loser/st/server/model/app" - "time" ) // ExtensionResponse 扩展响应 @@ -23,12 +23,13 @@ type ExtensionResponse struct { 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"` + 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"` @@ -36,16 +37,15 @@ type ExtensionResponse struct { SourceURL string `json:"sourceUrl"` Branch string `json:"branch"` AutoUpdate bool `json:"autoUpdate"` - InstallDate time.Time `json:"installDate"` - LastEnabled time.Time `json:"lastEnabled"` - LastUpdateCheck *time.Time `json:"lastUpdateCheck"` + 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"` - Metadata map[string]interface{} `json:"metadata"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` } // ExtensionListResponse 扩展列表响应 @@ -56,105 +56,20 @@ type ExtensionListResponse struct { 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"` +// 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 } -// 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 转换为扩展响应 +// ToExtensionResponse 将 AIExtension 转换为 ExtensionResponse 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, @@ -166,17 +81,18 @@ func ToExtensionResponse(ext *app.AIExtension) ExtensionResponse { Homepage: ext.Homepage, Repository: ext.Repository, License: ext.License, - Tags: tags, + Tags: unmarshalJSONB(ext.Tags, []string{}), ExtensionType: ext.ExtensionType, Category: ext.Category, - Dependencies: dependencies, - Conflicts: conflicts, - ManifestData: manifestData, + Dependencies: unmarshalJSONB(ext.Dependencies, map[string]string{}), + Conflicts: unmarshalJSONB(ext.Conflicts, []string{}), ScriptPath: ext.ScriptPath, StylePath: ext.StylePath, - AssetsPaths: assetsPaths, - Settings: settings, - Options: options, + 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, @@ -184,15 +100,14 @@ func ToExtensionResponse(ext *app.AIExtension) ExtensionResponse { SourceURL: ext.SourceURL, Branch: ext.Branch, AutoUpdate: ext.AutoUpdate, - InstallDate: ext.InstallDate, - LastEnabled: ext.LastEnabled, LastUpdateCheck: ext.LastUpdateCheck, AvailableVersion: ext.AvailableVersion, + InstallDate: ext.InstallDate, + LastEnabled: ext.LastEnabled, UsageCount: ext.UsageCount, ErrorCount: ext.ErrorCount, LoadTime: ext.LoadTime, - Metadata: metadata, - CreatedAt: ext.CreatedAt, - UpdatedAt: ext.UpdatedAt, + CreatedAt: ext.CreatedAt.Unix(), + UpdatedAt: ext.UpdatedAt.Unix(), } } diff --git a/server/router/app/extension.go b/server/router/app/extension.go index 1c00fba..57863cf 100644 --- a/server/router/app/extension.go +++ b/server/router/app/extension.go @@ -1,55 +1,33 @@ package app import ( - "git.echol.cn/loser/st/server/api/v1" + 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) { - extensionRouter := Router.Group("extension").Use(middleware.AppJWTAuth()) - extensionApi := v1.ApiGroupApp.AppApiGroup.ExtensionApi - + extRouter := Router.Group("extension").Use(middleware.AppJWTAuth()) + extApi := 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) // 更新扩展统计 - } - - // 扩展资源文件 - 公开路由(不需要鉴权) - // 原因: diff --git a/web-app-vue/src/views/extension/ExtensionList.vue b/web-app-vue/src/views/extension/ExtensionList.vue deleted file mode 100644 index 4c799ab..0000000 --- a/web-app-vue/src/views/extension/ExtensionList.vue +++ /dev/null @@ -1,522 +0,0 @@ - - - - - diff --git a/web-app-vue/src/views/extension/ExtensionListNew.vue b/web-app-vue/src/views/extension/ExtensionListNew.vue index a5ba419..1f88459 100644 --- a/web-app-vue/src/views/extension/ExtensionListNew.vue +++ b/web-app-vue/src/views/extension/ExtensionListNew.vue @@ -1,349 +1,187 @@