|
|
@@ -8,17 +8,17 @@ from vs_log_macros import VSC_ERROR, VSC_INFO, VSC_DEBUG # type: ignore
|
|
|
|
|
|
class VSCloudApi:
|
|
|
"""
|
|
|
- @brief VSCloudApi 的 Python 实现 (1:1 对应 C++ 版本)
|
|
|
- 用于对接 http://45.137.220.138:8888 的云端服务
|
|
|
+ @brief VSCloudApi 的 Python 实现
|
|
|
+ 用于对接云端服务 (打码、邮件、Session存储、任务调度)
|
|
|
"""
|
|
|
_instance = None
|
|
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
|
if cls._instance is None:
|
|
|
cls._instance = super(VSCloudApi, cls).__new__(cls)
|
|
|
- # 初始化默认配置,对应 C++ 构造函数参数
|
|
|
- cls._instance.base_url = "http://45.137.220.138:8000"
|
|
|
- cls._instance.api_token = "7x9EjFpmv7GjZc6AfVeqxuUBANpqkpkHAtxJM7CAW5oZhs0nEyCJBy39N4XXs5hgfYWXw3jFrcgXqQ42HAx9Qvwtk9vC2GvKBbWz"
|
|
|
+ # 初始化默认配置
|
|
|
+ cls._instance.base_url = "http://45.137.220.138:8888"
|
|
|
+ cls._instance.api_token = "Bearer tok_8cb26cf337cb405784eb346dfafb7f54"
|
|
|
cls._instance.session = requests.Session()
|
|
|
return cls._instance
|
|
|
|
|
|
@@ -33,59 +33,68 @@ class VSCloudApi:
|
|
|
"Accept": "application/json, text/plain, */*"
|
|
|
}
|
|
|
|
|
|
- def _handle_request(self, method: str, endpoint: str, **kwargs) -> Optional[Any]:
|
|
|
- url = f"{self.base_url}{endpoint}"
|
|
|
+ # =========================================================================
|
|
|
+ # VAS Task Management (新增 API)
|
|
|
+ # =========================================================================
|
|
|
+
|
|
|
+ def get_vas_task_pop(self, routing_key: str) -> Optional[Dict]:
|
|
|
+ """
|
|
|
+ 获取任务列表
|
|
|
+ API: GET /api/vas/task/list
|
|
|
+ """
|
|
|
+ url = f"{self.base_url}/api/vas/task/pop"
|
|
|
+ params = {
|
|
|
+ "queue_name": routing_key,
|
|
|
+ }
|
|
|
+
|
|
|
+ headers = self._get_headers()
|
|
|
+
|
|
|
try:
|
|
|
- resp = self.session.request(method, url, timeout=30, **kwargs)
|
|
|
+ resp = self.session.get(url, params=params, headers=headers, timeout=30)
|
|
|
if resp.status_code == 200:
|
|
|
- try:
|
|
|
- return resp.json()
|
|
|
- except json.JSONDecodeError:
|
|
|
- # 部分接口可能返回纯文本或空,视情况而定
|
|
|
- return resp.text
|
|
|
+ result = resp.json()
|
|
|
+ if result.get("code") == 0:
|
|
|
+ return result.get("data", {})
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"get_vas_task_list biz error: {result.get('message')}")
|
|
|
else:
|
|
|
- VSC_ERROR("vs_cloud", f"Request failed: {method} {url} [{resp.status_code}] {resp.text}")
|
|
|
+ VSC_ERROR("vs_cloud", f"get_vas_task_list failed: {resp.status_code} {resp.text[:100]}")
|
|
|
except Exception as e:
|
|
|
- VSC_ERROR("vs_cloud", f"Request exception: {method} {url} - {str(e)}")
|
|
|
- return None
|
|
|
-
|
|
|
- def query_user_pending(self, tech_provider: str, pending_users_out: Dict[str, Any]) -> bool:
|
|
|
- """查询待处理用户"""
|
|
|
- params = {"tech_provider": tech_provider}
|
|
|
- headers = self._get_headers()
|
|
|
- # C++: headers = curl_slist_append(headers, auth.c_str());
|
|
|
-
|
|
|
- result = self._handle_request("GET", "/api/autobooking/pending", params=params, headers=headers)
|
|
|
- if result is not None:
|
|
|
- pending_users_out.update(result)
|
|
|
- return True
|
|
|
- return False
|
|
|
-
|
|
|
- def query_user_info(self, uid: str, userinfo_out: Dict[str, Any]) -> bool:
|
|
|
- """查询 VFS 用户信息"""
|
|
|
- headers = self._get_headers()
|
|
|
- result = self._handle_request("GET", f"/api/autobooking/{uid}", headers=headers)
|
|
|
- if result is not None:
|
|
|
- userinfo_out.update(result)
|
|
|
- return True
|
|
|
+ VSC_ERROR("vs_cloud", f"get_vas_task_list exception: {e}")
|
|
|
return False
|
|
|
|
|
|
- def update_user_info(self, uid: str, uinfo: Dict[str, Any], updated_userinfo_out: Dict[str, Any]) -> bool:
|
|
|
- """更新 VFS 用户信息"""
|
|
|
+ def update_vas_task(self,
|
|
|
+ task_id: int,
|
|
|
+ update_data: Dict[str, Any]) -> Optional[Dict]:
|
|
|
+ """
|
|
|
+ 更新任务
|
|
|
+ API: POST /api/vas/task/update?id=1
|
|
|
+ Body: update_data (json)
|
|
|
+ """
|
|
|
+ url = f"{self.base_url}/api/vas/task/update"
|
|
|
+ params = {"id": task_id}
|
|
|
headers = self._get_headers()
|
|
|
- # C++: if (!uinfo.is_null()) put_data = uinfo.dump();
|
|
|
|
|
|
- result = self._handle_request("PUT", f"/api/autobooking/{uid}", headers=headers, json=uinfo)
|
|
|
- if result is not None:
|
|
|
- updated_userinfo_out.update(result)
|
|
|
- return True
|
|
|
- return False
|
|
|
+ try:
|
|
|
+ resp = self.session.post(url, params=params, json=update_data, headers=headers, timeout=30)
|
|
|
+ if resp.status_code == 200:
|
|
|
+ result = resp.json()
|
|
|
+ if result.get("code") == 0:
|
|
|
+ return result.get("data", {})
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"update_vas_task failed: {resp.status_code} {resp.text[:100]}")
|
|
|
+ except Exception as e:
|
|
|
+ VSC_ERROR("vs_cloud", f"update_vas_task exception: {e}")
|
|
|
+
|
|
|
+ return None
|
|
|
|
|
|
- def submit_anti_turnstile_task(self, proxy: str, website_url: str, task_out: Dict[str, Any]) -> bool:
|
|
|
+ def submit_anti_turnstile_task(self, proxy: str, website_url: str) -> Optional[Dict]:
|
|
|
"""
|
|
|
提交反 Turnstile 任务
|
|
|
- 注意 C++ 逻辑:args 字段是 JSON dump 后的字符串
|
|
|
"""
|
|
|
+ url = f"{self.base_url}/api/tasks"
|
|
|
headers = self._get_headers()
|
|
|
|
|
|
args = {
|
|
|
@@ -95,32 +104,46 @@ class VSCloudApi:
|
|
|
|
|
|
payload = {
|
|
|
"command": "AntiCloudflareTurnstileTask",
|
|
|
- "args": json.dumps(args), # C++: data["args"] = args.dump();
|
|
|
+ "args": json.dumps(args),
|
|
|
"status": 0
|
|
|
}
|
|
|
|
|
|
- result = self._handle_request("POST", "/api/tasks", headers=headers, json=payload)
|
|
|
- if result is not None:
|
|
|
- task_out.update(result)
|
|
|
- return True
|
|
|
- return False
|
|
|
+ try:
|
|
|
+ resp = self.session.post(url, headers=headers, json=payload, timeout=30)
|
|
|
+ if resp.status_code == 200:
|
|
|
+ result = resp.json()
|
|
|
+ if result.get("code") == 0:
|
|
|
+ return result.get("data", {})
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"submit_anti_turnstile_task failed: {resp.status_code} {resp.text[:100]}")
|
|
|
+ except Exception as e:
|
|
|
+ VSC_ERROR("vs_cloud", f"submit_anti_turnstile_task exception: {e}")
|
|
|
+ return None
|
|
|
|
|
|
- def get_anti_turnstile_result(self, task_id: str, result_out: Dict[str, Any]) -> bool:
|
|
|
+ def get_anti_turnstile_result(self, task_id: str) -> Optional[Dict]:
|
|
|
"""获取反 Turnstile 结果"""
|
|
|
+ url = f"{self.base_url}/api/tasks/{task_id}"
|
|
|
headers = self._get_headers()
|
|
|
- result = self._handle_request("GET", f"/api/tasks/{task_id}", headers=headers)
|
|
|
- if result is not None:
|
|
|
- result_out.update(result)
|
|
|
- return True
|
|
|
- return False
|
|
|
+ try:
|
|
|
+ resp = self.session.get(url, headers=headers, timeout=30)
|
|
|
+ if resp.status_code == 200:
|
|
|
+ result = resp.json()
|
|
|
+ if result.get("code") == 0:
|
|
|
+ return result.get("data", {})
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"get_anti_turnstile_result failed: {resp.status_code} {resp.text[:100]}")
|
|
|
+ except Exception as e:
|
|
|
+ VSC_ERROR("vs_cloud", f"get_anti_turnstile_result exception: {e}")
|
|
|
+ return None
|
|
|
|
|
|
def submit_anticloudflare_task(self, proxy: str, website_url: str) -> Optional[Dict]:
|
|
|
"""
|
|
|
提交 AntiCloudflareTask (用于 TLSContact 5s 盾)
|
|
|
"""
|
|
|
- # 注意:这里路径根据之前的 C++ 移植经验是 /api/tasks,
|
|
|
- # 如果你确定是 /common/api/tasks,请修改这里。为了兼容现有 base_url,我使用 /api/tasks
|
|
|
- # url = f"https://{self._base_url}/common/api/tasks"
|
|
|
url = f"{self.base_url}/api/tasks"
|
|
|
|
|
|
args = {
|
|
|
@@ -128,20 +151,21 @@ class VSCloudApi:
|
|
|
'websiteUrl': website_url
|
|
|
}
|
|
|
data = {
|
|
|
- "command": "AntiCloudflareTask", # 关键:Command 名字不同
|
|
|
+ "command": "AntiCloudflareTask",
|
|
|
"args": json.dumps(args),
|
|
|
"status": 0
|
|
|
}
|
|
|
- headers = {
|
|
|
- 'Authorization': self.api_token,
|
|
|
- 'Content-Type': 'application/json'
|
|
|
- }
|
|
|
+ headers = self._get_headers()
|
|
|
try:
|
|
|
response = self.session.post(url, headers=headers, json=data, timeout=30)
|
|
|
- if response.status_code != 200:
|
|
|
- VSC_ERROR("vs_cloud", f"submit_anticloudflare_task failed: {response.status_code}")
|
|
|
- return None
|
|
|
- return response.json()
|
|
|
+ if response.status_code == 200:
|
|
|
+ result = response.json()
|
|
|
+ if result.get("code") == 0:
|
|
|
+ return result.get("data", {})
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"get_anti_turnstile_result failed: {response.status_code} {response.text[:100]}")
|
|
|
except Exception as e:
|
|
|
VSC_ERROR("vs_cloud", f"submit_anticloudflare_task exception: {e}")
|
|
|
return None
|
|
|
@@ -151,26 +175,27 @@ class VSCloudApi:
|
|
|
获取 AntiCloudflareTask 结果 (带轮询)
|
|
|
"""
|
|
|
url = f"{self.base_url}/api/tasks/{task_id}"
|
|
|
- headers = {
|
|
|
- 'Authorization': self.api_token
|
|
|
- }
|
|
|
+ headers = self._get_headers()
|
|
|
try:
|
|
|
for attempt in range(max_retries):
|
|
|
response = self.session.get(url, headers=headers, timeout=30)
|
|
|
+
|
|
|
if response.status_code == 200:
|
|
|
- data = response.json()
|
|
|
- # status 2 表示成功
|
|
|
- if data.get("status") == 2:
|
|
|
- return data
|
|
|
- elif data.get("status") == 3:
|
|
|
- VSC_ERROR("vs_cloud", f"AntiCloudflareTask failed: {data.get('result')}")
|
|
|
- return None
|
|
|
+ result = response.json()
|
|
|
+ if result.get("code") == 0:
|
|
|
+ data = result.get("data", {})
|
|
|
+ # status 2 表示成功
|
|
|
+ if data.get("status") == 2:
|
|
|
+ return data
|
|
|
+ elif data.get("status") == 3:
|
|
|
+ VSC_ERROR("vs_cloud", f"AntiCloudflareTask failed: {data.get('result')}")
|
|
|
+ return None
|
|
|
+ else:
|
|
|
+ time.sleep(retry_interval)
|
|
|
else:
|
|
|
- # VSC_DEBUG("vs_cloud", f"Task {task_id} not ready, retrying...")
|
|
|
- time.sleep(retry_interval)
|
|
|
+ VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
|
|
|
else:
|
|
|
- VSC_ERROR("vs_cloud", f"Error getting task result: {response.text}")
|
|
|
- break
|
|
|
+ VSC_ERROR("vs_cloud", f"get_anti_turnstile_result failed: {response.status_code} {response.text[:100]}")
|
|
|
VSC_ERROR("vs_cloud", "Max retries reached, AntiCloudflareTask not completed.")
|
|
|
except Exception as e:
|
|
|
VSC_ERROR("vs_cloud", f"get_anticloudflare_result exception: {e}")
|
|
|
@@ -183,10 +208,10 @@ class VSCloudApi:
|
|
|
local_storage: str,
|
|
|
user_agent: str,
|
|
|
proxy: str,
|
|
|
- page: str,
|
|
|
- created_session_out: Dict[str, Any]
|
|
|
- ) -> bool:
|
|
|
+ page: str
|
|
|
+ ) -> Optional[Dict]:
|
|
|
"""创建 http session"""
|
|
|
+ url = f"{self.base_url}/api/http-session"
|
|
|
headers = self._get_headers()
|
|
|
|
|
|
payload = {
|
|
|
@@ -198,13 +223,19 @@ class VSCloudApi:
|
|
|
"session_id": session_id
|
|
|
}
|
|
|
|
|
|
- result = self._handle_request("POST", "/api/http-session", headers=headers, json=payload)
|
|
|
- if result is not None:
|
|
|
- # result 可能是 None 如果解析失败,或者是一个 dict
|
|
|
- if isinstance(result, dict):
|
|
|
- created_session_out.update(result)
|
|
|
- return True
|
|
|
- return False
|
|
|
+ try:
|
|
|
+ resp = self.session.post(url, headers=headers, json=payload, timeout=30)
|
|
|
+ if resp.status_code == 200:
|
|
|
+ result = resp.json()
|
|
|
+ if result.get("code") == 0:
|
|
|
+ return result.get("data", {})
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"create_http_session failed: {resp.status_code} {resp.text[:100]}")
|
|
|
+ except Exception as e:
|
|
|
+ VSC_ERROR("vs_cloud", f"create_http_session exception: {e}")
|
|
|
+ return None
|
|
|
|
|
|
def fetch_mail_content(
|
|
|
self,
|
|
|
@@ -214,12 +245,10 @@ class VSCloudApi:
|
|
|
subject_keywords: str,
|
|
|
body_keywords: str,
|
|
|
sent_date: str,
|
|
|
- expiry: int,
|
|
|
- content_out: list # Python string is immutable, use list to simulate pointer behavior like C++
|
|
|
- ) -> bool:
|
|
|
+ expiry: int
|
|
|
+ ) -> Optional[str]:
|
|
|
"""
|
|
|
获取邮件内容
|
|
|
- C++ logic: query params in URL + POST method + empty body
|
|
|
"""
|
|
|
params = {
|
|
|
"email": email,
|
|
|
@@ -231,26 +260,23 @@ class VSCloudApi:
|
|
|
"expiry": str(expiry)
|
|
|
}
|
|
|
|
|
|
- headers = self._get_headers()
|
|
|
- # C++: curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
|
|
|
-
|
|
|
- # requests.post(url, params=params) 会把 params 拼接到 URL 后面
|
|
|
url = f"{self.base_url}/api/email-authorizations/fetch"
|
|
|
+ headers = self._get_headers()
|
|
|
try:
|
|
|
resp = self.session.post(url, headers=headers, params=params, data="", timeout=30)
|
|
|
if resp.status_code == 200:
|
|
|
- # C++: content.assign(response_string.data(), response_string.length());
|
|
|
- # 这里假设 content_out 是一个 list,我们将结果 append 进去或修改第一个元素
|
|
|
- if isinstance(content_out, list):
|
|
|
- content_out.clear()
|
|
|
- content_out.append(resp.text)
|
|
|
- return True
|
|
|
+ result = resp.json()
|
|
|
+ if result.get('code', 0) == 0:
|
|
|
+ data = result.get('data', {})
|
|
|
+ return data.get('body', "")
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"fetch_mail_content biz error: {result.get('message')}")
|
|
|
else:
|
|
|
- VSC_ERROR("vs_cloud", f"fetch_mail_content failed: {resp.status_code} {resp.text}")
|
|
|
+ VSC_ERROR("vs_cloud", f"fetch_mail_content failed: {resp.status_code} {resp.text[:100]}")
|
|
|
except Exception as e:
|
|
|
VSC_ERROR("vs_cloud", f"fetch_mail_content exception: {e}")
|
|
|
|
|
|
- return False
|
|
|
+ return None
|
|
|
|
|
|
def fetch_mail_content_from_top(
|
|
|
self,
|
|
|
@@ -259,9 +285,8 @@ class VSCloudApi:
|
|
|
recipient: str,
|
|
|
subject_keywords: str,
|
|
|
body_keywords: str,
|
|
|
- top: int,
|
|
|
- content_out: list
|
|
|
- ) -> bool:
|
|
|
+ top: int
|
|
|
+ ) -> Optional[str]:
|
|
|
"""从顶部获取邮件内容"""
|
|
|
params = {
|
|
|
"email": email,
|
|
|
@@ -272,19 +297,20 @@ class VSCloudApi:
|
|
|
"top": str(top)
|
|
|
}
|
|
|
|
|
|
- headers = self._get_headers()
|
|
|
url = f"{self.base_url}/api/email-authorizations/fetch-top"
|
|
|
+ headers = self._get_headers()
|
|
|
|
|
|
try:
|
|
|
resp = self.session.post(url, headers=headers, params=params, data="", timeout=30)
|
|
|
if resp.status_code == 200:
|
|
|
- if isinstance(content_out, list):
|
|
|
- content_out.clear()
|
|
|
- content_out.append(resp.text)
|
|
|
- return True
|
|
|
+ result = resp.json()
|
|
|
+ if result.get('code', 0) == 0:
|
|
|
+ data = result.get('data', {})
|
|
|
+ return data.get('body', "")
|
|
|
+ else:
|
|
|
+ VSC_ERROR("vs_cloud", f"fetch_mail_content_from_top biz error: {result.get('message')}")
|
|
|
else:
|
|
|
- VSC_ERROR("vs_cloud", f"fetch_mail_content_from_top failed: {resp.status_code} {resp.text}")
|
|
|
+ VSC_ERROR("vs_cloud", f"fetch_mail_content_from_top failed: {resp.status_code} {resp.text[:100]}")
|
|
|
except Exception as e:
|
|
|
VSC_ERROR("vs_cloud", f"fetch_mail_content_from_top exception: {e}")
|
|
|
-
|
|
|
- return False
|
|
|
+ return None
|