diff --git a/.github/persona-brain/agent-registry.json b/.github/persona-brain/agent-registry.json index 30f994f2..944ac6d7 100644 --- a/.github/persona-brain/agent-registry.json +++ b/.github/persona-brain/agent-registry.json @@ -1121,6 +1121,32 @@ "daily_checkin_required": false, "parent_sys": "SYS-GLW-0001", "owner": "ICE-0002∞" + }, + { + "id": "AG-ZY-087", + "name": "天眼夜间自动修复引擎 v3.0", + "workflow": "tianyan-nightly-scan.yml", + "duty": "每日夜间扫描失败Workflow → Gemini分析 → 自动修复 → 天眼审核 → 创建PR", + "trigger": "定时 每天 23:00 CST + 手动 + repository_dispatch", + "self_repair": true, + "report_to": "ICE-GL-ZY001", + "status": "registered", + "daily_checkin_required": false, + "parent_sys": "SYS-GLW-0001", + "owner": "ICE-0002∞" + }, + { + "id": "AG-ZY-088", + "name": "Merge Watchdog · 合并指令看守者", + "workflow": "merge-watchdog.yml", + "duty": "跟踪已合并auto-repair PR → 验证修复生效 → 三振出局邮件通知", + "trigger": "PR合并事件(fix/auto-repair-*) + 手动", + "self_repair": false, + "report_to": "ICE-GL-ZY001", + "status": "registered", + "daily_checkin_required": false, + "parent_sys": "SYS-GLW-0001", + "owner": "ICE-0002∞" } ], "note": "铸渊已扫描所有workflow文件,按此格式逐一注册,编号从AG-ZY-001开始顺序分配", diff --git a/.github/scripts/merge-watchdog.js b/.github/scripts/merge-watchdog.js new file mode 100644 index 00000000..b1268b91 --- /dev/null +++ b/.github/scripts/merge-watchdog.js @@ -0,0 +1,219 @@ +#!/usr/bin/env node +// ═══════════════════════════════════════════════ +// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 +// 📜 Copyright: 国作登字-2026-A-00037559 +// ═══════════════════════════════════════════════ +// .github/scripts/merge-watchdog.js +// 👁️ Merge Watchdog · 合并指令看守者 +// +// 看守者核心逻辑: +// - 跟踪已合并的 auto-repair PR +// - 验证修复是否生效 +// - 更新仪表盘记录 +// - 三振出局时准备告警数据 +// +// 看守者五条铁律: +// 1. 看守者自身不修改代码 — 只观察、记录、通知 +// 2. 最多重试 3 次 — 硬上限 +// 3. 每轮修复策略必须不同 +// 4. 邮件必须包含具体操作步骤 +// 5. 成功后绝对静默 — 只更新仪表盘 + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '../..'); +const WATCHDOG_DATA_PATH = path.join(ROOT, 'docs/dashboard/watchdog-records.json'); +const DASHBOARD_PATH = path.join(ROOT, 'DASHBOARD.md'); + +// ━━━ 工具函数 ━━━ + +function loadJSON(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (e) { + return null; + } +} + +function saveJSON(filePath, data) { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8'); +} + +function getTimestamp() { + return new Date().toISOString(); +} + +// ━━━ 看守记录管理 ━━━ + +function loadWatchdogRecords() { + const data = loadJSON(WATCHDOG_DATA_PATH); + if (data && data.records) return data; + return { + merge_watchdog: { + last_updated: getTimestamp(), + active_watches: 0, + resolved_watches: 0, + escalated_watches: 0 + }, + records: [] + }; +} + +function saveWatchdogRecords(data) { + // Update counts + data.merge_watchdog.last_updated = getTimestamp(); + data.merge_watchdog.active_watches = data.records.filter(r => r.status === 'watching').length; + data.merge_watchdog.resolved_watches = data.records.filter(r => r.status === 'fixed').length; + data.merge_watchdog.escalated_watches = data.records.filter(r => r.status === 'escalated').length; + + saveJSON(WATCHDOG_DATA_PATH, data); +} + +// ━━━ 创建看守记录 ━━━ + +function createWatchRecord(prNumber, prTitle, targetWorkflows) { + return { + pr_number: prNumber, + pr_title: prTitle, + merged_at: getTimestamp(), + target_workflows: targetWorkflows, + retry_count: 0, + max_retries: 3, + status: 'watching', + repair_strategies_used: ['standard'], + last_check_time: null, + resolution: null + }; +} + +// ━━━ 记录修复成功(静默更新) ━━━ + +function recordSuccess(prNumber) { + const data = loadWatchdogRecords(); + const record = data.records.find(r => r.pr_number === prNumber); + + if (record) { + record.status = 'fixed'; + record.resolution = 'auto_fixed'; + record.last_check_time = getTimestamp(); + } else { + data.records.push({ + pr_number: prNumber, + status: 'fixed', + resolved_at: getTimestamp(), + resolution: 'auto_fixed', + retry_count: 0 + }); + } + + saveWatchdogRecords(data); + console.log(`✅ PR #${prNumber} 修复已确认生效 — 仪表盘已静默更新`); +} + +// ━━━ 记录修复失败 ━━━ + +function recordFailure(prNumber, retryRound) { + const data = loadWatchdogRecords(); + let record = data.records.find(r => r.pr_number === prNumber); + + if (!record) { + record = createWatchRecord(prNumber, '', []); + data.records.push(record); + } + + record.retry_count = retryRound; + record.last_check_time = getTimestamp(); + + // Strategy names for each round + const strategies = ['standard', 'deep_root_cause', 'conservative_fallback']; + if (retryRound <= 3) { + record.repair_strategies_used.push(strategies[retryRound - 1] || 'unknown'); + } + + if (retryRound >= 3) { + record.status = 'escalated'; + record.resolution = 'human_intervention'; + console.log(`⛔ PR #${prNumber} 三振出局 — 需人类介入`); + } else { + record.status = 'watching'; + console.log(`🔄 PR #${prNumber} 第${retryRound}次失败 — 等待重试`); + } + + saveWatchdogRecords(data); +} + +// ━━━ 注册新的 watch ━━━ + +function registerWatch(prNumber, prTitle, targetWorkflows) { + const data = loadWatchdogRecords(); + + // Check if already watching + const existing = data.records.find(r => r.pr_number === prNumber); + if (existing) { + console.log(`⚠️ PR #${prNumber} 已在看守列表中`); + return; + } + + const record = createWatchRecord(prNumber, prTitle, targetWorkflows); + data.records.push(record); + saveWatchdogRecords(data); + console.log(`👁️ 开始看守 PR #${prNumber}: ${prTitle}`); +} + +// ━━━ 获取看守状态 ━━━ + +function getWatchStatus(prNumber) { + const data = loadWatchdogRecords(); + const record = data.records.find(r => r.pr_number === prNumber); + return record || null; +} + +// ━━━ CLI 入口 ━━━ + +function main() { + const args = process.argv.slice(2); + const action = args[0]; + + switch (action) { + case 'register': { + const prNumber = parseInt(args[1]); + const prTitle = args[2] || ''; + const workflows = args[3] ? args[3].split(',') : []; + registerWatch(prNumber, prTitle, workflows); + break; + } + case 'success': { + const prNumber = parseInt(args[1]); + recordSuccess(prNumber); + break; + } + case 'failure': { + const prNumber = parseInt(args[1]); + const retryRound = parseInt(args[2]) || 1; + recordFailure(prNumber, retryRound); + break; + } + case 'status': { + const prNumber = parseInt(args[1]); + const status = getWatchStatus(prNumber); + console.log(JSON.stringify(status, null, 2)); + break; + } + case 'list': { + const data = loadWatchdogRecords(); + console.log(JSON.stringify(data, null, 2)); + break; + } + default: { + console.log('Usage: merge-watchdog.js [args]'); + console.log('Actions: register, success, failure, status, list'); + } + } +} + +main(); diff --git a/.github/scripts/tianyan-repair.js b/.github/scripts/tianyan-repair.js new file mode 100644 index 00000000..26e7c89a --- /dev/null +++ b/.github/scripts/tianyan-repair.js @@ -0,0 +1,433 @@ +#!/usr/bin/env node +// ═══════════════════════════════════════════════ +// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 +// 📜 Copyright: 国作登字-2026-A-00037559 +// ═══════════════════════════════════════════════ +// .github/scripts/tianyan-repair.js +// Phase C — 铸渊自动修复核心 +// +// 输入: /tmp/tianyan/analysis.json +// 输出: 修复后的文件写入修复分支, /tmp/tianyan/repair-result.json +// +// 对每个 auto_fixable 错误调用 Gemini 生成修复代码 +// 安全护栏:七条铁律 + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const { execSync } = require('child_process'); + +const ROOT = path.resolve(__dirname, '../..'); +const TMP_DIR = '/tmp/tianyan'; +const CONFIG_PATH = path.join(ROOT, '.github/tianyan-config.json'); + +// ━━━ 工具函数 ━━━ + +function loadJSON(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (e) { + return null; + } +} + +function ensureDir(dir) { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); +} + +function getTimestamp() { + return new Date().toISOString(); +} + +function getDateStr() { + return new Date().toISOString().slice(0, 10).replace(/-/g, ''); +} + +// ━━━ 安全护栏:七条铁律 ━━━ + +function isProtectedFile(filePath, config) { + const protectedPatterns = config.safety.protected_patterns || []; + const fileName = path.basename(filePath); + + for (const pattern of protectedPatterns) { + // Simple glob matching for tianyan-*.yml style patterns + const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'); + if (regex.test(fileName)) { + return true; + } + } + + return false; +} + +function validateRepairSafety(filePath, originalContent, repairedContent, config) { + const checks = []; + const fileName = path.basename(filePath); + + // Iron Rule 1: Never modify protected files + if (isProtectedFile(filePath, config)) { + checks.push({ rule: 'protected_file', pass: false, detail: `${fileName} is protected` }); + return { safe: false, checks }; + } + + // Iron Rule 2: Never delete security checks or tianyan scan steps + if (filePath.endsWith('.yml')) { + const origTianyanRefs = (originalContent.match(/tianyan/gi) || []).length; + const newTianyanRefs = (repairedContent.match(/tianyan/gi) || []).length; + if (newTianyanRefs < origTianyanRefs) { + checks.push({ rule: 'tianyan_refs_removed', pass: false, detail: 'Tianyan references were removed' }); + return { safe: false, checks }; + } + } + + // Iron Rule 3: Never add new secrets references + const origSecrets = (originalContent.match(/secrets\.\w+/g) || []); + const newSecrets = (repairedContent.match(/secrets\.\w+/g) || []); + const addedSecrets = newSecrets.filter(s => !origSecrets.includes(s)); + if (addedSecrets.length > 0) { + checks.push({ rule: 'new_secrets_added', pass: false, detail: `New secrets: ${addedSecrets.join(', ')}` }); + return { safe: false, checks }; + } + + // Iron Rule 4: Limit change size + const origLines = originalContent.split('\n').length; + const newLines = repairedContent.split('\n').length; + const diff = Math.abs(newLines - origLines); + if (diff > config.safety.max_lines_per_file) { + checks.push({ rule: 'too_many_changes', pass: false, detail: `${diff} lines changed (max ${config.safety.max_lines_per_file})` }); + return { safe: false, checks }; + } + + // Iron Rule 5: No hardcoded tokens + const tokenPatterns = [/ghp_[A-Za-z0-9]{36}/, /github_pat_[A-Za-z0-9]+/, /gho_[A-Za-z0-9]+/]; + for (const pat of tokenPatterns) { + if (pat.test(repairedContent)) { + checks.push({ rule: 'hardcoded_token', pass: false, detail: 'Hardcoded token detected' }); + return { safe: false, checks }; + } + } + + // Iron Rule 6: Never push directly to main (check for push to main patterns) + if (/git\s+push\s+origin\s+main/i.test(repairedContent) && !/git\s+push\s+origin\s+main/i.test(originalContent)) { + checks.push({ rule: 'push_to_main', pass: false, detail: 'New push to main detected' }); + return { safe: false, checks }; + } + + checks.push({ rule: 'all_checks', pass: true, detail: 'All safety checks passed' }); + return { safe: true, checks }; +} + +// ━━━ Gemini API 调用 ━━━ + +function callGeminiAPI(prompt, systemPrompt, config) { + return new Promise((resolve, reject) => { + const apiKey = process.env.GEMINI_API_KEY; + if (!apiKey) { + reject(new Error('GEMINI_API_KEY not set')); + return; + } + + const model = config.gemini.model || 'gemini-2.0-flash'; + const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; + const parsedUrl = new URL(url); + + const body = JSON.stringify({ + contents: [ + { + role: 'user', + parts: [{ text: `${systemPrompt}\n\n${prompt}` }] + } + ], + generationConfig: { + temperature: config.gemini.temperature || 0.1, + maxOutputTokens: config.gemini.max_output_tokens || 4096 + } + }); + + const options = { + hostname: parsedUrl.hostname, + port: 443, + path: parsedUrl.pathname + parsedUrl.search, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body) + } + }; + + const req = https.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { + try { + const json = JSON.parse(data); + const text = json.candidates?.[0]?.content?.parts?.[0]?.text || ''; + resolve(text); + } catch (e) { + reject(new Error(`Gemini response parse error: ${e.message}`)); + } + }); + }); + + req.on('error', (e) => reject(e)); + req.setTimeout(60000, () => { + req.destroy(); + reject(new Error('Gemini API timeout')); + }); + req.write(body); + req.end(); + }); +} + +// ━━━ 语法验证 ━━━ + +function validateSyntax(filePath, content) { + const ext = path.extname(filePath); + const tmpFile = path.join(TMP_DIR, `validate-${Date.now()}${ext}`); + + try { + fs.writeFileSync(tmpFile, content); + + if (ext === '.js') { + execSync(`node --check "${tmpFile}"`, { stdio: 'pipe' }); + } else if (ext === '.json') { + JSON.parse(content); + } else if (ext === '.yml' || ext === '.yaml') { + // Basic YAML validation: check for tab characters and obvious issues + if (content.includes('\t')) { + return { valid: false, error: 'YAML contains tab characters' }; + } + } + + return { valid: true }; + } catch (e) { + return { valid: false, error: e.message }; + } finally { + try { fs.unlinkSync(tmpFile); } catch (e) { /* ignore */ } + } +} + +// ━━━ 提取修复代码 ━━━ + +function extractFileContent(geminiResponse) { + // Try to extract file content from code blocks + const codeBlockMatch = geminiResponse.match(/```(?:yaml|yml|javascript|js|json|sh|bash)?\n([\s\S]*?)\n```/); + if (codeBlockMatch) { + return codeBlockMatch[1]; + } + + // If no code block, try to use the full response (minus any leading/trailing explanation) + const lines = geminiResponse.split('\n'); + const contentLines = []; + let inContent = false; + + for (const line of lines) { + if (!inContent && (line.startsWith('#') || line.startsWith('name:') || line.startsWith('{') || line.startsWith('/') || line.startsWith('const ') || line.startsWith('//') || line.startsWith('#!/'))) { + inContent = true; + } + if (inContent) { + contentLines.push(line); + } + } + + return contentLines.length > 0 ? contentLines.join('\n') : geminiResponse; +} + +// ━━━ 修复单个错误 ━━━ + +async function repairError(analysis, config) { + const result = { + workflow: analysis.workflow, + error_type: analysis.error_type, + files_modified: [], + success: false, + skipped: false, + error: null + }; + + if (!analysis.auto_fixable) { + result.skipped = true; + result.error = 'Not auto-fixable'; + return result; + } + + if (!analysis.affected_files || analysis.affected_files.length === 0) { + result.skipped = true; + result.error = 'No affected files identified'; + return result; + } + + // Check file count limit + if (analysis.affected_files.length > config.safety.max_files_per_repair) { + result.error = `Too many files (${analysis.affected_files.length} > max ${config.safety.max_files_per_repair})`; + return result; + } + + const systemPrompt = `你是铸渊核心大脑。根据以下分析结果生成修复代码。 +规则: +1. 只修改必要的行,最小化变更 +2. 保留所有注释和格式 +3. 输出完整的修复后文件内容 +4. 在修复位置添加注释:# [AUTO-FIX] ${new Date().toISOString().slice(0, 10)} by 铸渊 - ${analysis.root_cause} +5. 严禁修改任何与报错无关的代码 +6. 严禁删除任何安全检查或天眼扫描步骤 +7. 输出格式:将完整修复后文件内容放在代码块中`; + + for (const filePath of analysis.affected_files) { + const absPath = path.join(ROOT, filePath); + if (!fs.existsSync(absPath)) { + console.log(` ⚠️ File not found: ${filePath}`); + continue; + } + + const originalContent = fs.readFileSync(absPath, 'utf8'); + + const userPrompt = `分析结果: +${JSON.stringify(analysis, null, 2)} + +当前文件内容 (${filePath}): +\`\`\` +${originalContent} +\`\`\` + +请输出修复后的完整文件内容。`; + + try { + if (!process.env.GEMINI_API_KEY) { + console.log(` ⚠️ No GEMINI_API_KEY — skipping Gemini repair for ${filePath}`); + result.error = 'GEMINI_API_KEY not available'; + continue; + } + + const response = await callGeminiAPI(userPrompt, systemPrompt, config); + const repairedContent = extractFileContent(response); + + // Safety validation + const safety = validateRepairSafety(filePath, originalContent, repairedContent, config); + if (!safety.safe) { + const failedRule = safety.checks.find(c => !c.pass); + console.log(` 🛡️ Safety block for ${filePath}: ${failedRule.detail}`); + result.error = `Safety block: ${failedRule.detail}`; + continue; + } + + // Syntax validation + const syntax = validateSyntax(filePath, repairedContent); + if (!syntax.valid) { + console.log(` ❌ Syntax error in repair for ${filePath}: ${syntax.error}`); + result.error = `Syntax error: ${syntax.error}`; + continue; + } + + // Write repaired file + fs.writeFileSync(absPath, repairedContent); + result.files_modified.push(filePath); + result.success = true; + + console.log(` ✅ Repaired: ${filePath}`); + } catch (e) { + console.log(` ❌ Repair failed for ${filePath}: ${e.message}`); + result.error = e.message; + } + } + + return result; +} + +// ━━━ 主流程 ━━━ + +async function main() { + console.log('[铸渊核心大脑] Phase C 启动 — 自动修复'); + console.log('═══════════════════════════════════════════'); + + ensureDir(TMP_DIR); + + const config = loadJSON(CONFIG_PATH); + if (!config) { + console.error('❌ 无法加载 tianyan-config.json'); + process.exit(1); + } + + // Check weekend mode + const now = new Date(); + const dayOfWeek = now.getUTCDay(); + const utcHour = now.getUTCHours(); + // Saturday in CST = UTC Saturday (after 16:00 UTC Friday to 16:00 UTC Saturday) + if (config.weekend_mode.enabled && config.weekend_mode.skip_repair_on_saturday) { + // CST 20:00 = UTC 12:00, CST 00:00 = UTC 16:00 + if (dayOfWeek === 6 && utcHour >= 12) { + console.log('🌙 周六休眠期 — 跳过自动修复'); + const skipResult = { timestamp: getTimestamp(), skipped: true, reason: 'saturday_hibernation', repairs: [] }; + fs.writeFileSync(path.join(TMP_DIR, 'repair-result.json'), JSON.stringify(skipResult, null, 2)); + return; + } + } + + // Load analysis + const analysisPath = path.join(TMP_DIR, 'analysis.json'); + const analysis = loadJSON(analysisPath); + if (!analysis) { + console.error('❌ 无法加载 analysis.json'); + process.exit(1); + } + + const fixableItems = (analysis.analyses || []).filter(a => a.auto_fixable); + console.log(`📊 可修复项: ${fixableItems.length} / ${analysis.total}`); + + if (fixableItems.length === 0) { + console.log('⚠️ 没有可自动修复的错误'); + const emptyResult = { timestamp: getTimestamp(), total: 0, repaired: 0, failed: 0, repairs: [] }; + fs.writeFileSync(path.join(TMP_DIR, 'repair-result.json'), JSON.stringify(emptyResult, null, 2)); + return; + } + + // Create repair branch + const branchName = `fix/auto-repair-${getDateStr()}-001`; + console.log(`🌿 修复分支: ${branchName}`); + + const repairs = []; + for (const item of fixableItems) { + console.log(`\n⚒️ 修复: ${item.workflow} (${item.error_type})`); + const result = await repairError(item, config); + repairs.push(result); + + if (result.success && result.files_modified.length > 0) { + // Git commit for each repair + try { + const shortDesc = item.root_cause ? item.root_cause.slice(0, 60) : item.error_type; + for (const f of result.files_modified) { + execSync(`git add "${f}"`, { cwd: ROOT, stdio: 'pipe' }); + } + execSync( + `git commit -m "[AUTO-FIX] ${item.workflow}: ${shortDesc} (by 铸渊夜间修复引擎)"`, + { cwd: ROOT, stdio: 'pipe' } + ); + console.log(` 📝 Committed fix for: ${item.workflow}`); + } catch (e) { + console.log(` ⚠️ Git commit failed: ${e.message}`); + } + } + } + + const repairResult = { + timestamp: getTimestamp(), + branch: branchName, + total: fixableItems.length, + repaired: repairs.filter(r => r.success).length, + failed: repairs.filter(r => !r.success && !r.skipped).length, + skipped: repairs.filter(r => r.skipped).length, + repairs + }; + + fs.writeFileSync(path.join(TMP_DIR, 'repair-result.json'), JSON.stringify(repairResult, null, 2)); + + console.log('\n═══════════════════════════════════════════'); + console.log(`📊 修复结果: 成功 ${repairResult.repaired} · 失败 ${repairResult.failed} · 跳过 ${repairResult.skipped}`); +} + +main().catch(e => { + console.error(`❌ Phase C 异常: ${e.message}`); + process.exit(1); +}); diff --git a/.github/scripts/tianyan-review.js b/.github/scripts/tianyan-review.js new file mode 100644 index 00000000..ad445638 --- /dev/null +++ b/.github/scripts/tianyan-review.js @@ -0,0 +1,416 @@ +#!/usr/bin/env node +// ═══════════════════════════════════════════════ +// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 +// 📜 Copyright: 国作登字-2026-A-00037559 +// ═══════════════════════════════════════════════ +// .github/scripts/tianyan-review.js +// Phase D — 天眼审核修复代码 (H1-H9 Checklist) +// +// 输入: /tmp/tianyan/repair-result.json + git diff on repair branch +// 输出: /tmp/tianyan/review-result.json +// +// 天眼检查清单 v3.0: +// H1 → Secrets 安全:无硬编码令牌 +// H2 → 文件完整性:YAML/JSON/JS 语法正确 +// H3 → 依赖安全:无新增不受信依赖 +// H4 → 权限最小化:Workflow permissions 未被扩大 +// H5 → 天眼自保护:tianyan-*.yml 文件未被修改 +// H6 → 变更范围:总变更行数 < 200,单文件 < 50 +// H7 → Secrets引用完整性:修改前后 secrets.* 引用数量一致 +// H8 → 注释规范:每个修复位置都有 [AUTO-FIX] 注释 +// H9 → 无残留调试代码:无 console.log、echo debug 等临时代码 + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const ROOT = path.resolve(__dirname, '../..'); +const TMP_DIR = '/tmp/tianyan'; +const CONFIG_PATH = path.join(ROOT, '.github/tianyan-config.json'); + +// ━━━ 工具函数 ━━━ + +function loadJSON(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (e) { + return null; + } +} + +function getTimestamp() { + return new Date().toISOString(); +} + +function gitExec(cmd) { + try { + return execSync(cmd, { cwd: ROOT, encoding: 'utf8', stdio: 'pipe' }).trim(); + } catch (e) { + return ''; + } +} + +// ━━━ H1: Secrets 安全 ━━━ + +function checkH1_SecretsHardcoded(modifiedFiles) { + const result = { id: 'H1', name: 'Secrets安全', pass: true, details: [] }; + + const tokenPatterns = [ + { pattern: /ghp_[A-Za-z0-9]{36}/g, type: 'GitHub PAT' }, + { pattern: /github_pat_[A-Za-z0-9_]+/g, type: 'GitHub Fine-grained PAT' }, + { pattern: /gho_[A-Za-z0-9]+/g, type: 'GitHub OAuth token' }, + { pattern: /sk-[A-Za-z0-9]{40,}/g, type: 'API key (sk-)' }, + { pattern: /AIza[A-Za-z0-9_-]{35}/g, type: 'Google API key' } + ]; + + for (const filePath of modifiedFiles) { + const absPath = path.join(ROOT, filePath); + if (!fs.existsSync(absPath)) continue; + const content = fs.readFileSync(absPath, 'utf8'); + + for (const { pattern, type } of tokenPatterns) { + const matches = content.match(pattern); + if (matches) { + result.pass = false; + result.details.push(`${filePath}: Found hardcoded ${type}`); + } + } + } + + if (result.pass) result.details.push('No hardcoded tokens found'); + return result; +} + +// ━━━ H2: 文件完整性 ━━━ + +function checkH2_FileSyntax(modifiedFiles) { + const result = { id: 'H2', name: '文件完整性', pass: true, details: [] }; + + for (const filePath of modifiedFiles) { + const absPath = path.join(ROOT, filePath); + if (!fs.existsSync(absPath)) continue; + const content = fs.readFileSync(absPath, 'utf8'); + const ext = path.extname(filePath); + + try { + if (ext === '.json') { + JSON.parse(content); + } else if (ext === '.js') { + execSync(`node --check "${absPath}"`, { stdio: 'pipe' }); + } else if (ext === '.yml' || ext === '.yaml') { + if (content.includes('\t')) { + throw new Error('YAML contains tab characters'); + } + // Check for basic YAML structure (should start with valid YAML content) + const trimmed = content.trim(); + if (trimmed.length > 0 && !trimmed.startsWith('#') && !trimmed.startsWith('name:') && !trimmed.startsWith('on:') && !trimmed.startsWith('---') && !trimmed.match(/^[a-zA-Z_]/)) { + throw new Error('YAML does not start with valid content'); + } + } + result.details.push(`${filePath}: ✅ syntax OK`); + } catch (e) { + result.pass = false; + result.details.push(`${filePath}: ❌ ${e.message}`); + } + } + + return result; +} + +// ━━━ H3: 依赖安全 ━━━ + +function checkH3_DependencySafety(modifiedFiles) { + const result = { id: 'H3', name: '依赖安全', pass: true, details: [] }; + + for (const filePath of modifiedFiles) { + if (!filePath.endsWith('.yml') && !filePath.endsWith('.yaml')) continue; + const absPath = path.join(ROOT, filePath); + if (!fs.existsSync(absPath)) continue; + + const content = fs.readFileSync(absPath, 'utf8'); + + // Check for new uses: actions that aren't from trusted sources + const usesMatches = content.match(/uses:\s*([^\s#]+)/g) || []; + for (const use of usesMatches) { + const action = use.replace('uses:', '').trim(); + // Allow official actions/* and known trusted actions + const trusted = ['actions/', 'github/', 'peter-evans/', 'dawidd6/', 'peaceiris/']; + const isTrusted = trusted.some(t => action.startsWith(t)); + if (!isTrusted && !action.startsWith('./')) { + result.details.push(`${filePath}: ⚠️ External action: ${action}`); + // Don't fail, just warn + } + } + } + + if (result.details.length === 0) result.details.push('No dependency concerns'); + return result; +} + +// ━━━ H4: 权限最小化 ━━━ + +function checkH4_PermissionsMinimal(modifiedFiles) { + const result = { id: 'H4', name: '权限最小化', pass: true, details: [] }; + + for (const filePath of modifiedFiles) { + if (!filePath.endsWith('.yml') && !filePath.endsWith('.yaml')) continue; + const absPath = path.join(ROOT, filePath); + if (!fs.existsSync(absPath)) continue; + + const content = fs.readFileSync(absPath, 'utf8'); + + // Check for overly broad permissions + if (content.includes('permissions: write-all') || content.includes('permissions:\n contents: write\n actions: write\n issues: write\n pull-requests: write')) { + result.pass = false; + result.details.push(`${filePath}: ❌ Overly broad permissions detected`); + } + } + + if (result.pass) result.details.push('Permissions within acceptable scope'); + return result; +} + +// ━━━ H5: 天眼自保护 ━━━ + +function checkH5_TianyanProtection(modifiedFiles) { + const result = { id: 'H5', name: '天眼自保护', pass: true, details: [] }; + + for (const filePath of modifiedFiles) { + const fileName = path.basename(filePath); + if (fileName.match(/^tianyan-.*\.yml$/)) { + result.pass = false; + result.details.push(`${filePath}: ❌ tianyan workflow file was modified`); + } + } + + if (result.pass) result.details.push('No tianyan workflow files modified'); + return result; +} + +// ━━━ H6: 变更范围 ━━━ + +function checkH6_ChangeScope(modifiedFiles, config) { + const result = { id: 'H6', name: '变更范围', pass: true, details: [] }; + + let totalChanged = 0; + const maxPerFile = config.safety.max_lines_per_file || 50; + const maxTotal = config.safety.max_total_changed_lines || 200; + + for (const filePath of modifiedFiles) { + const diff = gitExec(`git diff --numstat HEAD~1 -- "${filePath}"`); + if (!diff) continue; + + const parts = diff.split('\t'); + const added = parseInt(parts[0]) || 0; + const deleted = parseInt(parts[1]) || 0; + const fileChanged = added + deleted; + totalChanged += fileChanged; + + if (fileChanged > maxPerFile) { + result.pass = false; + result.details.push(`${filePath}: ❌ ${fileChanged} lines changed (max ${maxPerFile})`); + } else { + result.details.push(`${filePath}: ✅ ${fileChanged} lines changed`); + } + } + + if (totalChanged > maxTotal) { + result.pass = false; + result.details.push(`Total: ❌ ${totalChanged} lines (max ${maxTotal})`); + } else { + result.details.push(`Total: ✅ ${totalChanged} lines`); + } + + return result; +} + +// ━━━ H7: Secrets 引用完整性 ━━━ + +function checkH7_SecretsIntegrity(modifiedFiles) { + const result = { id: 'H7', name: 'Secrets引用完整性', pass: true, details: [] }; + + for (const filePath of modifiedFiles) { + if (!filePath.endsWith('.yml') && !filePath.endsWith('.yaml')) continue; + + // Get original content from git + const originalContent = gitExec(`git show HEAD~1:"${filePath}" 2>/dev/null`); + const absPath = path.join(ROOT, filePath); + if (!fs.existsSync(absPath)) continue; + const newContent = fs.readFileSync(absPath, 'utf8'); + + const origSecrets = (originalContent.match(/secrets\.\w+/g) || []).sort(); + const newSecrets = (newContent.match(/secrets\.\w+/g) || []).sort(); + + const removed = origSecrets.filter(s => !newSecrets.includes(s)); + const added = newSecrets.filter(s => !origSecrets.includes(s)); + + if (removed.length > 0) { + result.pass = false; + result.details.push(`${filePath}: ❌ Secrets removed: ${removed.join(', ')}`); + } + if (added.length > 0) { + result.pass = false; + result.details.push(`${filePath}: ❌ Secrets added: ${added.join(', ')}`); + } + if (removed.length === 0 && added.length === 0) { + result.details.push(`${filePath}: ✅ Secrets references intact`); + } + } + + return result; +} + +// ━━━ H8: 注释规范 ━━━ + +function checkH8_AutoFixComments(modifiedFiles) { + const result = { id: 'H8', name: '注释规范', pass: true, details: [] }; + + let hasModifiedContent = false; + for (const filePath of modifiedFiles) { + const absPath = path.join(ROOT, filePath); + if (!fs.existsSync(absPath)) continue; + const content = fs.readFileSync(absPath, 'utf8'); + + if (content.includes('[AUTO-FIX]')) { + result.details.push(`${filePath}: ✅ [AUTO-FIX] comment found`); + hasModifiedContent = true; + } + } + + if (!hasModifiedContent && modifiedFiles.length > 0) { + result.pass = false; + result.details.push('❌ No [AUTO-FIX] comments found in modified files'); + } + + return result; +} + +// ━━━ H9: 无残留调试代码 ━━━ + +function checkH9_NoDebugCode(modifiedFiles) { + const result = { id: 'H9', name: '无残留调试代码', pass: true, details: [] }; + + const debugPatterns = [ + /console\.log\s*\(\s*['"]debug/gi, + /echo\s+['"]?debug/gi, + /TODO:\s*remove/gi, + /FIXME:\s*temp/gi, + /console\.log\s*\(\s*['"]test/gi + ]; + + for (const filePath of modifiedFiles) { + const absPath = path.join(ROOT, filePath); + if (!fs.existsSync(absPath)) continue; + const content = fs.readFileSync(absPath, 'utf8'); + + for (const pattern of debugPatterns) { + if (pattern.test(content)) { + result.pass = false; + result.details.push(`${filePath}: ❌ Debug code found: ${pattern.source}`); + } + } + } + + if (result.pass) result.details.push('No debug code residue found'); + return result; +} + +// ━━━ 主审核流程 ━━━ + +function main() { + console.log('[天眼 v3.0] Phase D 启动 — 审核修复代码'); + console.log('═══════════════════════════════════════════'); + + const config = loadJSON(CONFIG_PATH); + if (!config) { + console.error('❌ 无法加载 tianyan-config.json'); + process.exit(1); + } + + // Load repair result + const repairResult = loadJSON(path.join(TMP_DIR, 'repair-result.json')); + if (!repairResult) { + console.log('⚠️ repair-result.json 不存在'); + const result = { timestamp: getTimestamp(), pass: false, reason: 'No repair result found', checks: [] }; + fs.writeFileSync(path.join(TMP_DIR, 'review-result.json'), JSON.stringify(result, null, 2)); + return; + } + + if (repairResult.skipped) { + console.log('⚠️ 修复已跳过(周末模式)'); + const result = { timestamp: getTimestamp(), pass: false, reason: 'Repair skipped', checks: [] }; + fs.writeFileSync(path.join(TMP_DIR, 'review-result.json'), JSON.stringify(result, null, 2)); + return; + } + + // Collect all modified files + const modifiedFiles = []; + for (const repair of (repairResult.repairs || [])) { + if (repair.success && repair.files_modified) { + modifiedFiles.push(...repair.files_modified); + } + } + + const uniqueFiles = [...new Set(modifiedFiles)]; + console.log(`📁 审核文件: ${uniqueFiles.length} 个\n`); + + if (uniqueFiles.length === 0) { + console.log('⚠️ 没有修改的文件需要审核'); + const result = { timestamp: getTimestamp(), pass: false, reason: 'No files to review', checks: [], pr_needed: false }; + fs.writeFileSync(path.join(TMP_DIR, 'review-result.json'), JSON.stringify(result, null, 2)); + return; + } + + // Run all checks + const checks = [ + checkH1_SecretsHardcoded(uniqueFiles), + checkH2_FileSyntax(uniqueFiles), + checkH3_DependencySafety(uniqueFiles), + checkH4_PermissionsMinimal(uniqueFiles), + checkH5_TianyanProtection(uniqueFiles), + checkH6_ChangeScope(uniqueFiles, config), + checkH7_SecretsIntegrity(uniqueFiles), + checkH8_AutoFixComments(uniqueFiles), + checkH9_NoDebugCode(uniqueFiles) + ]; + + // Print results + console.log('📋 天眼检查清单 v3.0:'); + console.log('─────────────────────────────────'); + let allPass = true; + for (const check of checks) { + const icon = check.pass ? '✅' : '❌'; + console.log(` ${icon} ${check.id}: ${check.name}`); + for (const detail of check.details) { + console.log(` ${detail}`); + } + if (!check.pass) allPass = false; + } + console.log('─────────────────────────────────'); + + const reviewResult = { + timestamp: getTimestamp(), + pass: allPass, + pr_needed: allPass, + checks, + files_reviewed: uniqueFiles, + summary: { + total_checks: checks.length, + passed: checks.filter(c => c.pass).length, + failed: checks.filter(c => !c.pass).length + } + }; + + fs.writeFileSync(path.join(TMP_DIR, 'review-result.json'), JSON.stringify(reviewResult, null, 2)); + + if (allPass) { + console.log('\n✅ 天眼审核通过 — 准备创建 PR'); + } else { + const failedChecks = checks.filter(c => !c.pass).map(c => c.id).join(', '); + console.log(`\n❌ 天眼审核未通过 — 失败项: ${failedChecks}`); + } +} + +main(); diff --git a/.github/workflows/merge-watchdog.yml b/.github/workflows/merge-watchdog.yml new file mode 100644 index 00000000..15e6190e --- /dev/null +++ b/.github/workflows/merge-watchdog.yml @@ -0,0 +1,327 @@ +# ═══════════════════════════════════════════════ +# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 +# 📜 Copyright: 国作登字-2026-A-00037559 +# ═══════════════════════════════════════════════ +# .github/workflows/merge-watchdog.yml +# 👁️ Merge Watchdog · 合并指令看守者 +# +# 事件驱动:当 auto-repair PR 被合并到 main 时触发 +# 跟踪修复是否真正生效 +# 三次失败 → 邮件通知冰朔手动介入 +# 修复成功 → 静默更新仪表盘 +# +# 指令编号: ZY-MERGE-WATCHDOG-2026-0324-001 +# AG-ZY-088 + +name: "👁️ Merge Watchdog · 合并指令看守者" + +on: + pull_request: + types: [closed] + branches: [main] + workflow_dispatch: + inputs: + pr_number: + description: '手动指定要观察的 PR 编号' + type: number + required: false + +permissions: + actions: read + contents: write + pull-requests: write + issues: write + +env: + MAX_RETRIES: 3 + OBSERVATION_WINDOW_HOURS: 48 + WATCHDOG_DATA: "docs/dashboard/watchdog-records.json" + +jobs: + # ======================================== + # Gate: 只看行动类(已合并的 auto-repair PR) + # ======================================== + gate: + runs-on: ubuntu-latest + if: > + (github.event.pull_request.merged == true && + startsWith(github.event.pull_request.head.ref, 'fix/auto-repair-')) + || github.event_name == 'workflow_dispatch' + outputs: + should_watch: ${{ steps.check.outputs.should_watch }} + target_workflows: ${{ steps.check.outputs.target_workflows }} + pr_number: ${{ steps.check.outputs.pr_number }} + steps: + - name: "🔍 识别修复目标" + id: check + env: + GH_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} + run: | + echo "[Merge Watchdog] 检测到 auto-repair PR 合并" + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PR_NUM="${{ github.event.inputs.pr_number }}" + if [ -z "$PR_NUM" ] || [ "$PR_NUM" = "0" ]; then + echo "should_watch=false" >> $GITHUB_OUTPUT + echo "⚠️ 手动触发但未指定 PR 编号" + exit 0 + fi + PR_TITLE=$(gh api "repos/${{ github.repository }}/pulls/$PR_NUM" --jq '.title' 2>/dev/null || echo "") + else + PR_NUM="${{ github.event.pull_request.number }}" + PR_TITLE="${{ github.event.pull_request.title }}" + fi + + echo "PR #$PR_NUM: $PR_TITLE" + + # 从 PR 标题中提取目标 Workflow + # 格式约定: 🌙 [AUTO-FIX] YYYY-MM-DD 天眼夜间自动修复 (N个修复) + WORKFLOW=$(echo "$PR_TITLE" | grep -oP '\[AUTO-FIX\]\s+\K[^(]+' | sed 's/天眼夜间自动修复//' | xargs || echo "unknown") + + echo "target_workflows=$WORKFLOW" >> $GITHUB_OUTPUT + echo "should_watch=true" >> $GITHUB_OUTPUT + echo "pr_number=$PR_NUM" >> $GITHUB_OUTPUT + echo "👁️ 开始看守: PR #$PR_NUM" + + # ======================================== + # 注册看守 + 观察目标 Workflow 运行结果 + # ======================================== + observe: + needs: gate + if: needs.gate.outputs.should_watch == 'true' + runs-on: ubuntu-latest + outputs: + workflow_result: ${{ steps.wait.outputs.result }} + run_url: ${{ steps.wait.outputs.run_url }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: "📝 注册看守记录" + env: + PR_NUM: ${{ needs.gate.outputs.pr_number }} + PR_TITLE: ${{ github.event.pull_request.title }} + TARGET: ${{ needs.gate.outputs.target_workflows }} + run: | + node .github/scripts/merge-watchdog.js register "$PR_NUM" "$PR_TITLE" "$TARGET" + + git config user.name "watchdog-bot" + git config user.email "watchdog@guanghulab.com" + git add docs/dashboard/watchdog-records.json + git diff --cached --quiet || git commit -m "[WATCHDOG] 注册看守 PR #$PR_NUM" + git push origin main || true + + - name: "⏳ 等待目标 Workflow 运行" + id: wait + env: + GH_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} + TARGET: ${{ needs.gate.outputs.target_workflows }} + run: | + echo "[Merge Watchdog] 等待目标 Workflow 下一次运行..." + + MERGE_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) + # Wait up to 30 minutes checking every 2 minutes + # (the full 48h observation is handled by the nightly scan cycle) + MAX_WAIT=1800 + POLL_INTERVAL=120 + ELAPSED=0 + FOUND="false" + + while [ $ELAPSED -lt $MAX_WAIT ]; do + sleep $POLL_INTERVAL + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + + echo "⏳ 检查中... ($ELAPSED / $MAX_WAIT 秒)" + + # Query recent runs that completed after merge + RUNS=$(gh api \ + "repos/${{ github.repository }}/actions/runs?per_page=10&status=completed&created=>$MERGE_TIME" \ + --jq '.workflow_runs[] | {name: .name, conclusion: .conclusion, html_url: .html_url}' 2>/dev/null || echo "") + + if [ -n "$RUNS" ]; then + # Check if any run matches the target + CONCLUSION=$(echo "$RUNS" | jq -r 'select(.name != null) | .conclusion' | head -1) + RUN_URL=$(echo "$RUNS" | jq -r 'select(.name != null) | .html_url' | head -1) + + if [ "$CONCLUSION" = "success" ]; then + echo "result=success" >> $GITHUB_OUTPUT + echo "run_url=$RUN_URL" >> $GITHUB_OUTPUT + echo "✅ 目标 Workflow 运行成功" + FOUND="true" + break + elif [ "$CONCLUSION" = "failure" ]; then + echo "result=failure" >> $GITHUB_OUTPUT + echo "run_url=$RUN_URL" >> $GITHUB_OUTPUT + echo "❌ 目标 Workflow 仍然失败" + FOUND="true" + break + fi + fi + done + + if [ "$FOUND" = "false" ]; then + echo "result=pending" >> $GITHUB_OUTPUT + echo "run_url=" >> $GITHUB_OUTPUT + echo "⏰ 初始观察窗口结束 — 将在下次夜间扫描中继续跟踪" + fi + + # ======================================== + # 判定: 成功 → 静默 / 失败 → 计数+告警 + # ======================================== + verdict: + needs: [gate, observe] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: "📊 读取看守记录" + id: load + env: + PR_NUM: ${{ needs.gate.outputs.pr_number }} + run: | + if [ -f "$WATCHDOG_DATA" ]; then + RETRY_COUNT=$(jq -r ".records[] | select(.pr_number == $PR_NUM) | .retry_count // 0" "$WATCHDOG_DATA" | head -1) + echo "retry_count=${RETRY_COUNT:-0}" >> $GITHUB_OUTPUT + else + echo "retry_count=0" >> $GITHUB_OUTPUT + fi + + # -------- 成功路径:静默更新 -------- + - name: "✅ 修复确认 — 静默更新仪表盘" + if: needs.observe.outputs.workflow_result == 'success' + env: + GH_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} + PR_NUM: ${{ needs.gate.outputs.pr_number }} + run: | + echo "[Merge Watchdog] ✅ 修复已生效 — 静默更新仪表盘" + + # Update watchdog records + node .github/scripts/merge-watchdog.js success "$PR_NUM" + + # Update DASHBOARD.md + if [ -f DASHBOARD.md ]; then + sed -i "s/| 活跃看守 | .*/| 活跃看守 | $(jq '.merge_watchdog.active_watches' $WATCHDOG_DATA) |/" DASHBOARD.md + sed -i "s/| 已确认修复 | .*/| 已确认修复 | $(jq '.merge_watchdog.resolved_watches' $WATCHDOG_DATA) |/" DASHBOARD.md + fi + + git config user.name "watchdog-bot" + git config user.email "watchdog@guanghulab.com" + git add "$WATCHDOG_DATA" DASHBOARD.md + git diff --cached --quiet || git commit -m "[WATCHDOG] ✅ PR #$PR_NUM 修复已确认生效" + git push origin main || true + + # Add silent comment to PR + gh pr comment "$PR_NUM" \ + --body "✅ **Merge Watchdog 确认:修复已生效** — 仪表盘已静默更新。" 2>/dev/null || true + + echo "🤫 不发邮件,不通知,安静收工。" + + # -------- 失败路径 -------- + - name: "❌ 修复未生效 — 判断重试或告警" + if: needs.observe.outputs.workflow_result == 'failure' + env: + RETRY_COUNT: ${{ steps.load.outputs.retry_count }} + GH_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} + PR_NUM: ${{ needs.gate.outputs.pr_number }} + run: | + NEW_COUNT=$((RETRY_COUNT + 1)) + echo "[Merge Watchdog] ❌ 第 $NEW_COUNT 次失败" + + # Update watchdog records + node .github/scripts/merge-watchdog.js failure "$PR_NUM" "$NEW_COUNT" + + if [ $NEW_COUNT -lt ${{ env.MAX_RETRIES }} ]; then + echo "🔄 触发重新修复(策略 $NEW_COUNT)" + + # Trigger nightly scan for retry with different strategy + gh api "repos/${{ github.repository }}/dispatches" \ + --method POST \ + -f event_type="watchdog-retry" \ + -f "client_payload[pr_number]=$PR_NUM" \ + -f "client_payload[retry_round]=$NEW_COUNT" \ + -f "client_payload[run_url]=${{ needs.observe.outputs.run_url }}" 2>/dev/null || true + + git config user.name "watchdog-bot" + git config user.email "watchdog@guanghulab.com" + git add "$WATCHDOG_DATA" + git diff --cached --quiet || git commit -m "[WATCHDOG] 🔄 PR #$PR_NUM 第${NEW_COUNT}次修复失败,触发重试" + git push origin main || true + else + echo "⛔ 三振出局!" + + # Update dashboard + if [ -f DASHBOARD.md ]; then + sed -i "s/| 已升级(需人工) | .*/| 已升级(需人工) | $(jq '.merge_watchdog.escalated_watches' $WATCHDOG_DATA) |/" DASHBOARD.md + fi + + git config user.name "watchdog-bot" + git config user.email "watchdog@guanghulab.com" + git add "$WATCHDOG_DATA" DASHBOARD.md + git diff --cached --quiet || git commit -m "[WATCHDOG] ⛔ PR #$PR_NUM 三次修复失败,已升级通知" + git push origin main || true + + # Create Issue as fallback alert (in case email is not configured) + gh issue create \ + --title "⛔ [WATCHDOG] 自动修复三次失败 — PR #$PR_NUM 需手动介入" \ + --body "## ⛔ Merge Watchdog 三振出局通知 + + PR #$PR_NUM 的修复经过 **3轮自动修复** 仍未解决。 + + **最新失败日志:** ${{ needs.observe.outputs.run_url }} + + ### 📋 冰朔需要做什么: + 1. 查看上面的失败日志链接 + 2. 手动定位报错根因 + 3. 修复代码并推送到 main + 4. 等待下一次 Workflow 运行验证 + + > 看守者已暂停对该问题的自动修复。 + > — 光湖系统 · Merge Watchdog" \ + --label "tianyan-v3,auto-fix" 2>/dev/null || true + fi + + # -------- 三振出局:发邮件(如已配置) -------- + - name: "📧 发送人类介入邮件" + if: > + needs.observe.outputs.workflow_result == 'failure' && + steps.load.outputs.retry_count >= 2 + continue-on-error: true + uses: dawidd6/action-send-mail@v3 + with: + server_address: smtp.gmail.com + server_port: 465 + secure: true + username: ${{ secrets.MAIL_USERNAME }} + password: ${{ secrets.MAIL_PASSWORD }} + subject: "⛔ [光湖系统] 自动修复三次失败 — 需要手动介入" + to: ${{ secrets.HUMAN_EMAIL }} + from: "光湖 Merge Watchdog " + html_body: | +

⛔ 合并指令看守者 · 三振出局通知

+

以下修复经过 3轮自动修复 仍未解决:

+ + + + +
PR#${{ needs.gate.outputs.pr_number }}
目标 Workflow${{ needs.gate.outputs.target_workflows }}
最新失败日志点击查看
+

📋 你需要做什么:

+
    +
  1. 打开上面的失败日志链接
  2. +
  3. 查看报错信息和上下文
  4. +
  5. 手动修复代码并推送到 main
  6. +
  7. 等待下一次 Workflow 运行验证
  8. +
+

看守者已暂停对该 Workflow 的自动修复。

+

— 光湖系统 · Merge Watchdog

+ + # -------- Pending 路径 -------- + - name: "⏰ 观察待续" + if: needs.observe.outputs.workflow_result == 'pending' + run: | + echo "[Merge Watchdog] ⏰ 初始观察窗口内未检测到目标 Workflow 运行" + echo "将在下次天眼夜间扫描中继续跟踪" diff --git a/.github/workflows/tianyan-nightly-scan.yml b/.github/workflows/tianyan-nightly-scan.yml new file mode 100644 index 00000000..d8105cd8 --- /dev/null +++ b/.github/workflows/tianyan-nightly-scan.yml @@ -0,0 +1,372 @@ +# ═══════════════════════════════════════════════ +# 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 +# 📜 Copyright: 国作登字-2026-A-00037559 +# ═══════════════════════════════════════════════ +# .github/workflows/tianyan-nightly-scan.yml +# 🌙 天眼夜间自动修复引擎 v3.0 +# +# Phase A: 天眼夜间巡查(扫描过去24h失败Workflow) +# Phase B: 报错智能分析(Gemini API) +# Phase C: 铸渊自动修复(生成修复代码) +# Phase D: 天眼审核(H1-H9 检查清单) +# Phase E: 创建 PR(等待冰朔晨间合并) +# Phase F: 更新仪表盘 +# +# 指令编号: ZY-TIANYAN-AUTOFIX-2026-0324-001 +# AG-ZY-087 + +name: "🌙 天眼夜间自动修复引擎 v3.0" + +on: + schedule: + - cron: '0 15 * * *' # 23:00 CST = 15:00 UTC + workflow_dispatch: + inputs: + force_scan: + description: '强制扫描(忽略时间窗口)' + type: boolean + default: false + repository_dispatch: + types: [watchdog-retry] + +permissions: + actions: read + contents: write + pull-requests: write + checks: read + +env: + REPAIR_BRANCH_PREFIX: "fix/auto-repair" + MAX_FILES_PER_REPAIR: 5 + MAX_LINES_PER_FILE: 50 + MAX_RETRY_ROUNDS: 2 + GEMINI_MODEL: "gemini-2.0-flash" + PROTECTED_PATTERNS: "tianyan-*.yml,CODEOWNERS,.github/FUNDING.yml" + +jobs: + # ======================================== + # Phase A: 天眼夜间巡查 + # ======================================== + nightly-scan: + runs-on: ubuntu-latest + outputs: + has_errors: ${{ steps.scan.outputs.has_errors }} + error_report: ${{ steps.scan.outputs.error_report }} + steps: + - name: "🔭 扫描过去24小时 Workflow 运行记录" + id: scan + env: + GH_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} + run: | + echo "[天眼 v3.0] Phase A 启动 — 夜间巡查扫描" + echo "═══════════════════════════════════════════" + SINCE=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ) + echo "📅 扫描起始时间: $SINCE" + + # 获取失败的 workflow runs + FAILED_RUNS=$(gh api \ + "repos/${{ github.repository }}/actions/runs?created=>$SINCE&status=failure&per_page=50" \ + --jq '.workflow_runs' 2>/dev/null || echo '[]') + + FAIL_COUNT=$(echo "$FAILED_RUNS" | jq 'length') + TOTAL_RUNS=$(gh api \ + "repos/${{ github.repository }}/actions/runs?created=>$SINCE&per_page=1" \ + --jq '.total_count' 2>/dev/null || echo '0') + + echo "📊 过去24小时统计:" + echo " 总运行数: $TOTAL_RUNS" + echo " 失败运行: $FAIL_COUNT" + + if [ "$FAIL_COUNT" -eq 0 ] || [ "$FAIL_COUNT" = "null" ]; then + echo "has_errors=false" >> $GITHUB_OUTPUT + echo "✅ 今日无异常" + else + echo "has_errors=true" >> $GITHUB_OUTPUT + + # 生成结构化错误报告 + ERROR_JSON=$(echo "$FAILED_RUNS" | jq -c '[.[] | { + workflow: .name, + run_id: .id, + conclusion: .conclusion, + html_url: .html_url, + created_at: .created_at, + head_branch: .head_branch + }]') + + # Use heredoc for multiline output + echo "error_report<> $GITHUB_OUTPUT + echo "$ERROR_JSON" >> $GITHUB_OUTPUT + echo "TIANYAN_EOF" >> $GITHUB_OUTPUT + + echo "⚠️ 发现 $FAIL_COUNT 个失败运行,进入 Phase B" + fi + + # ======================================== + # Phase B+C+D: 分析 → 修复 → 审核 + # ======================================== + analyze-and-repair: + needs: nightly-scan + if: needs.nightly-scan.outputs.has_errors == 'true' + runs-on: ubuntu-latest + outputs: + repair_branch: ${{ steps.repair.outputs.branch_name }} + repair_summary: ${{ steps.create_summary.outputs.summary }} + pr_needed: ${{ steps.review.outputs.pr_needed }} + fix_count: ${{ steps.review.outputs.fix_count }} + overall_risk: ${{ steps.review.outputs.overall_risk }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: "🧠 Phase B — Gemini 报错分析" + id: analyze + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + ERROR_REPORT: ${{ needs.nightly-scan.outputs.error_report }} + run: | + echo "[铸渊核心大脑] Phase B 启动 — 报错智能分析" + mkdir -p /tmp/tianyan + node .github/scripts/tianyan-analyze.js + + - name: "⚒️ Phase C — 铸渊自动修复" + id: repair + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GH_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} + run: | + echo "[铸渊核心大脑] Phase C 启动 — 自动修复" + + # Check if there are fixable errors + FIXABLE=$(cat /tmp/tianyan/analysis.json | jq '.auto_fixable // 0') + if [ "$FIXABLE" -eq 0 ]; then + echo "⚠️ 没有可自动修复的错误" + echo "branch_name=" >> $GITHUB_OUTPUT + exit 0 + fi + + # Create repair branch + BRANCH="${REPAIR_BRANCH_PREFIX}-$(date +%Y%m%d)-001" + git config user.name "zhuyuan-bot" + git config user.email "zhuyuan@guanghulab.com" + git checkout -b "$BRANCH" + + # Run repair script + node .github/scripts/tianyan-repair.js + + # Check if there are changes to push + if git diff --quiet && git diff --cached --quiet; then + echo "⚠️ 修复脚本未产生变更" + echo "branch_name=" >> $GITHUB_OUTPUT + else + git push origin "$BRANCH" || echo "⚠️ Push failed" + echo "branch_name=$BRANCH" >> $GITHUB_OUTPUT + fi + + - name: "🔭 Phase D — 天眼审核修复代码" + id: review + if: steps.repair.outputs.branch_name != '' + run: | + echo "[天眼 v3.0] Phase D 启动 — 审核修复代码" + node .github/scripts/tianyan-review.js + + # Read review result + if [ -f /tmp/tianyan/review-result.json ]; then + PR_NEEDED=$(cat /tmp/tianyan/review-result.json | jq -r '.pr_needed // false') + echo "pr_needed=$PR_NEEDED" >> $GITHUB_OUTPUT + + FIX_COUNT=$(cat /tmp/tianyan/repair-result.json | jq '.repaired // 0') + echo "fix_count=$FIX_COUNT" >> $GITHUB_OUTPUT + echo "overall_risk=low" >> $GITHUB_OUTPUT + else + echo "pr_needed=false" >> $GITHUB_OUTPUT + echo "fix_count=0" >> $GITHUB_OUTPUT + fi + + - name: "📝 生成 PR 摘要" + id: create_summary + if: steps.review.outputs.pr_needed == 'true' + run: | + DATE=$(date +%Y-%m-%d) + FIX_COUNT="${{ steps.review.outputs.fix_count }}" + RISK="${{ steps.review.outputs.overall_risk }}" + + SUMMARY=$(cat <<'SUMMARY_EOF' + ## 🌙 天眼夜间自动修复引擎 v3.0 + + **扫描时间:** SCAN_DATE + **修复数量:** FIX_NUM 个 + **风险等级:** RISK_LEVEL + **天眼审核:** ✅ H1-H9 全部通过 + + ### 天眼审核结果 + + - [x] H1 Secrets安全 + - [x] H2 文件完整性 + - [x] H3 依赖安全 + - [x] H4 权限最小化 + - [x] H5 天眼自保护 + - [x] H6 变更范围 + - [x] H7 Secrets引用完整性 + - [x] H8 注释规范 + - [x] H9 无残留调试代码 + + --- + > 🤖 此 PR 由铸渊夜间修复引擎自动创建 + > 📋 指令编号: ZY-TIANYAN-AUTOFIX-2026-0324-001 + > ⏰ 请冰朔在晨间审阅后合并 + SUMMARY_EOF + ) + + SUMMARY="${SUMMARY//SCAN_DATE/$DATE}" + SUMMARY="${SUMMARY//FIX_NUM/$FIX_COUNT}" + SUMMARY="${SUMMARY//RISK_LEVEL/$RISK}" + + echo "summary<> $GITHUB_OUTPUT + echo "$SUMMARY" >> $GITHUB_OUTPUT + echo "SUMMARY_DELIM" >> $GITHUB_OUTPUT + + - name: "🔄 Phase D 重试(如审核未通过)" + if: steps.review.outputs.pr_needed == 'false' && steps.repair.outputs.branch_name != '' + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + run: | + echo "[天眼 v3.0] 审核未通过 — 进入重试流程" + RETRY=0 + MAX_RETRY=2 + + while [ $RETRY -lt $MAX_RETRY ]; do + RETRY=$((RETRY + 1)) + echo "🔄 重试第 $RETRY 轮..." + + # Re-run repair with retry context + node .github/scripts/tianyan-repair.js + node .github/scripts/tianyan-review.js + + PR_NEEDED=$(cat /tmp/tianyan/review-result.json | jq -r '.pr_needed // false') + if [ "$PR_NEEDED" = "true" ]; then + echo "✅ 重试第 $RETRY 轮通过" + break + fi + done + + if [ "$PR_NEEDED" != "true" ]; then + echo "❌ $MAX_RETRY 轮重试后仍未通过 — 放弃自动修复" + # Clean up repair branch + git checkout main + git branch -D "${{ steps.repair.outputs.branch_name }}" 2>/dev/null || true + git push origin --delete "${{ steps.repair.outputs.branch_name }}" 2>/dev/null || true + fi + + # ======================================== + # Phase E+F: 创建 PR + 更新仪表盘 + # ======================================== + create-pr-and-dashboard: + needs: analyze-and-repair + if: needs.analyze-and-repair.outputs.pr_needed == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: main + + - name: "📦 Phase E — 创建 Pull Request" + id: create_pr + env: + GH_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} + BRANCH: ${{ needs.analyze-and-repair.outputs.repair_branch }} + SUMMARY: ${{ needs.analyze-and-repair.outputs.repair_summary }} + FIX_COUNT: ${{ needs.analyze-and-repair.outputs.fix_count }} + RISK: ${{ needs.analyze-and-repair.outputs.overall_risk }} + run: | + echo "[天眼 v3.0] Phase E — 创建 Pull Request" + + PR_TITLE="🌙 [AUTO-FIX] $(date +%Y-%m-%d) 天眼夜间自动修复 (${FIX_COUNT}个修复)" + + # Create labels if they don't exist + gh label create "auto-fix" --color "0E8A16" --description "自动修复 PR" 2>/dev/null || true + gh label create "tianyan-v3" --color "1D76DB" --description "天眼 v3.0 引擎" 2>/dev/null || true + + PR_URL=$(gh pr create \ + --base main \ + --head "$BRANCH" \ + --title "$PR_TITLE" \ + --body "$SUMMARY" \ + --label "auto-fix,tianyan-v3" 2>/dev/null || echo "") + + if [ -n "$PR_URL" ]; then + echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT + echo "✅ PR 已创建: $PR_URL" + + # Add risk label + if [ "$RISK" = "low" ]; then + gh label create "safe-to-merge" --color "0E8A16" 2>/dev/null || true + gh pr edit "$PR_URL" --add-label "safe-to-merge" 2>/dev/null || true + elif [ "$RISK" = "medium" ]; then + gh label create "review-carefully" --color "FBCA04" 2>/dev/null || true + gh pr edit "$PR_URL" --add-label "review-carefully" 2>/dev/null || true + fi + else + echo "⚠️ PR 创建失败" + echo "pr_url=" >> $GITHUB_OUTPUT + fi + + - name: "📊 Phase F — 更新仪表盘" + env: + GH_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} + PR_URL: ${{ steps.create_pr.outputs.pr_url }} + FIX_COUNT: ${{ needs.analyze-and-repair.outputs.fix_count }} + run: | + echo "[天眼 v3.0] Phase F — 更新仪表盘" + DATE=$(date +%Y-%m-%d) + TIME=$(date +%H:%M) + + # Update DASHBOARD.md status table + if [ -f DASHBOARD.md ]; then + sed -i "s/| 上次扫描 | .*/| 上次扫描 | $DATE $TIME CST |/" DASHBOARD.md + sed -i "s/| 扫描结果 | .*/| 扫描结果 | ⚠️ 发现错误,已自动修复 |/" DASHBOARD.md + sed -i "s/| 自动修复 | .*/| 自动修复 | ✅ ${FIX_COUNT} 个已修复 |/" DASHBOARD.md + + if [ -n "$PR_URL" ]; then + PR_NUM=$(echo "$PR_URL" | grep -oP '\d+$' || echo "?") + sed -i "s/| 待合并 PR | .*/| 待合并 PR | 🟡 PR #${PR_NUM} |/" DASHBOARD.md + fi + + git config user.name "tianyan-bot" + git config user.email "tianyan@guanghulab.com" + git add DASHBOARD.md + git diff --cached --quiet || git commit -m "[DASHBOARD] 更新天眼仪表盘 $DATE — ${FIX_COUNT}个修复待合并" + git push origin main || true + fi + + # ======================================== + # 无错误时的仪表盘更新 + # ======================================== + dashboard-no-errors: + needs: nightly-scan + if: needs.nightly-scan.outputs.has_errors == 'false' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: "✅ 更新仪表盘 — 今日无异常" + env: + GH_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} + run: | + DATE=$(date +%Y-%m-%d) + TIME=$(date +%H:%M) + echo "[天眼 v3.0] 今日无异常,更新仪表盘" + + if [ -f DASHBOARD.md ]; then + sed -i "s/| 上次扫描 | .*/| 上次扫描 | $DATE $TIME CST |/" DASHBOARD.md + sed -i "s/| 扫描结果 | .*/| 扫描结果 | ✅ 今日无异常 |/" DASHBOARD.md + sed -i "s/| 自动修复 | .*/| 自动修复 | — |/" DASHBOARD.md + sed -i "s/| 需人工处理 | .*/| 需人工处理 | — |/" DASHBOARD.md + + git config user.name "tianyan-bot" + git config user.email "tianyan@guanghulab.com" + git add DASHBOARD.md + git diff --cached --quiet || git commit -m "[DASHBOARD] 今日无异常 $DATE" + git push origin main || true + fi diff --git a/DASHBOARD.md b/DASHBOARD.md new file mode 100644 index 00000000..c4d673de --- /dev/null +++ b/DASHBOARD.md @@ -0,0 +1,63 @@ +## 🌙 天眼夜间修复引擎 · 仪表盘 + +> 📜 Copyright: 国作登字-2026-A-00037559 · TCS-0002∞ 冰朔 +> +> 引擎版本: v3.0 | 指令编号: ZY-TIANYAN-AUTOFIX-2026-0324-001 + +--- + +### 系统状态 + +| 指标 | 值 | +|------|----| +| 引擎版本 | v3.0 | +| 上次扫描 | 待首次运行 | +| 扫描结果 | ⏳ 等待首次扫描 | +| 自动修复 | — | +| 需人工处理 | — | +| 待合并 PR | — | + +### 👁️ Merge Watchdog 状态 + +| 指标 | 值 | +|------|----| +| 活跃看守 | 0 | +| 已确认修复 | 0 | +| 已升级(需人工) | 0 | + +### 最近7天修复历史 + +| 日期 | 扫描 | 发现 | 自动修复 | 人工处理 | PR | 看守状态 | +|------|------|------|----------|----------|----|----------| +| — | — | — | — | — | — | — | + +### 待处理事项 + +> ✅ 当前无待处理事项 + +--- + +### CRON 调度总览 + +| 时间 (CST) | 任务 | Workflow | 类型 | +|------------|------|----------|------| +| 04:00-04:10 | 每日微休眠 | 系统内置 | 休眠 | +| 08:30 | 天眼每日巡检 | tianyan-daily-patrol.yml | 巡检 | +| 周一 10:00 | Token主刷新A | renew-gdrive-tokens.yml | 生命线 | +| 12:15 每天 | Token安全网 | check-token-health.yml | 生命线 | +| 周四 10:00 | Token主刷新B | renew-gdrive-tokens.yml | 生命线 | +| 周六 20:00-00:00 | 周末大休眠 | 系统内置 | 休眠 | +| **23:00 每天** | **天眼夜间自动修复** | **tianyan-nightly-scan.yml** | **生命线** ⭐ | + +### 看守者五条铁律 + +1. 🛡️ 看守者自身不修改代码 — 只观察、记录、通知 +2. 🔒 最多重试 3 次 — 硬上限,不可覆盖 +3. 🔄 每轮修复策略必须不同 +4. 📧 邮件必须包含具体操作步骤 +5. 🤫 成功后绝对静默 — 只更新仪表盘 + +--- + +> 🤖 此仪表盘由铸渊夜间修复引擎自动维护 +> ⏰ 冰朔每日晨间审阅即可 diff --git a/docs/dashboard/watchdog-records.json b/docs/dashboard/watchdog-records.json new file mode 100644 index 00000000..8130eb4a --- /dev/null +++ b/docs/dashboard/watchdog-records.json @@ -0,0 +1,9 @@ +{ + "merge_watchdog": { + "last_updated": "2026-03-24T19:20:00+08:00", + "active_watches": 0, + "resolved_watches": 0, + "escalated_watches": 0 + }, + "records": [] +} diff --git a/skyeye/infra-manifest.json b/skyeye/infra-manifest.json index 03b9a6c8..34bf9576 100644 --- a/skyeye/infra-manifest.json +++ b/skyeye/infra-manifest.json @@ -124,6 +124,16 @@ "priority": "P0", "alert_on_failure": true, "alert_channels": ["notion-dashboard", "agent-bulletin"] + }, + { + "id": "NIGHTLY-REPAIR", + "name": "天眼夜间自动修复引擎 v3.0", + "workflow": "tianyan-nightly-scan.yml", + "hibernate_exempt": true, + "priority": "P0", + "alert_on_failure": true, + "alert_channels": ["dashboard", "agent-bulletin"], + "note": "周六休眠期内仅执行扫描+仪表盘更新,跳过修复/PR" } ] }, @@ -146,6 +156,28 @@ "renew_b": "每周四 10:00 CST (02:00 UTC)", "health_check": "每天 12:15 CST (04:15 UTC)" } + }, + "tianyan_nightly_repair": { + "service": "TianYan Nightly Auto-Repair Engine v3.0", + "plan": "Internal", + "description": "天眼夜间自动修复引擎 · 扫描→分析→修复→审核→PR→仪表盘 七阶段闭环", + "config": ".github/tianyan-config.json", + "scripts": { + "analyze": ".github/scripts/tianyan-analyze.js", + "repair": ".github/scripts/tianyan-repair.js", + "review": ".github/scripts/tianyan-review.js" + }, + "workflows": { + "nightly_scan": "tianyan-nightly-scan.yml", + "merge_watchdog": "merge-watchdog.yml" + }, + "dashboard": "DASHBOARD.md", + "watchdog_records": "docs/dashboard/watchdog-records.json", + "schedule": { + "nightly_scan": "每天 23:00 CST (15:00 UTC)", + "merge_watchdog": "事件驱动(PR合并时触发)" + }, + "agents": ["AG-ZY-087", "AG-ZY-088"] } } }