feat(browser): replace playwright with nodriver for captcha service
Browse files- Add nodriver dependency for advanced browser automation
- Replace BrowserCaptchaService implementation with nodriver-based solution
- Remove headless environment detection logic as nodriver handles this natively
- Update browser captcha service to use undetected-chromedriver successor
- Maintain persistent browser profiles for login state preservation
fix(models): update video model keys for portrait and landscape variants
- Change veo_3_1_i2v_s_fast_fl to veo_3_1_i2v_s_fast_portrait_fl_ultra_relaxed
- Change veo_3_1_i2v_s_fast_fl to veo_3_1_i2v_s_fast_landscape_fl_ultra_relaxed
- Ensure proper aspect ratio configuration for video generation models
- requirements.txt +1 -0
- src/main.py +4 -31
- src/services/browser_captcha_personal.py +213 -122
- src/services/generation_handler.py +2 -2
requirements.txt
CHANGED
|
@@ -8,3 +8,4 @@ bcrypt==4.2.1
|
|
| 8 |
python-multipart==0.0.20
|
| 9 |
python-dateutil==2.8.2
|
| 10 |
playwright==1.53.0
|
|
|
|
|
|
| 8 |
python-multipart==0.0.20
|
| 9 |
python-dateutil==2.8.2
|
| 10 |
playwright==1.53.0
|
| 11 |
+
nodriver>=0.48.0
|
src/main.py
CHANGED
|
@@ -69,45 +69,18 @@ async def lifespan(app: FastAPI):
|
|
| 69 |
# Load captcha configuration from database
|
| 70 |
captcha_config = await db.get_captcha_config()
|
| 71 |
|
| 72 |
-
|
| 73 |
-
def is_headless_environment() -> bool:
|
| 74 |
-
"""Check if running in a headless environment (Docker, no display, etc.)"""
|
| 75 |
-
import os
|
| 76 |
-
# Check for DISPLAY environment variable (X11)
|
| 77 |
-
if not os.environ.get("DISPLAY"):
|
| 78 |
-
# Check if running in Docker
|
| 79 |
-
if os.path.exists("/.dockerenv") or os.environ.get("DOCKER_CONTAINER"):
|
| 80 |
-
return True
|
| 81 |
-
# Check for common CI/container indicators
|
| 82 |
-
if os.environ.get("CI") or os.environ.get("KUBERNETES_SERVICE_HOST"):
|
| 83 |
-
return True
|
| 84 |
-
# No DISPLAY and not explicitly local
|
| 85 |
-
return True
|
| 86 |
-
return False
|
| 87 |
-
|
| 88 |
-
# Determine effective captcha method
|
| 89 |
-
effective_captcha_method = captcha_config.captcha_method
|
| 90 |
-
|
| 91 |
-
# Auto-downgrade personal mode to browser mode in headless environments
|
| 92 |
-
if captcha_config.captcha_method == "personal" and is_headless_environment():
|
| 93 |
-
print("⚠️ WARNING: 'personal' captcha mode requires a display (X Server).")
|
| 94 |
-
print(" Detected headless environment (Docker/No Display).")
|
| 95 |
-
print(" Auto-switching to 'browser' (headless) mode.")
|
| 96 |
-
print(" To use 'personal' mode, run Flow2API on a machine with a display.")
|
| 97 |
-
effective_captcha_method = "browser"
|
| 98 |
-
|
| 99 |
-
config.set_captcha_method(effective_captcha_method)
|
| 100 |
config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
|
| 101 |
config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
|
| 102 |
|
| 103 |
# Initialize browser captcha service if needed
|
| 104 |
browser_service = None
|
| 105 |
-
if
|
| 106 |
from .services.browser_captcha_personal import BrowserCaptchaService
|
| 107 |
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 108 |
await browser_service.open_login_window()
|
| 109 |
-
print("✓ Browser captcha service initialized (
|
| 110 |
-
elif
|
| 111 |
from .services.browser_captcha import BrowserCaptchaService
|
| 112 |
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 113 |
print("✓ Browser captcha service initialized (headless mode)")
|
|
|
|
| 69 |
# Load captcha configuration from database
|
| 70 |
captcha_config = await db.get_captcha_config()
|
| 71 |
|
| 72 |
+
config.set_captcha_method(captcha_config.captcha_method)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
|
| 74 |
config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
|
| 75 |
|
| 76 |
# Initialize browser captcha service if needed
|
| 77 |
browser_service = None
|
| 78 |
+
if captcha_config.captcha_method == "personal":
|
| 79 |
from .services.browser_captcha_personal import BrowserCaptchaService
|
| 80 |
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 81 |
await browser_service.open_login_window()
|
| 82 |
+
print("✓ Browser captcha service initialized (nodriver mode)")
|
| 83 |
+
elif captcha_config.captcha_method == "browser":
|
| 84 |
from .services.browser_captcha import BrowserCaptchaService
|
| 85 |
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 86 |
print("✓ Browser captcha service initialized (headless mode)")
|
src/services/browser_captcha_personal.py
CHANGED
|
@@ -1,197 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import asyncio
|
| 2 |
import time
|
| 3 |
-
import re
|
| 4 |
import os
|
| 5 |
-
from typing import Optional
|
| 6 |
-
|
|
|
|
| 7 |
|
| 8 |
from ..core.logger import debug_logger
|
| 9 |
|
| 10 |
-
# ... (保持原来的 parse_proxy_url 和 validate_browser_proxy_url 函数不变) ...
|
| 11 |
-
def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]:
|
| 12 |
-
"""解析代理URL,分离协议、主机、端口、认证信息"""
|
| 13 |
-
proxy_pattern = r'^(socks5|http|https)://(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$'
|
| 14 |
-
match = re.match(proxy_pattern, proxy_url)
|
| 15 |
-
if match:
|
| 16 |
-
protocol, username, password, host, port = match.groups()
|
| 17 |
-
proxy_config = {'server': f'{protocol}://{host}:{port}'}
|
| 18 |
-
if username and password:
|
| 19 |
-
proxy_config['username'] = username
|
| 20 |
-
proxy_config['password'] = password
|
| 21 |
-
return proxy_config
|
| 22 |
-
return None
|
| 23 |
|
| 24 |
class BrowserCaptchaService:
|
| 25 |
-
"""浏览器自动化获取 reCAPTCHA token(
|
| 26 |
|
| 27 |
_instance: Optional['BrowserCaptchaService'] = None
|
| 28 |
_lock = asyncio.Lock()
|
| 29 |
|
| 30 |
def __init__(self, db=None):
|
| 31 |
"""初始化服务"""
|
| 32 |
-
|
| 33 |
-
self.
|
| 34 |
-
self.playwright = None
|
| 35 |
-
# 注意: 持久化模式下,我们操作的是 context 而不是 browser
|
| 36 |
-
self.context: Optional[BrowserContext] = None
|
| 37 |
self._initialized = False
|
| 38 |
self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 39 |
self.db = db
|
| 40 |
-
|
| 41 |
-
# === 修改点 2: 指定本地数据存储目录 ===
|
| 42 |
-
# 这会在脚本运行目录下生成 browser_data 文件夹,用于保存你的登录状态
|
| 43 |
self.user_data_dir = os.path.join(os.getcwd(), "browser_data")
|
| 44 |
|
| 45 |
@classmethod
|
| 46 |
async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
|
|
|
|
| 47 |
if cls._instance is None:
|
| 48 |
async with cls._lock:
|
| 49 |
if cls._instance is None:
|
| 50 |
cls._instance = cls(db)
|
| 51 |
-
# 首次调用不强制初始化,等待 get_token 时懒加载,或者可以在这里await
|
| 52 |
return cls._instance
|
| 53 |
|
| 54 |
async def initialize(self):
|
| 55 |
-
"""初始化
|
| 56 |
-
if self._initialized and self.
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
try:
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
launch_options = {
|
| 71 |
-
'headless': self.headless,
|
| 72 |
-
'user_data_dir': self.user_data_dir, # 指定数据目录
|
| 73 |
-
'viewport': {'width': 1280, 'height': 720}, # 设置默认窗口大小
|
| 74 |
-
'args': [
|
| 75 |
-
'--disable-blink-features=AutomationControlled',
|
| 76 |
-
'--disable-infobars',
|
| 77 |
'--no-sandbox',
|
|
|
|
| 78 |
'--disable-setuid-sandbox',
|
|
|
|
|
|
|
| 79 |
]
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
# 代理配置
|
| 83 |
-
if proxy_url:
|
| 84 |
-
proxy_config = parse_proxy_url(proxy_url)
|
| 85 |
-
if proxy_config:
|
| 86 |
-
launch_options['proxy'] = proxy_config
|
| 87 |
-
debug_logger.log_info(f"[BrowserCaptcha] 使用代理: {proxy_config['server']}")
|
| 88 |
-
|
| 89 |
-
# === 修改点 3: 使用 launch_persistent_context ===
|
| 90 |
-
# 这会启动一个带有状态的浏览器窗口
|
| 91 |
-
self.context = await self.playwright.chromium.launch_persistent_context(**launch_options)
|
| 92 |
-
|
| 93 |
-
# 设置默认超时
|
| 94 |
-
self.context.set_default_timeout(30000)
|
| 95 |
|
| 96 |
self._initialized = True
|
| 97 |
-
debug_logger.log_info(f"[BrowserCaptcha] ✅ 浏览器已启动 (Profile: {self.user_data_dir})")
|
| 98 |
-
|
| 99 |
except Exception as e:
|
| 100 |
debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
|
| 101 |
raise
|
| 102 |
|
| 103 |
async def get_token(self, project_id: str) -> Optional[str]:
|
| 104 |
-
"""获取 reCAPTCHA token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
# 确保浏览器已启动
|
| 106 |
-
if not self._initialized or not self.
|
| 107 |
await self.initialize()
|
| 108 |
|
| 109 |
start_time = time.time()
|
| 110 |
-
|
| 111 |
|
| 112 |
try:
|
| 113 |
-
# === 修改点 4: 在现有上下文中新建标签页,而不是新建上下文 ===
|
| 114 |
-
# 这样可以复用该上下文中已保存的 Cookie (你的登录状态)
|
| 115 |
-
page = await self.context.new_page()
|
| 116 |
-
|
| 117 |
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 118 |
debug_logger.log_info(f"[BrowserCaptcha] 访问页面: {website_url}")
|
| 119 |
|
| 120 |
-
# 访问页面
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
#
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
|
| 132 |
-
# ... 请将原代码中从 "检查并注入 reCAPTCHA v3 脚本" 到 token 获取部分的代码复制到这里 ...
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
const script = document.createElement('script');
|
| 140 |
script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
|
| 141 |
-
script.async = true;
|
| 142 |
document.head.appendChild(script);
|
| 143 |
-
}}
|
| 144 |
""")
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
if token:
|
| 158 |
-
debug_logger.log_info(f"[BrowserCaptcha] ✅ Token获取成功")
|
| 159 |
return token
|
| 160 |
else:
|
| 161 |
-
debug_logger.log_error("[BrowserCaptcha] Token获取失败")
|
| 162 |
return None
|
| 163 |
|
| 164 |
except Exception as e:
|
| 165 |
-
debug_logger.log_error(f"[BrowserCaptcha] 异常: {str(e)}")
|
| 166 |
return None
|
| 167 |
finally:
|
| 168 |
-
#
|
| 169 |
-
if
|
| 170 |
try:
|
| 171 |
-
await
|
| 172 |
-
except:
|
| 173 |
pass
|
| 174 |
|
| 175 |
async def close(self):
|
| 176 |
-
"""
|
| 177 |
try:
|
| 178 |
-
if self.
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
self._initialized = False
|
| 187 |
-
debug_logger.log_info("[BrowserCaptcha] 浏览器
|
| 188 |
except Exception as e:
|
| 189 |
-
debug_logger.log_error(f"[BrowserCaptcha] 关闭异常: {str(e)}")
|
| 190 |
|
| 191 |
-
# 增加一个辅助方法,用于手动登录
|
| 192 |
async def open_login_window(self):
|
| 193 |
-
"""
|
| 194 |
await self.initialize()
|
| 195 |
-
|
| 196 |
-
|
| 197 |
print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
浏览器自动化获取 reCAPTCHA token
|
| 3 |
+
使用 nodriver (undetected-chromedriver 继任者) 实现反检测浏览器
|
| 4 |
+
"""
|
| 5 |
import asyncio
|
| 6 |
import time
|
|
|
|
| 7 |
import os
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
import nodriver as uc
|
| 11 |
|
| 12 |
from ..core.logger import debug_logger
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
class BrowserCaptchaService:
|
| 16 |
+
"""浏览器自动化获取 reCAPTCHA token(nodriver 有头模式)"""
|
| 17 |
|
| 18 |
_instance: Optional['BrowserCaptchaService'] = None
|
| 19 |
_lock = asyncio.Lock()
|
| 20 |
|
| 21 |
def __init__(self, db=None):
|
| 22 |
"""初始化服务"""
|
| 23 |
+
self.headless = False # nodriver 有头模式
|
| 24 |
+
self.browser = None
|
|
|
|
|
|
|
|
|
|
| 25 |
self._initialized = False
|
| 26 |
self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 27 |
self.db = db
|
| 28 |
+
# 持久化 profile 目录
|
|
|
|
|
|
|
| 29 |
self.user_data_dir = os.path.join(os.getcwd(), "browser_data")
|
| 30 |
|
| 31 |
@classmethod
|
| 32 |
async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
|
| 33 |
+
"""获取单例实例"""
|
| 34 |
if cls._instance is None:
|
| 35 |
async with cls._lock:
|
| 36 |
if cls._instance is None:
|
| 37 |
cls._instance = cls(db)
|
|
|
|
| 38 |
return cls._instance
|
| 39 |
|
| 40 |
async def initialize(self):
|
| 41 |
+
"""初始化 nodriver 浏览器"""
|
| 42 |
+
if self._initialized and self.browser:
|
| 43 |
+
# 检查浏览器是否仍然存活
|
| 44 |
+
try:
|
| 45 |
+
# 尝试获取浏览器信息验证存活
|
| 46 |
+
if self.browser.stopped:
|
| 47 |
+
debug_logger.log_warning("[BrowserCaptcha] 浏览器已停止,重新初始化...")
|
| 48 |
+
self._initialized = False
|
| 49 |
+
else:
|
| 50 |
+
return
|
| 51 |
+
except Exception:
|
| 52 |
+
debug_logger.log_warning("[BrowserCaptcha] 浏览器无响应,重新初始化...")
|
| 53 |
+
self._initialized = False
|
| 54 |
|
| 55 |
try:
|
| 56 |
+
debug_logger.log_info(f"[BrowserCaptcha] 正在启动 nodriver 浏览器 (用户数据目录: {self.user_data_dir})...")
|
| 57 |
+
|
| 58 |
+
# 确保 user_data_dir 存在
|
| 59 |
+
os.makedirs(self.user_data_dir, exist_ok=True)
|
| 60 |
+
|
| 61 |
+
# 启动 nodriver 浏览器
|
| 62 |
+
self.browser = await uc.start(
|
| 63 |
+
headless=self.headless,
|
| 64 |
+
user_data_dir=self.user_data_dir,
|
| 65 |
+
browser_args=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
'--no-sandbox',
|
| 67 |
+
'--disable-dev-shm-usage',
|
| 68 |
'--disable-setuid-sandbox',
|
| 69 |
+
'--disable-gpu',
|
| 70 |
+
'--window-size=1280,720',
|
| 71 |
]
|
| 72 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
self._initialized = True
|
| 75 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ nodriver 浏览器已启动 (Profile: {self.user_data_dir})")
|
| 76 |
+
|
| 77 |
except Exception as e:
|
| 78 |
debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
|
| 79 |
raise
|
| 80 |
|
| 81 |
async def get_token(self, project_id: str) -> Optional[str]:
|
| 82 |
+
"""获取 reCAPTCHA token
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
project_id: Flow项目ID
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
reCAPTCHA token字符串,如果获取失败返回None
|
| 89 |
+
"""
|
| 90 |
# 确保浏览器已启动
|
| 91 |
+
if not self._initialized or not self.browser:
|
| 92 |
await self.initialize()
|
| 93 |
|
| 94 |
start_time = time.time()
|
| 95 |
+
tab = None
|
| 96 |
|
| 97 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 99 |
debug_logger.log_info(f"[BrowserCaptcha] 访问页面: {website_url}")
|
| 100 |
|
| 101 |
+
# 新建标签页并访问页面
|
| 102 |
+
tab = await self.browser.get(website_url)
|
| 103 |
+
|
| 104 |
+
# 等待页面完全加载(增加等待时间)
|
| 105 |
+
debug_logger.log_info("[BrowserCaptcha] 等待页面加载...")
|
| 106 |
+
await tab.sleep(3)
|
| 107 |
+
|
| 108 |
+
# 等待页面 DOM 完成
|
| 109 |
+
for _ in range(10):
|
| 110 |
+
ready_state = await tab.evaluate("document.readyState")
|
| 111 |
+
if ready_state == "complete":
|
| 112 |
+
break
|
| 113 |
+
await tab.sleep(0.5)
|
| 114 |
+
|
| 115 |
+
# 检测 reCAPTCHA 是否已加载
|
| 116 |
+
debug_logger.log_info("[BrowserCaptcha] 检测 reCAPTCHA...")
|
| 117 |
+
|
| 118 |
+
# 页面使用的是 reCAPTCHA Enterprise,检查 grecaptcha.enterprise.execute
|
| 119 |
+
is_enterprise = await tab.evaluate(
|
| 120 |
+
"typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
|
| 121 |
+
)
|
| 122 |
|
| 123 |
+
debug_logger.log_info(f"[BrowserCaptcha] 检测结果: is_enterprise={is_enterprise}")
|
|
|
|
| 124 |
|
| 125 |
+
recaptcha_type = "enterprise" if is_enterprise else None
|
| 126 |
+
|
| 127 |
+
# 如果没有检测到 reCAPTCHA,尝试注入脚本
|
| 128 |
+
if not recaptcha_type:
|
| 129 |
+
debug_logger.log_info("[BrowserCaptcha] 未检测到 reCAPTCHA,注入脚本...")
|
| 130 |
+
|
| 131 |
+
# 注入标准版 reCAPTCHA 脚本
|
| 132 |
+
await tab.evaluate(f"""
|
| 133 |
+
(() => {{
|
| 134 |
+
if (document.querySelector('script[src*="recaptcha"]')) return;
|
| 135 |
const script = document.createElement('script');
|
| 136 |
script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
|
| 137 |
+
script.async = true;
|
| 138 |
document.head.appendChild(script);
|
| 139 |
+
}})()
|
| 140 |
""")
|
| 141 |
+
|
| 142 |
+
# 等待脚本加载
|
| 143 |
+
await tab.sleep(3)
|
| 144 |
+
|
| 145 |
+
# 轮询等待 reCAPTCHA 加载
|
| 146 |
+
for i in range(20):
|
| 147 |
+
is_enterprise = await tab.evaluate(
|
| 148 |
+
"typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
if is_enterprise:
|
| 152 |
+
recaptcha_type = "enterprise"
|
| 153 |
+
debug_logger.log_info(f"[BrowserCaptcha] reCAPTCHA Enterprise 已加载(等待了 {i * 0.5} 秒)")
|
| 154 |
+
break
|
| 155 |
+
await tab.sleep(0.5)
|
| 156 |
+
else:
|
| 157 |
+
debug_logger.log_warning("[BrowserCaptcha] reCAPTCHA 加载超时")
|
| 158 |
+
|
| 159 |
+
if not recaptcha_type:
|
| 160 |
+
debug_logger.log_error("[BrowserCaptcha] reCAPTCHA 无法加载")
|
| 161 |
+
return None
|
| 162 |
+
|
| 163 |
+
# 执行 reCAPTCHA 并获取 token(使用 window 变量传递异步结果)
|
| 164 |
+
debug_logger.log_info(f"[BrowserCaptcha] 执行 reCAPTCHA 验证 (类型: {recaptcha_type})...")
|
| 165 |
+
|
| 166 |
+
# 生成唯一变量名避免冲突
|
| 167 |
+
ts = int(time.time() * 1000)
|
| 168 |
+
token_var = f"_recaptcha_token_{ts}"
|
| 169 |
+
error_var = f"_recaptcha_error_{ts}"
|
| 170 |
+
|
| 171 |
+
# 根据类型选择正确的 API
|
| 172 |
+
if recaptcha_type == "enterprise":
|
| 173 |
+
execute_script = f"""
|
| 174 |
+
(() => {{
|
| 175 |
+
window.{token_var} = null;
|
| 176 |
+
window.{error_var} = null;
|
| 177 |
+
|
| 178 |
+
try {{
|
| 179 |
+
grecaptcha.enterprise.ready(function() {{
|
| 180 |
+
grecaptcha.enterprise.execute('{self.website_key}', {{action: 'FLOW_GENERATION'}})
|
| 181 |
+
.then(function(token) {{
|
| 182 |
+
window.{token_var} = token;
|
| 183 |
+
}})
|
| 184 |
+
.catch(function(err) {{
|
| 185 |
+
window.{error_var} = err.message || 'execute failed';
|
| 186 |
+
}});
|
| 187 |
+
}});
|
| 188 |
+
}} catch (e) {{
|
| 189 |
+
window.{error_var} = e.message || 'exception';
|
| 190 |
+
}}
|
| 191 |
+
}})()
|
| 192 |
+
"""
|
| 193 |
+
else:
|
| 194 |
+
execute_script = f"""
|
| 195 |
+
(() => {{
|
| 196 |
+
window.{token_var} = null;
|
| 197 |
+
window.{error_var} = null;
|
| 198 |
+
|
| 199 |
+
try {{
|
| 200 |
+
if (grecaptcha.ready) {{
|
| 201 |
+
grecaptcha.ready(function() {{
|
| 202 |
+
grecaptcha.execute('{self.website_key}', {{action: 'FLOW_GENERATION'}})
|
| 203 |
+
.then(function(token) {{
|
| 204 |
+
window.{token_var} = token;
|
| 205 |
+
}})
|
| 206 |
+
.catch(function(err) {{
|
| 207 |
+
window.{error_var} = err.message || 'execute failed';
|
| 208 |
+
}});
|
| 209 |
+
}});
|
| 210 |
+
}} else {{
|
| 211 |
+
grecaptcha.execute('{self.website_key}', {{action: 'FLOW_GENERATION'}})
|
| 212 |
+
.then(function(token) {{
|
| 213 |
+
window.{token_var} = token;
|
| 214 |
+
}})
|
| 215 |
+
.catch(function(err) {{
|
| 216 |
+
window.{error_var} = err.message || 'execute failed';
|
| 217 |
+
}});
|
| 218 |
+
}}
|
| 219 |
+
}} catch (e) {{
|
| 220 |
+
window.{error_var} = e.message || 'exception';
|
| 221 |
+
}}
|
| 222 |
+
}})()
|
| 223 |
+
"""
|
| 224 |
+
|
| 225 |
+
# 注入执行脚本
|
| 226 |
+
await tab.evaluate(execute_script)
|
| 227 |
|
| 228 |
+
# 轮询等待结果(最多 15 秒)
|
| 229 |
+
token = None
|
| 230 |
+
for i in range(30):
|
| 231 |
+
await tab.sleep(0.5)
|
| 232 |
+
token = await tab.evaluate(f"window.{token_var}")
|
| 233 |
+
if token:
|
| 234 |
+
debug_logger.log_info(f"[BrowserCaptcha] Token 已获取(等待了 {i * 0.5} 秒)")
|
| 235 |
+
break
|
| 236 |
+
error = await tab.evaluate(f"window.{error_var}")
|
| 237 |
+
if error:
|
| 238 |
+
debug_logger.log_error(f"[BrowserCaptcha] reCAPTCHA 错误: {error}")
|
| 239 |
+
break
|
| 240 |
+
|
| 241 |
+
# 清理临时变量
|
| 242 |
+
try:
|
| 243 |
+
await tab.evaluate(f"delete window.{token_var}; delete window.{error_var};")
|
| 244 |
+
except:
|
| 245 |
+
pass
|
| 246 |
+
|
| 247 |
+
duration_ms = (time.time() - start_time) * 1000
|
| 248 |
+
|
| 249 |
if token:
|
| 250 |
+
debug_logger.log_info(f"[BrowserCaptcha] ✅ Token获取成功(耗时 {duration_ms:.0f}ms)")
|
| 251 |
return token
|
| 252 |
else:
|
| 253 |
+
debug_logger.log_error("[BrowserCaptcha] Token获取失败(返回null)")
|
| 254 |
return None
|
| 255 |
|
| 256 |
except Exception as e:
|
| 257 |
+
debug_logger.log_error(f"[BrowserCaptcha] 获取token异常: {str(e)}")
|
| 258 |
return None
|
| 259 |
finally:
|
| 260 |
+
# 关闭标签页(但保留浏览器)
|
| 261 |
+
if tab:
|
| 262 |
try:
|
| 263 |
+
await tab.close()
|
| 264 |
+
except Exception:
|
| 265 |
pass
|
| 266 |
|
| 267 |
async def close(self):
|
| 268 |
+
"""关闭浏览器"""
|
| 269 |
try:
|
| 270 |
+
if self.browser:
|
| 271 |
+
try:
|
| 272 |
+
self.browser.stop()
|
| 273 |
+
except Exception as e:
|
| 274 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 关闭浏览器时出现异常: {str(e)}")
|
| 275 |
+
finally:
|
| 276 |
+
self.browser = None
|
| 277 |
+
|
| 278 |
self._initialized = False
|
| 279 |
+
debug_logger.log_info("[BrowserCaptcha] 浏览器已关闭")
|
| 280 |
except Exception as e:
|
| 281 |
+
debug_logger.log_error(f"[BrowserCaptcha] 关闭浏览器异常: {str(e)}")
|
| 282 |
|
|
|
|
| 283 |
async def open_login_window(self):
|
| 284 |
+
"""打开登录窗口供用户手动登录 Google"""
|
| 285 |
await self.initialize()
|
| 286 |
+
tab = await self.browser.get("https://accounts.google.com/")
|
| 287 |
+
debug_logger.log_info("[BrowserCaptcha] 请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
|
| 288 |
print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
|
src/services/generation_handler.py
CHANGED
|
@@ -109,7 +109,7 @@ MODEL_CONFIG = {
|
|
| 109 |
"veo_3_1_i2v_s_fast_fl_portrait": {
|
| 110 |
"type": "video",
|
| 111 |
"video_type": "i2v",
|
| 112 |
-
"model_key": "
|
| 113 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
|
| 114 |
"supports_images": True,
|
| 115 |
"min_images": 1,
|
|
@@ -118,7 +118,7 @@ MODEL_CONFIG = {
|
|
| 118 |
"veo_3_1_i2v_s_fast_fl_landscape": {
|
| 119 |
"type": "video",
|
| 120 |
"video_type": "i2v",
|
| 121 |
-
"model_key": "
|
| 122 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
|
| 123 |
"supports_images": True,
|
| 124 |
"min_images": 1,
|
|
|
|
| 109 |
"veo_3_1_i2v_s_fast_fl_portrait": {
|
| 110 |
"type": "video",
|
| 111 |
"video_type": "i2v",
|
| 112 |
+
"model_key": "veo_3_1_i2v_s_fast_portrait_fl_ultra_relaxed",
|
| 113 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
|
| 114 |
"supports_images": True,
|
| 115 |
"min_images": 1,
|
|
|
|
| 118 |
"veo_3_1_i2v_s_fast_fl_landscape": {
|
| 119 |
"type": "video",
|
| 120 |
"video_type": "i2v",
|
| 121 |
+
"model_key": "veo_3_1_i2v_s_fast_landscape_fl_ultra_relaxed",
|
| 122 |
"aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
|
| 123 |
"supports_images": True,
|
| 124 |
"min_images": 1,
|