vs_cloud_api.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  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. # =========================================================================
  53. # VAS Task Management (新增 API)
  54. # =========================================================================
  55. def get_vas_task_pop(self, routing_key: str) -> Optional[Dict]:
  56. """
  57. 获取任务信息
  58. API: GET /api/vas/task/pop
  59. """
  60. url = f"{self.base_url}/api/vas/task/pop"
  61. params = {
  62. "queue_name": routing_key,
  63. }
  64. headers = self._get_headers()
  65. resp = self._perform_request('GET', url, params=params, headers=headers)
  66. result = resp.json()
  67. if result.get("code") == 0:
  68. return result.get("data", {})
  69. else:
  70. raise BizLogicError(message=f"Get vas task pop biz error: {result.get('message')}")
  71. def get_vas_task(self, task_id: str) -> dict:
  72. # 例如:请求 GET /api/v1/tasks/{task_id}
  73. # curl -X 'GET' \
  74. # 'http://45.137.220.138:8888/api/vas/task/get_by_order?order_id=ORD-20260306212306-7e604df8' \
  75. # -H 'accept: application/json' \
  76. # -H 'Authorization: Bearer tok_c9be86aa78274939a3c008db31ce9d22'
  77. url = f"{self.base_url}/api/vas/task/detail"
  78. params = {"task_id": task_id}
  79. headers = self._get_headers()
  80. resp = self._perform_request('GET', url, params=params, headers=headers)
  81. result = resp.json()
  82. if result.get("code") == 0:
  83. return result.get("data", {})
  84. else:
  85. raise BizLogicError(message=f"Get Task={task_id} error: {result.get('message')}")
  86. def update_vas_task(self,
  87. task_id: int,
  88. update_data: Dict[str, Any]) -> Optional[Dict]:
  89. """
  90. 更新任务
  91. API: POST /api/vas/task/update?id=1
  92. Body: update_data (json)
  93. """
  94. url = f"{self.base_url}/api/vas/task/update"
  95. params = {"id": task_id}
  96. headers = self._get_headers()
  97. resp = self._perform_request('POST', url, params=params, json_data=update_data, headers=headers)
  98. result = resp.json()
  99. if result.get("code") == 0:
  100. return result.get("data", {})
  101. else:
  102. raise BizLogicError(message=f"Update vas task biz error: {result.get('message')}")
  103. def return_vas_task_to_queue(self, task_id: int):
  104. """
  105. 重入队列
  106. API: POST /api/vas/task/return_to_queue?task_id=1
  107. """
  108. url = f"{self.base_url}/api/vas/task/return_to_queue"
  109. params = {"task_id": task_id}
  110. headers = self._get_headers()
  111. resp = self._perform_request('POST', url, params=params, headers=headers)
  112. result = resp.json()
  113. if result.get("code") == 0:
  114. return result.get("data", {})
  115. else:
  116. raise BizLogicError(message=f"Return vas task to queue biz error: {result.get('message')}")
  117. def push_weixin_text(self, text:str):
  118. """
  119. 推送微信文本消息
  120. API: POST https://visafly.top/api/wechat/send_no_token
  121. """
  122. url = f"{self.base_url}/api/wechat/send_no_token"
  123. payload = {"message": text}
  124. headers = self._get_headers()
  125. resp = self._perform_request('POST', url, json_data=payload, headers=headers)
  126. result = resp.json()
  127. if result.get("code") == 0:
  128. return result.get("data", {})
  129. else:
  130. raise BizLogicError(message=f"Return vas task to queue biz error: {result.get('message')}")
  131. def create_task(self, command: str, args: Dict) -> str:
  132. """
  133. [核心] 创建任务
  134. :param command: 任务指令 (e.g., "AntiCloudflareTurnstileTask", "AntiCloudflareTask")
  135. :param args: 任务参数字典 (e.g., {"proxy": "...", "websiteUrl": "..."})
  136. :return: task_id (直接返回任务ID,方便调用方)
  137. """
  138. url = f"{self.base_url}/api/tasks"
  139. headers = self._get_headers()
  140. payload = {
  141. "command": command,
  142. "args": args,
  143. "status": 0
  144. }
  145. # 发送请求
  146. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  147. result = resp.json()
  148. # 校验业务状态码
  149. if result.get("code") == 0:
  150. data = result.get("data", {})
  151. task_id = data.get("id")
  152. if not task_id:
  153. raise BizLogicError(message=f"Task created but no ID returned. Resp: {data}")
  154. return str(task_id)
  155. else:
  156. raise BizLogicError(message=f"Create task failed ({command}): {result.get('message')}")
  157. def get_task_result(self, task_id: str, timeout: int = 120, interval: int = 3) -> Dict:
  158. """
  159. [核心] 轮询获取任务结果
  160. :param task_id: 任务ID
  161. :param timeout: 最大等待时间(秒)
  162. :param interval: 轮询间隔(秒)
  163. :return: 任务成功后的 data 字典 (包含 token/cookies 等)
  164. """
  165. url = f"{self.base_url}/api/tasks/{task_id}"
  166. headers = self._get_headers()
  167. start_time = time.time()
  168. while True:
  169. # 1. 检查是否超时
  170. if time.time() - start_time > timeout:
  171. raise BizLogicError(message=f"Wait for task result timeout ({timeout}s). TaskID: {task_id}")
  172. try:
  173. # 2. 发起查询
  174. resp = self._perform_request('GET', url, headers=headers)
  175. result = resp.json()
  176. # 3. 校验 API 层面错误
  177. if result.get("code") != 0:
  178. raise BizLogicError(message=f"API Error fetching task: {result.get('message')}")
  179. data = result.get("data", {})
  180. status = data.get("status")
  181. # 4. 判断任务状态
  182. if status == 2: # 成功
  183. return data
  184. elif status == 3: # 失败
  185. error_msg = data.get("result", "Unknown error")
  186. raise BizLogicError(message=f"Task execution failed: {error_msg}")
  187. # status 为 0 (Pending) 或 1 (Running),继续等待
  188. except Exception as e:
  189. # 如果是 BizLogicError 直接抛出,不重试
  190. if isinstance(e, BizLogicError):
  191. raise e
  192. # 网络波动等其他异常,记录日志并重试
  193. VSC_WARN("vs_cloud", f"Polling exception: {str(e)}")
  194. # 等待下次轮询
  195. time.sleep(interval)
  196. def create_http_session(
  197. self,
  198. session_id: str,
  199. cookies: Optional[str] = None,
  200. local_storage: Optional[str] = None,
  201. user_agent: Optional[str] = None,
  202. proxy: Optional[str] = None,
  203. page: Optional[str] = None,
  204. session_storage: Optional[str] = None
  205. ) -> Optional[Dict]:
  206. """创建 http session"""
  207. url = f"{self.base_url}/api/http-session"
  208. headers = self._get_headers()
  209. payload = {
  210. "local_storage": local_storage,
  211. "session_storage": session_storage,
  212. "cookies": cookies,
  213. "user_agent": user_agent,
  214. "proxy": proxy,
  215. "page": page,
  216. "session_id": session_id
  217. }
  218. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  219. result = resp.json()
  220. if result.get("code") == 0:
  221. return result.get("data", {})
  222. else:
  223. raise BizLogicError(message=f"Create http session biz error: {result.get('message')}")
  224. def slot_refresh_start(
  225. self,
  226. routing_key: str,
  227. country: str = "",
  228. city: str = "",
  229. visa_type: str = "Tourist",
  230. snapshot_source: str = "worker",
  231. ):
  232. url = f"{self.base_url}/api/slot_refresh/start"
  233. payload = {
  234. "routing_key": routing_key,
  235. "country": country,
  236. "city": city,
  237. "visa_type": visa_type,
  238. "snapshot_source": snapshot_source,
  239. }
  240. headers = self._get_headers()
  241. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  242. result = resp.json()
  243. if result.get("code") == 0:
  244. return result.get("data", {})
  245. else:
  246. raise BizLogicError(message=f"Slot refresh start biz error: {result.get('message')}")
  247. def slot_refresh_success(
  248. self,
  249. routing_key: str,
  250. snapshot_source: str = "worker",
  251. ):
  252. url = f'{self.base_url}/api/slot_refresh/success'
  253. payload = {
  254. "routing_key": routing_key,
  255. "snapshot_source": snapshot_source,
  256. }
  257. headers = self._get_headers()
  258. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  259. result = resp.json()
  260. if result.get("code") == 0:
  261. return result.get("data", {})
  262. else:
  263. raise BizLogicError(message=f"Slot refresh success biz error: {result.get('message')}")
  264. def slot_refresh_fail(
  265. self,
  266. routing_key: str,
  267. error: str,
  268. snapshot_source: str = "worker",
  269. ):
  270. url = f'{self.base_url}/api/slot_refresh/fail'
  271. payload = {
  272. "routing_key": routing_key,
  273. "snapshot_source": snapshot_source,
  274. "error": error,
  275. }
  276. headers = self._get_headers()
  277. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  278. result = resp.json()
  279. if result.get("code") == 0:
  280. return result.get("data", {})
  281. else:
  282. raise BizLogicError(message=f"Slot refresh fail biz error: {result.get('message')}")
  283. def get_next_account(
  284. self,
  285. pool_name: str,
  286. account_cd: int = 60
  287. ):
  288. url = f'{self.base_url}/api/account/next'
  289. payload = {
  290. "pool_name": pool_name,
  291. "account_cd": account_cd
  292. }
  293. headers = self._get_headers()
  294. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  295. result = resp.json()
  296. if result.get("code") == 0:
  297. return result.get("data", {})
  298. else:
  299. raise BizLogicError(message=f"Get next account biz error: {result.get('message')}")
  300. # def get_next_proxy(
  301. # self,
  302. # pools: List[str],
  303. # proxy_cd: int = 60
  304. # ):
  305. # local = {
  306. # "pool_name": "local",
  307. # "proto": "http",
  308. # "ip": "127.0.0.1",
  309. # "port": 7890,
  310. # "username": "",
  311. # "password": "",
  312. # "time_zone": "Europe/Dublin",
  313. # "id": 213
  314. # }
  315. # node = switch_next_node()
  316. # VSC_INFO('-', f'proxy node={node}')
  317. # return local
  318. def get_next_proxy(
  319. self,
  320. pools: List[str],
  321. proxy_cd: int = 60
  322. ):
  323. url = f'{self.base_url}/api/proxy/next-ip'
  324. payload = {
  325. "pools": pools,
  326. "proxy_cd": proxy_cd
  327. }
  328. headers = self._get_headers()
  329. resp = self._perform_request('POST', url, headers=headers, json_data=payload)
  330. result = resp.json()
  331. if result.get("code") == 0:
  332. return result.get("data", {})
  333. else:
  334. raise BizLogicError(message=f"Get next account biz error: {result.get('message')}")
  335. def slot_snapshot_report(
  336. self,
  337. query_payload: Dict[str, Any] = {}
  338. ):
  339. url = f"{self.base_url}/api/slots/report"
  340. headers = self._get_headers()
  341. resp = self._perform_request("POST", url, headers=headers, json_data=query_payload)
  342. result = resp.json()
  343. if result.get("code") == 0:
  344. return result.get("data", {})
  345. else:
  346. raise BizLogicError(message=f"Slot refresh fail biz error: {result.get('message')}")
  347. def fetch_mail_content(
  348. self,
  349. email: str,
  350. sender: str,
  351. recipient: str,
  352. subject_keywords: str,
  353. body_keywords: str,
  354. sent_date: str,
  355. expiry: int
  356. ) -> Optional[str]:
  357. """
  358. 获取邮件内容
  359. """
  360. params = {
  361. "email": email,
  362. "sender": sender,
  363. "recipient": recipient,
  364. "subjectKeywords": subject_keywords,
  365. "bodyKeywords": body_keywords,
  366. "sentDate": sent_date,
  367. "expiry": str(expiry)
  368. }
  369. url = f"{self.base_url}/api/email-authorizations/fetch"
  370. headers = self._get_headers()
  371. resp = self._perform_request('POST', url, headers=headers, params=params, data="")
  372. result = resp.json()
  373. if result.get('code') == 0:
  374. data = result.get('data', {})
  375. return data.get('body', '')
  376. else:
  377. raise BizLogicError(message=f"Fetch mail content biz error: {result.get('message')}")
  378. def fetch_mail_content_from_top(
  379. self,
  380. email: str,
  381. sender: str,
  382. recipient: str,
  383. subject_keywords: str,
  384. body_keywords: str,
  385. top: int
  386. ) -> Optional[str]:
  387. """从顶部获取邮件内容"""
  388. params = {
  389. "email": email,
  390. "sender": sender,
  391. "recipient": recipient,
  392. "subjectKeywords": subject_keywords,
  393. "bodyKeywords": body_keywords,
  394. "top": str(top)
  395. }
  396. url = f"{self.base_url}/api/email-authorizations/fetch-top"
  397. headers = self._get_headers()
  398. resp = self._perform_request('POST', url, headers=headers, params=params, data="")
  399. result = resp.json()
  400. if result.get('code') == 0:
  401. data = result.get('data', {})
  402. return data.get('body', "")
  403. else:
  404. raise BizLogicError(message=f"Fetch mail content from top biz error: {result.get('message')}")