This commit is contained in:
@@ -29,5 +29,15 @@ public static class AccountBindingMatcher
|
||||
: new AccountMatch(null, nicknameMatches.Length == 0 ? "none" : "nickname", false);
|
||||
}
|
||||
|
||||
public static bool IsUniquelyMatchedToAccount(
|
||||
string expectedAccountId,
|
||||
UiAccountIdentity boundIdentity,
|
||||
IReadOnlyList<DatabaseAccountIdentity> databaseAccounts)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(expectedAccountId)) return false;
|
||||
var match = Match(boundIdentity, databaseAccounts);
|
||||
return match.IsMatched && string.Equals(match.AccountId, expectedAccountId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,29 @@ public sealed class RemoteAccountContext
|
||||
get { lock (_gate) return _snapshot; }
|
||||
}
|
||||
|
||||
public static string? SelectActiveAccountId(
|
||||
bool uiSessionAvailable,
|
||||
string? preferredAccountId,
|
||||
IReadOnlyCollection<RemoteAccountIdentity> identities,
|
||||
IReadOnlyCollection<string> liveBoundAccountIds)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(identities);
|
||||
ArgumentNullException.ThrowIfNull(liveBoundAccountIds);
|
||||
if (!uiSessionAvailable) return null;
|
||||
|
||||
var liveBoundIds = liveBoundAccountIds.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var eligible = identities
|
||||
.Where(identity => identity.Verified && liveBoundIds.Contains(identity.AccountId))
|
||||
.ToArray();
|
||||
if (!string.IsNullOrWhiteSpace(preferredAccountId))
|
||||
{
|
||||
var preferred = eligible.FirstOrDefault(identity =>
|
||||
string.Equals(identity.AccountId, preferredAccountId, StringComparison.OrdinalIgnoreCase));
|
||||
if (preferred is not null) return preferred.AccountId;
|
||||
}
|
||||
return eligible.Length == 1 ? eligible[0].AccountId : null;
|
||||
}
|
||||
|
||||
public RemoteAccountContextSnapshot SwitchTo(
|
||||
string accountId,
|
||||
IReadOnlyCollection<RemoteAccountIdentity> identities,
|
||||
|
||||
@@ -98,6 +98,10 @@ public sealed class WindowsAgentBackend(AccountBindingStore bindings, ServiceOpt
|
||||
if (refreshUiIdentity)
|
||||
await AutoBindMatchesAsync(accounts, targets, cancellationToken);
|
||||
var current = bindings.ReadAll();
|
||||
var databaseIdentities = accounts
|
||||
.Where(account => account.Identity is not null)
|
||||
.Select(account => account.Identity!)
|
||||
.ToArray();
|
||||
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 &&
|
||||
@@ -110,8 +114,17 @@ public sealed class WindowsAgentBackend(AccountBindingStore bindings, ServiceOpt
|
||||
{
|
||||
var binding = bindings.Get(account.AccountId);
|
||||
var isLive = binding is not null && live.Contains(binding.AccountId);
|
||||
var isVerifiedForReadOnlySync = binding is not null &&
|
||||
string.Equals(binding.AccountId, account.AccountId, StringComparison.OrdinalIgnoreCase) &&
|
||||
AccountBindingMatcher.IsUniquelyMatchedToAccount(
|
||||
account.AccountId,
|
||||
new UiAccountIdentity(binding.WechatId, binding.Nickname),
|
||||
databaseIdentities);
|
||||
return new AccountInfo(account.AccountId, account.Identity?.Nickname, account.Identity?.WechatId, null,
|
||||
account.AccountId, isLive, binding, binding is null ? "Unbound" : isLive ? "Bound" : "Stale");
|
||||
account.AccountId, isLive, binding, binding is null ? "Unbound" : isLive ? "Bound" : "Stale")
|
||||
{
|
||||
IsVerifiedForReadOnlySync = isVerifiedForReadOnlySync
|
||||
};
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ public sealed record Page<T>(IReadOnlyList<T> Items, int Limit, int Offset, bool
|
||||
{
|
||||
public ReadCoverage? Coverage { get; init; }
|
||||
}
|
||||
public sealed record AccountInfo(string AccountId, string? DisplayName, string? WechatId, string? Region, string DataFingerprint, bool IsUiBindingKnown, AccountBinding? Binding = null, string BindingStatus = "Unbound");
|
||||
public sealed record AccountInfo(string AccountId, string? DisplayName, string? WechatId, string? Region, string DataFingerprint, bool IsUiBindingKnown, AccountBinding? Binding = null, string BindingStatus = "Unbound")
|
||||
{
|
||||
public bool IsVerifiedForReadOnlySync { get; init; }
|
||||
}
|
||||
public sealed record SessionInfo(string Name, string AutomationId, bool IsCurrent);
|
||||
public sealed record MessageInfo(string Fingerprint, string Type, string? Sender, string? Summary, string? Content);
|
||||
public sealed record ListRequest(int Limit = 50, int Offset = 0, bool IncludeContent = false, string? AccountId = null, string? Session = null);
|
||||
|
||||
@@ -880,23 +880,18 @@ public sealed class RemoteAgentHostedService(
|
||||
var wechatRunning = GetBoolean(element, "wechatAvailable");
|
||||
var sessionAvailable = GetBoolean(element, "sessionAvailable");
|
||||
var sessionLocked = GetBoolean(element, "sessionLocked");
|
||||
// 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.
|
||||
// A persisted binding plus a key-verified database identity may authorize read-only sync.
|
||||
// UI tasks still require a live, available WeChat session and a live window binding.
|
||||
var accounts = await backend.AccountsAsync(cancellationToken, refreshUiIdentity: false);
|
||||
var identities = accounts.Select(account => new RemoteAccountIdentity(
|
||||
account.AccountId, HasLiveWeChatBinding(account))).ToArray();
|
||||
var activeAccountId = remote.ActiveAccountId;
|
||||
var boundAccounts = identities
|
||||
.Where(identity => identity.Verified)
|
||||
.Select(identity => identity.AccountId)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
account.AccountId, HasLiveWeChatBinding(account) || account.IsVerifiedForReadOnlySync)).ToArray();
|
||||
var liveBoundAccountIds = accounts
|
||||
.Where(HasLiveWeChatBinding)
|
||||
.Select(account => account.AccountId)
|
||||
.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 hasLiveUiSession = wechatRunning && sessionAvailable && !sessionLocked;
|
||||
var activeAccountId = RemoteAccountContext.SelectActiveAccountId(
|
||||
hasLiveUiSession, remote.ActiveAccountId, identities, liveBoundAccountIds);
|
||||
var activeAccountVerified = false;
|
||||
if (activeAccountId is { Length: > 0 })
|
||||
{
|
||||
@@ -904,9 +899,15 @@ public sealed class RemoteAgentHostedService(
|
||||
{
|
||||
accountContext.SwitchTo(activeAccountId, identities);
|
||||
activeAccountVerified = accountContext.IsConfirmedFor(activeAccountId);
|
||||
if (!activeAccountVerified)
|
||||
{
|
||||
activeAccountId = null;
|
||||
accountContext.Invalidate();
|
||||
}
|
||||
}
|
||||
catch (WxAgentException)
|
||||
{
|
||||
activeAccountId = null;
|
||||
accountContext.Invalidate();
|
||||
}
|
||||
}
|
||||
@@ -923,7 +924,7 @@ public sealed class RemoteAgentHostedService(
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new BackendSnapshot(RemoteNodeStatus.Degraded, false, false, false, remote.ActiveAccountId, 0, false, []);
|
||||
return new BackendSnapshot(RemoteNodeStatus.Degraded, false, false, false, null, 0, false, []);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user