vs_cloud_api.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. # toolkit/vs_cloud_api.py
  2. import requests
  3. import json
  4. import time
  5. import urllib.parse
  6. from typing import Dict, Any, Optional
  7. from vs_types import NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
  8. from vs_log_macros import VSC_ERROR, VSC_INFO, VSC_WARN, VSC_DEBUG
  9. class VSCloudApi:
  10. """
  11. @brief VSCloudApi 的 Python 实现
  12. 用于对接云端服务 (打码、邮件、Session存储、任务调度)
  13. """
  14. _instance = None
  15. def __new__(cls, *args, **kwargs):
  16. if cls._instance is None:
  17. cls._instance = super(VSCloudApi, cls).__new__(cls)
  18. # 初始化默认配置
  19. cls._instance.base_url = "https://visafly.top"
  20. cls._instance.api_token = "Bearer tok_e946329a60ff45ba807f3f41b0e8b7fc"
  21. cls._instance.session = requests.Session()
  22. return cls._instance
  23. @staticmethod
  24. def Instance():
  25. return VSCloudApi()
  26. def _get_headers(self, content_type: str = "application/json") -> Dict[str, str]:
  27. return {
  28. "Authorization": self.api_token,
  29. "Content-Type": content_type,
  30. "Accept": "application/json, text/plain, */*"
  31. }
  32. def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
  33. """
  34. 统一 HTTP 请求封装,严格复刻 C++ 逻辑:
  35. 1. 发送 OPTIONS 请求
  36. 2. 发送实际请求
  37. """
  38. resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params)
  39. VSC_INFO('vs_cloud', f'[perform request] {method} {url} {data} {json_data} {params} {resp.text}')
  40. if resp.status_code == 200:
  41. return resp
  42. elif resp.status_code == 401:
  43. raise SessionExpiredOrInvalidError()
  44. elif resp.status_code == 403:
  45. raise PermissionDeniedError()
  46. elif resp.status_code == 429:
  47. raise RateLimiteddError()
  48. else:
  49. raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
  50. # =========================================================================
  51. # VAS Task Management (新增 API)
  52. # =========================================================================
  53. def get_vas_task_pop(self, routing_key: str) -> Optional[Dict]:
  54. """
  55. 获取任务信息
  56. API: GET /api/vas/task/pop
  57. """
  58. url = f"{self.base_url}/api/vas/task/pop"
  59. params = {
  60. "queue_name": routing_key,
  61. }
  62. headers = self._get_headers()
  63. resp = self._perform_request('GET', url, params=params, headers=headers)
  64. result = resp.json()
  65. if result.get("code") == 0:
  66. return result.get("data", {})
  67. else:
  68. raise BizLogicError(message=f"Get vas task pop biz error: {result.get('message')}")
  69. def update_vas_task(self,
  70. task_id: int,
  71. update_data: Dict[str, Any]) -> Optional[Dict]:
  72. """
  73. 更新任务
  74. API: POST /api/vas/task/update?id=1
  75. Body: update_data (json)
  76. """
  77. url = f"{self.base_url}/api/vas/task/update"
  78. params = {"id": task_id}
  79. headers = self._get_headers()
  80. resp = self._perform_request('POST', url, params=params, json_data=update_data, headers=headers)
  81. result = resp.json()
  82. if result.get("code") == 0:
  83. return result.get("data", {})
  84. else:
  85. raise BizLogicError(message=f"Update vas task biz error: {result.get('message')}")
  86. def return_vas_task_to_queue(self, task_id: int):
  87. """
  88. 重入队列
  89. API: POST /api/vas/task/return_to_queue?task_id=1
  90. """
  91. url = f"{self.base_url}/api/vas/task/return_to_queue"
  92. params = {"task_id": task_id}
  93. headers = self._get_headers()
  94. resp = self._perform_request('POST', url, params=params, headers=headers)
  95. result = resp.json()
  96. if result.get("code") == 0:
  97. return result.get("data", {})
  98. else:
  99. raise BizLogicError(message=f"Return vas task to queue biz error: {result.get('message')}")
  100. def create_task(self, command: str, args: Dict) -> str:
  101. """
  102. [核心] 创建任务
  103. :param command: 任务指令 (e.g., "AntiCloudflareTurnstileTask", "AntiCloudflareTask")
  104. :param args: 任务参数字典 (e.g., {"proxy": "...", "websiteUrl": "..."})
  105. :return: task_id (直接返回任务ID,方便调用方)
  106. """
  107. url = f"{self.base_url}/api/tasks"
  108. headers = self._get_headers()
  109. payload = {
  110. "command": command,
  111. "args": args,
  112. "status": 0
  113. }
  114. # 发送请求
  115. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  116. result = resp.json()
  117. # 校验业务状态码
  118. if result.get("code") == 0:
  119. data = result.get("data", {})
  120. task_id = data.get("id")
  121. if not task_id:
  122. raise BizLogicError(message=f"Task created but no ID returned. Resp: {data}")
  123. return str(task_id)
  124. else:
  125. raise BizLogicError(message=f"Create task failed ({command}): {result.get('message')}")
  126. def get_task_result(self, task_id: str, timeout: int = 120, interval: int = 3) -> Dict:
  127. """
  128. [核心] 轮询获取任务结果
  129. :param task_id: 任务ID
  130. :param timeout: 最大等待时间(秒)
  131. :param interval: 轮询间隔(秒)
  132. :return: 任务成功后的 data 字典 (包含 token/cookies 等)
  133. """
  134. url = f"{self.base_url}/api/tasks/{task_id}"
  135. headers = self._get_headers()
  136. start_time = time.time()
  137. while True:
  138. # 1. 检查是否超时
  139. if time.time() - start_time > timeout:
  140. raise BizLogicError(message=f"Wait for task result timeout ({timeout}s). TaskID: {task_id}")
  141. try:
  142. # 2. 发起查询
  143. resp = self._perform_request('GET', url, headers=headers)
  144. result = resp.json()
  145. # 3. 校验 API 层面错误
  146. if result.get("code") != 0:
  147. raise BizLogicError(message=f"API Error fetching task: {result.get('message')}")
  148. data = result.get("data", {})
  149. status = data.get("status")
  150. # 4. 判断任务状态
  151. if status == 2: # 成功
  152. return data
  153. elif status == 3: # 失败
  154. error_msg = data.get("result", "Unknown error")
  155. raise BizLogicError(message=f"Task execution failed: {error_msg}")
  156. # status 为 0 (Pending) 或 1 (Running),继续等待
  157. except Exception as e:
  158. # 如果是 BizLogicError 直接抛出,不重试
  159. if isinstance(e, BizLogicError):
  160. raise e
  161. # 网络波动等其他异常,记录日志并重试
  162. VSC_WARN("vs_cloud", f"Polling exception: {str(e)}")
  163. # 等待下次轮询
  164. time.sleep(interval)
  165. def create_http_session(
  166. self,
  167. session_id: str,
  168. cookies: str,
  169. local_storage: str,
  170. user_agent: str,
  171. proxy: str,
  172. page: str
  173. ) -> Optional[Dict]:
  174. """创建 http session"""
  175. url = f"{self.base_url}/api/http-session"
  176. headers = self._get_headers()
  177. payload = {
  178. "local_storage": local_storage,
  179. "cookies": cookies,
  180. "user_agent": user_agent,
  181. "proxy": proxy,
  182. "page": page,
  183. "session_id": session_id
  184. }
  185. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  186. result = resp.json()
  187. if result.get("code") == 0:
  188. return result.get("data", {})
  189. else:
  190. raise BizLogicError(message=f"Create http session biz error: {result.get('message')}")
  191. def fetch_mail_content(
  192. self,
  193. email: str,
  194. sender: str,
  195. recipient: str,
  196. subject_keywords: str,
  197. body_keywords: str,
  198. sent_date: str,
  199. expiry: int
  200. ) -> Optional[str]:
  201. """
  202. 获取邮件内容
  203. """
  204. params = {
  205. "email": email,
  206. "sender": sender,
  207. "recipient": recipient,
  208. "subjectKeywords": subject_keywords,
  209. "bodyKeywords": body_keywords,
  210. "sentDate": sent_date,
  211. "expiry": str(expiry)
  212. }
  213. url = f"{self.base_url}/api/email-authorizations/fetch"
  214. headers = self._get_headers()
  215. resp = self._perform_request('POST', url, headers=headers, params=params, data="")
  216. result = resp.json()
  217. if result.get('code') == 0:
  218. data = result.get('data', {})
  219. return data.get('body', '')
  220. else:
  221. raise BizLogicError(message=f"Fetch mail content biz error: {result.get('message')}")
  222. def fetch_mail_content_from_top(
  223. self,
  224. email: str,
  225. sender: str,
  226. recipient: str,
  227. subject_keywords: str,
  228. body_keywords: str,
  229. top: int
  230. ) -> Optional[str]:
  231. """从顶部获取邮件内容"""
  232. params = {
  233. "email": email,
  234. "sender": sender,
  235. "recipient": recipient,
  236. "subjectKeywords": subject_keywords,
  237. "bodyKeywords": body_keywords,
  238. "top": str(top)
  239. }
  240. url = f"{self.base_url}/api/email-authorizations/fetch-top"
  241. headers = self._get_headers()
  242. resp = self._perform_request('POST', url, headers=headers, params=params, data="")
  243. result = resp.json()
  244. if result.get('code') == 0:
  245. data = result.get('data', {})
  246. return data.get('body', "")
  247. else:
  248. raise BizLogicError(message=f"Fetch mail content from top biz error: {result.get('message')}")