bifitnex-trading/check_errors.py
kroutony 36225df832 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>
2026-03-16 03:08:06 +00:00

160 lines
4.3 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""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
sys.path.insert(0, os.path.dirname(__file__))
import slack_notifier
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_FILES = [
"trading.log",
"sync_cost_basis.log",
"cron.log",
]
LOOKBACK_MINUTES = 60
def collect_recent_errors() -> list[str]:
since = datetime.now() - timedelta(minutes=LOOKBACK_MINUTES)
since_str = since.strftime("%Y-%m-%d %H:%M")
errors = []
for logfile in LOG_FILES:
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
if not line[:4].isdigit():
continue
if line[:16] >= since_str:
errors.append(line.rstrip())
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
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)} 筆)",
"",
]
for e in display:
lines.append(f"• `{e[:200]}`")
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)})
if __name__ == "__main__":
main()