Интегрирован 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,208 @@
"""Read-only проверки файловых SQLite для подсказок LLM (без произвольного SQL из чата)."""
from __future__ import annotations
import sqlite3
from pathlib import Path
from typing import Any, Dict, List, Optional
from flask import Flask
from app.services.admin_dashboard_service import sqlite_bind_paths
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
cur = conn.execute(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1",
(table,),
)
return cur.fetchone() is not None
def _open_sqlite_readonly(path: Path) -> sqlite3.Connection:
uri = path.as_uri() + "?mode=ro"
return sqlite3.connect(uri, uri=True, timeout=12.0)
def _pragma_integrity(conn: sqlite3.Connection) -> Dict[str, Any]:
rows = conn.execute("PRAGMA integrity_check").fetchall()
msgs = [str(r[0]) for r in rows if r and r[0] is not None]
ok = len(msgs) == 1 and str(msgs[0]).lower() == "ok"
return {"ok": ok, "messages": msgs[:25] if not ok else []}
def _pragma_foreign_keys(conn: sqlite3.Connection) -> Dict[str, Any]:
try:
rows = conn.execute("PRAGMA foreign_key_check").fetchall()
except sqlite3.OperationalError:
return {"count": 0, "samples": [], "note": "foreign_key_check недоступен"}
samples: List[str] = []
for r in rows[:8]:
samples.append("|".join(str(x) for x in r))
return {"count": len(rows), "samples": samples}
def _domain_recipes_bind(conn: sqlite3.Connection) -> Dict[str, Any]:
"""Типичные логические проблемы в основной БД (recipes)."""
out: Dict[str, Any] = {}
if _table_exists(conn, "loading_report"):
try:
n = conn.execute(
"SELECT COUNT(*) FROM loading_report WHERE "
"recipe_id IS NULL OR length(recipe_id) != 36"
).fetchone()[0]
out["loading_report_bad_recipe_id_count"] = int(n)
except sqlite3.Error as e:
out["loading_report_check_error"] = str(e)[:200]
if _table_exists(conn, "unloading_report"):
try:
n = conn.execute(
"SELECT COUNT(*) FROM unloading_report WHERE "
"recipe_id IS NULL OR length(recipe_id) != 36"
).fetchone()[0]
out["unloading_report_bad_recipe_id_count"] = int(n)
except sqlite3.Error as e:
out["unloading_report_check_error"] = str(e)[:200]
if _table_exists(conn, "sync_queue"):
try:
n = conn.execute(
"SELECT COUNT(*) FROM sync_queue WHERE "
"(COALESCE(is_deleted, 0) = 0) AND status = 'failed'"
).fetchone()[0]
out["sync_queue_failed_count"] = int(n)
except sqlite3.Error as e:
out["sync_queue_check_error"] = str(e)[:200]
return out
def _one_bind_health(bind_name: str, path: Path) -> Dict[str, Any]:
entry: Dict[str, Any] = {"bind": bind_name, "path": str(path)}
if not path.is_file():
entry["error"] = "файл не найден"
return entry
try:
entry["size_bytes"] = path.stat().st_size
except OSError as e:
entry["error"] = str(e)
return entry
try:
conn = _open_sqlite_readonly(path)
except sqlite3.Error as e:
entry["error"] = f"не удалось открыть read-only: {e}"
return entry
try:
ver = conn.execute("SELECT sqlite_version()").fetchone()
entry["sqlite_version"] = str(ver[0]) if ver else "?"
entry["integrity"] = _pragma_integrity(conn)
entry["foreign_keys"] = _pragma_foreign_keys(conn)
try:
ntbl = conn.execute(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
).fetchone()[0]
entry["user_table_count"] = int(ntbl)
except sqlite3.Error:
pass
if bind_name == "recipes":
entry["domain_checks"] = _domain_recipes_bind(conn)
finally:
conn.close()
return entry
def build_llm_db_health_payload(app: Flask) -> Dict[str, Any]:
"""Структура для API и для текста LLM."""
paths = sqlite_bind_paths(app)
if not paths:
return {
"binds": {},
"note": "Нет файловых баз SQLite в конфигурации (или только :memory:) — PRAGMA-проверки не запускались.",
}
binds_out: Dict[str, Any] = {}
for name, pth in sorted(paths.items(), key=lambda x: x[0]):
binds_out[name] = _one_bind_health(name, pth)
return {"binds": binds_out}
def build_llm_db_health_block(app: Flask) -> str:
"""Текст на русском для system: что проверено и что похоже на ошибку."""
payload = build_llm_db_health_payload(app)
if payload.get("note"):
return (
"Проверка баз данных SQLite\n"
+ str(payload["note"])
)
lines: List[str] = [
"Проверка баз данных SQLite (только чтение: PRAGMA integrity_check, foreign_key_check, несколько эвристик).",
"Ниже не полный аудит — ориентир для диагностики; исправления делает администратор (SQL/админка/миграции).",
"",
]
for bind_name in sorted(payload.get("binds", {}).keys()):
b = payload["binds"][bind_name]
title = f"База «{bind_name}»"
if b.get("error"):
lines.append(f"- {title}: ошибка — {b['error']}")
lines.append("")
continue
lines.append(f"- {title}: файл {b.get('path', '')}")
if b.get("size_bytes") is not None:
sz = int(b["size_bytes"])
lines.append(f" размер файла: {sz} байт")
if b.get("sqlite_version"):
lines.append(f" SQLite: {b['sqlite_version']}")
if b.get("user_table_count") is not None:
lines.append(f" пользовательских таблиц: {b['user_table_count']}")
integ = b.get("integrity") or {}
if integ.get("ok") is True:
lines.append(" PRAGMA integrity_check: ok")
else:
lines.append(" PRAGMA integrity_check: ПРОБЛЕМА")
for m in integ.get("messages") or ["(нет деталей)"]:
lines.append(f" · {m}")
fk = b.get("foreign_keys") or {}
fc = int(fk.get("count") or 0)
if fc == 0:
lines.append(" PRAGMA foreign_key_check: нарушений не найдено (0)")
else:
lines.append(f" PRAGMA foreign_key_check: найдено нарушений: {fc} (проверьте ссылки между таблицами)")
for s in fk.get("samples") or []:
lines.append(f" · {s}")
dom = b.get("domain_checks") or {}
if dom:
if "loading_report_bad_recipe_id_count" in dom:
n = dom["loading_report_bad_recipe_id_count"]
if n > 0:
lines.append(
f" Эвристика: в loading_report строк с recipe_id не UUID (NULL или длина≠36): {n}"
)
else:
lines.append(
" Эвристика: loading_report — подозрительных recipe_id (NULL / не UUID) не найдено"
)
if "unloading_report_bad_recipe_id_count" in dom:
n = dom["unloading_report_bad_recipe_id_count"]
if n > 0:
lines.append(
f" Эвристика: в unloading_report строк с recipe_id не UUID: {n}"
)
else:
lines.append(
" Эвристика: unloading_report — подозрительных recipe_id не найдено"
)
if "sync_queue_failed_count" in dom:
n = dom["sync_queue_failed_count"]
if n > 0:
lines.append(
f" Эвристика: sync_queue с status=failed (не удалённые): {n}"
)
else:
lines.append(" Эвристика: sync_queue — задач со status=failed не найдено")
lines.append("")
lines.append(
"Как отвечать: если пользователь спрашивает про ошибки в базе — опирайся на этот блок; "
"явно различай повреждение файла (integrity), битые FK (foreign_key_check) и логические эвристики (UUID, очередь sync)."
)
text = "\n".join(lines).strip()
max_len = 5500
if len(text) > max_len:
return text[: max_len - 1] + ""
return text