From ff73de44c8e20a298895305ba4c73beaaf70b500 Mon Sep 17 00:00:00 2001 From: Innei Date: Thu, 13 Nov 2025 00:42:43 +0800 Subject: [PATCH] feat(data-sync): implement logging for data synchronization process - Added logging functionality to the DataSyncService to track progress and errors during the manifest generation process. - Introduced DataSyncLogLevel and DataSyncLogPayload types for structured logging. - Updated emitLog method to handle different log levels (info, success, warn, error) and include relevant details. - Enhanced PhotoSyncProgressState to maintain a log of synchronization events, displayed in the PhotoSyncProgressPanel. Signed-off-by: Innei --- .../data-sync/data-sync.service.ts | 88 ++++++++++++++++++- .../data-sync/data-sync.types.ts | 15 ++++ .../modules/photos/components/PhotoPage.tsx | 31 +++++++ .../sync/PhotoSyncProgressPanel.tsx | 81 ++++++++++++++++- be/apps/dashboard/src/modules/photos/types.ts | 26 ++++++ packages/builder/src/image/exif.ts | 10 +-- 6 files changed, 244 insertions(+), 7 deletions(-) diff --git a/be/apps/core/src/modules/infrastructure/data-sync/data-sync.service.ts b/be/apps/core/src/modules/infrastructure/data-sync/data-sync.service.ts index b49b8b32..adbdba34 100644 --- a/be/apps/core/src/modules/infrastructure/data-sync/data-sync.service.ts +++ b/be/apps/core/src/modules/infrastructure/data-sync/data-sync.service.ts @@ -14,6 +14,7 @@ import type { ConflictPayload, DataSyncAction, DataSyncConflict, + DataSyncLogLevel, DataSyncOptions, DataSyncProgressEmitter, DataSyncProgressStage, @@ -368,6 +369,10 @@ export class DataSyncService { const result = await this.safeProcessStorageObject(storageObject, builder, { livePhotoMap, + progress: { + emitter: onProgress, + stage: 'missing-in-db', + }, }) if (!result?.item) { @@ -944,6 +949,33 @@ export class DataSyncService { }) } + private async emitLog( + emitter: DataSyncProgressEmitter | undefined, + payload: { + level: DataSyncLogLevel + message: string + stage?: DataSyncProgressStage | null + storageKey?: string + details?: Record | null + }, + ): Promise { + if (!emitter) { + return + } + + await emitter({ + type: 'log', + payload: { + level: payload.level, + message: payload.message, + stage: payload.stage ?? null, + storageKey: payload.storageKey, + details: payload.details ?? null, + timestamp: new Date().toISOString(), + }, + }) + } + private async emitComplete(emitter: DataSyncProgressEmitter | undefined, result: DataSyncResult): Promise { if (!emitter) { return @@ -1035,10 +1067,29 @@ export class DataSyncService { options: { existing?: PhotoManifestItem | null livePhotoMap?: Map + progress?: { + emitter?: DataSyncProgressEmitter + stage?: DataSyncProgressStage | null + } }, ) { + const progressContext = options.progress + const stage = progressContext?.stage ?? null + const emitter = progressContext?.emitter + + await this.emitLog(emitter, { + level: 'info', + message: '开始生成 manifest', + stage, + storageKey: storageObject.key, + details: { + hasExistingManifest: Boolean(options.existing), + hasLivePhotoMap: Boolean(options.livePhotoMap), + }, + }) + try { - return await this.photoBuilderService.processPhotoFromStorageObject(storageObject, { + const result = await this.photoBuilderService.processPhotoFromStorageObject(storageObject, { existingItem: options.existing ?? undefined, livePhotoMap: options.livePhotoMap, processorOptions: { @@ -1047,7 +1098,42 @@ export class DataSyncService { }, builder, }) + + if (result?.item) { + await this.emitLog(emitter, { + level: 'success', + message: '生成 manifest 成功', + stage, + storageKey: storageObject.key, + details: { + photoId: result.item.id, + resultType: result.type ?? null, + }, + }) + } else { + await this.emitLog(emitter, { + level: 'warn', + message: '生成 manifest 未返回照片数据', + stage, + storageKey: storageObject.key, + details: { + resultType: result?.type ?? null, + }, + }) + } + + return result } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error' + await this.emitLog(emitter, { + level: 'error', + message: '生成 manifest 失败', + stage, + storageKey: storageObject.key, + details: { + error: message, + }, + }) this.logger.error('Failed to process storage object', err) return null } diff --git a/be/apps/core/src/modules/infrastructure/data-sync/data-sync.types.ts b/be/apps/core/src/modules/infrastructure/data-sync/data-sync.types.ts index daee7e0f..1464e45e 100644 --- a/be/apps/core/src/modules/infrastructure/data-sync/data-sync.types.ts +++ b/be/apps/core/src/modules/infrastructure/data-sync/data-sync.types.ts @@ -89,6 +89,17 @@ export interface DataSyncStageTotals { 'status-reconciliation': number } +export type DataSyncLogLevel = 'info' | 'success' | 'warn' | 'error' + +export interface DataSyncLogPayload { + level: DataSyncLogLevel + message: string + timestamp: string + stage?: DataSyncProgressStage | null + storageKey?: string + details?: Record | null +} + export type DataSyncProgressEvent = | { type: 'start' @@ -128,5 +139,9 @@ export type DataSyncProgressEvent = message: string } } + | { + type: 'log' + payload: DataSyncLogPayload + } export type DataSyncProgressEmitter = (event: DataSyncProgressEvent) => Promise | void diff --git a/be/apps/dashboard/src/modules/photos/components/PhotoPage.tsx b/be/apps/dashboard/src/modules/photos/components/PhotoPage.tsx index 9c5866ea..04aa1757 100644 --- a/be/apps/dashboard/src/modules/photos/components/PhotoPage.tsx +++ b/be/apps/dashboard/src/modules/photos/components/PhotoPage.tsx @@ -42,6 +42,8 @@ const STAGE_ORDER: PhotoSyncProgressStage[] = [ 'status-reconciliation', ] +const MAX_SYNC_LOGS = 200 + function createInitialStages(totals: PhotoSyncProgressState['totals']): PhotoSyncProgressState['stages'] { return STAGE_ORDER.reduce( (acc, stage) => { @@ -109,6 +111,7 @@ export function PhotoPage() { stages: createInitialStages(totals), startedAt: Date.now(), updatedAt: Date.now(), + logs: [], lastAction: undefined, error: undefined, }) @@ -135,6 +138,34 @@ export function PhotoPage() { return } + if (event.type === 'log') { + setSyncProgress((prev) => { + if (!prev) { + return prev + } + + const parsedTimestamp = Date.parse(event.payload.timestamp) + const entry = { + id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + timestamp: Number.isNaN(parsedTimestamp) ? Date.now() : parsedTimestamp, + level: event.payload.level, + message: event.payload.message, + stage: event.payload.stage ?? null, + storageKey: event.payload.storageKey ?? undefined, + details: event.payload.details ?? null, + } + + const nextLogs = prev.logs.length >= MAX_SYNC_LOGS ? [...prev.logs.slice(1), entry] : [...prev.logs, entry] + + return { + ...prev, + logs: nextLogs, + updatedAt: Date.now(), + } + }) + return + } + if (event.type === 'stage') { setSyncProgress((prev) => { if (!prev) { diff --git a/be/apps/dashboard/src/modules/photos/components/sync/PhotoSyncProgressPanel.tsx b/be/apps/dashboard/src/modules/photos/components/sync/PhotoSyncProgressPanel.tsx index d398f2a1..4332fee1 100644 --- a/be/apps/dashboard/src/modules/photos/components/sync/PhotoSyncProgressPanel.tsx +++ b/be/apps/dashboard/src/modules/photos/components/sync/PhotoSyncProgressPanel.tsx @@ -2,7 +2,7 @@ import { Spring } from '@afilmory/utils' import { m } from 'motion/react' import { getConflictTypeLabel, PHOTO_ACTION_TYPE_CONFIG } from '../../constants' -import type { PhotoSyncAction, PhotoSyncProgressStage, PhotoSyncProgressState } from '../../types' +import type { PhotoSyncAction, PhotoSyncLogLevel, PhotoSyncProgressStage, PhotoSyncProgressState } from '../../types' import { BorderOverlay } from './PhotoSyncResultPanel' const STAGE_CONFIG: Record = { @@ -37,6 +37,13 @@ const STATUS_LABEL: Record = { + info: { label: '信息', className: 'border border-sky-500/30 bg-sky-500/10 text-sky-200' }, + success: { label: '成功', className: 'border border-emerald-500/30 bg-emerald-500/10 text-emerald-200' }, + warn: { label: '警告', className: 'border border-amber-500/30 bg-amber-500/10 text-amber-200' }, + error: { label: '错误', className: 'border border-rose-500/30 bg-rose-500/10 text-rose-200' }, +} + const SUMMARY_FIELDS: Array<{ key: keyof PhotoSyncProgressState['summary'] label: string @@ -46,6 +53,20 @@ const SUMMARY_FIELDS: Array<{ { key: 'conflicts', label: '冲突' }, ] +const timeFormatter = new Intl.DateTimeFormat(undefined, { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', +}) + +function formatLogTimestamp(timestamp: number): string { + try { + return timeFormatter.format(timestamp) + } catch { + return '--:--:--' + } +} + type PhotoSyncProgressPanelProps = { progress: PhotoSyncProgressState } @@ -94,6 +115,7 @@ export function PhotoSyncProgressPanel({ progress }: PhotoSyncProgressPanelProps })) const { lastAction } = progress + const recentLogs = progress.logs.slice(-8).reverse() return (
@@ -147,6 +169,63 @@ export function PhotoSyncProgressPanel({ progress }: PhotoSyncProgressPanelProps ))}
+ {recentLogs.length > 0 ? ( +
+ +
+

构建日志

+ 最新 {recentLogs.length} 条 +
+
+ {recentLogs.map((log) => { + const levelConfig = LOG_LEVEL_CONFIG[log.level] + const details = (log.details ?? null) as Record | null + const photoId = details && typeof details['photoId'] === 'string' ? (details['photoId'] as string) : null + const resultType = + details && typeof details['resultType'] === 'string' ? (details['resultType'] as string) : null + const error = details && typeof details['error'] === 'string' ? (details['error'] as string) : null + const hasExisting = + details && typeof details['hasExistingManifest'] === 'boolean' + ? (details['hasExistingManifest'] as boolean) + : null + const hasLivePhotoMap = + details && typeof details['hasLivePhotoMap'] === 'boolean' + ? (details['hasLivePhotoMap'] as boolean) + : null + + const detailSegments: string[] = [] + if (photoId) detailSegments.push(`ID ${photoId}`) + if (resultType) detailSegments.push(`结果 ${resultType}`) + if (typeof hasExisting === 'boolean') { + detailSegments.push(hasExisting ? '包含历史 manifest' : '无历史 manifest') + } + if (typeof hasLivePhotoMap === 'boolean') { + detailSegments.push(hasLivePhotoMap ? '包含 Live Photo' : '无 Live Photo') + } + if (error) detailSegments.push(`错误 ${error}`) + + return ( +
+ {formatLogTimestamp(log.timestamp)} + + {levelConfig.label} + + {log.message} + {log.storageKey ? {log.storageKey} : null} + {log.stage ? {STAGE_CONFIG[log.stage].label} : null} + {detailSegments.length > 0 ? ( + {detailSegments.join(' · ')} + ) : null} +
+ ) + })} +
+
+ ) : null} + {lastAction ? (
diff --git a/be/apps/dashboard/src/modules/photos/types.ts b/be/apps/dashboard/src/modules/photos/types.ts index dfb441b8..01551016 100644 --- a/be/apps/dashboard/src/modules/photos/types.ts +++ b/be/apps/dashboard/src/modules/photos/types.ts @@ -78,6 +78,17 @@ export interface PhotoSyncStageTotals { 'status-reconciliation': number } +export type PhotoSyncLogLevel = 'info' | 'success' | 'warn' | 'error' + +export interface PhotoSyncLogPayload { + level: PhotoSyncLogLevel + message: string + timestamp: string + stage?: PhotoSyncProgressStage | null + storageKey?: string + details?: Record | null +} + export type PhotoSyncProgressEvent = | { type: 'start' @@ -117,6 +128,10 @@ export type PhotoSyncProgressEvent = message: string } } + | { + type: 'log' + payload: PhotoSyncLogPayload + } export type PhotoSyncStageStatus = 'pending' | 'running' | 'completed' @@ -134,6 +149,7 @@ export interface PhotoSyncProgressState { > startedAt: number updatedAt: number + logs: PhotoSyncLogEntry[] lastAction?: { stage: PhotoSyncProgressStage index: number @@ -143,6 +159,16 @@ export interface PhotoSyncProgressState { error?: string } +export interface PhotoSyncLogEntry { + id: string + timestamp: number + level: PhotoSyncLogLevel + message: string + stage?: PhotoSyncProgressStage | null + storageKey?: string + details?: Record | null +} + export interface PhotoAssetManifestPayload { version: string data: PhotoManifestItem diff --git a/packages/builder/src/image/exif.ts b/packages/builder/src/image/exif.ts index 7105885d..bd227ed2 100644 --- a/packages/builder/src/image/exif.ts +++ b/packages/builder/src/image/exif.ts @@ -14,6 +14,9 @@ import type { PickedExif } from '../types/photo.js' export async function extractExifData(imageBuffer: Buffer, originalBuffer?: Buffer): Promise { const log = getGlobalLoggers().exif + await mkdir('/tmp/image_process', { recursive: true }) + const tempImagePath = path.resolve('/tmp/image_process', `${crypto.randomUUID()}.jpg`) + try { log.info('开始提取 EXIF 数据') @@ -35,15 +38,10 @@ export async function extractExifData(imageBuffer: Buffer, originalBuffer?: Buff return null } - await mkdir('/tmp/image_process', { recursive: true }) - const tempImagePath = path.resolve('/tmp/image_process', `${crypto.randomUUID()}.jpg`) - await writeFile(tempImagePath, originalBuffer || imageBuffer) const exifData = await exiftool.read(tempImagePath) const result = handleExifData(exifData, metadata) - await unlink(tempImagePath).catch(noop) - if (!exifData) { log.warn('EXIF 数据解析失败') return null @@ -59,6 +57,8 @@ export async function extractExifData(imageBuffer: Buffer, originalBuffer?: Buff } catch (error) { log.error('提取 EXIF 数据失败:', error) return null + } finally { + await unlink(tempImagePath).catch(noop) } }