fix: tighten AI verification and task pause state
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user