final
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
namespace WxAgent.Core;
|
||||
|
||||
public sealed record WechatDatabaseContact(string Username, string? NickName, string? Remark, string? AvatarUrl)
|
||||
{
|
||||
public string DisplayName => !string.IsNullOrWhiteSpace(Remark) ? Remark :
|
||||
!string.IsNullOrWhiteSpace(NickName) ? NickName : Username;
|
||||
}
|
||||
|
||||
public sealed record WechatContactPage(IReadOnlyList<WechatDatabaseContact> Contacts, int? NextOffset)
|
||||
{
|
||||
public bool HasMore => NextOffset is not null;
|
||||
}
|
||||
|
||||
public static class WechatContactQuery
|
||||
{
|
||||
// Stable IDs, not display names, define ordering and identity. No UI fallback.
|
||||
public const string Sql = """
|
||||
SELECT username, nick_name, remark, small_head_url FROM contact
|
||||
WHERE ($contains IS NULL OR instr(username, $contains) > 0
|
||||
OR instr(nick_name, $contains) > 0 OR instr(remark, $contains) > 0)
|
||||
AND ($groups IS NULL OR (substr(username, -9) = '@chatroom') = $groups)
|
||||
ORDER BY username COLLATE BINARY LIMIT $limit OFFSET $offset;
|
||||
""";
|
||||
|
||||
public static IReadOnlyList<KeyValuePair<string, object?>> Parameters(int limit, int offset, string? contains, bool? groupsOnly)
|
||||
{
|
||||
if (limit is < 1 or > 10000 || offset < 0 || offset > int.MaxValue - limit)
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Contact limit must be 1-10000 and offset must leave room for the next page.");
|
||||
return new KeyValuePair<string, object?>[]
|
||||
{
|
||||
new("$limit", limit + 1), new("$offset", offset), new("$contains", contains),
|
||||
new("$groups", groupsOnly is null ? null : groupsOnly.Value ? 1 : 0)
|
||||
};
|
||||
}
|
||||
|
||||
public static WechatContactPage Page(IReadOnlyList<WechatDatabaseContact> rows, int limit, int offset)
|
||||
{
|
||||
_ = Parameters(limit, offset, null, null);
|
||||
return new(rows.Take(limit).ToArray(), rows.Count > limit ? offset + limit : null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace WxAgent.Core;
|
||||
|
||||
public static class WechatDerivedText
|
||||
{
|
||||
// Only consume new text inside the bound message, never an entire window or an unbound OCR popup.
|
||||
public static string? AddedText(IReadOnlyList<string> before, IReadOnlyList<string> after)
|
||||
{
|
||||
var remaining = before.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.GroupBy(value => value, StringComparer.Ordinal).ToDictionary(group => group.Key, group => group.Count(), StringComparer.Ordinal);
|
||||
var added = new List<string>();
|
||||
foreach (var value in after.Where(value => !string.IsNullOrWhiteSpace(value)))
|
||||
{
|
||||
if (remaining.GetValueOrDefault(value) > 0) remaining[value]--;
|
||||
else added.Add(value);
|
||||
}
|
||||
if (remaining.Values.Any(count => count > 0) || added.Count == 0) return null;
|
||||
if (added.Any(value => value.Contains("识别中", StringComparison.Ordinal) || value.Contains("转换中", StringComparison.Ordinal)
|
||||
|| value.Contains("转换失败", StringComparison.Ordinal) || value.Contains("识别失败", StringComparison.Ordinal)
|
||||
|| value.Contains("未识别到", StringComparison.Ordinal) || value.Contains("无法转换", StringComparison.Ordinal))) return null;
|
||||
return string.Join(Environment.NewLine, added);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace WxAgent.Core;
|
||||
|
||||
public sealed record WechatDatabaseGroupMember(long MemberId, string Username, string DisplayName, bool IsOwner);
|
||||
|
||||
public sealed record WechatGroupMemberPage(string GroupUsername, IReadOnlyList<WechatDatabaseGroupMember> Members, int? NextOffset)
|
||||
{
|
||||
public bool HasMore => NextOffset is not null;
|
||||
}
|
||||
|
||||
public static class WechatGroupMemberQuery
|
||||
{
|
||||
public const string Sql = """
|
||||
SELECT m.member_id, c.username, c.nick_name, c.remark, cr.owner
|
||||
FROM chat_room AS cr
|
||||
JOIN chatroom_member AS m ON m.room_id = cr.id
|
||||
JOIN contact AS c ON c.id = m.member_id
|
||||
WHERE cr.username = $group
|
||||
ORDER BY m.member_id LIMIT $limit OFFSET $offset;
|
||||
""";
|
||||
|
||||
public static IReadOnlyList<KeyValuePair<string, object?>> Parameters(string groupUsername, int limit, int offset)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(groupUsername) || !groupUsername.EndsWith("@chatroom", StringComparison.Ordinal))
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "A stable @chatroom username is required.");
|
||||
if (limit is < 1 or > 10000 || offset < 0 || offset > int.MaxValue - limit)
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Member limit must be 1-10000 and offset must leave room for the next page.");
|
||||
return new KeyValuePair<string, object?>[]
|
||||
{
|
||||
new("$group", groupUsername), new("$limit", limit + 1), new("$offset", offset)
|
||||
};
|
||||
}
|
||||
|
||||
public static WechatGroupMemberPage Page(string groupUsername, IReadOnlyList<WechatDatabaseGroupMember> rows, int limit, int offset)
|
||||
{
|
||||
_ = Parameters(groupUsername, limit, offset);
|
||||
return new(groupUsername, rows.Take(limit).ToArray(), rows.Count > limit ? offset + limit : null);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ public sealed record WechatContactSnapshot(
|
||||
IReadOnlyList<string>? Tags = null,
|
||||
string? Signature = null,
|
||||
string? Source = null,
|
||||
int? CommonGroupCount = null);
|
||||
int? CommonGroupCount = null,
|
||||
string? Username = null);
|
||||
|
||||
public sealed record WechatNewFriendSnapshot(string DisplayName, string Status);
|
||||
|
||||
@@ -19,6 +20,9 @@ public sealed record WechatAccountSnapshot(string DisplayName, string? WechatId
|
||||
public sealed record WechatOperationResult(bool Success, WxAgentErrorCode? Code, string Message)
|
||||
{
|
||||
public static WechatOperationResult Ok(string message = "ok") => new(true, null, message);
|
||||
|
||||
public static WechatOperationResult Unconfirmed(string operation) => new(false, WxAgentErrorCode.ResultUnconfirmed,
|
||||
$"{operation} was submitted but its business result was not verified. Do not retry automatically.");
|
||||
}
|
||||
|
||||
public static class WechatOperationPolicy
|
||||
@@ -38,9 +42,20 @@ public static class WechatOperationPolicy
|
||||
public static void ValidateNames(IEnumerable<string> names, string parameterName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(names);
|
||||
if (!names.Any() || names.Any(string.IsNullOrWhiteSpace))
|
||||
var values = names.ToArray();
|
||||
if (values.Length == 0 || values.Any(string.IsNullOrWhiteSpace) ||
|
||||
values.Distinct(StringComparer.Ordinal).Count() != values.Length)
|
||||
{
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, $"{parameterName} must contain at least one non-empty name.");
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, $"{parameterName} must contain unique, non-empty names.");
|
||||
}
|
||||
}
|
||||
|
||||
public static T RequireUnique<T>(IEnumerable<T> candidates, string controlDescription)
|
||||
{
|
||||
var matches = candidates.Take(2).ToArray();
|
||||
if (matches.Length != 1)
|
||||
throw new WxAgentException(WxAgentErrorCode.ControlNotFound,
|
||||
$"A unique {controlDescription} was not found; no target was selected.");
|
||||
return matches[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ public enum WxAgentErrorCode
|
||||
DatabaseKeyNotFound,
|
||||
DatabaseOpenFailed,
|
||||
ClipboardUnavailable,
|
||||
InvalidArgument
|
||||
InvalidArgument,
|
||||
FeatureUnavailable
|
||||
}
|
||||
|
||||
public sealed class WxAgentException(WxAgentErrorCode code, string message, Exception? innerException = null)
|
||||
|
||||
Reference in New Issue
Block a user