diff --git a/Directory.Build.props b/Directory.Build.props
index 1902e39..c27c5d0 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -5,10 +5,10 @@
- 1.31.5
- 1.31.5.0
- 1.31.5.0
- 1.31.5
+ 1.31.6
+ 1.31.6.0
+ 1.31.6.0
+ 1.31.6
Telegram Panel Contributors
diff --git a/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs b/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs
index aa4d8ce..03a4bf9 100644
--- a/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs
+++ b/src/TelegramPanel.Core/Services/Telegram/AccountImportService.cs
@@ -242,10 +242,22 @@ public class AccountImportService
// ZipFile.ExtractToDirectory 会直接抛异常。这里改为手动解压并容错处理。
await ExtractZipToDirectorySafeAsync(tempZipPath, extractDir);
- var jsonFiles = Directory.EnumerateFiles(extractDir, "*.json", SearchOption.AllDirectories).ToList();
+ var tdataDirs = FindTdataDirectories(extractDir);
+ var allJsonFiles = Directory.EnumerateFiles(extractDir, "*.json", SearchOption.AllDirectories).ToList();
+ var jsonFiles = allJsonFiles
+ .Where(path => !IsPathInsideAnyDirectory(path, tdataDirs))
+ .ToList();
+
+ var skippedTdataJsonCount = allJsonFiles.Count - jsonFiles.Count;
+ if (skippedTdataJsonCount > 0)
+ {
+ _logger.LogInformation(
+ "Skipping {Count} json files inside tdata directories during zip import",
+ skippedTdataJsonCount);
+ }
+
if (jsonFiles.Count == 0)
{
- var tdataDirs = FindTdataDirectories(extractDir);
if (tdataDirs.Count > 0)
{
results.AddRange(await ImportFromTdataDirectoriesAsync(tdataDirs, categoryId, twoFactorPassword));
@@ -254,7 +266,7 @@ public class AccountImportService
return results;
}
- results.Add(new ImportResult(false, null, null, null, null, "压缩包内未找到任何 .json 文件"));
+ results.Add(new ImportResult(false, null, null, null, null, "压缩包内未找到任何账号配置 json 或可识别的 tdata 目录"));
return results;
}
@@ -676,6 +688,26 @@ public class AccountImportService
&& !string.IsNullOrWhiteSpace(apiHash);
}
+ private static bool IsPathInsideAnyDirectory(string filePath, IReadOnlyCollection directories)
+ {
+ if (directories.Count == 0)
+ return false;
+
+ var fullFilePath = Path.GetFullPath(filePath);
+ foreach (var directory in directories)
+ {
+ var fullDirectoryPath = Path.GetFullPath(directory);
+ var fullDirectoryPathWithSep = fullDirectoryPath.EndsWith(Path.DirectorySeparatorChar)
+ ? fullDirectoryPath
+ : fullDirectoryPath + Path.DirectorySeparatorChar;
+
+ if (fullFilePath.StartsWith(fullDirectoryPathWithSep, StringComparison.OrdinalIgnoreCase))
+ return true;
+ }
+
+ return false;
+ }
+
private static List FindTdataDirectories(string rootDirectory)
{
var result = new List();
diff --git a/src/TelegramPanel.Web/Components/Pages/Tasks.razor b/src/TelegramPanel.Web/Components/Pages/Tasks.razor
index 2e01ec6..bb13b36 100644
--- a/src/TelegramPanel.Web/Components/Pages/Tasks.razor
+++ b/src/TelegramPanel.Web/Components/Pages/Tasks.razor
@@ -1,12 +1,15 @@
@page "/tasks"
@inject BatchTaskManagementService TaskManagement
@inject TelegramPanel.Web.Modules.ModuleContributionRegistry Contributions
+@inject IEnumerable TaskRerunBuilders
@inject PanelTimeZoneService TimeZone
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@using TelegramPanel.Data.Entities
@using TelegramPanel.Core.BatchTasks
@using System.Text.Json
+@using TelegramPanel.Modules
+@using TelegramPanel.Web.Modules
@using TelegramPanel.Web.Services
@implements IDisposable
@@ -90,20 +93,20 @@
- @if (CanPauseUserChatActiveTask(context))
+ @if (CanPauseTask(context))
{
}
- @if (CanResumeUserChatActiveTask(context))
+ @if (CanResumeTask(context))
{
}
- @if (CanEditUserChatActiveTask(context))
+ @if (CanEditTask(context))
{
+ OnClick="@(() => OpenEditTask(context))" />
}
@if (CanCancelTask(context))
{
@@ -166,10 +169,10 @@
- @if (CanEditUserChatActiveTask(context))
+ @if (CanEditTask(context))
{
+ OnClick="@(() => OpenEditTask(context))" />
}
@if (CanRerunTask(context))
{
@@ -441,21 +444,54 @@
private static bool IsUserChatActiveTask(string taskType) =>
string.Equals((taskType ?? string.Empty).Trim(), BatchTaskTypes.UserChatActive, StringComparison.OrdinalIgnoreCase);
- private static bool CanPauseUserChatActiveTask(BatchTask task)
+ private RegisteredTaskDefinition? GetTaskDefinition(BatchTask task)
{
- return IsUserChatActiveTask(task.TaskType)
- && string.Equals(GetDisplayStatus(task), "running", StringComparison.OrdinalIgnoreCase);
+ var taskType = (task.TaskType ?? string.Empty).Trim();
+ if (taskType.Length == 0)
+ return null;
+
+ return Contributions.TaskTypeToDefinition.TryGetValue(taskType, out var definition)
+ ? definition
+ : null;
}
- private static bool CanResumeUserChatActiveTask(BatchTask task)
+ private static string? NormalizeNullableText(string? value)
{
- return IsUserChatActiveTask(task.TaskType)
- && string.Equals(GetDisplayStatus(task), "paused", StringComparison.OrdinalIgnoreCase);
+ var text = (value ?? string.Empty).Trim();
+ return text.Length == 0 ? null : text;
}
- private static bool CanEditUserChatActiveTask(BatchTask task)
+ private static bool HasStatus(BatchTask task, string status)
{
- return IsUserChatActiveTask(task.TaskType);
+ return string.Equals(GetDisplayStatus(task), status, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private bool CanPauseTask(BatchTask task)
+ {
+ var capabilities = GetTaskDefinition(task)?.Definition.TaskCenter;
+ return capabilities?.CanPause == true && HasStatus(task, "running");
+ }
+
+ private bool CanResumeTask(BatchTask task)
+ {
+ var capabilities = GetTaskDefinition(task)?.Definition.TaskCenter;
+ return capabilities?.CanResume == true && HasStatus(task, "paused");
+ }
+
+ private bool CanEditTask(BatchTask task)
+ {
+ var definition = GetTaskDefinition(task);
+ if (definition == null)
+ return false;
+
+ var capabilities = definition.Definition.TaskCenter;
+ if (!capabilities.CanEdit)
+ return false;
+
+ if (ModuleTaskEditorComponentResolver.ResolveEditEditor(definition) == null)
+ return false;
+
+ return !HasStatus(task, "running") || capabilities.AutoPauseBeforeEdit;
}
private bool CanRerunTask(BatchTask task)
@@ -463,18 +499,16 @@
if (!IsHistoryStatus(GetDisplayStatus(task)))
return false;
- var taskType = (task.TaskType ?? string.Empty).Trim();
- return taskType.Length > 0 && Contributions.TaskTypeToDefinition.ContainsKey(taskType);
+ return GetTaskDefinition(task)?.Definition.TaskCenter.CanRerun == true;
}
- private static bool CanCancelTask(BatchTask task)
+ private bool CanCancelTask(BatchTask task)
{
var status = GetDisplayStatus(task);
if (status is not ("pending" or "running"))
return false;
- // 持续活跃任务运行中优先使用“暂停”,避免直接进入历史任务导致不能快速调整配置。
- return !CanPauseUserChatActiveTask(task);
+ return !CanPauseTask(task);
}
private static List ExtractBotAdminFailureLines(BatchTask task)
@@ -692,7 +726,7 @@
private async Task PauseTask(BatchTask task)
{
- if (!CanPauseUserChatActiveTask(task))
+ if (!CanPauseTask(task))
{
Snackbar.Add("该任务当前不可暂停", Severity.Warning);
return;
@@ -700,7 +734,7 @@
bool? result = await DialogService.ShowMessageBox(
"确认暂停",
- $"确定要暂停任务 #{task.Id} 吗?暂停后可直接编辑配置并继续运行。",
+ $"确定要暂停任务 #{task.Id} 吗?暂停后可编辑配置并稍后继续运行。",
yesText: "暂停", cancelText: "返回");
if (result != true)
@@ -720,7 +754,7 @@
private async Task ResumeTask(BatchTask task)
{
- if (!CanResumeUserChatActiveTask(task))
+ if (!CanResumeTask(task))
{
Snackbar.Add("该任务当前不可继续运行", Severity.Warning);
return;
@@ -799,7 +833,7 @@
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
var dialog = DialogService.Show("确认清理", parameters, options)!;
var result = await dialog.Result;
- if (result.Canceled || result.Data is not string mode)
+ if (result is null || result.Canceled || result.Data is not string mode)
return;
var historyOnly = string.Equals(mode, "history", StringComparison.OrdinalIgnoreCase);
@@ -826,8 +860,8 @@
return;
}
- foreach (var task in historyTaskList)
- await TaskManagement.DeleteTaskAsync(task.Id);
+ foreach (var item in historyTaskList)
+ await TaskManagement.DeleteTaskAsync(item.Id);
Snackbar.Add($"已清理 {historyTaskList.Count} 条历史任务记录", Severity.Success);
}
@@ -836,8 +870,8 @@
foreach (var activeTask in allTasks.Where(t => t.Status is "pending" or "running" or "paused"))
await TaskManagement.CompleteTaskAsync(activeTask.Id, success: false);
- foreach (var task in allTasks)
- await TaskManagement.DeleteTaskAsync(task.Id);
+ foreach (var item in allTasks)
+ await TaskManagement.DeleteTaskAsync(item.Id);
tasks.Clear();
Snackbar.Add($"已清理 {allTasks.Count} 条任务记录", Severity.Success);
@@ -860,19 +894,35 @@
await LoadTasks();
}
- private async Task OpenEditUserChatActiveTask(BatchTask task)
+ private async Task OpenEditTask(BatchTask task)
{
- if (!IsUserChatActiveTask(task.TaskType))
+ var definition = GetTaskDefinition(task);
+ if (definition == null || !CanEditTask(task))
{
Snackbar.Add("该任务当前不可编辑", Severity.Warning);
return;
}
- if (string.Equals(GetDisplayStatus(task), "running", StringComparison.OrdinalIgnoreCase))
+ var capabilities = definition.Definition.TaskCenter;
+ if (HasStatus(task, "running"))
{
- await TaskManagement.PauseTaskAsync(task.Id);
- await LoadTasks();
- Snackbar.Add("已自动暂停任务,正在打开编辑窗口", Severity.Info);
+ if (!capabilities.AutoPauseBeforeEdit)
+ {
+ Snackbar.Add("该任务正在执行中,请先暂停后再编辑", Severity.Warning);
+ return;
+ }
+
+ try
+ {
+ await TaskManagement.PauseTaskAsync(task.Id);
+ await LoadTasks();
+ Snackbar.Add("已自动暂停任务,正在打开编辑窗口", Severity.Info);
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"自动暂停失败:{ex.Message}", Severity.Error);
+ return;
+ }
}
var parameters = new DialogParameters
@@ -880,7 +930,7 @@
["TaskId"] = task.Id
};
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
- var dialog = DialogService.Show($"编辑持续任务 #{task.Id}", parameters, options)!;
+ var dialog = DialogService.Show($"编辑任务 #{task.Id}", parameters, options)!;
var result = await dialog.Result;
if (result is { Canceled: false })
await LoadTasks();
@@ -904,7 +954,8 @@
try
{
- var created = await TaskManagement.CreateTaskAsync(BuildRerunTask(task));
+ var request = BuildRerunRequest(task);
+ var created = await TaskManagement.CreateTaskAsync(CreateBatchTaskFromRequest(request));
Snackbar.Add($"已创建重跑任务 #{created.Id}", Severity.Success);
await LoadTasks();
}
@@ -914,131 +965,71 @@
}
}
- private static BatchTask BuildRerunTask(BatchTask task)
+ private ModuleTaskCreateRequest BuildRerunRequest(BatchTask task)
{
var taskType = (task.TaskType ?? string.Empty).Trim();
if (taskType.Length == 0)
throw new InvalidOperationException("任务类型为空,无法重新运行");
- if (IsUserChatActiveTask(taskType))
- {
- var rerunConfig = BuildRerunConfig(task.Config);
- var total = rerunConfig.MaxMessages > 0 ? rerunConfig.MaxMessages : 0;
- var configJson = JsonSerializer.Serialize(rerunConfig, new JsonSerializerOptions { WriteIndented = true });
+ var builder = TaskRerunBuilders.FirstOrDefault(x =>
+ string.Equals((x.TaskType ?? string.Empty).Trim(), taskType, StringComparison.OrdinalIgnoreCase));
- return new BatchTask
- {
- TaskType = BatchTaskTypes.UserChatActive,
- Total = total,
- Completed = 0,
- Failed = 0,
- Config = configJson
- };
+ if (builder != null)
+ {
+ var request = builder.Build(CreateTaskSnapshot(task));
+ return NormalizeCreateRequest(request);
}
- return new BatchTask
+ return new ModuleTaskCreateRequest
{
TaskType = taskType,
Total = Math.Max(0, task.Total),
- Completed = 0,
- Failed = 0,
- Config = string.IsNullOrWhiteSpace(task.Config) ? null : task.Config.Trim()
+ Config = NormalizeNullableText(task.Config)
};
}
- private static UserChatActiveTaskConfig BuildRerunConfig(string? rawConfig)
+ private static ModuleTaskCreateRequest NormalizeCreateRequest(ModuleTaskCreateRequest? request)
{
- var raw = (rawConfig ?? string.Empty).Trim();
- if (raw.Length == 0)
- throw new InvalidOperationException("任务配置为空,无法重新运行");
+ if (request == null)
+ throw new InvalidOperationException("重跑构建结果为空");
- UserChatActiveTaskConfig cfg;
- try
+ var taskType = (request.TaskType ?? string.Empty).Trim();
+ if (taskType.Length == 0)
+ throw new InvalidOperationException("重跑任务类型为空");
+
+ return new ModuleTaskCreateRequest
{
- cfg = JsonSerializer.Deserialize(raw)
- ?? throw new InvalidOperationException("任务配置解析结果为空");
- }
- catch (Exception ex)
+ TaskType = taskType,
+ Total = Math.Max(0, request.Total),
+ Config = NormalizeNullableText(request.Config)
+ };
+ }
+
+ private static ModuleTaskSnapshot CreateTaskSnapshot(BatchTask task)
+ {
+ return new ModuleTaskSnapshot
{
- throw new InvalidOperationException($"任务配置 JSON 无效:{ex.Message}");
- }
-
- cfg.CategoryIds = NormalizeCategoryIdsForRerun(cfg.CategoryIds, cfg.CategoryId);
- if (cfg.CategoryIds.Count == 0)
- throw new InvalidOperationException("任务缺少账号分类,无法重新运行");
-
- cfg.CategoryId = cfg.CategoryIds[0];
- cfg.CategoryNames = NormalizeCategoryNamesForRerun(cfg.CategoryNames, cfg.CategoryName);
- cfg.CategoryName = cfg.CategoryNames.FirstOrDefault() ?? cfg.CategoryName;
-
- cfg.Targets = (cfg.Targets ?? new List())
- .Select(x => (x ?? string.Empty).Trim())
- .Where(x => x.Length > 0)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToList();
-
- cfg.Dictionary = (cfg.Dictionary ?? new List())
- .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();
-
- return cfg;
+ TaskId = task.Id,
+ TaskType = (task.TaskType ?? string.Empty).Trim(),
+ Status = GetDisplayStatus(task),
+ Total = Math.Max(0, task.Total),
+ Completed = Math.Max(0, task.Completed),
+ Failed = Math.Max(0, task.Failed),
+ Config = NormalizeNullableText(task.Config)
+ };
}
- private static List NormalizeCategoryIdsForRerun(IEnumerable? values, int fallback)
+ private static BatchTask CreateBatchTaskFromRequest(ModuleTaskCreateRequest request)
{
- var ids = (values ?? Array.Empty())
- .Where(x => x > 0)
- .Distinct()
- .ToList();
-
- if (ids.Count == 0 && fallback > 0)
- ids.Add(fallback);
-
- return ids;
- }
-
- private static List NormalizeCategoryNamesForRerun(IEnumerable? values, string? fallback)
- {
- var names = (values ?? Array.Empty())
- .Select(x => (x ?? string.Empty).Trim())
- .Where(x => x.Length > 0)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToList();
-
- var fallbackName = (fallback ?? string.Empty).Trim();
- if (names.Count == 0 && fallbackName.Length > 0)
- names.Add(fallbackName);
-
- return names;
- }
-
- private static string NormalizeModeValue(string? mode)
- {
- return string.Equals((mode ?? string.Empty).Trim(), UserChatActiveTaskModes.Queue, StringComparison.OrdinalIgnoreCase)
- ? UserChatActiveTaskModes.Queue
- : UserChatActiveTaskModes.Random;
+ var normalized = NormalizeCreateRequest(request);
+ return new BatchTask
+ {
+ TaskType = normalized.TaskType,
+ Total = normalized.Total,
+ Completed = 0,
+ Failed = 0,
+ Config = normalized.Config
+ };
}
}