feat(accounts): 批量操作支持外部 Bot 启停
- 批量弹窗改为输入外部 Bot 用户名/链接,支持带参数批量 /start - 批量停用 Bot 改为账号侧拉黑 Bot(可在 Telegram 手动解除) - 新增 AccountTelegramToolsService.StartExternalBotAsync/StopExternalBotAsync
This commit is contained in:
1 parent
1dfb80cdd2
commit
04257e1e86
2 files changed
+280
-149
No files matched your search
@@ -830,6 +830,96 @@ public class AccountTelegramToolsService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用外部 Bot(向 Bot 发送 /start,可带参数)。
|
||||
/// 支持:@xxxbot、xxxbot、https://t.me/xxxbot、tg://resolve?domain=xxxbot&start=abc
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error, string? BotUsername)> StartExternalBotAsync(
|
||||
int accountId,
|
||||
string botLinkOrUsername,
|
||||
string? startParameter = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (username, startFromLink) = NormalizeTelegramBotUsername(botLinkOrUsername);
|
||||
var normalizedManualStart = NormalizeBotStartParameter(startParameter);
|
||||
var finalStart = string.IsNullOrWhiteSpace(normalizedManualStart) ? startFromLink : normalizedManualStart;
|
||||
|
||||
if (finalStart.Length > 64)
|
||||
return (false, "启动参数过长(最多 64 字符)", null);
|
||||
|
||||
var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var resolved = await client.Contacts_ResolveUsername(username);
|
||||
var user = resolved.User;
|
||||
if (user.access_hash == 0)
|
||||
return (false, "无法获取 Bot access_hash", null);
|
||||
|
||||
var inputUser = new InputUser(user.id, user.access_hash);
|
||||
var randomId = Random.Shared.NextInt64();
|
||||
await client.Messages_StartBot(
|
||||
bot: inputUser,
|
||||
peer: new InputPeerSelf(),
|
||||
random_id: randomId,
|
||||
start_param: finalStart);
|
||||
|
||||
return (true, null, "@" + username);
|
||||
}
|
||||
catch (RpcException ex) when (ex.Code == 400 && string.Equals(ex.Message, "BOT_APP_INVALID", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (false, "目标不是可启动的 Bot(BOT_APP_INVALID)", null);
|
||||
}
|
||||
catch (RpcException ex) when (ex.Code == 400 && string.Equals(ex.Message, "PEER_FLOOD", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (false, "触发风控(PEER_FLOOD),请降低频率后重试", null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var (summary, details) = MapTelegramException(ex);
|
||||
var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
|
||||
return (false, msg, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停用外部 Bot(通过拉黑 Bot 实现)。
|
||||
/// 支持:@xxxbot、xxxbot、https://t.me/xxxbot、tg://resolve?domain=xxxbot
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error, string? BotUsername)> StopExternalBotAsync(
|
||||
int accountId,
|
||||
string botLinkOrUsername,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (username, _) = NormalizeTelegramBotUsername(botLinkOrUsername);
|
||||
|
||||
var client = await GetOrCreateConnectedClientAsync(accountId, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var resolved = await client.Contacts_ResolveUsername(username);
|
||||
var user = resolved.User;
|
||||
if (user.access_hash == 0)
|
||||
return (false, "无法获取 Bot access_hash", null);
|
||||
|
||||
await client.Contacts_Block(new InputPeerUser(user.id, user.access_hash));
|
||||
return (true, null, "@" + username);
|
||||
}
|
||||
catch (RpcException ex) when (ex.Code == 400 && string.Equals(ex.Message, "USER_NOT_MUTUAL_CONTACT", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 某些账号状态下会返回该错误,按“已停用”处理可避免批量任务中断。
|
||||
return (true, null, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var (summary, details) = MapTelegramException(ex);
|
||||
var msg = string.IsNullOrWhiteSpace(details) ? summary : $"{summary}:{details}";
|
||||
return (false, msg, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeTelegramJoinUrl(string input)
|
||||
{
|
||||
var s = (input ?? string.Empty).Trim();
|
||||
@@ -867,6 +957,112 @@ public class AccountTelegramToolsService
|
||||
return s;
|
||||
}
|
||||
|
||||
private static (string Username, string StartFromLink) NormalizeTelegramBotUsername(string input)
|
||||
{
|
||||
var s = (input ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(s))
|
||||
throw new ArgumentException("Bot 用户名为空", nameof(input));
|
||||
|
||||
string startFromLink = string.Empty;
|
||||
|
||||
// tg://resolve?domain=xxxbot&start=abc
|
||||
if (s.StartsWith("tg://", StringComparison.OrdinalIgnoreCase)
|
||||
&& Uri.TryCreate(s, UriKind.Absolute, out var tgUri))
|
||||
{
|
||||
var query = ParseQueryString(tgUri.Query);
|
||||
if (query.TryGetValue("domain", out var domain) && !string.IsNullOrWhiteSpace(domain))
|
||||
s = domain.Trim();
|
||||
if (query.TryGetValue("start", out var start) && !string.IsNullOrWhiteSpace(start))
|
||||
startFromLink = NormalizeBotStartParameter(start);
|
||||
}
|
||||
|
||||
// https://t.me/xxxbot?start=abc 或 t.me/xxxbot?start=abc
|
||||
if (s.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.StartsWith("t.me/", StringComparison.OrdinalIgnoreCase)
|
||||
|| s.StartsWith("telegram.me/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var url = s.Contains("://", StringComparison.Ordinal) ? s : "https://" + s;
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
throw new ArgumentException("Bot 链接格式无效", nameof(input));
|
||||
|
||||
var path = (uri.AbsolutePath ?? string.Empty).Trim('/');
|
||||
var firstSeg = path.Split('/', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(firstSeg))
|
||||
throw new ArgumentException("Bot 链接中缺少用户名", nameof(input));
|
||||
|
||||
s = firstSeg;
|
||||
|
||||
var query = ParseQueryString(uri.Query);
|
||||
if (query.TryGetValue("start", out var start) && !string.IsNullOrWhiteSpace(start))
|
||||
startFromLink = NormalizeBotStartParameter(start);
|
||||
}
|
||||
|
||||
s = s.Trim().TrimStart('@');
|
||||
var slash = s.IndexOf('/');
|
||||
if (slash >= 0)
|
||||
s = s[..slash];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(s))
|
||||
throw new ArgumentException("Bot 用户名为空", nameof(input));
|
||||
|
||||
if (s.StartsWith("+", StringComparison.Ordinal))
|
||||
throw new ArgumentException("邀请链接不是 Bot 用户名,请输入 @xxxbot 或 t.me/xxxbot", nameof(input));
|
||||
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(s, "^[A-Za-z0-9_]{5,64}$"))
|
||||
throw new ArgumentException("Bot 用户名格式无效", nameof(input));
|
||||
|
||||
if (!s.EndsWith("bot", StringComparison.OrdinalIgnoreCase))
|
||||
throw new ArgumentException("目标看起来不是 Bot 用户名(需以 bot 结尾)", nameof(input));
|
||||
|
||||
return (s, startFromLink);
|
||||
}
|
||||
|
||||
private static string NormalizeBotStartParameter(string? input)
|
||||
{
|
||||
var s = (input ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(s))
|
||||
return string.Empty;
|
||||
|
||||
if (s.StartsWith("/start", StringComparison.OrdinalIgnoreCase))
|
||||
s = s[6..].Trim();
|
||||
|
||||
if (s.StartsWith("@", StringComparison.Ordinal))
|
||||
{
|
||||
var idx = s.IndexOf(' ');
|
||||
s = idx > 0 ? s[(idx + 1)..].Trim() : string.Empty;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ParseQueryString(string query)
|
||||
{
|
||||
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
return map;
|
||||
|
||||
var raw = query.StartsWith("?", StringComparison.Ordinal) ? query[1..] : query;
|
||||
foreach (var part in raw.Split('&', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var idx = part.IndexOf('=');
|
||||
if (idx < 0)
|
||||
{
|
||||
var kOnly = Uri.UnescapeDataString(part).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(kOnly))
|
||||
map[kOnly] = string.Empty;
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = Uri.UnescapeDataString(part[..idx]).Trim();
|
||||
var val = Uri.UnescapeDataString(part[(idx + 1)..]).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
map[key] = val;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新当前账号头像(静态图片)。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@inject AccountManagementService AccountManagement
|
||||
@inject AccountTelegramToolsService AccountTelegramTools
|
||||
@inject BotManagementService BotManagement
|
||||
@inject TelegramPanel.Core.Services.AccountRiskService RiskService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
@@ -12,14 +11,14 @@
|
||||
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
|
||||
@if (IsJoin)
|
||||
{
|
||||
<span>将使用所选账号订阅频道/加入群组,并可选批量启用 Bot。</span>
|
||||
<span>将使用所选账号订阅频道/加入群组,并可选批量启用外部 Bot(发送 /start)。</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>将使用所选账号退出群组/取消订阅频道,并可选批量停用 Bot。</span>
|
||||
<span>将使用所选账号退出群组/取消订阅频道,并可选批量停用外部 Bot(拉黑 Bot)。</span>
|
||||
}
|
||||
<MudText Typo="Typo.body2" Class="mt-1">
|
||||
支持:<b>https://t.me/xxx</b>、<b>t.me/+hash</b>、<b>@@username</b>、<b>username</b>、<b>tg://join?invite=hash</b>
|
||||
群组/频道支持:<b>https://t.me/xxx</b>、<b>t.me/+hash</b>、<b>@@username</b>、<b>username</b>、<b>tg://join?invite=hash</b>
|
||||
</MudText>
|
||||
</MudAlert>
|
||||
|
||||
@@ -33,55 +32,39 @@
|
||||
|
||||
<MudDivider />
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.subtitle2">Bot 操作(可选)</MudText>
|
||||
<MudText Typo="Typo.subtitle2">外部 Bot 操作(可选)</MudText>
|
||||
<MudChip T="string" Size="Size.Small" Color="@(IsJoin ? Color.Success : Color.Warning)">
|
||||
@(IsJoin ? "批量启用" : "批量停用")
|
||||
@(IsJoin ? "批量启用 Bot" : "批量停用 Bot")
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
|
||||
@if (bots.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">暂无 Bot,已自动跳过 Bot 批量操作。</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudCheckBox T="bool"
|
||||
@bind-Value="enableBotOperation"
|
||||
Label="同时执行 Bot 批量操作"
|
||||
Disabled="@running" />
|
||||
<MudCheckBox T="bool"
|
||||
@bind-Value="enableBotOperation"
|
||||
Label="同时执行外部 Bot 批量操作"
|
||||
Disabled="@running" />
|
||||
|
||||
@if (enableBotOperation)
|
||||
@if (enableBotOperation)
|
||||
{
|
||||
<MudTextField @bind-Value="botTargetsText"
|
||||
Label="Bot 用户名/链接列表(换行分隔)"
|
||||
Variant="Variant.Outlined"
|
||||
Lines="6"
|
||||
Disabled="@running"
|
||||
HelperText="支持用户名 xxxbot 或链接 https://t.me/xxxbot(每行一个)" />
|
||||
|
||||
@if (IsJoin)
|
||||
{
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center" Style="flex-wrap: wrap;">
|
||||
<MudText Typo="Typo.caption">已选 Bot:@selectedBotIds.Count / @bots.Count</MudText>
|
||||
<MudButton Variant="Variant.Text" Size="Size.Small" Disabled="@running" OnClick="SelectRecommendedBots">选中推荐</MudButton>
|
||||
<MudButton Variant="Variant.Text" Size="Size.Small" Disabled="@running" OnClick="SelectAllBots">全选</MudButton>
|
||||
<MudButton Variant="Variant.Text" Size="Size.Small" Disabled="@running" OnClick="ClearSelectedBots">清空</MudButton>
|
||||
</MudStack>
|
||||
|
||||
<MudPaper Outlined="true" Class="pa-2">
|
||||
<MudGrid>
|
||||
@foreach (var bot in orderedBots)
|
||||
{
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudCheckBox T="bool"
|
||||
Value="@selectedBotIds.Contains(bot.Id)"
|
||||
ValueChanged="@(v => SetBotSelection(bot.Id, v))"
|
||||
Disabled="@running"
|
||||
Label="@BuildBotLabel(bot)" />
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@if (IsJoin)
|
||||
{
|
||||
<MudTextField @bind-Value="botStartParameters"
|
||||
Label="Bot 启动参数(可选)"
|
||||
Variant="Variant.Outlined"
|
||||
HelperText="当前版本仅执行启用动作,启动参数暂不生效。"
|
||||
Disabled="@running" />
|
||||
}
|
||||
<MudTextField @bind-Value="botStartParameters"
|
||||
Label="Bot 启动参数(可选)"
|
||||
Variant="Variant.Outlined"
|
||||
Disabled="@running"
|
||||
HelperText="可只填参数(如 abc),也可填 /start abc;将对所有 Bot 复用同一参数。" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
||||
说明:停用 Bot 通过“拉黑 Bot”实现;如需恢复,可在 Telegram 内手动解除拉黑。
|
||||
</MudText>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +85,7 @@
|
||||
@(running ? "停止" : "关闭")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Submit" Disabled="@(running || accounts.Count == 0)">
|
||||
@(IsJoin ? "开始加群/订阅" : "开始退群/退订")
|
||||
@(IsJoin ? "开始加群/订阅/启用Bot" : "开始退群/退订/停用Bot")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -115,12 +98,10 @@
|
||||
[Parameter] public string Operation { get; set; } = "join"; // join|leave
|
||||
|
||||
private List<Account> accounts = new();
|
||||
private List<Bot> bots = new();
|
||||
private List<Bot> orderedBots = new();
|
||||
private string linksText = "";
|
||||
private string botStartParameters = "";
|
||||
private bool enableBotOperation;
|
||||
private HashSet<int> selectedBotIds = new();
|
||||
private string botTargetsText = "";
|
||||
private string botStartParameters = "";
|
||||
private int delayMs = 2000;
|
||||
private bool running;
|
||||
private int total;
|
||||
@@ -136,25 +117,11 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var ids = (AccountIds ?? new List<int>()).Where(x => x > 0).Distinct().ToList();
|
||||
if (ids.Count == 0)
|
||||
return;
|
||||
|
||||
var loadAccountsTask = AccountManagement.GetAllAccountsAsync();
|
||||
var loadBotsTask = BotManagement.GetAllBotsAsync();
|
||||
|
||||
await Task.WhenAll(loadAccountsTask, loadBotsTask);
|
||||
|
||||
if (ids.Count > 0)
|
||||
{
|
||||
var all = loadAccountsTask.Result.ToList();
|
||||
accounts = all.Where(a => ids.Contains(a.Id)).ToList();
|
||||
}
|
||||
|
||||
bots = loadBotsTask.Result.ToList();
|
||||
orderedBots = bots
|
||||
.OrderBy(b => b.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(b => b.Id)
|
||||
.ToList();
|
||||
|
||||
SelectRecommendedBots();
|
||||
var all = (await AccountManagement.GetAllAccountsAsync()).ToList();
|
||||
accounts = all.Where(a => ids.Contains(a.Id)).ToList();
|
||||
}
|
||||
|
||||
private async Task Submit()
|
||||
@@ -163,20 +130,20 @@
|
||||
return;
|
||||
|
||||
var links = ParseLines(linksText);
|
||||
var botTargets = ParseLines(botTargetsText);
|
||||
var targetAccounts = accounts.ToList();
|
||||
var runMembershipOps = links.Count > 0;
|
||||
var selectedBots = GetSelectedBots();
|
||||
var runBotOps = enableBotOperation && selectedBots.Count > 0;
|
||||
var runBotOps = enableBotOperation && botTargets.Count > 0;
|
||||
|
||||
if (enableBotOperation && bots.Count > 0 && selectedBots.Count == 0)
|
||||
if (enableBotOperation && botTargets.Count == 0)
|
||||
{
|
||||
Snackbar.Add("已启用 Bot 批量操作,请至少选择 1 个 Bot", Severity.Warning);
|
||||
Snackbar.Add("已启用 Bot 批量操作,请至少输入一个 Bot 用户名/链接", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!runMembershipOps && !runBotOps)
|
||||
{
|
||||
Snackbar.Add("请至少输入一个频道/群链接,或选择 Bot 批量操作", Severity.Warning);
|
||||
Snackbar.Add("请至少输入一个频道/群链接,或启用 Bot 批量操作", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -214,17 +181,17 @@
|
||||
}
|
||||
|
||||
var membershipTotal = runMembershipOps ? targetAccounts.Count * links.Count : 0;
|
||||
var botTotal = runBotOps ? selectedBots.Count : 0;
|
||||
var botTotal = runBotOps ? targetAccounts.Count * botTargets.Count : 0;
|
||||
var confirmLines = new List<string>();
|
||||
|
||||
if (membershipTotal > 0)
|
||||
confirmLines.Add($"{(IsJoin ? "加群/订阅" : "退群/退订")}:{targetAccounts.Count} 个账号 × {links.Count} 个目标(共 {membershipTotal} 次操作)");
|
||||
|
||||
if (botTotal > 0)
|
||||
confirmLines.Add($"{(IsJoin ? "启用" : "停用")} Bot:{selectedBots.Count} 个");
|
||||
confirmLines.Add($"{(IsJoin ? "启用" : "停用")}外部 Bot:{targetAccounts.Count} 个账号 × {botTargets.Count} 个 Bot(共 {botTotal} 次操作)");
|
||||
|
||||
if (IsJoin && runBotOps && !string.IsNullOrWhiteSpace(botStartParameters))
|
||||
confirmLines.Add($"启动参数:{botStartParameters.Trim()}(当前版本暂不生效)");
|
||||
confirmLines.Add($"启动参数:{botStartParameters.Trim()}");
|
||||
|
||||
bool? confirm = await DialogService.ShowMessageBox(
|
||||
"确认执行",
|
||||
@@ -234,9 +201,6 @@
|
||||
if (confirm != true)
|
||||
return;
|
||||
|
||||
if (IsJoin && runBotOps && !string.IsNullOrWhiteSpace(botStartParameters))
|
||||
Snackbar.Add("已填写启动参数,但当前版本仅执行 Bot 启用,参数暂不生效。", Severity.Info);
|
||||
|
||||
running = true;
|
||||
total = membershipTotal + botTotal;
|
||||
done = 0;
|
||||
@@ -274,9 +238,7 @@
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
var wait = delayMs;
|
||||
if (wait < 0) wait = 0;
|
||||
if (wait > 10000) wait = 10000;
|
||||
var wait = ClampDelayMs(delayMs);
|
||||
var jitter = _rand.Next(0, 350);
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(wait + jitter), _cts.Token);
|
||||
}
|
||||
@@ -285,28 +247,45 @@
|
||||
|
||||
if (runBotOps)
|
||||
{
|
||||
foreach (var bot in selectedBots)
|
||||
foreach (var a in targetAccounts)
|
||||
{
|
||||
_cts.Token.ThrowIfCancellationRequested();
|
||||
foreach (var botTarget in botTargets)
|
||||
{
|
||||
_cts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
currentHint = $"{(IsJoin ? "启用" : "停用")} Bot => {bot.Name}";
|
||||
StateHasChanged();
|
||||
currentHint = $"{a.DisplayPhone} => {botTarget}";
|
||||
StateHasChanged();
|
||||
|
||||
(bool Success, string? Error, string? BotUsername) result;
|
||||
if (IsJoin)
|
||||
{
|
||||
result = await AccountTelegramTools.StartExternalBotAsync(
|
||||
a.Id,
|
||||
botTarget,
|
||||
botStartParameters,
|
||||
_cts.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await AccountTelegramTools.StopExternalBotAsync(
|
||||
a.Id,
|
||||
botTarget,
|
||||
_cts.Token);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await BotManagement.SetBotActiveStatusAsync(bot.Id, IsJoin);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failed++;
|
||||
failures.Add($"{(IsJoin ? "启用" : "停用")} Bot {bot.Name} 失败:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
done++;
|
||||
}
|
||||
if (!result.Success)
|
||||
{
|
||||
failed++;
|
||||
failures.Add($"{a.DisplayPhone} => {botTarget}:{result.Error}");
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
StateHasChanged();
|
||||
|
||||
var wait = ClampDelayMs(delayMs);
|
||||
var jitter = _rand.Next(0, 350);
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(wait + jitter), _cts.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,55 +329,11 @@
|
||||
MudDialog.Close();
|
||||
}
|
||||
|
||||
private List<Bot> GetSelectedBots()
|
||||
private static int ClampDelayMs(int value)
|
||||
{
|
||||
if (selectedBotIds.Count == 0 || bots.Count == 0)
|
||||
return new List<Bot>();
|
||||
|
||||
return bots
|
||||
.Where(b => selectedBotIds.Contains(b.Id))
|
||||
.OrderBy(b => b.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(b => b.Id)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void SelectRecommendedBots()
|
||||
{
|
||||
if (bots.Count == 0)
|
||||
{
|
||||
selectedBotIds.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
selectedBotIds = bots
|
||||
.Where(b => IsJoin ? !b.IsActive : b.IsActive)
|
||||
.Select(b => b.Id)
|
||||
.ToHashSet();
|
||||
}
|
||||
|
||||
private void SelectAllBots()
|
||||
{
|
||||
selectedBotIds = bots.Select(b => b.Id).ToHashSet();
|
||||
}
|
||||
|
||||
private void ClearSelectedBots()
|
||||
{
|
||||
selectedBotIds.Clear();
|
||||
}
|
||||
|
||||
private void SetBotSelection(int botId, bool selected)
|
||||
{
|
||||
if (selected)
|
||||
selectedBotIds.Add(botId);
|
||||
else
|
||||
selectedBotIds.Remove(botId);
|
||||
}
|
||||
|
||||
private static string BuildBotLabel(Bot bot)
|
||||
{
|
||||
var username = string.IsNullOrWhiteSpace(bot.Username) ? "-" : $"@{bot.Username}";
|
||||
var status = bot.IsActive ? "启用" : "停用";
|
||||
return $"{bot.Name}({username},当前{status})";
|
||||
if (value < 0) return 0;
|
||||
if (value > 10000) return 10000;
|
||||
return value;
|
||||
}
|
||||
|
||||
private static List<string> ParseLines(string? text)
|
||||
|
||||
Reference in new issue
Block a user