"""Central Nexus Agent version (SSOT) and semver-style comparison.""" from __future__ import annotations import re from functools import lru_cache from pathlib import Path _AGENT_PY = Path(__file__).resolve().parents[2] / "web" / "agent" / "agent.py" _AGENT_VERSION_CONST_RE = re.compile(r'^AGENT_VERSION\s*=\s*"([^"]+)"', re.MULTILINE) @lru_cache(maxsize=1) def get_central_agent_version() -> str: """Read AGENT_VERSION constant from web/agent/agent.py (heartbeat SSOT).""" text = _AGENT_PY.read_text(encoding="utf-8") match = _AGENT_VERSION_CONST_RE.search(text) if not match: raise RuntimeError(f"AGENT_VERSION not found in {_AGENT_PY}") return match.group(1) def version_tuple(version: str) -> tuple[int, ...]: """Parse dotted numeric prefix, e.g. 2.0.0 -> (2, 0, 0).""" version = (version or "").strip() if not version: return () parts: list[int] = [] for segment in re.split(r"[.-]", version): if segment.isdigit(): parts.append(int(segment)) else: break return tuple(parts) def is_version_lower(remote: str, central: str) -> bool: """True when remote is strictly older than central.""" remote_t = version_tuple(remote) central_t = version_tuple(central) if not remote_t: return True if not central_t: return False return remote_t < central_t def is_version_at_least(remote: str, central: str) -> bool: """True when remote >= central (equal counts as skip).""" remote_t = version_tuple(remote) central_t = version_tuple(central) if not remote_t: return False if not central_t: return bool(remote_t) return remote_t >= central_t def agent_is_installed( *, agent_version_db: str | None, heartbeat: dict[str, str] | None = None, ) -> bool: """True when Agent has reported a version or heartbeat (not merely a DB API key).""" if heartbeat: if (heartbeat.get("agent_version") or "").strip(): return True if heartbeat.get("last_heartbeat"): return True return bool((agent_version_db or "").strip()) def compute_agent_guidance( *, agent_version_db: str | None, heartbeat: dict[str, str] | None = None, central: str | None = None, ) -> dict[str, str | None]: """UI hints: install / upgrade / offline / ok.""" central_v = central or get_central_agent_version() live_ver = "" if heartbeat: live_ver = (heartbeat.get("agent_version") or "").strip() if not live_ver: live_ver = (agent_version_db or "").strip() has_heartbeat = bool(heartbeat and heartbeat.get("last_heartbeat")) installed = agent_is_installed(agent_version_db=agent_version_db, heartbeat=heartbeat) base: dict[str, str | None] = { "central_agent_version": central_v, "agent_action": "ok", "agent_action_message": None, } if not installed: return { **base, "agent_action": "install", "agent_action_message": ( f"尚未安装 Agent(主站版本 {central_v})。" "安装后可自动上报 CPU/内存/磁盘与在线状态;未安装时仅能通过 SSH 手动探活。" ), } if live_ver and is_version_lower(live_ver, central_v): return { **base, "agent_action": "upgrade", "agent_action_message": ( f"当前 Agent {live_ver},低于主站 {central_v},建议升级以获取最新心跳与探针能力。" ), } if not has_heartbeat: return { **base, "agent_action": "offline", "agent_action_message": ( "Agent 已登记但暂无心跳,请检查子机 nexus-agent 服务或点击「诊断」。" ), } return base