From d4984c8d7843685503cc71089c545cfaacd92b98 Mon Sep 17 00:00:00 2001 From: Flik Date: Mon, 29 Dec 2025 19:40:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20=E6=B7=BB=E5=8A=A0=E4=BB=8E?= =?UTF-8?q?=E5=95=86=E5=BA=97=E5=AE=89=E8=A3=85=E6=8F=92=E4=BB=B6=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 /store/install API 端点用于从商店安装插件 - 实现 StorePluginInstallRequest 请求结构体 - 添加 handleStoreInstall 处理函数实现插件下载和安装逻辑 - 在客户端视图中添加商店插件安装模态框和相关功能 - 在插件视图中添加商店插件安装按钮和安装到客户端功能 - 添加 installStorePlugin API 函数用于前端调用 - 实现客户端在线状态检查和插件安装流程 --- internal/server/router/api.go | 73 ++++++++++++++++++++++++++++ web/src/api/index.ts | 2 + web/src/views/ClientView.vue | 90 +++++++++++++++++++++++++++++++++-- web/src/views/PluginsView.vue | 82 ++++++++++++++++++++++++++++++- 4 files changed, 241 insertions(+), 6 deletions(-) diff --git a/internal/server/router/api.go b/internal/server/router/api.go index 1a5e05f..8c7547b 100644 --- a/internal/server/router/api.go +++ b/internal/server/router/api.go @@ -131,6 +131,7 @@ func RegisterRoutes(r *Router, app AppInterface) { api.HandleFunc("/plugins", h.handlePlugins) api.HandleFunc("/plugin/", h.handlePlugin) api.HandleFunc("/store/plugins", h.handleStorePlugins) + api.HandleFunc("/store/install", h.handleStoreInstall) api.HandleFunc("/client-plugin/", h.handleClientPlugin) api.HandleFunc("/js-plugin/", h.handleJSPlugin) api.HandleFunc("/js-plugins", h.handleJSPlugins) @@ -582,6 +583,13 @@ type StorePluginInfo struct { DownloadURL string `json:"download_url,omitempty"` } +// StorePluginInstallRequest 从商店安装插件的请求 +type StorePluginInstallRequest struct { + PluginName string `json:"plugin_name"` + DownloadURL string `json:"download_url"` + ClientID string `json:"client_id"` +} + // handleStorePlugins 处理扩展商店插件列表 func (h *APIHandler) handleStorePlugins(rw http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { @@ -625,6 +633,71 @@ func (h *APIHandler) handleStorePlugins(rw http.ResponseWriter, r *http.Request) }) } +// handleStoreInstall 从商店安装插件到客户端 +func (h *APIHandler) handleStoreInstall(rw http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req StorePluginInstallRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + if req.PluginName == "" || req.DownloadURL == "" || req.ClientID == "" { + http.Error(rw, "plugin_name, download_url and client_id required", http.StatusBadRequest) + return + } + + // 检查客户端是否在线 + online, _, _ := h.server.GetClientStatus(req.ClientID) + if !online { + http.Error(rw, "client not online", http.StatusBadRequest) + return + } + + // 下载插件 + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Get(req.DownloadURL) + if err != nil { + http.Error(rw, "Failed to download plugin: "+err.Error(), http.StatusBadGateway) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + http.Error(rw, "Plugin download failed with status: "+resp.Status, http.StatusBadGateway) + return + } + + source, err := io.ReadAll(resp.Body) + if err != nil { + http.Error(rw, "Failed to read plugin: "+err.Error(), http.StatusInternalServerError) + return + } + + // 安装到客户端 + installReq := JSPluginInstallRequest{ + PluginName: req.PluginName, + Source: string(source), + RuleName: req.PluginName, + AutoStart: true, + } + + if err := h.server.InstallJSPluginToClient(req.ClientID, installReq); err != nil { + http.Error(rw, "Failed to install plugin: "+err.Error(), http.StatusInternalServerError) + return + } + + h.jsonResponse(rw, map[string]interface{}{ + "status": "ok", + "plugin": req.PluginName, + "client": req.ClientID, + }) +} + // handleClientPlugin 处理客户端插件配置 // 路由: /api/client-plugin/{clientID}/{pluginName}/config func (h *APIHandler) handleClientPlugin(rw http.ResponseWriter, r *http.Request) { diff --git a/web/src/api/index.ts b/web/src/api/index.ts index c6efbff..b8e6e55 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -33,6 +33,8 @@ export const disablePlugin = (name: string) => post(`/plugin/${name}/disable`) // 扩展商店 export const getStorePlugins = () => get<{ plugins: StorePluginInfo[], store_url: string }>('/store/plugins') +export const installStorePlugin = (pluginName: string, downloadUrl: string, clientId: string) => + post('/store/install', { plugin_name: pluginName, download_url: downloadUrl, client_id: clientId }) // 客户端插件配置 export const getClientPluginConfig = (clientId: string, pluginName: string) => diff --git a/web/src/views/ClientView.vue b/web/src/views/ClientView.vue index 2bff890..bad83c0 100644 --- a/web/src/views/ClientView.vue +++ b/web/src/views/ClientView.vue @@ -4,18 +4,19 @@ import { useRoute, useRouter } from 'vue-router' import { NCard, NButton, NSpace, NTag, NTable, NEmpty, NFormItem, NInput, NInputNumber, NSelect, NModal, NCheckbox, NSwitch, - NIcon, useMessage, useDialog + NIcon, useMessage, useDialog, NSpin } from 'naive-ui' import { ArrowBackOutline, CreateOutline, TrashOutline, PushOutline, PowerOutline, AddOutline, SaveOutline, CloseOutline, - DownloadOutline, SettingsOutline + DownloadOutline, SettingsOutline, StorefrontOutline } from '@vicons/ionicons5' import { getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, - getPlugins, installPluginsToClient, getClientPluginConfig, updateClientPluginConfig + getPlugins, installPluginsToClient, getClientPluginConfig, updateClientPluginConfig, + getStorePlugins, installStorePlugin } from '../api' -import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField, RuleSchema } from '../types' +import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField, RuleSchema, StorePluginInfo } from '../types' const route = useRoute() const router = useRouter() @@ -77,6 +78,13 @@ const configSchema = ref([]) const configValues = ref>({}) const configLoading = ref(false) +// 商店插件安装相关 +const showStoreModal = ref(false) +const storePlugins = ref([]) +const storeLoading = ref(false) +const selectedStorePlugin = ref(null) +const storeInstalling = ref(false) + const loadPlugins = async () => { try { const { data } = await getPlugins() @@ -110,6 +118,42 @@ const openInstallModal = async () => { showInstallModal.value = true } +// 商店插件相关函数 +const openStoreModal = async () => { + showStoreModal.value = true + storeLoading.value = true + selectedStorePlugin.value = null + try { + const { data } = await getStorePlugins() + storePlugins.value = (data.plugins || []).filter(p => p.download_url) + } catch (e) { + console.error('Failed to load store plugins', e) + message.error('加载商店插件失败') + } finally { + storeLoading.value = false + } +} + +const handleInstallStorePlugin = async (plugin: StorePluginInfo) => { + if (!plugin.download_url) { + message.error('该插件没有下载地址') + return + } + storeInstalling.value = true + selectedStorePlugin.value = plugin + try { + await installStorePlugin(plugin.name, plugin.download_url, clientId) + message.success(`已安装 ${plugin.name}`) + showStoreModal.value = false + await loadClient() + } catch (e: any) { + message.error(e.response?.data || '安装失败') + } finally { + storeInstalling.value = false + selectedStorePlugin.value = null + } +} + const getTypeLabel = (type: string) => { const labels: Record = { proxy: '协议', app: '应用', service: '服务', tool: '工具' } return labels[type] || type @@ -350,6 +394,10 @@ const savePluginConfig = async () => { 安装插件 + + + 从商店安装 + 断开连接 @@ -598,5 +646,39 @@ const savePluginConfig = async () => { + + + + + + + + + + + {{ plugin.name }} + v{{ plugin.version }} + + {{ plugin.description }} + 作者: {{ plugin.author }} + + + 安装 + + + + + + + diff --git a/web/src/views/PluginsView.vue b/web/src/views/PluginsView.vue index 26e43ff..b558ab9 100644 --- a/web/src/views/PluginsView.vue +++ b/web/src/views/PluginsView.vue @@ -4,10 +4,13 @@ import { useRouter } from 'vue-router' import { NCard, NButton, NSpace, NTag, NStatistic, NGrid, NGi, NEmpty, NSpin, NIcon, NSwitch, NTabs, NTabPane, useMessage, - NSelect + NSelect, NModal } from 'naive-ui' import { ArrowBackOutline, ExtensionPuzzleOutline, StorefrontOutline, CodeSlashOutline } from '@vicons/ionicons5' -import { getPlugins, enablePlugin, disablePlugin, getStorePlugins, getJSPlugins, pushJSPluginToClient, getClients } from '../api' +import { + getPlugins, enablePlugin, disablePlugin, getStorePlugins, getJSPlugins, + pushJSPluginToClient, getClients, installStorePlugin +} from '../api' import type { PluginInfo, StorePluginInfo, JSPlugin, ClientStatus } from '../types' const router = useRouter() @@ -141,6 +144,43 @@ const handlePushJSPlugin = async (pluginName: string, clientId: string) => { const onlineClients = computed(() => clients.value.filter(c => c.online)) +// 商店插件安装相关 +const showInstallModal = ref(false) +const selectedStorePlugin = ref(null) +const selectedClientId = ref('') +const installing = ref(false) + +const openInstallModal = (plugin: StorePluginInfo) => { + selectedStorePlugin.value = plugin + selectedClientId.value = '' + showInstallModal.value = true +} + +const handleInstallStorePlugin = async () => { + if (!selectedStorePlugin.value || !selectedClientId.value) { + message.warning('请选择要安装到的客户端') + return + } + if (!selectedStorePlugin.value.download_url) { + message.error('该插件没有下载地址') + return + } + installing.value = true + try { + await installStorePlugin( + selectedStorePlugin.value.name, + selectedStorePlugin.value.download_url, + selectedClientId.value + ) + message.success(`已安装 ${selectedStorePlugin.value.name} 到客户端`) + showInstallModal.value = false + } catch (e: any) { + message.error(e.response?.data || '安装失败') + } finally { + installing.value = false + } +} + onMounted(() => { loadPlugins() loadClients() @@ -231,6 +271,16 @@ onMounted(() => { {{ plugin.name }} + v{{ plugin.version }} @@ -310,5 +360,33 @@ onMounted(() => { ... 已屏蔽 ... --> + + + + +
+

插件: {{ selectedStorePlugin.name }}

+

{{ selectedStorePlugin.description }}

+
+ +
+ +