diff --git a/internal/server/router/api.go b/internal/server/router/api.go index 7a3623b..87f467c 100644 --- a/internal/server/router/api.go +++ b/internal/server/router/api.go @@ -62,15 +62,22 @@ type ConfigField struct { Description string `json:"description,omitempty"` } +// RuleSchema 规则表单模式 +type RuleSchema struct { + NeedsLocalAddr bool `json:"needs_local_addr"` + ExtraFields []ConfigField `json:"extra_fields,omitempty"` +} + // PluginInfo 插件信息 type PluginInfo struct { - Name string `json:"name"` - Version string `json:"version"` - Type string `json:"type"` - Description string `json:"description"` - Source string `json:"source"` - Icon string `json:"icon,omitempty"` - Enabled bool `json:"enabled"` + Name string `json:"name"` + Version string `json:"version"` + Type string `json:"type"` + Description string `json:"description"` + Source string `json:"source"` + Icon string `json:"icon,omitempty"` + Enabled bool `json:"enabled"` + RuleSchema *RuleSchema `json:"rule_schema,omitempty"` } // AppInterface 应用接口 diff --git a/internal/server/tunnel/server.go b/internal/server/tunnel/server.go index da4f7cd..a68e519 100644 --- a/internal/server/tunnel/server.go +++ b/internal/server/tunnel/server.go @@ -525,14 +525,35 @@ func (s *Server) GetPluginList() []router.PluginInfo { } for _, info := range s.pluginRegistry.List() { - result = append(result, router.PluginInfo{ + pi := router.PluginInfo{ Name: info.Metadata.Name, Version: info.Metadata.Version, Type: string(info.Metadata.Type), Description: info.Metadata.Description, Source: string(info.Metadata.Source), Enabled: info.Enabled, - }) + } + + // 转换 RuleSchema + if info.Metadata.RuleSchema != nil { + rs := &router.RuleSchema{ + NeedsLocalAddr: info.Metadata.RuleSchema.NeedsLocalAddr, + } + for _, f := range info.Metadata.RuleSchema.ExtraFields { + rs.ExtraFields = append(rs.ExtraFields, router.ConfigField{ + Key: f.Key, + Label: f.Label, + Type: string(f.Type), + Default: f.Default, + Required: f.Required, + Options: f.Options, + Description: f.Description, + }) + } + pi.RuleSchema = rs + } + + result = append(result, pi) } return result } diff --git a/pkg/plugin/api.go b/pkg/plugin/api.go new file mode 100644 index 0000000..89a1eaf --- /dev/null +++ b/pkg/plugin/api.go @@ -0,0 +1,103 @@ +package plugin + +import ( + "net" + "time" +) + +// ============================================================================= +// 核心接口定义 - 按职责分离 +// ============================================================================= + +// Dialer 网络拨号接口(已在 types.go 中定义,此处为文档说明) +// type Dialer interface { +// Dial(network, address string) (net.Conn, error) +// } + +// PortManager 端口管理接口(仅服务端可用) +type PortManager interface { + // ReservePort 预留端口,返回错误如果端口已被占用 + ReservePort(port int) error + // ReleasePort 释放端口 + ReleasePort(port int) + // IsPortAvailable 检查端口是否可用 + IsPortAvailable(port int) bool +} + +// RuleManager 代理规则管理接口(仅服务端可用) +type RuleManager interface { + // CreateRule 创建代理规则 + CreateRule(rule *RuleConfig) error + // DeleteRule 删除代理规则 + DeleteRule(clientID, ruleName string) error + // GetRules 获取客户端的代理规则 + GetRules(clientID string) ([]RuleConfig, error) + // UpdateRule 更新代理规则 + UpdateRule(clientID string, rule *RuleConfig) error +} + +// ClientManager 客户端管理接口(仅服务端可用) +type ClientManager interface { + // GetClientList 获取所有客户端列表 + GetClientList() ([]ClientInfo, error) + // IsClientOnline 检查客户端是否在线 + IsClientOnline(clientID string) bool +} + +// Logger 日志接口 +type Logger interface { + // Log 记录日志 + Log(level LogLevel, format string, args ...interface{}) +} + +// ConfigStore 配置存储接口 +type ConfigStore interface { + // GetConfig 获取配置值 + GetConfig(key string) string + // SetConfig 设置配置值 + SetConfig(key, value string) +} + +// EventBus 事件总线接口 +type EventBus interface { + // OnEvent 订阅事件 + OnEvent(eventType EventType, handler EventHandler) + // EmitEvent 发送事件 + EmitEvent(event *Event) +} + +// ============================================================================= +// 组合接口 +// ============================================================================= + +// PluginAPI 插件 API 主接口,组合所有子接口 +// 插件可以通过此接口访问 GoTunnel 的功能 +type PluginAPI interface { + // 网络操作 + Dial(network, address string) (net.Conn, error) + DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) + Listen(network, address string) (net.Listener, error) + + // 端口管理(服务端) + PortManager + + // 规则管理(服务端) + RuleManager + + // 客户端管理(服务端) + ClientManager + + // 日志 + Logger + + // 配置 + ConfigStore + + // 事件 + EventBus + + // 上下文 + GetContext() *Context + GetClientID() string + GetServerInfo() *ServerInfo +} diff --git a/pkg/plugin/base.go b/pkg/plugin/base.go new file mode 100644 index 0000000..20d8bee --- /dev/null +++ b/pkg/plugin/base.go @@ -0,0 +1,90 @@ +package plugin + +import ( + "fmt" + "log" + "sync" +) + +// ============================================================================= +// 基础实现 - 提取公共代码 +// ============================================================================= + +// baseAPI 包含服务端和客户端共享的基础功能 +type baseAPI struct { + pluginName string + config map[string]string + configMu sync.RWMutex + + eventHandlers map[EventType][]EventHandler + eventMu sync.RWMutex +} + +// newBaseAPI 创建基础 API +func newBaseAPI(pluginName string, config map[string]string) *baseAPI { + cfg := config + if cfg == nil { + cfg = make(map[string]string) + } + return &baseAPI{ + pluginName: pluginName, + config: cfg, + eventHandlers: make(map[EventType][]EventHandler), + } +} + +// Log 记录日志 +func (b *baseAPI) Log(level LogLevel, format string, args ...interface{}) { + prefix := fmt.Sprintf("[Plugin:%s] ", b.pluginName) + msg := fmt.Sprintf(format, args...) + log.Printf("%s%s", prefix, msg) +} + +// GetConfig 获取配置值 +func (b *baseAPI) GetConfig(key string) string { + b.configMu.RLock() + defer b.configMu.RUnlock() + return b.config[key] +} + +// SetConfig 设置配置值 +func (b *baseAPI) SetConfig(key, value string) { + b.configMu.Lock() + defer b.configMu.Unlock() + b.config[key] = value +} + +// OnEvent 订阅事件 +func (b *baseAPI) OnEvent(eventType EventType, handler EventHandler) { + b.eventMu.Lock() + defer b.eventMu.Unlock() + b.eventHandlers[eventType] = append(b.eventHandlers[eventType], handler) +} + +// EmitEvent 发送事件(复制切片避免竞态条件) +func (b *baseAPI) EmitEvent(event *Event) { + b.eventMu.RLock() + handlers := make([]EventHandler, len(b.eventHandlers[event.Type])) + copy(handlers, b.eventHandlers[event.Type]) + b.eventMu.RUnlock() + + for _, handler := range handlers { + go handler(event) + } +} + +// getPluginName 获取插件名称 +func (b *baseAPI) getPluginName() string { + return b.pluginName +} + +// getConfigMap 获取配置副本 +func (b *baseAPI) getConfigMap() map[string]string { + b.configMu.RLock() + defer b.configMu.RUnlock() + result := make(map[string]string, len(b.config)) + for k, v := range b.config { + result[k] = v + } + return result +} diff --git a/pkg/plugin/builtin/socks5.go b/pkg/plugin/builtin/socks5.go index c14f37c..cde0e9a 100644 --- a/pkg/plugin/builtin/socks5.go +++ b/pkg/plugin/builtin/socks5.go @@ -50,6 +50,9 @@ func (p *SOCKS5Plugin) Metadata() plugin.PluginMetadata { Capabilities: []string{ "dial", "read", "write", "close", }, + RuleSchema: &plugin.RuleSchema{ + NeedsLocalAddr: false, // SOCKS5 不需要本地地址 + }, ConfigSchema: []plugin.ConfigField{ { Key: "auth", diff --git a/pkg/plugin/builtin/vnc.go b/pkg/plugin/builtin/vnc.go index 52a3d30..a68c6f4 100644 --- a/pkg/plugin/builtin/vnc.go +++ b/pkg/plugin/builtin/vnc.go @@ -34,6 +34,18 @@ func (p *VNCPlugin) Metadata() plugin.PluginMetadata { Capabilities: []string{ "dial", "read", "write", "close", }, + RuleSchema: &plugin.RuleSchema{ + NeedsLocalAddr: false, + ExtraFields: []plugin.ConfigField{ + { + Key: "vnc_addr", + Label: "VNC 地址", + Type: plugin.ConfigFieldString, + Default: "127.0.0.1:5900", + Description: "客户端本地 VNC 服务地址", + }, + }, + }, } } diff --git a/pkg/plugin/client_api.go b/pkg/plugin/client_api.go new file mode 100644 index 0000000..0a567cf --- /dev/null +++ b/pkg/plugin/client_api.go @@ -0,0 +1,161 @@ +package plugin + +import ( + "context" + "fmt" + "net" + "time" +) + +// ============================================================================= +// 客户端 API 实现 +// ============================================================================= + +// ClientAPI 客户端 PluginAPI 实现 +type ClientAPI struct { + *baseAPI + clientID string + dialer Dialer +} + +// ClientAPIOption 客户端 API 配置选项 +type ClientAPIOption struct { + PluginName string + ClientID string + Config map[string]string + Dialer Dialer +} + +// NewClientAPI 创建客户端 API +func NewClientAPI(opt ClientAPIOption) *ClientAPI { + return &ClientAPI{ + baseAPI: newBaseAPI(opt.PluginName, opt.Config), + clientID: opt.ClientID, + dialer: opt.Dialer, + } +} + +// --- 网络操作 --- + +// Dial 通过隧道建立连接 +func (c *ClientAPI) Dial(network, address string) (net.Conn, error) { + if c.dialer == nil { + return nil, ErrNotConnected + } + return c.dialer.Dial(network, address) +} + +// DialTimeout 带超时的连接(使用 context 避免 goroutine 泄漏) +func (c *ClientAPI) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { + if c.dialer == nil { + return nil, ErrNotConnected + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + type result struct { + conn net.Conn + err error + } + ch := make(chan result, 1) + + go func() { + conn, err := c.dialer.Dial(network, address) + select { + case ch <- result{conn, err}: + case <-ctx.Done(): + if conn != nil { + conn.Close() + } + } + }() + + select { + case r := <-ch: + return r.conn, r.err + case <-ctx.Done(): + return nil, fmt.Errorf("dial timeout") + } +} + +// Listen 客户端不支持监听 +func (c *ClientAPI) Listen(network, address string) (net.Listener, error) { + return nil, ErrNotSupported +} + +// --- 端口管理(客户端不支持)--- + +// ReservePort 客户端不支持 +func (c *ClientAPI) ReservePort(port int) error { + return ErrNotSupported +} + +// ReleasePort 客户端不支持 +func (c *ClientAPI) ReleasePort(port int) {} + +// IsPortAvailable 检查本地端口是否可用 +func (c *ClientAPI) IsPortAvailable(port int) bool { + ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return false + } + ln.Close() + return true +} + +// --- 规则管理(客户端不支持)--- + +// CreateRule 客户端不支持 +func (c *ClientAPI) CreateRule(rule *RuleConfig) error { + return ErrNotSupported +} + +// DeleteRule 客户端不支持 +func (c *ClientAPI) DeleteRule(clientID, ruleName string) error { + return ErrNotSupported +} + +// GetRules 客户端不支持 +func (c *ClientAPI) GetRules(clientID string) ([]RuleConfig, error) { + return nil, ErrNotSupported +} + +// UpdateRule 客户端不支持 +func (c *ClientAPI) UpdateRule(clientID string, rule *RuleConfig) error { + return ErrNotSupported +} + +// --- 客户端管理 --- + +// GetClientID 获取当前客户端 ID +func (c *ClientAPI) GetClientID() string { + return c.clientID +} + +// GetClientList 客户端不支持 +func (c *ClientAPI) GetClientList() ([]ClientInfo, error) { + return nil, ErrNotSupported +} + +// IsClientOnline 客户端不支持 +func (c *ClientAPI) IsClientOnline(clientID string) bool { + return false +} + +// --- 上下文 --- + +// GetContext 获取当前上下文 +func (c *ClientAPI) GetContext() *Context { + return &Context{ + PluginName: c.getPluginName(), + Side: SideClient, + ClientID: c.clientID, + Config: c.getConfigMap(), + } +} + +// GetServerInfo 客户端不支持 +func (c *ClientAPI) GetServerInfo() *ServerInfo { + return nil +} diff --git a/pkg/plugin/server_api.go b/pkg/plugin/server_api.go new file mode 100644 index 0000000..bc9ace9 --- /dev/null +++ b/pkg/plugin/server_api.go @@ -0,0 +1,180 @@ +package plugin + +import ( + "net" + "time" +) + +// ============================================================================= +// 服务端依赖接口(依赖注入) +// ============================================================================= + +// PortStore 端口存储接口 +type PortStore interface { + Reserve(port int, owner string) error + Release(port int) + IsAvailable(port int) bool +} + +// RuleStore 规则存储接口 +type RuleStore interface { + GetAll(clientID string) ([]RuleConfig, error) + Create(clientID string, rule *RuleConfig) error + Update(clientID string, rule *RuleConfig) error + Delete(clientID, ruleName string) error +} + +// ClientStore 客户端存储接口 +type ClientStore interface { + GetAll() ([]ClientInfo, error) + IsOnline(clientID string) bool +} + +// ============================================================================= +// 服务端 API 实现 +// ============================================================================= + +// ServerAPI 服务端 PluginAPI 实现 +type ServerAPI struct { + *baseAPI + portStore PortStore + ruleStore RuleStore + clientStore ClientStore + serverInfo *ServerInfo +} + +// ServerAPIOption 服务端 API 配置选项 +type ServerAPIOption struct { + PluginName string + Config map[string]string + PortStore PortStore + RuleStore RuleStore + ClientStore ClientStore + ServerInfo *ServerInfo +} + +// NewServerAPI 创建服务端 API +func NewServerAPI(opt ServerAPIOption) *ServerAPI { + return &ServerAPI{ + baseAPI: newBaseAPI(opt.PluginName, opt.Config), + portStore: opt.PortStore, + ruleStore: opt.RuleStore, + clientStore: opt.ClientStore, + serverInfo: opt.ServerInfo, + } +} + +// --- 网络操作 --- + +// Dial 服务端不支持隧道拨号 +func (s *ServerAPI) Dial(network, address string) (net.Conn, error) { + return nil, ErrNotSupported +} + +// DialTimeout 服务端不支持隧道拨号 +func (s *ServerAPI) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { + return nil, ErrNotSupported +} + +// Listen 在指定地址监听 +func (s *ServerAPI) Listen(network, address string) (net.Listener, error) { + return net.Listen(network, address) +} + +// --- 端口管理 --- + +// ReservePort 预留端口 +func (s *ServerAPI) ReservePort(port int) error { + if s.portStore == nil { + return ErrNotSupported + } + return s.portStore.Reserve(port, s.getPluginName()) +} + +// ReleasePort 释放端口 +func (s *ServerAPI) ReleasePort(port int) { + if s.portStore != nil { + s.portStore.Release(port) + } +} + +// IsPortAvailable 检查端口是否可用 +func (s *ServerAPI) IsPortAvailable(port int) bool { + if s.portStore == nil { + return false + } + return s.portStore.IsAvailable(port) +} + +// --- 规则管理 --- + +// CreateRule 创建代理规则 +func (s *ServerAPI) CreateRule(rule *RuleConfig) error { + if s.ruleStore == nil { + return ErrNotSupported + } + return s.ruleStore.Create(rule.ClientID, rule) +} + +// DeleteRule 删除代理规则 +func (s *ServerAPI) DeleteRule(clientID, ruleName string) error { + if s.ruleStore == nil { + return ErrNotSupported + } + return s.ruleStore.Delete(clientID, ruleName) +} + +// GetRules 获取客户端的代理规则 +func (s *ServerAPI) GetRules(clientID string) ([]RuleConfig, error) { + if s.ruleStore == nil { + return nil, ErrNotSupported + } + return s.ruleStore.GetAll(clientID) +} + +// UpdateRule 更新代理规则 +func (s *ServerAPI) UpdateRule(clientID string, rule *RuleConfig) error { + if s.ruleStore == nil { + return ErrNotSupported + } + return s.ruleStore.Update(clientID, rule) +} + +// --- 客户端管理 --- + +// GetClientID 服务端返回空 +func (s *ServerAPI) GetClientID() string { + return "" +} + +// GetClientList 获取所有客户端列表 +func (s *ServerAPI) GetClientList() ([]ClientInfo, error) { + if s.clientStore == nil { + return nil, ErrNotSupported + } + return s.clientStore.GetAll() +} + +// IsClientOnline 检查客户端是否在线 +func (s *ServerAPI) IsClientOnline(clientID string) bool { + if s.clientStore == nil { + return false + } + return s.clientStore.IsOnline(clientID) +} + +// --- 上下文 --- + +// GetContext 获取当前上下文 +func (s *ServerAPI) GetContext() *Context { + return &Context{ + PluginName: s.getPluginName(), + Side: SideServer, + Config: s.getConfigMap(), + } +} + +// GetServerInfo 获取服务端信息 +func (s *ServerAPI) GetServerInfo() *ServerInfo { + return s.serverInfo +} diff --git a/pkg/plugin/types.go b/pkg/plugin/types.go index 85a389a..2c93329 100644 --- a/pkg/plugin/types.go +++ b/pkg/plugin/types.go @@ -45,6 +45,12 @@ type ConfigField struct { Description string `json:"description,omitempty"` // 字段描述 } +// RuleSchema 规则表单模式定义 +type RuleSchema struct { + NeedsLocalAddr bool `json:"needs_local_addr"` // 是否需要本地地址 + ExtraFields []ConfigField `json:"extra_fields,omitempty"` // 额外字段 +} + // PluginMetadata 描述一个 plugin type PluginMetadata struct { Name string `json:"name"` // 唯一标识符 (如 "socks5") @@ -57,7 +63,8 @@ type PluginMetadata struct { Checksum string `json:"checksum,omitempty"` // WASM 二进制的 SHA256 Size int64 `json:"size,omitempty"` // WASM 二进制大小 Capabilities []string `json:"capabilities,omitempty"` // 所需 host functions - ConfigSchema []ConfigField `json:"config_schema,omitempty"`// 配置模式定义 + ConfigSchema []ConfigField `json:"config_schema,omitempty"`// 插件配置模式 + RuleSchema *RuleSchema `json:"rule_schema,omitempty"` // 规则表单模式 } // PluginInfo 组合元数据和运行时状态 @@ -90,6 +97,15 @@ type ProxyHandler interface { Close() error } +// ExtendedProxyHandler 扩展的代理处理器接口 +// 支持 PluginAPI 的插件应实现此接口 +type ExtendedProxyHandler interface { + ProxyHandler + + // SetAPI 设置 PluginAPI,允许插件调用系统功能 + SetAPI(api PluginAPI) +} + // LogLevel 日志级别 type LogLevel uint8 @@ -100,6 +116,101 @@ const ( LogError ) +// ============================================================================= +// API 相关类型 +// ============================================================================= + +// Side 运行侧 +type Side string + +const ( + SideServer Side = "server" + SideClient Side = "client" +) + +// Context 插件运行上下文 +type Context struct { + PluginName string + Side Side + ClientID string + Config map[string]string +} + +// ServerInfo 服务端信息 +type ServerInfo struct { + BindAddr string + BindPort int + Version string +} + +// RuleConfig 代理规则配置 +type RuleConfig struct { + ClientID string `json:"client_id"` + Name string `json:"name"` + Type string `json:"type"` + LocalIP string `json:"local_ip"` + LocalPort int `json:"local_port"` + RemotePort int `json:"remote_port"` + Enabled bool `json:"enabled"` + PluginName string `json:"plugin_name,omitempty"` + PluginConfig map[string]string `json:"plugin_config,omitempty"` +} + +// ClientInfo 客户端信息 +type ClientInfo struct { + ID string `json:"id"` + Nickname string `json:"nickname"` + Online bool `json:"online"` + LastPing string `json:"last_ping,omitempty"` +} + +// EventType 事件类型 +type EventType string + +const ( + EventClientConnect EventType = "client_connect" + EventClientDisconnect EventType = "client_disconnect" + EventRuleCreated EventType = "rule_created" + EventRuleDeleted EventType = "rule_deleted" + EventProxyConnect EventType = "proxy_connect" + EventProxyDisconnect EventType = "proxy_disconnect" +) + +// Event 事件 +type Event struct { + Type EventType `json:"type"` + Timestamp time.Time `json:"timestamp"` + Data map[string]interface{} `json:"data"` +} + +// EventHandler 事件处理函数 +type EventHandler func(event *Event) + +// ============================================================================= +// 错误定义 +// ============================================================================= + +// APIError API 错误 +type APIError struct { + Code int + Message string +} + +func (e *APIError) Error() string { + return e.Message +} + +// 常见 API 错误 +var ( + ErrNotSupported = &APIError{Code: 1, Message: "operation not supported"} + ErrClientNotFound = &APIError{Code: 2, Message: "client not found"} + ErrPortOccupied = &APIError{Code: 3, Message: "port already occupied"} + ErrRuleNotFound = &APIError{Code: 4, Message: "rule not found"} + ErrRuleExists = &APIError{Code: 5, Message: "rule already exists"} + ErrNotConnected = &APIError{Code: 6, Message: "not connected"} + ErrInvalidConfig = &APIError{Code: 7, Message: "invalid configuration"} +) + // ConnHandle WASM 连接句柄 type ConnHandle uint32 diff --git a/web/src/types/index.ts b/web/src/types/index.ts index fc4094d..c70c317 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -6,6 +6,7 @@ export interface ProxyRule { remote_port: number type?: string enabled?: boolean + plugin_config?: Record } // 客户端已安装的插件 @@ -27,6 +28,12 @@ export interface ConfigField { description?: string } +// 规则表单模式 +export interface RuleSchema { + needs_local_addr: boolean + extra_fields?: ConfigField[] +} + // 插件配置响应 export interface PluginConfigResponse { plugin_name: string @@ -89,6 +96,7 @@ export interface PluginInfo { source: string icon?: string enabled: boolean + rule_schema?: RuleSchema } // 扩展商店插件信息 diff --git a/web/src/views/ClientView.vue b/web/src/views/ClientView.vue index 35be8b4..ef2dcef 100644 --- a/web/src/views/ClientView.vue +++ b/web/src/views/ClientView.vue @@ -15,7 +15,7 @@ import { getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, getPlugins, installPluginsToClient, getClientPluginConfig, updateClientPluginConfig } from '../api' -import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField } from '../types' +import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField, RuleSchema } from '../types' const route = useRoute() const router = useRouter() @@ -32,14 +32,35 @@ const editing = ref(false) const editNickname = ref('') const editRules = ref([]) -const typeOptions = [ +// 内置类型 +const builtinTypes = [ { label: 'TCP', value: 'tcp' }, { label: 'UDP', value: 'udp' }, { label: 'HTTP', value: 'http' }, - { label: 'HTTPS', value: 'https' }, - { label: 'SOCKS5', value: 'socks5' } + { label: 'HTTPS', value: 'https' } ] +// 规则类型选项(内置 + 插件) +const typeOptions = ref([...builtinTypes]) + +// 插件 RuleSchema 映射 +const pluginRuleSchemas = ref>({}) + +// 判断类型是否需要本地地址 +const needsLocalAddr = (type: string) => { + // 内置类型 + if (['tcp', 'udp'].includes(type)) return true + // 插件类型:查询 RuleSchema + const schema = pluginRuleSchemas.value[type] + return schema?.needs_local_addr ?? false +} + +// 获取类型的额外字段 +const getExtraFields = (type: string): ConfigField[] => { + const schema = pluginRuleSchemas.value[type] + return schema?.extra_fields || [] +} + // 插件安装相关 const showInstallModal = ref(false) const availablePlugins = ref([]) @@ -56,6 +77,21 @@ const loadPlugins = async () => { try { const { data } = await getPlugins() availablePlugins.value = (data || []).filter(p => p.enabled) + + // 更新类型选项:内置类型 + proxy 类型插件 + const proxyPlugins = availablePlugins.value + .filter(p => p.type === 'proxy') + .map(p => ({ label: `${p.name.toUpperCase()} (插件)`, value: p.name })) + typeOptions.value = [...builtinTypes, ...proxyPlugins] + + // 保存插件的 RuleSchema + const schemas: Record = {} + for (const p of availablePlugins.value) { + if (p.rule_schema) { + schemas[p.name] = p.rule_schema + } + } + pluginRuleSchemas.value = schemas } catch (e) { console.error('Failed to load plugins', e) } @@ -63,6 +99,9 @@ const loadPlugins = async () => { const openInstallModal = async () => { await loadPlugins() + // 过滤掉已安装的插件 + const installedNames = clientPlugins.value.map(p => p.name) + availablePlugins.value = availablePlugins.value.filter(p => !installedNames.includes(p.name)) selectedPlugins.value = [] showInstallModal.value = true } @@ -85,7 +124,10 @@ const loadClient = async () => { } } -onMounted(loadClient) +onMounted(() => { + loadClient() + loadPlugins() +}) const startEdit = () => { editNickname.value = nickname.value @@ -316,9 +358,14 @@ const savePluginConfig = async () => { {{ rule.name || '未命名' }} - {{ rule.local_ip }}:{{ rule.local_port }} + + + - + {{ rule.remote_port }} - {{ rule.type || 'tcp' }} + {{ (rule.type || 'tcp').toUpperCase() }} {{ rule.enabled !== false ? '启用' : '禁用' }} @@ -336,7 +383,7 @@ const savePluginConfig = async () => { - + @@ -344,17 +391,31 @@ const savePluginConfig = async () => { - - - - - - - + + + + +