diff --git a/.dockerignore b/.dockerignore index 41723e7..ec6e49a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,15 +1,40 @@ +# Git & IDE .git .serena -.tmp +.vs +.vscode +.idea + +# Build outputs **/bin **/obj +artifacts + +# Docker runtime data docker-data + +# Temporary & logs +.tmp logs sessions todolist + +# Database & cache *.db *.log *.user *.suo *.cache +# Documentation & screenshots (not needed for build) +docs +screenshot +README.md + +# Module source (development only) +模块源码 + +# Misc +*.md +!Directory.Build.props + diff --git a/src/TelegramPanel.Core/Services/Telegram/BotUpdateHub.cs b/src/TelegramPanel.Core/Services/Telegram/BotUpdateHub.cs index 0f463fe..f3566f4 100644 --- a/src/TelegramPanel.Core/Services/Telegram/BotUpdateHub.cs +++ b/src/TelegramPanel.Core/Services/Telegram/BotUpdateHub.cs @@ -189,6 +189,11 @@ public sealed class BotUpdateHub : IAsyncDisposable if (lastUpdateId.HasValue) { nextOffset = lastUpdateId.Value + 1; + + // 恢复时:从上次的 offset 开始收集 my_chat_member(用于发现新加入的频道)。 + // 这些 updates 稍后会被主循环重新拉取(幂等),但手动同步时可以立即使用缓存。 + pendingMyChatMember = await QuickCollectMyChatMemberAsync( + token, nextOffset, botApi, logger, cancellationToken); } else { @@ -201,6 +206,72 @@ public sealed class BotUpdateHub : IAsyncDisposable return new BotPoller(botId, token, nextOffset, pendingMyChatMember, scopeFactory, botApi, logger); } + /// + /// 快速收集从指定 offset 开始的 my_chat_member updates(不推进主循环 offset)。 + /// 用于 Panel 恢复/手动同步时发现新加入的频道。 + /// + private static async Task?> QuickCollectMyChatMemberAsync( + string token, + long startOffset, + TelegramBotApiClient botApi, + ILogger logger, + CancellationToken cancellationToken) + { + var pending = new List(); + long offset = startOffset; + + // 最多尝试 20 次(2000 条),避免无限循环 + for (var i = 0; i < 20; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var updates = await botApi.CallAsync(token, "getUpdates", new Dictionary + { + ["offset"] = offset.ToString(), + ["timeout"] = "0", // 快速返回,不长轮询 + ["limit"] = "100", + ["allowed_updates"] = AllowedMyChatMemberOnlyJson + }, cancellationToken); + + if (updates.ValueKind != JsonValueKind.Array) + break; + + long? maxUpdateId = null; + foreach (var u in updates.EnumerateArray()) + { + if (!TryGetUpdateId(u, out var id)) + continue; + + maxUpdateId = maxUpdateId.HasValue ? Math.Max(maxUpdateId.Value, id) : id; + + if (u.ValueKind == JsonValueKind.Object && u.TryGetProperty("my_chat_member", out _)) + { + pending.Add(u.Clone()); + if (pending.Count > PendingMyChatMemberMax) + pending.RemoveAt(0); + } + } + + if (!maxUpdateId.HasValue) + break; + + offset = maxUpdateId.Value + 1; + } + catch (Exception ex) + { + logger.LogWarning(ex, "QuickCollectMyChatMember failed at offset {Offset}", offset); + break; + } + } + + if (pending.Count > 0) + logger.LogInformation("QuickCollectMyChatMember collected {Count} my_chat_member updates", pending.Count); + + return pending.Count > 0 ? pending : null; + } + public BotUpdateSubscription Subscribe() { var id = Guid.NewGuid();