using System.Runtime.CompilerServices; using System.Text; namespace Lskj.AgentPet.Host.Core.AstrBot; public sealed record SseFrame(string Event, string Data, string? Id); public static class SseParser { public const int MaxEventCharacters = 1024 * 1024; public static async IAsyncEnumerable ReadAsync( Stream source, [EnumeratorCancellation] CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(source); using StreamReader reader = new( source, new UTF8Encoding(false, true), detectEncodingFromByteOrderMarks: true, bufferSize: 4096, leaveOpen: true); string eventName = "message"; string? id = null; StringBuilder data = new(); while (true) { string? line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); if (line is null) { if (data.Length > 0) yield return new SseFrame(eventName, TrimFinalNewline(data), id); yield break; } if (line.Length == 0) { if (data.Length > 0) yield return new SseFrame(eventName, TrimFinalNewline(data), id); eventName = "message"; data.Clear(); continue; } if (line[0] == ':') continue; int separator = line.IndexOf(':'); string field = separator < 0 ? line : line[..separator]; string value = separator < 0 ? string.Empty : line[(separator + 1)..]; if (value.StartsWith(' ')) value = value[1..]; switch (field) { case "event": eventName = string.IsNullOrWhiteSpace(value) ? "message" : value; break; case "data": if (data.Length + value.Length + 1 > MaxEventCharacters) throw new HostError("astrbot_sse_event_too_large", "AstrBot SSE 单个事件超过 1 MB。 "); data.Append(value).Append('\n'); break; case "id": if (!value.Contains('\0')) id = value; break; } } } private static string TrimFinalNewline(StringBuilder value) { return value.Length > 0 && value[value.Length - 1] == '\n' ? value.ToString(0, value.Length - 1) : value.ToString(); } }