0b2d7ac50d
脚本 exec 立即返回 running 并在后台跑批次;/ws/alerts 推送 script_progress/complete; 新增 ScriptRunsPage、Telegram 汇总通知与可配置的浏览器完成提示音。 Co-authored-by: Cursor <cursoragent@cursor.com>
138 lines
4.4 KiB
Python
138 lines
4.4 KiB
Python
"""WebSocket + Telegram notifications for script execution progress."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any, Optional
|
|
|
|
from server.config import settings
|
|
from server.domain.repositories import ScriptExecutionRepository, ScriptRepository
|
|
from server.infrastructure.redis import script_execution_store as ses
|
|
|
|
logger = logging.getLogger("nexus.script_execution_notify")
|
|
|
|
|
|
def _script_board_url(execution_id: int) -> str:
|
|
base = (settings.API_BASE_URL or "").strip().rstrip("/")
|
|
if not base:
|
|
return f"/app/#/script-runs/{execution_id}"
|
|
return f"{base}/app/#/script-runs/{execution_id}"
|
|
|
|
|
|
async def _resolve_label(
|
|
live: dict[str, Any],
|
|
script_repo: Optional[ScriptRepository] = None,
|
|
) -> str:
|
|
label = (live.get("label") or "").strip()
|
|
if label:
|
|
return label
|
|
script_id = live.get("script_id")
|
|
if script_id and script_repo:
|
|
script = await script_repo.get_by_id(int(script_id))
|
|
if script and script.name:
|
|
return script.name
|
|
cmd = str(live.get("command") or "")
|
|
if cmd.startswith("[long_task] "):
|
|
cmd = cmd[len("[long_task] "):]
|
|
if len(cmd) > 48:
|
|
return cmd[:48] + "…"
|
|
return cmd or f"执行 #{live.get('id')}"
|
|
|
|
|
|
def _ws_payload(live: dict[str, Any], label: str) -> dict[str, Any]:
|
|
results = live.get("results") or {}
|
|
server_ids = live.get("server_ids") or []
|
|
stats = ses.execution_progress_stats(results, server_ids)
|
|
return {
|
|
"execution_id": int(live["id"]),
|
|
"label": label,
|
|
"status": live.get("status") or "running",
|
|
"long_task": bool(live.get("long_task")),
|
|
"operator": live.get("operator") or "",
|
|
"script_id": live.get("script_id"),
|
|
**stats,
|
|
}
|
|
|
|
|
|
async def notify_script_progress(
|
|
live: dict[str, Any],
|
|
*,
|
|
script_repo: Optional[ScriptRepository] = None,
|
|
) -> None:
|
|
"""Broadcast running progress (throttle at caller for per-server updates)."""
|
|
try:
|
|
from server.api.websocket import broadcast_script_progress
|
|
|
|
label = await _resolve_label(live, script_repo)
|
|
await broadcast_script_progress(_ws_payload(live, label))
|
|
except Exception as e:
|
|
logger.warning("script progress notify failed: %s", e)
|
|
|
|
|
|
async def notify_script_complete(
|
|
live: dict[str, Any],
|
|
*,
|
|
script_repo: Optional[ScriptRepository] = None,
|
|
) -> None:
|
|
"""Broadcast terminal state + optional Telegram summary."""
|
|
status = live.get("status") or ""
|
|
if status not in ses.TERMINAL_STATUSES:
|
|
return
|
|
try:
|
|
from server.api.websocket import broadcast_script_complete
|
|
|
|
label = await _resolve_label(live, script_repo)
|
|
payload = _ws_payload(live, label)
|
|
await broadcast_script_complete(payload)
|
|
|
|
from server.infrastructure.telegram import send_telegram_script_summary
|
|
|
|
results = live.get("results") or {}
|
|
server_ids = live.get("server_ids") or []
|
|
stats = ses.execution_progress_stats(results, server_ids)
|
|
await send_telegram_script_summary(
|
|
execution_id=int(live["id"]),
|
|
script_label=label,
|
|
operator=str(live.get("operator") or "admin"),
|
|
status=status,
|
|
success_count=int(stats["success"]),
|
|
failed_count=int(stats["failed"]),
|
|
total=int(stats["total"]),
|
|
board_url=_script_board_url(int(live["id"])),
|
|
)
|
|
except Exception as e:
|
|
logger.warning("script complete notify failed: %s", e)
|
|
|
|
|
|
async def notify_script_complete_for_execution(
|
|
execution_repo: ScriptExecutionRepository,
|
|
execution_id: int,
|
|
*,
|
|
script_repo: Optional[ScriptRepository] = None,
|
|
) -> None:
|
|
view = await ses.get_execution_view(execution_repo, execution_id)
|
|
if not view:
|
|
return
|
|
raw_ids = view.get("server_ids")
|
|
if isinstance(raw_ids, str):
|
|
try:
|
|
server_ids = json.loads(raw_ids or "[]")
|
|
except json.JSONDecodeError:
|
|
server_ids = []
|
|
else:
|
|
server_ids = raw_ids or []
|
|
live = {
|
|
"id": view["id"],
|
|
"script_id": view.get("script_id"),
|
|
"command": view.get("command"),
|
|
"server_ids": server_ids,
|
|
"status": view.get("status"),
|
|
"results": view.get("results") or {},
|
|
"operator": view.get("operator"),
|
|
"long_task": view.get("long_task", False),
|
|
"label": view.get("label"),
|
|
}
|
|
await notify_script_complete(live, script_repo=script_repo)
|
|
|