feat: TianYan Nightly Auto-Repair Engine v3.0 + Merge Watchdog
New files: - .github/workflows/tianyan-nightly-scan.yml (Phase A-F main workflow) - .github/workflows/merge-watchdog.yml (PR merge tracking) - .github/scripts/tianyan-analyze.js (Phase B Gemini analysis) - .github/scripts/tianyan-repair.js (Phase C auto-repair) - .github/scripts/tianyan-review.js (Phase D H1-H9 review) - .github/scripts/merge-watchdog.js (watchdog state management) - .github/tianyan-config.json (v3.0 configuration) - DASHBOARD.md (repair engine dashboard) - docs/dashboard/watchdog-records.json (watchdog tracking) Updated files: - .github/persona-brain/agent-registry.json (AG-ZY-087, AG-ZY-088) - skyeye/infra-manifest.json (lifeline task + service registration) Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/94378ee8-773d-4fc2-810a-e81a27aae9a9
This commit is contained in:
parent
d58a71d451
commit
f55dd9d3dd
|
|
@ -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开始顺序分配",
|
||||
|
|
|
|||
|
|
@ -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 <action> [args]');
|
||||
console.log('Actions: register, success, failure, status, list');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
|
|
@ -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 <noreply@guanghulab.com>"
|
||||
html_body: |
|
||||
<h2>⛔ 合并指令看守者 · 三振出局通知</h2>
|
||||
<p>以下修复经过 <strong>3轮自动修复</strong> 仍未解决:</p>
|
||||
<table border="1" cellpadding="8">
|
||||
<tr><td><b>PR</b></td><td>#${{ needs.gate.outputs.pr_number }}</td></tr>
|
||||
<tr><td><b>目标 Workflow</b></td><td>${{ needs.gate.outputs.target_workflows }}</td></tr>
|
||||
<tr><td><b>最新失败日志</b></td><td><a href="${{ needs.observe.outputs.run_url }}">点击查看</a></td></tr>
|
||||
</table>
|
||||
<h3>📋 你需要做什么:</h3>
|
||||
<ol>
|
||||
<li>打开上面的失败日志链接</li>
|
||||
<li>查看报错信息和上下文</li>
|
||||
<li>手动修复代码并推送到 main</li>
|
||||
<li>等待下一次 Workflow 运行验证</li>
|
||||
</ol>
|
||||
<p>看守者已暂停对该 Workflow 的自动修复。</p>
|
||||
<p style="color:gray;">— 光湖系统 · Merge Watchdog</p>
|
||||
|
||||
# -------- Pending 路径 --------
|
||||
- name: "⏰ 观察待续"
|
||||
if: needs.observe.outputs.workflow_result == 'pending'
|
||||
run: |
|
||||
echo "[Merge Watchdog] ⏰ 初始观察窗口内未检测到目标 Workflow 运行"
|
||||
echo "将在下次天眼夜间扫描中继续跟踪"
|
||||
|
|
@ -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<<TIANYAN_EOF" >> $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<<SUMMARY_DELIM" >> $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
|
||||
|
|
@ -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. 🤫 成功后绝对静默 — 只更新仪表盘
|
||||
|
||||
---
|
||||
|
||||
> 🤖 此仪表盘由铸渊夜间修复引擎自动维护
|
||||
> ⏰ 冰朔每日晨间审阅即可
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue