TaskPulse/backend/app/api/notifications.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

159 lines
5.8 KiB
Python

"""API router — Notifications & Alerts."""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.schemas import (
AlertAcknowledge,
AlertOut,
AlertRuleCreate,
AlertRuleOut,
NotificationChannelCreate,
NotificationChannelOut,
NotificationChannelUpdate,
)
from app.models import Alert, AlertRule, NotificationChannel, ScheduledTask
router = APIRouter(prefix="/api", tags=["notifications"])
# ── Notification Channels ─────────────────────────────────────────────
@router.post("/notification-channels", response_model=NotificationChannelOut, status_code=201)
async def create_channel(body: NotificationChannelCreate, db: AsyncSession = Depends(get_db)):
channel = NotificationChannel(
name=body.name,
channel_type=body.channel_type,
config=body.config,
)
db.add(channel)
await db.flush()
await db.refresh(channel)
await db.commit()
return NotificationChannelOut(**channel.__dict__)
@router.get("/notification-channels", response_model=list[NotificationChannelOut])
async def list_channels(db: AsyncSession = Depends(get_db)):
from sqlalchemy import select
result = await db.execute(select(NotificationChannel).order_by(NotificationChannel.id))
channels = result.scalars().all()
return [NotificationChannelOut(**c.__dict__) for c in channels]
@router.put("/notification-channels/{channel_id}", response_model=NotificationChannelOut)
async def update_channel(channel_id: int, body: NotificationChannelUpdate,
db: AsyncSession = Depends(get_db)):
channel = await db.get(NotificationChannel, channel_id)
if not channel:
raise HTTPException(404, "Channel not found")
for k, v in body.model_dump(exclude_none=True).items():
setattr(channel, k, v)
await db.flush()
await db.commit()
return NotificationChannelOut(**channel.__dict__)
@router.delete("/notification-channels/{channel_id}", status_code=204)
async def delete_channel(channel_id: int, db: AsyncSession = Depends(get_db)):
channel = await db.get(NotificationChannel, channel_id)
if not channel:
raise HTTPException(404, "Channel not found")
await db.delete(channel)
await db.flush()
await db.commit()
# ── Alert Rules ───────────────────────────────────────────────────────
@router.post("/alert-rules", response_model=AlertRuleOut, status_code=201)
async def create_alert_rule(body: AlertRuleCreate, db: AsyncSession = Depends(get_db)):
# Verify task & channel exist
task = await db.get(ScheduledTask, body.task_id)
if not task:
raise HTTPException(404, "Task not found")
channel = await db.get(NotificationChannel, body.channel_id)
if not channel:
raise HTTPException(404, "Channel not found")
rule = AlertRule(
task_id=body.task_id,
channel_id=body.channel_id,
alert_type=body.alert_type,
)
db.add(rule)
await db.flush()
await db.refresh(rule)
await db.commit()
return AlertRuleOut(**{**rule.__dict__, "channel_name": channel.name})
@router.get("/alert-rules", response_model=list[AlertRuleOut])
async def list_alert_rules(task_id: int | None = Query(None), db: AsyncSession = Depends(get_db)):
from sqlalchemy import select
stmt = select(AlertRule)
if task_id:
stmt = stmt.where(AlertRule.task_id == task_id)
result = await db.execute(stmt)
rules = result.scalars().all()
output = []
for r in rules:
ch = await db.get(NotificationChannel, r.channel_id)
output.append(AlertRuleOut(**{**r.__dict__, "channel_name": ch.name if ch else ""}))
return output
@router.delete("/alert-rules/{rule_id}", status_code=204)
async def delete_alert_rule(rule_id: int, db: AsyncSession = Depends(get_db)):
rule = await db.get(AlertRule, rule_id)
if not rule:
raise HTTPException(404, "Alert rule not found")
await db.delete(rule)
await db.flush()
await db.commit()
# ── Alerts ────────────────────────────────────────────────────────────
@router.get("/alerts", response_model=list[AlertOut])
async def list_alerts(
status: str | None = Query(None),
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db),
):
from sqlalchemy import select, desc
stmt = select(Alert).order_by(desc(Alert.id)).offset(offset).limit(limit)
if status:
stmt = stmt.where(Alert.status == status)
result = await db.execute(stmt)
alerts = result.scalars().all()
output = []
for a in alerts:
task_name = ""
if a.task_id:
task = await db.get(ScheduledTask, a.task_id)
task_name = task.name if task else ""
output.append(AlertOut(**{**a.__dict__, "task_name": task_name}))
return output
@router.post("/alerts/{alert_id}/acknowledge", response_model=AlertOut)
async def acknowledge_alert(alert_id: int, body: AlertAcknowledge,
db: AsyncSession = Depends(get_db)):
from datetime import datetime
alert = await db.get(Alert, alert_id)
if not alert:
raise HTTPException(404, "Alert not found")
alert.status = "acknowledged"
alert.acknowledged_at = datetime.utcnow()
await db.flush()
await db.commit()
task_name = ""
if alert.task_id:
task = await db.get(ScheduledTask, alert.task_id)
task_name = task.name if task else ""
return AlertOut(**{**alert.__dict__, "task_name": task_name})