mirror of
https://github.com/TheSmallHanCat/flow2api.git
synced 2026-05-07 22:43:16 +08:00
feat: 无头打码与YesCaptcha打码
This commit is contained in:
23
Dockerfile
23
Dockerfile
@@ -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
|
||||
|
||||
11
README.md
11
README.md
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"""
|
||||
|
||||
17
src/main.py
17
src/main.py
@@ -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")
|
||||
|
||||
|
||||
245
src/services/browser_captcha.py
Normal file
245
src/services/browser_captcha.py
Normal 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)}")
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user