TaskPulse/backend/app/main.py
Steven 521cf0269c feat: initial TaskPulse release
AI Agent 定时任务跟踪、监控、状态看板系统。

- FastAPI + Vue3 单体应用
- AI Agent 一键接入(批量注册 API)
- 暗色主题 Dashboard
- 后台调度器 + 超时告警
- 飞书/邮件/Webhook 多渠道通知
- Base URL 可配置
- Gunicorn 生产部署
2026-06-14 22:39:23 +08:00

81 lines
2.6 KiB
Python

"""FastAPI application entry point."""
import asyncio
import logging
from pathlib import Path
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from app.api.agents import router as agents_router
from app.api.tasks import router as tasks_router
from app.api.executions import router as executions_router
from app.api.notifications import router as notifications_router
from app.api.dashboard import router as dashboard_router
from app.api.system import router as system_router
from app.config import settings
from app.database import engine, Base
from app.services.scheduler import SchedulerService
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s")
logger = logging.getLogger("taskpulse")
scheduler = SchedulerService()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Startup: create tables + start background scheduler."""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info("Database tables ensured")
# Start background scheduler
task = asyncio.create_task(scheduler.start())
yield
# Shutdown
await scheduler.stop()
task.cancel()
await engine.dispose()
app = FastAPI(
title="TaskPulse — AI Agent Task Monitor",
description="Unified dashboard for tracking, monitoring and alerting on AI agent scheduled tasks.",
version="1.0.0",
lifespan=lifespan,
)
# API routers
app.include_router(agents_router)
app.include_router(tasks_router)
app.include_router(executions_router)
app.include_router(notifications_router)
app.include_router(dashboard_router)
app.include_router(system_router)
# Serve Vue3 static files (SPA — monolithic deployment)
FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
if FRONTEND_DIST.is_dir():
# Mount assets directory for JS/CSS/fonts
app.mount("/assets", StaticFiles(directory=str(FRONTEND_DIST / "assets")), name="assets")
@app.exception_handler(404)
async def spa_fallback(request: Request, exc):
"""Return index.html for any non-API route (Vue Router SPA)."""
if not request.url.path.startswith("/api"):
content = (FRONTEND_DIST / "index.html").read_text(encoding="utf-8")
return HTMLResponse(content=content, status_code=200)
raise exc
logger.info("Frontend SPA mounted from %s", FRONTEND_DIST)
else:
logger.info("Frontend dist not found at %s — API-only mode", FRONTEND_DIST)
@app.get("/api/health")
async def health():
return {"status": "ok", "service": "taskpulse"}