270 lines
8.1 KiB
JavaScript
270 lines
8.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Daily Self-Check Script
|
|
*
|
|
* Purpose: Quick daily review of yesterday's self-evaluations
|
|
* Runs: Daily at 06:00
|
|
*
|
|
* Features:
|
|
* 1. Review yesterday's self-review entries
|
|
* 2. Compare with previous 3 days
|
|
* 3. Detect immediate repetition (< 3 days)
|
|
* 4. Send instant alert if pattern repeats
|
|
*
|
|
* Difference from detect-patterns.js:
|
|
* - detect-patterns.js: Deep analysis, 7-day scan, 3+ threshold
|
|
* - daily-self-check.js: Quick check, 4-day window, instant feedback
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const https = require('https');
|
|
|
|
// ============================================================================
|
|
// Configuration
|
|
// ============================================================================
|
|
|
|
const CONFIG = {
|
|
// Paths
|
|
MEMORY_DIR: path.join(process.env.HOME, 'openclaw', 'memory'),
|
|
|
|
// Check window
|
|
DAYS_TO_CHECK: 4, // Yesterday + previous 3 days
|
|
|
|
// Similarity
|
|
SIMILARITY_THRESHOLD: 0.65, // Slightly higher for faster detection
|
|
|
|
// Discord
|
|
DISCORD_WEBHOOK: 'https://discord.com/api/webhooks/1469274752517537960/8qaedz2GgFICpNGME316opmDNuiMrw72ZHPGSr83iCOi-H_uvr6h5V-EKGqjQ_AjrAPh',
|
|
};
|
|
|
|
// ============================================================================
|
|
// Utilities (imported from detect-patterns.js)
|
|
// ============================================================================
|
|
|
|
function getLastNDays(n) {
|
|
const dates = [];
|
|
for (let i = 0; i < n; i++) {
|
|
const date = new Date();
|
|
date.setDate(date.getDate() - i);
|
|
dates.push(date.toISOString().split('T')[0]);
|
|
}
|
|
return dates;
|
|
}
|
|
|
|
function extractFailures(content) {
|
|
const failures = [];
|
|
const regex = /\*\*이번 실패\/미흡\*\*[^\n]*\n([\s\S]*?)(?=\n\s*│\s*\*\*|╰|$)/g;
|
|
let match;
|
|
|
|
while ((match = regex.exec(content)) !== null) {
|
|
const section = match[1];
|
|
const bullets = section.match(/│?\s*[•\-\*]\s*(.+)/g);
|
|
if (bullets) {
|
|
bullets.forEach(bullet => {
|
|
const text = bullet.replace(/│?\s*[•\-\*]\s*/, '').trim();
|
|
if (text && text !== '[구체적 사항]' && text.length > 5) {
|
|
failures.push(text);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return failures;
|
|
}
|
|
|
|
function extractKeywords(text) {
|
|
const stopwords = ['이', '그', '저', '것', '수', '등', '및', '를', '을', '가', '이', '은', '는', '의', '에', '와', '과'];
|
|
|
|
const words = text
|
|
.toLowerCase()
|
|
.replace(/[^\w가-힣\s]/g, ' ')
|
|
.split(/\s+/)
|
|
.filter(w => w.length > 1 && !stopwords.includes(w));
|
|
|
|
return [...new Set(words)];
|
|
}
|
|
|
|
function calculateSimilarity(text1, text2) {
|
|
const keywords1 = new Set(extractKeywords(text1));
|
|
const keywords2 = new Set(extractKeywords(text2));
|
|
|
|
const intersection = new Set([...keywords1].filter(k => keywords2.has(k)));
|
|
const union = new Set([...keywords1, ...keywords2]);
|
|
|
|
return union.size > 0 ? intersection.size / union.size : 0;
|
|
}
|
|
|
|
async function sendDiscordAlert(message) {
|
|
const data = JSON.stringify(message);
|
|
const url = new URL(CONFIG.DISCORD_WEBHOOK);
|
|
|
|
const options = {
|
|
hostname: url.hostname,
|
|
path: url.pathname + url.search,
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': data.length
|
|
}
|
|
};
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const req = https.request(options, (res) => {
|
|
if (res.statusCode === 204) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`Discord API returned ${res.statusCode}`));
|
|
}
|
|
});
|
|
|
|
req.on('error', reject);
|
|
req.write(data);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main Logic
|
|
// ============================================================================
|
|
|
|
async function main() {
|
|
console.log('🌅 Daily Self-Check');
|
|
console.log('===================\n');
|
|
|
|
const dates = getLastNDays(CONFIG.DAYS_TO_CHECK);
|
|
const yesterday = dates[0];
|
|
|
|
console.log(`Yesterday: ${yesterday}`);
|
|
console.log(`Comparing with: ${dates.slice(1).join(', ')}\n`);
|
|
|
|
// 1. Load yesterday's failures
|
|
const yesterdayFile = path.join(CONFIG.MEMORY_DIR, `self-review-${yesterday}.md`);
|
|
|
|
if (!fs.existsSync(yesterdayFile)) {
|
|
console.log(`⏭️ No self-review file for ${yesterday}`);
|
|
console.log('This is expected if no cron jobs ran yesterday.\n');
|
|
return;
|
|
}
|
|
|
|
const yesterdayContent = fs.readFileSync(yesterdayFile, 'utf8');
|
|
const yesterdayFailures = extractFailures(yesterdayContent);
|
|
|
|
console.log(`Yesterday's failures: ${yesterdayFailures.length}\n`);
|
|
|
|
if (yesterdayFailures.length === 0) {
|
|
console.log('✅ No failures recorded yesterday. Great job!');
|
|
return;
|
|
}
|
|
|
|
// 2. Load previous days' failures
|
|
const previousFailures = [];
|
|
|
|
for (const date of dates.slice(1)) {
|
|
const filepath = path.join(CONFIG.MEMORY_DIR, `self-review-${date}.md`);
|
|
|
|
if (fs.existsSync(filepath)) {
|
|
const content = fs.readFileSync(filepath, 'utf8');
|
|
const failures = extractFailures(content);
|
|
|
|
failures.forEach(text => {
|
|
previousFailures.push({ date, text });
|
|
});
|
|
|
|
console.log(` ${date}: ${failures.length} failures`);
|
|
} else {
|
|
console.log(` ${date}: (no file)`);
|
|
}
|
|
}
|
|
|
|
console.log(`\nPrevious failures total: ${previousFailures.length}\n`);
|
|
|
|
// 3. Check for repetitions
|
|
const repetitions = [];
|
|
|
|
for (const yesterdayFailure of yesterdayFailures) {
|
|
for (const prevFailure of previousFailures) {
|
|
const similarity = calculateSimilarity(yesterdayFailure, prevFailure.text);
|
|
|
|
if (similarity >= CONFIG.SIMILARITY_THRESHOLD) {
|
|
repetitions.push({
|
|
yesterday: yesterdayFailure,
|
|
previous: prevFailure,
|
|
similarity: similarity.toFixed(2)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`Repetitions detected: ${repetitions.length}\n`);
|
|
|
|
// 4. Alert if repetitions found
|
|
if (repetitions.length > 0) {
|
|
console.log('⚠️ REPEATED FAILURES:\n');
|
|
|
|
for (const rep of repetitions) {
|
|
console.log(` Yesterday: "${rep.yesterday.slice(0, 60)}..."`);
|
|
console.log(` Previous (${rep.previous.date}): "${rep.previous.text.slice(0, 60)}..."`);
|
|
console.log(` Similarity: ${(rep.similarity * 100).toFixed(0)}%\n`);
|
|
}
|
|
|
|
// Send Discord notification
|
|
try {
|
|
await sendDiscordAlert({
|
|
embeds: [{
|
|
title: '⚠️ 일일 체크: 반복 실패 감지',
|
|
description: `어제(${yesterday}) 기록된 실패/미흡 중 **${repetitions.length}건**이 최근 3일 내 반복되었습니다.`,
|
|
color: 0xFFA500, // Orange
|
|
fields: repetitions.slice(0, 3).map(rep => ({
|
|
name: `반복 패턴 (${rep.previous.date} → ${yesterday})`,
|
|
value: `\`\`\`${rep.yesterday.slice(0, 150)}${rep.yesterday.length > 150 ? '...' : ''}\`\`\``,
|
|
inline: false
|
|
})).concat([{
|
|
name: '권장 조치',
|
|
value: [
|
|
'1. 근본 원인 파악 필요',
|
|
'2. 즉시 개선 항목 재검토',
|
|
'3. 패턴 탐지 스크립트 실행: `node ~/openclaw/scripts/detect-patterns.js`'
|
|
].join('\n'),
|
|
inline: false
|
|
}]),
|
|
footer: {
|
|
text: 'Daily Self-Check V1.0'
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
}]
|
|
});
|
|
|
|
console.log('✅ Discord alert sent\n');
|
|
} catch (e) {
|
|
console.error(`❌ Failed to send Discord alert: ${e.message}\n`);
|
|
}
|
|
} else {
|
|
console.log('✅ No repetitions detected. All failures are new.\n');
|
|
}
|
|
|
|
// 5. Summary
|
|
console.log('===================');
|
|
console.log(`Summary: ${yesterdayFailures.length} failures yesterday, ${repetitions.length} repeated`);
|
|
|
|
if (repetitions.length > 0) {
|
|
console.log('\n💡 Next steps:');
|
|
console.log(' 1. Review repeated failures');
|
|
console.log(' 2. Update "즉시 개선" actions');
|
|
console.log(' 3. Consider logging to .learnings/');
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Run
|
|
// ============================================================================
|
|
|
|
if (require.main === module) {
|
|
main().catch(err => {
|
|
console.error('❌ Fatal error:', err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { main };
|