机器人管理:支持编辑 Bot 并更换 Token

This commit is contained in:
meoacgx committed 2026-01-10 17:34:31 +08:00
1 parent 0e94efab9f
commit a510f3c136
3 files changed
+214

No files matched your search

@@ -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);
@@ -0,0 +1,71 @@
@namespace TelegramPanel.Web.Components.Dialogs
@using TelegramPanel.Data.Entities
<MudDialog>
<DialogContent>
<MudStack Spacing="2">
<MudText Typo="Typo.subtitle1">编辑机器人</MudText>
<MudTextField T="string" @bind-Value="name" Label="名称" Variant="Variant.Outlined" Required="true" />
<MudTextField T="string" @bind-Value="username" Label="用户名(可选)" Variant="Variant.Outlined" Placeholder="bot_username" />
<MudText Typo="Typo.caption" Color="Color.Secondary">
不展示当前 Token;如需更换请输入新 Token(留空则不修改)。
</MudText>
<MudTextField T="string" @bind-Value="newToken" Label="新的 Bot Token(可选)" Variant="Variant.Outlined"
InputType="@(showToken ? InputType.Text : InputType.Password)"
Adornment="Adornment.End"
AdornmentIcon="@(showToken ? Icons.Material.Filled.VisibilityOff : Icons.Material.Filled.Visibility)"
OnAdornmentClick="ToggleTokenVisibility" />
</MudStack>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Cancel">取消</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Disabled="@string.IsNullOrWhiteSpace(name)" OnClick="Confirm">
保存
</MudButton>
</DialogActions>
</MudDialog>
@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);
}
@@ -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
<PageTitle>机器人管理 - Telegram Panel</PageTitle>
@@ -63,6 +66,10 @@
</MudTd>
<MudTd DataLabel="操作">
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Size="Size.Small">
<MudMenuItem Icon="@Icons.Material.Filled.Edit"
OnClick="@(async () => await EditBotAsync(context))">
编辑
</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.List"
OnClick="@(() => Navigation.NavigateTo($"/bots/channels?botId={context.Id}"))">
频道列表
@@ -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<EditBotDialog>("编辑机器人", 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);
}
}
}