From bb35b283da2149d09a04f5c40dc96e3c0be4c213 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:33:24 +0000 Subject: [PATCH 1/2] refactor: consolidate WebLoginStartResult and WebLoginWaitResult into WebLoginResult WebLoginStartResult and WebLoginWaitResult were byte-for-byte identical: both had the same 5 properties (Message, QrDataUrl, Connected, Error, RawResponse). The method names (WebLoginStartAsync / WebLoginWaitAsync) already distinguish the two flows semantically, so maintaining two separate types added no value. Replace both with a single WebLoginResult class. Update IOperatorGatewayClient, OpenClawGatewayClient, and the FakeOperatorGatewayClient test stub accordingly. ChannelsPage.xaml.cs uses both via property access with inferred types and required no changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.Shared/ChannelsSnapshot.cs | 22 +++++-------------- src/OpenClaw.Shared/IOperatorGatewayClient.cs | 4 ++-- src/OpenClaw.Shared/OpenClawGatewayClient.cs | 12 +++++----- .../OnboardingChatBootstrapperTests.cs | 4 ++-- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/OpenClaw.Shared/ChannelsSnapshot.cs b/src/OpenClaw.Shared/ChannelsSnapshot.cs index 721aaa3c5..b5866e1f7 100644 --- a/src/OpenClaw.Shared/ChannelsSnapshot.cs +++ b/src/OpenClaw.Shared/ChannelsSnapshot.cs @@ -234,22 +234,12 @@ public sealed class IMessageChannelStatus // ─── Web login (QR linking) ────────────────────────────────────────────────── -/// Result of web.login.start — a QR or status for a channel. -public sealed class WebLoginStartResult -{ - public string? Message { get; init; } - public string? QrDataUrl { get; init; } - public bool Connected { get; init; } - - /// Gateway-side error message when the call failed (ok=false), or transport exception. Null on success. - public string? Error { get; init; } - - /// Raw JSON of the gateway response (or stringified exception). Used by the diagnostic disclosure in the UI. - public string? RawResponse { get; init; } -} - -/// Result of web.login.wait — long-poll outcome. -public sealed class WebLoginWaitResult +/// +/// Result of a QR/web-login RPC call (web.login.start or web.login.wait). +/// Both calls return an identical wire shape; a single type removes duplication while +/// the calling method name already distinguishes the two flows. +/// +public sealed class WebLoginResult { public string? Message { get; init; } public string? QrDataUrl { get; init; } diff --git a/src/OpenClaw.Shared/IOperatorGatewayClient.cs b/src/OpenClaw.Shared/IOperatorGatewayClient.cs index 2d59f0389..d5ef81e54 100644 --- a/src/OpenClaw.Shared/IOperatorGatewayClient.cs +++ b/src/OpenClaw.Shared/IOperatorGatewayClient.cs @@ -107,8 +107,8 @@ public interface IOperatorGatewayClient /// Log out / unlink a channel (whatsapp, telegram). Sends channels.logout { channel }. Task LogoutChannelAsync(string channelName, int timeoutMs = 12000); /// Begin a QR linking flow (whatsapp, signal). Sends web.login.start { force, timeoutMs }. - Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000); + Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000); /// Long-poll for QR linking completion. Sends web.login.wait { currentQrDataUrl, timeoutMs }. - Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000); + Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000); Task SendWizardRequestAsync(string method, object? parameters = null, int timeoutMs = 30000); } diff --git a/src/OpenClaw.Shared/OpenClawGatewayClient.cs b/src/OpenClaw.Shared/OpenClawGatewayClient.cs index 2903700f4..ba1cbff1b 100644 --- a/src/OpenClaw.Shared/OpenClawGatewayClient.cs +++ b/src/OpenClaw.Shared/OpenClawGatewayClient.cs @@ -1210,7 +1210,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 } /// Begin a web/QR linking flow for the current default linking channel. - public async Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) + public async Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) { if (!IsConnected) return null; try @@ -1219,7 +1219,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 "web.login.start", new { force, timeoutMs }, timeoutMs + 5000); - return new WebLoginStartResult + return new WebLoginResult { Message = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String ? m.GetString() : null, QrDataUrl = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("qrDataUrl", out var q) && q.ValueKind == JsonValueKind.String ? q.GetString() : null, @@ -1233,7 +1233,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 // Return a populated result with the error so the UI can surface // it in the diagnostic disclosure. Returning null would lose the // gateway's actual reason for failing. - return new WebLoginStartResult + return new WebLoginResult { Error = ex.Message, RawResponse = ex.ToString(), @@ -1242,7 +1242,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 } /// Long-poll for QR linking completion. - public async Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) + public async Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) { if (!IsConnected) return null; try @@ -1251,7 +1251,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 "web.login.wait", new { currentQrDataUrl, timeoutMs }, timeoutMs + 5000); - return new WebLoginWaitResult + return new WebLoginResult { Message = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String ? m.GetString() : null, QrDataUrl = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("qrDataUrl", out var q) && q.ValueKind == JsonValueKind.String ? q.GetString() : null, @@ -1262,7 +1262,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 catch (Exception ex) { _logger.Warn($"web.login.wait failed: {ex.Message}"); - return new WebLoginWaitResult + return new WebLoginResult { Error = ex.Message, RawResponse = ex.ToString(), diff --git a/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs b/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs index d6843c572..adcb9b9d7 100644 --- a/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs +++ b/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs @@ -200,8 +200,8 @@ public Task NodeRenameAsync(string nodeId, string displayName) public Task StopChannelAsync(string channelName) => Task.FromResult(false); public Task GetChannelsStatusAsync(bool probe = false, int timeoutMs = 12000) => Task.FromResult(null); public Task LogoutChannelAsync(string channelName, int timeoutMs = 12000) => Task.FromResult(false); - public Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) => Task.FromResult(null); - public Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) => Task.FromResult(null); + public Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) => Task.FromResult(null); + public Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) => Task.FromResult(null); public Task SendWizardRequestAsync(string method, object? parameters = null, int timeoutMs = 30000) => Task.FromResult(default(JsonElement)); } #pragma warning restore CS0067 From 7601ceaab11072a502b18516ec7c0c595f8fa836 Mon Sep 17 00:00:00 2001 From: ranjeshj Date: Wed, 17 Jun 2026 23:41:27 -0700 Subject: [PATCH 2/2] Preserve web login result API Restore the public WebLoginStartResult and WebLoginWaitResult contract while keeping shared response parsing inside OpenClawGatewayClient. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/OpenClaw.Shared/ChannelsSnapshot.cs | 22 +++++++--- src/OpenClaw.Shared/IOperatorGatewayClient.cs | 4 +- src/OpenClaw.Shared/OpenClawGatewayClient.cs | 41 ++++++++++++------- .../OnboardingChatBootstrapperTests.cs | 4 +- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/OpenClaw.Shared/ChannelsSnapshot.cs b/src/OpenClaw.Shared/ChannelsSnapshot.cs index b5866e1f7..a598f69ad 100644 --- a/src/OpenClaw.Shared/ChannelsSnapshot.cs +++ b/src/OpenClaw.Shared/ChannelsSnapshot.cs @@ -234,12 +234,22 @@ public sealed class IMessageChannelStatus // ─── Web login (QR linking) ────────────────────────────────────────────────── -/// -/// Result of a QR/web-login RPC call (web.login.start or web.login.wait). -/// Both calls return an identical wire shape; a single type removes duplication while -/// the calling method name already distinguishes the two flows. -/// -public sealed class WebLoginResult +/// Result of web.login.start - a QR or status for a channel. +public sealed class WebLoginStartResult +{ + public string? Message { get; init; } + public string? QrDataUrl { get; init; } + public bool Connected { get; init; } + + /// Gateway-side error message when the call failed (ok=false), or transport exception. Null on success. + public string? Error { get; init; } + + /// Raw JSON of the gateway response (or stringified exception). Used by the diagnostic disclosure in the UI. + public string? RawResponse { get; init; } +} + +/// Result of web.login.wait - long-poll outcome. +public sealed class WebLoginWaitResult { public string? Message { get; init; } public string? QrDataUrl { get; init; } diff --git a/src/OpenClaw.Shared/IOperatorGatewayClient.cs b/src/OpenClaw.Shared/IOperatorGatewayClient.cs index d5ef81e54..2d59f0389 100644 --- a/src/OpenClaw.Shared/IOperatorGatewayClient.cs +++ b/src/OpenClaw.Shared/IOperatorGatewayClient.cs @@ -107,8 +107,8 @@ public interface IOperatorGatewayClient /// Log out / unlink a channel (whatsapp, telegram). Sends channels.logout { channel }. Task LogoutChannelAsync(string channelName, int timeoutMs = 12000); /// Begin a QR linking flow (whatsapp, signal). Sends web.login.start { force, timeoutMs }. - Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000); + Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000); /// Long-poll for QR linking completion. Sends web.login.wait { currentQrDataUrl, timeoutMs }. - Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000); + Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000); Task SendWizardRequestAsync(string method, object? parameters = null, int timeoutMs = 30000); } diff --git a/src/OpenClaw.Shared/OpenClawGatewayClient.cs b/src/OpenClaw.Shared/OpenClawGatewayClient.cs index ba1cbff1b..5a6c8cd21 100644 --- a/src/OpenClaw.Shared/OpenClawGatewayClient.cs +++ b/src/OpenClaw.Shared/OpenClawGatewayClient.cs @@ -1210,7 +1210,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 } /// Begin a web/QR linking flow for the current default linking channel. - public async Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) + public async Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) { if (!IsConnected) return null; try @@ -1219,12 +1219,13 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 "web.login.start", new { force, timeoutMs }, timeoutMs + 5000); - return new WebLoginResult + var result = ParseWebLoginResponse(response); + return new WebLoginStartResult { - Message = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String ? m.GetString() : null, - QrDataUrl = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("qrDataUrl", out var q) && q.ValueKind == JsonValueKind.String ? q.GetString() : null, - Connected = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("connected", out var c) && c.ValueKind == JsonValueKind.True, - RawResponse = response.ValueKind != JsonValueKind.Undefined ? response.GetRawText() : null, + Message = result.Message, + QrDataUrl = result.QrDataUrl, + Connected = result.Connected, + RawResponse = result.RawResponse, }; } catch (Exception ex) @@ -1233,7 +1234,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 // Return a populated result with the error so the UI can surface // it in the diagnostic disclosure. Returning null would lose the // gateway's actual reason for failing. - return new WebLoginResult + return new WebLoginStartResult { Error = ex.Message, RawResponse = ex.ToString(), @@ -1242,7 +1243,7 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 } /// Long-poll for QR linking completion. - public async Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) + public async Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) { if (!IsConnected) return null; try @@ -1251,18 +1252,19 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 "web.login.wait", new { currentQrDataUrl, timeoutMs }, timeoutMs + 5000); - return new WebLoginResult + var result = ParseWebLoginResponse(response); + return new WebLoginWaitResult { - Message = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String ? m.GetString() : null, - QrDataUrl = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("qrDataUrl", out var q) && q.ValueKind == JsonValueKind.String ? q.GetString() : null, - Connected = response.ValueKind == JsonValueKind.Object && response.TryGetProperty("connected", out var c) && c.ValueKind == JsonValueKind.True, - RawResponse = response.ValueKind != JsonValueKind.Undefined ? response.GetRawText() : null, + Message = result.Message, + QrDataUrl = result.QrDataUrl, + Connected = result.Connected, + RawResponse = result.RawResponse, }; } catch (Exception ex) { _logger.Warn($"web.login.wait failed: {ex.Message}"); - return new WebLoginResult + return new WebLoginWaitResult { Error = ex.Message, RawResponse = ex.ToString(), @@ -1270,6 +1272,17 @@ public async Task LogoutChannelAsync(string channelName, int timeoutMs = 1 } } + private static WebLoginResponse ParseWebLoginResponse(JsonElement response) + { + return new WebLoginResponse( + response.ValueKind == JsonValueKind.Object && response.TryGetProperty("message", out var m) && m.ValueKind == JsonValueKind.String ? m.GetString() : null, + response.ValueKind == JsonValueKind.Object && response.TryGetProperty("qrDataUrl", out var q) && q.ValueKind == JsonValueKind.String ? q.GetString() : null, + response.ValueKind == JsonValueKind.Object && response.TryGetProperty("connected", out var c) && c.ValueKind == JsonValueKind.True, + response.ValueKind != JsonValueKind.Undefined ? response.GetRawText() : null); + } + + private readonly record struct WebLoginResponse(string? Message, string? QrDataUrl, bool Connected, string? RawResponse); + private async Task SendConnectMessageAsync(string? nonce = null) { var requestId = Guid.NewGuid().ToString(); diff --git a/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs b/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs index adcb9b9d7..d6843c572 100644 --- a/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs +++ b/tests/OpenClaw.Tray.Tests/OnboardingChatBootstrapperTests.cs @@ -200,8 +200,8 @@ public Task NodeRenameAsync(string nodeId, string displayName) public Task StopChannelAsync(string channelName) => Task.FromResult(false); public Task GetChannelsStatusAsync(bool probe = false, int timeoutMs = 12000) => Task.FromResult(null); public Task LogoutChannelAsync(string channelName, int timeoutMs = 12000) => Task.FromResult(false); - public Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) => Task.FromResult(null); - public Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) => Task.FromResult(null); + public Task WebLoginStartAsync(bool force = false, int timeoutMs = 30000) => Task.FromResult(null); + public Task WebLoginWaitAsync(string? currentQrDataUrl = null, int timeoutMs = 30000) => Task.FromResult(null); public Task SendWizardRequestAsync(string method, object? parameters = null, int timeoutMs = 30000) => Task.FromResult(default(JsonElement)); } #pragma warning restore CS0067