feat: add image processing error handling and logging

- Introduced a new error code for image processing failures.
- Enhanced the DataSyncService to log effective storage configurations and errors during processing.
- Updated the PhotoBuilderService to remove unused methods and streamline the code.
- Refactored plugin loading to support ESM imports for better compatibility.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-10-30 00:19:50 +08:00
parent 9c61fed159
commit 4ffc8fb5c6
7 changed files with 70 additions and 72 deletions

View File

@@ -14,6 +14,9 @@ export enum ErrorCode {
TENANT_NOT_FOUND = 20, TENANT_NOT_FOUND = 20,
TENANT_SUSPENDED = 21, TENANT_SUSPENDED = 21,
TENANT_INACTIVE = 22, TENANT_INACTIVE = 22,
// Image Processing
IMAGE_PROCESSING_FAILED = 30,
} }
export interface ErrorDescriptor { export interface ErrorDescriptor {
@@ -62,4 +65,9 @@ export const ERROR_CODE_DESCRIPTORS: Record<ErrorCode, ErrorDescriptor> = {
httpStatus: 403, httpStatus: 403,
message: 'Tenant is not active', message: 'Tenant is not active',
}, },
[ErrorCode.IMAGE_PROCESSING_FAILED]: {
httpStatus: 500,
message: 'Image processing failed',
},
} }

View File

@@ -1,17 +1,21 @@
import type {BuilderConfig, PhotoManifestItem, StorageConfig, StorageManager, StorageObject} from '@afilmory/builder'; import type { BuilderConfig, PhotoManifestItem, StorageConfig, StorageManager, StorageObject } from '@afilmory/builder'
import { import { createDefaultBuilderConfig, StorageFactory } from '@afilmory/builder'
createDefaultBuilderConfig,
StorageFactory
} from '@afilmory/builder'
import { import {
EagleStorageProvider, EagleStorageProvider,
GitHubStorageProvider, GitHubStorageProvider,
LocalStorageProvider, LocalStorageProvider,
S3StorageProvider, S3StorageProvider,
} from '@afilmory/builder/storage' } from '@afilmory/builder/storage/index.js'
import type { EagleConfig, EagleRule, GitHubConfig, LocalConfig, S3Config } from '@afilmory/builder/storage/interfaces' import type {
EagleConfig,
EagleRule,
GitHubConfig,
LocalConfig,
S3Config,
} from '@afilmory/builder/storage/interfaces.js'
import type { PhotoAssetConflictPayload, PhotoAssetConflictSnapshot, PhotoAssetManifest } from '@afilmory/db' import type { PhotoAssetConflictPayload, PhotoAssetConflictSnapshot, PhotoAssetManifest } from '@afilmory/db'
import { CURRENT_PHOTO_MANIFEST_VERSION, photoAssets } from '@afilmory/db' import { CURRENT_PHOTO_MANIFEST_VERSION, photoAssets } from '@afilmory/db'
import { createLogger } from '@afilmory/framework'
import { BizException, ErrorCode } from 'core/errors' import { BizException, ErrorCode } from 'core/errors'
import { PhotoBuilderService } from 'core/modules/photo/photo.service' import { PhotoBuilderService } from 'core/modules/photo/photo.service'
import { and, eq } from 'drizzle-orm' import { and, eq } from 'drizzle-orm'
@@ -68,6 +72,7 @@ interface SyncPreparation {
@injectable() @injectable()
export class DataSyncService { export class DataSyncService {
private readonly logger = createLogger('DataSyncService')
constructor( constructor(
private readonly dbAccessor: DbAccessor, private readonly dbAccessor: DbAccessor,
private readonly photoBuilderService: PhotoBuilderService, private readonly photoBuilderService: PhotoBuilderService,
@@ -143,6 +148,8 @@ export class DataSyncService {
): Promise<SyncPreparation> { ): Promise<SyncPreparation> {
const builder = this.photoBuilderService.createBuilder(builderConfig) const builder = this.photoBuilderService.createBuilder(builderConfig)
const effectiveStorageConfig = storageConfig ?? builderConfig.storage const effectiveStorageConfig = storageConfig ?? builderConfig.storage
this.logger.verbose('effectiveStorageConfig', effectiveStorageConfig)
this.registerStorageProviderPlugin(builder, effectiveStorageConfig) this.registerStorageProviderPlugin(builder, effectiveStorageConfig)
if (storageConfig) { if (storageConfig) {
this.photoBuilderService.applyStorageConfig(builder, storageConfig) this.photoBuilderService.applyStorageConfig(builder, storageConfig)
@@ -222,7 +229,7 @@ export class DataSyncService {
break break
} }
default: { default: {
const provider = storageConfig.provider as string const provider = (storageConfig as StorageConfig)?.provider as string
const registered = StorageFactory.getRegisteredProviders() const registered = StorageFactory.getRegisteredProviders()
if (!registered.includes(provider)) { if (!registered.includes(provider)) {
throw new BizException(ErrorCode.COMMON_BAD_REQUEST, { throw new BizException(ErrorCode.COMMON_BAD_REQUEST, {
@@ -777,7 +784,8 @@ export class DataSyncService {
}, },
builder, builder,
}) })
} catch { } catch (err) {
this.logger.error('Failed to process storage object', err)
return null return null
} }
} }
@@ -964,7 +972,7 @@ export class DataSyncService {
const storageObject = storageObjects.find((object) => object.key === record.storageKey) const storageObject = storageObjects.find((object) => object.key === record.storageKey)
if (!storageObject) { if (!storageObject) {
throw new BizException(ErrorCode.COMMON_CONFLICT, { throw new BizException(ErrorCode.IMAGE_PROCESSING_FAILED, {
message: 'Storage object no longer exists; rerun data sync before resolving.', message: 'Storage object no longer exists; rerun data sync before resolving.',
}) })
} }
@@ -973,7 +981,7 @@ export class DataSyncService {
existing: record.manifest?.data as PhotoManifestItem | undefined, existing: record.manifest?.data as PhotoManifestItem | undefined,
}) })
if (!processResult?.item) { if (!processResult?.item) {
throw new BizException(ErrorCode.COMMON_CONFLICT, { message: 'Failed to reprocess storage object.' }) throw new BizException(ErrorCode.IMAGE_PROCESSING_FAILED, { message: 'Failed to reprocess storage object.' })
} }
const storageSnapshot = this.createStorageSnapshot(storageObject) const storageSnapshot = this.createStorageSnapshot(storageObject)

View File

@@ -6,12 +6,11 @@ import type {
PhotoProcessorOptions, PhotoProcessorOptions,
StorageConfig, StorageConfig,
StorageObject, StorageObject,
StorageProvider,
} from '@afilmory/builder' } from '@afilmory/builder'
import { AfilmoryBuilder, processPhotoWithPipeline, StorageFactory, StorageManager } from '@afilmory/builder' import { AfilmoryBuilder, processPhotoWithPipeline } from '@afilmory/builder'
import type { Logger as BuilderLogger } from '@afilmory/builder/logger' import type { Logger as BuilderLogger } from '@afilmory/builder/logger/index.js'
import type { PhotoProcessingLoggers } from '@afilmory/builder/photo' import type { PhotoProcessingLoggers } from '@afilmory/builder/photo/index.js'
import { createPhotoProcessingLoggers, setGlobalLoggers } from '@afilmory/builder/photo' import { createPhotoProcessingLoggers, setGlobalLoggers } from '@afilmory/builder/photo/index.js'
import type { _Object } from '@aws-sdk/client-s3' import type { _Object } from '@aws-sdk/client-s3'
import { injectable } from 'tsyringe' import { injectable } from 'tsyringe'
@@ -42,14 +41,6 @@ export class PhotoBuilderService {
return new AfilmoryBuilder(config) return new AfilmoryBuilder(config)
} }
createStorageManager(config: StorageConfig): StorageManager {
return new StorageManager(config)
}
resolveStorageProvider(config: StorageConfig): StorageProvider {
return StorageFactory.createProvider(config)
}
applyStorageConfig(builder: AfilmoryBuilder, config: StorageConfig): void { applyStorageConfig(builder: AfilmoryBuilder, config: StorageConfig): void {
builder.getStorageManager().switchProvider(config) builder.getStorageManager().switchProvider(config)
} }

View File

@@ -15,9 +15,9 @@ import type { PluginRunState } from '../plugins/manager.js'
import { PluginManager } from '../plugins/manager.js' import { PluginManager } from '../plugins/manager.js'
import type { import type {
BuilderPluginConfigEntry, BuilderPluginConfigEntry,
BuilderPluginESMImporter,
BuilderPluginEventPayloads, BuilderPluginEventPayloads,
} from '../plugins/types.js' } from '../plugins/types.js'
import { isPluginReferenceObject } from '../plugins/types.js'
import type { StorageProviderFactory } from '../storage/factory.js' import type { StorageProviderFactory } from '../storage/factory.js'
import { StorageFactory, StorageManager } from '../storage/index.js' import { StorageFactory, StorageManager } from '../storage/index.js'
import type { BuilderConfig } from '../types/config.js' import type { BuilderConfig } from '../types/config.js'
@@ -555,14 +555,6 @@ export class AfilmoryBuilder {
return return
} }
if (isPluginReferenceObject(ref)) {
const key = ref.resolve
if (seen.has(key)) return
seen.add(key)
references.push(ref)
return
}
const pluginName = ref.name const pluginName = ref.name
if (pluginName) { if (pluginName) {
const key = `plugin:${pluginName}` const key = `plugin:${pluginName}`
@@ -576,7 +568,7 @@ export class AfilmoryBuilder {
const hasPluginWithName = (name: string): boolean => { const hasPluginWithName = (name: string): boolean => {
return references.some((ref) => { return references.some((ref) => {
if (typeof ref === 'string' || isPluginReferenceObject(ref)) { if (typeof ref === 'string') {
return false return false
} }
return ref.name === name return ref.name === name
@@ -591,14 +583,16 @@ export class AfilmoryBuilder {
this.config.repo.enable && this.config.repo.enable &&
!hasPluginWithName('afilmory:github-repo-sync') !hasPluginWithName('afilmory:github-repo-sync')
) { ) {
addReference('@afilmory/builder/plugins/github-repo-sync') addReference(
() => import('@afilmory/builder/plugins/github-repo-sync.js'),
)
} }
const storagePluginByProvider: Record<string, string> = { const storagePluginByProvider: Record<string, BuilderPluginESMImporter> = {
s3: '@afilmory/builder/plugins/storage/s3', s3: () => import('@afilmory/builder/plugins/storage/s3.js'),
github: '@afilmory/builder/plugins/storage/github', github: () => import('@afilmory/builder/plugins/storage/github.js'),
eagle: '@afilmory/builder/plugins/storage/eagle', eagle: () => import('@afilmory/builder/plugins/storage/eagle.js'),
local: '@afilmory/builder/plugins/storage/local', local: () => import('@afilmory/builder/plugins/storage/local.js'),
} }
const storageProvider = this.config.storage.provider const storageProvider = this.config.storage.provider

View File

@@ -96,10 +96,7 @@ export async function processImageWithSharp(
if (isBitmap(imageBuffer)) { if (isBitmap(imageBuffer)) {
try { try {
// Convert the BMP image to JPEG format and create a new Sharp instance for the converted image. // Convert the BMP image to JPEG format and create a new Sharp instance for the converted image.
sharpInstance = await convertBmpToJpegSharpInstance( sharpInstance = await convertBmpToJpegSharpInstance(imageBuffer)
imageBuffer,
loggers.image.originalLogger,
)
// Update the image buffer to reflect the new JPEG data from the Sharp instance. // Update the image buffer to reflect the new JPEG data from the Sharp instance.
processedBuffer = await sharpInstance.toBuffer() processedBuffer = await sharpInstance.toBuffer()
} catch (error) { } catch (error) {
@@ -109,10 +106,7 @@ export async function processImageWithSharp(
} }
// 获取图片元数据(复用 Sharp 实例) // 获取图片元数据(复用 Sharp 实例)
const metadata = await getImageMetadataWithSharp( const metadata = await getImageMetadataWithSharp(sharpInstance)
sharpInstance,
loggers.image.originalLogger,
)
if (!metadata) { if (!metadata) {
loggers.image.error(`获取图片元数据失败:${photoKey}`) loggers.image.error(`获取图片元数据失败:${photoKey}`)
return null return null

View File

@@ -5,10 +5,11 @@ import { pathToFileURL } from 'node:url'
import type { import type {
BuilderPlugin, BuilderPlugin,
BuilderPluginConfigEntry, BuilderPluginConfigEntry,
BuilderPluginESMImporter,
BuilderPluginHooks, BuilderPluginHooks,
BuilderPluginReference, BuilderPluginReference,
} from './types.js' } from './types.js'
import { isPluginReferenceObject } from './types.js' import { isPluginESMImporter } from './types.js'
const requireResolver = createRequire(import.meta.url) const requireResolver = createRequire(import.meta.url)
@@ -26,16 +27,12 @@ interface NormalizedDescriptor {
function normalizeDescriptor( function normalizeDescriptor(
ref: BuilderPluginReference, ref: BuilderPluginReference,
): NormalizedDescriptor { ): NormalizedDescriptor | BuilderPluginESMImporter {
if (typeof ref === 'string') { if (typeof ref === 'string') {
return { specifier: ref } return { specifier: ref }
} }
return { return ref
specifier: ref.resolve,
name: ref.name,
options: ref.options,
}
} }
function resolveSpecifier( function resolveSpecifier(
@@ -127,8 +124,23 @@ export async function loadPlugins(
const results: LoadedPluginDefinition[] = [] const results: LoadedPluginDefinition[] = []
for (const entry of entries) { for (const entry of entries) {
if (typeof entry === 'string' || isPluginReferenceObject(entry)) { if (typeof entry === 'string') {
const descriptor = normalizeDescriptor(entry) const descriptor = normalizeDescriptor(entry)
if (isPluginESMImporter(descriptor)) {
const { default: pluginFactoryOrPlugin } = await descriptor()
const plugin = await instantiatePlugin(pluginFactoryOrPlugin)
const hooks = normalizeHooks(plugin)
const name = plugin.name || `lazy-loaded-plugin-${results.length}`
results.push({
name,
hooks,
pluginOptions: undefined,
})
continue
}
const { resolvedPath } = resolveSpecifier(descriptor.specifier, baseDir) const { resolvedPath } = resolveSpecifier(descriptor.specifier, baseDir)
const mod = await importModule(resolvedPath) const mod = await importModule(resolvedPath)

View File

@@ -15,19 +15,10 @@ import type {
} from '../types/manifest.js' } from '../types/manifest.js'
import type { PhotoManifestItem, ProcessPhotoResult } from '../types/photo.js' import type { PhotoManifestItem, ProcessPhotoResult } from '../types/photo.js'
export type BuilderPluginReference = export type BuilderPluginESMImporter = () => Promise<{
| string default: (() => BuilderPlugin | Promise<BuilderPlugin>) | BuilderPlugin
| { }>
resolve: string export type BuilderPluginReference = string | BuilderPluginESMImporter
/**
* Optional name override for the plugin. Falls back to the resolved name.
*/
name?: string
/**
* Arbitrary configuration passed to the plugin factory.
*/
options?: unknown
}
export type BuilderPluginConfigEntry = BuilderPluginReference | BuilderPlugin export type BuilderPluginConfigEntry = BuilderPluginReference | BuilderPlugin
@@ -185,8 +176,8 @@ export type BuilderPluginFactory =
| (() => BuilderPlugin | Promise<BuilderPlugin>) | (() => BuilderPlugin | Promise<BuilderPlugin>)
| ((options: unknown) => BuilderPlugin | Promise<BuilderPlugin>) | ((options: unknown) => BuilderPlugin | Promise<BuilderPlugin>)
export function isPluginReferenceObject( export function isPluginESMImporter(
value: BuilderPluginConfigEntry, value: BuilderPluginConfigEntry,
): value is Exclude<BuilderPluginReference, string> { ): value is BuilderPluginESMImporter {
return typeof value === 'object' && value !== null && 'resolve' in value return typeof value === 'function' && value.length === 0
} }