Initial import
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user