Files
omni-parse/omni/vault.py
ENI 2320acf3c5
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
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
2026-10-26 23:31:37 +03:00

128 lines
3.9 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.

"""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