vs_cloud_api.py 17 KB

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