|
|
@@ -435,53 +435,80 @@ class VfsPlugin2(IVSPlg):
|
|
|
def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None, retry_count=0):
|
|
|
"""
|
|
|
核心方法:在 DrissionPage 浏览器上下文中注入 JS 执行 fetch
|
|
|
+ 并记录详细的 Traffic 日志用于分析
|
|
|
"""
|
|
|
if not self.page:
|
|
|
raise BizLogicError("Browser session not initialized")
|
|
|
|
|
|
- # 1. 确保在正确的上下文 (VFS 登录页或 API 域名)
|
|
|
- # create_session 已经打开了页面,这里通常不需要额外跳转
|
|
|
- # 如果页面崩溃或跳转了,可能需要恢复
|
|
|
-
|
|
|
- # 2. 构造参数
|
|
|
+ # ---------------------------------------------------------
|
|
|
+ # 1. 预处理 URL (构造最终请求地址)
|
|
|
+ # ---------------------------------------------------------
|
|
|
+ req_url = url
|
|
|
if params:
|
|
|
- if '?' in url:
|
|
|
- url += '&' + urllib.parse.urlencode(params)
|
|
|
- else:
|
|
|
- url += '?' + urllib.parse.urlencode(params)
|
|
|
+ # 确保引用了 urllib
|
|
|
+ import urllib.parse
|
|
|
+ sep = '&' if '?' in req_url else '?'
|
|
|
+ req_url += sep + urllib.parse.urlencode(params)
|
|
|
|
|
|
+ # ---------------------------------------------------------
|
|
|
+ # 2. 构造 Body 和 Fetch 选项
|
|
|
+ # ---------------------------------------------------------
|
|
|
+ final_headers = headers or {}
|
|
|
+
|
|
|
fetch_options = {
|
|
|
"method": method.upper(),
|
|
|
- "headers": headers or {},
|
|
|
+ "headers": final_headers,
|
|
|
"credentials": "include" # 关键:带上浏览器 Cookie
|
|
|
}
|
|
|
|
|
|
+ # 用于日志记录的 Body 内容(字符串形式)
|
|
|
+ log_body = "None"
|
|
|
+
|
|
|
if json_data:
|
|
|
- fetch_options['body'] = json.dumps(json_data)
|
|
|
+ json_str = json.dumps(json_data)
|
|
|
+ fetch_options['body'] = json_str
|
|
|
fetch_options['headers']['Content-Type'] = 'application/json'
|
|
|
+ log_body = json_str
|
|
|
elif data:
|
|
|
if isinstance(data, dict):
|
|
|
- fetch_options['body'] = urllib.parse.urlencode(data)
|
|
|
+ import urllib.parse
|
|
|
+ encoded_data = urllib.parse.urlencode(data)
|
|
|
+ fetch_options['body'] = encoded_data
|
|
|
fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
|
+ log_body = encoded_data
|
|
|
else:
|
|
|
fetch_options['body'] = data
|
|
|
-
|
|
|
- # 3. 注入 JS
|
|
|
+ log_body = str(data)
|
|
|
+
|
|
|
+ # ---------------------------------------------------------
|
|
|
+ # [日志] 记录请求数据
|
|
|
+ # ---------------------------------------------------------
|
|
|
+ self._log(f"┌── [TRAFFIC REQUEST] {method} {req_url}")
|
|
|
+ self._log(f"├── Headers: {json.dumps(final_headers)}")
|
|
|
+ self._log(f"└── Body: {log_body}")
|
|
|
+
|
|
|
+ # ---------------------------------------------------------
|
|
|
+ # 3. 注入 JS 执行 Fetch
|
|
|
+ # ---------------------------------------------------------
|
|
|
js_script = f"""
|
|
|
- const url = "{url}";
|
|
|
+ const url = "{req_url}";
|
|
|
const options = {json.dumps(fetch_options)};
|
|
|
|
|
|
+ const startTime = Date.now();
|
|
|
+
|
|
|
return fetch(url, options)
|
|
|
.then(async response => {{
|
|
|
const text = await response.text();
|
|
|
const headers = {{}};
|
|
|
response.headers.forEach((value, key) => headers[key] = value);
|
|
|
+ const endTime = Date.now();
|
|
|
|
|
|
return {{
|
|
|
status: response.status,
|
|
|
body: text,
|
|
|
headers: headers,
|
|
|
- url: response.url
|
|
|
+ url: response.url,
|
|
|
+ duration: endTime - startTime
|
|
|
}};
|
|
|
}})
|
|
|
.catch(error => {{
|
|
|
@@ -489,48 +516,78 @@ class VfsPlugin2(IVSPlg):
|
|
|
status: 0,
|
|
|
body: error.toString(),
|
|
|
headers: {{}},
|
|
|
- url: url
|
|
|
+ url: url,
|
|
|
+ duration: Date.now() - startTime
|
|
|
}};
|
|
|
}});
|
|
|
"""
|
|
|
|
|
|
- if self.config.debug:
|
|
|
- self._log(f"[Browser Fetch] {method} {url}")
|
|
|
-
|
|
|
try:
|
|
|
# run_js 直接返回 return 的对象
|
|
|
- res_dict = self.page.run_js(js_script, timeout=30)
|
|
|
+ # 适当增加超时时间,防止网络慢导致 Python 侧报错
|
|
|
+ res_dict = self.page.run_js(js_script, timeout=60)
|
|
|
except Exception as e:
|
|
|
+ self._log(f"[TRAFFIC ERROR] JS Execution failed: {e}")
|
|
|
raise BizLogicError(f"Browser JS Execution Error: {e}")
|
|
|
|
|
|
resp = BrowserResponse(res_dict)
|
|
|
|
|
|
+ # ---------------------------------------------------------
|
|
|
+ # [日志] 记录响应数据
|
|
|
+ # ---------------------------------------------------------
|
|
|
+ duration = res_dict.get('duration', 0)
|
|
|
+ # 截取过长的响应体,避免日志文件爆炸 (保留前 500 字符)
|
|
|
+ # 如果需要完整分析,可以去掉 [:500]
|
|
|
+ resp_preview = resp.text[:500] + "..." if len(resp.text) > 500 else resp.text
|
|
|
+
|
|
|
+ self._log(f"┌── [TRAFFIC RESPONSE] Status: {resp.status_code} | Time: {duration}ms")
|
|
|
+ self._log(f"└── Body: {resp_preview}")
|
|
|
+
|
|
|
+ # ---------------------------------------------------------
|
|
|
# 4. 统一处理状态码
|
|
|
+ # ---------------------------------------------------------
|
|
|
if resp.status_code == 200:
|
|
|
return resp
|
|
|
+
|
|
|
elif resp.status_code == 401:
|
|
|
self.is_healthy = False
|
|
|
raise SessionExpiredOrInvalidError(f"401 Unauthorized: {resp.text[:100]}")
|
|
|
+
|
|
|
elif resp.status_code == 403:
|
|
|
+ # 检查是否是 Cloudflare 拦截
|
|
|
if "Just a moment" in resp.text or "cloudflare" in resp.text.lower():
|
|
|
- self._log(f"HTTP 403 (Cloudflare) detected. Re-verifying (Try {retry_count+1}/3)...")
|
|
|
+ self._log(f"[TRAFFIC] HTTP 403 (Cloudflare) detected. Re-verifying (Try {retry_count+1}/3)...")
|
|
|
+
|
|
|
if retry_count < 3:
|
|
|
+ # 调用过盾逻辑
|
|
|
new_token = self._refresh_turnstile_token()
|
|
|
+
|
|
|
if new_token:
|
|
|
- self._log("In-page verification success. Retrying...")
|
|
|
+ self._log("[TRAFFIC] In-page verification success. Retrying...")
|
|
|
+
|
|
|
+ # 如果原请求包含验证码字段,更新它
|
|
|
if json_data and "captcha_api_key" in json_data:
|
|
|
json_data["captcha_api_key"] = new_token
|
|
|
+
|
|
|
+ # 递归重试
|
|
|
return self._perform_request(method, url, headers, data, json_data, params, retry_count+1)
|
|
|
+
|
|
|
+ # 如果不是 CF 或者重试耗尽
|
|
|
raise PermissionDeniedError(f"HTTP 403 Forbidden: {resp.text[:100]}")
|
|
|
+
|
|
|
elif resp.status_code == 429:
|
|
|
self.is_healthy = False
|
|
|
raise RateLimiteddError(f"429 Rate Limit: {resp.text[:100]}")
|
|
|
+
|
|
|
elif resp.status_code == 0:
|
|
|
raise BizLogicError(f"Network Error (Fetch Failed): {resp.text}")
|
|
|
+
|
|
|
else:
|
|
|
# 允许 400 业务错误通过,交给上层解析 (例如登录失败)
|
|
|
if url.endswith("/login") and resp.status_code == 400:
|
|
|
return resp
|
|
|
+
|
|
|
+ # 其他错误视为业务逻辑异常
|
|
|
raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
|
|
|
|
|
|
def _handle_cookie_banner(self):
|