290 lines
10 KiB
Bash
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
|