vault ключей + мост abuse-radar + китайские источники: всё в одном реестре
- 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:
128
omni/vault.py
Normal file
128
omni/vault.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Vault: ключи сервисов с ротацией и остыванием.
|
||||
|
||||
Источник — data/vault.json (собирается tools/build_vault.py из дампов ключей).
|
||||
Значения живут только на диске и в памяти: в логах и панели печатается маска.
|
||||
|
||||
Правила:
|
||||
* env важнее файла (FIRECRAWL_API_KEY и т.п.) — ручное управление не ломается;
|
||||
* ключ, получивший 401/402/403, уходит в остывание на час; 429 — на 10 минут;
|
||||
* round-robin, чтобы не долбить один ключ и не выжигать квоту.
|
||||
|
||||
Использование:
|
||||
from . import vault
|
||||
key = vault.get("firecrawl") # или "" если ничего нет
|
||||
vault.mark("firecrawl", key, "dead") # 401/402/403
|
||||
vault.mark("firecrawl", key, "rate") # 429
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
VAULT = ROOT / "data" / "vault.json"
|
||||
STATE = ROOT / "data" / "vault_state.json"
|
||||
|
||||
ENV = {
|
||||
"firecrawl": "FIRECRAWL_API_KEY",
|
||||
"tavily": "TAVILY_API_KEY",
|
||||
"exa": "EXA_API_KEY",
|
||||
"serper": "SERPER_API_KEY",
|
||||
"jina": "JINA_API_KEY",
|
||||
"apify": "APIFY_TOKEN",
|
||||
"2captcha": "TWOCAPTCHA_KEY",
|
||||
"capmonster": "CAPMONSTER_KEY",
|
||||
"telegram": "TG_BOT_TOKEN",
|
||||
}
|
||||
|
||||
COOLDOWN = {"dead": 3600, "rate": 600, "empty": 1800}
|
||||
|
||||
_cache = {"data": None, "state": None, "at": 0.0}
|
||||
|
||||
|
||||
def _load():
|
||||
now = time.time()
|
||||
if _cache["data"] is not None and now - _cache["at"] < 30:
|
||||
return _cache["data"], _cache["state"]
|
||||
data, state = {}, {}
|
||||
try:
|
||||
data = json.loads(VAULT.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
data = {}
|
||||
try:
|
||||
state = json.loads(STATE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
state = {}
|
||||
_cache.update(data=data, state=state, at=now)
|
||||
return data, state
|
||||
|
||||
|
||||
def _save_state(state):
|
||||
try:
|
||||
STATE.parent.mkdir(parents=True, exist_ok=True)
|
||||
STATE.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
_cache["state"] = state
|
||||
|
||||
|
||||
def services():
|
||||
data, _ = _load()
|
||||
return sorted(data)
|
||||
|
||||
|
||||
def keys(service):
|
||||
data, _ = _load()
|
||||
return [r["key"] for r in (data.get(service) or []) if r.get("key")]
|
||||
|
||||
|
||||
def mask(k):
|
||||
k = k or ""
|
||||
return (k[:4] + "…" + k[-2:]) if len(k) > 8 else "…"
|
||||
|
||||
|
||||
def get(service, offset=0):
|
||||
"""Живой ключ сервиса: env, иначе ротация по пулу с учётом остывания."""
|
||||
env_name = ENV.get(service)
|
||||
if env_name and os.environ.get(env_name):
|
||||
return os.environ[env_name].strip()
|
||||
pool = keys(service)
|
||||
if not pool:
|
||||
return ""
|
||||
_, state = _load()
|
||||
st = state.get(service, {})
|
||||
now = time.time()
|
||||
avail = []
|
||||
for k in pool:
|
||||
until = st.get(k, 0)
|
||||
if until <= now:
|
||||
avail.append(k)
|
||||
if not avail:
|
||||
# всё остывает — берём тот, у кого срок истекает раньше
|
||||
k = min(pool, key=lambda x: st.get(x, 0))
|
||||
return k
|
||||
return avail[(offset + int(now)) % len(avail)] if len(avail) > 1 else avail[0]
|
||||
|
||||
|
||||
def mark(service, key, kind="dead"):
|
||||
if not key:
|
||||
return
|
||||
_, state = _load()
|
||||
st = state.setdefault(service, {})
|
||||
st[key] = time.time() + COOLDOWN.get(kind, 600)
|
||||
_save_state(state)
|
||||
|
||||
|
||||
def stats():
|
||||
data, state = _load()
|
||||
out = []
|
||||
now = time.time()
|
||||
for svc in sorted(data):
|
||||
rows = data[svc]
|
||||
st = state.get(svc, {})
|
||||
cooling = sum(1 for r in rows if st.get(r["key"], 0) > now)
|
||||
out.append({"service": svc, "keys": len(rows), "cooling": cooling,
|
||||
"ready": len(rows) - cooling,
|
||||
"env": bool(ENV.get(svc) and os.environ.get(ENV[svc]))})
|
||||
return out
|
||||
Reference in New Issue
Block a user