fix: 10 пунктов ревью — /run (кортеж store.add), валидация limit/offset в панели, close() соединений SQLite, троттлинг под локом, read-only бот без OMNI_BOT_OWNERS, eviction JOBS, push_new отдаёт свежие записи, ai.py не спит после последней попытки, дедуп реестра, подключён abuse-реестр (+191 источник); tests/test_regress.py 16 проверок + CI py3.10-3.12
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

This commit is contained in:
ENI
2026-10-26 20:15:50 +03:00
parent 136aa88dd6
commit c9c0a11ef7
16 changed files with 806 additions and 54 deletions

View File

@@ -3,6 +3,7 @@
Запуск: python -m omni serve --port 8077
"""
import json
import os
import pathlib
import re
import threading
@@ -25,11 +26,36 @@ def _jid(prefix):
return "%s%d" % (prefix, SEQ[0])
UI_TOKEN = os.environ.get("OMNI_UI_TOKEN", "")
def _ui_auth(req):
"""Панель наружу: без токена мутации открыты любому, кто дошёл до порта."""
if not UI_TOKEN:
return True
return req.headers.get("X-Omni-Token") == UI_TOKEN
def _intv(q, key, default, lo, hi):
"""int из query без падений: ?limit=abc раньше давал трейс и обрыв соединения,
?limit=-1 отдавал всю базу (SQLite LIMIT -1 = без лимита)."""
try:
v = int((q.get(key) or [str(default)])[0])
except (TypeError, ValueError):
return default
return max(lo, min(hi, v))
def _job(jid, **kw):
with _LOCK:
cur = JOBS.get(jid) or {}
cur.update(kw)
cur["_ts"] = time.time()
JOBS[jid] = cur
if len(JOBS) > 400: # раньше росло без границ (500 запусков -> 500 записей)
dead = sorted(JOBS.items(), key=lambda kv: kv[1].get("_ts", 0))[:len(JOBS) - 300]
for k, _v in dead:
JOBS.pop(k, None)
def _cat_health():
@@ -212,7 +238,7 @@ class Handler(BaseHTTPRequestHandler):
if path == "/api/runs":
try:
limit = max(1, min(500, int((q.get("limit") or ["80"])[0])))
limit = _intv(q, "limit", 80, 1, 500)
except ValueError:
limit = 80
import time as _t
@@ -241,7 +267,7 @@ class Handler(BaseHTTPRequestHandler):
if tag in ("all", "все"):
tag = None
try:
lim = max(1, min(200, int((q.get("limit") or ["40"])[0])))
lim = _intv(q, "limit", 40, 1, 200)
except ValueError:
lim = 40
return self._json(_ab.scan(limit=lim, tag=tag))
@@ -251,7 +277,7 @@ class Handler(BaseHTTPRequestHandler):
if tag in ("all", "все"):
tag = None
try:
lim = max(1, min(40, int((q.get("limit") or ["14"])[0])))
lim = _intv(q, "limit", 14, 1, 40)
except ValueError:
lim = 14
try:
@@ -302,8 +328,8 @@ class Handler(BaseHTTPRequestHandler):
if path == "/api/items":
cat = (q.get("category") or [None])[0]
query = (q.get("q") or [""])[0].strip()
limit = min(int((q.get("limit") or ["40"])[0]), 200)
offset = int((q.get("offset") or ["0"])[0])
limit = _intv(q, "limit", 40, 1, 200)
offset = _intv(q, "offset", 0, 0, 1_000_000)
sort = (q.get("sort") or ["score"])[0]
priced = (q.get("priced") or ["0"])[0] in ("1", "true", "on")
typ = (q.get("type") or ["all"])[0]
@@ -349,7 +375,9 @@ class Handler(BaseHTTPRequestHandler):
def do_POST(self):
u = urllib.parse.urlsplit(self.path)
n = int(self.headers.get("Content-Length") or 0)
if not _ui_auth(self):
return self._json({"error": "unauthorized"}, 401)
n = min(int(self.headers.get("Content-Length") or 0), 2_000_000)
body = {}
if n:
try:
@@ -360,8 +388,8 @@ class Handler(BaseHTTPRequestHandler):
if u.path == "/api/discover":
from . import discover as _d
q = urllib.parse.parse_qs(u.query)
hosts = max(5, min(120, int((q.get("hosts") or ["40"])[0])))
add = max(1, min(60, int((q.get("add") or ["25"])[0])))
hosts = _intv(q, "hosts", 40, 5, 120)
add = _intv(q, "add", 25, 1, 60)
dry = (q.get("dry") or ["0"])[0] not in ("0", "false", "")
res = _d.discover(limit_hosts=hosts, workers=8, max_add=add, dry=dry)
return self._json(res)
@@ -421,4 +449,6 @@ class Handler(BaseHTTPRequestHandler):
def serve(port=8077):
srv = ThreadingHTTPServer(("127.0.0.1", port), Handler)
print(f"omni ui: http://127.0.0.1:{port} (источников {len(sources.all_sources())})")
if not UI_TOKEN:
print("внимание: OMNI_UI_TOKEN не задан — POST /api/* без авторизации; наружу только за nginx с basic-auth")
srv.serve_forever()