diff --git a/src/TelegramPanel.Core/Services/BatchTaskManagementService.cs b/src/TelegramPanel.Core/Services/BatchTaskManagementService.cs index c1542a2..1c10619 100644 --- a/src/TelegramPanel.Core/Services/BatchTaskManagementService.cs +++ b/src/TelegramPanel.Core/Services/BatchTaskManagementService.cs @@ -26,7 +26,7 @@ public class BatchTaskManagementService public async Task GetTaskAsync(int id) { - return await _batchTaskRepository.GetByIdAsync(id); + return await _batchTaskRepository.GetFreshByIdAsync(id); } public async Task> GetAllTasksAsync() @@ -63,51 +63,51 @@ public class BatchTaskManagementService public async Task UpdateTaskProgressAsync(int taskId, int completed, int failed) { - var task = await _batchTaskRepository.GetByIdAsync(taskId); + var task = await _batchTaskRepository.GetFreshByIdAsync(taskId); if (task != null) { task.Completed = completed; task.Failed = failed; - await _batchTaskRepository.UpdateAsync(task); + await _batchTaskRepository.UpdateFreshAsync(task); } } public async Task UpdateTaskConfigAsync(int taskId, string? config) { - var task = await _batchTaskRepository.GetByIdAsync(taskId); + var task = await _batchTaskRepository.GetFreshByIdAsync(taskId); if (task != null) { task.Config = config; - await _batchTaskRepository.UpdateAsync(task); + await _batchTaskRepository.UpdateFreshAsync(task); } } public async Task UpdateTaskDraftAsync(int taskId, int total, string? config) { - var task = await _batchTaskRepository.GetByIdAsync(taskId); + var task = await _batchTaskRepository.GetFreshByIdAsync(taskId); if (task != null) { if (total < 0) total = 0; task.Total = total; task.Config = config; - await _batchTaskRepository.UpdateAsync(task); + await _batchTaskRepository.UpdateFreshAsync(task); } } public async Task StartTaskAsync(int taskId) { - var task = await _batchTaskRepository.GetByIdAsync(taskId); + var task = await _batchTaskRepository.GetFreshByIdAsync(taskId); if (task != null) { task.Status = "running"; task.StartedAt = DateTime.UtcNow; - await _batchTaskRepository.UpdateAsync(task); + await _batchTaskRepository.UpdateFreshAsync(task); } } public async Task PauseTaskAsync(int taskId) { - var task = await _batchTaskRepository.GetByIdAsync(taskId); + var task = await _batchTaskRepository.GetFreshByIdAsync(taskId); if (task == null) return; @@ -115,13 +115,13 @@ public class BatchTaskManagementService { task.Status = "paused"; task.CompletedAt = null; - await _batchTaskRepository.UpdateAsync(task); + await _batchTaskRepository.UpdateFreshAsync(task); } } public async Task ResumeTaskAsync(int taskId) { - var task = await _batchTaskRepository.GetByIdAsync(taskId); + var task = await _batchTaskRepository.GetFreshByIdAsync(taskId); if (task == null) return; @@ -130,25 +130,25 @@ public class BatchTaskManagementService task.Status = "pending"; task.StartedAt = null; task.CompletedAt = null; - await _batchTaskRepository.UpdateAsync(task); + await _batchTaskRepository.UpdateFreshAsync(task); } } public async Task CompleteTaskAsync(int taskId, bool success = true) { - var task = await _batchTaskRepository.GetByIdAsync(taskId); + var task = await _batchTaskRepository.GetFreshByIdAsync(taskId); if (task == null) return; task.Status = success ? "completed" : "failed"; task.CompletedAt = DateTime.UtcNow; - await _batchTaskRepository.UpdateAsync(task); + await _batchTaskRepository.UpdateFreshAsync(task); await TrimHistoryTasksIfNeededAsync(); } public async Task DeleteTaskAsync(int id) { - var task = await _batchTaskRepository.GetByIdAsync(id); + var task = await _batchTaskRepository.GetFreshByIdAsync(id); if (task != null) { await _batchTaskRepository.DeleteAsync(task); diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs index c605c68..e12b27b 100644 --- a/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs +++ b/src/TelegramPanel.Core/Services/Telegram/AccountTelegramToolsService.cs @@ -1274,7 +1274,10 @@ public class AccountTelegramToolsService var mentionsAccount = ContainsUsernameMention(update.Message.message, currentUsername); var replyToSent = update.ReplyToMessageId == sentMessageId; - return mentionsAccount || replyToSent; + if (!mentionsAccount && !replyToSent) + return false; + + return LooksLikeVerificationChallenge(update); } private async Task BuildVerificationCandidateAsync( @@ -1325,6 +1328,78 @@ public class AccountTelegramToolsService return messageText.Contains($"@{username}", StringComparison.OrdinalIgnoreCase); } + private static bool LooksLikeVerificationChallenge(TelegramAccountMessageUpdate update) + { + if (update.Buttons.Count > 0 || update.HasVisualMedia) + return true; + + var text = update.Text; + if (text.Length == 0) + return false; + + if (ContainsAny(text, "垃圾广告", "广告", "不予处理", "已删除", "违规", "封禁") + && !ContainsAny(text, "验证", "验证码", "校验", "captcha")) + { + return false; + } + + if (ContainsAny(text, + "验证", + "验证码", + "校验", + "请选择", + "点击", + "按钮", + "完成验证", + "请回复", + "答案", + "算式", + "等于多少", + "reply", + "captcha")) + { + return true; + } + + return LooksLikeMathChallenge(text); + } + + private static bool LooksLikeMathChallenge(string text) + { + var digitCount = 0; + foreach (var ch in text) + { + if (char.IsDigit(ch)) + digitCount++; + } + + if (digitCount < 2) + return false; + + return text.IndexOf('+') >= 0 + || text.IndexOf('-') >= 0 + || text.IndexOf('*') >= 0 + || text.IndexOf('/') >= 0 + || text.Contains("×", StringComparison.Ordinal) + || text.Contains("÷", StringComparison.Ordinal) + || text.Contains("=", StringComparison.Ordinal) + || text.IndexOf('=') >= 0; + } + + private static bool ContainsAny(string text, params string[] keywords) + { + foreach (var keyword in keywords) + { + if (!string.IsNullOrWhiteSpace(keyword) + && text.Contains(keyword, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + private static List ExtractInlineButtons(Message message) { if (message.reply_markup is not ReplyInlineMarkup markup) diff --git a/src/TelegramPanel.Data/Repositories/BatchTaskRepository.cs b/src/TelegramPanel.Data/Repositories/BatchTaskRepository.cs index e33169c..839841a 100644 --- a/src/TelegramPanel.Data/Repositories/BatchTaskRepository.cs +++ b/src/TelegramPanel.Data/Repositories/BatchTaskRepository.cs @@ -12,6 +12,19 @@ public class BatchTaskRepository : Repository, IBatchTaskRepository { } + public async Task GetFreshByIdAsync(int id) + { + DetachTrackedEntity(id); + return await _dbSet.AsNoTracking().FirstOrDefaultAsync(t => t.Id == id); + } + + public async Task UpdateFreshAsync(BatchTask entity) + { + DetachTrackedEntity(entity.Id); + _dbSet.Update(entity); + await SaveChangesWithSqliteLockRetryAsync(); + } + public async Task> GetByStatusAsync(string status) { return await _dbSet @@ -55,4 +68,13 @@ public class BatchTaskRepository : Repository, IBatchTaskRepository await SaveChangesWithSqliteLockRetryAsync(cancellationToken); return staleTasks.Count; } + + private void DetachTrackedEntity(int id) + { + foreach (var entry in _context.ChangeTracker.Entries()) + { + if (entry.Entity.Id == id) + entry.State = EntityState.Detached; + } + } } diff --git a/src/TelegramPanel.Data/Repositories/IBatchTaskRepository.cs b/src/TelegramPanel.Data/Repositories/IBatchTaskRepository.cs index 68c6a5a..0a3f557 100644 --- a/src/TelegramPanel.Data/Repositories/IBatchTaskRepository.cs +++ b/src/TelegramPanel.Data/Repositories/IBatchTaskRepository.cs @@ -7,6 +7,8 @@ namespace TelegramPanel.Data.Repositories; /// public interface IBatchTaskRepository : IRepository { + Task GetFreshByIdAsync(int id); + Task UpdateFreshAsync(BatchTask entity); Task> GetByStatusAsync(string status); Task> GetRunningTasksAsync(); Task> GetRecentTasksAsync(int count = 20); diff --git a/src/TelegramPanel.Web/Components/Pages/Tasks.razor b/src/TelegramPanel.Web/Components/Pages/Tasks.razor index 4260f0c..40ae804 100644 --- a/src/TelegramPanel.Web/Components/Pages/Tasks.razor +++ b/src/TelegramPanel.Web/Components/Pages/Tasks.razor @@ -474,13 +474,6 @@ private static string GetDisplayStatus(BatchTask task) { - if (task.Status == "pending" - && IsUserChatActiveTask(task.TaskType) - && task.Total == 0) - { - return "running"; - } - if (task.Status == "failed" && task.CompletedAt.HasValue && task.Total > 0 && task.Completed >= task.Total) return "completed"; @@ -565,7 +558,7 @@ private bool CanPauseTask(BatchTask task) { var capabilities = GetTaskDefinition(task)?.Definition.TaskCenter; - return capabilities?.CanPause == true && HasStatus(task, "running"); + return capabilities?.CanPause == true && task.Status is "running" or "pending"; } private bool CanResumeTask(BatchTask task) diff --git a/src/TelegramPanel.Web/Services/TelegramPanelAiService.cs b/src/TelegramPanel.Web/Services/TelegramPanelAiService.cs index a99f0e1..03eee10 100644 --- a/src/TelegramPanel.Web/Services/TelegramPanelAiService.cs +++ b/src/TelegramPanel.Web/Services/TelegramPanelAiService.cs @@ -44,11 +44,16 @@ public sealed class TelegramPanelAiService : ITelegramPanelAiService var model = settings.ResolveModel(request.Model)!; var buttons = request.Buttons ?? Array.Empty(); var buttonLines = buttons.Select(x => $"{x.Index}. {x.Text}").ToArray(); - var systemPrompt = - "你是 Telegram 验证助手。请根据消息文本、按钮列表和可选图片,输出唯一 JSON 决策。" + - "只允许输出 JSON,不要输出 Markdown,不要解释。JSON 格式:" + - "{\"mode\":\"click_button\"|\"reply_text\",\"button_index\":0,\"reply_text\":null,\"reason\":\"30字内\"}。" + - "如果需要点击按钮,mode=click_button,button_index 必须是 0 基索引;如果更适合直接回答,mode=reply_text,reply_text 必须填写最终回复内容。"; + var hasButtons = buttonLines.Length > 0; + var systemPrompt = hasButtons + ? "你是 Telegram 验证助手。当前消息存在可点击按钮。请根据消息文本、按钮列表和可选图片,只输出唯一 JSON 决策。" + + "只允许输出 JSON,不要输出 Markdown,不要解释。JSON 格式:" + + "{\"mode\":\"click_button\",\"button_index\":0,\"reply_text\":null,\"reason\":\"30字内\"}。" + + "注意:当前存在按钮时,不允许返回 reply_text,必须返回 click_button,button_index 必须是 0 基索引。" + : "你是 Telegram 验证助手。请根据消息文本、按钮列表和可选图片,输出唯一 JSON 决策。" + + "只允许输出 JSON,不要输出 Markdown,不要解释。JSON 格式:" + + "{\"mode\":\"click_button\"|\"reply_text\",\"button_index\":0,\"reply_text\":null,\"reason\":\"30字内\"}。" + + "如果需要点击按钮,mode=click_button,button_index 必须是 0 基索引;如果更适合直接回答,mode=reply_text,reply_text 必须填写最终回复内容。"; var text = new StringBuilder(); text.AppendLine("请处理下面这条 Telegram 验证消息。"); diff --git a/src/TelegramPanel.Web/Services/UserChatActiveAiVerificationService.cs b/src/TelegramPanel.Web/Services/UserChatActiveAiVerificationService.cs index 3723445..3629b8c 100644 --- a/src/TelegramPanel.Web/Services/UserChatActiveAiVerificationService.cs +++ b/src/TelegramPanel.Web/Services/UserChatActiveAiVerificationService.cs @@ -64,17 +64,23 @@ public sealed class UserChatActiveAiVerificationService if (replyText.Length == 0) return (false, "AI 返回了 reply_text,但内容为空", null); - var reply = await _accountTools.SendMessageToResolvedChatAsync( - account.Id, - target, - replyText, - replyToMessageId: candidate.MessageId, - cancellationToken: cancellationToken); + var mappedButton = TryMatchButtonByReplyText(candidate.Buttons, replyText); + if (mappedButton != null) + { + var mappedClick = await _accountTools.ClickInlineButtonAsync( + account.Id, + target, + candidate.MessageId, + mappedButton.CallbackData, + cancellationToken); - if (!reply.Success) - return (false, reply.Error, null); + if (!mappedClick.Success) + return (false, mappedClick.Error, null); - return (true, null, $"AI 文本回复:{replyText}"); + return (true, null, $"AI 文本映射按钮:{mappedButton.Text}"); + } + + return (false, $"验证码消息存在按钮,但 AI 返回了文本:{replyText}", null); } var button = candidate.Buttons.FirstOrDefault(x => x.Index == decision.ButtonIndex); @@ -134,4 +140,40 @@ public sealed class UserChatActiveAiVerificationService return (true, null, $"AI 文本回复:{replyContent}"); } + + private static TelegramInlineButtonOption? TryMatchButtonByReplyText( + IReadOnlyList buttons, + string replyText) + { + var normalizedReply = NormalizeButtonText(replyText); + if (normalizedReply.Length == 0) + return null; + + foreach (var button in buttons) + { + if (NormalizeButtonText(button.Text) == normalizedReply) + return button; + } + + foreach (var button in buttons) + { + var normalizedButton = NormalizeButtonText(button.Text); + if (normalizedButton.Contains(normalizedReply, StringComparison.OrdinalIgnoreCase) + || normalizedReply.Contains(normalizedButton, StringComparison.OrdinalIgnoreCase)) + { + return button; + } + } + + return null; + } + + private static string NormalizeButtonText(string? value) + { + return (value ?? string.Empty) + .Trim() + .Replace(" ", string.Empty, StringComparison.Ordinal) + .Replace("\r", string.Empty, StringComparison.Ordinal) + .Replace("\n", string.Empty, StringComparison.Ordinal); + } }