fix(bot-sync): 轮询前自动清理webhook并完善bot启停时webhook注册

This commit is contained in:
meoacgx committed 2026-03-04 20:47:26 +08:00
1 parent d1784d734b
commit 816d6b33a0
2 files changed
+80 -8

No files matched your search

@@ -26,6 +26,7 @@ public sealed class BotUpdateHub : IAsyncDisposable
private readonly SemaphoreSlim _gate = new(1, 1);
private readonly Dictionary<string, BotPoller> _pollersByToken = new(StringComparer.Ordinal);
private readonly HashSet<string> _pollingWebhookClearedTokens = new(StringComparer.Ordinal);
// Webhook 模式下的接收器(token -> receiver),不启动轮询
private readonly ConcurrentDictionary<string, BotWebhookReceiver> _webhookReceivers = new(StringComparer.Ordinal);
@@ -224,6 +225,10 @@ public sealed class BotUpdateHub : IAsyncDisposable
if (string.IsNullOrWhiteSpace(token))
throw new InvalidOperationException("Bot Token 为空");
// 重要:切回 Long Polling 时,Telegram 侧若残留 webhook,会导致 getUpdates 持续 409。
// 这里在首次订阅时兜底删一次 webhook(不丢 pending updates),避免“切模式后一直收不到更新”。
await EnsurePollingModeReadyAsync(token, cancellationToken);
if (!_pollersByToken.TryGetValue(token, out var poller))
{
poller = await BotPoller.CreateAsync(botId, token, bot.LastUpdateId, _scopeFactory, _botApi, _logger, cancellationToken);
@@ -248,6 +253,7 @@ public sealed class BotUpdateHub : IAsyncDisposable
{
pollers = _pollersByToken.Values.ToList();
_pollersByToken.Clear();
_pollingWebhookClearedTokens.Clear();
receivers = _webhookReceivers.Values.ToList();
_webhookReceivers.Clear();
@@ -270,6 +276,33 @@ public sealed class BotUpdateHub : IAsyncDisposable
}
}
private async Task EnsurePollingModeReadyAsync(string token, CancellationToken cancellationToken)
{
if (_pollingWebhookClearedTokens.Contains(token))
return;
try
{
await _botApi.DeleteWebhookAsync(token, dropPendingUpdates: false, cancellationToken);
_pollingWebhookClearedTokens.Add(token);
_logger.LogInformation("Polling mode ensured: webhook deleted for bot token {TokenHint}", MaskToken(token));
}
catch (Exception ex)
{
// 不中断订阅流程:即使删 webhook 失败,也允许继续尝试轮询;
// 下次订阅会再次尝试,便于网络抖动后的自恢复。
_logger.LogWarning(ex, "Failed to delete webhook before polling (token={TokenHint}), will retry later", MaskToken(token));
}
}
private static string MaskToken(string token)
{
token = (token ?? string.Empty).Trim();
if (token.Length <= 8)
return "***";
return $"{token[..4]}...{token[^4..]}";
}
public sealed class BotUpdateSubscription : IAsyncDisposable
{
private readonly Func<ValueTask> _dispose;
@@ -135,7 +135,11 @@
loading = true;
try
{
await BotManagement.CreateBotAsync(newName, newToken, newUsername);
var name = (newName ?? string.Empty).Trim();
var tokenToRegister = (newToken ?? string.Empty).Trim();
var username = string.IsNullOrWhiteSpace(newUsername) ? null : newUsername.Trim();
await BotManagement.CreateBotAsync(name, tokenToRegister, username);
await TryRegisterWebhookForBotAsync(tokenToRegister, showSuccessToast: false);
newName = "";
newToken = "";
newUsername = "";
@@ -192,6 +196,10 @@
{
await BotManagement.SetBotActiveStatusAsync(bot.Id, isActive);
bot.IsActive = isActive;
if (isActive)
await TryRegisterWebhookForBotAsync(bot.Token, showSuccessToast: false);
else
await TryDeleteWebhookForBotAsync(bot.Token, showSuccessToast: false);
Snackbar.Add(isActive ? "已启用 Bot" : "已停用 Bot", Severity.Success);
}
catch (Exception ex)
@@ -239,7 +247,7 @@
newToken: model.NewToken);
if (!string.IsNullOrWhiteSpace(model.NewToken))
await TryRegisterWebhookForBotAsync(model.NewToken);
await TryRegisterWebhookForBotAsync(model.NewToken, showSuccessToast: true);
await LoadBots();
Snackbar.Add("已保存", Severity.Success);
@@ -254,7 +262,7 @@
}
}
private async Task TryRegisterWebhookForBotAsync(string token)
private async Task TryRegisterWebhookForBotAsync(string token, bool showSuccessToast)
{
var webhookEnabled = string.Equals(
Configuration["Telegram:WebhookEnabled"]?.Trim(),
@@ -264,7 +272,11 @@
if (!webhookEnabled)
return;
var baseUrl = (Configuration["Telegram:WebhookBaseUrl"] ?? "").Trim();
var normalizedToken = (token ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(normalizedToken))
return;
var baseUrl = (Configuration["Telegram:WebhookBaseUrl"] ?? "").Trim().TrimEnd('/');
var secretToken = (Configuration["Telegram:WebhookSecretToken"] ?? "").Trim();
if (string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(secretToken))
@@ -275,20 +287,47 @@
try
{
var pathToken = WebhookTokenHelper.ToWebhookPathToken(token.Trim());
var webhookUrl = $"{baseUrl.TrimEnd('/')}/api/bot/webhook/{pathToken}";
var pathToken = WebhookTokenHelper.ToWebhookPathToken(normalizedToken);
var webhookUrl = $"{baseUrl}/api/bot/webhook/{pathToken}";
await BotApi.SetWebhookAsync(
token: token.Trim(),
token: normalizedToken,
url: webhookUrl,
secretToken: secretToken,
allowedUpdates: BotUpdateHub.AllowedUpdatesJson,
cancellationToken: CancellationToken.None);
Snackbar.Add("Webhook 已重新注册", Severity.Success);
if (showSuccessToast)
Snackbar.Add("Webhook 已重新注册", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"Token 已更换,但重新注册 Webhook 失败:{ex.Message}", Severity.Warning);
}
}
private async Task TryDeleteWebhookForBotAsync(string token, bool showSuccessToast)
{
var webhookEnabled = string.Equals(
Configuration["Telegram:WebhookEnabled"]?.Trim(),
"true",
StringComparison.OrdinalIgnoreCase);
if (!webhookEnabled)
return;
var normalizedToken = (token ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(normalizedToken))
return;
try
{
await BotApi.DeleteWebhookAsync(normalizedToken, dropPendingUpdates: false, cancellationToken: CancellationToken.None);
if (showSuccessToast)
Snackbar.Add("Webhook 已删除", Severity.Info);
}
catch (Exception ex)
{
Snackbar.Add($"停用 Bot 后删除 Webhook 失败:{ex.Message}", Severity.Warning);
}
}
}