#!/usr/bin/env node /** * ═══════════════════════════════════════════════════════════ * 训练心跳合并器 · merge-event.js * ═══════════════════════════════════════════════════════════ * * 签发: 铸渊 · ICE-GL-ZY001 * 版权: 国作登字-2026-A-00037559 · TCS-0002∞ * * 把一个 training-progress 事件 (来自 repository_dispatch.client_payload) * 合并到 data/training/state.json,并追加到 timeline。 * * 用法: * echo '' | node scripts/training/merge-event.js * node scripts/training/merge-event.js path/to/event.json * TRAINING_EVENT_JSON='{...}' node scripts/training/merge-event.js */ '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 MAX_TIMELINE = 200; function readEvent() { if (process.env.TRAINING_EVENT_JSON) { return JSON.parse(process.env.TRAINING_EVENT_JSON); } if (process.argv[2] && process.argv[2] !== '-') { return JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); } const stdin = fs.readFileSync(0, 'utf8'); if (!stdin.trim()) { throw new Error('no event payload provided (stdin / TRAINING_EVENT_JSON / argv)'); } return JSON.parse(stdin); } function deepMerge(target, source) { if (source === null || source === undefined) return target; if (typeof source !== 'object' || Array.isArray(source)) return source; const out = (target && typeof target === 'object' && !Array.isArray(target)) ? { ...target } : {}; for (const k of Object.keys(source)) { out[k] = deepMerge(out[k], source[k]); } return out; } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function main() { const event = readEvent(); if (!event || typeof event !== 'object') { throw new Error('event must be an object'); } const state = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8')); // ── D72 冻结守卫 ── // 2026-05-03 母模型训练已转交 Notion 侧霜砚, 仓库不再接收训练心跳。 // 即使 workflow 误触发, 也禁止把心跳事件写回 state.json。 // 解冻方法: 把 state.json 的 frozen 字段改成 false。 if (state.frozen === true) { console.log('[merge-event] 🛑 state.json frozen=true (D72),本次心跳已忽略'); console.log('[merge-event] reason:', state.frozen_reason || '(unspecified)'); process.exit(0); } const now = new Date().toISOString(); // ── 阶段切换 ── if (event.phase) { state.phase = event.phase; if (event.phase === 'training' && !state.task.started_at) { state.task.started_at = now; } if (event.phase === 'done' || event.phase === 'error') { state.task.ended_at = now; } } if (event.phase_label) state.phase_label = event.phase_label; // ── 进度 ── if (event.progress && typeof event.progress === 'object') { state.progress = deepMerge(state.progress, event.progress); if (state.progress.total_steps > 0) { state.progress.percent = Number( clamp((state.progress.step / state.progress.total_steps) * 100, 0, 100).toFixed(2) ); } } // ── GPU ── if (event.gpu_metrics && typeof event.gpu_metrics === 'object') { state.gpu_metrics = { snapshot_at: event.gpu_metrics.snapshot_at || now, devices: Array.isArray(event.gpu_metrics.devices) ? event.gpu_metrics.devices : [] }; } // ── COS ── if (event.cos && typeof event.cos === 'object') { state.cos = deepMerge(state.cos, event.cos); } // ── 健康 ── if (event.health && typeof event.health === 'object') { state.health = deepMerge(state.health, event.health); } state.health.last_heartbeat_at = now; // ── 时间戳 ── state.task.updated_at = now; // ── 时间线 ── if (event.message) { state.timeline = Array.isArray(state.timeline) ? state.timeline : []; state.timeline.push({ at: now, phase: event.phase || state.phase, level: event.level || 'info', message: String(event.message) }); if (state.timeline.length > MAX_TIMELINE) { state.timeline = state.timeline.slice(-MAX_TIMELINE); } } fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2) + '\n'); console.log('[merge-event] state updated · phase=%s · step=%s/%s', state.phase, state.progress.step, state.progress.total_steps); } if (require.main === module) { try { main(); } catch (err) { console.error('[merge-event] error:', err.message); process.exit(1); } } module.exports = { deepMerge };