netlops commited on
Commit
747d0cd
·
1 Parent(s): c21f770

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 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
- # Helper function to detect headless/Docker environment
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 effective_captcha_method == "personal":
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 (webui mode)")
110
- elif effective_captcha_method == "browser":
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, 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("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
 
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": "veo_3_1_i2v_s_fast_fl",
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": "veo_3_1_i2v_s_fast_fl",
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,