Files
Nexus/server/application/services/script_execution_notify.py
T
Nexus Agent 0b2d7ac50d feat(scripts): 异步执行、全局进度看板与完成提示音设置
脚本 exec 立即返回 running 并在后台跑批次;/ws/alerts 推送 script_progress/complete;
新增 ScriptRunsPage、Telegram 汇总通知与可配置的浏览器完成提示音。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 07:07:02 +08:00

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)