08a0157c95
调度页执行周期可视化/单次执行/分类选机/推送源对齐;登录 IP 门控与无缝导航; 服务器批量后台任务、执行记录、凭据合并、各页激活刷新与错误提示修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
6.6 KiB
Python
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
|