mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
249 lines
7.8 KiB
Python
249 lines
7.8 KiB
Python
"""
|
|
AstrBot Gateway - FastAPI server for the dashboard backend.
|
|
|
|
Provides REST API endpoints and WebSocket connections for the frontend dashboard.
|
|
The gateway acts as the communication bridge between the dashboard and the orchestrator.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from contextlib import asynccontextmanager
|
|
from typing import TYPE_CHECKING, Any, cast
|
|
|
|
from astrbot import logger
|
|
from astrbot._internal.abc.base_astrbot_gateway import BaseAstrbotGateway
|
|
from astrbot._internal.abc.base_astrbot_orchestrator import BaseAstrbotOrchestrator
|
|
from astrbot._internal.geteway.ws_manager import WebSocketManager
|
|
|
|
if TYPE_CHECKING:
|
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
else:
|
|
try:
|
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
except ImportError:
|
|
logger.warning("FastAPI not installed, gateway unavailable.")
|
|
FastAPI = cast(Any, None)
|
|
WebSocket = cast(Any, None)
|
|
WebSocketDisconnect = cast(Any, None)
|
|
CORSMiddleware = cast(Any, None)
|
|
|
|
log = logger
|
|
|
|
|
|
class AstrbotGateway(BaseAstrbotGateway):
|
|
"""
|
|
FastAPI-based gateway server for AstrBot.
|
|
|
|
Handles:
|
|
- REST API endpoints for configuration and stats
|
|
- WebSocket connections for real-time communication
|
|
- CORS middleware for dashboard access
|
|
"""
|
|
|
|
def __init__(self, orchestrator: BaseAstrbotOrchestrator) -> None:
|
|
self.orchestrator = orchestrator
|
|
self.ws_manager = WebSocketManager()
|
|
self._app: FastAPI | None = None
|
|
self._host = "0.0.0.0"
|
|
self._port = 8765
|
|
|
|
async def serve(self) -> None:
|
|
"""
|
|
Start the gateway server.
|
|
|
|
Creates and runs a FastAPI application with WebSocket support.
|
|
"""
|
|
if FastAPI is None:
|
|
raise RuntimeError("FastAPI is not installed")
|
|
|
|
log.info(f"Starting AstrBot Gateway on {self._host}:{self._port}")
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# Startup
|
|
log.info("Gateway server started.")
|
|
yield
|
|
# Shutdown
|
|
await self.ws_manager.broadcast({"type": "server_shutdown"})
|
|
log.info("Gateway server stopped.")
|
|
|
|
self._app = FastAPI(
|
|
title="AstrBot Gateway",
|
|
description="Backend API for AstrBot dashboard",
|
|
version="1.0.0",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
# CORS middleware
|
|
self._app.add_middleware(
|
|
cast(Any, CORSMiddleware),
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Include routers
|
|
self._setup_routes()
|
|
|
|
# Run with uvicorn
|
|
import uvicorn
|
|
|
|
config = uvicorn.Config(
|
|
self._app,
|
|
host=self._host,
|
|
port=self._port,
|
|
log_level="info",
|
|
)
|
|
server = uvicorn.Server(config)
|
|
await server.serve()
|
|
|
|
def _setup_routes(self) -> None:
|
|
"""Set up API routes."""
|
|
if self._app is None:
|
|
return
|
|
|
|
from fastapi import APIRouter
|
|
|
|
# Health check
|
|
@self._app.get("/health")
|
|
async def health():
|
|
return {"status": "ok"}
|
|
|
|
# WebSocket endpoint
|
|
@self._app.websocket("/ws")
|
|
async def websocket_endpoint(ws: WebSocket):
|
|
await self.ws_manager.connect(ws)
|
|
try:
|
|
while True:
|
|
data = await ws.receive_text()
|
|
try:
|
|
message = json.loads(data)
|
|
response = await self._handle_ws_message(message)
|
|
if response:
|
|
await ws.send_json(response)
|
|
except json.JSONDecodeError:
|
|
await ws.send_json({"error": "Invalid JSON"})
|
|
except WebSocketDisconnect:
|
|
self.ws_manager.disconnect(ws)
|
|
|
|
# Stats router
|
|
stats_router = APIRouter(prefix="/api/stats", tags=["stats"])
|
|
|
|
@stats_router.get("/overview")
|
|
async def get_overview():
|
|
return await self._get_stats_overview()
|
|
|
|
self._app.include_router(stats_router)
|
|
|
|
# Inspector router
|
|
inspector_router = APIRouter(prefix="/api/inspector", tags=["inspector"])
|
|
|
|
@inspector_router.get("/stars")
|
|
async def list_stars():
|
|
return await self._list_stars()
|
|
|
|
@inspector_router.get("/stars/{star_name}")
|
|
async def get_star(star_name: str):
|
|
return await self._get_star_detail(star_name)
|
|
|
|
self._app.include_router(inspector_router)
|
|
|
|
# Memory router
|
|
memory_router = APIRouter(prefix="/api/memory", tags=["memory"])
|
|
|
|
@memory_router.get("/")
|
|
async def get_memory():
|
|
return await self._get_memory_info()
|
|
|
|
self._app.include_router(memory_router)
|
|
|
|
async def _handle_ws_message(
|
|
self, message: dict[str, Any]
|
|
) -> dict[str, Any] | None:
|
|
"""
|
|
Handle an incoming WebSocket message.
|
|
|
|
Args:
|
|
message: Parsed JSON message from the client
|
|
|
|
Returns:
|
|
Response message to send back, or None for no response
|
|
"""
|
|
msg_type = message.get("type")
|
|
data = message.get("data", {})
|
|
|
|
if msg_type == "ping":
|
|
return {"type": "pong", "data": {}}
|
|
|
|
if msg_type == "call_tool":
|
|
return await self._handle_call_tool(data)
|
|
|
|
if msg_type == "get_stars":
|
|
return {"type": "stars_list", "data": await self._list_stars()}
|
|
|
|
return {
|
|
"type": "error",
|
|
"data": {"message": f"Unknown message type: {msg_type}"},
|
|
}
|
|
|
|
async def _handle_call_tool(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
"""Handle a tool call request via WebSocket."""
|
|
star_name = data.get("star")
|
|
tool_name = data.get("tool")
|
|
arguments = data.get("arguments", {})
|
|
|
|
if not star_name or not tool_name:
|
|
return {
|
|
"type": "tool_result",
|
|
"data": {"error": "Missing star or tool name"},
|
|
}
|
|
|
|
try:
|
|
result = await self.orchestrator.abp.call_star_tool(
|
|
star_name, tool_name, arguments
|
|
)
|
|
return {"type": "tool_result", "data": {"result": result}}
|
|
except Exception as e:
|
|
return {"type": "tool_result", "data": {"error": str(e)}}
|
|
|
|
async def _get_stats_overview(self) -> dict[str, Any]:
|
|
"""Get overview statistics."""
|
|
return {
|
|
"stars_count": len(self.orchestrator.abp._stars),
|
|
"lsp_connected": self.orchestrator.lsp._connected,
|
|
"mcp_sessions": getattr(self.orchestrator.mcp, "session", None) is not None,
|
|
"acp_clients": len(getattr(self.orchestrator.acp, "_clients", [])),
|
|
}
|
|
|
|
async def _list_stars(self) -> list[dict[str, Any]]:
|
|
"""List all registered stars."""
|
|
stars = []
|
|
for name in self.orchestrator.abp._stars:
|
|
stars.append({"name": name, "status": "active"})
|
|
return stars
|
|
|
|
async def _get_star_detail(self, star_name: str) -> dict[str, Any]:
|
|
"""Get details of a specific star."""
|
|
star = self.orchestrator.abp._stars.get(star_name)
|
|
if not star:
|
|
return {"error": f"Star '{star_name}' not found"}
|
|
return {"name": star_name, "status": "active"}
|
|
|
|
async def _get_memory_info(self) -> dict[str, Any]:
|
|
"""Get memory usage information."""
|
|
import gc
|
|
|
|
gc.collect()
|
|
return {
|
|
"gc_objects": len(gc.get_objects()),
|
|
"python_memory": "N/A", # Would need psutil for actual values
|
|
}
|
|
|
|
def set_listen_address(self, host: str, port: int) -> None:
|
|
"""Set the listen address for the gateway server."""
|
|
self._host = host
|
|
self._port = port
|