diff --git a/Dockerfile b/Dockerfile index d340750..667e535 100644 --- a/Dockerfile +++ b/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 diff --git a/README.md b/README.md index 213faca..1d4e156 100644 --- a/README.md +++ b/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) --- diff --git a/config/setting.toml b/config/setting.toml index 87eb66d..051cfd3 100644 --- a/config/setting.toml +++ b/config/setting.toml @@ -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" diff --git a/config/setting_warp.toml b/config/setting_warp.toml index ec6c2ac..ac58190 100644 --- a/config/setting_warp.toml +++ b/config/setting_warp.toml @@ -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" diff --git a/requirements.txt b/requirements.txt index 77dbbf7..3d652c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/src/api/admin.py b/src/api/admin.py index b47faaf..a0e4100 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -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 } diff --git a/src/core/config.py b/src/core/config.py index bc85058..3437de5 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -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() diff --git a/src/core/database.py b/src/core/database.py index be21585..ad5eab5 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -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() diff --git a/src/core/models.py b/src/core/models.py index e6c36b8..2c1f4c5 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -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""" diff --git a/src/main.py b/src/main.py index 7d7b329..9e79eba 100644 --- a/src/main.py +++ b/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") diff --git a/src/services/browser_captcha.py b/src/services/browser_captcha.py new file mode 100644 index 0000000..4f552a5 --- /dev/null +++ b/src/services/browser_captcha.py @@ -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)}") diff --git a/src/services/flow_client.py b/src/services/flow_client.py index cb70f4c..f5d6748 100644 --- a/src/services/flow_client.py +++ b/src/services/flow_client.py @@ -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