diff --git a/internal/client/tunnel/client.go b/internal/client/tunnel/client.go index 2550c14..3cc428f 100644 --- a/internal/client/tunnel/client.go +++ b/internal/client/tunnel/client.go @@ -550,10 +550,15 @@ func (c *Client) handleJSPluginInstall(stream net.Conn, msg *protocol.Message) { return } - c.logf("[Client] Installing JS plugin: %s", req.PluginName) + c.logf("[Client] Installing JS plugin: %s (ID: %s)", req.PluginName, req.PluginID) + + // 使用 PluginID 作为 key(如果有),否则回退到 pluginName:ruleName + key := req.PluginID + if key == "" { + key = req.PluginName + ":" + req.RuleName + } // 如果插件已经在运行,先停止它 - key := req.PluginName + ":" + req.RuleName c.pluginMu.Lock() if existingHandler, ok := c.runningPlugins[key]; ok { c.logf("[Client] Stopping existing plugin %s before reinstall", key) @@ -625,12 +630,16 @@ func (c *Client) startJSPlugin(handler plugin.ClientPlugin, req protocol.JSPlugi return } - key := req.PluginName + ":" + req.RuleName + // 使用 PluginID 作为 key(如果有),否则回退到 pluginName:ruleName + key := req.PluginID + if key == "" { + key = req.PluginName + ":" + req.RuleName + } c.pluginMu.Lock() c.runningPlugins[key] = handler c.pluginMu.Unlock() - c.logf("[Client] JS plugin %s started at %s", req.PluginName, localAddr) + c.logf("[Client] JS plugin %s (ID: %s) started at %s", req.PluginName, req.PluginID, localAddr) } // verifyJSPluginSignature 验证 JS 插件签名 @@ -1055,16 +1064,25 @@ func (c *Client) handlePluginAPIRequest(stream net.Conn, msg *protocol.Message) return } - c.logf("[Client] Plugin API request: %s %s for plugin %s", req.Method, req.Path, req.PluginName) + c.logf("[Client] Plugin API request: %s %s for plugin %s (ID: %s)", req.Method, req.Path, req.PluginName, req.PluginID) // 查找运行中的插件 c.pluginMu.RLock() var handler plugin.ClientPlugin - for key, p := range c.runningPlugins { - // key 格式为 "pluginName:ruleName" - if strings.HasPrefix(key, req.PluginName+":") { - handler = p - break + + // 优先使用 PluginID 查找 + if req.PluginID != "" { + handler = c.runningPlugins[req.PluginID] + } + + // 如果没找到,尝试通过 PluginName 匹配(向后兼容) + if handler == nil && req.PluginName != "" { + for key, p := range c.runningPlugins { + // key 可能是 PluginID 或 "pluginName:ruleName" 格式 + if strings.HasPrefix(key, req.PluginName+":") { + handler = p + break + } } } c.pluginMu.RUnlock() diff --git a/internal/server/db/interface.go b/internal/server/db/interface.go index e510586..03c59ca 100644 --- a/internal/server/db/interface.go +++ b/internal/server/db/interface.go @@ -15,6 +15,7 @@ type ConfigField struct { // ClientPlugin 客户端已安装的插件 type ClientPlugin struct { + ID string `json:"id"` // 插件实例唯一 ID Name string `json:"name"` Version string `json:"version"` Enabled bool `json:"enabled"` diff --git a/internal/server/router/handler/client.go b/internal/server/router/handler/client.go index bae7405..0ed1916 100644 --- a/internal/server/router/handler/client.go +++ b/internal/server/router/handler/client.go @@ -333,25 +333,43 @@ func (h *ClientHandler) InstallPlugins(c *gin.Context) { // @Produce json // @Security Bearer // @Param id path string true "客户端ID" -// @Param pluginName path string true "插件名称" +// @Param pluginID path string true "插件实例ID" // @Param action path string true "操作类型" Enums(start, stop, restart, config, delete) // @Param request body dto.ClientPluginActionRequest false "操作参数" // @Success 200 {object} Response // @Failure 400 {object} Response -// @Router /api/client/{id}/plugin/{pluginName}/{action} [post] +// @Router /api/client/{id}/plugin/{pluginID}/{action} [post] func (h *ClientHandler) PluginAction(c *gin.Context) { clientID := c.Param("id") - pluginName := c.Param("pluginName") + pluginID := c.Param("pluginID") action := c.Param("action") var req dto.ClientPluginActionRequest c.ShouldBindJSON(&req) // 忽略错误,使用默认值 + // 通过 pluginID 查找插件信息 + client, err := h.app.GetClientStore().GetClient(clientID) + if err != nil { + NotFound(c, "client not found") + return + } + + var pluginName string + for _, p := range client.Plugins { + if p.ID == pluginID { + pluginName = p.Name + break + } + } + if pluginName == "" { + NotFound(c, "plugin not found") + return + } + if req.RuleName == "" { req.RuleName = pluginName } - var err error switch action { case "start": err = h.app.GetServer().StartClientPlugin(clientID, pluginName, req.RuleName) @@ -366,7 +384,7 @@ func (h *ClientHandler) PluginAction(c *gin.Context) { } err = h.app.GetServer().UpdateClientPluginConfig(clientID, pluginName, req.RuleName, req.Config, req.Restart) case "delete": - err = h.deleteClientPlugin(clientID, pluginName) + err = h.deleteClientPlugin(clientID, pluginID) default: BadRequest(c, "unknown action: "+action) return @@ -378,13 +396,14 @@ func (h *ClientHandler) PluginAction(c *gin.Context) { } Success(c, gin.H{ - "status": "ok", - "action": action, - "plugin": pluginName, + "status": "ok", + "action": action, + "plugin_id": pluginID, + "plugin": pluginName, }) } -func (h *ClientHandler) deleteClientPlugin(clientID, pluginName string) error { +func (h *ClientHandler) deleteClientPlugin(clientID, pluginID string) error { client, err := h.app.GetClientStore().GetClient(clientID) if err != nil { return fmt.Errorf("client not found") @@ -393,7 +412,7 @@ func (h *ClientHandler) deleteClientPlugin(clientID, pluginName string) error { var newPlugins []db.ClientPlugin found := false for _, p := range client.Plugins { - if p.Name == pluginName { + if p.ID == pluginID { found = true continue } @@ -401,7 +420,7 @@ func (h *ClientHandler) deleteClientPlugin(clientID, pluginName string) error { } if !found { - return fmt.Errorf("plugin %s not found", pluginName) + return fmt.Errorf("plugin %s not found", pluginID) } client.Plugins = newPlugins diff --git a/internal/server/router/handler/interfaces.go b/internal/server/router/handler/interfaces.go index ec280fe..7250fb1 100644 --- a/internal/server/router/handler/interfaces.go +++ b/internal/server/router/handler/interfaces.go @@ -84,6 +84,7 @@ type PluginInfo struct { // JSPluginInstallRequest JS 插件安装请求 type JSPluginInstallRequest struct { + PluginID string `json:"plugin_id"` PluginName string `json:"plugin_name"` Source string `json:"source"` Signature string `json:"signature"` diff --git a/internal/server/router/handler/plugin_api.go b/internal/server/router/handler/plugin_api.go index 0f88186..966e616 100644 --- a/internal/server/router/handler/plugin_api.go +++ b/internal/server/router/handler/plugin_api.go @@ -27,16 +27,16 @@ func NewPluginAPIHandler(app AppInterface) *PluginAPIHandler { // @Accept json // @Produce json // @Security Bearer -// @Param clientID path string true "客户端 ID" -// @Param pluginName path string true "插件名称" +// @Param id path string true "客户端 ID" +// @Param pluginID path string true "插件实例 ID" // @Param route path string true "插件路由" // @Success 200 {object} object // @Failure 404 {object} Response // @Failure 502 {object} Response -// @Router /api/client/{clientID}/plugin/{pluginName}/{route} [get] +// @Router /api/client/{id}/plugin-api/{pluginID}/{route} [get] func (h *PluginAPIHandler) ProxyRequest(c *gin.Context) { - clientID := c.Param("clientID") - pluginName := c.Param("pluginName") + clientID := c.Param("id") + pluginID := c.Param("pluginID") route := c.Param("route") // 确保路由以 / 开头 @@ -68,12 +68,12 @@ func (h *PluginAPIHandler) ProxyRequest(c *gin.Context) { // 构建 API 请求 apiReq := protocol.PluginAPIRequest{ - PluginName: pluginName, - Method: c.Request.Method, - Path: route, - Query: c.Request.URL.RawQuery, - Headers: headers, - Body: body, + PluginID: pluginID, + Method: c.Request.Method, + Path: route, + Query: c.Request.URL.RawQuery, + Headers: headers, + Body: body, } // 发送请求到客户端 diff --git a/internal/server/router/handler/store.go b/internal/server/router/handler/store.go index dc0de50..966c881 100644 --- a/internal/server/router/handler/store.go +++ b/internal/server/router/handler/store.go @@ -6,6 +6,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/google/uuid" "github.com/gotunnel/internal/server/db" "github.com/gotunnel/internal/server/router/dto" "github.com/gotunnel/pkg/protocol" @@ -125,8 +126,24 @@ func (h *StoreHandler) Install(c *gin.Context) { return } + // 检查插件是否已存在,决定使用已有 ID 还是生成新 ID + pluginID := "" + dbClient, err := h.app.GetClientStore().GetClient(req.ClientID) + if err == nil { + for _, p := range dbClient.Plugins { + if p.Name == req.PluginName && p.ID != "" { + pluginID = p.ID + break + } + } + } + if pluginID == "" { + pluginID = uuid.New().String() + } + // 安装到客户端 installReq := JSPluginInstallRequest{ + PluginID: pluginID, PluginName: req.PluginName, Source: string(source), Signature: string(signature), @@ -152,14 +169,19 @@ func (h *StoreHandler) Install(c *gin.Context) { h.app.GetJSPluginStore().SaveJSPlugin(jsPlugin) // 将插件信息保存到客户端记录 - dbClient, err := h.app.GetClientStore().GetClient(req.ClientID) + // 重新获取 dbClient(可能已被修改) + dbClient, err = h.app.GetClientStore().GetClient(req.ClientID) if err == nil { - // 检查插件是否已存在 + // 检查插件是否已存在(通过名称匹配) pluginExists := false for i, p := range dbClient.Plugins { if p.Name == req.PluginName { dbClient.Plugins[i].Enabled = true dbClient.Plugins[i].RemotePort = req.RemotePort + // 确保有 ID + if dbClient.Plugins[i].ID == "" { + dbClient.Plugins[i].ID = pluginID + } pluginExists = true break } @@ -183,6 +205,7 @@ func (h *StoreHandler) Install(c *gin.Context) { }) } dbClient.Plugins = append(dbClient.Plugins, db.ClientPlugin{ + ID: pluginID, Name: req.PluginName, Version: version, Enabled: true, @@ -240,9 +263,10 @@ func (h *StoreHandler) Install(c *gin.Context) { } Success(c, gin.H{ - "status": "ok", - "plugin": req.PluginName, - "client": req.ClientID, + "status": "ok", + "plugin": req.PluginName, + "plugin_id": pluginID, + "client": req.ClientID, }) } diff --git a/internal/server/router/router.go b/internal/server/router/router.go index 2d0d67d..d521302 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -68,7 +68,7 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth, api.POST("/client/:id/disconnect", clientHandler.Disconnect) api.POST("/client/:id/restart", clientHandler.Restart) api.POST("/client/:id/install-plugins", clientHandler.InstallPlugins) - api.POST("/client/:id/plugin/:pluginName/:action", clientHandler.PluginAction) + api.POST("/client/:id/plugin/:pluginID/:action", clientHandler.PluginAction) // 配置管理 configHandler := handler.NewConfigHandler(app) @@ -112,7 +112,7 @@ func (r *GinRouter) SetupRoutes(app handler.AppInterface, jwtAuth *auth.JWTAuth, // 插件 API 代理 (通过 Web API 访问客户端插件) pluginAPIHandler := handler.NewPluginAPIHandler(app) - api.Any("/client/:clientID/plugin/:pluginName/*route", pluginAPIHandler.ProxyRequest) + api.Any("/client/:id/plugin-api/:pluginID/*route", pluginAPIHandler.ProxyRequest) } } diff --git a/internal/server/tunnel/server.go b/internal/server/tunnel/server.go index d5c0839..42a104a 100644 --- a/internal/server/tunnel/server.go +++ b/internal/server/tunnel/server.go @@ -991,6 +991,7 @@ func (s *Server) InstallJSPluginToClient(clientID string, req router.JSPluginIns defer stream.Close() installReq := protocol.JSPluginInstallRequest{ + PluginID: req.PluginID, PluginName: req.PluginName, Source: req.Source, Signature: req.Signature, @@ -1304,6 +1305,7 @@ func (s *Server) pushClientInstalledPlugins(cs *ClientSession, alreadyPushed map } req := router.JSPluginInstallRequest{ + PluginID: cp.ID, PluginName: cp.Name, Source: jsPlugin.Source, Signature: jsPlugin.Signature, diff --git a/pkg/protocol/message.go b/pkg/protocol/message.go index 70bca54..f1a6599 100644 --- a/pkg/protocol/message.go +++ b/pkg/protocol/message.go @@ -248,6 +248,7 @@ type PluginStatusQueryResponse struct { // JSPluginInstallRequest JS 插件安装请求 type JSPluginInstallRequest struct { + PluginID string `json:"plugin_id"` // 插件实例唯一 ID PluginName string `json:"plugin_name"` // 插件名称 Source string `json:"source"` // JS 源码 Signature string `json:"signature"` // 官方签名 (Base64) @@ -361,7 +362,8 @@ type LogStopRequest struct { // PluginAPIRequest 插件 API 请求 type PluginAPIRequest struct { - PluginName string `json:"plugin_name"` // 插件名称 + PluginID string `json:"plugin_id"` // 插件实例唯一 ID + PluginName string `json:"plugin_name"` // 插件名称 (向后兼容) Method string `json:"method"` // HTTP 方法: GET, POST, PUT, DELETE Path string `json:"path"` // 路由路径 Query string `json:"query"` // 查询参数 diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 1e670f1..34562ab 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -30,17 +30,17 @@ export const installPluginsToClient = (id: string, plugins: string[]) => // 规则配置模式 export const getRuleSchemas = () => get('/rule-schemas') -// 客户端插件控制 -export const startClientPlugin = (clientId: string, pluginName: string, ruleName: string) => - post(`/client/${clientId}/plugin/${pluginName}/start`, { rule_name: ruleName }) -export const stopClientPlugin = (clientId: string, pluginName: string, ruleName: string) => - post(`/client/${clientId}/plugin/${pluginName}/stop`, { rule_name: ruleName }) -export const restartClientPlugin = (clientId: string, pluginName: string, ruleName: string) => - post(`/client/${clientId}/plugin/${pluginName}/restart`, { rule_name: ruleName }) -export const deleteClientPlugin = (clientId: string, pluginName: string) => - post(`/client/${clientId}/plugin/${pluginName}/delete`) -export const updateClientPluginConfigWithRestart = (clientId: string, pluginName: string, ruleName: string, config: Record, restart: boolean) => - post(`/client/${clientId}/plugin/${pluginName}/config`, { rule_name: ruleName, config, restart }) +// 客户端插件控制(使用 pluginID) +export const startClientPlugin = (clientId: string, pluginId: string, ruleName: string) => + post(`/client/${clientId}/plugin/${pluginId}/start`, { rule_name: ruleName }) +export const stopClientPlugin = (clientId: string, pluginId: string, ruleName: string) => + post(`/client/${clientId}/plugin/${pluginId}/stop`, { rule_name: ruleName }) +export const restartClientPlugin = (clientId: string, pluginId: string, ruleName: string) => + post(`/client/${clientId}/plugin/${pluginId}/restart`, { rule_name: ruleName }) +export const deleteClientPlugin = (clientId: string, pluginId: string) => + post(`/client/${clientId}/plugin/${pluginId}/delete`) +export const updateClientPluginConfigWithRestart = (clientId: string, pluginId: string, ruleName: string, config: Record, restart: boolean) => + post(`/client/${clientId}/plugin/${pluginId}/config`, { rule_name: ruleName, config, restart }) // 插件管理 export const getPlugins = () => get('/plugins') @@ -71,6 +71,23 @@ export const updateJSPluginConfig = (name: string, config: Record post(`/js-plugin/${name}/${enabled ? 'enable' : 'disable'}`) +// 插件 API 代理(通过 pluginID 调用插件自定义 API) +export const callPluginAPI = (clientId: string, pluginId: string, method: string, route: string, body?: any) => { + const path = `/client/${clientId}/plugin-api/${pluginId}${route.startsWith('/') ? route : '/' + route}` + switch (method.toUpperCase()) { + case 'GET': + return get(path) + case 'POST': + return post(path, body) + case 'PUT': + return put(path, body) + case 'DELETE': + return del(path) + default: + return get(path) + } +} + // 更新管理 export interface UpdateInfo { available: boolean diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 737ba50..bbcde33 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -11,11 +11,13 @@ export interface ProxyRule { // 客户端已安装的插件 export interface ClientPlugin { + id: string // 插件实例唯一 ID name: string version: string enabled: boolean running: boolean config?: Record + remote_port?: number // 远程监听端口 } // 插件配置字段 diff --git a/web/src/views/ClientView.vue b/web/src/views/ClientView.vue index 3304fbe..329cf1e 100644 --- a/web/src/views/ClientView.vue +++ b/web/src/views/ClientView.vue @@ -278,7 +278,7 @@ const handleStartPlugin = async (plugin: ClientPlugin) => { const rule = rules.value.find(r => r.type === plugin.name) const ruleName = rule?.name || plugin.name try { - await startClientPlugin(clientId, plugin.name, ruleName) + await startClientPlugin(clientId, plugin.id, ruleName) message.success(`已启动 ${plugin.name}`) plugin.running = true } catch (e: any) { @@ -292,7 +292,7 @@ const handleRestartPlugin = async (plugin: ClientPlugin) => { const rule = rules.value.find(r => r.type === plugin.name) const ruleName = rule?.name || plugin.name try { - await restartClientPlugin(clientId, plugin.name, ruleName) + await restartClientPlugin(clientId, plugin.id, ruleName) message.success(`已重启 ${plugin.name}`) plugin.running = true } catch (e: any) { @@ -305,7 +305,7 @@ const handleStopPlugin = async (plugin: ClientPlugin) => { const rule = rules.value.find(r => r.type === plugin.name) const ruleName = rule?.name || plugin.name try { - await stopClientPlugin(clientId, plugin.name, ruleName) + await stopClientPlugin(clientId, plugin.id, ruleName) message.success(`已停止 ${plugin.name}`) plugin.running = false } catch (e: any) { @@ -316,7 +316,7 @@ const handleStopPlugin = async (plugin: ClientPlugin) => { const toggleClientPlugin = async (plugin: ClientPlugin) => { const newEnabled = !plugin.enabled const updatedPlugins = clientPlugins.value.map(p => - p.name === plugin.name ? { ...p, enabled: newEnabled } : p + p.id === plugin.id ? { ...p, enabled: newEnabled } : p ) try { await updateClient(clientId, { @@ -377,7 +377,7 @@ const handleDeletePlugin = (plugin: ClientPlugin) => { negativeText: '取消', onPositiveClick: async () => { try { - await deleteClientPlugin(clientId, plugin.name) + await deleteClientPlugin(clientId, plugin.id) message.success(`已删除 ${plugin.name}`) await loadClient() } catch (e: any) { @@ -596,7 +596,7 @@ const handleDeletePlugin = (plugin: ClientPlugin) => { - + {{ plugin.name }} v{{ plugin.version }}