/** * 🧊 冰朔人格体 · 自动部署诊断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 += `
\n点击展开成功列表(${categories.success.length} 个)\n\n`; for (const run of categories.success) { report += `- ✅ **${run.name}** · ${branchDisplayName(run.head_branch)} · ${formatTime(run.created_at)}\n`; } report += `\n
\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); });