From d58a71d4514d503d4b5f1f9dd04df79fed357451 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:50:40 +0000 Subject: [PATCH] Plan update: Include Merge Watchdog in scope Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/94378ee8-773d-4fc2-810a-e81a27aae9a9 --- .github/scripts/tianyan-analyze.js | 345 +++++++++++++++++++++++++++++ .github/tianyan-config.json | 63 ++++++ 2 files changed, 408 insertions(+) create mode 100644 .github/scripts/tianyan-analyze.js create mode 100644 .github/tianyan-config.json diff --git a/.github/scripts/tianyan-analyze.js b/.github/scripts/tianyan-analyze.js new file mode 100644 index 00000000..4fed2b17 --- /dev/null +++ b/.github/scripts/tianyan-analyze.js @@ -0,0 +1,345 @@ +#!/usr/bin/env node +// ═══════════════════════════════════════════════ +// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 +// 📜 Copyright: 国作登字-2026-A-00037559 +// ═══════════════════════════════════════════════ +// .github/scripts/tianyan-analyze.js +// Phase B — 报错智能分析引擎 +// +// 输入: 环境变量 ERROR_REPORT (JSON) +// 输出: /tmp/tianyan/analysis.json +// +// 调用 Gemini API 分析每个错误的根因、影响文件、修复方案、风险等级 + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +const ROOT = path.resolve(__dirname, '../..'); +const OUTPUT_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 classifyError(workflow, errorMessage) { + const msg = (errorMessage || '').toLowerCase(); + + if (msg.includes('json') && (msg.includes('parse') || msg.includes('syntax'))) return 'config_parse'; + if (msg.includes('yaml') && (msg.includes('parse') || msg.includes('syntax'))) return 'config_parse'; + if (msg.includes('401') || msg.includes('token expired') || msg.includes('unauthorized')) return 'token_expired'; + if (msg.includes('oauth') && msg.includes('failed')) return 'rclone_oauth_parse'; + if (msg.includes('rclone') && msg.includes('failed to create')) return 'rclone_oauth_parse'; + if (msg.includes('timeout') || msg.includes('etimedout') || msg.includes('503')) return 'network_timeout'; + if (msg.includes('403') || msg.includes('permission') || msg.includes('insufficient')) return 'permission_denied'; + if (msg.includes('command not found') || msg.includes('syntaxerror') || msg.includes('referenceerror')) return 'script_error'; + if (msg.includes('exit code') || msg.includes('process completed with exit code')) return 'script_error'; + + return 'unknown'; +} + +// ━━━ 判断是否可自动修复 ━━━ + +function isAutoFixable(errorType, config) { + if (config.error_classification.auto_fixable.includes(errorType)) return true; + return false; +} + +// ━━━ 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, + responseMimeType: 'application/json' + } + }); + + 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 || ''; + try { + resolve(JSON.parse(text)); + } catch { + resolve({ raw_response: text, parse_error: true }); + } + } catch (e) { + reject(new Error(`Gemini response parse error: ${e.message}`)); + } + }); + }); + + req.on('error', (e) => reject(e)); + req.setTimeout(30000, () => { + req.destroy(); + reject(new Error('Gemini API timeout')); + }); + req.write(body); + req.end(); + }); +} + +// ━━━ 获取仓库文件树(简化版) ━━━ + +function getRepoTree() { + const tree = []; + const wfDir = path.join(ROOT, '.github/workflows'); + const scriptsDir = path.join(ROOT, 'scripts'); + + try { + if (fs.existsSync(wfDir)) { + tree.push('.github/workflows/'); + fs.readdirSync(wfDir).filter(f => f.endsWith('.yml')).forEach(f => { + tree.push(` ${f}`); + }); + } + if (fs.existsSync(scriptsDir)) { + tree.push('scripts/'); + fs.readdirSync(scriptsDir).forEach(f => { + tree.push(` ${f}/`); + }); + } + } catch (e) { + tree.push(`(tree error: ${e.message})`); + } + + return tree.join('\n'); +} + +// ━━━ 分析单个错误 ━━━ + +async function analyzeError(error, config, treeOutput) { + const errorType = classifyError(error.workflow, error.error_message || ''); + const autoFixable = isAutoFixable(errorType, config); + + const systemPrompt = `你是铸渊核心大脑,零点原核频道的 CI/CD 工程人格。 +分析以下 GitHub Actions 报错日志,输出 JSON 格式: +{ + "root_cause": "错误根因描述", + "affected_files": ["文件路径列表"], + "fix_plan": "修复方案(具体到代码行级别)", + "risk": "low 或 medium 或 high", + "side_effects": "预计修复是否会影响其他 Workflow", + "auto_fixable": true或false +} +规则:只输出JSON,不要其他文字。`; + + const userPrompt = `Workflow: ${error.workflow} +Run ID: ${error.run_id} +Error type: ${errorType} +Error message: ${error.error_message || 'N/A'} +Log context: +${error.log_excerpt || 'No log excerpt available'} + +仓库文件结构: +${treeOutput}`; + + // If no Gemini API key, use local analysis + if (!process.env.GEMINI_API_KEY) { + console.log(` ⚠️ No GEMINI_API_KEY — using local classification for: ${error.workflow}`); + return { + workflow: error.workflow, + run_id: error.run_id, + error_type: errorType, + root_cause: `Classified as ${errorType} based on error patterns`, + affected_files: error.affected_files || [], + fix_plan: autoFixable ? `Auto-fix ${errorType} error` : 'Requires human intervention', + risk: autoFixable ? 'low' : 'high', + side_effects: 'none', + auto_fixable: autoFixable + }; + } + + try { + const result = await callGeminiAPI(userPrompt, systemPrompt, config); + + if (result.parse_error) { + console.log(` ⚠️ Gemini response not valid JSON for: ${error.workflow}`); + return { + workflow: error.workflow, + run_id: error.run_id, + error_type: errorType, + root_cause: 'Gemini analysis returned non-JSON', + affected_files: [], + fix_plan: 'Requires human review', + risk: 'high', + side_effects: 'unknown', + auto_fixable: false + }; + } + + // Merge Gemini analysis with local classification + return { + workflow: error.workflow, + run_id: error.run_id, + error_type: errorType, + root_cause: result.root_cause || 'Unknown', + affected_files: result.affected_files || [], + fix_plan: result.fix_plan || 'No plan generated', + risk: result.risk || 'high', + side_effects: result.side_effects || 'unknown', + auto_fixable: result.risk !== 'high' && autoFixable + }; + } catch (e) { + console.log(` ❌ Gemini API error for ${error.workflow}: ${e.message}`); + return { + workflow: error.workflow, + run_id: error.run_id, + error_type: errorType, + root_cause: `Analysis failed: ${e.message}`, + affected_files: [], + fix_plan: 'Requires human review', + risk: 'high', + side_effects: 'unknown', + auto_fixable: false + }; + } +} + +// ━━━ 主流程 ━━━ + +async function main() { + console.log('[铸渊核心大脑] Phase B 启动 — 报错智能分析'); + console.log('═══════════════════════════════════════════'); + + ensureDir(OUTPUT_DIR); + + // Load config + const config = loadJSON(CONFIG_PATH); + if (!config) { + console.error('❌ 无法加载 tianyan-config.json'); + process.exit(1); + } + + // Parse error report from env + const errorReportRaw = process.env.ERROR_REPORT; + if (!errorReportRaw) { + console.log('⚠️ ERROR_REPORT 环境变量为空,跳过分析'); + const emptyResult = { analyses: [], timestamp: getTimestamp(), total: 0, auto_fixable: 0 }; + fs.writeFileSync(path.join(OUTPUT_DIR, 'analysis.json'), JSON.stringify(emptyResult, null, 2)); + return; + } + + let errors; + try { + errors = JSON.parse(errorReportRaw); + } catch (e) { + console.error(`❌ ERROR_REPORT JSON 解析失败: ${e.message}`); + process.exit(1); + } + + if (!Array.isArray(errors)) { + errors = [errors]; + } + + console.log(`📊 待分析错误: ${errors.length} 个\n`); + + const treeOutput = getRepoTree(); + const analyses = []; + + for (const error of errors) { + console.log(`🔍 分析: ${error.workflow || error.name || 'unknown'} (run ${error.run_id || error.id || 'N/A'})`); + + // Normalize error object + const normalizedError = { + workflow: error.workflow || error.name || 'unknown', + run_id: error.run_id || error.id || 0, + error_message: error.error_message || error.conclusion || '', + log_excerpt: error.log_excerpt || '', + html_url: error.html_url || '', + created_at: error.created_at || '' + }; + + const analysis = await analyzeError(normalizedError, config, treeOutput); + analyses.push(analysis); + + const riskIcon = analysis.risk === 'low' ? '🟢' : analysis.risk === 'medium' ? '🟡' : '🔴'; + console.log(` ${riskIcon} Risk: ${analysis.risk} | Auto-fixable: ${analysis.auto_fixable}`); + console.log(` Root cause: ${analysis.root_cause}\n`); + } + + const result = { + timestamp: getTimestamp(), + total: analyses.length, + auto_fixable: analyses.filter(a => a.auto_fixable).length, + manual_only: analyses.filter(a => !a.auto_fixable).length, + analyses + }; + + // Risk filter: skip high-risk auto-fix + for (const a of result.analyses) { + if (a.risk === 'high') { + a.auto_fixable = false; + } + } + + const outputPath = path.join(OUTPUT_DIR, 'analysis.json'); + fs.writeFileSync(outputPath, JSON.stringify(result, null, 2)); + + console.log('═══════════════════════════════════════════'); + console.log(`📊 分析结果: 共 ${result.total} 个, 可自动修复 ${result.auto_fixable} 个, 需人工 ${result.manual_only} 个`); + console.log(`📁 结果保存: ${outputPath}`); + + // Output for GitHub Actions + console.log(`::set-output name=auto_fixable_count::${result.auto_fixable}`); + console.log(`::set-output name=total_errors::${result.total}`); +} + +main().catch(e => { + console.error(`❌ Phase B 异常: ${e.message}`); + process.exit(1); +}); diff --git a/.github/tianyan-config.json b/.github/tianyan-config.json new file mode 100644 index 00000000..5370e36b --- /dev/null +++ b/.github/tianyan-config.json @@ -0,0 +1,63 @@ +{ + "version": "3.0", + "engine": "tianyan-nightly-autofix", + "copyright": "国作登字-2026-A-00037559 · TCS-0002∞", + "schedule": { + "scan_time_utc": "15:00", + "scan_time_cst": "23:00", + "timezone": "Asia/Shanghai" + }, + "safety": { + "max_files_per_repair": 5, + "max_lines_per_file": 50, + "max_retry_rounds": 2, + "max_total_changed_lines": 200, + "protected_patterns": [ + "tianyan-*.yml", + "CODEOWNERS", + ".github/FUNDING.yml" + ], + "forbidden_operations": [ + "create_file", + "delete_file", + "modify_secrets_ref", + "modify_protected_file", + "push_to_main" + ] + }, + "error_classification": { + "auto_fixable": [ + "config_parse", + "rclone_oauth_parse", + "script_error" + ], + "partial_fixable": [ + "token_expired" + ], + "manual_only": [ + "network_timeout", + "permission_denied", + "unknown" + ] + }, + "gemini": { + "model": "gemini-2.0-flash", + "temperature": 0.1, + "max_output_tokens": 4096, + "response_format": "json" + }, + "dashboard": { + "file": "DASHBOARD.md", + "history_days": 7, + "alert_thresholds": { + "persistent_error_days": 3, + "urgent_alert_days": 7 + } + }, + "weekend_mode": { + "enabled": true, + "saturday_hibernation": "20:00-00:00", + "saturday_scan_only": true, + "skip_repair_on_saturday": true + } +}