- 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>
160 lines
4.3 KiB
Python
Executable File
160 lines
4.3 KiB
Python
Executable File
#!/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()
|