#!/usr/bin/env node 'use strict'; const fs = require('fs'); const path = require('path'); function parseArgs(argv) { const args = { caseDir: '', file: '', requireDocumentAll: false, requireRuyiTrace: false, requireFingerprintFixture: false, requireAddonFirst: true, addonFirstOptOut: false, fingerprintFixture: '', 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 === '--require-document-all') args.requireDocumentAll = true; else if (a === '--require-ruyitrace') args.requireRuyiTrace = true; else if (a === '--require-fingerprint-fixture') args.requireFingerprintFixture = true; else if (a === '--require-addon-first') args.requireAddonFirst = true; else if (a === '--no-require-addon-first' || a === '--allow-no-addon-first') { args.requireAddonFirst = false; args.addonFirstOptOut = true; } else if (a === '--fingerprint-fixture') args.fingerprintFixture = argv[++i] || ''; 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_env_realism.js --case-dir case --markdown node scripts/check_env_realism.js --case-dir case --no-require-addon-first --markdown node scripts/check_env_realism.js --case-dir case --require-document-all --require-ruyitrace --require-fingerprint-fixture --require-addon-first --json node scripts/check_env_realism.js --file case/result/src/env/index.js --markdown 说明:检查补环境交付代码是否体现原型链、属性描述符、访问器、函数 / 访问器 / 实例对象 toString 保护、addon-first/native fallback 规则、document.all 特殊对象处理、指纹终端 API 值回放策略,以及选择 RuyiTrace 时是否沉淀 NDJSON 证据。addon-first 是默认硬性要求;如果选择 xbs isolated-vm,则 window.xbs / globalThis.xbs 视为 native-first 证据;只有用户明确要求不使用 addon / 不做 addon-first 时,才允许传入 --no-require-addon-first 并在总结中记录豁免原因。`; } 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 rel(root, p) { return (path.relative(root, p) || '.').replace(/\\/g, '/'); } function ext(p) { return path.extname(p).toLowerCase(); } 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); } else if (st.isFile()) out.push(p); return out; } function codeFiles(root) { return walk(root).filter(p => ['.js', '.mjs', '.cjs', '.py'].includes(ext(p))); } function has(pattern, text) { return pattern.test(text); } function any(pattern, files) { return files.some(f => has(pattern, readText(f))); } function matchingFiles(pattern, files, root) { return files.filter(f => has(pattern, readText(f))).map(f => rel(root, f)); } function listNdjson(caseDir) { const dir = path.join(caseDir, 'ruyi-trace', 'logs'); if (!exists(dir)) return []; return walk(dir).filter(p => p.toLowerCase().endsWith('.ndjson')); } function inspectRuyiTrace(caseDir, requireRuyiTrace) { const problems = []; const warnings = []; const ndjson = listNdjson(caseDir); const shouldCheck = requireRuyiTrace || ndjson.length > 0; const summary = path.join(caseDir, 'notes', 'ruyitrace-summary.md'); const priority = path.join(caseDir, 'notes', 'missing-env-priority.md'); const result = { required: shouldCheck, ndjson: ndjson.map(p => rel(caseDir, p)), summary: exists(summary) ? rel(caseDir, summary) : '', priority: exists(priority) ? rel(caseDir, priority) : '', }; if (!shouldCheck) return { result, problems, warnings }; if (!ndjson.length) problems.push('已要求 RuyiTrace 优先诊断,但 case/ruyi-trace/logs/ 下未找到 NDJSON 日志。'); if (!exists(summary)) problems.push('已要求 RuyiTrace 优先诊断,但未找到 notes/ruyitrace-summary.md;应先运行 import_ruyitrace_log.js。'); else { const text = readText(summary); if (!/API|api|调用栈|stack|类别统计|RuyiTrace/i.test(text)) warnings.push('ruyitrace-summary.md 内容较弱,建议包含 API 统计、stack.file 和环境模块分类。'); } if (!exists(priority)) problems.push('已选择 / 提供 RuyiTrace 日志,但未找到 notes/missing-env-priority.md;补环境前应把 NDJSON 命中的 api、stack.file、line、col 和补齐优先级写入该文件。'); else { const text = readText(priority); if (!/RuyiTrace|NDJSON|api|stack\.file|line|col|证据/.test(text)) { problems.push('missing-env-priority.md 未体现 RuyiTrace/NDJSON 证据、api 或 stack.file/line/col;不能证明补环境阶段持续参考了 trace 日志。'); } } return { result, problems, warnings }; } function readJsonSafe(file) { try { return JSON.parse(readText(file)); } catch (err) { return { __error: err.message || String(err) }; } } function countArray(v) { return Array.isArray(v) ? v.length : 0; } function fingerprintFixturePath(caseDir, explicit) { if (explicit) return path.resolve(explicit); const candidates = [ path.join(caseDir, 'fixtures', 'fingerprint.fixture.json'), path.join(caseDir, 'fixtures', 'sample.fixture.json'), ]; return candidates.find(exists) || candidates[0]; } function inspectFingerprint(caseDir, files, args) { const problems = []; const warnings = []; const allText = files.map(f => readText(f)).join('\n'); const terminalApiMentioned = /toDataURL|getImageData|measureText|getParameter|getSupportedExtensions|getShaderPrecisionFormat|readPixels|getBoundingClientRect|offsetWidth|offsetHeight|requestAdapter|OfflineAudioContext/.test(allText); const valueReplayMentioned = /fingerprint|回放|replay|fixture|findReplay|installFingerprintValueReplay|fingerprint\.fixture/i.test(allText); const badRenderLib = /\b(require|import)\s*\(?\s*['"](?:canvas|node-canvas|gl|headless-gl)['"]|from\s+['"](?:canvas|node-canvas|gl|headless-gl)['"]/i.test(allText); const automationForFingerprint = /\b(playwright|puppeteer|selenium|cloakbrowser|ruyipage|page\.goto|browser\.launch|chromium\.launch)\b/i.test(allText); const fixtureFile = fingerprintFixturePath(caseDir, args.fingerprintFixture); let fixtureExists = exists(fixtureFile); let counts = {}; if (fixtureExists) { const raw = readJsonSafe(fixtureFile); if (raw.__error) { problems.push(`指纹 fixture 解析失败:${raw.__error}`); } else { const fp = raw.fingerprint && typeof raw.fingerprint === 'object' ? raw.fingerprint : raw; counts = { canvas: countArray(fp.canvas && fp.canvas.toDataURL) + countArray(fp.canvas && fp.canvas.measureText) + countArray(fp.canvas && fp.canvas.getImageData) + countArray(fp.canvas && fp.canvas.toBlob), webgl: countArray(fp.webgl && fp.webgl.getParameter) + countArray(fp.webgl && fp.webgl.getShaderPrecisionFormat) + countArray(fp.webgl && fp.webgl.readPixels) + (fp.webgl && fp.webgl.getSupportedExtensions ? 1 : 0), webgpu: countArray(fp.webgpu && fp.webgpu.requestAdapter), audio: countArray(fp.audio && fp.audio.startRendering) + countArray(fp.audio && fp.audio.getChannelData), domGeometry: countArray(fp.domGeometry && fp.domGeometry.getBoundingClientRect) + countArray(fp.domGeometry && fp.domGeometry.offset), }; } } if (badRenderLib) problems.push('发现 node-canvas / headless-gl 等渲染库依赖;指纹补环境应优先真实浏览器采样值回放,不要在 Node.js 中强行模拟渲染管线。'); if (automationForFingerprint) problems.push('补环境源码疑似包含浏览器自动化库或 page.goto / browser.launch;自动化只能用于前置取证采样,不能进入最终 env。'); if ((args.requireFingerprintFixture || terminalApiMentioned) && !fixtureExists) { problems.push(`已涉及或要求指纹终端 API 值回放,但未找到指纹 fixture:${fixtureFile}`); } if (terminalApiMentioned && !valueReplayMentioned) { warnings.push('源码涉及指纹终端 API,但未明显体现 fingerprint fixture / replay;请确认不是静默伪造默认值。'); } if (args.requireFingerprintFixture && fixtureExists && Object.values(counts).reduce((a, b) => a + b, 0) === 0) { problems.push('已要求指纹 fixture,但未发现 Canvas / WebGL / WebGPU / Audio / DOM 几何终端 API 采样值。'); } return { result: { required: !!args.requireFingerprintFixture || terminalApiMentioned, fixture: fixtureExists ? rel(caseDir, fixtureFile) : '', terminalApiMentioned, valueReplayMentioned, badRenderLib, automationForFingerprint, counts, }, problems, warnings, }; } function inspectAddonFirst(files, requireAddonFirst) { const allText = files.map(f => readText(f)).join('\n'); const result = { required: !!requireAddonFirst, usesNativeProtectFallback: /NativeProtect|markNativeFunction\s*\(|markObjectToString\s*\(|setNativeFunc\s*\(|setObjFunc\s*\(|Function\.prototype\.toString|Object\.prototype\.toString/.test(allText), usesNativeLikeCreation: /\b(?:createNativeFunction|createNativeConstructor|createNativeGetter|createNativeSetter|defineNativeValue|defineNativeGetter|defineNativeSetter|defineNativeAccessor|createUndetectable|createInterceptor|createNativeObject|createProtoChains|createNativeCollection|getMimeTypesAndPlugins|createDocument|getProtoChainRegistry|deleteProtoChainRegistryEntry|clearProtoChainRegistry|setPrivate|getPrivate|hasPrivate|deletePrivate|throwTypeError)\s*\(/.test(allText), directAddonApi: /(?:\b(?:addon|nativeAddon|addonApi)\.|\bxbs\s*\.\s*(?:dom\s*\.\s*)?|\b(?:window|globalThis)\s*\.\s*xbs\s*\.\s*(?:dom\s*\.\s*)?)(?:createNativeFunction|createGetter|createSetter|createNativeObject|createProtoChains|createUndetectable|createInterceptor|createNativeCollection|getMimeTypesAndPlugins|getProtoChainRegistry|deleteProtoChainRegistryEntry|clearProtoChainRegistry|getPrivate|setPrivate|hasPrivate|deletePrivate|throwTypeError|createDocument)\s*\(/.test(allText), addonAwareHelper: /\b(?:createNativeFunction|createNativeConstructor|createNativeGetter|createNativeSetter|defineNativeValue|defineNativeGetter|defineNativeSetter|defineNativeAccessor|createUndetectable|createInterceptor|createNativeObject|createProtoChains|createNativeCollection|getMimeTypesAndPlugins|createDocument|getProtoChainRegistry|deleteProtoChainRegistryEntry|clearProtoChainRegistry|setPrivate|getPrivate|hasPrivate|deletePrivate|throwTypeError)\s*\([\s\S]{0,260}\b(?:addon|nativeAddon|options\.addon|addonResult|xbs|options\.xbs|xbsApi)\b/.test(allText), normalizesAddonResult: /\b(?:normalizeAddon|getAddonApi)\s*\(|\baddonLike\.addon\b|\baddonResult\.addon\b|\baddon\s*&&\s*addon\.addon\b/.test(allText), loadsAddon: /loadNativeAddon\s*\(|load_native_addon|WEB_JS_ENV_PATCHER_ADDON|WEB_JS_ENV_PATCHER_XBS_ISOLATED_VM|assets[\\/]+native-addon|native-addon|addon\.node|xbs-isolated-vm|isolated_vm\.node|window\.xbs|globalThis\.xbs|require\s*\([^)]*\.node/.test(allText), acceptsAddonInput: /\boptions\.(?:addon|xbs|xbsIsolatedVmPath)\b|function\s+\w+\s*\([^)]*\b(?:addon|xbs)\b|,\s*(?:addon|xbs)\s*\)|\{\s*(?:addon|xbs)\s*\}/.test(allText), fallbackDocumented: /fallback|降级|addon 不可用|addon不可用|NativeProtect fallback|JS fallback/.test(allText), newProtoChainsApi: /\bcreateProtoChains\s*\(\s*\[|\.(?:createProtoChains)\s*\(\s*\[/.test(allText), oldStyleCreateProtoChains: /\bcreateProtoChains\s*\(\s*['"][^'"]+['"]\s*,/.test(allText), oldStyleCreateNativeObject: /\bcreateNativeObject\s*\(\s*['"][^'"]+['"]\s*,/.test(allText), }; result.addonFirstEvidence = result.directAddonApi || result.addonAwareHelper || result.normalizesAddonResult || result.newProtoChainsApi; result.needsAddonFirst = result.usesNativeProtectFallback || result.usesNativeLikeCreation; const problems = []; const warnings = []; if (requireAddonFirst && result.needsAddonFirst && !result.addonFirstEvidence) { problems.push('已要求 addon-first,但源码使用 native-like / NativeProtect 相关能力时未发现先尝试 addon.node 或 xbs native API 的证据;创建函数、getter、setter、document.all、原型链等应先走 native API,native 能力不可用时才降级 NativeProtect。'); } if (requireAddonFirst && result.needsAddonFirst && !result.loadsAddon && !result.acceptsAddonInput) { problems.push('已要求 addon-first,但源码既未加载 addon.node / xbs isolated-vm,也未显式接收 addon/options.addon/options.xbs;无法保证补环境时优先使用 native API。'); } if ((requireAddonFirst || result.usesNativeProtectFallback) && result.usesNativeProtectFallback && !result.addonFirstEvidence && !result.fallbackDocumented) { warnings.push('源码使用 NativeProtect / JS fallback,但未明显记录 addon.node / xbs native API 不可用或调用失败的降级原因;建议输出 native 能力可用性和降级说明。'); } if (result.oldStyleCreateProtoChains) { const msg = '发现旧式 createProtoChains(name, chain) 调用;新补环境代码应迁移为 createProtoChains(descriptors),旧式形态只能出现在兼容层并记录迁移 / fallback 原因。'; if (/旧式|兼容|fallback|迁移|历史/.test(allText)) warnings.push(msg); else problems.push(msg); } if (result.oldStyleCreateNativeObject) { const msg = '发现旧式 createNativeObject(tag, proto, properties) 调用;新补环境代码应使用 createNativeObject(options) 或优先 createProtoChains(descriptors),旧式形态只能出现在兼容层并记录迁移 / fallback 原因。'; if (/旧式|兼容|fallback|迁移|历史/.test(allText)) warnings.push(msg); else problems.push(msg); } 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 root = args.file ? path.dirname(path.resolve(args.file)) : path.join(caseDir, 'result'); const files = args.file ? [path.resolve(args.file)] : codeFiles(root); const allText = files.map(f => readText(f)).join('\n'); const problems = []; const warnings = []; if (!files.length) problems.push(`未找到可检查的补环境源码文件:${root}`); const checks = { descriptors: any(/Object\.definePropert(?:y|ies)\s*\(/, files), prototypeChain: any(/Object\.setPrototypeOf\s*\(|Object\.create\s*\([^\n;]*\.prototype|createProtoChains\s*\(/, files), functionToString: any(/NativeProtect|Function\.prototype\.toString|createNativeFunction\s*\(|createNativeConstructor\s*\(|markNativeFunction\s*\(|setNativeFunc\s*\(/, files), accessorToString: any(/createGetter\s*\(|createSetter\s*\(|createNativeGetter\s*\(|createNativeSetter\s*\(|defineNativeGetter\s*\(|defineNativeSetter\s*\(|defineNativeAccessor\s*\(|setNativeFunc\s*\([^\n]*(get|set)\s+/i, files), instanceToString: any(/setObjFunc\s*\(|markObjectToString\s*\(|Symbol\.toStringTag|createNativeObject\s*\(|createProtoChains\s*\(/, files), documentAllExact: any(/(?:createUndetectable\s*\(|xbs\s*\.\s*dom\s*\.\s*createDocument\s*\(|\bdom\s*\.\s*createDocument\s*\()/, files), documentAllMentioned: /document\.all|['"]all['"]/.test(allText), fingerprintValueReplay: any(/installFingerprintValueReplay|findReplay|fingerprint\.fixture|指纹.*回放|value replay/i, files), }; if (!checks.descriptors) problems.push('未发现 Object.defineProperty / defineProperties;补环境不能只用普通赋值,必须显式属性描述符。'); if (!checks.prototypeChain) problems.push('未发现 Object.create(...prototype) / Object.setPrototypeOf / createProtoChains;需要按规则补构造函数和原型链。'); if (!checks.functionToString) problems.push('未发现函数 toString 保护(NativeProtect / createNativeFunction / markNativeFunction 等)。'); if (!checks.accessorToString) problems.push('未发现访问器 getter/setter 的 toString 保护(createGetter/createSetter/defineNativeGetter/defineNativeSetter 等)。'); if (!checks.instanceToString) problems.push('未发现实例对象 Object.prototype.toString 保护(Symbol.toStringTag / setObjFunc / createNativeObject 等)。'); if (args.requireDocumentAll && !checks.documentAllExact) problems.push('本 case 要求 document.all,但未发现 xbs.dom.createDocument 或 createUndetectable;document.all 不应仅用普通对象或 undefined 近似,isolated-vm 下优先 xbs.dom.createDocument,手写 document 时才用 xbs/createUndetectable handlers。'); if (!args.requireDocumentAll && checks.documentAllMentioned && !checks.documentAllExact) warnings.push('源码提到 document.all 但未发现 xbs.dom.createDocument 或 createUndetectable;如目标检测 HTMLDDA,isolated-vm 下优先 xbs.dom.createDocument,手写 document 时才用 xbs/createUndetectable handlers。'); const addonFirst = inspectAddonFirst(files, args.requireAddonFirst); problems.push(...addonFirst.problems); warnings.push(...addonFirst.warnings); const ruyi = inspectRuyiTrace(caseDir, args.requireRuyiTrace); problems.push(...ruyi.problems); warnings.push(...ruyi.warnings); const fingerprint = inspectFingerprint(caseDir, files, args); problems.push(...fingerprint.problems); warnings.push(...fingerprint.warnings); return { caseDir, checkedRoot: root, clean: problems.length === 0, checks, matching: { descriptors: matchingFiles(/Object\.definePropert(?:y|ies)\s*\(/, files, caseDir), prototypeChain: matchingFiles(/Object\.setPrototypeOf\s*\(|Object\.create\s*\([^\n;]*\.prototype|createProtoChains\s*\(/, files, caseDir), functionToString: matchingFiles(/NativeProtect|Function\.prototype\.toString|createNativeFunction\s*\(|createNativeConstructor\s*\(|markNativeFunction\s*\(|setNativeFunc\s*\(/, files, caseDir), accessorToString: matchingFiles(/createGetter\s*\(|createSetter\s*\(|createNativeGetter\s*\(|createNativeSetter\s*\(|defineNativeGetter\s*\(|defineNativeSetter\s*\(|defineNativeAccessor\s*\(|setNativeFunc\s*\([^\n]*(get|set)\s+/i, files, caseDir), instanceToString: matchingFiles(/setObjFunc\s*\(|markObjectToString\s*\(|Symbol\.toStringTag|createNativeObject\s*\(|createProtoChains\s*\(/, files, caseDir), documentAllExact: matchingFiles(/(?:createUndetectable\s*\(|xbs\s*\.\s*dom\s*\.\s*createDocument\s*\(|\bdom\s*\.\s*createDocument\s*\()/, files, caseDir), fingerprintValueReplay: matchingFiles(/installFingerprintValueReplay|findReplay|fingerprint\.fixture|指纹.*回放|value replay/i, files, caseDir), addonFirstEvidence: matchingFiles(/(?:\b(?:addon|nativeAddon|addonApi)\.|\bxbs\s*\.\s*(?:dom\s*\.\s*)?|\b(?:window|globalThis)\s*\.\s*xbs\s*\.\s*(?:dom\s*\.\s*)?)(?:createNativeFunction|createGetter|createSetter|createNativeObject|createProtoChains|createUndetectable|createInterceptor|createNativeCollection|getMimeTypesAndPlugins|getProtoChainRegistry|deleteProtoChainRegistryEntry|clearProtoChainRegistry|getPrivate|setPrivate|hasPrivate|deletePrivate|throwTypeError|createDocument)\s*\(|\b(?:createNativeFunction|createNativeConstructor|createNativeGetter|createNativeSetter|defineNativeValue|defineNativeGetter|defineNativeSetter|defineNativeAccessor|createUndetectable|createInterceptor|createNativeObject|createProtoChains|createNativeCollection|getMimeTypesAndPlugins|createDocument|getProtoChainRegistry|deleteProtoChainRegistryEntry|clearProtoChainRegistry|setPrivate|getPrivate|hasPrivate|deletePrivate|throwTypeError)\s*\([\s\S]{0,260}\b(?:addon|nativeAddon|options\.addon|addonResult|xbs|options\.xbs|xbsApi)\b|\b(?:normalizeAddon|getAddonApi)\s*\(|\bcreateProtoChains\s*\(\s*\[/, files, caseDir), }, addonFirst: addonFirst.result, addonFirstOptOut: args.addonFirstOptOut, ruyiTrace: ruyi.result, fingerprint: fingerprint.result, problems, warnings, files: files.map(f => rel(caseDir, f)), }; } function renderMarkdown(result) { const lines = [ '# 补环境真实性与 RuyiTrace 证据检查', '', `case 目录:${result.caseDir}`, `检查范围:${result.checkedRoot}`, `是否通过:${result.clean ? '是' : '否'}`, '', '## 真实性检查项', `- 属性描述符:${result.checks.descriptors ? '通过' : '缺失'}`, `- 原型链 / 构造函数:${result.checks.prototypeChain ? '通过' : '缺失'}`, `- 函数 toString 保护:${result.checks.functionToString ? '通过' : '缺失'}`, `- 访问器 toString 保护:${result.checks.accessorToString ? '通过' : '缺失'}`, `- 实例对象 toString 保护:${result.checks.instanceToString ? '通过' : '缺失'}`, `- addon-first 规则:${result.addonFirst.required ? (result.addonFirst.addonFirstEvidence ? '已发现先尝试 addon.node / xbs native API 的证据' : '缺失 native 优先证据') : (result.addonFirstOptOut ? '用户明确豁免,未强制检查' : '未强制检查')}`, ` - 直接 addon API:${result.addonFirst.directAddonApi ? '是' : '否'};addon-aware helper:${result.addonFirst.addonAwareHelper ? '是' : '否'};加载 addon/xbs:${result.addonFirst.loadsAddon ? '是' : '否'};接收 addon/xbs 输入:${result.addonFirst.acceptsAddonInput ? '是' : '否'};新版 createProtoChains:${result.addonFirst.newProtoChainsApi ? '是' : '否'}`, ` - 旧式 API:createProtoChains(name, chain)=${result.addonFirst.oldStyleCreateProtoChains ? '发现' : '未发现'};createNativeObject(tag, proto, properties)=${result.addonFirst.oldStyleCreateNativeObject ? '发现' : '未发现'}`, `- document.all 不可检测对象:${result.checks.documentAllExact ? '已使用 xbs.dom.createDocument / createUndetectable' : (result.checks.documentAllMentioned ? '提到但未精确处理' : '未涉及')}`, `- 指纹终端 API 值回放:${result.checks.fingerprintValueReplay ? '已体现' : (result.fingerprint.terminalApiMentioned ? '涉及指纹 API 但未明显体现回放' : '未涉及')}`, '', '## RuyiTrace 证据检查', `- 是否要求检查:${result.ruyiTrace.required ? '是' : '否'}`, `- NDJSON:${result.ruyiTrace.ndjson.length ? result.ruyiTrace.ndjson.join('、') : '未发现'}`, `- 摘要:${result.ruyiTrace.summary || '未发现'}`, `- 优先级笔记:${result.ruyiTrace.priority || '未发现'}`, '', '## 指纹值回放检查', `- 是否要求检查:${result.fingerprint.required ? '是' : '否'}`, `- 指纹 fixture:${result.fingerprint.fixture || '未发现'}`, `- 是否涉及终端 API:${result.fingerprint.terminalApiMentioned ? '是' : '否'}`, `- 是否体现 fixture / replay:${result.fingerprint.valueReplayMentioned ? '是' : '否'}`, `- 是否发现渲染库依赖:${result.fingerprint.badRenderLib ? '是' : '否'}`, `- 是否发现自动化代码:${result.fingerprint.automationForFingerprint ? '是' : '否'}`, `- 样本计数:${JSON.stringify(result.fingerprint.counts || {})}`, '', ]; 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('## 检查文件'); for (const f of result.files) lines.push(`- ${f}`); 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); }