// 拉取 A7 全部页面(正式树 31 + backup 数据共享 3)到本地可渲染目录结构。 // 用法: node fetch-a7-pages.mjs [--only=a7-3-1] // 产物: .scratch/a7-parity/pages/<页面url去.html>/<原相对引用路径> (file:// 可直接渲染) // .scratch/a7-parity/oss-cache/ (全局去重缓存) import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; const ROOT = 'e:/code/crm-backend-matt/.scratch/a7-parity'; const LANHU = 'e:/code/crm-backend-matt/.scratch/lanhu-latest'; const OSS = 'https://lanhu-axure-file.oss-cn-beijing.aliyuncs.com/'; const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'; const CONCURRENCY = 8; const only = process.argv.find(a => a.startsWith('--only=')); const onlyKey = only ? only.split('=')[1] : null; const doc = JSON.parse(fs.readFileSync(path.join(LANHU, 'axure-v29.json'), 'utf-8')); const pages = doc.pages; // A7 页面(key = 页面名.html) const a7 = Object.entries(pages).filter(([k]) => k.toUpperCase().startsWith('A7')); console.log(`A7 pages in manifest: ${a7.length}`); if (onlyKey) { const filtered = a7.filter(([k]) => k.includes(onlyKey)); console.log(`--only=${onlyKey} matched: ${filtered.length}`); if (filtered.length) { a7.length = 0; a7.push(...filtered); } } const cacheDir = path.join(ROOT, 'oss-cache'); fs.mkdirSync(cacheDir, { recursive: true }); const stats = { downloaded: 0, cacheHit: 0, failed: [], bytes: 0 }; async function fetchBuf(url, tries = 3) { let lastErr; for (let i = 0; i < tries; i++) { try { const res = await fetch(url, { headers: { 'User-Agent': UA } }); if (!res.ok) throw new Error(`HTTP ${res.status} ${url}`); return Buffer.from(await res.arrayBuffer()); } catch (e) { lastErr = e; await new Promise(r => setTimeout(r, 800 * (i + 1))); } } throw lastErr; } // 下载 sign_md5 指向的 OSS 对象,优先 oss-cache async function fetchSigned(signMd5) { const isUrl = signMd5.startsWith('http'); const cacheKey = isUrl ? 'url_' + crypto.createHash('md5').update(signMd5).digest('hex') : signMd5; const cachePath = path.join(cacheDir, cacheKey); if (fs.existsSync(cachePath)) { stats.cacheHit++; return fs.readFileSync(cachePath); } const url = isUrl ? signMd5 : OSS + signMd5; const buf = await fetchBuf(url); fs.writeFileSync(cachePath, buf); stats.downloaded++; stats.bytes += buf.length; return buf; } function safeName(relKey) { // Windows 路径合法化(mapping key 已是解码后相对路径;替换非法字符) return relKey.split('/').map(seg => seg.replace(/[<>:"|?*]/g, '_')).join('/'); } // 单页:下载 mapping → html(data-src→src)→ 全部资源 async function fetchPage(key, entry) { const pageDir = path.join(ROOT, 'pages', safeName(key.replace(/\.html$/, ''))); const mappingBuf = await fetchSigned(entry.mapping_md5); const mapping = JSON.parse(mappingBuf.toString('utf-8')); const jobs = []; // [relKey, signMd5] for (const [rel, v] of Object.entries(mapping.styles || {})) jobs.push([rel, v.sign_md5]); for (const [rel, v] of Object.entries(mapping.scripts || {})) jobs.push([rel, v.sign_md5]); for (const [rel, v] of Object.entries(mapping.images || {})) jobs.push([rel, v.sign_md5]); if (mapping.dataJs?.src) jobs.push([mapping.dataJs.src, mapping.dataJs.sign_md5]); if (mapping.html?.src) jobs.push([mapping.html.src, mapping.html.sign_md5]); let htmlBuf = null; for (const [rel, sign] of jobs) { const buf = await fetchSigned(sign); const dest = path.join(pageDir, safeName(rel)); fs.mkdirSync(path.dirname(dest), { recursive: true }); if (rel === mapping.html.src) { // 主页面 HTML:蓝湖的三处改写需还原 —— // ① 被改成 data-src 懒加载 → 还原为 src; // ② 的 href 被改成 src(浏览器忽略 link 的 src,CSS 全部不加载)→ 还原为 href; // ③ 防闪烁隐藏,解除逻辑在蓝湖 web 端注入代码 → 直接剥掉 htmlBuf = buf.toString('utf-8') .replace(/data-src="/g, 'src="') .replace(/ rel="stylesheet" src="/g, ' rel="stylesheet" href="') .replace(/]*\sstyle="[^"]*display:\s*none[^"]*"[^>]*>/, '') .replace(/srcset="/g, 'data-srcset-disabled="'); fs.writeFileSync(dest, htmlBuf, 'utf-8'); } else { fs.writeFileSync(dest, buf); } } return { key, dir: pageDir, assets: jobs.length }; } // 并发队列 const queue = [...a7]; const results = []; async function worker(id) { while (queue.length) { const [key, entry] = queue.shift(); try { const r = await fetchPage(key, entry); console.log(`[w${id}] OK ${key} (${r.assets} assets)`); results.push({ key, ok: true, assets: r.assets }); } catch (e) { console.error(`[w${id}] FAIL ${key}: ${e.message}`); results.push({ key, ok: false, error: e.message }); stats.failed.push(key); } } } await Promise.all(Array.from({ length: CONCURRENCY }, (_, i) => worker(i))); console.log('=== summary ==='); console.log(`pages ok: ${results.filter(r => r.ok).length}/${a7.length}`); console.log(`oss downloads: ${stats.downloaded} (cache hits: ${stats.cacheHit}), ${(stats.bytes / 1048576).toFixed(1)} MB`); if (stats.failed.length) console.log('failed:', stats.failed.join(', ')); fs.writeFileSync(path.join(ROOT, 'fetch-a7.log'), JSON.stringify({ results, stats }, null, 2));