143 lines
5.0 KiB
C#
143 lines
5.0 KiB
C#
using System.Security.Cryptography;
|
||
using System.Text;
|
||
|
||
namespace WxAgent.Core;
|
||
|
||
public enum ChatMessageType
|
||
{
|
||
Text,
|
||
Image,
|
||
File,
|
||
Video,
|
||
Voice,
|
||
Link,
|
||
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<ChatMessageSnapshot> Parse(IEnumerable<string> accessibleNames) =>
|
||
Parse(accessibleNames.Select(text => new ChatMessageSource(text)));
|
||
|
||
public static IReadOnlyList<ChatMessageSnapshot> Parse(IEnumerable<ChatMessageSource> sources)
|
||
{
|
||
var occurrences = new Dictionary<string, int>(StringComparer.Ordinal);
|
||
var messages = new List<ChatMessageSnapshot>();
|
||
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 lines = accessibleName.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||
if (lines.Length >= 2 && lines[^1].StartsWith("引用 ", StringComparison.Ordinal))
|
||
{
|
||
var descriptor = lines[^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 (string.Join('\n', lines[..^1]), new QuotedMessageSnapshot(sender, quoteText));
|
||
}
|
||
}
|
||
|
||
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.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(int capacity = 2048)
|
||
{
|
||
private readonly Queue<string> _order = new();
|
||
private readonly HashSet<string> _seen = new(StringComparer.Ordinal);
|
||
|
||
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;
|
||
}
|
||
}
|