feat: enhance plugin configuration with remote port and schema support
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 33s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 2m35s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 55s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 3m7s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m20s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m59s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 1m6s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 2m16s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m55s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m8s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 2m57s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m56s

This commit is contained in:
2026-01-03 23:53:04 +08:00
parent 08262654d6
commit d6bde71c94
8 changed files with 148 additions and 78 deletions

View File

@@ -2,13 +2,26 @@ package db
import "github.com/gotunnel/pkg/protocol"
// ConfigField 配置字段定义
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"`
}
// ClientPlugin 客户端已安装的插件
type ClientPlugin struct {
Name string `json:"name"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
Running bool `json:"running"` // 运行状态
Config map[string]string `json:"config,omitempty"` // 插件配置
Name string `json:"name"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
Running bool `json:"running"` // 运行状态
Config map[string]string `json:"config,omitempty"` // 插件配置
RemotePort int `json:"remote_port,omitempty"`// 远程监听端口
ConfigSchema []ConfigField `json:"config_schema,omitempty"` // 配置模式
}
// Client 客户端数据

View File

@@ -85,24 +85,27 @@ type JSPluginInstallRequest struct {
// StorePluginInfo 扩展商店插件信息
// @Description 插件商店中的插件信息
type StorePluginInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Description string `json:"description"`
Author string `json:"author"`
Icon string `json:"icon,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
SignatureURL string `json:"signature_url,omitempty"`
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Description string `json:"description"`
Author string `json:"author"`
Icon string `json:"icon,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
SignatureURL string `json:"signature_url,omitempty"`
ConfigSchema []ConfigField `json:"config_schema,omitempty"`
}
// StoreInstallRequest 从商店安装插件请求
// @Description 从插件商店安装插件到客户端
type StoreInstallRequest struct {
PluginName string `json:"plugin_name" binding:"required"`
DownloadURL string `json:"download_url" binding:"required,url"`
SignatureURL string `json:"signature_url" binding:"required,url"`
ClientID string `json:"client_id" binding:"required"`
RemotePort int `json:"remote_port"`
PluginName string `json:"plugin_name" binding:"required"`
Version string `json:"version"`
DownloadURL string `json:"download_url" binding:"required,url"`
SignatureURL string `json:"signature_url" binding:"required,url"`
ClientID string `json:"client_id" binding:"required"`
RemotePort int `json:"remote_port"`
ConfigSchema []ConfigField `json:"config_schema,omitempty"`
}
// JSPluginPushRequest 推送 JS 插件到客户端请求

View File

@@ -1,8 +1,10 @@
package handler
import (
"fmt"
"github.com/gin-gonic/gin"
// removed router import
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/internal/server/router/dto"
"github.com/gotunnel/pkg/plugin"
)
@@ -145,40 +147,73 @@ func (h *PluginHandler) GetClientConfig(c *gin.Context) {
return
}
// 尝试从内置插件获取配置模式
schema, err := h.app.GetServer().GetPluginConfigSchema(pluginName)
var schemaFields []dto.ConfigField
if err != nil {
// 如果内置插件中找不到,尝试从 JS 插件获取
jsPlugin, jsErr := h.app.GetJSPluginStore().GetJSPlugin(pluginName)
if jsErr != nil {
// 两者都找不到,返回空 schema
schemaFields = []dto.ConfigField{}
} else {
// 使用 JS 插件的 config 作为动态 schema
for key := range jsPlugin.Config {
schemaFields = append(schemaFields, dto.ConfigField{
Key: key,
Label: key,
Type: "string",
})
}
}
} else {
schemaFields = convertRouterConfigFields(schema)
}
// 查找客户端的插件配置
var config map[string]string
for _, p := range client.Plugins {
// 查找客户端的插件
var clientPlugin *db.ClientPlugin
for i, p := range client.Plugins {
if p.Name == pluginName {
config = p.Config
clientPlugin = &client.Plugins[i]
break
}
}
if clientPlugin == nil {
NotFound(c, "plugin not installed on client")
return
}
var schemaFields []dto.ConfigField
// 优先使用客户端插件保存的 ConfigSchema
if len(clientPlugin.ConfigSchema) > 0 {
for _, f := range clientPlugin.ConfigSchema {
schemaFields = append(schemaFields, dto.ConfigField{
Key: f.Key,
Label: f.Label,
Type: f.Type,
Default: f.Default,
Required: f.Required,
Options: f.Options,
Description: f.Description,
})
}
} else {
// 尝试从内置插件获取配置模式
schema, err := h.app.GetServer().GetPluginConfigSchema(pluginName)
if err != nil {
// 如果内置插件中找不到,尝试从 JS 插件获取
jsPlugin, jsErr := h.app.GetJSPluginStore().GetJSPlugin(pluginName)
if jsErr == nil {
// 使用 JS 插件的 config 作为动态 schema
for key := range jsPlugin.Config {
schemaFields = append(schemaFields, dto.ConfigField{
Key: key,
Label: key,
Type: "string",
})
}
}
} else {
schemaFields = convertRouterConfigFields(schema)
}
}
// 添加 remote_port 作为系统配置字段(始终显示)
schemaFields = append([]dto.ConfigField{{
Key: "remote_port",
Label: "远程端口",
Type: "number",
Description: "服务端监听端口,修改后需重启插件生效",
}}, schemaFields...)
// 构建配置值
config := clientPlugin.Config
if config == nil {
config = make(map[string]string)
}
// 将 remote_port 加入配置
if clientPlugin.RemotePort > 0 {
config["remote_port"] = fmt.Sprintf("%d", clientPlugin.RemotePort)
}
Success(c, dto.PluginConfigResponse{
PluginName: pluginName,
@@ -218,8 +253,19 @@ func (h *PluginHandler) UpdateClientConfig(c *gin.Context) {
// 更新插件配置
found := false
portChanged := false
for i, p := range client.Plugins {
if p.Name == pluginName {
// 提取 remote_port 并单独处理
if portStr, ok := req.Config["remote_port"]; ok {
var newPort int
fmt.Sscanf(portStr, "%d", &newPort)
if newPort > 0 && newPort != client.Plugins[i].RemotePort {
client.Plugins[i].RemotePort = newPort
portChanged = true
}
delete(req.Config, "remote_port") // 不保存到 Config map
}
client.Plugins[i].Config = req.Config
found = true
break
@@ -242,12 +288,12 @@ func (h *PluginHandler) UpdateClientConfig(c *gin.Context) {
if online {
if err := h.app.GetServer().SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil {
// 配置已保存,但同步失败,返回警告
PartialSuccess(c, gin.H{"status": "partial"}, "config saved but sync failed: "+err.Error())
PartialSuccess(c, gin.H{"status": "partial", "port_changed": portChanged}, "config saved but sync failed: "+err.Error())
return
}
}
Success(c, gin.H{"status": "ok"})
Success(c, gin.H{"status": "ok", "port_changed": portChanged})
}
// convertConfigFields 转换插件配置字段到 DTO

View File

@@ -164,10 +164,29 @@ func (h *StoreHandler) Install(c *gin.Context) {
}
}
if !exists {
version := req.Version
if version == "" {
version = "1.0.0"
}
// 转换 ConfigSchema
var configSchema []db.ConfigField
for _, f := range req.ConfigSchema {
configSchema = append(configSchema, db.ConfigField{
Key: f.Key,
Label: f.Label,
Type: f.Type,
Default: f.Default,
Required: f.Required,
Options: f.Options,
Description: f.Description,
})
}
dbClient.Plugins = append(dbClient.Plugins, db.ClientPlugin{
Name: req.PluginName,
Version: "1.0.0",
Enabled: true,
Name: req.PluginName,
Version: version,
Enabled: true,
RemotePort: req.RemotePort,
ConfigSchema: configSchema,
})
}
h.app.GetClientStore().UpdateClient(dbClient)

View File

@@ -1,5 +1,5 @@
import { get, post, put, del, getToken } from '../config/axios'
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo, StorePluginInfo, PluginConfigResponse, JSPlugin, RuleSchemasMap, LogEntry, LogStreamOptions } from '../types'
import type { ClientConfig, ClientStatus, ClientDetail, ServerStatus, PluginInfo, StorePluginInfo, PluginConfigResponse, JSPlugin, RuleSchemasMap, LogEntry, LogStreamOptions, ConfigField } from '../types'
// 重新导出 token 管理方法
export { getToken, setToken, removeToken } from '../config/axios'
@@ -49,8 +49,8 @@ export const disablePlugin = (name: string) => post(`/plugin/${name}/disable`)
// 扩展商店
export const getStorePlugins = () => get<{ plugins: StorePluginInfo[] }>('/store/plugins')
export const installStorePlugin = (pluginName: string, downloadUrl: string, signatureUrl: string, clientId: string, remotePort?: number) =>
post('/store/install', { plugin_name: pluginName, download_url: downloadUrl, signature_url: signatureUrl, client_id: clientId, remote_port: remotePort || 0 })
export const installStorePlugin = (pluginName: string, downloadUrl: string, signatureUrl: string, clientId: string, remotePort?: number, version?: string, configSchema?: ConfigField[]) =>
post('/store/install', { plugin_name: pluginName, version: version || '', download_url: downloadUrl, signature_url: signatureUrl, client_id: clientId, remote_port: remotePort || 0, config_schema: configSchema || [] })
// 客户端插件配置
export const getClientPluginConfig = (clientId: string, pluginName: string) =>

View File

@@ -112,6 +112,7 @@ export interface StorePluginInfo {
icon?: string
download_url?: string
signature_url?: string
config_schema?: ConfigField[]
}
// JS 插件信息

View File

@@ -86,8 +86,7 @@ const configLoading = ref(false)
const showStoreModal = ref(false)
const storePlugins = ref<StorePluginInfo[]>([])
const storeLoading = ref(false)
const selectedStorePlugin = ref<StorePluginInfo | null>(null)
const storeInstalling = ref(false)
const storeInstalling = ref<string | null>(null) // 正在安装的插件名称
// 日志查看相关
const showLogViewer = ref(false)
@@ -96,7 +95,6 @@ const showLogViewer = ref(false)
const openStoreModal = async () => {
showStoreModal.value = true
storeLoading.value = true
selectedStorePlugin.value = null
try {
const { data } = await getStorePlugins()
storePlugins.value = (data.plugins || []).filter(p => p.download_url)
@@ -113,18 +111,17 @@ const handleInstallStorePlugin = async (plugin: StorePluginInfo) => {
message.error('该插件没有下载地址')
return
}
storeInstalling.value = true
selectedStorePlugin.value = plugin
storeInstalling.value = plugin.name
try {
await installStorePlugin(plugin.name, plugin.download_url, plugin.signature_url || '', clientId)
message.success(`已安装 ${plugin.name}`)
await installStorePlugin(plugin.name, plugin.download_url, plugin.signature_url || '', clientId, 8080, plugin.version, plugin.config_schema)
message.success(`已安装 ${plugin.name},可在配置中修改端口和其他设置`)
showStoreModal.value = false
await loadClient()
} catch (e: any) {
message.error(e.response?.data || '安装失败')
} finally {
storeInstalling.value = false
selectedStorePlugin.value = null
storeInstalling.value = null
}
}
@@ -724,7 +721,7 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
<n-button
size="small"
type="primary"
:loading="storeInstalling && selectedStorePlugin?.name === plugin.name"
:loading="storeInstalling === plugin.name"
@click="handleInstallStorePlugin(plugin)"
>
安装

View File

@@ -230,13 +230,11 @@ const toggleJSPlugin = async (plugin: JSPlugin) => {
const showInstallModal = ref(false)
const selectedStorePlugin = ref<StorePluginInfo | null>(null)
const selectedClientId = ref('')
const storePluginRemotePort = ref<number | null>(8080)
const installing = ref(false)
const openInstallModal = (plugin: StorePluginInfo) => {
selectedStorePlugin.value = plugin
selectedClientId.value = ''
storePluginRemotePort.value = 8080
showInstallModal.value = true
}
@@ -260,9 +258,11 @@ const handleInstallStorePlugin = async () => {
selectedStorePlugin.value.download_url,
selectedStorePlugin.value.signature_url,
selectedClientId.value,
storePluginRemotePort.value || 0
8080, // 默认端口,可在配置中修改
selectedStorePlugin.value.version,
selectedStorePlugin.value.config_schema
)
message.success(`已安装 ${selectedStorePlugin.value.name} 到客户端`)
message.success(`已安装 ${selectedStorePlugin.value.name},可在客户端配置中修改端口和其他设置`)
showInstallModal.value = false
} catch (e: any) {
message.error(e.response?.data || '安装失败')
@@ -464,16 +464,7 @@ onMounted(() => {
placeholder="选择要安装到的客户端"
:options="onlineClients.map(c => ({ label: c.nickname || c.id, value: c.id }))"
/>
<div>
<p style="margin: 0 0 8px 0; color: #666; font-size: 13px;">远程端口服务端监听端口:</p>
<n-input-number
v-model:value="storePluginRemotePort"
:min="1"
:max="65535"
placeholder="输入端口号"
style="width: 100%;"
/>
</div>
<p style="margin: 0; color: #999; font-size: 12px;">安装后可在客户端详情页配置端口和其他设置</p>
</n-space>
<template #footer>
<n-space justify="end">