verify-refs.mjs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. #!/usr/bin/env node
  2. /**
  3. * verify-refs.mjs v2 —— 检查 dist 里文档引用的文件是否真实存在
  4. * 区分:真实依赖(必须存在) vs 示例性提及(可忽略)
  5. */
  6. import fs from 'node:fs';
  7. import path from 'node:path';
  8. const args = process.argv.slice(2);
  9. const CWD = process.cwd();
  10. const SHOW_ALL = args.includes('--all');
  11. const pathArg = args.find(a => !a.startsWith('--'));
  12. const DIST = path.resolve(pathArg || path.join(CWD, 'dist'));
  13. const skillsRoot = path.join(DIST, 'skills');
  14. if (!fs.existsSync(skillsRoot)) {
  15. console.error('dist/skills 不存在,先跑 npm run build');
  16. process.exit(1);
  17. }
  18. const C = { g: '\x1b[32m', y: '\x1b[33m', r: '\x1b[31m', d: '\x1b[2m', b: '\x1b[1m', x: '\x1b[0m' };
  19. function allMd(dir, out = []) {
  20. for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
  21. const p = path.join(dir, e.name);
  22. if (e.isDirectory()) allMd(p, out);
  23. else if (e.name.endsWith('.md')) out.push(p);
  24. }
  25. return out;
  26. }
  27. function extractRefs(text) {
  28. const refs = new Set();
  29. const patterns = [
  30. /`([^`\n]+\.(?:md|mjs|js|cjs|json|sh|py|ts|html|css))`/g,
  31. /\]\(\.?\/?([^)\s]+\.(?:md|mjs|js|json|sh|py))\)/g,
  32. /(?:详见|参见|见|参考)\s*[`「]?([\w./-]+\.(?:md|mjs|js|json|sh|py))/g,
  33. ];
  34. for (const re of patterns) {
  35. let m;
  36. while ((m = re.exec(text)) !== null) {
  37. const p = m[1].trim();
  38. if (!p.includes(' ')) refs.add(p);
  39. }
  40. }
  41. return [...refs];
  42. }
  43. /** 判定是否为示例性提及(无需真实存在) */
  44. function isExample(ref) {
  45. // 通配符 / 通配占位
  46. if (/[*?{}]/.test(ref)) return true;
  47. // 占位符形式
  48. if (/<[^>]+>/.test(ref)) return true;
  49. // 通用命名示例
  50. if (/^(kebab-case|PascalCase|xxx|Xxx|your-|my-|page-)/.test(ref)) return true;
  51. // 典型项目入口文件(描述用户项目结构,非技能资产)
  52. if (/^(\.?\/)?(index\.html|app\.js|index\.js|main\.js|plain\.html|theme\.css)$/.test(ref)) return true;
  53. // 用户项目目录结构示例(技能教用户怎么建,文件由用户项目生成)
  54. if (/^(src|lib|components|pages|routes|services|utils|public|assets|dist|build|workdir|user|modules)\//.test(ref)) return true;
  55. // 运行时生成物(技能运行时产出,非技能包内资产)
  56. if (/^(mint\.json|collect\.sh|url\.txt|report\.html|.*\.log)$/.test(ref)) return true;
  57. // 用户环境配置文件(由平台/用户在运行环境生成)
  58. if (/(fmode-identity\.json|settings\.local\.json|config\.json|\.fmode\/)/.test(ref)) return true;
  59. // 平台内部路径(不属于技能资产)
  60. if (/^\/npm\/|^\/index\.html$/.test(ref)) return true;
  61. // 用户上传 key 路径示例(uploader --key 的示例值)
  62. if (/^[a-z0-9-]+\/(index\.html|.*\.html)$/.test(ref) && !ref.startsWith('references/')) return true;
  63. // 代码文件名示例(文档教用户按此命名创建,非技能自带资产)
  64. if (/^(validate|machine|format|storage|browser-transport|utils|helpers)\.js$/.test(ref)) return true;
  65. if (/^(scripts\/check\.mjs|scripts\/deploy\.sh)$/.test(ref)) return true;
  66. // 示例路径(如 ./components/x.js,教用户按此建目录)
  67. if (/^\.?\/?(components|routes|services|utils|modules)\/[a-z0-9-]*x?\.js$/.test(ref)) return true;
  68. // 历史继承来源说明(如“继承自 xxx-v4.mjs”,旧版本已被替代)
  69. if (/^analyze-photos-v\d.*\.mjs$/.test(ref)) return true;
  70. // 外部规范/标准文档引用(不在本技能包内分发)
  71. if (/^[a-z-]+\/[a-z-]*standard\.md$/.test(ref)) return true;
  72. return false;
  73. }
  74. function isExternal(ref) {
  75. if (/^https?:\/\//.test(ref)) return true;
  76. if (/^(node:|npm |npx |pip |git )/.test(ref)) return true;
  77. if (ref.startsWith('/opt/') || ref.startsWith('/root/') || ref.startsWith('~')) return true; // 环境绝对路径
  78. if (ref.startsWith('./.') || /^\.fmode\//.test(ref)) return true; // 用户环境配置
  79. return false;
  80. }
  81. const results = [];
  82. for (const skill of fs.readdirSync(skillsRoot)) {
  83. const skillDir = path.join(skillsRoot, skill);
  84. if (!fs.statSync(skillDir).isDirectory()) continue;
  85. const mds = allMd(skillDir);
  86. const critical = []; // 真实依赖缺失(严重)
  87. const examples = []; // 示例性提及(可忽略)
  88. const checked = new Set();
  89. for (const mdFile of mds) {
  90. const text = fs.readFileSync(mdFile, 'utf8');
  91. const relDoc = path.relative(skillDir, mdFile);
  92. const docDir = path.dirname(mdFile);
  93. for (const ref of extractRefs(text)) {
  94. if (isExternal(ref)) continue;
  95. const key = `${relDoc}::${ref}`;
  96. if (checked.has(key)) continue;
  97. checked.add(key);
  98. const candidates = [
  99. path.resolve(docDir, ref),
  100. path.resolve(skillDir, ref),
  101. path.resolve(skillDir, 'references', ref),
  102. path.resolve(skillDir, 'scripts', ref),
  103. path.resolve(skillDir, 'scripts/prompts', ref),
  104. path.resolve(skillDir, 'assets', ref),
  105. path.resolve(skillDir, 'assets/theme', ref),
  106. path.resolve(skillDir, ref.replace(/^references\//, '')),
  107. ];
  108. const found = candidates.some(c => fs.existsSync(c) && fs.statSync(c).isFile());
  109. if (found) continue;
  110. // CHANGELOG 里的历史文件名不算依赖
  111. if (/CHANGELOG/i.test(relDoc)) continue;
  112. (isExample(ref) ? examples : critical).push({ doc: relDoc, ref });
  113. }
  114. }
  115. results.push({ skill, docs: mds.length, checked: checked.size, critical, examples });
  116. }
  117. let totalCritical = 0;
  118. console.log(`\n${C.b}文档引用完整性检查${C.x} (dist/skills)\n`);
  119. for (const r of results) {
  120. if (r.critical.length === 0) {
  121. const ex = r.examples.length ? ` ${C.d}(示例性提及 ${r.examples.length} 处,可忽略)${C.x}` : '';
  122. console.log(`${C.g}✓${C.x} ${r.skill} ${C.d}(${r.docs}篇/${r.checked}处)${C.x}${ex}`);
  123. } else {
  124. totalCritical += r.critical.length;
  125. console.log(`${C.r}✗${C.x} ${r.skill} ${C.d}— ${r.critical.length} 处真实依赖缺失${C.x}`);
  126. for (const m of (SHOW_ALL ? r.critical : r.critical.slice(0, 8))) {
  127. console.log(` ${C.r}${m.ref}${C.x} ${C.d}← ${m.doc}${C.x}`);
  128. }
  129. if (!SHOW_ALL && r.critical.length > 8) console.log(` ${C.d}...还有 ${r.critical.length - 8} 处 (--all 查看全部)${C.x}`);
  130. }
  131. }
  132. console.log(`\n${C.b}汇总${C.x}: ${results.length} 技能 | ${C.r}真实依赖缺失 ${totalCritical} 处${C.x}\n`);
  133. process.exit(totalCritical > 0 ? 1 : 0);