From a12ce6598bdaad1d4a8c66f6642400430046745e Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Dec 2025 02:53:47 -0800 Subject: [PATCH 1/4] [Core] Recursively preprocess entities in MultiMsg Previously, MultiMsgEntity only uploaded the outer container but failed to upload internal entities (like images), causing empty MsgInfo and -400 errors. This fix ensures all internal entities are recursively preprocessed using the parent message context. --- .../Message/Entities/MultiMsgEntity.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Lagrange.Core/Message/Entities/MultiMsgEntity.cs b/Lagrange.Core/Message/Entities/MultiMsgEntity.cs index 44be011c..d7aca564 100644 --- a/Lagrange.Core/Message/Entities/MultiMsgEntity.cs +++ b/Lagrange.Core/Message/Entities/MultiMsgEntity.cs @@ -26,9 +26,34 @@ public async Task Preprocess(BotContext context, BotMessage message) { if (string.IsNullOrEmpty(ResId)) { + // Recursively preprocess internal messages + Console.WriteLine($"[MultiMsgEntity] Preprocessing {Messages.Count} messages for forward chain..."); + + foreach (var innerMsg in Messages) + { + foreach (var entity in innerMsg.Entities) + { + try + { + await entity.Preprocess(context, message); + + if (entity is ImageEntity img) + { + if (img.MsgInfo != null) + Console.WriteLine($"[MultiMsgEntity] Image uploaded successfully. Size: {img.ImageSize}"); + else + Console.WriteLine("[MultiMsgEntity] WARNING: Image MsgInfo is NULL after preprocess!"); + } + } + catch (Exception ex) + { + Console.WriteLine($"[MultiMsgEntity] Error preprocessing entity: {ex.Message}"); + } + } + } + var result = await context.EventContext.SendEvent(new LongMsgSendEventReq(message.Receiver, Messages)); ResId = result.ResId; - } } From 776107921ccb81a2a5119d38e83ab3eb73251064 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 1 Dec 2025 03:14:16 -0800 Subject: [PATCH 2/4] [Proto] Correctly handle negative values in VarInt encoding This PR fixes two critical issues related to negative integer handling in VarInt encoding: 1. Fixed `IndexOutOfRangeException` in `ProtoHelper.GetVarIntLength`. - Previously, `uint.CreateSaturating` converted negative values to 0, causing `BitOperations.LeadingZeroCount(0)` to return 32, which exceeded the `VarIntLengths32` array bounds. - Changed to `CreateTruncating` to preserve the bit pattern. 2. Fixed packet corruption in `ProtoWriter.EncodeVarInt`. - The check `if (value < 0x80)` was performing a signed comparison. Negative values (e.g., -1) were treated as single-byte values, breaking the Protobuf MSB rule and corrupting the stream. - Fixed by casting to `ulong` for unsigned comparison. Added regression tests to verify correctness for negative integer serialization. --- Lagrange.Proto.Test/CrashTest.cs | 45 ++++++++++++++++++++++++ Lagrange.Proto/Primitives/ProtoWriter.cs | 3 +- Lagrange.Proto/Utility/ProtoHelper.cs | 6 ++-- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 Lagrange.Proto.Test/CrashTest.cs diff --git a/Lagrange.Proto.Test/CrashTest.cs b/Lagrange.Proto.Test/CrashTest.cs new file mode 100644 index 00000000..6b4c37f8 --- /dev/null +++ b/Lagrange.Proto.Test/CrashTest.cs @@ -0,0 +1,45 @@ +using NUnit.Framework; +using Lagrange.Proto.Utility; + +namespace Lagrange.Proto.Test +{ + [TestFixture] + public class CrashTest + { + [Test] + public void TestNegativeInt32VarIntLength() + { + // This test is used to reproduce the IndexOutOfRangeException Bug + int value = -1; + + int length = ProtoHelper.GetVarIntLength(value); + + // Verify: For a 32-bit all-ones number (0xFFFF), VarInt encoding should be 5 bytes + Assert.That(length, Is.EqualTo(5)); + } + + [Test] + public void TestNegativeInt64VarIntLength() + { + long value = -1; + + // For 64-bit numbers with all 1s, VarInt encoding should be 10 bytes + int length = ProtoHelper.GetVarIntLength(value); + + Assert.That(length, Is.EqualTo(10)); + } + + [Test] + public void TestOtherNegativeValues() + { + // Test other negative values to ensure stability + int val1 = -100; + int len1 = ProtoHelper.GetVarIntLength(val1); + Assert.That(len1, Is.GreaterThan(0)); + + long val2 = long.MinValue; // 0x8000000000000000 + int len2 = ProtoHelper.GetVarIntLength(val2); + Assert.That(len2, Is.EqualTo(10)); + } + } +} \ No newline at end of file diff --git a/Lagrange.Proto/Primitives/ProtoWriter.cs b/Lagrange.Proto/Primitives/ProtoWriter.cs index 55b5da29..a59946b2 100644 --- a/Lagrange.Proto/Primitives/ProtoWriter.cs +++ b/Lagrange.Proto/Primitives/ProtoWriter.cs @@ -96,7 +96,8 @@ public void EncodeVarInt(T value) where T : unmanaged, INumber { if (_memory.Length - BytesPending >= 10) { - if (value < T.CreateTruncating(0x80)) + // For-1 (int), converting to ulong is 0xFF... FF, much greater than 0x80, will not mistakenly enter this branch. + if (ulong.CreateTruncating(value) < 0x80) { Unsafe.Add(ref MemoryMarshal.GetReference(_memory.Span), BytesPending++) = byte.CreateTruncating(value); return; diff --git a/Lagrange.Proto/Utility/ProtoHelper.cs b/Lagrange.Proto/Utility/ProtoHelper.cs index 01517c05..399db314 100644 --- a/Lagrange.Proto/Utility/ProtoHelper.cs +++ b/Lagrange.Proto/Utility/ProtoHelper.cs @@ -36,12 +36,12 @@ public static unsafe int GetVarIntLength(T value) where T : unmanaged, INumbe if (sizeof(T) <= 4) { - int leadingZeros = BitOperations.LeadingZeroCount(uint.CreateSaturating(value)); + int leadingZeros = BitOperations.LeadingZeroCount(uint.CreateTruncating(value)); return VarIntLengths32[leadingZeros]; } else { - int leadingZeros = BitOperations.LeadingZeroCount(ulong.CreateSaturating(value)); + int leadingZeros = BitOperations.LeadingZeroCount(ulong.CreateTruncating(value)); return VarIntLengths64[leadingZeros]; } } @@ -98,4 +98,4 @@ public static int CountBytes(ReadOnlySpan str) { return GetVarIntLength(str.Length) + str.Length; } -} \ No newline at end of file +} From 3a49f3fbebb9a58ed802419614354562cf8a2204 Mon Sep 17 00:00:00 2001 From: citydirector <45933075+citydirector@users.noreply.github.com> Date: Mon, 29 Dec 2025 05:50:34 -0800 Subject: [PATCH 3/4] Add GitHub Actions workflow for upstream sync This workflow automatically syncs the forked repository with the upstream repository on a daily schedule and allows manual triggering. --- .github/workflows/sync-upstream.yml | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/sync-upstream.yml diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 00000000..9e9bc802 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,44 @@ +name: Upstream Sync + +permissions: + contents: write + +on: + schedule: + - cron: "0 0 * * *" # 每天 UTC 时间 0点 (北京时间早上8点) 自动执行 + workflow_dispatch: + +jobs: + sync_with_upstream: + name: Sync with Upstream + runs-on: ubuntu-latest + if: ${{ github.event.repository.fork }} # 仅在当前仓库是 Fork 的仓库时运行 + + steps: + - name: Checkout target branch + uses: actions/checkout@v3 + + - name: Sync Upstream + uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 + with: + + # 1. 设置上游仓库地址 (格式:用户名/仓库名) + upstream_sync_repo: LagrangeDev/LagrangeV2 + + # 2. 设置上游仓库的分支 (通常是 main 或 master) + upstream_sync_branch: main + + # 3. 设置您当前仓库需要更新的分支 + target_sync_branch: main + + # --- 配置结束 --- + + target_repo_token: ${{ secrets.GITHUB_TOKEN }} + test_mode: false + + - name: Check for Failure + if: failure() + run: | + echo "[Error] 由于存在冲突,自动同步失败。" + echo "请手动解决冲突:git pull upstream main 并在本地合并后推送。" + exit 1 From ba6f9be03a1503f4da7b88c9692950b037151d7c Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 8 Jan 2026 10:56:45 -0800 Subject: [PATCH 4/4] feat: implement auto-fetch latest sequence for GetHistoryMessages API when StartMessageSeq is null --- Lagrange.Core/Common/Entity/BotGroup.cs | 8 ++++- .../Services/System/FetchGroupsService.cs | 3 +- .../Message/GetHistoryMessagesHandler.cs | 36 ++++++++++++++++--- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/Lagrange.Core/Common/Entity/BotGroup.cs b/Lagrange.Core/Common/Entity/BotGroup.cs index 20ad0c3d..4af526b8 100644 --- a/Lagrange.Core/Common/Entity/BotGroup.cs +++ b/Lagrange.Core/Common/Entity/BotGroup.cs @@ -8,7 +8,8 @@ public class BotGroup( long createTime, string? description, string? question, - string? announcement) : BotContact + string? announcement, + uint lastestSeq = 0) : BotContact { public long GroupUin { get; } = groupUin; @@ -25,6 +26,11 @@ public class BotGroup( public string? Question { get; } = question; public string? Announcement { get; } = announcement; + + /// + /// The latest message sequence number for this group. + /// + public uint LastestSeq { get; } = lastestSeq; public override long Uin => GroupUin; diff --git a/Lagrange.Core/Internal/Services/System/FetchGroupsService.cs b/Lagrange.Core/Internal/Services/System/FetchGroupsService.cs index 8ff696f5..36a35c50 100644 --- a/Lagrange.Core/Internal/Services/System/FetchGroupsService.cs +++ b/Lagrange.Core/Internal/Services/System/FetchGroupsService.cs @@ -38,7 +38,8 @@ private protected override Task ProcessResponse(FetchGroup raw.Info.CreatedTime, raw.Info.Description, raw.Info.Question, - raw.Info.Announcement + raw.Info.Announcement, + raw.CustomInfo?.LastestSeq ?? 0 ))])); } } \ No newline at end of file diff --git a/Lagrange.Milky/Api/Handler/Message/GetHistoryMessagesHandler.cs b/Lagrange.Milky/Api/Handler/Message/GetHistoryMessagesHandler.cs index 5d64ced2..39eedc21 100644 --- a/Lagrange.Milky/Api/Handler/Message/GetHistoryMessagesHandler.cs +++ b/Lagrange.Milky/Api/Handler/Message/GetHistoryMessagesHandler.cs @@ -16,11 +16,39 @@ public class GetHistoryMessagesHandler(BotContext bot, EntityConvert convert) : public async Task HandleAsync(GetHistoryMessagesParameter parameter, CancellationToken token) { int start; - if (parameter.StartMessageSeq.HasValue) start = (int)(parameter.StartMessageSeq.Value - parameter.Limit); - // TODO: No start sequence - else throw new NotImplementedException(); + int end; - int end = start + parameter.Limit; + if (parameter.StartMessageSeq.HasValue) + { + start = (int)(parameter.StartMessageSeq.Value - parameter.Limit); + end = (int)parameter.StartMessageSeq.Value; + } + else + { + // No start sequence provided, try to get the latest sequence + switch (parameter.MessageScene) + { + case "group": + var groups = await _bot.FetchGroups(); + var group = groups.FirstOrDefault(g => g.GroupUin == parameter.PeerId) + ?? throw new ApiException(-1, $"Group {parameter.PeerId} not found"); + + if (group.LastestSeq == 0) + throw new ApiException(-1, $"Failed to get latest sequence for group {parameter.PeerId}"); + + end = (int)group.LastestSeq; + start = end - parameter.Limit; + break; + + case "friend": + throw new ApiException(-1, "Getting latest messages for friends without start_message_seq is not supported"); + + default: + throw new NotSupportedException($"Message scene '{parameter.MessageScene}' is not supported"); + } + } + + if (start < 0) start = 0; var messages = parameter.MessageScene switch {