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