9c9a51263c
汇总未提交工作区:Servers 列表 site_url 链接、终端页与选机器 UI、SSH 连通性与 Telegram 通知、agent/offline/unset_path 后台调度与 gate_ai_review 门控;更新 AGENTS 与 agent-exploration 规则(仅本机 commit);同步 web/app 构建产物与相关文档。
112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
"""Scheduled nightly detect-path for servers with unset target_path."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from server.config import settings
|
|
from server.domain.models import Server
|
|
from server.infrastructure.database.server_repo import ServerRepositoryImpl
|
|
from server.infrastructure.database.session import AsyncSessionLocal
|
|
from server.infrastructure.redis import server_batch_store as sbs
|
|
from server.infrastructure.redis.client import get_redis
|
|
from server.utils.display_time import to_beijing
|
|
|
|
logger = logging.getLogger("nexus.unset_path_detect_schedule")
|
|
|
|
REDIS_LAST_RUN_KEY = "nexus:unset_path_detect:last_run_date"
|
|
SCHEDULED_OPERATOR = "system"
|
|
|
|
|
|
def _enabled() -> bool:
|
|
return str(getattr(settings, "UNSET_PATH_DETECT_ENABLED", "true")).lower() == "true"
|
|
|
|
|
|
def _cron_expr() -> str:
|
|
expr = (getattr(settings, "UNSET_PATH_DETECT_CRON", None) or "0 2 * * *").strip()
|
|
return expr or "0 2 * * *"
|
|
|
|
|
|
def server_ssh_configured(server: Server) -> bool:
|
|
auth = (server.auth_method or "key").lower()
|
|
if auth == "password":
|
|
return bool(server.password)
|
|
return bool(server.ssh_key_private or server.ssh_key_configured)
|
|
|
|
|
|
async def list_unset_server_ids(*, ssh_only: bool = True) -> list[int]:
|
|
"""Return server IDs with unset target_path (empty or ``/www/wwwroot``)."""
|
|
async with AsyncSessionLocal() as session:
|
|
repo = ServerRepositoryImpl(session)
|
|
servers, _total = await repo.get_filtered(target_path_unset=True, max_rows=5000)
|
|
ids: list[int] = []
|
|
for server in servers:
|
|
if ssh_only and not server_ssh_configured(server):
|
|
continue
|
|
ids.append(server.id)
|
|
return ids
|
|
|
|
|
|
async def _detect_path_batch_running() -> bool:
|
|
for live in await sbs.list_running_live_jobs():
|
|
if live.get("op") == "detect-path":
|
|
return True
|
|
return False
|
|
|
|
|
|
async def _mark_ran_today() -> None:
|
|
today = to_beijing(datetime.now(timezone.utc)).strftime("%Y-%m-%d")
|
|
redis = get_redis()
|
|
await redis.set(REDIS_LAST_RUN_KEY, today, ex=86400 * 2)
|
|
|
|
|
|
async def _already_ran_today() -> bool:
|
|
today = to_beijing(datetime.now(timezone.utc)).strftime("%Y-%m-%d")
|
|
redis = get_redis()
|
|
last = await redis.get(REDIS_LAST_RUN_KEY)
|
|
if last is None:
|
|
return False
|
|
if isinstance(last, bytes):
|
|
last = last.decode()
|
|
return str(last).strip() == today
|
|
|
|
|
|
async def run_scheduled_unset_path_detect(*, force: bool = False) -> dict[str, Any]:
|
|
"""Start batch detect-path for unset servers; idempotent once per Beijing calendar day."""
|
|
if not _enabled() and not force:
|
|
return {"action": "skipped", "reason": "disabled"}
|
|
|
|
if not force and await _already_ran_today():
|
|
return {"action": "skipped", "reason": "already_ran_today"}
|
|
|
|
if await _detect_path_batch_running():
|
|
return {"action": "skipped", "reason": "detect_path_batch_running"}
|
|
|
|
server_ids = await list_unset_server_ids()
|
|
if not server_ids:
|
|
await _mark_ran_today()
|
|
return {"action": "skipped", "reason": "no_unset_servers", "server_count": 0}
|
|
|
|
from server.application.services.server_batch_service import start_batch_job
|
|
|
|
started = await start_batch_job(
|
|
op="detect-path",
|
|
server_ids=server_ids,
|
|
operator=SCHEDULED_OPERATOR,
|
|
params={"scheduled": True, "cron": _cron_expr()},
|
|
)
|
|
await _mark_ran_today()
|
|
logger.info(
|
|
"Scheduled unset target_path detect started job_id=%s servers=%s",
|
|
started.get("job_id"),
|
|
len(server_ids),
|
|
)
|
|
return {
|
|
"action": "started",
|
|
"job_id": started.get("job_id"),
|
|
"server_count": len(server_ids),
|
|
"label": started.get("label"),
|
|
}
|