diff --git a/src/TelegramPanel.Core/Services/Telegram/BotTelegramService.cs b/src/TelegramPanel.Core/Services/Telegram/BotTelegramService.cs index 4236c1b..9dd86af 100644 --- a/src/TelegramPanel.Core/Services/Telegram/BotTelegramService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/BotTelegramService.cs @@ -824,7 +824,8 @@ public sealed record BotAdminRights( IReadOnlyList channelTelegramIds, long userId, bool permanentBan, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + Func? perChatCallback = null) { var bot = await _botManagement.GetBotAsync(botId) ?? throw new InvalidOperationException($"机器人不存在:{botId}"); @@ -936,6 +937,12 @@ public sealed record BotAdminRights( } } + if (perChatCallback != null) + { + failures.TryGetValue(chatId, out var err); + await perChatCallback(chatId, err, ok, failures.Count); + } + // 降速:避免 Bot API 429 if (delayMs > 0 && index < distinctIds.Count - 1) { diff --git a/src/TelegramPanel.Web/Components/ModulePages/KickApiPage.razor b/src/TelegramPanel.Web/Components/ModulePages/KickApiPage.razor new file mode 100644 index 0000000..cbc38ef --- /dev/null +++ b/src/TelegramPanel.Web/Components/ModulePages/KickApiPage.razor @@ -0,0 +1,196 @@ +@namespace TelegramPanel.Web.Components.ModulePages +@inject TelegramPanel.Core.Services.BotManagementService BotManagement +@inject TelegramPanel.Core.Services.BatchTaskManagementService TaskManagement +@inject ISnackbar Snackbar +@inject NavigationManager Navigation +@using System.Text.Json +@using TelegramPanel.Core.BatchTasks +@using TelegramPanel.Data.Entities +@using TelegramPanel.Web.ExternalApi +@using Microsoft.AspNetCore.Components + +踢人/封禁 + + + + + 提交后会在后台执行,可到【任务中心】查看进度与结果。频道数量较多时可能需要较长时间。 + + + + 全部机器人(谨慎) + @foreach (var b in bots) + { + @b.Name + } + + + + + + + + + @if (!useAllChats && selectedBotId != 0) + { + + } + + + + 提交任务 + + + 打开任务中心 + + + + + +@code +{ + [Parameter] public string ModuleId { get; set; } = ""; + [Parameter] public string PageKey { get; set; } = ""; + + private bool loading = true; + private List bots = new(); + private int selectedBotId = 0; + private long userId; + private bool permanentBan; + private bool useAllChats = true; + private string chatIdsText = ""; + + private bool CanSubmit => !loading && userId > 0 && (selectedBotId == 0 || bots.Any(x => x.Id == selectedBotId)); + + protected override async Task OnInitializedAsync() + { + try + { + bots = (await BotManagement.GetAllBotsAsync()) + .Where(b => b.IsActive) + .OrderBy(b => b.Id) + .ToList(); + } + catch (Exception ex) + { + Snackbar.Add($"加载机器人失败:{ex.Message}", Severity.Error); + } + finally + { + loading = false; + } + } + + private async Task OnSelectedBotChanged(int id) + { + selectedBotId = id; + if (selectedBotId == 0) + useAllChats = true; + await Task.CompletedTask; + } + + private async Task SubmitAsync() + { + if (!CanSubmit) + return; + + var botId = selectedBotId; + var runUseAllChats = botId == 0 ? true : useAllChats; + var chatIds = runUseAllChats ? new List() : ParseChatIds(chatIdsText); + + loading = true; + try + { + var total = await CountTargetsAsync(botId, runUseAllChats, chatIds); + if (total <= 0) + { + Snackbar.Add("未匹配到任何可操作的频道/群组(请先同步 Bot 频道数据,或检查 chat_id 列表)", Severity.Warning); + return; + } + + var log = new KickTaskLog + { + ApiName = "模块页面", + BotId = botId, + UseAllChats = runUseAllChats, + ChatIds = chatIds.Where(x => x != 0).Distinct().OrderBy(x => x).ToList(), + UserId = userId, + PermanentBan = permanentBan, + RequestedAtUtc = DateTime.UtcNow + }; + + var task = new BatchTask + { + TaskType = BatchTaskTypes.ExternalApiKick, + Total = total, + Completed = 0, + Failed = 0, + Config = JsonSerializer.Serialize(log, new JsonSerializerOptions { WriteIndented = true }) + }; + + var created = await TaskManagement.CreateTaskAsync(task); + Snackbar.Add($"已提交后台任务 #{created.Id}。请到【任务中心】查看进度。", Severity.Success); + Navigation.NavigateTo("/tasks"); + } + catch (Exception ex) + { + Snackbar.Add($"提交失败:{ex.Message}", Severity.Error); + } + finally + { + loading = false; + } + } + + private async Task CountTargetsAsync(int botId, bool runUseAllChats, IReadOnlyCollection chatIds) + { + if (botId > 0) + { + var chats = (await BotManagement.GetChatsAsync(botId)).ToList(); + if (!runUseAllChats && chatIds.Count > 0) + chats = chats.Where(c => chatIds.Contains(c.TelegramId)).ToList(); + return chats.Count; + } + + // botId==0: all bots (force useAllChats) + var botsList = (await BotManagement.GetAllBotsAsync()) + .Where(b => b.IsActive) + .OrderBy(b => b.Id) + .ToList(); + + var total = 0; + foreach (var b in botsList) + { + var chats = await BotManagement.GetChatsAsync(b.Id); + total += chats.Count(); + } + return total; + } + + private static List ParseChatIds(string raw) + { + raw = (raw ?? "").Trim(); + if (raw.Length == 0) + return new List(); + + var parts = raw + .Replace("\r", "\n", StringComparison.Ordinal) + .Split(new[] { ',', ';', ' ', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + var ids = new List(); + foreach (var p in parts) + { + if (long.TryParse(p, out var v) && v != 0) + ids.Add(v); + } + return ids; + } +} diff --git a/src/TelegramPanel.Web/ExternalApi/ExternalApiKickTaskHandler.cs b/src/TelegramPanel.Web/ExternalApi/ExternalApiKickTaskHandler.cs new file mode 100644 index 0000000..788140e --- /dev/null +++ b/src/TelegramPanel.Web/ExternalApi/ExternalApiKickTaskHandler.cs @@ -0,0 +1,244 @@ +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TelegramPanel.Core.BatchTasks; +using TelegramPanel.Core.Services; +using TelegramPanel.Core.Services.Telegram; +using TelegramPanel.Data.Entities; +using TelegramPanel.Modules; + +namespace TelegramPanel.Web.ExternalApi; + +public sealed class ExternalApiKickTaskHandler : IModuleTaskHandler +{ + public string TaskType => BatchTaskTypes.ExternalApiKick; + + public async Task ExecuteAsync(IModuleTaskExecutionHost host, CancellationToken cancellationToken) + { + var logger = host.Services.GetRequiredService>(); + var taskManagement = host.Services.GetRequiredService(); + var botManagement = host.Services.GetRequiredService(); + var botTelegram = host.Services.GetRequiredService(); + + var rawConfig = (host.Config ?? "").Trim(); + if (rawConfig.Length == 0) + throw new InvalidOperationException("任务缺少 Config"); + + KickTaskLog? input; + try + { + input = JsonSerializer.Deserialize(rawConfig); + } + catch (Exception ex) + { + throw new InvalidOperationException($"任务 Config JSON 无效:{ex.Message}"); + } + + if (input == null) + throw new InvalidOperationException("任务 Config JSON 为空"); + + if (input.UserId <= 0) + throw new InvalidOperationException("任务 user_id 无效"); + + input.ChatIds ??= new List(); + + var configuredBotId = input.BotId; + var useAllChats = configuredBotId == 0 ? true : input.UseAllChats; + var chatIdSet = new HashSet(input.ChatIds.Where(x => x != 0)); + + var targets = await ResolveTargetsAsync(botManagement, configuredBotId, useAllChats, chatIdSet, cancellationToken); + if (targets.TotalChats == 0) + throw new InvalidOperationException("未匹配到任何可操作的频道/群组"); + + var results = new List(targets.TotalChats); + var completed = 0; + var failed = 0; + + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var linkedToken = linkedCts.Token; + + try + { + foreach (var group in targets.Groups) + { + linkedToken.ThrowIfCancellationRequested(); + + if (!await host.IsStillRunningAsync(linkedToken)) + { + await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(new KickTaskLog + { + ApiName = input.ApiName ?? "", + BotId = configuredBotId, + UseAllChats = useAllChats, + ChatIds = chatIdSet.OrderBy(x => x).ToList(), + UserId = input.UserId, + PermanentBan = input.PermanentBan, + RequestedAtUtc = input.RequestedAtUtc, + Results = results, + Canceled = true + })); + return; + } + + var baseCompleted = completed; + var baseFailed = failed; + var chatById = group.Chats.ToDictionary(c => c.TelegramId); + var recorded = new HashSet(); + + try + { + _ = await botTelegram.BanChatMemberAsync( + botId: group.Bot.Id, + channelTelegramIds: group.Chats.Select(x => x.TelegramId).ToList(), + userId: input.UserId, + permanentBan: input.PermanentBan, + cancellationToken: linkedToken, + perChatCallback: async (chatId, error, okCount, failCount) => + { + if (!recorded.Add(chatId)) + return; + + var ok = error == null; + results.Add(new KickResultItem( + ChatId: chatId.ToString(), + Title: chatById.TryGetValue(chatId, out var ch) ? ch.Title : chatId.ToString(), + Success: ok, + Error: error)); + + completed = baseCompleted + okCount; + failed = baseFailed + failCount; + await host.UpdateProgressAsync(completed, failed, linkedToken); + + if (!await host.IsStillRunningAsync(linkedToken)) + linkedCts.Cancel(); + }); + } + catch (OperationCanceledException) when (linkedCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + { + // 用户主动取消(把任务状态改为非 running) + await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(new KickTaskLog + { + ApiName = input.ApiName ?? "", + BotId = configuredBotId, + UseAllChats = useAllChats, + ChatIds = chatIdSet.OrderBy(x => x).ToList(), + UserId = input.UserId, + PermanentBan = input.PermanentBan, + RequestedAtUtc = input.RequestedAtUtc, + Results = results, + Canceled = true + })); + return; + } + catch (Exception ex) + { + logger.LogWarning(ex, "ExternalApiKick failed on bot {BotId} (chats={Count})", group.Bot.Id, group.Chats.Count); + foreach (var chat in group.Chats) + { + if (!recorded.Add(chat.TelegramId)) + continue; + + results.Add(new KickResultItem( + ChatId: chat.TelegramId.ToString(), + Title: chat.Title, + Success: false, + Error: ex.Message)); + failed++; + } + + await host.UpdateProgressAsync(completed, failed, linkedToken); + } + } + } + catch (OperationCanceledException) when (linkedCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + { + await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(new KickTaskLog + { + ApiName = input.ApiName ?? "", + BotId = configuredBotId, + UseAllChats = useAllChats, + ChatIds = chatIdSet.OrderBy(x => x).ToList(), + UserId = input.UserId, + PermanentBan = input.PermanentBan, + RequestedAtUtc = input.RequestedAtUtc, + Results = results, + Canceled = true + })); + return; + } + catch (Exception ex) + { + logger.LogWarning(ex, "ExternalApiKick failed (taskId={TaskId})", host.TaskId); + await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(new KickTaskLog + { + ApiName = input.ApiName ?? "", + BotId = configuredBotId, + UseAllChats = useAllChats, + ChatIds = chatIdSet.OrderBy(x => x).ToList(), + UserId = input.UserId, + PermanentBan = input.PermanentBan, + RequestedAtUtc = input.RequestedAtUtc, + Results = results, + Error = ex.Message + })); + throw; + } + + await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(new KickTaskLog + { + ApiName = input.ApiName ?? "", + BotId = configuredBotId, + UseAllChats = useAllChats, + ChatIds = chatIdSet.OrderBy(x => x).ToList(), + UserId = input.UserId, + PermanentBan = input.PermanentBan, + RequestedAtUtc = input.RequestedAtUtc, + Results = results + })); + } + + private static async Task ResolveTargetsAsync( + BotManagementService botManagement, + int configuredBotId, + bool useAllChats, + HashSet configuredChatIds, + CancellationToken cancellationToken) + { + var bots = (await botManagement.GetAllBotsAsync()) + .Where(b => b.IsActive) + .OrderBy(b => b.Id) + .ToList(); + + if (configuredBotId > 0) + bots = bots.Where(b => b.Id == configuredBotId).ToList(); + + var groups = new List(); + var totalChats = 0; + + foreach (var bot in bots) + { + cancellationToken.ThrowIfCancellationRequested(); + + var chats = (await botManagement.GetChatsAsync(bot.Id)).ToList(); + if (!useAllChats && configuredChatIds.Count > 0) + chats = chats.Where(c => configuredChatIds.Contains(c.TelegramId)).ToList(); + + if (chats.Count == 0) + continue; + + groups.Add(new TargetGroup(bot, chats)); + totalChats += chats.Count; + } + + return new TargetResolution(groups, totalChats); + } + + private static string SerializeIndented(T value) + { + return JsonSerializer.Serialize(value, new JsonSerializerOptions { WriteIndented = true }); + } + + private sealed record TargetGroup(Bot Bot, List Chats); + private sealed record TargetResolution(List Groups, int TotalChats); +} + diff --git a/src/TelegramPanel.Web/ExternalApi/KickApi.cs b/src/TelegramPanel.Web/ExternalApi/KickApi.cs index c43bf51..18bc037 100644 --- a/src/TelegramPanel.Web/ExternalApi/KickApi.cs +++ b/src/TelegramPanel.Web/ExternalApi/KickApi.cs @@ -1,6 +1,5 @@ using System.Security.Cryptography; using System.Text; -using System.Text.Json.Serialization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -8,7 +7,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.Text.Json; using TelegramPanel.Core.Services; -using TelegramPanel.Core.Services.Telegram; using TelegramPanel.Core.BatchTasks; using TelegramPanel.Data.Entities; @@ -27,7 +25,6 @@ public static class KickApi HttpContext http, IConfiguration configuration, BotManagementService botManagement, - BotTelegramService botTelegram, BatchTaskManagementService taskManagement, ILoggerFactory loggerFactory, CancellationToken cancellationToken) @@ -86,10 +83,10 @@ public static class KickApi false, "未配置任何可操作的频道/群组(请先在页面选择机器人与频道/群组)", new KickSummary(0, 0, 0), - Array.Empty())); + Array.Empty())); } - // 记录到任务中心(即使外部调用返回了,也能追溯这次操作) + // 提交到任务中心,由后台执行(避免反代/CF 524 超时) var createdTask = await taskManagement.CreateTaskAsync(new BatchTask { TaskType = BatchTaskTypes.ExternalApiKick, @@ -108,120 +105,17 @@ public static class KickApi }) }); - await taskManagement.StartTaskAsync(createdTask.Id); + logger.LogInformation("Kick task queued: taskId={TaskId} api={ApiName} botId={BotId} total={Total} userId={UserId} permanent={Permanent}", + createdTask.Id, matched.Name ?? "", configuredBotId, targets.TotalChats, request.UserId, permanentBan); - var results = new List(targets.TotalChats); - var completed = 0; - var failedCount = 0; - - try - { - foreach (var group in targets.Groups) - { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - var banResult = await botTelegram.BanChatMemberAsync( - botId: group.Bot.Id, - channelTelegramIds: group.Chats.Select(x => x.TelegramId).ToList(), - userId: request.UserId, - permanentBan: permanentBan, - cancellationToken: cancellationToken); - - var failures = banResult.Failures; - foreach (var chat in group.Chats) - { - failures.TryGetValue(chat.TelegramId, out var err); - var ok = err == null; - results.Add(new KickResultItem( - ChatId: chat.TelegramId.ToString(), - Title: chat.Title, - Success: ok, - Error: err)); - - if (ok) completed++; else failedCount++; - } - } - catch (Exception ex) - { - logger.LogWarning(ex, "Kick API failed on bot {BotId} (chats={Count})", group.Bot.Id, group.Chats.Count); - foreach (var chat in group.Chats) - { - results.Add(new KickResultItem( - ChatId: chat.TelegramId.ToString(), - Title: chat.Title, - Success: false, - Error: ex.Message)); - failedCount++; - } - } - - await taskManagement.UpdateTaskProgressAsync(createdTask.Id, completed, failedCount); - } - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - await taskManagement.UpdateTaskProgressAsync(createdTask.Id, completed, failedCount == 0 ? 1 : failedCount); - await taskManagement.UpdateTaskConfigAsync(createdTask.Id, SerializeIndented(new KickTaskLog - { - ApiName = matched.Name ?? "", - BotId = configuredBotId, - UseAllChats = useAllChats, - ChatIds = configuredChatSet.OrderBy(x => x).ToList(), - UserId = request.UserId, - PermanentBan = permanentBan, - RequestedAtUtc = DateTime.UtcNow, - Results = results, - Canceled = true - })); - await taskManagement.CompleteTaskAsync(createdTask.Id, success: false); - throw; - } - catch (Exception ex) - { - logger.LogWarning(ex, "Kick API failed (taskId={TaskId})", createdTask.Id); - await taskManagement.UpdateTaskProgressAsync(createdTask.Id, completed, failedCount == 0 ? 1 : failedCount); - await taskManagement.UpdateTaskConfigAsync(createdTask.Id, SerializeIndented(new KickTaskLog - { - ApiName = matched.Name ?? "", - BotId = configuredBotId, - UseAllChats = useAllChats, - ChatIds = configuredChatSet.OrderBy(x => x).ToList(), - UserId = request.UserId, - PermanentBan = permanentBan, - RequestedAtUtc = DateTime.UtcNow, - Results = results, - Error = ex.Message - })); - await taskManagement.CompleteTaskAsync(createdTask.Id, success: false); - throw; - } - - var okCount = results.Count(x => x.Success); - var total = results.Count; - var failedTotal = total - okCount; - var actionText = permanentBan ? "Banned" : "Kicked"; - var message = $"{actionText} user {request.UserId} from {okCount}/{total} chats"; - - await taskManagement.UpdateTaskConfigAsync(createdTask.Id, SerializeIndented(new KickTaskLog - { - ApiName = matched.Name ?? "", - BotId = configuredBotId, - UseAllChats = useAllChats, - ChatIds = configuredChatSet.OrderBy(x => x).ToList(), - UserId = request.UserId, - PermanentBan = permanentBan, - RequestedAtUtc = DateTime.UtcNow, - Results = results - })); - await taskManagement.CompleteTaskAsync(createdTask.Id, success: failedTotal == 0); - - return Results.Ok(new KickResponse( - Success: true, - Message: message, - Summary: new KickSummary(total, okCount, failedTotal), - Results: results)); + return Results.Json( + new KickResponse( + Success: true, + Message: $"已提交后台任务 #{createdTask.Id},请到面板【任务中心】查看进度与结果。", + Summary: new KickSummary(targets.TotalChats, 0, 0), + Results: Array.Empty(), + TaskId: createdTask.Id), + statusCode: StatusCodes.Status202Accepted); } private static async Task ResolveTargetsAsync( @@ -280,41 +174,7 @@ public static class KickApi return JsonSerializer.Serialize(value, new JsonSerializerOptions { WriteIndented = true }); } - public sealed record KickRequest( - [property: JsonPropertyName("user_id")] long UserId, - [property: JsonPropertyName("permanent_ban")] bool? PermanentBan = null); - - public sealed record KickResponse( - [property: JsonPropertyName("success")] bool Success, - [property: JsonPropertyName("message")] string Message, - [property: JsonPropertyName("summary")] KickSummary Summary, - [property: JsonPropertyName("results")] IReadOnlyList Results); - - public sealed record KickSummary( - [property: JsonPropertyName("total")] int Total, - [property: JsonPropertyName("success")] int Success, - [property: JsonPropertyName("failed")] int Failed); - - public sealed record KickResultItem( - [property: JsonPropertyName("chat_id")] string ChatId, - [property: JsonPropertyName("title")] string Title, - [property: JsonPropertyName("success")] bool Success, - [property: JsonPropertyName("error")] string? Error); - private sealed record TargetGroup(Bot Bot, List Chats); private sealed record TargetResolution(List Groups, int TotalChats); - private sealed class KickTaskLog - { - public string ApiName { get; set; } = ""; - public int BotId { get; set; } - public bool UseAllChats { get; set; } - public List ChatIds { get; set; } = new(); - public long UserId { get; set; } - public bool PermanentBan { get; set; } - public DateTime RequestedAtUtc { get; set; } - public bool Canceled { get; set; } - public string? Error { get; set; } - public List? Results { get; set; } - } } diff --git a/src/TelegramPanel.Web/ExternalApi/KickModels.cs b/src/TelegramPanel.Web/ExternalApi/KickModels.cs new file mode 100644 index 0000000..3bc7681 --- /dev/null +++ b/src/TelegramPanel.Web/ExternalApi/KickModels.cs @@ -0,0 +1,62 @@ +using System.Text.Json.Serialization; + +namespace TelegramPanel.Web.ExternalApi; + +public sealed record KickRequest( + [property: JsonPropertyName("user_id")] long UserId, + [property: JsonPropertyName("permanent_ban")] bool? PermanentBan = null); + +public sealed record KickResponse( + [property: JsonPropertyName("success")] bool Success, + [property: JsonPropertyName("message")] string Message, + [property: JsonPropertyName("summary")] KickSummary Summary, + [property: JsonPropertyName("results")] IReadOnlyList Results, + [property: JsonPropertyName("task_id")] int? TaskId = null); + +public sealed record KickSummary( + [property: JsonPropertyName("total")] int Total, + [property: JsonPropertyName("success")] int Success, + [property: JsonPropertyName("failed")] int Failed); + +public sealed record KickResultItem( + [property: JsonPropertyName("chat_id")] string ChatId, + [property: JsonPropertyName("title")] string Title, + [property: JsonPropertyName("success")] bool Success, + [property: JsonPropertyName("error")] string? Error); + +public sealed class KickTaskLog +{ + [JsonPropertyName("api_name")] + public string ApiName { get; set; } = ""; + + /// + /// 0=all bots + /// + [JsonPropertyName("bot_id")] + public int BotId { get; set; } + + [JsonPropertyName("use_all_chats")] + public bool UseAllChats { get; set; } + + [JsonPropertyName("chat_ids")] + public List ChatIds { get; set; } = new(); + + [JsonPropertyName("user_id")] + public long UserId { get; set; } + + [JsonPropertyName("permanent_ban")] + public bool PermanentBan { get; set; } + + [JsonPropertyName("requested_at_utc")] + public DateTime RequestedAtUtc { get; set; } + + [JsonPropertyName("canceled")] + public bool Canceled { get; set; } + + [JsonPropertyName("error")] + public string? Error { get; set; } + + [JsonPropertyName("results")] + public List? Results { get; set; } +} + diff --git a/src/TelegramPanel.Web/Modules/BuiltIn/KickApiModule.cs b/src/TelegramPanel.Web/Modules/BuiltIn/KickApiModule.cs index 1bcf14a..c52ea2e 100644 --- a/src/TelegramPanel.Web/Modules/BuiltIn/KickApiModule.cs +++ b/src/TelegramPanel.Web/Modules/BuiltIn/KickApiModule.cs @@ -2,12 +2,14 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using MudBlazor; using TelegramPanel.Modules; +using TelegramPanel.Web.Components.ModulePages; using TelegramPanel.Web.ExternalApi; namespace TelegramPanel.Web.Modules.BuiltIn; -public sealed class KickApiModule : ITelegramPanelModule, IModuleApiProvider +public sealed class KickApiModule : ITelegramPanelModule, IModuleApiProvider, IModuleUiProvider { public KickApiModule(string version) { @@ -25,7 +27,7 @@ public sealed class KickApiModule : ITelegramPanelModule, IModuleApiProvider public void ConfigureServices(IServiceCollection services, ModuleHostContext context) { - // built-in:无需额外注入 + services.AddScoped(); } public void MapEndpoints(IEndpointRouteBuilder endpoints, ModuleHostContext context) @@ -49,4 +51,22 @@ public sealed class KickApiModule : ITelegramPanelModule, IModuleApiProvider Order = 10 }; } + + public IEnumerable GetNavItems(ModuleHostContext context) + { + yield break; + } + + public IEnumerable GetPages(ModuleHostContext context) + { + yield return new ModulePageDefinition + { + Key = "kick", + Title = "踢人/封禁", + Icon = Icons.Material.Filled.PersonRemove, + Group = "外部 API", + Order = 10, + ComponentType = typeof(KickApiPage).AssemblyQualifiedName ?? "" + }; + } } diff --git a/src/TelegramPanel.Web/Program.cs b/src/TelegramPanel.Web/Program.cs index 5743969..81187ba 100644 --- a/src/TelegramPanel.Web/Program.cs +++ b/src/TelegramPanel.Web/Program.cs @@ -262,7 +262,13 @@ if (serilogEnabled) { loggerConfig = loggerConfig .ReadFrom.Configuration(builder.Configuration) - .WriteTo.File("logs/telegram-panel-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: retainedFileCountLimit); + // buffered=true:降低磁盘抖动/IO 阻塞导致的请求卡顿风险(尤其是低配 VPS/挂载卷/杀软扫描场景) + .WriteTo.File( + "logs/telegram-panel-.log", + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: retainedFileCountLimit, + buffered: true, + flushToDiskInterval: TimeSpan.FromSeconds(1)); } else {