using ShiroBot.SDK.Models; using ShiroBot.SDK.Plugin; using ShiroBot.Model.QQ; using MyAgent; using MyAgent.Core; using MyAgent.Tools; using Channel = ShiroBot.SDK.Models.Channel; namespace Shirobot.Plugin.MyAgent; internal sealed class ShirobotGroupPlatform(IBotContext context) : IGroupPlatform { private static readonly HttpClient DownloadClient = new() { Timeout = TimeSpan.FromSeconds(20) }; public async Task SendAsync( long groupId, string text, IReadOnlyList attachments, string? replyToMessageId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); using var scope = context.UsePlatform("qq"); var segments = new List(); if (!string.IsNullOrWhiteSpace(replyToMessageId)) segments.Add(new QuoteSegment(replyToMessageId)); if (!string.IsNullOrWhiteSpace(text)) segments.Add(new TextSegment(text)); foreach (var attachment in attachments) { var uri = !string.IsNullOrWhiteSpace(attachment.LocalPath) && File.Exists(attachment.LocalPath) ? "base64://" + Convert.ToBase64String(await File.ReadAllBytesAsync(attachment.LocalPath, cancellationToken)) : attachment.SourceUrl; if (!string.IsNullOrWhiteSpace(uri)) segments.Add(new ImageSegment(uri) { Summary = "[图片]" }); } if (segments.Count == 0) return null; var sent = await context.Message.SendMessageAsync(Channel.Group(groupId.ToString()), segments); return new MessageSendResult { Ok = true, MessageId = sent.MessageId, Timestamp = sent.Timestamp }; } public async Task> GetHistoryAsync(long groupId, int limit, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); using var scope = context.UsePlatform("qq"); var messages = await context.Message.GetHistoryMessagesAsync(Channel.Group(groupId.ToString()), limit: Math.Clamp(limit, 1, 100)); return messages.Where(message => message.Sender.Id != message.SelfId) .Select(message => GroupChatEvent.User(groupId, message.Sender.Id, message.Member?.Nick ?? message.Sender.Name ?? message.Sender.Id, message.GetPlainText(), message.Timestamp, ToAttachments(message.Segments), message.MessageId)) .ToArray(); } public async Task GetProfileAsync(long groupId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); using var scope = context.UsePlatform("qq"); var channel = await context.Channel.GetChannelAsync(groupId.ToString()); var members = await context.Channel.GetMembersAsync(groupId.ToString()); return new GroupProfileResult { GroupId = groupId, GroupName = channel?.Name ?? groupId.ToString(), AvatarUrl = $"https://p.qlogo.cn/gh/{groupId}/{groupId}/100", MemberCount = members.Count, Members = members.Select(member => new GroupMemberInfo { UserId = long.TryParse(member.User.Id, out var id) ? id : 0, Name = member.Nick ?? member.User.Name ?? member.User.Id, Role = member.Role.ToString().ToLowerInvariant(), AvatarUrl = $"https://q1.qlogo.cn/g?b=qq&nk={member.User.Id}&s=100" }).ToList() }; } internal static ChatAttachment[] ToAttachments(IReadOnlyList segments) => segments .OfType() .Select(segment => new ChatAttachment { Type = segment switch { ImageSegment => "image", AudioSegment => "audio", VideoSegment => "video", FileSegment => "file", _ => "resource" }, Url = segment.Uri, SourceUrl = segment.Uri, Title = segment.FileName ?? "", Size = segment is FileSegment file ? file.FileSize ?? 0 : 0 }).ToArray(); internal async Task> ToAttachmentsAsync( IReadOnlyList segments, long groupId, DocumentTextExtractor documents, CancellationToken cancellationToken) { using var scope = context.UsePlatform("qq"); var attachments = new List(); foreach (var segment in segments.OfType()) { cancellationToken.ThrowIfCancellationRequested(); var url = segment.Uri; if (string.IsNullOrWhiteSpace(url) && !string.IsNullOrWhiteSpace(segment.ResourceId)) { if (segment is FileSegment) url = await (context.GetAdapterExtension()?.GetGroupFileDownloadUrlAsync(groupId, segment.ResourceId) ?? Task.FromResult("")); else url = await context.Message.GetResourceUrlAsync(segment.ResourceId); } var attachment = new ChatAttachment { Type = segment switch { ImageSegment => "image", AudioSegment => "audio", VideoSegment => "video", FileSegment => "file", _ => "resource" }, Url = url, SourceUrl = url, Title = segment.FileName ?? "", Size = segment is FileSegment file ? file.FileSize ?? 0 : 0 }; if (segment is FileSegment && Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Scheme is "http" or "https") { try { var cache = Path.Combine(context.PluginDirectory, "data", "attachments"); Directory.CreateDirectory(cache); var suffix = Path.GetExtension(attachment.Title); var path = Path.Combine(cache, Guid.NewGuid().ToString("n") + suffix); using var response = await DownloadClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); response.EnsureSuccessStatusCode(); if (response.Content.Headers.ContentLength is > 15_000_000) throw new InvalidDataException("Attachment exceeds 15 MB"); await using (var input = await response.Content.ReadAsStreamAsync(cancellationToken)) await using (var output = File.Create(path)) { var buffer = new byte[81920]; long total = 0; int read; while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0) { total += read; if (total > 15_000_000) throw new InvalidDataException("Attachment exceeds 15 MB"); await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken); } } attachment.LocalPath = path; attachment.TextContent = await documents.TryExtractAsync(path, attachment.Title, response.Content.Headers.ContentType?.MediaType ?? "", cancellationToken); } catch (Exception ex) when (ex is not OperationCanceledException) { ShiroBot.SDK.Abstractions.BotLog.Warning($"Could not read group attachment: {ex.Message}"); } } attachments.Add(attachment); } return attachments; } }