#!/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-url --output [--cookies ]'); } 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}`);