149 lines
4.8 KiB
JavaScript
149 lines
4.8 KiB
JavaScript
|
|
#!/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 '<json>' | 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 };
|