From 15b0477b448edbac2b4680dcc467c3a4b2fe5a86 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sun, 20 Sep 2026 23:25:46 +0800 Subject: [PATCH] fix: keep remote heartbeat UI side effect free --- .../WxAgent.Host/WindowsAgentBackend.cs | 22 +++++++++++------ node-agent/WxAgent.Service/AgentService.cs | 1 + .../RemoteAgentHostedService.cs | 24 +++++++++++++++---- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/node-agent/WxAgent.Host/WindowsAgentBackend.cs b/node-agent/WxAgent.Host/WindowsAgentBackend.cs index d0983fb..6d0552c 100644 --- a/node-agent/WxAgent.Host/WindowsAgentBackend.cs +++ b/node-agent/WxAgent.Host/WindowsAgentBackend.cs @@ -85,16 +85,22 @@ public sealed class WindowsAgentBackend(AccountBindingStore bindings, ServiceOpt ? string.Equals(binding.WechatId, current.WechatId, StringComparison.OrdinalIgnoreCase) : string.Equals(binding.Nickname, current.DisplayName, StringComparison.Ordinal); - public async Task> AccountsAsync(CancellationToken cancellationToken) + public Task> AccountsAsync(CancellationToken cancellationToken) => + AccountsAsync(cancellationToken, refreshUiIdentity: true); + + public async Task> AccountsAsync(CancellationToken cancellationToken, bool refreshUiIdentity) { - var targets = await ReadUiTargetsAsync(cancellationToken, true); + var targets = await ReadUiTargetsAsync(cancellationToken, refreshUiIdentity); var accounts = await ReadDatabaseAccountsAsync(cancellationToken); - await AutoBindMatchesAsync(accounts, targets, cancellationToken); + if (refreshUiIdentity) + await AutoBindMatchesAsync(accounts, targets, cancellationToken); var current = bindings.ReadAll(); + var singleBoundTarget = !refreshUiIdentity && current.Count == 1 && targets.Count == 1; var live = current.Where(binding => targets.Any(target => - target.ProcessId == binding.ProcessId && target.WindowHandle == binding.WindowHandle && - (string.IsNullOrWhiteSpace(target.WechatId) && string.IsNullOrWhiteSpace(target.Nickname) || - BindingMatches(binding, new WechatAccountSnapshot(target.Nickname ?? string.Empty, target.WechatId))))) + (target.ProcessId == binding.ProcessId && target.WindowHandle == binding.WindowHandle && + (!refreshUiIdentity || string.IsNullOrWhiteSpace(target.WechatId) && string.IsNullOrWhiteSpace(target.Nickname) || + BindingMatches(binding, new WechatAccountSnapshot(target.Nickname ?? string.Empty, target.WechatId)))) || + singleBoundTarget)) .Select(binding => binding.AccountId) .ToHashSet(StringComparer.OrdinalIgnoreCase); return accounts.Select(account => @@ -466,7 +472,9 @@ public sealed class WindowsAgentBackend(AccountBindingStore bindings, ServiceOpt return Task.FromResult(new { serviceOnline = true, - wechatAvailable = report.Errors.Count == 0, + // Process/window presence is availability; a changed control tree is a degraded UI state, + // not a stopped WeChat process. + wechatAvailable = report.Processes.Count > 0 && report.WindowFound, sessionAvailable = report.UserInteractive && report.InputDesktopAvailable, sessionLocked = report.Errors.Contains(WxAgentErrorCode.SessionLocked), report.WindowFound, diff --git a/node-agent/WxAgent.Service/AgentService.cs b/node-agent/WxAgent.Service/AgentService.cs index eaf5d3a..541e1ca 100644 --- a/node-agent/WxAgent.Service/AgentService.cs +++ b/node-agent/WxAgent.Service/AgentService.cs @@ -14,6 +14,7 @@ public interface IAgentBackend Task StatusAsync(CancellationToken cancellationToken); Task DiagnoseAsync(CancellationToken cancellationToken) => StatusAsync(cancellationToken); Task> AccountsAsync(CancellationToken cancellationToken) => Task.FromResult>([]); + Task> AccountsAsync(CancellationToken cancellationToken, bool refreshUiIdentity) => AccountsAsync(cancellationToken); Task> UiTargetsAsync(CancellationToken cancellationToken) => Task.FromResult>([]); Task BindAccountAsync(string accountId, string targetId, CancellationToken cancellationToken) => throw new ServiceException("Unsupported", 501, "Account binding is not available."); Task UnbindAccountAsync(string accountId, CancellationToken cancellationToken) => throw new ServiceException("Unsupported", 501, "Account binding is not available."); diff --git a/node-agent/WxAgent.Service/RemoteAgentHostedService.cs b/node-agent/WxAgent.Service/RemoteAgentHostedService.cs index 90ffa6c..6fa590a 100644 --- a/node-agent/WxAgent.Service/RemoteAgentHostedService.cs +++ b/node-agent/WxAgent.Service/RemoteAgentHostedService.cs @@ -401,17 +401,31 @@ public sealed class RemoteAgentHostedService( var wechatRunning = GetBoolean(element, "wechatAvailable"); var sessionAvailable = GetBoolean(element, "sessionAvailable"); var sessionLocked = GetBoolean(element, "sessionLocked"); - var accounts = await backend.AccountsAsync(cancellationToken); + // Heartbeats must not refresh UI identity. Binding already contains the verified identity; + // the explicit accounts API remains the only path that may inspect the profile. + var accounts = await backend.AccountsAsync(cancellationToken, refreshUiIdentity: false); var identities = accounts.Select(account => new RemoteAccountIdentity( account.AccountId, account.IsUiBindingKnown && account.Binding is not null && string.Equals(account.BindingStatus, "Bound", StringComparison.Ordinal))).ToArray(); + var activeAccountId = remote.ActiveAccountId; + var boundAccounts = accounts + .Where(account => account.IsUiBindingKnown && account.Binding is not null && string.Equals(account.BindingStatus, "Bound", StringComparison.Ordinal)) + .Select(account => account.AccountId) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + if ((string.IsNullOrWhiteSpace(activeAccountId) || !identities.Any(identity => identity.Verified && string.Equals(identity.AccountId, activeAccountId, StringComparison.Ordinal))) + && boundAccounts.Length == 1) + { + // A single verified binding is safe to use when the optional GUI value is empty or a display name. + activeAccountId = boundAccounts[0]; + } var activeAccountVerified = false; - if (remote.ActiveAccountId is { Length: > 0 }) + if (activeAccountId is { Length: > 0 }) { try { - accountContext.SwitchTo(remote.ActiveAccountId, identities); - activeAccountVerified = accountContext.IsConfirmedFor(remote.ActiveAccountId); + accountContext.SwitchTo(activeAccountId, identities); + activeAccountVerified = accountContext.IsConfirmedFor(activeAccountId); } catch (WxAgentException) { @@ -427,7 +441,7 @@ public sealed class RemoteAgentHostedService( : !sessionAvailable ? RemoteNodeStatus.WechatNotLoggedIn : !activeAccountVerified ? RemoteNodeStatus.Degraded : RemoteNodeStatus.Online; - return new BackendSnapshot(nodeStatus, wechatRunning, sessionAvailable, sessionLocked, remote.ActiveAccountId, 0, activeAccountVerified, identities); + return new BackendSnapshot(nodeStatus, wechatRunning, sessionAvailable, sessionLocked, activeAccountId, 0, activeAccountVerified, identities); } catch {