fix: expand local session results and reject unsafe search targets
This commit is contained in:
@@ -27,7 +27,13 @@ public static class UiSnapshotSanitizer
|
||||
"tool_bar_accessible"
|
||||
};
|
||||
|
||||
public static string SanitizeName(string? value) => Sanitize(value, [], "text");
|
||||
private static readonly HashSet<string> SafeSearchLabels = new(StringComparer.Ordinal)
|
||||
{
|
||||
"联系人", "群聊", "功能", "聊天记录", "网络搜索", "搜一搜", "返回",
|
||||
"更多联系人", "更多群聊", "查看更多联系人", "查看更多群聊", "查看全部", "显示全部"
|
||||
};
|
||||
|
||||
public static string SanitizeName(string? value) => Sanitize(value, SafeSearchLabels, "text");
|
||||
|
||||
public static string SanitizeAutomationId(string? value) => Sanitize(value, SafeAutomationIds, "id");
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ public static class WechatLocators
|
||||
public const string ChatBubbleItem = "chat_message_list.qt_scrollarea_viewport.chat_bubble_item_view";
|
||||
public const string FileTransferAssistant = "文件传输助手";
|
||||
public const string Search = "搜索";
|
||||
public const string SearchList = "search_list";
|
||||
public const string Send = "发送";
|
||||
public const string FileTransferAssistantSearchResult = "search_item_function_文件传输助手";
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace WxAgent.Core;
|
||||
|
||||
public sealed record WechatSearchExpansion(string Category, int RowIndex, int ExpectedCount);
|
||||
public sealed record WechatSearchPage(
|
||||
IReadOnlyList<WechatSessionSearchResult> Results, IReadOnlyList<WechatSearchExpansion> Expansions);
|
||||
|
||||
public static class WechatSearchRules
|
||||
{
|
||||
private static readonly Regex More = new(@"^查看全部\s*[((](\d+)[))]$", RegexOptions.CultureInvariant);
|
||||
|
||||
public static bool IsLocalCategory(string? name) => name is "联系人" or "群聊" or "功能" or "最常使用";
|
||||
|
||||
public static bool IsExpansionLabel(string name) => More.IsMatch(name.Trim());
|
||||
|
||||
public static WechatSearchPage Parse(
|
||||
IReadOnlyList<(string AutomationId, string AccessibleName)> rows, string query, string? expandedCategory = null)
|
||||
{
|
||||
if (expandedCategory is not null && expandedCategory is not ("联系人" or "群聊"))
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Only local contacts and group chats may be expanded.");
|
||||
var results = new List<WechatSessionSearchResult>();
|
||||
var expansions = new List<WechatSearchExpansion>();
|
||||
var category = expandedCategory;
|
||||
for (var index = 0; index < rows.Count; index++)
|
||||
{
|
||||
var row = rows[index];
|
||||
var name = row.AccessibleName.Trim();
|
||||
if (!row.AutomationId.StartsWith("search_item_", StringComparison.Ordinal))
|
||||
{
|
||||
if (IsLocalCategory(name)) { category = name; continue; }
|
||||
var more = More.Match(name);
|
||||
if (more.Success && category is "联系人" or "群聊")
|
||||
{
|
||||
if (!int.TryParse(more.Groups[1].Value, out var count) || count is < 1 or > 10000)
|
||||
throw new WxAgentException(WxAgentErrorCode.UiStructureChanged, "Invalid local search result count.");
|
||||
expansions.Add(new WechatSearchExpansion(category, index, count));
|
||||
continue;
|
||||
}
|
||||
// An unrecognized section must not inherit local trust from an expanded category.
|
||||
category = null;
|
||||
continue;
|
||||
}
|
||||
if (category is null || row.AutomationId.StartsWith("search_item_web_", StringComparison.Ordinal)
|
||||
|| row.AutomationId.StartsWith("search_item_online_", StringComparison.Ordinal)) continue;
|
||||
var parsed = WechatSessionParser.ParseSearchResults([row], query).SingleOrDefault();
|
||||
if (parsed is null) continue;
|
||||
var lines = row.AccessibleName.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var idName = row.AutomationId["search_item_".Length..];
|
||||
var displayName = lines.Contains(idName, StringComparer.Ordinal) ? idName
|
||||
: parsed.Name == category ? lines.LastOrDefault(line => line != category) ?? string.Empty : parsed.Name;
|
||||
if (displayName.Length == 0 || (category == "功能" && displayName != WechatLocators.FileTransferAssistant)) continue;
|
||||
// The promoted local row uses the same name-based ID as contacts/groups.
|
||||
// Do not admit arbitrary function, online, or future result families under this mixed header.
|
||||
if (category == "最常使用" && row.AutomationId != "search_item_" + displayName
|
||||
&& row.AutomationId != WechatLocators.FileTransferAssistantSearchResult) continue;
|
||||
// Preserve duplicates: two same-named local conversations must be reported as ambiguous, not silently merged.
|
||||
results.Add(parsed with { Name = displayName, Category = category, IsExactMatch = string.Equals(displayName, query, StringComparison.Ordinal) });
|
||||
}
|
||||
return new WechatSearchPage(results, expansions);
|
||||
}
|
||||
|
||||
public static WechatSessionSearchResult RequireExact(IReadOnlyList<WechatSessionSearchResult> results, string name)
|
||||
{
|
||||
var matches = results.Where(result => IsLocalCategory(result.Category)
|
||||
&& string.Equals(result.Name, name, StringComparison.Ordinal)).ToArray();
|
||||
return matches.Length switch
|
||||
{
|
||||
1 => matches[0],
|
||||
0 => throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "The exact local conversation was not found."),
|
||||
_ => throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, "Multiple local conversations have the same name; refusing an ambiguous selection.")
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user