#!/usr/bin/env node // ═══════════════════════════════════════════════ // 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 // 📜 Copyright: 国作登字-2026-A-00037559 // ═══════════════════════════════════════════════ // scripts/deploy-log-collector.js // 📡 铸渊全链路部署观测 · 日志采集器 // // 核心理念 (冰朔第三十一次对话): // 铸渊必须能看见自己写的代码部署后的运行状态 // 不依赖冰朔转述和截图 · 自主观测 · 自主修复 // // 功能: // 1. collect — 采集部署工作流的运行日志 (通过GitHub API) // 2. archive — 成功部署日志归档 + 经验入库 // 3. dashboard — 生成观测仪表盘 // // 用法: // node deploy-log-collector.js collect // node deploy-log-collector.js archive --log-file // node deploy-log-collector.js dashboard // // 环境变量: // GH_TOKEN — GitHub API令牌 (由Actions自动提供) // TRIGGER_RUN_ID — 触发此观测的部署工作流运行ID // TRIGGER_CONCLUSION — 部署工作流的结论 (success/failure/cancelled) // TRIGGER_WORKFLOW — 部署工作流的名称 // TRIGGER_HEAD_SHA — 部署的代码SHA // TRIGGER_HEAD_BRANCH — 部署的分支 // TRIGGER_ACTOR — 触发部署的操作者 // ═══════════════════════════════════════════════ 'use strict'; const https = require('https'); const http = require('http'); const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const LOGS_DIR = path.join(ROOT, 'data/deploy-logs'); const ARCHIVE_DIR = path.join(LOGS_DIR, 'archive'); const EXPERIENCE_DB = path.join(ROOT, 'brain/dev-experience/experience-db.json'); const ERROR_PATTERNS_DB = path.join(ROOT, 'brain/dev-experience/error-patterns.json'); // 确保目录存在 function ensureDir(dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } // 智能截断日志:保留头部和尾部,确保错误信息不丢失 function truncateLog(content, maxLen) { if (!content || content.length <= maxLen) return content; const headSize = Math.floor(maxLen * 0.3); // 30%给头部 const tailSize = maxLen - headSize - 100; // 70%给尾部(错误通常在末尾) const head = content.slice(0, headSize); const tail = content.slice(-tailSize); return `${head}\n\n... [截断 ${content.length - headSize - tailSize} 字符] ...\n\n${tail}`; } // ── GitHub API 请求 ──────────────────────────── function githubApi(endpoint) { const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN; if (!token) { throw new Error('需要 GH_TOKEN 或 GITHUB_TOKEN 环境变量'); } return new Promise((resolve, reject) => { const options = { hostname: 'api.github.com', path: endpoint, method: 'GET', headers: { 'User-Agent': 'ZY-Deploy-Observer/1.0', 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github.v3+json' }, timeout: 30000 }; const req = https.request(options, (res) => { let data = ''; res.on('data', (d) => { data += d; }); res.on('end', () => { try { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(JSON.parse(data)); } else { reject(new Error(`GitHub API ${res.statusCode}: ${data.slice(0, 300)}`)); } } catch (e) { reject(new Error(`GitHub API 响应解析失败: ${e.message}`)); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('GitHub API 超时')); }); req.end(); }); } // GitHub API 请求 (返回原始文本) function githubApiRaw(endpoint) { const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN; if (!token) { throw new Error('需要 GH_TOKEN 或 GITHUB_TOKEN 环境变量'); } return new Promise((resolve, reject) => { const options = { hostname: 'api.github.com', path: endpoint, method: 'GET', headers: { 'User-Agent': 'ZY-Deploy-Observer/1.0', 'Authorization': `Bearer ${token}`, 'Accept': 'application/vnd.github.v3+json' }, timeout: 30000 }; const req = https.request(options, (res) => { // 处理重定向 (GitHub日志URL会302到Azure Blob Storage) if (res.statusCode === 302 && res.headers.location) { const redirectUrl = new URL(res.headers.location); const redirectModule = redirectUrl.protocol === 'https:' ? https : http; const redirectReq = redirectModule.request(redirectUrl, (redirectRes) => { let data = ''; redirectRes.on('data', (d) => { data += d; }); redirectRes.on('end', () => resolve(data)); }); redirectReq.on('error', reject); redirectReq.end(); return; } let data = ''; res.on('data', (d) => { data += d; }); res.on('end', () => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(data); } else { reject(new Error(`GitHub API ${res.statusCode}: ${data.slice(0, 300)}`)); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('GitHub API 超时')); }); req.end(); }); } // ── 获取仓库信息 ──────────────────────────── function getRepoInfo() { const repo = process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab'; const [owner, name] = repo.split('/'); return { owner, name, full: repo }; } // ── 采集部署工作流日志 ──────────────────────── async function collectLogs() { console.log('📡 铸渊全链路部署观测 · 日志采集器启动'); console.log('═'.repeat(60)); const runId = process.env.TRIGGER_RUN_ID; const conclusion = process.env.TRIGGER_CONCLUSION; const workflowName = process.env.TRIGGER_WORKFLOW; const headSha = process.env.TRIGGER_HEAD_SHA; const headBranch = process.env.TRIGGER_HEAD_BRANCH; const actor = process.env.TRIGGER_ACTOR; if (!runId) { console.log('⚠️ 无触发工作流运行ID · 跳过采集'); setOutput('deploy_status', 'skipped'); setOutput('needs_repair', 'false'); return; } const repo = getRepoInfo(); console.log(`📋 观测目标:`); console.log(` 工作流: ${workflowName}`); console.log(` 运行ID: ${runId}`); console.log(` 结论: ${conclusion}`); console.log(` 提交: ${headSha?.slice(0, 8)}`); console.log(` 分支: ${headBranch}`); console.log(` 操作者: ${actor}`); console.log(''); // §1 获取工作流运行详情 let runDetails; try { runDetails = await githubApi(`/repos/${repo.full}/actions/runs/${runId}`); console.log(`✅ 运行详情获取成功`); } catch (err) { console.error(`❌ 获取运行详情失败: ${err.message}`); setOutput('deploy_status', 'error'); setOutput('needs_repair', 'false'); return; } // §2 获取工作流的所有Job信息 let jobs = []; try { const jobsData = await githubApi(`/repos/${repo.full}/actions/runs/${runId}/jobs`); jobs = jobsData.jobs || []; console.log(`✅ 获取到 ${jobs.length} 个Job`); } catch (err) { console.error(`⚠️ 获取Jobs失败: ${err.message}`); } // §3 采集失败Job的详细日志 const failedJobs = jobs.filter(j => j.conclusion === 'failure'); const jobLogs = []; for (const job of failedJobs) { console.log(`📋 采集失败Job日志: ${job.name} (ID: ${job.id})`); try { // 获取Job日志 (GitHub API返回日志下载URL) const logContent = await githubApiRaw( `/repos/${repo.full}/actions/jobs/${job.id}/logs` ); jobLogs.push({ job_id: job.id, job_name: job.name, conclusion: job.conclusion, started_at: job.started_at, completed_at: job.completed_at, steps: (job.steps || []).map(s => ({ name: s.name, status: s.status, conclusion: s.conclusion, number: s.number })), log_content: truncateLog(logContent, 10000) // 智能截断:保留头部和尾部 }); console.log(` ✅ 日志采集成功 (${logContent.length} 字符)`); } catch (err) { console.log(` ⚠️ 日志采集失败: ${err.message}`); jobLogs.push({ job_id: job.id, job_name: job.name, conclusion: job.conclusion, steps: (job.steps || []).map(s => ({ name: s.name, status: s.status, conclusion: s.conclusion, number: s.number })), log_content: `[日志获取失败: ${err.message}]` }); } } // §4 构建结构化日志记录 const timestamp = new Date().toISOString(); const logRecord = { _meta: { observer: '铸渊全链路部署观测系统 v1.0', copyright: '国作登字-2026-A-00037559', collected_at: timestamp }, run: { id: parseInt(runId), workflow_name: workflowName, conclusion: conclusion, status: runDetails.status, html_url: runDetails.html_url, head_sha: headSha, head_branch: headBranch, actor: actor, created_at: runDetails.created_at, updated_at: runDetails.updated_at, run_started_at: runDetails.run_started_at }, jobs_summary: { total: jobs.length, success: jobs.filter(j => j.conclusion === 'success').length, failure: failedJobs.length, skipped: jobs.filter(j => j.conclusion === 'skipped').length, cancelled: jobs.filter(j => j.conclusion === 'cancelled').length }, failed_jobs: jobLogs, all_jobs: jobs.map(j => ({ id: j.id, name: j.name, status: j.status, conclusion: j.conclusion, started_at: j.started_at, completed_at: j.completed_at })), analysis: { needs_repair: conclusion === 'failure', error_summary: '', matched_patterns: [], recommendation: '' } }; // §5 简单错误分析 (模式匹配) if (conclusion === 'failure' && jobLogs.length > 0) { const allLogContent = jobLogs.map(j => j.log_content).join('\n'); const analysis = simpleAnalysis(allLogContent); logRecord.analysis.error_summary = analysis.summary; logRecord.analysis.matched_patterns = analysis.patterns; logRecord.analysis.recommendation = analysis.recommendation; } // §6 保存结构化日志 ensureDir(LOGS_DIR); const logFileName = `deploy-${runId}-${timestamp.slice(0, 10)}.json`; const logFilePath = path.join(LOGS_DIR, logFileName); fs.writeFileSync(logFilePath, JSON.stringify(logRecord, null, 2) + '\n'); console.log(`\n💾 日志已保存: data/deploy-logs/${logFileName}`); // §7 更新最新日志索引 updateLatestIndex(logRecord); // §8 设置输出 setOutput('deploy_status', conclusion); setOutput('deploy_conclusion', conclusion); setOutput('log_file', `data/deploy-logs/${logFileName}`); setOutput('needs_repair', conclusion === 'failure' ? 'true' : 'false'); setOutput('workflow_name', workflowName); setOutput('run_id', runId); console.log(''); console.log('═'.repeat(60)); if (conclusion === 'success') { console.log('✅ 部署成功 · 日志已采集 · 等待归档'); } else if (conclusion === 'failure') { console.log('❌ 部署失败 · 日志已采集 · 启动自动修复流程'); } else { console.log(`ℹ️ 部署状态: ${conclusion} · 日志已采集`); } } // ── 简单错误分析 (模式匹配) ──────────────────── function simpleAnalysis(logContent) { // 基础错误模式 + 动态加载经验库中的错误模式 const patterns = [ { regex: /EADDRINUSE/i, name: '端口被占用', fix: '重启PM2进程释放端口', severity: 'medium' }, { regex: /ECONNREFUSED/i, name: '服务连接被拒绝', fix: '检查PM2进程是否运行', severity: 'high' }, { regex: /ENOMEM/i, name: '内存不足', fix: '重启服务或增加swap空间', severity: 'high' }, { regex: /ENOSPC/i, name: '磁盘空间不足', fix: '清理日志和临时文件', severity: 'critical' }, { regex: /nginx.*failed|nginx.*error/i, name: 'Nginx配置错误', fix: '运行nginx -t检查配置', severity: 'high' }, { regex: /502 Bad Gateway/i, name: '上游服务不可达', fix: '重启PM2后端进程', severity: 'high' }, { regex: /permission denied/i, name: '权限不足', fix: '检查文件权限和用户', severity: 'medium' }, { regex: /MODULE_NOT_FOUND/i, name: 'Node模块缺失', fix: '运行npm install', severity: 'high' }, { regex: /SyntaxError/i, name: 'JavaScript语法错误', fix: '检查最近代码变更', severity: 'high' }, { regex: /ssh.*connection.*refused|ssh.*timeout/i, name: 'SSH连接失败', fix: '检查服务器状态和SSH配置', severity: 'critical' }, { regex: /rsync.*error/i, name: '文件同步失败', fix: '检查rsync路径和权限', severity: 'high' }, { regex: /npm ERR!/i, name: 'npm安装错误', fix: '检查package.json和网络', severity: 'medium' }, { regex: /pm2.*error|pm2.*fail/i, name: 'PM2进程管理错误', fix: '重启PM2并检查ecosystem配置', severity: 'high' }, { regex: /ssl.*error|certificate/i, name: 'SSL证书问题', fix: '检查证书文件路径和有效期', severity: 'high' }, { regex: /HEALTH_FAIL/i, name: '健康检查失败', fix: '检查应用是否正常启动', severity: 'high' }, { regex: /exit code [1-9]/i, name: '命令执行失败', fix: '查看上方具体错误信息', severity: 'medium' } ]; // 尝试从经验数据库动态加载错误模式 try { const errPatternsDb = JSON.parse(fs.readFileSync(ERROR_PATTERNS_DB, 'utf8')); if (errPatternsDb.patterns) { for (const p of errPatternsDb.patterns) { // 提取经验库中的关键词作为匹配模式 const keywords = p.pattern.split(/[·\s+]/g).filter(k => k.length > 3); if (keywords.length > 0) { patterns.push({ regex: new RegExp(keywords.join('|'), 'i'), name: `[经验库] ${p.pattern}`, fix: (p.prevention || []).join(' → ') || '参考经验库', severity: p.severity || 'medium' }); } } } } catch { // 经验库加载失败不影响基础分析 } const matched = []; for (const p of patterns) { if (p.regex.test(logContent)) { matched.push({ name: p.name, fix: p.fix, severity: p.severity }); } } if (matched.length === 0) { return { summary: '未匹配到已知错误模式 · 需要深度分析', patterns: [], recommendation: '建议唤醒铸渊进行LLM深度推理分析' }; } const summary = matched.map(m => `[${m.severity}] ${m.name}`).join('; '); const hasCritical = matched.some(m => m.severity === 'critical'); const recommendation = hasCritical ? '存在严重问题 · 建议立即人工干预' : '副将可尝试自动修复: ' + matched.map(m => m.fix).join(' → '); return { summary, patterns: matched, recommendation }; } // ── 更新最新日志索引 ──────────────────────────── function updateLatestIndex(logRecord) { const indexFile = path.join(LOGS_DIR, 'latest-index.json'); let index; try { index = JSON.parse(fs.readFileSync(indexFile, 'utf8')); } catch { index = { _meta: { name: '铸渊部署观测 · 最新日志索引', copyright: '国作登字-2026-A-00037559' }, stats: { total_observed: 0, success: 0, failure: 0, repair_attempted: 0, repair_success: 0 }, recent_deploys: [] }; } // 更新统计 index.stats.total_observed++; if (logRecord.run.conclusion === 'success') index.stats.success++; if (logRecord.run.conclusion === 'failure') index.stats.failure++; // 追加到最近部署列表 (保留最近30条) index.recent_deploys.unshift({ run_id: logRecord.run.id, workflow: logRecord.run.workflow_name, conclusion: logRecord.run.conclusion, head_sha: logRecord.run.head_sha?.slice(0, 8), actor: logRecord.run.actor, observed_at: logRecord._meta.collected_at, error_summary: logRecord.analysis.error_summary || '', log_file: `deploy-${logRecord.run.id}-${logRecord._meta.collected_at.slice(0, 10)}.json` }); index.recent_deploys = index.recent_deploys.slice(0, 30); index.last_updated = new Date().toISOString(); fs.writeFileSync(indexFile, JSON.stringify(index, null, 2) + '\n'); console.log('✅ 最新日志索引已更新'); } // ── 归档成功部署日志 ──────────────────────────── async function archiveLogs() { const args = parseArgs(); const logFile = args['log-file']; console.log('📦 部署成功归档'); console.log('═'.repeat(60)); if (logFile) { const logPath = path.join(ROOT, logFile); if (fs.existsSync(logPath)) { // 移动到归档目录 ensureDir(ARCHIVE_DIR); const fileName = path.basename(logPath); const archivePath = path.join(ARCHIVE_DIR, fileName); fs.copyFileSync(logPath, archivePath); console.log(`✅ 日志已归档: data/deploy-logs/archive/${fileName}`); // 更新经验数据库 (记录成功部署) try { const logData = JSON.parse(fs.readFileSync(logPath, 'utf8')); updateExperienceDb(logData); } catch (err) { console.log(`⚠️ 经验数据库更新失败: ${err.message}`); } } else { console.log(`⚠️ 日志文件不存在: ${logFile}`); } } // 清理旧的非归档日志文件 (保留最近10个) cleanOldLogs(); } // ── 更新经验数据库 ──────────────────────────── function updateExperienceDb(logData) { if (!logData || !logData.run) return; try { const db = JSON.parse(fs.readFileSync(EXPERIENCE_DB, 'utf8')); const conclusion = logData.run.conclusion; // 更新统计 if (conclusion === 'success') { db.stats.success_count = (db.stats.success_count || 0) + 1; } else if (conclusion === 'failure') { db.stats.failed_count = (db.stats.failed_count || 0) + 1; } // 如果有匹配的错误模式,更新错误模式库 if (logData.analysis && logData.analysis.matched_patterns && logData.analysis.matched_patterns.length > 0) { updateErrorPatterns(logData.analysis.matched_patterns); } db._meta.last_updated = new Date().toISOString(); fs.writeFileSync(EXPERIENCE_DB, JSON.stringify(db, null, 2) + '\n'); console.log('✅ 经验数据库已更新'); } catch (err) { console.log(`⚠️ 经验数据库更新失败: ${err.message}`); } } // ── 更新错误模式库 ──────────────────────────── function updateErrorPatterns(matchedPatterns) { try { const db = JSON.parse(fs.readFileSync(ERROR_PATTERNS_DB, 'utf8')); for (const matched of matchedPatterns) { // 查找是否已存在此模式 const existing = db.patterns.find(p => p.pattern.toLowerCase().includes(matched.name.toLowerCase()) ); if (existing) { existing.occurrence_count = (existing.occurrence_count || 0) + 1; existing.last_seen = new Date().toISOString().slice(0, 10); } // 新错误模式不自动添加,需要铸渊审核 } db._meta.last_updated = new Date().toISOString(); fs.writeFileSync(ERROR_PATTERNS_DB, JSON.stringify(db, null, 2) + '\n'); console.log('✅ 错误模式库已更新'); } catch (err) { console.log(`⚠️ 错误模式库更新失败: ${err.message}`); } } // ── 清理旧日志 ──────────────────────────── function cleanOldLogs() { try { const files = fs.readdirSync(LOGS_DIR) .filter(f => f.startsWith('deploy-') && f.endsWith('.json')) .sort() .reverse(); // 保留最近10个日志文件 const toDelete = files.slice(10); for (const f of toDelete) { // 移动到归档而不是删除 const src = path.join(LOGS_DIR, f); ensureDir(ARCHIVE_DIR); const dest = path.join(ARCHIVE_DIR, f); if (!fs.existsSync(dest)) { fs.copyFileSync(src, dest); } fs.unlinkSync(src); } if (toDelete.length > 0) { console.log(`🗑️ 已归档 ${toDelete.length} 个旧日志文件`); } } catch (err) { console.log(`⚠️ 清理旧日志失败: ${err.message}`); } } // ── 生成观测仪表盘 ──────────────────────────── async function generateDashboard() { console.log('📊 生成铸渊部署观测仪表盘'); console.log('═'.repeat(60)); const indexFile = path.join(LOGS_DIR, 'latest-index.json'); const alertFile = path.join(LOGS_DIR, 'alert-status.json'); const repairFile = path.join(LOGS_DIR, 'repair-history.json'); let index, alerts, repairs; try { index = JSON.parse(fs.readFileSync(indexFile, 'utf8')); } catch { index = { stats: {}, recent_deploys: [] }; } try { alerts = JSON.parse(fs.readFileSync(alertFile, 'utf8')); } catch { alerts = { alerts: [] }; } try { repairs = JSON.parse(fs.readFileSync(repairFile, 'utf8')); } catch { repairs = { repairs: [] }; } const dashboard = { _meta: { name: '铸渊部署观测仪表盘', copyright: '国作登字-2026-A-00037559', generated_at: new Date().toISOString(), generator: 'scripts/deploy-log-collector.js' }, overview: { total_observed: index.stats.total_observed || 0, success: index.stats.success || 0, failure: index.stats.failure || 0, success_rate: index.stats.total_observed ? ((index.stats.success || 0) / index.stats.total_observed * 100).toFixed(1) + '%' : 'N/A', repair_attempted: index.stats.repair_attempted || 0, repair_success: index.stats.repair_success || 0, active_alerts: (alerts.alerts || []).filter(a => !a.resolved).length }, recent_deploys: (index.recent_deploys || []).slice(0, 10), active_alerts: (alerts.alerts || []).filter(a => !a.resolved).slice(0, 5), recent_repairs: (repairs.repairs || []).slice(0, 5) }; ensureDir(LOGS_DIR); const dashboardFile = path.join(LOGS_DIR, 'observer-dashboard.json'); fs.writeFileSync(dashboardFile, JSON.stringify(dashboard, null, 2) + '\n'); console.log(''); console.log('📊 仪表盘概览:'); console.log(` 总观测次数: ${dashboard.overview.total_observed}`); console.log(` 成功部署: ${dashboard.overview.success}`); console.log(` 失败部署: ${dashboard.overview.failure}`); console.log(` 成功率: ${dashboard.overview.success_rate}`); console.log(` 修复尝试: ${dashboard.overview.repair_attempted}`); console.log(` 修复成功: ${dashboard.overview.repair_success}`); console.log(` 活跃告警: ${dashboard.overview.active_alerts}`); console.log(''); console.log('✅ 仪表盘已更新: data/deploy-logs/observer-dashboard.json'); } // ── 工具函数 ──────────────────────────────── function setOutput(key, value) { if (process.env.GITHUB_OUTPUT) { fs.appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`); } } function parseArgs() { const args = process.argv.slice(2); const cmd = args[0]; const params = {}; for (let i = 1; i < args.length; i++) { if (args[i].startsWith('--')) { params[args[i].slice(2)] = args[i + 1] || ''; i++; } } return { cmd, ...params }; } // ── 主入口 ────────────────────────────────── async function main() { const args = parseArgs(); switch (args.cmd) { case 'collect': await collectLogs(); break; case 'archive': await archiveLogs(); break; case 'dashboard': await generateDashboard(); break; default: console.log('📡 铸渊全链路部署观测 · 日志采集器'); console.log(''); console.log('用法:'); console.log(' node deploy-log-collector.js collect'); console.log(' node deploy-log-collector.js archive --log-file '); console.log(' node deploy-log-collector.js dashboard'); break; } } main().catch(err => { console.error(`❌ 日志采集器错误: ${err.message}`); process.exit(1); });