#!/usr/bin/env node // Copyright (c) 未来飞马 // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // Trademark Notice: // The MPL-2.0 license grants copyright permissions for source code only. // It does NOT grant any rights to use trademarks including "未来飞马", // "Harness Loop", "RSI", and associated slogan "让AI进化提前发生,让AI落地快人一步". // Any use of these trademarks requires separate written permission. /** * 冒烟检查:不联网、不调模型、不写仓库。 * * 检查项: * 1. 必需文件齐全(含标签字典可解析) * 2. 运行器自检(office 解包 + 分组 + 打标 + 合规 + reorder) * 3. 单元测试(node --test) * 4. 未把 token / 私钥 / 内部地址写进包内任何文件 */ import fs from 'node:fs'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const failures = []; const checks = []; function record(name, ok, detail) { checks.push({ name, ok, detail: String(detail ?? '') }); if (!ok) failures.push(name); } // 1. 必需文件 const REQUIRED = [ 'SKILL.md', 'README.md', 'package.json', 'skill-package-manifest.json', 'references/tag-dictionary.json', 'scripts/case-intake.mjs', 'scripts/office_extract.py', 'scripts/vision-bridge.mjs', 'scripts/listen-bridge.mjs', 'scripts/cloud-client.mjs', 'scripts/grouping.mjs', 'scripts/classify.mjs', 'scripts/image-size.mjs', 'scripts/lib.mjs', 'bin/skill-case-get.js', ]; { const missing = REQUIRED.filter((rel) => !fs.existsSync(path.join(ROOT, rel))); record('必需文件齐全', missing.length === 0, missing.length ? `缺少:${missing.join(', ')}` : `${REQUIRED.length} 个文件`); } // 2. 标签字典可解析 { try { const dict = JSON.parse(fs.readFileSync(path.join(ROOT, 'references', 'tag-dictionary.json'), 'utf8')); const dims = Object.keys(dict.dimensions || {}); record('标签字典可解析', dims.length >= 8, `维度:${dims.length} 个(${dims.join(', ')})`); } catch (error) { record('标签字典可解析', false, error.message); } } // 3. 运行器自检 { const run = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'case-intake.mjs'), 'selftest'], { encoding: 'utf8' }); record('case-intake selftest', run.status === 0, run.stdout ? run.stdout.slice(0, 200) : run.stderr); } // 4. 单元测试 { const run = spawnSync(process.execPath, ['--test', path.join(ROOT, 'scripts', 'tests')], { encoding: 'utf8' }); // node --test 传目录在部分版本上会当模块解析;退回 glob const globRun = run.status === 0 ? run : spawnSync( process.execPath, ['--test', path.join(ROOT, 'scripts', 'tests', 'grouping.test.mjs'), path.join(ROOT, 'scripts', 'tests', 'classify.test.mjs')], { encoding: 'utf8' }, ); const output = `${globRun.stdout || ''}${globRun.stderr || ''}`; const passMatch = output.match(/# pass (\d+)/) || output.match(/ℹ pass (\d+)/); const failMatch = output.match(/# fail (\d+)/) || output.match(/ℹ fail (\d+)/); record('单元测试', globRun.status === 0, `pass=${passMatch ? passMatch[1] : '?'} fail=${failMatch ? failMatch[1] : '?'}`); } // 5. 包内不得出现凭据 / 内部地址 { const FORBIDDEN = [ { label: 'internal-address', re: new RegExp(`\\b${['local', 'host'].join('')}\\b`, 'i') }, { label: 'loopback', re: /\b(127\.0\.0\.1|0\.0\.0\.0)\b/ }, { label: 'parse-session-token', re: /\br:[a-f0-9]{20,}\b/i }, { label: 'api-key', re: /\bsk-[a-zA-Z0-9-]{20,}\b/ }, { label: 'mongo-uri', re: /mongodb(\+srv)?:\/\//i }, ]; const BINARY = new Set(['.png', '.jpg', '.jpeg', '.gif', '.zip', '.pdf']); // 本文件自身就写着这些正则(和仓库 self-check.mjs 一样自跳过),否则会自己命中自己 const SELF = new Set([path.resolve(fileURLToPath(import.meta.url))]); const offenders = []; const walk = (dir) => { for (const entry of fs.readdirSync(dir)) { if (['node_modules', 'outputs', '.git', '.claude', 'case-get-outputs'].includes(entry)) continue; const full = path.join(dir, entry); const stat = fs.statSync(full); if (stat.isDirectory()) { walk(full); continue; } if (SELF.has(path.resolve(full))) continue; if (BINARY.has(path.extname(full).toLowerCase())) continue; let content = ''; try { content = fs.readFileSync(full, 'utf8'); } catch { continue; } for (const item of FORBIDDEN) { if (item.re.test(content)) offenders.push(`${path.relative(ROOT, full)}:${item.label}`); } } }; walk(ROOT); record('包内无凭据 / 无内网地址', offenders.length === 0, offenders.length ? offenders.join(', ') : '干净'); } // 输出 const passed = checks.filter((c) => c.ok).length; for (const check of checks) { process.stdout.write(`${check.ok ? '✔' : '✖'} ${check.name} — ${check.detail}\n`); } process.stdout.write(`\n冒烟检查:${passed}/${checks.length} 通过\n`); process.exit(failures.length ? 1 : 0);