using Foxel.Models.DataBase; using Foxel.Services.AI; using Foxel.Services.Storage; using Foxel.Services.VectorDB; using Foxel.Utils; using Microsoft.EntityFrameworkCore; using System.Text.Json; // using Foxel.Services.Attributes; // StorageType enum might not be directly needed here anymore using Microsoft.Extensions.DependencyInjection; // For CreateScope using Microsoft.AspNetCore.Hosting; // For IWebHostEnvironment using Microsoft.Extensions.Logging; // For ILogger namespace Foxel.Services.Background.Processors { public class VisualRecognitionPayload { public int PictureId { get; set; } public int? UserIdForPicture { get; set; } } public class VisualRecognitionTaskProcessor : ITaskProcessor { 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 async Task ProcessAsync(BackgroundTask backgroundTask) { // ... (payload deserialization and validation logic remains the same) ... if (backgroundTask.Payload == null) { await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "任务 Payload 为空。"); _logger.LogError("视觉识别任务 Payload 为空: TaskId={TaskId}", backgroundTask.Id); return; } VisualRecognitionPayload? payload; try { payload = JsonSerializer.Deserialize(backgroundTask.Payload); } catch (JsonException ex) { _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。"); return; } var pictureId = payload.PictureId; string thumbnailForAiDownloadPath = string.Empty; bool isTempThumbnailFile = false; 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); return; } var picture = await dbContext.Pictures .Include(p => p.User) .ThenInclude(u => u!.Tags) // Ensure Tags on User is loaded if needed .Include(p => p.StorageMode) // Include StorageMode .Include(p => p.Tags) // Include picture's own tags .FirstOrDefaultAsync(p => p.Id == pictureId); try { await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 10, currentBackgroundTaskState: currentBackgroundTaskState); if (picture == null) { throw new Exception($"找不到ID为{pictureId}的图片。"); } if (picture.StorageMode == null || picture.StorageModeId < 0) { throw new Exception($"图片ID {pictureId} 缺少有效的 StorageMode 配置。"); } if (string.IsNullOrEmpty(picture.ThumbnailPath)) { // It's possible the thumbnail is generated by PictureTaskProcessor but this task runs before it completes. // Or thumbnail generation failed. _logger.LogWarning("图片ID {PictureId} 的缩略图路径为空。AI分析将无法进行或可能失败。", pictureId); // Depending on requirements, you might throw, or try to generate it here, or skip AI. // For now, let's assume it should exist. throw new Exception($"图片ID {pictureId} 的缩略图路径为空,无法进行AI分析。"); } using var scope = _serviceProvider.CreateScope(); var aiService = scope.ServiceProvider.GetRequiredService(); var storageService = scope.ServiceProvider.GetRequiredService(); string contentRootPath = _environment.ContentRootPath; string actualThumbnailPathForAI; // Check the StorageType of the associated StorageMode if (picture.StorageMode.StorageType == Attributes.StorageType.Local) { // As with PictureTaskProcessor, safer to use DownloadFileAsync for consistency _logger.LogInformation("Picture {PictureId} thumbnail is Local. Attempting to download via StorageService for AI.", pictureId); // Fall-through } // else // Remote storage or consistent handling for Local // { await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 15, currentBackgroundTaskState: currentBackgroundTaskState); // Use picture.StorageModeId to download the thumbnail thumbnailForAiDownloadPath = await storageService.ExecuteAsync(picture.StorageModeId, provider => provider.DownloadFileAsync(picture.ThumbnailPath)); actualThumbnailPathForAI = thumbnailForAiDownloadPath; isTempThumbnailFile = true; // } if (string.IsNullOrEmpty(actualThumbnailPathForAI) || !File.Exists(actualThumbnailPathForAI)) { throw new Exception($"找不到用于AI分析的缩略图文件: {actualThumbnailPathForAI} (源存储路径: {picture.ThumbnailPath})"); } await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 20, currentBackgroundTaskState: currentBackgroundTaskState); string base64Image = await ImageHelper.ConvertImageToBase64(actualThumbnailPathForAI); 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); // Fallback to existing name string finalDescription = !string.IsNullOrWhiteSpace(description) && description != "AI生成的描述" ? description : picture.Description; // Fallback to existing description picture.Name = finalTitle; picture.Description = finalDescription; await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 60, currentBackgroundTaskState: currentBackgroundTaskState); var combinedText = $"{finalTitle}. {finalDescription}"; var embedding = await aiService.GetEmbeddingAsync(combinedText); picture.Embedding = embedding; 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, // Ensure Picture.Id can be cast to ulong or adjust type Name = picture.Name, Embedding = embedding }); } 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); var matchedTagNames = await aiService.GenerateTagsFromImageAsync(base64Image, availableTagNames, true); await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 90, currentBackgroundTaskState: currentBackgroundTaskState); if (matchedTagNames.Any()) // Apply tags even if user is null, if that's desired { picture.Tags ??= new List(); foreach (var tagName in matchedTagNames) { 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); // await dbContext.SaveChangesAsync(); // Save tag immediately or batch } if (picture.Tags.All(t => t.Id != existingTag.Id)) picture.Tags.Add(existingTag); // Add to user's tags if user exists if (picture.User != null) { picture.User.Tags ??= new List(); if (picture.User.Tags.All(t => t.Id != existingTag.Id)) picture.User.Tags.Add(existingTag); } } } 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); } finally { if (isTempThumbnailFile && File.Exists(thumbnailForAiDownloadPath)) { try { File.Delete(thumbnailForAiDownloadPath); } catch (Exception ex) { _logger.LogWarning(ex, "删除临时AI缩略图文件失败: {FilePath}", thumbnailForAiDownloadPath); } } } } // ... (UpdateTaskStatusInDb remains the same) ... 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(); var taskToUpdate = currentBackgroundTaskState ?? await dbContext.BackgroundTasks.FindAsync(taskId); if (taskToUpdate != null) { 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; if (startedAt.HasValue) taskToUpdate.StartedAt = startedAt; if (completedAt.HasValue) taskToUpdate.CompletedAt = completedAt; if ((status == TaskExecutionStatus.Completed || status == TaskExecutionStatus.Failed) && !taskToUpdate.StartedAt.HasValue) { taskToUpdate.StartedAt = taskToUpdate.CreatedAt; } if (status == TaskExecutionStatus.Completed || status == TaskExecutionStatus.Failed) { taskToUpdate.CompletedAt ??= DateTime.UtcNow; } await dbContext.SaveChangesAsync(); _logger.LogInformation( "任务状态更新 (VisualRecognitionProcessor): TaskId={TaskId}, Status={Status}, Progress={Progress}%", taskId, status, progress); } else { _logger.LogWarning("尝试在 VisualRecognitionProcessor 中更新不存在的任务状态: TaskId={TaskId}", taskId); } } } }