guanghulab/scripts/bingshuo-deploy-agent.js

493 lines
17 KiB
JavaScript
Raw Permalink Normal View History

/**
* 🧊 冰朔人格体 · 自动部署诊断Agent
*
* 功能
* 1. 自动检查所有 GitHub Actions 工作流运行状态
* 2. 诊断失败原因提取错误日志
* 3. 自动重新运行失败的工作流
* 4. 生成纯中文部署报告Issue 评论
*
* 触发方式
* - Issue 评论包含 "启动冰朔人格体"
* - workflow_dispatch 手动触发
* - Issue 模板创建
*
* 环境变量
* GITHUB_TOKEN - GitHub API 令牌
* GITHUB_REPOSITORY - owner/repo
* ISSUE_NUMBER - 可选回复到指定 Issue
* DEPLOY_ACTION - check | rerun | full默认 full
* HOURS_BACK - 检查最近多少小时的运行记录默认 24
*/
const https = require('https');
const fs = require('fs');
// === 配置 ===
const REPO = process.env.GITHUB_REPOSITORY || '';
const TOKEN = process.env.GITHUB_TOKEN || '';
const ISSUE_NUMBER = process.env.ISSUE_NUMBER || '';
const ISSUE_BODY = process.env.ISSUE_BODY || '';
// 从 Issue body 解析参数(支持 Issue 模板触发)
function parseActionFromBody(body) {
if (body.includes('只看报告')) return 'check';
if (body.includes('只重新运行')) return 'rerun';
return 'full';
}
function parseHoursFromBody(body) {
if (body.includes('最近一周')) return 168;
if (body.includes('72小时')) return 72;
if (body.includes('48小时')) return 48;
return 24;
}
const ACTION = process.env.DEPLOY_ACTION || parseActionFromBody(ISSUE_BODY);
const HOURS_BACK = parseInt(process.env.HOURS_BACK || String(parseHoursFromBody(ISSUE_BODY)), 10);
const [OWNER, REPO_NAME] = REPO.split('/');
if (!OWNER || !REPO_NAME || !TOKEN) {
console.error('❌ 缺少必要环境变量GITHUB_REPOSITORY, GITHUB_TOKEN');
process.exit(1);
}
// === GitHub API 请求封装 ===
function githubAPI(method, path, data) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path,
method,
headers: {
'Authorization': `Bearer ${TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'BingShuo-Deploy-Agent'
}
};
if (data) {
options.headers['Content-Type'] = 'application/json';
}
const req = https.request(options, (res) => {
let body = '';
res.on('data', chunk => { body += chunk; });
res.on('end', () => {
try {
const json = body ? JSON.parse(body) : {};
resolve({ status: res.statusCode, data: json });
} catch {
resolve({ status: res.statusCode, data: body });
}
});
});
req.on('error', reject);
if (data) req.write(JSON.stringify(data));
req.end();
});
}
// === 获取最近的工作流运行记录 ===
async function getRecentRuns() {
const since = new Date(Date.now() - HOURS_BACK * 60 * 60 * 1000).toISOString();
const result = await githubAPI('GET',
`/repos/${OWNER}/${REPO_NAME}/actions/runs?per_page=100&created=%3E${since}`
);
if (result.status !== 200) {
console.error('❌ 获取工作流运行记录失败:', result.status);
return [];
}
return result.data.workflow_runs || [];
}
// === 获取某次运行的作业详情 ===
async function getJobsForRun(runId) {
const result = await githubAPI('GET',
`/repos/${OWNER}/${REPO_NAME}/actions/runs/${runId}/jobs`
);
if (result.status !== 200) return [];
return result.data.jobs || [];
}
// === 从 jobs 中提取失败信息 ===
function extractFailureInfo(jobs) {
const failures = [];
for (const job of jobs) {
if (job.conclusion === 'failure') {
const failedSteps = (job.steps || []).filter(s => s.conclusion === 'failure');
failures.push({
jobName: job.name,
failedSteps: failedSteps.map(s => ({
name: s.name,
number: s.number
})),
startedAt: job.started_at,
completedAt: job.completed_at
});
}
}
return failures;
}
// === 重新运行失败的工作流 ===
async function rerunFailedJobs(runId) {
const result = await githubAPI('POST',
`/repos/${OWNER}/${REPO_NAME}/actions/runs/${runId}/rerun-failed-jobs`
);
return result.status === 201;
}
// === 分类工作流运行记录 ===
function categorizeRuns(runs) {
const categories = {
success: [],
failure: [],
cancelled: [],
inProgress: [],
actionRequired: [],
skipped: []
};
for (const run of runs) {
const status = run.status;
const conclusion = run.conclusion;
if (status === 'in_progress' || status === 'queued' || status === 'waiting') {
categories.inProgress.push(run);
} else if (conclusion === 'success') {
categories.success.push(run);
} else if (conclusion === 'failure') {
categories.failure.push(run);
} else if (conclusion === 'cancelled') {
categories.cancelled.push(run);
} else if (conclusion === 'action_required') {
categories.actionRequired.push(run);
} else {
categories.skipped.push(run);
}
}
return categories;
}
// === 格式化时间 ===
function formatTime(isoStr) {
if (!isoStr) return '未知';
const d = new Date(isoStr);
return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
}
// === 获取分支显示名称 ===
function branchDisplayName(branch) {
if (branch === 'main') return '主分支 (main)';
if (branch && branch.startsWith('copilot/')) return `Copilot分支 (${branch})`;
return branch || '未知';
}
// === 生成诊断报告 ===
async function generateReport(runs) {
const categories = categorizeRuns(runs);
const total = runs.length;
let report = `## 🧊 冰朔人格体 · 部署诊断报告\n\n`;
report += `> 📅 报告时间:${formatTime(new Date().toISOString())}\n`;
report += `> 📊 检查范围:最近 ${HOURS_BACK} 小时内的工作流运行记录\n\n`;
// --- 总览 ---
report += `### 📊 总览\n\n`;
report += `| 状态 | 数量 |\n|------|------|\n`;
report += `| ✅ 成功 | ${categories.success.length} |\n`;
report += `| ❌ 失败 | ${categories.failure.length} |\n`;
report += `| ⏳ 进行中 | ${categories.inProgress.length} |\n`;
report += `| ⚠️ 需要操作 | ${categories.actionRequired.length} |\n`;
report += `| 🚫 已取消 | ${categories.cancelled.length} |\n`;
report += `| 总计 | ${total} |\n\n`;
// --- 失败详情 ---
if (categories.failure.length > 0) {
report += `### ❌ 失败的工作流\n\n`;
report += `> 冰朔不用担心,下面是每个失败工作流的详细诊断:\n\n`;
for (let i = 0; i < categories.failure.length; i++) {
const run = categories.failure[i];
const jobs = await getJobsForRun(run.id);
const failureInfo = extractFailureInfo(jobs);
report += `#### ${i + 1}. ${run.name}\n\n`;
report += `- 🌿 分支:${branchDisplayName(run.head_branch)}\n`;
report += `- ⏰ 时间:${formatTime(run.created_at)}\n`;
report += `- 🔗 触发方式:${translateEvent(run.event)}\n`;
report += `- 📝 提交信息:${run.display_title || '无'}\n`;
if (failureInfo.length > 0) {
report += `- 💥 失败的作业:\n`;
for (const f of failureInfo) {
report += ` - **${f.jobName}**\n`;
if (f.failedSteps.length > 0) {
report += ` - 失败步骤:${f.failedSteps.map(s => `\`${s.name}\``).join('、')}\n`;
}
}
}
report += `- 💡 建议:${generateSuggestion(run, failureInfo)}\n\n`;
}
}
// --- 需要操作的工作流 ---
if (categories.actionRequired.length > 0) {
report += `### ⚠️ 需要操作的工作流\n\n`;
report += `> 这些工作流需要审批或配置才能继续运行:\n\n`;
for (const run of categories.actionRequired) {
report += `- **${run.name}** · ${branchDisplayName(run.head_branch)} · ${formatTime(run.created_at)}\n`;
}
report += `\n`;
}
// --- 正在进行的工作流 ---
if (categories.inProgress.length > 0) {
report += `### ⏳ 正在运行的工作流\n\n`;
for (const run of categories.inProgress) {
report += `- **${run.name}** · ${branchDisplayName(run.head_branch)} · 开始于 ${formatTime(run.created_at)}\n`;
}
report += `\n`;
}
// --- 成功的工作流(简洁列表)---
if (categories.success.length > 0) {
report += `### ✅ 成功的工作流\n\n`;
report += `<details>\n<summary>点击展开成功列表(${categories.success.length} 个)</summary>\n\n`;
for (const run of categories.success) {
report += `- ✅ **${run.name}** · ${branchDisplayName(run.head_branch)} · ${formatTime(run.created_at)}\n`;
}
report += `\n</details>\n\n`;
}
return report;
}
// === 翻译触发事件 ===
function translateEvent(event) {
const map = {
push: '代码推送',
pull_request: 'PR 合并请求',
workflow_dispatch: '手动触发',
schedule: '定时任务',
issues: 'Issue 创建',
issue_comment: 'Issue 评论',
dynamic: '自动触发'
};
return map[event] || event;
}
// === 生成修复建议 ===
function generateSuggestion(run, failureInfo) {
const name = run.name.toLowerCase();
const branch = run.head_branch || '';
// 根据工作流名称和失败信息给出建议
if (name.includes('pages') || name.includes('部署') || name.includes('deploy')) {
return '这是页面部署失败。通常是 docs/ 目录下文件格式问题。冰朔人格体可以尝试重新部署。';
}
if (name.includes('结构检查') || name.includes('check-structure')) {
return '模块结构检查失败,可能是开发者上传的模块缺少 README.md 文件。请通知对应开发者补充。';
}
if (name.includes('bridge') || name.includes('notion')) {
return '这是和 Notion 的同步失败。通常是 Notion API Token 过期或网络问题。建议稍后重试。';
}
if (name.includes('contract') || name.includes('hli')) {
return '接口契约检查失败。可能是新接口的 schema 不匹配。需要检查 src/schemas/ 下的定义文件。';
}
if (name.includes('模块文档') || name.includes('module-doc')) {
return '模块文档自动生成失败。通常是模块目录结构不符合规范。请检查最近上传的模块。';
}
// 根据失败步骤给出更具体的建议
if (failureInfo.length > 0) {
const stepNames = failureInfo.flatMap(f => f.failedSteps.map(s => s.name.toLowerCase()));
if (stepNames.some(s => s.includes('npm') || s.includes('install'))) {
return '依赖安装失败。可能是 package.json 配置问题或网络超时。建议重试。';
}
if (stepNames.some(s => s.includes('build'))) {
return '构建步骤失败。代码可能有语法错误。需要检查最近的代码变更。';
}
if (stepNames.some(s => s.includes('test'))) {
return '测试失败。可能是新代码引入了问题。需要开发者修复后重新提交。';
}
}
return '建议重新运行此工作流。如果持续失败,冰朔可以告诉铸渊具体情况,铸渊会帮你排查。';
}
// === 执行重新运行 ===
async function executeRerun(failedRuns) {
if (failedRuns.length === 0) {
return '\n### 🔄 重新运行\n\n没有需要重新运行的失败工作流。一切正常✨\n\n';
}
let report = `\n### 🔄 自动重新运行\n\n`;
report += `> 冰朔人格体正在自动重新运行失败的工作流...\n\n`;
let rerunCount = 0;
for (const run of failedRuns) {
// 只重试主分支的失败任务,避免重试无关分支
const success = await rerunFailedJobs(run.id);
if (success) {
report += `- ✅ 已重新触发:**${run.name}** (${branchDisplayName(run.head_branch)})\n`;
rerunCount++;
} else {
report += `- ⚠️ 无法重新触发:**${run.name}**(可能需要更高权限或该运行已过期)\n`;
}
}
report += `\n📌 已重新触发 ${rerunCount}/${failedRuns.length} 个工作流。预计 3-5 分钟后可查看结果。\n`;
report += `如果还是失败,冰朔可以再次启动冰朔人格体,我会继续诊断。\n\n`;
return report;
}
// === 生成总结 ===
function generateSummary(categories) {
let summary = `### 📋 总结\n\n`;
if (categories.failure.length === 0 && categories.actionRequired.length === 0) {
summary += `🎉 **所有工作流运行正常!** 冰朔不需要做任何操作。\n\n`;
summary += `系统健康状态:✅ 正常\n\n`;
} else if (categories.failure.length > 0) {
summary += `⚠️ 有 ${categories.failure.length} 个工作流失败。冰朔人格体已自动分析原因并尝试修复。\n\n`;
summary += `如果问题持续,冰朔可以:\n`;
summary += `1. 再次创建 Issue 触发冰朔人格体重新诊断\n`;
summary += `2. 把错误信息发给铸渊,铸渊会帮你定位具体代码问题\n\n`;
}
summary += `---\n`;
summary += `*—— 🧊 冰朔人格体 · 自动部署诊断Agent*\n`;
summary += `*由铸渊为冰朔专属开发 · 纯语言驱动,无需点击任何按钮*\n`;
return summary;
}
// === 发布报告到 Issue ===
async function postReport(report) {
if (ISSUE_NUMBER) {
// 回复到已有 Issue
await githubAPI('POST',
`/repos/${OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/comments`,
{ body: report }
);
// 添加标签
await githubAPI('POST',
`/repos/${OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/labels`,
{ labels: ['🧊deploy-report', '✅answered'] }
);
// 移除 pending 标签如果不存在会返回404属正常情况
const removeResult = await githubAPI('DELETE',
`/repos/${OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/labels/${encodeURIComponent('pending')}`
);
if (removeResult.status !== 200 && removeResult.status !== 404) {
console.warn('⚠️ 移除 pending 标签时出现异常:', removeResult.status);
}
console.log(`✅ 报告已发布到 Issue #${ISSUE_NUMBER}`);
} else {
// 创建新 Issue
const result = await githubAPI('POST',
`/repos/${OWNER}/${REPO_NAME}/issues`,
{
title: `🧊 冰朔人格体 · 部署诊断报告 · ${new Date().toLocaleDateString('zh-CN')}`,
body: report,
labels: ['🧊deploy-report']
}
);
if (result.status === 201) {
console.log(`✅ 诊断报告已创建Issue #${result.data.number}`);
} else {
console.error('❌ 创建 Issue 失败:', result.status);
}
}
}
// === 主流程 ===
async function main() {
console.log('🧊 冰朔人格体启动...');
console.log(`📊 检查范围:最近 ${HOURS_BACK} 小时`);
console.log(`🔧 执行模式:${ACTION}`);
// 1. 获取最近的工作流运行记录
const runs = await getRecentRuns();
console.log(`📋 获取到 ${runs.length} 条运行记录`);
if (runs.length === 0) {
const report = `## 🧊 冰朔人格体 · 部署诊断报告\n\n` +
`最近 ${HOURS_BACK} 小时内没有任何工作流运行记录。系统很安静。✨\n\n` +
`---\n*—— 🧊 冰朔人格体*`;
await postReport(report);
return;
}
// 2. 分类
const categories = categorizeRuns(runs);
// 3. 生成诊断报告
let report = '';
if (ACTION === 'check' || ACTION === 'full') {
report += await generateReport(runs);
}
// 4. 如果是 full 模式,自动重新运行失败的工作流
if (ACTION === 'full' || ACTION === 'rerun') {
report += await executeRerun(categories.failure);
}
// 5. 总结
report += generateSummary(categories);
// 6. 发布报告
await postReport(report);
// 7. 更新 memory.json
await updateMemory(categories);
console.log('🧊 冰朔人格体任务完成。');
}
// === 更新铸渊记忆 ===
async function updateMemory(categories) {
try {
const memoryPath = '.github/brain/memory.json';
if (!fs.existsSync(memoryPath)) return;
const memory = JSON.parse(fs.readFileSync(memoryPath, 'utf8'));
// 添加事件记录
if (!memory.recent_events) memory.recent_events = [];
memory.recent_events.unshift({
timestamp: new Date().toISOString(),
type: 'bingshuo_deploy_agent',
result: categories.failure.length === 0 ? 'all_pass' : 'has_failures',
details: {
total: categories.success.length + categories.failure.length + categories.inProgress.length,
success: categories.success.length,
failure: categories.failure.length,
in_progress: categories.inProgress.length
}
});
// 保留最近100条
memory.recent_events = memory.recent_events.slice(0, 100);
fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2) + '\n');
console.log('📝 铸渊记忆已更新');
} catch (err) {
console.warn('⚠️ 更新记忆失败(不影响报告):', err.message);
}
}
// === 执行 ===
main().catch(err => {
console.error('❌ 冰朔人格体执行失败:', err.message);
process.exit(1);
});