import logging from typing import Optional from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect from sqlmodel import Session from core.database import engine, get_session from core.docker_instance import docker_manager from core.websocket_manager import manager from services.bot_runtime_service import ( delete_cron_job as delete_cron_job_service, ensure_monitor_websocket_access, get_bot_logs as get_bot_logs_service, list_cron_jobs as list_cron_jobs_service, relogin_weixin as relogin_weixin_service, start_cron_job as start_cron_job_service, stop_cron_job as stop_cron_job_service, ) from services.runtime_service import docker_callback router = APIRouter() logger = logging.getLogger("dashboard.backend") @router.get("/api/bots/{bot_id}/logs") def get_bot_logs( bot_id: str, tail: Optional[int] = 300, offset: int = 0, limit: Optional[int] = None, reverse: bool = False, session: Session = Depends(get_session), ): try: return get_bot_logs_service( session, bot_id=bot_id, tail=tail, offset=offset, limit=limit, reverse=reverse, ) except LookupError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @router.post("/api/bots/{bot_id}/weixin/relogin") async def relogin_weixin(bot_id: str, session: Session = Depends(get_session)): try: return await relogin_weixin_service(session, bot_id=bot_id) except LookupError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc except ValueError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc except RuntimeError as exc: raise HTTPException(status_code=500, detail=str(exc)) from exc @router.get("/api/bots/{bot_id}/cron/jobs") def list_cron_jobs(bot_id: str, include_disabled: bool = True, session: Session = Depends(get_session)): try: return list_cron_jobs_service(session, bot_id=bot_id, include_disabled=include_disabled) except LookupError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @router.post("/api/bots/{bot_id}/cron/jobs/{job_id}/stop") def stop_cron_job(bot_id: str, job_id: str, session: Session = Depends(get_session)): try: return stop_cron_job_service(session, bot_id=bot_id, job_id=job_id) except LookupError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @router.post("/api/bots/{bot_id}/cron/jobs/{job_id}/start") def start_cron_job(bot_id: str, job_id: str, session: Session = Depends(get_session)): try: return start_cron_job_service(session, bot_id=bot_id, job_id=job_id) except LookupError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @router.delete("/api/bots/{bot_id}/cron/jobs/{job_id}") def delete_cron_job(bot_id: str, job_id: str, session: Session = Depends(get_session)): try: return delete_cron_job_service(session, bot_id=bot_id, job_id=job_id) except LookupError as exc: raise HTTPException(status_code=404, detail=str(exc)) from exc @router.websocket("/ws/monitor/{bot_id}") async def websocket_endpoint(websocket: WebSocket, bot_id: str): with Session(engine) as session: try: ensure_monitor_websocket_access(session, websocket, bot_id) except PermissionError: await websocket.close(code=4401, reason="Bot or panel authentication required") return except LookupError: await websocket.close(code=4404, reason="Bot not found") return connected = False try: await manager.connect(bot_id, websocket) connected = True except Exception as exc: logger.warning("websocket connect failed bot_id=%s detail=%s", bot_id, exc) try: await websocket.close(code=1011, reason="WebSocket accept failed") except Exception: pass return docker_manager.ensure_monitor(bot_id, docker_callback) try: while True: await websocket.receive_text() with Session(engine) as session: try: ensure_monitor_websocket_access(session, websocket, bot_id) except PermissionError: await websocket.close(code=4401, reason="Authentication expired") return except LookupError: await websocket.close(code=4404, reason="Bot not found") return except WebSocketDisconnect: pass except RuntimeError as exc: msg = str(exc or "").lower() if "need to call \"accept\" first" not in msg and "not connected" not in msg: logger.exception("websocket runtime error bot_id=%s", bot_id) except Exception: logger.exception("websocket unexpected error bot_id=%s", bot_id) finally: if connected: manager.disconnect(bot_id, websocket)