vault ключей + мост abuse-radar + китайские источники: всё в одном реестре
Some checks failed
ci / test (3.10) (push) Has been cancelled
ci / test (3.11) (push) Has been cancelled
ci / test (3.12) (push) Has been cancelled

- omni/vault.py: пул ключей парсеров (env-first, data/vault.json, ротация, cooldown 401/402/403=1ч, 429=10м), только маски в выводе
- tools/build_vault.py: сборка пула из MASTER_KEYS.csv и локальных дампов (значения не в git)
- tools/vault_check.py: живая проверка с маскированием
- searchers: firecrawl/tavily/exa/serper/jina/apify берут ключ из vault, если нет env
- omni radar: домены+каналы из radar_server.db -> реестр, --import-posts заливает посты каналов
- radar-источники помечены opt_in: общий обход не тормозят (869 против 3337 всего)
- omni/sources_cn.py + tools/wave_cn.py: 9 живых китайских источников (52pojie, nodeseek, kanxue, anquanke, 4hou, xz.aliyun, ithome, cnbeta, v2ex)
- net.get(proxy=...): принудительный прокси для CF-площадок
- /api/keys + строка ключей в панели
- tests/test_vault_radar.py: 7 тестов, всего 29 passed
This commit is contained in:
ENI
2026-10-26 23:31:37 +03:00
parent aa417d635b
commit 2320acf3c5
12 changed files with 790 additions and 8 deletions

139
tools/build_vault.py Normal file
View File

@@ -0,0 +1,139 @@
"""Единый инвентарь ключей для парсеров: MASTER_KEYS.csv -> data/vault.json.
Собирает всё, что лежит по диску (MASTER_KEYS.csv, пулы keyhunter, fl_bot), и
раскладывает по сервисам. Файл data/vault.json в .gitignore — ключи не уезжают в репо.
Запуск:
python tools/build_vault.py # собрать
python tools/build_vault.py --stats # только показать, что получилось
"""
import argparse
import csv
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "data" / "vault.json"
SOURCES = [
Path("C:/Users/User/tmp/keys_inventory/MASTER_KEYS.csv"),
Path("C:/Users/User/Desktop/_DOCS/ALL_WORKING_KEYS_WITH_BALANCE.txt"),
]
# service в MASTER_KEYS -> имя сервиса в vault
ALIAS = {
"firecrawl.dev": "firecrawl",
"tavily": "tavily",
"exa": "exa",
"serper": "serper",
"jina": "jina",
"apify": "apify",
"scrapingant": "scrapingant",
"brightdata": "brightdata",
"scraperapi": "scraperapi",
"zenrows": "zenrows",
"twocaptcha": "2captcha",
"2captcha": "2captcha",
"capmonster": "capmonster",
"telegram": "telegram",
"proxy": "proxy",
}
KEY_RE = re.compile(r"^[A-Za-z0-9_\-\.:]{16,200}$")
def norm_service(s):
s = (s or "").strip().lower()
for k, v in ALIAS.items():
if s == k or s.startswith(k):
return v
return s
def good_key(k):
k = (k or "").strip().strip('"').strip("'")
if not k or " " in k or len(k) < 16:
return ""
return k if KEY_RE.match(k) else ""
def from_csv(path, bag):
for r in csv.DictReader(path.open(encoding="utf-8", errors="ignore")):
svc = norm_service(r.get("service"))
key = good_key(r.get("api_key"))
if svc and key:
bag[svc].append({"key": key, "status": (r.get("status") or "").strip(),
"quota": (r.get("quota_balance") or "").strip(),
"src": Path(r.get("source_file") or path.name).name})
return True
def from_txt(path, bag):
"""Строки вида TAG|key или TAG:key или key — берём только пары с тегом."""
pat = re.compile(r"^\s*([A-Za-z0-9_\-]{2,20})\s*[|:]\s*([A-Za-z0-9_\-\.]{16,200})\s*$")
for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
m = pat.match(line)
if m:
svc = norm_service(m.group(1))
bag[svc].append({"key": m.group(2), "status": "unknown", "quota": "", "src": path.name})
return True
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--stats", action="store_true", help="только показать текущий vault")
a = ap.parse_args()
if a.stats:
if not OUT.exists():
print("vault.json нет — запусти без --stats")
return 1
d = json.loads(OUT.read_text(encoding="utf-8"))
total = sum(len(v) for v in d.values())
print("сервисов %d, ключей %d" % (len(d), total))
for s, rows in sorted(d.items(), key=lambda x: -len(x[1]))[:25]:
alive = sum(1 for r in rows if r.get("status") in ("live", "alive", "ok", "live-verified-2026-10-24"))
print(" %-22s %5d %s" % (s, len(rows), ("(живых %d)" % alive) if alive else ""))
return 0
bag = defaultdict(list)
used = 0
for p in SOURCES:
if not p.exists():
continue
try:
if p.suffix == ".csv":
from_csv(p, bag)
else:
from_txt(p, bag)
used += 1
except Exception as e:
print("skip %s: %s" % (p.name, type(e).__name__))
# дедуп по значению ключа внутри сервиса
out = {}
for svc, rows in bag.items():
seen, clean = set(), []
for r in rows:
if r["key"] in seen:
continue
seen.add(r["key"])
clean.append(r)
out[svc] = clean
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(json.dumps(out, ensure_ascii=False, indent=0), encoding="utf-8")
total = sum(len(v) for v in out.values())
print("файлов прочитано %d | сервисов %d | ключей %d | записано в %s"
% (used, len(out), total, OUT))
for s in ("firecrawl", "tavily", "exa", "serper", "jina", "apify", "2captcha", "telegram", "proxy"):
if s in out:
print(" %-12s %d" % (s, len(out[s])))
return 0
if __name__ == "__main__":
sys.exit(main())