doctor.mjs 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. // lib/doctor.mjs — 环境自检
  2. // 检查 node / ffmpeg / ffprobe / 凭据 / 各通道探活 / 可选依赖,给出可操作结论。
  3. import { VERSION, checkVersionConsistency, log } from './index.mjs';
  4. import { describeConfig, loadConfig } from './config.mjs';
  5. import { checkFfmpeg, checkDemucs } from './ffmpeg.mjs';
  6. import { probeAll } from './providers.mjs';
  7. export async function runDoctor(argv = []) {
  8. if (argv.includes('--help') || argv.includes('-h')) {
  9. console.log(`
  10. fmode-mv doctor — 环境自检
  11. 检查项:Node 版本 / 三处版本一致性 / ffmpeg & ffprobe / 凭据 / 可选依赖(demucs,edge-tts) / 各通道探活
  12. 用法:
  13. fmode-mv doctor
  14. 无需参数。全部通过返回 0,有阻塞项返回 1。
  15. `);
  16. return 0;
  17. }
  18. let fail = 0;
  19. let warn = 0;
  20. console.log(`\n${'fmode-mv doctor'.padEnd(30, ' ')} v${VERSION}\n`);
  21. // 1. Node
  22. const major = Number(process.versions.node.split('.')[0]);
  23. (major >= 18 ? log.ok : log.err)(`Node.js ${process.versions.node}${major >= 18 ? '' : ' —— 需要 >= 18(内置 fetch)'}`);
  24. if (major < 18) fail++;
  25. // 2. 版本一致性
  26. const v = checkVersionConsistency();
  27. (v.ok ? log.ok : log.err)(
  28. `版本一致性:index=${v.version} package=${v.pkgVersion} SKILL.md=${v.skillVersion}` + (v.ok ? '' : ' ← 不一致')
  29. );
  30. if (!v.ok) fail++;
  31. // 3. ffmpeg / ffprobe
  32. const ff = checkFfmpeg();
  33. for (const k of ['ffmpeg', 'ffprobe']) {
  34. (ff[k].available ? log.ok : log.err)(`${k}: ${ff[k].version || '未找到'}`);
  35. if (!ff[k].available) fail++;
  36. }
  37. // 4. 凭据
  38. const cfg = loadConfig();
  39. if (cfg.token) {
  40. log.ok(`凭据已加载(${cfg.tokenSource}),令牌已脱敏:${'*'.repeat(6)}`);
  41. } else {
  42. log.err('未找到凭据:设置 FEME_API_TOKEN,或在 /opt/data/fmode-identity.json 写入 newapi_token');
  43. fail++;
  44. }
  45. log.dim(`网关: ${cfg.baseUrl}`);
  46. log.dim(`身份文件: ${cfg.identityPath || '未找到'}`);
  47. // 5. 可选依赖
  48. const dm = checkDemucs();
  49. (dm.available ? log.ok : log.warn)(`demucs(高质量音轨分离,可选): ${dm.available ? dm.version : '未安装 —— 将用 ffmpeg 相位相减兜底'}`);
  50. if (!dm.available) warn++;
  51. // 6. 通道探活
  52. console.log('');
  53. log.step('通道探活(轻量,不做真实提交)');
  54. if (cfg.token) {
  55. const rows = await probeAll({ deep: false });
  56. for (const r of rows) {
  57. const mark = r.available === true ? '✓' : r.available === null ? '·' : '!';
  58. const paint = r.available === true ? log.ok : r.available === null ? log.dim : log.warn;
  59. paint(`${r.kind.padEnd(8)} ${r.detail}${r.model ? ` [${r.model}]` : ''}`);
  60. if (r.available === false) warn++;
  61. }
  62. } else {
  63. log.warn('跳过(无凭据)');
  64. }
  65. console.log('');
  66. console.log('─'.repeat(56));
  67. if (fail === 0) {
  68. console.log(`✅ 环境就绪${warn ? `(${warn} 项非阻塞提示)` : ''}`);
  69. console.log(' 下一步:fmode-mv providers 查看能力现状;fmode-mv lyrics "主题" 试跑');
  70. return 0;
  71. }
  72. console.log(`❌ ${fail} 项阻塞问题需要处理(见上)`);
  73. return 1;
  74. }