booker_standalone.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import os
  2. import time
  3. import random
  4. from datetime import datetime
  5. from typing import List, Dict, Callable
  6. from vs_types import BookerStandaloneConfig, QueryWaitMode, VSPlgConfig, VSQueryResult, VSBookResult
  7. from vs_plg_factory import VSPlgFactory
  8. from utils.safe_redis_cli import SafeRedisClient
  9. from toolkit.vs_cloud_api import VSCloudApi
  10. from toolkit.thread_pool import ThreadPool
  11. class BookerStandalone:
  12. def __init__(self, config: BookerStandaloneConfig, redis_conf: Dict, logger: Callable[[str], None] = None):
  13. self.m_logger = logger
  14. self.m_factory = VSPlgFactory()
  15. self.m_cfg = config
  16. self.redis_client = SafeRedisClient(redis_conf, self.m_logger)
  17. self.m_instance = None
  18. self.m_running = False
  19. self.m_next_query_time = 0.0
  20. self.m_last_login_time = 0.0
  21. self.m_current_dates = {}
  22. def _log(self, message):
  23. if self.m_logger:
  24. self.m_logger(f'[BOOKER] {message}')
  25. else:
  26. print(f'[BOOKER] {message}')
  27. def _get_wait_interval(self) -> float:
  28. """根据配置计算下一次查询的等待时间"""
  29. if self.m_cfg.query_wait.mode == QueryWaitMode.Loop:
  30. return 1.0
  31. elif self.m_cfg.query_wait.mode == QueryWaitMode.Fixed:
  32. return float(self.m_cfg.query_wait.fixed_wait)
  33. elif self.m_cfg.query_wait.mode == QueryWaitMode.Random:
  34. return random.uniform(self.m_cfg.query_wait.random_min, self.m_cfg.query_wait.random_max)
  35. return 30.0
  36. def _is_within_active_hours(self) -> bool:
  37. """
  38. 判断当前是否在允许创建实例的时间段内
  39. """
  40. start_str = self.m_cfg.active_time_start
  41. end_str = self.m_cfg.active_time_end
  42. current_bj_time = datetime.utcnow().time()
  43. start_time = datetime.strptime(start_str, "%H:%M").time()
  44. end_time = datetime.strptime(end_str, "%H:%M").time()
  45. return start_time <= current_bj_time <= end_time
  46. def update_config(self, new_cfg: BookerStandaloneConfig):
  47. """
  48. 动态更新配置
  49. """
  50. pass
  51. def _notify_date_changes(self, query_result: VSQueryResult):
  52. current_earliest_date = query_result.earliest_date
  53. apt_type = query_result.apt_type
  54. if current_earliest_date:
  55. last_date = self.m_current_dates.get(apt_type.routing_key)
  56. if last_date != current_earliest_date:
  57. self.m_current_dates[apt_type.routing_key] = current_earliest_date
  58. def _push_to_wx():
  59. try:
  60. push_content = (
  61. f"📢【档期变化通知】\n"
  62. f"最早日期: {query_result.earliest_date}\n"
  63. f"目标国家: {apt_type.country}\n"
  64. f"递交城市: {apt_type.city}\n"
  65. f"签证类型: {apt_type.visa_type}\n"
  66. f"Routing: {apt_type.routing_key}"
  67. )
  68. VSCloudApi.Instance().push_weixin_text(push_content)
  69. except Exception as e:
  70. self._log(f"Failed to notify to cloud: {e}")
  71. ThreadPool.getInstance().enqueue(_push_to_wx)
  72. def _notify_book_result(self, book_result: VSBookResult):
  73. if book_result.success:
  74. def _update_cloud_success():
  75. try:
  76. push_content = (
  77. f"🎉 【预定成功通知】\n"
  78. f"━━━━━━━━━━━━━━━\n"
  79. f"预约账号: {book_result.account}\n"
  80. f"预约日期: {book_result.book_date}\n"
  81. f"预约时间: {book_result.book_time}\n"
  82. f"预约编号: {book_result.urn}\n"
  83. f"支付链接: {book_result.payment_link if book_result.payment_link else '无需支付/暂无'}\n"
  84. f"━━━━━━━━━━━━━━━\n"
  85. )
  86. VSCloudApi.Instance().push_weixin_text(push_content)
  87. except Exception as e:
  88. self._log(f"Failed to update success state to cloud: {e}")
  89. ThreadPool.getInstance().enqueue(_update_cloud_success)
  90. def _countdown_wait(self, seconds: float, wait_for='') -> bool:
  91. """倒计时等待,动态刷新同一行,返回 False 表示被 stop 中断"""
  92. end = time.time() + seconds
  93. last_remaining = None
  94. while self.m_running and time.time() < end:
  95. remaining = int(end - time.time())
  96. if remaining != last_remaining:
  97. print(f"[BOOKER] Next {wait_for} in {remaining} seconds... (stop to exit)", end='\r', flush=True)
  98. last_remaining = remaining
  99. time.sleep(1)
  100. print()
  101. return self.m_running
  102. def _init_instance(self):
  103. """初始化单实例并进行登录创建会话"""
  104. if self.m_instance is not None:
  105. self.m_instance.cleanup()
  106. now = time.time()
  107. if now < self.m_last_login_time + self.m_cfg.login_interval:
  108. wait = self.m_last_login_time + self.m_cfg.login_interval - now
  109. self._countdown_wait(wait, wait_for='init instance')
  110. plugin_name = self.m_cfg.plugin_config.plugin_name
  111. class_name = "".join(part.title() for part in plugin_name.split('_'))
  112. plugin_path = os.path.join(self.m_cfg.plugin_config.lib_path, self.m_cfg.plugin_config.plugin_bin)
  113. self.m_factory.register_plugin(plugin_name, plugin_path, class_name)
  114. plg_cfg = VSPlgConfig()
  115. plg_cfg.debug = self.m_cfg.debug
  116. plg_cfg.account = self.m_cfg.account
  117. proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, proxy_cd=600)
  118. plg_cfg.proxy = type(plg_cfg.proxy)(**proxy)
  119. plg_cfg.free_config = self.m_cfg.free_config
  120. plg_cfg.session_max_life = self.m_cfg.session_max_life
  121. self.m_instance = self.m_factory.create("single_task", plugin_name)
  122. self.m_instance.set_log(self.m_logger)
  123. self.m_instance.set_config(plg_cfg)
  124. self.m_instance.create_session()
  125. self.m_last_login_time = time.time()
  126. self._log("Session created successfully.")
  127. def start(self):
  128. """
  129. 单线程无限循环:心跳保活 -> 时间判定 -> 查询 -> 预定
  130. """
  131. self.m_running = True
  132. self._log("Auto Booker Started.")
  133. while self.m_running:
  134. if not self._is_within_active_hours():
  135. continue
  136. try:
  137. self._init_instance()
  138. break
  139. except Exception as e:
  140. self._log(f"Failed to create session: {e}. Retrying in 10s...")
  141. time.sleep(10)
  142. while self.m_running:
  143. try:
  144. now = time.time()
  145. if not self._is_within_active_hours():
  146. continue
  147. if not self.m_instance.health_check():
  148. self._log("Health check failed. Session dead. Recreating session...")
  149. self._init_instance()
  150. continue
  151. if now < self.m_next_query_time:
  152. wait = self.m_next_query_time - now
  153. self._countdown_wait(wait, wait_for="query slot")
  154. apt_types = self.m_cfg.appointment_types
  155. weights = [float(t.weight) for t in apt_types]
  156. apt_type = random.choices(apt_types, weights=weights, k=1)[0]
  157. self._log(f"Querying slots for {apt_type.routing_key}...")
  158. query_result = self.m_instance.query(apt_type)
  159. query_result.apt_type = apt_type
  160. self.m_next_query_time = time.time() + self._get_wait_interval()
  161. self._notify_date_changes(query_result)
  162. if query_result.success:
  163. self._log("🔥 SLOT FOUND! Initiating AUTO-BOOKING...")
  164. # 5. 执行预定
  165. book_result = self.m_instance.book(query_result, self.m_cfg.user_preferences)
  166. self._notify_book_result(book_result)
  167. if book_result.success:
  168. self._log(f"🎉 BOOKING SUCCESSFUL for {apt_type.routing_key}!")
  169. break
  170. except Exception as e:
  171. self._log(f"Loop Exception: {e}")
  172. def stop(self):
  173. """外部中断时调用"""
  174. self.m_running = False