Интегрирован wesp в сайт
CI / quality (push) Canceled after 0s

This commit is contained in:
влад
2026-07-17 12:57:18 +03:00
parent 5dfa06ddbe
commit 355c0ef9f1
883 changed files with 194576 additions and 177 deletions
@@ -0,0 +1,215 @@
"""Журнал вызовов локального ИИ в админке (JSONL): ping, сводки, чат."""
from __future__ import annotations
import json
import logging
import os
import threading
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
_LOG = logging.getLogger(__name__)
_LOCK = threading.Lock()
_VALID_LEVELS = frozenset({"ok", "err", "warn"})
_VALID_EVENTS = frozenset(
{
"ping",
"summary",
"chat",
"diagnostics",
"settings",
"status",
}
)
def _llm_log_path(app) -> str:
return (app.config.get("WESP_ADMIN_LLM_LOG_PATH") or "").strip()
def _line_unix_ts(obj: Dict[str, Any]) -> Optional[float]:
u = obj.get("u")
if isinstance(u, (int, float)):
return float(u)
return None
def _retention_days(app) -> int:
return max(1, min(int(app.config.get("WESP_ADMIN_LLM_LOG_RETENTION_DAYS", 7) or 7), 30))
def _resolve_path(app, path_str: str) -> Path:
path = Path(path_str)
if not path.is_absolute():
path = Path(app.config.get("BASE_DIR", ".")) / path
return path
def append_admin_llm_activity(
app,
*,
event: str,
level: str = "ok",
user_login: str = "",
detail: str = "",
duration_ms: Optional[int] = None,
meta: Optional[Dict[str, Any]] = None,
) -> None:
"""Дописывает одну строку JSON; старые записи обрезаются по сроку хранения."""
try:
_append_admin_llm_activity_impl(
app,
event=event,
level=level,
user_login=user_login,
detail=detail,
duration_ms=duration_ms,
meta=meta,
)
except Exception:
_LOG.exception("Сбой журнала ИИ (WESP_ADMIN_LLM_LOG_PATH).")
def _append_admin_llm_activity_impl(
app,
*,
event: str,
level: str,
user_login: str,
detail: str,
duration_ms: Optional[int],
meta: Optional[Dict[str, Any]],
) -> None:
path_str = _llm_log_path(app)
if not path_str or app.config.get("TESTING"):
return
ev = str(event or "chat").strip().lower()
if ev not in _VALID_EVENTS:
ev = "chat"
lvl = str(level or "ok").strip().lower()
if lvl not in _VALID_LEVELS:
lvl = "ok"
row: Dict[str, Any] = {
"u": int(time.time()),
"event": ev,
"level": lvl,
"user": (user_login or "")[:128],
"detail": (detail or "")[:2000],
}
if isinstance(duration_ms, (int, float)) and duration_ms >= 0:
row["duration_ms"] = int(duration_ms)
if meta and isinstance(meta, dict):
slim: Dict[str, Any] = {}
for k, v in list(meta.items())[:24]:
key = str(k)[:64]
if isinstance(v, (str, int, float, bool)) or v is None:
slim[key] = v if v is None else (str(v)[:500] if isinstance(v, str) else v)
elif isinstance(v, (list, dict)):
try:
slim[key] = json.loads(json.dumps(v, ensure_ascii=False)[:2000])
except (TypeError, ValueError):
slim[key] = str(v)[:500]
if slim:
row["meta"] = slim
line = json.dumps(row, ensure_ascii=False, separators=(",", ":")) + "\n"
path = _resolve_path(app, path_str)
cutoff = time.time() - float(_retention_days(app) * 86400)
with _LOCK:
path.parent.mkdir(parents=True, exist_ok=True)
_rewrite_prune_and_append(path, cutoff, line)
def _rewrite_prune_and_append(path: Path, cutoff_ts: float, new_line: str) -> None:
lines_kept: List[str] = []
if path.is_file():
try:
with path.open("r", encoding="utf-8") as f:
for ln in f:
raw = ln.strip()
if not raw:
continue
try:
obj = json.loads(raw)
except json.JSONDecodeError:
continue
ts = _line_unix_ts(obj)
if ts is not None and ts >= cutoff_ts:
lines_kept.append(raw + "\n")
except OSError:
lines_kept = []
tmp = path.with_name(f"{path.name}.tmp.{os.getpid()}")
try:
with tmp.open("w", encoding="utf-8") as out:
for ln in lines_kept:
out.write(ln)
out.write(new_line)
os.replace(str(tmp), str(path))
except OSError:
try:
if tmp.is_file():
tmp.unlink(missing_ok=True)
except OSError:
pass
def format_llm_log_line_text(obj: Dict[str, Any]) -> str:
"""Одна строка для ленты активности и UI."""
ev = str(obj.get("event") or "llm")
detail = str(obj.get("detail") or "").strip()
dur = obj.get("duration_ms")
user = str(obj.get("user") or "").strip()
bits = [f"llm/{ev}"]
if detail:
bits.append(detail)
if isinstance(dur, (int, float)) and dur >= 0:
bits.append(f"{int(dur)} ms")
if user:
bits.append(f"· {user}")
return " ".join(bits)[:2000]
def read_admin_llm_activity(
app, *, max_lines: int = 200
) -> tuple[List[Dict[str, Any]], str]:
"""Последние записи за окно хранения (для API и ленты)."""
path_str = _llm_log_path(app)
if not path_str or app.config.get("TESTING"):
return [], ""
path = _resolve_path(app, path_str)
if not path.is_file():
return [], str(path)
cutoff = time.time() - float(_retention_days(app) * 86400)
cap = max(1, min(int(max_lines or 200), 500))
out: List[Dict[str, Any]] = []
try:
with path.open("r", encoding="utf-8") as f:
for ln in f:
raw = ln.strip()
if not raw:
continue
try:
obj = json.loads(raw)
except json.JSONDecodeError:
continue
if not isinstance(obj, dict):
continue
ts = _line_unix_ts(obj)
if ts is None or ts < cutoff:
continue
out.append(obj)
except OSError:
return [], str(path)
out.sort(key=lambda x: float(_line_unix_ts(x) or 0))
return out[-cap:], str(path)