From 1f050991f071689b2243f8a0779071b9927504b3 Mon Sep 17 00:00:00 2001 From: huangzhuang_3rd Date: Thu, 13 Aug 2026 00:46:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20V1=20-=20DAG=E7=BC=96=E6=8E=92/?= =?UTF-8?q?=E5=91=8A=E8=AD=A6=E7=B3=BB=E7=BB=9F/Agent=E5=BE=AA=E7=8E=AF/?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BA=93RAG/Webhook=20+=20=E5=AE=A1=E6=A0=B8?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=AF=8C=E4=B8=8A=E4=B8=8B=E6=96=87=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/alert_service.py | 101 ++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 backend/app/services/alert_service.py diff --git a/backend/app/services/alert_service.py b/backend/app/services/alert_service.py new file mode 100644 index 0000000..a542813 --- /dev/null +++ b/backend/app/services/alert_service.py @@ -0,0 +1,101 @@ +"""Alert service: budget monitoring and system alerts.""" +from sqlalchemy.orm import Session +from typing import List, Optional +from app.models.alert import Alert +from app.models.project import Project +from app.models.worker import AIWorker +from app.services.cost_service import CostService + + +class AlertService: + @staticmethod + def check_and_alert(db: Session, tenant_id: int, project_id: int) -> Optional[Alert]: + """Check project budget and create alert if needed.""" + project = db.query(Project).filter( + Project.id == project_id, Project.tenant_id == tenant_id + ).first() + if not project or not project.budget_limit_cents: + return None + + spent = CostService.get_project_cost(db, tenant_id, project_id) + limit = project.budget_limit_cents + pct = (spent / limit) if limit > 0 else 0 + + if pct >= 1.0: + # Pause project + project.status = "paused" + db.commit() + return AlertService._create( + db, tenant_id, "budget_exceeded", "critical", + f"项目「{project.name}」预算已超限!已花费 ¥{spent/100:.2f} / ¥{limit/100:.2f},项目已自动暂停。", + project_id=project_id, + ) + elif pct >= 0.8: + return AlertService._create( + db, tenant_id, "budget_warning", "warning", + f"项目「{project.name}」预算使用达 {pct*100:.0f}%(¥{spent/100:.2f} / ¥{limit/100:.2f})", + project_id=project_id, + ) + return None + + @staticmethod + def check_worker_budget(db: Session, tenant_id: int, worker_id: int) -> Optional[Alert]: + worker = db.query(AIWorker).filter( + AIWorker.id == worker_id, AIWorker.tenant_id == tenant_id + ).first() + if not worker or not worker.max_cost_per_month_cents: + return None + + spent = CostService.get_worker_monthly_cost(db, tenant_id, worker_id) + limit = worker.max_cost_per_month_cents + pct = (spent / limit) if limit > 0 else 0 + + if pct >= 1.0: + worker.is_active = False + db.commit() + return AlertService._create( + db, tenant_id, "budget_exceeded", "critical", + f"Worker「{worker.name}」月度预算已超限!已花费 ¥{spent/100:.2f} / ¥{limit/100:.2f},Worker已停用。", + worker_id=worker_id, + ) + elif pct >= 0.8: + return AlertService._create( + db, tenant_id, "budget_warning", "warning", + f"Worker「{worker.name}」月度预算使用达 {pct*100:.0f}%", + worker_id=worker_id, + ) + return None + + @staticmethod + def _create(db, tenant_id, alert_type, severity, message, project_id=None, worker_id=None): + alert = Alert( + tenant_id=tenant_id, alert_type=alert_type, severity=severity, + message=message, project_id=project_id, worker_id=worker_id, + ) + db.add(alert) + db.commit() + db.refresh(alert) + return alert + + @staticmethod + def list_alerts(db, tenant_id, is_read=None, skip=0, limit=50): + q = db.query(Alert).filter(Alert.tenant_id == tenant_id) + if is_read is not None: + q = q.filter(Alert.is_read == is_read) + return q.order_by(Alert.created_at.desc()).offset(skip).limit(limit).all() + + @staticmethod + def unread_count(db, tenant_id): + return db.query(Alert).filter( + Alert.tenant_id == tenant_id, Alert.is_read == False + ).count() + + @staticmethod + def mark_read(db, tenant_id, alert_id): + alert = db.query(Alert).filter( + Alert.id == alert_id, Alert.tenant_id == tenant_id + ).first() + if alert: + alert.is_read = True + db.commit() + return alert