From 3cb9e0405cfb4d50e79201eb0d4367eb5db1542d Mon Sep 17 00:00:00 2001 From: dantynoel Date: Thu, 18 Dec 2025 16:43:50 +0800 Subject: [PATCH] =?UTF-8?q?fork:=20=E4=B8=AA=E4=BA=BA=E7=94=A8=E9=80=94?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E5=9C=A8=E5=86=85=E7=BD=AE=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=E4=B8=8A=E7=99=BB=E5=BD=95=E5=AF=B9=E5=BA=94=E7=9A=84?= =?UTF-8?q?=E8=B0=B7=E6=AD=8C=E8=B4=A6=E5=8F=B7=EF=BC=8C=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=8F=AF=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + config/setting.toml | 2 +- request.py | 150 +++++++++++++++++ src/main.py | 7 +- src/services/browser_captcha_personal.py | 197 +++++++++++++++++++++++ src/services/flow_client.py | 122 +++++++------- 6 files changed, 421 insertions(+), 59 deletions(-) create mode 100644 request.py create mode 100644 src/services/browser_captcha_personal.py diff --git a/.gitignore b/.gitignore index 710398a..6b0d2df 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ logs.txt *.tmp *.bak *.cache + +browser_data diff --git a/config/setting.toml b/config/setting.toml index 051cfd3..fe48545 100644 --- a/config/setting.toml +++ b/config/setting.toml @@ -12,7 +12,7 @@ max_poll_attempts = 200 [server] host = "0.0.0.0" -port = 8000 +port = 8106 [debug] enabled = false diff --git a/request.py b/request.py new file mode 100644 index 0000000..80de579 --- /dev/null +++ b/request.py @@ -0,0 +1,150 @@ +import os +import json +import re +import base64 +import aiohttp # Async test. Need to install +import asyncio + + +# --- 配置区域 --- +BASE_URL = os.getenv('GEMINI_FLOW2API_URL', 'http://127.0.0.1:8106') +BACKEND_URL = BASE_URL + "/v1/chat/completions" +API_KEY = os.getenv('GEMINI_FLOW2API_APIKEY', 'Bearer han1234') +if API_KEY is None: + raise ValueError('[gemini flow2api] api key not set') +MODEL_LANDSCAPE = "gemini-3.0-pro-image-landscape" +MODEL_PORTRAIT = "gemini-3.0-pro-image-portrait" + +# 修改: 增加 model 参数,默认为 None +async def request_backend_generation( + prompt: str, + images: list[bytes] = None, + model: str = None) -> bytes | None: + """ + 请求后端生成图片。 + :param prompt: 提示词 + :param images: 图片二进制列表 + :param model: 指定模型名称 (可选) + :return: 成功返回图片bytes,失败返回None + """ + # 更新token + images = images or [] + + # 逻辑: 如果未指定 model,默认使用 Landscape + use_model = model if model else MODEL_LANDSCAPE + + # 1. 构造 Payload + if images: + content_payload = [{"type": "text", "text": prompt}] + print(f"[Backend] 正在处理 {len(images)} 张图片输入...") + for img_bytes in images: + b64_str = base64.b64encode(img_bytes).decode('utf-8') + content_payload.append({ + "type": "image_url", + "image_url": {"url": f"data:image/jpeg;base64,{b64_str}"} + }) + else: + content_payload = prompt + + payload = { + "model": use_model, # 使用选定的模型 + "messages": [{"role": "user", "content": content_payload}], + "stream": True + } + + headers = { + "Authorization": API_KEY, + "Content-Type": "application/json" + } + + image_url = None + print(f"[Backend] Model: {use_model} | 发起请求: {prompt[:20]}...") + + try: + async with aiohttp.ClientSession() as session: + async with session.post(BACKEND_URL, json=payload, headers=headers, timeout=120) as response: + if response.status != 200: + err_text = await response.text() + content = response.content + print(f"[Backend Error] Status {response.status}: {err_text} {content}") + raise Exception(f"API Error: {response.status}: {err_text}") + + async for line in response.content: + line_str = line.decode('utf-8').strip() + if line_str.startswith('{"error'): + chunk = json.loads(data_str) + delta = chunk.get("choices", [{}])[0].get("delta", {}) + msg = delta['reasoning_content'] + if '401' in msg: + msg += '\nAccess Token 已失效,需重新配置。' + elif '400' in msg: + msg += '\n返回内容被拦截。' + raise Exception(msg) + + if not line_str or not line_str.startswith('data: '): + continue + + data_str = line_str[6:] + if data_str == '[DONE]': + break + + try: + chunk = json.loads(data_str) + delta = chunk.get("choices", [{}])[0].get("delta", {}) + + # 打印思考过程 + if "reasoning_content" in delta: + print(delta['reasoning_content'], end="", flush=True) + + # 提取内容中的图片链接 + if "content" in delta: + content_text = delta["content"] + img_match = re.search(r'!\[.*?\]\((.*?)\)', content_text) + if img_match: + image_url = img_match.group(1) + print(f"\n[Backend] 捕获图片链接: {image_url}") + except json.JSONDecodeError: + continue + + # 3. 下载生成的图片 + if image_url: + async with session.get(image_url) as img_resp: + if img_resp.status == 200: + image_bytes = await img_resp.read() + return image_bytes + else: + print(f"[Backend Error] 图片下载失败: {img_resp.status}") + except Exception as e: + print(f"[Backend Exception] {e}") + raise e + + return None + +if __name__ == '__main__': + async def main(): + print("=== AI 绘图接口测试 ===") + user_prompt = input("请输入提示词 (例如 '一只猫'): ").strip() + if not user_prompt: + user_prompt = "A cute cat in the garden" + + print(f"正在请求: {user_prompt}") + + # 这里的 images 传空列表用于测试文生图 + # 如果想测试图生图,你需要手动读取本地文件: + # with open("output_test.jpg", "rb") as f: img_data = f.read() + # result = await request_backend_generation(user_prompt, [img_data]) + + result = await request_backend_generation(user_prompt) + + if result: + filename = "output_test.jpg" + with open(filename, "wb") as f: + f.write(result) + print(f"\n[Success] 图片已保存为 {filename},大小: {len(result)} bytes") + else: + print("\n[Failed] 生成失败") + + # 运行测试 + if os.name == 'nt': # Windows 兼容性 + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + asyncio.run(main()) \ No newline at end of file diff --git a/src/main.py b/src/main.py index 5705d26..0688623 100644 --- a/src/main.py +++ b/src/main.py @@ -74,7 +74,12 @@ async def lifespan(app: FastAPI): # Initialize browser captcha service if needed browser_service = None - if captcha_config.captcha_method == "browser": + if True: + from .services.browser_captcha_personal import BrowserCaptchaService + browser_service = await BrowserCaptchaService.get_instance(db) + await browser_service.open_login_window() + print("✓ Browser captcha service initialized (webui mode)") + elif captcha_config.captcha_method == "browser": from .services.browser_captcha import BrowserCaptchaService browser_service = await BrowserCaptchaService.get_instance(db) print("✓ Browser captcha service initialized (headless mode)") diff --git a/src/services/browser_captcha_personal.py b/src/services/browser_captcha_personal.py new file mode 100644 index 0000000..d35d626 --- /dev/null +++ b/src/services/browser_captcha_personal.py @@ -0,0 +1,197 @@ +import asyncio +import time +import re +import os +from typing import Optional, Dict +from playwright.async_api import async_playwright, BrowserContext, Page + +from ..core.logger import debug_logger + +# ... (保持原来的 parse_proxy_url 和 validate_browser_proxy_url 函数不变) ... +def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]: + """解析代理URL,分离协议、主机、端口、认证信息""" + proxy_pattern = r'^(socks5|http|https)://(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$' + match = re.match(proxy_pattern, proxy_url) + if match: + protocol, username, password, host, port = match.groups() + proxy_config = {'server': f'{protocol}://{host}:{port}'} + if username and password: + proxy_config['username'] = username + proxy_config['password'] = password + return proxy_config + return None + +class BrowserCaptchaService: + """浏览器自动化获取 reCAPTCHA token(持久化有头模式)""" + + _instance: Optional['BrowserCaptchaService'] = None + _lock = asyncio.Lock() + + def __init__(self, db=None): + """初始化服务""" + # === 修改点 1: 设置为有头模式 === + self.headless = False + self.playwright = None + # 注意: 持久化模式下,我们操作的是 context 而不是 browser + self.context: Optional[BrowserContext] = None + self._initialized = False + self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV" + self.db = db + + # === 修改点 2: 指定本地数据存储目录 === + # 这会在脚本运行目录下生成 browser_data 文件夹,用于保存你的登录状态 + self.user_data_dir = os.path.join(os.getcwd(), "browser_data") + + @classmethod + async def get_instance(cls, db=None) -> 'BrowserCaptchaService': + if cls._instance is None: + async with cls._lock: + if cls._instance is None: + cls._instance = cls(db) + # 首次调用不强制初始化,等待 get_token 时懒加载,或者可以在这里await + return cls._instance + + async def initialize(self): + """初始化持久化浏览器上下文""" + if self._initialized and self.context: + return + + try: + proxy_url = None + if self.db: + captcha_config = await self.db.get_captcha_config() + if captcha_config.browser_proxy_enabled and captcha_config.browser_proxy_url: + proxy_url = captcha_config.browser_proxy_url + + debug_logger.log_info(f"[BrowserCaptcha] 正在启动浏览器 (用户数据目录: {self.user_data_dir})...") + self.playwright = await async_playwright().start() + + # 配置启动参数 + launch_options = { + 'headless': self.headless, + 'user_data_dir': self.user_data_dir, # 指定数据目录 + 'viewport': {'width': 1280, 'height': 720}, # 设置默认窗口大小 + 'args': [ + '--disable-blink-features=AutomationControlled', + '--disable-infobars', + '--no-sandbox', + '--disable-setuid-sandbox', + ] + } + + # 代理配置 + if proxy_url: + proxy_config = parse_proxy_url(proxy_url) + if proxy_config: + launch_options['proxy'] = proxy_config + debug_logger.log_info(f"[BrowserCaptcha] 使用代理: {proxy_config['server']}") + + # === 修改点 3: 使用 launch_persistent_context === + # 这会启动一个带有状态的浏览器窗口 + self.context = await self.playwright.chromium.launch_persistent_context(**launch_options) + + # 设置默认超时 + self.context.set_default_timeout(30000) + + self._initialized = True + debug_logger.log_info(f"[BrowserCaptcha] ✅ 浏览器已启动 (Profile: {self.user_data_dir})") + + 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""" + # 确保浏览器已启动 + if not self._initialized or not self.context: + await self.initialize() + + start_time = time.time() + page: Optional[Page] = None + + try: + # === 修改点 4: 在现有上下文中新建标签页,而不是新建上下文 === + # 这样可以复用该上下文中已保存的 Cookie (你的登录状态) + page = await self.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") + except Exception as e: + debug_logger.log_warning(f"[BrowserCaptcha] 页面加载警告: {str(e)}") + + # --- 关键点:如果需要人工介入 --- + # 你可以在这里加入一段逻辑,如果是第一次运行,或者检测到未登录, + # 可以暂停脚本,等你手动操作完再继续。 + # 例如: await asyncio.sleep(30) + + # ... (中间注入脚本和执行 reCAPTCHA 的代码逻辑与原版完全一致,此处省略以节省篇幅) ... + # ... 请将原代码中从 "检查并注入 reCAPTCHA v3 脚本" 到 token 获取部分的代码复制到这里 ... + + # 这里为了演示,简写注入逻辑(请保留你原有的完整注入逻辑): + script_loaded = await page.evaluate("() => { return !!(window.grecaptcha && window.grecaptcha.execute); }") + if not script_loaded: + await page.evaluate(f""" + () => {{ + const script = document.createElement('script'); + script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}'; + script.async = true; script.defer = true; + document.head.appendChild(script); + }} + """) + # 等待加载... (保留你原有的等待循环) + await page.wait_for_timeout(2000) + + # 执行获取 Token (保留你原有的 execute 逻辑) + token = await page.evaluate(f""" + async () => {{ + try {{ + return await window.grecaptcha.execute('{self.website_key}', {{ action: 'FLOW_GENERATION' }}); + }} catch (e) {{ return null; }} + }} + """) + + if token: + debug_logger.log_info(f"[BrowserCaptcha] ✅ Token获取成功") + return token + else: + debug_logger.log_error("[BrowserCaptcha] Token获取失败") + return None + + except Exception as e: + debug_logger.log_error(f"[BrowserCaptcha] 异常: {str(e)}") + return None + finally: + # === 修改点 5: 只关闭 Page (标签页),不关闭 Context (浏览器窗口) === + if page: + try: + await page.close() + except: + pass + + async def close(self): + """完全关闭浏览器(清理资源时调用)""" + try: + if self.context: + await self.context.close() # 这会关闭整个浏览器窗口 + self.context = None + + if self.playwright: + await self.playwright.stop() + self.playwright = None + + self._initialized = False + debug_logger.log_info("[BrowserCaptcha] 浏览器服务已关闭") + except Exception as e: + debug_logger.log_error(f"[BrowserCaptcha] 关闭异常: {str(e)}") + + # 增加一个辅助方法,用于手动登录 + async def open_login_window(self): + """调用此方法打开一个永久窗口供你登录Google""" + await self.initialize() + page = await self.context.new_page() + await page.goto("https://accounts.google.com/") + print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。") \ No newline at end of file diff --git a/src/services/flow_client.py b/src/services/flow_client.py index f5d6748..691e8db 100644 --- a/src/services/flow_client.py +++ b/src/services/flow_client.py @@ -687,8 +687,16 @@ class FlowClient: """获取reCAPTCHA token - 支持两种方式""" captcha_method = config.captcha_method + if True: + try: + from .browser_captcha_personal 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 # 浏览器打码 - if captcha_method == "browser": + elif captcha_method == "browser": try: from .browser_captcha import BrowserCaptchaService service = await BrowserCaptchaService.get_instance(self.proxy_manager) @@ -696,61 +704,61 @@ class FlowClient: 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) - + else: + # YesCaptcha打码 + client_key = config.yescaptcha_api_key + if not client_key: + debug_logger.log_info("[reCAPTCHA] API key not configured, skipping") return None - except Exception as e: - debug_logger.log_error(f"[reCAPTCHA] error: {str(e)}") - 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