v1.6.0: max_bot fixes — feature keys, flush→commit, test-run, categories, broadcast page, proxy error handling, deploy scripts

This commit is contained in:
2026-05-24 07:50:38 +03:00
parent bd048ea23d
commit 493e0b37a1
127 changed files with 6082 additions and 65 deletions
+245 -64
View File
@@ -49,7 +49,8 @@ def ctx(request, user, **kw):
"role_permissions": getattr(request.state, 'role_permissions', {}),
"role_override_active": original_user is not None,
"original_role": original_user["role"] if original_user else None,
"available_roles": available_roles.get(user["role"], [user["role"]]) if user else []}
"available_roles": available_roles.get(user["role"], [user["role"]]) if user else [],
"page_name": kw.get("active_page", "")}
c.update(kw)
return c
@@ -76,11 +77,13 @@ async def users_create(request: Request, db: AsyncSession = Depends(get_db), use
if not login or not password:
return RedirectResponse(url="/service/users", status_code=302)
from app.auth import hash_password
max_id_val = form.get("max_user_id")
new_user = User(
login=login,
password_hash=hash_password(password),
full_name=form.get("full_name"), role=form.get("role", "technician"),
phone=form.get("phone", ""), email=form.get("email", ""),
max_user_id=int(max_id_val) if max_id_val and max_id_val.strip() else None,
is_active=form.get("is_active", "1") == "1",
)
db.add(new_user); await db.flush()
@@ -102,6 +105,8 @@ async def users_edit(request: Request, db: AsyncSession = Depends(get_db), user:
u.role = form.get("role", u.role)
u.phone = form.get("phone", u.phone)
u.email = form.get("email", u.email)
max_id_val = form.get("max_user_id")
u.max_user_id = int(max_id_val) if max_id_val and max_id_val.strip() else None
if "is_active" in form:
u.is_active = form["is_active"] == "1"
from app.auth import hash_password
@@ -1243,104 +1248,85 @@ import aiohttp
MAX_BOT_URL = "http://127.0.0.1:8002"
async def _proxy(method, path, json_body=None):
try:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
async with session.request(method, f"{MAX_BOT_URL}{path}", json=json_body) as resp:
return JSONResponse(await resp.json())
except aiohttp.ClientError as e:
return JSONResponse({"error": f"Bot service unavailable: {e}", "ok": False}, status_code=503)
except Exception as e:
return JSONResponse({"error": str(e), "ok": False}, status_code=500)
@router.get("/service/api/bot/settings")
async def api_bot_settings(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
async with aiohttp.ClientSession() as session:
async with session.get(f"{MAX_BOT_URL}/api/bot/settings") as resp:
return JSONResponse(await resp.json())
return await _proxy("GET", "/api/bot/settings")
@router.post("/service/api/bot/settings/save")
async def api_bot_settings_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
async with aiohttp.ClientSession() as session:
async with session.post(f"{MAX_BOT_URL}/api/bot/settings/save", json=body) as resp:
return JSONResponse(await resp.json())
return await _proxy("POST", "/api/bot/settings/save", body)
@router.get("/service/api/bot/templates")
async def api_bot_templates(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
async with aiohttp.ClientSession() as session:
async with session.get(f"{MAX_BOT_URL}/api/bot/templates") as resp:
return JSONResponse(await resp.json())
return await _proxy("GET", "/api/bot/templates")
@router.post("/service/api/bot/templates/save")
async def api_bot_templates_save(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
async with aiohttp.ClientSession() as session:
async with session.post(f"{MAX_BOT_URL}/api/bot/templates/save", json=body) as resp:
return JSONResponse(await resp.json())
return await _proxy("POST", "/api/bot/templates/save", body)
@router.get("/service/api/bot/features")
async def api_bot_features(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
async with aiohttp.ClientSession() as session:
async with session.get(f"{MAX_BOT_URL}/api/bot/features") as resp:
return JSONResponse(await resp.json())
return await _proxy("GET", "/api/bot/features")
@router.post("/service/api/bot/features/toggle")
async def api_bot_features_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
async with aiohttp.ClientSession() as session:
async with session.post(f"{MAX_BOT_URL}/api/bot/features/toggle", json=body) as resp:
return JSONResponse(await resp.json())
return await _proxy("POST", "/api/bot/features/toggle", body)
@router.get("/service/api/bot/categories")
async def api_bot_categories(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
async with aiohttp.ClientSession() as session:
async with session.get(f"{MAX_BOT_URL}/api/bot/categories") as resp:
return JSONResponse(await resp.json())
return await _proxy("GET", "/api/bot/categories")
@router.post("/service/api/bot/categories/create")
async def api_bot_categories_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
async with aiohttp.ClientSession() as session:
async with session.post(f"{MAX_BOT_URL}/api/bot/categories/create", json=body) as resp:
return JSONResponse(await resp.json())
return await _proxy("POST", "/api/bot/categories/create", body)
@router.post("/service/api/bot/categories/edit")
async def api_bot_categories_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
async with aiohttp.ClientSession() as session:
async with session.post(f"{MAX_BOT_URL}/api/bot/categories/edit", json=body) as resp:
return JSONResponse(await resp.json())
return await _proxy("POST", "/api/bot/categories/edit", body)
@router.post("/service/api/bot/categories/delete")
async def api_bot_categories_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
async with aiohttp.ClientSession() as session:
async with session.post(f"{MAX_BOT_URL}/api/bot/categories/delete", json=body) as resp:
return JSONResponse(await resp.json())
return await _proxy("POST", "/api/bot/categories/delete", body)
@router.get("/service/api/bot/kb")
async def api_bot_kb(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
async with aiohttp.ClientSession() as session:
async with session.get(f"{MAX_BOT_URL}/api/bot/kb") as resp:
return JSONResponse(await resp.json())
return await _proxy("GET", "/api/bot/kb")
@router.post("/service/api/bot/kb/create")
async def api_bot_kb_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
async with aiohttp.ClientSession() as session:
async with session.post(f"{MAX_BOT_URL}/api/bot/kb/create", json=body) as resp:
return JSONResponse(await resp.json())
return await _proxy("POST", "/api/bot/kb/create", body)
@router.post("/service/api/bot/kb/edit")
async def api_bot_kb_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
async with aiohttp.ClientSession() as session:
async with session.post(f"{MAX_BOT_URL}/api/bot/kb/edit", json=body) as resp:
return JSONResponse(await resp.json())
return await _proxy("POST", "/api/bot/kb/edit", body)
@router.post("/service/api/bot/kb/delete")
async def api_bot_kb_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
async with aiohttp.ClientSession() as session:
async with session.post(f"{MAX_BOT_URL}/api/bot/kb/delete", json=body) as resp:
return JSONResponse(await resp.json())
return await _proxy("POST", "/api/bot/kb/delete", body)
@router.get("/service/api/bot/conversations")
async def api_bot_conversations(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
async with aiohttp.ClientSession() as session:
async with session.get(f"{MAX_BOT_URL}/api/bot/conversations") as resp:
return JSONResponse(await resp.json())
return await _proxy("GET", "/api/bot/conversations")
@router.get("/service/api/bot/users")
async def api_bot_users(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
@@ -1350,22 +1336,16 @@ async def api_bot_users(db: AsyncSession = Depends(get_db), user: dict = Depends
params = f"?page={page}&limit={limit}&sort_by={sort_by}&sort_dir={sort_dir}"
if consent:
params += f"&consent={consent}"
async with aiohttp.ClientSession() as session:
async with session.get(f"{MAX_BOT_URL}/api/bot/users{params}") as resp:
return JSONResponse(await resp.json())
return await _proxy("GET", f"/api/bot/users{params}")
@router.get("/service/api/bot/analytics")
async def api_bot_analytics(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
async with aiohttp.ClientSession() as session:
async with session.get(f"{MAX_BOT_URL}/api/bot/analytics") as resp:
return JSONResponse(await resp.json())
return await _proxy("GET", "/api/bot/analytics")
@router.post("/service/api/bot/test-run")
async def api_bot_test_run(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
async with aiohttp.ClientSession() as session:
async with session.post(f"{MAX_BOT_URL}/api/bot/test-run", json=body) as resp:
return JSONResponse(await resp.json())
return await _proxy("POST", "/api/bot/test-run", body)
@router.get("/service/api/bot/broadcast/recipients")
async def api_bot_broadcast_recipients(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
@@ -1373,20 +1353,221 @@ async def api_bot_broadcast_recipients(db: AsyncSession = Depends(get_db), user:
params = f"?page={page}&limit={limit}"
if q:
params += f"&q={q}"
async with aiohttp.ClientSession() as session:
async with session.get(f"{MAX_BOT_URL}/api/bot/broadcast/recipients{params}") as resp:
return JSONResponse(await resp.json())
return await _proxy("GET", f"/api/bot/broadcast/recipients{params}")
@router.get("/service/api/bot/broadcast/history")
async def api_bot_broadcast_history(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
page: int = Query(1), limit: int = Query(20)):
async with aiohttp.ClientSession() as session:
async with session.get(f"{MAX_BOT_URL}/api/bot/broadcast/history?page={page}&limit={limit}") as resp:
return JSONResponse(await resp.json())
return await _proxy("GET", f"/api/bot/broadcast/history?page={page}&limit={limit}")
@router.post("/service/api/bot/broadcast")
async def api_bot_broadcast(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
async with aiohttp.ClientSession() as session:
async with session.post(f"{MAX_BOT_URL}/api/bot/broadcast", json=body) as resp:
return JSONResponse(await resp.json())
return await _proxy("POST", "/api/bot/broadcast", body)
@router.get("/service/bot-settings/handoffs")
async def bot_handoffs_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_handoffs.html", ctx(request, user=user, active_page="bot-handoffs", title="Handoff-диалоги"))
@router.get("/service/bot-settings/handoffs/poll")
async def bot_handoffs_poll(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/handoffs")
@router.post("/service/bot-settings/handoffs/{handoff_id}/take")
async def bot_handoff_take(handoff_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("POST", f"/api/bot/handoffs/{handoff_id}/take")
@router.get("/service/bot-settings/notifications")
async def bot_notifications_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_notifications.html", ctx(request, user=user, active_page="bot-notifications", title="Уведомления бота"))
@router.get("/service/bot-settings/notifications/data")
async def bot_notifications_data(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/notifications")
@router.post("/service/bot-settings/notifications/{notif_id}/read")
async def bot_notification_read(notif_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("POST", f"/api/bot/notifications/{notif_id}/read")
# ===================== BOT RISK QUESTIONS =====================
@router.get("/service/bot-settings/risk-questions")
async def bot_risk_questions_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_risk_questions.html", ctx(request, user=user, active_page="bot-risk-questions", title="Вопросы риск-инжиниринга"))
@router.get("/service/bot-settings/storage")
async def bot_storage_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_storage.html", ctx(request, user=user, active_page="bot-storage", title="Яндекс.Диск"))
@router.get("/service/bot-settings/broadcasts")
async def bot_broadcasts_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_broadcasts.html", ctx(request, user=user, active_page="bot-broadcasts", title="Рассылки"))
@router.get("/service/api/bot/risk-questions")
async def api_bot_risk_questions(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/risk-questions")
@router.post("/service/api/bot/risk-questions/create")
async def api_bot_risk_questions_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/risk-questions/create", body)
@router.post("/service/api/bot/risk-questions/edit")
async def api_bot_risk_questions_edit(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/risk-questions/edit", body)
@router.post("/service/api/bot/risk-questions/toggle")
async def api_bot_risk_questions_toggle(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/risk-questions/toggle", body)
@router.post("/service/api/bot/risk-questions/delete")
async def api_bot_risk_questions_delete(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/risk-questions/delete", body)
# ===================== BOT TICKETS =====================
@router.get("/service/bot-settings/tickets")
async def bot_tickets_page(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return templates.TemplateResponse("pages/bot_tickets.html", ctx(request, user=user, active_page="bot-tickets", title="Заявки"))
@router.get("/service/api/bot/tickets")
async def api_bot_tickets(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner")),
status: str = Query(None), priority: str = Query(None)):
params = ""
if status: params += f"status={status}&"
if priority: params += f"priority={priority}"
return await _proxy("GET", f"/api/bot/tickets?{params}")
@router.get("/service/api/bot/tickets/{ticket_id}")
async def api_bot_ticket_get(ticket_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", f"/api/bot/tickets/{ticket_id}")
@router.post("/service/api/bot/tickets/create")
async def api_bot_tickets_create(request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", "/api/bot/tickets/create", body)
@router.post("/service/api/bot/tickets/{ticket_id}/message")
async def api_bot_tickets_message(ticket_id: int, request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", f"/api/bot/tickets/{ticket_id}/message", body)
@router.post("/service/api/bot/tickets/{ticket_id}/status")
async def api_bot_tickets_status(ticket_id: int, request: Request, db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
body = await request.json()
return await _proxy("POST", f"/api/bot/tickets/{ticket_id}/status", body)
@router.get("/service/api/bot/engineers")
async def api_bot_engineers(db: AsyncSession = Depends(get_db), user: dict = Depends(require_role("owner"))):
return await _proxy("GET", "/api/bot/engineers")
# ===================== BOT STORAGE (Yandex Disk) =====================
async def _get_yandex_client(db: AsyncSession):
from app.yandex_disk import YandexDiskClient
result = await db.execute(text("SELECT value FROM bot_settings WHERE key = 'yandex_disk_token'"))
row = result.scalar_one_or_none()
if not row:
return None
return YandexDiskClient(row)
@router.get("/service/api/bot/storage/list")
async def api_bot_storage_list(path: str = Query(""), db: AsyncSession = Depends(get_db),
user: dict = Depends(require_role("owner"))):
client = await _get_yandex_client(db)
if not client:
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
try:
items = await client.list_folder(path)
return JSONResponse(items)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
finally:
await client.close()
@router.get("/service/api/bot/storage/search")
async def api_bot_storage_search(query: str = Query(""), db: AsyncSession = Depends(get_db),
user: dict = Depends(require_role("owner"))):
client = await _get_yandex_client(db)
if not client:
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
try:
items = await client.search(query)
return JSONResponse(items)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
finally:
await client.close()
@router.get("/service/api/bot/storage/public-link")
async def api_bot_storage_public_link(path: str = Query(""), expiry: int = Query(30),
db: AsyncSession = Depends(get_db),
user: dict = Depends(require_role("owner"))):
client = await _get_yandex_client(db)
if not client:
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
try:
url = await client.get_public_url(path, expiry)
return JSONResponse({"url": url})
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
finally:
await client.close()
@router.get("/service/api/bot/storage/download")
async def api_bot_storage_download(path: str = Query(""), db: AsyncSession = Depends(get_db),
user: dict = Depends(require_role("owner"))):
client = await _get_yandex_client(db)
if not client:
return JSONResponse({"error": "Yandex Disk not configured"}, status_code=400)
try:
info = await client.get_file_info(path)
dl_url = info.get("public_url", "")
if not dl_url:
dl_info = await client._request("GET", "/resources/download", params={"path": path})
dl_url = dl_info.get("href", "")
return JSONResponse({"url": dl_url})
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
finally:
await client.close()
# ===================== HANDOFF CLOSE =====================
@router.post("/service/api/bot/handoffs/{handoff_id}/close")
async def api_bot_handoff_close(handoff_id: int, db: AsyncSession = Depends(get_db),
user: dict = Depends(require_role("owner"))):
return await _proxy("POST", f"/api/bot/handoffs/{handoff_id}/close")