guanghulab/skyeye/hibernation/sleep-scheduler.js

500 lines
15 KiB
JavaScript
Raw Normal View History

#!/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();
}