🎨 移除扩展模块相关代码

This commit is contained in:
2026-02-24 13:38:29 +08:00
parent 0f9c9c9b9c
commit 0ebe197cc1
28 changed files with 39 additions and 4996 deletions

View File

@@ -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 中声明
)

View File

@@ -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
}
// 文件大小限制100MBzip 包可能较大)
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)
}

View File

@@ -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

View File

@@ -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"` // 数据目录
}

View File

@@ -94,7 +94,6 @@ func RegisterTables() {
app.AIPreset{},
app.AIWorldInfo{},
app.AIUsageStat{},
app.AIExtension{},
app.AIRegexScript{},
app.AICharacterRegexScript{},
)

View File

@@ -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/*

View File

@@ -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"
}

View File

@@ -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"`
}

View File

@@ -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(),
}
}

View File

@@ -4,7 +4,6 @@ type RouterGroup struct {
AuthRouter
CharacterRouter
WorldInfoRouter
ExtensionRouter
RegexScriptRouter
ProviderRouter
ChatRouter

View File

@@ -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) // 更新统计
}
}

View File

@@ -4,7 +4,6 @@ type AppServiceGroup struct {
AuthService
CharacterService
WorldInfoService
ExtensionService
RegexScriptService
ProviderService
ChatService

View File

@@ -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
}

View File

@@ -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 URLhttps://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
}

View File

@@ -8,10 +8,12 @@
"name": "web-app-vue",
"version": "0.0.0",
"dependencies": {
"@types/dompurify": "^3.0.5",
"@types/lodash": "^4.17.23",
"@types/uuid": "^10.0.0",
"@vueuse/core": "^14.2.1",
"axios": "^1.13.5",
"dompurify": "^3.3.1",
"element-plus": "^2.13.2",
"jquery": "^4.0.0",
"lodash": "^4.17.23",
@@ -1291,6 +1293,15 @@
"win32"
]
},
"node_modules/@types/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"license": "MIT",
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1340,6 +1351,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
@@ -1751,6 +1768,15 @@
"node": ">=8"
}
},
"node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View File

@@ -9,10 +9,12 @@
"preview": "vite preview"
},
"dependencies": {
"@types/dompurify": "^3.0.5",
"@types/lodash": "^4.17.23",
"@types/uuid": "^10.0.0",
"@vueuse/core": "^14.2.1",
"axios": "^1.13.5",
"dompurify": "^3.3.1",
"element-plus": "^2.13.2",
"jquery": "^4.0.0",
"lodash": "^4.17.23",

View File

@@ -1,174 +0,0 @@
import request from '@/utils/request'
import type {
Extension,
ExtensionListParams,
ExtensionListResponse,
CreateExtensionRequest,
UpdateExtensionRequest,
ToggleExtensionRequest,
UpdateExtensionSettingsRequest,
ExtensionStatsRequest,
} from '@/types/extension'
// 创建扩展
export function createExtension(data: CreateExtensionRequest) {
return request<Extension>({
url: '/app/extension',
method: 'post',
data,
})
}
// 更新扩展
export function updateExtension(id: number, data: UpdateExtensionRequest) {
return request({
url: `/app/extension/${id}`,
method: 'put',
data,
})
}
// 删除扩展
export function deleteExtension(id: number, deleteFiles = false) {
return request({
url: `/app/extension/${id}`,
method: 'delete',
params: deleteFiles ? { deleteFiles: true } : undefined,
})
}
// 获取扩展详情
export function getExtension(id: number) {
return request<Extension>({
url: `/app/extension/${id}`,
method: 'get',
})
}
// 获取扩展列表
export function getExtensionList(params?: ExtensionListParams) {
return request<ExtensionListResponse>({
url: '/app/extension',
method: 'get',
params,
})
}
// 获取启用的扩展列表
export function getEnabledExtensions() {
return request<Extension[]>({
url: '/app/extension/enabled',
method: 'get',
})
}
// 启用/禁用扩展
export function toggleExtension(id: number, data: ToggleExtensionRequest) {
return request({
url: `/app/extension/${id}/toggle`,
method: 'post',
data,
})
}
// 获取扩展设置
export function getExtensionSettings(id: number) {
return request<Record<string, any>>({
url: `/app/extension/${id}/settings`,
method: 'get',
})
}
// 更新扩展设置
export function updateExtensionSettings(id: number, data: UpdateExtensionSettingsRequest) {
return request({
url: `/app/extension/${id}/settings`,
method: 'put',
data,
})
}
// 获取扩展 manifest
export function getExtensionManifest(id: number) {
return request<Record<string, any>>({
url: `/app/extension/${id}/manifest`,
method: 'get',
})
}
// 从 URL 安装扩展(智能识别 Git URL 或 Manifest URL
export function installExtensionFromUrl(url: string, branch = 'main') {
return request<Extension>({
url: '/app/extension/install/url',
method: 'post',
data: { url, branch },
})
}
// 从 Git URL 安装扩展
export function installExtensionFromGit(gitUrl: string, branch = 'main') {
return request<Extension>({
url: '/app/extension/install/git',
method: 'post',
data: { url: gitUrl, branch },
})
}
// 升级扩展
export function upgradeExtension(id: number, force = false) {
return request<Extension>({
url: `/app/extension/${id}/upgrade`,
method: 'post',
data: { force },
})
}
// 导入扩展(上传 zip 压缩包或文件夹)
export function importExtension(file: File) {
const formData = new FormData()
formData.append('file', file)
return request<Extension>({
url: '/app/extension/import',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
})
}
// 导出扩展
export function exportExtension(id: number) {
return request<Extension>({
url: `/app/extension/${id}/export`,
method: 'get',
})
}
// 更新扩展统计
export function updateExtensionStats(data: ExtensionStatsRequest) {
return request({
url: `/app/extension/${data.extensionId}/stats`,
method: 'post',
data: {
action: data.action,
value: data.value || 1,
},
})
}
// 下载扩展 JSON 文件
export function downloadExtensionJSON(id: number, name: string) {
return exportExtension(id).then((data) => {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${name || 'extension'}_${Date.now()}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
})
}

View File

@@ -59,7 +59,6 @@ declare module 'vue' {
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload']
ExtensionDrawer: typeof import('./components/ExtensionDrawer.vue')['default']
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

View File

@@ -1,904 +0,0 @@
<template>
<el-drawer
v-model="drawerVisible"
title="扩展管理"
:size="600"
direction="rtl"
>
<div class="extension-drawer-content">
<!-- 扩展注入的设置面板从全局 #extensions_settings 搬入显示 -->
<div v-show="hasExtensionSettingsUI" class="extensions-settings-section">
<el-divider content-position="left">扩展设置面板</el-divider>
<div ref="settingsMountRef" class="extensions-settings-mount"></div>
</div>
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索扩展..."
clearable
size="small"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<!-- 操作按钮 -->
<div class="action-bar">
<el-button size="small" @click="showInstallDialog = true">
<el-icon><Plus /></el-icon>
安装扩展
</el-button>
<el-button
size="small"
@click="handleUpdateAll"
:disabled="!hasUpdatableExtensions"
>
<el-icon><Refresh /></el-icon>
全部更新
</el-button>
<el-button size="small" @click="handleRefresh">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
<!-- 扩展折叠面板 -->
<div class="extension-list" v-loading="extensionStore.loading">
<el-empty v-if="filteredExtensions.length === 0" description="暂无扩展" />
<el-collapse v-else v-model="activeNames">
<el-collapse-item
v-for="ext in filteredExtensions"
:key="ext.id"
:name="ext.id"
class="extension-item"
>
<!-- 扩展头部 -->
<template #title>
<div class="extension-header" @click.stop>
<el-switch
v-model="ext.isEnabled"
size="default"
@change="handleToggle(ext)"
style="margin-right: 12px"
/>
<div class="extension-title">
<span class="name">{{ ext.displayName || ext.name }}</span>
<el-tag v-if="ext.isSystemExt" size="small" type="info" style="margin-left: 8px">
系统
</el-tag>
<el-tag :type="getRunningStatus(ext).type" size="small" style="margin-left: 8px">
{{ getRunningStatus(ext).text }}
</el-tag>
</div>
</div>
</template>
<!-- 扩展内容 -->
<div class="extension-content">
<div class="extension-description" v-if="ext.description">
<p>{{ ext.description }}</p>
</div>
<!-- 扩展配置 -->
<el-divider content-position="left">配置</el-divider>
<!-- 自定义配置容器扩展可以在运行时注入 UI -->
<div
:id="`extension-settings-${ext.name}`"
class="extension-custom-settings"
:data-extension-id="ext.id"
:data-extension-name="ext.name"
>
<!-- 扩展的自定义配置 UI 将动态注入到这里 -->
</div>
<!-- 基于 manifest schema 的配置表单兜底方案 -->
<el-form label-position="top" size="small">
<template v-for="(setting, key) in getSettingsSchema(ext)" :key="key">
<!-- Text 输入框 -->
<el-form-item
v-if="setting.type === 'text' || !setting.type"
:label="setting.label || key"
>
<el-input
v-model="extensionSettings[ext.id][key]"
:placeholder="setting.placeholder"
@change="handleSettingChange(ext.id, key)"
/>
<div v-if="setting.description" class="setting-hint">
{{ setting.description }}
</div>
</el-form-item>
<!-- Number 数字输入 -->
<el-form-item
v-else-if="setting.type === 'number'"
:label="setting.label || key"
>
<el-input-number
v-model="extensionSettings[ext.id][key]"
:min="setting.min"
:max="setting.max"
:step="setting.step || 1"
@change="handleSettingChange(ext.id, key)"
style="width: 100%"
/>
<div v-if="setting.description" class="setting-hint">
{{ setting.description }}
</div>
</el-form-item>
<!-- Boolean 开关 -->
<el-form-item
v-else-if="setting.type === 'boolean'"
:label="setting.label || key"
>
<el-switch
v-model="extensionSettings[ext.id][key]"
@change="handleSettingChange(ext.id, key)"
/>
<div v-if="setting.description" class="setting-hint">
{{ setting.description }}
</div>
</el-form-item>
<!-- Select 下拉选择 -->
<el-form-item
v-else-if="setting.type === 'select'"
:label="setting.label || key"
>
<el-select
v-model="extensionSettings[ext.id][key]"
@change="handleSettingChange(ext.id, key)"
style="width: 100%"
>
<el-option
v-for="option in setting.options"
:key="option.value || option"
:label="option.label || option"
:value="option.value || option"
/>
</el-select>
<div v-if="setting.description" class="setting-hint">
{{ setting.description }}
</div>
</el-form-item>
<!-- Textarea 文本域 -->
<el-form-item
v-else-if="setting.type === 'textarea'"
:label="setting.label || key"
>
<el-input
v-model="extensionSettings[ext.id][key]"
type="textarea"
:rows="setting.rows || 3"
:placeholder="setting.placeholder"
@change="handleSettingChange(ext.id, key)"
/>
<div v-if="setting.description" class="setting-hint">
{{ setting.description }}
</div>
</el-form-item>
<!-- Slider 滑块 -->
<el-form-item
v-else-if="setting.type === 'slider'"
:label="setting.label || key"
>
<el-slider
v-model="extensionSettings[ext.id][key]"
:min="setting.min || 0"
:max="setting.max || 100"
:step="setting.step || 1"
:show-input="true"
@change="handleSettingChange(ext.id, key)"
/>
<div v-if="setting.description" class="setting-hint">
{{ setting.description }}
</div>
</el-form-item>
</template>
<!-- 没有配置项仅当: manifest schema 配置 无扩展注入的自定义 UI 无全局设置面板 -->
<el-empty
v-if="Object.keys(getSettingsSchema(ext)).length === 0 && !extensionsWithCustomUI.has(ext.name) && !hasExtensionSettingsUI"
description="此扩展没有可配置项"
:image-size="60"
/>
<div
v-else-if="Object.keys(getSettingsSchema(ext)).length === 0 && hasExtensionSettingsUI"
class="setting-hint" style="text-align: center; padding: 8px 0; color: var(--el-text-color-secondary);"
>
此扩展的设置面板已显示在上方扩展设置面板区域
</div>
</el-form>
<!-- 操作按钮 -->
<div class="extension-actions">
<el-button
v-if="ext.isEnabled && getRunningStatus(ext).isRunning"
size="small"
type="warning"
@click="handleReload(ext)"
>
<el-icon><RefreshRight /></el-icon>
重新加载
</el-button>
<el-button
v-if="ext.sourceUrl && (ext.installSource === 'git' || ext.installSource === 'url')"
size="small"
type="primary"
@click="handleUpdate(ext)"
>
<el-icon><Refresh /></el-icon>
更新
</el-button>
<el-button
v-if="!ext.isSystemExt"
size="small"
type="danger"
@click="handleDelete(ext)"
>
<el-icon><Delete /></el-icon>
卸载
</el-button>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
<!-- 安装扩展对话框 -->
<el-dialog
v-model="showInstallDialog"
title="安装扩展"
width="500px"
append-to-body
>
<el-tabs v-model="installTab">
<el-tab-pane label="上传文件" name="file">
<el-upload
drag
:auto-upload="false"
:on-change="handleFileChange"
:show-file-list="true"
:limit="1"
accept=".zip,.json"
>
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
<div class="el-upload__text">
将扩展包.zip manifest.json 拖到此处<em>点击上传</em>
</div>
</el-upload>
</el-tab-pane>
<el-tab-pane label="URL 安装" name="url">
<el-form :model="installForm" label-width="120px">
<el-form-item label="URL">
<el-input
v-model="installForm.url"
placeholder="Git 仓库 URL 或 manifest.json URL"
>
<template #append>
<el-tooltip content="支持 Git 仓库 URL如 GitHub、GitLab或直接的 manifest.json URL" placement="top">
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</template>
</el-input>
</el-form-item>
<el-form-item label="分支(可选)">
<el-input
v-model="installForm.branch"
placeholder="main"
>
<template #append>
<el-tooltip content="Git 仓库的分支名称,默认为 main" placement="top">
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</template>
</el-input>
</el-form-item>
</el-form>
<el-alert type="info" :closable="false" show-icon>
<template #title>
<div style="font-size: 12px;">
示例<br />
https://github.com/user/extension.git<br />
https://gitlab.com/user/extension<br />
https://example.com/manifest.json
</div>
</template>
</el-alert>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="showInstallDialog = false">取消</el-button>
<el-button type="primary" @click="handleInstall" :loading="installing">
安装
</el-button>
</template>
</el-dialog>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch, nextTick, onBeforeUnmount } from 'vue'
import { useExtensionStore } from '@/stores/extension'
import { extensionRuntime } from '@/utils/extensionRuntime'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Search,
Plus,
Refresh,
RefreshRight,
Delete,
UploadFilled,
QuestionFilled,
} from '@element-plus/icons-vue'
import type { Extension } from '@/types/extension'
// Props
interface Props {
visible?: boolean
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
})
// Emits
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
}>()
// Store
const extensionStore = useExtensionStore()
// State
const drawerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val),
})
const searchKeyword = ref('')
const activeNames = ref<number[]>([])
const extensionSettings = reactive<Record<number, Record<string, any>>>({})
const showInstallDialog = ref(false)
const installTab = ref('file')
const installing = ref(false)
const selectedFile = ref<File | null>(null)
const installForm = reactive({
url: '',
branch: 'main',
})
// 扩展设置面板挂载点
const settingsMountRef = ref<HTMLElement | null>(null)
const hasExtensionSettingsUI = ref(false)
// 跟踪哪些扩展已经注入了自定义 UI
const extensionsWithCustomUI = reactive(new Set<string>())
let settingsObserver: MutationObserver | null = null
/**
* 将全局 #extensions_settings 容器中的扩展 UI 搬入 Drawer 中显示
* 原版 SillyTavern 扩展通过 jQuery 向 #extensions_settings 追加自定义 HTML
* 我们在 Drawer 打开时把这些内容移入显示,关闭时归还。
*/
const mountExtensionSettings = () => {
const globalContainer = document.getElementById('extensions_settings')
const globalContainer2 = document.getElementById('extensions_settings2')
const mountPoint = settingsMountRef.value
if (!mountPoint) return
const hasContent = (globalContainer && globalContainer.children.length > 0) ||
(globalContainer2 && globalContainer2.children.length > 0)
if (hasContent) {
// 将全局容器移入 Drawer 挂载点中(保持 DOM 引用不变,扩展的事件绑定不会丢失)
if (globalContainer && globalContainer.children.length > 0) {
globalContainer.style.display = ''
mountPoint.appendChild(globalContainer)
}
if (globalContainer2 && globalContainer2.children.length > 0) {
globalContainer2.style.display = ''
mountPoint.appendChild(globalContainer2)
}
hasExtensionSettingsUI.value = true
} else {
hasExtensionSettingsUI.value = false
}
}
/**
* 将扩展设置容器归还到 bodyDrawer 关闭时调用)
*/
const unmountExtensionSettings = () => {
const globalContainer = document.getElementById('extensions_settings')
const globalContainer2 = document.getElementById('extensions_settings2')
if (globalContainer) {
globalContainer.style.display = 'none'
document.body.appendChild(globalContainer)
}
if (globalContainer2) {
globalContainer2.style.display = 'none'
document.body.appendChild(globalContainer2)
}
}
/**
* 检查每个已注册扩展的 per-extension 容器是否有内容
* 并监听全局 #extensions_settings 容器变化
*/
const scanCustomSettingsUI = () => {
// 检查每个扩展的专属容器
for (const ext of extensionStore.extensions) {
const container = document.getElementById(`extension-settings-${ext.name}`)
if (container && container.children.length > 0) {
extensionsWithCustomUI.add(ext.name)
}
}
// 重新检测全局容器
const globalContainer = document.getElementById('extensions_settings')
const globalContainer2 = document.getElementById('extensions_settings2')
const hasContent = (globalContainer && globalContainer.children.length > 0) ||
(globalContainer2 && globalContainer2.children.length > 0)
hasExtensionSettingsUI.value = hasContent
}
/**
* 启动 MutationObserver 监听全局 #extensions_settings 容器变化
* 扩展可能在加载完成后异步注入设置 UI需要动态检测
*/
const startSettingsObserver = () => {
if (settingsObserver) settingsObserver.disconnect()
settingsObserver = new MutationObserver(() => {
scanCustomSettingsUI()
// 如果 Drawer 已打开,重新尝试挂载
if (drawerVisible.value) {
mountExtensionSettings()
}
})
// 监听全局容器
const targets = [
document.getElementById('extensions_settings'),
document.getElementById('extensions_settings2'),
].filter(Boolean) as HTMLElement[]
for (const target of targets) {
settingsObserver.observe(target, { childList: true, subtree: true })
}
// 也监听 body 上的 per-extension 容器变化(扩展可能通过 id 定位并注入)
settingsObserver.observe(document.body, { childList: true, subtree: false })
}
// 组件卸载时归还容器并清理 Observer
onBeforeUnmount(() => {
unmountExtensionSettings()
if (settingsObserver) {
settingsObserver.disconnect()
settingsObserver = null
}
})
// Computed
const filteredExtensions = computed(() => {
let extensions = extensionStore.extensions
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
extensions = extensions.filter(ext =>
(ext.name?.toLowerCase().includes(keyword)) ||
(ext.displayName?.toLowerCase().includes(keyword)) ||
(ext.description?.toLowerCase().includes(keyword))
)
}
return extensions
})
// 是否有可更新的扩展
const hasUpdatableExtensions = computed(() => {
return extensionStore.extensions.some(ext =>
ext.sourceUrl && (ext.installSource === 'git' || ext.installSource === 'url')
)
})
// Methods
const getSettingsSchema = (ext: Extension) => {
try {
const manifest = typeof ext.manifestData === 'string'
? JSON.parse(ext.manifestData)
: ext.manifestData
return manifest?.settings || {}
} catch {
return {}
}
}
const getRunningStatus = (ext: Extension) => {
const instance = extensionRuntime.getInstance(ext.id)
if (!ext.isEnabled) {
return { text: '已禁用', type: 'info', isRunning: false }
}
if (instance?.isRunning) {
return { text: '运行中', type: 'success', isRunning: true }
}
if (instance?.isLoaded) {
return { text: '已加载', type: 'warning', isRunning: false }
}
return { text: '未加载', type: 'danger', isRunning: false }
}
const handleToggle = async (ext: Extension) => {
try {
// 传递当前的启用状态
await extensionStore.toggleExtension(ext.id, ext.isEnabled)
// 根据状态加载/卸载扩展
if (ext.isEnabled) {
const success = await extensionRuntime.enableExtension(ext)
if (!success) {
ext.isEnabled = false
ElMessage.error('启用扩展失败')
} else {
ElMessage.success('扩展已启用并运行')
}
} else {
await extensionRuntime.disableExtension(ext.id)
ElMessage.success('扩展已禁用')
}
} catch (error) {
console.error('切换扩展状态失败:', error)
// 恢复状态
ext.isEnabled = !ext.isEnabled
}
}
const handleSettingChange = async (extId: number, key: string) => {
try {
await extensionStore.updateExtensionSettings(extId, {
[key]: extensionSettings[extId][key],
})
// 通知扩展配置已更改
extensionRuntime.emitEvent('settingsChanged', {
extensionId: extId,
key,
value: extensionSettings[extId][key],
})
} catch (error) {
console.error('保存设置失败:', error)
ElMessage.error('保存设置失败')
}
}
const handleReload = async (ext: Extension) => {
try {
await extensionRuntime.reloadExtension(ext)
ElMessage.success('扩展已重新加载')
} catch (error) {
console.error('重新加载失败:', error)
ElMessage.error('重新加载失败')
}
}
const handleUpdate = async (ext: Extension) => {
try {
await ElMessageBox.confirm(
`确定要更新扩展 "${ext.displayName || ext.name}" 吗?这将从原始安装源重新下载扩展。`,
'确认更新',
{
type: 'info',
confirmButtonText: '更新',
cancelButtonText: '取消',
}
)
// 更新扩展
await extensionStore.upgradeExtension(ext.id)
// 如果扩展正在运行,重新加载
if (ext.isEnabled) {
await extensionRuntime.reloadExtension(ext)
}
ElMessage.success('扩展已更新')
} catch (error) {
if (error !== 'cancel') {
console.error('更新失败:', error)
}
}
}
const handleDelete = async (ext: Extension) => {
try {
await ElMessageBox.confirm(
`确定要卸载扩展 "${ext.displayName || ext.name}" 吗?`,
'确认卸载',
{ type: 'warning' }
)
// 先卸载运行时
await extensionRuntime.unloadExtension(ext.id)
// 再删除数据
await extensionStore.deleteExtension(ext.id)
ElMessage.success('扩展已卸载')
} catch (error) {
if (error !== 'cancel') {
console.error('卸载失败:', error)
}
}
}
const handleRefresh = () => {
extensionStore.fetchExtensionList()
}
// 批量更新所有扩展
const handleUpdateAll = async () => {
try {
await ElMessageBox.confirm(
'确定要更新所有可更新的扩展吗?',
'批量更新',
{
type: 'info',
confirmButtonText: '更新',
cancelButtonText: '取消',
}
)
const updatableExtensions = extensionStore.extensions.filter(ext =>
ext.sourceUrl && (ext.installSource === 'git' || ext.installSource === 'url')
)
if (updatableExtensions.length === 0) {
ElMessage.info('没有可更新的扩展')
return
}
ElMessage.info(`开始更新 ${updatableExtensions.length} 个扩展...`)
let successCount = 0
let failCount = 0
for (const ext of updatableExtensions) {
try {
await extensionStore.upgradeExtension(ext.id)
successCount++
} catch (error) {
console.error(`更新扩展 ${ext.name} 失败:`, error)
failCount++
}
}
if (failCount === 0) {
ElMessage.success(`成功更新 ${successCount} 个扩展`)
} else {
ElMessage.warning(`更新完成:成功 ${successCount} 个,失败 ${failCount}`)
}
// 重新加载所有启用的扩展
await extensionStore.fetchExtensionList()
} catch (error) {
if (error !== 'cancel') {
console.error('批量更新失败:', error)
}
}
}
const handleFileChange = (file: any) => {
selectedFile.value = file.raw
}
const handleInstall = async () => {
try {
installing.value = true
if (installTab.value === 'file') {
if (!selectedFile.value) {
ElMessage.warning('请选择扩展文件(.zip 或 .json')
return
}
await extensionStore.importExtension(selectedFile.value)
} else {
if (!installForm.url) {
ElMessage.warning('请输入 Manifest URL')
return
}
// 从 URL 安装扩展(支持分支参数)
await installFromURL(installForm.url, installForm.branch || 'main')
}
showInstallDialog.value = false
selectedFile.value = null
installForm.url = ''
installForm.branch = 'main'
} catch (error) {
console.error('安装失败:', error)
} finally {
installing.value = false
}
}
// 从 URL 安装扩展(智能识别 Git URL 或 Manifest URL
const installFromURL = async (url: string, branch = 'main') => {
try {
// 验证 URL 格式
if (!url.startsWith('http://') && !url.startsWith('https://')) {
ElMessage.error('请输入有效的 HTTP(S) URL')
return
}
ElMessage.info('正在安装扩展...')
// 调用后端智能安装 API自动识别 Git URL 或 Manifest URL
await extensionStore.installExtensionFromUrl(url, branch)
ElMessage.success('扩展安装成功')
} catch (error: any) {
console.error('安装失败:', error)
ElMessage.error(error.message || '安装失败')
throw error
}
}
// 监听扩展列表变化,初始化设置
watch(
() => extensionStore.extensions,
(extensions) => {
extensions.forEach(ext => {
if (!extensionSettings[ext.id]) {
extensionSettings[ext.id] = {}
}
// 加载已保存的设置
if (ext.settings) {
const settings = typeof ext.settings === 'string'
? JSON.parse(ext.settings)
: ext.settings
Object.assign(extensionSettings[ext.id], settings)
}
// 如果扩展已启用,自动加载
if (ext.isEnabled && !extensionRuntime.getInstance(ext.id)) {
extensionRuntime.enableExtension(ext)
}
})
},
{ immediate: true, deep: true }
)
// 当抽屉打开时刷新列表并挂载扩展设置 UI
watch(drawerVisible, async (visible) => {
if (visible) {
handleRefresh()
// 等待 DOM 更新后再挂载扩展设置面板
await nextTick()
// 启动 MutationObserver 以检测扩展动态注入的 UI
startSettingsObserver()
// 扫描已有的自定义 settings UI
scanCustomSettingsUI()
setTimeout(() => {
mountExtensionSettings()
// 再次扫描(给扩展一点时间注入 UI
scanCustomSettingsUI()
}, 200)
} else {
unmountExtensionSettings()
if (settingsObserver) {
settingsObserver.disconnect()
}
}
})
</script>
<style scoped lang="scss">
.extension-drawer-content {
height: 100%;
display: flex;
flex-direction: column;
.search-bar {
margin-bottom: 12px;
}
.action-bar {
margin-bottom: 16px;
display: flex;
gap: 8px;
}
.extensions-settings-section {
margin-bottom: 16px;
}
.extensions-settings-mount {
width: 100%;
:deep(#extensions_settings),
:deep(#extensions_settings2) {
display: flex !important;
flex-direction: column;
gap: 8px;
width: 100%;
}
:deep(.extension_container) {
margin-bottom: 8px;
}
:deep(.inline-drawer) {
margin-bottom: 8px;
}
}
.extension-list {
flex: 1;
overflow-y: auto;
.extension-item {
margin-bottom: 8px;
.extension-header {
display: flex;
align-items: center;
width: 100%;
.extension-title {
flex: 1;
display: flex;
align-items: center;
.name {
font-weight: 500;
}
}
}
.extension-content {
.extension-description {
margin-bottom: 16px;
padding: 12px;
background-color: var(--el-fill-color-light);
border-radius: 4px;
p {
margin: 0;
color: var(--el-text-color-regular);
font-size: 14px;
}
}
.setting-hint {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 4px;
}
.extension-actions {
margin-top: 16px;
display: flex;
gap: 8px;
}
}
}
}
}
</style>

View File

@@ -38,17 +38,6 @@
<span>AI 配置</span>
</el-menu-item>
</el-menu>
<!-- 扩展快捷按钮 -->
<el-button
v-if="authStore.isLoggedIn"
text
@click="extensionDrawerVisible = true"
class="extension-menu-button"
>
<el-icon><Connection /></el-icon>
<span>扩展</span>
</el-button>
</div>
<div class="header-right">
@@ -85,26 +74,19 @@
<el-main class="layout-main">
<router-view />
</el-main>
<!-- 全局扩展抽屉 -->
<ExtensionDrawer v-model:visible="extensionDrawerVisible" />
</el-container>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { Grid, Files, Reading, Connection, MagicStick, Setting, ChatDotRound } from '@element-plus/icons-vue'
import ExtensionDrawer from '@/components/ExtensionDrawer.vue'
import { Grid, Files, Reading, MagicStick, Setting, ChatDotRound } from '@element-plus/icons-vue'
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
// 扩展抽屉状态
const extensionDrawerVisible = ref(false)
// 初始化用户信息
authStore.initUserInfo()
@@ -176,15 +158,6 @@ function handleCommand(command: string) {
border: none;
background: transparent;
}
.extension-menu-button {
margin-left: 12px;
font-size: 14px;
.el-icon {
margin-right: 4px;
}
}
}
.header-right {

View File

@@ -7,13 +7,7 @@ import router from './router'
import App from './App.vue'
import './assets/styles/index.scss'
// SillyTavern 扩展兼容:注入 jQuery 到全局(扩展 JS 依赖 $ 和 jQuery
import jQuery from 'jquery'
; (window as any).$ = jQuery
; (window as any).jQuery = jQuery
// 引入兼容层 (lodash, toastr)
import '@/utils/compatibility'
const app = createApp(App)
const pinia = createPinia()

View File

@@ -76,18 +76,6 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/worldbook/WorldBookEdit.vue'),
meta: { title: '编辑世界书', requiresAuth: true },
},
{
path: 'extension',
name: 'ExtensionList',
component: () => import('@/views/extension/ExtensionListNew.vue'),
meta: { title: '扩展管理', requiresAuth: true },
},
{
path: 'extension/settings/:id',
name: 'ExtensionSettings',
component: () => import('@/views/extension/ExtensionSettings.vue'),
meta: { title: '扩展配置', requiresAuth: true },
},
{
path: 'regex',
name: 'RegexScriptList',

View File

@@ -1,368 +0,0 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import * as extensionApi from '@/api/extension'
import type {
Extension,
ExtensionListParams,
CreateExtensionRequest,
UpdateExtensionRequest,
} from '@/types/extension'
export const useExtensionStore = defineStore('extension', () => {
// 状态
const extensions = ref<Extension[]>([])
const enabledExtensions = ref<Extension[]>([])
const currentExtension = ref<Extension | null>(null)
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(20)
const loading = ref(false)
// 过滤条件
const filters = ref<ExtensionListParams>({
extensionType: '',
category: '',
isEnabled: undefined,
tag: '',
})
/**
* 获取扩展列表
*/
const fetchExtensionList = async (params?: ExtensionListParams) => {
try {
loading.value = true
const requestParams: ExtensionListParams = {
page: currentPage.value,
pageSize: pageSize.value,
...filters.value,
...params,
}
const response = await extensionApi.getExtensionList(requestParams)
extensions.value = response.data.list
total.value = response.data.total
currentPage.value = response.data.page
pageSize.value = response.data.pageSize
} catch (error: any) {
console.error('获取扩展列表失败:', error)
ElMessage.error(error.response?.data?.msg || '获取扩展列表失败')
throw error
} finally {
loading.value = false
}
}
/**
* 获取启用的扩展列表
*/
const fetchEnabledExtensions = async () => {
try {
const response = await extensionApi.getEnabledExtensions()
enabledExtensions.value = response.data
return response.data
} catch (error: any) {
console.error('获取启用扩展列表失败:', error)
return []
}
}
/**
* 获取扩展详情
*/
const fetchExtension = async (id: number) => {
try {
loading.value = true
const response = await extensionApi.getExtension(id)
currentExtension.value = response.data
return response.data
} catch (error: any) {
console.error('获取扩展详情失败:', error)
ElMessage.error(error.response?.data?.msg || '获取扩展详情失败')
throw error
} finally {
loading.value = false
}
}
/**
* 创建/安装扩展
*/
const createExtension = async (data: CreateExtensionRequest) => {
try {
loading.value = true
const response = await extensionApi.createExtension(data)
ElMessage.success('安装成功')
await fetchExtensionList()
return response.data
} catch (error: any) {
console.error('创建扩展失败:', error)
ElMessage.error(error.response?.data?.msg || '创建扩展失败')
throw error
} finally {
loading.value = false
}
}
/**
* 更新扩展
*/
const updateExtension = async (id: number, data: UpdateExtensionRequest) => {
try {
loading.value = true
await extensionApi.updateExtension(id, data)
ElMessage.success('更新成功')
await fetchExtensionList()
} catch (error: any) {
console.error('更新扩展失败:', error)
ElMessage.error(error.response?.data?.msg || '更新扩展失败')
throw error
} finally {
loading.value = false
}
}
/**
* 删除/卸载扩展
*/
const deleteExtension = async (id: number, deleteFiles = false) => {
try {
loading.value = true
await extensionApi.deleteExtension(id, deleteFiles)
ElMessage.success('卸载成功')
await fetchExtensionList()
} catch (error: any) {
console.error('删除扩展失败:', error)
ElMessage.error(error.response?.data?.msg || '删除扩展失败')
throw error
} finally {
loading.value = false
}
}
/**
* 启用/禁用扩展
*/
const toggleExtension = async (id: number, isEnabled: boolean) => {
try {
await extensionApi.toggleExtension(id, { isEnabled })
ElMessage.success(isEnabled ? '启用成功' : '禁用成功')
await fetchExtensionList()
await fetchEnabledExtensions()
} catch (error: any) {
console.error('切换扩展状态失败:', error)
ElMessage.error(error.response?.data?.msg || '操作失败')
throw error
}
}
/**
* 获取扩展配置
*/
const getExtensionSettings = async (id: number) => {
try {
const response = await extensionApi.getExtensionSettings(id)
return response.data
} catch (error: any) {
console.error('获取扩展配置失败:', error)
ElMessage.error(error.response?.data?.msg || '获取扩展配置失败')
throw error
}
}
/**
* 更新扩展配置
*/
const updateExtensionSettings = async (id: number, settings: Record<string, any>) => {
try {
await extensionApi.updateExtensionSettings(id, { settings })
ElMessage.success('配置更新成功')
} catch (error: any) {
console.error('更新扩展配置失败:', error)
ElMessage.error(error.response?.data?.msg || '更新配置失败')
throw error
}
}
/**
* 获取扩展 manifest
*/
const getExtensionManifest = async (id: number) => {
try {
const response = await extensionApi.getExtensionManifest(id)
return response.data
} catch (error: any) {
console.error('获取扩展 manifest 失败:', error)
throw error
}
}
/**
* 导入扩展
*/
const importExtension = async (file: File) => {
try {
loading.value = true
const response = await extensionApi.importExtension(file)
ElMessage.success('导入成功')
await fetchExtensionList()
return response.data
} catch (error: any) {
console.error('导入扩展失败:', error)
ElMessage.error(error.response?.data?.msg || '导入扩展失败')
throw error
} finally {
loading.value = false
}
}
/**
* 导出扩展
*/
const exportExtension = async (id: number, name: string) => {
try {
const response = await extensionApi.exportExtension(id)
// 创建下载链接
const blob = new Blob([response.data], { type: 'application/json' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${name}_manifest.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} catch (error: any) {
console.error('导出扩展失败:', error)
ElMessage.error(error.response?.data?.msg || '导出扩展失败')
throw error
}
}
/**
* 智能安装扩展(自动识别 Git URL 或 Manifest URL
*/
const installExtensionFromUrl = async (url: string, branch = 'main') => {
try {
loading.value = true
const response = await extensionApi.installExtensionFromUrl(url, branch)
ElMessage.success('安装成功')
await fetchExtensionList()
return response.data
} catch (error: any) {
console.error('安装扩展失败:', error)
ElMessage.error(error.response?.data?.msg || error.message || '安装扩展失败')
throw error
} finally {
loading.value = false
}
}
/**
* 从 Git URL 安装扩展
*/
const installExtensionFromGit = async (gitUrl: string, branch = 'main') => {
try {
loading.value = true
const response = await extensionApi.installExtensionFromGit(gitUrl, branch)
ElMessage.success('安装成功')
await fetchExtensionList()
return response.data
} catch (error: any) {
console.error('从 Git 安装扩展失败:', error)
ElMessage.error(error.response?.data?.msg || '安装扩展失败')
throw error
} finally {
loading.value = false
}
}
/**
* 升级扩展版本(从源重新安装)
*/
const upgradeExtension = async (id: number, force = false) => {
try {
loading.value = true
const response = await extensionApi.upgradeExtension(id, force)
ElMessage.success('升级成功')
await fetchExtensionList()
return response.data
} catch (error: any) {
console.error('升级扩展失败:', error)
ElMessage.error(error.response?.data?.msg || '升级扩展失败')
throw error
} finally {
loading.value = false
}
}
/**
* 更新扩展统计
*/
const updateStats = async (extensionId: number, action: 'usage' | 'error' | 'load', value = 1) => {
try {
await extensionApi.updateExtensionStats({ extensionId, action, value })
} catch (error: any) {
console.error('更新扩展统计失败:', error)
}
}
/**
* 设置过滤条件
*/
const setFilters = (newFilters: Partial<ExtensionListParams>) => {
filters.value = { ...filters.value, ...newFilters }
currentPage.value = 1
fetchExtensionList()
}
/**
* 重置过滤条件
*/
const resetFilters = () => {
filters.value = {
extensionType: '',
category: '',
isEnabled: undefined,
tag: '',
}
currentPage.value = 1
fetchExtensionList()
}
return {
// 状态
extensions,
enabledExtensions,
currentExtension,
total,
currentPage,
pageSize,
loading,
filters,
// 操作
fetchExtensionList,
fetchEnabledExtensions,
fetchExtension,
createExtension,
updateExtension,
deleteExtension,
toggleExtension,
getExtensionSettings,
updateExtensionSettings,
getExtensionManifest,
importExtension,
exportExtension,
installExtensionFromUrl,
installExtensionFromGit,
upgradeExtension,
updateStats,
setFilters,
resetFilters,
}
})

View File

@@ -1,129 +0,0 @@
// 扩展类型定义(兼容 SillyTavern Extension 规范)
export interface Extension {
id: number
userId: number
name: string
displayName: string
version: string
author: string
description: string
homepage: string
repository: string
license: string
tags: string[]
extensionType: 'ui' | 'server' | 'hybrid'
category: 'utilities' | 'themes' | 'integrations' | 'tools' | string
dependencies: Record<string, string>
conflicts: string[]
manifestData: Record<string, any>
scriptPath: string
stylePath: string
assetsPaths: string[]
settings: Record<string, any>
options: Record<string, any>
isEnabled: boolean
isInstalled: boolean
isSystemExt: boolean
installSource: 'url' | 'git' | 'file' | 'marketplace'
sourceUrl: string // 原始安装 URL用于更新
branch: string // Git 分支
autoUpdate: boolean // 是否自动更新
lastUpdateCheck?: string // 最后检查更新时间
availableVersion?: string // 可用的新版本
installDate: string
lastEnabled: string
usageCount: number
errorCount: number
loadTime: number
metadata: Record<string, any>
createdAt: string
updatedAt: string
}
export interface ExtensionManifest {
name: string
display_name?: string
version: string
description: string
author: string
homepage?: string
repository?: string
license?: string
tags?: string[]
type?: 'ui' | 'server' | 'hybrid'
category?: string
dependencies?: Record<string, string>
conflicts?: string[]
entry?: string
style?: string
assets?: string[]
settings?: Record<string, any>
options?: Record<string, any>
metadata?: Record<string, any>
}
export interface CreateExtensionRequest {
name: string
displayName?: string
version?: string
author?: string
description?: string
homepage?: string
repository?: string
license?: string
tags?: string[]
extensionType: 'ui' | 'server' | 'hybrid'
category?: string
dependencies?: Record<string, string>
conflicts?: string[]
manifestData: Record<string, any>
scriptPath?: string
stylePath?: string
assetsPaths?: string[]
settings?: Record<string, any>
options?: Record<string, any>
installSource?: string
metadata?: Record<string, any>
}
export interface UpdateExtensionRequest {
displayName?: string
description?: string
settings?: Record<string, any>
options?: Record<string, any>
metadata?: Record<string, any>
}
export interface ExtensionListParams {
page?: number
pageSize?: number
keyword?: string
name?: string
extensionType?: string
category?: string
isEnabled?: boolean
isInstalled?: boolean
tag?: string
}
export interface ExtensionListResponse {
list: Extension[]
total: number
page: number
pageSize: number
}
export interface ToggleExtensionRequest {
isEnabled: boolean
}
export interface UpdateExtensionSettingsRequest {
settings: Record<string, any>
}
export interface ExtensionStatsRequest {
extensionId: number
action: 'usage' | 'error' | 'load'
value?: number
}

View File

@@ -1,19 +1,15 @@
import _ from 'lodash'
import { ElMessage } from 'element-plus'
// SillyTavern 扩展兼容层:注入全局库
// 原版扩展依赖 jquery, lodash, toastr 等全局变量
// 全局库兼容层:注入 lodash 和 toastr
// 这些库可能被其他模块使用
// 1. jQuery (需要在 main.ts 中引入,因为它是通过 import jQuery form 'jquery' 引入的,这里再引入一份可能导致多例,但挂载 window 是安全的)
// 由于 main.ts 已经处理了 jQuery这里暂不处理或者为了统一可以移过来。
// 目前 main.ts 已经有了,就先不管 jQuery。
// 2. Lodash
// Lodash
if (!(window as any)._) {
; (window as any)._ = _
}
// 3. Toastr (使用 ElementPlus Message 模拟)
// Toastr (使用 ElementPlus Message 模拟)
if (!(window as any).toastr) {
; (window as any).toastr = {
info: (msg: string) => ElMessage.info(msg),

View File

@@ -1,576 +0,0 @@
/**
* SillyTavern 扩展运行时环境
* 负责加载、运行和管理扩展的生命周期
*/
import { ElMessage } from 'element-plus'
import type { Extension } from '@/types/extension'
/**
* SillyTavern 扩展兼容层:注入扩展依赖的全局 API
* 原版 SillyTavern 扩展依赖 window.toastr、lodash 等全局库
*/
function initGlobalCompat() {
// toastr 兼容层(使用 ElMessage 替代)
if (!(window as any).toastr) {
; (window as any).toastr = {
info: (msg: string) => ElMessage.info(msg),
success: (msg: string) => ElMessage.success(msg),
warning: (msg: string) => ElMessage.warning(msg),
error: (msg: string) => ElMessage.error(msg),
clear: () => { },
}
}
// 创建扩展设置容器(原版 SillyTavern 扩展通过 jQuery 向这些容器追加设置面板 UI
// ExtensionDrawer.vue 在打开时会将这些容器移入 Drawer 中显示
if (!document.getElementById('extensions_settings')) {
const container = document.createElement('div')
container.id = 'extensions_settings'
container.style.display = 'none'
document.body.appendChild(container)
}
if (!document.getElementById('extensions_settings2')) {
const container2 = document.createElement('div')
container2.id = 'extensions_settings2'
container2.style.display = 'none'
document.body.appendChild(container2)
}
}
// 立即初始化兼容层
initGlobalCompat()
interface ExtensionInstance {
extension: Extension
manifest: any
scriptElement?: HTMLScriptElement | HTMLStyleElement
styleElement?: HTMLStyleElement
isLoaded: boolean
isRunning: boolean
api?: any
}
class ExtensionRuntime {
private instances: Map<number, ExtensionInstance> = new Map()
private stAPI: any = null
// 扩展设置存储(兼容原版 SillyTavern 的 extension_settings
private extensionSettings: Record<string, any> = {}
constructor() {
this.initSillyTavernAPI()
}
/**
* 获取扩展的配置容器 DOM 元素
*/
private getExtensionSettingsContainer(extensionName: string): HTMLElement | null {
return document.getElementById(`extension-settings-${extensionName}`)
}
/**
* 加载扩展的已保存设置
*/
private loadExtensionSettings(extensionName: string): any {
if (!this.extensionSettings[extensionName]) {
const key = `ext_settings_${extensionName}`
const saved = localStorage.getItem(key)
if (saved) {
try {
this.extensionSettings[extensionName] = JSON.parse(saved)
} catch (e) {
console.error(`[ExtensionRuntime] 加载设置失败: ${extensionName}`, e)
this.extensionSettings[extensionName] = {}
}
} else {
this.extensionSettings[extensionName] = {}
}
}
return this.extensionSettings[extensionName]
}
/**
* 保存扩展设置到 localStorage
*/
private saveExtensionSettingsToStorage(extensionName: string) {
const key = `ext_settings_${extensionName}`
try {
localStorage.setItem(key, JSON.stringify(this.extensionSettings[extensionName] || {}))
} catch (e) {
console.error(`[ExtensionRuntime] 保存设置失败: ${extensionName}`, e)
}
}
/**
* 初始化 SillyTavern API兼容层
*/
private initSillyTavernAPI() {
// 创建全局 SillyTavern API 对象
this.stAPI = {
// 扩展管理
extensions: {
register: (name: string, init: Function) => {
console.log(`[Extension] 注册扩展: ${name}`)
// 执行扩展的初始化函数
try {
init()
} catch (error) {
console.error(`[Extension] 初始化失败: ${name}`, error)
}
},
// 获取扩展设置
getSettings: (extName: string) => {
// 从 localStorage 或 store 获取设置
const settings = localStorage.getItem(`ext_settings_${extName}`)
return settings ? JSON.parse(settings) : {}
},
// 保存扩展设置
saveSettings: (extName: string, settings: any) => {
localStorage.setItem(`ext_settings_${extName}`, JSON.stringify(settings))
// 触发设置变更事件
this.emitEvent('extensionSettingsLoaded', { name: extName })
},
// 获取扩展列表
list: () => {
return Array.from(this.instances.values()).map(inst => ({
name: inst.extension.name,
displayName: inst.extension.displayName,
version: inst.extension.version,
enabled: inst.extension.isEnabled,
loaded: inst.isLoaded,
running: inst.isRunning,
}))
},
},
// UI 工具
ui: {
// 创建设置面板
createSettings: (title: string, _content: HTMLElement) => {
console.log(`[Extension] 创建设置面板: ${title}`)
// 可以在这里创建一个设置面板并添加到页面
},
// 显示通知
notify: (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => {
ElMessage[type](message)
},
// 添加 UI 元素到指定位置
addElement: (element: HTMLElement, targetSelector: string) => {
const target = document.querySelector(targetSelector)
if (target) {
target.appendChild(element)
}
},
// 创建按钮
createButton: (text: string, onClick: Function) => {
const button = document.createElement('button')
button.textContent = text
button.onclick = onClick as any
return button
},
},
// 事件系统
eventSource: {
on: (event: string, callback: Function) => {
window.addEventListener(`st:${event}`, (e: any) => callback(e.detail))
},
once: (event: string, callback: Function) => {
const handler = (e: any) => {
callback(e.detail)
window.removeEventListener(`st:${event}`, handler)
}
window.addEventListener(`st:${event}`, handler)
},
off: (event: string, callback?: Function) => {
if (callback) {
window.removeEventListener(`st:${event}`, callback as any)
}
},
emit: (event: string, data?: any) => {
window.dispatchEvent(new CustomEvent(`st:${event}`, { detail: data }))
},
},
// 获取当前上下文
getContext: () => {
// 尝试从路由或 store 获取当前上下文
const context = {
characterId: null as number | null,
chatId: null as number | null,
groupId: null as number | null,
userName: '',
characterName: '',
// extension_settings 对象(兼容原版 SillyTavern
extension_settings: new Proxy(this.extensionSettings, {
get: (target, prop: string) => {
if (typeof prop === 'string') {
// 自动加载设置(如果还没加载)
this.loadExtensionSettings(prop)
return target[prop] || {}
}
return undefined
},
set: (target, prop: string, value) => {
if (typeof prop === 'string') {
target[prop] = value
// 自动保存到 localStorage
this.saveExtensionSettingsToStorage(prop)
return true
}
return false
},
}),
// 获取扩展配置容器的方法
getExtensionSettingsContainer: (extensionName: string) => {
return this.getExtensionSettingsContainer(extensionName)
},
// 原版酒馆:保存设置(防抖),扩展会调用
saveSettingsDebounced: () => {
// 我们通过 extension_settings 的 set 已自动存 localStorage此处可触发事件通知
this.emitEvent('settingsSaved')
},
saveSettings: () => {
this.emitEvent('settingsSaved')
},
}
// 可以从实际应用状态获取
try {
const route = (window as any).$route
if (route) {
context.characterId = route.params.characterId ? parseInt(route.params.characterId) : null
context.chatId = route.params.chatId ? parseInt(route.params.chatId) : null
}
} catch (e) {
// ignore
}
return context
},
// 工具函数
utils: {
// 延迟执行
delay: (ms: number) => new Promise(resolve => setTimeout(resolve, ms)),
// 生成唯一 ID
generateId: () => `ext_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
// 安全解析 JSON
parseJSON: (str: string, fallback: any = null) => {
try {
return JSON.parse(str)
} catch {
return fallback
}
},
},
// 存储工具
storage: {
get: (key: string, defaultValue: any = null) => {
const value = localStorage.getItem(`st_${key}`)
return value ? JSON.parse(value) : defaultValue
},
set: (key: string, value: any) => {
localStorage.setItem(`st_${key}`, JSON.stringify(value))
},
remove: (key: string) => {
localStorage.removeItem(`st_${key}`)
},
clear: () => {
const keys = Object.keys(localStorage).filter(k => k.startsWith('st_'))
keys.forEach(k => localStorage.removeItem(k))
},
},
}
// 原版酒馆:渲染扩展 HTML 模板(扩展会调用)
// 注意:大部分扩展通过 import { renderExtensionTemplateAsync } from '...extensions.js' 使用原版实现,
// 此处仅作为后备兼容(如果扩展直接调用 window.SillyTavern.renderExtensionTemplateAsync
this.stAPI.renderExtensionTemplateAsync = async (extensionName: string, templateId: string, _templateData: Record<string, any> = {}) => {
// 与原版 SillyTavern 一致:模板路径为 /scripts/extensions/{extensionName}/{templateId}.html
const apiBase = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8888'
const url = `${apiBase}/scripts/extensions/${extensionName}/${templateId}.html`
try {
const res = await fetch(url)
if (!res.ok) return document.createElement('div')
const html = await res.text()
const wrap = document.createElement('div')
wrap.innerHTML = html
return wrap.firstElementChild || wrap
} catch (e) {
console.warn(`[ExtensionRuntime] 加载模板失败: ${url}`, e)
return document.createElement('div')
}
}
this.stAPI.renderExtensionTemplate = (_extensionName: string, _templateId: string, _templateData: Record<string, any> = {}) => {
console.warn('[ExtensionRuntime] renderExtensionTemplate 同步版未实现,请使用 renderExtensionTemplateAsync')
return document.createElement('div')
}
// 挂载到 window 对象
; (window as any).SillyTavern = this.stAPI
; (window as any).st = this.stAPI // 简写别名
// 触发 API 初始化完成事件
this.emitEvent('apiReady')
}
/**
* 加载扩展
*/
async loadExtension(extension: Extension): Promise<boolean> {
try {
console.log(`[Extension] 加载扩展: ${extension.name}`)
// 检查是否已加载
if (this.instances.has(extension.id) && this.instances.get(extension.id)?.isLoaded) {
console.log(`[Extension] 扩展已加载: ${extension.name}`)
return true
}
// 解析 manifest
const manifest = typeof extension.manifestData === 'string'
? JSON.parse(extension.manifestData)
: extension.manifestData
const instance: ExtensionInstance = {
extension,
manifest,
isLoaded: false,
isRunning: false,
}
// 加载样式文件
if (extension.stylePath) {
await this.loadStyle(extension, instance)
}
// 加载脚本文件
if (extension.scriptPath) {
await this.loadScript(extension, instance)
} else if (manifest.inline_script) {
// 支持内联脚本
await this.loadInlineScript(extension, instance, manifest.inline_script)
}
instance.isLoaded = true
this.instances.set(extension.id, instance)
console.log(`[Extension] 加载成功: ${extension.name}`)
return true
} catch (error) {
console.error(`[Extension] 加载失败: ${extension.name}`, error)
ElMessage.error(`加载扩展失败: ${extension.name}`)
return false
}
}
/**
* 加载扩展样式
* 资源路由为公开路由,直接使用 <link> 标签加载(与原版 SillyTavern 一致)
*/
private async loadStyle(extension: Extension, instance: ExtensionInstance): Promise<void> {
const url = this.getAssetURL(extension, extension.stylePath!)
console.log(`[Extension] 加载样式: ${extension.name}, URL=${url}`)
return new Promise((resolve, reject) => {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = url
link.dataset.extension = extension.name
link.onload = () => {
instance.styleElement = link as any
resolve()
}
link.onerror = () => {
reject(new Error(`样式加载失败: ${extension.stylePath}`))
}
document.head.appendChild(link)
})
}
/**
* 加载扩展脚本
* 与原版 SillyTavern 一致:使用 <script type="module"> 加载 ES module 格式的扩展脚本。
* 不能用 Blob URL因为 ES module 的 import 语句使用相对路径,
* 需要从实际 URL 加载才能正确解析(如 ../../../../../scripts/utils.js -> /scripts/utils.js
* 资源路由为公开路由,不需要 JWT header。
*/
private async loadScript(extension: Extension, instance: ExtensionInstance): Promise<void> {
const url = this.getAssetURL(extension, extension.scriptPath!)
console.log(`[Extension] 加载脚本: ${extension.name}, URL=${url}`)
// 记录 base URL 供扩展内部使用renderExtensionTemplateAsync 用)
const baseUrl = url.replace(/\/[^/]*$/, '')
; (window as any).__extensionBaseUrl = (window as any).__extensionBaseUrl || {}
; (window as any).__extensionBaseUrl[extension.name] = baseUrl
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.type = 'module'
script.src = url
script.dataset.extension = extension.name
script.async = true
script.onload = () => {
instance.scriptElement = script
instance.isRunning = true
resolve()
}
script.onerror = (e) => {
console.error(`[Extension] 脚本加载详细错误:`, e)
reject(new Error(`脚本加载失败: ${extension.scriptPath} (请检查控制台 Network 标签页的响应)`))
}
document.body.appendChild(script)
})
}
/**
* 加载内联脚本
*/
private async loadInlineScript(extension: Extension, instance: ExtensionInstance, scriptCode: string): Promise<void> {
return new Promise((resolve, reject) => {
try {
const script = document.createElement('script')
script.textContent = scriptCode
script.dataset.extension = extension.name
script.dataset.inline = 'true'
document.body.appendChild(script)
instance.scriptElement = script
instance.isRunning = true
resolve()
} catch (error) {
reject(new Error(`内联脚本执行失败: ${error}`))
}
})
}
/**
* 卸载扩展
*/
async unloadExtension(extensionId: number): Promise<boolean> {
const instance = this.instances.get(extensionId)
if (!instance) {
return false
}
try {
console.log(`[Extension] 卸载扩展: ${instance.extension.name}`)
// 移除样式
if (instance.styleElement) {
instance.styleElement.remove()
}
// 移除脚本
if (instance.scriptElement) {
instance.scriptElement.remove()
}
instance.isLoaded = false
instance.isRunning = false
this.instances.delete(extensionId)
console.log(`[Extension] 卸载成功: ${instance.extension.name}`)
return true
} catch (error) {
console.error(`[Extension] 卸载失败: ${instance.extension.name}`, error)
return false
}
}
/**
* 启用扩展
*/
async enableExtension(extension: Extension): Promise<boolean> {
if (!extension.isEnabled) {
return false
}
return await this.loadExtension(extension)
}
/**
* 禁用扩展
*/
async disableExtension(extensionId: number): Promise<boolean> {
return await this.unloadExtension(extensionId)
}
/**
* 重新加载扩展
*/
async reloadExtension(extension: Extension): Promise<boolean> {
await this.unloadExtension(extension.id)
return await this.loadExtension(extension)
}
/**
* 获取资源 URL扩展文件存储在后端本地通过 API 接口访问)
*/
private getAssetURL(extension: Extension, path: string): string {
if (path.startsWith('http://') || path.startsWith('https://')) {
return path
}
// 与原版 SillyTavern 完全一致的路径结构:/scripts/extensions/third-party/{name}/{path}
// 这样扩展 JS 中的相对路径 import如 ../../../extensions.js能正确解析
// 资源由后端 Router.Static("/scripts", ...) 直接提供,无需额外 API 路由
const apiBase = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8888'
return `${apiBase}/scripts/extensions/third-party/${extension.name}/${path.replace(/^\//, '')}`
}
/**
* 获取扩展实例
*/
getInstance(extensionId: number): ExtensionInstance | undefined {
return this.instances.get(extensionId)
}
/**
* 获取所有已加载的扩展
*/
getLoadedExtensions(): ExtensionInstance[] {
return Array.from(this.instances.values()).filter(inst => inst.isLoaded)
}
/**
* 触发扩展事件
*/
emitEvent(event: string, data?: any) {
this.stAPI.eventSource.emit(event, data)
}
}
// 创建单例
export const extensionRuntime = new ExtensionRuntime()
// 导出类型
export type { ExtensionInstance }

View File

@@ -1,441 +0,0 @@
<template>
<div class="extension-list">
<el-card shadow="never">
<!-- 搜索和操作栏 -->
<div class="header-actions">
<div class="search-bar">
<el-input
v-model="searchParams.keyword"
placeholder="搜索扩展名称、描述"
style="width: 300px"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-select
v-model="searchParams.extensionType"
placeholder="类型"
style="width: 120px; margin-left: 10px"
clearable
@change="handleSearch"
>
<el-option label="UI 扩展" value="ui" />
<el-option label="服务端" value="server" />
<el-option label="混合" value="hybrid" />
</el-select>
<el-select
v-model="searchParams.isEnabled"
placeholder="状态"
style="width: 120px; margin-left: 10px"
clearable
@change="handleSearch"
>
<el-option label="已启用" :value="true" />
<el-option label="已禁用" :value="false" />
</el-select>
<el-button type="primary" :icon="Search" @click="handleSearch" style="margin-left: 10px">
搜索
</el-button>
</div>
<div class="action-buttons">
<el-upload
:show-file-list="false"
:before-upload="handleImport"
accept=".zip,.json"
>
<el-button :icon="Upload">导入扩展</el-button>
</el-upload>
<el-button :icon="Link" @click="showInstallDialog = true">
URL 安装
</el-button>
</div>
</div>
<!-- 扩展卡片列表 -->
<div v-loading="extensionStore.loading" class="extension-cards">
<el-empty v-if="extensionStore.extensions.length === 0 && !extensionStore.loading" description="暂无扩展" />
<div v-for="ext in extensionStore.extensions" :key="ext.id" class="extension-card">
<div class="card-header">
<div class="card-info">
<div class="card-title">
<span class="ext-name">{{ ext.displayName || ext.name }}</span>
<el-tag size="small" type="info">v{{ ext.version }}</el-tag>
<el-tag v-if="ext.isSystemExt" size="small" type="warning">系统</el-tag>
<el-tag size="small" :type="typeTagColor(ext.extensionType)">
{{ typeLabel(ext.extensionType) }}
</el-tag>
</div>
<div class="card-meta">
<span v-if="ext.author" class="meta-item">
<el-icon><User /></el-icon> {{ ext.author }}
</span>
<span v-if="ext.category" class="meta-item">
<el-icon><FolderOpened /></el-icon> {{ ext.category }}
</span>
</div>
<p v-if="ext.description" class="card-desc">{{ ext.description }}</p>
</div>
<div class="card-actions">
<el-switch
v-model="ext.isEnabled"
@change="handleToggle(ext)"
/>
</div>
</div>
<div class="card-footer">
<div class="card-tags" v-if="ext.tags && ext.tags.length">
<el-tag
v-for="tag in ext.tags.slice(0, 3)"
:key="tag"
size="small"
type="info"
effect="plain"
>
{{ tag }}
</el-tag>
</div>
<div class="card-ops">
<el-button
type="primary"
text
size="small"
:icon="Setting"
@click="handleSettings(ext.id)"
>
设置
</el-button>
<el-button
type="info"
text
size="small"
:icon="Download"
@click="handleExport(ext)"
>
导出
</el-button>
<el-popconfirm
title="确定要删除这个扩展吗?"
@confirm="handleDelete(ext.id)"
>
<template #reference>
<el-button
type="danger"
text
size="small"
:icon="Delete"
:disabled="ext.isSystemExt"
>
删除
</el-button>
</template>
</el-popconfirm>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div style="margin-top: 20px; display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="extensionStore.currentPage"
v-model:page-size="extensionStore.pageSize"
:total="extensionStore.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="handleSearch"
@current-change="handleSearch"
/>
</div>
</el-card>
<!-- URL 安装对话框 -->
<el-dialog v-model="showInstallDialog" title="从 URL 安装扩展" width="480px">
<el-form :model="installForm" label-width="80px">
<el-form-item label="URL 地址">
<el-input
v-model="installForm.url"
placeholder="请输入 Git 仓库或 manifest.json 地址"
/>
</el-form-item>
<el-form-item label="分支">
<el-input
v-model="installForm.branch"
placeholder="main"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showInstallDialog = false">取消</el-button>
<el-button
type="primary"
:loading="installLoading"
@click="handleInstallFromUrl"
>
安装
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
Search,
Upload,
Download,
Delete,
Setting,
Link,
User,
FolderOpened,
} from '@element-plus/icons-vue'
import { useExtensionStore } from '@/stores/extension'
import type { Extension } from '@/types/extension'
const router = useRouter()
const extensionStore = useExtensionStore()
// 搜索参数
const searchParams = reactive({
keyword: '',
extensionType: undefined as string | undefined,
isEnabled: undefined as boolean | undefined,
})
// 安装对话框
const showInstallDialog = ref(false)
const installLoading = ref(false)
const installForm = reactive({
url: '',
branch: 'main',
})
// 类型标签颜色映射
const typeTagColor = (type: string) => {
const map: Record<string, string> = {
ui: '',
server: 'success',
hybrid: 'warning',
}
return map[type] || 'info'
}
// 类型显示名
const typeLabel = (type: string) => {
const map: Record<string, string> = {
ui: 'UI',
server: '服务端',
hybrid: '混合',
}
return map[type] || type
}
// 搜索
const handleSearch = () => {
extensionStore.currentPage = 1
extensionStore.fetchExtensionList({
keyword: searchParams.keyword || undefined,
extensionType: searchParams.extensionType || undefined,
isEnabled: searchParams.isEnabled,
})
}
// 切换启用/禁用
const handleToggle = async (ext: Extension) => {
try {
await extensionStore.toggleExtension(ext.id, ext.isEnabled)
} catch (error) {
ext.isEnabled = !ext.isEnabled
console.error('切换状态失败:', error)
}
}
// 设置
const handleSettings = (id: number) => {
router.push(`/extension/settings/${id}`)
}
// 删除
const handleDelete = async (id: number) => {
try {
await extensionStore.deleteExtension(id)
} catch (error) {
console.error('删除失败:', error)
}
}
// 导出
const handleExport = async (ext: Extension) => {
try {
await extensionStore.exportExtension(ext.id, ext.name)
} catch (error) {
console.error('导出失败:', error)
}
}
// 导入(支持 zip 和 json 文件)
const handleImport = async (file: File) => {
try {
await extensionStore.importExtension(file)
} catch (error) {
console.error('导入失败:', error)
}
return false
}
// 从 URL 安装
const handleInstallFromUrl = async () => {
if (!installForm.url) {
ElMessage.warning('请输入 URL 地址')
return
}
installLoading.value = true
try {
await extensionStore.installExtensionFromUrl(
installForm.url,
installForm.branch || 'main',
)
showInstallDialog.value = false
installForm.url = ''
installForm.branch = 'main'
} catch (error) {
console.error('安装失败:', error)
} finally {
installLoading.value = false
}
}
onMounted(() => {
extensionStore.fetchExtensionList()
})
</script>
<style scoped lang="scss">
.extension-list {
padding: 20px;
.header-actions {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
.search-bar {
display: flex;
align-items: center;
flex: 1;
min-width: 400px;
}
.action-buttons {
display: flex;
gap: 10px;
}
}
.extension-cards {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.extension-card {
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
padding: 16px;
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.card-info {
flex: 1;
min-width: 0;
}
.card-title {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
.ext-name {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.card-meta {
margin-top: 6px;
display: flex;
gap: 16px;
font-size: 13px;
color: var(--el-text-color-secondary);
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
}
.card-desc {
margin-top: 8px;
font-size: 13px;
color: var(--el-text-color-regular);
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.card-actions {
flex-shrink: 0;
margin-left: 16px;
}
.card-footer {
margin-top: 12px;
display: flex;
justify-content: space-between;
align-items: center;
.card-tags {
display: flex;
gap: 6px;
}
.card-ops {
display: flex;
gap: 4px;
}
}
}
}
</style>

View File

@@ -1,291 +0,0 @@
<template>
<div class="extension-settings">
<el-page-header @back="goBack" style="margin-bottom: 20px">
<template #content>
<span class="page-title">{{ extensionName }} - 扩展配置</span>
</template>
</el-page-header>
<el-row :gutter="20">
<!-- 扩展信息 -->
<el-col :span="8">
<el-card shadow="never" v-loading="extensionStore.loading">
<template #header>
<span>扩展信息</span>
</template>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="名称">{{ extensionStore.currentExtension?.displayName || extensionStore.currentExtension?.name }}</el-descriptions-item>
<el-descriptions-item label="版本">{{ extensionStore.currentExtension?.version }}</el-descriptions-item>
<el-descriptions-item label="作者">{{ extensionStore.currentExtension?.author || '-' }}</el-descriptions-item>
<el-descriptions-item label="类型">
<el-tag size="small">{{ typeLabel(extensionStore.currentExtension?.extensionType || '') }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="分类">{{ extensionStore.currentExtension?.category || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="extensionStore.currentExtension?.isEnabled ? 'success' : 'info'" size="small">
{{ extensionStore.currentExtension?.isEnabled ? '已启用' : '已禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="安装来源">{{ extensionStore.currentExtension?.installSource || '-' }}</el-descriptions-item>
<el-descriptions-item label="许可证">{{ extensionStore.currentExtension?.license || '-' }}</el-descriptions-item>
</el-descriptions>
<p v-if="extensionStore.currentExtension?.description" class="ext-description">
{{ extensionStore.currentExtension.description }}
</p>
<div v-if="extensionStore.currentExtension?.homepage || extensionStore.currentExtension?.repository" class="ext-links">
<el-link
v-if="extensionStore.currentExtension?.homepage"
type="primary"
:href="extensionStore.currentExtension.homepage"
target="_blank"
>
主页
</el-link>
<el-link
v-if="extensionStore.currentExtension?.repository"
type="primary"
:href="extensionStore.currentExtension.repository"
target="_blank"
>
仓库
</el-link>
</div>
</el-card>
</el-col>
<!-- 设置编辑 -->
<el-col :span="16">
<el-card shadow="never" v-loading="settingsLoading">
<template #header>
<div class="settings-header">
<span>扩展设置</span>
<div>
<el-button size="small" @click="resetSettings">重置</el-button>
<el-button type="primary" size="small" :loading="saving" @click="saveSettings">
保存设置
</el-button>
</div>
</div>
</template>
<el-empty v-if="!settingsKeys.length && !settingsLoading" description="该扩展暂无可配置项" />
<el-form v-else label-width="160px" label-position="left">
<el-form-item
v-for="key in settingsKeys"
:key="key"
:label="key"
>
<!-- 布尔值 -->
<el-switch
v-if="typeof settingsData[key] === 'boolean'"
v-model="settingsData[key]"
/>
<!-- 数字 -->
<el-input-number
v-else-if="typeof settingsData[key] === 'number'"
v-model="settingsData[key]"
:controls="true"
/>
<!-- 字符串 -->
<el-input
v-else-if="typeof settingsData[key] === 'string'"
v-model="settingsData[key]"
:autosize="{ minRows: 1, maxRows: 4 }"
type="textarea"
/>
<!-- 对象/数组 - JSON 编辑 -->
<el-input
v-else
v-model="settingsJsonStrings[key]"
type="textarea"
:autosize="{ minRows: 2, maxRows: 8 }"
@blur="parseJsonField(key)"
/>
</el-form-item>
</el-form>
</el-card>
<!-- Manifest 数据 -->
<el-card shadow="never" style="margin-top: 20px" v-loading="extensionStore.loading">
<template #header>
<span>Manifest 数据</span>
</template>
<pre v-if="manifest" class="manifest-json">{{ JSON.stringify(manifest, null, 2) }}</pre>
<el-empty v-else description="无 Manifest 数据" />
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useExtensionStore } from '@/stores/extension'
const router = useRouter()
const route = useRoute()
const extensionStore = useExtensionStore()
const extensionId = computed(() => Number(route.params.id))
// 状态
const settingsLoading = ref(false)
const saving = ref(false)
const settingsData = ref<Record<string, any>>({})
const settingsJsonStrings = ref<Record<string, string>>({})
const originalSettings = ref<Record<string, any>>({})
const manifest = ref<Record<string, any> | null>(null)
const extensionName = computed(() =>
extensionStore.currentExtension?.displayName || extensionStore.currentExtension?.name || '扩展',
)
const settingsKeys = computed(() => Object.keys(settingsData.value))
// 类型标签
const typeLabel = (type: string) => {
const map: Record<string, string> = {
ui: 'UI 扩展',
server: '服务端扩展',
hybrid: '混合扩展',
}
return map[type] || type
}
// 获取扩展详情
const fetchExtension = async () => {
try {
await extensionStore.fetchExtension(extensionId.value)
} catch (error) {
console.error('获取扩展详情失败:', error)
ElMessage.error('获取扩展详情失败')
}
}
// 获取扩展设置
const fetchSettings = async () => {
settingsLoading.value = true
try {
const data = await extensionStore.getExtensionSettings(extensionId.value)
if (data) {
settingsData.value = { ...data }
originalSettings.value = JSON.parse(JSON.stringify(data))
// 为复杂类型生成 JSON 字符串
for (const key of Object.keys(data)) {
if (typeof data[key] === 'object' && data[key] !== null) {
settingsJsonStrings.value[key] = JSON.stringify(data[key], null, 2)
}
}
}
} catch (error) {
console.error('获取设置失败:', error)
} finally {
settingsLoading.value = false
}
}
// 获取 manifest
const fetchManifest = async () => {
try {
const data = await extensionStore.getExtensionManifest(extensionId.value)
if (data) {
manifest.value = data
}
} catch (error) {
console.error('获取 manifest 失败:', error)
}
}
// 解析 JSON 字段
const parseJsonField = (key: string) => {
try {
settingsData.value[key] = JSON.parse(settingsJsonStrings.value[key])
} catch {
ElMessage.warning(`字段 "${key}" 的 JSON 格式无效`)
}
}
// 保存设置
const saveSettings = async () => {
saving.value = true
try {
await extensionStore.updateExtensionSettings(extensionId.value, settingsData.value)
originalSettings.value = JSON.parse(JSON.stringify(settingsData.value))
} catch (error) {
console.error('保存设置失败:', error)
} finally {
saving.value = false
}
}
// 重置设置
const resetSettings = () => {
settingsData.value = JSON.parse(JSON.stringify(originalSettings.value))
for (const key of Object.keys(settingsData.value)) {
if (typeof settingsData.value[key] === 'object' && settingsData.value[key] !== null) {
settingsJsonStrings.value[key] = JSON.stringify(settingsData.value[key], null, 2)
}
}
ElMessage.info('已重置')
}
// 返回
const goBack = () => {
router.push('/extension')
}
onMounted(() => {
fetchExtension()
fetchSettings()
fetchManifest()
})
</script>
<style scoped lang="scss">
.extension-settings {
padding: 20px;
.page-title {
font-size: 16px;
font-weight: 600;
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.ext-description {
margin-top: 16px;
font-size: 13px;
color: var(--el-text-color-regular);
line-height: 1.6;
}
.ext-links {
margin-top: 12px;
display: flex;
gap: 16px;
}
.manifest-json {
background: var(--el-fill-color-lighter);
padding: 12px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
max-height: 400px;
white-space: pre-wrap;
word-break: break-all;
}
}
</style>