diff --git a/Directory.Build.props b/Directory.Build.props
index 537a8fa..7cb3e26 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -5,10 +5,10 @@
- 1.2.6
- 1.2.6.0
- 1.2.6.0
- 1.2.6
+ 1.2.7
+ 1.2.7.0
+ 1.2.7.0
+ 1.2.7
Telegram Panel Contributors
diff --git a/README.md b/README.md
index f22c713..d6fdb21 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,8 @@
- 🔐 **二级密码(2FA)与找回邮箱**:支持单个/批量修改二级密码;支持绑定/换绑 2FA 找回邮箱(验证码确认)
- 🧯 **忘记二级密码可申请重置**:支持单个或批量向 Telegram 提交“忘记密码重置”申请(通常等待 7 天后可重新设置)
- 🪪 **账号资料管理**:支持单账号编辑昵称/Bio/用户名/头像;支持批量修改昵称(自动追加手机号后 4 位便于区分)与批量修改 Bio
-- 🤖 **Bot 频道管理**:用于管理“频道创建人不在系统中”的频道(把 Bot 设为管理员即可纳入管理),支持批量导出链接、批量设置管理员;支持对每个 Bot 一键启用/停用(停用后相关后台不再轮询 getUpdates)
+- 🤖 **Bot 频道管理**:用于管理“频道创建人不在系统中”的频道(把 Bot 设为管理员即可纳入管理),支持批量导出链接、批量邀请成员/设置管理员(可按管理员列表匹配系统账号执行);支持对每个 Bot 一键启用/停用(停用后相关后台不再轮询 getUpdates)
+- 🏷️ **账号分类(排除操作)**:支持把“卖号/不参与运营”的账号分类标记为“排除操作”,不会出现在创建频道/批量任务的执行账号下拉里
## 🧊 防冻结指南(新号必看)
diff --git a/src/TelegramPanel.Data/AppDbContext.cs b/src/TelegramPanel.Data/AppDbContext.cs
index d0f249e..abe16ca 100644
--- a/src/TelegramPanel.Data/AppDbContext.cs
+++ b/src/TelegramPanel.Data/AppDbContext.cs
@@ -60,6 +60,7 @@ public class AppDbContext : DbContext
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
entity.Property(e => e.Color).HasMaxLength(20);
entity.Property(e => e.Description).HasMaxLength(500);
+ entity.Property(e => e.ExcludeFromOperations).HasDefaultValue(false);
entity.HasIndex(e => e.Name).IsUnique();
});
diff --git a/src/TelegramPanel.Data/Entities/AccountCategory.cs b/src/TelegramPanel.Data/Entities/AccountCategory.cs
index 3d5a197..65c0056 100644
--- a/src/TelegramPanel.Data/Entities/AccountCategory.cs
+++ b/src/TelegramPanel.Data/Entities/AccountCategory.cs
@@ -9,6 +9,10 @@ public class AccountCategory
public string Name { get; set; } = null!;
public string? Color { get; set; }
public string? Description { get; set; }
+ ///
+ /// 排除操作:该分类下的账号不出现在“创建频道/批量邀请/批量设置管理员”等操作的执行账号选择中
+ ///
+ public bool ExcludeFromOperations { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// 导航属性
diff --git a/src/TelegramPanel.Data/Migrations/20260102000000_AddAccountCategoryExcludeFromOperations.cs b/src/TelegramPanel.Data/Migrations/20260102000000_AddAccountCategoryExcludeFromOperations.cs
new file mode 100644
index 0000000..fea1cb6
--- /dev/null
+++ b/src/TelegramPanel.Data/Migrations/20260102000000_AddAccountCategoryExcludeFromOperations.cs
@@ -0,0 +1,33 @@
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace TelegramPanel.Data.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260102000000_AddAccountCategoryExcludeFromOperations")]
+ ///
+ public partial class AddAccountCategoryExcludeFromOperations : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "ExcludeFromOperations",
+ table: "AccountCategories",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "ExcludeFromOperations",
+ table: "AccountCategories");
+ }
+ }
+}
+
diff --git a/src/TelegramPanel.Data/Migrations/AppDbContextModelSnapshot.cs b/src/TelegramPanel.Data/Migrations/AppDbContextModelSnapshot.cs
index 6ad7565..1d6e917 100644
--- a/src/TelegramPanel.Data/Migrations/AppDbContextModelSnapshot.cs
+++ b/src/TelegramPanel.Data/Migrations/AppDbContextModelSnapshot.cs
@@ -112,6 +112,10 @@ namespace TelegramPanel.Data.Migrations
.HasMaxLength(500)
.HasColumnType("TEXT");
+ b.Property("ExcludeFromOperations")
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(false);
+
b.Property("Name")
.IsRequired()
.HasMaxLength(100)
diff --git a/src/TelegramPanel.Web/Components/Dialogs/AccountCategoryEditDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/AccountCategoryEditDialog.razor
index 3c7b528..12c92ba 100644
--- a/src/TelegramPanel.Web/Components/Dialogs/AccountCategoryEditDialog.razor
+++ b/src/TelegramPanel.Web/Components/Dialogs/AccountCategoryEditDialog.razor
@@ -9,6 +9,8 @@
+
@@ -28,12 +30,14 @@
private string name = "";
private MudColor color = "#9E9E9E";
private string? description;
+ private bool excludeFromOperations;
protected override void OnParametersSet()
{
name = (Category?.Name ?? "").Trim();
description = (Category?.Description ?? "").Trim();
color = string.IsNullOrWhiteSpace(Category?.Color) ? "#9E9E9E" : Category!.Color!;
+ excludeFromOperations = Category?.ExcludeFromOperations ?? false;
}
private void Cancel()
@@ -46,9 +50,9 @@
var model = new AccountCategoryEditModel(
Name: name.Trim(),
Color: string.IsNullOrWhiteSpace(color.Value) ? null : color.Value,
- Description: string.IsNullOrWhiteSpace(description) ? null : description!.Trim());
+ Description: string.IsNullOrWhiteSpace(description) ? null : description!.Trim(),
+ ExcludeFromOperations: excludeFromOperations);
MudDialog.Close(DialogResult.Ok(model));
}
}
-
diff --git a/src/TelegramPanel.Web/Components/Dialogs/AccountCategoryEditModel.cs b/src/TelegramPanel.Web/Components/Dialogs/AccountCategoryEditModel.cs
index 837a34c..12464f3 100644
--- a/src/TelegramPanel.Web/Components/Dialogs/AccountCategoryEditModel.cs
+++ b/src/TelegramPanel.Web/Components/Dialogs/AccountCategoryEditModel.cs
@@ -1,4 +1,3 @@
namespace TelegramPanel.Web.Components.Dialogs;
-public sealed record AccountCategoryEditModel(string Name, string? Color, string? Description);
-
+public sealed record AccountCategoryEditModel(string Name, string? Color, string? Description, bool ExcludeFromOperations);
diff --git a/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchAdminsDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchAdminsDialog.razor
new file mode 100644
index 0000000..4ed6de8
--- /dev/null
+++ b/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchAdminsDialog.razor
@@ -0,0 +1,609 @@
+@inject AccountManagementService AccountManagement
+@inject IChannelService ChannelService
+@inject BotTelegramService BotTelegram
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@inject TelegramPanel.Web.Services.ChannelAdminDefaultsService DefaultsService
+@inject TelegramPanel.Web.Services.ChannelAdminPresetsService PresetsService
+@using TelegramPanel.Data.Entities
+
+
+
+
+
+ 将使用“执行账号”为所选 Bot 频道批量设置管理员。执行账号必须是该频道管理员(通过 Bot API 获取管理员列表并与系统账号匹配)。
+
+ 支持:@@username、username
+
+
+
+ 目标频道:@channels.Count 个
+
+ @if (loadingAdmins)
+ {
+
+ 正在读取频道管理员列表(Bot API)...
+ }
+
+ @if (!string.IsNullOrWhiteSpace(adminSummary))
+ {
+ @adminSummary
+ }
+
+
+
+
+ (不使用预设)
+ @foreach (var p in Presets)
+ {
+ @p.Name
+ }
+
+
+
+
+ 删除预设
+
+
+
+
+
+ 自动选择(按频道)
+ @foreach (var a in eligibleAccounts)
+ {
+ @FormatAccountLabel(a)
+ }
+
+
+ @if (eligibleAccounts.Count == 0 && !loadingAdmins && channels.Count > 0)
+ {
+
+ 当前所选频道中未发现“既是频道管理员又在系统中的账号”。你仍可继续执行,但这些频道会被跳过。
+
+ }
+
+
+
+
+
+
+
+
+
+ 保存为预设
+
+
+
+
+
+
+ 权限(可多选)
+
+ 说明:这里选择的是“你希望目标账号拥有的权限”。如果设置后发现某些权限未生效,面板会提示缺失的权限名称。
+
+
+
+ 常用权限
+ 全选权限
+ 清空
+ 保存为默认权限
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (running)
+ {
+
+ 进度:@done / @total(失败:@failed)
+
+ @if (!string.IsNullOrWhiteSpace(currentHint))
+ {
+ @currentHint
+ }
+ }
+
+
+
+
+ @(running ? "停止" : "关闭")
+
+
+ 开始设置
+
+
+
+
+@code
+{
+ [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter] public int BotId { get; set; }
+ [Parameter] public List Channels { get; set; } = new();
+
+ private readonly Random _rand = new();
+ private readonly List eligibleAccounts = new();
+ private readonly Dictionary accountsByUserId = new();
+ private readonly Dictionary> channelAdminUserIds = new();
+
+ private List channels = new();
+ private int selectedAccountId;
+ private string usernamesText = "";
+ private string adminTitle = "Admin";
+ private int delayMs = 1500;
+ private const string UsernamesHelperText = "每行一个 username 或 @username,将按“频道 × 用户”的顺序依次执行。";
+
+ private bool loadingAdmins;
+ private string? adminSummary;
+
+ private bool loadingPresets;
+ private List Presets { get; set; } = new();
+ private string selectedPresetName = "";
+ private string presetNameToSave = "";
+
+ private RightsModel rights = new();
+ private static readonly AdminRights FullRights =
+ AdminRights.ChangeInfo
+ | AdminRights.PostMessages
+ | AdminRights.EditMessages
+ | AdminRights.DeleteMessages
+ | AdminRights.BanUsers
+ | AdminRights.InviteUsers
+ | AdminRights.PinMessages
+ | AdminRights.ManageCall
+ | AdminRights.AddAdmins
+ | AdminRights.Anonymous
+ | AdminRights.ManageTopics;
+
+ private bool running;
+ private int total;
+ private int done;
+ private int failed;
+ private string? currentHint;
+ private CancellationTokenSource? _cts;
+
+ private int Progress => total <= 0 ? 0 : (int)Math.Round(done * 100d / total);
+
+ private AdminRights SelectedRights => rights.ToRights();
+
+ protected override async Task OnInitializedAsync()
+ {
+ channels = (Channels ?? new List())
+ .Where(x => x != null)
+ .GroupBy(x => x.TelegramId)
+ .Select(x => x.First())
+ .ToList();
+
+ await LoadAccountsAsync();
+
+ // 默认权限:优先读取上次保存的默认值;否则使用“常用权限”
+ var defaults = await DefaultsService.GetAsync();
+ ApplyRights(defaults?.Rights ?? AdminRights.BasicAdmin);
+
+ await ReloadPresetsAsync();
+
+ if (BotId > 0 && channels.Count > 0)
+ await LoadAdminsAndEligibleAccountsAsync();
+ }
+
+ private async Task LoadAccountsAsync()
+ {
+ var all = (await AccountManagement.GetAllAccountsAsync()).ToList();
+ accountsByUserId.Clear();
+ foreach (var a in all.Where(x => x.IsActive && x.Category?.ExcludeFromOperations != true))
+ {
+ if (a.UserId <= 0)
+ continue;
+ if (!accountsByUserId.ContainsKey(a.UserId))
+ accountsByUserId[a.UserId] = a;
+ }
+ }
+
+ private async Task LoadAdminsAndEligibleAccountsAsync()
+ {
+ loadingAdmins = true;
+ adminSummary = null;
+ eligibleAccounts.Clear();
+ channelAdminUserIds.Clear();
+ try
+ {
+ var eligibleUserIds = new HashSet();
+ var matchedChannels = 0;
+
+ foreach (var ch in channels)
+ {
+ var adminIds = new HashSet();
+ try
+ {
+ var admins = await BotTelegram.GetChatAdminsAsync(BotId, ch.TelegramId, CancellationToken.None);
+ foreach (var a in admins)
+ {
+ adminIds.Add(a.UserId);
+ if (accountsByUserId.ContainsKey(a.UserId))
+ eligibleUserIds.Add(a.UserId);
+ }
+
+ if (admins.Any(a => accountsByUserId.ContainsKey(a.UserId)))
+ matchedChannels++;
+ }
+ catch
+ {
+ // ignore
+ }
+
+ channelAdminUserIds[ch.TelegramId] = adminIds;
+ }
+
+ foreach (var uid in eligibleUserIds)
+ {
+ if (accountsByUserId.TryGetValue(uid, out var acc))
+ eligibleAccounts.Add(acc);
+ }
+
+ eligibleAccounts.Sort((a, b) => string.Compare(FormatAccountLabel(a), FormatAccountLabel(b), StringComparison.OrdinalIgnoreCase));
+ adminSummary = $"可用执行账号:{eligibleAccounts.Count} 个;可执行频道:{matchedChannels}/{channels.Count}。";
+ }
+ finally
+ {
+ loadingAdmins = false;
+ }
+ }
+
+ private async Task ReloadPresetsAsync()
+ {
+ loadingPresets = true;
+ try
+ {
+ Presets = (await PresetsService.GetPresetsAsync()).ToList();
+ }
+ finally
+ {
+ loadingPresets = false;
+ }
+ }
+
+ private Task OnPresetChanged(string? name)
+ {
+ selectedPresetName = name ?? "";
+ if (string.IsNullOrWhiteSpace(selectedPresetName))
+ return Task.CompletedTask;
+
+ var p = Presets.FirstOrDefault(x => string.Equals(x.Name, selectedPresetName, StringComparison.OrdinalIgnoreCase));
+ if (p != null)
+ usernamesText = string.Join(Environment.NewLine, p.Usernames.Select(x => $"@{x}"));
+
+ return Task.CompletedTask;
+ }
+
+ private async Task SavePreset()
+ {
+ var name = (presetNameToSave ?? "").Trim();
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ Snackbar.Add("请输入预设名称", Severity.Warning);
+ return;
+ }
+
+ var usernames = ParseUsernames(usernamesText);
+ if (usernames.Count == 0)
+ {
+ Snackbar.Add("请至少输入一个用户名", Severity.Warning);
+ return;
+ }
+
+ try
+ {
+ await PresetsService.SavePresetAsync(name, usernames);
+ Snackbar.Add("已保存预设", Severity.Success);
+ presetNameToSave = "";
+ await ReloadPresetsAsync();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"保存预设失败:{ex.Message}", Severity.Error);
+ }
+ }
+
+ private async Task DeleteSelectedPreset()
+ {
+ if (string.IsNullOrWhiteSpace(selectedPresetName))
+ return;
+
+ bool? ok = await DialogService.ShowMessageBox(
+ title: "删除预设",
+ message: $"确定删除预设“{selectedPresetName}”吗?",
+ yesText: "删除",
+ cancelText: "取消");
+
+ if (ok != true)
+ return;
+
+ try
+ {
+ await PresetsService.DeletePresetAsync(selectedPresetName);
+ Snackbar.Add("已删除预设", Severity.Success);
+ selectedPresetName = "";
+ await ReloadPresetsAsync();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"删除预设失败:{ex.Message}", Severity.Error);
+ }
+ }
+
+ private void ApplyRights(AdminRights selected)
+ {
+ rights = RightsModel.FromRights(selected);
+ }
+
+ private async Task SaveAsDefaultRightsAsync()
+ {
+ try
+ {
+ await DefaultsService.SaveAsync(new TelegramPanel.Web.Services.ChannelAdminDefaults(SelectedRights));
+ Snackbar.Add("已保存默认权限", Severity.Success);
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"保存失败:{ex.Message}", Severity.Error);
+ }
+ }
+
+ private async Task Submit()
+ {
+ if (running)
+ return;
+
+ if (BotId <= 0)
+ {
+ Snackbar.Add("BotId 无效", Severity.Error);
+ return;
+ }
+
+ var usernames = ParseUsernames(usernamesText);
+ if (usernames.Count == 0)
+ {
+ Snackbar.Add("请至少输入一个用户名", Severity.Warning);
+ return;
+ }
+
+ if (channels.Count == 0)
+ {
+ Snackbar.Add("未选择任何频道", Severity.Info);
+ return;
+ }
+
+ var title = string.IsNullOrWhiteSpace(adminTitle) ? "Admin" : adminTitle.Trim();
+
+ bool? confirm = await DialogService.ShowMessageBox(
+ "确认执行",
+ $"设置管理员:{channels.Count} 个频道 × {usernames.Count} 个用户(共 {channels.Count * usernames.Count} 次操作)。是否继续?",
+ yesText: "继续", cancelText: "取消");
+
+ if (confirm != true)
+ return;
+
+ running = true;
+ total = channels.Count * usernames.Count;
+ done = 0;
+ failed = 0;
+ currentHint = null;
+ _cts = new CancellationTokenSource();
+
+ var failures = new List();
+
+ try
+ {
+ foreach (var ch in channels)
+ {
+ var executorId = ResolveExecutorAccountId(ch);
+ if (executorId == null)
+ {
+ foreach (var _ in usernames)
+ {
+ done++;
+ failed++;
+ }
+
+ failures.Add($"{ch.Title}:无可用执行账号(需要该频道管理员且存在于系统账号)");
+ StateHasChanged();
+ continue;
+ }
+
+ foreach (var username in usernames)
+ {
+ _cts.Token.ThrowIfCancellationRequested();
+
+ currentHint = $"{ch.Title} => {username}";
+ StateHasChanged();
+
+ try
+ {
+ var ok = await ChannelService.SetAdminAsync(executorId.Value, ch.TelegramId, username, SelectedRights, title);
+ if (!ok)
+ {
+ failed++;
+ failures.Add($"{ch.Title} => {username}:失败");
+ }
+ }
+ catch (Exception ex)
+ {
+ failed++;
+ failures.Add($"{ch.Title} => {username}:{ex.Message}");
+ }
+ finally
+ {
+ done++;
+ }
+
+ StateHasChanged();
+
+ var wait = delayMs;
+ if (wait < 0) wait = 0;
+ if (wait > 30000) wait = 30000;
+ var jitter = _rand.Next(500, 1000);
+ await Task.Delay(TimeSpan.FromMilliseconds(wait + jitter), _cts.Token);
+ }
+ }
+
+ var summary = $"完成:{done}/{total}(失败:{failed})";
+ Snackbar.Add(summary, failed == 0 ? Severity.Success : Severity.Warning);
+
+ if (failures.Count > 0)
+ {
+ var details = string.Join(Environment.NewLine, failures.Take(80));
+ await DialogService.ShowMessageBox("失败明细(前 80 条)", details, "关闭");
+ }
+
+ MudDialog.Close(DialogResult.Ok(new { Done = done, Failed = failed }));
+ }
+ catch (OperationCanceledException)
+ {
+ Snackbar.Add("已停止执行", Severity.Info);
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"执行异常:{ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ running = false;
+ currentHint = null;
+ _cts?.Dispose();
+ _cts = null;
+ StateHasChanged();
+ }
+ }
+
+ private int? ResolveExecutorAccountId(BotChannel channel)
+ {
+ if (!channelAdminUserIds.TryGetValue(channel.TelegramId, out var adminIds) || adminIds.Count == 0)
+ return null;
+
+ if (selectedAccountId > 0)
+ {
+ var selected = eligibleAccounts.FirstOrDefault(x => x.Id == selectedAccountId);
+ if (selected == null || selected.UserId <= 0)
+ return null;
+
+ return adminIds.Contains(selected.UserId) ? selected.Id : null;
+ }
+
+ foreach (var uid in adminIds)
+ {
+ if (accountsByUserId.TryGetValue(uid, out var acc))
+ return acc.Id;
+ }
+
+ return null;
+ }
+
+ private void CancelOrClose()
+ {
+ if (running)
+ {
+ _cts?.Cancel();
+ return;
+ }
+
+ MudDialog.Close();
+ }
+
+ private static List ParseUsernames(string? text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ return new List();
+
+ return text
+ .Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => (x ?? string.Empty).Trim())
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x.StartsWith("@", StringComparison.Ordinal) ? x.Substring(1) : x)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+
+ private static string FormatAccountLabel(Account account)
+ {
+ var nickname = string.IsNullOrWhiteSpace(account.Nickname) ? null : account.Nickname.Trim();
+ var username = string.IsNullOrWhiteSpace(account.Username) ? null : account.Username.Trim();
+
+ var namePart = nickname ?? account.Phone;
+ if (!string.IsNullOrWhiteSpace(username))
+ return $"{namePart} (@{username})";
+ return namePart;
+ }
+
+ private sealed class RightsModel
+ {
+ public bool ChangeInfo { get; set; }
+ public bool PostMessages { get; set; }
+ public bool EditMessages { get; set; }
+ public bool DeleteMessages { get; set; }
+ public bool BanUsers { get; set; }
+ public bool InviteUsers { get; set; }
+ public bool PinMessages { get; set; }
+ public bool ManageCall { get; set; }
+ public bool AddAdmins { get; set; }
+ public bool Anonymous { get; set; }
+ public bool ManageTopics { get; set; }
+
+ public AdminRights ToRights()
+ {
+ var r = AdminRights.None;
+ if (ChangeInfo) r |= AdminRights.ChangeInfo;
+ if (PostMessages) r |= AdminRights.PostMessages;
+ if (EditMessages) r |= AdminRights.EditMessages;
+ if (DeleteMessages) r |= AdminRights.DeleteMessages;
+ if (BanUsers) r |= AdminRights.BanUsers;
+ if (InviteUsers) r |= AdminRights.InviteUsers;
+ if (PinMessages) r |= AdminRights.PinMessages;
+ if (ManageCall) r |= AdminRights.ManageCall;
+ if (AddAdmins) r |= AdminRights.AddAdmins;
+ if (Anonymous) r |= AdminRights.Anonymous;
+ if (ManageTopics) r |= AdminRights.ManageTopics;
+ return r;
+ }
+
+ public static RightsModel FromRights(AdminRights rights)
+ {
+ return new RightsModel
+ {
+ ChangeInfo = rights.HasFlag(AdminRights.ChangeInfo),
+ PostMessages = rights.HasFlag(AdminRights.PostMessages),
+ EditMessages = rights.HasFlag(AdminRights.EditMessages),
+ DeleteMessages = rights.HasFlag(AdminRights.DeleteMessages),
+ BanUsers = rights.HasFlag(AdminRights.BanUsers),
+ InviteUsers = rights.HasFlag(AdminRights.InviteUsers),
+ PinMessages = rights.HasFlag(AdminRights.PinMessages),
+ ManageCall = rights.HasFlag(AdminRights.ManageCall),
+ AddAdmins = rights.HasFlag(AdminRights.AddAdmins),
+ Anonymous = rights.HasFlag(AdminRights.Anonymous),
+ ManageTopics = rights.HasFlag(AdminRights.ManageTopics)
+ };
+ }
+ }
+}
diff --git a/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchInviteDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchInviteDialog.razor
new file mode 100644
index 0000000..94a02a2
--- /dev/null
+++ b/src/TelegramPanel.Web/Components/Dialogs/BotChannelBatchInviteDialog.razor
@@ -0,0 +1,490 @@
+@inject AccountManagementService AccountManagement
+@inject IChannelService ChannelService
+@inject BotTelegramService BotTelegram
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+@inject TelegramPanel.Web.Services.ChannelInvitePresetsService PresetsService
+@using TelegramPanel.Data.Entities
+
+
+
+
+
+ 将使用“执行账号”邀请用户加入所选 Bot 频道。执行账号必须是该频道管理员(通过 Bot API 获取管理员列表并与系统账号匹配)。
+
+ 支持:@@username、username
+
+
+
+ 目标频道:@channels.Count 个
+
+ @if (loadingAdmins)
+ {
+
+ 正在读取频道管理员列表(Bot API)...
+ }
+
+ @if (!string.IsNullOrWhiteSpace(adminSummary))
+ {
+ @adminSummary
+ }
+
+
+
+
+ (不使用预设)
+ @foreach (var p in Presets)
+ {
+ @p.Name
+ }
+
+
+
+
+ 删除预设
+
+
+
+
+
+ 自动选择(按频道)
+ @foreach (var a in eligibleAccounts)
+ {
+ @FormatAccountLabel(a)
+ }
+
+
+ @if (eligibleAccounts.Count == 0 && !loadingAdmins && channels.Count > 0)
+ {
+
+ 当前所选频道中未发现“既是频道管理员又在系统中的账号”。你仍可继续执行,但这些频道会被跳过。
+
+ }
+
+
+
+
+
+
+
+
+
+ 保存为预设
+
+
+
+
+
+
+ @if (running)
+ {
+
+ 进度:@done / @total(失败:@failed)
+
+ @if (!string.IsNullOrWhiteSpace(currentHint))
+ {
+ @currentHint
+ }
+ }
+
+
+
+
+ @(running ? "停止" : "关闭")
+
+
+ 开始邀请
+
+
+
+
+@code
+{
+ [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
+
+ [Parameter] public int BotId { get; set; }
+ [Parameter] public List Channels { get; set; } = new();
+
+ private readonly Random _rand = new();
+ private readonly List eligibleAccounts = new();
+ private readonly Dictionary accountsByUserId = new();
+ private readonly Dictionary> channelAdminUserIds = new();
+
+ private List channels = new();
+ private int selectedAccountId;
+ private string usernamesText = "";
+ private int delayMs = 2000;
+ private const string UsernamesHelperText = "每行一个 username 或 @username,将按“频道 × 用户”的顺序依次执行。";
+
+ private bool loadingAdmins;
+ private string? adminSummary;
+
+ private bool loadingPresets;
+ private List Presets { get; set; } = new();
+ private string selectedPresetName = "";
+ private string presetNameToSave = "";
+
+ private bool running;
+ private int total;
+ private int done;
+ private int failed;
+ private string? currentHint;
+ private CancellationTokenSource? _cts;
+
+ private int Progress => total <= 0 ? 0 : (int)Math.Round(done * 100d / total);
+
+ protected override async Task OnInitializedAsync()
+ {
+ channels = (Channels ?? new List())
+ .Where(x => x != null)
+ .GroupBy(x => x.TelegramId)
+ .Select(x => x.First())
+ .ToList();
+
+ await LoadAccountsAsync();
+ await ReloadPresetsAsync();
+
+ if (BotId > 0 && channels.Count > 0)
+ await LoadAdminsAndEligibleAccountsAsync();
+ }
+
+ private async Task LoadAccountsAsync()
+ {
+ var all = (await AccountManagement.GetAllAccountsAsync()).ToList();
+ accountsByUserId.Clear();
+ foreach (var a in all)
+ {
+ if (!a.IsActive)
+ continue;
+ if (a.Category?.ExcludeFromOperations == true)
+ continue;
+
+ if (a.UserId <= 0)
+ continue;
+ if (!accountsByUserId.ContainsKey(a.UserId))
+ accountsByUserId[a.UserId] = a;
+ }
+ }
+
+ private async Task LoadAdminsAndEligibleAccountsAsync()
+ {
+ loadingAdmins = true;
+ adminSummary = null;
+ eligibleAccounts.Clear();
+ channelAdminUserIds.Clear();
+ try
+ {
+ var eligibleUserIds = new HashSet();
+ var matchedChannels = 0;
+
+ foreach (var ch in channels)
+ {
+ var adminIds = new HashSet();
+ try
+ {
+ var admins = await BotTelegram.GetChatAdminsAsync(BotId, ch.TelegramId, CancellationToken.None);
+ foreach (var a in admins)
+ {
+ adminIds.Add(a.UserId);
+ if (accountsByUserId.ContainsKey(a.UserId))
+ eligibleUserIds.Add(a.UserId);
+ }
+
+ if (admins.Any(a => accountsByUserId.ContainsKey(a.UserId)))
+ matchedChannels++;
+ }
+ catch
+ {
+ // ignore: 无权限/风控/频道已失效等,后续按“无可用执行账号”处理
+ }
+
+ channelAdminUserIds[ch.TelegramId] = adminIds;
+ }
+
+ foreach (var uid in eligibleUserIds)
+ {
+ if (accountsByUserId.TryGetValue(uid, out var acc))
+ eligibleAccounts.Add(acc);
+ }
+
+ eligibleAccounts.Sort((a, b) => string.Compare(FormatAccountLabel(a), FormatAccountLabel(b), StringComparison.OrdinalIgnoreCase));
+ adminSummary = $"可用执行账号:{eligibleAccounts.Count} 个;可执行频道:{matchedChannels}/{channels.Count}。";
+ }
+ finally
+ {
+ loadingAdmins = false;
+ }
+ }
+
+ private async Task ReloadPresetsAsync()
+ {
+ loadingPresets = true;
+ try
+ {
+ Presets = (await PresetsService.GetPresetsAsync()).ToList();
+ }
+ finally
+ {
+ loadingPresets = false;
+ }
+ }
+
+ private Task OnPresetChanged(string value)
+ {
+ selectedPresetName = value ?? "";
+ if (string.IsNullOrWhiteSpace(selectedPresetName))
+ return Task.CompletedTask;
+
+ var preset = Presets.FirstOrDefault(x => string.Equals(x.Name, selectedPresetName, StringComparison.OrdinalIgnoreCase));
+ if (preset != null)
+ usernamesText = string.Join(Environment.NewLine, preset.Usernames);
+
+ return Task.CompletedTask;
+ }
+
+ private async Task SavePreset()
+ {
+ var name = (presetNameToSave ?? "").Trim();
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ Snackbar.Add("请输入预设名称", Severity.Warning);
+ return;
+ }
+
+ var usernames = ParseUsernames(usernamesText);
+ if (usernames.Count == 0)
+ {
+ Snackbar.Add("请至少输入一个用户名", Severity.Warning);
+ return;
+ }
+
+ try
+ {
+ await PresetsService.SavePresetAsync(name, usernames);
+ Snackbar.Add("已保存预设", Severity.Success);
+ presetNameToSave = "";
+ await ReloadPresetsAsync();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"保存预设失败:{ex.Message}", Severity.Error);
+ }
+ }
+
+ private async Task DeleteSelectedPreset()
+ {
+ if (string.IsNullOrWhiteSpace(selectedPresetName))
+ return;
+
+ bool? ok = await DialogService.ShowMessageBox(
+ title: "删除预设",
+ message: $"确定删除预设“{selectedPresetName}”吗?",
+ yesText: "删除",
+ cancelText: "取消");
+
+ if (ok != true)
+ return;
+
+ try
+ {
+ await PresetsService.DeletePresetAsync(selectedPresetName);
+ Snackbar.Add("已删除预设", Severity.Success);
+ selectedPresetName = "";
+ await ReloadPresetsAsync();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"删除预设失败:{ex.Message}", Severity.Error);
+ }
+ }
+
+ private async Task Submit()
+ {
+ if (running)
+ return;
+
+ if (BotId <= 0)
+ {
+ Snackbar.Add("BotId 无效", Severity.Error);
+ return;
+ }
+
+ var usernames = ParseUsernames(usernamesText);
+ if (usernames.Count == 0)
+ {
+ Snackbar.Add("请至少输入一个用户名", Severity.Warning);
+ return;
+ }
+
+ if (channels.Count == 0)
+ {
+ Snackbar.Add("未选择任何频道", Severity.Info);
+ return;
+ }
+
+ bool? confirm = await DialogService.ShowMessageBox(
+ "确认执行",
+ $"邀请:{channels.Count} 个频道 × {usernames.Count} 个用户(共 {channels.Count * usernames.Count} 次操作)。是否继续?",
+ yesText: "继续", cancelText: "取消");
+
+ if (confirm != true)
+ return;
+
+ running = true;
+ total = channels.Count * usernames.Count;
+ done = 0;
+ failed = 0;
+ currentHint = null;
+ _cts = new CancellationTokenSource();
+
+ var failures = new List();
+
+ try
+ {
+ foreach (var ch in channels)
+ {
+ var executorId = ResolveExecutorAccountId(ch);
+ if (executorId == null)
+ {
+ foreach (var _ in usernames)
+ {
+ done++;
+ failed++;
+ }
+
+ failures.Add($"{ch.Title}:无可用执行账号(需要该频道管理员且存在于系统账号)");
+ StateHasChanged();
+ continue;
+ }
+
+ foreach (var username in usernames)
+ {
+ _cts.Token.ThrowIfCancellationRequested();
+
+ currentHint = $"{ch.Title} => {username}";
+ StateHasChanged();
+
+ try
+ {
+ var result = await ChannelService.InviteUserAsync(executorId.Value, ch.TelegramId, username);
+ if (!result.Success)
+ {
+ failed++;
+ failures.Add($"{ch.Title} => {username}:{result.Error}");
+ }
+ }
+ catch (Exception ex)
+ {
+ failed++;
+ failures.Add($"{ch.Title} => {username}:{ex.Message}");
+ }
+ finally
+ {
+ done++;
+ }
+
+ StateHasChanged();
+
+ var wait = delayMs;
+ if (wait < 0) wait = 0;
+ if (wait > 30000) wait = 30000;
+ var jitter = _rand.Next(500, 1500);
+ await Task.Delay(TimeSpan.FromMilliseconds(wait + jitter), _cts.Token);
+ }
+ }
+
+ var summary = $"完成:{done}/{total}(失败:{failed})";
+ Snackbar.Add(summary, failed == 0 ? Severity.Success : Severity.Warning);
+
+ if (failures.Count > 0)
+ {
+ var details = string.Join(Environment.NewLine, failures.Take(80));
+ await DialogService.ShowMessageBox("失败明细(前 80 条)", details, "关闭");
+ }
+
+ MudDialog.Close(DialogResult.Ok(new { Done = done, Failed = failed }));
+ }
+ catch (OperationCanceledException)
+ {
+ Snackbar.Add("已停止执行", Severity.Info);
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"执行异常:{ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ running = false;
+ currentHint = null;
+ _cts?.Dispose();
+ _cts = null;
+ StateHasChanged();
+ }
+ }
+
+ private int? ResolveExecutorAccountId(BotChannel channel)
+ {
+ if (!channelAdminUserIds.TryGetValue(channel.TelegramId, out var adminIds) || adminIds.Count == 0)
+ return null;
+
+ if (selectedAccountId > 0)
+ {
+ var selected = eligibleAccounts.FirstOrDefault(x => x.Id == selectedAccountId);
+ if (selected == null || selected.UserId <= 0)
+ return null;
+
+ return adminIds.Contains(selected.UserId) ? selected.Id : null;
+ }
+
+ foreach (var uid in adminIds)
+ {
+ if (accountsByUserId.TryGetValue(uid, out var acc))
+ return acc.Id;
+ }
+
+ return null;
+ }
+
+ private void CancelOrClose()
+ {
+ if (running)
+ {
+ _cts?.Cancel();
+ return;
+ }
+
+ MudDialog.Close();
+ }
+
+ private static List ParseUsernames(string? text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ return new List();
+
+ return text
+ .Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => (x ?? string.Empty).Trim())
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x.StartsWith("@", StringComparison.Ordinal) ? x.Substring(1) : x)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+
+ private static string FormatAccountLabel(Account account)
+ {
+ var nickname = string.IsNullOrWhiteSpace(account.Nickname) ? null : account.Nickname.Trim();
+ var username = string.IsNullOrWhiteSpace(account.Username) ? null : account.Username.Trim();
+
+ var namePart = nickname ?? account.Phone;
+ if (!string.IsNullOrWhiteSpace(username))
+ return $"{namePart} (@{username})";
+ return namePart;
+ }
+}
diff --git a/src/TelegramPanel.Web/Components/Dialogs/ChannelBatchAdminsDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/ChannelBatchAdminsDialog.razor
index 70d6bbe..6a5fb06 100644
--- a/src/TelegramPanel.Web/Components/Dialogs/ChannelBatchAdminsDialog.razor
+++ b/src/TelegramPanel.Web/Components/Dialogs/ChannelBatchAdminsDialog.razor
@@ -11,7 +11,7 @@
- 将使用“执行账号”为所选频道批量设置管理员。支持输入多个用户名,换行分隔。
+ 将使用“执行账号”为所选频道批量设置管理员(默认:每个频道的创建账号)。支持输入多个用户名,换行分隔。
支持:@@username、username
@@ -39,6 +39,7 @@
+ 每个频道创建账号(默认)
@foreach (var a in accounts)
{
@FormatAccountLabel(a)
@@ -173,12 +174,8 @@
.ToList();
var all = (await AccountManagement.GetAllAccountsAsync()).ToList();
- accounts.AddRange(all.Where(a => a.IsActive));
-
- if (DefaultAccountId > 0 && accounts.Any(a => a.Id == DefaultAccountId))
- selectedAccountId = DefaultAccountId;
- else
- selectedAccountId = accounts.FirstOrDefault()?.Id ?? 0;
+ accounts.AddRange(all.Where(a => a.IsActive && a.Category?.ExcludeFromOperations != true));
+ selectedAccountId = 0;
// 默认权限:优先读取上次保存的默认值;否则使用“常用权限”
var defaults = await DefaultsService.GetAsync();
@@ -187,6 +184,21 @@
await ReloadPresetsAsync();
}
+ private int? ResolveExecuteAccountId(Channel channel)
+ {
+ if (channel == null)
+ return null;
+
+ if (selectedAccountId > 0)
+ return selectedAccountId;
+
+ var creatorId = channel.CreatorAccountId;
+ if (creatorId == null || creatorId.Value <= 0)
+ return null;
+
+ return accounts.Any(a => a.Id == creatorId.Value) ? creatorId.Value : null;
+ }
+
private async Task ReloadPresetsAsync()
{
loadingPresets = true;
@@ -291,12 +303,6 @@
if (running)
return;
- if (selectedAccountId <= 0)
- {
- Snackbar.Add("请选择一个执行账号", Severity.Warning);
- return;
- }
-
var usernames = ParseUsernames(usernamesText);
if (usernames.Count == 0)
{
@@ -335,6 +341,16 @@
{
foreach (var ch in channels)
{
+ var executeAccountId = ResolveExecuteAccountId(ch);
+ if (executeAccountId == null)
+ {
+ done += usernames.Count;
+ failed += usernames.Count;
+ failures.Add($"{ch.Title}:无可用执行账号(将使用创建账号执行;但该频道无创建账号/创建账号不可用)");
+ StateHasChanged();
+ continue;
+ }
+
foreach (var username in usernames)
{
_cts.Token.ThrowIfCancellationRequested();
@@ -344,7 +360,7 @@
try
{
- var ok = await ChannelService.SetAdminAsync(selectedAccountId, ch.TelegramId, username, SelectedRights, title);
+ var ok = await ChannelService.SetAdminAsync(executeAccountId.Value, ch.TelegramId, username, SelectedRights, title);
if (!ok)
{
failed++;
diff --git a/src/TelegramPanel.Web/Components/Dialogs/ChannelBatchInviteDialog.razor b/src/TelegramPanel.Web/Components/Dialogs/ChannelBatchInviteDialog.razor
index 71b8db8..9b44ae8 100644
--- a/src/TelegramPanel.Web/Components/Dialogs/ChannelBatchInviteDialog.razor
+++ b/src/TelegramPanel.Web/Components/Dialogs/ChannelBatchInviteDialog.razor
@@ -10,7 +10,7 @@
- 将使用“执行账号”邀请用户加入所选频道。支持输入多个用户名,换行分隔。
+ 将使用“执行账号”邀请用户加入所选频道(默认:每个频道的创建账号)。支持输入多个用户名,换行分隔。
支持:@@username、username
@@ -38,6 +38,7 @@
+ 每个频道创建账号(默认)
@foreach (var a in accounts)
{
@FormatAccountLabel(a)
@@ -123,16 +124,27 @@
.ToList();
var all = (await AccountManagement.GetAllAccountsAsync()).ToList();
- accounts.AddRange(all.Where(a => a.IsActive));
-
- if (DefaultAccountId > 0 && accounts.Any(a => a.Id == DefaultAccountId))
- selectedAccountId = DefaultAccountId;
- else
- selectedAccountId = accounts.FirstOrDefault()?.Id ?? 0;
+ accounts.AddRange(all.Where(a => a.IsActive && a.Category?.ExcludeFromOperations != true));
+ selectedAccountId = 0;
await ReloadPresetsAsync();
}
+ private int? ResolveExecuteAccountId(Channel channel)
+ {
+ if (channel == null)
+ return null;
+
+ if (selectedAccountId > 0)
+ return selectedAccountId;
+
+ var creatorId = channel.CreatorAccountId;
+ if (creatorId == null || creatorId.Value <= 0)
+ return null;
+
+ return accounts.Any(a => a.Id == creatorId.Value) ? creatorId.Value : null;
+ }
+
private async Task ReloadPresetsAsync()
{
loadingPresets = true;
@@ -218,12 +230,6 @@
if (running)
return;
- if (selectedAccountId <= 0)
- {
- Snackbar.Add("请选择一个执行账号", Severity.Warning);
- return;
- }
-
var usernames = ParseUsernames(usernamesText);
if (usernames.Count == 0)
{
@@ -258,6 +264,16 @@
{
foreach (var ch in channels)
{
+ var executeAccountId = ResolveExecuteAccountId(ch);
+ if (executeAccountId == null)
+ {
+ done += usernames.Count;
+ failed += usernames.Count;
+ failures.Add($"{ch.Title}:无可用执行账号(将使用创建账号执行;但该频道无创建账号/创建账号不可用)");
+ StateHasChanged();
+ continue;
+ }
+
foreach (var username in usernames)
{
_cts.Token.ThrowIfCancellationRequested();
@@ -267,7 +283,7 @@
try
{
- var result = await ChannelService.InviteUserAsync(selectedAccountId, ch.TelegramId, username);
+ var result = await ChannelService.InviteUserAsync(executeAccountId.Value, ch.TelegramId, username);
if (!result.Success)
{
failed++;
diff --git a/src/TelegramPanel.Web/Components/Pages/AccountCategories.razor b/src/TelegramPanel.Web/Components/Pages/AccountCategories.razor
index 398a32d..d0f0917 100644
--- a/src/TelegramPanel.Web/Components/Pages/AccountCategories.razor
+++ b/src/TelegramPanel.Web/Components/Pages/AccountCategories.razor
@@ -19,6 +19,8 @@
+
分类名称
颜色
描述
+ 排除操作
账号数量
操作
@@ -51,6 +54,16 @@
@(context.Description ?? "-")
+
+ @if (context.ExcludeFromOperations)
+ {
+ 是
+ }
+ else
+ {
+ -
+ }
+
@context.Accounts.Count
categories = new();
private bool loading = true;
@@ -215,6 +229,7 @@
Name = newCategoryName,
Color = newCategoryColor.Value,
Description = newCategoryDesc,
+ ExcludeFromOperations = newCategoryExcludeFromOperations,
CreatedAt = DateTime.UtcNow
};
@@ -226,6 +241,7 @@
newCategoryName = "";
newCategoryColor = "#1976d2";
newCategoryDesc = "";
+ newCategoryExcludeFromOperations = false;
}
catch (Exception ex)
{
@@ -290,6 +306,7 @@
category.Name = model.Name.Trim();
category.Color = model.Color;
category.Description = model.Description;
+ category.ExcludeFromOperations = model.ExcludeFromOperations;
await CategoryManagement.UpdateCategoryAsync(category);
await LoadCategories();
diff --git a/src/TelegramPanel.Web/Components/Pages/BotChannelsHome.razor b/src/TelegramPanel.Web/Components/Pages/BotChannelsHome.razor
index c3d0225..1157541 100644
--- a/src/TelegramPanel.Web/Components/Pages/BotChannelsHome.razor
+++ b/src/TelegramPanel.Web/Components/Pages/BotChannelsHome.razor
@@ -65,7 +65,9 @@
导出邀请(已选)
- 批量设置管理员(已选)
+ 批量邀请成员(已选)
+ 批量设置管理员(账号执行)
+ 批量设置管理员(机器人/ID)
批量设置分类(已选)
踢出用户(已选)
@@ -450,6 +452,58 @@
}
}
+ private async Task OpenBatchInviteMembers()
+ {
+ if (selectedBotId <= 0)
+ return;
+
+ if (selected.Count == 0)
+ {
+ Snackbar.Add("请先选择频道", Severity.Info);
+ return;
+ }
+
+ var parameters = new DialogParameters
+ {
+ ["BotId"] = selectedBotId,
+ ["Channels"] = selected.ToList()
+ };
+ var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
+ var dialog = DialogService.Show("批量邀请成员", parameters, options);
+ var result = await dialog.Result;
+ if (!result.Canceled)
+ {
+ // 邀请不影响本地数据,但用户可能期望刷新一下
+ await LoadBotData();
+ }
+ }
+
+ private async Task OpenBatchSetAdminsByAccount()
+ {
+ if (selectedBotId <= 0)
+ return;
+
+ if (selected.Count == 0)
+ {
+ Snackbar.Add("请先选择频道", Severity.Info);
+ return;
+ }
+
+ var parameters = new DialogParameters
+ {
+ ["BotId"] = selectedBotId,
+ ["Channels"] = selected.ToList()
+ };
+ var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
+ var dialog = DialogService.Show("批量设置管理员(账号执行)", parameters, options);
+ var result = await dialog.Result;
+ if (!result.Canceled)
+ {
+ // 管理员设置后不影响本地数据,但用户可能期望刷新一下
+ await LoadBotData();
+ }
+ }
+
private async Task OpenBatchSetCategory()
{
if (selectedBotId <= 0)
diff --git a/src/TelegramPanel.Web/Components/Pages/ChannelCreate.razor b/src/TelegramPanel.Web/Components/Pages/ChannelCreate.razor
index 2c51de0..f31c624 100644
--- a/src/TelegramPanel.Web/Components/Pages/ChannelCreate.razor
+++ b/src/TelegramPanel.Web/Components/Pages/ChannelCreate.razor
@@ -101,7 +101,9 @@
try
{
var accountList = await AccountManagement.GetActiveAccountsAsync();
- accounts = accountList.ToList();
+ accounts = accountList
+ .Where(a => a.Category?.ExcludeFromOperations != true)
+ .ToList();
}
catch (Exception ex)
{