diff --git a/Models/DataBase/BackgroundTask.cs b/Models/DataBase/BackgroundTask.cs index 9d2081a..b6fc6d1 100644 --- a/Models/DataBase/BackgroundTask.cs +++ b/Models/DataBase/BackgroundTask.cs @@ -18,10 +18,10 @@ namespace Foxel.Models.DataBase Failed // 处理失败 } - public class BackgroundTask + public class BackgroundTask : BaseModel { [Key] - public Guid Id { get; set; } // 任务的唯一标识符 + public new Guid Id { get; set; } // 任务的唯一标识符 public TaskType Type { get; set; } // 任务类型 @@ -34,8 +34,6 @@ namespace Foxel.Models.DataBase public string? ErrorMessage { get; set; } // 错误信息(如果任务失败) - public DateTime CreatedAt { get; set; } // 创建时间 - public DateTime? StartedAt { get; set; } // 开始处理时间 public DateTime? CompletedAt { get; set; } // 完成时间 @@ -43,7 +41,7 @@ namespace Foxel.Models.DataBase public int? UserId { get; set; } // 关联的用户ID public User? User { get; set; } - public int? RelatedEntityId { get; set; } + public int? RelatedEntityId { get; set; } public BackgroundTask() { diff --git a/Services/Background/IBackgroundTaskQueue.cs b/Services/Background/IBackgroundTaskQueue.cs index 23aaffd..95283e9 100644 --- a/Services/Background/IBackgroundTaskQueue.cs +++ b/Services/Background/IBackgroundTaskQueue.cs @@ -1,5 +1,5 @@ using Foxel.Models.DataBase; -using Foxel.Services.Background.Processors; // For VisualRecognitionPayload +using Foxel.Services.Background.Processors; namespace Foxel.Services.Background; diff --git a/Services/Background/Processors/ITaskProcessor.cs b/Services/Background/Processors/ITaskProcessor.cs index c3eeb30..7dc199a 100644 --- a/Services/Background/Processors/ITaskProcessor.cs +++ b/Services/Background/Processors/ITaskProcessor.cs @@ -1,5 +1,4 @@ using Foxel.Models.DataBase; -using System.Threading.Tasks; namespace Foxel.Services.Background.Processors { diff --git a/Services/Background/Processors/PictureTaskProcessor.cs b/Services/Background/Processors/PictureTaskProcessor.cs index dc83110..389e6cd 100644 --- a/Services/Background/Processors/PictureTaskProcessor.cs +++ b/Services/Background/Processors/PictureTaskProcessor.cs @@ -17,11 +17,7 @@ namespace Foxel.Services.Background.Processors 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 { diff --git a/Services/Background/Processors/VisualRecognitionTaskProcessor.cs b/Services/Background/Processors/VisualRecognitionTaskProcessor.cs index b30ed63..d948253 100644 --- a/Services/Background/Processors/VisualRecognitionTaskProcessor.cs +++ b/Services/Background/Processors/VisualRecognitionTaskProcessor.cs @@ -9,32 +9,25 @@ using Foxel.Services.Attributes; namespace Foxel.Services.Background.Processors { - - public class VisualRecognitionTaskProcessor : ITaskProcessor + public class VisualRecognitionPayload { - private readonly IDbContextFactory _contextFactory; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly IWebHostEnvironment _environment; - - public VisualRecognitionTaskProcessor( - IDbContextFactory contextFactory, - IServiceProvider serviceProvider, - ILogger logger, - IWebHostEnvironment environment) - { - _contextFactory = contextFactory; - _serviceProvider = serviceProvider; - _logger = logger; - _environment = environment; - } + public int PictureId { get; set; } + public int? UserIdForPicture { get; set; } + } + public class VisualRecognitionTaskProcessor( + IDbContextFactory contextFactory, + IServiceProvider serviceProvider, + ILogger logger, + IWebHostEnvironment environment) + : ITaskProcessor + { public async Task ProcessAsync(BackgroundTask backgroundTask) { if (backgroundTask.Payload == null) { await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "任务 Payload 为空。"); - _logger.LogError("视觉识别任务 Payload 为空: TaskId={TaskId}", backgroundTask.Id); + logger.LogError("视觉识别任务 Payload 为空: TaskId={TaskId}", backgroundTask.Id); return; } @@ -45,49 +38,53 @@ namespace Foxel.Services.Background.Processors } 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 解析失败。"); return; } if (payload == null || payload.PictureId == 0) { - _logger.LogError("视觉识别任务的 Payload 无效或缺少 PictureId: TaskId={TaskId}", backgroundTask.Id); - await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "Payload 无效或缺少 PictureId。"); + logger.LogError("视觉识别任务的 Payload 无效或缺少 PictureId: TaskId={TaskId}", backgroundTask.Id); + await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, + "Payload 无效或缺少 PictureId。"); return; } 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; - await using var dbContext = await _contextFactory.CreateDbContextAsync(); + await using var dbContext = await contextFactory.CreateDbContextAsync(); var currentBackgroundTaskState = await dbContext.BackgroundTasks.FindAsync(backgroundTask.Id); if (currentBackgroundTaskState == null) { - _logger.LogError("在 VisualRecognitionTaskProcessor 中找不到后台任务: TaskId={TaskId}", backgroundTask.Id); + logger.LogError("在 VisualRecognitionTaskProcessor 中找不到后台任务: TaskId={TaskId}", backgroundTask.Id); 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 { - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 10, currentBackgroundTaskState: currentBackgroundTaskState); + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 10, + currentBackgroundTaskState: currentBackgroundTaskState); if (picture == null) { throw new Exception($"找不到ID为{pictureId}的图片。"); } + if (string.IsNullOrEmpty(picture.ThumbnailPath)) { throw new Exception($"图片ID {pictureId} 的缩略图路径为空,无法进行AI分析。"); } - using var scope = _serviceProvider.CreateScope(); + using var scope = serviceProvider.CreateScope(); var aiService = scope.ServiceProvider.GetRequiredService(); var storageService = scope.ServiceProvider.GetRequiredService(); - string contentRootPath = _environment.ContentRootPath; + string contentRootPath = environment.ContentRootPath; string actualThumbnailPathForAI; if (picture.StorageType == StorageType.Local) @@ -96,10 +93,11 @@ namespace Foxel.Services.Background.Processors } else // Remote storage { - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 15, currentBackgroundTaskState: currentBackgroundTaskState); - thumbnailForAIDownloadPath = await storageService.ExecuteAsync(picture.StorageType, + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 15, + currentBackgroundTaskState: currentBackgroundTaskState); + thumbnailForAiDownloadPath = await storageService.ExecuteAsync(picture.StorageType, provider => provider.DownloadFileAsync(picture.ThumbnailPath)); - actualThumbnailPathForAI = thumbnailForAIDownloadPath; + actualThumbnailPathForAI = thumbnailForAiDownloadPath; isTempThumbnailFile = true; } @@ -107,19 +105,26 @@ namespace Foxel.Services.Background.Processors { 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); - 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); - string finalTitle = !string.IsNullOrWhiteSpace(title) && title != "AI生成的标题" ? title : Path.GetFileNameWithoutExtension(picture.Name); - string finalDescription = !string.IsNullOrWhiteSpace(description) && description != "AI生成的描述" ? description : picture.Description; + string finalTitle = !string.IsNullOrWhiteSpace(title) && title != "AI生成的标题" + ? 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.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 embedding = await aiService.GetEmbeddingAsync(combinedText); picture.Embedding = embedding; @@ -127,79 +132,103 @@ namespace Foxel.Services.Background.Processors if (picture.UserId.HasValue && embedding != null && embedding.Length > 0) { var vectorDbService = scope.ServiceProvider.GetRequiredService(); - await vectorDbService.AddPictureToUserCollectionAsync(picture.UserId.Value, new Models.Vector.PictureVector - { - Id = (ulong)picture.Id, - Name = picture.Name, - Embedding = embedding - }); + await vectorDbService.AddPictureToUserCollectionAsync(picture.UserId.Value, + new Models.Vector.PictureVector + { + Id = (ulong)picture.Id, + 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(); - - 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); - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 90, currentBackgroundTaskState: currentBackgroundTaskState); + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 90, + currentBackgroundTaskState: currentBackgroundTaskState); if (picture.User != null && matchedTagNames.Any()) { picture.Tags ??= new List(); 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) { existingTag = new Tag { Name = tagName.Trim(), Description = tagName.Trim() }; 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(); - 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 UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Completed, 100, completedAt: DateTime.UtcNow, currentBackgroundTaskState: currentBackgroundTaskState); + await dbContext.SaveChangesAsync(); + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Completed, 100, + completedAt: DateTime.UtcNow, currentBackgroundTaskState: currentBackgroundTaskState); } catch (Exception ex) { - _logger.LogError(ex, "视觉识别任务失败: TaskId={TaskId}, PictureId={PictureId}", currentBackgroundTaskState.Id, pictureId); - await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Failed, currentBackgroundTaskState.Progress, ex.Message, currentBackgroundTaskState: currentBackgroundTaskState); - // dbContext.SaveChangesAsync() might be called in UpdateTaskStatusInDb or here if picture state needs saving on error + logger.LogError(ex, "视觉识别任务失败: TaskId={TaskId}, PictureId={PictureId}", currentBackgroundTaskState.Id, + pictureId); + await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Failed, + currentBackgroundTaskState.Progress, ex.Message, + currentBackgroundTaskState: currentBackgroundTaskState); } 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); 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); } taskToUpdate.Status = status; 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 (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) { taskToUpdate.CompletedAt ??= DateTime.UtcNow; // Ensure CompletedAt is set @@ -207,12 +236,14 @@ namespace Foxel.Services.Background.Processors 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 { - _logger.LogWarning("尝试在 VisualRecognitionProcessor 中更新不存在的任务状态: TaskId={TaskId}", taskId); + logger.LogWarning("尝试在 VisualRecognitionProcessor 中更新不存在的任务状态: TaskId={TaskId}", taskId); } } } -} +} \ No newline at end of file diff --git a/Services/Background/QueuedHostedService.cs b/Services/Background/QueuedHostedService.cs index 17a4dd9..9859f84 100644 --- a/Services/Background/QueuedHostedService.cs +++ b/Services/Background/QueuedHostedService.cs @@ -11,7 +11,6 @@ public class QueuedHostedService( try { - // 从数据库恢复未完成的任务 using var scope = serviceProvider.CreateScope(); var backgroundTaskQueue = scope.ServiceProvider.GetRequiredService(); await backgroundTaskQueue.RestoreUnfinishedTasksAsync(); diff --git a/Web/src/pages/admin/system/ConfigTabs.tsx b/Web/src/pages/admin/system/ConfigTabs.tsx index f182219..dddea1d 100644 --- a/Web/src/pages/admin/system/ConfigTabs.tsx +++ b/Web/src/pages/admin/system/ConfigTabs.tsx @@ -289,13 +289,13 @@ const ConfigTabs: React.FC = ({ isMobile={isMobile} >
- {renderConfigFormItems(formsMap.AppSettings, "AppSettings", ['ServerUrl'])} + {renderConfigFormItems(formsMap.AppSettings, "AppSettings", ['ServerUrl', 'MaxConcurrentTasks'])}