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);
+ }
+ }
}