vs_cloud_api.py 15 KB

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