Files
wx-win-agent/src/WxAgent.Core/ChatMessages.cs
T
2026-09-04 21:46:38 +08:00

143 lines
5.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}