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

View File

@@ -298,6 +298,122 @@ def cmd_pump(a):
return 0
def cmd_keys(a):
"""Сводка vault: сколько ключей на сервис, сколько готово, что остывает."""
from . import vault
st = vault.stats()
if a.service:
st = [x for x in st if x["service"] == a.service]
if not st:
print("vault пуст — собери: python tools/build_vault.py")
return 1
w = max(len(x["service"]) for x in st)
print("%-*s %8s %8s %9s %s" % (w, "сервис", "ключей", "готово", "остывают", "env"))
for x in st:
print("%-*s %8d %8d %9d %s" % (w, x["service"], x["keys"], x["ready"],
x["cooling"], "*" if x["env"] else ""))
if a.verbose:
print()
for x in st:
for r in (vault._load()[0].get(x["service"]) or []):
print(" %-*s %s %-7s %s" % (w, x["service"], vault.mask(r["key"]),
r.get("status") or "?", (r.get("note") or "")[:30]))
return 0
def cmd_radar(a):
"""Мост из abuse-radar: домены и Telegram-каналы радара -> реестр omni."""
import json as _json
import sqlite3 as _sq
from pathlib import Path as _P
from . import sources as _src
cand = [a.db] if a.db else [
str(_P("C:/Users/User/tmp/abuse_radar/radar_server.db")),
str(_P("C:/Users/User/tmp/abuse_radar/radar.db")),
"data/radar.db",
]
db = next((c for c in cand if _P(c).exists()), "")
if not db:
print("radar.db не найден, укажи --db")
return 2
c = _sq.connect(db)
doms = c.execute("SELECT domain, score, info FROM domains ORDER BY score DESC").fetchall()
chans = c.execute("SELECT name, ok, fails FROM channels WHERE ok=1").fetchall()
posts = c.execute("SELECT chan, mid, date, text FROM messages").fetchall() if a.import_posts else []
c.close()
if a.import_posts:
def head_line(txt):
for ln in (txt or "").splitlines():
ln = ln.strip()
if len(ln) >= 20 and sum(ch.isalpha() for ch in ln) >= 10:
return ln[:150]
return ((txt or "").strip().splitlines() or [""])[0][:150]
items, seen_u = [], set()
for chan, mid, date, text in posts:
text = (text or "").strip()
if len(text) < 30:
continue
url_u = "https://t.me/%s/%s" % (chan, mid)
if url_u in seen_u:
continue
seen_u.add(url_u)
items.append(Item("rad_" + chan, "post", url=url_u, title=head_line(text), text=text,
author="@" + chan, published=(date or "")[:19],
source="rad_" + chan, extra={"via": "radar"}))
if a.limit:
items = items[:a.limit]
n_new = n_dup = 0
for i in range(0, len(items), 500):
n1, n2 = store.add(items[i:i + 500])
n_new += n1
n_dup += n2
print("постов радара: %d | новых %d, дублей %d" % (len(items), n_new, n_dup))
have = {s.url.rstrip('/').lower() for s in _src.all_sources()}
have_names = {s.name for s in _src.all_sources()}
new_dom, new_chan = [], []
for dom, score, info in doms:
if a.min_score and (score or 0) < a.min_score:
continue
url = "https://" + dom
if url.lower() in have or ("rad_" + dom.replace('.', '_').replace('-', '_')) in have_names:
continue
title = ""
try:
title = (_json.loads(info or "{}") or {}).get("title") or ""
except Exception:
pass
new_dom.append((dom, score, title))
for name, ok, fails in chans:
if ("tg_" + name) in have_names:
continue
new_chan.append(name)
if a.limit:
new_dom, new_chan = new_dom[:a.limit], new_chan[:a.limit]
print("радар: %s" % db)
print("доменов всего %d, новых для omni %d | каналов живых %d, новых %d"
% (len(doms), len(new_dom), len(chans), len(new_chan)))
for dom, score, title in new_dom[:5]:
print(" %-28s score %-4s %s" % (dom, score, (title or "")[:44]))
if new_chan[:5]:
print(" каналы:", ", ".join(new_chan[:8]))
if not a.apply:
print("dry-run: добавь --apply, чтобы записать в data/radar_sources.json")
return 0
out = _P("data") / "radar_sources.json"
out.write_text(_json.dumps({"domains": new_dom, "channels": new_chan},
ensure_ascii=False, indent=1), encoding="utf-8")
print("записано: %s (доменов %d, каналов %d)" % (out, len(new_dom), len(new_chan)))
return 0
def main(argv=None):
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
@@ -405,6 +521,19 @@ def main(argv=None):
sp.add_argument("--apply", action="store_true")
sp.set_defaults(fn=cmd_pump)
sp = sub.add_parser("keys", help="ключи парсеров из vault: сервисы, счётчики, остывание")
sp.add_argument("--service", default="", help="только один сервис")
sp.add_argument("-v", "--verbose", action="store_true", help="по ключам, масками")
sp.set_defaults(fn=cmd_keys)
sp = sub.add_parser("radar", help="забрать источники из abuse-radar (домены + каналы)")
sp.add_argument("--db", default="", help="путь к radar.db (по умолчанию ищет рядом)")
sp.add_argument("--apply", action="store_true", help="записать в реестр omni")
sp.add_argument("--min-score", type=int, default=20, help="минимальный score домена")
sp.add_argument("--import-posts", action="store_true", help="залить посты каналов радара в базу")
sp.add_argument("--limit", type=int, default=0)
sp.set_defaults(fn=cmd_radar)
args = p.parse_args(argv)
return args.fn(args)