vs_cloud_api.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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 = "http://45.137.220.138:8888"
  20. cls._instance.api_token = "Bearer tok_8cb26cf337cb405784eb346dfafb7f54"
  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. print(f'[perform request] {method} {url} {data} {json_data} {params}')
  39. resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params)
  40. VSC_INFO('vs_cloud', resp.text)
  41. if resp.status_code == 200:
  42. return resp
  43. elif resp.status_code == 401:
  44. raise SessionExpiredOrInvalidError()
  45. elif resp.status_code == 403:
  46. raise PermissionDeniedError()
  47. elif resp.status_code == 429:
  48. raise RateLimiteddError()
  49. else:
  50. raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
  51. # =========================================================================
  52. # VAS Task Management (新增 API)
  53. # =========================================================================
  54. def get_vas_task_pop(self, routing_key: str) -> Optional[Dict]:
  55. """
  56. 获取任务信息
  57. API: GET /api/vas/task/pop
  58. """
  59. url = f"{self.base_url}/api/vas/task/pop"
  60. params = {
  61. "queue_name": routing_key,
  62. }
  63. headers = self._get_headers()
  64. resp = self._perform_request('GET', url, params=params, headers=headers)
  65. result = resp.json()
  66. if result.get("code") == 0:
  67. return result.get("data", {})
  68. else:
  69. raise BizLogicError(message=f"Get vas task pop biz error: {result.get('message')}")
  70. def update_vas_task(self,
  71. task_id: int,
  72. update_data: Dict[str, Any]) -> Optional[Dict]:
  73. """
  74. 更新任务
  75. API: POST /api/vas/task/update?id=1
  76. Body: update_data (json)
  77. """
  78. url = f"{self.base_url}/api/vas/task/update"
  79. params = {"id": task_id}
  80. headers = self._get_headers()
  81. resp = self._perform_request('POST', url, params=params, json_data=update_data, headers=headers)
  82. result = resp.json()
  83. if result.get("code") == 0:
  84. return result.get("data", {})
  85. else:
  86. raise BizLogicError(message=f"Update vas task biz error: {result.get('message')}")
  87. def return_vas_task_to_queue(self, task_id: int):
  88. """
  89. 重入队列
  90. API: POST /api/vas/task/return_to_queue?task_id=1
  91. """
  92. url = f"{self.base_url}/api/vas/task/return_to_queue"
  93. params = {"task_id": task_id}
  94. headers = self._get_headers()
  95. resp = self._perform_request('POST', url, params=params, headers=headers)
  96. result = resp.json()
  97. if result.get("code") == 0:
  98. return result.get("data", {})
  99. else:
  100. raise BizLogicError(message=f"Return vas task to queue biz error: {result.get('message')}")
  101. def submit_anti_turnstile_task(self, proxy: str, website_url: str) -> Optional[Dict]:
  102. """
  103. 提交反 Turnstile 任务
  104. """
  105. url = f"{self.base_url}/api/tasks"
  106. headers = self._get_headers()
  107. args = {
  108. "proxy": proxy,
  109. "websiteUrl": website_url
  110. }
  111. payload = {
  112. "command": "AntiCloudflareTurnstileTask",
  113. "args": json.dumps(args),
  114. "status": 0
  115. }
  116. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  117. result = resp.json()
  118. if result.get("code") == 0:
  119. return result.get("data", {})
  120. else:
  121. raise BizLogicError(message=f"Submit anti turnstile task biz error: {result.get('message')}")
  122. def get_anti_turnstile_result(self, task_id: str) -> Optional[Dict]:
  123. """获取反 Turnstile 结果"""
  124. url = f"{self.base_url}/api/tasks/{task_id}"
  125. headers = self._get_headers()
  126. resp = self._perform_request('GET', url, headers=headers)
  127. result = resp.json()
  128. if result.get("code") == 0:
  129. return result.get("data", {})
  130. else:
  131. raise BizLogicError(message=f"Get anti turnstile result biz error: {result.get('message')}")
  132. def submit_anticloudflare_task(self, proxy: str, website_url: str) -> Optional[Dict]:
  133. """
  134. 提交 AntiCloudflareTask (用于 TLSContact 5s 盾)
  135. """
  136. url = f"{self.base_url}/api/tasks"
  137. args = {
  138. 'proxy': proxy,
  139. 'websiteUrl': website_url
  140. }
  141. data = {
  142. "command": "AntiCloudflareTask",
  143. "args": json.dumps(args),
  144. "status": 0
  145. }
  146. headers = self._get_headers()
  147. resp = self._perform_request('POST', url, headers=headers, json_data=data)
  148. result = resp.json()
  149. if result.get("code") == 0:
  150. return result.get("data", {})
  151. else:
  152. raise BizLogicError(message=f"Submit anticloudflare task biz error: {result.get('message')}")
  153. def get_anticloudflare_result(self, task_id, retry_interval=5, max_retries=20) -> Optional[Dict]:
  154. """
  155. 获取 AntiCloudflareTask 结果 (带轮询)
  156. """
  157. url = f"{self.base_url}/api/tasks/{task_id}"
  158. headers = self._get_headers()
  159. for attempt in range(1, max_retries + 1):
  160. try:
  161. resp = self._perform_request('GET', url, headers=headers)
  162. result = resp.json()
  163. if result.get("code") == 0:
  164. data = result.get("data", {})
  165. # status 2 表示成功
  166. if data.get("status") == 2:
  167. return data
  168. elif data.get("status") == 3:
  169. VSC_ERROR("vs_cloud", f"AntiCloudflareTask failed: {data.get('result')}")
  170. return None
  171. else:
  172. time.sleep(retry_interval)
  173. except Exception as e:
  174. VSC_WARN(
  175. "vs_cloud",
  176. "Get anticloudflare result exception, attempt %d/%d",
  177. attempt, max_retries
  178. )
  179. if attempt >= max_retries:
  180. break
  181. time.sleep(retry_interval)
  182. continue
  183. return None
  184. def create_http_session(
  185. self,
  186. session_id: str,
  187. cookies: str,
  188. local_storage: str,
  189. user_agent: str,
  190. proxy: str,
  191. page: str
  192. ) -> Optional[Dict]:
  193. """创建 http session"""
  194. url = f"{self.base_url}/api/http-session"
  195. headers = self._get_headers()
  196. payload = {
  197. "local_storage": local_storage,
  198. "cookies": cookies,
  199. "user_agent": user_agent,
  200. "proxy": proxy,
  201. "page": page,
  202. "session_id": session_id
  203. }
  204. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  205. result = resp.json()
  206. if result.get("code") == 0:
  207. return result.get("data", {})
  208. else:
  209. raise BizLogicError(message=f"Create http session biz error: {result.get('message')}")
  210. def fetch_mail_content(
  211. self,
  212. email: str,
  213. sender: str,
  214. recipient: str,
  215. subject_keywords: str,
  216. body_keywords: str,
  217. sent_date: str,
  218. expiry: int
  219. ) -> Optional[str]:
  220. """
  221. 获取邮件内容
  222. """
  223. params = {
  224. "email": email,
  225. "sender": sender,
  226. "recipient": recipient,
  227. "subjectKeywords": subject_keywords,
  228. "bodyKeywords": body_keywords,
  229. "sentDate": sent_date,
  230. "expiry": str(expiry)
  231. }
  232. url = f"{self.base_url}/api/email-authorizations/fetch"
  233. headers = self._get_headers()
  234. resp = self._perform_request('POST', url, headers=headers, params=params, data="")
  235. result = resp.json()
  236. if result.get('code') == 0:
  237. data = result.get('data', {})
  238. return data.get('body', '')
  239. else:
  240. raise BizLogicError(message=f"Fetch mail content biz error: {result.get('message')}")
  241. def fetch_mail_content_from_top(
  242. self,
  243. email: str,
  244. sender: str,
  245. recipient: str,
  246. subject_keywords: str,
  247. body_keywords: str,
  248. top: int
  249. ) -> Optional[str]:
  250. """从顶部获取邮件内容"""
  251. params = {
  252. "email": email,
  253. "sender": sender,
  254. "recipient": recipient,
  255. "subjectKeywords": subject_keywords,
  256. "bodyKeywords": body_keywords,
  257. "top": str(top)
  258. }
  259. url = f"{self.base_url}/api/email-authorizations/fetch-top"
  260. headers = self._get_headers()
  261. resp = self._perform_request('POST', url, headers=headers, params=params, data="")
  262. result = resp.json()
  263. if result.get('code') == 0:
  264. data = result.get('data', {})
  265. return data.get('body', "")
  266. else:
  267. raise BizLogicError(message=f"Fetch mail content from top biz error: {result.get('message')}")