Files
Nexus/server/application/services/execution_record_service.py
T
Nexus Agent 08a0157c95 feat: 调度表单重构、登录门控、批量任务与页面缓存对齐
调度页执行周期可视化/单次执行/分类选机/推送源对齐;登录 IP 门控与无缝导航;
服务器批量后台任务、执行记录、凭据合并、各页激活刷新与错误提示修复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 11:17:21 +08:00

190 lines
6.6 KiB
Python

"""Unified execution records: script runs + server batch jobs."""
from __future__ import annotations
import json
from typing import Any, Optional
from server.application.services.script_service import ScriptService
from server.application.services.server_batch_service import get_batch_job, list_batch_jobs
from server.infrastructure.redis import server_batch_store as sbs
def _parse_server_ids(raw: Any) -> list[int]:
if isinstance(raw, list):
return [int(x) for x in raw if str(x).isdigit()]
if isinstance(raw, str):
try:
parsed = json.loads(raw or "[]")
except json.JSONDecodeError:
return []
if isinstance(parsed, list):
return [int(x) for x in parsed if str(x).isdigit()]
return []
def _script_label(view: dict[str, Any]) -> str:
label = (view.get("label") or "").strip()
if label:
return label
cmd = (view.get("command") or "").replace("[long_task] ", "").strip()
if len(cmd) > 48:
return cmd[:48] + "…"
return cmd or f"脚本执行 #{view.get('id')}"
def script_view_to_record(view: dict[str, Any]) -> dict[str, Any]:
sids = _parse_server_ids(view.get("server_ids"))
if not sids and view.get("results"):
sids = [int(k) for k in view["results"] if str(k).isdigit()]
return {
"id": int(view["id"]),
"kind": "script",
"label": _script_label(view),
"status": view.get("status") or "running",
"progress": view.get("progress") or "—",
"server_count": len(sids),
"operator": view.get("operator") or "",
"started_at": view.get("started_at"),
"completed_at": view.get("completed_at"),
"command": view.get("command"),
"op": None,
}
def batch_view_to_record(view: dict[str, Any]) -> dict[str, Any]:
total = int(view.get("total") or 0)
if not total:
total = len(_parse_server_ids(view.get("server_ids")))
return {
"id": int(view.get("job_id") or view.get("id") or 0),
"kind": "server_batch",
"label": view.get("label") or "服务器批量任务",
"status": view.get("status") or "running",
"progress": view.get("progress") or "—",
"server_count": total,
"operator": view.get("operator") or "",
"started_at": view.get("started_at"),
"completed_at": view.get("completed_at"),
"command": None,
"op": view.get("op") or "",
}
async def _overlay_batch_live(row_api: dict[str, Any]) -> dict[str, Any]:
job_id = int(row_api.get("job_id") or row_api.get("id") or 0)
if not job_id:
return row_api
live = await sbs.get_live(job_id)
if live:
fresh = sbs.live_to_api(live)
fresh["started_at"] = row_api.get("started_at")
fresh["completed_at"] = row_api.get("completed_at")
return fresh
return row_api
async def list_execution_records(
*,
script_service: ScriptService,
limit: int = 50,
offset: int = 0,
status: Optional[str] = None,
kind: Optional[str] = None,
) -> dict[str, Any]:
"""Merge script executions and server batch jobs, newest first."""
if kind == "script":
script_data = await script_service.list_executions(limit=limit, offset=offset, status=status)
items = [script_view_to_record(v) for v in script_data.get("items") or []]
return {
"items": items,
"total": int(script_data.get("total") or 0),
"limit": limit,
"offset": offset,
}
if kind == "server_batch":
batch_data = await list_batch_jobs(limit=limit, offset=offset, op=None)
items = []
for raw in batch_data.get("items") or []:
if status and raw.get("status") != status:
continue
overlaid = await _overlay_batch_live(raw)
items.append(batch_view_to_record(overlaid))
total = int(batch_data.get("total") or 0)
if status:
total = len(items)
return {"items": items, "total": total, "limit": limit, "offset": offset}
fetch_n = min(max(limit + offset, limit), 200)
script_data = await script_service.list_executions(limit=fetch_n, offset=0, status=status)
batch_data = await list_batch_jobs(limit=fetch_n, offset=0)
merged: list[dict[str, Any]] = []
for view in script_data.get("items") or []:
merged.append(script_view_to_record(view))
for raw in batch_data.get("items") or []:
if status and raw.get("status") != status:
continue
overlaid = await _overlay_batch_live(raw)
merged.append(batch_view_to_record(overlaid))
merged.sort(key=lambda x: x.get("started_at") or "", reverse=True)
page = merged[offset: offset + limit]
total = int(script_data.get("total") or 0) + int(batch_data.get("total") or 0)
return {"items": page, "total": total, "limit": limit, "offset": offset}
async def get_execution_record_detail(
*,
script_service: ScriptService,
record_id: int,
kind: str,
fetch_logs: bool = False,
) -> Optional[dict[str, Any]]:
if kind == "server_batch":
view = await get_batch_job(record_id)
if not view:
return None
results: dict[str, Any] = {}
for row in view.get("results") or []:
sid = str(row.get("server_id") or "")
if not sid.isdigit():
continue
ok = bool(row.get("success"))
results[sid] = {
"status": "success" if ok else "failed",
"stdout": row.get("stdout") or "",
"stderr": row.get("error") or "",
"exit_code": 0 if ok else 1,
}
server_ids = [int(k) for k in results]
server_names = {
str(r.get("server_id")): (r.get("server_name") or "")
for r in (view.get("results") or [])
if r.get("server_id") is not None
}
return {
"id": record_id,
"kind": "server_batch",
"label": view.get("label") or "",
"op": view.get("op") or "",
"status": view.get("status") or "running",
"progress": view.get("progress") or "—",
"operator": view.get("operator") or "",
"started_at": view.get("started_at"),
"completed_at": view.get("completed_at"),
"command": view.get("label") or "",
"server_ids": json.dumps(server_ids),
"results": results,
"server_names": server_names,
"long_task": False,
}
view = await script_service.get_execution_detail(record_id, fetch_logs=fetch_logs)
if not view:
return None
view["kind"] = "script"
return view