298 lines
10 KiB
JavaScript
298 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Level 2: Weekly Analysis (Semi-Automatic)
|
|
*
|
|
* 주간 로그 분석 + 파라미터 조정 제안 생성
|
|
* - 자동 실행 (매주 일요일 23:00)
|
|
* - Discord로 제안 전송
|
|
* - 수동 승인 대기 (자동 적용 안 함)
|
|
*/
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { LogAnalyzer } = require('../lib/log-analyzer');
|
|
const { ParameterOptimizer } = require('../lib/parameter-optimizer');
|
|
|
|
// ============================================================================
|
|
// Configuration
|
|
// ============================================================================
|
|
|
|
const CONFIG = {
|
|
logPath: path.join(process.env.HOME, 'openclaw/logs/auto-retry.jsonl'),
|
|
outputDir: path.join(process.env.HOME, 'openclaw/logs/level2'),
|
|
discordWebhook: process.env.DISCORD_WEBHOOK_URL || process.env.OPENCLAW_DISCORD_WEBHOOK,
|
|
timeWindow: 7 * 24 * 3600 * 1000 // 7 days
|
|
};
|
|
|
|
// ============================================================================
|
|
// Discord Notification
|
|
// ============================================================================
|
|
|
|
async function sendDiscordReport(analysis, recommendations) {
|
|
if (!CONFIG.discordWebhook) {
|
|
console.log('⚠️ No Discord webhook configured, skipping notification');
|
|
return;
|
|
}
|
|
|
|
const https = require('https');
|
|
const url = new URL(CONFIG.discordWebhook);
|
|
|
|
// Build Discord embed
|
|
const embed = {
|
|
title: '📊 Level 2: Weekly Auto-Retry Analysis',
|
|
description: `Analysis period: ${Math.round(CONFIG.timeWindow / (24 * 3600 * 1000))} days`,
|
|
color: recommendations.length > 0 ? 0xFFA500 : 0x00FF00, // Orange if recs, green if none
|
|
fields: [],
|
|
footer: { text: 'Level 2 Parameter Tuning (Semi-Automatic)' },
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
// Overall summary
|
|
embed.fields.push({
|
|
name: '📈 Overall Summary',
|
|
value: [
|
|
`Total Executions: **${analysis.summary.overall.totalExecutions}**`,
|
|
`Success Rate: **${analysis.summary.overall.successRate}**`,
|
|
`Retry Rate: **${analysis.summary.overall.retryRate}**`,
|
|
`Failure Rate: **${analysis.summary.overall.failureRate}**`,
|
|
`Avg Duration: **${analysis.summary.overall.avgDuration}**`
|
|
].join('\n'),
|
|
inline: false
|
|
});
|
|
|
|
// Recommendations
|
|
if (recommendations.length > 0) {
|
|
const recText = recommendations.slice(0, 3).map((rec, i) => {
|
|
const icon = rec.severity === 'high' ? '🔴' : rec.severity === 'medium' ? '🟡' : '🟢';
|
|
const safeIcon = rec.safe ? '✅' : '⚠️';
|
|
return [
|
|
`${i + 1}. ${icon} **${rec.cron}**`,
|
|
` ${safeIcon} \`${rec.param}\`: ${rec.current} → **${rec.proposed}**`,
|
|
` 📝 ${rec.reason}`,
|
|
` 💡 ${rec.expectedImprovement}`,
|
|
` 🎯 Confidence: ${rec.confidence}`
|
|
].join('\n');
|
|
}).join('\n\n');
|
|
|
|
embed.fields.push({
|
|
name: `💡 Recommendations (${recommendations.length} total)`,
|
|
value: recText + (recommendations.length > 3 ? `\n\n_...and ${recommendations.length - 3} more_` : ''),
|
|
inline: false
|
|
});
|
|
|
|
// How to apply
|
|
embed.fields.push({
|
|
name: '🔧 How to Apply',
|
|
value: [
|
|
'```bash',
|
|
'# Review recommendations',
|
|
'cat ~/openclaw/logs/level2/recommendations-latest.json',
|
|
'',
|
|
'# Apply a specific recommendation',
|
|
'node ~/openclaw/scripts/apply-recommendation.js --id=0',
|
|
'```'
|
|
].join('\n'),
|
|
inline: false
|
|
});
|
|
} else {
|
|
embed.fields.push({
|
|
name: '✅ Status',
|
|
value: 'No optimization needed - all metrics within normal range',
|
|
inline: false
|
|
});
|
|
}
|
|
|
|
// Top errors (if any)
|
|
if (analysis.summary.topErrors.length > 0) {
|
|
const errorsText = analysis.summary.topErrors.slice(0, 3).map(e =>
|
|
`- **${e.category}** (${e.count}x): ${e.topType}`
|
|
).join('\n');
|
|
|
|
embed.fields.push({
|
|
name: '🚨 Top Errors',
|
|
value: errorsText,
|
|
inline: false
|
|
});
|
|
}
|
|
|
|
// Send to Discord
|
|
const message = { embeds: [embed] };
|
|
const data = JSON.stringify(message);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const options = {
|
|
hostname: url.hostname,
|
|
path: url.pathname,
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': data.length
|
|
}
|
|
};
|
|
|
|
const req = https.request(options, (res) => {
|
|
if (res.statusCode === 204) {
|
|
console.log('✅ Discord notification sent');
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`Discord returned ${res.statusCode}`));
|
|
}
|
|
});
|
|
|
|
req.on('error', reject);
|
|
req.write(data);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main Analysis
|
|
// ============================================================================
|
|
|
|
async function main() {
|
|
console.log('╔═══════════════════════════════════════════════════════════╗');
|
|
console.log('║ 📊 Level 2: Weekly Auto-Retry Analysis ║');
|
|
console.log('╚═══════════════════════════════════════════════════════════╝\n');
|
|
|
|
// 1. Ensure output directory exists
|
|
if (!fs.existsSync(CONFIG.outputDir)) {
|
|
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
|
|
}
|
|
|
|
// 2. Analyze logs
|
|
console.log('📖 Reading logs...');
|
|
const analyzer = new LogAnalyzer({
|
|
timeWindow: CONFIG.timeWindow,
|
|
minSampleSize: 5 // Low threshold for pattern detection, actual tuning has higher threshold
|
|
});
|
|
|
|
const analysis = await analyzer.analyze(CONFIG.logPath);
|
|
console.log(` ✅ Analyzed ${analysis.metadata.analyzedEntries} entries\n`);
|
|
|
|
// 3. Print summary
|
|
analyzer.printResults(analysis);
|
|
|
|
// 4. Generate recommendations
|
|
console.log('\n💡 Generating recommendations...');
|
|
const optimizer = new ParameterOptimizer({
|
|
aggressiveness: 'conservative',
|
|
requireStatisticalSignificance: true
|
|
});
|
|
|
|
const recommendations = optimizer.generateRecommendations(
|
|
analysis.patterns,
|
|
analysis.stats,
|
|
analysis.trends
|
|
);
|
|
|
|
console.log(` ✅ Generated ${recommendations.length} recommendation(s)\n`);
|
|
|
|
// 5. Print recommendations
|
|
if (recommendations.length > 0) {
|
|
console.log('═'.repeat(60));
|
|
console.log('💡 RECOMMENDATIONS');
|
|
console.log('═'.repeat(60) + '\n');
|
|
|
|
recommendations.forEach((rec, i) => {
|
|
const icon = rec.severity === 'high' ? '🔴' : rec.severity === 'medium' ? '🟡' : '🟢';
|
|
const safeIcon = rec.safe ? '✅ SAFE' : '⚠️ REVIEW REQUIRED';
|
|
|
|
console.log(`${i}. ${icon} ${rec.cron}`);
|
|
console.log(` Parameter: ${rec.param}`);
|
|
console.log(` Current: ${rec.current}`);
|
|
console.log(` Proposed: ${rec.proposed}`);
|
|
console.log(` Reason: ${rec.reason}`);
|
|
console.log(` Expected: ${rec.expectedImprovement}`);
|
|
console.log(` Severity: ${rec.severity}`);
|
|
console.log(` Confidence: ${rec.confidence}`);
|
|
console.log(` Safety: ${safeIcon}`);
|
|
if (rec.warning) {
|
|
console.log(` ⚠️ Warning: ${rec.warning}`);
|
|
}
|
|
if (rec.recommendation) {
|
|
console.log(` 📝 Note: ${rec.recommendation}`);
|
|
}
|
|
console.log('');
|
|
});
|
|
|
|
console.log('═'.repeat(60));
|
|
console.log('🔧 TO APPLY:');
|
|
console.log('═'.repeat(60));
|
|
console.log('');
|
|
console.log('# Review the recommendations');
|
|
console.log('cat ~/openclaw/logs/level2/recommendations-latest.json');
|
|
console.log('');
|
|
console.log('# Apply a specific recommendation (by index)');
|
|
console.log('node ~/openclaw/scripts/apply-recommendation.js --id=0');
|
|
console.log('');
|
|
console.log('# Apply all safe recommendations');
|
|
console.log('node ~/openclaw/scripts/apply-recommendation.js --all-safe');
|
|
console.log('');
|
|
} else {
|
|
console.log('✅ No recommendations - all metrics within normal range\n');
|
|
}
|
|
|
|
// 6. Save results
|
|
const timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0];
|
|
const outputPath = path.join(CONFIG.outputDir, `recommendations-${timestamp}.json`);
|
|
const latestPath = path.join(CONFIG.outputDir, 'recommendations-latest.json');
|
|
|
|
const output = {
|
|
timestamp: new Date().toISOString(),
|
|
analysis: {
|
|
summary: analysis.summary,
|
|
patterns: analysis.patterns,
|
|
trends: analysis.trends,
|
|
metadata: analysis.metadata
|
|
},
|
|
recommendations,
|
|
config: {
|
|
timeWindow: CONFIG.timeWindow,
|
|
aggressiveness: 'conservative'
|
|
}
|
|
};
|
|
|
|
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
|
|
fs.writeFileSync(latestPath, JSON.stringify(output, null, 2));
|
|
|
|
console.log(`📁 Saved to: ${outputPath}`);
|
|
console.log(`📁 Latest: ${latestPath}\n`);
|
|
|
|
// 7. Send Discord notification
|
|
try {
|
|
await sendDiscordReport(analysis, recommendations);
|
|
} catch (error) {
|
|
console.error('❌ Discord notification failed:', error.message);
|
|
}
|
|
|
|
// 8. Summary
|
|
console.log('╔═══════════════════════════════════════════════════════════╗');
|
|
console.log('║ ✅ Weekly Analysis Complete ║');
|
|
console.log('╚═══════════════════════════════════════════════════════════╝');
|
|
console.log('');
|
|
console.log('Next steps:');
|
|
if (recommendations.length > 0) {
|
|
console.log(' 1. Review recommendations in Discord or log file');
|
|
console.log(' 2. Apply selected recommendations manually');
|
|
console.log(' 3. Monitor results for 24-48 hours');
|
|
console.log(' 4. Rollback if needed');
|
|
} else {
|
|
console.log(' - Continue monitoring (no action needed)');
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
// ============================================================================
|
|
// Run
|
|
// ============================================================================
|
|
|
|
if (require.main === module) {
|
|
main().catch(err => {
|
|
console.error('❌ Error:', err.message);
|
|
console.error(err.stack);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { main };
|