/** * 上传结果上报抽象(Reporter)。 * 默认实现为控制台输出,可扩展为企业微信、钉钉等通知。 */ import chalk from 'chalk'; import { SingleBar, Presets } from 'cli-progress'; export type UploadStatus = 'success' | 'failed' | 'skipped'; /** 单个文件的上传结果,供 Reporter 使用 */ export interface UploadFileResult { localPath: string; relativePath: string; bucket: string; key: string; status: UploadStatus; error?: Error; /** 配置了 CDN 时的完整访问地址,由 buildCdnAccessUrl 生成 */ accessUrl?: string; } /** 整次上传的汇总统计(含前缀等上下文,便于查验) */ export interface UploadSummary { total: number; success: string[]; failed: string[]; skipped: string[]; /** 本次上传的前缀/配置,供报告展示 */ context?: { bucket: string; basePrefix: string; pathPrefix?: string; localDir: string; cdnBaseUrl?: string; }; } /** Reporter 接口:上传开始、进度、每个文件结果、全部完成时回调 */ export interface Reporter { onStart?(context: { env?: string; bucket: string; basePrefix: string; pathPrefix?: string; localDir: string; cdnBaseUrl?: string; }): void | Promise; /** 每完成一个文件后调用,便于显示进度(当前数、总数) */ onProgress?(current: number, total: number): void | Promise; onFileResult?(result: UploadFileResult): void | Promise; onComplete?(summary: UploadSummary): void | Promise; } /** 默认实现:将上传进度与结果输出到控制台 */ export class ConsoleReporter implements Reporter { private progressBar: SingleBar | null = null; onStart(context: { env?: string; bucket: string; basePrefix: string; pathPrefix?: string; localDir: string; cdnBaseUrl?: string; }): void { // const { cdnBaseUrl } = context; // if (cdnBaseUrl) { // console.log(` CDN 根地址: ${cdnBaseUrl}`); // } } onProgress(current: number, total: number): void { if (this.progressBar === null) { this.progressBar = new SingleBar( { format: ' 上传进度 |{bar}| {percentage}% | {value}/{total} 文件', barCompleteChar: '\u2588', barIncompleteChar: '\u2591', hideCursor: true, }, Presets.shades_classic, ); this.progressBar.start(total, 0); } this.progressBar.update(current); if (current >= total) { this.progressBar.stop(); this.progressBar = null; } } onFileResult(_result: UploadFileResult): void {} onComplete(summary: UploadSummary): void { const { total, success, failed, skipped, context } = summary; console.log(chalk.bold.cyan('\n ─────────── 上传完成 ───────────')); if (context) { console.log(chalk.bold(' 前缀 / 配置(便于查验)')); console.log(` bucket: ${context.bucket}`); console.log(` basePrefix: ${context.basePrefix}`); if (context.pathPrefix !== undefined && context.pathPrefix !== '') { console.log(` pathPrefix: ${context.pathPrefix}`); } console.log(` localDir: ${context.localDir}`); if (context.cdnBaseUrl) { console.log(` cdnBaseUrl: ${context.cdnBaseUrl}`); } console.log(''); } console.log( ` 总数: ${chalk.bold(total)} ` + chalk.green(`成功: ${success.length}`) + ` ` + chalk.gray(`跳过: ${skipped.length}`) + ` ` + (failed.length > 0 ? chalk.red.bold(`失败: ${failed.length}`) : `失败: ${failed.length}`), ); if (success.length > 0) { console.log(chalk.green(`\n ✓ 成功 (${success.length})`)); success.forEach((key) => console.log(chalk.green(` ${key}`))); } if (skipped.length > 0) { console.log(chalk.gray(`\n - 跳过 (${skipped.length})`)); skipped.forEach((key) => console.log(chalk.gray(` ${key}`))); } if (failed.length > 0) { console.log(chalk.red(`\n ✗ 失败 (${failed.length})`)); failed.forEach((key) => console.log(chalk.red(` ${key}`))); } console.log(''); } } /** 脚本模式:进度与摘要打 stderr,便于调用方从 stdout 只拿到结果数组(如带 --hash 时) */ export class ScriptReporter implements Reporter { onStart(context: { env?: string; bucket: string; basePrefix: string; pathPrefix?: string; localDir: string; cdnBaseUrl?: string; }): void {} onProgress(current: number, total: number): void { process.stderr.write(` 上传进度 ${current}/${total} 文件\n`); } onFileResult(_result: UploadFileResult): void {} onComplete(summary: UploadSummary): void { const { total, success, failed, skipped } = summary; process.stderr.write( `\n 上传完成 总数: ${total} 成功: ${success.length} 跳过: ${skipped.length} 失败: ${failed.length}\n`, ); } }