Files
omni-parse/tools/panel_hunt.py
2026-10-27 01:09:30 +03:00

153 lines
6.1 KiB
Python
Raw Permalink 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.

"""Охота за панелями new-api: /api/status -> /api/pricing -> бесплатные модели.
Панель new-api (одна из самых частых раздач в абуз-каналах) отдаёт каталог
моделей и цены публично. Нулевая цена = бесплатный доступ = находка для абузов.
Запуск:
python tools/panel_hunt.py # домены из data/radar_sources.json
python tools/panel_hunt.py --hosts a.com b.com
python tools/panel_hunt.py --limit 400 --workers 24
python tools/panel_hunt.py --apply # записать data/panels.json (источники omni)
Пишет только пути/счётчики. Ключи и токены не трогает.
"""
from __future__ import annotations
import argparse
import concurrent.futures as cf
import json
import ssl
import sys
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / "data"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0 Safari/537.36"
CTX = ssl.create_default_context()
CTX.check_hostname = False
CTX.verify_mode = ssl.CERT_NONE
MARKERS = ("HeaderNavModules", "system_name", "chats", "quota_per_unit", "display_in_currency", "announcements_enabled")
def _get(url, timeout=8, cap=400000):
req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "application/json"})
with urllib.request.urlopen(req, timeout=timeout, context=CTX) as r:
return r.status, r.read(cap).decode("utf-8", "ignore")
def _is_newapi(body: str) -> bool:
if '"data"' not in body:
return False
return any(m in body for m in MARKERS)
def probe(host: str):
"""-> dict | None. Проверяет сам хост и api.<host>."""
for h in (host, "api." + host if not host.startswith("api.") else host):
try:
st, body = _get("https://" + h + "/api/status", timeout=7)
if st != 200 or not _is_newapi(body):
continue
except Exception:
continue
rec = {"host": h, "status": "newapi", "models": 0, "free": [], "free_count": 0}
try:
st, body = _get("https://" + h + "/api/pricing", timeout=12)
if st == 200:
js = json.loads(body)
data = js.get("data") if isinstance(js, dict) else None
if isinstance(data, list):
rec["models"] = len(data)
free = []
for m in data:
if not isinstance(m, dict):
continue
try:
zero = float(m.get("model_ratio") or 0) == 0 and float(m.get("completion_ratio") or 0) == 0
except Exception:
zero = False
if zero and m.get("model_name"):
free.append(m["model_name"])
rec["free"] = free[:80]
rec["free_count"] = len(free)
except Exception:
rec["pricing"] = "err"
return rec
return None
def load_hosts(args) -> list:
if args.hosts:
return list(dict.fromkeys(args.hosts))
p = DATA / "radar_sources.json"
if not p.exists():
return []
try:
d = json.loads(p.read_text(encoding="utf-8"))
except Exception:
return []
out = []
for item in d.get("domains") or []:
dom = item[0] if isinstance(item, list) else (item.get("domain") or "")
if dom:
out.append(str(dom))
return list(dict.fromkeys(out))
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--hosts", nargs="*", default=None)
ap.add_argument("--limit", type=int, default=0, help="0 = все домены")
ap.add_argument("--workers", type=int, default=24)
ap.add_argument("--apply", action="store_true", help="записать data/panels.json")
ap.add_argument("--min-free", type=int, default=0, help="только панели с >= N бесплатных моделей")
a = ap.parse_args()
hosts = load_hosts(a)
if a.limit:
hosts = hosts[:a.limit]
if not hosts:
print("нет доменов: сначала omni radar --apply")
return 1
res = []
with cf.ThreadPoolExecutor(max(4, a.workers)) as ex:
futs = {ex.submit(probe, h): h for h in hosts}
for i, f in enumerate(cf.as_completed(futs), 1):
try:
r = f.result()
except Exception:
r = None
if r:
res.append(r)
print(" %s | new-api | моделей %d | free %d" % (r["host"], r["models"], r["free_count"]))
if i % 200 == 0:
print("... проверено %d/%d, найдено %d" % (i, len(hosts), len(res)), flush=True)
res.sort(key=lambda x: x["free_count"], reverse=True)
if a.min_free:
res = [r for r in res if r["free_count"] >= a.min_free]
print("итог: проверено %d | панелей new-api %d | с бесплатными моделями %d | free-моделей всего %d"
% (len(hosts), len(res), sum(1 for r in res if r["free_count"]), sum(r["free_count"] for r in res)))
if a.apply:
out = DATA / "panels.json"
prev = {}
if out.exists():
try:
for r in json.loads(out.read_text(encoding="utf-8")).get("panels") or []:
prev[r.get("host")] = r
except Exception:
prev = {}
for r in res:
prev[r["host"]] = r # свежие данные перекрывают старые, мёртвые не затираются
payload = {"panels": sorted(prev.values(), key=lambda x: x.get("free_count") or 0, reverse=True)}
out.write_text(json.dumps(payload, ensure_ascii=False, indent=1), encoding="utf-8")
print("записано: %s (панелей %d)" % (out, len(payload["panels"])))
return 0
if __name__ == "__main__":
sys.exit(main())