dantynoel commited on
Commit ·
3cb9e04
1
Parent(s): 89cede9
fork: 个人用途可以在内置浏览器上登录对应的谷歌账号,测试可行
Browse files- .gitignore +2 -0
- config/setting.toml +1 -1
- request.py +150 -0
- src/main.py +6 -1
- src/services/browser_captcha_personal.py +197 -0
- src/services/flow_client.py +54 -46
.gitignore
CHANGED
|
@@ -52,3 +52,5 @@ logs.txt
|
|
| 52 |
*.tmp
|
| 53 |
*.bak
|
| 54 |
*.cache
|
|
|
|
|
|
|
|
|
| 52 |
*.tmp
|
| 53 |
*.bak
|
| 54 |
*.cache
|
| 55 |
+
|
| 56 |
+
browser_data
|
config/setting.toml
CHANGED
|
@@ -12,7 +12,7 @@ max_poll_attempts = 200
|
|
| 12 |
|
| 13 |
[server]
|
| 14 |
host = "0.0.0.0"
|
| 15 |
-
port =
|
| 16 |
|
| 17 |
[debug]
|
| 18 |
enabled = false
|
|
|
|
| 12 |
|
| 13 |
[server]
|
| 14 |
host = "0.0.0.0"
|
| 15 |
+
port = 8106
|
| 16 |
|
| 17 |
[debug]
|
| 18 |
enabled = false
|
request.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
import base64
|
| 5 |
+
import aiohttp # Async test. Need to install
|
| 6 |
+
import asyncio
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# --- 配置区域 ---
|
| 10 |
+
BASE_URL = os.getenv('GEMINI_FLOW2API_URL', 'http://127.0.0.1:8106')
|
| 11 |
+
BACKEND_URL = BASE_URL + "/v1/chat/completions"
|
| 12 |
+
API_KEY = os.getenv('GEMINI_FLOW2API_APIKEY', 'Bearer han1234')
|
| 13 |
+
if API_KEY is None:
|
| 14 |
+
raise ValueError('[gemini flow2api] api key not set')
|
| 15 |
+
MODEL_LANDSCAPE = "gemini-3.0-pro-image-landscape"
|
| 16 |
+
MODEL_PORTRAIT = "gemini-3.0-pro-image-portrait"
|
| 17 |
+
|
| 18 |
+
# 修改: 增加 model 参数,默认为 None
|
| 19 |
+
async def request_backend_generation(
|
| 20 |
+
prompt: str,
|
| 21 |
+
images: list[bytes] = None,
|
| 22 |
+
model: str = None) -> bytes | None:
|
| 23 |
+
"""
|
| 24 |
+
请求后端生成图片。
|
| 25 |
+
:param prompt: 提示词
|
| 26 |
+
:param images: 图片二进制列表
|
| 27 |
+
:param model: 指定模型名称 (可选)
|
| 28 |
+
:return: 成功返回图片bytes,失败返回None
|
| 29 |
+
"""
|
| 30 |
+
# 更新token
|
| 31 |
+
images = images or []
|
| 32 |
+
|
| 33 |
+
# 逻辑: 如果未指定 model,默认使用 Landscape
|
| 34 |
+
use_model = model if model else MODEL_LANDSCAPE
|
| 35 |
+
|
| 36 |
+
# 1. 构造 Payload
|
| 37 |
+
if images:
|
| 38 |
+
content_payload = [{"type": "text", "text": prompt}]
|
| 39 |
+
print(f"[Backend] 正在处理 {len(images)} 张图片输入...")
|
| 40 |
+
for img_bytes in images:
|
| 41 |
+
b64_str = base64.b64encode(img_bytes).decode('utf-8')
|
| 42 |
+
content_payload.append({
|
| 43 |
+
"type": "image_url",
|
| 44 |
+
"image_url": {"url": f"data:image/jpeg;base64,{b64_str}"}
|
| 45 |
+
})
|
| 46 |
+
else:
|
| 47 |
+
content_payload = prompt
|
| 48 |
+
|
| 49 |
+
payload = {
|
| 50 |
+
"model": use_model, # 使用选定的模型
|
| 51 |
+
"messages": [{"role": "user", "content": content_payload}],
|
| 52 |
+
"stream": True
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
headers = {
|
| 56 |
+
"Authorization": API_KEY,
|
| 57 |
+
"Content-Type": "application/json"
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
image_url = None
|
| 61 |
+
print(f"[Backend] Model: {use_model} | 发起请求: {prompt[:20]}...")
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
async with aiohttp.ClientSession() as session:
|
| 65 |
+
async with session.post(BACKEND_URL, json=payload, headers=headers, timeout=120) as response:
|
| 66 |
+
if response.status != 200:
|
| 67 |
+
err_text = await response.text()
|
| 68 |
+
content = response.content
|
| 69 |
+
print(f"[Backend Error] Status {response.status}: {err_text} {content}")
|
| 70 |
+
raise Exception(f"API Error: {response.status}: {err_text}")
|
| 71 |
+
|
| 72 |
+
async for line in response.content:
|
| 73 |
+
line_str = line.decode('utf-8').strip()
|
| 74 |
+
if line_str.startswith('{"error'):
|
| 75 |
+
chunk = json.loads(data_str)
|
| 76 |
+
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
| 77 |
+
msg = delta['reasoning_content']
|
| 78 |
+
if '401' in msg:
|
| 79 |
+
msg += '\nAccess Token 已失效,需重新配置。'
|
| 80 |
+
elif '400' in msg:
|
| 81 |
+
msg += '\n返回内容被拦截。'
|
| 82 |
+
raise Exception(msg)
|
| 83 |
+
|
| 84 |
+
if not line_str or not line_str.startswith('data: '):
|
| 85 |
+
continue
|
| 86 |
+
|
| 87 |
+
data_str = line_str[6:]
|
| 88 |
+
if data_str == '[DONE]':
|
| 89 |
+
break
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
chunk = json.loads(data_str)
|
| 93 |
+
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
| 94 |
+
|
| 95 |
+
# 打印思考过程
|
| 96 |
+
if "reasoning_content" in delta:
|
| 97 |
+
print(delta['reasoning_content'], end="", flush=True)
|
| 98 |
+
|
| 99 |
+
# 提取内容中的图片链接
|
| 100 |
+
if "content" in delta:
|
| 101 |
+
content_text = delta["content"]
|
| 102 |
+
img_match = re.search(r'!\[.*?\]\((.*?)\)', content_text)
|
| 103 |
+
if img_match:
|
| 104 |
+
image_url = img_match.group(1)
|
| 105 |
+
print(f"\n[Backend] 捕获图片链接: {image_url}")
|
| 106 |
+
except json.JSONDecodeError:
|
| 107 |
+
continue
|
| 108 |
+
|
| 109 |
+
# 3. 下载生成的图片
|
| 110 |
+
if image_url:
|
| 111 |
+
async with session.get(image_url) as img_resp:
|
| 112 |
+
if img_resp.status == 200:
|
| 113 |
+
image_bytes = await img_resp.read()
|
| 114 |
+
return image_bytes
|
| 115 |
+
else:
|
| 116 |
+
print(f"[Backend Error] 图片下载失败: {img_resp.status}")
|
| 117 |
+
except Exception as e:
|
| 118 |
+
print(f"[Backend Exception] {e}")
|
| 119 |
+
raise e
|
| 120 |
+
|
| 121 |
+
return None
|
| 122 |
+
|
| 123 |
+
if __name__ == '__main__':
|
| 124 |
+
async def main():
|
| 125 |
+
print("=== AI 绘图接口测试 ===")
|
| 126 |
+
user_prompt = input("请输入提示词 (例如 '一只猫'): ").strip()
|
| 127 |
+
if not user_prompt:
|
| 128 |
+
user_prompt = "A cute cat in the garden"
|
| 129 |
+
|
| 130 |
+
print(f"正在请求: {user_prompt}")
|
| 131 |
+
|
| 132 |
+
# 这里的 images 传空列表用于测试文生图
|
| 133 |
+
# 如果想测试图生图,你需要手动读取本地文件:
|
| 134 |
+
# with open("output_test.jpg", "rb") as f: img_data = f.read()
|
| 135 |
+
# result = await request_backend_generation(user_prompt, [img_data])
|
| 136 |
+
|
| 137 |
+
result = await request_backend_generation(user_prompt)
|
| 138 |
+
|
| 139 |
+
if result:
|
| 140 |
+
filename = "output_test.jpg"
|
| 141 |
+
with open(filename, "wb") as f:
|
| 142 |
+
f.write(result)
|
| 143 |
+
print(f"\n[Success] 图片已保存为 {filename},大小: {len(result)} bytes")
|
| 144 |
+
else:
|
| 145 |
+
print("\n[Failed] 生成失败")
|
| 146 |
+
|
| 147 |
+
# 运行测试
|
| 148 |
+
if os.name == 'nt': # Windows 兼容性
|
| 149 |
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
| 150 |
+
asyncio.run(main())
|
src/main.py
CHANGED
|
@@ -74,7 +74,12 @@ async def lifespan(app: FastAPI):
|
|
| 74 |
|
| 75 |
# Initialize browser captcha service if needed
|
| 76 |
browser_service = None
|
| 77 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
from .services.browser_captcha import BrowserCaptchaService
|
| 79 |
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 80 |
print("✓ Browser captcha service initialized (headless mode)")
|
|
|
|
| 74 |
|
| 75 |
# Initialize browser captcha service if needed
|
| 76 |
browser_service = None
|
| 77 |
+
if True:
|
| 78 |
+
from .services.browser_captcha_personal import BrowserCaptchaService
|
| 79 |
+
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 80 |
+
await browser_service.open_login_window()
|
| 81 |
+
print("✓ Browser captcha service initialized (webui mode)")
|
| 82 |
+
elif captcha_config.captcha_method == "browser":
|
| 83 |
from .services.browser_captcha import BrowserCaptchaService
|
| 84 |
browser_service = await BrowserCaptchaService.get_instance(db)
|
| 85 |
print("✓ Browser captcha service initialized (headless mode)")
|
src/services/browser_captcha_personal.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import time
|
| 3 |
+
import re
|
| 4 |
+
import os
|
| 5 |
+
from typing import Optional, Dict
|
| 6 |
+
from playwright.async_api import async_playwright, BrowserContext, Page
|
| 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 |
+
# === 修改点 1: 设置为有头模式 ===
|
| 33 |
+
self.headless = False
|
| 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.context:
|
| 57 |
+
return
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
proxy_url = None
|
| 61 |
+
if self.db:
|
| 62 |
+
captcha_config = await self.db.get_captcha_config()
|
| 63 |
+
if captcha_config.browser_proxy_enabled and captcha_config.browser_proxy_url:
|
| 64 |
+
proxy_url = captcha_config.browser_proxy_url
|
| 65 |
+
|
| 66 |
+
debug_logger.log_info(f"[BrowserCaptcha] 正在启动浏览器 (用户数据目录: {self.user_data_dir})...")
|
| 67 |
+
self.playwright = await async_playwright().start()
|
| 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.context:
|
| 107 |
+
await self.initialize()
|
| 108 |
+
|
| 109 |
+
start_time = time.time()
|
| 110 |
+
page: Optional[Page] = None
|
| 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 |
+
try:
|
| 122 |
+
await page.goto(website_url, wait_until="domcontentloaded")
|
| 123 |
+
except Exception as e:
|
| 124 |
+
debug_logger.log_warning(f"[BrowserCaptcha] 页面加载警告: {str(e)}")
|
| 125 |
+
|
| 126 |
+
# --- 关键点:如果需要人工介入 ---
|
| 127 |
+
# 你可以在这里加入一段逻辑,如果是第一次运行,或者检测到未登录,
|
| 128 |
+
# 可以暂停脚本,等你手动操作完再继续。
|
| 129 |
+
# 例如: await asyncio.sleep(30)
|
| 130 |
+
|
| 131 |
+
# ... (中间注入脚本和执行 reCAPTCHA 的代码逻辑与原版完全一致,此处省略以节省篇幅) ...
|
| 132 |
+
# ... 请将原代码中从 "检查并注入 reCAPTCHA v3 脚本" 到 token 获取部分的代码复制到这里 ...
|
| 133 |
+
|
| 134 |
+
# 这里为了演示,简写注入逻辑(请保留你原有的完整注入逻辑):
|
| 135 |
+
script_loaded = await page.evaluate("() => { return !!(window.grecaptcha && window.grecaptcha.execute); }")
|
| 136 |
+
if not script_loaded:
|
| 137 |
+
await page.evaluate(f"""
|
| 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; script.defer = true;
|
| 142 |
+
document.head.appendChild(script);
|
| 143 |
+
}}
|
| 144 |
+
""")
|
| 145 |
+
# 等待加载... (保留你原有的等待循环)
|
| 146 |
+
await page.wait_for_timeout(2000)
|
| 147 |
+
|
| 148 |
+
# 执行获取 Token (保留你原有的 execute 逻辑)
|
| 149 |
+
token = await page.evaluate(f"""
|
| 150 |
+
async () => {{
|
| 151 |
+
try {{
|
| 152 |
+
return await window.grecaptcha.execute('{self.website_key}', {{ action: 'FLOW_GENERATION' }});
|
| 153 |
+
}} catch (e) {{ return null; }}
|
| 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 |
+
# === 修改点 5: 只关闭 Page (标签页),不关闭 Context (浏览器窗口) ===
|
| 169 |
+
if page:
|
| 170 |
+
try:
|
| 171 |
+
await page.close()
|
| 172 |
+
except:
|
| 173 |
+
pass
|
| 174 |
+
|
| 175 |
+
async def close(self):
|
| 176 |
+
"""完全关闭浏览器(清理资源时调用)"""
|
| 177 |
+
try:
|
| 178 |
+
if self.context:
|
| 179 |
+
await self.context.close() # 这会关闭整个浏览器窗口
|
| 180 |
+
self.context = None
|
| 181 |
+
|
| 182 |
+
if self.playwright:
|
| 183 |
+
await self.playwright.stop()
|
| 184 |
+
self.playwright = None
|
| 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 |
+
"""调用此方法打开一个永久窗口供你登录Google"""
|
| 194 |
+
await self.initialize()
|
| 195 |
+
page = await self.context.new_page()
|
| 196 |
+
await page.goto("https://accounts.google.com/")
|
| 197 |
+
print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
|
src/services/flow_client.py
CHANGED
|
@@ -687,8 +687,16 @@ class FlowClient:
|
|
| 687 |
"""获取reCAPTCHA token - 支持两种方式"""
|
| 688 |
captcha_method = config.captcha_method
|
| 689 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 690 |
# 浏览器打码
|
| 691 |
-
|
| 692 |
try:
|
| 693 |
from .browser_captcha import BrowserCaptchaService
|
| 694 |
service = await BrowserCaptchaService.get_instance(self.proxy_manager)
|
|
@@ -696,61 +704,61 @@ class FlowClient:
|
|
| 696 |
except Exception as e:
|
| 697 |
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
|
| 698 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
return None
|
| 705 |
-
|
| 706 |
-
website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 707 |
-
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 708 |
-
base_url = config.yescaptcha_base_url
|
| 709 |
-
page_action = "FLOW_GENERATION"
|
| 710 |
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
|
|
|
| 721 |
}
|
| 722 |
-
}
|
| 723 |
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
|
| 728 |
-
|
| 729 |
|
| 730 |
-
|
| 731 |
-
|
| 732 |
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
|
| 742 |
-
|
| 743 |
|
| 744 |
-
|
| 745 |
-
|
| 746 |
|
| 747 |
-
|
| 748 |
-
|
| 749 |
|
| 750 |
-
|
| 751 |
|
| 752 |
-
|
| 753 |
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
|
|
|
| 687 |
"""获取reCAPTCHA token - 支持两种方式"""
|
| 688 |
captcha_method = config.captcha_method
|
| 689 |
|
| 690 |
+
if True:
|
| 691 |
+
try:
|
| 692 |
+
from .browser_captcha_personal import BrowserCaptchaService
|
| 693 |
+
service = await BrowserCaptchaService.get_instance(self.proxy_manager)
|
| 694 |
+
return await service.get_token(project_id)
|
| 695 |
+
except Exception as e:
|
| 696 |
+
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
|
| 697 |
+
return None
|
| 698 |
# 浏览器打码
|
| 699 |
+
elif captcha_method == "browser":
|
| 700 |
try:
|
| 701 |
from .browser_captcha import BrowserCaptchaService
|
| 702 |
service = await BrowserCaptchaService.get_instance(self.proxy_manager)
|
|
|
|
| 704 |
except Exception as e:
|
| 705 |
debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
|
| 706 |
return None
|
| 707 |
+
else:
|
| 708 |
+
# YesCaptcha打码
|
| 709 |
+
client_key = config.yescaptcha_api_key
|
| 710 |
+
if not client_key:
|
| 711 |
+
debug_logger.log_info("[reCAPTCHA] API key not configured, skipping")
|
| 712 |
+
return None
|
| 713 |
|
| 714 |
+
website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
|
| 715 |
+
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
|
| 716 |
+
base_url = config.yescaptcha_base_url
|
| 717 |
+
page_action = "FLOW_GENERATION"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
|
| 719 |
+
try:
|
| 720 |
+
async with AsyncSession() as session:
|
| 721 |
+
create_url = f"{base_url}/createTask"
|
| 722 |
+
create_data = {
|
| 723 |
+
"clientKey": client_key,
|
| 724 |
+
"task": {
|
| 725 |
+
"websiteURL": website_url,
|
| 726 |
+
"websiteKey": website_key,
|
| 727 |
+
"type": "RecaptchaV3TaskProxylessM1",
|
| 728 |
+
"pageAction": page_action
|
| 729 |
+
}
|
| 730 |
}
|
|
|
|
| 731 |
|
| 732 |
+
result = await session.post(create_url, json=create_data, impersonate="chrome110")
|
| 733 |
+
result_json = result.json()
|
| 734 |
+
task_id = result_json.get('taskId')
|
| 735 |
|
| 736 |
+
debug_logger.log_info(f"[reCAPTCHA] created task_id: {task_id}")
|
| 737 |
|
| 738 |
+
if not task_id:
|
| 739 |
+
return None
|
| 740 |
|
| 741 |
+
get_url = f"{base_url}/getTaskResult"
|
| 742 |
+
for i in range(40):
|
| 743 |
+
get_data = {
|
| 744 |
+
"clientKey": client_key,
|
| 745 |
+
"taskId": task_id
|
| 746 |
+
}
|
| 747 |
+
result = await session.post(get_url, json=get_data, impersonate="chrome110")
|
| 748 |
+
result_json = result.json()
|
| 749 |
|
| 750 |
+
debug_logger.log_info(f"[reCAPTCHA] polling #{i+1}: {result_json}")
|
| 751 |
|
| 752 |
+
solution = result_json.get('solution', {})
|
| 753 |
+
response = solution.get('gRecaptchaResponse')
|
| 754 |
|
| 755 |
+
if response:
|
| 756 |
+
return response
|
| 757 |
|
| 758 |
+
time.sleep(3)
|
| 759 |
|
| 760 |
+
return None
|
| 761 |
|
| 762 |
+
except Exception as e:
|
| 763 |
+
debug_logger.log_error(f"[reCAPTCHA] error: {str(e)}")
|
| 764 |
+
return None
|