reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,277 @@
#!/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);
});