fix(bot-sync): 轮询前自动清理webhook并完善bot启停时webhook注册
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user