#!/usr/bin/env node /** * skyeye/hibernation/sleep-scheduler.js * ═══════════════════════════════════════════════ * 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 * 📜 Copyright: 国作登字-2026-A-00037559 * ═══════════════════════════════════════════════ * * 🧠 动态休眠决策引擎 * 根据当日/本周运行数据,动态计算休眠时长 * * 用法: * node sleep-scheduler.js --mode=daily → 计算日休眠时长 * node sleep-scheduler.js --mode=weekly → 计算周休眠时长 * * 输出: JSON 格式的休眠决策结果到 stdout */ 'use strict'; const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '../..'); const SKYEYE_DIR = path.resolve(__dirname, '..'); const GUARDS_DIR = path.join(SKYEYE_DIR, 'guards'); const LOGS_DIR = path.join(SKYEYE_DIR, 'logs'); const BUFFER_DIR = path.join(ROOT, 'buffer'); const CP_DIR = path.join(__dirname, 'checkpoints'); // ━━━ 工具函数 ━━━ function getTimestamp() { return new Date().toISOString(); } function getBeijingTime() { return new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }); } function getDateStr() { return new Date().toISOString().slice(0, 10).replace(/-/g, ''); } function loadJSON(filePath) { try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (e) { return null; } } function countFiles(dir, filter) { try { if (!fs.existsSync(dir)) return 0; const files = fs.readdirSync(dir); return filter ? files.filter(filter).length : files.length; } catch (e) { return 0; } } // ━━━ 日休眠评估因子 ━━━ const DAILY_EVALUATION_FACTORS = { base_minutes: 5, factors: [ { name: 'workflow_load', description: '今日 workflow 触发总次数', weight: { low: 0, // < 50 次 medium: 2, // 50-200 次 high: 5, // 200-500 次 extreme: 8 // > 500 次 } }, { name: 'guard_self_heal_count', description: '今日 Guard 自愈触发次数', weight: { zero: 0, low: 2, // 1-3 次 high: 5, // 4-10 次 critical: 10 // > 10 次 } }, { name: 'error_count', description: '今日异常/错误计数', weight: { zero: 0, low: 1, // 1-5 个 medium: 3, // 6-15 个 high: 8 // > 15 个 } }, { name: 'buffer_backlog', description: 'buffer 堆积量', weight: { empty: 0, normal: 1, // < 100 条 heavy: 3, // 100-500 条 overflow: 5 // > 500 条 } }, { name: 'quota_burn_rate', description: '今日配额消耗速度 vs 日均值', weight: { normal: 0, elevated: 2, // 高于均值 30% dangerous: 5 // 高于均值 60% } }, { name: 'checkin_anomaly', description: '小兵签到异常数', weight: { zero: 0, few: 1, // 1-2 个 many: 3 // > 2 个 } } ], max_minutes: 30, calculate: function (evaluatedFactors) { let total = DAILY_EVALUATION_FACTORS.base_minutes; for (const factor of evaluatedFactors) { total += factor.computed_weight; } return Math.min(total, DAILY_EVALUATION_FACTORS.max_minutes); } }; // ━━━ 周休眠评估因子 ━━━ const WEEKLY_EVALUATION_FACTORS = { base_hours: 2, factors: [ { name: 'weekly_health_trend', description: '本周 6 个 daily-checkpoint 的健康趋势', weight: { stable: 0, declining: 0.5, volatile: 1 } }, { name: 'optimization_requests', description: '本周自优化申请数量', weight: { few: 0, moderate: 0.5, many: 1 } }, { name: 'self_heal_total', description: '本周累计自愈次数', weight: { low: 0, medium: 0.5, high: 1 } }, { name: 'structural_changes', description: '本周是否有新应用接入/架构变更', weight: { none: 0, minor: 0.5, major: 1.5 } }, { name: 'upgrade_distribution_scope', description: '需要分发升级的小兵数量', weight: { few: 0, moderate: 0.5, many: 1 } }, { name: 'notion_sync_volume', description: 'Notion 端需要同步的变更量', weight: { light: 0, moderate: 0.5, heavy: 1 } } ], max_hours: 8, calculate: function (evaluatedFactors) { let total = WEEKLY_EVALUATION_FACTORS.base_hours; for (const factor of evaluatedFactors) { total += factor.computed_weight; } return Math.min(total, WEEKLY_EVALUATION_FACTORS.max_hours); } }; // ━━━ 数据采集 ━━━ function collectDailyData() { const data = { workflow_triggers: 0, guard_self_heals: 0, error_count: 0, buffer_backlog: 0, quota_burn_rate: 'normal', checkin_anomalies: 0 }; // 1. 读取今日日志目录统计 workflow 触发 const dailyLogDir = path.join(LOGS_DIR, 'daily'); if (fs.existsSync(dailyLogDir)) { const files = fs.readdirSync(dailyLogDir).filter(f => f.endsWith('.json')); data.workflow_triggers = files.length * 10; // 估算:每个日志文件约代表 10 次触发 } // 2. 读取 Guard 配置统计自愈次数 if (fs.existsSync(GUARDS_DIR)) { const guardFiles = fs.readdirSync(GUARDS_DIR) .filter(f => f.endsWith('.json') && f !== 'guard-template.json'); for (const file of guardFiles) { const guard = loadJSON(path.join(GUARDS_DIR, file)); if (guard && guard.health_check) { data.guard_self_heals += (guard.health_check.consecutive_failures || 0); } } } // 3. 统计 buffer 堆积量 const inboxDir = path.join(BUFFER_DIR, 'inbox'); if (fs.existsSync(inboxDir)) { try { const devDirs = fs.readdirSync(inboxDir).filter(d => { return fs.statSync(path.join(inboxDir, d)).isDirectory(); }); for (const dev of devDirs) { data.buffer_backlog += countFiles(path.join(inboxDir, dev), f => f.endsWith('.json')); } } catch (e) { // ignore } } // 4. 读取配额 ledger 判断消耗速度 const ledger = loadJSON(path.join(SKYEYE_DIR, 'quota-ledger.json')); if (ledger && ledger.services) { const actions = ledger.services.github_actions; if (actions) { const usedPct = actions.monthly_limit > 0 ? (actions.current_used || 0) / actions.monthly_limit * 100 : 0; if (usedPct > 60) data.quota_burn_rate = 'dangerous'; else if (usedPct > 30) data.quota_burn_rate = 'elevated'; } } return data; } function evaluateDailyFactors(data) { const results = []; // workflow_load let wl = 0; if (data.workflow_triggers > 500) wl = 8; else if (data.workflow_triggers > 200) wl = 5; else if (data.workflow_triggers >= 50) wl = 2; results.push({ name: 'workflow_load', value: data.workflow_triggers, level: wl > 5 ? 'extreme' : wl > 2 ? 'high' : wl > 0 ? 'medium' : 'low', computed_weight: wl }); // guard_self_heal_count let gh = 0; if (data.guard_self_heals > 10) gh = 10; else if (data.guard_self_heals >= 4) gh = 5; else if (data.guard_self_heals >= 1) gh = 2; results.push({ name: 'guard_self_heal_count', value: data.guard_self_heals, level: gh > 5 ? 'critical' : gh > 2 ? 'high' : gh > 0 ? 'low' : 'zero', computed_weight: gh }); // error_count let ec = 0; if (data.error_count > 15) ec = 8; else if (data.error_count >= 6) ec = 3; else if (data.error_count >= 1) ec = 1; results.push({ name: 'error_count', value: data.error_count, level: ec > 3 ? 'high' : ec > 1 ? 'medium' : ec > 0 ? 'low' : 'zero', computed_weight: ec }); // buffer_backlog let bb = 0; if (data.buffer_backlog > 500) bb = 5; else if (data.buffer_backlog >= 100) bb = 3; else if (data.buffer_backlog > 0) bb = 1; results.push({ name: 'buffer_backlog', value: data.buffer_backlog, level: bb > 3 ? 'overflow' : bb > 1 ? 'heavy' : bb > 0 ? 'normal' : 'empty', computed_weight: bb }); // quota_burn_rate let qb = 0; if (data.quota_burn_rate === 'dangerous') qb = 5; else if (data.quota_burn_rate === 'elevated') qb = 2; results.push({ name: 'quota_burn_rate', value: data.quota_burn_rate, level: data.quota_burn_rate, computed_weight: qb }); // checkin_anomaly let ca = 0; if (data.checkin_anomalies > 2) ca = 3; else if (data.checkin_anomalies >= 1) ca = 1; results.push({ name: 'checkin_anomaly', value: data.checkin_anomalies, level: ca > 1 ? 'many' : ca > 0 ? 'few' : 'zero', computed_weight: ca }); return results; } function collectWeeklyData() { const data = { health_trend: 'stable', optimization_requests: 0, self_heal_total: 0, structural_changes: 'none', upgrade_targets: 0, notion_sync_volume: 'light' }; // 读取本周 checkpoint if (fs.existsSync(CP_DIR)) { const cpFiles = fs.readdirSync(CP_DIR) .filter(f => f.startsWith('daily-cp-') && f.endsWith('.json')) .sort() .slice(-6); let totalHeals = 0; for (const file of cpFiles) { const cp = loadJSON(path.join(CP_DIR, file)); if (cp && cp.health_summary) { const guards = cp.health_summary.guards || {}; for (const g of Object.values(guards)) { totalHeals += (g.self_heals_today || 0); } } } data.self_heal_total = totalHeals; } // 统计子仓库数量(upgrade targets) const spokeDir = path.join(ROOT, 'spoke-deployments'); if (fs.existsSync(spokeDir)) { try { data.upgrade_targets = fs.readdirSync(spokeDir) .filter(d => fs.statSync(path.join(spokeDir, d)).isDirectory()).length; } catch (e) { // ignore } } // 读取 ASOP 请求(optimization_requests) const asopDir = path.join(ROOT, 'data', 'asop-requests'); if (fs.existsSync(asopDir)) { data.optimization_requests = countFiles(asopDir, f => f.endsWith('.json')); } return data; } function evaluateWeeklyFactors(data) { const results = []; // weekly_health_trend let ht = 0; if (data.health_trend === 'volatile') ht = 1; else if (data.health_trend === 'declining') ht = 0.5; results.push({ name: 'weekly_health_trend', value: data.health_trend, computed_weight: ht }); // optimization_requests let or_ = 0; if (data.optimization_requests > 10) or_ = 1; else if (data.optimization_requests >= 4) or_ = 0.5; results.push({ name: 'optimization_requests', value: data.optimization_requests, computed_weight: or_ }); // self_heal_total let sh = 0; if (data.self_heal_total > 30) sh = 1; else if (data.self_heal_total >= 10) sh = 0.5; results.push({ name: 'self_heal_total', value: data.self_heal_total, computed_weight: sh }); // structural_changes let sc = 0; if (data.structural_changes === 'major') sc = 1.5; else if (data.structural_changes === 'minor') sc = 0.5; results.push({ name: 'structural_changes', value: data.structural_changes, computed_weight: sc }); // upgrade_distribution_scope let ud = 0; if (data.upgrade_targets > 15) ud = 1; else if (data.upgrade_targets >= 5) ud = 0.5; results.push({ name: 'upgrade_distribution_scope', value: data.upgrade_targets, computed_weight: ud }); // notion_sync_volume let ns = 0; if (data.notion_sync_volume === 'heavy') ns = 1; else if (data.notion_sync_volume === 'moderate') ns = 0.5; results.push({ name: 'notion_sync_volume', value: data.notion_sync_volume, computed_weight: ns }); return results; } // ━━━ 主流程 ━━━ function run() { const args = process.argv.slice(2); const modeArg = args.find(a => a.startsWith('--mode=')); const mode = modeArg ? modeArg.split('=')[1] : 'daily'; console.log(`[SkyEye Sleep Scheduler] Mode: ${mode}`); console.log(`[SkyEye Sleep Scheduler] Timestamp: ${getTimestamp()}`); console.log(`[SkyEye Sleep Scheduler] Beijing Time: ${getBeijingTime()}`); let result; if (mode === 'weekly') { const data = collectWeeklyData(); const factors = evaluateWeeklyFactors(data); const totalHours = WEEKLY_EVALUATION_FACTORS.calculate(factors); const totalMinutes = Math.round(totalHours * 60); result = { scheduler_id: `SLEEP-WEEKLY-${getDateStr()}`, mode: 'weekly', timestamp: getTimestamp(), beijing_time: getBeijingTime(), raw_data: data, evaluated_factors: factors, decision: { base_hours: WEEKLY_EVALUATION_FACTORS.base_hours, additional_hours: totalHours - WEEKLY_EVALUATION_FACTORS.base_hours, total_hours: totalHours, total_minutes: totalMinutes, max_hours: WEEKLY_EVALUATION_FACTORS.max_hours, human_readable: `约 ${Math.floor(totalHours)} 小时 ${totalMinutes % 60} 分钟` } }; console.log(`[SkyEye Sleep Scheduler] Weekly hibernation decision: ${result.decision.human_readable}`); } else { const data = collectDailyData(); const factors = evaluateDailyFactors(data); const totalMinutes = DAILY_EVALUATION_FACTORS.calculate(factors); result = { scheduler_id: `SLEEP-DAILY-${getDateStr()}`, mode: 'daily', timestamp: getTimestamp(), beijing_time: getBeijingTime(), raw_data: data, evaluated_factors: factors, decision: { base_minutes: DAILY_EVALUATION_FACTORS.base_minutes, additional_minutes: totalMinutes - DAILY_EVALUATION_FACTORS.base_minutes, total_minutes: totalMinutes, max_minutes: DAILY_EVALUATION_FACTORS.max_minutes, human_readable: `约 ${totalMinutes} 分钟` } }; console.log(`[SkyEye Sleep Scheduler] Daily hibernation decision: ${result.decision.human_readable}`); } // 输出结果到 stdout 供下游脚本读取 const outputPath = path.join(__dirname, `sleep-decision-${mode}-${getDateStr()}.json`); fs.writeFileSync(outputPath, JSON.stringify(result, null, 2) + '\n', 'utf8'); console.log(`[SkyEye Sleep Scheduler] Decision written to: ${outputPath}`); // 输出关键结果到 stdout 方便 workflow 日志检索 console.log(`[RESULT] sleep_minutes=${result.decision.total_minutes || 0}`); console.log(`[RESULT] sleep_decision=${JSON.stringify(result.decision)}`); return result; } // ━━━ 导出供其他模块使用 ━━━ module.exports = { DAILY_EVALUATION_FACTORS, WEEKLY_EVALUATION_FACTORS, collectDailyData, evaluateDailyFactors, collectWeeklyData, evaluateWeeklyFactors, run }; if (require.main === module) { run(); }