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,284 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const { spawn, spawnSync } = require('child_process');
function parseArgs(argv) {
const args = {
url: '',
caseDir: 'case',
outDir: '',
profileDir: '',
ruyitraceHome: '',
ruyitraceExe: '',
duration: 60,
limit: 200000,
dryRun: false,
importAfter: false,
json: false,
markdown: false,
};
for (let i = 2; i < argv.length; i += 1) {
const a = argv[i];
if (a === '--url') args.url = argv[++i] || '';
else if (a === '--case-dir' || a === '--dir') args.caseDir = argv[++i] || '';
else if (a === '--out-dir') args.outDir = argv[++i] || '';
else if (a === '--profile-dir') args.profileDir = argv[++i] || '';
else if (a === '--ruyitrace-home') args.ruyitraceHome = argv[++i] || '';
else if (a === '--ruyitrace-exe') args.ruyitraceExe = argv[++i] || '';
else if (a === '--duration') args.duration = Number(argv[++i] || '60');
else if (a === '--limit') args.limit = Number(argv[++i] || '200000');
else if (a === '--dry-run') args.dryRun = true;
else if (a === '--import-after') args.importAfter = true;
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 (!args.json && !args.markdown) args.markdown = true;
if (!Number.isFinite(args.duration) || args.duration <= 0) args.duration = 60;
if (!Number.isFinite(args.limit) || args.limit <= 0) args.limit = 200000;
return args;
}
function usage() {
return `用法:
node scripts/capture_ruyitrace_log.js --url <target-page-url> --case-dir case --ruyitrace-home <RuyiTrace-dir> --markdown
node scripts/capture_ruyitrace_log.js --url <target-page-url> --case-dir case --duration 90 --import-after --markdown
node scripts/capture_ruyitrace_log.js --url <target-page-url> --case-dir case --dry-run --json
说明:确认 RuyiTrace 已安装后,优先使用随 RuyiTrace 提供的 trace Firefox 和 MOZ_DOM_TRACE 环境变量自动捕获 NDJSON。自动捕获失败、需要登录/验证码/权限交互、或目标路径未覆盖时,才要求用户手动协助。`;
}
function exists(p) {
try { return !!p && fs.existsSync(p); } catch { return false; }
}
function isDir(p) {
try { return !!p && fs.statSync(p).isDirectory(); } catch { return false; }
}
function ensureDir(p) {
fs.mkdirSync(p, { recursive: true });
}
function run(cmd, args, timeout = 8000) {
const ret = spawnSync(cmd, args, { encoding: 'utf8', timeout, windowsHide: true });
return { ok: ret.status === 0, stdout: ret.stdout || '', stderr: ret.stderr || '' };
}
function whereCommand(name) {
const cmd = process.platform === 'win32' ? 'where' : 'which';
const ret = run(cmd, [name]);
return ret.ok ? ret.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean) : [];
}
function normalizeTraceHome(args) {
if (args.ruyitraceHome) return path.resolve(args.ruyitraceHome);
if (args.ruyitraceExe) return path.dirname(path.resolve(args.ruyitraceExe));
if (process.env.RUYI_TRACE_HOME) return path.resolve(process.env.RUYI_TRACE_HOME);
if (process.env.RUYITRACE_HOME) return path.resolve(process.env.RUYITRACE_HOME);
const found = whereCommand(process.platform === 'win32' ? 'RuyiTrace.exe' : 'RuyiTrace');
return found.length ? path.dirname(found[0]) : '';
}
function detectRuyiTrace(args) {
const home = normalizeTraceHome(args);
const exeName = process.platform === 'win32' ? 'RuyiTrace.exe' : 'RuyiTrace';
const exe = args.ruyitraceExe ? path.resolve(args.ruyitraceExe) : (home ? path.join(home, exeName) : '');
const firefoxExe = home ? path.join(home, 'firefox', process.platform === 'win32' ? 'firefox.exe' : 'firefox') : '';
const marker = home ? path.join(home, 'firefox', 'RUYI_DOMTRACE.txt') : '';
const installed = exists(exe) && exists(firefoxExe) && exists(marker);
return {
installed,
home,
exe,
exeExists: exists(exe),
firefoxExe,
firefoxExists: exists(firefoxExe),
marker,
markerExists: exists(marker),
reason: installed ? '' : 'RuyiTrace 不完整:需要 RuyiTrace 可执行文件、firefox 可执行文件以及 firefox/RUYI_DOMTRACE.txt',
};
}
function timestamp() {
return new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
}
function buildPlan(args, trace) {
const caseDir = path.resolve(args.caseDir || 'case');
const outDir = path.resolve(args.outDir || path.join(caseDir, 'ruyi-trace', 'logs'));
const profileDir = path.resolve(args.profileDir || path.join(caseDir, 'tmp', 'ruyitrace-profile'));
const traceFile = path.join(outDir, `trace-${timestamp()}.ndjson`);
const firefoxArgs = ['-no-remote', '-new-instance', '-profile', profileDir];
if (args.url) firefoxArgs.push(args.url);
return {
caseDir,
outDir,
profileDir,
traceFile,
firefoxExe: trace.firefoxExe,
firefoxArgs,
env: {
MOZ_DOM_TRACE: '1',
MOZ_DOM_TRACE_FILE: traceFile,
MOZ_DOM_TRACE_LIMIT: String(args.limit),
MOZ_DISABLE_LAUNCHER_PROCESS: '1',
},
};
}
function listNdjsonFiles(dir, sinceMs) {
if (!isDir(dir)) return [];
return fs.readdirSync(dir)
.filter((name) => name.toLowerCase().endsWith('.ndjson'))
.map((name) => path.join(dir, name))
.filter((file) => {
try { return fs.statSync(file).mtimeMs >= sinceMs - 1000; } catch { return false; }
})
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
}
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function waitForExit(child, timeoutMs) {
return new Promise((resolve) => {
let done = false;
const finish = (value) => {
if (done) return;
done = true;
resolve(value);
};
child.once('exit', (code, signal) => finish({ exited: true, code, signal }));
setTimeout(() => finish({ exited: false, code: null, signal: null }), timeoutMs).unref();
});
}
function importLog(caseDir, file, markdown) {
const script = path.join(__dirname, 'import_ruyitrace_log.js');
const args = [script, '--input', file, '--case-dir', caseDir, '--truncation-threshold', '3900', markdown ? '--markdown' : '--json'];
const ret = spawnSync(process.execPath, args, { encoding: 'utf8', windowsHide: true });
return { ok: ret.status === 0, status: ret.status, stdout: ret.stdout || '', stderr: ret.stderr || '' };
}
async function capture(args, plan) {
ensureDir(plan.outDir);
ensureDir(plan.profileDir);
const startedAt = Date.now();
const child = spawn(plan.firefoxExe, plan.firefoxArgs, {
env: { ...process.env, ...plan.env },
stdio: 'ignore',
windowsHide: false,
});
const result = {
launched: true,
pid: child.pid,
waitedSeconds: args.duration,
killAttempted: false,
exit: null,
logs: [],
importResult: null,
};
child.on('error', (err) => { result.launchError = err.message || String(err); });
await wait(args.duration * 1000);
const exitBeforeKill = await waitForExit(child, 200);
if (!exitBeforeKill.exited) {
result.killAttempted = true;
try { child.kill(); } catch (err) { result.killError = err.message || String(err); }
result.exit = await waitForExit(child, 3000);
} else {
result.exit = exitBeforeKill;
}
result.logs = listNdjsonFiles(plan.outDir, startedAt);
if (args.importAfter && result.logs.length) {
result.importResult = importLog(plan.caseDir, result.logs[0], args.markdown);
}
return result;
}
function renderMarkdown(obj) {
const { args, trace, plan, result } = obj;
const lines = ['# RuyiTrace 自动捕获日志', ''];
lines.push(`- RuyiTrace 检测结果:${trace.installed ? '通过' : '不通过'}`);
if (trace.home) lines.push(`- RuyiTrace 目录:${trace.home}`);
if (trace.exe) lines.push(`- RuyiTrace 可执行文件:${trace.exeExists ? '存在' : '不存在'} - ${trace.exe}`);
if (trace.firefoxExe) lines.push(`- trace Firefox:${trace.firefoxExists ? '存在' : '不存在'} - ${trace.firefoxExe}`);
if (trace.marker) lines.push(`- trace 标志文件:${trace.markerExists ? '存在' : '不存在'} - ${trace.marker}`);
if (trace.reason) lines.push(`- 原因:${trace.reason}`);
lines.push('', '## 自动捕获计划');
lines.push(`- 目标页面:${args.url || '未提供'}`);
lines.push(`- 输出目录:${plan.outDir}`);
lines.push(`- Profile 目录:${plan.profileDir}`);
lines.push(`- 计划 trace 文件:${plan.traceFile}`);
lines.push(`- 采集时长:${args.duration} 秒`);
lines.push(`- DOM trace 行数上限:${args.limit}`);
lines.push(`- 启动参数:${[plan.firefoxExe].concat(plan.firefoxArgs).join(' ')}`);
lines.push('- 环境变量:MOZ_DOM_TRACE=1,MOZ_DOM_TRACE_FILE=<case trace file>,MOZ_DOM_TRACE_LIMIT=<limit>,MOZ_DISABLE_LAUNCHER_PROCESS=1');
if (args.dryRun) {
lines.push('', '## Dry-run 结果');
lines.push('- 未启动浏览器,未创建日志文件。');
if (trace.installed) {
lines.push('- RuyiTrace 检测通过且用户授权后,应优先执行本脚本自动捕获 NDJSON,而不是默认等待用户手动 trace。');
} else {
lines.push('- RuyiTrace 检测未通过,不能进入自动捕获;应先让用户安装 / 提供 RuyiTrace 路径,或明确确认降级为仅 ruyiPage。');
}
return lines.join('\n') + '\n';
}
lines.push('', '## 捕获结果');
if (result.launchError) lines.push(`- 启动错误:${result.launchError}`);
lines.push(`- 是否已启动:${result.launched ? '是' : '否'}`);
if (result.pid) lines.push(`- 进程 PID:${result.pid}`);
lines.push(`- 是否尝试结束进程:${result.killAttempted ? '是' : '否'}`);
lines.push(`- 发现 NDJSON 数量:${result.logs.length}`);
for (const file of result.logs) lines.push(` - ${file}`);
if (!result.logs.length) {
lines.push('- 未发现 NDJSON:应检查 RuyiTrace trace Firefox 是否能写入日志、目标页面是否触发了环境访问、是否需要登录/验证码/权限交互;自动捕获失败后才要求用户手动协助采集。');
}
if (result.importResult) {
lines.push('', '## 导入结果');
lines.push(`- 导入是否成功:${result.importResult.ok ? '是' : '否'}`);
if (result.importResult.stdout.trim()) lines.push('', '```text', result.importResult.stdout.trim(), '```');
if (result.importResult.stderr.trim()) lines.push('', '```text', result.importResult.stderr.trim(), '```');
}
return lines.join('\n') + '\n';
}
async function main() {
const args = parseArgs(process.argv);
if (args.help) {
console.log(usage());
return;
}
if (!args.url) throw new Error('缺少 --url;自动捕获需要目标页面 URL。');
const trace = detectRuyiTrace(args);
const plan = buildPlan(args, trace);
if (!trace.installed) {
const obj = { args, trace, plan, result: { launched: false, logs: [] } };
if (args.json) process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
if (args.markdown) process.stdout.write(renderMarkdown(obj));
process.exitCode = 2;
return;
}
if (args.dryRun) {
const obj = { args, trace, plan, result: { launched: false, logs: [] } };
if (args.json) process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
if (args.markdown) process.stdout.write(renderMarkdown(obj));
return;
}
const result = await capture(args, plan);
const obj = { args, trace, plan, result };
if (args.json) process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
if (args.markdown) process.stdout.write(renderMarkdown(obj));
if (!result.logs.length) process.exitCode = 3;
}
main().catch((err) => {
console.error(err.message || String(err));
console.error(usage());
process.exit(1);
});