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
This commit is contained in:
parent
4b1b33eecf
commit
d58a71d451
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue