using System.Security.Cryptography; using System.Text; namespace WxAgent.Core; public enum ChatMessageType { Text, Image, File, Video, Voice, Link, Location, Emotion, Merge, PersonalCard, Note, Other, Quote, System } public sealed record QuotedMessageSnapshot(string? Sender, string Text); public sealed record ChatMessageSource(string Text, string? SourceId = null); public sealed record ChatMessageSnapshot( string Text, string Fingerprint, int VisibleIndex, ChatMessageType Type, QuotedMessageSnapshot? Quote = null); public static class VisibleMessageParser { public static IReadOnlyList Parse(IEnumerable accessibleNames) => Parse(accessibleNames.Select(text => new ChatMessageSource(text))); public static IReadOnlyList Parse(IEnumerable sources) { var occurrences = new Dictionary(StringComparer.Ordinal); var messages = new List(); foreach (var source in sources.Where(value => !string.IsNullOrWhiteSpace(value.Text))) { var text = source.Text; var occurrence = occurrences.TryGetValue(text, out var count) ? count : 0; occurrences[text] = occurrence + 1; var identity = $"{occurrence}:{text}"; var fingerprint = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(identity))).ToLowerInvariant(); var (replyText, quote) = ParseQuote(text); var type = quote is null ? Classify(replyText) : ChatMessageType.Quote; messages.Add(new ChatMessageSnapshot(replyText, fingerprint, messages.Count, type, quote)); } return messages; } public static (string Text, QuotedMessageSnapshot? Quote) ParseQuote(string accessibleName) { var normalized = accessibleName.ReplaceLineEndings("\n"); var lastLine = normalized.LastIndexOf('\n'); if (lastLine >= 0 && normalized[(lastLine + 1)..].StartsWith("引用 ", StringComparison.Ordinal)) { var descriptor = normalized[(lastLine + 1)..]; var messageMarker = descriptor.IndexOf(" 的消息", StringComparison.Ordinal); var separator = descriptor.IndexOfAny([':', ':']); if (messageMarker > 3 && separator > messageMarker) { var sender = descriptor[3..messageMarker].Trim(); var quoteText = descriptor[(separator + 1)..].Trim(); return (normalized[..lastLine], new QuotedMessageSnapshot(sender, quoteText)); } } var lines = accessibleName.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var quoteIndex = Array.FindIndex(lines, line => string.Equals(line, "引用", StringComparison.Ordinal) || line.StartsWith("引用:", StringComparison.Ordinal)); if (quoteIndex < 0 || lines.Length - quoteIndex < 3) { return (accessibleName, null); } var marker = lines[quoteIndex]; var legacySender = marker.Length > 3 ? marker[3..].Trim() : null; var legacyQuoteText = string.Join('\n', lines[(quoteIndex + 1)..^1]); return (lines[^1], new QuotedMessageSnapshot(string.IsNullOrWhiteSpace(legacySender) ? null : legacySender, legacyQuoteText)); } public static ChatMessageType Classify(string text) { if (text is "[图片]" or "图片" || text.StartsWith("图片\n", StringComparison.Ordinal)) { return ChatMessageType.Image; } if (text is "[视频]" or "视频" || text.StartsWith("视频\n", StringComparison.Ordinal)) { return ChatMessageType.Video; } if (text is "[语音]" or "语音" || text.StartsWith("语音\n", StringComparison.Ordinal)) { return ChatMessageType.Voice; } if (Uri.TryCreate(text, UriKind.Absolute, out var uri) && uri.Scheme is "http" or "https") { return ChatMessageType.Link; } if (text.StartsWith("文件\n", StringComparison.Ordinal)) { return ChatMessageType.File; } if (text is "[位置]" or "位置" || text.StartsWith("位置\n", StringComparison.Ordinal)) return ChatMessageType.Location; if (text is "[动画表情]" or "动画表情" or "表情") return ChatMessageType.Emotion; if (text.Contains("聊天记录", StringComparison.Ordinal)) return ChatMessageType.Merge; if (text is "[名片]" or "个人名片" || text.StartsWith("名片\n", StringComparison.Ordinal)) return ChatMessageType.PersonalCard; if (text is "[笔记]" or "笔记" || text.StartsWith("笔记\n", StringComparison.Ordinal)) return ChatMessageType.Note; if (!text.Contains(Path.DirectorySeparatorChar) && !text.Contains(Path.AltDirectorySeparatorChar) && Path.GetExtension(text) is { Length: > 1 } extension && extension.Length <= 10) { return ChatMessageType.File; } return text.StartsWith("[", StringComparison.Ordinal) && text.EndsWith("]", StringComparison.Ordinal) ? ChatMessageType.System : ChatMessageType.Text; } } public sealed class BoundedMessageDeduper { private readonly int _capacity; private readonly Queue _order = new(); private readonly HashSet _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); if (!_seen.Add(fingerprint)) { return false; } _order.Enqueue(fingerprint); if (_order.Count > _capacity) { _seen.Remove(_order.Dequeue()); } return true; } }