diff --git a/src/TelegramPanel.Core/Services/BotManagementService.cs b/src/TelegramPanel.Core/Services/BotManagementService.cs index 499ab28..316621f 100644 --- a/src/TelegramPanel.Core/Services/BotManagementService.cs +++ b/src/TelegramPanel.Core/Services/BotManagementService.cs @@ -73,6 +73,55 @@ public class BotManagementService await _botRepository.UpdateAsync(bot); } + public async Task UpdateBotProfileAsync(int botId, string name, string? username, string? newToken) + { + name = (name ?? string.Empty).Trim(); + username = string.IsNullOrWhiteSpace(username) ? null : username.Trim().TrimStart('@'); + newToken = string.IsNullOrWhiteSpace(newToken) ? null : newToken.Trim(); + + if (botId <= 0) + throw new ArgumentException("BotId 无效", nameof(botId)); + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("机器人名称不能为空", nameof(name)); + + var bot = await _botRepository.GetByIdAsync(botId) + ?? throw new InvalidOperationException($"机器人不存在:{botId}"); + + if (!string.Equals(bot.Name, name, StringComparison.Ordinal)) + { + var existing = await _botRepository.GetByNameAsync(name); + if (existing != null && existing.Id != botId) + throw new InvalidOperationException("机器人名称已存在"); + } + + bot.Name = name; + bot.Username = username; + if (newToken != null) + bot.Token = newToken; + + try + { + await _botRepository.UpdateAsync(bot); + } + catch (DbUpdateException ex) + { + var msg = GetInnermostMessage(ex); + if (msg.Contains("UNIQUE constraint failed: Bots.Name", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("机器人名称已存在"); + + if (msg.Contains("database is locked", StringComparison.OrdinalIgnoreCase) + || msg.Contains("database is busy", StringComparison.OrdinalIgnoreCase) + || msg.Contains("SQLite Error 5", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("数据库正被占用(SQLite database is locked),请稍后重试;若云端部署了多个实例共享同一个 sqlite 文件,请改为单实例或换用 MySQL/PostgreSQL。"); + + if (msg.Contains("no such table: Bots", StringComparison.OrdinalIgnoreCase) + || msg.Contains("no such column", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException($"数据库结构过旧或未完成迁移,请重启主程序以自动迁移数据库。详情:{msg}"); + + throw new InvalidOperationException($"更新机器人失败:{msg}", ex); + } + } + public async Task SetBotActiveStatusAsync(int botId, bool isActive) { var bot = await _botRepository.GetByIdAsync(botId); diff --git a/src/TelegramPanel.Web/Components/Dialogs/EditBotDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/EditBotDialog.razor new file mode 100644 index 0000000..61b3a71 --- /dev/null +++ b/src/TelegramPanel.Web/Components/Dialogs/EditBotDialog.razor @@ -0,0 +1,71 @@ +@namespace TelegramPanel.Web.Components.Dialogs +@using TelegramPanel.Data.Entities + + + + + 编辑机器人 + + + + + + 不展示当前 Token;如需更换请输入新 Token(留空则不修改)。 + + + + + + 取消 + + 保存 + + + + +@code +{ + [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!; + + [Parameter] public Bot Bot { get; set; } = default!; + + private string name = ""; + private string? username; + private string? newToken; + private bool showToken; + + protected override void OnParametersSet() + { + name = (Bot?.Name ?? "").Trim(); + username = string.IsNullOrWhiteSpace(Bot?.Username) ? null : Bot!.Username!.Trim().TrimStart('@'); + newToken = ""; + showToken = false; + } + + private void ToggleTokenVisibility() + { + showToken = !showToken; + } + + private void Cancel() + { + MudDialog.Cancel(); + } + + private void Confirm() + { + var model = new EditBotModel( + Name: name.Trim(), + Username: string.IsNullOrWhiteSpace(username) ? null : username!.Trim().TrimStart('@'), + NewToken: string.IsNullOrWhiteSpace(newToken) ? null : newToken!.Trim()); + + MudDialog.Close(DialogResult.Ok(model)); + } + + public sealed record EditBotModel(string Name, string? Username, string? NewToken); +} + diff --git a/src/TelegramPanel.Web/Components/Pages/Bots.razor b/src/TelegramPanel.Web/Components/Pages/Bots.razor index 989f96e..85ab4b5 100644 --- a/src/TelegramPanel.Web/Components/Pages/Bots.razor +++ b/src/TelegramPanel.Web/Components/Pages/Bots.razor @@ -1,9 +1,12 @@ @page "/bots" @inject BotManagementService BotManagement +@inject TelegramBotApiClient BotApi +@inject IConfiguration Configuration @inject ISnackbar Snackbar @inject IDialogService DialogService @inject NavigationManager Navigation @using TelegramPanel.Data.Entities +@using TelegramPanel.Web.Components.Dialogs 机器人管理 - Telegram Panel @@ -63,6 +66,10 @@ + + 编辑 + 频道列表 @@ -196,4 +203,91 @@ togglingBotIds.Remove(bot.Id); } } + + private async Task EditBotAsync(Bot bot) + { + if (loading) + return; + + var parameters = new DialogParameters + { + ["Bot"] = bot + }; + + var options = new DialogOptions + { + CloseButton = true, + MaxWidth = MaxWidth.Small, + FullWidth = true + }; + + var dialog = DialogService.Show("编辑机器人", parameters, options)!; + var result = await dialog.Result; + if (result is null || result.Canceled) + return; + + if (result.Data is not EditBotDialog.EditBotModel model) + return; + + loading = true; + try + { + await BotManagement.UpdateBotProfileAsync( + botId: bot.Id, + name: model.Name, + username: model.Username, + newToken: model.NewToken); + + if (!string.IsNullOrWhiteSpace(model.NewToken)) + await TryRegisterWebhookForBotAsync(model.NewToken); + + await LoadBots(); + Snackbar.Add("已保存", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"保存失败:{ex.Message}", Severity.Error); + } + finally + { + loading = false; + } + } + + private async Task TryRegisterWebhookForBotAsync(string token) + { + var webhookEnabled = string.Equals( + Configuration["Telegram:WebhookEnabled"]?.Trim(), + "true", + StringComparison.OrdinalIgnoreCase); + + if (!webhookEnabled) + return; + + var baseUrl = (Configuration["Telegram:WebhookBaseUrl"] ?? "").Trim(); + var secretToken = (Configuration["Telegram:WebhookSecretToken"] ?? "").Trim(); + + if (string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(secretToken)) + { + Snackbar.Add("已更换 Token,但 WebhookBaseUrl/WebhookSecretToken 未配置,无法自动重新注册 Webhook。", Severity.Warning); + return; + } + + try + { + var webhookUrl = $"{baseUrl.TrimEnd('/')}/api/bot/webhook/{Uri.EscapeDataString(token.Trim())}"; + await BotApi.SetWebhookAsync( + token: token.Trim(), + url: webhookUrl, + secretToken: secretToken, + allowedUpdates: BotUpdateHub.AllowedUpdatesJson, + cancellationToken: CancellationToken.None); + + Snackbar.Add("Webhook 已重新注册", Severity.Success); + } + catch (Exception ex) + { + Snackbar.Add($"Token 已更换,但重新注册 Webhook 失败:{ex.Message}", Severity.Warning); + } + } }