#!/usr/bin/env node /** * ═══════════════════════════════════════════════════════════ * 训练实时仪表盘渲染器 · render-readme.js * ═══════════════════════════════════════════════════════════ * * 签发: 铸渊 · ICE-GL-ZY001 * 版权: 国作登字-2026-A-00037559 · TCS-0002∞ * * 读 data/training/state.json → 渲染 README.md 中 * ... * 之间的内容。 * * 不修改标记之外的任何内容。 * * 用法: * node scripts/training/render-readme.js # 写回 README.md * node scripts/training/render-readme.js --dry # 只输出,不写 */ 'use strict'; const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..', '..'); const STATE_PATH = path.join(ROOT, 'data', 'training', 'state.json'); const README_PATH = path.join(ROOT, 'README.md'); const START_MARK = ''; const END_MARK = ''; const PHASE_BADGE = { idle: '⚪ 等待启动', bootstrapping: '🛠️ 环境配置中', 'downloading-corpus': '📥 下载语料', preprocessing: '🔧 数据预处理', training: '🔥 训练进行中', evaluating: '📊 评估中', done: '✅ 训练完成', error: '❌ 异常' }; const HEALTH_BADGE = { ok: '🟢 正常', warning: '🟡 注意', error: '🔴 异常', idle: '⚪ 待命' }; function fmtNum(n, digits = 4) { if (n === null || n === undefined || Number.isNaN(n)) return '—'; if (typeof n !== 'number') return String(n); if (Number.isInteger(n)) return n.toLocaleString('en-US'); return Number(n).toFixed(digits); } function fmtDuration(seconds) { if (!seconds || seconds < 0) return '—'; const s = Math.floor(seconds); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const ss = s % 60; if (h > 0) return `${h}h ${m}m ${ss}s`; if (m > 0) return `${m}m ${ss}s`; return `${ss}s`; } function fmtTimestamp(iso) { if (!iso) return '—'; return iso.replace('T', ' ').replace(/\..*Z$/, ' UTC').replace(/Z$/, ' UTC'); } function progressBar(percent, width = 24) { const pct = Math.max(0, Math.min(100, Number(percent) || 0)); const filled = Math.round((pct / 100) * width); return '`' + '█'.repeat(filled) + '░'.repeat(width - filled) + '` ' + pct.toFixed(1) + '%'; } function renderHeader(state) { const phase = PHASE_BADGE[state.phase] || `⚪ ${state.phase}`; const health = HEALTH_BADGE[state.health?.status] || '⚪ unknown'; return [ '## 🔥 训练实时仪表盘 · ZY-TRAIN-001', '', `> ${state.task?.name || ''}`, `> **基座模型**: \`${state.task?.model_base || '—'}\` · **任务 ID**: \`${state.task?.id || '—'}\``, `> **最后更新**: ${fmtTimestamp(state.task?.updated_at)} · **副将心跳**: ${fmtTimestamp(state.health?.last_heartbeat_at)}`, '', `| 当前阶段 | 健康 | 训练用时 |`, `|---------|------|---------|`, `| **${phase}** · ${state.phase_label || ''} | **${health}** · ${state.health?.message || ''} | ${fmtDuration(state.progress?.elapsed_seconds)} |`, '' ].join('\n'); } function renderProgress(state) { const p = state.progress || {}; const pct = p.total_steps > 0 ? (p.step / p.total_steps) * 100 : (p.percent || 0); return [ '### 📈 训练进度', '', progressBar(pct), '', `| 指标 | 值 |`, `|------|----|`, `| Step | **${fmtNum(p.step)} / ${fmtNum(p.total_steps)}** |`, `| Epoch | ${fmtNum(p.epoch)} / ${fmtNum(p.total_epochs)} |`, `| Loss | ${fmtNum(p.loss)} |`, `| Learning Rate | ${fmtNum(p.learning_rate, 6)} |`, `| 吞吐 | ${p.throughput_samples_per_sec ? fmtNum(p.throughput_samples_per_sec, 2) + ' samples/s' : '—'} |`, `| 预估剩余 | ${fmtDuration(p.eta_seconds)} |`, '' ].join('\n'); } function renderGpu(state) { const m = state.gpu_metrics || {}; if (!Array.isArray(m.devices) || m.devices.length === 0) { return [ '### 🎮 GPU 状态', '', `> 暂无 GPU 实时数据 · 等待 progress-reporter 心跳。`, '' ].join('\n'); } const lines = [ '### 🎮 GPU 状态', '', `> 采样时间: ${fmtTimestamp(m.snapshot_at)}`, '', `| GPU | 利用率 | 显存 | 温度 | 功率 |`, `|-----|--------|------|------|------|` ]; for (const d of m.devices) { lines.push( `| ${d.index ?? '?'} · ${d.name || 'V100'} | ${d.util_percent ?? '—'}% | ${d.memory_used_mib ?? '—'} / ${d.memory_total_mib ?? '—'} MiB | ${d.temperature_c ?? '—'}°C | ${d.power_w ?? '—'} W |` ); } lines.push(''); return lines.join('\n'); } function renderServer(state) { const s = state.server || {}; const c = state.cos || {}; return [ '### 🖥️ 服务器 · COS', '', `| 项 | 值 |`, `|----|----|`, `| 实例 | \`${s.instance_name || '—'}\` · ${s.instance_type || '—'} |`, `| 公网 IP | \`${s.host || '—'}\` |`, `| 地域 | ${s.region || '—'} |`, `| GPU | ${s.gpu || '—'} |`, `| 系统 | ${s.os || '—'} · CUDA ${s.cuda || '—'} · cuDNN ${s.cudnn || '—'} |`, `| COS 桶 | \`${c.bucket || '—'}\` (${c.region || '—'}) |`, `| 语料文件 | raw=${fmtNum(c.raw_files)} · processed=${fmtNum(c.processed_files)} · checkpoints=${fmtNum(c.checkpoints)} |`, '' ].join('\n'); } function renderTimeline(state) { const tl = Array.isArray(state.timeline) ? state.timeline : []; const recent = tl.slice(-10).reverse(); const lines = [ '### 🕐 最近事件(最新 10 条)', '' ]; if (recent.length === 0) { lines.push('> 暂无事件。', ''); return lines.join('\n'); } lines.push('| 时间 (UTC) | 阶段 | 等级 | 事件 |', '|-----------|------|------|------|'); for (const e of recent) { const lvl = e.level === 'error' ? '🔴' : e.level === 'warning' ? '🟡' : 'ℹ️'; // 转义 Markdown 表格元字符: 先转义反斜杠,再转义管道,最后压成单行 const msg = String(e.message || '') .replace(/\\/g, '\\\\') .replace(/\|/g, '\\|') .replace(/[\r\n]+/g, ' '); lines.push(`| ${fmtTimestamp(e.at)} | ${e.phase || '—'} | ${lvl} | ${msg} |`); } lines.push(''); return lines.join('\n'); } function renderAsk() { return [ '### 💬 问铸渊副将', '', '> 冰朔可以**直接打开 Issue 问副将**,副将会读 `data/training/state.json` 并回答你「训练正不正常」「卡在哪里」「下一步该做什么」。', '', '📌 **[点击向副将提问 →](../../issues/new?template=ask-zhuyuan-training.md)**', '', '> 留言带 `deputy-message-board` 标签即可触发副将自动回复(已有 LLM 多模型降级链)。', '' ].join('\n'); } function renderDashboard(state) { const updated = state.task?.updated_at ? `· 更新: ${fmtTimestamp(state.task.updated_at)}` : ''; return [ `_铸渊副将守护 · ZY-DEPUTY-001 · 服务器动他也动 ${updated}_`, '', renderHeader(state), renderProgress(state), renderGpu(state), renderServer(state), renderTimeline(state), renderAsk() ].join('\n'); } function spliceMarkers(readme, body) { const startIdx = readme.indexOf(START_MARK); const endIdx = readme.indexOf(END_MARK); if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) { throw new Error(`README 中未找到训练仪表盘标记 ${START_MARK} / ${END_MARK}`); } const before = readme.slice(0, startIdx + START_MARK.length); const after = readme.slice(endIdx); return `${before}\n\n${body}\n${after}`; } function main() { const dry = process.argv.includes('--dry'); const state = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')); const body = renderDashboard(state); if (dry) { process.stdout.write(body + '\n'); return; } const readme = fs.readFileSync(README_PATH, 'utf8'); const next = spliceMarkers(readme, body); if (next === readme) { console.log('[render-readme] no change'); return; } fs.writeFileSync(README_PATH, next); console.log('[render-readme] README.md updated'); } if (require.main === module) { try { main(); } catch (err) { console.error('[render-readme] error:', err.message); process.exit(1); } } module.exports = { renderDashboard, spliceMarkers };