You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

135 lines
5.4 KiB

17 hours ago
// 拉取 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/<sign_md5> (全局去重缓存)
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:蓝湖的三处改写需还原 ——
// ① <img src> 被改成 data-src 懒加载 → 还原为 src;
// ② <link> 的 href 被改成 src(浏览器忽略 link 的 src,CSS 全部不加载)→ 还原为 href;
// ③ <body style="display:none;opacity:0"> 防闪烁隐藏,解除逻辑在蓝湖 web 端注入代码 → 直接剥掉
htmlBuf = buf.toString('utf-8')
.replace(/data-src="/g, 'src="')
.replace(/ rel="stylesheet" src="/g, ' rel="stylesheet" href="')
.replace(/<body[^>]*\sstyle="[^"]*display:\s*none[^"]*"[^>]*>/, '<body>')
.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));