#!/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 };