feat: add remote control plane and whitelist reads
Build web service image / build (push) Successful in 1m53s

This commit is contained in:
2026-09-12 09:46:05 +08:00
parent c86ba9c4d7
commit 13c31fc902
53 changed files with 9878 additions and 43 deletions
@@ -100,29 +100,40 @@ public static partial class WechatChatClient
public static Task<WechatAccountSnapshot> GetMyInfoAsync(CancellationToken cancellationToken = default) =>
WithMainWindowAsync(async (main, automation) =>
{
ClickProfileAvatar(main);
var desktop = automation.GetDesktop();
AutomationElement? profile = null;
for (var attempt = 0; attempt < 30 && profile is null; attempt++)
{
profile = desktop.FindAllChildren(cf => cf.ByControlType(ControlType.Window))
.FirstOrDefault(window => IsActionable(window)
&& string.Equals(SafeName(window), WechatLocators.ProfileWindowTitle, StringComparison.Ordinal)
&& FindByAutomationId(window, WechatLocators.ProfileDisplayName) is not null);
if (profile is null) await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
if (profile is null) throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Account profile window was not found.");
var settings = FindPreferenceWindow(desktop);
var closeSettings = settings is null;
settings ??= await OpenPreferenceWindowAsync(main, automation, cancellationToken).ConfigureAwait(false);
try
{
var displayName = SafeName(FindByAutomationId(profile, WechatLocators.ProfileDisplayName));
var wechatId = SafeName(FindByAutomationId(profile, WechatLocators.ProfileWechatId));
if (string.IsNullOrWhiteSpace(displayName) && string.IsNullOrWhiteSpace(wechatId))
throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Account identity was not found in the profile window.");
return new WechatAccountSnapshot(
string.IsNullOrWhiteSpace(displayName) ? wechatId : displayName,
string.IsNullOrWhiteSpace(wechatId) ? null : wechatId);
var accountLabel = settings.FindAllDescendants(cf => cf.ByControlType(ControlType.Text))
.FirstOrDefault(element => SafeName(element) == "账号");
if (accountLabel is null)
throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Account section was not found in WeChat settings.");
var labelBounds = accountLabel.BoundingRectangle;
var values = settings.FindAllDescendants(cf => cf.ByControlType(ControlType.Text))
.Select(element => (Name: SafeName(element), Bounds: element.BoundingRectangle))
.Where(value => !string.IsNullOrWhiteSpace(value.Name)
&& value.Bounds.Top >= labelBounds.Bottom
&& value.Bounds.Bottom <= labelBounds.Bottom + 90
&& value.Bounds.Left >= labelBounds.Left
&& value.Bounds.Right <= labelBounds.Right - 80)
.OrderBy(value => value.Bounds.Top)
.Select(value => value.Name)
.Distinct(StringComparer.Ordinal)
.ToArray();
if (values.Length < 2)
throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "Account identity was not found in WeChat settings.");
return new WechatAccountSnapshot(values[0], values[1]);
}
finally
{
if (closeSettings)
{
try { settings.AsWindow().Close(); } catch { }
}
}
finally { profile.AsWindow().Close(); }
}, cancellationToken);
/// <summary>Reads non-group contact rows, up to maxCount. Use the paged API for completeness and account selection.</summary>
@@ -581,18 +592,47 @@ public static partial class WechatChatClient
throw new WxAgentException(WxAgentErrorCode.ControlNotFound, $"Person checkbox '{name}' was not exposed.");
}
// ponytail: WeChat exposes the avatar as a nameless custom widget; use the first rail slot until it exposes a stable UIA locator.
private static void ClickProfileAvatar(AutomationElement main)
private static async Task<AutomationElement> OpenPreferenceWindowAsync(
AutomationElement main, UIA3Automation automation, CancellationToken cancellationToken)
{
var tabBar = FindByAutomationId(main, WechatLocators.MainTabBar)
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "WeChat navigation bar was not found.");
var bounds = tabBar.BoundingRectangle;
if (bounds.Width <= 0 || bounds.Height <= bounds.Width)
throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "WeChat navigation bar has no usable bounds.");
ExecuteInputStep("open-profile", () => Mouse.Click(
new System.Drawing.Point(bounds.Left + bounds.Width / 2, bounds.Top + bounds.Width), MouseButton.Left));
var desktop = automation.GetDesktop();
var menu = FindSettingsMenu(desktop);
if (menu is null)
{
var settingsButton = FindByAutomationId(main, WechatLocators.SettingsMenuButton)
?? throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "WeChat settings button was not found.");
ExecuteInputStep("open-settings-menu", () => settingsButton.Click());
}
var clickedSettings = false;
for (var attempt = 0; attempt < 40; attempt++)
{
cancellationToken.ThrowIfCancellationRequested();
var settings = FindPreferenceWindow(desktop);
if (settings is not null) return settings;
menu ??= FindSettingsMenu(desktop);
if (menu is not null && !clickedSettings)
{
ClickNamed(menu, "设置", ControlType.Button);
clickedSettings = true;
}
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "WeChat settings window was not found.");
}
private static AutomationElement? FindPreferenceWindow(AutomationElement desktop) =>
desktop.FindAllChildren(cf => cf.ByControlType(ControlType.Window))
.FirstOrDefault(window => IsActionable(window)
&& (SafeAutomationId(window) == WechatLocators.PreferenceWindow
|| SafeName(window) == "设置"));
private static AutomationElement? FindSettingsMenu(AutomationElement desktop) =>
desktop.FindAllChildren(cf => cf.ByControlType(ControlType.Window))
.FirstOrDefault(window => IsActionable(window)
&& window.FindAllDescendants(cf => cf.ByControlType(ControlType.Button))
.Any(button => IsActionable(button) && SafeName(button) == "设置"));
private static bool IsActionable(AutomationElement element) =>
!element.Properties.IsOffscreen.ValueOrDefault && element.Properties.IsEnabled.ValueOrDefault && !element.BoundingRectangle.IsEmpty;