scheduler.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. from __future__ import annotations
  2. import asyncio
  3. import logging
  4. import os
  5. from datetime import datetime, timedelta, timezone
  6. from apscheduler.schedulers.asyncio import AsyncIOScheduler
  7. from apscheduler.triggers.interval import IntervalTrigger
  8. from app.db import get_pool
  9. from app.services.scraper import ScraperService
  10. from app.services.key_pool_pusher import push_all_groups_to_pool
  11. logger = logging.getLogger(__name__)
  12. _scheduler = AsyncIOScheduler()
  13. _scraper = ScraperService()
  14. JOB_ID = "auto_scrape"
  15. JOB_ID_POOL_PUSH = "pool_push"
  16. async def _run_auto_scrape() -> None:
  17. """爬取所有已注册模型。"""
  18. pool = get_pool()
  19. async with pool.acquire() as conn:
  20. cfg = await conn.fetchrow("SELECT * FROM scrape_schedule WHERE id = 1")
  21. if not cfg or not cfg["enabled"]:
  22. return
  23. async with pool.acquire() as conn:
  24. rows = await conn.fetch("SELECT url FROM models ORDER BY created_at")
  25. urls = [r["url"] for r in rows]
  26. if not urls:
  27. logger.info("[scheduler] 没有已注册的模型,跳过")
  28. return
  29. logger.info(f"[scheduler] 开始自动爬取 {len(urls)} 个模型")
  30. job_id_row = await pool.fetchrow(
  31. "INSERT INTO scrape_jobs (urls, status) VALUES ($1, 'pending') RETURNING id",
  32. urls,
  33. )
  34. asyncio.create_task(_scraper.run_job(str(job_id_row["id"]), urls, pool))
  35. def _reschedule(interval_days: int, start_hour: int) -> None:
  36. if _scheduler.get_job(JOB_ID):
  37. _scheduler.remove_job(JOB_ID)
  38. # start_hour 是用户输入的本地时间(UTC+8),转换为 UTC
  39. tz_offset = int(os.environ.get("TZ_OFFSET_HOURS", "8"))
  40. start_hour_utc = (start_hour - tz_offset) % 24
  41. now = datetime.now(timezone.utc)
  42. next_run = now.replace(hour=start_hour_utc, minute=0, second=0, microsecond=0)
  43. if next_run <= now:
  44. next_run += timedelta(days=1)
  45. _scheduler.add_job(
  46. _run_auto_scrape,
  47. trigger=IntervalTrigger(days=interval_days, start_date=next_run),
  48. id=JOB_ID,
  49. replace_existing=True,
  50. )
  51. logger.info(f"[scheduler] 已设置每 {interval_days} 天本地 {start_hour}:00 (UTC {start_hour_utc}:00) 执行,下次: {next_run}")
  52. async def start_scheduler() -> None:
  53. pool = get_pool()
  54. async with pool.acquire() as conn:
  55. cfg = await conn.fetchrow("SELECT * FROM scrape_schedule WHERE id = 1")
  56. interval = cfg["interval_days"] if cfg else 1
  57. start_hour = cfg["start_hour"] if cfg else 2
  58. _reschedule(interval, start_hour)
  59. # 添加号池推送定时任务(每5分钟推送一次)
  60. _scheduler.add_job(
  61. _run_pool_push,
  62. trigger=IntervalTrigger(minutes=5),
  63. id=JOB_ID_POOL_PUSH,
  64. replace_existing=True,
  65. )
  66. _scheduler.start()
  67. logger.info("[scheduler] 调度器已启动(包含号池推送,每5分钟)")
  68. async def _run_pool_push() -> None:
  69. """定时推送号池数据"""
  70. pool = get_pool()
  71. logger.info("[scheduler] 开始推送号池数据到company_base_pool")
  72. await push_all_groups_to_pool(pool)
  73. async def stop_scheduler() -> None:
  74. if _scheduler.running:
  75. _scheduler.shutdown(wait=False)
  76. def get_scheduler() -> AsyncIOScheduler:
  77. return _scheduler
  78. def reschedule(interval_days: int, start_hour: int) -> None:
  79. _reschedule(interval_days, start_hour)