import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; import * as fsp from 'node:fs/promises'; // Node 22+ ships fs.promises.glob but TS types lag behind const glob = (fsp as unknown as { glob: (pattern: string, opts: { cwd: string }) => AsyncIterable; }).glob; const WX_CACHE_ROOT = join( /*turbopackIgnore: true*/ homedir(), 'Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files', ); let _userDirCache: string | null = null; /** 找到当前微信用户目录(取最近修改的) */ function findUserDir(): string | null { if (_userDirCache && existsSync(/*turbopackIgnore: true*/ _userDirCache)) return _userDirCache; if (!existsSync(/*turbopackIgnore: true*/ WX_CACHE_ROOT)) return null; const entries = readdirSync(/*turbopackIgnore: true*/ WX_CACHE_ROOT, { withFileTypes: true }) .filter((e) => e.isDirectory() && e.name !== 'all_users' && e.name !== 'Backup') .map((e) => { const p = join(/*turbopackIgnore: true*/ WX_CACHE_ROOT, e.name); return { p, mtime: statSync(/*turbopackIgnore: true*/ p).mtimeMs }; }) .sort((a, b) => b.mtime - a.mtime); if (entries.length === 0) return null; _userDirCache = entries[0].p; return _userDirCache; } /** 列出按月分的子目录,按时间倒序(最近月份优先) */ function listMonthDirs(userDir: string): string[] { const cacheDir = join(/*turbopackIgnore: true*/ userDir, 'cache'); if (!existsSync(/*turbopackIgnore: true*/ cacheDir)) return []; return readdirSync(/*turbopackIgnore: true*/ cacheDir) .filter((d) => /^\d{4}-\d{2}$/.test(d)) .sort((a, b) => b.localeCompare(a)); } export interface ResolvedImage { path: string; type: 'hd' | 'mid' | 'thumb'; format: 'png' | 'jpeg' | 'gif' | 'bmp' | 'bin'; } /** 检测文件 magic bytes */ function detectFormat(path: string): ResolvedImage['format'] { try { const fd = readFileSync(/*turbopackIgnore: true*/ path, { flag: 'r' }); const h = fd.subarray(0, 4); if (h[0] === 0xff && h[1] === 0xd8) return 'jpeg'; if (h[0] === 0x89 && h[1] === 0x50 && h[2] === 0x4e && h[3] === 0x47) return 'png'; if (h[0] === 0x47 && h[1] === 0x49 && h[2] === 0x46) return 'gif'; if (h[0] === 0x42 && h[1] === 0x4d) return 'bmp'; } catch {} return 'bin'; } // 月份 → { localId → ResolvedImage } 索引(懒加载) const monthIndexCache = new Map>(); const monthIndexLoading = new Map>(); async function buildMonthIndex(userDir: string, month: string): Promise { if (monthIndexCache.has(month)) return; const existing = monthIndexLoading.get(month); if (existing) return existing; const p = (async () => { const monthRoot = join(/*turbopackIgnore: true*/ userDir, 'cache', month, 'Message'); if (!existsSync(/*turbopackIgnore: true*/ monthRoot)) { monthIndexCache.set(month, new Map()); return; } const idx = new Map(); const priority: Record = { hd: 3, mid: 2, thumb: 1 }; const consider = (path: string, type: ResolvedImage['type']) => { const m = /\/(\d+)_/.exec(path); if (!m) return; const id = Number(m[1]); const cur = idx.get(id); if (!cur || priority[type] > priority[cur.type]) { // 推迟 detectFormat 到实际请求时 idx.set(id, { path, type, format: 'bin' }); } }; for await (const p of glob('*/ImageTemp/*hd_temp_convert', { cwd: monthRoot })) { consider(join(/*turbopackIgnore: true*/ monthRoot, String(p)), 'hd'); } for await (const p of glob('*/ImageTemp/*mid_temp_convert', { cwd: monthRoot })) { consider(join(/*turbopackIgnore: true*/ monthRoot, String(p)), 'mid'); } for await (const p of glob('*/Thumb/*thumb.jpg', { cwd: monthRoot })) { consider(join(/*turbopackIgnore: true*/ monthRoot, String(p)), 'thumb'); } monthIndexCache.set(month, idx); })(); monthIndexLoading.set(month, p); await p; monthIndexLoading.delete(month); } /** * 按 local_id 在 wx 缓存找图。优先 hint 月份,否则按月扫到旧。 * 用懒加载的内存索引加速:每月扫一次后命中 ~1ms。 */ export async function resolveWxImage( localId: number, hintMonth?: string, ): Promise { const userDir = findUserDir(); if (!userDir) return null; const months = listMonthDirs(userDir); if (months.length === 0) return null; const ordered = hintMonth && months.includes(hintMonth) ? [hintMonth, ...months.filter((m) => m !== hintMonth)] : months; for (const m of ordered) { await buildMonthIndex(userDir, m); const idx = monthIndexCache.get(m); if (!idx) continue; const hit = idx.get(localId); if (hit) { return { ...hit, format: detectFormat(hit.path) }; } } return null; } const MIME: Record = { png: 'image/png', jpeg: 'image/jpeg', gif: 'image/gif', bmp: 'image/bmp', bin: 'application/octet-stream', }; export function mimeFor(format: ResolvedImage['format']): string { return MIME[format]; }