guanghulab/scripts/bingshuo-deploy-agent.js
2026-05-10 13:12:44 +08:00

493 lines
17 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.

/**
* 🧊 冰朔人格体 · 自动部署诊断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);
});