初始化 HyperNode-AI 控制面。

包含节点账本、每日清算,以及 Ubuntu 交互式部署脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-10 21:53:20 +08:00
commit 2bbdb00a4d
31 changed files with 2450 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
"""进程内定时任务:离线巡检、随机任务下发、每日清算。"""
from __future__ import annotations
import logging
from datetime import timedelta
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
from zoneinfo import ZoneInfo
from app.config import settings
from app.database import session_scope
from app.jobs.daily_yield import run_daily_yield_cron
from app.services.heartbeat import mark_stale_nodes
from app.services.locks import acquire_lock
from app.services.tasks import dispatch_random_tasks
logger = logging.getLogger(__name__)
UTC = ZoneInfo("UTC")
_scheduler: BackgroundScheduler | None = None
def start_scheduler() -> None:
global _scheduler
if settings.disable_scheduler:
logger.info("scheduler disabled")
return
if _scheduler is not None and _scheduler.running:
return
scheduler = BackgroundScheduler(timezone=UTC, job_defaults={"coalesce": True, "max_instances": 1})
scheduler.add_job(
_guard(_offline_job),
IntervalTrigger(minutes=settings.offline_check_minutes, timezone=UTC),
id="offline_check",
replace_existing=True,
misfire_grace_time=120,
)
scheduler.add_job(
_guard(_dispatch_job),
IntervalTrigger(minutes=settings.task_dispatch_minutes, timezone=UTC),
id="task_dispatch",
replace_existing=True,
misfire_grace_time=120,
)
yield_zone = ZoneInfo(settings.yield_timezone)
scheduler.add_job(
_guard(_yield_job),
CronTrigger(hour=settings.yield_hour, minute=settings.yield_minute, timezone=yield_zone),
id="daily_yield",
replace_existing=True,
misfire_grace_time=3600,
)
scheduler.start()
_scheduler = scheduler
logger.info(
"scheduler started; daily yield at %02d:%02d %s",
settings.yield_hour,
settings.yield_minute,
settings.yield_timezone,
)
def stop_scheduler() -> None:
global _scheduler
if _scheduler is not None and _scheduler.running:
_scheduler.shutdown(wait=False)
_scheduler = None
def _guard(func):
def wrapper() -> None:
try:
func()
except Exception:
logger.exception("scheduled job failed: %s", func.__name__)
return wrapper
def _offline_job() -> None:
ttl = timedelta(seconds=max(30, settings.offline_check_minutes * 60 - 30))
if not acquire_lock("offline_check", ttl):
return
with session_scope() as session:
changed = mark_stale_nodes(session)
if changed:
logger.info("marked offline: %s", ",".join(changed))
def _dispatch_job() -> None:
ttl = timedelta(seconds=max(30, settings.task_dispatch_minutes * 60 - 30))
if not acquire_lock("task_dispatch", ttl):
return
with session_scope() as session:
tasks = dispatch_random_tasks(session)
if tasks:
logger.info("dispatched %s tasks", len(tasks))
def _yield_job() -> None:
if not acquire_lock("daily_yield", timedelta(hours=2)):
logger.info("daily yield skipped, lock held")
return
run_daily_yield_cron()