mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 23:57:19 +08:00
refactor(background): enhance BackgroundTask and VisualRecognitionTaskProcessor structure and logging
This commit is contained in:
@@ -18,10 +18,10 @@ namespace Foxel.Models.DataBase
|
|||||||
Failed // 处理失败
|
Failed // 处理失败
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BackgroundTask
|
public class BackgroundTask : BaseModel
|
||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
public Guid Id { get; set; } // 任务的唯一标识符
|
public new Guid Id { get; set; } // 任务的唯一标识符
|
||||||
|
|
||||||
public TaskType Type { get; set; } // 任务类型
|
public TaskType Type { get; set; } // 任务类型
|
||||||
|
|
||||||
@@ -34,8 +34,6 @@ namespace Foxel.Models.DataBase
|
|||||||
|
|
||||||
public string? ErrorMessage { get; set; } // 错误信息(如果任务失败)
|
public string? ErrorMessage { get; set; } // 错误信息(如果任务失败)
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; } // 创建时间
|
|
||||||
|
|
||||||
public DateTime? StartedAt { get; set; } // 开始处理时间
|
public DateTime? StartedAt { get; set; } // 开始处理时间
|
||||||
|
|
||||||
public DateTime? CompletedAt { get; set; } // 完成时间
|
public DateTime? CompletedAt { get; set; } // 完成时间
|
||||||
@@ -43,7 +41,7 @@ namespace Foxel.Models.DataBase
|
|||||||
public int? UserId { get; set; } // 关联的用户ID
|
public int? UserId { get; set; } // 关联的用户ID
|
||||||
public User? User { get; set; }
|
public User? User { get; set; }
|
||||||
|
|
||||||
public int? RelatedEntityId { get; set; }
|
public int? RelatedEntityId { get; set; }
|
||||||
|
|
||||||
public BackgroundTask()
|
public BackgroundTask()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Foxel.Models.DataBase;
|
using Foxel.Models.DataBase;
|
||||||
using Foxel.Services.Background.Processors; // For VisualRecognitionPayload
|
using Foxel.Services.Background.Processors;
|
||||||
|
|
||||||
namespace Foxel.Services.Background;
|
namespace Foxel.Services.Background;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Foxel.Models.DataBase;
|
using Foxel.Models.DataBase;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Foxel.Services.Background.Processors
|
namespace Foxel.Services.Background.Processors
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,11 +17,7 @@ namespace Foxel.Services.Background.Processors
|
|||||||
public int? UserIdForPicture { get; set; }
|
public int? UserIdForPicture { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class VisualRecognitionPayload // Define new payload
|
|
||||||
{
|
|
||||||
public int PictureId { get; set; }
|
|
||||||
public int? UserIdForPicture { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PictureTaskProcessor : ITaskProcessor
|
public class PictureTaskProcessor : ITaskProcessor
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,32 +9,25 @@ using Foxel.Services.Attributes;
|
|||||||
|
|
||||||
namespace Foxel.Services.Background.Processors
|
namespace Foxel.Services.Background.Processors
|
||||||
{
|
{
|
||||||
|
public class VisualRecognitionPayload
|
||||||
public class VisualRecognitionTaskProcessor : ITaskProcessor
|
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<MyDbContext> _contextFactory;
|
public int PictureId { get; set; }
|
||||||
private readonly IServiceProvider _serviceProvider;
|
public int? UserIdForPicture { get; set; }
|
||||||
private readonly ILogger<VisualRecognitionTaskProcessor> _logger;
|
}
|
||||||
private readonly IWebHostEnvironment _environment;
|
|
||||||
|
|
||||||
public VisualRecognitionTaskProcessor(
|
|
||||||
IDbContextFactory<MyDbContext> contextFactory,
|
|
||||||
IServiceProvider serviceProvider,
|
|
||||||
ILogger<VisualRecognitionTaskProcessor> logger,
|
|
||||||
IWebHostEnvironment environment)
|
|
||||||
{
|
|
||||||
_contextFactory = contextFactory;
|
|
||||||
_serviceProvider = serviceProvider;
|
|
||||||
_logger = logger;
|
|
||||||
_environment = environment;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public class VisualRecognitionTaskProcessor(
|
||||||
|
IDbContextFactory<MyDbContext> contextFactory,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
ILogger<VisualRecognitionTaskProcessor> logger,
|
||||||
|
IWebHostEnvironment environment)
|
||||||
|
: ITaskProcessor
|
||||||
|
{
|
||||||
public async Task ProcessAsync(BackgroundTask backgroundTask)
|
public async Task ProcessAsync(BackgroundTask backgroundTask)
|
||||||
{
|
{
|
||||||
if (backgroundTask.Payload == null)
|
if (backgroundTask.Payload == null)
|
||||||
{
|
{
|
||||||
await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "任务 Payload 为空。");
|
await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "任务 Payload 为空。");
|
||||||
_logger.LogError("视觉识别任务 Payload 为空: TaskId={TaskId}", backgroundTask.Id);
|
logger.LogError("视觉识别任务 Payload 为空: TaskId={TaskId}", backgroundTask.Id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,49 +38,53 @@ namespace Foxel.Services.Background.Processors
|
|||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "无法解析视觉识别任务的 Payload: TaskId={TaskId}", backgroundTask.Id);
|
logger.LogError(ex, "无法解析视觉识别任务的 Payload: TaskId={TaskId}", backgroundTask.Id);
|
||||||
await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "Payload 解析失败。");
|
await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "Payload 解析失败。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload == null || payload.PictureId == 0)
|
if (payload == null || payload.PictureId == 0)
|
||||||
{
|
{
|
||||||
_logger.LogError("视觉识别任务的 Payload 无效或缺少 PictureId: TaskId={TaskId}", backgroundTask.Id);
|
logger.LogError("视觉识别任务的 Payload 无效或缺少 PictureId: TaskId={TaskId}", backgroundTask.Id);
|
||||||
await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "Payload 无效或缺少 PictureId。");
|
await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0,
|
||||||
|
"Payload 无效或缺少 PictureId。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pictureId = payload.PictureId;
|
var pictureId = payload.PictureId;
|
||||||
string thumbnailForAIDownloadPath = string.Empty; // Path if thumbnail needs to be downloaded
|
string thumbnailForAiDownloadPath = string.Empty; // Path if thumbnail needs to be downloaded
|
||||||
bool isTempThumbnailFile = false;
|
bool isTempThumbnailFile = false;
|
||||||
|
|
||||||
await using var dbContext = await _contextFactory.CreateDbContextAsync();
|
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||||
var currentBackgroundTaskState = await dbContext.BackgroundTasks.FindAsync(backgroundTask.Id);
|
var currentBackgroundTaskState = await dbContext.BackgroundTasks.FindAsync(backgroundTask.Id);
|
||||||
if (currentBackgroundTaskState == null)
|
if (currentBackgroundTaskState == null)
|
||||||
{
|
{
|
||||||
_logger.LogError("在 VisualRecognitionTaskProcessor 中找不到后台任务: TaskId={TaskId}", backgroundTask.Id);
|
logger.LogError("在 VisualRecognitionTaskProcessor 中找不到后台任务: TaskId={TaskId}", backgroundTask.Id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var picture = await dbContext.Pictures.Include(p => p.User).ThenInclude(u => u.Tags).FirstOrDefaultAsync(p => p.Id == pictureId);
|
var picture = await dbContext.Pictures.Include(p => p.User).ThenInclude(u => u.Tags)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == pictureId);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 10, currentBackgroundTaskState: currentBackgroundTaskState);
|
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 10,
|
||||||
|
currentBackgroundTaskState: currentBackgroundTaskState);
|
||||||
|
|
||||||
if (picture == null)
|
if (picture == null)
|
||||||
{
|
{
|
||||||
throw new Exception($"找不到ID为{pictureId}的图片。");
|
throw new Exception($"找不到ID为{pictureId}的图片。");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(picture.ThumbnailPath))
|
if (string.IsNullOrEmpty(picture.ThumbnailPath))
|
||||||
{
|
{
|
||||||
throw new Exception($"图片ID {pictureId} 的缩略图路径为空,无法进行AI分析。");
|
throw new Exception($"图片ID {pictureId} 的缩略图路径为空,无法进行AI分析。");
|
||||||
}
|
}
|
||||||
|
|
||||||
using var scope = _serviceProvider.CreateScope();
|
using var scope = serviceProvider.CreateScope();
|
||||||
var aiService = scope.ServiceProvider.GetRequiredService<IAiService>();
|
var aiService = scope.ServiceProvider.GetRequiredService<IAiService>();
|
||||||
var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>();
|
var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>();
|
||||||
string contentRootPath = _environment.ContentRootPath;
|
string contentRootPath = environment.ContentRootPath;
|
||||||
string actualThumbnailPathForAI;
|
string actualThumbnailPathForAI;
|
||||||
|
|
||||||
if (picture.StorageType == StorageType.Local)
|
if (picture.StorageType == StorageType.Local)
|
||||||
@@ -96,10 +93,11 @@ namespace Foxel.Services.Background.Processors
|
|||||||
}
|
}
|
||||||
else // Remote storage
|
else // Remote storage
|
||||||
{
|
{
|
||||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 15, currentBackgroundTaskState: currentBackgroundTaskState);
|
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 15,
|
||||||
thumbnailForAIDownloadPath = await storageService.ExecuteAsync(picture.StorageType,
|
currentBackgroundTaskState: currentBackgroundTaskState);
|
||||||
|
thumbnailForAiDownloadPath = await storageService.ExecuteAsync(picture.StorageType,
|
||||||
provider => provider.DownloadFileAsync(picture.ThumbnailPath));
|
provider => provider.DownloadFileAsync(picture.ThumbnailPath));
|
||||||
actualThumbnailPathForAI = thumbnailForAIDownloadPath;
|
actualThumbnailPathForAI = thumbnailForAiDownloadPath;
|
||||||
isTempThumbnailFile = true;
|
isTempThumbnailFile = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,19 +105,26 @@ namespace Foxel.Services.Background.Processors
|
|||||||
{
|
{
|
||||||
throw new Exception($"找不到用于AI分析的缩略图文件: {actualThumbnailPathForAI} (源路径: {picture.ThumbnailPath})");
|
throw new Exception($"找不到用于AI分析的缩略图文件: {actualThumbnailPathForAI} (源路径: {picture.ThumbnailPath})");
|
||||||
}
|
}
|
||||||
|
|
||||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 20, currentBackgroundTaskState: currentBackgroundTaskState);
|
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 20,
|
||||||
|
currentBackgroundTaskState: currentBackgroundTaskState);
|
||||||
string base64Image = await ImageHelper.ConvertImageToBase64(actualThumbnailPathForAI);
|
string base64Image = await ImageHelper.ConvertImageToBase64(actualThumbnailPathForAI);
|
||||||
|
|
||||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 40, currentBackgroundTaskState: currentBackgroundTaskState);
|
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 40,
|
||||||
|
currentBackgroundTaskState: currentBackgroundTaskState);
|
||||||
var (title, description) = await aiService.AnalyzeImageAsync(base64Image);
|
var (title, description) = await aiService.AnalyzeImageAsync(base64Image);
|
||||||
|
|
||||||
string finalTitle = !string.IsNullOrWhiteSpace(title) && title != "AI生成的标题" ? title : Path.GetFileNameWithoutExtension(picture.Name);
|
string finalTitle = !string.IsNullOrWhiteSpace(title) && title != "AI生成的标题"
|
||||||
string finalDescription = !string.IsNullOrWhiteSpace(description) && description != "AI生成的描述" ? description : picture.Description;
|
? title
|
||||||
|
: Path.GetFileNameWithoutExtension(picture.Name);
|
||||||
|
string finalDescription = !string.IsNullOrWhiteSpace(description) && description != "AI生成的描述"
|
||||||
|
? description
|
||||||
|
: picture.Description;
|
||||||
picture.Name = finalTitle; // Potentially overwrites name set from filename
|
picture.Name = finalTitle; // Potentially overwrites name set from filename
|
||||||
picture.Description = finalDescription;
|
picture.Description = finalDescription;
|
||||||
|
|
||||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 60, currentBackgroundTaskState: currentBackgroundTaskState);
|
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 60,
|
||||||
|
currentBackgroundTaskState: currentBackgroundTaskState);
|
||||||
var combinedText = $"{finalTitle}. {finalDescription}";
|
var combinedText = $"{finalTitle}. {finalDescription}";
|
||||||
var embedding = await aiService.GetEmbeddingAsync(combinedText);
|
var embedding = await aiService.GetEmbeddingAsync(combinedText);
|
||||||
picture.Embedding = embedding;
|
picture.Embedding = embedding;
|
||||||
@@ -127,79 +132,103 @@ namespace Foxel.Services.Background.Processors
|
|||||||
if (picture.UserId.HasValue && embedding != null && embedding.Length > 0)
|
if (picture.UserId.HasValue && embedding != null && embedding.Length > 0)
|
||||||
{
|
{
|
||||||
var vectorDbService = scope.ServiceProvider.GetRequiredService<IVectorDbService>();
|
var vectorDbService = scope.ServiceProvider.GetRequiredService<IVectorDbService>();
|
||||||
await vectorDbService.AddPictureToUserCollectionAsync(picture.UserId.Value, new Models.Vector.PictureVector
|
await vectorDbService.AddPictureToUserCollectionAsync(picture.UserId.Value,
|
||||||
{
|
new Models.Vector.PictureVector
|
||||||
Id = (ulong)picture.Id,
|
{
|
||||||
Name = picture.Name,
|
Id = (ulong)picture.Id,
|
||||||
Embedding = embedding
|
Name = picture.Name,
|
||||||
});
|
Embedding = embedding
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 70, currentBackgroundTaskState: currentBackgroundTaskState);
|
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 70,
|
||||||
|
currentBackgroundTaskState: currentBackgroundTaskState);
|
||||||
var availableTagNames = await dbContext.Tags.Select(t => t.Name).ToListAsync();
|
var availableTagNames = await dbContext.Tags.Select(t => t.Name).ToListAsync();
|
||||||
|
|
||||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 80, currentBackgroundTaskState: currentBackgroundTaskState);
|
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 80,
|
||||||
|
currentBackgroundTaskState: currentBackgroundTaskState);
|
||||||
var matchedTagNames = await aiService.GenerateTagsFromImageAsync(base64Image, availableTagNames, true);
|
var matchedTagNames = await aiService.GenerateTagsFromImageAsync(base64Image, availableTagNames, true);
|
||||||
|
|
||||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 90, currentBackgroundTaskState: currentBackgroundTaskState);
|
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 90,
|
||||||
|
currentBackgroundTaskState: currentBackgroundTaskState);
|
||||||
if (picture.User != null && matchedTagNames.Any())
|
if (picture.User != null && matchedTagNames.Any())
|
||||||
{
|
{
|
||||||
picture.Tags ??= new List<Tag>();
|
picture.Tags ??= new List<Tag>();
|
||||||
foreach (var tagName in matchedTagNames)
|
foreach (var tagName in matchedTagNames)
|
||||||
{
|
{
|
||||||
var existingTag = await dbContext.Tags.FirstOrDefaultAsync(t => t.Name.ToLower() == tagName.ToLower());
|
var existingTag =
|
||||||
|
await dbContext.Tags.FirstOrDefaultAsync(t => t.Name.ToLower() == tagName.ToLower());
|
||||||
if (existingTag == null)
|
if (existingTag == null)
|
||||||
{
|
{
|
||||||
existingTag = new Tag { Name = tagName.Trim(), Description = tagName.Trim() };
|
existingTag = new Tag { Name = tagName.Trim(), Description = tagName.Trim() };
|
||||||
dbContext.Tags.Add(existingTag);
|
dbContext.Tags.Add(existingTag);
|
||||||
}
|
}
|
||||||
if (!picture.Tags.Any(t => t.Id == existingTag.Id)) picture.Tags.Add(existingTag);
|
|
||||||
|
if (picture.Tags.All(t => t.Id != existingTag.Id)) picture.Tags.Add(existingTag);
|
||||||
|
|
||||||
picture.User.Tags ??= new List<Tag>();
|
picture.User.Tags ??= new List<Tag>();
|
||||||
if (!picture.User.Tags.Any(t => t.Id == existingTag.Id)) picture.User.Tags.Add(existingTag);
|
if (picture.User.Tags.All(t => t.Id != existingTag.Id)) picture.User.Tags.Add(existingTag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync(); // Save all AI-related changes to Picture
|
await dbContext.SaveChangesAsync();
|
||||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Completed, 100, completedAt: DateTime.UtcNow, currentBackgroundTaskState: currentBackgroundTaskState);
|
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Completed, 100,
|
||||||
|
completedAt: DateTime.UtcNow, currentBackgroundTaskState: currentBackgroundTaskState);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "视觉识别任务失败: TaskId={TaskId}, PictureId={PictureId}", currentBackgroundTaskState.Id, pictureId);
|
logger.LogError(ex, "视觉识别任务失败: TaskId={TaskId}, PictureId={PictureId}", currentBackgroundTaskState.Id,
|
||||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Failed, currentBackgroundTaskState.Progress, ex.Message, currentBackgroundTaskState: currentBackgroundTaskState);
|
pictureId);
|
||||||
// dbContext.SaveChangesAsync() might be called in UpdateTaskStatusInDb or here if picture state needs saving on error
|
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Failed,
|
||||||
|
currentBackgroundTaskState.Progress, ex.Message,
|
||||||
|
currentBackgroundTaskState: currentBackgroundTaskState);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (isTempThumbnailFile && File.Exists(thumbnailForAIDownloadPath))
|
if (isTempThumbnailFile && File.Exists(thumbnailForAiDownloadPath))
|
||||||
{
|
{
|
||||||
try { File.Delete(thumbnailForAIDownloadPath); } catch (Exception ex) { _logger.LogWarning(ex, "删除临时AI缩略图文件失败: {FilePath}", thumbnailForAIDownloadPath); }
|
try
|
||||||
|
{
|
||||||
|
File.Delete(thumbnailForAiDownloadPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "删除临时AI缩略图文件失败: {FilePath}", thumbnailForAiDownloadPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateTaskStatusInDb(Guid taskId, TaskExecutionStatus status, int progress, string? error = null, DateTime? startedAt = null, DateTime? completedAt = null, BackgroundTask? currentBackgroundTaskState = null)
|
private async Task UpdateTaskStatusInDb(Guid taskId, TaskExecutionStatus status, int progress,
|
||||||
|
string? error = null, DateTime? startedAt = null, DateTime? completedAt = null,
|
||||||
|
BackgroundTask? currentBackgroundTaskState = null)
|
||||||
{
|
{
|
||||||
await using var dbContext = await _contextFactory.CreateDbContextAsync();
|
await using var dbContext = await contextFactory.CreateDbContextAsync();
|
||||||
var taskToUpdate = currentBackgroundTaskState ?? await dbContext.BackgroundTasks.FindAsync(taskId);
|
var taskToUpdate = currentBackgroundTaskState ?? await dbContext.BackgroundTasks.FindAsync(taskId);
|
||||||
|
|
||||||
if (taskToUpdate != null)
|
if (taskToUpdate != null)
|
||||||
{
|
{
|
||||||
if (currentBackgroundTaskState != null && dbContext.Entry(currentBackgroundTaskState).State == EntityState.Detached)
|
if (currentBackgroundTaskState != null &&
|
||||||
|
dbContext.Entry(currentBackgroundTaskState).State == EntityState.Detached)
|
||||||
{
|
{
|
||||||
dbContext.BackgroundTasks.Attach(currentBackgroundTaskState);
|
dbContext.BackgroundTasks.Attach(currentBackgroundTaskState);
|
||||||
}
|
}
|
||||||
|
|
||||||
taskToUpdate.Status = status;
|
taskToUpdate.Status = status;
|
||||||
taskToUpdate.Progress = progress;
|
taskToUpdate.Progress = progress;
|
||||||
taskToUpdate.ErrorMessage = string.IsNullOrEmpty(error) ? taskToUpdate.ErrorMessage : error; // Keep existing error if new one is null/empty
|
taskToUpdate.ErrorMessage =
|
||||||
|
string.IsNullOrEmpty(error)
|
||||||
|
? taskToUpdate.ErrorMessage
|
||||||
|
: error; // Keep existing error if new one is null/empty
|
||||||
if (startedAt.HasValue) taskToUpdate.StartedAt = startedAt;
|
if (startedAt.HasValue) taskToUpdate.StartedAt = startedAt;
|
||||||
if (completedAt.HasValue) taskToUpdate.CompletedAt = completedAt;
|
if (completedAt.HasValue) taskToUpdate.CompletedAt = completedAt;
|
||||||
|
|
||||||
if ((status == TaskExecutionStatus.Completed || status == TaskExecutionStatus.Failed) && !taskToUpdate.StartedAt.HasValue)
|
if ((status == TaskExecutionStatus.Completed || status == TaskExecutionStatus.Failed) &&
|
||||||
|
!taskToUpdate.StartedAt.HasValue)
|
||||||
{
|
{
|
||||||
taskToUpdate.StartedAt = taskToUpdate.CreatedAt; // Ensure StartedAt is set
|
taskToUpdate.StartedAt = taskToUpdate.CreatedAt; // Ensure StartedAt is set
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status == TaskExecutionStatus.Completed || status == TaskExecutionStatus.Failed)
|
if (status == TaskExecutionStatus.Completed || status == TaskExecutionStatus.Failed)
|
||||||
{
|
{
|
||||||
taskToUpdate.CompletedAt ??= DateTime.UtcNow; // Ensure CompletedAt is set
|
taskToUpdate.CompletedAt ??= DateTime.UtcNow; // Ensure CompletedAt is set
|
||||||
@@ -207,12 +236,14 @@ namespace Foxel.Services.Background.Processors
|
|||||||
|
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
_logger.LogInformation("任务状态更新 (VisualRecognitionProcessor): TaskId={TaskId}, Status={Status}, Progress={Progress}%", taskId, status, progress);
|
logger.LogInformation(
|
||||||
|
"任务状态更新 (VisualRecognitionProcessor): TaskId={TaskId}, Status={Status}, Progress={Progress}%",
|
||||||
|
taskId, status, progress);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogWarning("尝试在 VisualRecognitionProcessor 中更新不存在的任务状态: TaskId={TaskId}", taskId);
|
logger.LogWarning("尝试在 VisualRecognitionProcessor 中更新不存在的任务状态: TaskId={TaskId}", taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,6 @@ public class QueuedHostedService(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 从数据库恢复未完成的任务
|
|
||||||
using var scope = serviceProvider.CreateScope();
|
using var scope = serviceProvider.CreateScope();
|
||||||
var backgroundTaskQueue = scope.ServiceProvider.GetRequiredService<IBackgroundTaskQueue>();
|
var backgroundTaskQueue = scope.ServiceProvider.GetRequiredService<IBackgroundTaskQueue>();
|
||||||
await backgroundTaskQueue.RestoreUnfinishedTasksAsync();
|
await backgroundTaskQueue.RestoreUnfinishedTasksAsync();
|
||||||
|
|||||||
@@ -289,13 +289,13 @@ const ConfigTabs: React.FC<ConfigTabsProps> = ({
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
>
|
>
|
||||||
<Form form={formsMap.AppSettings} layout="vertical" size={isMobile ? "middle" : "large"}>
|
<Form form={formsMap.AppSettings} layout="vertical" size={isMobile ? "middle" : "large"}>
|
||||||
{renderConfigFormItems(formsMap.AppSettings, "AppSettings", ['ServerUrl'])}
|
{renderConfigFormItems(formsMap.AppSettings, "AppSettings", ['ServerUrl', 'MaxConcurrentTasks'])}
|
||||||
<Divider style={{ margin: '12px 0 20px' }} />
|
<Divider style={{ margin: '12px 0 20px' }} />
|
||||||
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
|
<Form.Item style={{ marginBottom: 0, textAlign: 'center' }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
onClick={() => onSaveAllForGroup(formsMap.AppSettings, "AppSettings", ['ServerUrl'])}
|
onClick={() => onSaveAllForGroup(formsMap.AppSettings, "AppSettings", ['ServerUrl', 'MaxConcurrentTasks'])}
|
||||||
style={{ width: isMobile ? '100%' : '240px' }}
|
style={{ width: isMobile ? '100%' : '240px' }}
|
||||||
>
|
>
|
||||||
保存所有应用设置
|
保存所有应用设置
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ const allDescriptions: Record<string, Record<string, string>> = {
|
|||||||
LinuxDoCallbackUrl: 'LinuxDo OAuth 认证回调地址'
|
LinuxDoCallbackUrl: 'LinuxDo OAuth 认证回调地址'
|
||||||
},
|
},
|
||||||
AppSettings: {
|
AppSettings: {
|
||||||
ServerUrl: '服务器URL'
|
ServerUrl: '服务器URL',
|
||||||
|
MaxConcurrentTasks: '后台任务最大并发处理数量 (例如: 图像分析、标签生成等)'
|
||||||
},
|
},
|
||||||
Storage: {
|
Storage: {
|
||||||
DefaultStorage: '已登录用户上传文件时的默认存储位置',
|
DefaultStorage: '已登录用户上传文件时的默认存储位置',
|
||||||
|
|||||||
Reference in New Issue
Block a user