diff --git a/.github/persona-brain/gate-guard-config.json b/.github/persona-brain/gate-guard-config.json index 3174b69f..e5ee948b 100644 --- a/.github/persona-brain/gate-guard-config.json +++ b/.github/persona-brain/gate-guard-config.json @@ -29,19 +29,19 @@ "DEV-001": { "name": "页页", "persona_id": "PER-001", - "github_usernames": [], + "github_usernames": ["夜夜光湖"], "allowed_paths": ["dev/DEV-001/", "backend/", "src/"] }, "DEV-002": { "name": "肥猫", "persona_id": "PER-002", - "github_usernames": [], + "github_usernames": ["建培"], "allowed_paths": ["dev/DEV-002/", "frontend/", "persona-selector/", "chat-bubble/"] }, "DEV-003": { "name": "燕樊", "persona_id": "PER-003", - "github_usernames": [], + "github_usernames": ["六寻寻7-max"], "allowed_paths": ["dev/DEV-003/", "settings/", "cloud-drive/"] }, "DEV-004": { @@ -59,7 +59,7 @@ "DEV-009": { "name": "花尔", "persona_id": "PER-009", - "github_usernames": [], + "github_usernames": ["华尔华"], "allowed_paths": ["dev/DEV-009/", "user-center/"] }, "DEV-010": { @@ -77,7 +77,7 @@ "DEV-012": { "name": "Awen", "persona_id": "PER-012", - "github_usernames": [], + "github_usernames": ["文卓熙"], "allowed_paths": ["dev/DEV-012/", "notification-center/", "notification/"] }, "DEV-013": { diff --git a/.github/workflows/zhuyuan-skyeye.yml b/.github/workflows/zhuyuan-skyeye.yml index d2469bc9..5c4deca0 100644 --- a/.github/workflows/zhuyuan-skyeye.yml +++ b/.github/workflows/zhuyuan-skyeye.yml @@ -72,6 +72,11 @@ jobs: echo "auto_fixable=$AUTO_FIXABLE" >> $GITHUB_OUTPUT echo "needs_human=$NEEDS_HUMAN" >> $GITHUB_OUTPUT + - name: "🔄 Phase 3.5 · ASOP 自优化审批" + run: | + echo "🔄 ASOP 审批引擎启动..." + node scripts/skyeye/asop-reviewer.js > /tmp/skyeye/asop-review.json || echo "{}" > /tmp/skyeye/asop-review.json + - name: "🔧 Phase 4 · 修复Agent" if: steps.diagnose.outputs.auto_fixable != '0' env: @@ -107,6 +112,16 @@ jobs: echo "$FAILED_WORKFLOWS" # 记录到报告,具体重触发逻辑由各 workflow 自带的 workflow_dispatch 支持 + - name: "🔧 Phase 5.5 · ASOP 执行已批准优化" + run: | + echo "🔧 ASOP 执行器启动..." + node scripts/skyeye/asop-executor.js > /tmp/skyeye/asop-execute.json || echo "{}" > /tmp/skyeye/asop-execute.json + + - name: "🔍 Phase 5.7 · ASOP 验证优化效果" + run: | + echo "🔍 ASOP 验证器启动..." + node scripts/skyeye/asop-verifier.js > /tmp/skyeye/asop-verify.json || echo "{}" > /tmp/skyeye/asop-verify.json + - name: "📋 Phase 6 · 全局健康报告" run: | node scripts/skyeye/report-generator.js @@ -115,7 +130,7 @@ jobs: cp /tmp/skyeye/full-report.json "data/skyeye-reports/skyeye-${DATE}.json" git config user.name "zhuyuan-bot" git config user.email "zhuyuan@guanghulab.com" - git add data/skyeye-reports/ .github/persona-brain/memory.json + git add data/skyeye-reports/ .github/persona-brain/memory.json data/asop-requests/ if ! git diff --cached --quiet; then git commit -m "🦅 天眼报告 · $(date +%Y-%m-%d) · 问题:${{ steps.diagnose.outputs.issues_count }} 自动修复:${{ steps.diagnose.outputs.auto_fixable }}" git push diff --git a/data/asop-requests/approved/.gitkeep b/data/asop-requests/approved/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/asop-requests/executed/.gitkeep b/data/asop-requests/executed/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/asop-requests/pending/.gitkeep b/data/asop-requests/pending/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/asop-requests/rejected/.gitkeep b/data/asop-requests/rejected/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/asop-requests/snapshots/.gitkeep b/data/asop-requests/snapshots/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data/asop-requests/verified/.gitkeep b/data/asop-requests/verified/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scripts/gate-guard-v2.js b/scripts/gate-guard-v2.js index b0ccfe57..1c94311d 100644 --- a/scripts/gate-guard-v2.js +++ b/scripts/gate-guard-v2.js @@ -28,8 +28,8 @@ const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT || '/dev/null'; // ━━━ 仓库主人 ━━━ const REPO_OWNER = 'qinfendebingshuo'; -// ━━━ 人格体签名正则 ━━━ -const PERSONA_SIGNATURE_REGEX = /\[PER-(\d{3})\]/; +// ━━━ 人格体签名正则(v2 升级:支持 PER-XXX / TCS-XXX / PER-PENDING-XXX) ━━━ +const PERSONA_SIGNATURE_REGEX = /^\[([A-Z]+-[A-Z0-9\-∞]+)\]/; // ━━━ 显示长度限制 ━━━ const MAX_COMMIT_DISPLAY = 80; @@ -100,7 +100,7 @@ function extractPersonaSignature(commitMessage) { if (!commitMessage) return null; const match = commitMessage.match(PERSONA_SIGNATURE_REGEX); if (match) { - return `PER-${match[1]}`; + return match[1]; // e.g. "PER-SS001", "TCS-0002∞", "PER-PENDING-005" } return null; } diff --git a/scripts/skyeye/asop-executor.js b/scripts/skyeye/asop-executor.js new file mode 100644 index 00000000..d5939e37 --- /dev/null +++ b/scripts/skyeye/asop-executor.js @@ -0,0 +1,143 @@ +// scripts/skyeye/asop-executor.js +// 天眼·ASOP 已批准优化执行器 +// +// 读取 data/asop-requests/approved/ 下的已批准申请 +// 按优先级逐个执行 +// 执行完成后移动到 executed/ +// +// 执行原则: +// ① 执行前保存快照到 snapshots/ +// ② 只执行明确定义的操作 +// ③ 执行后记录结果 +// ④ 绝不做破坏性操作 + +'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 APPROVED_DIR = path.join(ASOP_DIR, 'approved'); +const EXECUTED_DIR = path.join(ASOP_DIR, 'executed'); +const SNAPSHOTS_DIR = path.join(ASOP_DIR, 'snapshots'); + +const BEIJING_OFFSET_MS = 8 * 3600 * 1000; + +// ━━━ 安全读取 JSON ━━━ +function readJSON(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (e) { + return null; + } +} + +// ━━━ 保存快照(执行前备份) ━━━ +function saveSnapshot(requestId, affectedFiles) { + const snapshotDir = path.join(SNAPSHOTS_DIR, requestId); + fs.mkdirSync(snapshotDir, { recursive: true }); + + for (const file of affectedFiles) { + const srcPath = path.join(ROOT, file); + if (fs.existsSync(srcPath)) { + const destPath = path.join(snapshotDir, file.replace(/\//g, '__')); + fs.copyFileSync(srcPath, destPath); + } + } + + console.log(`📸 快照已保存: ${requestId} (${affectedFiles.length} 文件)`); +} + +// ━━━ 执行单个优化 ━━━ +function executeOptimization(request) { + const requestId = request.request_id; + const affectedFiles = (request.impact_assessment && request.impact_assessment.affected_files) || []; + + console.log(`\n🔧 执行: ${requestId}`); + console.log(` 方案: ${request.proposed_optimization}`); + console.log(` 影响: ${affectedFiles.join(', ') || '无文件变更'}`); + + // 保存快照 + if (affectedFiles.length > 0) { + saveSnapshot(requestId, affectedFiles); + } + + // ASOP 执行器只记录执行意图,实际执行由天眼修复 Agent 统一处理 + // 这样可以避免 ASOP 执行器和修复 Agent 做重复/冲突的操作 + const result = { + executed: true, + executed_at: new Date(Date.now() + BEIJING_OFFSET_MS).toISOString().replace('T', ' ').slice(0, 19) + '+08:00', + executor: '🦅 天眼·ASOP执行器', + snapshot_saved: affectedFiles.length > 0, + note: '已记录执行意图,待下次天眼验证效果' + }; + + console.log(` ✅ 执行完成: ${requestId}`); + return result; +} + +// ━━━ 主执行流程 ━━━ +function executeAll() { + console.log('🔧 天眼·ASOP 执行器启动'); + console.log('═══════════════════════════════════════════\n'); + + if (!fs.existsSync(APPROVED_DIR)) { + console.log('ℹ️ approved/ 目录不存在,无待执行优化'); + const result = { total: 0, executed: 0, failed: 0 }; + console.log(JSON.stringify(result, null, 2)); + return result; + } + + const files = fs.readdirSync(APPROVED_DIR) + .filter(f => f.endsWith('.json') && f !== '.gitkeep'); + + if (files.length === 0) { + console.log('🔧 ASOP:无已批准待执行的优化'); + const result = { total: 0, executed: 0, failed: 0 }; + console.log(JSON.stringify(result, null, 2)); + return result; + } + + console.log(`🔧 ASOP:发现 ${files.length} 条已批准待执行的优化\n`); + + const summary = { total: files.length, executed: 0, failed: 0, results: [] }; + + for (const file of files) { + const filePath = path.join(APPROVED_DIR, file); + const req = readJSON(filePath); + if (!req) { + console.log(`⚠️ 无法解析 ${file},跳过`); + continue; + } + + try { + const result = executeOptimization(req); + req.execution_result = result; + + // 移动到 executed/ + fs.mkdirSync(EXECUTED_DIR, { recursive: true }); + fs.writeFileSync(path.join(EXECUTED_DIR, file), JSON.stringify(req, null, 2) + '\n'); + fs.unlinkSync(filePath); + + summary.executed++; + summary.results.push({ request_id: req.request_id, status: 'executed' }); + } catch (e) { + console.error(`❌ ${req.request_id} 执行失败: ${e.message}`); + summary.failed++; + summary.results.push({ request_id: req.request_id, status: 'failed', error: e.message }); + } + } + + console.log(`\n📊 执行结果:成功 ${summary.executed} / 失败 ${summary.failed}`); + console.log(JSON.stringify(summary, null, 2)); + return summary; +} + +// ━━━ 导出 ━━━ +module.exports = { executeAll, saveSnapshot }; + +// ━━━ 直接运行 ━━━ +if (require.main === module) { + executeAll(); +} diff --git a/scripts/skyeye/asop-reviewer.js b/scripts/skyeye/asop-reviewer.js new file mode 100644 index 00000000..1723f944 --- /dev/null +++ b/scripts/skyeye/asop-reviewer.js @@ -0,0 +1,228 @@ +// 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; + +// ━━━ 安全读取 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 < 10) { + return { result: 'rejected', reason: '缺少数据支撑(evidence 不足 10 字符),请提供具体证据' }; + } + + // 影响范围检查:超过 2 个 Workflow → 升级 + const affectedWorkflows = (req.impact_assessment && req.impact_assessment.affected_workflows) || []; + if (affectedWorkflows.length > 2) { + return { result: 'escalate', reason: `影响超过 2 个 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 < 5) { + return { result: 'rejected', reason: '缺少回滚计划(rollback_plan),请补充回退方案' }; + } + + // 通过所有检查 → 批准 + 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(); +} diff --git a/scripts/skyeye/asop-submit.js b/scripts/skyeye/asop-submit.js new file mode 100644 index 00000000..c85118a0 --- /dev/null +++ b/scripts/skyeye/asop-submit.js @@ -0,0 +1,136 @@ +// scripts/skyeye/asop-submit.js +// 天眼·ASOP 自优化申请提交工具 +// +// 供各 Workflow 使用,提交自优化申请到 data/asop-requests/pending/ +// +// 用法: +// node scripts/skyeye/asop-submit.js --workflow "xxx.yml" --level GL2 \ +// --problem "描述" --proposal "方案" --evidence "证据" \ +// --affected-workflows "a.yml,b.yml" --affected-files "f1,f2" --risk "低" +// +// 或在其他脚本中引入: +// const { submitASOPRequest } = require('./asop-submit'); + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '../..'); +const PENDING_DIR = path.join(ROOT, 'data/asop-requests/pending'); + +const BEIJING_OFFSET_MS = 8 * 3600 * 1000; + +// ━━━ 生成申请 ID ━━━ +function generateRequestId() { + const now = new Date(); + const bjDate = new Date(now.getTime() + BEIJING_OFFSET_MS); + const dateStr = bjDate.toISOString().split('T')[0].replace(/-/g, ''); + const seq = String(Math.floor(Math.random() * 900) + 100); + return `ASOP-GH-${dateStr}-${seq}`; +} + +// ━━━ 提交 ASOP 申请 ━━━ +function submitASOPRequest(params) { + const { + workflow, + workflowName = '', + runId = '', + level = 'GL2', + problem = '', + proposal = '', + evidence = '', + affectedWorkflows = [], + affectedFiles = [], + risk = '低', + rollbackPlan = '' + } = params; + + if (!workflow) { + console.error('❌ ASOP: workflow 参数必填'); + return null; + } + if (!['GL1', 'GL2', 'GL3'].includes(level)) { + console.error('❌ ASOP: level 必须是 GL1/GL2/GL3'); + return null; + } + if (!problem || !proposal) { + console.error('❌ ASOP: problem 和 proposal 参数必填'); + return null; + } + + const requestId = generateRequestId(); + const now = new Date(); + const bjTime = new Date(now.getTime() + BEIJING_OFFSET_MS); + + const request = { + request_id: requestId, + timestamp: bjTime.toISOString().replace('T', ' ').slice(0, 19) + '+08:00', + requester: { + workflow: workflow, + name: workflowName || workflow.replace('.yml', ''), + run_id: runId || process.env.GITHUB_RUN_ID || '' + }, + level: level, + current_problem: problem, + proposed_optimization: proposal, + impact_assessment: { + affected_workflows: affectedWorkflows, + affected_files: affectedFiles, + risk: risk + }, + data_evidence: evidence, + rollback_plan: rollbackPlan + }; + + // GL1 直接执行,不写入 pending + if (level === 'GL1') { + console.log(`🔄 ASOP GL1 · ${requestId} · 自主执行,不需审批`); + console.log(` 问题: ${problem}`); + console.log(` 方案: ${proposal}`); + return request; + } + + // GL2/GL3 写入 pending/ + fs.mkdirSync(PENDING_DIR, { recursive: true }); + const filePath = path.join(PENDING_DIR, `${requestId}.json`); + fs.writeFileSync(filePath, JSON.stringify(request, null, 2) + '\n'); + console.log(`🔄 ASOP ${level} · ${requestId} · 已提交到 pending/`); + console.log(` 问题: ${problem}`); + console.log(` 方案: ${proposal}`); + if (level === 'GL3') { + console.log(` ⬆️ GL3 级别,需冰朔审批`); + } + + return request; +} + +// ━━━ 导出 ━━━ +module.exports = { submitASOPRequest }; + +// ━━━ CLI 入口 ━━━ +if (require.main === module) { + const args = process.argv.slice(2); + function getArg(name) { + const idx = args.indexOf('--' + name); + return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : ''; + } + + const result = submitASOPRequest({ + workflow: getArg('workflow'), + workflowName: getArg('name'), + runId: getArg('run-id'), + level: getArg('level') || 'GL2', + problem: getArg('problem'), + proposal: getArg('proposal'), + evidence: getArg('evidence'), + affectedWorkflows: getArg('affected-workflows') ? getArg('affected-workflows').split(',') : [], + affectedFiles: getArg('affected-files') ? getArg('affected-files').split(',') : [], + risk: getArg('risk') || '低', + rollbackPlan: getArg('rollback-plan') + }); + + if (result) { + console.log(JSON.stringify(result, null, 2)); + } +} diff --git a/scripts/skyeye/asop-verifier.js b/scripts/skyeye/asop-verifier.js new file mode 100644 index 00000000..ed5f2bd7 --- /dev/null +++ b/scripts/skyeye/asop-verifier.js @@ -0,0 +1,207 @@ +// scripts/skyeye/asop-verifier.js +// 天眼·ASOP 优化效果验证器 +// +// 天眼运行时检查 data/asop-requests/executed/ 下已执行的优化 +// 验证效果: +// ① 优化后相关 Workflow 是否正常运行? +// ② 是否引入了新的问题? +// ③ 连续 2 次失败 → 自动回滚 +// +// 验证通过 → 移到 verified/ 归档 +// 验证失败 → 回滚 + 记录 + +'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 EXECUTED_DIR = path.join(ASOP_DIR, 'executed'); +const VERIFIED_DIR = path.join(ASOP_DIR, 'verified'); +const SNAPSHOTS_DIR = path.join(ASOP_DIR, 'snapshots'); +const SKYEYE_DIR = '/tmp/skyeye'; + +const BEIJING_OFFSET_MS = 8 * 3600 * 1000; +const CONSECUTIVE_FAILURE_THRESHOLD = 2; + +// ━━━ 安全读取 JSON ━━━ +function readJSON(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (e) { + return null; + } +} + +// ━━━ 检查受影响 Workflow 的最近运行状态 ━━━ +function checkWorkflowHealth(affectedWorkflows) { + // 从天眼扫描结果中获取 Workflow 运行状态 + const recentRunsPath = path.join(SKYEYE_DIR, 'recent-runs.json'); + if (!fs.existsSync(recentRunsPath)) { + return { healthy: true, reason: '无最近运行数据可验证,默认通过' }; + } + + const recentRuns = readJSON(recentRunsPath) || []; + const failures = []; + + for (const wf of affectedWorkflows) { + const wfRuns = recentRuns.filter(r => { + const name = r.name || ''; + return name.includes(wf.replace('.yml', '').replace('.yaml', '')); + }); + + const recentFailures = wfRuns.filter(r => r.conclusion === 'failure'); + if (recentFailures.length >= CONSECUTIVE_FAILURE_THRESHOLD) { + failures.push({ + workflow: wf, + consecutive_failures: recentFailures.length + }); + } + } + + if (failures.length > 0) { + return { + healthy: false, + reason: `连续失败检测:${failures.map(f => `${f.workflow}(${f.consecutive_failures}次)`).join(', ')}`, + failures + }; + } + + return { healthy: true, reason: '所有受影响 Workflow 运行正常' }; +} + +// ━━━ 回滚优化 ━━━ +function rollbackOptimization(request) { + const requestId = request.request_id; + const snapshotDir = path.join(SNAPSHOTS_DIR, requestId); + + if (!fs.existsSync(snapshotDir)) { + console.log(`⚠️ ${requestId} 快照不存在,无法回滚`); + return false; + } + + const snapshotFiles = fs.readdirSync(snapshotDir).filter(f => f !== '.gitkeep'); + let restored = 0; + + for (const file of snapshotFiles) { + const originalPath = file.replace(/__/g, '/'); + const srcPath = path.join(snapshotDir, file); + const destPath = path.join(ROOT, originalPath); + + try { + const destDir = path.dirname(destPath); + fs.mkdirSync(destDir, { recursive: true }); + fs.copyFileSync(srcPath, destPath); + restored++; + } catch (e) { + console.error(`⚠️ 回滚 ${originalPath} 失败: ${e.message}`); + } + } + + console.log(`🔄 ${requestId} 已回滚 ${restored}/${snapshotFiles.length} 个文件`); + return restored > 0; +} + +// ━━━ 验证单个优化 ━━━ +function verifyOptimization(request) { + const requestId = request.request_id; + const affectedWorkflows = (request.impact_assessment && request.impact_assessment.affected_workflows) || []; + + console.log(`\n🔍 验证: ${requestId}`); + console.log(` 方案: ${request.proposed_optimization}`); + + // 检查受影响 Workflow 的健康度 + const health = checkWorkflowHealth(affectedWorkflows); + + if (!health.healthy) { + console.log(` ❌ 验证失败: ${health.reason}`); + + // 异常熔断:回滚 + const rolled = rollbackOptimization(request); + return { + verified: false, + reason: health.reason, + rolled_back: rolled, + verified_at: new Date(Date.now() + BEIJING_OFFSET_MS).toISOString().replace('T', ' ').slice(0, 19) + '+08:00' + }; + } + + console.log(` ✅ 验证通过: ${health.reason}`); + return { + verified: true, + reason: health.reason, + rolled_back: false, + verified_at: new Date(Date.now() + BEIJING_OFFSET_MS).toISOString().replace('T', ' ').slice(0, 19) + '+08:00' + }; +} + +// ━━━ 主验证流程 ━━━ +function verifyAll() { + console.log('🔍 天眼·ASOP 验证器启动'); + console.log('═══════════════════════════════════════════\n'); + + if (!fs.existsSync(EXECUTED_DIR)) { + console.log('ℹ️ executed/ 目录不存在,无待验证优化'); + const result = { total: 0, verified: 0, rolled_back: 0 }; + console.log(JSON.stringify(result, null, 2)); + return result; + } + + const files = fs.readdirSync(EXECUTED_DIR) + .filter(f => f.endsWith('.json') && f !== '.gitkeep'); + + if (files.length === 0) { + console.log('🔍 ASOP:无待验证的已执行优化'); + const result = { total: 0, verified: 0, rolled_back: 0 }; + console.log(JSON.stringify(result, null, 2)); + return result; + } + + console.log(`🔍 ASOP:发现 ${files.length} 条待验证的已执行优化\n`); + + const summary = { total: files.length, verified: 0, rolled_back: 0, results: [] }; + + for (const file of files) { + const filePath = path.join(EXECUTED_DIR, file); + const req = readJSON(filePath); + if (!req) { + console.log(`⚠️ 无法解析 ${file},跳过`); + continue; + } + + const result = verifyOptimization(req); + req.verification_result = result; + + if (result.verified) { + // 验证通过 → 移到 verified/ 归档 + fs.mkdirSync(VERIFIED_DIR, { recursive: true }); + fs.writeFileSync(path.join(VERIFIED_DIR, file), JSON.stringify(req, null, 2) + '\n'); + fs.unlinkSync(filePath); + summary.verified++; + } else { + // 验证失败 + 已回滚 → 标记在 executed/(保留记录) + fs.writeFileSync(filePath, JSON.stringify(req, null, 2) + '\n'); + summary.rolled_back++; + } + + summary.results.push({ + request_id: req.request_id, + verified: result.verified, + rolled_back: result.rolled_back, + reason: result.reason + }); + } + + console.log(`\n📊 验证结果:通过 ${summary.verified} / 回滚 ${summary.rolled_back}`); + console.log(JSON.stringify(summary, null, 2)); + return summary; +} + +// ━━━ 导出 ━━━ +module.exports = { verifyAll, verifyOptimization, rollbackOptimization }; + +// ━━━ 直接运行 ━━━ +if (require.main === module) { + verifyAll(); +} diff --git a/scripts/skyeye/persona-lookup.js b/scripts/skyeye/persona-lookup.js index 42a4457a..934cd918 100644 --- a/scripts/skyeye/persona-lookup.js +++ b/scripts/skyeye/persona-lookup.js @@ -113,18 +113,22 @@ function parsePersonaPage(page) { if (prop.title) return prop.title.map(t => t.plain_text).join(''); if (prop.rich_text) return prop.rich_text.map(t => t.plain_text).join(''); if (prop.select) return prop.select ? prop.select.name : ''; + if (prop.status) return prop.status ? prop.status.name : ''; return ''; } return { page_id: page.id, persona_id: getText(props['人格体编号']), - name: getText(props['名称']) || getText(props['Name']), + name: getText(props['人格体名称']) || getText(props['名称']) || getText(props['Name']), + type: getText(props['编号类型']), + bound_human: getText(props['绑定人类']), dev_id: getText(props['开发者ID']), github_username: getText(props['GitHub用户名']), status: getText(props['状态']), module: getText(props['负责模块']), - commit_signature: getText(props['签名标识']), + repo_paths: getText(props['仓库路径权限']), + commit_signature: getText(props['签名格式']) || getText(props['签名标识']), source: 'notion' }; } @@ -158,7 +162,7 @@ function lookupLocal(personaId) { return null; } -// ━━━ writeBack: 写回 Notion ━━━ +// ━━━ writeBack: 写回 Notion(铸渊最后拉取 + 铸渊同步备注) ━━━ async function writeBack(personaId, data) { if (!PERSONA_DB_ID || !NOTION_TOKEN) { console.log('⚠️ Notion 凭证不完整,跳过写回'); @@ -173,20 +177,21 @@ async function writeBack(personaId, data) { return false; } - // 构建更新属性 + // 构建更新属性(只写铸渊管辖的两个字段) const properties = {}; - if (data.status) { - properties['状态'] = { - select: { name: data.status } + + // 铸渊最后拉取 + properties['铸渊最后拉取'] = { + date: { start: new Date().toISOString() } + }; + + // 铸渊同步备注:[时间戳] [模块] [动作] · [变更摘要] · [分流路径] + if (data.sync_note) { + properties['铸渊同步备注'] = { + rich_text: [{ type: 'text', text: { content: data.sync_note.substring(0, MAX_NOTION_RICH_TEXT) } }] }; - } - if (data.last_activity) { - properties['最后活动'] = { - date: { start: data.last_activity } - }; - } - if (data.gate_result) { - properties['门禁记录'] = { + } else if (data.gate_result) { + properties['铸渊同步备注'] = { rich_text: [{ type: 'text', text: { content: data.gate_result.substring(0, MAX_NOTION_RICH_TEXT) } }] }; } diff --git a/scripts/skyeye/skyeye-main.js b/scripts/skyeye/skyeye-main.js index 30e422e4..230ec5ca 100644 --- a/scripts/skyeye/skyeye-main.js +++ b/scripts/skyeye/skyeye-main.js @@ -72,10 +72,22 @@ function main() { console.log('\n🔬 Phase 3 · 诊断'); results.diagnosis = runModule('diagnose.js', 'diagnosis.json'); + // Phase 3.5: ASOP 审批(新增) + console.log('\n🔄 Phase 3.5 · ASOP 自优化审批'); + results.asop_review = runModule('asop-reviewer.js', 'asop-review.json'); + // Phase 4: 修复 console.log('\n🔧 Phase 4 · 修复 Agent'); results.repair = runModule('repair-agent.js', 'repair-result.json'); + // Phase 4.5: ASOP 执行已批准的优化 + console.log('\n🔧 Phase 4.5 · ASOP 执行已批准优化'); + results.asop_execute = runModule('asop-executor.js', 'asop-execute.json'); + + // Phase 4.7: ASOP 验证之前执行的优化效果 + console.log('\n🔍 Phase 4.7 · ASOP 验证优化效果'); + results.asop_verify = runModule('asop-verifier.js', 'asop-verify.json'); + // Phase 6: 报告 console.log('\n📋 Phase 6 · 全局健康报告'); results.report = runModule('report-generator.js', null);