@@ -9,32 +9,25 @@ using Foxel.Services.Attributes;
namespace Foxel.Services.Background.Processors
{
public class VisualRecognitionTaskProcessor : ITaskProcessor
public class VisualRecognitionPayload
{
private readonly IDbContextFactory < MyDbContex t > _contextFactory ;
private readonly IServiceProvider _serviceProvider ;
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 int PictureId { ge t ; set ; }
public int? UserIdForPicture { get ; set ; }
}
public class VisualRecognitionTaskProcessor (
IDbContextFactory < MyDbContext > contextFactory ,
IServiceProvider serviceProvider ,
ILogger < VisualRecognitionTaskProcessor > 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 thumbnailForAI DownloadPath = string . Empty ; // Path if thumbnail needs to be downloaded
string thumbnailForAi DownloadPath = 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 < IAiService > ( ) ;
var storageService = scope . ServiceProvider . GetRequiredService < IStorageService > ( ) ;
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 = thumbnailForAI DownloadPath ;
actualThumbnailPathForAI = thumbnailForAi DownloadPath ;
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 < IVectorDbService > ( ) ;
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 < Tag > ( ) ;
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 < 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 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 ( thumbnailForAI DownloadPath ) )
if ( isTempThumbnailFile & & File . Exists ( thumbnailForAi DownloadPath ) )
{
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 ) ;
}
}
}
}
}