Add Claude auto-fix to check_errors.py, distinguish LLM failure in Slack reports

- 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) <noreply@anthropic.com>
This commit is contained in:
kroutony 2026-03-16 03:08:06 +00:00
parent a9981cb881
commit 36225df832
5 changed files with 109 additions and 7 deletions

View File

@ -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 = [
"<!channel>",
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)})

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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,6 +100,9 @@ def send_cycle_report(
if holds:
lines.append(f" ⏸️ 其餘 {len(holds)} 幣種 HOLD")
if not buys and not sells:
if not llm_ok:
lines.append(" ⚠️ LLM 分析失敗,本次跳過。")
else:
lines.append(" All symbols → HOLD, no action this cycle.")
lines.append("")