"""KEYHUNTER AGENT — автономный оператор парсер-фермы (Apodex-стиль). Отдельный софт: FastAPI :8779 + дашборд + автопилот движков. Движки (gh_eternal, bitbucket, codeberg, clojars, cran, crates, goproxy) запускаются как подпроцессы, статус в памяти, ключи в data/keys.db. Запуск: python agent.py (или python agent.py --port 8779) """ import argparse import os import sqlite3 import subprocess import sys import threading import time from pathlib import Path from fastapi import FastAPI from fastapi.responses import HTMLResponse, PlainTextResponse ROOT = Path(__file__).resolve().parent DB = ROOT / "data" / "keys.db" # ---------------------------------------------------------------- движки ENGINES = { "gh": {"script": "gh_eternal.py", "log": "gh_eternal.log", "desc": "GitHub dork-hunt 787"}, "bitbucket": {"script": "bitbucket_eternal.py", "log": "bitbucket_eternal.log", "desc": "Bitbucket repos"}, "codeberg": {"script": "codeberg_eternal.py", "log": "codeberg_eternal.log", "desc": "Codeberg repos"}, "clojars": {"script": "clojars_eternal.py", "log": "clojars_eternal.log", "desc": "Clojars (maven)"}, "cran": {"script": "cran_eternal.py", "log": "cran_eternal.log", "desc": "CRAN packages"}, "crates": {"script": "crates_eternal.py", "log": "crates_eternal.log", "desc": "crates.io"}, "goproxy": {"script": "goproxy_eternal.py", "log": "goproxy_eternal.log", "desc": "Go proxy"}, } _proc: dict = {} # engine_id -> Popen _proc_lock = threading.Lock() AUTOS = {"gh", "crates", "goproxy"} # автопилот: держим живыми def _spawn(eid: str) -> bool: eng = ENGINES.get(eid) if not eng or not (ROOT / eng["script"]).exists(): return False with _proc_lock: p = _proc.get(eid) if p and p.poll() is None: return True # ponytail: CREATE_NEW_PROCESS_GROUP -> корректный terminate на Windows _proc[eid] = subprocess.Popen( [sys.executable, str(ROOT / eng["script"])], cwd=str(ROOT), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0, ) return True def _stop(eid: str) -> bool: with _proc_lock: p = _proc.pop(eid, None) if not p: return False try: p.terminate() p.wait(timeout=8) except Exception: try: p.kill() except Exception: pass return True def engines_alive() -> dict: with _proc_lock: items = dict(_proc) return {k: (p.poll() is None) for k, p in items.items()} def autopilot_loop(): """Раз в минуту: поднимать движки из AUTOS, мёртвые рестартить.""" while True: for eid in AUTOS: alive = engines_alive().get(eid, False) if not alive: _spawn(eid) time.sleep(60) # ---------------------------------------------------------------- db def q(sql: str, args=()): con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True, timeout=30) try: con.row_factory = sqlite3.Row return [dict(r) for r in con.execute(sql, args).fetchall()] finally: con.close() def stats() -> dict: total = q("SELECT COUNT(*) n FROM keys")[0]["n"] by_status = q("SELECT status, COUNT(*) n FROM keys GROUP BY status ORDER BY n DESC") by_prov = q("SELECT prov, COUNT(*) n FROM keys GROUP BY prov ORDER BY n DESC LIMIT 20") today = q( "SELECT COUNT(*) n FROM keys WHERE found LIKE ?", (time.strftime("%Y-%m-%d") + "%",), )[0]["n"] alive = engines_alive() return { "total": total, "today": today, "by_status": by_status, "by_prov": by_prov, "engines": {k: {"alive": alive.get(k, False), "auto": k in AUTOS, "desc": v["desc"]} for k, v in ENGINES.items()}, "ts": time.strftime("%Y-%m-%d %H:%M:%S"), } def tail(path: Path, n=80) -> str: try: with open(path, "rb") as f: f.seek(0, 2) size = f.tell() f.seek(max(0, size - 200_000)) chunk = f.read().decode("utf-8", errors="replace") return "\n".join(chunk.splitlines()[-n:]) except Exception: return "" # ---------------------------------------------------------------- api app = FastAPI(title="KeyHunter Agent") @app.get("/api/stats") def api_stats(): return stats() @app.get("/api/keys") def api_keys(status: str = "", prov: str = "", limit: int = 50, offset: int = 0): limit = max(1, min(limit, 500)) sql, args = "SELECT id, prov, status, plan, price, remaining, found, repo, val FROM keys", [] w = [] if status: w.append("status=?"); args.append(status) if prov: w.append("prov=?"); args.append(prov) if w: sql += " WHERE " + " AND ".join(w) sql += " ORDER BY id DESC LIMIT ? OFFSET ?" rows = q(sql, (*args, limit, offset)) for r in rows: # маскируем ключи в выдаче v = r.get("val") or "" r["val"] = (v[:6] + "…" + v[-4:]) if len(v) > 14 else v return {"rows": rows, "offset": offset, "limit": limit} @app.get("/api/log") def api_log(engine: str = "gh", n: int = 80): eng = ENGINES.get(engine) path = ROOT / (eng["log"] if eng else "gh_eternal.log") return PlainTextResponse(tail(Path(path), min(n, 400)) or "(лог пуст)") @app.post("/api/engine/{eid}/start") async def api_start(eid: str): ok = _spawn(eid) return {"ok": ok, "alive": engines_alive().get(eid, False)} @app.post("/api/engine/{eid}/stop") async def api_stop(eid: str): ok = _stop(eid) return {"ok": ok} @app.get("/api/export") def api_export(status: str = "NEW", prov: str = ""): sql, args = "SELECT val, prov, status, repo FROM keys", [] w = [] if status: w.append("status=?"); args.append(status) if prov: w.append("prov=?"); args.append(prov) if w: sql += " WHERE " + " AND ".join(w) body = "\n".join(f"{r['val']}\t{r['prov']}\t{r['repo']}" for r in q(sql, args)) return PlainTextResponse(body) @app.get("/health") def health(): return {"ok": True, "engines": engines_alive()} # ---------------------------------------------------------------- dashboard _CSS = """ :root{--bg:#0b0e14;--panel:#121826;--line:#1e2839;--fg:#c9d4e3;--dim:#5d6b80;--acc:#38bdf8;--ok:#34d399;--warn:#fbbf24;--bad:#f87171} *{margin:0;padding:0;box-sizing:border-box} body{background:var(--bg);color:var(--fg);font:15px/1.55 ui-sans-serif,system-ui,'Segoe UI',Roboto,sans-serif} .wrap{max-width:1080px;margin:0 auto;padding:28px 20px 60px} h1{font-size:26px;letter-spacing:-.02em} h1 b{color:var(--acc)} .sub{color:var(--dim);margin:6px 0 22px} .hero{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:24px} .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px} .card .k{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--dim)} .card .v{font-size:26px;font-weight:700;margin-top:4px} .card .v.acc{color:var(--acc)}.card .v.ok{color:var(--ok)} h2{font-size:17px;margin:28px 0 12px;display:flex;gap:10px;align-items:baseline} h2 small{color:var(--dim);font-weight:400;font-size:12px} table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden;font-size:13.5px} th{position:sticky;top:0;background:#0e141f;color:var(--dim);text-transform:uppercase;font-size:11px;letter-spacing:.06em;text-align:left;padding:9px 12px} td{padding:8px 12px;border-top:1px solid var(--line);max-width:340px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} tr:hover td{background:#151d2e} .badge{display:inline-block;padding:1px 9px;border-radius:99px;font-size:11.5px;font-weight:600} .b-NEW{background:#0e3a2d;color:var(--ok)}.b-DEAD{background:#3a1414;color:var(--bad)}.b-ERR{background:#3a2b0e;color:var(--warn)} .eng{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px} .eng .card{display:flex;justify-content:space-between;align-items:center;gap:10px} .dot{width:10px;height:10px;border-radius:99px;display:inline-block;margin-right:7px} .dot.on{background:var(--ok);box-shadow:0 0 8px var(--ok)} .dot.off{background:#39445a} button{background:#1a2333;color:var(--fg);border:1px solid var(--line);border-radius:8px;padding:7px 14px;font-size:13px;cursor:pointer} button:hover{border-color:var(--acc);color:var(--acc)} button.stop:hover{border-color:var(--bad);color:var(--bad)} #log{background:#0a0d12;border:1px solid var(--line);border-radius:12px;padding:14px;font:12.5px/1.5 ui-monospace,Consolas,monospace;white-space:pre-wrap;color:#8fa3bd;max-height:320px;overflow:auto} .bar{height:7px;background:#1a2333;border-radius:99px;overflow:hidden;margin-top:8px} .bar i{display:block;height:100%;background:var(--acc)} .filters{display:flex;gap:8px;margin:10px 0;flex-wrap:wrap} .filters select,.filters input{background:var(--panel);border:1px solid var(--line);color:var(--fg);border-radius:8px;padding:7px 10px;font-size:13px} footer{margin-top:34px;color:var(--dim);font-size:12px} """ _JS = """ async function jget(u){const r=await fetch(u);return r.json();} async function post(u){const r=await fetch(u,{method:'POST'});return r.json();} async function refresh(){ try{ const s=await jget('/api/stats'); el('m-total').textContent=s.total.toLocaleString('ru'); el('m-today').textContent=s.today.toLocaleString('ru'); const alive=Object.values(s.engines).filter(e=>e.alive).length; el('m-eng').textContent=alive+' / '+Object.keys(s.engines).length; el('m-ts').textContent=s.ts; // движки const box=el('eng');box.innerHTML=''; for(const [id,e] of Object.entries(s.engines)){ const c=document.createElement('div');c.className='card'; c.innerHTML=`
${id}
${e.desc}${e.auto?' · auto':''}
`; const btn=document.createElement('button'); btn.textContent=e.alive?'стоп':'старт'; if(e.alive)btn.className='stop'; btn.onclick=async()=>{e.alive?await post('/api/engine/'+id+'/stop'):await post('/api/engine/'+id+'/start');refresh();}; c.appendChild(btn);box.appendChild(c); } // статусы const st=el('t-status');st.innerHTML=''; const mx=Math.max(...s.by_status.map(x=>x.n),1); for(const r of s.by_status){ const tr=document.createElement('tr'); tr.innerHTML=`${r.status}${r.n.toLocaleString('ru')}
`; st.appendChild(tr); } // провайдеры const pv=el('t-prov');pv.innerHTML=''; for(const r of s.by_prov){ const tr=document.createElement('tr'); tr.innerHTML=`${r.prov}${r.n.toLocaleString('ru')}`; tr.style.cursor='pointer'; tr.onclick=()=>{el('f-prov').value=r.prov;loadKeys();}; pv.appendChild(tr); } loadKeys(); loadLog(); }catch(e){console.error(e);} } async function loadKeys(){ const st=el('f-status').value,pv=el('f-prov').value,lim=el('f-limit').value||50; const d=await jget(`/api/keys?status=${st}&prov=${pv}&limit=${lim}`); const t=el('t-keys');t.innerHTML=''; for(const r of d.rows){ const tr=document.createElement('tr'); tr.innerHTML=`${r.status}${r.prov} ${r.val}${r.repo||''}${r.found||''}`; t.appendChild(tr); } el('k-count').textContent=d.rows.length+' показано'; } async function loadLog(){ const r=await fetch('/api/log?engine='+el('f-log').value); el('log').textContent=await r.text(); el('log').scrollTop=el('log').scrollHeight; } function el(id){return document.getElementById(id);} function exportKeys(){const st=el('f-status').value,pv=el('f-prov').value; window.open(`/api/export?status=${st}&prov=${pv}`,'_blank');} refresh(); setInterval(refresh,5000); """ _HTML = f""" KeyHunter Agent

KeyHunter Agent

автономный оператор парсер-фермы · GitHub / Bitbucket / Codeberg / Clojars / CRAN / crates.io / Go
Всего ключей
—
Найдено сегодня
—
Движки живы
—
Обновлено
—

Движки кнопки старт/стоп · авто = автопилот держит живым

Статусы NEW = не проверен

СтатусКлючейДоля

Провайдеры клик — фильтр в таблице ниже

ПровайдерКлючей

Последние ключи

СтатусПровКлючИсточникКогда

Лог движка живой хвост

""" @app.get("/dashboard", response_class=HTMLResponse) def dashboard(): return _HTML @app.get("/", response_class=HTMLResponse) def root(): return _HTML def main(): ap = argparse.ArgumentParser() ap.add_argument("--port", type=int, default=8779) ap.add_argument("--host", default="127.0.0.1") ap.add_argument("--no-auto", action="store_true", help="не запускать автопилот") a = ap.parse_args() if not a.no_auto: threading.Thread(target=autopilot_loop, daemon=True).start() import uvicorn print(f"KEYHUNTER AGENT on http://{a.host}:{a.port}/dashboard", flush=True) uvicorn.run(app, host=a.host, port=a.port, log_level="warning") if __name__ == "__main__": main()