fix(bot): 修复手动同步无法发现新频道的问题

- BotUpdateHub: 添加 QuickCollectMyChatMemberAsync 方法
- 在 BotPoller 恢复时也收集 my_chat_member updates
- 解决 BotAutoSyncEnabled=false 时手动同步失效的问题

同时优化 .dockerignore:
- 排除 artifacts 目录 (7.2GB)
- 排除模块源码和文档目录

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
meoacgxandClaude committed 2026-01-02 13:50:32 +08:00
1 parent f5ba6be472
commit 30ce5f12cd
2 files changed
+97 -1

No files matched your search

+26 -1
View File
@@ -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
@@ -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);
}
/// <summary>
/// 快速收集从指定 offset 开始的 my_chat_member updates(不推进主循环 offset)。
/// 用于 Panel 恢复/手动同步时发现新加入的频道。
/// </summary>
private static async Task<List<JsonElement>?> QuickCollectMyChatMemberAsync(
string token,
long startOffset,
TelegramBotApiClient botApi,
ILogger logger,
CancellationToken cancellationToken)
{
var pending = new List<JsonElement>();
long offset = startOffset;
// 最多尝试 20 次(2000 条),避免无限循环
for (var i = 0; i < 20; i++)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var updates = await botApi.CallAsync(token, "getUpdates", new Dictionary<string, string?>
{
["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();