1
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 31s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m18s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m24s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m11s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m43s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m28s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 1m2s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m33s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m51s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m1s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m38s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m10s

This commit is contained in:
2026-01-11 11:43:07 +08:00
parent 47603b0574
commit 24b3b47ccd
8 changed files with 89 additions and 34 deletions

View File

@@ -525,13 +525,24 @@ func (c *Client) handleClientPluginConn(stream net.Conn, msg *protocol.Message)
return return
} }
key := req.PluginName + ":" + req.RuleName
c.pluginMu.RLock() c.pluginMu.RLock()
handler, ok := c.runningPlugins[key] var handler plugin.ClientPlugin
var ok bool
// 优先使用 PluginID 查找
if req.PluginID != "" {
handler, ok = c.runningPlugins[req.PluginID]
}
// 如果没找到,回退到 pluginName:ruleName
if !ok {
key := req.PluginName + ":" + req.RuleName
handler, ok = c.runningPlugins[key]
}
c.pluginMu.RUnlock() c.pluginMu.RUnlock()
if !ok { if !ok {
c.logWarnf("[Client] Plugin %s not running", key) c.logWarnf("[Client] Plugin %s (ID: %s) not running", req.PluginName, req.PluginID)
stream.Close() stream.Close()
return return
} }
@@ -696,10 +707,25 @@ func (c *Client) handleClientPluginStop(stream net.Conn, msg *protocol.Message)
return return
} }
key := req.PluginName + ":" + req.RuleName
c.pluginMu.Lock() c.pluginMu.Lock()
handler, ok := c.runningPlugins[key] var handler plugin.ClientPlugin
var key string
var ok bool
// 优先使用 PluginID 查找
if req.PluginID != "" {
handler, ok = c.runningPlugins[req.PluginID]
if ok {
key = req.PluginID
}
}
// 如果没找到,回退到 pluginName:ruleName
if !ok {
key = req.PluginName + ":" + req.RuleName
handler, ok = c.runningPlugins[key]
}
if ok { if ok {
if err := handler.Stop(); err != nil { if err := handler.Stop(); err != nil {
c.logErrorf("[Client] Plugin %s stop error: %v", key, err) c.logErrorf("[Client] Plugin %s stop error: %v", key, err)
@@ -754,13 +780,28 @@ func (c *Client) handlePluginConfigUpdate(stream net.Conn, msg *protocol.Message
return return
} }
key := req.PluginName + ":" + req.RuleName
c.logf("[Client] Config update for plugin %s", key)
c.pluginMu.RLock() c.pluginMu.RLock()
handler, ok := c.runningPlugins[key] var handler plugin.ClientPlugin
var key string
var ok bool
// 优先使用 PluginID 查找
if req.PluginID != "" {
handler, ok = c.runningPlugins[req.PluginID]
if ok {
key = req.PluginID
}
}
// 如果没找到,回退到 pluginName:ruleName
if !ok {
key = req.PluginName + ":" + req.RuleName
handler, ok = c.runningPlugins[key]
}
c.pluginMu.RUnlock() c.pluginMu.RUnlock()
c.logf("[Client] Config update for plugin %s", key)
if !ok { if !ok {
c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, false, "plugin not running") c.sendPluginConfigUpdateResult(stream, req.PluginName, req.RuleName, false, "plugin not running")
return return

View File

@@ -1,2 +0,0 @@
# This file ensures the dist directory exists for go:embed
# The actual frontend files are built during CI/CD

View File

@@ -372,17 +372,17 @@ func (h *ClientHandler) PluginAction(c *gin.Context) {
switch action { switch action {
case "start": case "start":
err = h.app.GetServer().StartClientPlugin(clientID, pluginName, req.RuleName) err = h.app.GetServer().StartClientPlugin(clientID, pluginID, pluginName, req.RuleName)
case "stop": case "stop":
err = h.app.GetServer().StopClientPlugin(clientID, pluginName, req.RuleName) err = h.app.GetServer().StopClientPlugin(clientID, pluginID, pluginName, req.RuleName)
case "restart": case "restart":
err = h.app.GetServer().RestartClientPlugin(clientID, pluginName, req.RuleName) err = h.app.GetServer().RestartClientPlugin(clientID, pluginID, pluginName, req.RuleName)
case "config": case "config":
if req.Config == nil { if req.Config == nil {
BadRequest(c, "config required") BadRequest(c, "config required")
return return
} }
err = h.app.GetServer().UpdateClientPluginConfig(clientID, pluginName, req.RuleName, req.Config, req.Restart) err = h.app.GetServer().UpdateClientPluginConfig(clientID, pluginID, pluginName, req.RuleName, req.Config, req.Restart)
case "delete": case "delete":
err = h.deleteClientPlugin(clientID, pluginID) err = h.deleteClientPlugin(clientID, pluginID)
default: default:

View File

@@ -37,10 +37,10 @@ type ServerInterface interface {
SyncPluginConfigToClient(clientID string, pluginName string, config map[string]string) error SyncPluginConfigToClient(clientID string, pluginName string, config map[string]string) error
InstallJSPluginToClient(clientID string, req JSPluginInstallRequest) error InstallJSPluginToClient(clientID string, req JSPluginInstallRequest) error
RestartClient(clientID string) error RestartClient(clientID string) error
StartClientPlugin(clientID, pluginName, ruleName string) error StartClientPlugin(clientID, pluginID, pluginName, ruleName string) error
StopClientPlugin(clientID, pluginName, ruleName string) error StopClientPlugin(clientID, pluginID, pluginName, ruleName string) error
RestartClientPlugin(clientID, pluginName, ruleName string) error RestartClientPlugin(clientID, pluginID, pluginName, ruleName string) error
UpdateClientPluginConfig(clientID, pluginName, ruleName string, config map[string]string, restart bool) error UpdateClientPluginConfig(clientID, pluginID, pluginName, ruleName string, config map[string]string, restart bool) error
SendUpdateToClient(clientID, downloadURL string) error SendUpdateToClient(clientID, downloadURL string) error
// 日志流 // 日志流
StartClientLogStream(clientID, sessionID string, lines int, follow bool, level string) (<-chan protocol.LogEntry, error) StartClientLogStream(clientID, sessionID string, lines int, follow bool, level string) (<-chan protocol.LogEntry, error)

View File

@@ -235,6 +235,7 @@ func (h *StoreHandler) Install(c *gin.Context) {
dbClient.Rules[i].Type = req.PluginName dbClient.Rules[i].Type = req.PluginName
dbClient.Rules[i].RemotePort = req.RemotePort dbClient.Rules[i].RemotePort = req.RemotePort
dbClient.Rules[i].Enabled = boolPtr(true) dbClient.Rules[i].Enabled = boolPtr(true)
dbClient.Rules[i].PluginID = pluginID
dbClient.Rules[i].AuthEnabled = req.AuthEnabled dbClient.Rules[i].AuthEnabled = req.AuthEnabled
dbClient.Rules[i].AuthUsername = req.AuthUsername dbClient.Rules[i].AuthUsername = req.AuthUsername
dbClient.Rules[i].AuthPassword = req.AuthPassword dbClient.Rules[i].AuthPassword = req.AuthPassword
@@ -250,6 +251,7 @@ func (h *StoreHandler) Install(c *gin.Context) {
Type: req.PluginName, Type: req.PluginName,
RemotePort: req.RemotePort, RemotePort: req.RemotePort,
Enabled: boolPtr(true), Enabled: boolPtr(true),
PluginID: pluginID,
AuthEnabled: req.AuthEnabled, AuthEnabled: req.AuthEnabled,
AuthUsername: req.AuthUsername, AuthUsername: req.AuthUsername,
AuthPassword: req.AuthPassword, AuthPassword: req.AuthPassword,
@@ -268,6 +270,7 @@ func (h *StoreHandler) Install(c *gin.Context) {
Type: req.PluginName, // 使用插件名作为类型,让 isClientPlugin 识别 Type: req.PluginName, // 使用插件名作为类型,让 isClientPlugin 识别
RemotePort: req.RemotePort, RemotePort: req.RemotePort,
Enabled: boolPtr(true), Enabled: boolPtr(true),
PluginID: pluginID,
AuthEnabled: req.AuthEnabled, AuthEnabled: req.AuthEnabled,
AuthUsername: req.AuthUsername, AuthUsername: req.AuthUsername,
AuthPassword: req.AuthPassword, AuthPassword: req.AuthPassword,

View File

@@ -1126,14 +1126,18 @@ func (s *Server) acceptClientPluginConns(cs *ClientSession, ln net.Listener, rul
func (s *Server) handleClientPluginConn(cs *ClientSession, conn net.Conn, rule protocol.ProxyRule) { func (s *Server) handleClientPluginConn(cs *ClientSession, conn net.Conn, rule protocol.ProxyRule) {
defer conn.Close() defer conn.Close()
log.Printf("[Server] handleClientPluginConn: plugin=%s, auth=%v", rule.Type, rule.AuthEnabled)
// 如果启用了 HTTP Basic Auth先进行认证 // 如果启用了 HTTP Basic Auth先进行认证
var bufferedData []byte var bufferedData []byte
if rule.AuthEnabled { if rule.AuthEnabled {
authenticated, data := s.checkHTTPBasicAuth(conn, rule.AuthUsername, rule.AuthPassword) authenticated, data := s.checkHTTPBasicAuth(conn, rule.AuthUsername, rule.AuthPassword)
if !authenticated { if !authenticated {
log.Printf("[Server] Auth failed for plugin %s", rule.Type)
return return
} }
bufferedData = data bufferedData = data
log.Printf("[Server] Auth success, buffered %d bytes", len(bufferedData))
} }
stream, err := cs.Session.Open() stream, err := cs.Session.Open()
@@ -1144,6 +1148,7 @@ func (s *Server) handleClientPluginConn(cs *ClientSession, conn net.Conn, rule p
defer stream.Close() defer stream.Close()
req := protocol.ClientPluginConnRequest{ req := protocol.ClientPluginConnRequest{
PluginID: rule.PluginID,
PluginName: rule.Type, PluginName: rule.Type,
RuleName: rule.Name, RuleName: rule.Name,
} }
@@ -1331,6 +1336,7 @@ func (s *Server) pushClientInstalledPlugins(cs *ClientSession, alreadyPushed map
Type: cp.Name, Type: cp.Name,
RemotePort: cp.RemotePort, RemotePort: cp.RemotePort,
Enabled: boolPtr(true), Enabled: boolPtr(true),
PluginID: cp.ID,
AuthEnabled: cp.AuthEnabled, AuthEnabled: cp.AuthEnabled,
AuthUsername: cp.AuthUsername, AuthUsername: cp.AuthUsername,
AuthPassword: cp.AuthPassword, AuthPassword: cp.AuthPassword,
@@ -1386,7 +1392,7 @@ func (s *Server) RestartClient(clientID string) error {
} }
// StartClientPlugin 启动客户端插件 // StartClientPlugin 启动客户端插件
func (s *Server) StartClientPlugin(clientID, pluginName, ruleName string) error { func (s *Server) StartClientPlugin(clientID, pluginID, pluginName, ruleName string) error {
s.mu.RLock() s.mu.RLock()
_, ok := s.clients[clientID] _, ok := s.clients[clientID]
s.mu.RUnlock() s.mu.RUnlock()
@@ -1400,7 +1406,7 @@ func (s *Server) StartClientPlugin(clientID, pluginName, ruleName string) error
} }
// StopClientPlugin 停止客户端插件 // StopClientPlugin 停止客户端插件
func (s *Server) StopClientPlugin(clientID, pluginName, ruleName string) error { func (s *Server) StopClientPlugin(clientID, pluginID, pluginName, ruleName string) error {
s.mu.RLock() s.mu.RLock()
cs, ok := s.clients[clientID] cs, ok := s.clients[clientID]
s.mu.RUnlock() s.mu.RUnlock()
@@ -1409,11 +1415,11 @@ func (s *Server) StopClientPlugin(clientID, pluginName, ruleName string) error {
return fmt.Errorf("client %s not found or not online", clientID) return fmt.Errorf("client %s not found or not online", clientID)
} }
return s.sendClientPluginStop(cs.Session, pluginName, ruleName) return s.sendClientPluginStop(cs.Session, pluginID, pluginName, ruleName)
} }
// sendClientPluginStop 发送客户端插件停止命令 // sendClientPluginStop 发送客户端插件停止命令
func (s *Server) sendClientPluginStop(session *yamux.Session, pluginName, ruleName string) error { func (s *Server) sendClientPluginStop(session *yamux.Session, pluginID, pluginName, ruleName string) error {
stream, err := session.Open() stream, err := session.Open()
if err != nil { if err != nil {
return err return err
@@ -1421,6 +1427,7 @@ func (s *Server) sendClientPluginStop(session *yamux.Session, pluginName, ruleNa
defer stream.Close() defer stream.Close()
req := protocol.ClientPluginStopRequest{ req := protocol.ClientPluginStopRequest{
PluginID: pluginID,
PluginName: pluginName, PluginName: pluginName,
RuleName: ruleName, RuleName: ruleName,
} }
@@ -1566,7 +1573,7 @@ func (s *Server) ProxyPluginAPIRequest(clientID string, req protocol.PluginAPIRe
} }
// RestartClientPlugin 重启客户端 JS 插件 // RestartClientPlugin 重启客户端 JS 插件
func (s *Server) RestartClientPlugin(clientID, pluginName, ruleName string) error { func (s *Server) RestartClientPlugin(clientID, pluginID, pluginName, ruleName string) error {
s.mu.RLock() s.mu.RLock()
_, ok := s.clients[clientID] _, ok := s.clients[clientID]
s.mu.RUnlock() s.mu.RUnlock()
@@ -1670,7 +1677,7 @@ func (s *Server) sendJSPluginRestart(session *yamux.Session, pluginName, ruleNam
} }
// UpdateClientPluginConfig 更新客户端插件配置 // UpdateClientPluginConfig 更新客户端插件配置
func (s *Server) UpdateClientPluginConfig(clientID, pluginName, ruleName string, config map[string]string, restart bool) error { func (s *Server) UpdateClientPluginConfig(clientID, pluginID, pluginName, ruleName string, config map[string]string, restart bool) error {
s.mu.RLock() s.mu.RLock()
cs, ok := s.clients[clientID] cs, ok := s.clients[clientID]
s.mu.RUnlock() s.mu.RUnlock()
@@ -1687,6 +1694,7 @@ func (s *Server) UpdateClientPluginConfig(clientID, pluginName, ruleName string,
defer stream.Close() defer stream.Close()
req := protocol.PluginConfigUpdateRequest{ req := protocol.PluginConfigUpdateRequest{
PluginID: pluginID,
PluginName: pluginName, PluginName: pluginName,
RuleName: ruleName, RuleName: ruleName,
Config: config, Config: config,

View File

@@ -101,6 +101,7 @@ type ProxyRule struct {
RemotePort int `json:"remote_port" yaml:"remote_port"` // 服务端监听端口 RemotePort int `json:"remote_port" yaml:"remote_port"` // 服务端监听端口
Enabled *bool `json:"enabled,omitempty" yaml:"enabled"` // 是否启用,默认为 true Enabled *bool `json:"enabled,omitempty" yaml:"enabled"` // 是否启用,默认为 true
// Plugin 支持字段 // Plugin 支持字段
PluginID string `json:"plugin_id,omitempty" yaml:"plugin_id"` // 插件实例ID
PluginName string `json:"plugin_name,omitempty" yaml:"plugin_name"` PluginName string `json:"plugin_name,omitempty" yaml:"plugin_name"`
PluginVersion string `json:"plugin_version,omitempty" yaml:"plugin_version"` PluginVersion string `json:"plugin_version,omitempty" yaml:"plugin_version"`
PluginConfig map[string]string `json:"plugin_config,omitempty" yaml:"plugin_config"` PluginConfig map[string]string `json:"plugin_config,omitempty" yaml:"plugin_config"`
@@ -218,6 +219,7 @@ type ClientPluginStartRequest struct {
// ClientPluginStopRequest 停止客户端插件请求 // ClientPluginStopRequest 停止客户端插件请求
type ClientPluginStopRequest struct { type ClientPluginStopRequest struct {
PluginID string `json:"plugin_id,omitempty"` // 插件ID优先使用
PluginName string `json:"plugin_name"` // 插件名称 PluginName string `json:"plugin_name"` // 插件名称
RuleName string `json:"rule_name"` // 规则名称 RuleName string `json:"rule_name"` // 规则名称
} }
@@ -233,6 +235,7 @@ type ClientPluginStatusResponse struct {
// ClientPluginConnRequest 客户端插件连接请求 // ClientPluginConnRequest 客户端插件连接请求
type ClientPluginConnRequest struct { type ClientPluginConnRequest struct {
PluginID string `json:"plugin_id,omitempty"` // 插件ID优先使用
PluginName string `json:"plugin_name"` // 插件名称 PluginName string `json:"plugin_name"` // 插件名称
RuleName string `json:"rule_name"` // 规则名称 RuleName string `json:"rule_name"` // 规则名称
} }
@@ -280,6 +283,7 @@ type ClientRestartResponse struct {
// PluginConfigUpdateRequest 插件配置更新请求 // PluginConfigUpdateRequest 插件配置更新请求
type PluginConfigUpdateRequest struct { type PluginConfigUpdateRequest struct {
PluginID string `json:"plugin_id,omitempty"` // 插件ID优先使用
PluginName string `json:"plugin_name"` // 插件名称 PluginName string `json:"plugin_name"` // 插件名称
RuleName string `json:"rule_name"` // 规则名称 RuleName string `json:"rule_name"` // 规则名称
Config map[string]string `json:"config"` // 新配置 Config map[string]string `json:"config"` // 新配置

View File

@@ -37,6 +37,7 @@ $Platforms = @(
@{OS="darwin"; Arch="amd64"}, @{OS="darwin"; Arch="amd64"},
@{OS="darwin"; Arch="arm64"} @{OS="darwin"; Arch="arm64"}
) )
)
# 颜色输出函数 # 颜色输出函数
function Write-Info { function Write-Info {