From 6558d1acdb3cc2b2d2fdc31029868a9f94ae2114 Mon Sep 17 00:00:00 2001 From: Flik Date: Thu, 19 Mar 2026 20:49:23 +0800 Subject: [PATCH] Refactor install command generation and update response structure --- internal/server/router/handler/install.go | 50 +++++++---------------- web/src/App.vue | 48 +++++++++++++++++++++- web/src/types/index.ts | 7 +--- web/src/views/ClientsView.vue | 30 ++++++++++++-- 4 files changed, 87 insertions(+), 48 deletions(-) diff --git a/internal/server/router/handler/install.go b/internal/server/router/handler/install.go index 9c8fee4..57a272e 100644 --- a/internal/server/router/handler/install.go +++ b/internal/server/router/handler/install.go @@ -3,7 +3,6 @@ package handler import ( "crypto/rand" "encoding/hex" - "fmt" "net/http" "time" @@ -11,41 +10,38 @@ import ( "github.com/gotunnel/internal/server/db" ) -// InstallHandler 安装处理器 type InstallHandler struct { app AppInterface } -// NewInstallHandler 创建安装处理器 func NewInstallHandler(app AppInterface) *InstallHandler { return &InstallHandler{app: app} } -// InstallCommandResponse 安装命令响应 type InstallCommandResponse struct { - Token string `json:"token"` - Commands map[string]string `json:"commands"` - ExpiresAt int64 `json:"expires_at"` - ServerAddr string `json:"server_addr"` + Token string `json:"token"` + ExpiresAt int64 `json:"expires_at"` + TunnelPort int `json:"tunnel_port"` } -// GenerateInstallCommand 生成安装命令 -// @Summary 生成客户端安装命令 +// GenerateInstallCommand creates a one-time install token and returns +// the tunnel port so the frontend can build a host-aware command. +// +// @Summary Generate install command payload // @Tags install // @Produce json // @Success 200 {object} InstallCommandResponse // @Router /api/install/generate [post] func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) { - // 生成随机token tokenBytes := make([]byte, 32) if _, err := rand.Read(tokenBytes); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "生成token失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"}) return } - token := hex.EncodeToString(tokenBytes) - // 保存到数据库 + token := hex.EncodeToString(tokenBytes) now := time.Now().Unix() + installToken := &db.InstallToken{ Token: token, CreatedAt: now, @@ -54,36 +50,18 @@ func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) { store, ok := h.app.GetClientStore().(db.InstallTokenStore) if !ok { - c.JSON(http.StatusInternalServerError, gin.H{"error": "存储不支持安装token"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "install token store is not supported"}) return } if err := store.CreateInstallToken(installToken); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "保存token失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to persist token"}) return } - // 获取服务器地址 - serverAddr := fmt.Sprintf("%s:%d", h.app.GetConfig().Server.BindAddr, h.app.GetServer().GetBindPort()) - if h.app.GetConfig().Server.BindAddr == "" || h.app.GetConfig().Server.BindAddr == "0.0.0.0" { - serverAddr = fmt.Sprintf("your-server-ip:%d", h.app.GetServer().GetBindPort()) - } - - // 生成安装命令 - expiresAt := now + 3600 // 1小时过期 - commands := map[string]string{ - "linux": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s", - serverAddr, token), - "macos": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s", - serverAddr, token), - "windows": fmt.Sprintf("powershell -c \"irm https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.ps1 | iex; Install-GoTunnel -Server '%s' -Token '%s'\"", - serverAddr, token), - } - c.JSON(http.StatusOK, InstallCommandResponse{ Token: token, - Commands: commands, - ExpiresAt: expiresAt, - ServerAddr: serverAddr, + ExpiresAt: now + 3600, + TunnelPort: h.app.GetServer().GetBindPort(), }) } diff --git a/web/src/App.vue b/web/src/App.vue index 5bef7ef..cafe305 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -189,7 +189,7 @@ onUnmounted(() => {
-
+
Workspace

{{ navItems.find((item) => item.key === activeNav)?.label || '控制台' }}

@@ -375,6 +375,34 @@ onUnmounted(() => { align-items: center; justify-content: space-between; gap: 20px; + position: relative; + overflow: visible; + z-index: 30; + padding: 18px 22px; + border-radius: 22px; + background: + radial-gradient(circle at top right, var(--color-accent-glow), transparent 38%), + linear-gradient(135deg, var(--glass-bg) 0%, var(--glass-bg-light) 100%); + border-color: rgba(255, 255, 255, 0.1); +} + +.app-topbar::after { + content: ''; + position: absolute; + inset: auto 18px -18px auto; + width: 120px; + height: 120px; + border-radius: 999px; + background: var(--color-accent-glow); + opacity: 0.18; + filter: blur(28px); + pointer-events: none; +} + +.topbar-intro { + position: relative; + z-index: 1; + min-width: 0; } .app-topbar h1 { @@ -387,6 +415,9 @@ onUnmounted(() => { display: flex; align-items: center; gap: 10px; + position: relative; + z-index: 2; + flex-shrink: 0; } .topbar-icon-btn, @@ -401,6 +432,15 @@ onUnmounted(() => { background: var(--glass-bg-light); color: var(--color-text-primary); cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease; +} + +.topbar-icon-btn:hover, +.profile-button:hover { + transform: translateY(-1px); + border-color: rgba(59, 130, 246, 0.28); + background: rgba(255, 255, 255, 0.08); + box-shadow: var(--shadow-sm); } .topbar-icon-btn svg, @@ -412,6 +452,7 @@ onUnmounted(() => { .menu-wrap { position: relative; + z-index: 4; } .floating-menu { @@ -424,7 +465,9 @@ onUnmounted(() => { background: var(--glass-bg); border: 1px solid var(--color-border); box-shadow: var(--shadow-lg); - z-index: 10; + backdrop-filter: var(--glass-blur-light); + -webkit-backdrop-filter: var(--glass-blur-light); + z-index: 50; } .floating-menu--right { @@ -514,6 +557,7 @@ onUnmounted(() => { .app-topbar { flex-direction: column; align-items: flex-start; + overflow: visible; } .topbar-actions { diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 750dfde..30ebeea 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -67,11 +67,6 @@ export interface LogStreamOptions { // 安装命令响应 export interface InstallCommandResponse { token: string - commands: { - linux: string - macos: string - windows: string - } expires_at: number - server_addr: string + tunnel_port: number } diff --git a/web/src/views/ClientsView.vue b/web/src/views/ClientsView.vue index 1449e13..c12018b 100644 --- a/web/src/views/ClientsView.vue +++ b/web/src/views/ClientsView.vue @@ -17,6 +17,27 @@ const showInstallModal = ref(false) const installData = ref(null) const generatingInstall = ref(false) const search = ref('') +const installScriptUrl = 'https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh' +const installPs1Url = 'https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.ps1' + +const quoteShellArg = (value: string) => `'${value.replace(/'/g, `'\"'\"'`)}'` + +const resolveTunnelHost = () => window.location.hostname || 'localhost' + +const formatServerAddr = (host: string, port: number) => { + const normalizedHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host + return `${normalizedHost}:${port}` +} + +const buildInstallCommands = (data: InstallCommandResponse) => { + const serverAddr = formatServerAddr(resolveTunnelHost(), data.tunnel_port) + + return { + linux: `bash <(curl -fsSL ${installScriptUrl}) -s ${quoteShellArg(serverAddr)} -t ${quoteShellArg(data.token)}`, + macos: `bash <(curl -fsSL ${installScriptUrl}) -s ${quoteShellArg(serverAddr)} -t ${quoteShellArg(data.token)}`, + windows: `powershell -c \"irm ${installPs1Url} | iex; Install-GoTunnel -Server '${serverAddr}' -Token '${data.token}'\"`, + } +} const loadClients = async () => { loading.value = true @@ -67,6 +88,7 @@ const filteredClients = computed(() => { const onlineClients = computed(() => clients.value.filter((client) => client.online).length) const offlineClients = computed(() => Math.max(clients.value.length - onlineClients.value, 0)) +const installCommands = computed(() => (installData.value ? buildInstallCommands(installData.value) : null)) onMounted(loadClients) @@ -126,11 +148,11 @@ onMounted(loadClients) -
+
{{ item.label }}