From 0b60c0d8847e822701437524b64ac5a5dd3cb9fe Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Mon, 26 Oct 2026 15:15:23 +0800 Subject: [PATCH] feat(PushPanel): add /healthz reporting adapter connection state BotService gains IsConnected (OneBot reads the live WebSocket, others fall back to having a robot account). The panel serves it unauthenticated so status.nbtca.space can probe it. --- .../ForwardWebSocketClient.cs | 2 ++ .../OneBotAdapter.cs | 2 ++ .../PanelServer.cs | 23 +++++++++++++++++- .../PluginMain.cs | 8 ++++++- src/HuaJiBot.NET.UnitTest/PushPanelTest.cs | 24 +++++++++++++++++++ src/HuaJiBot.NET/Bot/BotService.cs | 1 + src/HuaJiBot.NET/Interfaces/IPluginService.cs | 3 +++ src/HuaJiBot.NET/Websocket/WebsocketClient.cs | 2 ++ 8 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/HuaJiBot.NET.Adapter.OneBot/ForwardWebSocketClient.cs b/src/HuaJiBot.NET.Adapter.OneBot/ForwardWebSocketClient.cs index 4d61995..aeab86e 100644 --- a/src/HuaJiBot.NET.Adapter.OneBot/ForwardWebSocketClient.cs +++ b/src/HuaJiBot.NET.Adapter.OneBot/ForwardWebSocketClient.cs @@ -10,6 +10,8 @@ internal class ForwardWebSocketClient private readonly WebsocketClient _client; internal readonly OneBotApi Api; + public bool IsConnected => _client.IsConnected; + public Task StartAsync() => _client.StartAsync(); public ValueTask ConnectAsync() => _client.ConnectAsync(); diff --git a/src/HuaJiBot.NET.Adapter.OneBot/OneBotAdapter.cs b/src/HuaJiBot.NET.Adapter.OneBot/OneBotAdapter.cs index 019750c..7109fb6 100644 --- a/src/HuaJiBot.NET.Adapter.OneBot/OneBotAdapter.cs +++ b/src/HuaJiBot.NET.Adapter.OneBot/OneBotAdapter.cs @@ -19,6 +19,8 @@ protected override void ReconnectCore() protected override Task SetupServiceAsyncCore() => Client.StartAsync(); + public override bool IsConnected => Client.IsConnected; + public override string[] AllRobots => Client.QQ is not null ? [Client.QQ] : []; public override async Task SendGroupMessageAsync( diff --git a/src/HuaJiBot.NET.Plugin.PushPanel/PanelServer.cs b/src/HuaJiBot.NET.Plugin.PushPanel/PanelServer.cs index 320d426..37dca7e 100644 --- a/src/HuaJiBot.NET.Plugin.PushPanel/PanelServer.cs +++ b/src/HuaJiBot.NET.Plugin.PushPanel/PanelServer.cs @@ -14,12 +14,20 @@ internal sealed class PanelServer : IDisposable private readonly string _token; private readonly ConfigWrapper _config; private readonly string _page; + private readonly Func _isConnected; private readonly CancellationTokenSource _cts = new(); - public PanelServer(string host, int port, string token, ConfigWrapper config) + public PanelServer( + string host, + int port, + string token, + ConfigWrapper config, + Func? isConnected = null + ) { _token = token; _config = config; + _isConnected = isConnected ?? (() => true); _page = ReadPage(); _listener.Prefixes.Add($"http://{host}:{port}/"); } @@ -81,6 +89,8 @@ private async Task HandleAsync(HttpListenerContext context) { if (method == "GET" && path is "/" or "/index.html") return ServePage; + if (method == "GET" && path == "/healthz") + return ServeHealth; if (path != "/api/rules") return null; return method switch @@ -91,6 +101,17 @@ private async Task HandleAsync(HttpListenerContext context) }; } + // Unauthenticated and public: reports connection state only, nothing from the config. + private Task ServeHealth(HttpListenerContext context) + { + var connected = _isConnected(); + return WriteJson( + context.Response, + connected ? 200 : 503, + new { status = connected ? "ok" : "disconnected" } + ); + } + private Task ServePage(HttpListenerContext context) => WriteText(context.Response, 200, "text/html; charset=utf-8", _page); diff --git a/src/HuaJiBot.NET.Plugin.PushPanel/PluginMain.cs b/src/HuaJiBot.NET.Plugin.PushPanel/PluginMain.cs index 39621fe..657d3bc 100644 --- a/src/HuaJiBot.NET.Plugin.PushPanel/PluginMain.cs +++ b/src/HuaJiBot.NET.Plugin.PushPanel/PluginMain.cs @@ -41,7 +41,13 @@ protected override void Initialize() } try { - _server = new PanelServer(Config.Host, Config.Port, Config.Token, Service.Config); + _server = new PanelServer( + Config.Host, + Config.Port, + Config.Token, + Service.Config, + () => Service.IsConnected + ); _server.Start(); Service.Log($"[PushPanel] http://{Config.Host}:{Config.Port}/"); } diff --git a/src/HuaJiBot.NET.UnitTest/PushPanelTest.cs b/src/HuaJiBot.NET.UnitTest/PushPanelTest.cs index 759b381..a774154 100644 --- a/src/HuaJiBot.NET.UnitTest/PushPanelTest.cs +++ b/src/HuaJiBot.NET.UnitTest/PushPanelTest.cs @@ -176,6 +176,30 @@ public async Task Panel_RequiresTokenAndSaves() }); } + [Test] + public async Task Panel_HealthzReportsConnectionWithoutToken() + { + using var scope = new ConfigScope(); + var connected = true; + var port = FreePort(); + using var server = new PanelServer("127.0.0.1", port, "panel-secret", scope.Wrapper, () => connected); + server.Start(); + using var http = new HttpClient(); + + var up = await http.GetAsync($"http://127.0.0.1:{port}/healthz"); + connected = false; + var down = await http.GetAsync($"http://127.0.0.1:{port}/healthz"); + var body = await down.Content.ReadAsStringAsync(); + + Assert.Multiple(() => + { + Assert.That(up.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + Assert.That(down.StatusCode, Is.EqualTo(HttpStatusCode.ServiceUnavailable)); + Assert.That(body, Does.Contain("disconnected")); + Assert.That(body, Does.Not.Contain("panel-secret")); + }); + } + private static int FreePort() { var listener = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0); diff --git a/src/HuaJiBot.NET/Bot/BotService.cs b/src/HuaJiBot.NET/Bot/BotService.cs index 18ab46f..7159d79 100644 --- a/src/HuaJiBot.NET/Bot/BotService.cs +++ b/src/HuaJiBot.NET/Bot/BotService.cs @@ -120,6 +120,7 @@ private string AppendSourceInfo(object msg, string? file = null, int? line = nul public Config.ConfigWrapper Config { get; internal set; } = null!; public abstract IEvents Events { get; } public abstract string[] AllRobots { get; } + public virtual bool IsConnected => AllRobots.Length > 0; protected abstract void ReconnectCore(); protected abstract Task SetupServiceAsyncCore(); diff --git a/src/HuaJiBot.NET/Interfaces/IPluginService.cs b/src/HuaJiBot.NET/Interfaces/IPluginService.cs index 52545f0..2bc2076 100644 --- a/src/HuaJiBot.NET/Interfaces/IPluginService.cs +++ b/src/HuaJiBot.NET/Interfaces/IPluginService.cs @@ -45,6 +45,9 @@ Func> fallback void RecallMessage(string? robotId, string targetGroup, string msgId); Task FeedbackAt(string? robotId, string targetGroup, string msgId, string text); string[] AllRobots { get; } + + /// Whether the adapter is currently connected to its platform. + bool IsConnected { get; } ConfigWrapper Config { get; } ILogger Logger { get; } IReadOnlyDictionary> ExportFunctions { get; } diff --git a/src/HuaJiBot.NET/Websocket/WebsocketClient.cs b/src/HuaJiBot.NET/Websocket/WebsocketClient.cs index 84c0635..390c434 100644 --- a/src/HuaJiBot.NET/Websocket/WebsocketClient.cs +++ b/src/HuaJiBot.NET/Websocket/WebsocketClient.cs @@ -71,6 +71,8 @@ public WebsocketClient( _client.MessageReceived += OnMessageReceived; } + public bool IsConnected => _client.Connected; + public async Task StartAsync() { await ConnectAsync();