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,632 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
function parseArgs(argv) {
const args = {
caseDir: null,
file: null,
requireFinalSummary: true,
finalSummaryOptOut: false,
json: false,
markdown: false,
};
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--case-dir' || a === '--dir' || a === '-d') args.caseDir = argv[++i];
else if (a === '--file' || a === '-f') args.file = argv[++i];
else if (a === '--no-require-final-summary' || a === '--allow-no-final-summary') {
args.requireFinalSummary = false;
args.finalSummaryOptOut = 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;
return args;
}
function usage() {
return `用法:
node scripts/check_final_artifact.js --case-dir case --markdown
node scripts/check_final_artifact.js --case-dir case --no-require-final-summary --markdown
node scripts/check_final_artifact.js --case-dir case --file case/result/final.js --json
说明:检查最终交付项目是否是规范目录、只有一个执行入口、无浏览器自动化代码,并确认入口链路最终由已确认的 TLS 指纹兼容 Node.js / Python Session 请求客户端发起少量模拟请求,或明确不发真实请求;同时检查是否硬编码 / 复用了 cURL 或 fixture 中的样本加密参数值。最终总结 result/最终项目总结.md 是默认硬性要求,且必须包含 Trace-runtime 可执行闭环、XHR/fetch Session Bridge、XHR/fetch 请求语义审计、对象形状审计矩阵、WebAPI 环境检测矩阵和高强度环境检测覆盖矩阵;只有用户明确要求不生成最终总结时,才允许传入 --no-require-final-summary 并在阶段输出中记录原因。`;
}
function exists(p) {
try { fs.accessSync(p); return true; } catch { return false; }
}
function stat(p) {
try { return fs.statSync(p); } catch { return null; }
}
function readText(p) {
return fs.readFileSync(p, 'utf8').replace(/^\uFEFF/, '');
}
function walk(p, out = []) {
if (!exists(p)) return out;
const st = stat(p);
if (!st) return out;
if (st.isDirectory()) {
let names = [];
try { names = fs.readdirSync(p); } catch { names = []; }
for (const name of names) walk(path.join(p, name), out);
}
out.push(p);
return out;
}
function rel(root, p) {
return (path.relative(root, p) || '.').replace(/\\/g, '/');
}
function ext(p) {
return path.extname(p).toLowerCase();
}
const AUTOMATION_PATTERNS = [
/\bruyipage\b/i,
/\bFirefoxPage\b/,
/\bRuyiTrace\b/i,
/\bplaywright\b/i,
/\bpuppeteer\b/i,
/\bpyppeteer\b/i,
/\bselenium\b/i,
/\bwebdriver\b/i,
/\bCloakBrowser\b/i,
/\bcloakbrowser\b/i,
/\bcamoufox\b/i,
/\bcamoufox_reverse_mcp\b/i,
/\bCamoufox\b/,
/\bAsyncCamoufox\b/,
/\bbrowser\.new_page\b/i,
/\bbrowser\.newPage\b/i,
/\blaunch_browser\s*\(/i,
/\bnetwork_capture\s*\(/i,
/\bbrowser-use\b/i,
/\bchromium\.launch\b/i,
/\bfirefox\.launch\b/i,
/\bbrowser\.launch\b/i,
/\bpage\.goto\s*\(/i,
/\bpage\.capture\b/i,
/\bCDP\b/,
/\bMarionette\b/i,
];
const JS_REQUEST_PATTERNS = [
/\bcycleTLS\s*\(/i,
/\bCycleTLS\b/,
/\bcycletls\b/i,
/\bimpers\b/i,
/\bimpersFetch\b/i,
/\bcurl-cffi\b/i,
/\bcurl-cffi-node\b/i,
/require\s*\(\s*['"]curl-cffi['"]\s*\)/,
/from\s+['"]curl-cffi['"]/,
/\bCurlSession\b/,
/\bCurlRequest\b/,
/\breq\.request\s*\(/,
];
const REQUEST_SESSION_PATTERNS = [
/\bcreateRequestSession\s*\(/i,
/\bcreate_request_session\s*\(/i,
/\bSession\s*\(/,
/\brequests\.Session\s*\(/,
/\bCurlSession\b/,
/\bCookieJar\b/i,
/\btough-cookie\b/i,
/\bcookieJar\b/,
/\bsession\.request\s*\(/i,
/\bsession\.close\s*\(/i,
/\bjar\.setCookie/i,
/Set-Cookie/i,
/销毁\s*session/i,
/清理\s*Cookie\s*jar/i,
];
const PY_REQUEST_PATTERNS = [
/\bcurl_cffi\b/,
/\bcffi_curl\b/,
/\bcyCronet\b/i,
/\bcycronet\b/i,
];
const NO_REAL_REQUEST_PATTERNS = [
/不发真实请求/,
/不发送真实请求/,
/只输出本地\s*(sign|参数)/i,
/仅输出本地\s*(sign|参数)/i,
/\bnoRealRequest\b/i,
/\bno-real-request\b/i,
/\bdryRunOnly\b/i,
/\blocalSignOnly\b/i,
];
const FINGERPRINT_RENDER_PATTERNS = [
/\b(require|import)\s*\(?\s*['"](?:canvas|node-canvas|gl|headless-gl)['"]|from\s+['"](?:canvas|node-canvas|gl|headless-gl)['"]/i,
/__WEB_JS_ENV_PATCHER_FINGERPRINT__/,
/generate_fingerprint_hook/i,
];
function findMatches(text, patterns) {
const hits = [];
for (const pattern of patterns) {
const m = text.match(pattern);
if (m) hits.push(pattern.toString());
}
return hits;
}
function isTextLikeFile(p) {
return ['.js', '.mjs', '.cjs', '.py', '.json', '.md', '.txt', '.yaml', '.yml', '.curl', '.http', '.har'].includes(ext(p));
}
function isCodeLikeFile(p) {
return ['.js', '.mjs', '.cjs', '.py', '.json'].includes(ext(p));
}
function isTempOrTestFile(p) {
const n = path.basename(p).toLowerCase();
const normalized = String(p).replace(/\\/g, '/').toLowerCase();
if (/(^|\/)(tmp|\.tmp|temp|\.temp|cache|\.cache|browser-profile|firefox-profile|chrome-profile|profile|screenshots|hooks|ruyi-trace|logs|trace)(\/|$)/.test(normalized)) return true;
if (/\.(tmp|temp|log|jsonl|har|png|jpg|jpeg|webp|trace|cache|bak|old|orig|retry|partial|download|crdownload)$/i.test(n)) return true;
if (/^(test-|tmp-|temp-|debug-|scratch-)/i.test(n)) return true;
if (/\b(test|spec|debug|scratch|trace|hook|fixture|mock)\b/i.test(n)) return true;
return false;
}
function isRootSecondEntry(resultDir, p, primary) {
if (path.dirname(p) !== resultDir) return false;
if (path.resolve(p) === path.resolve(primary)) return false;
const n = path.basename(p).toLowerCase();
if (n === 'package.json' || n === 'requirements.txt' || n === 'config.example.json') return false;
return ['.js', '.mjs', '.cjs', '.py'].includes(ext(p));
}
function inspectPackageJson(resultDir, problems, warnings) {
const pkg = path.join(resultDir, 'package.json');
if (!exists(pkg)) return;
let data;
try {
data = JSON.parse(readText(pkg));
} catch (err) {
problems.push(`package.json 解析失败:${err.message}`);
return;
}
const scripts = data.scripts || {};
for (const [name, cmd] of Object.entries(scripts)) {
const lowerName = name.toLowerCase();
const lowerCmd = String(cmd).toLowerCase();
if (lowerName === 'start') {
if (!/\bnode\s+(\.\/)?final\.js\b/.test(lowerCmd)) warnings.push('package.json 的 start 脚本建议只指向 node final.js');
} else if (/(test|debug|dev|server|serve|watch|browser|playwright|puppeteer)/i.test(lowerName + ' ' + lowerCmd)) {
problems.push(`package.json 存在非交付用途脚本:${name}=${cmd}`);
}
}
}
const CRYPTO_PARAM_NAME_RE = /^(?:sign|signature|_signature|x[-_]?sign|x[-_]?s|x[-_]?t|a[_-]?bogus|h5st|mtgsig|w[_-]?rid|x[-_]?bogus|x[-_]?(?:ladon|argus|gorgon|khronos|ss[-_]?stub|mini[-_]?wua|umt)|token|access[-_]?token|auth[-_]?token|x[-_]?token|csrf[-_]?token|csrftoken|verify|digest|hash|hmac|mac|nonce|sig)$/i;
function isPlaceholderValue(value) {
const s = String(value || '');
return !s || s.length < 8 || /^(?:xxx+|test+|example|placeholder|todo|replace|redacted|your[_-]?)/i.test(s) || /^[*]+$/.test(s);
}
function isLikelyConcatenationFragment(value) {
const s = String(value || '');
const t = s.trim();
return /(?:^|\s)\+\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*(?:\s*\+\s*)?$/.test(s)
|| /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\s*\+(?:\s|$)/.test(t)
|| (/^\+/.test(t) || /\+$/.test(t)) && /\s/.test(s);
}
function maskCryptoValue(value) {
const s = String(value || '');
if (s.length <= 8) return '*'.repeat(s.length);
return `${s.slice(0, 4)}...${s.slice(-4)}(len=${s.length})`;
}
function addSampleCryptoValue(out, name, value, sourceFile, sourcePath) {
if (!CRYPTO_PARAM_NAME_RE.test(String(name || ''))) return;
if (isPlaceholderValue(value)) return;
const key = `${String(name).toLowerCase()}:${String(value)}`;
if (out.has(key)) return;
out.set(key, { name: String(name), value: String(value), masked: maskCryptoValue(value), sourceFile, sourcePath });
}
function decodeURIComponentSafe(s) { try { return decodeURIComponent(s); } catch { return s; } }
function extractJsonCryptoValues(obj, out, sourceFile, prefix = 'json') {
if (!obj || typeof obj !== 'object') return;
for (const [k, v] of Object.entries(obj)) {
const p = `${prefix}.${k}`;
if (v && typeof v === 'object') extractJsonCryptoValues(v, out, sourceFile, p);
else addSampleCryptoValue(out, k, v, sourceFile, p);
}
}
function extractSampleCryptoValuesFromText(text, sourceFile) {
const out = new Map();
const urlRe = /https?:\/\/[^\s'"<>]+/g;
let m;
while ((m = urlRe.exec(text))) {
try {
const u = new URL(m[0]);
for (const [k, v] of u.searchParams.entries()) addSampleCryptoValue(out, k, v, sourceFile, `query.${k}`);
} catch (_) {}
}
try { extractJsonCryptoValues(JSON.parse(text), out, sourceFile, 'json'); } catch (_) {}
const pairRe = /["']?([A-Za-z_$][\w$-]{0,80})["']?\s*(?::|=)\s*["']([^"'\r\n&;,}\s]{8,})["']/g;
while ((m = pairRe.exec(text))) addSampleCryptoValue(out, m[1], m[2], sourceFile, `pair.${m[1]}`);
const formRe = /(?:^|[?&\s])([A-Za-z_$][\w$-]{0,80})=([^&\s'"<>]{8,})/g;
while ((m = formRe.exec(text))) addSampleCryptoValue(out, m[1], decodeURIComponentSafe(m[2]), sourceFile, `form.${m[1]}`);
const headerRe = /(?:^|\s)(?:-H|--header)\s+['"]([^:'"]{1,100})\s*:\s*([^'"\r\n]+)['"]/g;
while ((m = headerRe.exec(text))) {
const headerName = m[1].trim();
const headerValue = m[2].trim();
addSampleCryptoValue(out, headerName, headerValue, sourceFile, `header.${headerName}`);
if (headerName.toLowerCase() === 'cookie') {
for (const part of headerValue.split(';')) {
const idx = part.indexOf('=');
if (idx !== -1) {
const cookieName = part.slice(0, idx).trim();
const cookieValue = part.slice(idx + 1).trim();
addSampleCryptoValue(out, cookieName, cookieValue, sourceFile, `cookie.${cookieName}`);
}
}
}
}
return [...out.values()];
}
function collectSampleCryptoValues(caseDir) {
const roots = ['requests', 'fixtures'].map(d => path.join(caseDir, d)).filter(exists);
const values = [];
for (const root of roots) {
for (const f of walk(root).filter(p => stat(p) && stat(p).isFile() && isTextLikeFile(p))) {
for (const item of extractSampleCryptoValuesFromText(readText(f), rel(caseDir, f))) values.push(item);
}
}
return values;
}
function inspectReusedSampleCryptoValues(caseDir, resultFiles, textFiles) {
const sampleValues = collectSampleCryptoValues(caseDir);
const reused = [];
for (const sample of sampleValues) {
for (const f of textFiles) {
const text = readText(f);
if (text.includes(sample.value)) reused.push({ file: rel(caseDir, f), name: sample.name, value: sample.masked, source: sample.sourceFile, sourcePath: sample.sourcePath });
}
}
const hardcoded = [];
const hardcodedRe = /["']?([A-Za-z_$][\w$-]{0,80})["']?\s*(?::|=)\s*["']([^"'\r\n]{8,})["']/g;
for (const f of resultFiles.filter(isCodeLikeFile)) {
const text = readText(f);
let m;
while ((m = hardcodedRe.exec(text))) {
const name = m[1];
const value = m[2];
if (CRYPTO_PARAM_NAME_RE.test(name) && !isPlaceholderValue(value) && !isLikelyConcatenationFragment(value)) {
hardcoded.push({ file: rel(caseDir, f), name, value: maskCryptoValue(value) });
}
}
}
const generationEvidence = [];
const generationRe = /install.*env|补环境|signer|makeSign|generate(?:Sign|Token|Params?)|compute(?:Sign|Token|Params?)|build(?:Sign|Token|Params?)|runTarget|target[\\/]entry|src[\\/]signer|vm\.Script|load.*(?:bundle|target|js)|createContext/i;
for (const f of resultFiles.filter(isCodeLikeFile)) {
const text = readText(f);
if (generationRe.test(text)) generationEvidence.push(rel(caseDir, f));
}
return {
sampleValues: sampleValues.map(x => ({ name: x.name, value: x.masked, source: x.sourceFile, sourcePath: x.sourcePath })),
reused,
hardcoded,
generationEvidence,
};
}
function inspectStageReports(caseDir) {
const stageDir = path.join(caseDir, '阶段报告');
const result = { dir: stageDir, present: exists(stageDir), files: [], initialPresent: false, chineseFileNames: true, mojibakeSuspected: false };
const problems = [];
const warnings = [];
if (!result.present) {
problems.push('缺少阶段报告目录 case/阶段报告;至少应在前置阶段生成中文命名阶段报告 01-需求信息确认.md。');
return { result, problems, warnings };
}
let names = [];
try { names = fs.readdirSync(stageDir); } catch { names = []; }
for (const name of names) {
const file = path.join(stageDir, name);
const st = stat(file);
if (!st || !st.isFile() || ext(file) !== '.md') continue;
const item = { file, chineseFileName: /[\u4e00-\u9fff]/.test(name), utf8Readable: false, mojibakeSuspected: false };
if (!item.chineseFileName) result.chineseFileNames = false;
try {
const text = readText(file);
item.utf8Readable = true;
const questionRuns = text.match(/\?{3,}/g) || [];
item.mojibakeSuspected = text.includes('\uFFFD') || (questionRuns.reduce((n, x) => n + x.length, 0) >= 8 && !/[\u4e00-\u9fff]/.test(text));
if (item.mojibakeSuspected) result.mojibakeSuspected = true;
} catch (_) {}
result.files.push(item);
}
result.initialPresent = result.files.some(x => path.basename(x.file) === '01-需求信息确认.md');
if (!result.files.length) problems.push('case/阶段报告 中没有 Markdown 阶段报告。');
if (!result.initialPresent) problems.push('缺少前置阶段报告:case/阶段报告/01-需求信息确认.md。');
if (!result.chineseFileNames) problems.push('阶段报告文件名必须包含中文,不能只使用英文文件名。');
if (result.mojibakeSuspected) problems.push('阶段报告疑似存在中文乱码或连续问号。');
return { result, problems, warnings };
}
const FINAL_SUMMARY_REQUIRED_SECTIONS = [
/阶段报告索引/,
/native addon\s*\/\s*NativeProtect 使用情况/i,
/环境与指纹\s*API\s*调用回放明细/i,
/Trace-runtime 可执行闭环/i,
/XHR\/fetch Session Bridge/i,
/XHR\/fetch 请求语义审计/i,
/对象形状审计矩阵与私有状态泄露/,
/WebAPI 环境检测矩阵/,
/高强度环境检测覆盖矩阵/,
/加密参数生成与样本复用检查/,
/代码质量与中文注释/,
/最终交付结构/,
/指纹基线一致性/,
/Session\s*请求链|Session 模式|TLS 请求验证与 Session 请求链/i,
/测试结果/,
/清理结果/,
];
function inspectFinalSummary(resultDir, requireFinalSummary) {
const finalSummary = path.join(resultDir, '最终项目总结.md');
const legacyFinalSummary = path.join(resultDir, 'final-summary.md');
const result = {
required: !!requireFinalSummary,
file: exists(finalSummary) ? finalSummary : '',
legacyFile: exists(legacyFinalSummary) ? legacyFinalSummary : '',
present: exists(finalSummary),
utf8Readable: false,
mojibakeSuspected: false,
missingSections: [],
};
const problems = [];
const warnings = [];
if (!requireFinalSummary) return { result, problems, warnings };
if (!result.present) {
if (exists(legacyFinalSummary)) problems.push('最终总结必须使用中文文件名 result/最终项目总结.md;当前只检测到旧文件名 result/final-summary.md,请改名或重新写入。');
else problems.push('项目完成后必须默认生成最终总结 result/最终项目总结.md;只有用户明确要求不生成时才可跳过,并需运行检查脚本时传入 --no-require-final-summary。');
return { result, problems, warnings };
}
let text = '';
try {
text = readText(finalSummary);
result.utf8Readable = true;
} catch (err) {
problems.push(`最终总结无法按 UTF-8 读取:${err.message || String(err)}`);
return { result, problems, warnings };
}
if (/\uFFFD/.test(text) || /\?{6,}/.test(text) || (!/[\u4e00-\u9fff]/.test(text) && /[?]{3,}/.test(text))) {
result.mojibakeSuspected = true;
problems.push('最终总结疑似存在中文编码乱码或连续问号,请使用 write_markdown_utf8.js 重新生成 UTF-8 Markdown。');
}
for (const pattern of FINAL_SUMMARY_REQUIRED_SECTIONS) {
if (!pattern.test(text)) result.missingSections.push(pattern.toString());
}
if (result.missingSections.length) {
problems.push(`最终总结缺少必要章节:${result.missingSections.join('、')}。项目完成后的总结必须包含 native addon / NativeProtect 使用情况、指纹基线一致性、环境与指纹 API 调用回放明细、Trace-runtime 可执行闭环、XHR/fetch Session Bridge、XHR/fetch 请求语义审计、对象形状审计矩阵与私有状态泄露、WebAPI 环境检测矩阵、高强度环境检测覆盖矩阵、最终请求 Session 请求链、加密参数生成与样本复用检查、代码质量与中文注释、最终交付结构、测试结果和清理结果。`);
}
if (!/^#\s+/.test(text.trim())) warnings.push('最终总结建议以一级标题开头。');
return { result, problems, warnings };
}
function check(args) {
if (!args.caseDir && !args.file) throw new Error('必须提供 --case-dir 或 --file');
const caseDir = args.caseDir ? path.resolve(args.caseDir) : path.resolve(path.dirname(args.file), '..');
const resultDir = path.join(caseDir, 'result');
const problems = [];
const warnings = [];
if (!exists(caseDir)) problems.push(`case 目录不存在:${caseDir}`);
if (!exists(resultDir)) problems.push(`结果目录不存在:${resultDir}`);
const candidate = args.file ? path.resolve(args.file) : null;
let primary = candidate;
if (!primary && exists(resultDir)) {
const candidates = ['final.js', 'final.py'].map(name => path.join(resultDir, name)).filter(exists);
if (candidates.length === 1) primary = candidates[0];
else if (candidates.length === 0) problems.push('未找到唯一执行入口:result/final.js 或 result/final.py');
else problems.push('同时存在 final.js 和 final.py;最终项目只能有一个执行入口');
}
const resultFiles = exists(resultDir) ? walk(resultDir).filter(p => stat(p) && stat(p).isFile()) : [];
const textFiles = resultFiles.filter(isTextLikeFile);
const codeFiles = resultFiles.filter(isCodeLikeFile);
if (primary && !exists(primary)) problems.push(`指定执行入口不存在:${primary}`);
if (primary && exists(primary)) {
const primaryExt = ext(primary);
if (!['.js', '.py'].includes(primaryExt)) problems.push('执行入口必须是 final.js 或 final.py');
if (path.dirname(primary) !== resultDir) warnings.push('执行入口不在 case/result/ 目录中,建议移动到 result/final.js 或 result/final.py');
const text = readText(primary);
if (primaryExt === '.js' && !/require\.main\s*===\s*module|import\.meta\.url|main\s*\(\s*\)\.catch|await\s+main\s*\(/.test(text)) {
warnings.push('未检测到清晰的 Node.js 直接运行入口;建议提供 main() 并在命令行运行时调用');
}
if (primaryExt === '.py' && !/if\s+__name__\s*==\s*['"]__main__['"]/.test(text)) {
warnings.push('未检测到 Python 直接运行入口 if __name__ == "__main__"');
}
const rootSecondEntries = resultFiles.filter(p => isRootSecondEntry(resultDir, p, primary));
if (rootSecondEntries.length) {
problems.push(`结果目录根部存在多个疑似执行入口:${rootSecondEntries.map(p => rel(caseDir, p)).join('、')}`);
}
const requestPatterns = primaryExt === '.py' ? PY_REQUEST_PATTERNS : JS_REQUEST_PATTERNS;
const requestSearchFiles = codeFiles.filter(p => primaryExt === '.py' ? ext(p) === '.py' : ['.js', '.mjs', '.cjs'].includes(ext(p)));
const requestHits = [];
const noRealRequestHits = [];
for (const f of requestSearchFiles) {
const text = readText(f);
const hits = findMatches(text, requestPatterns);
if (hits.length) requestHits.push({ file: rel(caseDir, f), hits });
const noRealHits = findMatches(text, NO_REAL_REQUEST_PATTERNS);
if (noRealHits.length) noRealRequestHits.push({ file: rel(caseDir, f), hits: noRealHits });
}
const sessionHits = [];
for (const f of requestSearchFiles) {
const text = readText(f);
const hits = findMatches(text, REQUEST_SESSION_PATTERNS);
if (hits.length) sessionHits.push({ file: rel(caseDir, f), hits });
}
if (!requestHits.length && !noRealRequestHits.length) {
problems.push('未检测到已确认的 TLS 指纹兼容请求客户端(Node.js CycleTLS / impers / curl-cffi-node,或 Python curl_cffi / cffi_curl / cyCronet);最终验证不能依赖普通 fetch/requests 或浏览器自动化。如用户不发真实请求,入口必须明确只输出本地 sign / 参数。');
}
if (requestHits.length && !noRealRequestHits.length && !sessionHits.length) {
problems.push('最终请求必须使用 Session 模式:即使只有一个请求,也要创建 session client,复用 Cookie jar / Header / UA / Client Hints / TLS 指纹,并在成功或失败后销毁 session。未检测到 createRequestSession / requests.Session / CookieJar / session.close 等证据。');
}
}
const automationHits = [];
for (const f of codeFiles) {
const hits = findMatches(readText(f), AUTOMATION_PATTERNS);
if (hits.length) automationHits.push({ file: rel(caseDir, f), hits });
}
if (automationHits.length) {
problems.push(`最终项目源码疑似包含浏览器自动化 / 取证代码:${automationHits.map(x => `${x.file}(${x.hits.join('、')})`).join(';')}`);
}
const fingerprintRenderHits = [];
for (const f of codeFiles) {
const hits = findMatches(readText(f), FINGERPRINT_RENDER_PATTERNS);
if (hits.length) fingerprintRenderHits.push({ file: rel(caseDir, f), hits });
}
if (fingerprintRenderHits.length) {
problems.push(`最终项目源码疑似包含指纹采样 Hook 或 Node.js 渲染库:${fingerprintRenderHits.map(x => `${x.file}(${x.hits.join('、')})`).join(';')}。指纹应由真实浏览器采样 fixture + 终端 API 值回放实现,采样 Hook 不得进入 result/。`);
}
const reuse = exists(resultDir) ? inspectReusedSampleCryptoValues(caseDir, resultFiles, textFiles) : { sampleValues: [], reused: [], hardcoded: [], generationEvidence: [] };
if (reuse.reused.length) {
problems.push(`最终项目疑似直接复用了请求样本 / fixture 中的加密参数值:${reuse.reused.map(x => `${x.file} 中 ${x.name}=${x.value}(来源 ${x.source}:${x.sourcePath})`).join(';')}。这些值只能作为 expected fixture,必须通过补环境重新生成。`);
}
if (reuse.hardcoded.length) {
problems.push(`最终项目疑似硬编码加密参数字面量:${reuse.hardcoded.map(x => `${x.file} 中 ${x.name}=${x.value}`).join(';')}。应调用补环境后的目标 JS 入口或 signer 模块生成,不能写死 cURL 里的值。`);
}
if (reuse.sampleValues.length && !reuse.generationEvidence.length) {
warnings.push('发现请求样本中存在可疑加密参数,但最终项目未明显体现补环境 / signer / 目标 JS 入口调用痕迹;请人工确认不是直接复用样本参数。');
}
const tempLike = resultFiles.filter(p => isTempOrTestFile(rel(resultDir, p)));
if (tempLike.length) problems.push(`结果目录存在临时 / 测试 / 调试产物:${tempLike.map(p => rel(caseDir, p)).join('、')}`);
inspectPackageJson(resultDir, problems, warnings);
const stageReports = inspectStageReports(caseDir);
problems.push(...stageReports.problems);
warnings.push(...stageReports.warnings);
const finalSummary = exists(resultDir) ? inspectFinalSummary(resultDir, args.requireFinalSummary) : {
result: { required: !!args.requireFinalSummary, file: '', present: false, utf8Readable: false, mojibakeSuspected: false, missingSections: [] },
problems: [],
warnings: [],
};
problems.push(...finalSummary.problems);
warnings.push(...finalSummary.warnings);
return {
caseDir,
resultDir,
entryFile: primary || null,
clean: problems.length === 0,
problems,
warnings,
reusedCryptoCheck: reuse,
finalSummary: finalSummary.result,
stageReports: stageReports.result,
finalSummaryOptOut: args.finalSummaryOptOut,
resultFiles: resultFiles.map(p => rel(caseDir, p)),
};
}
function renderMarkdown(result) {
const lines = [
'# 最终项目检查结果',
'',
`case 目录:${result.caseDir}`,
`结果目录:${result.resultDir}`,
`唯一执行入口:${result.entryFile || '未找到'}`,
`是否通过:${result.clean ? '是' : '否'}`,
'',
'## 检查项',
`- 是否只有一个执行入口:${result.problems.some(p => p.includes('执行入口')) ? '否' : '是'}`,
`- 是否不含浏览器自动化代码:${result.problems.some(p => p.includes('自动化')) ? '否' : '是'}`,
`- 是否检测到 TLS 指纹兼容请求客户端或明确不发真实请求:${result.problems.some(p => p.includes('TLS 指纹兼容请求客户端')) ? '否' : '是'}`,
`- 是否使用 Session 模式并具备销毁逻辑:${result.problems.some(p => p.includes('Session 模式')) ? '否' : '是'}`,
`- 是否不含指纹采样 Hook / Node.js 渲染库:${result.problems.some(p => p.includes('指纹采样 Hook') || p.includes('渲染库')) ? '否' : '是'}`,
`- 是否未复用 cURL / fixture 中的加密参数样本值:${result.reusedCryptoCheck.reused.length || result.reusedCryptoCheck.hardcoded.length ? '否' : '是'}`,
`- 是否已生成中文命名最终总结且包含 API 调用回放明细、Trace-runtime 闭环、XHR/fetch Session Bridge、请求语义审计、对象形状审计矩阵、WebAPI 环境检测矩阵和高强度检测矩阵:${result.finalSummary.required ? (result.finalSummary.present && !result.finalSummary.mojibakeSuspected && !result.finalSummary.missingSections.length ? '是' : '否') : (result.finalSummaryOptOut ? '用户明确豁免' : '未强制检查')}`,
`- result 目录是否无临时 / 测试产物:${result.problems.some(p => p.includes('临时') || p.includes('测试')) ? '否' : '是'}`,
'',
'## 样本加密参数复用检查',
`- 样本中发现的可疑加密参数值数量:${result.reusedCryptoCheck.sampleValues.length}`,
`- 直接复用样本值:${result.reusedCryptoCheck.reused.length ? '是' : '否'}`,
`- 硬编码加密参数字面量:${result.reusedCryptoCheck.hardcoded.length ? '是' : '否'}`,
`- 补环境 / signer 生成痕迹:${result.reusedCryptoCheck.generationEvidence.length ? result.reusedCryptoCheck.generationEvidence.join('、') : '未明显发现'}`,
'',
'## 阶段报告检查',
`- 目录:${result.stageReports.dir}`,
`- 是否存在 01-需求信息确认.md:${result.stageReports.initialPresent ? '是' : '否'}`,
`- 文件名是否均含中文:${result.stageReports.chineseFileNames ? '是' : '否'}`,
`- 疑似乱码:${result.stageReports.mojibakeSuspected ? '是' : '否'}`,
`- 报告数量:${result.stageReports.files.length}`,
'',
'## 最终总结检查',
`- 默认要求:${result.finalSummary.required ? '是' : '否'}`,
`- 文件:${result.finalSummary.file ? rel(result.resultDir, result.finalSummary.file) : '未发现,应为 最终项目总结.md'}`,
`- UTF-8 可读:${result.finalSummary.utf8Readable ? '是' : '否'}`,
`- 疑似乱码:${result.finalSummary.mojibakeSuspected ? '是' : '否'}`,
`- 缺少章节:${result.finalSummary.missingSections.length ? result.finalSummary.missingSections.join('、') : '无'}`,
'',
];
if (result.problems.length) {
lines.push('## 问题');
for (const p of result.problems) lines.push(`- ${p}`);
lines.push('');
}
if (result.warnings.length) {
lines.push('## 提醒');
for (const w of result.warnings) lines.push(`- ${w}`);
lines.push('');
}
lines.push('## result 文件列表');
if (result.resultFiles.length) for (const f of result.resultFiles) lines.push(`- ${f}`);
else lines.push('- 无');
return lines.join('\n') + '\n';
}
try {
const args = parseArgs(process.argv);
if (args.help) { console.log(usage()); process.exit(0); }
const result = check(args);
if (args.json) console.log(JSON.stringify(result, null, 2));
if (args.markdown) process.stdout.write(renderMarkdown(result));
process.exit(result.clean ? 0 : 1);
} catch (err) {
console.error(err.message || String(err));
console.error(usage());
process.exit(1);
}
module.exports = { check, extractSampleCryptoValuesFromText };