🎨 优化扩展模块
This commit is contained in:
95
docs/扩展功能模块开发文档.md
Normal file
95
docs/扩展功能模块开发文档.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
1. 系统架构概述本系统采用前后端分离架构,旨在支持高度自定义的角色扮演体验。
|
||||||
|
前端 (Vue 3):负责 UI 渲染、插件面板挂载、脚本沙箱执行及动态 HTML 处理。
|
||||||
|
后端 (Go):处理业务逻辑、Prompt 组装、插件权限校验及静态资源分发。
|
||||||
|
数据库 (PostgreSQL):存储用户数据、角色卡设定、对话历史及插件配置(JSONB)。
|
||||||
|
|
||||||
|
2. 插件分类定义系统支持以下两类插件,并区分 公共 (Public) 与 私人 (Private) 权限:
|
||||||
|
插件类型实现技术核心功能设定集 (Lorebook)后端注入 + 数据库检索关键词触发知识点注入 Prompt。
|
||||||
|
功能扩展 (Extension)前端沙箱 (Iframe/JS)增加 UI 面板、导入脚本库、渲染自定义 HTML。
|
||||||
|
|
||||||
|
3. 数据库模型设计3.1 插件主表 (plugins)
|
||||||
|
```SQL
|
||||||
|
CREATE TABLE plugins (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL, -- 'lorebook' 或 'extension'
|
||||||
|
is_public BOOLEAN DEFAULT false,
|
||||||
|
owner_id UUID REFERENCES users(id),
|
||||||
|
manifest JSONB, -- 存储版本、作者、入口点等元数据
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
```
|
||||||
|
3.2 设定条目表 (lore_items)
|
||||||
|
```SQL
|
||||||
|
CREATE TABLE lore_items (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
plugin_id UUID REFERENCES plugins(id) ON DELETE CASCADE,
|
||||||
|
keywords TEXT[], -- 触发词:["魔法", "火球"]
|
||||||
|
content TEXT NOT NULL, -- 注入内容
|
||||||
|
priority INT DEFAULT 10 -- 插入顺序
|
||||||
|
);
|
||||||
|
```
|
||||||
|
3.3 用户配置表 (user_plugin_configs)
|
||||||
|
```SQL
|
||||||
|
CREATE TABLE user_plugin_configs (
|
||||||
|
user_id UUID REFERENCES users(id),
|
||||||
|
plugin_id UUID REFERENCES plugins(id),
|
||||||
|
is_enabled BOOLEAN DEFAULT true,
|
||||||
|
settings JSONB, -- 存储插件的个性化设置
|
||||||
|
PRIMARY KEY (user_id, plugin_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 后端开发:设定集注入逻辑 (Go)
|
||||||
|
4.1 Prompt 组装流程后端在向 LLM 发送请求前,需执行以下伪逻辑:
|
||||||
|
分词匹配:提取用户最新输入及其上下文,比对数据库中的 keywords。
|
||||||
|
内容提取:获取命中条目的 content。
|
||||||
|
模板拼接:
|
||||||
|
```Plaintext
|
||||||
|
{{System_Prompt}}
|
||||||
|
[相关设定注入区]
|
||||||
|
{{Injected_Lore_Content}}
|
||||||
|
----------------
|
||||||
|
{{Chat_History}}
|
||||||
|
{{User_Input}}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 前端开发:扩展功能实现 (Vue 3)
|
||||||
|
5.1 插件面板挂载参考 src/Panel.vue 的设计,利用动态组件加载插件配置:功能:读取插件的 manifest.settings_schema。交互:通过 Vue 响应式表单实时预览设置效果,并保存至后端 JSONB 字段。
|
||||||
|
5.2 脚本沙箱与 HTML 渲染参考你提供的 Iframe.vue 逻辑,为“酒馆助手”类插件提供运行环境:A. 沙箱初始化使用不可见的 <iframe> 运行插件 JS,防止污染主页面全局变量。通过 postMessage 实现主窗口与插件的通信。B. 动态 HTML 接管在聊天渲染组件中预留“渲染钩子”:JavaScript
|
||||||
|
```
|
||||||
|
// 在消息渲染循环中
|
||||||
|
const renderMessage = (msg) => {
|
||||||
|
if (helperPlugin.enabled && helperPlugin.hasRenderer) {
|
||||||
|
// 调用插件提供的 HTML 渲染逻辑
|
||||||
|
return helperPlugin.process(msg.content);
|
||||||
|
}
|
||||||
|
return standardMarkdownRender(msg.content);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
6. 目录结构规范后端存储 (Go 服务端)
|
||||||
|
```Plaintext
|
||||||
|
/storage/plugins
|
||||||
|
/public/ # 公共插件(如官方酒馆助手)
|
||||||
|
/tavern-helper/
|
||||||
|
manifest.json
|
||||||
|
index.js
|
||||||
|
/private/ # 用户上传
|
||||||
|
/{user_id}/
|
||||||
|
/my-custom-ui/
|
||||||
|
```
|
||||||
|
前端状态 (Pinia/Vuex)
|
||||||
|
```TypeScript
|
||||||
|
interface PluginState {
|
||||||
|
enabledPlugins: Plugin[];
|
||||||
|
activeLore: LoreItem[];
|
||||||
|
// 存储插件注入的自定义 HTML 元素
|
||||||
|
injectedUIElements: Map<string, HTMLElement>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
7. 开发路线图 (Roadmap)
|
||||||
|
阶段一 (核心):完成 Go 后端的关键词匹配算法,实现简单的设定集注入。
|
||||||
|
阶段二 (管理):开发 Vue 前端的插件管理列表,支持从 JSON 导入设定集。
|
||||||
|
阶段三 (高级):实现基于 Iframe 的脚本加载器,支持插件向侧边栏注入自定义 HTML 面板。
|
||||||
|
阶段四 (优化):引入 PostgreSQL 的向量搜索 (pgvector),取代简单的关键词匹配,实现语义级设定调取
|
||||||
72
docs/扩展加载面板.md
Normal file
72
docs/扩展加载面板.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
你的实现逻辑应为:扫描:Vue 前端获取插件列表,读取每个插件的 manifest.json。识别:寻找 settings_file 或类似的字段(或者约定插件根目录下的 settings.html 为面板入口)。渲染:点击插件设置时,由 Vue 动态创建一个 <iframe> 或受控的 DIV 来加载该 HTML。2. 前端实现:插件设置包装组件 (PluginSettingsWrapper.vue)由于 LittleWhiteBox 等插件是基于原生 HTML/JS 编写的,直接注入 Vue 可能会有 CSS 污染。推荐使用 Iframe 沙箱 模式。代码段<template>
|
||||||
|
<div class="plugin-settings-container">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg font-bold">{{ plugin.name }}</h3>
|
||||||
|
<el-switch v-model="isEnabled" @change="togglePlugin" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasSettingsUI" class="settings-iframe-wrapper">
|
||||||
|
<iframe
|
||||||
|
ref="settingsIframe"
|
||||||
|
:src="settingsUrl"
|
||||||
|
class="w-full h-[500px] border-none"
|
||||||
|
@load="injectBridge"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="text-gray-500">
|
||||||
|
该插件没有独立设置面板。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps(['plugin']); // 后端返回的插件对象
|
||||||
|
const settingsIframe = ref(null);
|
||||||
|
|
||||||
|
// 计算 HTML 入口地址:指向你的 Go 后端静态资源接口
|
||||||
|
const settingsUrl = computed(() => {
|
||||||
|
return `${import.meta.env.VITE_API_BASE}/plugins/files/${props.plugin.id}/settings.html`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasSettingsUI = computed(() => props.plugin.has_settings);
|
||||||
|
|
||||||
|
// 核心:注入桥接脚本(Bridge)
|
||||||
|
const injectBridge = () => {
|
||||||
|
const frame = settingsIframe.value;
|
||||||
|
if (!frame) return;
|
||||||
|
|
||||||
|
// 向 Iframe 注入一个全局变量,让插件能调用你的系统功能
|
||||||
|
// 例如:LittleWhiteBox 里的 JS 可能会寻找 'SillyTavern' 对象
|
||||||
|
const bridgeScript = `
|
||||||
|
window.parent.SillyTavern = {
|
||||||
|
saveSettings: (data) => {
|
||||||
|
window.parent.postMessage({ type: 'SAVE_PLUGIN_SETTINGS', data: data }, '*');
|
||||||
|
},
|
||||||
|
getSettings: () => ${JSON.stringify(props.plugin.user_settings)}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const scriptTag = frame.contentDocument.createElement('script');
|
||||||
|
scriptTag.textContent = bridgeScript;
|
||||||
|
frame.contentDocument.head.appendChild(scriptTag);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("跨域限制或插件加载失败", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
3. 后端支持 (Go):静态资源与配置下发你的 Go 后端需要能够提供插件目录下的文件访问。Go// Go 伪代码:静态文件分发
|
||||||
|
r.GET("/plugins/files/:plugin_id/*filepath", func(c *gin.Context) {
|
||||||
|
pluginID := c.Param("plugin_id")
|
||||||
|
filePath := c.Param("filepath")
|
||||||
|
|
||||||
|
// 从数据库查询该插件的物理路径
|
||||||
|
basePath := db.GetPluginPath(pluginID)
|
||||||
|
|
||||||
|
// 返回对应的 HTML/JS/CSS
|
||||||
|
c.File(filepath.Join(basePath, filePath))
|
||||||
|
})
|
||||||
|
4. 针对 LittleWhiteBox 的适配建议通过分析 LittleWhiteBox 的代码,它包含了大量的模块(如 modules/tts, modules/novel-draw)。这些模块通常通过 index.js 加载。如果你要完美支持它,需要解决以下两个关键点:CSS 样式映射:LittleWhiteBox 的样式可能依赖原版酒馆的 CSS 类名(如 .menu_button, .panel)。解决方案:在你的 Vue 项目中,为插件 iframe 注入一个包含“酒馆兼容层样式”的 CSS 文件,模拟原版的 UI 环境。事件监听 (Event Bus):LittleWhiteBox 里的很多功能(如 modules/event-manager.js)是基于事件触发的。解决方案:在你的 PluginContext 中实现一个简单的事件中心,当你的 Vue 界面发生“消息发送”、“收到回复”等动作时,通过 postMessage 通知 Iframe 内的插件。5. 文档补全:插件设置渲染规范步骤开发者操作系统行为检测后端扫描 manifest.json发现 settings_file: "settings.html"。入口Vue 点击插件列表中的“设置”图标在右侧弹出 Drawer 或 Modal,挂载 PluginSettingsWrapper。握手Iframe 加载完成后执行注入将当前用户的 PgSQL 配置注入插件内存。持久化插件内点击保存插件调用 SillyTavern.saveSettings -> Vue 捕获消息 -> Go 写入 PgSQL。
|
||||||
@@ -17,4 +17,5 @@ var (
|
|||||||
characterService = service.ServiceGroupApp.AppServiceGroup.CharacterService
|
characterService = service.ServiceGroupApp.AppServiceGroup.CharacterService
|
||||||
providerService = service.ServiceGroupApp.AppServiceGroup.ProviderService
|
providerService = service.ServiceGroupApp.AppServiceGroup.ProviderService
|
||||||
chatService = service.ServiceGroupApp.AppServiceGroup.ChatService
|
chatService = service.ServiceGroupApp.AppServiceGroup.ChatService
|
||||||
|
// extensionService 已在 extension.go 中声明
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.echol.cn/loser/st/server/global"
|
"git.echol.cn/loser/st/server/global"
|
||||||
@@ -23,9 +18,9 @@ type ExtensionApi struct{}
|
|||||||
|
|
||||||
var extensionService = service.ServiceGroupApp.AppServiceGroup.ExtensionService
|
var extensionService = service.ServiceGroupApp.AppServiceGroup.ExtensionService
|
||||||
|
|
||||||
// CreateExtension 创建/安装扩展
|
// CreateExtension 创建扩展
|
||||||
// @Summary 创建/安装扩展
|
// @Summary 创建扩展
|
||||||
// @Description 创建一个新的扩展或安装扩展
|
// @Description 创建一个新的扩展
|
||||||
// @Tags 扩展管理
|
// @Tags 扩展管理
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@@ -41,14 +36,14 @@ func (a *ExtensionApi) CreateExtension(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
extension, err := extensionService.CreateExtension(userID, &req)
|
ext, err := extensionService.CreateExtension(userID, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("创建扩展失败", zap.Error(err))
|
global.GVA_LOG.Error("创建扩展失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("创建失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("创建失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
sysResponse.OkWithData(response.ToExtensionResponse(ext), c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateExtension 更新扩展
|
// UpdateExtension 更新扩展
|
||||||
@@ -64,14 +59,13 @@ func (a *ExtensionApi) CreateExtension(c *gin.Context) {
|
|||||||
func (a *ExtensionApi) UpdateExtension(c *gin.Context) {
|
func (a *ExtensionApi) UpdateExtension(c *gin.Context) {
|
||||||
userID := middleware.GetAppUserID(c)
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
// 从路径参数获取 ID
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
extensionID := uint(id)
|
extID := uint(id)
|
||||||
|
|
||||||
var req request.UpdateExtensionRequest
|
var req request.UpdateExtensionRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -79,7 +73,7 @@ func (a *ExtensionApi) UpdateExtension(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := extensionService.UpdateExtension(userID, extensionID, &req); err != nil {
|
if err := extensionService.UpdateExtension(userID, extID, &req); err != nil {
|
||||||
global.GVA_LOG.Error("更新扩展失败", zap.Error(err))
|
global.GVA_LOG.Error("更新扩展失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
@@ -88,30 +82,27 @@ func (a *ExtensionApi) UpdateExtension(c *gin.Context) {
|
|||||||
sysResponse.OkWithMessage("更新成功", c)
|
sysResponse.OkWithMessage("更新成功", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteExtension 删除/卸载扩展
|
// DeleteExtension 删除扩展
|
||||||
// @Summary 删除/卸载扩展
|
// @Summary 删除扩展
|
||||||
// @Description 删除扩展
|
// @Description 删除/卸载扩展
|
||||||
// @Tags 扩展管理
|
// @Tags 扩展管理
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path int true "扩展ID"
|
// @Param id path int true "扩展ID"
|
||||||
// @Param deleteFiles query bool false "是否删除文件"
|
|
||||||
// @Success 200 {object} response.Response
|
// @Success 200 {object} response.Response
|
||||||
// @Router /app/extension/:id [delete]
|
// @Router /app/extension/:id [delete]
|
||||||
func (a *ExtensionApi) DeleteExtension(c *gin.Context) {
|
func (a *ExtensionApi) DeleteExtension(c *gin.Context) {
|
||||||
userID := middleware.GetAppUserID(c)
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
// 从路径参数获取 ID
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
extensionID := uint(id)
|
extID := uint(id)
|
||||||
deleteFiles := c.Query("deleteFiles") == "true"
|
|
||||||
|
|
||||||
if err := extensionService.DeleteExtension(userID, extensionID, deleteFiles); err != nil {
|
if err := extensionService.DeleteExtension(userID, extID); err != nil {
|
||||||
global.GVA_LOG.Error("删除扩展失败", zap.Error(err))
|
global.GVA_LOG.Error("删除扩展失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("删除失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("删除失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
@@ -132,23 +123,22 @@ func (a *ExtensionApi) DeleteExtension(c *gin.Context) {
|
|||||||
func (a *ExtensionApi) GetExtension(c *gin.Context) {
|
func (a *ExtensionApi) GetExtension(c *gin.Context) {
|
||||||
userID := middleware.GetAppUserID(c)
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
// 从路径参数获取 ID
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
extensionID := uint(id)
|
extID := uint(id)
|
||||||
|
|
||||||
extension, err := extensionService.GetExtension(userID, extensionID)
|
ext, err := extensionService.GetExtension(userID, extID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("获取扩展失败", zap.Error(err))
|
global.GVA_LOG.Error("获取扩展失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
sysResponse.OkWithData(response.ToExtensionResponse(ext), c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExtensionList 获取扩展列表
|
// GetExtensionList 获取扩展列表
|
||||||
@@ -157,9 +147,14 @@ func (a *ExtensionApi) GetExtension(c *gin.Context) {
|
|||||||
// @Tags 扩展管理
|
// @Tags 扩展管理
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param data query request.ExtensionListRequest true "查询参数"
|
// @Param keyword query string false "关键词"
|
||||||
|
// @Param extensionType query string false "扩展类型"
|
||||||
|
// @Param category query string false "分类"
|
||||||
|
// @Param isEnabled query bool false "是否启用"
|
||||||
|
// @Param page query int false "页码"
|
||||||
|
// @Param pageSize query int false "每页大小"
|
||||||
// @Success 200 {object} response.Response{data=response.ExtensionListResponse}
|
// @Success 200 {object} response.Response{data=response.ExtensionListResponse}
|
||||||
// @Router /app/extension/list [get]
|
// @Router /app/extension [get]
|
||||||
func (a *ExtensionApi) GetExtensionList(c *gin.Context) {
|
func (a *ExtensionApi) GetExtensionList(c *gin.Context) {
|
||||||
userID := middleware.GetAppUserID(c)
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
@@ -177,14 +172,50 @@ func (a *ExtensionApi) GetExtensionList(c *gin.Context) {
|
|||||||
req.PageSize = 20
|
req.PageSize = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := extensionService.GetExtensionList(userID, &req)
|
extensions, total, err := extensionService.GetExtensionList(userID, &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("获取扩展列表失败", zap.Error(err))
|
global.GVA_LOG.Error("获取扩展列表失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sysResponse.OkWithData(result, c)
|
list := make([]response.ExtensionResponse, 0, len(extensions))
|
||||||
|
for i := range extensions {
|
||||||
|
list = append(list, response.ToExtensionResponse(&extensions[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
sysResponse.OkWithData(response.ExtensionListResponse{
|
||||||
|
List: list,
|
||||||
|
Total: total,
|
||||||
|
Page: req.Page,
|
||||||
|
PageSize: req.PageSize,
|
||||||
|
}, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnabledExtensions 获取启用的扩展列表
|
||||||
|
// @Summary 获取启用的扩展列表
|
||||||
|
// @Description 获取当前用户所有已启用的扩展
|
||||||
|
// @Tags 扩展管理
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} response.Response{data=[]response.ExtensionResponse}
|
||||||
|
// @Router /app/extension/enabled [get]
|
||||||
|
func (a *ExtensionApi) GetEnabledExtensions(c *gin.Context) {
|
||||||
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
|
extensions, err := extensionService.GetEnabledExtensions(userID)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("获取启用扩展失败", zap.Error(err))
|
||||||
|
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
list := make([]response.ExtensionResponse, 0, len(extensions))
|
||||||
|
for i := range extensions {
|
||||||
|
list = append(list, response.ToExtensionResponse(&extensions[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
sysResponse.OkWithData(list, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToggleExtension 启用/禁用扩展
|
// ToggleExtension 启用/禁用扩展
|
||||||
@@ -200,96 +231,56 @@ func (a *ExtensionApi) GetExtensionList(c *gin.Context) {
|
|||||||
func (a *ExtensionApi) ToggleExtension(c *gin.Context) {
|
func (a *ExtensionApi) ToggleExtension(c *gin.Context) {
|
||||||
userID := middleware.GetAppUserID(c)
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
// 从路径参数获取 ID
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
extensionID := uint(id)
|
extID := uint(id)
|
||||||
|
|
||||||
var req request.ToggleExtensionRequest
|
var req request.ToggleExtensionRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
sysResponse.FailWithMessage("请求参数错误", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.IsEnabled == nil {
|
|
||||||
sysResponse.FailWithMessage("isEnabled 参数不能为空", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := extensionService.ToggleExtension(userID, extensionID, *req.IsEnabled); err != nil {
|
|
||||||
global.GVA_LOG.Error("切换扩展状态失败", zap.Error(err))
|
|
||||||
sysResponse.FailWithMessage("操作失败: "+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sysResponse.OkWithMessage("操作成功", c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateExtensionSettings 更新扩展配置
|
|
||||||
// @Summary 更新扩展配置
|
|
||||||
// @Description 更新扩展的用户配置
|
|
||||||
// @Tags 扩展管理
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path int true "扩展ID"
|
|
||||||
// @Param data body request.UpdateExtensionSettingsRequest true "配置信息"
|
|
||||||
// @Success 200 {object} response.Response
|
|
||||||
// @Router /app/extension/:id/settings [put]
|
|
||||||
func (a *ExtensionApi) UpdateExtensionSettings(c *gin.Context) {
|
|
||||||
userID := middleware.GetAppUserID(c)
|
|
||||||
|
|
||||||
// 从路径参数获取 ID
|
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
extensionID := uint(id)
|
|
||||||
|
|
||||||
var req request.UpdateExtensionSettingsRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
sysResponse.FailWithMessage(err.Error(), c)
|
sysResponse.FailWithMessage(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := extensionService.UpdateExtensionSettings(userID, extensionID, req.Settings); err != nil {
|
if err := extensionService.ToggleExtension(userID, extID, req.IsEnabled); err != nil {
|
||||||
global.GVA_LOG.Error("更新扩展配置失败", zap.Error(err))
|
global.GVA_LOG.Error("切换扩展状态失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("操作失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sysResponse.OkWithMessage("更新成功", c)
|
msg := "已禁用"
|
||||||
|
if req.IsEnabled {
|
||||||
|
msg = "已启用"
|
||||||
|
}
|
||||||
|
sysResponse.OkWithMessage(msg, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExtensionSettings 获取扩展配置
|
// GetExtensionSettings 获取扩展设置
|
||||||
// @Summary 获取扩展配置
|
// @Summary 获取扩展设置
|
||||||
// @Description 获取扩展的用户配置
|
// @Description 获取扩展的个性化设置
|
||||||
// @Tags 扩展管理
|
// @Tags 扩展管理
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path int true "扩展ID"
|
// @Param id path int true "扩展ID"
|
||||||
// @Success 200 {object} response.Response{data=map[string]interface{}}
|
// @Success 200 {object} response.Response
|
||||||
// @Router /app/extension/:id/settings [get]
|
// @Router /app/extension/:id/settings [get]
|
||||||
func (a *ExtensionApi) GetExtensionSettings(c *gin.Context) {
|
func (a *ExtensionApi) GetExtensionSettings(c *gin.Context) {
|
||||||
userID := middleware.GetAppUserID(c)
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
// 从路径参数获取 ID
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
extensionID := uint(id)
|
extID := uint(id)
|
||||||
|
|
||||||
settings, err := extensionService.GetExtensionSettings(userID, extensionID)
|
settings, err := extensionService.GetExtensionSettings(userID, extID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("获取扩展配置失败", zap.Error(err))
|
global.GVA_LOG.Error("获取扩展设置失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -297,28 +288,63 @@ func (a *ExtensionApi) GetExtensionSettings(c *gin.Context) {
|
|||||||
sysResponse.OkWithData(settings, c)
|
sysResponse.OkWithData(settings, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExtensionManifest 获取扩展 manifest
|
// UpdateExtensionSettings 更新扩展设置
|
||||||
// @Summary 获取扩展 manifest
|
// @Summary 更新扩展设置
|
||||||
// @Description 获取扩展的 manifest.json
|
// @Description 更新扩展的个性化设置
|
||||||
// @Tags 扩展管理
|
// @Tags 扩展管理
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path int true "扩展ID"
|
// @Param id path int true "扩展ID"
|
||||||
// @Success 200 {object} response.Response{data=response.ExtensionManifestResponse}
|
// @Param data body request.UpdateExtensionSettingsRequest true "设置数据"
|
||||||
// @Router /app/extension/:id/manifest [get]
|
// @Success 200 {object} response.Response
|
||||||
func (a *ExtensionApi) GetExtensionManifest(c *gin.Context) {
|
// @Router /app/extension/:id/settings [put]
|
||||||
|
func (a *ExtensionApi) UpdateExtensionSettings(c *gin.Context) {
|
||||||
userID := middleware.GetAppUserID(c)
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
// 从路径参数获取 ID
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
extensionID := uint(id)
|
extID := uint(id)
|
||||||
|
|
||||||
manifest, err := extensionService.GetExtensionManifest(userID, extensionID)
|
var req request.UpdateExtensionSettingsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
sysResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := extensionService.UpdateExtensionSettings(userID, extID, req.Settings); err != nil {
|
||||||
|
global.GVA_LOG.Error("更新扩展设置失败", zap.Error(err))
|
||||||
|
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sysResponse.OkWithMessage("设置已保存", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExtensionManifest 获取扩展 manifest
|
||||||
|
// @Summary 获取扩展 manifest
|
||||||
|
// @Description 获取扩展的 manifest 数据
|
||||||
|
// @Tags 扩展管理
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "扩展ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Router /app/extension/:id/manifest [get]
|
||||||
|
func (a *ExtensionApi) GetExtensionManifest(c *gin.Context) {
|
||||||
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
|
idStr := c.Param("id")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
extID := uint(id)
|
||||||
|
|
||||||
|
manifest, err := extensionService.GetExtensionManifest(userID, extID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("获取扩展 manifest 失败", zap.Error(err))
|
global.GVA_LOG.Error("获取扩展 manifest 失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
||||||
@@ -330,11 +356,11 @@ func (a *ExtensionApi) GetExtensionManifest(c *gin.Context) {
|
|||||||
|
|
||||||
// ImportExtension 导入扩展
|
// ImportExtension 导入扩展
|
||||||
// @Summary 导入扩展
|
// @Summary 导入扩展
|
||||||
// @Description 从文件导入扩展
|
// @Description 从 ZIP 压缩包或 JSON 文件导入扩展
|
||||||
// @Tags 扩展管理
|
// @Tags 扩展管理
|
||||||
// @Accept multipart/form-data
|
// @Accept multipart/form-data
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param file formData file true "扩展文件(manifest.json)"
|
// @Param file formData file true "扩展文件(zip/json)"
|
||||||
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
||||||
// @Router /app/extension/import [post]
|
// @Router /app/extension/import [post]
|
||||||
func (a *ExtensionApi) ImportExtension(c *gin.Context) {
|
func (a *ExtensionApi) ImportExtension(c *gin.Context) {
|
||||||
@@ -343,13 +369,13 @@ func (a *ExtensionApi) ImportExtension(c *gin.Context) {
|
|||||||
// 获取文件
|
// 获取文件
|
||||||
file, err := c.FormFile("file")
|
file, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sysResponse.FailWithMessage("请上传扩展文件", c)
|
sysResponse.FailWithMessage("请上传扩展文件(支持 .zip 或 .json)", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件大小限制(5MB)
|
// 文件大小限制(100MB,zip 包可能较大)
|
||||||
if file.Size > 5<<20 {
|
if file.Size > 100<<20 {
|
||||||
sysResponse.FailWithMessage("文件大小不能超过 5MB", c)
|
sysResponse.FailWithMessage("文件大小不能超过 100MB", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,269 +395,183 @@ func (a *ExtensionApi) ImportExtension(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导入扩展
|
filename := file.Filename
|
||||||
extension, err := extensionService.ImportExtension(userID, fileData)
|
ext, err := extensionService.ImportExtension(userID, filename, fileData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("导入扩展失败", zap.Error(err))
|
global.GVA_LOG.Error("导入扩展失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("导入失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("导入失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
sysResponse.OkWithData(response.ToExtensionResponse(ext), c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportExtension 导出扩展
|
// ExportExtension 导出扩展
|
||||||
// @Summary 导出扩展
|
// @Summary 导出扩展
|
||||||
// @Description 导出扩展为 manifest.json 文件
|
// @Description 导出扩展数据为 JSON
|
||||||
// @Tags 扩展管理
|
// @Tags 扩展管理
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce application/json
|
// @Produce json
|
||||||
// @Param id path int true "扩展ID"
|
// @Param id path int true "扩展ID"
|
||||||
// @Success 200 {object} response.ExtensionManifestResponse
|
// @Success 200 {object} response.ExtensionResponse
|
||||||
// @Router /app/extension/:id/export [get]
|
// @Router /app/extension/:id/export [get]
|
||||||
func (a *ExtensionApi) ExportExtension(c *gin.Context) {
|
func (a *ExtensionApi) ExportExtension(c *gin.Context) {
|
||||||
userID := middleware.GetAppUserID(c)
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
// 从路径参数获取 ID
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
extensionID := uint(id)
|
extID := uint(id)
|
||||||
|
|
||||||
exportData, err := extensionService.ExportExtension(userID, extensionID)
|
exportData, err := extensionService.ExportExtension(userID, extID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("导出扩展失败", zap.Error(err))
|
global.GVA_LOG.Error("导出扩展失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("导出失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("导出失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置响应头
|
sysResponse.OkWithData(exportData, c)
|
||||||
filename := fmt.Sprintf("extension_%d_manifest.json", extensionID)
|
|
||||||
c.Header("Content-Type", "application/json")
|
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
|
||||||
|
|
||||||
// 直接返回 JSON 数据
|
|
||||||
c.Data(http.StatusOK, "application/json", exportData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateExtensionStats 更新扩展统计
|
// InstallFromUrl 从 URL 安装扩展
|
||||||
// @Summary 更新扩展统计
|
// @Summary 从 URL 安装扩展
|
||||||
// @Description 更新扩展的使用统计
|
// @Description 智能识别 Git URL 或 Manifest URL 安装扩展
|
||||||
// @Tags 扩展管理
|
// @Tags 扩展管理
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param data body request.ExtensionStatsRequest true "统计信息"
|
// @Param data body request.InstallExtensionRequest true "安装参数"
|
||||||
// @Success 200 {object} response.Response
|
// @Success 200 {object} response.Response
|
||||||
// @Router /app/extension/stats [post]
|
// @Router /app/extension/install/url [post]
|
||||||
func (a *ExtensionApi) UpdateExtensionStats(c *gin.Context) {
|
func (a *ExtensionApi) InstallFromUrl(c *gin.Context) {
|
||||||
userID := middleware.GetAppUserID(c)
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
var req request.ExtensionStatsRequest
|
var req request.InstallExtensionRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
sysResponse.FailWithMessage(err.Error(), c)
|
sysResponse.FailWithMessage(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := extensionService.UpdateExtensionStats(userID, req.ExtensionID, req.Action, req.Value); err != nil {
|
branch := req.Branch
|
||||||
global.GVA_LOG.Error("更新扩展统计失败", zap.Error(err))
|
if branch == "" {
|
||||||
sysResponse.FailWithMessage("更新失败: "+err.Error(), c)
|
branch = "main"
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sysResponse.OkWithMessage("更新成功", c)
|
// 智能识别 URL 类型并安装
|
||||||
}
|
ext, err := extensionService.InstallExtensionFromURL(userID, req.URL, branch)
|
||||||
|
|
||||||
// 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 {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("获取启用扩展列表失败", zap.Error(err))
|
global.GVA_LOG.Error("从 URL 安装扩展失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("获取失败: "+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sysResponse.OkWithData(extensions, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallExtensionFromURL 智能安装扩展(自动识别 Git URL 或 Manifest URL)
|
|
||||||
// @Summary 智能安装扩展
|
|
||||||
// @Description 自动识别 Git 仓库 URL 或 Manifest.json URL 并安装扩展(兼容 SillyTavern)
|
|
||||||
// @Tags 扩展管理
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param data body request.InstallExtensionFromURLRequest true "安装 URL 信息"
|
|
||||||
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
|
||||||
// @Router /app/extension/install/url [post]
|
|
||||||
func (a *ExtensionApi) InstallExtensionFromURL(c *gin.Context) {
|
|
||||||
userID := middleware.GetAppUserID(c)
|
|
||||||
|
|
||||||
var req request.InstallExtensionFromURLRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
sysResponse.FailWithMessage("请求参数错误: "+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置默认分支
|
|
||||||
if req.Branch == "" {
|
|
||||||
req.Branch = "main"
|
|
||||||
}
|
|
||||||
|
|
||||||
extension, err := extensionService.InstallExtensionFromURL(userID, req.URL, req.Branch)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("从 URL 安装扩展失败", zap.Error(err), zap.String("url", req.URL))
|
|
||||||
sysResponse.FailWithMessage("安装失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("安装失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
sysResponse.OkWithData(response.ToExtensionResponse(ext), c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpgradeExtension 升级扩展版本
|
// InstallFromGit 从 Git URL 安装扩展
|
||||||
// @Summary 升级扩展版本
|
|
||||||
// @Description 根据扩展的安装来源自动选择更新方式(Git pull 或重新下载)
|
|
||||||
// @Tags 扩展管理
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path int true "扩展ID"
|
|
||||||
// @Param data body request.UpdateExtensionRequest false "更新选项"
|
|
||||||
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
|
||||||
// @Router /app/extension/:id/update [post]
|
|
||||||
func (a *ExtensionApi) UpgradeExtension(c *gin.Context) {
|
|
||||||
userID := middleware.GetAppUserID(c)
|
|
||||||
|
|
||||||
// 从路径参数获取 ID
|
|
||||||
idStr := c.Param("id")
|
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
extensionID := uint(id)
|
|
||||||
|
|
||||||
var req request.UpdateExtensionRequest
|
|
||||||
// 允许不传 body(使用默认值)
|
|
||||||
_ = c.ShouldBindJSON(&req)
|
|
||||||
|
|
||||||
extension, err := extensionService.UpgradeExtension(userID, extensionID, req.Force)
|
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error("升级扩展失败", zap.Error(err), zap.Uint("extensionID", extensionID))
|
|
||||||
sysResponse.FailWithMessage("升级失败: "+err.Error(), c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallExtensionFromGit 从 Git URL 安装扩展
|
|
||||||
// @Summary 从 Git URL 安装扩展
|
// @Summary 从 Git URL 安装扩展
|
||||||
// @Description 从 Git 仓库 URL 克隆并安装扩展
|
// @Description 从 Git 仓库克隆安装扩展
|
||||||
// @Tags 扩展管理
|
// @Tags 扩展管理
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param data body request.InstallExtensionFromGitRequest true "Git URL 信息"
|
// @Param data body request.InstallExtensionRequest true "安装参数"
|
||||||
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
||||||
// @Router /app/extension/install/git [post]
|
// @Router /app/extension/install/git [post]
|
||||||
func (a *ExtensionApi) InstallExtensionFromGit(c *gin.Context) {
|
func (a *ExtensionApi) InstallFromGit(c *gin.Context) {
|
||||||
userID := middleware.GetAppUserID(c)
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
var req request.InstallExtensionFromGitRequest
|
var req request.InstallExtensionRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
sysResponse.FailWithMessage(err.Error(), c)
|
sysResponse.FailWithMessage(err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置默认分支
|
branch := req.Branch
|
||||||
if req.Branch == "" {
|
if branch == "" {
|
||||||
req.Branch = "main"
|
branch = "main"
|
||||||
}
|
}
|
||||||
|
|
||||||
extension, err := extensionService.InstallExtensionFromGit(userID, req.GitUrl, req.Branch)
|
// 执行 git clone 安装
|
||||||
|
ext, err := extensionService.InstallExtensionFromGit(userID, req.URL, branch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("从 Git 安装扩展失败", zap.Error(err))
|
global.GVA_LOG.Error("从 Git 安装扩展失败", zap.Error(err))
|
||||||
sysResponse.FailWithMessage("安装失败: "+err.Error(), c)
|
sysResponse.FailWithMessage("安装失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sysResponse.OkWithData(response.ToExtensionResponse(extension), c)
|
sysResponse.OkWithData(response.ToExtensionResponse(ext), c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyExtensionAsset 获取扩展资源文件(从本地文件系统读取)
|
// UpgradeExtension 升级扩展
|
||||||
// @Summary 获取扩展资源文件
|
// @Summary 升级扩展
|
||||||
// @Description 从本地存储读取扩展的 JS/CSS 等资源文件(与原版 SillyTavern 一致,扩展文件存储在本地)
|
// @Description 从源地址重新安装以升级扩展
|
||||||
// @Tags 扩展管理
|
// @Tags 扩展管理
|
||||||
// @Produce octet-stream
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
// @Param id path int true "扩展ID"
|
// @Param id path int true "扩展ID"
|
||||||
// @Param path path string true "资源文件路径"
|
// @Success 200 {object} response.Response{data=response.ExtensionResponse}
|
||||||
// @Success 200 {file} binary
|
// @Router /app/extension/:id/upgrade [post]
|
||||||
// @Router /app/extension/:id/asset/*path [get]
|
func (a *ExtensionApi) UpgradeExtension(c *gin.Context) {
|
||||||
func (a *ExtensionApi) ProxyExtensionAsset(c *gin.Context) {
|
userID := middleware.GetAppUserID(c)
|
||||||
|
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("id")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sysResponse.FailWithMessage("无效的扩展ID", c)
|
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
extensionID := uint(id)
|
extID := uint(id)
|
||||||
|
|
||||||
// 获取资源路径(去掉前导 /)
|
ext, err := extensionService.UpgradeExtension(userID, extID)
|
||||||
assetPath := c.Param("path")
|
|
||||||
if len(assetPath) > 0 && assetPath[0] == '/' {
|
|
||||||
assetPath = assetPath[1:]
|
|
||||||
}
|
|
||||||
if assetPath == "" {
|
|
||||||
sysResponse.FailWithMessage("资源路径不能为空", c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过扩展 ID 查库获取信息(公开路由,不做 userID 过滤)
|
|
||||||
extInfo, err := extensionService.GetExtensionByID(extensionID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sysResponse.FailWithMessage("扩展不存在", c)
|
global.GVA_LOG.Error("升级扩展失败", zap.Error(err))
|
||||||
|
sysResponse.FailWithMessage("升级失败: "+err.Error(), c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从本地文件系统读取资源
|
sysResponse.OkWithData(response.ToExtensionResponse(ext), c)
|
||||||
localPath, err := extensionService.GetExtensionAssetLocalPath(extInfo.Name, assetPath)
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("获取扩展资源失败",
|
sysResponse.FailWithMessage("无效的扩展ID", c)
|
||||||
zap.Error(err),
|
return
|
||||||
zap.String("name", extInfo.Name),
|
}
|
||||||
zap.String("asset", assetPath))
|
extID := uint(id)
|
||||||
sysResponse.FailWithMessage("资源不存在: "+err.Error(), c)
|
|
||||||
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取文件内容
|
if req.Value == 0 {
|
||||||
data, err := os.ReadFile(localPath)
|
req.Value = 1
|
||||||
if err != nil {
|
}
|
||||||
global.GVA_LOG.Error("读取扩展资源文件失败", zap.Error(err), zap.String("path", localPath))
|
|
||||||
sysResponse.FailWithMessage("资源读取失败", c)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据文件扩展名设置正确的 Content-Type
|
sysResponse.OkWithMessage("统计已更新", c)
|
||||||
fileExt := filepath.Ext(assetPath)
|
|
||||||
contentType := mime.TypeByExtension(fileExt)
|
|
||||||
if contentType == "" {
|
|
||||||
contentType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置缓存和 CORS 头
|
|
||||||
c.Header("Content-Type", contentType)
|
|
||||||
c.Header("Cache-Control", "public, max-age=3600")
|
|
||||||
c.Header("Access-Control-Allow-Origin", "*")
|
|
||||||
|
|
||||||
c.Data(http.StatusOK, contentType, data)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,148 +3,65 @@ package app
|
|||||||
import (
|
import (
|
||||||
"git.echol.cn/loser/st/server/global"
|
"git.echol.cn/loser/st/server/global"
|
||||||
"gorm.io/datatypes"
|
"gorm.io/datatypes"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AIExtension 扩展表 (兼容 SillyTavern Extension 规范)
|
// AIExtension 扩展(Extension)表
|
||||||
type AIExtension struct {
|
type AIExtension struct {
|
||||||
global.GVA_MODEL
|
global.GVA_MODEL
|
||||||
UserID uint `json:"userId" gorm:"not null;index;comment:所属用户ID"`
|
UserID uint `json:"userId" gorm:"not null;index;comment:所属用户ID"`
|
||||||
User *AppUser `json:"user" gorm:"foreignKey:UserID"`
|
User *AppUser `json:"user" gorm:"foreignKey:UserID"`
|
||||||
Name string `json:"name" gorm:"type:varchar(500);not null;index;comment:扩展名称"`
|
|
||||||
DisplayName string `json:"displayName" gorm:"type:varchar(500);comment:显示名称"`
|
// 基础信息
|
||||||
Version string `json:"version" gorm:"type:varchar(50);comment:版本号"`
|
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:作者"`
|
Author string `json:"author" gorm:"type:varchar(200);comment:作者"`
|
||||||
Description string `json:"description" gorm:"type:text;comment:扩展描述"`
|
Description string `json:"description" gorm:"type:text;comment:扩展描述"`
|
||||||
Homepage string `json:"homepage" gorm:"type:varchar(1024);comment:主页地址"`
|
Homepage string `json:"homepage" gorm:"type:varchar(500);comment:主页链接"`
|
||||||
Repository string `json:"repository" gorm:"type:varchar(1024);comment:仓库地址"`
|
Repository string `json:"repository" gorm:"type:varchar(500);comment:仓库地址"`
|
||||||
License string `json:"license" gorm:"type:varchar(100);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:标签列表"`
|
Tags datatypes.JSON `json:"tags" gorm:"type:jsonb;comment:标签列表"`
|
||||||
|
|
||||||
// 扩展类型和功能
|
// 依赖管理
|
||||||
ExtensionType string `json:"extensionType" gorm:"type:varchar(50);default:'ui';comment:扩展类型(ui/server/hybrid)"` // ui, server, hybrid
|
Dependencies datatypes.JSON `json:"dependencies" gorm:"type:jsonb;comment:依赖扩展"`
|
||||||
Category string `json:"category" gorm:"type:varchar(100);comment:分类(utilities/themes/integrations/tools)"`
|
Conflicts datatypes.JSON `json:"conflicts" gorm:"type:jsonb;comment:冲突扩展"`
|
||||||
|
|
||||||
// 依赖关系
|
// 文件路径
|
||||||
Dependencies datatypes.JSON `json:"dependencies" gorm:"type:jsonb;comment:依赖的其他扩展"`
|
ScriptPath string `json:"scriptPath" gorm:"type:varchar(500);comment:脚本文件路径"`
|
||||||
Conflicts datatypes.JSON `json:"conflicts" gorm:"type:jsonb;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;not null;comment:manifest.json 完整内容"`
|
ManifestData datatypes.JSON `json:"manifestData" gorm:"type:jsonb;comment:manifest 元数据"`
|
||||||
ScriptPath string `json:"scriptPath" gorm:"type:varchar(1024);comment:主脚本文件路径"`
|
Settings datatypes.JSON `json:"settings" gorm:"type:jsonb;comment:扩展配置"`
|
||||||
StylePath string `json:"stylePath" gorm:"type:varchar(1024);comment:样式文件路径"`
|
|
||||||
AssetsPaths datatypes.JSON `json:"assetsPaths" gorm:"type:jsonb;comment:资源文件路径列表"`
|
|
||||||
|
|
||||||
// 扩展配置
|
|
||||||
Settings datatypes.JSON `json:"settings" gorm:"type:jsonb;comment:扩展配置项"`
|
|
||||||
Options datatypes.JSON `json:"options" 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;index;comment:是否启用"`
|
IsEnabled bool `json:"isEnabled" gorm:"default:false;comment:是否启用"`
|
||||||
IsInstalled bool `json:"isInstalled" gorm:"default:true;index;comment:是否已安装"`
|
IsInstalled bool `json:"isInstalled" gorm:"default:true;comment:是否已安装"`
|
||||||
IsSystemExt bool `json:"isSystemExt" gorm:"default:false;comment:是否系统内置扩展"`
|
IsSystemExt bool `json:"isSystemExt" gorm:"default:false;comment:是否系统扩展"`
|
||||||
InstallSource string `json:"installSource" gorm:"type:varchar(500);comment:安装来源(url/git/file/marketplace)"`
|
|
||||||
SourceURL string `json:"sourceUrl" gorm:"type:varchar(1000);comment:原始安装 URL(用于更新)"`
|
|
||||||
Branch string `json:"branch" gorm:"type:varchar(100);comment:Git 分支名称"`
|
|
||||||
InstallDate time.Time `json:"installDate" gorm:"comment:安装日期"`
|
|
||||||
LastEnabled time.Time `json:"lastEnabled" gorm:"comment:最后启用时间"`
|
|
||||||
|
|
||||||
// 更新相关
|
// 安装信息
|
||||||
|
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:是否自动更新"`
|
AutoUpdate bool `json:"autoUpdate" gorm:"default:false;comment:是否自动更新"`
|
||||||
LastUpdateCheck *time.Time `json:"lastUpdateCheck" gorm:"comment:最后检查更新时间"`
|
LastUpdateCheck *int64 `json:"lastUpdateCheck" gorm:"comment:最后检查更新时间戳"`
|
||||||
AvailableVersion string `json:"availableVersion" gorm:"type:varchar(50);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:使用次数"`
|
UsageCount int `json:"usageCount" gorm:"default:0;comment:使用次数"`
|
||||||
ErrorCount int `json:"errorCount" gorm:"default:0;comment:错误次数"`
|
ErrorCount int `json:"errorCount" gorm:"default:0;comment:错误次数"`
|
||||||
LoadTime int `json:"loadTime" gorm:"default:0;comment:平均加载时间(ms)"`
|
LoadTime int `json:"loadTime" gorm:"default:0;comment:加载时间(ms)"`
|
||||||
|
|
||||||
// 元数据
|
|
||||||
Metadata datatypes.JSON `json:"metadata" gorm:"type:jsonb;comment:扩展元数据"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AIExtension) TableName() string {
|
func (AIExtension) TableName() string {
|
||||||
return "ai_extensions"
|
return "ai_extensions"
|
||||||
}
|
}
|
||||||
|
|
||||||
// AIExtensionManifest 扩展清单结构 (对应 manifest.json,兼容 SillyTavern 格式)
|
|
||||||
type AIExtensionManifest struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
DisplayName string `json:"display_name,omitempty"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
Homepage string `json:"homepage,omitempty"`
|
|
||||||
HomePage string `json:"homePage,omitempty"` // SillyTavern 兼容(驼峰命名)
|
|
||||||
Repository string `json:"repository,omitempty"`
|
|
||||||
License string `json:"license,omitempty"`
|
|
||||||
Tags []string `json:"tags,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"` // ui, server, hybrid
|
|
||||||
Category string `json:"category,omitempty"` // utilities, themes, integrations, tools
|
|
||||||
Dependencies map[string]string `json:"dependencies,omitempty"` // {"extension-name": ">=1.0.0"}
|
|
||||||
Conflicts []string `json:"conflicts,omitempty"`
|
|
||||||
Entry string `json:"entry,omitempty"` // 主入口文件
|
|
||||||
Js string `json:"js,omitempty"` // SillyTavern 兼容: JS 入口文件
|
|
||||||
Style string `json:"style,omitempty"` // 样式文件
|
|
||||||
Css string `json:"css,omitempty"` // SillyTavern 兼容: CSS 样式文件
|
|
||||||
Assets []string `json:"assets,omitempty"` // 资源文件列表
|
|
||||||
Settings map[string]interface{} `json:"settings,omitempty"` // 默认设置
|
|
||||||
Options map[string]interface{} `json:"options,omitempty"` // 扩展选项
|
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"` // 扩展元数据
|
|
||||||
AutoUpdate bool `json:"auto_update,omitempty"` // 是否自动更新(SillyTavern 兼容)
|
|
||||||
InlineScript string `json:"inline_script,omitempty"` // 内联脚本(SillyTavern 兼容)
|
|
||||||
Requires []string `json:"requires,omitempty"` // SillyTavern 兼容: 依赖列表
|
|
||||||
Optional []string `json:"optional,omitempty"` // SillyTavern 兼容: 可选依赖
|
|
||||||
LoadingOrder int `json:"loading_order,omitempty"` // SillyTavern 兼容: 加载顺序
|
|
||||||
I18n map[string]string `json:"i18n,omitempty"` // SillyTavern 兼容: 国际化文件
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEffectiveName 获取有效名称,兼容 SillyTavern manifest 没有 name 字段的情况
|
|
||||||
func (m *AIExtensionManifest) GetEffectiveName() string {
|
|
||||||
if m.Name != "" {
|
|
||||||
return m.Name
|
|
||||||
}
|
|
||||||
if m.DisplayName != "" {
|
|
||||||
return m.DisplayName
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEffectiveHomepage 获取有效主页地址
|
|
||||||
func (m *AIExtensionManifest) GetEffectiveHomepage() string {
|
|
||||||
if m.Homepage != "" {
|
|
||||||
return m.Homepage
|
|
||||||
}
|
|
||||||
return m.HomePage
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEffectiveEntry 获取有效的 JS 入口文件路径
|
|
||||||
func (m *AIExtensionManifest) GetEffectiveEntry() string {
|
|
||||||
if m.Entry != "" {
|
|
||||||
return m.Entry
|
|
||||||
}
|
|
||||||
return m.Js
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEffectiveStyle 获取有效的 CSS 样式文件路径
|
|
||||||
func (m *AIExtensionManifest) GetEffectiveStyle() string {
|
|
||||||
if m.Style != "" {
|
|
||||||
return m.Style
|
|
||||||
}
|
|
||||||
return m.Css
|
|
||||||
}
|
|
||||||
|
|
||||||
// AIExtensionSettings 用户的扩展配置(已废弃,配置现在直接存储在 AIExtension.Settings 中)
|
|
||||||
// type AIExtensionSettings struct {
|
|
||||||
// global.GVA_MODEL
|
|
||||||
// UserID uint `json:"userId" gorm:"not null;index:idx_user_ext,unique;comment:用户ID"`
|
|
||||||
// User *AppUser `json:"user" gorm:"foreignKey:UserID"`
|
|
||||||
// ExtensionID uint `json:"extensionId" gorm:"not null;index:idx_user_ext,unique;comment:扩展ID"`
|
|
||||||
// Extension *AIExtension `json:"extension" gorm:"foreignKey:ExtensionID"`
|
|
||||||
// Settings datatypes.JSON `json:"settings" gorm:"type:jsonb;not null;comment:用户配置"`
|
|
||||||
// IsEnabled bool `json:"isEnabled" gorm:"default:true;comment:用户是否启用"`
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// func (AIExtensionSettings) TableName() string {
|
|
||||||
// return "ai_extension_settings"
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
common "git.echol.cn/loser/st/server/model/common/request"
|
common "git.echol.cn/loser/st/server/model/common/request"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateExtensionRequest 创建/安装扩展请求
|
// CreateExtensionRequest 创建扩展请求
|
||||||
type CreateExtensionRequest struct {
|
type CreateExtensionRequest struct {
|
||||||
Name string `json:"name" binding:"required,min=1,max=500"`
|
Name string `json:"name" binding:"required"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
@@ -15,93 +15,69 @@ type CreateExtensionRequest struct {
|
|||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
License string `json:"license"`
|
License string `json:"license"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
ExtensionType string `json:"extensionType" binding:"required,oneof=ui server hybrid"`
|
ExtensionType string `json:"extensionType" binding:"required"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Dependencies map[string]string `json:"dependencies"`
|
Dependencies map[string]string `json:"dependencies"`
|
||||||
Conflicts []string `json:"conflicts"`
|
Conflicts []string `json:"conflicts"`
|
||||||
ManifestData map[string]interface{} `json:"manifestData" binding:"required"`
|
ManifestData map[string]interface{} `json:"manifestData"`
|
||||||
ScriptPath string `json:"scriptPath"`
|
ScriptPath string `json:"scriptPath"`
|
||||||
StylePath string `json:"stylePath"`
|
StylePath string `json:"stylePath"`
|
||||||
AssetsPaths []string `json:"assetsPaths"`
|
AssetPaths []string `json:"assetPaths"`
|
||||||
Settings map[string]interface{} `json:"settings"`
|
Settings map[string]interface{} `json:"settings"`
|
||||||
Options map[string]interface{} `json:"options"`
|
Options map[string]interface{} `json:"options"`
|
||||||
InstallSource string `json:"installSource"`
|
InstallSource string `json:"installSource"`
|
||||||
SourceURL string `json:"sourceUrl"` // 原始安装 URL(用于更新)
|
SourceURL string `json:"sourceUrl"`
|
||||||
Branch string `json:"branch"` // Git 分支
|
Branch string `json:"branch"`
|
||||||
AutoUpdate bool `json:"autoUpdate"` // 是否自动更新
|
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"`
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtensionListRequest 扩展列表查询请求
|
// ExtensionListRequest 扩展列表查询请求
|
||||||
type ExtensionListRequest struct {
|
type ExtensionListRequest struct {
|
||||||
common.PageInfo
|
common.PageInfo
|
||||||
Name string `json:"name" form:"name"` // 扩展名称(模糊搜索)
|
Keyword string `json:"keyword" form:"keyword"` // 搜索关键词
|
||||||
|
Name string `json:"name" form:"name"` // 扩展名称
|
||||||
ExtensionType string `json:"extensionType" form:"extensionType"` // 扩展类型
|
ExtensionType string `json:"extensionType" form:"extensionType"` // 扩展类型
|
||||||
Category string `json:"category" form:"category"` // 分类
|
Category string `json:"category" form:"category"` // 分类
|
||||||
IsEnabled *bool `json:"isEnabled" form:"isEnabled"` // 是否启用
|
IsEnabled *bool `json:"isEnabled" form:"isEnabled"` // 是否启用
|
||||||
IsInstalled *bool `json:"isInstalled" form:"isInstalled"` // 是否已安装
|
IsInstalled *bool `json:"isInstalled" form:"isInstalled"` // 是否已安装
|
||||||
Tag string `json:"tag" form:"tag"` // 标签过滤
|
Tag string `json:"tag" form:"tag"` // 标签
|
||||||
}
|
|
||||||
|
|
||||||
// InstallExtensionRequest 安装扩展请求
|
|
||||||
type InstallExtensionRequest struct {
|
|
||||||
Source string `json:"source" binding:"required,oneof=url file marketplace"` // 安装来源
|
|
||||||
URL string `json:"url"` // URL 安装
|
|
||||||
ManifestData []byte `json:"manifestData"` // 文件安装
|
|
||||||
MarketplaceID string `json:"marketplaceId"` // 市场安装
|
|
||||||
}
|
|
||||||
|
|
||||||
// UninstallExtensionRequest 卸载扩展请求
|
|
||||||
type UninstallExtensionRequest struct {
|
|
||||||
DeleteFiles bool `json:"deleteFiles"` // 是否删除文件
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToggleExtensionRequest 启用/禁用扩展请求
|
// ToggleExtensionRequest 启用/禁用扩展请求
|
||||||
type ToggleExtensionRequest struct {
|
type ToggleExtensionRequest struct {
|
||||||
IsEnabled *bool `json:"isEnabled"` // 使用指针类型,允许 false 值
|
IsEnabled bool `json:"isEnabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateExtensionSettingsRequest 更新扩展配置请求
|
// UpdateExtensionSettingsRequest 更新扩展设置请求
|
||||||
type UpdateExtensionSettingsRequest struct {
|
type UpdateExtensionSettingsRequest struct {
|
||||||
Settings map[string]interface{} `json:"settings" binding:"required"`
|
Settings map[string]interface{} `json:"settings" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportExtensionRequest 导入扩展请求
|
// InstallExtensionRequest 从URL安装扩展请求
|
||||||
type ImportExtensionRequest struct {
|
type InstallExtensionRequest struct {
|
||||||
Format string `json:"format" binding:"required,oneof=zip folder"`
|
URL string `json:"url" binding:"required"`
|
||||||
}
|
Branch string `json:"branch"`
|
||||||
|
|
||||||
// ExportExtensionRequest 导出扩展请求
|
|
||||||
type ExportExtensionRequest struct {
|
|
||||||
Format string `json:"format" binding:"required,oneof=zip folder"`
|
|
||||||
IncludeAssets bool `json:"includeAssets"` // 是否包含资源文件
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtensionStatsRequest 扩展统计请求
|
|
||||||
type ExtensionStatsRequest struct {
|
|
||||||
ExtensionID uint `json:"extensionId" binding:"required"`
|
|
||||||
Action string `json:"action" binding:"required,oneof=usage error load"` // 统计类型
|
|
||||||
Value int `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallExtensionFromGitRequest 从 Git URL 安装扩展请求
|
|
||||||
type InstallExtensionFromGitRequest struct {
|
|
||||||
GitUrl string `json:"gitUrl" binding:"required"` // Git 仓库 URL
|
|
||||||
Branch string `json:"branch" binding:"omitempty,max=100"` // 分支名称(可选,默认 main)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallExtensionFromURLRequest 从 URL 安装扩展请求(智能识别 Git URL 或 Manifest URL)
|
|
||||||
type InstallExtensionFromURLRequest struct {
|
|
||||||
URL string `json:"url" binding:"required"` // Git 仓库 URL 或 Manifest.json URL
|
|
||||||
Branch string `json:"branch"` // Git 分支名称(可选,默认 main)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateExtensionRequest 更新扩展请求
|
|
||||||
type UpdateExtensionRequest struct {
|
|
||||||
Force bool `json:"force"` // 是否强制更新(忽略本地修改)
|
|
||||||
DisplayName string `json:"displayName"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Settings map[string]interface{} `json:"settings"`
|
|
||||||
Options map[string]interface{} `json:"options"`
|
|
||||||
Metadata map[string]interface{} `json:"metadata"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package response
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"git.echol.cn/loser/st/server/model/app"
|
"git.echol.cn/loser/st/server/model/app"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExtensionResponse 扩展响应
|
// ExtensionResponse 扩展响应
|
||||||
@@ -23,12 +23,13 @@ type ExtensionResponse struct {
|
|||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Dependencies map[string]string `json:"dependencies"`
|
Dependencies map[string]string `json:"dependencies"`
|
||||||
Conflicts []string `json:"conflicts"`
|
Conflicts []string `json:"conflicts"`
|
||||||
ManifestData map[string]interface{} `json:"manifestData"`
|
|
||||||
ScriptPath string `json:"scriptPath"`
|
ScriptPath string `json:"scriptPath"`
|
||||||
StylePath string `json:"stylePath"`
|
StylePath string `json:"stylePath"`
|
||||||
AssetsPaths []string `json:"assetsPaths"`
|
AssetPaths []string `json:"assetPaths"`
|
||||||
|
ManifestData map[string]interface{} `json:"manifestData"`
|
||||||
Settings map[string]interface{} `json:"settings"`
|
Settings map[string]interface{} `json:"settings"`
|
||||||
Options map[string]interface{} `json:"options"`
|
Options map[string]interface{} `json:"options"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
IsEnabled bool `json:"isEnabled"`
|
IsEnabled bool `json:"isEnabled"`
|
||||||
IsInstalled bool `json:"isInstalled"`
|
IsInstalled bool `json:"isInstalled"`
|
||||||
IsSystemExt bool `json:"isSystemExt"`
|
IsSystemExt bool `json:"isSystemExt"`
|
||||||
@@ -36,16 +37,15 @@ type ExtensionResponse struct {
|
|||||||
SourceURL string `json:"sourceUrl"`
|
SourceURL string `json:"sourceUrl"`
|
||||||
Branch string `json:"branch"`
|
Branch string `json:"branch"`
|
||||||
AutoUpdate bool `json:"autoUpdate"`
|
AutoUpdate bool `json:"autoUpdate"`
|
||||||
InstallDate time.Time `json:"installDate"`
|
LastUpdateCheck *int64 `json:"lastUpdateCheck"`
|
||||||
LastEnabled time.Time `json:"lastEnabled"`
|
|
||||||
LastUpdateCheck *time.Time `json:"lastUpdateCheck"`
|
|
||||||
AvailableVersion string `json:"availableVersion"`
|
AvailableVersion string `json:"availableVersion"`
|
||||||
|
InstallDate *int64 `json:"installDate"`
|
||||||
|
LastEnabled *int64 `json:"lastEnabled"`
|
||||||
UsageCount int `json:"usageCount"`
|
UsageCount int `json:"usageCount"`
|
||||||
ErrorCount int `json:"errorCount"`
|
ErrorCount int `json:"errorCount"`
|
||||||
LoadTime int `json:"loadTime"`
|
LoadTime int `json:"loadTime"`
|
||||||
Metadata map[string]interface{} `json:"metadata"`
|
CreatedAt int64 `json:"createdAt"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
UpdatedAt int64 `json:"updatedAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtensionListResponse 扩展列表响应
|
// ExtensionListResponse 扩展列表响应
|
||||||
@@ -56,105 +56,20 @@ type ExtensionListResponse struct {
|
|||||||
PageSize int `json:"pageSize"`
|
PageSize int `json:"pageSize"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtensionManifestResponse manifest.json 响应
|
// unmarshalJSONB 通用 JSONB 反序列化辅助函数
|
||||||
type ExtensionManifestResponse struct {
|
func unmarshalJSONB[T any](data []byte, fallback T) T {
|
||||||
Name string `json:"name"`
|
if len(data) == 0 {
|
||||||
DisplayName string `json:"display_name,omitempty"`
|
return fallback
|
||||||
Version string `json:"version"`
|
}
|
||||||
Description string `json:"description"`
|
var result T
|
||||||
Author string `json:"author"`
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
Homepage string `json:"homepage,omitempty"`
|
return fallback
|
||||||
Repository string `json:"repository,omitempty"`
|
}
|
||||||
License string `json:"license,omitempty"`
|
return result
|
||||||
Tags []string `json:"tags,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Category string `json:"category,omitempty"`
|
|
||||||
Dependencies map[string]string `json:"dependencies,omitempty"`
|
|
||||||
Conflicts []string `json:"conflicts,omitempty"`
|
|
||||||
Entry string `json:"entry,omitempty"`
|
|
||||||
Style string `json:"style,omitempty"`
|
|
||||||
Assets []string `json:"assets,omitempty"`
|
|
||||||
Settings map[string]interface{} `json:"settings,omitempty"`
|
|
||||||
Options map[string]interface{} `json:"options,omitempty"`
|
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtensionStatsResponse 扩展统计响应
|
// ToExtensionResponse 将 AIExtension 转换为 ExtensionResponse
|
||||||
type ExtensionStatsResponse struct {
|
|
||||||
ExtensionID uint `json:"extensionId"`
|
|
||||||
ExtensionName string `json:"extensionName"`
|
|
||||||
UsageCount int `json:"usageCount"`
|
|
||||||
ErrorCount int `json:"errorCount"`
|
|
||||||
LoadTime int `json:"loadTime"`
|
|
||||||
LastUsed time.Time `json:"lastUsed"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToExtensionResponse 转换为扩展响应
|
|
||||||
func ToExtensionResponse(ext *app.AIExtension) ExtensionResponse {
|
func ToExtensionResponse(ext *app.AIExtension) ExtensionResponse {
|
||||||
var tags []string
|
|
||||||
if ext.Tags != nil {
|
|
||||||
_ = json.Unmarshal([]byte(ext.Tags), &tags)
|
|
||||||
}
|
|
||||||
if tags == nil {
|
|
||||||
tags = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dependencies map[string]string
|
|
||||||
if ext.Dependencies != nil {
|
|
||||||
_ = json.Unmarshal([]byte(ext.Dependencies), &dependencies)
|
|
||||||
}
|
|
||||||
if dependencies == nil {
|
|
||||||
dependencies = map[string]string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var conflicts []string
|
|
||||||
if ext.Conflicts != nil {
|
|
||||||
_ = json.Unmarshal([]byte(ext.Conflicts), &conflicts)
|
|
||||||
}
|
|
||||||
if conflicts == nil {
|
|
||||||
conflicts = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var manifestData map[string]interface{}
|
|
||||||
if ext.ManifestData != nil {
|
|
||||||
_ = json.Unmarshal([]byte(ext.ManifestData), &manifestData)
|
|
||||||
}
|
|
||||||
if manifestData == nil {
|
|
||||||
manifestData = map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var assetsPaths []string
|
|
||||||
if ext.AssetsPaths != nil {
|
|
||||||
_ = json.Unmarshal([]byte(ext.AssetsPaths), &assetsPaths)
|
|
||||||
}
|
|
||||||
if assetsPaths == nil {
|
|
||||||
assetsPaths = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var settings map[string]interface{}
|
|
||||||
if ext.Settings != nil {
|
|
||||||
_ = json.Unmarshal([]byte(ext.Settings), &settings)
|
|
||||||
}
|
|
||||||
if settings == nil {
|
|
||||||
settings = map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var options map[string]interface{}
|
|
||||||
if ext.Options != nil {
|
|
||||||
_ = json.Unmarshal([]byte(ext.Options), &options)
|
|
||||||
}
|
|
||||||
if options == nil {
|
|
||||||
options = map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata map[string]interface{}
|
|
||||||
if ext.Metadata != nil {
|
|
||||||
_ = json.Unmarshal([]byte(ext.Metadata), &metadata)
|
|
||||||
}
|
|
||||||
if metadata == nil {
|
|
||||||
metadata = map[string]interface{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExtensionResponse{
|
return ExtensionResponse{
|
||||||
ID: ext.ID,
|
ID: ext.ID,
|
||||||
UserID: ext.UserID,
|
UserID: ext.UserID,
|
||||||
@@ -166,17 +81,18 @@ func ToExtensionResponse(ext *app.AIExtension) ExtensionResponse {
|
|||||||
Homepage: ext.Homepage,
|
Homepage: ext.Homepage,
|
||||||
Repository: ext.Repository,
|
Repository: ext.Repository,
|
||||||
License: ext.License,
|
License: ext.License,
|
||||||
Tags: tags,
|
Tags: unmarshalJSONB(ext.Tags, []string{}),
|
||||||
ExtensionType: ext.ExtensionType,
|
ExtensionType: ext.ExtensionType,
|
||||||
Category: ext.Category,
|
Category: ext.Category,
|
||||||
Dependencies: dependencies,
|
Dependencies: unmarshalJSONB(ext.Dependencies, map[string]string{}),
|
||||||
Conflicts: conflicts,
|
Conflicts: unmarshalJSONB(ext.Conflicts, []string{}),
|
||||||
ManifestData: manifestData,
|
|
||||||
ScriptPath: ext.ScriptPath,
|
ScriptPath: ext.ScriptPath,
|
||||||
StylePath: ext.StylePath,
|
StylePath: ext.StylePath,
|
||||||
AssetsPaths: assetsPaths,
|
AssetPaths: unmarshalJSONB(ext.AssetPaths, []string{}),
|
||||||
Settings: settings,
|
ManifestData: unmarshalJSONB(ext.ManifestData, map[string]interface{}{}),
|
||||||
Options: options,
|
Settings: unmarshalJSONB(ext.Settings, map[string]interface{}{}),
|
||||||
|
Options: unmarshalJSONB(ext.Options, map[string]interface{}{}),
|
||||||
|
Metadata: unmarshalJSONB(ext.Metadata, map[string]interface{}{}),
|
||||||
IsEnabled: ext.IsEnabled,
|
IsEnabled: ext.IsEnabled,
|
||||||
IsInstalled: ext.IsInstalled,
|
IsInstalled: ext.IsInstalled,
|
||||||
IsSystemExt: ext.IsSystemExt,
|
IsSystemExt: ext.IsSystemExt,
|
||||||
@@ -184,15 +100,14 @@ func ToExtensionResponse(ext *app.AIExtension) ExtensionResponse {
|
|||||||
SourceURL: ext.SourceURL,
|
SourceURL: ext.SourceURL,
|
||||||
Branch: ext.Branch,
|
Branch: ext.Branch,
|
||||||
AutoUpdate: ext.AutoUpdate,
|
AutoUpdate: ext.AutoUpdate,
|
||||||
InstallDate: ext.InstallDate,
|
|
||||||
LastEnabled: ext.LastEnabled,
|
|
||||||
LastUpdateCheck: ext.LastUpdateCheck,
|
LastUpdateCheck: ext.LastUpdateCheck,
|
||||||
AvailableVersion: ext.AvailableVersion,
|
AvailableVersion: ext.AvailableVersion,
|
||||||
|
InstallDate: ext.InstallDate,
|
||||||
|
LastEnabled: ext.LastEnabled,
|
||||||
UsageCount: ext.UsageCount,
|
UsageCount: ext.UsageCount,
|
||||||
ErrorCount: ext.ErrorCount,
|
ErrorCount: ext.ErrorCount,
|
||||||
LoadTime: ext.LoadTime,
|
LoadTime: ext.LoadTime,
|
||||||
Metadata: metadata,
|
CreatedAt: ext.CreatedAt.Unix(),
|
||||||
CreatedAt: ext.CreatedAt,
|
UpdatedAt: ext.UpdatedAt.Unix(),
|
||||||
UpdatedAt: ext.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,33 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.echol.cn/loser/st/server/api/v1"
|
v1 "git.echol.cn/loser/st/server/api/v1"
|
||||||
"git.echol.cn/loser/st/server/middleware"
|
"git.echol.cn/loser/st/server/middleware"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExtensionRouter struct{}
|
type ExtensionRouter struct{}
|
||||||
|
|
||||||
|
// InitExtensionRouter 初始化扩展路由
|
||||||
func (r *ExtensionRouter) InitExtensionRouter(Router *gin.RouterGroup) {
|
func (r *ExtensionRouter) InitExtensionRouter(Router *gin.RouterGroup) {
|
||||||
extensionRouter := Router.Group("extension").Use(middleware.AppJWTAuth())
|
extRouter := Router.Group("extension").Use(middleware.AppJWTAuth())
|
||||||
extensionApi := v1.ApiGroupApp.AppApiGroup.ExtensionApi
|
extApi := v1.ApiGroupApp.AppApiGroup.ExtensionApi
|
||||||
|
|
||||||
{
|
{
|
||||||
// 扩展管理
|
extRouter.POST("", extApi.CreateExtension) // 创建扩展
|
||||||
extensionRouter.POST("", extensionApi.CreateExtension) // 创建/安装扩展
|
extRouter.PUT(":id", extApi.UpdateExtension) // 更新扩展
|
||||||
extensionRouter.PUT("/:id", extensionApi.UpdateExtension) // 更新扩展
|
extRouter.DELETE(":id", extApi.DeleteExtension) // 删除扩展
|
||||||
extensionRouter.DELETE("/:id", extensionApi.DeleteExtension) // 删除/卸载扩展
|
extRouter.GET(":id", extApi.GetExtension) // 获取扩展详情
|
||||||
extensionRouter.GET("/:id", extensionApi.GetExtension) // 获取扩展详情
|
extRouter.GET("", extApi.GetExtensionList) // 获取扩展列表
|
||||||
extensionRouter.GET("/list", extensionApi.GetExtensionList) // 获取扩展列表
|
extRouter.GET("enabled", extApi.GetEnabledExtensions) // 获取启用的扩展
|
||||||
extensionRouter.GET("/enabled", extensionApi.GetEnabledExtensions) // 获取启用的扩展列表
|
extRouter.POST(":id/toggle", extApi.ToggleExtension) // 启用/禁用扩展
|
||||||
|
extRouter.GET(":id/settings", extApi.GetExtensionSettings) // 获取扩展设置
|
||||||
// 扩展操作
|
extRouter.PUT(":id/settings", extApi.UpdateExtensionSettings) // 更新扩展设置
|
||||||
extensionRouter.POST("/:id/toggle", extensionApi.ToggleExtension) // 启用/禁用扩展
|
extRouter.GET(":id/manifest", extApi.GetExtensionManifest) // 获取 manifest
|
||||||
extensionRouter.POST("/:id/update", extensionApi.UpgradeExtension) // 升级扩展版本
|
extRouter.POST("install/url", extApi.InstallFromUrl) // 从 URL 安装
|
||||||
|
extRouter.POST("install/git", extApi.InstallFromGit) // 从 Git URL 安装
|
||||||
// 扩展配置
|
extRouter.POST(":id/upgrade", extApi.UpgradeExtension) // 升级扩展
|
||||||
extensionRouter.GET("/:id/settings", extensionApi.GetExtensionSettings) // 获取扩展配置
|
extRouter.POST("import", extApi.ImportExtension) // 文件导入(zip/文件夹)
|
||||||
extensionRouter.PUT("/:id/settings", extensionApi.UpdateExtensionSettings) // 更新扩展配置
|
extRouter.GET(":id/export", extApi.ExportExtension) // 导出扩展
|
||||||
|
extRouter.POST(":id/stats", extApi.UpdateStats) // 更新统计
|
||||||
// 扩展元数据
|
|
||||||
extensionRouter.GET("/:id/manifest", extensionApi.GetExtensionManifest) // 获取 manifest.json
|
|
||||||
|
|
||||||
// 导入导出
|
|
||||||
extensionRouter.POST("/import", extensionApi.ImportExtension) // 导入扩展
|
|
||||||
extensionRouter.GET("/:id/export", extensionApi.ExportExtension) // 导出扩展
|
|
||||||
|
|
||||||
// 安装方式
|
|
||||||
extensionRouter.POST("/install/url", extensionApi.InstallExtensionFromURL) // 从 URL 安装扩展(后端代理)
|
|
||||||
extensionRouter.POST("/install/git", extensionApi.InstallExtensionFromGit) // 从 Git URL 安装扩展
|
|
||||||
|
|
||||||
// 统计
|
|
||||||
extensionRouter.POST("/stats", extensionApi.UpdateExtensionStats) // 更新扩展统计
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扩展资源文件 - 公开路由(不需要鉴权)
|
|
||||||
// 原因:<script type="module"> 标签无法携带 JWT header,
|
|
||||||
// 且 ES module 的 import 语句也无法携带认证信息。
|
|
||||||
// 与原版 SillyTavern 一致:扩展文件作为公开静态资源提供。
|
|
||||||
extensionPublicRouter := Router.Group("extension")
|
|
||||||
{
|
|
||||||
extensionPublicRouter.GET("/:id/asset/*path", extensionApi.ProxyExtensionAsset)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
764
server/service/app/extension_installer.go
Normal file
764
server/service/app/extension_installer.go
Normal file
@@ -0,0 +1,764 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.echol.cn/loser/st/server/global"
|
||||||
|
"git.echol.cn/loser/st/server/model/app"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/datatypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// extensionsBaseDir 扩展文件存放根目录(与 router.go 中的静态服务路径一致)
|
||||||
|
const extensionsBaseDir = "data/st-core-scripts/scripts/extensions/third-party"
|
||||||
|
|
||||||
|
// STManifest SillyTavern 扩展 manifest.json 结构
|
||||||
|
type STManifest struct {
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
Loading string `json:"loading_order"` // 加载顺序
|
||||||
|
Requires []string `json:"requires"`
|
||||||
|
Optional []string `json:"optional"`
|
||||||
|
Js string `json:"js"` // 入口 JS 文件
|
||||||
|
Css string `json:"css"` // 入口 CSS 文件
|
||||||
|
Author string `json:"author"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Homepages string `json:"homepages"`
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
AutoUpdate bool `json:"auto_update"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Settings map[string]interface{} `json:"settings"`
|
||||||
|
Raw map[string]interface{} `json:"-"` // 原始 JSON 数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// getExtensionDir 获取指定扩展的文件系统目录
|
||||||
|
func getExtensionDir(extName string) string {
|
||||||
|
return filepath.Join(extensionsBaseDir, extName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureExtensionsBaseDir 确保扩展基础目录存在
|
||||||
|
func ensureExtensionsBaseDir() error {
|
||||||
|
return os.MkdirAll(extensionsBaseDir, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseManifestFile 从扩展目录中读取并解析 manifest.json
|
||||||
|
func parseManifestFile(dir string) (*STManifest, error) {
|
||||||
|
manifestPath := filepath.Join(dir, "manifest.json")
|
||||||
|
data, err := os.ReadFile(manifestPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("无法读取 manifest.json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest STManifest
|
||||||
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析 manifest.json 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留原始 JSON 用于存储到数据库
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &raw); err == nil {
|
||||||
|
manifest.Raw = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
return &manifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseManifestBytes 从字节数组解析 manifest.json
|
||||||
|
func parseManifestBytes(data []byte) (*STManifest, error) {
|
||||||
|
var manifest STManifest
|
||||||
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析 manifest.json 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &raw); err == nil {
|
||||||
|
manifest.Raw = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
return &manifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallExtensionFromGit 从 Git 仓库安装扩展
|
||||||
|
func (s *ExtensionService) InstallExtensionFromGit(userID uint, gitURL string, branch string) (*app.AIExtension, error) {
|
||||||
|
if branch == "" {
|
||||||
|
branch = "main"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureExtensionsBaseDir(); err != nil {
|
||||||
|
return nil, fmt.Errorf("创建扩展目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 URL 提取扩展名
|
||||||
|
extName := extractRepoName(gitURL)
|
||||||
|
if extName == "" {
|
||||||
|
return nil, errors.New("无法从 URL 中提取扩展名")
|
||||||
|
}
|
||||||
|
|
||||||
|
extDir := getExtensionDir(extName)
|
||||||
|
|
||||||
|
// 检查目录是否已存在
|
||||||
|
if _, err := os.Stat(extDir); err == nil {
|
||||||
|
return nil, fmt.Errorf("扩展 '%s' 已存在,请先删除或使用升级功能", extName)
|
||||||
|
}
|
||||||
|
|
||||||
|
global.GVA_LOG.Info("从 Git 安装扩展",
|
||||||
|
zap.String("url", gitURL),
|
||||||
|
zap.String("branch", branch),
|
||||||
|
zap.String("dir", extDir),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 执行 git clone
|
||||||
|
cmd := exec.Command("git", "clone", "--depth", "1", "--branch", branch, gitURL, extDir)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("git clone 失败",
|
||||||
|
zap.String("output", string(output)),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
// 清理失败的目录
|
||||||
|
_ = os.RemoveAll(extDir)
|
||||||
|
return nil, fmt.Errorf("git clone 失败: %s", strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
global.GVA_LOG.Info("git clone 成功", zap.String("name", extName))
|
||||||
|
|
||||||
|
// 如果扩展需要构建(有 package.json 的 build 脚本且 dist 不存在),执行构建
|
||||||
|
if err := buildExtensionIfNeeded(extDir); err != nil {
|
||||||
|
global.GVA_LOG.Warn("扩展构建失败(不影响安装)",
|
||||||
|
zap.String("name", extName),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建数据库记录
|
||||||
|
return s.createExtensionFromDir(userID, extDir, extName, "git", gitURL, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallExtensionFromManifestURL 从 manifest URL 安装扩展
|
||||||
|
func (s *ExtensionService) InstallExtensionFromManifestURL(userID uint, manifestURL string, branch string) (*app.AIExtension, error) {
|
||||||
|
if err := ensureExtensionsBaseDir(); err != nil {
|
||||||
|
return nil, fmt.Errorf("创建扩展目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
global.GVA_LOG.Info("从 Manifest URL 安装扩展", zap.String("url", manifestURL))
|
||||||
|
|
||||||
|
// 下载 manifest.json
|
||||||
|
manifestData, err := httpGet(manifestURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("下载 manifest.json 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, err := parseManifestBytes(manifestData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定扩展名
|
||||||
|
extName := sanitizeName(manifest.DisplayName)
|
||||||
|
if extName == "" {
|
||||||
|
extName = extractNameFromURL(manifestURL)
|
||||||
|
}
|
||||||
|
if extName == "" {
|
||||||
|
return nil, errors.New("无法确定扩展名,manifest 中缺少 display_name")
|
||||||
|
}
|
||||||
|
|
||||||
|
extDir := getExtensionDir(extName)
|
||||||
|
if _, err := os.Stat(extDir); err == nil {
|
||||||
|
return nil, fmt.Errorf("扩展 '%s' 已存在,请先删除或使用升级功能", extName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(extDir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("创建扩展目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存 manifest.json
|
||||||
|
if err := os.WriteFile(filepath.Join(extDir, "manifest.json"), manifestData, 0644); err != nil {
|
||||||
|
_ = os.RemoveAll(extDir)
|
||||||
|
return nil, fmt.Errorf("保存 manifest.json 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 manifest URL 的基础路径
|
||||||
|
baseURL := manifestURL[:strings.LastIndex(manifestURL, "/")+1]
|
||||||
|
|
||||||
|
// 下载 JS 入口文件
|
||||||
|
if manifest.Js != "" {
|
||||||
|
jsURL := baseURL + manifest.Js
|
||||||
|
jsData, err := httpGet(jsURL)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Warn("下载 JS 文件失败", zap.String("url", jsURL), zap.Error(err))
|
||||||
|
} else {
|
||||||
|
if err := os.WriteFile(filepath.Join(extDir, manifest.Js), jsData, 0644); err != nil {
|
||||||
|
global.GVA_LOG.Warn("保存 JS 文件失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载 CSS 文件
|
||||||
|
if manifest.Css != "" {
|
||||||
|
cssURL := baseURL + manifest.Css
|
||||||
|
cssData, err := httpGet(cssURL)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Warn("下载 CSS 文件失败", zap.String("url", cssURL), zap.Error(err))
|
||||||
|
} else {
|
||||||
|
if err := os.WriteFile(filepath.Join(extDir, manifest.Css), cssData, 0644); err != nil {
|
||||||
|
global.GVA_LOG.Warn("保存 CSS 文件失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建数据库记录
|
||||||
|
return s.createExtensionFromDir(userID, extDir, extName, "url", manifestURL, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportExtensionFromZip 从 zip 文件导入扩展
|
||||||
|
func (s *ExtensionService) ImportExtensionFromZip(userID uint, filename string, zipData []byte) (*app.AIExtension, error) {
|
||||||
|
if err := ensureExtensionsBaseDir(); err != nil {
|
||||||
|
return nil, fmt.Errorf("创建扩展目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先解压到临时目录
|
||||||
|
tmpDir, err := os.MkdirTemp("", "ext-import-*")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建临时目录失败: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// 解压 zip
|
||||||
|
if err := extractZip(zipData, tmpDir); err != nil {
|
||||||
|
return nil, fmt.Errorf("解压 zip 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到 manifest.json 所在目录(可能在根目录或子目录)
|
||||||
|
manifestDir, err := findManifestDir(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 manifest
|
||||||
|
manifest, err := parseManifestFile(manifestDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定扩展名
|
||||||
|
extName := sanitizeName(manifest.DisplayName)
|
||||||
|
if extName == "" {
|
||||||
|
extName = strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
extDir := getExtensionDir(extName)
|
||||||
|
if _, err := os.Stat(extDir); err == nil {
|
||||||
|
return nil, fmt.Errorf("扩展 '%s' 已存在,请先删除或使用升级功能", extName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动文件到目标目录
|
||||||
|
if err := os.Rename(manifestDir, extDir); err != nil {
|
||||||
|
// 如果跨分区移动失败,回退为复制
|
||||||
|
if err := copyDir(manifestDir, extDir); err != nil {
|
||||||
|
return nil, fmt.Errorf("移动扩展文件失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.GVA_LOG.Info("ZIP 扩展导入成功",
|
||||||
|
zap.String("name", extName),
|
||||||
|
zap.String("dir", extDir),
|
||||||
|
)
|
||||||
|
|
||||||
|
return s.createExtensionFromDir(userID, extDir, extName, "file", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpgradeExtensionFromSource 从源地址升级扩展
|
||||||
|
func (s *ExtensionService) UpgradeExtensionFromSource(userID, extID uint) (*app.AIExtension, error) {
|
||||||
|
ext, err := s.GetExtension(userID, extID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext.SourceURL == "" {
|
||||||
|
return nil, errors.New("该扩展没有源地址,无法升级")
|
||||||
|
}
|
||||||
|
|
||||||
|
extDir := getExtensionDir(ext.Name)
|
||||||
|
|
||||||
|
switch ext.InstallSource {
|
||||||
|
case "git":
|
||||||
|
// Git 扩展:执行 git pull
|
||||||
|
global.GVA_LOG.Info("从 Git 升级扩展",
|
||||||
|
zap.String("name", ext.Name),
|
||||||
|
zap.String("dir", extDir),
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := exec.Command("git", "-C", extDir, "pull", "--ff-only")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("git pull 失败",
|
||||||
|
zap.String("output", string(output)),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return nil, fmt.Errorf("git pull 失败: %s", strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
global.GVA_LOG.Info("git pull 成功", zap.String("output", string(output)))
|
||||||
|
|
||||||
|
// 如果扩展需要构建,执行构建
|
||||||
|
if err := buildExtensionIfNeeded(extDir); err != nil {
|
||||||
|
global.GVA_LOG.Warn("升级后扩展构建失败",
|
||||||
|
zap.String("name", ext.Name),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "url":
|
||||||
|
// URL 扩展:重新下载 manifest 和文件
|
||||||
|
manifestData, err := httpGet(ext.SourceURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("下载 manifest.json 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, err := parseManifestBytes(manifestData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖写入 manifest.json
|
||||||
|
if err := os.WriteFile(filepath.Join(extDir, "manifest.json"), manifestData, 0644); err != nil {
|
||||||
|
return nil, fmt.Errorf("保存 manifest.json 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := ext.SourceURL[:strings.LastIndex(ext.SourceURL, "/")+1]
|
||||||
|
|
||||||
|
// 重新下载 JS
|
||||||
|
if manifest.Js != "" {
|
||||||
|
if jsData, err := httpGet(baseURL + manifest.Js); err == nil {
|
||||||
|
_ = os.WriteFile(filepath.Join(extDir, manifest.Js), jsData, 0644)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 重新下载 CSS
|
||||||
|
if manifest.Css != "" {
|
||||||
|
if cssData, err := httpGet(baseURL + manifest.Css); err == nil {
|
||||||
|
_ = os.WriteFile(filepath.Join(extDir, manifest.Css), cssData, 0644)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, errors.New("该扩展的安装来源不支持升级")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新解析 manifest 并更新数据库
|
||||||
|
manifest, _ := parseManifestFile(extDir)
|
||||||
|
if manifest != nil {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"last_update_check": &now,
|
||||||
|
}
|
||||||
|
if manifest.Version != "" {
|
||||||
|
updates["version"] = manifest.Version
|
||||||
|
}
|
||||||
|
if manifest.Description != "" {
|
||||||
|
updates["description"] = manifest.Description
|
||||||
|
}
|
||||||
|
if manifest.Author != "" {
|
||||||
|
updates["author"] = manifest.Author
|
||||||
|
}
|
||||||
|
if manifest.Js != "" {
|
||||||
|
updates["script_path"] = manifest.Js
|
||||||
|
}
|
||||||
|
if manifest.Css != "" {
|
||||||
|
updates["style_path"] = manifest.Css
|
||||||
|
}
|
||||||
|
if manifest.Raw != nil {
|
||||||
|
if raw, err := json.Marshal(manifest.Raw); err == nil {
|
||||||
|
updates["manifest_data"] = datatypes.JSON(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
global.GVA_DB.Model(&app.AIExtension{}).Where("id = ? AND user_id = ?", extID, userID).Updates(updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetExtension(userID, extID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallExtensionFromURL 智能安装:根据 URL 判断是 Git 仓库还是 Manifest URL
|
||||||
|
func (s *ExtensionService) InstallExtensionFromURL(userID uint, url string, branch string) (*app.AIExtension, error) {
|
||||||
|
if isGitURL(url) {
|
||||||
|
return s.InstallExtensionFromGit(userID, url, branch)
|
||||||
|
}
|
||||||
|
return s.InstallExtensionFromManifestURL(userID, url, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// 辅助函数
|
||||||
|
// ---------------------
|
||||||
|
|
||||||
|
// createExtensionFromDir 从扩展目录创建数据库记录
|
||||||
|
func (s *ExtensionService) createExtensionFromDir(userID uint, extDir, extName, installSource, sourceURL, branch string) (*app.AIExtension, error) {
|
||||||
|
manifest, err := parseManifestFile(extDir)
|
||||||
|
if err != nil {
|
||||||
|
// manifest 解析失败不阻止安装,使用基本信息
|
||||||
|
global.GVA_LOG.Warn("解析 manifest 失败,使用基本信息", zap.Error(err))
|
||||||
|
manifest = &STManifest{}
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
|
displayName := manifest.DisplayName
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = extName
|
||||||
|
}
|
||||||
|
version := manifest.Version
|
||||||
|
if version == "" {
|
||||||
|
version = "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := app.AIExtension{
|
||||||
|
UserID: userID,
|
||||||
|
Name: extName,
|
||||||
|
DisplayName: displayName,
|
||||||
|
Version: version,
|
||||||
|
Author: manifest.Author,
|
||||||
|
Description: manifest.Description,
|
||||||
|
Homepage: manifest.Homepages,
|
||||||
|
Repository: manifest.Repository,
|
||||||
|
ExtensionType: "ui",
|
||||||
|
ScriptPath: manifest.Js,
|
||||||
|
StylePath: manifest.Css,
|
||||||
|
InstallSource: installSource,
|
||||||
|
SourceURL: sourceURL,
|
||||||
|
Branch: branch,
|
||||||
|
IsInstalled: true,
|
||||||
|
IsEnabled: false,
|
||||||
|
InstallDate: &now,
|
||||||
|
AutoUpdate: manifest.AutoUpdate,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储 manifest 原始数据
|
||||||
|
if manifest.Raw != nil {
|
||||||
|
if raw, err := json.Marshal(manifest.Raw); err == nil {
|
||||||
|
ext.ManifestData = datatypes.JSON(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储标签
|
||||||
|
if len(manifest.Tags) > 0 {
|
||||||
|
if tags, err := json.Marshal(manifest.Tags); err == nil {
|
||||||
|
ext.Tags = datatypes.JSON(tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储默认设置
|
||||||
|
if manifest.Settings != nil {
|
||||||
|
if settings, err := json.Marshal(manifest.Settings); err == nil {
|
||||||
|
ext.Settings = datatypes.JSON(settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储依赖
|
||||||
|
if len(manifest.Requires) > 0 {
|
||||||
|
deps := make(map[string]string)
|
||||||
|
for _, r := range manifest.Requires {
|
||||||
|
deps[r] = "*"
|
||||||
|
}
|
||||||
|
if depsJSON, err := json.Marshal(deps); err == nil {
|
||||||
|
ext.Dependencies = datatypes.JSON(depsJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := global.GVA_DB.Create(&ext).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("创建扩展记录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
global.GVA_LOG.Info("扩展安装成功",
|
||||||
|
zap.String("name", extName),
|
||||||
|
zap.String("source", installSource),
|
||||||
|
zap.String("version", version),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &ext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildExtensionIfNeeded 如果扩展目录中有 package.json 且包含 build 脚本,
|
||||||
|
// 而 manifest 中指定的入口 JS 文件不存在,则自动执行 npm/pnpm install && build
|
||||||
|
func buildExtensionIfNeeded(extDir string) error {
|
||||||
|
// 读取 manifest 获取入口文件路径
|
||||||
|
manifest, err := parseManifestFile(extDir)
|
||||||
|
if err != nil || manifest.Js == "" {
|
||||||
|
return nil // 无 manifest 或无 JS 入口,不需要构建
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查入口 JS 文件是否存在
|
||||||
|
jsPath := filepath.Join(extDir, manifest.Js)
|
||||||
|
if _, err := os.Stat(jsPath); err == nil {
|
||||||
|
return nil // 入口文件已存在,无需构建
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 package.json 是否存在
|
||||||
|
pkgPath := filepath.Join(extDir, "package.json")
|
||||||
|
pkgData, err := os.ReadFile(pkgPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil // 无 package.json,不是需要构建的扩展
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有 build 脚本
|
||||||
|
var pkg struct {
|
||||||
|
Scripts map[string]string `json:"scripts"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(pkgData, &pkg); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, hasBuild := pkg.Scripts["build"]; !hasBuild {
|
||||||
|
return nil // 没有 build 脚本
|
||||||
|
}
|
||||||
|
|
||||||
|
global.GVA_LOG.Info("扩展需要构建,开始安装依赖及构建",
|
||||||
|
zap.String("dir", extDir),
|
||||||
|
zap.String("entry", manifest.Js),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 判断使用 pnpm 还是 npm
|
||||||
|
var pkgManager string
|
||||||
|
if _, err := os.Stat(filepath.Join(extDir, "pnpm-lock.yaml")); err == nil {
|
||||||
|
pkgManager = "pnpm"
|
||||||
|
} else if _, err := os.Stat(filepath.Join(extDir, "pnpm-workspace.yaml")); err == nil {
|
||||||
|
pkgManager = "pnpm"
|
||||||
|
} else {
|
||||||
|
pkgManager = "npm"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认包管理器可用
|
||||||
|
if _, err := exec.LookPath(pkgManager); err != nil {
|
||||||
|
// 回退到 npm
|
||||||
|
pkgManager = "npm"
|
||||||
|
if _, err := exec.LookPath("npm"); err != nil {
|
||||||
|
return fmt.Errorf("未找到 npm 或 pnpm,无法构建扩展")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.GVA_LOG.Info("使用包管理器", zap.String("manager", pkgManager))
|
||||||
|
|
||||||
|
// 第一步:安装依赖
|
||||||
|
installCmd := exec.Command(pkgManager, "install")
|
||||||
|
installCmd.Dir = extDir
|
||||||
|
installOutput, err := installCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("依赖安装失败",
|
||||||
|
zap.String("output", string(installOutput)),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return fmt.Errorf("%s install 失败: %s", pkgManager, strings.TrimSpace(string(installOutput)))
|
||||||
|
}
|
||||||
|
global.GVA_LOG.Info("依赖安装成功", zap.String("manager", pkgManager))
|
||||||
|
|
||||||
|
// 第二步:执行构建
|
||||||
|
buildCmd := exec.Command(pkgManager, "run", "build")
|
||||||
|
buildCmd.Dir = extDir
|
||||||
|
buildOutput, err := buildCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("构建失败",
|
||||||
|
zap.String("output", string(buildOutput)),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return fmt.Errorf("%s run build 失败: %s", pkgManager, strings.TrimSpace(string(buildOutput)))
|
||||||
|
}
|
||||||
|
global.GVA_LOG.Info("扩展构建成功", zap.String("dir", extDir))
|
||||||
|
|
||||||
|
// 验证入口文件是否已生成
|
||||||
|
if _, err := os.Stat(jsPath); err != nil {
|
||||||
|
return fmt.Errorf("构建完成但入口文件仍不存在: %s", manifest.Js)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isGitURL 判断 URL 是否为 Git 仓库
|
||||||
|
func isGitURL(url string) bool {
|
||||||
|
url = strings.ToLower(url)
|
||||||
|
if strings.HasSuffix(url, ".git") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.Contains(url, "github.com/") ||
|
||||||
|
strings.Contains(url, "gitlab.com/") ||
|
||||||
|
strings.Contains(url, "gitee.com/") ||
|
||||||
|
strings.Contains(url, "bitbucket.org/") {
|
||||||
|
// 排除以 .json 结尾的 URL
|
||||||
|
if strings.HasSuffix(url, ".json") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractRepoName 从 Git URL 提取仓库名
|
||||||
|
func extractRepoName(gitURL string) string {
|
||||||
|
gitURL = strings.TrimSuffix(gitURL, ".git")
|
||||||
|
gitURL = strings.TrimRight(gitURL, "/")
|
||||||
|
parts := strings.Split(gitURL, "/")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractNameFromURL 从 URL 路径中提取名称
|
||||||
|
func extractNameFromURL(url string) string {
|
||||||
|
// 对于 manifest URL:https://example.com/extensions/my-ext/manifest.json
|
||||||
|
// 提取上一级目录名
|
||||||
|
url = strings.TrimRight(url, "/")
|
||||||
|
parts := strings.Split(url, "/")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
filename := parts[len(parts)-1]
|
||||||
|
if strings.Contains(filename, "manifest") {
|
||||||
|
return parts[len(parts)-2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeName 清理扩展名(移除不安全字符)
|
||||||
|
func sanitizeName(name string) string {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
// 将空格替换为连字符
|
||||||
|
name = strings.ReplaceAll(name, " ", "-")
|
||||||
|
// 只保留字母、数字、连字符、下划线
|
||||||
|
var result strings.Builder
|
||||||
|
for _, c := range name {
|
||||||
|
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
|
||||||
|
result.WriteRune(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpGet 发送 HTTP GET 请求
|
||||||
|
func httpGet(url string) ([]byte, error) {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractZip 解压 zip 文件到指定目录
|
||||||
|
func extractZip(zipData []byte, destDir string) error {
|
||||||
|
reader, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("打开 zip 文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range reader.File {
|
||||||
|
// 安全检查:防止 zip slip 攻击
|
||||||
|
destPath := filepath.Join(destDir, file.Name)
|
||||||
|
if !strings.HasPrefix(filepath.Clean(destPath), filepath.Clean(destDir)+string(os.PathSeparator)) {
|
||||||
|
return fmt.Errorf("非法的 zip 文件路径: %s", file.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.FileInfo().IsDir() {
|
||||||
|
if err := os.MkdirAll(destPath, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保父目录存在
|
||||||
|
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile, err := os.Create(destPath)
|
||||||
|
if err != nil {
|
||||||
|
rc.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(outFile, rc)
|
||||||
|
outFile.Close()
|
||||||
|
rc.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findManifestDir 在解压的目录中查找 manifest.json 所在目录
|
||||||
|
func findManifestDir(rootDir string) (string, error) {
|
||||||
|
// 先检查根目录
|
||||||
|
if _, err := os.Stat(filepath.Join(rootDir, "manifest.json")); err == nil {
|
||||||
|
return rootDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查一级子目录(常见的 zip 结构是 zip 内包含一个项目文件夹)
|
||||||
|
entries, err := os.ReadDir(rootDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("读取目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
subDir := filepath.Join(rootDir, entry.Name())
|
||||||
|
if _, err := os.Stat(filepath.Join(subDir, "manifest.json")); err == nil {
|
||||||
|
return subDir, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("未找到 manifest.json,请确保 zip 文件包含有效的 SillyTavern 扩展")
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyDir 递归复制目录
|
||||||
|
func copyDir(src, dst string) error {
|
||||||
|
if err := os.MkdirAll(dst, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
srcPath := filepath.Join(src, entry.Name())
|
||||||
|
dstPath := filepath.Join(dst, entry.Name())
|
||||||
|
|
||||||
|
if entry.IsDir() {
|
||||||
|
if err := copyDir(srcPath, dstPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data, err := os.ReadFile(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(dstPath, data, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,71 +1,137 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import type {
|
import type {
|
||||||
Extension,
|
Extension,
|
||||||
ExtensionManifest,
|
|
||||||
CreateExtensionRequest,
|
|
||||||
UpdateExtensionRequest,
|
|
||||||
ExtensionListParams,
|
ExtensionListParams,
|
||||||
ExtensionListResponse,
|
ExtensionListResponse,
|
||||||
|
CreateExtensionRequest,
|
||||||
|
UpdateExtensionRequest,
|
||||||
ToggleExtensionRequest,
|
ToggleExtensionRequest,
|
||||||
UpdateExtensionSettingsRequest,
|
UpdateExtensionSettingsRequest,
|
||||||
ExtensionStatsRequest,
|
ExtensionStatsRequest,
|
||||||
} from '@/types/extension'
|
} from '@/types/extension'
|
||||||
|
|
||||||
// 创建/安装扩展
|
// 创建扩展
|
||||||
export function createExtension(data: CreateExtensionRequest) {
|
export function createExtension(data: CreateExtensionRequest) {
|
||||||
return request.post<Extension>('/app/extension', data)
|
return request<Extension>({
|
||||||
|
url: '/app/extension',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新扩展
|
// 更新扩展
|
||||||
export function updateExtension(id: number, data: UpdateExtensionRequest) {
|
export function updateExtension(id: number, data: UpdateExtensionRequest) {
|
||||||
return request.put(`/app/extension/${id}`, data)
|
return request({
|
||||||
|
url: `/app/extension/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除/卸载扩展
|
// 删除扩展
|
||||||
export function deleteExtension(id: number, deleteFiles = false) {
|
export function deleteExtension(id: number, deleteFiles = false) {
|
||||||
return request.delete(`/app/extension/${id}`, { params: { deleteFiles } })
|
return request({
|
||||||
|
url: `/app/extension/${id}`,
|
||||||
|
method: 'delete',
|
||||||
|
params: deleteFiles ? { deleteFiles: true } : undefined,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取扩展详情
|
// 获取扩展详情
|
||||||
export function getExtension(id: number) {
|
export function getExtension(id: number) {
|
||||||
return request.get<Extension>(`/app/extension/${id}`)
|
return request<Extension>({
|
||||||
|
url: `/app/extension/${id}`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取扩展列表
|
// 获取扩展列表
|
||||||
export function getExtensionList(params?: ExtensionListParams) {
|
export function getExtensionList(params?: ExtensionListParams) {
|
||||||
return request.get<ExtensionListResponse>('/app/extension/list', { params })
|
return request<ExtensionListResponse>({
|
||||||
|
url: '/app/extension',
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取启用的扩展列表
|
// 获取启用的扩展列表
|
||||||
export function getEnabledExtensions() {
|
export function getEnabledExtensions() {
|
||||||
return request.get<Extension[]>('/app/extension/enabled')
|
return request<Extension[]>({
|
||||||
|
url: '/app/extension/enabled',
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启用/禁用扩展
|
// 启用/禁用扩展
|
||||||
export function toggleExtension(id: number, data: ToggleExtensionRequest) {
|
export function toggleExtension(id: number, data: ToggleExtensionRequest) {
|
||||||
return request.post(`/app/extension/${id}/toggle`, data)
|
return request({
|
||||||
|
url: `/app/extension/${id}/toggle`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取扩展配置
|
// 获取扩展设置
|
||||||
export function getExtensionSettings(id: number) {
|
export function getExtensionSettings(id: number) {
|
||||||
return request.get<Record<string, any>>(`/app/extension/${id}/settings`)
|
return request<Record<string, any>>({
|
||||||
|
url: `/app/extension/${id}/settings`,
|
||||||
|
method: 'get',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新扩展配置
|
// 更新扩展设置
|
||||||
export function updateExtensionSettings(id: number, data: UpdateExtensionSettingsRequest) {
|
export function updateExtensionSettings(id: number, data: UpdateExtensionSettingsRequest) {
|
||||||
return request.put(`/app/extension/${id}/settings`, data)
|
return request({
|
||||||
|
url: `/app/extension/${id}/settings`,
|
||||||
|
method: 'put',
|
||||||
|
data,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取扩展 manifest
|
// 获取扩展 manifest
|
||||||
export function getExtensionManifest(id: number) {
|
export function getExtensionManifest(id: number) {
|
||||||
return request.get<ExtensionManifest>(`/app/extension/${id}/manifest`)
|
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) {
|
export function importExtension(file: File) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
return request.post<Extension>('/app/extension/import', formData, {
|
|
||||||
|
return request<Extension>({
|
||||||
|
url: '/app/extension/import',
|
||||||
|
method: 'post',
|
||||||
|
data: formData,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
@@ -74,30 +140,35 @@ export function importExtension(file: File) {
|
|||||||
|
|
||||||
// 导出扩展
|
// 导出扩展
|
||||||
export function exportExtension(id: number) {
|
export function exportExtension(id: number) {
|
||||||
return request.get(`/app/extension/${id}/export`, {
|
return request<Extension>({
|
||||||
responseType: 'blob',
|
url: `/app/extension/${id}/export`,
|
||||||
|
method: 'get',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新扩展统计
|
// 更新扩展统计
|
||||||
export function updateExtensionStats(data: ExtensionStatsRequest) {
|
export function updateExtensionStats(data: ExtensionStatsRequest) {
|
||||||
return request.post('/app/extension/stats', data)
|
return request({
|
||||||
}
|
url: `/app/extension/${data.extensionId}/stats`,
|
||||||
|
method: 'post',
|
||||||
// 智能安装扩展(自动识别 Git URL 或 Manifest URL,兼容 SillyTavern)
|
data: {
|
||||||
export function installExtensionFromUrl(url: string, branch = 'main') {
|
action: data.action,
|
||||||
return request.post<Extension>('/app/extension/install/url', { url, branch })
|
value: data.value || 1,
|
||||||
}
|
},
|
||||||
|
|
||||||
// 从 Git URL 安装扩展
|
|
||||||
export function installExtensionFromGit(gitUrl: string, branch = 'main') {
|
|
||||||
return request.post<Extension>('/app/extension/install/git', {
|
|
||||||
gitUrl,
|
|
||||||
branch,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 升级扩展版本(从源重新安装)
|
// 下载扩展 JSON 文件
|
||||||
export function upgradeExtension(id: number, force = false) {
|
export function downloadExtensionJSON(id: number, name: string) {
|
||||||
return request.post<Extension>(`/app/extension/${id}/update`, { force })
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
2
web-app-vue/src/components.d.ts
vendored
2
web-app-vue/src/components.d.ts
vendored
@@ -17,6 +17,7 @@ declare module 'vue' {
|
|||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
@@ -45,6 +46,7 @@ declare module 'vue' {
|
|||||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||||
|
|||||||
@@ -204,12 +204,18 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 没有配置项 -->
|
<!-- 没有配置项(仅当: 无 manifest schema 配置 且 无扩展注入的自定义 UI 且 无全局设置面板) -->
|
||||||
<el-empty
|
<el-empty
|
||||||
v-if="Object.keys(getSettingsSchema(ext)).length === 0"
|
v-if="Object.keys(getSettingsSchema(ext)).length === 0 && !extensionsWithCustomUI.has(ext.name) && !hasExtensionSettingsUI"
|
||||||
description="此扩展没有可配置项"
|
description="此扩展没有可配置项"
|
||||||
:image-size="60"
|
: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>
|
</el-form>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
@@ -263,11 +269,11 @@
|
|||||||
:on-change="handleFileChange"
|
:on-change="handleFileChange"
|
||||||
:show-file-list="true"
|
:show-file-list="true"
|
||||||
:limit="1"
|
:limit="1"
|
||||||
accept=".json"
|
accept=".zip,.json"
|
||||||
>
|
>
|
||||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||||
<div class="el-upload__text">
|
<div class="el-upload__text">
|
||||||
将 manifest.json 拖到此处,或<em>点击上传</em>
|
将扩展包(.zip)或 manifest.json 拖到此处,或<em>点击上传</em>
|
||||||
</div>
|
</div>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@@ -377,6 +383,10 @@ const installForm = reactive({
|
|||||||
const settingsMountRef = ref<HTMLElement | null>(null)
|
const settingsMountRef = ref<HTMLElement | null>(null)
|
||||||
const hasExtensionSettingsUI = ref(false)
|
const hasExtensionSettingsUI = ref(false)
|
||||||
|
|
||||||
|
// 跟踪哪些扩展已经注入了自定义 UI
|
||||||
|
const extensionsWithCustomUI = reactive(new Set<string>())
|
||||||
|
let settingsObserver: MutationObserver | null = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将全局 #extensions_settings 容器中的扩展 UI 搬入 Drawer 中显示
|
* 将全局 #extensions_settings 容器中的扩展 UI 搬入 Drawer 中显示
|
||||||
* 原版 SillyTavern 扩展通过 jQuery 向 #extensions_settings 追加自定义 HTML,
|
* 原版 SillyTavern 扩展通过 jQuery 向 #extensions_settings 追加自定义 HTML,
|
||||||
@@ -424,9 +434,63 @@ const unmountExtensionSettings = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件卸载时归还容器
|
/**
|
||||||
|
* 检查每个已注册扩展的 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(() => {
|
onBeforeUnmount(() => {
|
||||||
unmountExtensionSettings()
|
unmountExtensionSettings()
|
||||||
|
if (settingsObserver) {
|
||||||
|
settingsObserver.disconnect()
|
||||||
|
settingsObserver = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
@@ -648,7 +712,7 @@ const handleInstall = async () => {
|
|||||||
|
|
||||||
if (installTab.value === 'file') {
|
if (installTab.value === 'file') {
|
||||||
if (!selectedFile.value) {
|
if (!selectedFile.value) {
|
||||||
ElMessage.warning('请选择 manifest.json 文件')
|
ElMessage.warning('请选择扩展文件(.zip 或 .json)')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await extensionStore.importExtension(selectedFile.value)
|
await extensionStore.importExtension(selectedFile.value)
|
||||||
@@ -727,9 +791,20 @@ watch(drawerVisible, async (visible) => {
|
|||||||
handleRefresh()
|
handleRefresh()
|
||||||
// 等待 DOM 更新后再挂载扩展设置面板
|
// 等待 DOM 更新后再挂载扩展设置面板
|
||||||
await nextTick()
|
await nextTick()
|
||||||
setTimeout(mountExtensionSettings, 100)
|
// 启动 MutationObserver 以检测扩展动态注入的 UI
|
||||||
|
startSettingsObserver()
|
||||||
|
// 扫描已有的自定义 settings UI
|
||||||
|
scanCustomSettingsUI()
|
||||||
|
setTimeout(() => {
|
||||||
|
mountExtensionSettings()
|
||||||
|
// 再次扫描(给扩展一点时间注入 UI)
|
||||||
|
scanCustomSettingsUI()
|
||||||
|
}, 200)
|
||||||
} else {
|
} else {
|
||||||
unmountExtensionSettings()
|
unmountExtensionSettings()
|
||||||
|
if (settingsObserver) {
|
||||||
|
settingsObserver.disconnect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,522 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="extension-list-page">
|
|
||||||
<el-card>
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<h2>扩展管理</h2>
|
|
||||||
<div class="header-actions">
|
|
||||||
<el-button type="primary" @click="showInstallDialog = true">
|
|
||||||
<el-icon><Plus /></el-icon>
|
|
||||||
安装扩展
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 过滤器 -->
|
|
||||||
<div class="filters">
|
|
||||||
<el-input
|
|
||||||
v-model="searchKeyword"
|
|
||||||
placeholder="搜索扩展名称..."
|
|
||||||
clearable
|
|
||||||
@change="handleSearch"
|
|
||||||
style="width: 300px; margin-right: 10px"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<el-icon><Search /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
|
|
||||||
<el-select
|
|
||||||
v-model="filterType"
|
|
||||||
placeholder="扩展类型"
|
|
||||||
clearable
|
|
||||||
@change="handleFilterChange"
|
|
||||||
style="width: 150px; margin-right: 10px"
|
|
||||||
>
|
|
||||||
<el-option label="UI 扩展" value="ui" />
|
|
||||||
<el-option label="服务端扩展" value="server" />
|
|
||||||
<el-option label="混合扩展" value="hybrid" />
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
<el-select
|
|
||||||
v-model="filterCategory"
|
|
||||||
placeholder="分类"
|
|
||||||
clearable
|
|
||||||
@change="handleFilterChange"
|
|
||||||
style="width: 150px; margin-right: 10px"
|
|
||||||
>
|
|
||||||
<el-option label="工具" value="utilities" />
|
|
||||||
<el-option label="主题" value="themes" />
|
|
||||||
<el-option label="集成" value="integrations" />
|
|
||||||
<el-option label="其他" value="tools" />
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
<el-select
|
|
||||||
v-model="filterEnabled"
|
|
||||||
placeholder="状态"
|
|
||||||
clearable
|
|
||||||
@change="handleFilterChange"
|
|
||||||
style="width: 120px; margin-right: 10px"
|
|
||||||
>
|
|
||||||
<el-option label="已启用" :value="true" />
|
|
||||||
<el-option label="已禁用" :value="false" />
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
<el-button @click="handleResetFilters">重置</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 扩展列表 -->
|
|
||||||
<el-table
|
|
||||||
v-loading="extensionStore.loading"
|
|
||||||
:data="extensionStore.extensions"
|
|
||||||
style="width: 100%; margin-top: 20px"
|
|
||||||
>
|
|
||||||
<el-table-column prop="displayName" label="扩展名称" width="200">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="extension-name">
|
|
||||||
<strong>{{ row.displayName || row.name }}</strong>
|
|
||||||
<el-tag
|
|
||||||
v-if="row.isSystemExt"
|
|
||||||
size="small"
|
|
||||||
type="info"
|
|
||||||
style="margin-left: 8px"
|
|
||||||
>
|
|
||||||
系统
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column prop="description" label="描述" show-overflow-tooltip />
|
|
||||||
|
|
||||||
<el-table-column prop="version" label="版本" width="100" />
|
|
||||||
|
|
||||||
<el-table-column prop="author" label="作者" width="120" />
|
|
||||||
|
|
||||||
<el-table-column prop="extensionType" label="类型" width="100">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="getTypeTagType(row.extensionType)" size="small">
|
|
||||||
{{ getTypeLabel(row.extensionType) }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column prop="category" label="分类" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ getCategoryLabel(row.category) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column prop="isEnabled" label="状态" width="100">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-switch
|
|
||||||
v-model="row.isEnabled"
|
|
||||||
:disabled="row.isSystemExt"
|
|
||||||
@change="handleToggle(row)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<el-table-column label="操作" width="280" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
@click="handleViewDetail(row)"
|
|
||||||
>
|
|
||||||
详情
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
@click="handleSettings(row)"
|
|
||||||
>
|
|
||||||
配置
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
@click="handleExport(row)"
|
|
||||||
>
|
|
||||||
导出
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-if="!row.isSystemExt"
|
|
||||||
size="small"
|
|
||||||
type="danger"
|
|
||||||
@click="handleDelete(row)"
|
|
||||||
>
|
|
||||||
卸载
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="extensionStore.currentPage"
|
|
||||||
v-model:page-size="extensionStore.pageSize"
|
|
||||||
:total="extensionStore.total"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@current-change="handlePageChange"
|
|
||||||
style="margin-top: 20px; justify-content: flex-end"
|
|
||||||
/>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 安装扩展对话框 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="showInstallDialog"
|
|
||||||
title="安装扩展"
|
|
||||||
width="600px"
|
|
||||||
>
|
|
||||||
<el-tabs v-model="installTab">
|
|
||||||
<el-tab-pane label="从文件导入" name="file">
|
|
||||||
<el-upload
|
|
||||||
ref="uploadRef"
|
|
||||||
:auto-upload="false"
|
|
||||||
:limit="1"
|
|
||||||
accept=".json"
|
|
||||||
:on-change="handleFileSelect"
|
|
||||||
drag
|
|
||||||
>
|
|
||||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
|
||||||
<div class="el-upload__text">
|
|
||||||
将 manifest.json 拖到此处,或<em>点击上传</em>
|
|
||||||
</div>
|
|
||||||
<template #tip>
|
|
||||||
<div class="el-upload__tip">
|
|
||||||
只能上传 manifest.json 文件
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-upload>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="从 Git 安装" name="git">
|
|
||||||
<el-form :model="installForm" label-width="120px">
|
|
||||||
<el-form-item label="Git 仓库 URL">
|
|
||||||
<el-input
|
|
||||||
v-model="installForm.gitUrl"
|
|
||||||
placeholder="https://github.com/username/extension-name.git"
|
|
||||||
/>
|
|
||||||
<template #extra>
|
|
||||||
<div style="font-size: 12px; color: #909399; margin-top: 5px">
|
|
||||||
支持 GitHub、GitLab、Gitee 等 Git 仓库<br />
|
|
||||||
示例:https://github.com/SillyTavern/Extension-Example.git
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="分支(可选)">
|
|
||||||
<el-input
|
|
||||||
v-model="installForm.branch"
|
|
||||||
placeholder="main"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="从 URL 安装" name="url">
|
|
||||||
<el-form :model="installForm" label-width="120px">
|
|
||||||
<el-form-item label="Manifest URL">
|
|
||||||
<el-input
|
|
||||||
v-model="installForm.url"
|
|
||||||
placeholder="https://example.com/manifest.json"
|
|
||||||
/>
|
|
||||||
<template #extra>
|
|
||||||
<div style="font-size: 12px; color: #909399; margin-top: 5px">
|
|
||||||
直接指向 manifest.json 文件的 URL
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="showInstallDialog = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleInstall" :loading="extensionStore.loading">
|
|
||||||
安装
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 扩展详情对话框 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="showDetailDialog"
|
|
||||||
:title="currentExtension?.displayName || currentExtension?.name"
|
|
||||||
width="800px"
|
|
||||||
>
|
|
||||||
<el-descriptions v-if="currentExtension" :column="2" border>
|
|
||||||
<el-descriptions-item label="扩展名称">
|
|
||||||
{{ currentExtension.displayName || currentExtension.name }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="版本">
|
|
||||||
{{ currentExtension.version }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="作者">
|
|
||||||
{{ currentExtension.author }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="许可证">
|
|
||||||
{{ currentExtension.license || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="类型">
|
|
||||||
{{ getTypeLabel(currentExtension.extensionType) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="分类">
|
|
||||||
{{ getCategoryLabel(currentExtension.category) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="主页" :span="2">
|
|
||||||
<a v-if="currentExtension.homepage" :href="currentExtension.homepage" target="_blank">
|
|
||||||
{{ currentExtension.homepage }}
|
|
||||||
</a>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="仓库" :span="2">
|
|
||||||
<a v-if="currentExtension.repository" :href="currentExtension.repository" target="_blank">
|
|
||||||
{{ currentExtension.repository }}
|
|
||||||
</a>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="描述" :span="2">
|
|
||||||
{{ currentExtension.description }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="标签" :span="2">
|
|
||||||
<el-tag
|
|
||||||
v-for="tag in currentExtension.tags"
|
|
||||||
:key="tag"
|
|
||||||
size="small"
|
|
||||||
style="margin-right: 5px"
|
|
||||||
>
|
|
||||||
{{ tag }}
|
|
||||||
</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="安装日期">
|
|
||||||
{{ formatDate(currentExtension.installDate) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="使用次数">
|
|
||||||
{{ currentExtension.usageCount }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
|
||||||
import { Plus, Search, UploadFilled } from '@element-plus/icons-vue'
|
|
||||||
import { useExtensionStore } from '@/stores/extension'
|
|
||||||
import type { Extension } from '@/types/extension'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const extensionStore = useExtensionStore()
|
|
||||||
|
|
||||||
// 搜索和过滤
|
|
||||||
const searchKeyword = ref('')
|
|
||||||
const filterType = ref('')
|
|
||||||
const filterCategory = ref('')
|
|
||||||
const filterEnabled = ref<boolean | undefined>(undefined)
|
|
||||||
|
|
||||||
// 对话框
|
|
||||||
const showInstallDialog = ref(false)
|
|
||||||
const showDetailDialog = ref(false)
|
|
||||||
const installTab = ref('git')
|
|
||||||
const installForm = ref({
|
|
||||||
gitUrl: '',
|
|
||||||
branch: 'main',
|
|
||||||
url: '',
|
|
||||||
})
|
|
||||||
const uploadRef = ref()
|
|
||||||
const selectedFile = ref<File | null>(null)
|
|
||||||
const currentExtension = ref<Extension | null>(null)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
extensionStore.fetchExtensionList()
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
extensionStore.fetchExtensionList({ keyword: searchKeyword.value })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFilterChange = () => {
|
|
||||||
extensionStore.setFilters({
|
|
||||||
extensionType: filterType.value,
|
|
||||||
category: filterCategory.value,
|
|
||||||
isEnabled: filterEnabled.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleResetFilters = () => {
|
|
||||||
searchKeyword.value = ''
|
|
||||||
filterType.value = ''
|
|
||||||
filterCategory.value = ''
|
|
||||||
filterEnabled.value = undefined
|
|
||||||
extensionStore.resetFilters()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
extensionStore.currentPage = page
|
|
||||||
extensionStore.fetchExtensionList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSizeChange = (size: number) => {
|
|
||||||
extensionStore.pageSize = size
|
|
||||||
extensionStore.fetchExtensionList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleToggle = async (row: Extension) => {
|
|
||||||
try {
|
|
||||||
await extensionStore.toggleExtension(row.id, row.isEnabled)
|
|
||||||
} catch (error) {
|
|
||||||
// 切换失败,还原状态
|
|
||||||
row.isEnabled = !row.isEnabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleViewDetail = (row: Extension) => {
|
|
||||||
currentExtension.value = row
|
|
||||||
showDetailDialog.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSettings = (row: Extension) => {
|
|
||||||
router.push(`/extension/settings/${row.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExport = async (row: Extension) => {
|
|
||||||
try {
|
|
||||||
await extensionStore.exportExtension(row.id, row.name)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('导出失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = async (row: Extension) => {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
`确定要卸载扩展 "${row.displayName || row.name}" 吗?`,
|
|
||||||
'确认卸载',
|
|
||||||
{
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
await extensionStore.deleteExtension(row.id, false)
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error !== 'cancel') {
|
|
||||||
console.error('卸载失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileSelect = (file: any) => {
|
|
||||||
selectedFile.value = file.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInstall = async () => {
|
|
||||||
try {
|
|
||||||
if (installTab.value === 'file') {
|
|
||||||
if (!selectedFile.value) {
|
|
||||||
ElMessage.warning('请选择要上传的文件')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await extensionStore.importExtension(selectedFile.value)
|
|
||||||
} else if (installTab.value === 'git') {
|
|
||||||
if (!installForm.value.gitUrl) {
|
|
||||||
ElMessage.warning('请输入 Git 仓库 URL')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await extensionStore.installExtensionFromGit(
|
|
||||||
installForm.value.gitUrl,
|
|
||||||
installForm.value.branch || 'main'
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if (!installForm.value.url) {
|
|
||||||
ElMessage.warning('请输入 Manifest URL')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await extensionStore.installExtensionFromUrl(installForm.value.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
showInstallDialog.value = false
|
|
||||||
selectedFile.value = null
|
|
||||||
installForm.value = { gitUrl: '', branch: 'main', url: '' }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('安装失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTypeLabel = (type: string) => {
|
|
||||||
const labels: Record<string, string> = {
|
|
||||||
ui: 'UI',
|
|
||||||
server: '服务端',
|
|
||||||
hybrid: '混合',
|
|
||||||
}
|
|
||||||
return labels[type] || type
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTypeTagType = (type: string) => {
|
|
||||||
const types: Record<string, any> = {
|
|
||||||
ui: 'primary',
|
|
||||||
server: 'success',
|
|
||||||
hybrid: 'warning',
|
|
||||||
}
|
|
||||||
return types[type] || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCategoryLabel = (category: string) => {
|
|
||||||
const labels: Record<string, string> = {
|
|
||||||
utilities: '工具',
|
|
||||||
themes: '主题',
|
|
||||||
integrations: '集成',
|
|
||||||
tools: '其他',
|
|
||||||
}
|
|
||||||
return labels[category] || category || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (date: string) => {
|
|
||||||
if (!date) return '-'
|
|
||||||
return new Date(date).toLocaleString('zh-CN')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.extension-list-page {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.extension-name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-icon--upload {
|
|
||||||
font-size: 67px;
|
|
||||||
color: #c0c4cc;
|
|
||||||
margin: 40px 0 16px;
|
|
||||||
line-height: 50px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,378 +1,291 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="extension-settings-page">
|
<div class="extension-settings">
|
||||||
<el-card v-loading="loading">
|
<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>
|
<template #header>
|
||||||
<div class="card-header">
|
<span>扩展信息</span>
|
||||||
<div>
|
</template>
|
||||||
<h2>{{ extension?.displayName || extension?.name }} - 配置</h2>
|
<el-descriptions :column="1" border size="small">
|
||||||
<p class="subtitle">{{ extension?.description }}</p>
|
<el-descriptions-item label="名称">{{ extensionStore.currentExtension?.displayName || extensionStore.currentExtension?.name }}</el-descriptions-item>
|
||||||
</div>
|
<el-descriptions-item label="版本">{{ extensionStore.currentExtension?.version }}</el-descriptions-item>
|
||||||
<div class="header-actions">
|
<el-descriptions-item label="作者">{{ extensionStore.currentExtension?.author || '-' }}</el-descriptions-item>
|
||||||
<el-button @click="handleBack">返回</el-button>
|
<el-descriptions-item label="类型">
|
||||||
<el-button
|
<el-tag size="small">{{ typeLabel(extensionStore.currentExtension?.extensionType || '') }}</el-tag>
|
||||||
v-if="hasSettings"
|
</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"
|
type="primary"
|
||||||
@click="handleSave"
|
:href="extensionStore.currentExtension.homepage"
|
||||||
:loading="saving"
|
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>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 扩展信息 -->
|
<el-empty v-if="!settingsKeys.length && !settingsLoading" description="该扩展暂无可配置项" />
|
||||||
<el-descriptions :column="3" border style="margin-bottom: 20px">
|
|
||||||
<el-descriptions-item label="版本">
|
|
||||||
{{ extension?.version }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="作者">
|
|
||||||
{{ extension?.author }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="类型">
|
|
||||||
{{ getTypeLabel(extension?.extensionType) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
|
|
||||||
<!-- 配置表单 -->
|
<el-form v-else label-width="160px" label-position="left">
|
||||||
<el-form
|
|
||||||
v-if="extension"
|
|
||||||
ref="formRef"
|
|
||||||
:model="formData"
|
|
||||||
label-width="200px"
|
|
||||||
style="max-width: 800px"
|
|
||||||
>
|
|
||||||
<el-divider content-position="left">扩展配置</el-divider>
|
|
||||||
|
|
||||||
<!-- 动态生成配置项 -->
|
|
||||||
<template v-if="hasSettings">
|
|
||||||
<el-form-item
|
<el-form-item
|
||||||
v-for="(value, key) in settings"
|
v-for="key in settingsKeys"
|
||||||
:key="key"
|
:key="key"
|
||||||
:label="getSettingLabel(key)"
|
:label="key"
|
||||||
>
|
>
|
||||||
<!-- 布尔值 -->
|
<!-- 布尔值 -->
|
||||||
<el-switch
|
<el-switch
|
||||||
v-if="typeof value === 'boolean'"
|
v-if="typeof settingsData[key] === 'boolean'"
|
||||||
v-model="formData[key]"
|
v-model="settingsData[key]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 数字 -->
|
<!-- 数字 -->
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-else-if="typeof value === 'number'"
|
v-else-if="typeof settingsData[key] === 'number'"
|
||||||
v-model="formData[key]"
|
v-model="settingsData[key]"
|
||||||
:min="0"
|
:controls="true"
|
||||||
/>
|
/>
|
||||||
|
<!-- 字符串 -->
|
||||||
<!-- 选择器(如果有 options) -->
|
|
||||||
<el-select
|
|
||||||
v-else-if="isSelectField(key)"
|
|
||||||
v-model="formData[key]"
|
|
||||||
placeholder="请选择"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="option in getSelectOptions(key)"
|
|
||||||
:key="option.value"
|
|
||||||
:label="option.label"
|
|
||||||
:value="option.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
|
|
||||||
<!-- 多行文本 -->
|
|
||||||
<el-input
|
<el-input
|
||||||
v-else-if="isTextareaField(key)"
|
v-else-if="typeof settingsData[key] === 'string'"
|
||||||
v-model="formData[key]"
|
v-model="settingsData[key]"
|
||||||
|
:autosize="{ minRows: 1, maxRows: 4 }"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="4"
|
|
||||||
/>
|
/>
|
||||||
|
<!-- 对象/数组 - JSON 编辑 -->
|
||||||
<!-- 默认文本输入 -->
|
|
||||||
<el-input
|
<el-input
|
||||||
v-else
|
v-else
|
||||||
v-model="formData[key]"
|
v-model="settingsJsonStrings[key]"
|
||||||
:placeholder="getSettingPlaceholder(key)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 没有配置项:SillyTavern 扩展的配置由扩展自身管理 -->
|
|
||||||
<template v-else>
|
|
||||||
<el-alert
|
|
||||||
type="info"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
style="margin-bottom: 20px"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
此扩展的配置由扩展自身管理
|
|
||||||
</template>
|
|
||||||
<p style="margin: 5px 0 0 0">
|
|
||||||
SillyTavern 扩展通过内置的 extension_settings API 管理配置,
|
|
||||||
启用扩展后可在聊天界面中进行配置。
|
|
||||||
</p>
|
|
||||||
</el-alert>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 高级选项 -->
|
|
||||||
<template v-if="extension.options && Object.keys(extension.options).length > 0">
|
|
||||||
<el-divider content-position="left">高级选项</el-divider>
|
|
||||||
|
|
||||||
<el-form-item
|
|
||||||
v-for="(value, key) in extension.options"
|
|
||||||
:key="'option_' + key"
|
|
||||||
:label="getSettingLabel(key)"
|
|
||||||
>
|
|
||||||
<el-input
|
|
||||||
v-model="optionsData[key]"
|
|
||||||
:placeholder="getSettingPlaceholder(key)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</template>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<!-- Manifest 信息 -->
|
|
||||||
<el-divider content-position="left">Manifest 信息</el-divider>
|
|
||||||
<el-descriptions v-if="extension" :column="2" border>
|
|
||||||
<el-descriptions-item label="扩展名称">
|
|
||||||
{{ extension.name || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="显示名称">
|
|
||||||
{{ extension.displayName || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="主脚本">
|
|
||||||
{{ extension.scriptPath || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="样式文件">
|
|
||||||
{{ extension.stylePath || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="安装来源">
|
|
||||||
{{ getInstallSourceLabel(extension.installSource) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="源地址">
|
|
||||||
<el-link
|
|
||||||
v-if="extension.sourceUrl"
|
|
||||||
:href="extension.sourceUrl"
|
|
||||||
type="primary"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{ extension.sourceUrl }}
|
|
||||||
</el-link>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="依赖扩展" :span="2">
|
|
||||||
<template v-if="extension.dependencies && Object.keys(extension.dependencies).length > 0">
|
|
||||||
<el-tag
|
|
||||||
v-for="(version, name) in extension.dependencies"
|
|
||||||
:key="name"
|
|
||||||
size="small"
|
|
||||||
style="margin-right: 5px"
|
|
||||||
>
|
|
||||||
{{ name }} {{ version }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
<span v-else>无</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="冲突扩展" :span="2">
|
|
||||||
<template v-if="extension.conflicts && extension.conflicts.length > 0">
|
|
||||||
<el-tag
|
|
||||||
v-for="conflict in extension.conflicts"
|
|
||||||
:key="conflict"
|
|
||||||
size="small"
|
|
||||||
type="danger"
|
|
||||||
style="margin-right: 5px"
|
|
||||||
>
|
|
||||||
{{ conflict }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
<span v-else>无</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
|
|
||||||
<!-- Manifest 原始数据 -->
|
|
||||||
<template v-if="extension?.manifestData && Object.keys(extension.manifestData).length > 0">
|
|
||||||
<el-divider content-position="left">Manifest 原始数据</el-divider>
|
|
||||||
<el-input
|
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:model-value="JSON.stringify(extension.manifestData, null, 2)"
|
:autosize="{ minRows: 2, maxRows: 8 }"
|
||||||
:rows="10"
|
@blur="parseJsonField(key)"
|
||||||
readonly
|
|
||||||
style="font-family: monospace"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
<!-- 统计信息 -->
|
|
||||||
<el-divider content-position="left">使用统计</el-divider>
|
|
||||||
<el-descriptions v-if="extension" :column="3" border>
|
|
||||||
<el-descriptions-item label="使用次数">
|
|
||||||
{{ extension.usageCount }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="错误次数">
|
|
||||||
{{ extension.errorCount }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="平均加载时间">
|
|
||||||
{{ extension.loadTime }}ms
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="安装日期">
|
|
||||||
{{ formatDate(extension.installDate) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="最后启用时间">
|
|
||||||
{{ formatDate(extension.lastEnabled) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="自动更新">
|
|
||||||
{{ extension.autoUpdate ? '是' : '否' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-card>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useExtensionStore } from '@/stores/extension'
|
import { useExtensionStore } from '@/stores/extension'
|
||||||
import type { Extension } from '@/types/extension'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const extensionStore = useExtensionStore()
|
const extensionStore = useExtensionStore()
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const saving = ref(false)
|
|
||||||
const extension = ref<Extension | null>(null)
|
|
||||||
const settings = ref<Record<string, any> | null>(null)
|
|
||||||
const formData = ref<Record<string, any>>({})
|
|
||||||
const optionsData = ref<Record<string, any>>({})
|
|
||||||
const formRef = ref()
|
|
||||||
|
|
||||||
const extensionId = computed(() => Number(route.params.id))
|
const extensionId = computed(() => Number(route.params.id))
|
||||||
|
|
||||||
const hasSettings = computed(() => {
|
// 状态
|
||||||
return settings.value && Object.keys(settings.value).length > 0
|
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)
|
||||||
|
|
||||||
onMounted(async () => {
|
const extensionName = computed(() =>
|
||||||
await loadExtension()
|
extensionStore.currentExtension?.displayName || extensionStore.currentExtension?.name || '扩展',
|
||||||
await loadSettings()
|
)
|
||||||
})
|
|
||||||
|
|
||||||
const loadExtension = async () => {
|
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 {
|
try {
|
||||||
loading.value = true
|
await extensionStore.fetchExtension(extensionId.value)
|
||||||
extension.value = await extensionStore.fetchExtension(extensionId.value)
|
} catch (error) {
|
||||||
|
console.error('获取扩展详情失败:', error)
|
||||||
|
ElMessage.error('获取扩展详情失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化 options 数据
|
// 获取扩展设置
|
||||||
if (extension.value.options) {
|
const fetchSettings = async () => {
|
||||||
optionsData.value = { ...extension.value.options }
|
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) {
|
} catch (error) {
|
||||||
console.error('加载扩展失败:', error)
|
console.error('获取设置失败:', error)
|
||||||
ElMessage.error('加载扩展失败')
|
|
||||||
router.back()
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
settingsLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadSettings = async () => {
|
// 获取 manifest
|
||||||
|
const fetchManifest = async () => {
|
||||||
try {
|
try {
|
||||||
settings.value = await extensionStore.getExtensionSettings(extensionId.value)
|
const data = await extensionStore.getExtensionManifest(extensionId.value)
|
||||||
formData.value = { ...settings.value }
|
if (data) {
|
||||||
|
manifest.value = data
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载配置失败:', error)
|
console.error('获取 manifest 失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = async () => {
|
// 解析 JSON 字段
|
||||||
|
const parseJsonField = (key: string) => {
|
||||||
try {
|
try {
|
||||||
|
settingsData.value[key] = JSON.parse(settingsJsonStrings.value[key])
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning(`字段 "${key}" 的 JSON 格式无效`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存设置
|
||||||
|
const saveSettings = async () => {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
await extensionStore.updateExtensionSettings(extensionId.value, formData.value)
|
try {
|
||||||
ElMessage.success('配置保存成功')
|
await extensionStore.updateExtensionSettings(extensionId.value, settingsData.value)
|
||||||
|
originalSettings.value = JSON.parse(JSON.stringify(settingsData.value))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存失败:', error)
|
console.error('保存设置失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBack = () => {
|
// 重置设置
|
||||||
router.back()
|
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 getTypeLabel = (type?: string) => {
|
// 返回
|
||||||
if (!type) return '-'
|
const goBack = () => {
|
||||||
const labels: Record<string, string> = {
|
router.push('/extension')
|
||||||
ui: 'UI 扩展',
|
|
||||||
server: '服务端扩展',
|
|
||||||
hybrid: '混合扩展',
|
|
||||||
}
|
|
||||||
return labels[type] || type
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSettingLabel = (key: string) => {
|
onMounted(() => {
|
||||||
return key
|
fetchExtension()
|
||||||
.replace(/([A-Z])/g, ' $1')
|
fetchSettings()
|
||||||
.replace(/^./, (str) => str.toUpperCase())
|
fetchManifest()
|
||||||
.replace(/_/g, ' ')
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const getSettingPlaceholder = (key: string) => {
|
|
||||||
return `请输入 ${getSettingLabel(key)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelectField = (_key: string) => {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSelectOptions = (_key: string) => {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTextareaField = (key: string) => {
|
|
||||||
const textareaFields = ['description', 'content', 'notes', 'script', 'code']
|
|
||||||
return textareaFields.some(field => key.toLowerCase().includes(field))
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInstallSourceLabel = (source: string) => {
|
|
||||||
const labels: Record<string, string> = {
|
|
||||||
url: 'URL 下载',
|
|
||||||
git: 'Git 仓库',
|
|
||||||
file: '文件导入',
|
|
||||||
marketplace: '应用市场',
|
|
||||||
}
|
|
||||||
return labels[source] || source
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (date: string) => {
|
|
||||||
if (!date) return '-'
|
|
||||||
return new Date(date).toLocaleString('zh-CN')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
.extension-settings-page {
|
.extension-settings {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.settings-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header h2 {
|
.ext-description {
|
||||||
margin: 0 0 5px 0;
|
margin-top: 16px;
|
||||||
font-size: 20px;
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.ext-links {
|
||||||
margin: 0;
|
margin-top: 12px;
|
||||||
font-size: 14px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user