guanghulab/scripts/deploy-log-collector.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

669 lines
24 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
// ═══════════════════════════════════════════════
// 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 <path>
// 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 <path>');
console.log(' node deploy-log-collector.js dashboard');
break;
}
}
main().catch(err => {
console.error(`❌ 日志采集器错误: ${err.message}`);
process.exit(1);
});