feat(tasks): 持续任务支持编辑与重新运行

This commit is contained in:
meoacgx committed 2026-03-06 03:00:01 +08:00
1 parent 365147c17f
commit 4e4c04ba41
4 files changed
+339

No files matched your search

@@ -68,6 +68,18 @@ public class BatchTaskManagementService
}
}
public async Task UpdateTaskDraftAsync(int taskId, int total, string? config)
{
var task = await _batchTaskRepository.GetByIdAsync(taskId);
if (task != null)
{
if (total < 0) total = 0;
task.Total = total;
task.Config = config;
await _batchTaskRepository.UpdateAsync(task);
}
}
public async Task StartTaskAsync(int taskId)
{
var task = await _batchTaskRepository.GetByIdAsync(taskId);
@@ -0,0 +1,133 @@
@namespace TelegramPanel.Web.Components.Dialogs
@inject BatchTaskManagementService TaskManagement
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using TelegramPanel.Core.BatchTasks
@using TelegramPanel.Data.Entities
@using TelegramPanel.Modules
@using Microsoft.AspNetCore.Components
<MudDialog>
<DialogContent>
@if (loading)
{
<MudStack Spacing="2">
<MudProgressCircular Indeterminate="true" Size="Size.Small" Color="Color.Primary" />
<MudText Typo="Typo.caption">正在加载任务配置...</MudText>
</MudStack>
}
else if (task == null)
{
<MudAlert Severity="Severity.Error" Variant="Variant.Outlined" Dense="true">
任务不存在或已被删除。
</MudAlert>
}
else if (!string.Equals(task.TaskType, BatchTaskTypes.UserChatActive, StringComparison.OrdinalIgnoreCase))
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
仅支持编辑“账号持续活跃”任务。
</MudAlert>
}
else if (string.Equals(task.Status, "running", StringComparison.OrdinalIgnoreCase))
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true">
该任务正在执行中,请先取消后再编辑。
</MudAlert>
}
else
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Dense="true">
编辑后会更新当前任务配置。建议编辑完成后使用“重新运行”创建新任务执行。
</MudAlert>
<UserChatActiveTaskEditor Draft="draft" DraftChanged="OnDraftChanged" InitialConfigJson="@task.Config" />
@if (!string.IsNullOrWhiteSpace(draft.ValidationError))
{
<MudText Typo="Typo.caption" Color="Color.Error">@draft.ValidationError</MudText>
}
}
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Text" Color="Color.Default" OnClick="Cancel" Disabled="@saving">关闭</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Save" Disabled="@(saving || !CanSave)">
@if (saving)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
<span>保存中...</span>
}
else
{
<span>保存配置</span>
}
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public int TaskId { get; set; }
private BatchTask? task;
private bool loading = true;
private bool saving;
private ModuleTaskDraft draft = new(0, null, false, null);
private bool CanSave =>
task != null
&& string.Equals(task.TaskType, BatchTaskTypes.UserChatActive, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(task.Status, "running", StringComparison.OrdinalIgnoreCase)
&& draft.CanSubmit
&& !string.IsNullOrWhiteSpace(draft.Config);
protected override async Task OnInitializedAsync()
{
try
{
if (TaskId > 0)
task = await TaskManagement.GetTaskAsync(TaskId);
}
finally
{
loading = false;
}
}
private void OnDraftChanged(ModuleTaskDraft value)
{
draft = value;
}
private async Task Save()
{
if (!CanSave || task == null || saving)
return;
bool? confirm = await DialogService.ShowMessageBox(
"确认保存",
$"将更新任务 #{task.Id} 的配置,是否继续?",
yesText: "保存", cancelText: "取消");
if (confirm != true)
return;
saving = true;
try
{
var config = draft.Config?.Trim();
await TaskManagement.UpdateTaskDraftAsync(task.Id, draft.Total < 0 ? 0 : draft.Total, config);
Snackbar.Add($"任务 #{task.Id} 配置已更新", Severity.Success);
MudDialog.Close(DialogResult.Ok(task.Id));
}
catch (Exception ex)
{
Snackbar.Add($"保存失败:{ex.Message}", Severity.Error);
}
finally
{
saving = false;
}
}
private void Cancel() => MudDialog.Close();
}
@@ -85,9 +85,11 @@
@code {
[Parameter] public ModuleTaskDraft Draft { get; set; }
[Parameter] public EventCallback<ModuleTaskDraft> DraftChanged { get; set; }
[Parameter] public string? InitialConfigJson { get; set; }
private List<AccountCategory> categories = new();
private bool loading = true;
private bool initializedFromConfig;
private int selectedCategoryId;
private string targetsText = "";
@@ -102,6 +104,8 @@
protected override async Task OnInitializedAsync()
{
ApplyInitialConfigIfNeeded();
try
{
categories = (await CategoryManagement.GetAllCategoriesAsync())
@@ -119,6 +123,41 @@
}
}
private void ApplyInitialConfigIfNeeded()
{
if (initializedFromConfig)
return;
initializedFromConfig = true;
var raw = (InitialConfigJson ?? string.Empty).Trim();
if (raw.Length == 0)
return;
try
{
var cfg = JsonSerializer.Deserialize<UserChatActiveTaskConfig>(raw);
if (cfg == null)
return;
if (cfg.CategoryId > 0)
selectedCategoryId = cfg.CategoryId;
targetsText = string.Join(Environment.NewLine, cfg.Targets ?? new List<string>());
dictionaryText = string.Join(Environment.NewLine, cfg.Dictionary ?? new List<string>());
delayMinMs = cfg.DelayMinMs;
delayMaxMs = cfg.DelayMaxMs;
maxMessages = cfg.MaxMessages;
accountMode = NormalizeMode(cfg.AccountMode);
targetMode = NormalizeMode(cfg.TargetMode);
messageMode = NormalizeMode(cfg.MessageMode);
}
catch
{
// 忽略旧配置解析失败,回退到默认值。
}
}
private async Task OnInputsChanged()
{
await PushDraftAsync();
@@ -209,6 +248,13 @@
|| string.Equals(value, UserChatActiveTaskModes.Queue, StringComparison.OrdinalIgnoreCase);
}
private static string NormalizeMode(string? value)
{
return string.Equals(value, UserChatActiveTaskModes.Queue, StringComparison.OrdinalIgnoreCase)
? UserChatActiveTaskModes.Queue
: UserChatActiveTaskModes.Random;
}
private static List<string> ParseTargetLines(string? text)
{
if (string.IsNullOrWhiteSpace(text))
@@ -90,6 +90,11 @@
<MudTd DataLabel="操作">
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Color="Color.Info"
OnClick="@(() => ShowTaskDetails(context))" />
@if (CanEditUserChatActiveTask(context))
{
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" Color="Color.Primary"
OnClick="@(() => OpenEditUserChatActiveTask(context))" />
}
@if (IsActiveStatus(GetDisplayStatus(context)))
{
<MudIconButton Icon="@Icons.Material.Filled.Cancel" Size="Size.Small" Color="Color.Warning"
@@ -149,6 +154,16 @@
<MudTd DataLabel="操作">
<MudIconButton Icon="@Icons.Material.Filled.Info" Size="Size.Small" Color="Color.Info"
OnClick="@(() => ShowTaskDetails(context))" />
@if (CanEditUserChatActiveTask(context))
{
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" Color="Color.Primary"
OnClick="@(() => OpenEditUserChatActiveTask(context))" />
}
@if (CanRerunUserChatActiveTask(context))
{
<MudIconButton Icon="@Icons.Material.Filled.Replay" Size="Size.Small" Color="Color.Secondary"
OnClick="@(() => RerunUserChatActiveTask(context))" />
}
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
OnClick="@(() => DeleteTask(context.Id))" />
</MudTd>
@@ -398,6 +413,18 @@
private static bool IsUserChatActiveTask(string taskType) =>
taskType is BatchTaskTypes.UserChatActive;
private static bool CanEditUserChatActiveTask(BatchTask task)
{
return IsUserChatActiveTask(task.TaskType)
&& !string.Equals(task.Status, "running", StringComparison.OrdinalIgnoreCase);
}
private static bool CanRerunUserChatActiveTask(BatchTask task)
{
return IsUserChatActiveTask(task.TaskType)
&& !string.Equals(task.Status, "running", StringComparison.OrdinalIgnoreCase);
}
private static List<string> ExtractBotAdminFailureLines(BatchTask task)
{
var lines = new List<string>();
@@ -632,4 +659,125 @@
if (result is { Canceled: false })
await LoadTasks();
}
private async Task OpenEditUserChatActiveTask(BatchTask task)
{
if (!CanEditUserChatActiveTask(task))
{
Snackbar.Add("该任务当前不可编辑", Severity.Warning);
return;
}
var parameters = new DialogParameters
{
["TaskId"] = task.Id
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
var dialog = DialogService.Show<TelegramPanel.Web.Components.Dialogs.EditUserChatActiveTaskDialog>($"编辑持续任务 #{task.Id}", parameters, options)!;
var result = await dialog.Result;
if (result is { Canceled: false })
await LoadTasks();
}
private async Task RerunUserChatActiveTask(BatchTask task)
{
if (!CanRerunUserChatActiveTask(task))
{
Snackbar.Add("该任务当前不可重新运行", Severity.Warning);
return;
}
bool? confirm = await DialogService.ShowMessageBox(
"确认重新运行",
$"将基于任务 #{task.Id} 的配置创建一个新任务,是否继续?",
yesText: "继续", cancelText: "取消");
if (confirm != true)
return;
try
{
var rerunConfig = BuildRerunConfig(task.Config);
var total = rerunConfig.MaxMessages > 0 ? rerunConfig.MaxMessages : 0;
var configJson = JsonSerializer.Serialize(rerunConfig, new JsonSerializerOptions { WriteIndented = true });
var created = await TaskManagement.CreateTaskAsync(new BatchTask
{
TaskType = BatchTaskTypes.UserChatActive,
Total = total,
Completed = 0,
Failed = 0,
Config = configJson
});
Snackbar.Add($"已创建重跑任务 #{created.Id}", Severity.Success);
await LoadTasks();
}
catch (Exception ex)
{
Snackbar.Add($"重新运行失败:{ex.Message}", Severity.Error);
}
}
private static UserChatActiveTaskConfig BuildRerunConfig(string? rawConfig)
{
var raw = (rawConfig ?? string.Empty).Trim();
if (raw.Length == 0)
throw new InvalidOperationException("任务配置为空,无法重新运行");
UserChatActiveTaskConfig cfg;
try
{
cfg = JsonSerializer.Deserialize<UserChatActiveTaskConfig>(raw)
?? throw new InvalidOperationException("任务配置解析结果为空");
}
catch (Exception ex)
{
throw new InvalidOperationException($"任务配置 JSON 无效:{ex.Message}");
}
if (cfg.CategoryId <= 0)
throw new InvalidOperationException("任务缺少账号分类,无法重新运行");
cfg.Targets = (cfg.Targets ?? new List<string>())
.Select(x => (x ?? string.Empty).Trim())
.Where(x => x.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
cfg.Dictionary = (cfg.Dictionary ?? new List<string>())
.Select(x => (x ?? string.Empty).Trim())
.Where(x => x.Length > 0)
.ToList();
if (cfg.Targets.Count == 0)
throw new InvalidOperationException("任务缺少目标群组/频道,无法重新运行");
if (cfg.Dictionary.Count == 0)
throw new InvalidOperationException("任务缺少词典消息,无法重新运行");
if (cfg.DelayMinMs < 0) cfg.DelayMinMs = 0;
if (cfg.DelayMaxMs < 0) cfg.DelayMaxMs = 0;
if (cfg.DelayMinMs > 600000) cfg.DelayMinMs = 600000;
if (cfg.DelayMaxMs > 600000) cfg.DelayMaxMs = 600000;
if (cfg.DelayMaxMs < cfg.DelayMinMs) cfg.DelayMaxMs = cfg.DelayMinMs;
if (cfg.MaxMessages < 0) cfg.MaxMessages = 0;
cfg.AccountMode = NormalizeModeValue(cfg.AccountMode);
cfg.TargetMode = NormalizeModeValue(cfg.TargetMode);
cfg.MessageMode = NormalizeModeValue(cfg.MessageMode);
cfg.Canceled = false;
cfg.Error = null;
cfg.RecentFailures = new List<UserChatActiveTaskRuntimeFailure>();
return cfg;
}
private static string NormalizeModeValue(string? mode)
{
return string.Equals((mode ?? string.Empty).Trim(), UserChatActiveTaskModes.Queue, StringComparison.OrdinalIgnoreCase)
? UserChatActiveTaskModes.Queue
: UserChatActiveTaskModes.Random;
}
}