111
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 29s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 48s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 37s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 59s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 36s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 57s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m6s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 50s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 35s
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 52s

This commit is contained in:
Flik
2025-12-28 16:51:40 +08:00
parent abfc235357
commit 17f38f7ef2
11 changed files with 782 additions and 25 deletions

View File

@@ -62,6 +62,12 @@ type ConfigField struct {
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
} }
// RuleSchema 规则表单模式
type RuleSchema struct {
NeedsLocalAddr bool `json:"needs_local_addr"`
ExtraFields []ConfigField `json:"extra_fields,omitempty"`
}
// PluginInfo 插件信息 // PluginInfo 插件信息
type PluginInfo struct { type PluginInfo struct {
Name string `json:"name"` Name string `json:"name"`
@@ -71,6 +77,7 @@ type PluginInfo struct {
Source string `json:"source"` Source string `json:"source"`
Icon string `json:"icon,omitempty"` Icon string `json:"icon,omitempty"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
RuleSchema *RuleSchema `json:"rule_schema,omitempty"`
} }
// AppInterface 应用接口 // AppInterface 应用接口

View File

@@ -525,15 +525,36 @@ func (s *Server) GetPluginList() []router.PluginInfo {
} }
for _, info := range s.pluginRegistry.List() { for _, info := range s.pluginRegistry.List() {
result = append(result, router.PluginInfo{ pi := router.PluginInfo{
Name: info.Metadata.Name, Name: info.Metadata.Name,
Version: info.Metadata.Version, Version: info.Metadata.Version,
Type: string(info.Metadata.Type), Type: string(info.Metadata.Type),
Description: info.Metadata.Description, Description: info.Metadata.Description,
Source: string(info.Metadata.Source), Source: string(info.Metadata.Source),
Enabled: info.Enabled, 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 return result
} }

103
pkg/plugin/api.go Normal file
View File

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

90
pkg/plugin/base.go Normal file
View File

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

View File

@@ -50,6 +50,9 @@ func (p *SOCKS5Plugin) Metadata() plugin.PluginMetadata {
Capabilities: []string{ Capabilities: []string{
"dial", "read", "write", "close", "dial", "read", "write", "close",
}, },
RuleSchema: &plugin.RuleSchema{
NeedsLocalAddr: false, // SOCKS5 不需要本地地址
},
ConfigSchema: []plugin.ConfigField{ ConfigSchema: []plugin.ConfigField{
{ {
Key: "auth", Key: "auth",

View File

@@ -34,6 +34,18 @@ func (p *VNCPlugin) Metadata() plugin.PluginMetadata {
Capabilities: []string{ Capabilities: []string{
"dial", "read", "write", "close", "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 服务地址",
},
},
},
} }
} }

161
pkg/plugin/client_api.go Normal file
View File

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

180
pkg/plugin/server_api.go Normal file
View File

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

View File

@@ -45,6 +45,12 @@ type ConfigField struct {
Description string `json:"description,omitempty"` // 字段描述 Description string `json:"description,omitempty"` // 字段描述
} }
// RuleSchema 规则表单模式定义
type RuleSchema struct {
NeedsLocalAddr bool `json:"needs_local_addr"` // 是否需要本地地址
ExtraFields []ConfigField `json:"extra_fields,omitempty"` // 额外字段
}
// PluginMetadata 描述一个 plugin // PluginMetadata 描述一个 plugin
type PluginMetadata struct { type PluginMetadata struct {
Name string `json:"name"` // 唯一标识符 (如 "socks5") Name string `json:"name"` // 唯一标识符 (如 "socks5")
@@ -57,7 +63,8 @@ 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 []ConfigField `json:"config_schema,omitempty"`// 配置模式定义 ConfigSchema []ConfigField `json:"config_schema,omitempty"`// 插件配置模式
RuleSchema *RuleSchema `json:"rule_schema,omitempty"` // 规则表单模式
} }
// PluginInfo 组合元数据和运行时状态 // PluginInfo 组合元数据和运行时状态
@@ -90,6 +97,15 @@ type ProxyHandler interface {
Close() error Close() error
} }
// ExtendedProxyHandler 扩展的代理处理器接口
// 支持 PluginAPI 的插件应实现此接口
type ExtendedProxyHandler interface {
ProxyHandler
// SetAPI 设置 PluginAPI允许插件调用系统功能
SetAPI(api PluginAPI)
}
// LogLevel 日志级别 // LogLevel 日志级别
type LogLevel uint8 type LogLevel uint8
@@ -100,6 +116,101 @@ const (
LogError 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 连接句柄 // ConnHandle WASM 连接句柄
type ConnHandle uint32 type ConnHandle uint32

View File

@@ -6,6 +6,7 @@ export interface ProxyRule {
remote_port: number remote_port: number
type?: string type?: string
enabled?: boolean enabled?: boolean
plugin_config?: Record<string, string>
} }
// 客户端已安装的插件 // 客户端已安装的插件
@@ -27,6 +28,12 @@ export interface ConfigField {
description?: string description?: string
} }
// 规则表单模式
export interface RuleSchema {
needs_local_addr: boolean
extra_fields?: ConfigField[]
}
// 插件配置响应 // 插件配置响应
export interface PluginConfigResponse { export interface PluginConfigResponse {
plugin_name: string plugin_name: string
@@ -89,6 +96,7 @@ export interface PluginInfo {
source: string source: string
icon?: string icon?: string
enabled: boolean enabled: boolean
rule_schema?: RuleSchema
} }
// 扩展商店插件信息 // 扩展商店插件信息

View File

@@ -15,7 +15,7 @@ import {
getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient, getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient,
getPlugins, installPluginsToClient, getClientPluginConfig, updateClientPluginConfig getPlugins, installPluginsToClient, getClientPluginConfig, updateClientPluginConfig
} from '../api' } from '../api'
import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField } from '../types' import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField, RuleSchema } from '../types'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -32,14 +32,35 @@ const editing = ref(false)
const editNickname = ref('') const editNickname = ref('')
const editRules = ref<ProxyRule[]>([]) const editRules = ref<ProxyRule[]>([])
const typeOptions = [ // 内置类型
const builtinTypes = [
{ label: 'TCP', value: 'tcp' }, { label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' }, { label: 'UDP', value: 'udp' },
{ label: 'HTTP', value: 'http' }, { label: 'HTTP', value: 'http' },
{ label: 'HTTPS', value: 'https' }, { label: 'HTTPS', value: 'https' }
{ label: 'SOCKS5', value: 'socks5' }
] ]
// 规则类型选项(内置 + 插件)
const typeOptions = ref([...builtinTypes])
// 插件 RuleSchema 映射
const pluginRuleSchemas = ref<Record<string, RuleSchema>>({})
// 判断类型是否需要本地地址
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 showInstallModal = ref(false)
const availablePlugins = ref<PluginInfo[]>([]) const availablePlugins = ref<PluginInfo[]>([])
@@ -56,6 +77,21 @@ const loadPlugins = async () => {
try { try {
const { data } = await getPlugins() const { data } = await getPlugins()
availablePlugins.value = (data || []).filter(p => p.enabled) 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<string, RuleSchema> = {}
for (const p of availablePlugins.value) {
if (p.rule_schema) {
schemas[p.name] = p.rule_schema
}
}
pluginRuleSchemas.value = schemas
} catch (e) { } catch (e) {
console.error('Failed to load plugins', e) console.error('Failed to load plugins', e)
} }
@@ -63,6 +99,9 @@ const loadPlugins = async () => {
const openInstallModal = async () => { const openInstallModal = async () => {
await loadPlugins() await loadPlugins()
// 过滤掉已安装的插件
const installedNames = clientPlugins.value.map(p => p.name)
availablePlugins.value = availablePlugins.value.filter(p => !installedNames.includes(p.name))
selectedPlugins.value = [] selectedPlugins.value = []
showInstallModal.value = true showInstallModal.value = true
} }
@@ -85,7 +124,10 @@ const loadClient = async () => {
} }
} }
onMounted(loadClient) onMounted(() => {
loadClient()
loadPlugins()
})
const startEdit = () => { const startEdit = () => {
editNickname.value = nickname.value editNickname.value = nickname.value
@@ -316,9 +358,14 @@ const savePluginConfig = async () => {
<tbody> <tbody>
<tr v-for="rule in rules" :key="rule.name"> <tr v-for="rule in rules" :key="rule.name">
<td>{{ rule.name || '未命名' }}</td> <td>{{ rule.name || '未命名' }}</td>
<td>{{ rule.local_ip }}:{{ rule.local_port }}</td> <td>
<template v-if="needsLocalAddr(rule.type || 'tcp')">
{{ rule.local_ip }}:{{ rule.local_port }}
</template>
<span v-else style="color: #999;">-</span>
</td>
<td>{{ rule.remote_port }}</td> <td>{{ rule.remote_port }}</td>
<td><n-tag size="small">{{ rule.type || 'tcp' }}</n-tag></td> <td><n-tag size="small">{{ (rule.type || 'tcp').toUpperCase() }}</n-tag></td>
<td> <td>
<n-tag size="small" :type="rule.enabled !== false ? 'success' : 'default'"> <n-tag size="small" :type="rule.enabled !== false ? 'success' : 'default'">
{{ rule.enabled !== false ? '启用' : '禁用' }} {{ rule.enabled !== false ? '启用' : '禁用' }}
@@ -336,7 +383,7 @@ const savePluginConfig = async () => {
<n-input v-model:value="editNickname" placeholder="给客户端起个名字(可选)" style="max-width: 300px;" /> <n-input v-model:value="editNickname" placeholder="给客户端起个名字(可选)" style="max-width: 300px;" />
</n-form-item> </n-form-item>
<n-card v-for="(rule, i) in editRules" :key="i" size="small"> <n-card v-for="(rule, i) in editRules" :key="i" size="small">
<n-space align="center"> <n-space align="center" wrap>
<n-form-item label="启用" :show-feedback="false"> <n-form-item label="启用" :show-feedback="false">
<n-switch v-model:value="rule.enabled" /> <n-switch v-model:value="rule.enabled" />
</n-form-item> </n-form-item>
@@ -344,17 +391,31 @@ const savePluginConfig = async () => {
<n-input v-model:value="rule.name" placeholder="规则名称" /> <n-input v-model:value="rule.name" placeholder="规则名称" />
</n-form-item> </n-form-item>
<n-form-item label="类型" :show-feedback="false"> <n-form-item label="类型" :show-feedback="false">
<n-select v-model:value="rule.type" :options="typeOptions" style="width: 100px;" /> <n-select v-model:value="rule.type" :options="typeOptions" style="width: 140px;" />
</n-form-item> </n-form-item>
<!-- tcp/udp 显示本地地址 -->
<template v-if="needsLocalAddr(rule.type || 'tcp')">
<n-form-item label="本地IP" :show-feedback="false"> <n-form-item label="本地IP" :show-feedback="false">
<n-input v-model:value="rule.local_ip" placeholder="127.0.0.1" /> <n-input v-model:value="rule.local_ip" placeholder="127.0.0.1" />
</n-form-item> </n-form-item>
<n-form-item label="本地端口" :show-feedback="false"> <n-form-item label="本地端口" :show-feedback="false">
<n-input-number v-model:value="rule.local_port" :show-button="false" /> <n-input-number v-model:value="rule.local_port" :show-button="false" />
</n-form-item> </n-form-item>
</template>
<n-form-item label="远程端口" :show-feedback="false"> <n-form-item label="远程端口" :show-feedback="false">
<n-input-number v-model:value="rule.remote_port" :show-button="false" /> <n-input-number v-model:value="rule.remote_port" :show-button="false" />
</n-form-item> </n-form-item>
<!-- 插件额外字段 -->
<template v-for="field in getExtraFields(rule.type || '')" :key="field.key">
<n-form-item :label="field.label" :show-feedback="false">
<n-input
v-if="field.type === 'string'"
:value="rule.plugin_config?.[field.key] || field.default || ''"
@update:value="(v: string) => { if (!rule.plugin_config) rule.plugin_config = {}; rule.plugin_config[field.key] = v }"
:placeholder="field.description"
/>
</n-form-item>
</template>
<n-button v-if="editRules.length > 1" quaternary type="error" @click="removeRule(i)"> <n-button v-if="editRules.length > 1" quaternary type="error" @click="removeRule(i)">
<template #icon><n-icon><TrashOutline /></n-icon></template> <template #icon><n-icon><TrashOutline /></n-icon></template>
</n-button> </n-button>