feat(tasks): 持续任务支持编辑与重新运行
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user