mirror of
https://github.com/TheSmallHanCat/flow2api.git
synced 2026-09-03 06:44:36 +08:00
YesCaptcha/CapMonster/EzCaptcha/CapSolver 等 API 打码返回的 solution 包含 gRecaptchaResponse 与 userAgent 两个字段。 _get_api_captcha_token 之前只返回 gRecaptchaResponse, 后续 Flow API 请求继续用 _generate_user_agent 生成的 macOS UA, 与打码时的 Windows UA 不一致, 触发 PUBLIC_ERROR_UNUSUAL_ACTIVITY: reCAPTCHA evaluation failed。 将 userAgent 合并到 fingerprint context, 让 _make_request 自动 沿用打码 UA, sec-ch-ua-platform 由 fallback 逻辑按 UA 自动推断 (Windows → "Windows", mac → "macOS" 等)。 新增 tests/test_api_captcha_fingerprint.py 覆盖该路径。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
90 lines
3.2 KiB
Python
90 lines
3.2 KiB
Python
"""Verify API-mode captcha injects solution user_agent into the request fingerprint.
|
|
|
|
背景: YesCaptcha / CapMonster / EzCaptcha / CapSolver 返回的 solution 包含
|
|
gRecaptchaResponse 与 userAgent。Google reCAPTCHA V3 评估会校验 token 与
|
|
提交请求的 User-Agent 一致性, 因此调用 Flow API 时必须沿用打码服务返回的
|
|
UA, 否则服务端判定 UNUSUAL_ACTIVITY 并返回 reCAPTCHA evaluation failed。
|
|
"""
|
|
|
|
import unittest
|
|
from unittest.mock import patch, AsyncMock, MagicMock
|
|
|
|
from src.services.flow_client import FlowClient
|
|
|
|
|
|
class _FakeProxyManager:
|
|
async def get_request_proxy_url(self):
|
|
return None
|
|
|
|
|
|
class _FakeAsyncSession:
|
|
"""模拟 curl_cffi 的 AsyncSession: createTask 返回 taskId, getTaskResult 返回 ready。"""
|
|
|
|
def __init__(self):
|
|
self._calls = 0
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *args):
|
|
return False
|
|
|
|
async def post(self, *args, **kwargs):
|
|
self._calls += 1
|
|
response = MagicMock()
|
|
if self._calls == 1:
|
|
# createTask
|
|
response.status_code = 200
|
|
response.json.return_value = {
|
|
"errorId": 0,
|
|
"taskId": "tid-xyz",
|
|
}
|
|
else:
|
|
# getTaskResult
|
|
response.status_code = 200
|
|
response.json.return_value = {
|
|
"errorId": 0,
|
|
"status": "ready",
|
|
"solution": {
|
|
"gRecaptchaResponse": "token-abc",
|
|
"userAgent": (
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
"Chrome/147.0.0.0 Safari/537.36"
|
|
),
|
|
},
|
|
}
|
|
return response
|
|
|
|
|
|
class ApiCaptchaFingerprintTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_api_captcha_returns_token_and_user_agent(self):
|
|
"""_get_api_captcha_token 必须返回 (token, userAgent) 元组。"""
|
|
flow = FlowClient.__new__(FlowClient)
|
|
flow.proxy_manager = _FakeProxyManager()
|
|
fake_session = _FakeAsyncSession()
|
|
|
|
with patch("src.services.flow_client.AsyncSession", lambda *a, **kw: fake_session), \
|
|
patch("src.services.flow_client.config") as cfg, \
|
|
patch("asyncio.sleep", new=AsyncMock()):
|
|
cfg.yescaptcha_api_key = "key"
|
|
cfg.yescaptcha_base_url = "https://api.yescaptcha.com"
|
|
cfg.yescaptcha_task_type = "RecaptchaV3TaskProxylessM1"
|
|
cfg.debug_enabled = False
|
|
|
|
result = await flow._get_api_captcha_token(
|
|
method="yescaptcha",
|
|
project_id="proj-1",
|
|
action="IMAGE_GENERATION",
|
|
)
|
|
|
|
self.assertIsNotNone(result, "函数不应返回 None, 因为我们 mock 了 ready 状态")
|
|
self.assertIsInstance(result, tuple, "_get_api_captcha_token 应返回 (token, userAgent) 元组")
|
|
token, user_agent = result
|
|
self.assertEqual(token, "token-abc")
|
|
self.assertIn("Windows", user_agent, "userAgent 应当来自打码服务 solution, 包含 Windows")
|
|
self.assertIn("Chrome/147", user_agent)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main() |