fix: 全量修复批量选择、JWT 60min、script-callback 认证与心跳数据流

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-06-01 12:17:47 +08:00
parent 73fdcafb81
commit 8311821590
25 changed files with 506 additions and 152 deletions
+17 -12
View File
@@ -147,7 +147,7 @@ async def receive_heartbeat(
1. Write heartbeat to Redis (frontend reads Redis for live status)
2. Detect alerts (CPU/mem/disk > threshold) → WebSocket + Telegram push
3. Detect recoveries (previously alerted metrics now normal) → push
4. Also update MySQL via ServerService (for persistent record)
4. MySQL history via heartbeat_flush (10min), not per-request writes
"""
server_id = payload.server_id
@@ -171,6 +171,14 @@ async def receive_heartbeat(
if not await _verify_server_api_key(server_id, api_key, service):
raise HTTPException(status_code=401, detail="Invalid API key for this server")
server = await service.get_server(server_id)
if not server:
logger.warning(
"Heartbeat discarded: unknown server_id=%s (unregistered host)",
server_id,
)
return {"status": "discarded", "reason": "unknown_server"}
# ── 1. Write to Redis (real-time data) ──
# Time drift detection: compare agent_time with central server time
@@ -221,14 +229,8 @@ async def receive_heartbeat(
logger.error(f"Redis heartbeat write failed for server {server_id}: {e}")
# Redis write failed — still continue with MySQL update
# ── 1b. Look up server name for alert broadcasts ──
server_name = f"server-{server_id}"
try:
server = await service.get_server(server_id)
if server:
server_name = server.name
except Exception:
logger.debug(f"Failed to look up server name for server {server_id}")
# ── 1b. Server name for alert broadcasts ──
server_name = server.name or f"server-{server_id}"
# ── 1c. Time drift Telegram alert (critical only, 5-min cooldown like metric alerts) ──
if drift_level == "crit":
@@ -284,8 +286,7 @@ async def receive_heartbeat(
except Exception as e:
logger.error(f"Recovery detection failed for server {server_id}: {e}")
# ── 4. Also update MySQL (persistent record for non-real-time queries) ──
await service.update_heartbeat(server_id, payload.model_dump())
# MySQL history is flushed every 10min by heartbeat_flush (Redis → MySQL).
return {"status": "ok", "server_id": server_id}
@@ -293,12 +294,16 @@ async def receive_heartbeat(
async def script_job_callback(
payload: AgentScriptCallback,
request: Request,
api_key: str = Depends(_verify_api_key),
service: ServerService = Depends(get_server_service),
db: AsyncSession = Depends(get_db),
):
"""Receive long-task completion from child host (curl after nohup script exits).
Authenticated by one-time job_id + secret stored in Redis at exec time.
Authenticated by X-API-Key (per-server or global) + one-time job_id + secret.
"""
if not await _verify_server_api_key(payload.server_id, api_key, service):
raise HTTPException(status_code=401, detail="Invalid API key for this server")
from server.application.services.script_job_callback import apply_script_job_callback
from server.api.websocket import broadcast_system_event
from server.infrastructure.database.script_repo import ScriptExecutionRepositoryImpl