v1.8.0: YandexGPT-only NLP, unknown questions system, callback dedup, consent flow fixes, DELETE cascade, UTC+3, auto-detect sidebar menu, tel:/mailto: links

This commit is contained in:
2026-06-01 18:07:13 +03:00
parent aa1269e013
commit cc87bcf72c
20 changed files with 1453 additions and 301 deletions
+91 -3
View File
@@ -1158,6 +1158,11 @@ async def bot_kb_page(request: Request, db: AsyncSession = Depends(get_db), user
return templates.TemplateResponse("pages/bot_kb.html", ctx(request, user=user, active_page="bot-kb", title="База знаний чат-бота"))
@router.get("/service/bot-unknown")
async def bot_unknown_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
return templates.TemplateResponse("pages/bot_unknown.html", ctx(request, user=user, active_page="bot-unknown", title="Неизвестные вопросы"))
@router.get("/service/bot-test")
async def bot_test_page_redirect():
return RedirectResponse(url="/service/bot-settings", status_code=302)
@@ -1180,6 +1185,13 @@ async def bot_tickets_page(request: Request, db: AsyncSession = Depends(get_db),
return templates.TemplateResponse("pages/bot_tickets.html", ctx(request, user=user, active_page="bot-tickets", title="Заявки от чат-бота"))
@router.get("/service/bot-dialogues")
async def bot_dialogues_page(request: Request, user: dict = Depends(get_current_user)):
if not user:
return RedirectResponse(url="/service/login", status_code=302)
return templates.TemplateResponse("pages/bot_dialogues.html", ctx(request, user=user, active_page="bot-dialogues", title="Диалоги чат-бота"))
# ---- Role permissions API ----
@router.get("/service/api/role-permissions")
async def api_role_permissions_get(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
@@ -1208,6 +1220,44 @@ async def api_role_permissions_save(request: Request, db: AsyncSession = Depends
return JSONResponse({"ok": True})
import re
import os
def parse_sidebar_menu():
sidebar_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates", "_sidebar.html")
try:
with open(sidebar_path, "r", encoding="utf-8") as f:
html = f.read()
except FileNotFoundError:
return []
items = []
current_section = ""
for line in html.split("\n"):
sec = re.search(r'sidebar-section[^>]*>([^<]+)', line)
if sec:
current_section = sec.group(1).strip()
perm = re.search(r"role_permissions\.get\('(\w+)'", line)
label = re.search(r'>([^<]+)</a>', line)
if perm and label:
items.append({"key": perm.group(1), "label": label.group(1).strip(), "section": current_section})
return items
@router.get("/service/api/menu-items")
async def api_menu_items(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
from app.models.models import RoleMenuPermission
from sqlalchemy import select as sa_select
items = parse_sidebar_menu()
existing = await db.execute(sa_select(RoleMenuPermission))
existing_keys = {(r.role, r.menu_key) for r in existing.scalars().all()}
for item in items:
for role in ("engineer", "technician"):
if (role, item["key"]) not in existing_keys:
db.add(RoleMenuPermission(role=role, menu_key=item["key"], visible=False))
await db.commit()
return JSONResponse(items)
# ===================== ROLE SWITCH =====================
@router.post("/service/api/role/switch")
@@ -1334,6 +1384,13 @@ async def proxy_bot_broadcast_preview(request: Request, user: dict = Depends(get
return await _proxy("POST", "/api/bot/broadcast/preview", body)
@router.post("/api/bot/test/clear")
async def proxy_bot_test_clear(user: dict = Depends(get_current_user)):
if not user:
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
return await _proxy("POST", "/api/bot/test/clear")
@router.post("/api/bot/webhook")
async def proxy_bot_webhook(request: Request):
body = await request.json()
@@ -1341,8 +1398,39 @@ async def proxy_bot_webhook(request: Request):
secret = request.headers.get("X-Max-Bot-Api-Secret")
if secret:
headers["X-Max-Bot-Api-Secret"] = secret
async with httpx.AsyncClient() as client:
resp = await client.post(f"{BOT_SERVICE_URL}/webhook", json=body, headers=headers)
return JSONResponse(resp.json(), status_code=resp.status_code)
try:
async with httpx.AsyncClient(timeout=120.0) as client:
resp = await client.post(f"{BOT_SERVICE_URL}/webhook", json=body, headers=headers)
return JSONResponse(resp.json(), status_code=resp.status_code)
except httpx.TimeoutException:
return JSONResponse({"ok": False, "error": "Max bot timeout"}, status_code=504)
except Exception as e:
return JSONResponse({"ok": False, "error": str(e)}, status_code=502)
@router.get("/api/bot/conversations")
async def proxy_bot_conversations(user: dict = Depends(get_current_user)):
if not user:
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
return await _proxy("GET", "/api/bot/conversations")
@router.get("/api/bot/conversations/{conv_id}/messages")
async def proxy_bot_conversation_messages(conv_id: int, user: dict = Depends(get_current_user)):
if not user:
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
return await _proxy("GET", f"/api/bot/conversations/{conv_id}/messages")
@router.get("/api/bot/conversations/{conv_id}/logs")
async def proxy_bot_conversation_logs(conv_id: int, user: dict = Depends(get_current_user)):
if not user:
return JSONResponse({"ok": False, "error": "Not authenticated"}, status_code=401)
return await _proxy("GET", f"/api/bot/conversations/{conv_id}/logs")
@router.delete("/api/bot/conversations/{conv_id}")
async def proxy_delete_conversation(conv_id: int, request: Request, user: dict = Depends(require_role("owner"))):
return await _proxy("DELETE", f"/api/bot/conversations/{conv_id}")