234 lines
7.9 KiB
JavaScript
234 lines
7.9 KiB
JavaScript
|
|
// scripts/skyeye/asop-reviewer.js
|
|||
|
|
// 天眼·ASOP 自优化审批引擎
|
|||
|
|
//
|
|||
|
|
// 天眼每日 06:00 运行时扫描 data/asop-requests/pending/
|
|||
|
|
// 按 ASOP 三级边界审批:GL1 自主 / GL2 天眼审批 / GL3 升级冰朔
|
|||
|
|
//
|
|||
|
|
// 审批标准:
|
|||
|
|
// 1. 申请是否在该 Workflow 职责范围内?
|
|||
|
|
// 2. 优化后是否不影响其他 Workflow 和系统整体架构?
|
|||
|
|
// 3. 优化理由是否有数据支撑?
|
|||
|
|
// 4. 是否违反核心不可变区?
|
|||
|
|
// 5. GL3 级别 → 自动升级到冰朔
|
|||
|
|
//
|
|||
|
|
// 输出:审批结果 JSON → stdout
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const ROOT = path.resolve(__dirname, '../..');
|
|||
|
|
const ASOP_DIR = path.join(ROOT, 'data/asop-requests');
|
|||
|
|
const PENDING_DIR = path.join(ASOP_DIR, 'pending');
|
|||
|
|
const APPROVED_DIR = path.join(ASOP_DIR, 'approved');
|
|||
|
|
const REJECTED_DIR = path.join(ASOP_DIR, 'rejected');
|
|||
|
|
|
|||
|
|
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
|
|||
|
|
|
|||
|
|
// ━━━ 核心不可变区(仓库侧) ━━━
|
|||
|
|
const IMMUTABLE_FILES = [
|
|||
|
|
'.github/workflows/zhuyuan-skyeye.yml',
|
|||
|
|
'scripts/skyeye/diagnose.js',
|
|||
|
|
'.github/persona-brain/routing-map.json'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const IMMUTABLE_CONCEPTS = [
|
|||
|
|
'whitelist',
|
|||
|
|
'persona_id_format',
|
|||
|
|
'skyeye_report_schema',
|
|||
|
|
'secrets_key_names'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// ━━━ 30天变更累计上限 ━━━
|
|||
|
|
const MAX_GL2_PER_WORKFLOW_30D = 3;
|
|||
|
|
|
|||
|
|
// ━━━ 审批阈值 ━━━
|
|||
|
|
const MIN_EVIDENCE_LENGTH = 10;
|
|||
|
|
const MAX_AFFECTED_WORKFLOWS_GL2 = 2;
|
|||
|
|
const MIN_ROLLBACK_PLAN_LENGTH = 5;
|
|||
|
|
|
|||
|
|
// ━━━ 安全读取 JSON ━━━
|
|||
|
|
function readJSON(filePath) {
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|||
|
|
} catch (e) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 移动申请文件到目标目录 ━━━
|
|||
|
|
function moveRequest(requestId, fromDir, toDir, decision) {
|
|||
|
|
const srcFile = path.join(fromDir, `${requestId}.json`);
|
|||
|
|
if (!fs.existsSync(srcFile)) return;
|
|||
|
|
|
|||
|
|
const request = readJSON(srcFile);
|
|||
|
|
if (!request) return;
|
|||
|
|
|
|||
|
|
request.decision = {
|
|||
|
|
result: decision.result,
|
|||
|
|
reason: decision.reason,
|
|||
|
|
reviewed_at: new Date(Date.now() + BEIJING_OFFSET_MS).toISOString().replace('T', ' ').slice(0, 19) + '+08:00',
|
|||
|
|
reviewer: '🦅 天眼·ASOP审批引擎'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (decision.execution_steps) {
|
|||
|
|
request.decision.execution_steps = decision.execution_steps;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fs.mkdirSync(toDir, { recursive: true });
|
|||
|
|
fs.writeFileSync(path.join(toDir, `${requestId}.json`), JSON.stringify(request, null, 2) + '\n');
|
|||
|
|
fs.unlinkSync(srcFile);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 统计 30 天内某 Workflow 已批准的 GL2 数量 ━━━
|
|||
|
|
function countRecentApprovals(workflow) {
|
|||
|
|
const dirs = [APPROVED_DIR, path.join(ASOP_DIR, 'executed'), path.join(ASOP_DIR, 'verified')];
|
|||
|
|
const thirtyDaysAgo = Date.now() - 30 * 24 * 3600 * 1000;
|
|||
|
|
let count = 0;
|
|||
|
|
|
|||
|
|
for (const dir of dirs) {
|
|||
|
|
if (!fs.existsSync(dir)) continue;
|
|||
|
|
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json') && f !== '.gitkeep');
|
|||
|
|
for (const file of files) {
|
|||
|
|
const req = readJSON(path.join(dir, file));
|
|||
|
|
if (!req) continue;
|
|||
|
|
if (req.requester && req.requester.workflow === workflow && req.level === 'GL2') {
|
|||
|
|
const ts = req.timestamp ? new Date(req.timestamp.replace('+08:00', '+0800')).getTime() : 0;
|
|||
|
|
if (ts > thirtyDaysAgo) count++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return count;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 审批单个申请 ━━━
|
|||
|
|
function evaluateRequest(req) {
|
|||
|
|
// GL3 → 自动升级到冰朔
|
|||
|
|
if (req.level === 'GL3') {
|
|||
|
|
return { result: 'escalate', reason: 'GL3 级别需冰朔审批' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 核心不可变区检查
|
|||
|
|
const affectedFiles = (req.impact_assessment && req.impact_assessment.affected_files) || [];
|
|||
|
|
const touchesImmutable = affectedFiles.some(f => IMMUTABLE_FILES.includes(f));
|
|||
|
|
if (touchesImmutable) {
|
|||
|
|
return { result: 'escalate', reason: '涉及核心不可变区文件,自动升级到冰朔审批' };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 数据支撑检查
|
|||
|
|
if (!req.data_evidence || req.data_evidence.length < MIN_EVIDENCE_LENGTH) {
|
|||
|
|
return { result: 'rejected', reason: `缺少数据支撑(evidence 不足 ${MIN_EVIDENCE_LENGTH} 字符),请提供具体证据` };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 影响范围检查:超过阈值个 Workflow → 升级
|
|||
|
|
const affectedWorkflows = (req.impact_assessment && req.impact_assessment.affected_workflows) || [];
|
|||
|
|
if (affectedWorkflows.length > MAX_AFFECTED_WORKFLOWS_GL2) {
|
|||
|
|
return { result: 'escalate', reason: `影响超过 ${MAX_AFFECTED_WORKFLOWS_GL2} 个 Workflow(${affectedWorkflows.length} 个),需冰朔评估` };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 30 天变更累计检查
|
|||
|
|
const workflow = req.requester && req.requester.workflow;
|
|||
|
|
if (workflow) {
|
|||
|
|
const recentCount = countRecentApprovals(workflow);
|
|||
|
|
if (recentCount >= MAX_GL2_PER_WORKFLOW_30D) {
|
|||
|
|
return {
|
|||
|
|
result: 'escalate',
|
|||
|
|
reason: `${workflow} 在 30 天内已有 ${recentCount} 次 GL2 变更(上限 ${MAX_GL2_PER_WORKFLOW_30D}),自动冻结,升级冰朔审查`
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 回滚计划检查
|
|||
|
|
if (!req.rollback_plan || req.rollback_plan.length < MIN_ROLLBACK_PLAN_LENGTH) {
|
|||
|
|
return { result: 'rejected', reason: `缺少回滚计划(rollback_plan 不足 ${MIN_ROLLBACK_PLAN_LENGTH} 字符),请补充回退方案` };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 通过所有检查 → 批准
|
|||
|
|
return {
|
|||
|
|
result: 'approved',
|
|||
|
|
reason: '申请合理 · 影响可控 · 数据充分 · 回滚计划完整'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 主审批流程 ━━━
|
|||
|
|
function reviewAll() {
|
|||
|
|
console.log('🔄 天眼·ASOP 审批引擎启动');
|
|||
|
|
console.log('═══════════════════════════════════════════\n');
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(PENDING_DIR)) {
|
|||
|
|
console.log('ℹ️ pending/ 目录不存在,无待审批申请');
|
|||
|
|
const result = { total: 0, approved: 0, rejected: 0, escalated: 0 };
|
|||
|
|
console.log(JSON.stringify(result, null, 2));
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const files = fs.readdirSync(PENDING_DIR)
|
|||
|
|
.filter(f => f.endsWith('.json') && f !== '.gitkeep');
|
|||
|
|
|
|||
|
|
if (files.length === 0) {
|
|||
|
|
console.log('🔄 ASOP:无待审批申请');
|
|||
|
|
const result = { total: 0, approved: 0, rejected: 0, escalated: 0 };
|
|||
|
|
console.log(JSON.stringify(result, null, 2));
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`🔄 ASOP:发现 ${files.length} 条待审批申请\n`);
|
|||
|
|
|
|||
|
|
const summary = { total: files.length, approved: 0, rejected: 0, escalated: 0, decisions: [] };
|
|||
|
|
|
|||
|
|
for (const file of files) {
|
|||
|
|
const filePath = path.join(PENDING_DIR, file);
|
|||
|
|
const req = readJSON(filePath);
|
|||
|
|
if (!req) {
|
|||
|
|
console.log(`⚠️ 无法解析 ${file},跳过`);
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const decision = evaluateRequest(req);
|
|||
|
|
const requestId = req.request_id || file.replace('.json', '');
|
|||
|
|
|
|||
|
|
switch (decision.result) {
|
|||
|
|
case 'approved':
|
|||
|
|
moveRequest(requestId, PENDING_DIR, APPROVED_DIR, decision);
|
|||
|
|
summary.approved++;
|
|||
|
|
console.log(`✅ ASOP 批准:${requestId} — ${decision.reason}`);
|
|||
|
|
break;
|
|||
|
|
case 'rejected':
|
|||
|
|
moveRequest(requestId, PENDING_DIR, REJECTED_DIR, decision);
|
|||
|
|
summary.rejected++;
|
|||
|
|
console.log(`❌ ASOP 拒绝:${requestId} — ${decision.reason}`);
|
|||
|
|
break;
|
|||
|
|
case 'escalate':
|
|||
|
|
// 升级的申请保留在 pending/ 等冰朔处理,标记已升级
|
|||
|
|
req.escalated = true;
|
|||
|
|
req.escalate_reason = decision.reason;
|
|||
|
|
fs.writeFileSync(filePath, JSON.stringify(req, null, 2) + '\n');
|
|||
|
|
summary.escalated++;
|
|||
|
|
console.log(`⬆️ ASOP 升级:${requestId} — ${decision.reason}`);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
summary.decisions.push({
|
|||
|
|
request_id: requestId,
|
|||
|
|
level: req.level,
|
|||
|
|
workflow: req.requester ? req.requester.workflow : '',
|
|||
|
|
result: decision.result,
|
|||
|
|
reason: decision.reason
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`\n📊 审批结果:批准 ${summary.approved} / 拒绝 ${summary.rejected} / 升级 ${summary.escalated}`);
|
|||
|
|
console.log(JSON.stringify(summary, null, 2));
|
|||
|
|
return summary;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ━━━ 导出 ━━━
|
|||
|
|
module.exports = { reviewAll, evaluateRequest };
|
|||
|
|
|
|||
|
|
// ━━━ 直接运行 ━━━
|
|||
|
|
if (require.main === module) {
|
|||
|
|
reviewAll();
|
|||
|
|
}
|