111
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m26s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 49s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m1s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 54s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 39s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m32s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 51s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m0s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 58s
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m26s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 49s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m1s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 54s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 39s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m32s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 51s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m0s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 58s
This commit is contained in:
@@ -194,6 +194,9 @@ func (c *Client) handleStream(stream net.Conn) {
|
|||||||
c.handleProxyConnect(stream, msg)
|
c.handleProxyConnect(stream, msg)
|
||||||
case protocol.MsgTypeUDPData:
|
case protocol.MsgTypeUDPData:
|
||||||
c.handleUDPData(stream, msg)
|
c.handleUDPData(stream, msg)
|
||||||
|
case protocol.MsgTypePluginConfig:
|
||||||
|
defer stream.Close()
|
||||||
|
c.handlePluginConfig(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,3 +349,28 @@ func (c *Client) findRuleByPort(port int) *protocol.ProxyRule {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handlePluginConfig 处理插件配置同步
|
||||||
|
func (c *Client) handlePluginConfig(msg *protocol.Message) {
|
||||||
|
var cfg protocol.PluginConfigSync
|
||||||
|
if err := msg.ParsePayload(&cfg); err != nil {
|
||||||
|
log.Printf("[Client] Parse plugin config error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[Client] Received config for plugin: %s", cfg.PluginName)
|
||||||
|
|
||||||
|
// 应用配置到插件
|
||||||
|
if c.pluginRegistry != nil {
|
||||||
|
handler, err := c.pluginRegistry.Get(cfg.PluginName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Client] Plugin %s not found: %v", cfg.PluginName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := handler.Init(cfg.Config); err != nil {
|
||||||
|
log.Printf("[Client] Plugin %s init error: %v", cfg.PluginName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[Client] Plugin %s config applied", cfg.PluginName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type ClientPlugin struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
Config map[string]string `json:"config,omitempty"` // 插件配置
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client 客户端数据
|
// Client 客户端数据
|
||||||
|
|||||||
@@ -46,6 +46,20 @@ type ServerInterface interface {
|
|||||||
EnablePlugin(name string) error
|
EnablePlugin(name string) error
|
||||||
DisablePlugin(name string) error
|
DisablePlugin(name string) error
|
||||||
InstallPluginsToClient(clientID string, plugins []string) error
|
InstallPluginsToClient(clientID string, plugins []string) error
|
||||||
|
// 插件配置
|
||||||
|
GetPluginConfigSchema(name string) ([]ConfigField, error)
|
||||||
|
SyncPluginConfigToClient(clientID string, pluginName string, config map[string]string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigField 配置字段(从 plugin 包导出)
|
||||||
|
type ConfigField struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Default string `json:"default,omitempty"`
|
||||||
|
Required bool `json:"required,omitempty"`
|
||||||
|
Options []string `json:"options,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PluginInfo 插件信息
|
// PluginInfo 插件信息
|
||||||
@@ -92,6 +106,7 @@ func RegisterRoutes(r *Router, app AppInterface) {
|
|||||||
api.HandleFunc("/plugins", h.handlePlugins)
|
api.HandleFunc("/plugins", h.handlePlugins)
|
||||||
api.HandleFunc("/plugin/", h.handlePlugin)
|
api.HandleFunc("/plugin/", h.handlePlugin)
|
||||||
api.HandleFunc("/store/plugins", h.handleStorePlugins)
|
api.HandleFunc("/store/plugins", h.handleStorePlugins)
|
||||||
|
api.HandleFunc("/client-plugin/", h.handleClientPlugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *APIHandler) handleStatus(rw http.ResponseWriter, r *http.Request) {
|
func (h *APIHandler) handleStatus(rw http.ResponseWriter, r *http.Request) {
|
||||||
@@ -586,3 +601,143 @@ func (h *APIHandler) handleStorePlugins(rw http.ResponseWriter, r *http.Request)
|
|||||||
"store_url": storeURL,
|
"store_url": storeURL,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleClientPlugin 处理客户端插件配置
|
||||||
|
// 路由: /api/client-plugin/{clientID}/{pluginName}/config
|
||||||
|
func (h *APIHandler) handleClientPlugin(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
path := r.URL.Path[len("/api/client-plugin/"):]
|
||||||
|
if path == "" {
|
||||||
|
http.Error(rw, "client id required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析路径: clientID/pluginName/action
|
||||||
|
parts := splitPathMulti(path)
|
||||||
|
if len(parts) < 3 {
|
||||||
|
http.Error(rw, "invalid path, expected: /api/client-plugin/{clientID}/{pluginName}/config", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientID := parts[0]
|
||||||
|
pluginName := parts[1]
|
||||||
|
action := parts[2]
|
||||||
|
|
||||||
|
if action != "config" {
|
||||||
|
http.Error(rw, "invalid action", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
h.getClientPluginConfig(rw, clientID, pluginName)
|
||||||
|
case http.MethodPut:
|
||||||
|
h.updateClientPluginConfig(rw, r, clientID, pluginName)
|
||||||
|
default:
|
||||||
|
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitPathMulti 分割路径为多个部分
|
||||||
|
func splitPathMulti(path string) []string {
|
||||||
|
var parts []string
|
||||||
|
start := 0
|
||||||
|
for i, c := range path {
|
||||||
|
if c == '/' {
|
||||||
|
if i > start {
|
||||||
|
parts = append(parts, path[start:i])
|
||||||
|
}
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start < len(path) {
|
||||||
|
parts = append(parts, path[start:])
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClientPluginConfig 获取客户端插件配置
|
||||||
|
func (h *APIHandler) getClientPluginConfig(rw http.ResponseWriter, clientID, pluginName string) {
|
||||||
|
client, err := h.clientStore.GetClient(clientID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, "client not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取插件配置模式
|
||||||
|
schema, err := h.server.GetPluginConfigSchema(pluginName)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找客户端的插件配置
|
||||||
|
var config map[string]string
|
||||||
|
for _, p := range client.Plugins {
|
||||||
|
if p.Name == pluginName {
|
||||||
|
config = p.Config
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config == nil {
|
||||||
|
config = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.jsonResponse(rw, map[string]interface{}{
|
||||||
|
"plugin_name": pluginName,
|
||||||
|
"schema": schema,
|
||||||
|
"config": config,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateClientPluginConfig 更新客户端插件配置
|
||||||
|
func (h *APIHandler) updateClientPluginConfig(rw http.ResponseWriter, r *http.Request, clientID, pluginName string) {
|
||||||
|
var req struct {
|
||||||
|
Config map[string]string `json:"config"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := h.clientStore.GetClient(clientID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, "client not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新插件配置
|
||||||
|
found := false
|
||||||
|
for i, p := range client.Plugins {
|
||||||
|
if p.Name == pluginName {
|
||||||
|
client.Plugins[i].Config = req.Config
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
http.Error(rw, "plugin not installed on client", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
if err := h.clientStore.UpdateClient(client); err != nil {
|
||||||
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果客户端在线,同步配置
|
||||||
|
online, _ := h.server.GetClientStatus(clientID)
|
||||||
|
if online {
|
||||||
|
if err := h.server.SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil {
|
||||||
|
// 配置已保存,但同步失败,返回警告
|
||||||
|
h.jsonResponse(rw, map[string]interface{}{
|
||||||
|
"status": "partial",
|
||||||
|
"message": "config saved but sync failed: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.jsonResponse(rw, map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
|
|||||||
@@ -700,3 +700,62 @@ func (s *Server) sendUDPPacket(cs *ClientSession, conn *net.UDPConn, clientAddr
|
|||||||
conn.WriteToUDP(respPacket.Data, clientAddr)
|
conn.WriteToUDP(respPacket.Data, clientAddr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPluginConfigSchema 获取插件配置模式
|
||||||
|
func (s *Server) GetPluginConfigSchema(name string) ([]router.ConfigField, error) {
|
||||||
|
if s.pluginRegistry == nil {
|
||||||
|
return nil, fmt.Errorf("plugin registry not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
handler, err := s.pluginRegistry.Get(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("plugin %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := handler.Metadata()
|
||||||
|
var result []router.ConfigField
|
||||||
|
for _, f := range metadata.ConfigSchema {
|
||||||
|
result = append(result, router.ConfigField{
|
||||||
|
Key: f.Key,
|
||||||
|
Label: f.Label,
|
||||||
|
Type: string(f.Type),
|
||||||
|
Default: f.Default,
|
||||||
|
Required: f.Required,
|
||||||
|
Options: f.Options,
|
||||||
|
Description: f.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncPluginConfigToClient 同步插件配置到客户端
|
||||||
|
func (s *Server) SyncPluginConfigToClient(clientID string, pluginName string, config map[string]string) error {
|
||||||
|
s.mu.RLock()
|
||||||
|
cs, ok := s.clients[clientID]
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("client %s not online", clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.sendPluginConfig(cs.Session, pluginName, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendPluginConfig 发送插件配置到客户端
|
||||||
|
func (s *Server) sendPluginConfig(session *yamux.Session, pluginName string, config map[string]string) error {
|
||||||
|
stream, err := session.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stream.Close()
|
||||||
|
|
||||||
|
req := protocol.PluginConfigSync{
|
||||||
|
PluginName: pluginName,
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
msg, err := protocol.NewMessage(protocol.MsgTypePluginConfig, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return protocol.WriteMessage(stream, msg)
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ func init() {
|
|||||||
const (
|
const (
|
||||||
socks5Version = 0x05
|
socks5Version = 0x05
|
||||||
noAuth = 0x00
|
noAuth = 0x00
|
||||||
|
userPassAuth = 0x02
|
||||||
|
noAcceptable = 0xFF
|
||||||
|
userPassAuthVer = 0x01
|
||||||
|
authSuccess = 0x00
|
||||||
|
authFailure = 0x01
|
||||||
cmdConnect = 0x01
|
cmdConnect = 0x01
|
||||||
atypIPv4 = 0x01
|
atypIPv4 = 0x01
|
||||||
atypDomain = 0x03
|
atypDomain = 0x03
|
||||||
@@ -45,6 +50,28 @@ func (p *SOCKS5Plugin) Metadata() plugin.PluginMetadata {
|
|||||||
Capabilities: []string{
|
Capabilities: []string{
|
||||||
"dial", "read", "write", "close",
|
"dial", "read", "write", "close",
|
||||||
},
|
},
|
||||||
|
ConfigSchema: []plugin.ConfigField{
|
||||||
|
{
|
||||||
|
Key: "auth",
|
||||||
|
Label: "认证方式",
|
||||||
|
Type: plugin.ConfigFieldSelect,
|
||||||
|
Default: "none",
|
||||||
|
Options: []string{"none", "password"},
|
||||||
|
Description: "SOCKS5 认证方式",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "username",
|
||||||
|
Label: "用户名",
|
||||||
|
Type: plugin.ConfigFieldString,
|
||||||
|
Description: "认证用户名(仅 password 认证时需要)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "password",
|
||||||
|
Label: "密码",
|
||||||
|
Type: plugin.ConfigFieldPassword,
|
||||||
|
Description: "认证密码(仅 password 认证时需要)",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +137,31 @@ func (p *SOCKS5Plugin) handshake(conn net.Conn) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应:使用无认证
|
// 检查是否需要密码认证
|
||||||
|
if p.config["auth"] == "password" {
|
||||||
|
// 检查客户端是否支持用户名密码认证
|
||||||
|
supported := false
|
||||||
|
for _, m := range methods {
|
||||||
|
if m == userPassAuth {
|
||||||
|
supported = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !supported {
|
||||||
|
conn.Write([]byte{socks5Version, noAcceptable})
|
||||||
|
return errors.New("client does not support password auth")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择用户名密码认证
|
||||||
|
if _, err := conn.Write([]byte{socks5Version, userPassAuth}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行用户名密码认证
|
||||||
|
return p.authenticateUserPass(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无认证
|
||||||
_, err := conn.Write([]byte{socks5Version, noAuth})
|
_, err := conn.Write([]byte{socks5Version, noAuth})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -163,6 +214,48 @@ func (p *SOCKS5Plugin) readRequest(conn net.Conn) (string, error) {
|
|||||||
return fmt.Sprintf("%s:%d", host, port), nil
|
return fmt.Sprintf("%s:%d", host, port), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// authenticateUserPass 用户名密码认证
|
||||||
|
func (p *SOCKS5Plugin) authenticateUserPass(conn net.Conn) error {
|
||||||
|
// 读取认证版本
|
||||||
|
buf := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if buf[0] != userPassAuthVer {
|
||||||
|
return errors.New("unsupported auth version")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取用户名
|
||||||
|
ulen := int(buf[1])
|
||||||
|
username := make([]byte, ulen)
|
||||||
|
if _, err := io.ReadFull(conn, username); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取密码长度和密码
|
||||||
|
plenBuf := make([]byte, 1)
|
||||||
|
if _, err := io.ReadFull(conn, plenBuf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
plen := int(plenBuf[0])
|
||||||
|
password := make([]byte, plen)
|
||||||
|
if _, err := io.ReadFull(conn, password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证用户名密码
|
||||||
|
expectedUser := p.config["username"]
|
||||||
|
expectedPass := p.config["password"]
|
||||||
|
|
||||||
|
if string(username) == expectedUser && string(password) == expectedPass {
|
||||||
|
conn.Write([]byte{userPassAuthVer, authSuccess})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Write([]byte{userPassAuthVer, authFailure})
|
||||||
|
return errors.New("authentication failed")
|
||||||
|
}
|
||||||
|
|
||||||
// sendReply 发送响应
|
// sendReply 发送响应
|
||||||
func (p *SOCKS5Plugin) sendReply(conn net.Conn, rep byte) error {
|
func (p *SOCKS5Plugin) sendReply(conn net.Conn, rep byte) error {
|
||||||
reply := []byte{socks5Version, rep, 0x00, atypIPv4, 0, 0, 0, 0, 0, 0}
|
reply := []byte{socks5Version, rep, 0x00, atypIPv4, 0, 0, 0, 0, 0, 0}
|
||||||
|
|||||||
@@ -23,6 +23,28 @@ const (
|
|||||||
PluginSourceWASM PluginSource = "wasm" // WASM 模块
|
PluginSourceWASM PluginSource = "wasm" // WASM 模块
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ConfigFieldType 配置字段类型
|
||||||
|
type ConfigFieldType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigFieldString ConfigFieldType = "string"
|
||||||
|
ConfigFieldNumber ConfigFieldType = "number"
|
||||||
|
ConfigFieldBool ConfigFieldType = "bool"
|
||||||
|
ConfigFieldSelect ConfigFieldType = "select" // 下拉选择
|
||||||
|
ConfigFieldPassword ConfigFieldType = "password" // 密码输入
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigField 配置字段定义
|
||||||
|
type ConfigField struct {
|
||||||
|
Key string `json:"key"` // 配置键名
|
||||||
|
Label string `json:"label"` // 显示标签
|
||||||
|
Type ConfigFieldType `json:"type"` // 字段类型
|
||||||
|
Default string `json:"default,omitempty"` // 默认值
|
||||||
|
Required bool `json:"required,omitempty"` // 是否必填
|
||||||
|
Options []string `json:"options,omitempty"` // select 类型的选项
|
||||||
|
Description string `json:"description,omitempty"` // 字段描述
|
||||||
|
}
|
||||||
|
|
||||||
// PluginMetadata 描述一个 plugin
|
// PluginMetadata 描述一个 plugin
|
||||||
type PluginMetadata struct {
|
type PluginMetadata struct {
|
||||||
Name string `json:"name"` // 唯一标识符 (如 "socks5")
|
Name string `json:"name"` // 唯一标识符 (如 "socks5")
|
||||||
@@ -35,7 +57,7 @@ type PluginMetadata struct {
|
|||||||
Checksum string `json:"checksum,omitempty"` // WASM 二进制的 SHA256
|
Checksum string `json:"checksum,omitempty"` // WASM 二进制的 SHA256
|
||||||
Size int64 `json:"size,omitempty"` // WASM 二进制大小
|
Size int64 `json:"size,omitempty"` // WASM 二进制大小
|
||||||
Capabilities []string `json:"capabilities,omitempty"` // 所需 host functions
|
Capabilities []string `json:"capabilities,omitempty"` // 所需 host functions
|
||||||
ConfigSchema map[string]string `json:"config_schema,omitempty"`
|
ConfigSchema []ConfigField `json:"config_schema,omitempty"`// 配置模式定义
|
||||||
}
|
}
|
||||||
|
|
||||||
// PluginInfo 组合元数据和运行时状态
|
// PluginInfo 组合元数据和运行时状态
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const (
|
|||||||
|
|
||||||
// 插件安装消息
|
// 插件安装消息
|
||||||
MsgTypeInstallPlugins uint8 = 24 // 服务端推送安装插件列表
|
MsgTypeInstallPlugins uint8 = 24 // 服务端推送安装插件列表
|
||||||
|
MsgTypePluginConfig uint8 = 25 // 插件配置同步
|
||||||
)
|
)
|
||||||
|
|
||||||
// Message 基础消息结构
|
// Message 基础消息结构
|
||||||
@@ -155,6 +156,12 @@ type InstallPluginsRequest struct {
|
|||||||
Plugins []string `json:"plugins"` // 要安装的插件名称列表
|
Plugins []string `json:"plugins"` // 要安装的插件名称列表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PluginConfigSync 插件配置同步
|
||||||
|
type PluginConfigSync struct {
|
||||||
|
PluginName string `json:"plugin_name"` // 插件名称
|
||||||
|
Config map[string]string `json:"config"` // 配置内容
|
||||||
|
}
|
||||||
|
|
||||||
// UDPPacket UDP 数据包
|
// UDPPacket UDP 数据包
|
||||||
type UDPPacket struct {
|
type UDPPacket struct {
|
||||||
RemotePort int `json:"remote_port"` // 服务端监听端口
|
RemotePort int `json:"remote_port"` // 服务端监听端口
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { get, post, put, del } from '../config/axios'
|
import { get, post, put, del } from '../config/axios'
|
||||||
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo, StorePluginInfo } from '../types'
|
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo, StorePluginInfo, PluginConfigResponse } from '../types'
|
||||||
|
|
||||||
// 重新导出 token 管理方法
|
// 重新导出 token 管理方法
|
||||||
export { getToken, setToken, removeToken } from '../config/axios'
|
export { getToken, setToken, removeToken } from '../config/axios'
|
||||||
@@ -33,3 +33,9 @@ export const disablePlugin = (name: string) => post(`/plugin/${name}/disable`)
|
|||||||
|
|
||||||
// 扩展商店
|
// 扩展商店
|
||||||
export const getStorePlugins = () => get<{ plugins: StorePluginInfo[], store_url: string }>('/store/plugins')
|
export const getStorePlugins = () => get<{ plugins: StorePluginInfo[], store_url: string }>('/store/plugins')
|
||||||
|
|
||||||
|
// 客户端插件配置
|
||||||
|
export const getClientPluginConfig = (clientId: string, pluginName: string) =>
|
||||||
|
get<PluginConfigResponse>(`/client-plugin/${clientId}/${pluginName}/config`)
|
||||||
|
export const updateClientPluginConfig = (clientId: string, pluginName: string, config: Record<string, string>) =>
|
||||||
|
put(`/client-plugin/${clientId}/${pluginName}/config`, { config })
|
||||||
|
|||||||
@@ -13,6 +13,25 @@ export interface ClientPlugin {
|
|||||||
name: string
|
name: string
|
||||||
version: string
|
version: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
config?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件配置字段
|
||||||
|
export interface ConfigField {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
type: 'string' | 'number' | 'bool' | 'select' | 'password'
|
||||||
|
default?: string
|
||||||
|
required?: boolean
|
||||||
|
options?: string[]
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件配置响应
|
||||||
|
export interface PluginConfigResponse {
|
||||||
|
plugin_name: string
|
||||||
|
schema: ConfigField[]
|
||||||
|
config: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
// 客户端配置
|
// 客户端配置
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import {
|
|||||||
import {
|
import {
|
||||||
ArrowBackOutline, CreateOutline, TrashOutline,
|
ArrowBackOutline, CreateOutline, TrashOutline,
|
||||||
PushOutline, PowerOutline, AddOutline, SaveOutline, CloseOutline,
|
PushOutline, PowerOutline, AddOutline, SaveOutline, CloseOutline,
|
||||||
DownloadOutline
|
DownloadOutline, SettingsOutline
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
import { getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, getPlugins, installPluginsToClient } from '../api'
|
import {
|
||||||
import type { ProxyRule, PluginInfo, ClientPlugin } from '../types'
|
getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient,
|
||||||
|
getPlugins, installPluginsToClient, getClientPluginConfig, updateClientPluginConfig
|
||||||
|
} from '../api'
|
||||||
|
import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField } from '../types'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -42,6 +45,13 @@ const showInstallModal = ref(false)
|
|||||||
const availablePlugins = ref<PluginInfo[]>([])
|
const availablePlugins = ref<PluginInfo[]>([])
|
||||||
const selectedPlugins = ref<string[]>([])
|
const selectedPlugins = ref<string[]>([])
|
||||||
|
|
||||||
|
// 插件配置相关
|
||||||
|
const showConfigModal = ref(false)
|
||||||
|
const configPluginName = ref('')
|
||||||
|
const configSchema = ref<ConfigField[]>([])
|
||||||
|
const configValues = ref<Record<string, string>>({})
|
||||||
|
const configLoading = ref(false)
|
||||||
|
|
||||||
const loadPlugins = async () => {
|
const loadPlugins = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await getPlugins()
|
const { data } = await getPlugins()
|
||||||
@@ -189,6 +199,42 @@ const toggleClientPlugin = async (plugin: ClientPlugin) => {
|
|||||||
message.error('操作失败')
|
message.error('操作失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开插件配置模态框
|
||||||
|
const openConfigModal = async (plugin: ClientPlugin) => {
|
||||||
|
configPluginName.value = plugin.name
|
||||||
|
configLoading.value = true
|
||||||
|
showConfigModal.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await getClientPluginConfig(clientId, plugin.name)
|
||||||
|
configSchema.value = data.schema || []
|
||||||
|
configValues.value = { ...data.config }
|
||||||
|
// 填充默认值
|
||||||
|
for (const field of configSchema.value) {
|
||||||
|
if (field.default && !configValues.value[field.key]) {
|
||||||
|
configValues.value[field.key] = field.default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e.response?.data || '加载配置失败')
|
||||||
|
showConfigModal.value = false
|
||||||
|
} finally {
|
||||||
|
configLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存插件配置
|
||||||
|
const savePluginConfig = async () => {
|
||||||
|
try {
|
||||||
|
await updateClientPluginConfig(clientId, configPluginName.value, configValues.value)
|
||||||
|
message.success('配置已保存')
|
||||||
|
showConfigModal.value = false
|
||||||
|
loadClient()
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e.response?.data || '保存失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -331,6 +377,7 @@ const toggleClientPlugin = async (plugin: ClientPlugin) => {
|
|||||||
<th>名称</th>
|
<th>名称</th>
|
||||||
<th>版本</th>
|
<th>版本</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -340,6 +387,12 @@ const toggleClientPlugin = async (plugin: ClientPlugin) => {
|
|||||||
<td>
|
<td>
|
||||||
<n-switch :value="plugin.enabled" @update:value="toggleClientPlugin(plugin)" />
|
<n-switch :value="plugin.enabled" @update:value="toggleClientPlugin(plugin)" />
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<n-button size="small" quaternary @click="openConfigModal(plugin)">
|
||||||
|
<template #icon><n-icon><SettingsOutline /></n-icon></template>
|
||||||
|
配置
|
||||||
|
</n-button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</n-table>
|
</n-table>
|
||||||
@@ -377,5 +430,60 @@ const toggleClientPlugin = async (plugin: ClientPlugin) => {
|
|||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|
||||||
|
<!-- 插件配置模态框 -->
|
||||||
|
<n-modal v-model:show="showConfigModal" preset="card" :title="`${configPluginName} 配置`" style="width: 500px;">
|
||||||
|
<n-empty v-if="configLoading" description="加载中..." />
|
||||||
|
<n-empty v-else-if="configSchema.length === 0" description="该插件暂无可配置项" />
|
||||||
|
<n-space v-else vertical :size="16">
|
||||||
|
<n-form-item v-for="field in configSchema" :key="field.key" :label="field.label">
|
||||||
|
<!-- 字符串输入 -->
|
||||||
|
<n-input
|
||||||
|
v-if="field.type === 'string'"
|
||||||
|
v-model:value="configValues[field.key]"
|
||||||
|
:placeholder="field.description || field.label"
|
||||||
|
/>
|
||||||
|
<!-- 密码输入 -->
|
||||||
|
<n-input
|
||||||
|
v-else-if="field.type === 'password'"
|
||||||
|
v-model:value="configValues[field.key]"
|
||||||
|
type="password"
|
||||||
|
show-password-on="click"
|
||||||
|
:placeholder="field.description || field.label"
|
||||||
|
/>
|
||||||
|
<!-- 数字输入 -->
|
||||||
|
<n-input-number
|
||||||
|
v-else-if="field.type === 'number'"
|
||||||
|
:value="configValues[field.key] ? Number(configValues[field.key]) : undefined"
|
||||||
|
@update:value="(v: number | null) => configValues[field.key] = v !== null ? String(v) : ''"
|
||||||
|
:placeholder="field.description"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
<!-- 下拉选择 -->
|
||||||
|
<n-select
|
||||||
|
v-else-if="field.type === 'select'"
|
||||||
|
v-model:value="configValues[field.key]"
|
||||||
|
:options="(field.options || []).map(o => ({ label: o, value: o }))"
|
||||||
|
/>
|
||||||
|
<!-- 布尔开关 -->
|
||||||
|
<n-switch
|
||||||
|
v-else-if="field.type === 'bool'"
|
||||||
|
:value="configValues[field.key] === 'true'"
|
||||||
|
@update:value="(v: boolean) => configValues[field.key] = String(v)"
|
||||||
|
/>
|
||||||
|
<template #feedback v-if="field.description && field.type !== 'string' && field.type !== 'password'">
|
||||||
|
<span style="color: #999; font-size: 12px;">{{ field.description }}</span>
|
||||||
|
</template>
|
||||||
|
</n-form-item>
|
||||||
|
</n-space>
|
||||||
|
<template #footer>
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button @click="showConfigModal = false">取消</n-button>
|
||||||
|
<n-button type="primary" @click="savePluginConfig" :disabled="configSchema.length === 0">
|
||||||
|
保存
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user