fix: restore existing WeChat instance and add M6 validation tools

This commit is contained in:
2026-09-05 22:49:18 +08:00
parent 1bd0344e8b
commit 1e8e894dba
23 changed files with 1357 additions and 48 deletions
+10 -2
View File
@@ -130,11 +130,19 @@ public static class VisibleMessageParser
}
}
public sealed class BoundedMessageDeduper(int capacity = 2048)
public sealed class BoundedMessageDeduper
{
private readonly int _capacity;
private readonly Queue<string> _order = new();
private readonly HashSet<string> _seen = new(StringComparer.Ordinal);
public BoundedMessageDeduper(int capacity = 2048)
{
if (capacity <= 0)
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be positive.");
_capacity = capacity;
}
public bool TryAdd(string fingerprint)
{
ArgumentException.ThrowIfNullOrWhiteSpace(fingerprint);
@@ -144,7 +152,7 @@ public sealed class BoundedMessageDeduper(int capacity = 2048)
}
_order.Enqueue(fingerprint);
if (_order.Count > capacity)
if (_order.Count > _capacity)
{
_seen.Remove(_order.Dequeue());
}
+44
View File
@@ -0,0 +1,44 @@
namespace WxAgent.Core;
public sealed class UiTraversalGuard(int maxNodes = 10_000, int maxDepth = 40)
{
private readonly HashSet<string> _visited = new(StringComparer.Ordinal);
public int NodeCount { get; private set; }
public bool Truncated { get; private set; }
public bool CycleDetected { get; private set; }
public bool TryVisit(string runtimeId, int depth)
{
if (NodeCount >= maxNodes || depth > maxDepth)
{
Truncated = true;
return false;
}
if (runtimeId.Length > 0 && !_visited.Add(runtimeId))
{
CycleDetected = true;
return false;
}
NodeCount++;
return true;
}
}
public sealed record UiSnapshotComparison(IReadOnlyList<string> AddedAutomationIds,
IReadOnlyList<string> RemovedAutomationIds, IReadOnlyList<string> MissingRequiredControls)
{
public static UiSnapshotComparison Compare(UiNodeSnapshot baseline, UiNodeSnapshot current)
{
static IEnumerable<string> ReadIds(UiNodeSnapshot root) =>
new[] { root.AutomationId }.Concat(root.Children.SelectMany(ReadIds));
static HashSet<string> Ids(UiNodeSnapshot root) => ReadIds(root)
.Where(id => id.Length > 0 && !id.StartsWith("[id:", StringComparison.Ordinal))
.ToHashSet(StringComparer.Ordinal);
var before = Ids(baseline);
var after = Ids(current);
return new UiSnapshotComparison(
after.Except(before).Order(StringComparer.Ordinal).ToArray(),
before.Except(after).Order(StringComparer.Ordinal).ToArray(),
WechatLocators.RequiredAutomationIds.Where(id => !after.Contains(id)).ToArray());
}
}
+10
View File
@@ -3,6 +3,16 @@ namespace WxAgent.Core;
public static class WechatLocators
{
public const string MainView = "MainView";
public const string MainTabBar = "MainView.main_tabbar";
public static bool IsWechatTrayName(string name) =>
name is "微信" or "WeChat" or "Weixin"
|| name.StartsWith("微信\n", StringComparison.Ordinal)
|| name.StartsWith("微信\r", StringComparison.Ordinal);
public static bool IsNativeMainWindow(string className, string title) =>
title == "微信" && className.StartsWith("Qt", StringComparison.Ordinal)
&& className.EndsWith("QWindowIcon", StringComparison.Ordinal);
public const string SessionList = "session_list";
public const string ChatPage = "chat_message_page";
public const string MessageList = "chat_message_list";
+85
View File
@@ -0,0 +1,85 @@
using System.Text.Json;
namespace WxAgent.Core;
public sealed record WechatStabilitySample(
DateTimeOffset Timestamp,
int ProcessCount,
long WorkingSetBytes,
int HandleCount,
int ThreadCount,
bool WindowFound,
IReadOnlyList<WxAgentErrorCode> Errors,
long MessageEvents,
long ReconnectEvents,
long AgentWorkingSetBytes = 0,
int AgentHandleCount = 0,
int AgentThreadCount = 0,
DateTimeOffset? LastSnapshotAt = null);
public sealed record WechatStabilityReport(
DateTimeOffset StartedAt,
DateTimeOffset FinishedAt,
IReadOnlyList<WechatStabilitySample> Samples,
long MessageEvents,
long ReconnectEvents,
long WorkingSetGrowthBytes,
int HandleGrowth,
int ThreadGrowth,
bool Healthy,
IReadOnlyList<string> Findings,
double RequestedSeconds = 0,
double ObservedSeconds = 0,
bool CompletedDuration = false);
public static class WechatStabilityAnalyzer
{
public static WechatStabilityReport Analyze(
DateTimeOffset startedAt,
DateTimeOffset finishedAt,
IReadOnlyList<WechatStabilitySample> samples,
long messageEvents,
long reconnectEvents)
{
ArgumentNullException.ThrowIfNull(samples);
if (samples.Count == 0)
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "At least one stability sample is required.");
var first = samples[0];
var last = samples[^1];
var memoryGrowth = last.WorkingSetBytes - first.WorkingSetBytes;
var handleGrowth = last.HandleCount - first.HandleCount;
var threadGrowth = last.ThreadCount - first.ThreadCount;
var findings = new List<string>();
var recoveredWindowLoss = last.WindowFound && reconnectEvents > 0;
if (samples.Any(sample => !sample.WindowFound) && !recoveredWindowLoss) findings.Add("wechat-window-unavailable");
foreach (var error in last.Errors) findings.Add("final-error:" + error);
if (last.AgentWorkingSetBytes - first.AgentWorkingSetBytes > 128L * 1024 * 1024) findings.Add("agent-working-set-growth-over-128mb");
if (last.AgentHandleCount - first.AgentHandleCount > 100) findings.Add("agent-handle-growth-over-100");
if (last.AgentThreadCount - first.AgentThreadCount > 20) findings.Add("agent-thread-growth-over-20");
if (memoryGrowth > 256L * 1024 * 1024) findings.Add("working-set-growth-over-256mb");
if (handleGrowth > 500) findings.Add("handle-growth-over-500");
if (threadGrowth > 100) findings.Add("thread-growth-over-100");
if (samples.Any(sample => sample.LastSnapshotAt is { } lastSnapshot
&& sample.Timestamp - lastSnapshot > TimeSpan.FromMinutes(5))) findings.Add("listener-snapshot-gap-over-5min");
return new WechatStabilityReport(
startedAt,
finishedAt,
samples,
messageEvents,
reconnectEvents,
memoryGrowth,
handleGrowth,
threadGrowth,
findings.Count == 0,
findings);
}
public static async Task WriteJsonAsync(WechatStabilityReport report, string path, CancellationToken cancellationToken)
{
var fullPath = Path.GetFullPath(path);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
await File.WriteAllTextAsync(fullPath, JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true }), cancellationToken);
}
}