From e7a67a85558bda44cf721fba68c0ed229ccb72eb Mon Sep 17 00:00:00 2001 From: meoacgx Date: Tue, 10 Mar 2026 23:40:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=BB=AD=E6=B4=BB?= =?UTF-8?q?=E8=B7=83=E5=B0=8F=E6=95=B0=E9=97=B4=E9=9A=94=E4=B8=8E=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E7=8A=B6=E6=80=81=EF=BC=8C=E9=AA=8C=E8=AF=81=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E5=B9=B6=E4=BF=9D=E6=8C=81=E4=B8=A5=E6=A0=BC=E9=97=B4?= =?UTF-8?q?=E9=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/BatchTaskManagementService.cs | 15 + src/TelegramPanel.Data/Entities/BatchTask.cs | 2 +- .../Repositories/BatchTaskRepository.cs | 2 +- .../Dialogs/UserChatActiveTaskEditor.razor | 38 ++- .../Components/Pages/Tasks.razor | 12 +- .../Services/BatchTaskBackgroundService.cs | 2 +- .../Services/UserChatActiveTaskHandler.cs | 310 ++++++++++++++---- 7 files changed, 304 insertions(+), 77 deletions(-) diff --git a/src/TelegramPanel.Core/Services/BatchTaskManagementService.cs b/src/TelegramPanel.Core/Services/BatchTaskManagementService.cs index 1c10619..3c7d243 100644 --- a/src/TelegramPanel.Core/Services/BatchTaskManagementService.cs +++ b/src/TelegramPanel.Core/Services/BatchTaskManagementService.cs @@ -146,6 +146,21 @@ public class BatchTaskManagementService await TrimHistoryTasksIfNeededAsync(); } + public async Task CancelTaskAsync(int taskId) + { + var task = await _batchTaskRepository.GetFreshByIdAsync(taskId); + if (task == null) + return; + + if (task.Status is "completed" or "failed" or "canceled") + return; + + task.Status = "canceled"; + task.CompletedAt = DateTime.UtcNow; + await _batchTaskRepository.UpdateFreshAsync(task); + await TrimHistoryTasksIfNeededAsync(); + } + public async Task DeleteTaskAsync(int id) { var task = await _batchTaskRepository.GetFreshByIdAsync(id); diff --git a/src/TelegramPanel.Data/Entities/BatchTask.cs b/src/TelegramPanel.Data/Entities/BatchTask.cs index 62f4c7b..c9754e5 100644 --- a/src/TelegramPanel.Data/Entities/BatchTask.cs +++ b/src/TelegramPanel.Data/Entities/BatchTask.cs @@ -7,7 +7,7 @@ public class BatchTask { public int Id { get; set; } public string TaskType { get; set; } = null!; // invite/set_admin/create_channel等 - public string Status { get; set; } = "pending"; // pending/running/paused/completed/failed + public string Status { get; set; } = "pending"; // pending/running/paused/completed/failed/canceled public int Total { get; set; } public int Completed { get; set; } public int Failed { get; set; } diff --git a/src/TelegramPanel.Data/Repositories/BatchTaskRepository.cs b/src/TelegramPanel.Data/Repositories/BatchTaskRepository.cs index 839841a..2b9d080 100644 --- a/src/TelegramPanel.Data/Repositories/BatchTaskRepository.cs +++ b/src/TelegramPanel.Data/Repositories/BatchTaskRepository.cs @@ -55,7 +55,7 @@ public class BatchTaskRepository : Repository, IBatchTaskRepository return 0; var staleTasks = await _dbSet - .Where(t => t.Status == "completed" || t.Status == "failed") + .Where(t => t.Status == "completed" || t.Status == "failed" || t.Status == "canceled") .OrderByDescending(t => t.CreatedAt) .ThenByDescending(t => t.Id) .Skip(keepCount) diff --git a/src/TelegramPanel.Web/Components/Dialogs/UserChatActiveTaskEditor.razor b/src/TelegramPanel.Web/Components/Dialogs/UserChatActiveTaskEditor.razor index 5e282d2..500f673 100644 --- a/src/TelegramPanel.Web/Components/Dialogs/UserChatActiveTaskEditor.razor +++ b/src/TelegramPanel.Web/Components/Dialogs/UserChatActiveTaskEditor.razor @@ -60,12 +60,14 @@ - + - + selectedCategoryIds = Array.Empty(); private string targetsText = string.Empty; private string dictionaryText = string.Empty; - private int delayMinMs = 15; - private int delayMaxMs = 45; + private decimal delayMinSeconds = 15m; + private decimal delayMaxSeconds = 45m; private int maxMessages; private bool enableAiVerification; private int verificationTimeoutSeconds = 15; @@ -222,8 +224,8 @@ selectedCategoryIds = NormalizeCategoryIds(cfg.CategoryIds, cfg.CategoryId); targetsText = string.Join(Environment.NewLine, cfg.Targets ?? new List()); dictionaryText = string.Join(Environment.NewLine, cfg.Dictionary ?? new List()); - delayMinMs = ConvertMillisecondsToSeconds(cfg.DelayMinMs); - delayMaxMs = ConvertMillisecondsToSeconds(cfg.DelayMaxMs); + delayMinSeconds = ConvertMillisecondsToSeconds(cfg.DelayMinMs); + delayMaxSeconds = ConvertMillisecondsToSeconds(cfg.DelayMaxMs); maxMessages = cfg.MaxMessages; accountMode = NormalizeMode(cfg.AccountMode); targetMode = NormalizeMode(cfg.TargetMode); @@ -343,13 +345,13 @@ } } - if (delayMinMs < 0 || delayMaxMs < 0) + if (delayMinSeconds < 0 || delayMaxSeconds < 0) { await DraftChanged.InvokeAsync(new ModuleTaskDraft(0, null, false, "间隔不能为负数")); return; } - if (delayMaxMs < delayMinMs) + if (delayMaxSeconds < delayMinSeconds) { await DraftChanged.InvokeAsync(new ModuleTaskDraft(0, null, false, "最大间隔不能小于最小间隔")); return; @@ -392,8 +394,8 @@ CategoryNames = categoryNames, Targets = targets, Dictionary = dictionary, - DelayMinMs = delayMinMs * 1000, - DelayMaxMs = delayMaxMs * 1000, + DelayMinMs = ConvertSecondsToMilliseconds(delayMinSeconds), + DelayMaxMs = ConvertSecondsToMilliseconds(delayMaxSeconds), MaxMessages = maxMessages, AccountMode = accountMode, TargetMode = targetMode, @@ -421,12 +423,20 @@ return ids; } - private static int ConvertMillisecondsToSeconds(int value) + private static decimal ConvertMillisecondsToSeconds(int value) + { + if (value <= 0) + return 0m; + + return Math.Round(value / 1000m, 3, MidpointRounding.AwayFromZero); + } + + private static int ConvertSecondsToMilliseconds(decimal value) { if (value <= 0) return 0; - return Math.Max(1, (int)Math.Ceiling(value / 1000d)); + return (int)Math.Round(value * 1000m, MidpointRounding.AwayFromZero); } private static bool IsValidMode(string? value) diff --git a/src/TelegramPanel.Web/Components/Pages/Tasks.razor b/src/TelegramPanel.Web/Components/Pages/Tasks.razor index 40ae804..1d809fc 100644 --- a/src/TelegramPanel.Web/Components/Pages/Tasks.razor +++ b/src/TelegramPanel.Web/Components/Pages/Tasks.razor @@ -45,6 +45,7 @@ 全部 已完成 失败 + 已取消 @@ -450,6 +451,7 @@ "paused" => "已暂停", "completed" => "已完成", "failed" => "失败", + "canceled" => "已取消", _ => status }; @@ -460,6 +462,7 @@ "paused" => Color.Warning, "pending" => Color.Default, "failed" => Color.Error, + "canceled" => Color.Secondary, _ => Color.Default }; @@ -469,6 +472,7 @@ "running" => Color.Primary, "paused" => Color.Warning, "failed" => Color.Error, + "canceled" => Color.Secondary, _ => Color.Default }; @@ -484,7 +488,7 @@ status is "pending" or "running" or "paused"; private static bool IsHistoryStatus(string status) => - status is "completed" or "failed"; + status is "completed" or "failed" or "canceled"; private static string GetScheduledStatusName(string status) => string.Equals((status ?? string.Empty).Trim(), ScheduledTaskStatuses.Paused, StringComparison.OrdinalIgnoreCase) @@ -594,10 +598,10 @@ private bool CanCancelTask(BatchTask task) { var status = GetDisplayStatus(task); - if (status is not ("pending" or "running")) + if (status is not ("pending" or "running" or "paused")) return false; - return !CanPauseTask(task); + return true; } private static List ExtractBotAdminFailureLines(BatchTask task) @@ -872,7 +876,7 @@ { try { - await TaskManagement.CompleteTaskAsync(id, false); + await TaskManagement.CancelTaskAsync(id); await LoadTasks(); Snackbar.Add("任务已取消", Severity.Warning); } diff --git a/src/TelegramPanel.Web/Services/BatchTaskBackgroundService.cs b/src/TelegramPanel.Web/Services/BatchTaskBackgroundService.cs index 8217592..8fab35c 100644 --- a/src/TelegramPanel.Web/Services/BatchTaskBackgroundService.cs +++ b/src/TelegramPanel.Web/Services/BatchTaskBackgroundService.cs @@ -155,7 +155,7 @@ public sealed class BatchTaskBackgroundService : BackgroundService failed = after.Failed; } - // 如果任务被用户取消(当前实现:Cancel 会把状态写成 failed),则不覆盖它 + // 如果任务被用户取消(状态变为 canceled),则不覆盖它 var latest = await taskManagement.GetTaskAsync(pending.Id); if (latest != null && latest.Status != "running") return; diff --git a/src/TelegramPanel.Web/Services/UserChatActiveTaskHandler.cs b/src/TelegramPanel.Web/Services/UserChatActiveTaskHandler.cs index adbad1b..77d2bbf 100644 --- a/src/TelegramPanel.Web/Services/UserChatActiveTaskHandler.cs +++ b/src/TelegramPanel.Web/Services/UserChatActiveTaskHandler.cs @@ -1,3 +1,5 @@ +using System.Collections.Concurrent; +using System.Diagnostics; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -30,6 +32,7 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler ValidateAndNormalizeConfig(config); config.Canceled = false; config.Error = null; + var configGate = new SemaphoreSlim(1, 1); if (config.EnableAiVerification) { @@ -55,7 +58,7 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler if (!await host.IsStillRunningAsync(cancellationToken)) { config.Canceled = true; - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); + await PersistConfigAsync(taskManagement, host.TaskId, config, configGate, cancellationToken); return; } @@ -66,7 +69,7 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler if (!await host.IsStillRunningAsync(cancellationToken)) { config.Canceled = true; - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); + await PersistConfigAsync(taskManagement, host.TaskId, config, configGate, cancellationToken); return; } @@ -87,14 +90,15 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler if (accountSlots.Count == 0) { config.Error = "没有可用的账号-目标组合(请确认账号已加入目标群组/频道)"; - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); + await PersistConfigAsync(taskManagement, host.TaskId, config, configGate, cancellationToken); throw new InvalidOperationException(config.Error); } - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); + await PersistConfigAsync(taskManagement, host.TaskId, config, configGate, cancellationToken); - var completed = 0; - var failed = 0; + var progress = new TaskProgressCounter(); + var verificationTasks = new ConcurrentDictionary(); + using var verificationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var accountQueueIndex = 0; var messageQueueIndex = 0; var targetQueueIndexByAccountId = new Dictionary(); @@ -102,6 +106,18 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler try { + async Task DelayUntilNextSendAsync(Stopwatch timer, int intervalMs) + { + if (intervalMs <= 0) + return true; + + var remaining = intervalMs - (int)timer.ElapsedMilliseconds; + if (remaining <= 0) + return true; + + return await DelayWithPauseCheckAsync(host, remaining, cancellationToken); + } + while (!cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); @@ -109,12 +125,16 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler if (!await host.IsStillRunningAsync(cancellationToken)) { config.Canceled = true; + verificationTokenSource.Cancel(); break; } - if (config.MaxMessages > 0 && completed >= config.MaxMessages) + if (config.MaxMessages > 0 && progress.Completed >= config.MaxMessages) break; + var intervalMs = NextDelayMilliseconds(config.DelayMinMs, config.DelayMaxMs); + var loopTimer = Stopwatch.StartNew(); + var accountIdx = SelectIndex(config.AccountMode, accountSlots.Count, ref accountQueueIndex); var accountSlot = accountSlots[accountIdx]; @@ -132,6 +152,7 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler if (!await host.IsStillRunningAsync(cancellationToken)) { config.Canceled = true; + verificationTokenSource.Cancel(); break; } @@ -142,25 +163,32 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler } catch (Exception ex) { - completed++; - failed++; + var completed = Interlocked.Increment(ref progress.Completed); + Interlocked.Increment(ref progress.Failed); var hadTemplateFailure = true; - AddFailure(config, accountSlot.Account, targetSlot.RawTarget, $"词典模板解析失败:{ex.Message}"); - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); + await AddFailureAndPersistAsync( + taskManagement, + host.TaskId, + config, + accountSlot.Account, + targetSlot.RawTarget, + $"词典模板解析失败:{ex.Message}", + configGate, + cancellationToken); if (ShouldPersistProgress(completed, hadTemplateFailure, lastProgressPersistAt)) { - await host.UpdateProgressAsync(completed, failed, cancellationToken); + await host.UpdateProgressAsync(completed, progress.Failed, cancellationToken); lastProgressPersistAt = DateTime.UtcNow; } if (config.MaxMessages > 0 && completed >= config.MaxMessages) break; - var templateFailDelayMs = NextDelayMilliseconds(config.DelayMinMs, config.DelayMaxMs); - if (templateFailDelayMs > 0 && !await DelayWithPauseCheckAsync(host, templateFailDelayMs, cancellationToken)) + if (!await DelayUntilNextSendAsync(loopTimer, intervalMs)) { config.Canceled = true; + verificationTokenSource.Cancel(); break; } @@ -169,25 +197,32 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler if (text.Length == 0) { - completed++; - failed++; + var completed = Interlocked.Increment(ref progress.Completed); + Interlocked.Increment(ref progress.Failed); var hadEmptyMessageFailure = true; - AddFailure(config, accountSlot.Account, targetSlot.RawTarget, "词典模板解析结果为空,无法发送"); - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); + await AddFailureAndPersistAsync( + taskManagement, + host.TaskId, + config, + accountSlot.Account, + targetSlot.RawTarget, + "词典模板解析结果为空,无法发送", + configGate, + cancellationToken); if (ShouldPersistProgress(completed, hadEmptyMessageFailure, lastProgressPersistAt)) { - await host.UpdateProgressAsync(completed, failed, cancellationToken); + await host.UpdateProgressAsync(completed, progress.Failed, cancellationToken); lastProgressPersistAt = DateTime.UtcNow; } if (config.MaxMessages > 0 && completed >= config.MaxMessages) break; - var emptyDelayMs = NextDelayMilliseconds(config.DelayMinMs, config.DelayMaxMs); - if (emptyDelayMs > 0 && !await DelayWithPauseCheckAsync(host, emptyDelayMs, cancellationToken)) + if (!await DelayUntilNextSendAsync(loopTimer, intervalMs)) { config.Canceled = true; + verificationTokenSource.Cancel(); break; } @@ -200,14 +235,22 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler text, cancellationToken: cancellationToken); - completed++; + var sendCompleted = Interlocked.Increment(ref progress.Completed); var hadFailureThisRound = false; if (!send.Success) { - failed++; + Interlocked.Increment(ref progress.Failed); hadFailureThisRound = true; - AddFailure(config, accountSlot.Account, targetSlot.RawTarget, NormalizeReason(send.Error)); + await AddFailureAndPersistAsync( + taskManagement, + host.TaskId, + config, + accountSlot.Account, + targetSlot.RawTarget, + NormalizeReason(send.Error), + configGate, + cancellationToken); if (LooksLikePeerInvalid(send.Error)) { @@ -219,80 +262,101 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler if (refresh.Success && refresh.Target != null) targetSlot.Resolved = refresh.Target; } - - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); } else if (config.EnableAiVerification) { if (!send.MessageId.HasValue || send.MessageId.Value <= 0) { - failed++; + Interlocked.Increment(ref progress.Failed); hadFailureThisRound = true; - AddFailure(config, accountSlot.Account, targetSlot.RawTarget, "消息已发送,但未获取到消息 ID,无法执行 AI 验证"); - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); + await AddFailureAndPersistAsync( + taskManagement, + host.TaskId, + config, + accountSlot.Account, + targetSlot.RawTarget, + "消息已发送,但未获取到消息 ID,无法执行 AI 验证", + configGate, + cancellationToken); } else { - var verification = await aiVerification.TryHandleAsync( + var verificationTaskId = Guid.NewGuid(); + var verificationTask = RunVerificationAsync( + aiVerification, accountSlot.Account, targetSlot.Resolved, + targetSlot.RawTarget, send.MessageId.Value, config, - cancellationToken); + taskManagement, + host, + progress, + configGate, + logger, + verificationTokenSource.Token); - if (!verification.Success) - { - failed++; - hadFailureThisRound = true; - AddFailure(config, accountSlot.Account, targetSlot.RawTarget, NormalizeReason(verification.Error)); - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); - } - else - { - logger.LogInformation( - "UserChatActive AI verification completed: taskId={TaskId}, accountId={AccountId}, target={Target}, action={Action}", - host.TaskId, - accountSlot.Account.Id, - targetSlot.RawTarget, - verification.ActionSummary ?? "(none)"); - } + verificationTasks[verificationTaskId] = verificationTask; + _ = verificationTask.ContinueWith( + _ => verificationTasks.TryRemove(verificationTaskId, out _), + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); } } - if (ShouldPersistProgress(completed, hadFailureThisRound, lastProgressPersistAt)) + if (ShouldPersistProgress(sendCompleted, hadFailureThisRound, lastProgressPersistAt)) { - await host.UpdateProgressAsync(completed, failed, cancellationToken); + await host.UpdateProgressAsync(sendCompleted, progress.Failed, cancellationToken); lastProgressPersistAt = DateTime.UtcNow; } - if (config.MaxMessages > 0 && completed >= config.MaxMessages) + if (config.MaxMessages > 0 && sendCompleted >= config.MaxMessages) break; - var delayMs = NextDelayMilliseconds(config.DelayMinMs, config.DelayMaxMs); - if (delayMs > 0 && !await DelayWithPauseCheckAsync(host, delayMs, cancellationToken)) + if (!await DelayUntilNextSendAsync(loopTimer, intervalMs)) { config.Canceled = true; + verificationTokenSource.Cancel(); break; } } + + var pendingVerifications = verificationTasks.Values.ToArray(); + if (pendingVerifications.Length > 0) + await Task.WhenAll(pendingVerifications); } catch (Exception ex) { + verificationTokenSource.Cancel(); + var pendingVerifications = verificationTasks.Values.ToArray(); + if (pendingVerifications.Length > 0) + { + try + { + await Task.WhenAll(pendingVerifications); + } + catch + { + // 忽略验证任务的二次异常,避免覆盖主异常。 + } + } + logger.LogWarning(ex, "UserChatActive task failed (taskId={TaskId})", host.TaskId); config.Error = ex.Message; - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); + await PersistConfigAsync(taskManagement, host.TaskId, config, configGate, cancellationToken); throw; } - await host.UpdateProgressAsync(completed, failed, cancellationToken); + await host.UpdateProgressAsync(progress.Completed, progress.Failed, cancellationToken); if (config.Canceled) { - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); + await PersistConfigAsync(taskManagement, host.TaskId, config, configGate, cancellationToken); return; } config.Error = null; - await taskManagement.UpdateTaskConfigAsync(host.TaskId, SerializeIndented(config)); + await PersistConfigAsync(taskManagement, host.TaskId, config, configGate, cancellationToken); } private static UserChatActiveTaskConfig DeserializeConfig(string? rawConfig) @@ -499,6 +563,140 @@ public sealed class UserChatActiveTaskHandler : IModuleTaskHandler return JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true }); } + private static async Task PersistConfigAsync( + BatchTaskManagementService taskManagement, + int taskId, + UserChatActiveTaskConfig config, + SemaphoreSlim gate, + CancellationToken cancellationToken) + { + await gate.WaitAsync(cancellationToken); + try + { + await taskManagement.UpdateTaskConfigAsync(taskId, SerializeIndented(config)); + } + finally + { + gate.Release(); + } + } + + private static async Task AddFailureAndPersistAsync( + BatchTaskManagementService taskManagement, + int taskId, + UserChatActiveTaskConfig config, + Account account, + string rawTarget, + string reason, + SemaphoreSlim gate, + CancellationToken cancellationToken) + { + await gate.WaitAsync(cancellationToken); + try + { + AddFailure(config, account, rawTarget, reason); + await taskManagement.UpdateTaskConfigAsync(taskId, SerializeIndented(config)); + } + finally + { + gate.Release(); + } + } + + private static async Task RunVerificationAsync( + UserChatActiveAiVerificationService aiVerification, + Account account, + AccountTelegramToolsService.ResolvedChatTarget target, + string rawTarget, + int sentMessageId, + UserChatActiveTaskConfig config, + BatchTaskManagementService taskManagement, + IModuleTaskExecutionHost host, + TaskProgressCounter progress, + SemaphoreSlim configGate, + ILogger logger, + CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return; + + var timeoutSeconds = Math.Clamp(config.VerificationTimeoutSeconds, 3, 300); + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds + 10)); + + try + { + var verification = await aiVerification.TryHandleAsync( + account, + target, + sentMessageId, + config, + timeoutCts.Token); + + if (!verification.Success) + { + var failed = Interlocked.Increment(ref progress.Failed); + await AddFailureAndPersistAsync( + taskManagement, + host.TaskId, + config, + account, + rawTarget, + NormalizeReason(verification.Error), + configGate, + CancellationToken.None); + await host.UpdateProgressAsync(progress.Completed, failed, CancellationToken.None); + } + else + { + logger.LogInformation( + "UserChatActive AI verification completed: taskId={TaskId}, accountId={AccountId}, target={Target}, action={Action}", + host.TaskId, + account.Id, + rawTarget, + verification.ActionSummary ?? "(none)"); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // 任务被取消时忽略验证 + } + catch (OperationCanceledException) + { + var failed = Interlocked.Increment(ref progress.Failed); + await AddFailureAndPersistAsync( + taskManagement, + host.TaskId, + config, + account, + rawTarget, + "验证处理超时", + configGate, + CancellationToken.None); + await host.UpdateProgressAsync(progress.Completed, failed, CancellationToken.None); + } + catch (Exception ex) + { + var failed = Interlocked.Increment(ref progress.Failed); + await AddFailureAndPersistAsync( + taskManagement, + host.TaskId, + config, + account, + rawTarget, + $"验证处理异常:{ex.Message}", + configGate, + CancellationToken.None); + await host.UpdateProgressAsync(progress.Completed, failed, CancellationToken.None); + } + } + + private sealed class TaskProgressCounter + { + public int Completed; + public int Failed; + } + private sealed class AccountSlot { public AccountSlot(Account account)