fix: tighten AI verification and task pause state

This commit is contained in:
meoacgx committed 2026-03-10 05:04:24 +08:00
1 parent 322a1bb164
commit df096ec6aa
7 files changed
+178 -39

No files matched your search

@@ -26,7 +26,7 @@ public class BatchTaskManagementService
public async Task<BatchTask?> GetTaskAsync(int id)
{
return await _batchTaskRepository.GetByIdAsync(id);
return await _batchTaskRepository.GetFreshByIdAsync(id);
}
public async Task<IEnumerable<BatchTask>> 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);
@@ -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<TelegramVerificationMessageCandidate?> 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<TelegramInlineButtonOption> ExtractInlineButtons(Message message)
{
if (message.reply_markup is not ReplyInlineMarkup markup)
@@ -12,6 +12,19 @@ public class BatchTaskRepository : Repository<BatchTask>, IBatchTaskRepository
{
}
public async Task<BatchTask?> 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<IEnumerable<BatchTask>> GetByStatusAsync(string status)
{
return await _dbSet
@@ -55,4 +68,13 @@ public class BatchTaskRepository : Repository<BatchTask>, IBatchTaskRepository
await SaveChangesWithSqliteLockRetryAsync(cancellationToken);
return staleTasks.Count;
}
private void DetachTrackedEntity(int id)
{
foreach (var entry in _context.ChangeTracker.Entries<BatchTask>())
{
if (entry.Entity.Id == id)
entry.State = EntityState.Detached;
}
}
}
@@ -7,6 +7,8 @@ namespace TelegramPanel.Data.Repositories;
/// </summary>
public interface IBatchTaskRepository : IRepository<BatchTask>
{
Task<BatchTask?> GetFreshByIdAsync(int id);
Task UpdateFreshAsync(BatchTask entity);
Task<IEnumerable<BatchTask>> GetByStatusAsync(string status);
Task<IEnumerable<BatchTask>> GetRunningTasksAsync();
Task<IEnumerable<BatchTask>> GetRecentTasksAsync(int count = 20);
@@ -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)
@@ -44,11 +44,16 @@ public sealed class TelegramPanelAiService : ITelegramPanelAiService
var model = settings.ResolveModel(request.Model)!;
var buttons = request.Buttons ?? Array.Empty<TelegramPanelAiButtonOption>();
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 验证消息。");
@@ -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<TelegramInlineButtonOption> 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);
}
}