vs_cloud_api.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. # toolkit/vs_cloud_api.py
  2. import requests
  3. import json
  4. import time
  5. import urllib.parse
  6. from datetime import datetime
  7. from typing import Dict, Any, List, Optional
  8. from vs_types import NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
  9. from vs_log_macros import VSC_ERROR, VSC_INFO, VSC_WARN, VSC_DEBUG
  10. class VSCloudApi:
  11. """
  12. @brief VSCloudApi 的 Python 实现
  13. 用于对接云端服务 (打码、邮件、Session存储、任务调度)
  14. """
  15. _instance = None
  16. def __new__(cls, *args, **kwargs):
  17. if cls._instance is None:
  18. cls._instance = super(VSCloudApi, cls).__new__(cls)
  19. # 初始化默认配置
  20. cls._instance.base_url = "https://visafly.top"
  21. cls._instance.api_token = "Bearer tok_e946329a60ff45ba807f3f41b0e8b7fc"
  22. cls._instance.session = requests.Session()
  23. return cls._instance
  24. @staticmethod
  25. def Instance():
  26. return VSCloudApi()
  27. def _get_headers(self, content_type: str = "application/json") -> Dict[str, str]:
  28. return {
  29. "Authorization": self.api_token,
  30. "Content-Type": content_type,
  31. "Accept": "application/json, text/plain, */*"
  32. }
  33. def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
  34. """
  35. 统一 HTTP 请求封装,严格复刻 C++ 逻辑:
  36. 1. 发送 OPTIONS 请求
  37. 2. 发送实际请求
  38. """
  39. resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params)
  40. VSC_DEBUG('vs_cloud', f'[perform request] {method} {url} {data} {json_data} {params} {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 get_vas_task(self, task_id: str) -> dict:
  71. # 例如:请求 GET /api/v1/tasks/{task_id}
  72. # curl -X 'GET' \
  73. # 'http://45.137.220.138:8888/api/vas/task/get_by_order?order_id=ORD-20260306212306-7e604df8' \
  74. # -H 'accept: application/json' \
  75. # -H 'Authorization: Bearer tok_c9be86aa78274939a3c008db31ce9d22'
  76. url = f"{self.base_url}/api/vas/task/detail"
  77. params = {"task_id": task_id}
  78. headers = self._get_headers()
  79. resp = self._perform_request('GET', url, params=params, headers=headers)
  80. result = resp.json()
  81. if result.get("code") == 0:
  82. return result.get("data", {})
  83. else:
  84. raise BizLogicError(message=f"Get Task={task_id} error: {result.get('message')}")
  85. def update_vas_task(self,
  86. task_id: int,
  87. update_data: Dict[str, Any]) -> Optional[Dict]:
  88. """
  89. 更新任务
  90. API: POST /api/vas/task/update?id=1
  91. Body: update_data (json)
  92. """
  93. url = f"{self.base_url}/api/vas/task/update"
  94. params = {"id": task_id}
  95. headers = self._get_headers()
  96. resp = self._perform_request('POST', url, params=params, json_data=update_data, headers=headers)
  97. result = resp.json()
  98. if result.get("code") == 0:
  99. return result.get("data", {})
  100. else:
  101. raise BizLogicError(message=f"Update vas task biz error: {result.get('message')}")
  102. def return_vas_task_to_queue(self, task_id: int):
  103. """
  104. 重入队列
  105. API: POST /api/vas/task/return_to_queue?task_id=1
  106. """
  107. url = f"{self.base_url}/api/vas/task/return_to_queue"
  108. params = {"task_id": task_id}
  109. headers = self._get_headers()
  110. resp = self._perform_request('POST', url, params=params, headers=headers)
  111. result = resp.json()
  112. if result.get("code") == 0:
  113. return result.get("data", {})
  114. else:
  115. raise BizLogicError(message=f"Return vas task to queue biz error: {result.get('message')}")
  116. def push_weixin_text(self, text:str):
  117. """
  118. 推送微信文本消息
  119. API: POST https://visafly.top/api/wechat/send_no_token
  120. """
  121. url = f"{self.base_url}/api/wechat/send_no_token"
  122. payload = {"message": text}
  123. headers = self._get_headers()
  124. resp = self._perform_request('POST', url, json_data=payload, headers=headers)
  125. result = resp.json()
  126. if result.get("code") == 0:
  127. return result.get("data", {})
  128. else:
  129. raise BizLogicError(message=f"Return vas task to queue biz error: {result.get('message')}")
  130. def create_task(self, command: str, args: Dict) -> str:
  131. """
  132. [核心] 创建任务
  133. :param command: 任务指令 (e.g., "AntiCloudflareTurnstileTask", "AntiCloudflareTask")
  134. :param args: 任务参数字典 (e.g., {"proxy": "...", "websiteUrl": "..."})
  135. :return: task_id (直接返回任务ID,方便调用方)
  136. """
  137. url = f"{self.base_url}/api/tasks"
  138. headers = self._get_headers()
  139. payload = {
  140. "command": command,
  141. "args": args,
  142. "status": 0
  143. }
  144. # 发送请求
  145. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  146. result = resp.json()
  147. # 校验业务状态码
  148. if result.get("code") == 0:
  149. data = result.get("data", {})
  150. task_id = data.get("id")
  151. if not task_id:
  152. raise BizLogicError(message=f"Task created but no ID returned. Resp: {data}")
  153. return str(task_id)
  154. else:
  155. raise BizLogicError(message=f"Create task failed ({command}): {result.get('message')}")
  156. def get_task_result(self, task_id: str, timeout: int = 120, interval: int = 3) -> Dict:
  157. """
  158. [核心] 轮询获取任务结果
  159. :param task_id: 任务ID
  160. :param timeout: 最大等待时间(秒)
  161. :param interval: 轮询间隔(秒)
  162. :return: 任务成功后的 data 字典 (包含 token/cookies 等)
  163. """
  164. url = f"{self.base_url}/api/tasks/{task_id}"
  165. headers = self._get_headers()
  166. start_time = time.time()
  167. while True:
  168. # 1. 检查是否超时
  169. if time.time() - start_time > timeout:
  170. raise BizLogicError(message=f"Wait for task result timeout ({timeout}s). TaskID: {task_id}")
  171. try:
  172. # 2. 发起查询
  173. resp = self._perform_request('GET', url, headers=headers)
  174. result = resp.json()
  175. # 3. 校验 API 层面错误
  176. if result.get("code") != 0:
  177. raise BizLogicError(message=f"API Error fetching task: {result.get('message')}")
  178. data = result.get("data", {})
  179. status = data.get("status")
  180. # 4. 判断任务状态
  181. if status == 2: # 成功
  182. return data
  183. elif status == 3: # 失败
  184. error_msg = data.get("result", "Unknown error")
  185. raise BizLogicError(message=f"Task execution failed: {error_msg}")
  186. # status 为 0 (Pending) 或 1 (Running),继续等待
  187. except Exception as e:
  188. # 如果是 BizLogicError 直接抛出,不重试
  189. if isinstance(e, BizLogicError):
  190. raise e
  191. # 网络波动等其他异常,记录日志并重试
  192. VSC_WARN("vs_cloud", f"Polling exception: {str(e)}")
  193. # 等待下次轮询
  194. time.sleep(interval)
  195. def create_http_session(
  196. self,
  197. session_id: str,
  198. cookies: Optional[str] = None,
  199. local_storage: Optional[str] = None,
  200. user_agent: Optional[str] = None,
  201. proxy: Optional[str] = None,
  202. page: Optional[str] = None,
  203. session_storage: Optional[str] = None
  204. ) -> Optional[Dict]:
  205. """创建 http session"""
  206. url = f"{self.base_url}/api/http-session"
  207. headers = self._get_headers()
  208. payload = {
  209. "local_storage": local_storage,
  210. "session_storage": session_storage,
  211. "cookies": cookies,
  212. "user_agent": user_agent,
  213. "proxy": proxy,
  214. "page": page,
  215. "session_id": session_id
  216. }
  217. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  218. result = resp.json()
  219. if result.get("code") == 0:
  220. return result.get("data", {})
  221. else:
  222. raise BizLogicError(message=f"Create http session biz error: {result.get('message')}")
  223. def slot_refresh_start(
  224. self,
  225. routing_key: str,
  226. country: str = "",
  227. city: str = "",
  228. visa_type: str = "Tourist",
  229. snapshot_source: str = "worker",
  230. ):
  231. url = f"{self.base_url}/api/slot_refresh/start"
  232. payload = {
  233. "routing_key": routing_key,
  234. "country": country,
  235. "city": city,
  236. "visa_type": visa_type,
  237. "snapshot_source": snapshot_source,
  238. }
  239. headers = self._get_headers()
  240. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  241. result = resp.json()
  242. if result.get("code") == 0:
  243. return result.get("data", {})
  244. else:
  245. raise BizLogicError(message=f"Slot refresh start biz error: {result.get('message')}")
  246. def slot_refresh_success(
  247. self,
  248. routing_key: str,
  249. snapshot_source: str = "worker",
  250. ):
  251. url = f'{self.base_url}/api/slot_refresh/success'
  252. payload = {
  253. "routing_key": routing_key,
  254. "snapshot_source": snapshot_source,
  255. }
  256. headers = self._get_headers()
  257. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  258. result = resp.json()
  259. if result.get("code") == 0:
  260. return result.get("data", {})
  261. else:
  262. raise BizLogicError(message=f"Slot refresh success biz error: {result.get('message')}")
  263. def slot_refresh_fail(
  264. self,
  265. routing_key: str,
  266. error: str,
  267. snapshot_source: str = "worker",
  268. ):
  269. url = f'{self.base_url}/api/slot_refresh/fail'
  270. payload = {
  271. "routing_key": routing_key,
  272. "snapshot_source": snapshot_source,
  273. "error": error,
  274. }
  275. headers = self._get_headers()
  276. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  277. result = resp.json()
  278. if result.get("code") == 0:
  279. return result.get("data", {})
  280. else:
  281. raise BizLogicError(message=f"Slot refresh fail biz error: {result.get('message')}")
  282. def get_next_account(
  283. self,
  284. pool_name: str,
  285. account_cd: int = 60
  286. ):
  287. url = f'{self.base_url}/api/account/next'
  288. payload = {
  289. "pool_name": pool_name,
  290. "account_cd": account_cd
  291. }
  292. headers = self._get_headers()
  293. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  294. result = resp.json()
  295. if result.get("code") == 0:
  296. return result.get("data", {})
  297. else:
  298. raise BizLogicError(message=f"Get next account biz error: {result.get('message')}")
  299. def get_next_proxy(
  300. self,
  301. pools: List[str],
  302. proxy_cd: int = 60
  303. ):
  304. url = f'{self.base_url}/api/proxy/next-ip'
  305. payload = {
  306. "pools": pools,
  307. "proxy_cd": proxy_cd
  308. }
  309. headers = self._get_headers()
  310. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  311. result = resp.json()
  312. if result.get("code") == 0:
  313. return result.get("data", {})
  314. else:
  315. raise BizLogicError(message=f"Get next account biz error: {result.get('message')}")
  316. def slot_snapshot_report(
  317. self,
  318. query_payload: Dict[str, Any] = {}
  319. ):
  320. url = f"{self.base_url}/api/slots/report"
  321. headers = self._get_headers()
  322. resp = self._perform_request("POST", url, headers=headers, json_data=query_payload)
  323. result = resp.json()
  324. if result.get("code") == 0:
  325. return result.get("data", {})
  326. else:
  327. raise BizLogicError(message=f"Slot refresh fail biz error: {result.get('message')}")
  328. def fetch_mail_content(
  329. self,
  330. email: str,
  331. sender: str,
  332. recipient: str,
  333. subject_keywords: str,
  334. body_keywords: str,
  335. sent_date: str,
  336. expiry: int
  337. ) -> Optional[str]:
  338. """
  339. 获取邮件内容
  340. """
  341. params = {
  342. "email": email,
  343. "sender": sender,
  344. "recipient": recipient,
  345. "subjectKeywords": subject_keywords,
  346. "bodyKeywords": body_keywords,
  347. "sentDate": sent_date,
  348. "expiry": str(expiry)
  349. }
  350. url = f"{self.base_url}/api/email-authorizations/fetch"
  351. headers = self._get_headers()
  352. resp = self._perform_request('POST', url, headers=headers, params=params, data="")
  353. result = resp.json()
  354. if result.get('code') == 0:
  355. data = result.get('data', {})
  356. return data.get('body', '')
  357. else:
  358. raise BizLogicError(message=f"Fetch mail content biz error: {result.get('message')}")
  359. def fetch_mail_content_from_top(
  360. self,
  361. email: str,
  362. sender: str,
  363. recipient: str,
  364. subject_keywords: str,
  365. body_keywords: str,
  366. top: int
  367. ) -> Optional[str]:
  368. """从顶部获取邮件内容"""
  369. params = {
  370. "email": email,
  371. "sender": sender,
  372. "recipient": recipient,
  373. "subjectKeywords": subject_keywords,
  374. "bodyKeywords": body_keywords,
  375. "top": str(top)
  376. }
  377. url = f"{self.base_url}/api/email-authorizations/fetch-top"
  378. headers = self._get_headers()
  379. resp = self._perform_request('POST', url, headers=headers, params=params, data="")
  380. result = resp.json()
  381. if result.get('code') == 0:
  382. data = result.get('data', {})
  383. return data.get('body', "")
  384. else:
  385. raise BizLogicError(message=f"Fetch mail content from top biz error: {result.get('message')}")