Funk-lab/tools/hermes-auto-improve/trigger-server.py
Alkatrazz 7d02efd9fa feat(hermes-auto-improve): passe quotidienne complète à 22h (tous les docs)
- Ajout systemd timer hermes-auto-improve-daily.timer (22h00, Persistent=true)
- Service oneshot : --all puis --daily-pr (TimeoutSec=7200 pour 28 fichiers)
- auto-improve.py : flag --all qui bypass MAX_FILES et traite tout admin/
- trigger-server.py : timeout 600s → 7200s pour les runs --all via HTTP

La rotation par batch (MAX_FILES=5) reste active pour les triggers n8n
à la demande. Le workflow n8n 30-min doit être désactivé manuellement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 13:46:14 +02:00

146 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""
Serveur HTTP minimal pour déclencher hermes-auto-improve depuis n8n.
Écoute sur 0.0.0.0:9095.
Les POST /trigger* retournent immédiatement {"started": true}.
Le script tourne en background thread.
Polling du résultat via GET /status.
POST /trigger → run standard (MAX_FILES fichiers)
POST /trigger/subdir → body JSON {"subdir": "ia"}
POST /trigger/dry-run → analyse sans commit/push
POST /trigger/daily-pr → crée PR GitHub + résumé journalier
GET /status → dernier résultat + état running
GET /health → {"ok": true}
"""
import json
import subprocess
import threading
from datetime import datetime
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
SCRIPT = "/usr/local/bin/hermes-auto-improve"
STATE_FILE = Path("/var/lib/hermes-auto-improve/state.json")
LOG_FILE = Path("/var/log/hermes-auto-improve.log")
_lock = threading.Lock()
_running = False
_last_result = None
def _extract_last_json(text: str):
"""Find the outermost JSON object at the end of mixed text+JSON output."""
end = text.rfind("}")
if end < 0:
return None
depth = 0
for i in range(end, -1, -1):
if text[i] == "}":
depth += 1
elif text[i] == "{":
depth -= 1
if depth == 0:
try:
return json.loads(text[i:end + 1])
except json.JSONDecodeError:
return None
return None
def run_script(extra_args: list = None):
global _running, _last_result
with _lock:
if _running:
return
_running = True
try:
cmd = [SCRIPT] + (extra_args or [])
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(LOG_FILE, "a") as log:
log.write(f"\n=== {datetime.now().isoformat()}{' '.join(cmd)} ===\n")
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=7200
)
log.write(result.stdout)
if result.stderr:
log.write(f"STDERR: {result.stderr}")
out = result.stdout
_last_result = _extract_last_json(out) or {"error": "no_json", "stdout": out[-500:]}
except subprocess.TimeoutExpired:
_last_result = {"error": "timeout"}
except Exception as e:
_last_result = {"error": str(e)}
finally:
_running = False
def trigger_async(extra_args: list = None) -> dict:
with _lock:
if _running:
return {"started": False, "error": "already_running"}
t = threading.Thread(target=run_script, args=(extra_args,), daemon=True)
t.start()
return {"started": True, "running": True}
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
pass
def send_json(self, code: int, data: dict):
body = json.dumps(data, ensure_ascii=False, indent=2).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/health":
self.send_json(200, {"ok": True, "running": _running})
elif self.path == "/status":
state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else None
self.send_json(200, {"state": state, "last_result": _last_result, "running": _running})
else:
self.send_json(404, {"error": "not_found"})
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length)) if length > 0 else {}
if self.path == "/trigger":
self.send_json(200, trigger_async())
elif self.path == "/trigger/all":
self.send_json(200, trigger_async(["--all"]))
elif self.path == "/trigger/subdir":
subdir = body.get("subdir")
if not subdir:
self.send_json(400, {"error": "missing 'subdir' in body"})
return
self.send_json(200, trigger_async(["--subdir", subdir]))
elif self.path == "/trigger/dry-run":
self.send_json(200, trigger_async(["--dry-run"]))
elif self.path == "/trigger/daily-pr":
self.send_json(200, trigger_async(["--daily-pr"]))
elif self.path == "/trigger/reset":
self.send_json(200, trigger_async(["--reset"]))
else:
self.send_json(404, {"error": "not_found"})
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", 9095), Handler)
print("hermes-auto-improve trigger server — port 9095", flush=True)
server.serve_forever()