using System.Net.WebSockets; using System.Text.Json; namespace Zcbot.WindowsNode; internal sealed class NodeSession { private static readonly TimeSpan[] Backoff = [ TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(60), ]; private readonly NodeConfig config; private readonly Func runtimePayload; private readonly Func connected; private readonly Func heartbeat; private readonly Func received; private readonly Action report; private readonly Func> connect; private readonly Func delay; internal NodeSession( NodeConfig config, Func runtimePayload, Func connected, Func heartbeat, Func received, Action report, Func>? connect = null, Func? delay = null) { this.config = config; this.runtimePayload = runtimePayload; this.connected = connected; this.heartbeat = heartbeat; this.received = received; this.report = report; this.connect = connect ?? NodeProtocolClient.ConnectAsync; this.delay = delay ?? Task.Delay; } internal async Task RunAsync(CancellationToken cancellationToken) { var attempt = 0; while (!cancellationToken.IsCancellationRequested) { try { report(NodeState.Connecting, "正在连接 zcbot…"); await ConnectOnceAsync(cancellationToken); attempt = 0; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (WebSocketException exception) { report(NodeState.Offline, "连接中断,等待重连"); Console.Error.WriteLine($"[WARN] WebSocket disconnected: {exception.Message}"); } catch (IOException exception) { report(NodeState.Offline, "网络不可用,等待重连"); Console.Error.WriteLine($"[WARN] Connection I/O failed: {exception.Message}"); } catch (JsonException exception) { report(NodeState.Offline, "服务端消息无效,等待重连"); Console.Error.WriteLine($"[WARN] Invalid server message: {exception.Message}"); } catch (NodeEndpointException exception) { report(NodeState.Offline, "WebSocket 握手被拒绝,请检查服务端或反向代理"); Console.Error.WriteLine($"[WARN] WebSocket handshake rejected: {exception.Message}"); } var baseDelay = Backoff[Math.Min(attempt, Backoff.Length - 1)]; attempt++; var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 750)); var delay = baseDelay + jitter; Console.WriteLine($"[INFO] Reconnecting in {delay.TotalSeconds:F1}s."); await this.delay(delay, cancellationToken); } } private async Task ConnectOnceAsync(CancellationToken cancellationToken) { await using var client = await connect(config, cancellationToken); Console.WriteLine("[OK] Node connected."); report(NodeState.Online, "已连接"); await client.SendAsync("hello", runtimePayload(), cancellationToken); await connected(client, cancellationToken); using var heartbeatStop = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var heartbeatTask = HeartbeatLoopAsync(client, heartbeatStop.Token); try { while (await client.ReceiveAsync(cancellationToken) is { } message) { Console.WriteLine($"[INFO] Server message: {message.Type}."); await received(client, message, cancellationToken); } } finally { heartbeatStop.Cancel(); try { await heartbeatTask; } catch (OperationCanceledException) when (heartbeatStop.IsCancellationRequested) { } } } private async Task HeartbeatLoopAsync( NodeProtocolClient client, CancellationToken cancellationToken) { using var timer = new PeriodicTimer(TimeSpan.FromSeconds(config.HeartbeatSeconds)); while (await timer.WaitForNextTickAsync(cancellationToken)) { await client.SendAsync("heartbeat", runtimePayload(), cancellationToken); await heartbeat(client, cancellationToken); } } }