feat: 无头打码与YesCaptcha打码

This commit is contained in:
TheSmallHanCat
2025-12-17 18:51:10 +08:00
parent ea88387d4c
commit de31fe0877
12 changed files with 563 additions and 13 deletions

View File

@@ -2,9 +2,32 @@ FROM python:3.11-slim
WORKDIR /app
# 安装 Playwright 所需的系统依赖
RUN apt-get update && apt-get install -y \
libnss3 \
libnspr4 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libcups2 \
libdrm2 \
libxkbcommon0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libasound2 \
libpango-1.0-0 \
libcairo2 \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 安装 Playwright 浏览器(仅 Chromium
RUN playwright install chromium --with-deps
COPY . .
EXPOSE 8000

View File

@@ -21,6 +21,7 @@
- 🚀 **负载均衡** - 多 Token 轮询和并发控制
- 🌐 **代理支持** - 支持 HTTP/SOCKS5 代理
- 📱 **Web 管理界面** - 直观的 Token 和配置管理
- 🎨 **图片生成连续对话**
## 🚀 快速开始
@@ -29,6 +30,9 @@
- Docker 和 Docker Compose推荐
- 或 Python 3.8+
- 由于Flow增加了额外的验证码你可以自行选择使用浏览器打码或第三发打码
注册[YesCaptcha](https://yescaptcha.com/i/13Xd8K)并获取api key将其填入系统配置页面```YesCaptcha API密钥```区域
### 方式一Docker 部署(推荐)
#### 标准模式(不使用代理)
@@ -80,13 +84,11 @@ python main.py
### 首次访问
服务启动后,访问管理后台: **http://localhost:8000**
服务启动后,访问管理后台: **http://localhost:8000**,首次登录后请立即修改密码!
- **用户名**: `admin`
- **密码**: `admin`
⚠️ **重要**: 首次登录后请立即修改密码!
## 📋 支持的模型
### 图片生成
@@ -246,6 +248,8 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \
## 🙏 致谢
- [PearNoDec](https://github.com/PearNoDec) 提供的YesCaptcha打码方案
- [raomaiping](https://github.com/raomaiping) 提供的无头打码方案
感谢所有贡献者和使用者的支持!
---
@@ -253,7 +257,6 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \
## 📞 联系方式
- 提交 Issue[GitHub Issues](https://github.com/TheSmallHanCat/flow2api/issues)
- 讨论:[GitHub Discussions](https://github.com/TheSmallHanCat/flow2api/discussions)
---

View File

@@ -35,3 +35,8 @@ error_ban_threshold = 3
enabled = false
timeout = 7200 # 缓存超时时间(秒), 默认2小时
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
[captcha]
captcha_method = "browser" # 打码方式: yescaptcha 或 browser
yescaptcha_api_key = "" # YesCaptcha API密钥
yescaptcha_base_url = "https://api.yescaptcha.com"

View File

@@ -35,3 +35,8 @@ error_ban_threshold = 3
enabled = false
timeout = 7200 # 缓存超时时间(秒), 默认2小时
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
[captcha]
captcha_method = "browser" # 打码方式: yescaptcha 或 browser
yescaptcha_api_key = "" # YesCaptcha API密钥
yescaptcha_base_url = "https://api.yescaptcha.com"

View File

@@ -2,8 +2,9 @@ fastapi==0.119.0
uvicorn[standard]==0.32.1
aiosqlite==0.20.0
pydantic==2.10.4
curl-cffi
curl-cffi==0.7.3
tomli==2.2.1
bcrypt==4.2.1
python-multipart==0.0.20
python-dateutil==2.8.2
playwright==1.48.0

View File

@@ -839,10 +839,12 @@ async def update_captcha_config(
token: str = Depends(verify_admin_token)
):
"""Update captcha configuration"""
captcha_method = request.get("captcha_method")
yescaptcha_api_key = request.get("yescaptcha_api_key")
yescaptcha_base_url = request.get("yescaptcha_base_url")
await db.update_captcha_config(
captcha_method=captcha_method,
yescaptcha_api_key=yescaptcha_api_key,
yescaptcha_base_url=yescaptcha_base_url
)
@@ -858,6 +860,7 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)):
"""Get captcha configuration"""
captcha_config = await db.get_captcha_config()
return {
"captcha_method": captcha_config.captcha_method,
"yescaptcha_api_key": captcha_config.yescaptcha_api_key,
"yescaptcha_base_url": captcha_config.yescaptcha_base_url
}

View File

@@ -179,5 +179,40 @@ class Config:
self._config["cache"] = {}
self._config["cache"]["base_url"] = base_url
# Captcha configuration
@property
def captcha_method(self) -> str:
"""Get captcha method"""
return self._config.get("captcha", {}).get("captcha_method", "yescaptcha")
def set_captcha_method(self, method: str):
"""Set captcha method"""
if "captcha" not in self._config:
self._config["captcha"] = {}
self._config["captcha"]["captcha_method"] = method
@property
def yescaptcha_api_key(self) -> str:
"""Get YesCaptcha API key"""
return self._config.get("captcha", {}).get("yescaptcha_api_key", "")
def set_yescaptcha_api_key(self, api_key: str):
"""Set YesCaptcha API key"""
if "captcha" not in self._config:
self._config["captcha"] = {}
self._config["captcha"]["yescaptcha_api_key"] = api_key
@property
def yescaptcha_base_url(self) -> str:
"""Get YesCaptcha base URL"""
return self._config.get("captcha", {}).get("yescaptcha_base_url", "https://api.yescaptcha.com")
def set_yescaptcha_base_url(self, base_url: str):
"""Set YesCaptcha base URL"""
if "captcha" not in self._config:
self._config["captcha"] = {}
self._config["captcha"]["yescaptcha_base_url"] = base_url
# Global config instance
config = Config()

View File

@@ -4,7 +4,7 @@ import json
from datetime import datetime
from typing import Optional, List
from pathlib import Path
from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, GenerationConfig, CacheConfig, Project
from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, GenerationConfig, CacheConfig, Project, CaptchaConfig
class Database:
@@ -148,6 +148,25 @@ class Database:
VALUES (1, ?, ?, ?, ?)
""", (debug_enabled, log_requests, log_responses, mask_token))
# Ensure captcha_config has a row
cursor = await db.execute("SELECT COUNT(*) FROM captcha_config")
count = await cursor.fetchone()
if count[0] == 0:
captcha_method = "browser"
yescaptcha_api_key = ""
yescaptcha_base_url = "https://api.yescaptcha.com"
if config_dict:
captcha_config = config_dict.get("captcha", {})
captcha_method = captcha_config.get("captcha_method", "browser")
yescaptcha_api_key = captcha_config.get("yescaptcha_api_key", "")
yescaptcha_base_url = captcha_config.get("yescaptcha_base_url", "https://api.yescaptcha.com")
await db.execute("""
INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url)
VALUES (1, ?, ?, ?)
""", (captcha_method, yescaptcha_api_key, yescaptcha_base_url))
async def check_and_migrate_db(self, config_dict: dict = None):
"""Check database integrity and perform migrations if needed
@@ -179,6 +198,22 @@ class Database:
)
""")
# Check and create captcha_config table if missing
if not await self._table_exists(db, "captcha_config"):
print(" ✓ Creating missing table: captcha_config")
await db.execute("""
CREATE TABLE captcha_config (
id INTEGER PRIMARY KEY DEFAULT 1,
captcha_method TEXT DEFAULT 'browser',
yescaptcha_api_key TEXT DEFAULT '',
yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
page_action TEXT DEFAULT 'FLOW_GENERATION',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# ========== Step 2: Add missing columns to existing tables ==========
# Check and add missing columns to tokens table
if await self._table_exists(db, "tokens"):
@@ -234,8 +269,8 @@ class Database:
# ========== Step 3: Ensure all config tables have default rows ==========
# Note: This will NOT overwrite existing config rows
# It only ensures missing rows are created with default values
await self._ensure_config_rows(db, config_dict=None)
# It only ensures missing rows are created with default values from setting.toml
await self._ensure_config_rows(db, config_dict=config_dict)
await db.commit()
print("Database migration check completed.")
@@ -395,6 +430,20 @@ class Database:
)
""")
# Captcha config table
await db.execute("""
CREATE TABLE IF NOT EXISTS captcha_config (
id INTEGER PRIMARY KEY DEFAULT 1,
captcha_method TEXT DEFAULT 'browser',
yescaptcha_api_key TEXT DEFAULT '',
yescaptcha_base_url TEXT DEFAULT 'https://api.yescaptcha.com',
website_key TEXT DEFAULT '6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV',
page_action TEXT DEFAULT 'FLOW_GENERATION',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Create indexes
await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
await db.execute("CREATE INDEX IF NOT EXISTS idx_token_st ON tokens(st)")
@@ -944,6 +993,13 @@ class Database:
if debug_config:
config.set_debug_enabled(debug_config.enabled)
# Reload captcha config
captcha_config = await self.get_captcha_config()
if captcha_config:
config.set_captcha_method(captcha_config.captcha_method)
config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
# Cache config operations
async def get_cache_config(self) -> CacheConfig:
"""Get cache configuration"""
@@ -1046,3 +1102,49 @@ class Database:
""", (new_enabled, new_log_requests, new_log_responses, new_mask_token))
await db.commit()
# Captcha config operations
async def get_captcha_config(self) -> CaptchaConfig:
"""Get captcha configuration"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("SELECT * FROM captcha_config WHERE id = 1")
row = await cursor.fetchone()
if row:
return CaptchaConfig(**dict(row))
return CaptchaConfig()
async def update_captcha_config(
self,
captcha_method: str = None,
yescaptcha_api_key: str = None,
yescaptcha_base_url: str = None
):
"""Update captcha configuration"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("SELECT * FROM captcha_config WHERE id = 1")
row = await cursor.fetchone()
if row:
current = dict(row)
new_method = captcha_method if captcha_method is not None else current.get("captcha_method", "yescaptcha")
new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else current.get("yescaptcha_api_key", "")
new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else current.get("yescaptcha_base_url", "https://api.yescaptcha.com")
await db.execute("""
UPDATE captcha_config
SET captcha_method = ?, yescaptcha_api_key = ?, yescaptcha_base_url = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = 1
""", (new_method, new_api_key, new_base_url))
else:
new_method = captcha_method if captcha_method is not None else "yescaptcha"
new_api_key = yescaptcha_api_key if yescaptcha_api_key is not None else ""
new_base_url = yescaptcha_base_url if yescaptcha_base_url is not None else "https://api.yescaptcha.com"
await db.execute("""
INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url)
VALUES (1, ?, ?, ?)
""", (new_method, new_api_key, new_base_url))
await db.commit()

View File

@@ -144,6 +144,18 @@ class DebugConfig(BaseModel):
updated_at: Optional[datetime] = None
class CaptchaConfig(BaseModel):
"""Captcha configuration"""
id: int = 1
captcha_method: str = "browser" # yescaptcha 或 browser
yescaptcha_api_key: str = ""
yescaptcha_base_url: str = "https://api.yescaptcha.com"
website_key: str = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
page_action: str = "FLOW_GENERATION"
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
# OpenAI Compatible Request Models
class ChatMessage(BaseModel):
"""Chat message"""

View File

@@ -66,6 +66,19 @@ async def lifespan(app: FastAPI):
debug_config = await db.get_debug_config()
config.set_debug_enabled(debug_config.enabled)
# Load captcha configuration from database
captcha_config = await db.get_captcha_config()
config.set_captcha_method(captcha_config.captcha_method)
config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
# Initialize browser captcha service if needed
browser_service = None
if captcha_config.captcha_method == "browser":
from .services.browser_captcha import BrowserCaptchaService
browser_service = await BrowserCaptchaService.get_instance(proxy_manager)
print("✓ Browser captcha service initialized (headless mode)")
# Initialize concurrency manager
tokens = await token_manager.get_all_tokens()
await concurrency_manager.initialize(tokens)
@@ -106,6 +119,10 @@ async def lifespan(app: FastAPI):
await auto_unban_task_handle
except asyncio.CancelledError:
pass
# Close browser if initialized
if browser_service:
await browser_service.close()
print("✓ Browser captcha service closed")
print("✓ File cache cleanup task stopped")
print("✓ 429 auto-unban task stopped")

View File

@@ -0,0 +1,245 @@
"""
浏览器自动化获取 reCAPTCHA token
使用 Playwright 访问页面并执行 reCAPTCHA 验证
"""
import asyncio
import time
from typing import Optional
from playwright.async_api import async_playwright, Browser, BrowserContext
from ..core.logger import debug_logger
class BrowserCaptchaService:
"""浏览器自动化获取 reCAPTCHA token单例模式"""
_instance: Optional['BrowserCaptchaService'] = None
_lock = asyncio.Lock()
def __init__(self, proxy_manager=None):
"""初始化服务(始终使用无头模式)"""
self.headless = True # 始终无头
self.playwright = None
self.browser: Optional[Browser] = None
self._initialized = False
self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
self.proxy_manager = proxy_manager
@classmethod
async def get_instance(cls, proxy_manager=None) -> 'BrowserCaptchaService':
"""获取单例实例"""
if cls._instance is None:
async with cls._lock:
if cls._instance is None:
cls._instance = cls(proxy_manager)
await cls._instance.initialize()
return cls._instance
async def initialize(self):
"""初始化浏览器(启动一次)"""
if self._initialized:
return
try:
# 获取代理配置
proxy_url = None
if self.proxy_manager:
proxy_url = await self.proxy_manager.get_proxy_url()
debug_logger.log_info(f"[BrowserCaptcha] 正在启动浏览器... (proxy={proxy_url or 'None'})")
self.playwright = await async_playwright().start()
# 配置浏览器启动参数
launch_options = {
'headless': self.headless,
'args': [
'--disable-blink-features=AutomationControlled',
'--disable-dev-shm-usage',
'--no-sandbox',
'--disable-setuid-sandbox'
]
}
# 如果有代理,添加代理配置
if proxy_url:
launch_options['proxy'] = {'server': proxy_url}
self.browser = await self.playwright.chromium.launch(**launch_options)
self._initialized = True
debug_logger.log_info(f"[BrowserCaptcha] ✅ 浏览器已启动 (headless={self.headless}, proxy={proxy_url or 'None'})")
except Exception as e:
debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
raise
async def get_token(self, project_id: str) -> Optional[str]:
"""获取 reCAPTCHA token
Args:
project_id: Flow项目ID
Returns:
reCAPTCHA token字符串如果获取失败返回None
"""
if not self._initialized:
await self.initialize()
start_time = time.time()
context = None
try:
# 创建新的上下文
context = await self.browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
locale='en-US',
timezone_id='America/New_York'
)
page = await context.new_page()
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
debug_logger.log_info(f"[BrowserCaptcha] 访问页面: {website_url}")
# 访问页面
try:
await page.goto(website_url, wait_until="domcontentloaded", timeout=30000)
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] 页面加载超时或失败: {str(e)}")
# 检查并注入 reCAPTCHA v3 脚本
debug_logger.log_info("[BrowserCaptcha] 检查并加载 reCAPTCHA v3 脚本...")
script_loaded = await page.evaluate("""
() => {
if (window.grecaptcha && typeof window.grecaptcha.execute === 'function') {
return true;
}
return false;
}
""")
if not script_loaded:
# 注入脚本
debug_logger.log_info("[BrowserCaptcha] 注入 reCAPTCHA v3 脚本...")
await page.evaluate(f"""
() => {{
return new Promise((resolve) => {{
const script = document.createElement('script');
script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
script.async = true;
script.defer = true;
script.onload = () => resolve(true);
script.onerror = () => resolve(false);
document.head.appendChild(script);
}});
}}
""")
# 等待reCAPTCHA加载和初始化
debug_logger.log_info("[BrowserCaptcha] 等待reCAPTCHA初始化...")
for i in range(20):
grecaptcha_ready = await page.evaluate("""
() => {
return window.grecaptcha &&
typeof window.grecaptcha.execute === 'function';
}
""")
if grecaptcha_ready:
debug_logger.log_info(f"[BrowserCaptcha] reCAPTCHA 已准备好(等待了 {i*0.5} 秒)")
break
await asyncio.sleep(0.5)
else:
debug_logger.log_warning("[BrowserCaptcha] reCAPTCHA 初始化超时,继续尝试执行...")
# 额外等待确保完全初始化
await page.wait_for_timeout(1000)
# 执行reCAPTCHA并获取token
debug_logger.log_info("[BrowserCaptcha] 执行reCAPTCHA验证...")
token = await page.evaluate("""
async (websiteKey) => {
try {
if (!window.grecaptcha) {
console.error('[BrowserCaptcha] window.grecaptcha 不存在');
return null;
}
if (typeof window.grecaptcha.execute !== 'function') {
console.error('[BrowserCaptcha] window.grecaptcha.execute 不是函数');
return null;
}
// 确保grecaptcha已准备好
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('reCAPTCHA加载超时'));
}, 15000);
if (window.grecaptcha && window.grecaptcha.ready) {
window.grecaptcha.ready(() => {
clearTimeout(timeout);
resolve();
});
} else {
clearTimeout(timeout);
resolve();
}
});
// 执行reCAPTCHA v3
const token = await window.grecaptcha.execute(websiteKey, {
action: 'FLOW_GENERATION'
});
return token;
} catch (error) {
console.error('[BrowserCaptcha] reCAPTCHA执行错误:', error);
return null;
}
}
""", self.website_key)
duration_ms = (time.time() - start_time) * 1000
if token:
debug_logger.log_info(f"[BrowserCaptcha] ✅ Token获取成功耗时 {duration_ms:.0f}ms")
return token
else:
debug_logger.log_error("[BrowserCaptcha] Token获取失败返回null")
return None
except Exception as e:
debug_logger.log_error(f"[BrowserCaptcha] 获取token异常: {str(e)}")
return None
finally:
# 关闭上下文
if context:
try:
await context.close()
except:
pass
async def close(self):
"""关闭浏览器"""
try:
if self.browser:
try:
await self.browser.close()
except Exception as e:
# 忽略连接关闭错误(正常关闭场景)
if "Connection closed" not in str(e):
debug_logger.log_warning(f"[BrowserCaptcha] 关闭浏览器时出现异常: {str(e)}")
finally:
self.browser = None
if self.playwright:
try:
await self.playwright.stop()
except Exception:
pass # 静默处理 playwright 停止异常
finally:
self.playwright = None
self._initialized = False
debug_logger.log_info("[BrowserCaptcha] 浏览器已关闭")
except Exception as e:
debug_logger.log_error(f"[BrowserCaptcha] 关闭浏览器异常: {str(e)}")

View File

@@ -308,10 +308,17 @@ class FlowClient:
"""
url = f"{self.api_base_url}/projects/{project_id}/flowMedia:batchGenerateImages"
# 获取 reCAPTCHA token
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
session_id = self._generate_session_id()
# 构建请求
request_data = {
"clientContext": {
"sessionId": self._generate_session_id()
"recaptchaToken": recaptcha_token,
"projectId": project_id,
"sessionId": session_id,
"tool": "PINHOLE"
},
"seed": random.randint(1, 99999),
"imageModelName": model_name,
@@ -321,6 +328,10 @@ class FlowClient:
}
json_data = {
"clientContext": {
"recaptchaToken": recaptcha_token,
"sessionId": session_id
},
"requests": [request_data]
}
@@ -367,11 +378,15 @@ class FlowClient:
"""
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoText"
# 获取 reCAPTCHA token
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
session_id = self._generate_session_id()
scene_id = str(uuid.uuid4())
json_data = {
"clientContext": {
"sessionId": self._generate_session_id(),
"recaptchaToken": recaptcha_token,
"sessionId": session_id,
"projectId": project_id,
"tool": "PINHOLE",
"userPaygateTier": user_paygate_tier
@@ -425,11 +440,15 @@ class FlowClient:
"""
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoReferenceImages"
# 获取 reCAPTCHA token
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
session_id = self._generate_session_id()
scene_id = str(uuid.uuid4())
json_data = {
"clientContext": {
"sessionId": self._generate_session_id(),
"recaptchaToken": recaptcha_token,
"sessionId": session_id,
"projectId": project_id,
"tool": "PINHOLE",
"userPaygateTier": user_paygate_tier
@@ -486,11 +505,15 @@ class FlowClient:
"""
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
# 获取 reCAPTCHA token
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
session_id = self._generate_session_id()
scene_id = str(uuid.uuid4())
json_data = {
"clientContext": {
"sessionId": self._generate_session_id(),
"recaptchaToken": recaptcha_token,
"sessionId": session_id,
"projectId": project_id,
"tool": "PINHOLE",
"userPaygateTier": user_paygate_tier
@@ -550,11 +573,15 @@ class FlowClient:
"""
url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
# 获取 reCAPTCHA token
recaptcha_token = await self._get_recaptcha_token(project_id) or ""
session_id = self._generate_session_id()
scene_id = str(uuid.uuid4())
json_data = {
"clientContext": {
"sessionId": self._generate_session_id(),
"recaptchaToken": recaptcha_token,
"sessionId": session_id,
"projectId": project_id,
"tool": "PINHOLE",
"userPaygateTier": user_paygate_tier
@@ -655,3 +682,75 @@ class FlowClient:
def _generate_scene_id(self) -> str:
"""生成sceneId: UUID"""
return str(uuid.uuid4())
async def _get_recaptcha_token(self, project_id: str) -> Optional[str]:
"""获取reCAPTCHA token - 支持两种方式"""
captcha_method = config.captcha_method
# 浏览器打码
if captcha_method == "browser":
try:
from .browser_captcha import BrowserCaptchaService
service = await BrowserCaptchaService.get_instance(self.proxy_manager)
return await service.get_token(project_id)
except Exception as e:
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
return None
# YesCaptcha打码
client_key = config.yescaptcha_api_key
if not client_key:
debug_logger.log_info("[reCAPTCHA] API key not configured, skipping")
return None
website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
base_url = config.yescaptcha_base_url
page_action = "FLOW_GENERATION"
try:
async with AsyncSession() as session:
create_url = f"{base_url}/createTask"
create_data = {
"clientKey": client_key,
"task": {
"websiteURL": website_url,
"websiteKey": website_key,
"type": "RecaptchaV3TaskProxylessM1",
"pageAction": page_action
}
}
result = await session.post(create_url, json=create_data, impersonate="chrome110")
result_json = result.json()
task_id = result_json.get('taskId')
debug_logger.log_info(f"[reCAPTCHA] created task_id: {task_id}")
if not task_id:
return None
get_url = f"{base_url}/getTaskResult"
for i in range(40):
get_data = {
"clientKey": client_key,
"taskId": task_id
}
result = await session.post(get_url, json=get_data, impersonate="chrome110")
result_json = result.json()
debug_logger.log_info(f"[reCAPTCHA] polling #{i+1}: {result_json}")
solution = result_json.get('solution', {})
response = solution.get('gRecaptchaResponse')
if response:
return response
time.sleep(3)
return None
except Exception as e:
debug_logger.log_error(f"[reCAPTCHA] error: {str(e)}")
return None