From bd1cd6f565bb4c1af7fb1ad3be8ecbc1d92336ff Mon Sep 17 00:00:00 2001 From: Lilyltt <1806552019@qq.com> Date: Thu, 11 Sep 2025 18:46:27 +0800 Subject: [PATCH 1/2] [Core] Add FlashTransfer Logic, Fix Aot and mac sending image issue Co-authored-by: sisi0318 --- Lagrange.Core/BotContext.cs | 2 + .../Internal/Context/FlashTransferContext.cs | 126 ++++++++++++++++++ .../Packets/Service/FlashTransferUploadReq.cs | 31 +++++ .../Service/FlashTransferUploadResp.cs | 10 ++ Lagrange.Core/Message/Entities/ImageEntity.cs | 12 +- 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 Lagrange.Core/Internal/Context/FlashTransferContext.cs create mode 100644 Lagrange.Core/Internal/Packets/Service/FlashTransferUploadReq.cs create mode 100644 Lagrange.Core/Internal/Packets/Service/FlashTransferUploadResp.cs diff --git a/Lagrange.Core/BotContext.cs b/Lagrange.Core/BotContext.cs index f702ff6f..c938e2c6 100644 --- a/Lagrange.Core/BotContext.cs +++ b/Lagrange.Core/BotContext.cs @@ -23,6 +23,7 @@ internal BotContext(BotConfig config, BotKeystore keystore, BotAppInfo appInfo) SocketContext = new SocketContext(this); EventContext = new EventContext(this); HighwayContext = new HighwayContext(this); + FlashTransferContext = new FlashTransferContext(this); } public BotConfig Config { get; } @@ -41,6 +42,7 @@ internal BotContext(BotConfig config, BotKeystore keystore, BotAppInfo appInfo) internal EventContext EventContext { get; } internal HighwayContext HighwayContext { get; } + internal FlashTransferContext FlashTransferContext { get; } #region Shortcut Methods diff --git a/Lagrange.Core/Internal/Context/FlashTransferContext.cs b/Lagrange.Core/Internal/Context/FlashTransferContext.cs new file mode 100644 index 00000000..ea6f56c3 --- /dev/null +++ b/Lagrange.Core/Internal/Context/FlashTransferContext.cs @@ -0,0 +1,126 @@ +using System.Security.Cryptography; +using Lagrange.Core.Internal.Packets.Service; +using Lagrange.Core.Utility; +using Lagrange.Core.Utility.Cryptography; + +namespace Lagrange.Core.Internal.Context; + +public class FlashTransferContext +{ + private const string Tag = nameof(FlashTransferContext); + private readonly BotContext _botContext; + private readonly HttpClient _client; + private readonly string? _url; + private const uint ChunkSize = 1024 * 1024; + + internal FlashTransferContext(BotContext botContext) + { + _botContext = botContext; + _client = new HttpClient(); + _client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip"); + _url = "https://multimedia.qfile.qq.com/sliceupload"; + } + + public async Task UploadFile(string uKey, Stream bodyStream) + { + byte[] body; + if (bodyStream is MemoryStream ms) + { + body = ms.ToArray(); + } + else + { + bodyStream.Position = 0; + var buffer = new byte[bodyStream.Length]; + await bodyStream.ReadExactlyAsync(buffer, 0, buffer.Length); + body = buffer; + } + + return await UploadFile(uKey, body); + } + + public async Task UploadFile(string uKey, byte[] body) + { + var chunkSha1S = new FlashTransferSha1StateV { State = [] }; + var chunkBuffers = new List(); + var chunkCount = (uint)((body.Length + ChunkSize - 1) / ChunkSize); + + using var accStream = new MemoryStream(); + for (uint i = 0; i < chunkCount; i++) + { + var chunkBuffer = body.AsSpan((int)(i * ChunkSize), (int)Math.Min(ChunkSize, body.Length - i * ChunkSize)) + .ToArray(); + chunkBuffers.Add(chunkBuffer); + + if (i != chunkCount - 1) + { + accStream.Write(chunkBuffer, 0, chunkBuffer.Length); + var accBytes = accStream.ToArray(); + var sha1Stream = new Sha1Stream(); + var digest = new byte[20]; + sha1Stream.Update(accBytes); + sha1Stream.Hash(digest, false); + chunkSha1S.State.Add(digest); + } + else + { + chunkSha1S.State.Add(SHA1.HashData(body)); + } + } + + // cnm闹禅tx为什么不能并发,回答我 + foreach (var chunkBuffer in chunkBuffers) + { + var success = await UploadChunk(uKey, (uint)(chunkBuffers.IndexOf(chunkBuffer) * ChunkSize), chunkSha1S, chunkBuffer); + if (!success) + { + return false; + } + } + + return true; + } + + private async Task UploadChunk(string uKey, uint start, FlashTransferSha1StateV chunkSha1S, byte[] body) + { + var req = new FlashTransferUploadReq + { + FieId1 = 0, + AppId = 1407, + FileId3 = 2, + Body = new FlashTransferUploadBody + { + FieId1 = [], + UKey = uKey, + Start = start, + End = (uint)(start + body.Length - 1), + Sha1 = SHA1.HashData(body), + Sha1StateV = chunkSha1S, + Body = body + } + }; + var payload = ProtoHelper.Serialize(req).ToArray(); + var request = new HttpRequestMessage(HttpMethod.Post, _url) + { + Headers = + { + { "Accept", "*/*" }, + { "Expect", "100-continue" }, + { "Connection", "Keep-Alive" } + }, + Content = new ByteArrayContent(payload) + }; + var response = await _client.SendAsync(request); + var responseBytes = await response.Content.ReadAsByteArrayAsync(); + var resp = ProtoHelper.Deserialize(responseBytes); + + if (resp.Status != "success") + { + _botContext.LogError(Tag, + $"FlashTransfer Upload chunk {start} failed: {resp.Status}"); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadReq.cs b/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadReq.cs new file mode 100644 index 00000000..f44213e1 --- /dev/null +++ b/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadReq.cs @@ -0,0 +1,31 @@ +using Lagrange.Proto; + +#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 'required' 修饰符或声明为可以为 null。 +namespace Lagrange.Core.Internal.Packets.Service; + +[ProtoPackable] +internal partial class FlashTransferUploadReq +{ + [ProtoMember(1)] public uint FieId1 { get; set; } // 0 + [ProtoMember(2)] public uint AppId { get; set; } // 1407 + [ProtoMember(3)] public uint FileId3 { get; set; } // 0 + [ProtoMember(107)] public FlashTransferUploadBody Body { get; set; } +} + +[ProtoPackable] +internal partial class FlashTransferUploadBody +{ + [ProtoMember(1)] public byte[] FieId1 { get; set; } // Empty + [ProtoMember(2)] public string UKey { get; set; } + [ProtoMember(3)] public uint Start { get; set; } // Start + [ProtoMember(4)] public uint End { get; set; } // Start + Size - 1 + [ProtoMember(5)] public byte[] Sha1 { get; set; } + [ProtoMember(6)] public FlashTransferSha1StateV Sha1StateV { get; set; } + [ProtoMember(7)] public byte[] Body { get; set; } +} + +[ProtoPackable] +internal partial class FlashTransferSha1StateV +{ + [ProtoMember(1)] public List State { get; set; } +} \ No newline at end of file diff --git a/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadResp.cs b/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadResp.cs new file mode 100644 index 00000000..f576555f --- /dev/null +++ b/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadResp.cs @@ -0,0 +1,10 @@ +using Lagrange.Proto; +#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 'required' 修饰符或声明为可以为 null。 + +namespace Lagrange.Core.Internal.Packets.Service; + +[ProtoPackable] +internal partial class FlashTransferUploadResp +{ + [ProtoMember(5)] public string Status { get; set; } +} \ No newline at end of file diff --git a/Lagrange.Core/Message/Entities/ImageEntity.cs b/Lagrange.Core/Message/Entities/ImageEntity.cs index 3af59c02..9d830634 100644 --- a/Lagrange.Core/Message/Entities/ImageEntity.cs +++ b/Lagrange.Core/Message/Entities/ImageEntity.cs @@ -1,4 +1,6 @@ using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Lagrange.Core.Internal.Events.Message; using Lagrange.Core.Internal.Packets.Message; using Lagrange.Core.Internal.Packets.Service; @@ -43,7 +45,15 @@ public override async Task Preprocess(BotContext context, BotMessage message) if (result.Ext != null) { - await context.HighwayContext.UploadFile(Stream.Value, message.IsGroup() ? 1004 : 1003, ProtoHelper.Serialize(result.Ext)); + // Aot 和 MacOS 下使用 FlashTransfer 上传 + if (RuntimeFeature.IsDynamicCodeCompiled && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + await context.HighwayContext.UploadFile(Stream.Value, message.IsGroup() ? 1004 : 1003, ProtoHelper.Serialize(result.Ext)); + } + else + { + await context.FlashTransferContext.UploadFile(result.Ext.UKey, Stream.Value); + } } } finally From fdd2de67bf6fa4583ae12d65cf3145001097e482 Mon Sep 17 00:00:00 2001 From: Lilyltt <1806552019@qq.com> Date: Thu, 11 Sep 2025 19:42:24 +0800 Subject: [PATCH 2/2] [Core] FlashTransferUpload Perf Optimize --- .../Internal/Context/FlashTransferContext.cs | 65 ++++++++----------- .../Utility/Cryptography/Sha1Stream.cs | 2 +- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/Lagrange.Core/Internal/Context/FlashTransferContext.cs b/Lagrange.Core/Internal/Context/FlashTransferContext.cs index ea6f56c3..d5124819 100644 --- a/Lagrange.Core/Internal/Context/FlashTransferContext.cs +++ b/Lagrange.Core/Internal/Context/FlashTransferContext.cs @@ -2,6 +2,7 @@ using Lagrange.Core.Internal.Packets.Service; using Lagrange.Core.Utility; using Lagrange.Core.Utility.Cryptography; +using Lagrange.Core.Utility.Extension; namespace Lagrange.Core.Internal.Context; @@ -23,59 +24,45 @@ internal FlashTransferContext(BotContext botContext) public async Task UploadFile(string uKey, Stream bodyStream) { - byte[] body; - if (bodyStream is MemoryStream ms) - { - body = ms.ToArray(); - } - else - { - bodyStream.Position = 0; - var buffer = new byte[bodyStream.Length]; - await bodyStream.ReadExactlyAsync(buffer, 0, buffer.Length); - body = buffer; - } - - return await UploadFile(uKey, body); - } - - public async Task UploadFile(string uKey, byte[] body) - { - var chunkSha1S = new FlashTransferSha1StateV { State = [] }; - var chunkBuffers = new List(); - var chunkCount = (uint)((body.Length + ChunkSize - 1) / ChunkSize); + var sha1StateVs = new FlashTransferSha1StateV { State = [] }; + var chunkCount = (uint)((bodyStream.Length + ChunkSize - 1) / ChunkSize); - using var accStream = new MemoryStream(); + var sha1Stream = new Sha1Stream(); for (uint i = 0; i < chunkCount; i++) { - var chunkBuffer = body.AsSpan((int)(i * ChunkSize), (int)Math.Min(ChunkSize, body.Length - i * ChunkSize)) - .ToArray(); - chunkBuffers.Add(chunkBuffer); - if (i != chunkCount - 1) { - accStream.Write(chunkBuffer, 0, chunkBuffer.Length); - var accBytes = accStream.ToArray(); - var sha1Stream = new Sha1Stream(); + var accLength = (int)((i + 1) * ChunkSize); + var accBuffer = new byte[accLength]; + + bodyStream.Position = 0; + await bodyStream.ReadExactlyAsync(accBuffer, 0, accLength); + + var accSpan = accBuffer.AsSpan(); var digest = new byte[20]; - sha1Stream.Update(accBytes); + sha1Stream.Update(accSpan); sha1Stream.Hash(digest, false); - chunkSha1S.State.Add(digest); + sha1Stream.Reset(); + sha1StateVs.State.Add(digest.ToArray()); } else { - chunkSha1S.State.Add(SHA1.HashData(body)); + bodyStream.Position = 0; + sha1StateVs.State.Add(bodyStream.Sha1()); } } - // cnm闹禅tx为什么不能并发,回答我 - foreach (var chunkBuffer in chunkBuffers) + for (uint i = 0; i < chunkCount; i++) { - var success = await UploadChunk(uKey, (uint)(chunkBuffers.IndexOf(chunkBuffer) * ChunkSize), chunkSha1S, chunkBuffer); - if (!success) - { - return false; - } + var chunkStart = (long)(i * ChunkSize); + var chunkLength = (int)Math.Min(ChunkSize, bodyStream.Length - chunkStart); + + bodyStream.Position = chunkStart; + var uploadBuffer = new byte[chunkLength]; + await bodyStream.ReadExactlyAsync(uploadBuffer, 0, chunkLength); + + var success = await UploadChunk(uKey, (uint)chunkStart, sha1StateVs, uploadBuffer); + if (!success) return false; } return true; diff --git a/Lagrange.Core/Utility/Cryptography/Sha1Stream.cs b/Lagrange.Core/Utility/Cryptography/Sha1Stream.cs index 7711754c..2bcd7fbe 100644 --- a/Lagrange.Core/Utility/Cryptography/Sha1Stream.cs +++ b/Lagrange.Core/Utility/Cryptography/Sha1Stream.cs @@ -30,7 +30,7 @@ public Sha1Stream() // Initialize SHA1 context Reset(); } - private void Reset() + public void Reset() { _state[0] = 0x67452301; _state[1] = 0xEFCDAB89;