#!/usr/bin/env node 'use strict'; const fs = require('fs'); const path = require('path'); function parseArgs(argv) { const args = { caseDir: '', fixture: '', envFile: '', require: '', 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 === '--fixture') args.fixture = argv[++i] || ''; else if (a === '--env-file') args.envFile = argv[++i] || ''; else if (a === '--require') args.require = 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_fingerprint_fixture.js --case-dir case --require canvas,webgl --markdown node scripts/check_fingerprint_fixture.js --fixture case/fixtures/fingerprint.fixture.json --env-file case/result/src/env/fingerprint-env.js --json 说明:检查浏览器指纹 fixture 是否覆盖 Canvas / WebGL / WebGPU / Audio / DOM 几何等终端 API,是否绑定同一 fingerprint baseline,并检查最终 env 是否避免 node-canvas / headless-gl / 自动化浏览器等错误方向。`; } 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 readJson(p) { return JSON.parse(readText(p)); } 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 countArray(v) { return Array.isArray(v) ? v.length : 0; } function hasResultObject(v) { return !!v && typeof v === 'object' && Object.prototype.hasOwnProperty.call(v, 'result'); } function pickBaselineId(obj) { if (!obj || typeof obj !== 'object') return ''; return String(obj.baselineId || (obj.source && obj.source.baselineId) || ''); } const BAD_VALUE_SOURCE_RE = /(AI\s*(?:推断|猜|生成|guess)|猜测|编造|伪造|默认值|随机值|mock|fake|placeholder|static\s*analysis|静态分析|inferred|guessed|random\s*value|jsdom|node-canvas|headless-gl)/i; const REAL_VALUE_SOURCE_RE = /(RuyiTrace|NDJSON|ruyiPage|Camoufox|CloakBrowser|手动|真实浏览器|browser|Hook|HAR|MCP|用户提供|console|network)/i; const TRACE_TOOL_RE = /(RuyiTrace|NDJSON)/i; const AUTOMATION_OR_USER_TOOL_RE = /(ruyiPage|Camoufox|CloakBrowser|手动|真实浏览器|browser|Hook|HAR|MCP|用户提供|console|network)/i; const LONG_VALUE_THRESHOLD = 3900; const BAD_TRUNCATED_KEY_RE = /^(visiblePreview|visibleSha256|sha256OfVisible|visibleValue|visibleSnippet|preview|clippedPreview)$/i; const BAD_TRUNCATED_TEXT_RE = /(\.\.\.<(?:clipped|truncated)>|||__ruyiTraceLongString__|sha256OfVisible|visibleSha256|visiblePreview|realLength["']?\s*[:=]\s*["']?unknown)/i; const CHUNK_ENCODING_RE = /^(string-chunks|base64-chunks)$/; function jsonText(v) { try { return JSON.stringify(v || {}); } catch { return String(v || ''); } } function sourceLabel(source) { if (!source || typeof source !== 'object') return ''; return [source.capturedBy, source.tool, source.mode, source.sourceType, source.type, source.origin, source.evidence] .filter(Boolean).map(String).join(' '); } function traceStatusOf(source, record) { return String((record && record.traceStatus) || (source && source.traceStatus) || ''); } function hasSourceMeta(source) { if (!source || typeof source !== 'object') return false; return !!(source.capturedBy || source.tool || source.mode || source.sourceType || source.type || source.origin || source.evidence); } function isTruncatedTraceStatus(status) { const s = String(status || ''); if (/used-untruncated|untruncated|not-truncated/i.test(s)) return false; return /(^|[-_\s])truncated($|[-_\s])|疑似截断|截断/i.test(s); } function isTraceValueSource(source, record) { const status = traceStatusOf(source, record); const label = sourceLabel(source); if (/used-untruncated/i.test(status)) return true; if (/truncated|missing|not-covered|baseline-conflict|unused/i.test(status) && AUTOMATION_OR_USER_TOOL_RE.test(label) && !TRACE_TOOL_RE.test(String(source && source.capturedBy || ''))) return false; return TRACE_TOOL_RE.test(label); } function collectStringLengths(value, out = []) { if (typeof value === 'string') out.push(value.length); else if (Array.isArray(value)) for (const item of value) collectStringLengths(item, out); else if (value && typeof value === 'object') for (const item of Object.values(value)) collectStringLengths(item, out); return out; } function isChunkedValue(value) { return !!( value && typeof value === 'object' && CHUNK_ENCODING_RE.test(String(value.encoding || '')) && Array.isArray(value.chunks) ); } function hasOwn(obj, key) { return !!(obj && typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, key)); } function hasLengthAndHashOn(value, record, source) { if (isChunkedValue(value)) { return Number.isFinite(value.valueLength) && typeof value.sha256 === 'string' && /^[a-f0-9]{64}$/i.test(value.sha256); } if (value && typeof value === 'object') { const metrics = collectValueMetrics(value); if (metrics.chunked.length) { return metrics.chunked.every(info => Number.isFinite(info.value.valueLength) && typeof info.value.sha256 === 'string' && /^[a-f0-9]{64}$/i.test(info.value.sha256) ); } } return ( (hasOwn(record, 'valueLength') || hasOwn(source, 'valueLength')) && (hasOwn(record, 'sha256') || hasOwn(source, 'sha256')) ); } function collectValueMetrics(value, out = { maxLen: 0, chunked: [], badMarkers: [] }, pathParts = []) { const label = pathParts.join('.') || 'result'; if (typeof value === 'string') { out.maxLen = Math.max(out.maxLen, value.length); if (BAD_TRUNCATED_TEXT_RE.test(value)) out.badMarkers.push(`${label} 包含截断标记或可见片段标记`); } else if (Array.isArray(value)) { value.forEach((item, idx) => collectValueMetrics(item, out, pathParts.concat(String(idx)))); } else if (value && typeof value === 'object') { if (isChunkedValue(value)) { const joined = value.chunks.join(''); out.maxLen = Math.max(out.maxLen, Number(value.valueLength) || joined.length); out.chunked.push({ path: label, value, joinedLength: joined.length }); } for (const [key, item] of Object.entries(value)) { if (BAD_TRUNCATED_KEY_RE.test(key)) out.badMarkers.push(`${label}.${key} 使用了 Trace 可见片段字段,不能作为最终值`); collectValueMetrics(item, out, pathParts.concat(key)); } } return out; } function validateChunkedValue(itemPath, chunk) { const problems = []; const warnings = []; const encoding = String(chunk.encoding || ''); const joined = Array.isArray(chunk.chunks) ? chunk.chunks.join('') : ''; if (!CHUNK_ENCODING_RE.test(encoding)) problems.push(`样本 ${itemPath} 的分片 encoding 不合法:${encoding || '未指定'}。`); if (!Array.isArray(chunk.chunks) || chunk.chunks.some(v => typeof v !== 'string')) problems.push(`样本 ${itemPath} 的 chunks 必须是字符串数组。`); if (!Number.isFinite(chunk.valueLength)) problems.push(`样本 ${itemPath} 的分片结果缺少数值型 valueLength。`); if (!Number.isFinite(chunk.chunkSize) || chunk.chunkSize <= 0) problems.push(`样本 ${itemPath} 的分片结果缺少有效 chunkSize。`); if (chunk.truncated !== false) problems.push(`样本 ${itemPath} 的分片结果必须显式 truncated:false。`); if (Number.isFinite(chunk.valueLength) && joined.length !== chunk.valueLength) { problems.push(`样本 ${itemPath} 的 chunks 拼接长度 ${joined.length} 与 valueLength ${chunk.valueLength} 不一致。`); } if (Number.isFinite(chunk.chunkSize)) { const tooLarge = (chunk.chunks || []).find(v => v.length > chunk.chunkSize); if (tooLarge) warnings.push(`样本 ${itemPath} 存在超过 chunkSize 的分片;请确认分片写入逻辑没有异常。`); } if (typeof chunk.sha256 !== 'string' || !/^[a-f0-9]{64}$/i.test(chunk.sha256)) { problems.push(`样本 ${itemPath} 的长指纹分片缺少完整 sha256;必须先在真实浏览器采样 Hook 中 finalize,再写入 fixture。`); } if (BAD_TRUNCATED_TEXT_RE.test(joined)) problems.push(`样本 ${itemPath} 的分片拼接结果包含截断标记,不能作为最终值。`); return { problems, warnings }; } function collectResultRecords(node, out = [], pathParts = []) { if (!node || typeof node !== 'object') return out; if (Object.prototype.hasOwnProperty.call(node, 'result')) { out.push({ path: pathParts.join('.') || '$', record: node, value: node.result }); } for (const [key, value] of Object.entries(node)) { if (key === 'source' || key === 'valueSource' || key === 'match') continue; if (value && typeof value === 'object') collectResultRecords(value, out, pathParts.concat(key)); } return out; } function validateValueSources(fp) { const problems = []; const warnings = []; const sourceSummary = { totalRecords: 0, inheritedSource: 0, explicitSource: 0, traceSource: 0, sampledSource: 0, longValueRecords: 0, chunkedLongValueRecords: 0, blockedTruncatedResultRecords: 0, badSourceRecords: 0 }; const rootSource = fp && fp.source; function validateSource(source, label, record, inherited) { if (!source || typeof source !== 'object') { problems.push(`${label} 缺少真实值来源 source / capturedBy;必须优先使用未截断 Trace,Trace 不可用时用已确认取证工具采样,不能由 AI 猜值。`); return; } const sourceText = jsonText(source); const labelText = sourceLabel(source); const isFixtureRoot = label === 'fixture.source'; if (!hasSourceMeta(source)) problems.push(`${label} 的 source 缺少 mode / capturedBy / tool / sourceType 等来源字段。`); if (!REAL_VALUE_SOURCE_RE.test(sourceText)) problems.push(`${label} 的 source 未体现真实浏览器证据来源;请记录 RuyiTrace 未截断值,或 ruyiPage / Camoufox / CloakBrowser / 手动浏览器采样来源。`); if (BAD_VALUE_SOURCE_RE.test(sourceText)) { if (!isFixtureRoot) sourceSummary.badSourceRecords += 1; problems.push(`${label} 的 source 疑似为 AI 猜值、静态分析、默认值、随机值、mock 值或 Node.js 模拟库结果,不能作为最终指纹回放值。`); } const status = traceStatusOf(source, record); if (!status) warnings.push(`${label} 缺少 traceStatus;建议记录 used-untruncated / unused / missing / not-covered / truncated / baseline-conflict,明确是否优先检查 Trace。`); if (!isFixtureRoot && isTraceValueSource(source, record)) sourceSummary.traceSource += 1; if (!isFixtureRoot && AUTOMATION_OR_USER_TOOL_RE.test(labelText)) sourceSummary.sampledSource += 1; } validateSource(rootSource, 'fixture.source', null, false); const records = collectResultRecords(fp); sourceSummary.totalRecords = records.length; for (const item of records) { const source = item.record.source || item.record.valueSource || rootSource; const inherited = !(item.record.source || item.record.valueSource); if (inherited) sourceSummary.inheritedSource += 1; else sourceSummary.explicitSource += 1; validateSource(source, `样本 ${item.path}`, item.record, inherited); const metrics = collectValueMetrics(item.value); const maxLen = metrics.maxLen; if (maxLen >= LONG_VALUE_THRESHOLD) sourceSummary.longValueRecords += 1; if (metrics.chunked.length) sourceSummary.chunkedLongValueRecords += metrics.chunked.length; for (const marker of metrics.badMarkers) { sourceSummary.blockedTruncatedResultRecords += 1; problems.push(`样本 ${item.path} ${marker};不能把 RuyiTrace 可见片段、clip 片段或未知真实长度写入最终 fixture。`); } for (const chunkInfo of metrics.chunked) { const chunkResult = validateChunkedValue(`${item.path}.${chunkInfo.path}`, chunkInfo.value); problems.push(...chunkResult.problems); warnings.push(...chunkResult.warnings); } const valueIsMarkedTruncated = item.record.truncated === true || (source && source.truncated === true); const status = traceStatusOf(source, item.record); if (valueIsMarkedTruncated) problems.push(`样本 ${item.path} 标记为 truncated=true,不能作为最终回放值;请用同一 baseline 下的取证工具补采完整值。`); if (isTraceValueSource(source, item.record) && (maxLen >= LONG_VALUE_THRESHOLD || isTruncatedTraceStatus(status) || valueIsMarkedTruncated)) { sourceSummary.blockedTruncatedResultRecords += 1; problems.push(`样本 ${item.path} 来自 Trace 且疑似长字段截断;不能把 RuyiTrace 4000 / 4096 可见片段作为完整指纹值,请用同一 baseline 下的 ruyiPage / Camoufox / CloakBrowser / 手动浏览器补采完整值。`); } if (maxLen >= LONG_VALUE_THRESHOLD && inherited) { problems.push(`样本 ${item.path} 是长字符串 / 大结果但未记录每条样本自己的 source、valueLength 和 sha256;请补充完整浏览器采样来源,避免误把 Trace 4000 / 4096 字符可见片段当成真实值。`); } if (maxLen >= LONG_VALUE_THRESHOLD && !hasLengthAndHashOn(item.value, item.record, source)) { problems.push(`样本 ${item.path} 长度达到 ${maxLen},但缺少完整 valueLength 与 sha256;长指纹必须通过真实浏览器完整采样或分片保存并记录完整 hash。`); } } return { problems, warnings, sourceSummary }; } function inspectFixture(file) { const problems = []; const warnings = []; const counts = { canvasToDataURL: 0, canvasToBlob: 0, canvasMeasureText: 0, canvasGetImageData: 0, webglGetParameter: 0, webglGetSupportedExtensions: 0, webglGetExtension: 0, webglGetShaderPrecisionFormat: 0, webglReadPixels: 0, webgpuRequestAdapter: 0, audioStartRendering: 0, audioGetChannelData: 0, domGetBoundingClientRect: 0, domOffset: 0, }; let fixture = null; if (!file || !exists(file)) { problems.push(`未找到指纹 fixture:${file || '未指定'}`); return { fixture, counts, problems, warnings }; } try { fixture = readJson(file); } catch (err) { problems.push(`指纹 fixture JSON 解析失败:${err.message}`); return { fixture, counts, problems, warnings }; } const fp = fixture.fingerprint && typeof fixture.fingerprint === 'object' ? fixture.fingerprint : fixture; counts.canvasToDataURL = countArray(fp.canvas && fp.canvas.toDataURL); counts.canvasToBlob = countArray(fp.canvas && fp.canvas.toBlob); counts.canvasMeasureText = countArray(fp.canvas && fp.canvas.measureText); counts.canvasGetImageData = countArray(fp.canvas && fp.canvas.getImageData); counts.webglGetParameter = countArray(fp.webgl && fp.webgl.getParameter); counts.webglGetSupportedExtensions = hasResultObject(fp.webgl && fp.webgl.getSupportedExtensions) ? 1 : countArray(fp.webgl && fp.webgl.getSupportedExtensions); counts.webglGetExtension = countArray(fp.webgl && fp.webgl.getExtension); counts.webglGetShaderPrecisionFormat = countArray(fp.webgl && fp.webgl.getShaderPrecisionFormat); counts.webglReadPixels = countArray(fp.webgl && fp.webgl.readPixels); counts.webgpuRequestAdapter = countArray(fp.webgpu && fp.webgpu.requestAdapter); counts.audioStartRendering = countArray(fp.audio && fp.audio.startRendering); counts.audioGetChannelData = countArray(fp.audio && fp.audio.getChannelData); counts.domGetBoundingClientRect = countArray(fp.domGeometry && fp.domGeometry.getBoundingClientRect); counts.domOffset = countArray(fp.domGeometry && fp.domGeometry.offset); const baselineId = pickBaselineId(fp); if (!baselineId) problems.push('指纹 fixture 缺少 baselineId;必须先创建 case/notes/fingerprint-baseline.json,并让 fixture 绑定同一 baselineId。'); if (!fp.source) problems.push('指纹 fixture 缺少 source 字段;必须记录真实值来源,优先使用未截断 Trace,Trace 不可用时使用已确认取证工具采样。'); const sourceResult = validateValueSources(fp); problems.push(...sourceResult.problems); warnings.push(...sourceResult.warnings); return { fixture: fp, baselineId, counts, problems, warnings, sourceSummary: sourceResult.sourceSummary }; } function inspectBaseline(caseDir, fixtureResult) { const problems = []; const warnings = []; const baselineFile = path.join(caseDir, 'notes', 'fingerprint-baseline.json'); const result = { file: baselineFile, present: exists(baselineFile), baselineId: '', conflicts: [] }; if (!result.present) { problems.push('缺少指纹基线文件 case/notes/fingerprint-baseline.json;涉及指纹采样时必须先固定同一 case 的 fingerprint baseline。'); return { result, problems, warnings }; } let baseline; try { baseline = readJson(baselineFile); } catch (err) { problems.push(`指纹基线 JSON 解析失败:${err.message}`); return { result, problems, warnings }; } result.baselineId = String(baseline.baselineId || ''); if (!result.baselineId) problems.push('fingerprint-baseline.json 缺少 baselineId。'); if (fixtureResult.baselineId && result.baselineId && fixtureResult.baselineId !== result.baselineId) { problems.push(`指纹 fixture baselineId 与基线不一致:fixture=${fixtureResult.baselineId},baseline=${result.baselineId}。不得混用不同随机指纹样本。`); } const fp = fixtureResult.fixture || {}; const source = fp.source || {}; const checks = [ ['userAgent', source.userAgent, baseline.navigator && baseline.navigator.userAgent], ['timezone', source.timezone, baseline.network && baseline.network.timezone], ['locale', source.locale, baseline.network && baseline.network.locale], ]; for (const [name, a, b] of checks) { if (a && b && String(a) !== String(b)) result.conflicts.push({ field: name, fixture: String(a), baseline: String(b) }); } if (result.conflicts.length) { problems.push(`指纹 fixture 与 baseline 核心字段冲突:${result.conflicts.map(x => `${x.field}: fixture=${x.fixture}, baseline=${x.baseline}`).join(';')}。请重新采样或生成新 baseline。`); } return { result, problems, warnings }; } const REQUIRE_RULES = { canvas: ['canvasToDataURL', 'canvasMeasureText', 'canvasGetImageData'], webgl: ['webglGetParameter', 'webglGetSupportedExtensions', 'webglGetShaderPrecisionFormat', 'webglReadPixels'], webgpu: ['webgpuRequestAdapter'], audio: ['audioStartRendering', 'audioGetChannelData'], 'dom-geometry': ['domGetBoundingClientRect', 'domOffset'], dom: ['domGetBoundingClientRect', 'domOffset'], font: ['canvasMeasureText', 'domOffset'], fonts: ['canvasMeasureText', 'domOffset'], }; const BAD_RENDER_PATTERNS = [ { name: 'node-canvas/canvas', pattern: /\b(require|import)\s*\(?\s*['"](?:canvas|node-canvas)['"]|from\s+['"](?:canvas|node-canvas)['"]/i }, { name: 'headless-gl/gl', pattern: /\b(require|import)\s*\(?\s*['"](?:gl|headless-gl)['"]|from\s+['"](?:gl|headless-gl)['"]/i }, { name: '浏览器自动化', pattern: /\b(playwright|puppeteer|selenium|cloakbrowser|ruyipage|page\.goto|browser\.launch|chromium\.launch)\b/i }, ]; function inspectEnvCode(files, root) { const problems = []; const warnings = []; const hits = []; for (const file of files) { if (!exists(file)) continue; const text = readText(file); for (const item of BAD_RENDER_PATTERNS) { if (item.pattern.test(text)) hits.push({ file: rel(root, file), type: item.name }); } if (/toDataURL|getImageData|measureText|getParameter|getBoundingClientRect/.test(text) && !/fingerprint|回放|replay|fixture/i.test(text)) { warnings.push(`${rel(root, file)} 涉及指纹终端 API,但未明显体现 fixture / replay;请确认不是在 Node.js 中伪造渲染过程。`); } } if (hits.length) { problems.push(`发现不推荐的指纹实现方向:${hits.map(h => `${h.file}(${h.type})`).join('、')}。应使用真实浏览器采样值回放,不要在最终项目中依赖渲染库或自动化浏览器计算指纹。`); } return { problems, warnings, hits }; } function defaultFixture(caseDir) { const candidates = [ path.join(caseDir, 'fixtures', 'fingerprint.fixture.json'), path.join(caseDir, 'fixtures', 'sample.fixture.json'), ]; return candidates.find(exists) || candidates[0]; } function defaultEnvFiles(caseDir) { const result = path.join(caseDir, 'result'); return walk(result).filter(p => ['.js', '.mjs', '.cjs'].includes(ext(p))); } function check(args) { const caseDir = args.caseDir ? path.resolve(args.caseDir) : (args.fixture ? path.resolve(path.dirname(args.fixture), '..') : process.cwd()); const fixtureFile = args.fixture ? path.resolve(args.fixture) : defaultFixture(caseDir); const envFiles = args.envFile ? [path.resolve(args.envFile)] : defaultEnvFiles(caseDir); const required = String(args.require || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean); const problems = []; const warnings = []; const fixtureResult = inspectFixture(fixtureFile); problems.push(...fixtureResult.problems); warnings.push(...fixtureResult.warnings); for (const item of required) { const rules = REQUIRE_RULES[item] || []; if (!rules.length) warnings.push(`未知 require 类型:${item}`); const ok = rules.some(k => fixtureResult.counts[k] > 0); if (!ok) problems.push(`要求 ${item} 指纹样本,但 fixture 中未发现对应终端 API 返回值。`); } const baselineResult = inspectBaseline(caseDir, fixtureResult); problems.push(...baselineResult.problems); warnings.push(...baselineResult.warnings); const envResult = inspectEnvCode(envFiles, caseDir); problems.push(...envResult.problems); warnings.push(...envResult.warnings); return { caseDir, fixtureFile, envFiles: envFiles.map(p => rel(caseDir, p)), required, clean: problems.length === 0, baseline: baselineResult.result, fixtureBaselineId: fixtureResult.baselineId || '', counts: fixtureResult.counts, sourceSummary: fixtureResult.sourceSummary || { totalRecords: 0, inheritedSource: 0, explicitSource: 0, traceSource: 0, sampledSource: 0, longValueRecords: 0, chunkedLongValueRecords: 0, blockedTruncatedResultRecords: 0, badSourceRecords: 0 }, badImplementationHits: envResult.hits, problems, warnings, }; } function renderMarkdown(result) { const lines = [ '# 指纹 fixture 与回放实现检查', '', `case 目录:${result.caseDir}`, `fixture:${result.fixtureFile}`, `是否通过:${result.clean ? '是' : '否'}`, '', '## 指纹基线', `- baseline 文件:${result.baseline.file}`, `- baseline 是否存在:${result.baseline.present ? '是' : '否'}`, `- baselineId:${result.baseline.baselineId || '未发现'}`, `- fixture baselineId:${result.fixtureBaselineId || '未发现'}`, `- 核心字段冲突:${result.baseline.conflicts.length ? '是' : '否'}`, '', '## 样本覆盖统计', ]; for (const [k, v] of Object.entries(result.counts)) lines.push(`- ${k}:${v}`); lines.push('', '## 指纹值来源检查'); lines.push(`- result 样本数量:${result.sourceSummary.totalRecords}`); lines.push(`- 显式 source 样本:${result.sourceSummary.explicitSource}`); lines.push(`- 继承全局 source 样本:${result.sourceSummary.inheritedSource}`); lines.push(`- Trace 来源样本:${result.sourceSummary.traceSource}`); lines.push(`- 自动化 / 手动浏览器采样来源样本:${result.sourceSummary.sampledSource}`); lines.push(`- 长字段样本:${result.sourceSummary.longValueRecords}`); lines.push(`- 分片长字段样本:${result.sourceSummary.chunkedLongValueRecords}`); lines.push(`- 被阻断的截断 / 可见片段样本:${result.sourceSummary.blockedTruncatedResultRecords}`); lines.push(`- 疑似猜值 / 默认值 / 模拟库来源样本:${result.sourceSummary.badSourceRecords}`); lines.push('', '## 检查的 env 文件'); if (result.envFiles.length) for (const f of result.envFiles) lines.push(`- ${f}`); else lines.push('- 未发现 result 下的 JS env 文件'); if (result.problems.length) { lines.push('', '## 问题'); for (const p of result.problems) lines.push(`- ${p}`); } if (result.warnings.length) { lines.push('', '## 提醒'); for (const w of result.warnings) lines.push(`- ${w}`); } 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); }