🎨 移除扩展模块相关代码
This commit is contained in:
@@ -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 中声明
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"` // 数据目录
|
||||
}
|
||||
|
||||
@@ -94,7 +94,6 @@ func RegisterTables() {
|
||||
app.AIPreset{},
|
||||
app.AIWorldInfo{},
|
||||
app.AIUsageStat{},
|
||||
app.AIExtension{},
|
||||
app.AIRegexScript{},
|
||||
app.AICharacterRegexScript{},
|
||||
)
|
||||
|
||||
@@ -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/*
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ type RouterGroup struct {
|
||||
AuthRouter
|
||||
CharacterRouter
|
||||
WorldInfoRouter
|
||||
ExtensionRouter
|
||||
RegexScriptRouter
|
||||
ProviderRouter
|
||||
ChatRouter
|
||||
|
||||
@@ -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) // 更新统计
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ type AppServiceGroup struct {
|
||||
AuthService
|
||||
CharacterService
|
||||
WorldInfoService
|
||||
ExtensionService
|
||||
RegexScriptService
|
||||
ProviderService
|
||||
ChatService
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user