diff --git a/internal/server/db/interface.go b/internal/server/db/interface.go index 49bf303..e510586 100644 --- a/internal/server/db/interface.go +++ b/internal/server/db/interface.go @@ -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 客户端数据 diff --git a/internal/server/router/dto/plugin.go b/internal/server/router/dto/plugin.go index 43e3886..a974b0e 100644 --- a/internal/server/router/dto/plugin.go +++ b/internal/server/router/dto/plugin.go @@ -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 插件到客户端请求 diff --git a/internal/server/router/handler/plugin.go b/internal/server/router/handler/plugin.go index a191606..cf9f7a7 100644 --- a/internal/server/router/handler/plugin.go +++ b/internal/server/router/handler/plugin.go @@ -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 diff --git a/internal/server/router/handler/store.go b/internal/server/router/handler/store.go index 2c996cd..991dc28 100644 --- a/internal/server/router/handler/store.go +++ b/internal/server/router/handler/store.go @@ -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) diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 5f6299c..1e670f1 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -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) => diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 1438a05..737ba50 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -112,6 +112,7 @@ export interface StorePluginInfo { icon?: string download_url?: string signature_url?: string + config_schema?: ConfigField[] } // JS 插件信息 diff --git a/web/src/views/ClientView.vue b/web/src/views/ClientView.vue index c7f70ab..3304fbe 100644 --- a/web/src/views/ClientView.vue +++ b/web/src/views/ClientView.vue @@ -86,8 +86,7 @@ const configLoading = ref(false) const showStoreModal = ref(false) const storePlugins = ref([]) const storeLoading = ref(false) -const selectedStorePlugin = ref(null) -const storeInstalling = ref(false) +const storeInstalling = ref(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) => { 安装 diff --git a/web/src/views/PluginsView.vue b/web/src/views/PluginsView.vue index e20f7e5..2797920 100644 --- a/web/src/views/PluginsView.vue +++ b/web/src/views/PluginsView.vue @@ -230,13 +230,11 @@ const toggleJSPlugin = async (plugin: JSPlugin) => { const showInstallModal = ref(false) const selectedStorePlugin = ref(null) const selectedClientId = ref('') -const storePluginRemotePort = ref(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 }))" /> -
-

远程端口(服务端监听端口):

- -
+

安装后可在客户端详情页配置端口和其他设置