feat: update plugin handling to use unique PluginID across client and server interactions
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Successful in 56s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 7m28s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 2m44s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 7m27s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m18s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 5m49s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 2m17s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 5m4s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 5m57s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 4m36s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Failing after 13m47s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Failing after 26m24s

This commit is contained in:
2026-01-04 21:31:36 +08:00
parent 78982a26b0
commit 02f8c521c2
12 changed files with 143 additions and 57 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
}
// 发送请求到客户端

View File

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

View File

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

View File

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