| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- #!/usr/bin/env node
- /**
- * verify-refs.mjs v2 —— 检查 dist 里文档引用的文件是否真实存在
- * 区分:真实依赖(必须存在) vs 示例性提及(可忽略)
- */
- import fs from 'node:fs';
- import path from 'node:path';
- const args = process.argv.slice(2);
- const CWD = process.cwd();
- const SHOW_ALL = args.includes('--all');
- const pathArg = args.find(a => !a.startsWith('--'));
- const DIST = path.resolve(pathArg || path.join(CWD, 'dist'));
- const skillsRoot = path.join(DIST, 'skills');
- if (!fs.existsSync(skillsRoot)) {
- console.error('dist/skills 不存在,先跑 npm run build');
- process.exit(1);
- }
- const C = { g: '\x1b[32m', y: '\x1b[33m', r: '\x1b[31m', d: '\x1b[2m', b: '\x1b[1m', x: '\x1b[0m' };
- function allMd(dir, out = []) {
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
- const p = path.join(dir, e.name);
- if (e.isDirectory()) allMd(p, out);
- else if (e.name.endsWith('.md')) out.push(p);
- }
- return out;
- }
- function extractRefs(text) {
- const refs = new Set();
- const patterns = [
- /`([^`\n]+\.(?:md|mjs|js|cjs|json|sh|py|ts|html|css))`/g,
- /\]\(\.?\/?([^)\s]+\.(?:md|mjs|js|json|sh|py))\)/g,
- /(?:详见|参见|见|参考)\s*[`「]?([\w./-]+\.(?:md|mjs|js|json|sh|py))/g,
- ];
- for (const re of patterns) {
- let m;
- while ((m = re.exec(text)) !== null) {
- const p = m[1].trim();
- if (!p.includes(' ')) refs.add(p);
- }
- }
- return [...refs];
- }
- /** 判定是否为示例性提及(无需真实存在) */
- function isExample(ref) {
- // 通配符 / 通配占位
- if (/[*?{}]/.test(ref)) return true;
- // 占位符形式
- if (/<[^>]+>/.test(ref)) return true;
- // 通用命名示例
- if (/^(kebab-case|PascalCase|xxx|Xxx|your-|my-|page-)/.test(ref)) return true;
- // 典型项目入口文件(描述用户项目结构,非技能资产)
- if (/^(\.?\/)?(index\.html|app\.js|index\.js|main\.js|plain\.html|theme\.css)$/.test(ref)) return true;
- // 用户项目目录结构示例(技能教用户怎么建,文件由用户项目生成)
- if (/^(src|lib|components|pages|routes|services|utils|public|assets|dist|build|workdir|user|modules)\//.test(ref)) return true;
- // 运行时生成物(技能运行时产出,非技能包内资产)
- if (/^(mint\.json|collect\.sh|url\.txt|report\.html|.*\.log)$/.test(ref)) return true;
- // 用户环境配置文件(由平台/用户在运行环境生成)
- if (/(fmode-identity\.json|settings\.local\.json|config\.json|\.fmode\/)/.test(ref)) return true;
- // 平台内部路径(不属于技能资产)
- if (/^\/npm\/|^\/index\.html$/.test(ref)) return true;
- // 用户上传 key 路径示例(uploader --key 的示例值)
- if (/^[a-z0-9-]+\/(index\.html|.*\.html)$/.test(ref) && !ref.startsWith('references/')) return true;
- // 代码文件名示例(文档教用户按此命名创建,非技能自带资产)
- if (/^(validate|machine|format|storage|browser-transport|utils|helpers)\.js$/.test(ref)) return true;
- if (/^(scripts\/check\.mjs|scripts\/deploy\.sh)$/.test(ref)) return true;
- // 示例路径(如 ./components/x.js,教用户按此建目录)
- if (/^\.?\/?(components|routes|services|utils|modules)\/[a-z0-9-]*x?\.js$/.test(ref)) return true;
- // 历史继承来源说明(如“继承自 xxx-v4.mjs”,旧版本已被替代)
- if (/^analyze-photos-v\d.*\.mjs$/.test(ref)) return true;
- // 外部规范/标准文档引用(不在本技能包内分发)
- if (/^[a-z-]+\/[a-z-]*standard\.md$/.test(ref)) return true;
- return false;
- }
- function isExternal(ref) {
- if (/^https?:\/\//.test(ref)) return true;
- if (/^(node:|npm |npx |pip |git )/.test(ref)) return true;
- if (ref.startsWith('/opt/') || ref.startsWith('/root/') || ref.startsWith('~')) return true; // 环境绝对路径
- if (ref.startsWith('./.') || /^\.fmode\//.test(ref)) return true; // 用户环境配置
- return false;
- }
- const results = [];
- for (const skill of fs.readdirSync(skillsRoot)) {
- const skillDir = path.join(skillsRoot, skill);
- if (!fs.statSync(skillDir).isDirectory()) continue;
- const mds = allMd(skillDir);
- const critical = []; // 真实依赖缺失(严重)
- const examples = []; // 示例性提及(可忽略)
- const checked = new Set();
- for (const mdFile of mds) {
- const text = fs.readFileSync(mdFile, 'utf8');
- const relDoc = path.relative(skillDir, mdFile);
- const docDir = path.dirname(mdFile);
- for (const ref of extractRefs(text)) {
- if (isExternal(ref)) continue;
- const key = `${relDoc}::${ref}`;
- if (checked.has(key)) continue;
- checked.add(key);
- const candidates = [
- path.resolve(docDir, ref),
- path.resolve(skillDir, ref),
- path.resolve(skillDir, 'references', ref),
- path.resolve(skillDir, 'scripts', ref),
- path.resolve(skillDir, 'scripts/prompts', ref),
- path.resolve(skillDir, 'assets', ref),
- path.resolve(skillDir, 'assets/theme', ref),
- path.resolve(skillDir, ref.replace(/^references\//, '')),
- ];
- const found = candidates.some(c => fs.existsSync(c) && fs.statSync(c).isFile());
- if (found) continue;
- // CHANGELOG 里的历史文件名不算依赖
- if (/CHANGELOG/i.test(relDoc)) continue;
- (isExample(ref) ? examples : critical).push({ doc: relDoc, ref });
- }
- }
- results.push({ skill, docs: mds.length, checked: checked.size, critical, examples });
- }
- let totalCritical = 0;
- console.log(`\n${C.b}文档引用完整性检查${C.x} (dist/skills)\n`);
- for (const r of results) {
- if (r.critical.length === 0) {
- const ex = r.examples.length ? ` ${C.d}(示例性提及 ${r.examples.length} 处,可忽略)${C.x}` : '';
- console.log(`${C.g}✓${C.x} ${r.skill} ${C.d}(${r.docs}篇/${r.checked}处)${C.x}${ex}`);
- } else {
- totalCritical += r.critical.length;
- console.log(`${C.r}✗${C.x} ${r.skill} ${C.d}— ${r.critical.length} 处真实依赖缺失${C.x}`);
- for (const m of (SHOW_ALL ? r.critical : r.critical.slice(0, 8))) {
- console.log(` ${C.r}${m.ref}${C.x} ${C.d}← ${m.doc}${C.x}`);
- }
- if (!SHOW_ALL && r.critical.length > 8) console.log(` ${C.d}...还有 ${r.critical.length - 8} 处 (--all 查看全部)${C.x}`);
- }
- }
- console.log(`\n${C.b}汇总${C.x}: ${results.length} 技能 | ${C.r}真实依赖缺失 ${totalCritical} 处${C.x}\n`);
- process.exit(totalCritical > 0 ? 1 : 0);
|