vs_cloud_api.py 15 KB

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