1 Commits

Author SHA1 Message Date
Flik
0a41e10793 feat(client, server): add client name handling and machine ID retrieval
Some checks failed
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Has been cancelled
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Has been cancelled
Build Multi-Platform Binaries / build-frontend (push) Has been cancelled
2026-01-27 08:56:05 +08:00
13 changed files with 196 additions and 55 deletions

2
go.mod
View File

@@ -59,7 +59,7 @@ require (
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.58.0 // indirect github.com/quic-go/quic-go v0.58.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.7 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect

8
go.sum
View File

@@ -122,10 +122,10 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

View File

@@ -32,7 +32,6 @@ const (
reconnectDelay = 5 * time.Second reconnectDelay = 5 * time.Second
disconnectDelay = 3 * time.Second disconnectDelay = 3 * time.Second
udpBufferSize = 65535 udpBufferSize = 65535
idFileName = "id"
) )
// Client 隧道客户端 // Client 隧道客户端
@@ -40,6 +39,7 @@ type Client struct {
ServerAddr string ServerAddr string
Token string Token string
ID string ID string
Name string // 客户端名称(主机名)
TLSEnabled bool TLSEnabled bool
TLSConfig *tls.Config TLSConfig *tls.Config
DataDir string // 数据目录 DataDir string // 数据目录
@@ -64,10 +64,14 @@ func NewClient(serverAddr, token, id string) *Client {
log.Printf("Failed to create data dir: %v", err) log.Printf("Failed to create data dir: %v", err)
} }
// ID 优先级:命令行参数 > 机器ID
if id == "" { if id == "" {
id = loadClientID(dataDir) id = getMachineID()
} }
// 获取主机名作为客户端名称
hostname, _ := os.Hostname()
// 初始化日志收集器 // 初始化日志收集器
logger, err := NewLogger(dataDir) logger, err := NewLogger(dataDir)
if err != nil { if err != nil {
@@ -78,6 +82,7 @@ func NewClient(serverAddr, token, id string) *Client {
ServerAddr: serverAddr, ServerAddr: serverAddr,
Token: token, Token: token,
ID: id, ID: id,
Name: hostname,
DataDir: dataDir, DataDir: dataDir,
runningPlugins: make(map[string]plugin.ClientPlugin), runningPlugins: make(map[string]plugin.ClientPlugin),
logger: logger, logger: logger,
@@ -94,27 +99,6 @@ func (c *Client) InitVersionStore() error {
return nil return nil
} }
// getIDFilePath 获取 ID 文件路径
func getIDFilePath(dataDir string) string {
return filepath.Join(dataDir, idFileName)
}
// loadClientID 从本地文件加载客户端 ID
func loadClientID(dataDir string) string {
data, err := os.ReadFile(getIDFilePath(dataDir))
if err != nil {
return ""
}
return string(data)
}
// saveClientID 保存客户端 ID 到本地文件
func saveClientID(dataDir, id string) {
if err := os.WriteFile(getIDFilePath(dataDir), []byte(id), 0600); err != nil {
log.Printf("Failed to save client ID: %v", err)
}
}
// SetPluginRegistry 设置插件注册表 // SetPluginRegistry 设置插件注册表
func (c *Client) SetPluginRegistry(registry *plugin.Registry) { func (c *Client) SetPluginRegistry(registry *plugin.Registry) {
c.pluginRegistry = registry c.pluginRegistry = registry
@@ -181,6 +165,7 @@ func (c *Client) connect() error {
authReq := protocol.AuthRequest{ authReq := protocol.AuthRequest{
ClientID: c.ID, ClientID: c.ID,
Token: c.Token, Token: c.Token,
Name: c.Name,
OS: runtime.GOOS, OS: runtime.GOOS,
Arch: runtime.GOARCH, Arch: runtime.GOARCH,
Version: version.Version, Version: version.Version,
@@ -207,11 +192,10 @@ func (c *Client) connect() error {
return fmt.Errorf("auth failed: %s", authResp.Message) return fmt.Errorf("auth failed: %s", authResp.Message)
} }
// 如果服务端分配了新 ID则更新并保存 // 如果服务端分配了新 ID则更新
if authResp.ClientID != "" && authResp.ClientID != c.ID { if authResp.ClientID != "" && authResp.ClientID != c.ID {
c.ID = authResp.ClientID c.ID = authResp.ClientID
saveClientID(c.DataDir, c.ID) c.logf("ID updated to: %s", c.ID)
c.logf("New ID assigned and saved: %s", c.ID)
} }
c.logf("Authenticated as %s", c.ID) c.logf("Authenticated as %s", c.ID)

View File

@@ -0,0 +1,133 @@
package tunnel
import (
"crypto/sha256"
"encoding/hex"
"net"
"os"
"os/exec"
"runtime"
"strings"
)
// getMachineID 获取机器唯一标识
// 优先级系统机器ID > MAC地址哈希
func getMachineID() string {
// 尝试获取系统机器 ID
if id := getSystemMachineID(); id != "" {
return hashID(id)
}
// 备选:使用主网卡 MAC 地址
if id := getMACAddress(); id != "" {
return hashID(id)
}
// 都失败则返回空,让服务端生成
return ""
}
// getSystemMachineID 获取系统机器 ID
func getSystemMachineID() string {
switch runtime.GOOS {
case "linux":
return getLinuxMachineID()
case "darwin":
return getDarwinMachineID()
case "windows":
return getWindowsMachineID()
default:
return ""
}
}
// 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()
if err != nil {
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
}
}
}
return ""
}
// getWindowsMachineID 获取 Windows 机器 ID
func getWindowsMachineID() string {
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]
}
}
}
return ""
}
// getMACAddress 获取主网卡 MAC 地址
func getMACAddress() string {
interfaces, err := net.Interfaces()
if err != nil {
return ""
}
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()
}
return ""
}
// hashID 对 ID 进行哈希处理,生成固定长度的客户端 ID
func hashID(id string) string {
hash := sha256.Sum256([]byte(id))
// 取前 16 个字符作为客户端 ID
return hex.EncodeToString(hash[:])[:16]
}

View File

@@ -115,7 +115,7 @@ func (h *ClientHandler) Get(c *gin.Context) {
return return
} }
online, lastPing, remoteAddr, clientOS, clientArch, clientVersion := h.app.GetServer().GetClientStatus(clientID) online, lastPing, remoteAddr, clientName, clientOS, clientArch, clientVersion := h.app.GetServer().GetClientStatus(clientID)
// 复制插件列表 // 复制插件列表
plugins := make([]db.ClientPlugin, len(client.Plugins)) plugins := make([]db.ClientPlugin, len(client.Plugins))
@@ -145,9 +145,15 @@ func (h *ClientHandler) Get(c *gin.Context) {
} }
} }
// 如果客户端在线且有名称,优先使用在线名称
nickname := client.Nickname
if online && clientName != "" && nickname == "" {
nickname = clientName
}
resp := dto.ClientResponse{ resp := dto.ClientResponse{
ID: client.ID, ID: client.ID,
Nickname: client.Nickname, Nickname: nickname,
Rules: client.Rules, Rules: client.Rules,
Plugins: plugins, Plugins: plugins,
Online: online, Online: online,
@@ -242,8 +248,7 @@ func (h *ClientHandler) Delete(c *gin.Context) {
func (h *ClientHandler) PushConfig(c *gin.Context) { func (h *ClientHandler) PushConfig(c *gin.Context) {
clientID := c.Param("id") clientID := c.Param("id")
online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) if !h.app.GetServer().IsClientOnline(clientID) {
if !online {
ClientNotOnline(c) ClientNotOnline(c)
return return
} }
@@ -311,8 +316,7 @@ func (h *ClientHandler) Restart(c *gin.Context) {
func (h *ClientHandler) InstallPlugins(c *gin.Context) { func (h *ClientHandler) InstallPlugins(c *gin.Context) {
clientID := c.Param("id") clientID := c.Param("id")
online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) if !h.app.GetServer().IsClientOnline(clientID) {
if !online {
ClientNotOnline(c) ClientNotOnline(c)
return return
} }

View File

@@ -19,11 +19,13 @@ type AppInterface interface {
// ServerInterface 服务端接口 // ServerInterface 服务端接口
type ServerInterface interface { type ServerInterface interface {
GetClientStatus(clientID string) (online bool, lastPing, remoteAddr, clientOS, clientArch, clientVersion string) IsClientOnline(clientID string) bool
GetClientStatus(clientID string) (online bool, lastPing, remoteAddr, clientName, clientOS, clientArch, clientVersion string)
GetAllClientStatus() map[string]struct { GetAllClientStatus() map[string]struct {
Online bool Online bool
LastPing string LastPing string
RemoteAddr string RemoteAddr string
Name string
OS string OS string
Arch string Arch string
Version string Version string

View File

@@ -177,8 +177,7 @@ func (h *JSPluginHandler) PushToClient(c *gin.Context) {
c.ShouldBindJSON(&pushReq) // 忽略错误,允许空请求体 c.ShouldBindJSON(&pushReq) // 忽略错误,允许空请求体
// 检查客户端是否在线 // 检查客户端是否在线
online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) if !h.app.GetServer().IsClientOnline(clientID) {
if !online {
ClientNotOnline(c) ClientNotOnline(c)
return return
} }

View File

@@ -35,8 +35,7 @@ func (h *LogHandler) StreamLogs(c *gin.Context) {
clientID := c.Param("id") clientID := c.Param("id")
// 检查客户端是否在线 // 检查客户端是否在线
online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) if !h.app.GetServer().IsClientOnline(clientID) {
if !online {
c.JSON(400, gin.H{"code": 400, "message": "client not online"}) c.JSON(400, gin.H{"code": 400, "message": "client not online"})
return return
} }

View File

@@ -371,8 +371,7 @@ func (h *PluginHandler) UpdateClientConfig(c *gin.Context) {
} }
// 如果客户端在线,同步配置 // 如果客户端在线,同步配置
online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) if h.app.GetServer().IsClientOnline(clientID) {
if online {
if err := h.app.GetServer().SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil { if err := h.app.GetServer().SyncPluginConfigToClient(clientID, pluginName, req.Config); err != nil {
PartialSuccess(c, gin.H{"status": "partial", "port_changed": portChanged}, "config saved but sync failed: "+err.Error()) PartialSuccess(c, gin.H{"status": "partial", "port_changed": portChanged}, "config saved but sync failed: "+err.Error())
return return

View File

@@ -45,8 +45,7 @@ func (h *PluginAPIHandler) ProxyRequest(c *gin.Context) {
} }
// 检查客户端是否在线 // 检查客户端是否在线
online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(clientID) if !h.app.GetServer().IsClientOnline(clientID) {
if !online {
ClientNotOnline(c) ClientNotOnline(c)
return return
} }

View File

@@ -82,8 +82,7 @@ func (h *StoreHandler) Install(c *gin.Context) {
} }
// 检查客户端是否在线 // 检查客户端是否在线
online, _, _, _, _, _ := h.app.GetServer().GetClientStatus(req.ClientID) if !h.app.GetServer().IsClientOnline(req.ClientID) {
if !online {
ClientNotOnline(c) ClientNotOnline(c)
return return
} }

View File

@@ -83,6 +83,7 @@ type JSPluginEntry struct {
// ClientSession 客户端会话 // ClientSession 客户端会话
type ClientSession struct { type ClientSession struct {
ID string ID string
Name string // 客户端名称(主机名)
RemoteAddr string // 客户端 IP 地址 RemoteAddr string // 客户端 IP 地址
OS string // 客户端操作系统 OS string // 客户端操作系统
Arch string // 客户端架构 Arch string // 客户端架构
@@ -269,13 +270,21 @@ func (s *Server) handleConnection(conn net.Conn) {
// 检查客户端是否存在,不存在则自动创建 // 检查客户端是否存在,不存在则自动创建
exists, err := s.clientStore.ClientExists(clientID) exists, err := s.clientStore.ClientExists(clientID)
if err != nil || !exists { if err != nil || !exists {
newClient := &db.Client{ID: clientID, Rules: []protocol.ProxyRule{}} newClient := &db.Client{ID: clientID, Nickname: authReq.Name, Rules: []protocol.ProxyRule{}}
if err := s.clientStore.CreateClient(newClient); err != nil { if err := s.clientStore.CreateClient(newClient); err != nil {
log.Printf("[Server] Create client error: %v", err) log.Printf("[Server] Create client error: %v", err)
s.sendAuthResponse(conn, false, "failed to create client", "") s.sendAuthResponse(conn, false, "failed to create client", "")
return return
} }
log.Printf("[Server] New client registered: %s", clientID) log.Printf("[Server] New client registered: %s (%s)", clientID, authReq.Name)
} else if authReq.Name != "" {
// 客户端已存在,更新名称(如果提供了新名称)
if client, err := s.clientStore.GetClient(clientID); err == nil {
if client.Nickname == "" || client.Nickname != authReq.Name {
client.Nickname = authReq.Name
s.clientStore.UpdateClient(client)
}
}
} }
rules, _ := s.clientStore.GetClientRules(clientID) rules, _ := s.clientStore.GetClientRules(clientID)
@@ -290,11 +299,11 @@ func (s *Server) handleConnection(conn net.Conn) {
} }
security.LogAuthSuccess(clientIP, clientID) security.LogAuthSuccess(clientIP, clientID)
s.setupClientSession(conn, clientID, authReq.OS, authReq.Arch, authReq.Version, rules) s.setupClientSession(conn, clientID, authReq.Name, authReq.OS, authReq.Arch, authReq.Version, rules)
} }
// setupClientSession 建立客户端会话 // setupClientSession 建立客户端会话
func (s *Server) setupClientSession(conn net.Conn, clientID, clientOS, clientArch, clientVersion string, rules []protocol.ProxyRule) { func (s *Server) setupClientSession(conn net.Conn, clientID, clientName, clientOS, clientArch, clientVersion string, rules []protocol.ProxyRule) {
session, err := yamux.Server(conn, nil) session, err := yamux.Server(conn, nil)
if err != nil { if err != nil {
log.Printf("[Server] Yamux error: %v", err) log.Printf("[Server] Yamux error: %v", err)
@@ -309,6 +318,7 @@ func (s *Server) setupClientSession(conn net.Conn, clientID, clientOS, clientArc
cs := &ClientSession{ cs := &ClientSession{
ID: clientID, ID: clientID,
Name: clientName,
RemoteAddr: remoteAddr, RemoteAddr: remoteAddr,
OS: clientOS, OS: clientOS,
Arch: clientArch, Arch: clientArch,
@@ -586,16 +596,24 @@ func (s *Server) sendHeartbeat(cs *ClientSession) bool {
} }
// GetClientStatus 获取客户端状态 // GetClientStatus 获取客户端状态
func (s *Server) GetClientStatus(clientID string) (online bool, lastPing, remoteAddr, clientOS, clientArch, clientVersion string) { func (s *Server) GetClientStatus(clientID string) (online bool, lastPing, remoteAddr, clientName, clientOS, clientArch, clientVersion string) {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
if cs, ok := s.clients[clientID]; ok { if cs, ok := s.clients[clientID]; ok {
cs.mu.Lock() cs.mu.Lock()
defer cs.mu.Unlock() defer cs.mu.Unlock()
return true, cs.LastPing.Format(time.RFC3339), cs.RemoteAddr, cs.OS, cs.Arch, cs.Version return true, cs.LastPing.Format(time.RFC3339), cs.RemoteAddr, cs.Name, cs.OS, cs.Arch, cs.Version
} }
return false, "", "", "", "", "" return false, "", "", "", "", "", ""
}
// IsClientOnline 检查客户端是否在线
func (s *Server) IsClientOnline(clientID string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
_, ok := s.clients[clientID]
return ok
} }
// GetClientPluginStatus 获取客户端插件运行状态 // GetClientPluginStatus 获取客户端插件运行状态
@@ -646,6 +664,7 @@ func (s *Server) GetAllClientStatus() map[string]struct {
Online bool Online bool
LastPing string LastPing string
RemoteAddr string RemoteAddr string
Name string
OS string OS string
Arch string Arch string
Version string Version string
@@ -662,6 +681,7 @@ func (s *Server) GetAllClientStatus() map[string]struct {
Online bool Online bool
LastPing string LastPing string
RemoteAddr string RemoteAddr string
Name string
OS string OS string
Arch string Arch string
Version string Version string
@@ -673,6 +693,7 @@ func (s *Server) GetAllClientStatus() map[string]struct {
Online bool Online bool
LastPing string LastPing string
RemoteAddr string RemoteAddr string
Name string
OS string OS string
Arch string Arch string
Version string Version string
@@ -680,6 +701,7 @@ func (s *Server) GetAllClientStatus() map[string]struct {
Online: true, Online: true,
LastPing: cs.LastPing.Format(time.RFC3339), LastPing: cs.LastPing.Format(time.RFC3339),
RemoteAddr: cs.RemoteAddr, RemoteAddr: cs.RemoteAddr,
Name: cs.Name,
OS: cs.OS, OS: cs.OS,
Arch: cs.Arch, Arch: cs.Arch,
Version: cs.Version, Version: cs.Version,

View File

@@ -87,6 +87,7 @@ type Message struct {
type AuthRequest struct { type AuthRequest struct {
ClientID string `json:"client_id"` ClientID string `json:"client_id"`
Token string `json:"token"` Token string `json:"token"`
Name string `json:"name,omitempty"` // 客户端名称(主机名)
OS string `json:"os,omitempty"` // 客户端操作系统 OS string `json:"os,omitempty"` // 客户端操作系统
Arch string `json:"arch,omitempty"` // 客户端架构 Arch string `json:"arch,omitempty"` // 客户端架构
Version string `json:"version,omitempty"` // 客户端版本 Version string `json:"version,omitempty"` // 客户端版本