Files
live-hub-py/services/yyb-worker/runtime/scripts/generate-devicefp-jsdom.mjs
T

133 lines
4.8 KiB
JavaScript

#!/usr/bin/env node
/**
* 在 jsdom 中执行 goods.shtml 的原始脚本并导出 fp-behv 参数。
*
* 该脚本只生成 DeviceFP,不会提交 fp-behv 或 web_save。
* 调用方必须把同一份 goods HTML、goods URL 与生成的 fp 参数配对使用。
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { JSDOM, ResourceLoader } from 'jsdom';
import { patchEnvironment } from './jsdom-patch-env.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const argv = process.argv.slice(2);
const getArg = (name, fallback = '') => {
const index = argv.indexOf(name);
return index === -1 ? fallback : argv[index + 1];
};
const htmlPath = getArg('--html');
const goodsUrl = getArg('--goods-url');
const cookiePath = getArg('--cookies');
const outputPath = getArg('--output');
const waitMs = Number.parseInt(getArg('--wait', '12000'), 10);
const useArchivedAssets = argv.includes('--archived-assets');
function fail(message) {
console.error(`generate-devicefp-jsdom: ${message}`);
process.exit(2);
}
if (!htmlPath || !goodsUrl || !outputPath) {
fail('用法: node scripts/generate-devicefp-jsdom.mjs --html <goods.html> --goods-url <URL> --output <fp.json> [--cookies <session.json>]');
}
if (!fs.existsSync(htmlPath)) fail(`HTML 不存在: ${htmlPath}`);
if (cookiePath && !fs.existsSync(cookiePath)) fail(`会话文件不存在: ${cookiePath}`);
const archived = {
'vendor.': path.join(ROOT, 'replay', 'vendor.js'),
'cgiVendor.': path.join(ROOT, 'replay', 'cgiVendor.js'),
'goodsBiz.': path.join(ROOT, 'replay', 'goodsBiz.js'),
'goods.': path.join(ROOT, 'replay', 'goods.js'),
};
class GoodsLoader extends ResourceLoader {
fetch(url, options) {
if (!useArchivedAssets) return super.fetch(url, options);
try {
const file = new URL(url).pathname.split('/').pop();
for (const [prefix, localPath] of Object.entries(archived)) {
if (file.startsWith(prefix)) return Promise.resolve(fs.readFileSync(localPath));
}
} catch {
// jsdom will surface resource errors through its virtual console.
}
return Promise.resolve(Buffer.from(''));
}
}
const session = cookiePath ? JSON.parse(fs.readFileSync(cookiePath, 'utf8')) : {};
const cookies = session.cookies || {};
const captured = [];
const resourceErrors = [];
const dom = new JSDOM(fs.readFileSync(htmlPath, 'utf8'), {
url: goodsUrl,
referrer: 'https://z.iwan.yyb.qq.com/',
pretendToBeVisual: true,
runScripts: 'dangerously',
resources: new GoodsLoader(),
beforeParse(window) {
patchEnvironment(window);
for (const [name, value] of Object.entries(cookies)) {
if (value) window.document.cookie = `${name}=${value}`;
}
// goods boot only needs an accepted fp-behv response to continue. Keep all
// traffic local so this process cannot accidentally submit a payment.
const NativeXHR = window.XMLHttpRequest;
window.XMLHttpRequest = class extends NativeXHR {
open(method, url, async = true) {
this.__yybMethod = method;
this.__yybUrl = String(url);
return super.open(method, url, async);
}
send(body) {
const requestBody = String(body || '');
if (this.__yybUrl.includes('fp-behv.fcg')) {
captured.push({ url: this.__yybUrl, body: requestBody });
}
this.readyState = 4;
this.status = 200;
this.responseText = '{"ret":0,"msg":"","interval":10,"page_num":8,"report_flag":0}';
if (typeof this.onreadystatechange === 'function') this.onreadystatechange();
if (typeof this.onload === 'function') this.onload();
}
};
window.addEventListener('error', event => {
resourceErrors.push(String(event.error || event.message || 'unknown error'));
});
},
});
await new Promise(resolve => setTimeout(resolve, Number.isFinite(waitMs) ? waitMs : 12000));
const fp = captured.at(-1);
if (!fp) {
dom.window.close();
fail(`未生成 fp-behv;加载错误: ${resourceErrors.slice(0, 3).join(' | ') || '无'}`);
}
const values = Object.fromEntries(new URLSearchParams(fp.body));
if (!values.SessionID || !values.DeviceFP) {
dom.window.close();
fail('fp-behv 缺少 SessionID 或 DeviceFP');
}
const result = {
generated_at: new Date().toISOString(),
goods_url: goodsUrl,
fp_url: new URL(fp.url, goodsUrl).href,
fp_body: fp.body,
session_id: values.SessionID,
device_fp_length: values.DeviceFP.length,
archived_assets: useArchivedAssets,
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(result, null, 2) + '\n');
dom.window.close();
console.log(`DeviceFP 已保存: ${outputPath}`);
console.log(`SessionID: ${result.session_id}`);
console.log(`DeviceFP 长度: ${result.device_fp_length}`);