// lib/doctor.mjs — 环境自检 // 检查 node / ffmpeg / ffprobe / 凭据 / 各通道探活 / 可选依赖,给出可操作结论。 import { VERSION, checkVersionConsistency, log } from './index.mjs'; import { describeConfig, loadConfig } from './config.mjs'; import { checkFfmpeg, checkDemucs } from './ffmpeg.mjs'; import { probeAll } from './providers.mjs'; export async function runDoctor(argv = []) { if (argv.includes('--help') || argv.includes('-h')) { console.log(` fmode-mv doctor — 环境自检 检查项:Node 版本 / 三处版本一致性 / ffmpeg & ffprobe / 凭据 / 可选依赖(demucs,edge-tts) / 各通道探活 用法: fmode-mv doctor 无需参数。全部通过返回 0,有阻塞项返回 1。 `); return 0; } let fail = 0; let warn = 0; console.log(`\n${'fmode-mv doctor'.padEnd(30, ' ')} v${VERSION}\n`); // 1. Node const major = Number(process.versions.node.split('.')[0]); (major >= 18 ? log.ok : log.err)(`Node.js ${process.versions.node}${major >= 18 ? '' : ' —— 需要 >= 18(内置 fetch)'}`); if (major < 18) fail++; // 2. 版本一致性 const v = checkVersionConsistency(); (v.ok ? log.ok : log.err)( `版本一致性:index=${v.version} package=${v.pkgVersion} SKILL.md=${v.skillVersion}` + (v.ok ? '' : ' ← 不一致') ); if (!v.ok) fail++; // 3. ffmpeg / ffprobe const ff = checkFfmpeg(); for (const k of ['ffmpeg', 'ffprobe']) { (ff[k].available ? log.ok : log.err)(`${k}: ${ff[k].version || '未找到'}`); if (!ff[k].available) fail++; } // 4. 凭据 const cfg = loadConfig(); if (cfg.token) { log.ok(`凭据已加载(${cfg.tokenSource}),令牌已脱敏:${'*'.repeat(6)}`); } else { log.err('未找到凭据:设置 FEME_API_TOKEN,或在 /opt/data/fmode-identity.json 写入 newapi_token'); fail++; } log.dim(`网关: ${cfg.baseUrl}`); log.dim(`身份文件: ${cfg.identityPath || '未找到'}`); // 5. 可选依赖 const dm = checkDemucs(); (dm.available ? log.ok : log.warn)(`demucs(高质量音轨分离,可选): ${dm.available ? dm.version : '未安装 —— 将用 ffmpeg 相位相减兜底'}`); if (!dm.available) warn++; // 6. 通道探活 console.log(''); log.step('通道探活(轻量,不做真实提交)'); if (cfg.token) { const rows = await probeAll({ deep: false }); for (const r of rows) { const mark = r.available === true ? '✓' : r.available === null ? '·' : '!'; const paint = r.available === true ? log.ok : r.available === null ? log.dim : log.warn; paint(`${r.kind.padEnd(8)} ${r.detail}${r.model ? ` [${r.model}]` : ''}`); if (r.available === false) warn++; } } else { log.warn('跳过(无凭据)'); } console.log(''); console.log('─'.repeat(56)); if (fail === 0) { console.log(`✅ 环境就绪${warn ? `(${warn} 项非阻塞提示)` : ''}`); console.log(' 下一步:fmode-mv providers 查看能力现状;fmode-mv lyrics "主题" 试跑'); return 0; } console.log(`❌ ${fail} 项阻塞问题需要处理(见上)`); return 1; }