Files
reverse-skills-max/vendor-new/xbsReverseSkill/web-js-env-patcher/scripts/import_ruyitrace_log.js

278 lines
13 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const crypto = require('crypto');
function parseArgs(argv) {
const args = {
input: '',
caseDir: '',
name: '',
maxExamples: 10,
truncationThreshold: 3900,
maxTruncationExamples: 50,
json: false,
markdown: false,
};
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--input') args.input = argv[++i] || '';
else if (a === '--case-dir' || a === '--dir') args.caseDir = argv[++i] || '';
else if (a === '--name') args.name = argv[++i] || '';
else if (a === '--max-examples') args.maxExamples = Number(argv[++i] || '10');
else if (a === '--truncation-threshold') args.truncationThreshold = Number(argv[++i] || '3900');
else if (a === '--max-truncation-examples') args.maxTruncationExamples = Number(argv[++i] || '50');
else if (a === '--json') args.json = true;
else if (a === '--markdown') args.markdown = true;
else if (a === '--help' || a === '-h') args.help = true;
else throw new Error(`未知参数:${a}`);
}
if (!Number.isFinite(args.truncationThreshold) || args.truncationThreshold < 1) args.truncationThreshold = 3900;
if (!Number.isFinite(args.maxTruncationExamples) || args.maxTruncationExamples < 1) args.maxTruncationExamples = 50;
if (!args.json && !args.markdown) args.markdown = true;
return args;
}
function usage() {
return `用法:
node scripts/import_ruyitrace_log.js --input <trace.ndjson> --case-dir case --markdown
node scripts/import_ruyitrace_log.js --input <trace.ndjson> --case-dir case --truncation-threshold 3900 --json
说明:复制 RuyiTrace NDJSON 日志到 case/ruyi-trace/logs/,生成 notes/ruyitrace-summary.md,并标记接近 4000 / 4096 字符的字段为“疑似被 RuyiTrace 截断”。`;
}
function exists(p) {
try { return fs.existsSync(p); } catch { return false; }
}
function safeName(name) {
return String(name || '').replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').slice(0, 180);
}
function inc(map, key) {
key = key || '(空)';
map.set(key, (map.get(key) || 0) + 1);
}
function top(map, n) {
return Array.from(map.entries()).sort((a, b) => b[1] - a[1] || String(a[0]).localeCompare(String(b[0]))).slice(0, n).map(([key, count]) => ({ key, count }));
}
function classifyApi(api) {
api = String(api || '');
if (/Canvas|CanvasRenderingContext2D|OffscreenCanvas/.test(api)) return 'canvas';
if (/WebGL|GLRenderingContext/.test(api)) return 'webgl';
if (/Audio|Oscillator|Analyser|OfflineAudioContext/.test(api)) return 'audio';
if (/Navigator|navigator/.test(api)) return 'navigator';
if (/Screen|screen/.test(api)) return 'screen';
if (/Crypto|getRandomValues|randomUUID/.test(api)) return 'crypto';
if (/Performance|performance/.test(api)) return 'performance';
if (/Storage|localStorage|sessionStorage|IndexedDB|IDB/.test(api)) return 'storage';
if (/WebRTC|RTCPeerConnection|MediaDevices/.test(api)) return 'webrtc';
if (/Worker|ServiceWorker|postMessage|MessageChannel/.test(api)) return 'worker-message';
if (/Document|Element|Node|CSS|Style|Layout|DOMRect/.test(api)) return 'dom-layout';
return 'other';
}
function visibleHash(value) {
return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
}
function preview(value, side = 80) {
const text = String(value || '');
if (text.length <= side * 2) return text;
return `${text.slice(0, side)}...${text.slice(-side)}`;
}
function stackBrief(evt) {
const stack = Array.isArray(evt && evt.stack) ? evt.stack : [];
const first = stack.find(s => s && (s.file || s.line || s.col));
if (!first) return '';
const loc = [first.file || '', first.line || '', first.col || ''].filter(v => v !== '').join(':');
return loc;
}
function walkStrings(value, visitor, currentPath = '') {
if (typeof value === 'string') {
visitor(currentPath || '$', value);
return;
}
if (!value || typeof value !== 'object') return;
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) walkStrings(value[i], visitor, `${currentPath}[${i}]`);
return;
}
for (const [key, child] of Object.entries(value)) {
const nextPath = currentPath ? `${currentPath}.${key}` : key;
walkStrings(child, visitor, nextPath);
}
}
function collectTruncationSignals(evt, lineNo, threshold, maxExamples, state) {
const api = evt.api || evt.name || evt.path || '';
walkStrings(evt, (fieldPath, value) => {
const visibleLength = value.length;
if (visibleLength < threshold) return;
state.totalSuspectedFields++;
state.maxVisibleLength = Math.max(state.maxVisibleLength, visibleLength);
inc(state.byFieldPath, fieldPath);
inc(state.byApi, api || '(空)');
if (state.examples.length >= maxExamples) return;
state.examples.push({
line: lineNo,
api: api || '(空)',
fieldPath,
visibleLength,
minLength: visibleLength,
actualLength: 'unknown',
truncationSuspected: true,
reason: `字段可见长度达到阈值 ${threshold},RuyiTrace 可能已截断长字符串,不能把可见长度当成真实长度。`,
stack: stackBrief(evt),
visibleSha256: visibleHash(value),
visiblePreview: preview(value),
});
});
}
function sanitizeLongStrings(value, threshold, currentPath = '') {
if (typeof value === 'string') {
if (value.length < threshold) return value;
return {
__ruyiTraceLongString__: true,
fieldPath: currentPath || '$',
visibleLength: value.length,
minLength: value.length,
actualLength: 'unknown',
truncationSuspected: true,
visibleSha256: visibleHash(value),
visiblePreview: preview(value),
note: '该字符串接近或超过 RuyiTrace 截断阈值,示例中不保留完整可见内容,真实长度未知。',
};
}
if (!value || typeof value !== 'object') return value;
if (Array.isArray(value)) return value.map((item, index) => sanitizeLongStrings(item, threshold, `${currentPath}[${index}]`));
const out = {};
for (const [key, child] of Object.entries(value)) {
const nextPath = currentPath ? `${currentPath}.${key}` : key;
out[key] = sanitizeLongStrings(child, threshold, nextPath);
}
return out;
}
async function summarizeNdjson(file, options) {
const apiCounts = new Map();
const typeCounts = new Map();
const categoryCounts = new Map();
const fileCounts = new Map();
const examples = [];
const truncationState = { totalSuspectedFields: 0, maxVisibleLength: 0, byFieldPath: new Map(), byApi: new Map(), examples: [] };
let lines = 0, parsed = 0, invalid = 0;
const rl = readline.createInterface({ input: fs.createReadStream(file, { encoding: 'utf8' }), crlfDelay: Infinity });
for await (const raw of rl) {
const line = raw.replace(/^\uFEFF/, '').trim();
if (!line) continue;
lines++;
let evt;
try { evt = JSON.parse(line); parsed++; } catch { invalid++; continue; }
const api = evt.api || evt.name || evt.path || '';
inc(apiCounts, api);
inc(typeCounts, evt.t || evt.type || '');
inc(categoryCounts, classifyApi(api));
const stack = Array.isArray(evt.stack) ? evt.stack : [];
for (const s of stack) if (s && s.file) inc(fileCounts, s.file);
collectTruncationSignals(evt, lines, options.truncationThreshold, options.maxTruncationExamples, truncationState);
if (examples.length < options.maxExamples) examples.push(sanitizeLongStrings(evt, options.truncationThreshold));
}
return {
lines,
parsed,
invalid,
topApis: top(apiCounts, 30),
topTypes: top(typeCounts, 20),
topCategories: top(categoryCounts, 20),
topStackFiles: top(fileCounts, 30),
truncation: {
threshold: options.truncationThreshold,
totalSuspectedFields: truncationState.totalSuspectedFields,
maxVisibleLength: truncationState.maxVisibleLength,
topFieldPaths: top(truncationState.byFieldPath, 20),
topApis: top(truncationState.byApi, 20),
examples: truncationState.examples,
rule: '可见长度达到阈值的字符串一律按疑似截断处理;Canvas / WebGL / WebGPU / Audio 等长指纹值不得使用日志可见片段作为最终值,真实长度为 unknown,只能确认至少达到可见长度。',
},
examples,
};
}
function renderMarkdown(result) {
const lines = ['# RuyiTrace 日志导入摘要', '', `- 原始日志:${result.input}`, `- 复制后日志:${result.copiedTo}`, `- 行数:${result.summary.lines}`, `- 成功解析:${result.summary.parsed}`, `- 解析失败:${result.summary.invalid}`];
lines.push('', '## API 类别统计');
for (const item of result.summary.topCategories) lines.push(`- ${item.key}:${item.count}`);
lines.push('', '## 高频 API');
for (const item of result.summary.topApis.slice(0, 20)) lines.push(`- ${item.key}:${item.count}`);
lines.push('', '## 高频调用栈文件');
if (!result.summary.topStackFiles.length) lines.push('- 未发现 stack.file');
for (const item of result.summary.topStackFiles.slice(0, 20)) lines.push(`- ${item.key}:${item.count}`);
const truncation = result.summary.truncation;
lines.push('', '## 长字段截断风险');
lines.push(`- 检测阈值:${truncation.threshold}`);
lines.push(`- 疑似截断字段数:${truncation.totalSuspectedFields}`);
lines.push(`- 最大可见长度:${truncation.maxVisibleLength}`);
lines.push('- 规则:达到或接近阈值的字符串只能说明“至少达到该可见长度”,真实长度记为 unknown,不能把 4000 或可见长度当成真实长度。');
if (!truncation.examples.length) {
lines.push('- 未发现达到阈值的长字符串字段。');
} else {
lines.push('', '| 行号 | API | 字段路径 | 可见长度 | 真实长度判断 | 调用栈 | 可见值 SHA256 |');
lines.push('|---|---|---|---|---|---|---|');
for (const item of truncation.examples.slice(0, 20)) {
lines.push(`| ${item.line} | ${item.api} | ${item.fieldPath} | ${item.visibleLength} | unknown,疑似被 RuyiTrace 截断 | ${item.stack || '未记录'} | ${item.visibleSha256} |`);
}
lines.push('', '### 长字段补采要求');
lines.push('- 不要根据 RuyiTrace 中的 4000 / 4096 字符可见值判断完整加密参数、长 token、长 Cookie、长 body、Canvas dataURL、WebGL readPixels、WebGPU adapter 信息或 Audio channel data 的真实长度。');
lines.push('- 如果该字段影响签名、补环境验证或指纹回放,必须通过 HAR/cURL、ruyiPage / Camoufox / CloakBrowser / 手动浏览器采样、专用 Hook 分片落盘、或最终 Node.js signer 输出重新确认完整值。');
lines.push('- 写入 `notes/missing-env-priority.md` 时标明 `actualLength: unknown`、`minLength: 可见长度`、`truncationSuspected: true`。');
}
lines.push('', '## 建议下一步');
lines.push('- 将高频 API 映射到 `env-module-levels.md` 的 Level 1/2/3 环境模块。');
lines.push('- 结合 stack.file / line / col 更新 `notes/entry-chain.md` 和 `notes/missing-env-priority.md`。');
lines.push('- 对长字段优先补采完整值或记录 hash / 长度 / 前后片段,避免把 RuyiTrace 的截断值误当作完整值。');
lines.push('- 仅把摘要写入最终报告,原始 NDJSON 作为本地证据文件保存或由用户确认删除。');
return lines.join('\n') + '\n';
}
async function main() {
const args = parseArgs(process.argv);
if (args.help) { console.log(usage()); return; }
if (!args.input) throw new Error('必须提供 --input');
if (!args.caseDir) throw new Error('必须提供 --case-dir');
const input = path.resolve(args.input);
const caseDir = path.resolve(args.caseDir);
if (!exists(input)) throw new Error(`日志文件不存在:${input}`);
if (!exists(caseDir)) fs.mkdirSync(caseDir, { recursive: true });
const logDir = path.join(caseDir, 'ruyi-trace', 'logs');
const notesDir = path.join(caseDir, 'notes');
fs.mkdirSync(logDir, { recursive: true });
fs.mkdirSync(notesDir, { recursive: true });
const dstName = safeName(args.name || path.basename(input) || `trace-${Date.now()}.ndjson`);
const copiedTo = path.join(logDir, dstName.endsWith('.ndjson') ? dstName : `${dstName}.ndjson`);
fs.copyFileSync(input, copiedTo);
const summary = await summarizeNdjson(copiedTo, args);
const result = { input, copiedTo, summary };
const md = renderMarkdown(result);
fs.writeFileSync(path.join(notesDir, 'ruyitrace-summary.md'), md, 'utf8');
if (args.json) console.log(JSON.stringify(result, null, 2));
if (args.markdown) process.stdout.write(md);
}
main().catch(err => {
console.error(err.message || String(err));
console.error(usage());
process.exit(1);
});