guanghulab/.github/scripts/tianyan-analyze.js
2026-05-10 13:12:44 +08:00

348 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 (using GITHUB_OUTPUT env file)
if (process.env.GITHUB_OUTPUT) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `auto_fixable_count=${result.auto_fixable}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `total_errors=${result.total}\n`);
}
}
main().catch(e => {
console.error(`❌ Phase B 异常: ${e.message}`);
process.exit(1);
});