Files

290 lines
10 KiB
Bash

#!/bin/bash
# Morning Briefing (통합) - 매일 아침 08:00 자동 실행
# 스탠드업 + 시스템 상태를 하나로 합쳐 Discord 전송
#
# 데이터 소스:
# 1. 시장 현황 (TQQQ 시세 - auto-retry 로그에서 추출)
# 2. Git log (어제 커밋)
# 3. Auto-retry 로그 (어제 실행 요약)
# 4. 인프라 메트릭 (Gateway, 메모리, CPU, 지연, 업타임)
# 5. Google Tasks (미완료 할 일)
# 6. Google Calendar (오늘 일정)
# 7. 블로커 감지 (연속 실패 + 인프라 이상 + 시장 급변)
set -euo pipefail
# .env에서 웹훅 로드
if [ -f "$HOME/openclaw/.env" ]; then
source "$HOME/openclaw/.env"
fi
WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
OPENCLAW_DIR="$HOME/openclaw"
LOG_FILE="$OPENCLAW_DIR/logs/auto-retry.jsonl"
METRICS_FILE="$HOME/.openclaw/metrics/current.prom"
HISTORY_LOG="$HOME/.openclaw/metrics/history.csv"
GOG="$(which gog 2>/dev/null || echo '/opt/homebrew/bin/gog')"
GOG_ACCOUNT="yuiopnm1931@gmail.com"
TASKS_LIST_ID="MDE3MjE5NzU0MjA3NTAxOTg4ODc6MDow"
TODAY=$(date '+%Y-%m-%d')
YESTERDAY=$(date -v-1d '+%Y-%m-%d')
DAY_KR=$(date '+%a')
case "$DAY_KR" in
Mon) DAY_KR="월" ;; Tue) DAY_KR="화" ;; Wed) DAY_KR="수" ;;
Thu) DAY_KR="목" ;; Fri) DAY_KR="금" ;; Sat) DAY_KR="토" ;; Sun) DAY_KR="일" ;;
esac
echo "📋 Morning Briefing 생성 중... ($TODAY)"
# ============================================================================
# 1. 시장 현황 - TQQQ / SOXL / NVDA (yf 직접 호출)
# (stock-briefing-with-retry.js 통합)
# ============================================================================
YF_CMD="$HOME/openclaw/skills/yahoo-finance/yf"
SYMBOLS="TQQQ SOXL NVDA"
MARKET_SECTION=""
parse_yf_oneline() {
# yf 테이블 출력에서 한줄 요약 추출
python3 -c "
import sys, re
out = sys.stdin.read()
sym = '${1}'
price = re.search(r'현재가 \(USD\)\s*│\s*(\\\$[\d.]+)', out)
change = re.search(r'변동 \(전일比\)\s*│\s*([^│]+)', out)
p = price.group(1).strip() if price else '?'
c = change.group(1).strip() if change else '?'
pct = re.search(r'([-+]?[\d.]+)%', c)
pct_val = abs(float(pct.group(1))) if pct else 0
alert = ''
if pct_val >= 5:
alert = ' 🚨'
elif pct_val >= 3:
alert = ' ⚠️'
print(f' {sym}: {p} {c}{alert}')
" 2>/dev/null
}
for SYM in $SYMBOLS; do
YF_OUT=$("$YF_CMD" "$SYM" 2>/dev/null || echo "")
if [ -n "$YF_OUT" ]; then
LINE=$(echo "$YF_OUT" | parse_yf_oneline "$SYM")
MARKET_SECTION="${MARKET_SECTION}${LINE}\n"
else
MARKET_SECTION="${MARKET_SECTION} ${SYM}: 조회 실패\n"
fi
done
# 환율 (마지막 yf 출력에서 추출)
if [ -n "$YF_OUT" ]; then
FX=$(echo "$YF_OUT" | grep "환율" | sed 's/.*│[[:space:]]*//' | sed 's/[[:space:]]*│.*//' | tr -d ' ')
[ -n "$FX" ] && MARKET_SECTION="${MARKET_SECTION} 💱 ${FX}\n"
fi
# ============================================================================
# 2. 어제 한 일 - Git Commits
# ============================================================================
GIT_COMMITS=""
if [ -d "$OPENCLAW_DIR/.git" ]; then
GIT_COMMITS=$(cd "$OPENCLAW_DIR" && git log --since="yesterday 00:00" --until="today 00:00" --oneline --all 2>/dev/null || echo "")
fi
COMMITS_SECTION=""
if [ -n "$GIT_COMMITS" ]; then
while IFS= read -r line; do
MSG=$(echo "$line" | sed 's/^[a-f0-9]* //')
COMMITS_SECTION="${COMMITS_SECTION}${MSG}\n"
done <<< "$GIT_COMMITS"
else
COMMITS_SECTION=" • 커밋 없음\n"
fi
# ============================================================================
# 3. 어제 Auto-Retry 로그 요약
# ============================================================================
RETRY_SECTION=""
if [ -f "$LOG_FILE" ]; then
TOTAL=$(grep "\"$YESTERDAY" "$LOG_FILE" 2>/dev/null | wc -l | tr -d ' ')
SUCCESS=$(grep "\"$YESTERDAY" "$LOG_FILE" 2>/dev/null | grep '"type":"success"' | wc -l | tr -d ' ')
FAIL=$(grep "\"$YESTERDAY" "$LOG_FILE" 2>/dev/null | grep '"type":"failure"' | wc -l | tr -d ' ')
if [ "$TOTAL" -gt 0 ]; then
RATE=$(echo "scale=0; $SUCCESS * 100 / $TOTAL" | bc)
RETRY_SECTION=" • Auto-Retry: ${TOTAL}회 실행, 성공률 ${RATE}%"
if [ "$FAIL" -gt 0 ]; then
RETRY_SECTION="${RETRY_SECTION} (실패 ${FAIL}건)"
fi
RETRY_SECTION="${RETRY_SECTION}\n"
else
RETRY_SECTION=" • Auto-Retry: 어제 실행 없음\n"
fi
else
RETRY_SECTION=" • Auto-Retry: 로그 없음\n"
fi
# ============================================================================
# 4. 인프라 메트릭 (metrics-report.sh daily 통합)
# ============================================================================
INFRA_SECTION=""
if [ -f "$METRICS_FILE" ]; then
GW_UP=$(grep "^openclaw_gateway_up " "$METRICS_FILE" | awk '{print $2}')
MEM_BYTES=$(grep "^openclaw_memory_bytes " "$METRICS_FILE" | awk '{print $2}')
CPU_PCT=$(grep "^openclaw_cpu_percent " "$METRICS_FILE" | awk '{print $2}')
UPTIME_SEC=$(grep "^openclaw_uptime_seconds " "$METRICS_FILE" | awk '{print $2}')
HEALTH_LAT=$(grep "^openclaw_health_latency_ms " "$METRICS_FILE" | awk '{print $2}')
DISCORD_UP=$(grep "^openclaw_discord_up " "$METRICS_FILE" | awk '{print $2}')
CRASHES=$(grep "^openclaw_crash_count " "$METRICS_FILE" | awk '{print $2}')
MEM_MB=$((MEM_BYTES / 1024 / 1024))
UPTIME_H=$((UPTIME_SEC / 3600))
UPTIME_M=$(( (UPTIME_SEC % 3600) / 60 ))
GW_ICON="✅"; [ "$GW_UP" != "1" ] && GW_ICON="🔴"
DC_ICON="✅"; [ "$DISCORD_UP" != "1" ] && DC_ICON="🔴"
INFRA_SECTION=" ${GW_ICON} Gateway: $([ "$GW_UP" = "1" ] && echo "Running" || echo "DOWN") | ${DC_ICON} Discord: $([ "$DISCORD_UP" = "1" ] && echo "OK" || echo "Off")\n"
INFRA_SECTION="${INFRA_SECTION} 💾 ${MEM_MB}MB | ⚡ ${CPU_PCT}% | 📡 ${HEALTH_LAT}ms | ⏱️ ${UPTIME_H}h${UPTIME_M}m\n"
# 24시간 업타임 통계
if [ -f "$HISTORY_LOG" ]; then
DAY_AGO=$(( $(date +%s) - 86400 ))
UPTIME_PCT=$(awk -F',' -v cutoff="$DAY_AGO" '
NR > 1 && $1 >= cutoff { count++; if ($2 == 1) up++ }
END { if (count > 0) printf "%.1f", (up/count)*100; else print "N/A" }
' "$HISTORY_LOG")
INFRA_SECTION="${INFRA_SECTION} 📊 24h Uptime: ${UPTIME_PCT}% | Crashes: ${CRASHES}\n"
fi
else
INFRA_SECTION=" • 메트릭 파일 없음\n"
fi
# ============================================================================
# 5. 오늘 할 일 - Google Tasks
# ============================================================================
TASKS_SECTION=""
TASKS_OUTPUT=$("$GOG" tasks list "$TASKS_LIST_ID" --account "$GOG_ACCOUNT" 2>/dev/null || echo "")
if [ -n "$TASKS_OUTPUT" ]; then
TASKS_SECTION=$(echo "$TASKS_OUTPUT" | grep "needsAction" | python3 -c "
import sys, re
for line in sys.stdin:
line = line.strip()
if not line:
continue
parts = re.split(r'\s{2,}', line)
if len(parts) >= 4:
title = parts[1].strip()[:40]
due = parts[3].strip().split('T')[0] if len(parts) > 3 else ''
if due and due != 'DUE':
due_short = '/'.join(due.split('-')[1:])
print(f' • [ ] {title} (마감: {due_short})')
else:
print(f' • [ ] {title}')
" 2>/dev/null || echo "")
if [ -n "$TASKS_SECTION" ]; then
TASKS_SECTION="${TASKS_SECTION}\n"
fi
fi
if [ -z "$TASKS_SECTION" ]; then
TASKS_SECTION=" • 할 일 없음\n"
fi
# ============================================================================
# 6. 오늘 일정 - Google Calendar
# ============================================================================
CALENDAR_SECTION=""
CAL_OUTPUT=$("$GOG" calendar list --from today --to today --account "$GOG_ACCOUNT" 2>/dev/null || echo "")
if [ -n "$CAL_OUTPUT" ] && [ "$CAL_OUTPUT" != "No events" ]; then
while IFS= read -r line; do
if [ -n "$line" ] && [ "$line" != "No events" ]; then
CALENDAR_SECTION="${CALENDAR_SECTION}${line}\n"
fi
done <<< "$CAL_OUTPUT"
fi
if [ -z "$CALENDAR_SECTION" ]; then
CALENDAR_SECTION=" • 오늘 일정 없음\n"
fi
# ============================================================================
# 7. 블로커 감지 (auto-retry 실패 + 인프라 이상)
# ============================================================================
BLOCKER_SECTION=""
# auto-retry 연속 실패
if [ -f "$LOG_FILE" ]; then
TODAY_FAILS=$(grep "\"$TODAY" "$LOG_FILE" 2>/dev/null | grep '"type":"failure"' | wc -l | tr -d ' ')
if [ "$TODAY_FAILS" -ge 3 ]; then
FAIL_TASKS=$(grep "\"$TODAY" "$LOG_FILE" 2>/dev/null | grep '"type":"failure"' | \
python3 -c "import sys,json; [print(json.loads(l).get('context',{}).get('cron','unknown')) for l in sys.stdin]" 2>/dev/null | \
sort | uniq -c | sort -rn | head -3)
BLOCKER_SECTION=" • ⚠️ 오늘 ${TODAY_FAILS}건 실패 감지\n"
while IFS= read -r line; do
[ -n "$line" ] && BLOCKER_SECTION="${BLOCKER_SECTION}${line}\n"
done <<< "$FAIL_TASKS"
fi
fi
# Gateway 다운
if [ -f "$METRICS_FILE" ]; then
GW_CHECK=$(grep "^openclaw_gateway_up " "$METRICS_FILE" | awk '{print $2}')
[ "$GW_CHECK" != "1" ] && BLOCKER_SECTION="${BLOCKER_SECTION} • 🔴 Gateway DOWN\n"
fi
if [ -z "$BLOCKER_SECTION" ]; then
BLOCKER_SECTION=" • 없음\n"
fi
# ============================================================================
# 메시지 조합
# ============================================================================
MESSAGE=$(printf "## ☀️ Morning Briefing - %s (%s)
**📈 시장**
%b
**✅ 어제 한 일**
%b%b
**🖥️ 시스템 상태**
%b
**📌 오늘 할 일**
%b
**📅 오늘 일정**
%b
**🚧 블로커**
%b" \
"$TODAY" "$DAY_KR" \
"$MARKET_SECTION" \
"$COMMITS_SECTION" "$RETRY_SECTION" \
"$INFRA_SECTION" \
"$TASKS_SECTION" \
"$CALENDAR_SECTION" \
"$BLOCKER_SECTION")
# ============================================================================
# Discord 전송
# ============================================================================
echo "$MESSAGE"
echo ""
if [ ${#MESSAGE} -gt 1900 ]; then
MESSAGE="${MESSAGE:0:1900}...(truncated)"
fi
if [ -z "$WEBHOOK_URL" ]; then
echo "⚠️ DISCORD_WEBHOOK_URL이 설정되지 않음"
exit 1
fi
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg content "$MESSAGE" '{content: $content}')" \
2>&1)
if [ "$RESPONSE" = "204" ] || [ "$RESPONSE" = "200" ]; then
echo "✅ Discord 전송 완료"
else
echo "⚠️ Discord 전송 실패 (HTTP $RESPONSE)"
fi