diff --git a/AGENTS.md b/AGENTS.md index 9440e32..29f32b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,8 +14,7 @@ go build -o client ./cmd/client ./server -c server.yaml # with config file # Run client -./client -s :7000 -t -id -./client -s :7000 -t -id -no-tls # disable TLS +./client -s :7000 -t # Web UI development (in web/ directory) cd web && npm install && npm run dev # development server @@ -86,7 +85,7 @@ External User → Server Port → Yamux Stream → Client → Local Service ### Configuration - Server: YAML config + SQLite database for client rules and JS plugins -- Client: Command-line flags only (server address, token, client ID) +- Client: Command-line flags only (server address, token) - Default ports: 7000 (tunnel), 7500 (web console) ## API Documentation diff --git a/CLAUDE.md b/CLAUDE.md index 38a1cb7..be08fbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,8 +14,7 @@ go build -o client ./cmd/client ./server -c server.yaml # with config file # Run client -./client -s :7000 -t -id -./client -s :7000 -t -id -no-tls # disable TLS +./client -s :7000 -t # Web UI development (in web/ directory) cd web && npm install && npm run dev # development server @@ -86,7 +85,7 @@ External User → Server Port → Yamux Stream → Client → Local Service ### Configuration - Server: YAML config + SQLite database for client rules and JS plugins -- Client: Command-line flags only (server address, token, client ID) +- Client: Command-line flags only (server address, token) - Default ports: 7000 (tunnel), 7500 (web console) ## API Documentation diff --git a/README.md b/README.md index 7911b30..4c09463 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ GoTunnel 是一个类似 frp 的内网穿透解决方案,核心特点是**服 | TLS 证书 | 自动生成,零配置 | 需手动配置 | | 管理界面 | 内置 Web 控制台 (naive-ui) | 需额外部署 Dashboard | | 客户端部署 | 仅需 2 个参数 | 需配置文件 | -| 客户端 ID | 可选,服务端自动分配 | 需手动配置 | +| 客户端 ID | 自动根据设备标识计算 | 需手动配置 | ### 架构设计 @@ -111,14 +111,9 @@ go build -o client ./cmd/client ### 客户端启动 ```bash -# 最简启动(ID 由服务端自动分配) +# 最简启动(ID 由客户端根据设备标识自动计算) ./client -s <服务器IP>:7000 -t -# 指定客户端 ID -./client -s <服务器IP>:7000 -t -id <客户端ID> - -# 禁用 TLS(需服务端也禁用) -./client -s <服务器IP>:7000 -t -no-tls ``` **参数说明:** @@ -127,9 +122,6 @@ go build -o client ./cmd/client |------|------|------| | `-s` | 服务器地址 (ip:port) | 是 | | `-t` | 认证 Token | 是 | -| `-id` | 客户端 ID | 否(服务端自动分配) | -| `-no-tls` | 禁用 TLS 加密 | 否 | -| `-skip-verify` | 跳过证书验证(不安全,仅测试用) | 否 | ## 配置系统 @@ -388,7 +380,7 @@ curl -X POST http://server:7500/api/clients \ -d '{"id":"home","rules":[{"name":"web","type":"tcp","local_ip":"127.0.0.1","local_port":80,"remote_port":8080}]}' # 客户端连接 -./client -s server:7000 -t -id home +./client -s server:7000 -t # 访问:http://server:8080 -> 内网 127.0.0.1:80 ``` @@ -411,7 +403,7 @@ A: 在 Web 控制台点击客户端详情,进入编辑模式即可设置昵称 **Q: 如何禁用 TLS?** -A: 服务端配置 `tls_disabled: true`,客户端使用 `-no-tls` 参数。 +A: 客户端命令行默认使用 TLS;如需兼容旧的非 TLS 部署,请改用客户端配置文件中的 `no_tls: true`。 **Q: 端口被占用怎么办?** @@ -419,7 +411,7 @@ A: 服务端会自动检测端口冲突,请检查日志并更换端口。 **Q: 客户端 ID 是如何分配的?** -A: 如果客户端未指定 `-id` 参数,服务端会自动生成 16 位随机 ID。 +A: 客户端会把系统机器 ID、全部可用 MAC、主机名和网卡名等稳定标识组合后再进行哈希,得到固定客户端 ID;服务端不再为客户端分配或修正 ID。 **Q: 如何更新服务端/客户端?** diff --git a/cmd/client/main.go b/cmd/client/main.go index 413611e..7d77a5c 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -23,8 +23,6 @@ func init() { func main() { server := flag.String("s", "", "server address (ip:port)") token := flag.String("t", "", "auth token") - id := flag.String("id", "", "client id (optional, auto-assigned if empty)") - noTLS := flag.Bool("no-tls", false, "disable TLS") configPath := flag.String("c", "", "config file path") flag.Parse() @@ -47,18 +45,12 @@ func main() { if *token != "" { cfg.Token = *token } - if *id != "" { - cfg.ID = *id - } - if *noTLS { - cfg.NoTLS = *noTLS - } if cfg.Server == "" || cfg.Token == "" { - log.Fatal("Usage: client [-c config.yaml] | [-s -t [-id ] [-no-tls]]") + log.Fatal("Usage: client [-c config.yaml] | [-s -t ]") } - client := tunnel.NewClient(cfg.Server, cfg.Token, cfg.ID) + client := tunnel.NewClient(cfg.Server, cfg.Token) // TLS 默认启用,默认跳过证书验证(类似 frp) if !cfg.NoTLS { diff --git a/internal/client/config/config.go b/internal/client/config/config.go index 543991d..157f816 100644 --- a/internal/client/config/config.go +++ b/internal/client/config/config.go @@ -10,7 +10,6 @@ import ( type ClientConfig struct { Server string `yaml:"server"` // 服务器地址 Token string `yaml:"token"` // 认证 Token - ID string `yaml:"id"` // 客户端 ID NoTLS bool `yaml:"no_tls"` // 禁用 TLS } diff --git a/internal/client/tunnel/client.go b/internal/client/tunnel/client.go index 0ca9c89..ceffcb6 100644 --- a/internal/client/tunnel/client.go +++ b/internal/client/tunnel/client.go @@ -48,7 +48,7 @@ type Client struct { } // NewClient 创建客户端 -func NewClient(serverAddr, token, id string) *Client { +func NewClient(serverAddr, token string) *Client { // 默认数据目录:优先使用用户主目录,失败时回退到当前工作目录 var dataDir string if home, err := os.UserHomeDir(); err == nil && home != "" { @@ -71,9 +71,7 @@ func NewClient(serverAddr, token, id string) *Client { } // ID 优先级:命令行参数 > 机器ID - if id == "" { - id = getMachineID() - } + id := getMachineID() // 获取主机名作为客户端名称 hostname, _ := os.Hostname() @@ -99,7 +97,6 @@ func (c *Client) InitVersionStore() error { return nil } - // logf 安全地记录日志(同时输出到标准日志和日志收集器) func (c *Client) logf(format string, args ...interface{}) { msg := fmt.Sprintf(format, args...) @@ -190,8 +187,8 @@ func (c *Client) connect() error { // 如果服务端分配了新 ID,则更新 if authResp.ClientID != "" && authResp.ClientID != c.ID { - c.ID = authResp.ClientID - c.logf("ID updated to: %s", c.ID) + conn.Close() + return fmt.Errorf("server returned unexpected client id: %s", authResp.ClientID) } c.logf("Authenticated as %s", c.ID) @@ -416,15 +413,6 @@ func (c *Client) findRuleByPort(port int) *protocol.ProxyRule { return nil } - - - - - - - - - // handleClientRestart 处理客户端重启请求 func (c *Client) handleClientRestart(stream net.Conn, msg *protocol.Message) { defer stream.Close() @@ -449,8 +437,6 @@ func (c *Client) handleClientRestart(stream net.Conn, msg *protocol.Message) { } } - - // handleUpdateDownload 处理更新下载请求 func (c *Client) handleUpdateDownload(stream net.Conn, msg *protocol.Message) { defer stream.Close() @@ -528,7 +514,7 @@ func (c *Client) performSelfUpdate(downloadURL string) error { // Windows 需要特殊处理 if runtime.GOOS == "windows" { - return performWindowsClientUpdate(binaryPath, currentPath, c.ServerAddr, c.Token, c.ID) + return performWindowsClientUpdate(binaryPath, currentPath, c.ServerAddr, c.Token) } // 确定目标路径 @@ -579,7 +565,7 @@ func (c *Client) performSelfUpdate(downloadURL string) error { c.logf("Update completed successfully, restarting...") // 重启进程(从新路径启动) - restartClientProcess(targetPath, c.ServerAddr, c.Token, c.ID) + restartClientProcess(targetPath, c.ServerAddr, c.Token) return nil } @@ -608,15 +594,10 @@ func (c *Client) checkUpdatePermissions(execPath string) error { return nil } - // performWindowsClientUpdate Windows 平台更新 -func performWindowsClientUpdate(newFile, currentPath, serverAddr, token, id string) error { +func performWindowsClientUpdate(newFile, currentPath, serverAddr, token string) error { // 创建批处理脚本 args := fmt.Sprintf(`-s "%s" -t "%s"`, serverAddr, token) - if id != "" { - args += fmt.Sprintf(` -id "%s"`, id) - } - batchScript := fmt.Sprintf(`@echo off :: Check for admin rights, request UAC elevation if needed net session >nul 2>&1 @@ -647,11 +628,8 @@ del "%%~f0" } // restartClientProcess 重启客户端进程 -func restartClientProcess(path, serverAddr, token, id string) { +func restartClientProcess(path, serverAddr, token string) { args := []string{"-s", serverAddr, "-t", token} - if id != "" { - args = append(args, "-id", id) - } cmd := exec.Command(path, args...) cmd.Stdout = os.Stdout @@ -660,7 +638,6 @@ func restartClientProcess(path, serverAddr, token, id string) { os.Exit(0) } - // handleLogRequest 处理日志请求 func (c *Client) handleLogRequest(stream net.Conn, msg *protocol.Message) { if c.logger == nil { @@ -737,8 +714,6 @@ func (c *Client) handleLogStop(stream net.Conn, msg *protocol.Message) { c.logger.Unsubscribe(req.SessionID) } - - // handleSystemStatsRequest 处理系统状态请求 func (c *Client) handleSystemStatsRequest(stream net.Conn, msg *protocol.Message) { defer stream.Close() diff --git a/internal/client/tunnel/machine_id.go b/internal/client/tunnel/machine_id.go index 9beb5a8..d25bcb1 100644 --- a/internal/client/tunnel/machine_id.go +++ b/internal/client/tunnel/machine_id.go @@ -7,27 +7,38 @@ import ( "os" "os/exec" "runtime" + "sort" "strings" ) -// getMachineID 获取机器唯一标识 -// 优先级:系统机器ID > MAC地址哈希 +// getMachineID builds a stable fingerprint from multiple host identifiers +// and hashes the combined result into the client ID we expose externally. func getMachineID() string { - // 尝试获取系统机器 ID - if id := getSystemMachineID(); id != "" { - return hashID(id) - } - - // 备选:使用主网卡 MAC 地址 - if id := getMACAddress(); id != "" { - return hashID(id) - } - - // 都失败则返回空,让服务端生成 - return "" + return hashID(strings.Join(collectMachineIDParts(), "|")) +} + +func collectMachineIDParts() []string { + parts := []string{"os=" + runtime.GOOS, "arch=" + runtime.GOARCH} + + if id := getSystemMachineID(); id != "" { + parts = append(parts, "system="+id) + } + + if hostname, err := os.Hostname(); err == nil && hostname != "" { + parts = append(parts, "host="+hostname) + } + + if macs := getMACAddresses(); len(macs) > 0 { + parts = append(parts, "macs="+strings.Join(macs, ",")) + } + + if names := getInterfaceNames(); len(names) > 0 { + parts = append(parts, "ifaces="+strings.Join(names, ",")) + } + + return parts } -// getSystemMachineID 获取系统机器 ID func getSystemMachineID() string { switch runtime.GOOS { case "linux": @@ -41,20 +52,16 @@ func getSystemMachineID() string { } } -// getLinuxMachineID 获取 Linux 机器 ID func getLinuxMachineID() string { - // 优先读取 /etc/machine-id if data, err := os.ReadFile("/etc/machine-id"); err == nil { return strings.TrimSpace(string(data)) } - // 备选 /var/lib/dbus/machine-id if data, err := os.ReadFile("/var/lib/dbus/machine-id"); err == nil { return strings.TrimSpace(string(data)) } return "" } -// getDarwinMachineID 获取 macOS 机器 ID (IOPlatformUUID) func getDarwinMachineID() string { cmd := exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice") output, err := cmd.Output() @@ -62,72 +69,84 @@ func getDarwinMachineID() string { return "" } - // 解析 IOPlatformUUID - lines := strings.Split(string(output), "\n") - for _, line := range lines { - if strings.Contains(line, "IOPlatformUUID") { - parts := strings.Split(line, "=") - if len(parts) == 2 { - uuid := strings.TrimSpace(parts[1]) - uuid = strings.Trim(uuid, "\"") - return uuid - } + for _, line := range strings.Split(string(output), "\n") { + if !strings.Contains(line, "IOPlatformUUID") { + continue } + + parts := strings.Split(line, "=") + if len(parts) != 2 { + continue + } + + uuid := strings.TrimSpace(parts[1]) + return strings.Trim(uuid, "\"") } + return "" } -// getWindowsMachineID 获取 Windows 机器 ID func getWindowsMachineID() string { - cmd := exec.Command("reg", "query", - `HKLM\SOFTWARE\Microsoft\Cryptography`, - "/v", "MachineGuid") + cmd := exec.Command("reg", "query", `HKLM\SOFTWARE\Microsoft\Cryptography`, "/v", "MachineGuid") output, err := cmd.Output() if err != nil { return "" } - // 解析注册表输出 - lines := strings.Split(string(output), "\n") - for _, line := range lines { - if strings.Contains(line, "MachineGuid") { - fields := strings.Fields(line) - if len(fields) >= 3 { - return fields[len(fields)-1] - } + for _, line := range strings.Split(string(output), "\n") { + if !strings.Contains(line, "MachineGuid") { + continue + } + + fields := strings.Fields(line) + if len(fields) >= 3 { + return fields[len(fields)-1] } } + return "" } -// getMACAddress 获取主网卡 MAC 地址 -func getMACAddress() string { +func getMACAddresses() []string { interfaces, err := net.Interfaces() if err != nil { - return "" + return nil } + macs := make([]string, 0, len(interfaces)) for _, iface := range interfaces { - // 跳过回环和无效接口 if iface.Flags&net.FlagLoopback != 0 { continue } - if iface.Flags&net.FlagUp == 0 { - continue - } if len(iface.HardwareAddr) == 0 { continue } - - // 返回第一个有效的 MAC 地址 - return iface.HardwareAddr.String() + macs = append(macs, iface.HardwareAddr.String()) } - return "" + + sort.Strings(macs) + return macs +} + +func getInterfaceNames() []string { + interfaces, err := net.Interfaces() + if err != nil { + return nil + } + + names := make([]string, 0, len(interfaces)) + for _, iface := range interfaces { + if iface.Flags&net.FlagLoopback != 0 { + continue + } + names = append(names, iface.Name) + } + + sort.Strings(names) + return names } -// hashID 对 ID 进行哈希处理,生成固定长度的客户端 ID func hashID(id string) string { hash := sha256.Sum256([]byte(id)) - // 取前 16 个字符作为客户端 ID return hex.EncodeToString(hash[:])[:16] } diff --git a/internal/server/db/install_token.go b/internal/server/db/install_token.go index 0748ce5..f6a1462 100644 --- a/internal/server/db/install_token.go +++ b/internal/server/db/install_token.go @@ -5,8 +5,8 @@ func (s *SQLiteStore) CreateInstallToken(token *InstallToken) error { s.mu.Lock() defer s.mu.Unlock() - _, err := s.db.Exec(`INSERT INTO install_tokens (token, client_id, created_at, used) VALUES (?, ?, ?, ?)`, - token.Token, token.ClientID, token.CreatedAt, 0) + _, err := s.db.Exec(`INSERT INTO install_tokens (token, client_id, created_at, used) VALUES (?, '', ?, ?)`, + token.Token, token.CreatedAt, 0) return err } @@ -17,8 +17,8 @@ func (s *SQLiteStore) GetInstallToken(token string) (*InstallToken, error) { var t InstallToken var used int - err := s.db.QueryRow(`SELECT token, client_id, created_at, used FROM install_tokens WHERE token = ?`, token). - Scan(&t.Token, &t.ClientID, &t.CreatedAt, &used) + err := s.db.QueryRow(`SELECT token, created_at, used FROM install_tokens WHERE token = ?`, token). + Scan(&t.Token, &t.CreatedAt, &used) if err != nil { return nil, err } diff --git a/internal/server/db/interface.go b/internal/server/db/interface.go index 6ad72ae..ce14918 100644 --- a/internal/server/db/interface.go +++ b/internal/server/db/interface.go @@ -46,7 +46,6 @@ type TrafficStore interface { // InstallToken 安装token type InstallToken struct { Token string `json:"token"` - ClientID string `json:"client_id"` CreatedAt int64 `json:"created_at"` Used bool `json:"used"` } diff --git a/internal/server/db/sqlite.go b/internal/server/db/sqlite.go index 183670d..63698e9 100644 --- a/internal/server/db/sqlite.go +++ b/internal/server/db/sqlite.go @@ -81,7 +81,7 @@ func (s *SQLiteStore) init() error { _, err = s.db.Exec(` CREATE TABLE IF NOT EXISTS install_tokens ( token TEXT PRIMARY KEY, - client_id TEXT NOT NULL, + client_id TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL, used INTEGER NOT NULL DEFAULT 0 ) diff --git a/internal/server/router/handler/install.go b/internal/server/router/handler/install.go index bbb9b30..9c8fee4 100644 --- a/internal/server/router/handler/install.go +++ b/internal/server/router/handler/install.go @@ -21,34 +21,21 @@ func NewInstallHandler(app AppInterface) *InstallHandler { return &InstallHandler{app: app} } -// GenerateInstallCommandRequest 生成安装命令请求 -type GenerateInstallCommandRequest struct { - ClientID string `json:"client_id" binding:"required"` -} - // 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"` + Commands map[string]string `json:"commands"` + ExpiresAt int64 `json:"expires_at"` + ServerAddr string `json:"server_addr"` } // GenerateInstallCommand 生成安装命令 // @Summary 生成客户端安装命令 // @Tags install -// @Accept json // @Produce json -// @Param body body GenerateInstallCommandRequest true "客户端ID" // @Success 200 {object} InstallCommandResponse // @Router /api/install/generate [post] func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) { - var req GenerateInstallCommandRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - // 生成随机token tokenBytes := make([]byte, 32) if _, err := rand.Read(tokenBytes); err != nil { @@ -61,7 +48,6 @@ func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) { now := time.Now().Unix() installToken := &db.InstallToken{ Token: token, - ClientID: req.ClientID, CreatedAt: now, Used: false, } @@ -85,18 +71,13 @@ func (h *InstallHandler) GenerateInstallCommand(c *gin.Context) { // 生成安装命令 expiresAt := now + 3600 // 1小时过期 - tlsFlag := "" - if h.app.GetConfig().Server.TLSDisabled { - tlsFlag = " -no-tls" - } - 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 -id %s%s", - serverAddr, token, req.ClientID, tlsFlag), - "macos": fmt.Sprintf("curl -fsSL https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.sh | bash -s -- -s %s -t %s -id %s%s", - serverAddr, token, req.ClientID, tlsFlag), - "windows": fmt.Sprintf("powershell -c \"irm https://raw.githubusercontent.com/gotunnel/gotunnel/main/scripts/install.ps1 | iex; Install-GoTunnel -Server '%s' -Token '%s' -ClientID '%s'%s\"", - serverAddr, token, req.ClientID, tlsFlag), + "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{ diff --git a/internal/server/tunnel/server.go b/internal/server/tunnel/server.go index 54b20a4..6c79661 100644 --- a/internal/server/tunnel/server.go +++ b/internal/server/tunnel/server.go @@ -1,10 +1,8 @@ package tunnel import ( - "crypto/rand" "crypto/tls" "encoding/base64" - "encoding/hex" "fmt" "log" "net" @@ -38,13 +36,6 @@ func isValidClientID(id string) bool { return clientIDRegex.MatchString(id) } -// generateClientID 生成随机客户端 ID -func generateClientID() string { - bytes := make([]byte, 8) - rand.Read(bytes) - return hex.EncodeToString(bytes) -} - // Server 隧道服务端 type Server struct { clientStore db.ClientStore @@ -239,13 +230,7 @@ func (s *Server) handleConnection(conn net.Conn) { validToken = true isInstallToken = true // 验证客户端ID匹配 - if authReq.ClientID != "" && authReq.ClientID != installToken.ClientID { - security.LogInvalidClientID(clientIP, authReq.ClientID) - s.sendAuthResponse(conn, false, "client id mismatch", "") - return - } // 使用token中的客户端ID - authReq.ClientID = installToken.ClientID } } } @@ -259,9 +244,7 @@ func (s *Server) handleConnection(conn net.Conn) { // 处理客户端 ID clientID := authReq.ClientID - if clientID == "" { - clientID = generateClientID() - } else if !isValidClientID(clientID) { + if clientID == "" || !isValidClientID(clientID) { security.LogInvalidClientID(clientIP, clientID) s.sendAuthResponse(conn, false, "invalid client id format", "") return @@ -757,11 +740,6 @@ func (s *Server) DisconnectClient(clientID string) error { return cs.Session.Close() } - - - - - // startUDPListener 启动 UDP 监听 func (s *Server) startUDPListener(cs *ClientSession, rule *protocol.ProxyRule) { if err := s.portManager.Reserve(rule.RemotePort, cs.ID); err != nil { @@ -856,15 +834,6 @@ func (s *Server) sendUDPPacket(cs *ClientSession, conn *net.UDPConn, clientAddr } } - - - - - - - - - // checkHTTPBasicAuth 检查 HTTP Basic Auth // 返回 (认证成功, 已读取的数据) func (s *Server) checkHTTPBasicAuth(conn net.Conn, username, password string) (bool, []byte) { @@ -933,8 +902,6 @@ func (s *Server) sendHTTPUnauthorized(conn net.Conn) { conn.Write([]byte(response)) } - - // shouldPushToClient 检查是否应推送到指定客户端 func (s *Server) shouldPushToClient(autoPush []string, clientID string) bool { if len(autoPush) == 0 { @@ -980,11 +947,6 @@ func (s *Server) RestartClient(clientID string) error { return nil } - - - - - // IsPortAvailable 检查端口是否可用 func (s *Server) IsPortAvailable(port int, excludeClientID string) bool { // 检查系统端口 @@ -1008,11 +970,6 @@ func (s *Server) IsPortAvailable(port int, excludeClientID string) bool { return true } - - - - - // SendUpdateToClient 发送更新命令到客户端 func (s *Server) SendUpdateToClient(clientID, downloadURL string) error { s.mu.RLock() diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 989c7df..3b32cd2 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -182,5 +182,5 @@ export const getServerConfig = () => get('/config') export const updateServerConfig = (config: UpdateServerConfigRequest) => put('/config', config) // 安装命令生成 -export const generateInstallCommand = (clientId: string) => - post('/install/generate', { client_id: clientId }) +export const generateInstallCommand = () => + post('/install/generate') diff --git a/web/src/views/ClientsView.vue b/web/src/views/ClientsView.vue index c629d94..f472170 100644 --- a/web/src/views/ClientsView.vue +++ b/web/src/views/ClientsView.vue @@ -9,7 +9,6 @@ const clients = ref([]) const loading = ref(true) const showInstallModal = ref(false) const installData = ref(null) -const installClientId = ref('') const generatingInstall = ref(false) const loadClients = async () => { @@ -31,10 +30,9 @@ const viewClient = (id: string) => { } const openInstallModal = async () => { - installClientId.value = `client-${Date.now()}` generatingInstall.value = true try { - const { data } = await generateInstallCommand(installClientId.value) + const { data } = await generateInstallCommand() installData.value = data showInstallModal.value = true } catch (e) { @@ -164,7 +162,7 @@ onMounted(loadClients) -

客户端ID: {{ installClientId }}

+

客户端 ID 会在目标机器上根据多种设备标识自动计算。

⚠️ 此命令包含一次性token,使用后需重新生成