Files
st/server/api/v1/app/extension.go

638 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package app
import (
"fmt"
"io"
"mime"
"net/http"
"os"
"path/filepath"
"strconv"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/middleware"
"git.echol.cn/loser/st/server/model/app/request"
"git.echol.cn/loser/st/server/model/app/response"
sysResponse "git.echol.cn/loser/st/server/model/common/response"
"git.echol.cn/loser/st/server/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type ExtensionApi struct{}
var extensionService = service.ServiceGroupApp.AppServiceGroup.ExtensionService
// CreateExtension 创建/安装扩展
// @Summary 创建/安装扩展
// @Description 创建一个新的扩展或安装扩展
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param data body request.CreateExtensionRequest true "扩展信息"
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
// @Router /app/extension [post]
func (a *ExtensionApi) CreateExtension(c *gin.Context) {
userID := middleware.GetAppUserID(c)
var req request.CreateExtensionRequest
if err := c.ShouldBindJSON(&req); err != nil {
sysResponse.FailWithMessage(err.Error(), c)
return
}
extension, err := extensionService.CreateExtension(userID, &req)
if err != nil {
global.GVA_LOG.Error("创建扩展失败", zap.Error(err))
sysResponse.FailWithMessage("创建失败: "+err.Error(), c)
return
}
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
}
// UpdateExtension 更新扩展
// @Summary 更新扩展
// @Description 更新扩展信息
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param id path int true "扩展ID"
// @Param data body request.UpdateExtensionRequest true "扩展信息"
// @Success 200 {object} response.Response
// @Router /app/extension/:id [put]
func (a *ExtensionApi) UpdateExtension(c *gin.Context) {
userID := middleware.GetAppUserID(c)
// 从路径参数获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
sysResponse.FailWithMessage("无效的扩展ID", c)
return
}
extensionID := uint(id)
var req request.UpdateExtensionRequest
if err := c.ShouldBindJSON(&req); err != nil {
sysResponse.FailWithMessage(err.Error(), c)
return
}
if err := extensionService.UpdateExtension(userID, extensionID, &req); err != nil {
global.GVA_LOG.Error("更新扩展失败", zap.Error(err))
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
return
}
sysResponse.OkWithMessage("更新成功", c)
}
// DeleteExtension 删除/卸载扩展
// @Summary 删除/卸载扩展
// @Description 删除扩展
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param id path int true "扩展ID"
// @Param deleteFiles query bool false "是否删除文件"
// @Success 200 {object} response.Response
// @Router /app/extension/:id [delete]
func (a *ExtensionApi) DeleteExtension(c *gin.Context) {
userID := middleware.GetAppUserID(c)
// 从路径参数获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
sysResponse.FailWithMessage("无效的扩展ID", c)
return
}
extensionID := uint(id)
deleteFiles := c.Query("deleteFiles") == "true"
if err := extensionService.DeleteExtension(userID, extensionID, deleteFiles); err != nil {
global.GVA_LOG.Error("删除扩展失败", zap.Error(err))
sysResponse.FailWithMessage("删除失败: "+err.Error(), c)
return
}
sysResponse.OkWithMessage("删除成功", c)
}
// GetExtension 获取扩展详情
// @Summary 获取扩展详情
// @Description 获取扩展详细信息
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param id path int true "扩展ID"
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
// @Router /app/extension/:id [get]
func (a *ExtensionApi) GetExtension(c *gin.Context) {
userID := middleware.GetAppUserID(c)
// 从路径参数获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
sysResponse.FailWithMessage("无效的扩展ID", c)
return
}
extensionID := uint(id)
extension, err := extensionService.GetExtension(userID, extensionID)
if err != nil {
global.GVA_LOG.Error("获取扩展失败", zap.Error(err))
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
return
}
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
}
// GetExtensionList 获取扩展列表
// @Summary 获取扩展列表
// @Description 获取扩展列表
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param data query request.ExtensionListRequest true "查询参数"
// @Success 200 {object} response.Response{data=response.ExtensionListResponse}
// @Router /app/extension/list [get]
func (a *ExtensionApi) GetExtensionList(c *gin.Context) {
userID := middleware.GetAppUserID(c)
var req request.ExtensionListRequest
if err := c.ShouldBindQuery(&req); err != nil {
sysResponse.FailWithMessage(err.Error(), c)
return
}
// 默认分页
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 {
req.PageSize = 20
}
result, err := extensionService.GetExtensionList(userID, &req)
if err != nil {
global.GVA_LOG.Error("获取扩展列表失败", zap.Error(err))
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
return
}
sysResponse.OkWithData(result, c)
}
// ToggleExtension 启用/禁用扩展
// @Summary 启用/禁用扩展
// @Description 切换扩展的启用状态
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param id path int true "扩展ID"
// @Param data body request.ToggleExtensionRequest true "启用状态"
// @Success 200 {object} response.Response
// @Router /app/extension/:id/toggle [post]
func (a *ExtensionApi) ToggleExtension(c *gin.Context) {
userID := middleware.GetAppUserID(c)
// 从路径参数获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
sysResponse.FailWithMessage("无效的扩展ID", c)
return
}
extensionID := uint(id)
var req request.ToggleExtensionRequest
if err := c.ShouldBindJSON(&req); err != nil {
sysResponse.FailWithMessage("请求参数错误", c)
return
}
if req.IsEnabled == nil {
sysResponse.FailWithMessage("isEnabled 参数不能为空", c)
return
}
if err := extensionService.ToggleExtension(userID, extensionID, *req.IsEnabled); err != nil {
global.GVA_LOG.Error("切换扩展状态失败", zap.Error(err))
sysResponse.FailWithMessage("操作失败: "+err.Error(), c)
return
}
sysResponse.OkWithMessage("操作成功", c)
}
// UpdateExtensionSettings 更新扩展配置
// @Summary 更新扩展配置
// @Description 更新扩展的用户配置
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param id path int true "扩展ID"
// @Param data body request.UpdateExtensionSettingsRequest true "配置信息"
// @Success 200 {object} response.Response
// @Router /app/extension/:id/settings [put]
func (a *ExtensionApi) UpdateExtensionSettings(c *gin.Context) {
userID := middleware.GetAppUserID(c)
// 从路径参数获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
sysResponse.FailWithMessage("无效的扩展ID", c)
return
}
extensionID := uint(id)
var req request.UpdateExtensionSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
sysResponse.FailWithMessage(err.Error(), c)
return
}
if err := extensionService.UpdateExtensionSettings(userID, extensionID, req.Settings); err != nil {
global.GVA_LOG.Error("更新扩展配置失败", zap.Error(err))
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
return
}
sysResponse.OkWithMessage("更新成功", c)
}
// GetExtensionSettings 获取扩展配置
// @Summary 获取扩展配置
// @Description 获取扩展的用户配置
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param id path int true "扩展ID"
// @Success 200 {object} response.Response{data=map[string]interface{}}
// @Router /app/extension/:id/settings [get]
func (a *ExtensionApi) GetExtensionSettings(c *gin.Context) {
userID := middleware.GetAppUserID(c)
// 从路径参数获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
sysResponse.FailWithMessage("无效的扩展ID", c)
return
}
extensionID := uint(id)
settings, err := extensionService.GetExtensionSettings(userID, extensionID)
if err != nil {
global.GVA_LOG.Error("获取扩展配置失败", zap.Error(err))
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
return
}
sysResponse.OkWithData(settings, c)
}
// GetExtensionManifest 获取扩展 manifest
// @Summary 获取扩展 manifest
// @Description 获取扩展的 manifest.json
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param id path int true "扩展ID"
// @Success 200 {object} response.Response{data=response.ExtensionManifestResponse}
// @Router /app/extension/:id/manifest [get]
func (a *ExtensionApi) GetExtensionManifest(c *gin.Context) {
userID := middleware.GetAppUserID(c)
// 从路径参数获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
sysResponse.FailWithMessage("无效的扩展ID", c)
return
}
extensionID := uint(id)
manifest, err := extensionService.GetExtensionManifest(userID, extensionID)
if err != nil {
global.GVA_LOG.Error("获取扩展 manifest 失败", zap.Error(err))
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
return
}
sysResponse.OkWithData(manifest, c)
}
// ImportExtension 导入扩展
// @Summary 导入扩展
// @Description 从文件导入扩展
// @Tags 扩展管理
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "扩展文件manifest.json"
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
// @Router /app/extension/import [post]
func (a *ExtensionApi) ImportExtension(c *gin.Context) {
userID := middleware.GetAppUserID(c)
// 获取文件
file, err := c.FormFile("file")
if err != nil {
sysResponse.FailWithMessage("请上传扩展文件", c)
return
}
// 文件大小限制5MB
if file.Size > 5<<20 {
sysResponse.FailWithMessage("文件大小不能超过 5MB", c)
return
}
// 读取文件内容
src, err := file.Open()
if err != nil {
global.GVA_LOG.Error("打开文件失败", zap.Error(err))
sysResponse.FailWithMessage("文件读取失败", c)
return
}
defer src.Close()
fileData, err := io.ReadAll(src)
if err != nil {
global.GVA_LOG.Error("读取文件内容失败", zap.Error(err))
sysResponse.FailWithMessage("文件读取失败", c)
return
}
// 导入扩展
extension, err := extensionService.ImportExtension(userID, fileData)
if err != nil {
global.GVA_LOG.Error("导入扩展失败", zap.Error(err))
sysResponse.FailWithMessage("导入失败: "+err.Error(), c)
return
}
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
}
// ExportExtension 导出扩展
// @Summary 导出扩展
// @Description 导出扩展为 manifest.json 文件
// @Tags 扩展管理
// @Accept json
// @Produce application/json
// @Param id path int true "扩展ID"
// @Success 200 {object} response.ExtensionManifestResponse
// @Router /app/extension/:id/export [get]
func (a *ExtensionApi) ExportExtension(c *gin.Context) {
userID := middleware.GetAppUserID(c)
// 从路径参数获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
sysResponse.FailWithMessage("无效的扩展ID", c)
return
}
extensionID := uint(id)
exportData, err := extensionService.ExportExtension(userID, extensionID)
if err != nil {
global.GVA_LOG.Error("导出扩展失败", zap.Error(err))
sysResponse.FailWithMessage("导出失败: "+err.Error(), c)
return
}
// 设置响应头
filename := fmt.Sprintf("extension_%d_manifest.json", extensionID)
c.Header("Content-Type", "application/json")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
// 直接返回 JSON 数据
c.Data(http.StatusOK, "application/json", exportData)
}
// UpdateExtensionStats 更新扩展统计
// @Summary 更新扩展统计
// @Description 更新扩展的使用统计
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param data body request.ExtensionStatsRequest true "统计信息"
// @Success 200 {object} response.Response
// @Router /app/extension/stats [post]
func (a *ExtensionApi) UpdateExtensionStats(c *gin.Context) {
userID := middleware.GetAppUserID(c)
var req request.ExtensionStatsRequest
if err := c.ShouldBindJSON(&req); err != nil {
sysResponse.FailWithMessage(err.Error(), c)
return
}
if err := extensionService.UpdateExtensionStats(userID, req.ExtensionID, req.Action, req.Value); err != nil {
global.GVA_LOG.Error("更新扩展统计失败", zap.Error(err))
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
return
}
sysResponse.OkWithMessage("更新成功", c)
}
// GetEnabledExtensions 获取启用的扩展列表
// @Summary 获取启用的扩展列表
// @Description 获取用户启用的所有扩展(用于前端加载)
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Success 200 {object} response.Response{data=[]response.ExtensionResponse}
// @Router /app/extension/enabled [get]
func (a *ExtensionApi) GetEnabledExtensions(c *gin.Context) {
userID := middleware.GetAppUserID(c)
extensions, err := extensionService.GetEnabledExtensions(userID)
if err != nil {
global.GVA_LOG.Error("获取启用扩展列表失败", zap.Error(err))
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
return
}
sysResponse.OkWithData(extensions, c)
}
// InstallExtensionFromURL 智能安装扩展(自动识别 Git URL 或 Manifest URL
// @Summary 智能安装扩展
// @Description 自动识别 Git 仓库 URL 或 Manifest.json URL 并安装扩展(兼容 SillyTavern
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param data body request.InstallExtensionFromURLRequest true "安装 URL 信息"
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
// @Router /app/extension/install/url [post]
func (a *ExtensionApi) InstallExtensionFromURL(c *gin.Context) {
userID := middleware.GetAppUserID(c)
var req request.InstallExtensionFromURLRequest
if err := c.ShouldBindJSON(&req); err != nil {
sysResponse.FailWithMessage("请求参数错误: "+err.Error(), c)
return
}
// 设置默认分支
if req.Branch == "" {
req.Branch = "main"
}
extension, err := extensionService.InstallExtensionFromURL(userID, req.URL, req.Branch)
if err != nil {
global.GVA_LOG.Error("从 URL 安装扩展失败", zap.Error(err), zap.String("url", req.URL))
sysResponse.FailWithMessage("安装失败: "+err.Error(), c)
return
}
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
}
// UpgradeExtension 升级扩展版本
// @Summary 升级扩展版本
// @Description 根据扩展的安装来源自动选择更新方式Git pull 或重新下载)
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param id path int true "扩展ID"
// @Param data body request.UpdateExtensionRequest false "更新选项"
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
// @Router /app/extension/:id/update [post]
func (a *ExtensionApi) UpgradeExtension(c *gin.Context) {
userID := middleware.GetAppUserID(c)
// 从路径参数获取 ID
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
sysResponse.FailWithMessage("无效的扩展ID", c)
return
}
extensionID := uint(id)
var req request.UpdateExtensionRequest
// 允许不传 body使用默认值
_ = c.ShouldBindJSON(&req)
extension, err := extensionService.UpgradeExtension(userID, extensionID, req.Force)
if err != nil {
global.GVA_LOG.Error("升级扩展失败", zap.Error(err), zap.Uint("extensionID", extensionID))
sysResponse.FailWithMessage("升级失败: "+err.Error(), c)
return
}
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
}
// InstallExtensionFromGit 从 Git URL 安装扩展
// @Summary 从 Git URL 安装扩展
// @Description 从 Git 仓库 URL 克隆并安装扩展
// @Tags 扩展管理
// @Accept json
// @Produce json
// @Param data body request.InstallExtensionFromGitRequest true "Git URL 信息"
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
// @Router /app/extension/install/git [post]
func (a *ExtensionApi) InstallExtensionFromGit(c *gin.Context) {
userID := middleware.GetAppUserID(c)
var req request.InstallExtensionFromGitRequest
if err := c.ShouldBindJSON(&req); err != nil {
sysResponse.FailWithMessage(err.Error(), c)
return
}
// 设置默认分支
if req.Branch == "" {
req.Branch = "main"
}
extension, err := extensionService.InstallExtensionFromGit(userID, req.GitUrl, req.Branch)
if err != nil {
global.GVA_LOG.Error("从 Git 安装扩展失败", zap.Error(err))
sysResponse.FailWithMessage("安装失败: "+err.Error(), c)
return
}
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
}
// ProxyExtensionAsset 获取扩展资源文件(从本地文件系统读取)
// @Summary 获取扩展资源文件
// @Description 从本地存储读取扩展的 JS/CSS 等资源文件(与原版 SillyTavern 一致,扩展文件存储在本地)
// @Tags 扩展管理
// @Produce octet-stream
// @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) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
sysResponse.FailWithMessage("无效的扩展ID", c)
return
}
extensionID := 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)
if err != nil {
sysResponse.FailWithMessage("扩展不存在", 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)
}