export type FsErrorCode = | "EXDEV" // 跨设备,不会自动降级 | "EEXIST" // 目标已存在(no-clobber) | "ENOENT" | "EACCES" | "ENOSPC" | "VERIFY_FAILED" | "SOURCE_CHANGED" | "ROOT_OFFLINE" | "ABORTED" /** 文件操作挂死(进度停滞/快操作超时),执行子进程已被回收;与 IO 同属可退避重试,不代表文件缺失 */ | "TIMEOUT" /** 本地文件操作收到 Alist 虚拟路径(/@alist/…):调用方没按存储分派,拒绝而不是写错位置 */ | "REMOTE_PATH_ON_LOCAL_FS" | "IO"; /** * 错误码 → 处置类别(对照 MP:所有 IO 故障归 OSError 复用重试;我们更细): * - retry:可退避重试(IO / TIMEOUT / ROOT_OFFLINE / SOURCE_CHANGED),超限后进待处理 * - attention:直接进待处理、不重试(EACCES / ENOSPC / EXDEV) * - manual:需人工判断(EEXIST / VERIFY_FAILED / ENOENT) * - aborted:被取消 */ export type FsErrorClass = "retry" | "attention" | "manual" | "aborted"; export function fsErrorClass(code: string): FsErrorClass { switch (code) { case "IO": case "TIMEOUT": case "ROOT_OFFLINE": case "SOURCE_CHANGED": return "retry"; case "EACCES": case "ENOSPC": case "EXDEV": return "attention"; case "ABORTED": return "aborted"; default: return "manual"; } } export class FsError extends Error { override readonly name = "FsError"; constructor( readonly code: FsErrorCode, message: string, readonly detail: { path?: string; syscall?: string; cause?: unknown } = {}, ) { super(message); } } export function errnoOf(e: unknown): string | undefined { return (e as NodeJS.ErrnoException | undefined)?.code; } export function toFsError(e: unknown, path?: string): FsError { if (e instanceof FsError) return e; const code = errnoOf(e); const mapped: FsErrorCode = code === "EXDEV" || code === "EEXIST" || code === "ENOENT" || code === "ENOSPC" ? code : code === "EACCES" || code === "EPERM" ? "EACCES" : code === "ETIMEDOUT" ? "TIMEOUT" : (e as Error)?.name === "AbortError" ? "ABORTED" : "IO"; return new FsError(mapped, (e as Error)?.message ?? String(e), { ...(path !== undefined ? { path } : {}), ...((e as NodeJS.ErrnoException)?.syscall ? { syscall: (e as NodeJS.ErrnoException).syscall! } : {}), cause: e, }); }