Files
wx-win-agent/node-agent/WxAgent.Core/ChatMessages.cs
T

165 lines
6.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,
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<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 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<string> _order = new();
private readonly HashSet<string> _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;
}
}