From 36225df8325b25ac1b05a766214c84c7d66fcae7 Mon Sep 17 00:00:00 2001 From: kroutony Date: Mon, 16 Mar 2026 03:08:06 +0000 Subject: [PATCH] Add Claude auto-fix to check_errors.py, distinguish LLM failure in Slack reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - check_errors.py: on errors, call Claude CLI to diagnose and attempt auto-fix, include fix report in Slack alert - slack_notifier.py: show "LLM 分析失敗" when LLM fails instead of "All HOLD" - main.py: track llm_ok flag and pass to Slack reporter - setup.sh: restore ~/.local/bin in crontab PATH for claude CLI - llm_analyzer.py: use shutil.which for robust claude binary lookup Co-Authored-By: Claude Opus 4.6 (1M context) --- check_errors.py | 99 +++++++++++++++++++++++++++++++++++++++++++++-- llm_analyzer.py | 6 ++- main.py | 3 ++ setup.sh | 2 +- slack_notifier.py | 6 ++- 5 files changed, 109 insertions(+), 7 deletions(-) diff --git a/check_errors.py b/check_errors.py index a7928c8..5872882 100755 --- a/check_errors.py +++ b/check_errors.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 -"""Check API errors in the last hour and send Slack alert if any found.""" +"""Check API errors in the last hour, attempt auto-fix via Claude, and report to Slack.""" +import json import os +import shutil +import subprocess import sys from datetime import datetime, timedelta @@ -9,6 +12,7 @@ sys.path.insert(0, os.path.dirname(__file__)) import slack_notifier +PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) LOG_FILES = [ "trading.log", @@ -25,14 +29,13 @@ def collect_recent_errors() -> list[str]: errors = [] for logfile in LOG_FILES: - path = os.path.join(os.path.dirname(__file__), logfile) + path = os.path.join(PROJECT_DIR, logfile) if not os.path.exists(path): continue with open(path) as f: for line in f: if "[ERROR]" not in line: continue - # Only compare lines that start with a timestamp if not line[:4].isdigit(): continue if line[:16] >= since_str: @@ -41,16 +44,90 @@ def collect_recent_errors() -> list[str]: return errors +def attempt_auto_fix(errors: list[str]) -> str | None: + """Ask Claude to diagnose and fix the errors. Returns a summary or None on failure.""" + error_text = "\n".join(errors[:20]) + prompt = f"""你是 bifitnex-trading 專案的維運工程師。 +專案目錄: {PROJECT_DIR} + +以下是最近一小時的錯誤 log: +``` +{error_text} +``` + +請: +1. 診斷錯誤的根因 +2. 如果是程式碼問題,直接修復(編輯檔案) +3. 如果是外部問題(API 暫時不可用、網路問題等),不需要改程式碼,只需說明 + +最後用以下 JSON 格式回覆: +```json +{{ + "diagnosis": "錯誤根因簡述", + "fixed": true/false, + "changes": "修改了什麼(沒改就寫 null)", + "suggestion": "如需人工介入的建議(不需要就寫 null)" +}} +``` +只回傳 JSON,不要其他文字。""" + + claude_bin = shutil.which("claude") or os.path.expanduser("~/.local/bin/claude") + + try: + result = subprocess.run( + [claude_bin, "-p", prompt, "--output-format", "json", + "--no-session-persistence", "--allowedTools", "Read,Edit,Glob,Grep"], + capture_output=True, + text=True, + timeout=180, + cwd=PROJECT_DIR, + ) + if result.returncode != 0: + return None + return _parse_fix_response(result.stdout) + except (subprocess.TimeoutExpired, FileNotFoundError, Exception): + return None + + +def _parse_fix_response(raw: str) -> dict | None: + """Parse Claude's JSON response.""" + try: + wrapper = json.loads(raw) + text = wrapper.get("result", raw) if isinstance(wrapper, dict) else raw + except json.JSONDecodeError: + text = raw + + # Try to extract JSON from the text + try: + return json.loads(text) + except json.JSONDecodeError: + pass + + # Try finding JSON block in text + start = text.find("{") + end = text.rfind("}") + 1 + if start >= 0 and end > start: + try: + return json.loads(text[start:end]) + except json.JSONDecodeError: + pass + + return None + + def main(): errors = collect_recent_errors() if not errors: return - # Deduplicate and limit unique = list(dict.fromkeys(errors)) display = unique[:20] remaining = len(unique) - len(display) + # Try auto-fix + fix_result = attempt_auto_fix(unique) + + # Build Slack message lines = [ "", f"⚠️ *API 錯誤警報* (最近 {LOOKBACK_MINUTES} 分鐘,共 {len(unique)} 筆)", @@ -61,6 +138,20 @@ def main(): if remaining > 0: lines.append(f"\n...另有 {remaining} 筆錯誤") + # Append fix report + lines.append("") + if fix_result: + lines.append("🔧 *自動修復報告:*") + lines.append(f" 診斷:{fix_result.get('diagnosis', '未知')}") + if fix_result.get("fixed"): + lines.append(f" ✅ 已修復:{fix_result.get('changes', '-')}") + else: + lines.append(" ❌ 未自動修復") + if fix_result.get("suggestion"): + lines.append(f" 💡 建議:{fix_result.get('suggestion')}") + else: + lines.append("🔧 *自動修復:* Claude 分析失敗,請人工檢查") + slack_notifier._send({"text": "\n".join(lines)}) diff --git a/llm_analyzer.py b/llm_analyzer.py index 204a794..a2165bc 100644 --- a/llm_analyzer.py +++ b/llm_analyzer.py @@ -1,6 +1,8 @@ import json import logging +import os import re +import shutil import subprocess logger = logging.getLogger(__name__) @@ -66,9 +68,11 @@ def analyze_market(indicator_summary: str, account_status: str) -> list[dict]: ``` 只回傳 JSON,不要其他文字。""" + claude_bin = shutil.which("claude") or os.path.expanduser("~/.local/bin/claude") + try: result = subprocess.run( - ["claude", "-p", prompt, "--output-format", "json", "--no-session-persistence"], + [claude_bin, "-p", prompt, "--output-format", "json", "--no-session-persistence"], capture_output=True, text=True, timeout=120, diff --git a/main.py b/main.py index e0b3e7f..fbe61bd 100644 --- a/main.py +++ b/main.py @@ -338,8 +338,10 @@ def run_cycle(): # 9. Call LLM for analysis signals = [] + llm_ok = False try: signals = llm_analyzer.analyze_market(indicator_summary, account_str) + llm_ok = True logger.info("LLM returned %d signals", len(signals)) except Exception as e: logger.error("LLM analysis failed: %s", e) @@ -473,6 +475,7 @@ def run_cycle(): rejected=rejected, tp_closed=tp_closed, portfolio_summary=portfolio_summary, + llm_ok=llm_ok, ) # 12. Save final state diff --git a/setup.sh b/setup.sh index dfc8bad..2ec7032 100755 --- a/setup.sh +++ b/setup.sh @@ -126,7 +126,7 @@ VENV_PYTHON="$PROJECT_DIR/.venv/bin/python" CRON_MAIN="*/5 * * * * sleep 30 && cd $PROJECT_DIR && $VENV_PYTHON main.py >> cron.log 2>&1" CRON_SYNC="2,32 * * * * cd $PROJECT_DIR && $VENV_PYTHON sync_cost_basis.py >> sync_cost_basis_cron.log 2>&1" CRON_CHECK="7 * * * * cd $PROJECT_DIR && $VENV_PYTHON check_errors.py 2>&1" -CRON_ENV="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +CRON_ENV="PATH=$HOME/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" # 檢查是否已有此專案的 crontab if crontab -l 2>/dev/null | grep -q "bifitnex-trading"; then diff --git a/slack_notifier.py b/slack_notifier.py index 39d4a79..fc76277 100644 --- a/slack_notifier.py +++ b/slack_notifier.py @@ -58,6 +58,7 @@ def send_cycle_report( rejected: list[dict], tp_closed: list[dict], portfolio_summary: str, + llm_ok: bool = True, ): """Send a unified cycle report combining analysis, execution, and portfolio status.""" lines = [f"📊 *Trading Cycle Report #{cycle_number}*\n"] @@ -99,7 +100,10 @@ def send_cycle_report( if holds: lines.append(f" ⏸️ 其餘 {len(holds)} 幣種 HOLD") if not buys and not sells: - lines.append(" All symbols → HOLD, no action this cycle.") + if not llm_ok: + lines.append(" ⚠️ LLM 分析失敗,本次跳過。") + else: + lines.append(" All symbols → HOLD, no action this cycle.") lines.append("") # --- Executed trades ---