genz27 Warp commited on
Commit
99bedf8
·
1 Parent(s): aec69d2

更新:1.本地打码方式修改为有头并增加并行(可设置启动浏览器数量)2.修复flow视频和图片请求体报错400 3.新增图片和视频画质提升包括4K,2K以及1080p等 4.调整管理员端配置UI布局

Browse files
src/api/admin.py CHANGED
@@ -885,6 +885,7 @@ async def update_captcha_config(
885
  capsolver_base_url = request.get("capsolver_base_url")
886
  browser_proxy_enabled = request.get("browser_proxy_enabled", False)
887
  browser_proxy_url = request.get("browser_proxy_url", "")
 
888
 
889
  # 验证浏览器代理URL格式
890
  if browser_proxy_enabled and browser_proxy_url:
@@ -903,9 +904,19 @@ async def update_captcha_config(
903
  capsolver_api_key=capsolver_api_key,
904
  capsolver_base_url=capsolver_base_url,
905
  browser_proxy_enabled=browser_proxy_enabled,
906
- browser_proxy_url=browser_proxy_url if browser_proxy_enabled else None
 
907
  )
908
 
 
 
 
 
 
 
 
 
 
909
  # 🔥 Hot reload: sync database config to memory
910
  await db.reload_config_to_memory()
911
 
@@ -927,7 +938,8 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)):
927
  "capsolver_api_key": captcha_config.capsolver_api_key,
928
  "capsolver_base_url": captcha_config.capsolver_base_url,
929
  "browser_proxy_enabled": captcha_config.browser_proxy_enabled,
930
- "browser_proxy_url": captcha_config.browser_proxy_url or ""
 
931
  }
932
 
933
 
 
885
  capsolver_base_url = request.get("capsolver_base_url")
886
  browser_proxy_enabled = request.get("browser_proxy_enabled", False)
887
  browser_proxy_url = request.get("browser_proxy_url", "")
888
+ browser_count = request.get("browser_count", 1)
889
 
890
  # 验证浏览器代理URL格式
891
  if browser_proxy_enabled and browser_proxy_url:
 
904
  capsolver_api_key=capsolver_api_key,
905
  capsolver_base_url=capsolver_base_url,
906
  browser_proxy_enabled=browser_proxy_enabled,
907
+ browser_proxy_url=browser_proxy_url if browser_proxy_enabled else None,
908
+ browser_count=max(1, int(browser_count)) if browser_count else 1
909
  )
910
 
911
+ # 如果使用 browser 打码,热重载浏览器数量配置
912
+ if captcha_method == "browser":
913
+ try:
914
+ from ..services.browser_captcha import BrowserCaptchaService
915
+ service = await BrowserCaptchaService.get_instance(db)
916
+ await service.reload_browser_count()
917
+ except Exception:
918
+ pass
919
+
920
  # 🔥 Hot reload: sync database config to memory
921
  await db.reload_config_to_memory()
922
 
 
938
  "capsolver_api_key": captcha_config.capsolver_api_key,
939
  "capsolver_base_url": captcha_config.capsolver_base_url,
940
  "browser_proxy_enabled": captcha_config.browser_proxy_enabled,
941
+ "browser_proxy_url": captcha_config.browser_proxy_url or "",
942
+ "browser_count": captcha_config.browser_count
943
  }
944
 
945
 
src/core/config.py CHANGED
@@ -145,6 +145,17 @@ class Config:
145
  self._config["generation"] = {}
146
  self._config["generation"]["video_timeout"] = timeout
147
 
 
 
 
 
 
 
 
 
 
 
 
148
  # Cache configuration
149
  @property
150
  def cache_enabled(self) -> bool:
 
145
  self._config["generation"] = {}
146
  self._config["generation"]["video_timeout"] = timeout
147
 
148
+ @property
149
+ def upsample_timeout(self) -> int:
150
+ """Get upsample (4K/2K) timeout in seconds"""
151
+ return self._config.get("generation", {}).get("upsample_timeout", 300)
152
+
153
+ def set_upsample_timeout(self, timeout: int):
154
+ """Set upsample (4K/2K) timeout in seconds"""
155
+ if "generation" not in self._config:
156
+ self._config["generation"] = {}
157
+ self._config["generation"]["upsample_timeout"] = timeout
158
+
159
  # Cache configuration
160
  @property
161
  def cache_enabled(self) -> bool:
src/core/database.py CHANGED
@@ -289,6 +289,7 @@ class Database:
289
  ("ezcaptcha_base_url", "TEXT DEFAULT 'https://api.ez-captcha.com'"),
290
  ("capsolver_api_key", "TEXT DEFAULT ''"),
291
  ("capsolver_base_url", "TEXT DEFAULT 'https://api.capsolver.com'"),
 
292
  ]
293
 
294
  for col_name, col_type in captcha_columns_to_add:
@@ -1224,7 +1225,8 @@ class Database:
1224
  capsolver_api_key: str = None,
1225
  capsolver_base_url: str = None,
1226
  browser_proxy_enabled: bool = None,
1227
- browser_proxy_url: str = None
 
1228
  ):
1229
  """Update captcha configuration"""
1230
  async with aiosqlite.connect(self.db_path) as db:
@@ -1245,6 +1247,7 @@ class Database:
1245
  new_cs_url = capsolver_base_url if capsolver_base_url is not None else current.get("capsolver_base_url", "https://api.capsolver.com")
1246
  new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else current.get("browser_proxy_enabled", False)
1247
  new_proxy_url = browser_proxy_url if browser_proxy_url is not None else current.get("browser_proxy_url")
 
1248
 
1249
  await db.execute("""
1250
  UPDATE captcha_config
@@ -1252,10 +1255,10 @@ class Database:
1252
  capmonster_api_key = ?, capmonster_base_url = ?,
1253
  ezcaptcha_api_key = ?, ezcaptcha_base_url = ?,
1254
  capsolver_api_key = ?, capsolver_base_url = ?,
1255
- browser_proxy_enabled = ?, browser_proxy_url = ?, updated_at = CURRENT_TIMESTAMP
1256
  WHERE id = 1
1257
  """, (new_method, new_yes_key, new_yes_url, new_cap_key, new_cap_url,
1258
- new_ez_key, new_ez_url, new_cs_key, new_cs_url, new_proxy_enabled, new_proxy_url))
1259
  else:
1260
  new_method = captcha_method if captcha_method is not None else "yescaptcha"
1261
  new_yes_key = yescaptcha_api_key if yescaptcha_api_key is not None else ""
@@ -1268,14 +1271,15 @@ class Database:
1268
  new_cs_url = capsolver_base_url if capsolver_base_url is not None else "https://api.capsolver.com"
1269
  new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else False
1270
  new_proxy_url = browser_proxy_url
 
1271
 
1272
  await db.execute("""
1273
  INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url,
1274
  capmonster_api_key, capmonster_base_url, ezcaptcha_api_key, ezcaptcha_base_url,
1275
- capsolver_api_key, capsolver_base_url, browser_proxy_enabled, browser_proxy_url)
1276
- VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1277
  """, (new_method, new_yes_key, new_yes_url, new_cap_key, new_cap_url,
1278
- new_ez_key, new_ez_url, new_cs_key, new_cs_url, new_proxy_enabled, new_proxy_url))
1279
 
1280
  await db.commit()
1281
 
 
289
  ("ezcaptcha_base_url", "TEXT DEFAULT 'https://api.ez-captcha.com'"),
290
  ("capsolver_api_key", "TEXT DEFAULT ''"),
291
  ("capsolver_base_url", "TEXT DEFAULT 'https://api.capsolver.com'"),
292
+ ("browser_count", "INTEGER DEFAULT 1"),
293
  ]
294
 
295
  for col_name, col_type in captcha_columns_to_add:
 
1225
  capsolver_api_key: str = None,
1226
  capsolver_base_url: str = None,
1227
  browser_proxy_enabled: bool = None,
1228
+ browser_proxy_url: str = None,
1229
+ browser_count: int = None
1230
  ):
1231
  """Update captcha configuration"""
1232
  async with aiosqlite.connect(self.db_path) as db:
 
1247
  new_cs_url = capsolver_base_url if capsolver_base_url is not None else current.get("capsolver_base_url", "https://api.capsolver.com")
1248
  new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else current.get("browser_proxy_enabled", False)
1249
  new_proxy_url = browser_proxy_url if browser_proxy_url is not None else current.get("browser_proxy_url")
1250
+ new_browser_count = browser_count if browser_count is not None else current.get("browser_count", 1)
1251
 
1252
  await db.execute("""
1253
  UPDATE captcha_config
 
1255
  capmonster_api_key = ?, capmonster_base_url = ?,
1256
  ezcaptcha_api_key = ?, ezcaptcha_base_url = ?,
1257
  capsolver_api_key = ?, capsolver_base_url = ?,
1258
+ browser_proxy_enabled = ?, browser_proxy_url = ?, browser_count = ?, updated_at = CURRENT_TIMESTAMP
1259
  WHERE id = 1
1260
  """, (new_method, new_yes_key, new_yes_url, new_cap_key, new_cap_url,
1261
+ new_ez_key, new_ez_url, new_cs_key, new_cs_url, new_proxy_enabled, new_proxy_url, new_browser_count))
1262
  else:
1263
  new_method = captcha_method if captcha_method is not None else "yescaptcha"
1264
  new_yes_key = yescaptcha_api_key if yescaptcha_api_key is not None else ""
 
1271
  new_cs_url = capsolver_base_url if capsolver_base_url is not None else "https://api.capsolver.com"
1272
  new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else False
1273
  new_proxy_url = browser_proxy_url
1274
+ new_browser_count = browser_count if browser_count is not None else 1
1275
 
1276
  await db.execute("""
1277
  INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url,
1278
  capmonster_api_key, capmonster_base_url, ezcaptcha_api_key, ezcaptcha_base_url,
1279
+ capsolver_api_key, capsolver_base_url, browser_proxy_enabled, browser_proxy_url, browser_count)
1280
+ VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1281
  """, (new_method, new_yes_key, new_yes_url, new_cap_key, new_cap_url,
1282
+ new_ez_key, new_ez_url, new_cs_key, new_cs_url, new_proxy_enabled, new_proxy_url, new_browser_count))
1283
 
1284
  await db.commit()
1285
 
src/core/models.py CHANGED
@@ -160,6 +160,7 @@ class CaptchaConfig(BaseModel):
160
  page_action: str = "FLOW_GENERATION"
161
  browser_proxy_enabled: bool = False # 浏览器打码是否启用代理
162
  browser_proxy_url: Optional[str] = None # 浏览器打码代理URL
 
163
  created_at: Optional[datetime] = None
164
  updated_at: Optional[datetime] = None
165
 
 
160
  page_action: str = "FLOW_GENERATION"
161
  browser_proxy_enabled: bool = False # 浏览器打码是否启用代理
162
  browser_proxy_url: Optional[str] = None # 浏览器打码代理URL
163
+ browser_count: int = 1 # 浏览器打码实例数量
164
  created_at: Optional[datetime] = None
165
  updated_at: Optional[datetime] = None
166
 
src/services/browser_captcha.py CHANGED
@@ -1,317 +1,535 @@
1
  """
2
- 浏览器自动化获取 reCAPTCHA token
3
- 使用 Playwright 访问页面并执行 reCAPTCHA 验证
4
  """
5
  import asyncio
6
  import time
 
7
  import re
 
 
8
  from typing import Optional, Dict
9
- from playwright.async_api import async_playwright, Browser, BrowserContext
10
-
11
- from ..core.logger import debug_logger
12
 
 
13
 
14
- def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]:
15
- """解析代理URL,分离协议、主机、端口、认证信息
16
 
17
- Args:
18
- proxy_url: 代理URL,格式:protocol://[username:password@]host:port
19
 
20
- Returns:
21
- 代理配置字典,包含server、username、password(如果有认证)
22
- """
23
- proxy_pattern = r'^(socks5|http|https)://(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$'
24
- match = re.match(proxy_pattern, proxy_url)
25
 
 
 
 
 
 
 
 
 
26
  if match:
27
  protocol, username, password, host, port = match.groups()
28
  proxy_config = {'server': f'{protocol}://{host}:{port}'}
29
-
30
  if username and password:
31
  proxy_config['username'] = username
32
  proxy_config['password'] = password
33
-
34
  return proxy_config
35
  return None
36
 
37
-
38
  def validate_browser_proxy_url(proxy_url: str) -> tuple[bool, str]:
39
- """验证浏览器代理URL格式(仅支持HTTP和无认证SOCKS5)
40
-
41
- Args:
42
- proxy_url: 代理URL
43
-
44
- Returns:
45
- (是否有效, 错误信息)
46
- """
47
- if not proxy_url or not proxy_url.strip():
48
- return True, "" # 空URL视为有效(不使用代理)
49
-
50
- proxy_url = proxy_url.strip()
51
  parsed = parse_proxy_url(proxy_url)
 
 
52
 
53
- if not parsed:
54
- return False, "代理URL格式错误,正确格式http://host:port socks5://host:port"
55
-
56
- # 检查否有认证信息
57
- has_auth = 'username' in parsed
58
-
59
- # 获取协议
60
- protocol = parsed['server'].split('://')[0]
61
-
62
- # SOCKS5不支持认证
63
- if protocol == 'socks5' and has_auth:
64
- return False, "浏览器不支持带认证的SOCKS5代理,请使用HTTP代理或移除SOCKS5认证"
65
-
66
- # HTTP/HTTPS支持认证
67
- if protocol in ['http', 'https']:
68
- return True, ""
69
-
70
- # SOCKS5无认证支持
71
- if protocol == 'socks5' and not has_auth:
72
- return True, ""
73
-
74
- return False, f"不支持的代理协议:{protocol}"
75
-
76
-
77
- class BrowserCaptchaService:
78
- """浏览器自动化获取 reCAPTCHA token(单例模式)"""
79
-
80
- _instance: Optional['BrowserCaptchaService'] = None
81
- _lock = asyncio.Lock()
82
-
83
- def __init__(self, db=None):
84
- """初始化服务(始终使用无头模式)"""
85
- self.headless = True # 始终无头
86
- self.playwright = None
87
- self.browser: Optional[Browser] = None
88
- self._initialized = False
89
- self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  self.db = db
91
-
92
- @classmethod
93
- async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
94
- """获取单例实例"""
95
- if cls._instance is None:
96
- async with cls._lock:
97
- if cls._instance is None:
98
- cls._instance = cls(db)
99
- await cls._instance.initialize()
100
- return cls._instance
101
-
102
- async def initialize(self):
103
- """初始化浏览器(启动一次)"""
104
- if self._initialized:
105
- return
106
-
 
 
107
  try:
108
- # 获取浏览器专用代理配置
109
- proxy_url = None
110
  if self.db:
111
  captcha_config = await self.db.get_captcha_config()
112
- if captcha_config.browser_proxy_enabled and captcha_config.browser_proxy_url:
113
- proxy_url = captcha_config.browser_proxy_url
114
-
115
- debug_logger.log_info(f"[BrowserCaptcha] 正在启动浏览器... (proxy={proxy_url or 'None'})")
116
- self.playwright = await async_playwright().start()
117
-
118
- # 配置浏览器启动参数
119
- launch_options = {
120
- 'headless': self.headless,
121
- 'args': [
 
 
122
  '--disable-blink-features=AutomationControlled',
123
- '--disable-dev-shm-usage',
124
  '--no-sandbox',
125
- '--disable-setuid-sandbox'
 
 
 
 
 
 
126
  ]
127
- }
128
-
129
- # 如果有代理,解析并添加代理配置
130
- if proxy_url:
131
- proxy_config = parse_proxy_url(proxy_url)
132
- if proxy_config:
133
- launch_options['proxy'] = proxy_config
134
- auth_info = "auth=yes" if 'username' in proxy_config else "auth=no"
135
- debug_logger.log_info(f"[BrowserCaptcha] 代理配置: {proxy_config['server']} ({auth_info})")
136
- else:
137
- debug_logger.log_warning(f"[BrowserCaptcha] 代理URL格式错误: {proxy_url}")
138
-
139
- self.browser = await self.playwright.chromium.launch(**launch_options)
140
- self._initialized = True
141
- debug_logger.log_info(f"[BrowserCaptcha] ✅ 浏览器已启动 (headless={self.headless}, proxy={proxy_url or 'None'})")
142
  except Exception as e:
143
- debug_logger.log_error(f"[BrowserCaptcha] 浏览器启动失败: {str(e)}")
 
 
 
 
 
144
  raise
145
-
146
- async def get_token(self, project_id: str) -> Optional[str]:
147
- """获取 reCAPTCHA token
148
-
149
- Args:
150
- project_id: Flow项目ID
151
-
152
- Returns:
153
- reCAPTCHA token字符串,如果获取失败返回None
154
- """
155
- if not self._initialized:
156
- await self.initialize()
157
-
158
- start_time = time.time()
159
- context = None
160
-
 
 
 
161
  try:
162
- # 创建新的上下文
163
- context = await self.browser.new_context(
164
- viewport={'width': 1920, 'height': 1080},
165
- 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',
166
- locale='en-US',
167
- timezone_id='America/New_York'
168
- )
169
  page = await context.new_page()
170
-
171
- website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
172
-
173
- debug_logger.log_info(f"[BrowserCaptcha] 访问页面: {website_url}")
174
-
175
- # 访问页面
 
 
 
 
 
 
 
 
176
  try:
177
- await page.goto(website_url, wait_until="domcontentloaded", timeout=30000)
178
  except Exception as e:
179
- debug_logger.log_warning(f"[BrowserCaptcha] 页面加载超时或失败: {str(e)}")
180
-
181
- # 检查并注入 reCAPTCHA v3 脚本
182
- debug_logger.log_info("[BrowserCaptcha] 检查并加载 reCAPTCHA v3 脚本...")
183
- script_loaded = await page.evaluate("""
184
- () => {
185
- if (window.grecaptcha && typeof window.grecaptcha.execute === 'function') {
186
- return true;
187
- }
188
- return false;
189
- }
190
- """)
191
-
192
- if not script_loaded:
193
- # 注入脚本
194
- debug_logger.log_info("[BrowserCaptcha] 注入 reCAPTCHA v3 脚本...")
195
- await page.evaluate(f"""
196
- () => {{
197
- return new Promise((resolve) => {{
198
- const script = document.createElement('script');
199
- script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
200
- script.async = true;
201
- script.defer = true;
202
- script.onload = () => resolve(true);
203
- script.onerror = () => resolve(false);
204
- document.head.appendChild(script);
205
  }});
206
  }}
207
- """)
208
-
209
- # 等待reCAPTCHA加载和初始化
210
- debug_logger.log_info("[BrowserCaptcha] 等待reCAPTCHA初始化...")
211
- for i in range(20):
212
- grecaptcha_ready = await page.evaluate("""
213
- () => {
214
- return window.grecaptcha &&
215
- typeof window.grecaptcha.execute === 'function';
216
- }
217
- """)
218
- if grecaptcha_ready:
219
- debug_logger.log_info(f"[BrowserCaptcha] reCAPTCHA 已准备好(等待了 {i*0.5} 秒)")
220
- break
221
- await asyncio.sleep(0.5)
222
- else:
223
- debug_logger.log_warning("[BrowserCaptcha] reCAPTCHA 初始化超时,继续尝试执行...")
224
-
225
- # 额外等待确保完全初始化
226
- await page.wait_for_timeout(1000)
227
-
228
- # 执行reCAPTCHA并获取token
229
- debug_logger.log_info("[BrowserCaptcha] 执行reCAPTCHA验证...")
230
- token = await page.evaluate("""
231
- async (websiteKey) => {
232
- try {
233
- if (!window.grecaptcha) {
234
- console.error('[BrowserCaptcha] window.grecaptcha 不存在');
235
- return null;
236
- }
237
-
238
- if (typeof window.grecaptcha.execute !== 'function') {
239
- console.error('[BrowserCaptcha] window.grecaptcha.execute 不是函数');
240
- return null;
241
- }
242
-
243
- // 确保grecaptcha已准备好
244
- await new Promise((resolve, reject) => {
245
- const timeout = setTimeout(() => {
246
- reject(new Error('reCAPTCHA加载超时'));
247
- }, 15000);
248
-
249
- if (window.grecaptcha && window.grecaptcha.ready) {
250
- window.grecaptcha.ready(() => {
251
- clearTimeout(timeout);
252
- resolve();
253
- });
254
- } else {
255
- clearTimeout(timeout);
256
- resolve();
257
- }
258
- });
259
-
260
- // 执行reCAPTCHA v3
261
- const token = await window.grecaptcha.execute(websiteKey, {
262
- action: 'FLOW_GENERATION'
263
- });
264
-
265
- return token;
266
- } catch (error) {
267
- console.error('[BrowserCaptcha] reCAPTCHA执行错误:', error);
268
- return null;
269
- }
270
- }
271
- """, self.website_key)
272
-
273
- duration_ms = (time.time() - start_time) * 1000
274
-
275
- if token:
276
- debug_logger.log_info(f"[BrowserCaptcha] ✅ Token获取成功(耗时 {duration_ms:.0f}ms)")
277
- return token
278
- else:
279
- debug_logger.log_error("[BrowserCaptcha] Token获取失败(返回null)")
280
- return None
281
-
282
  except Exception as e:
283
- debug_logger.log_error(f"[BrowserCaptcha] 获取token异常: {str(e)}")
 
284
  return None
285
  finally:
286
- # 关闭上下文
287
- if context:
288
- try:
289
- await context.close()
290
- except:
291
- pass
292
-
293
- async def close(self):
294
- """关闭浏览器"""
295
- try:
296
- if self.browser:
 
 
297
  try:
298
- await self.browser.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  except Exception as e:
300
- # 忽略连接关闭错误(正常关闭场景)
301
- if "Connection closed" not in str(e):
302
- debug_logger.log_warning(f"[BrowserCaptcha] 关闭浏览器时出现异常: {str(e)}")
303
  finally:
304
- self.browser = None
 
 
 
 
 
 
 
 
305
 
306
- if self.playwright:
307
- try:
308
- await self.playwright.stop()
309
- except Exception:
310
- pass # 静默处理 playwright 停止异常
311
- finally:
312
- self.playwright = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
 
314
- self._initialized = False
315
- debug_logger.log_info("[BrowserCaptcha] 浏览器已关闭")
316
- except Exception as e:
317
- debug_logger.log_error(f"[BrowserCaptcha] 关闭浏览器异常: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ 基于 RT 的本地 reCAPTCHA 打码服务 (终极闭环版 - 无 fake_useragent 纯净版)
3
+ 支持:自动刷新 Session Token、外部触发指纹切换、死磕重试
4
  """
5
  import asyncio
6
  import time
7
+ import os
8
  import re
9
+ import random
10
+ from pathlib import Path
11
  from typing import Optional, Dict
12
+ from datetime import datetime
13
+ from urllib.parse import urlparse, unquote
 
14
 
15
+ from playwright.async_api import async_playwright, Route, BrowserContext
16
 
17
+ from ..core.logger import debug_logger
 
18
 
 
 
19
 
20
+ # 配置
21
+ LABS_URL = "https://labs.google/fx/tools/flow"
 
 
 
22
 
23
+ # ==========================================
24
+ # 代理解析工具函数
25
+ # ==========================================
26
+ def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]:
27
+ """解析代理URL"""
28
+ if not proxy_url: return None
29
+ if not re.match(r'^(http|https|socks5)://', proxy_url): proxy_url = f"http://{proxy_url}"
30
+ match = re.match(r'^(socks5|http|https)://(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$', proxy_url)
31
  if match:
32
  protocol, username, password, host, port = match.groups()
33
  proxy_config = {'server': f'{protocol}://{host}:{port}'}
 
34
  if username and password:
35
  proxy_config['username'] = username
36
  proxy_config['password'] = password
 
37
  return proxy_config
38
  return None
39
 
 
40
  def validate_browser_proxy_url(proxy_url: str) -> tuple[bool, str]:
41
+ if not proxy_url: return True, None
 
 
 
 
 
 
 
 
 
 
 
42
  parsed = parse_proxy_url(proxy_url)
43
+ if not parsed: return False, "代理格式错误"
44
+ return True, None
45
 
46
+ class TokenBrowser:
47
+ """简化版浏览器每次获取 token 时启动新浏览器,用完即关
48
+
49
+ 每次都新的随机 UA,避免长时间运行导致的各种问题
50
+ """
51
+
52
+ # UA 池
53
+ UA_LIST = [
54
+ # Windows Chrome (120-132)
55
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
56
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
57
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
58
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
59
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
60
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
61
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
62
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
63
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
64
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
65
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
66
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
67
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
68
+ # Windows Chrome 完整版本号
69
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.83 Safari/537.36",
70
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.139 Safari/537.36",
71
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.117 Safari/537.36",
72
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.100 Safari/537.36",
73
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.138 Safari/537.36",
74
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.120 Safari/537.36",
75
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36",
76
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.141 Safari/537.36",
77
+ "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
78
+ # Windows Edge (120-132)
79
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0",
80
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
81
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0",
82
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
83
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0",
84
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0",
85
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0",
86
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
87
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
88
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0",
89
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.83 Safari/537.36 Edg/132.0.2957.115",
90
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.139 Safari/537.36 Edg/131.0.2903.99",
91
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.117 Safari/537.36 Edg/130.0.2849.80",
92
+ # macOS Chrome (120-132)
93
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
94
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
95
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
96
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
97
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
98
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
99
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
100
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
101
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
102
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
103
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
104
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
105
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
106
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
107
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
108
+ # macOS Safari
109
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15",
110
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Safari/605.1.15",
111
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15",
112
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15",
113
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15",
114
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15",
115
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
116
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
117
+ # macOS Edge
118
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0",
119
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
120
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0",
121
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
122
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0",
123
+ # Linux Chrome
124
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
125
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
126
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
127
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
128
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
129
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
130
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
131
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
132
+ "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
133
+ "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
134
+ "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
135
+ # Linux Firefox
136
+ "Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
137
+ "Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0",
138
+ "Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0",
139
+ "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0",
140
+ "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0",
141
+ "Mozilla/5.0 (X11; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0",
142
+ "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
143
+ "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
144
+ "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0",
145
+ "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0",
146
+ "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
147
+ # Windows Firefox
148
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0",
149
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0",
150
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0",
151
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
152
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0",
153
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0",
154
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
155
+ # macOS Firefox
156
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.4; rv:134.0) Gecko/20100101 Firefox/134.0",
157
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.3; rv:133.0) Gecko/20100101 Firefox/133.0",
158
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.2; rv:132.0) Gecko/20100101 Firefox/132.0",
159
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:134.0) Gecko/20100101 Firefox/134.0",
160
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0",
161
+ # Opera
162
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 OPR/116.0.0.0",
163
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0",
164
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 OPR/114.0.0.0",
165
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/113.0.0.0",
166
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 OPR/112.0.0.0",
167
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 OPR/116.0.0.0",
168
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0",
169
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 OPR/116.0.0.0",
170
+ # Brave
171
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Brave/131",
172
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Brave/130",
173
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Brave/131",
174
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Brave/131",
175
+ # Vivaldi
176
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Vivaldi/6.9.3447.54",
177
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Vivaldi/6.8.3381.55",
178
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Vivaldi/6.9.3447.54",
179
+ ]
180
+
181
+ # 分辨率池
182
+ RESOLUTIONS = [
183
+ (1920, 1080), (2560, 1440), (3840, 2160), (1366, 768), (1536, 864),
184
+ (1600, 900), (1280, 720), (1360, 768), (1920, 1200),
185
+ (1440, 900), (1680, 1050), (1280, 800), (2560, 1600),
186
+ (2880, 1800), (3024, 1890), (3456, 2160),
187
+ (1280, 1024), (1024, 768), (1400, 1050),
188
+ (1920, 1280), (2736, 1824), (2880, 1920), (3000, 2000),
189
+ (2256, 1504), (2496, 1664), (3240, 2160),
190
+ (3200, 1800), (2304, 1440), (1800, 1200),
191
+ ]
192
+
193
+ def __init__(self, token_id: int, user_data_dir: str, db=None):
194
+ self.token_id = token_id
195
+ self.user_data_dir = user_data_dir
196
  self.db = db
197
+ self._semaphore = asyncio.Semaphore(1) # 同时只能有一个任务
198
+ self._solve_count = 0
199
+ self._error_count = 0
200
+
201
+ async def _create_browser(self) -> tuple:
202
+ """创建新浏览器实例(新 UA),返回 (playwright, browser, context)"""
203
+ import random
204
+
205
+ random_ua = random.choice(self.UA_LIST)
206
+ base_w, base_h = random.choice(self.RESOLUTIONS)
207
+ width, height = base_w, base_h - random.randint(0, 80)
208
+ viewport = {"width": width, "height": height}
209
+
210
+ playwright = await async_playwright().start()
211
+ Path(self.user_data_dir).mkdir(parents=True, exist_ok=True)
212
+
213
+ # 代理配置
214
+ proxy_option = None
215
  try:
 
 
216
  if self.db:
217
  captcha_config = await self.db.get_captcha_config()
218
+ raw_url = captcha_config.browser_proxy_enabled and captcha_config.browser_proxy_url
219
+ if raw_url:
220
+ proxy_option = parse_proxy_url(raw_url.strip())
221
+ if proxy_option:
222
+ debug_logger.log_info(f"[BrowserCaptcha] Token-{self.token_id} 使用代理: {proxy_option['server']}")
223
+ except: pass
224
+
225
+ try:
226
+ browser = await playwright.chromium.launch(
227
+ headless=False,
228
+ proxy=proxy_option,
229
+ args=[
230
  '--disable-blink-features=AutomationControlled',
 
231
  '--no-sandbox',
232
+ '--disable-dev-shm-usage',
233
+ '--disable-setuid-sandbox',
234
+ '--no-first-run',
235
+ '--no-zygote',
236
+ f'--window-size={width},{height}',
237
+ '--disable-infobars',
238
+ '--hide-scrollbars',
239
  ]
240
+ )
241
+ context = await browser.new_context(
242
+ user_agent=random_ua,
243
+ viewport=viewport,
244
+ )
245
+ return playwright, browser, context
 
 
 
 
 
 
 
 
 
246
  except Exception as e:
247
+ debug_logger.log_error(f"[BrowserCaptcha] Token-{self.token_id} 启动浏览器失败: {type(e).__name__}: {str(e)[:200]}")
248
+ # 确保清理已创建的对象
249
+ try:
250
+ if playwright:
251
+ await playwright.stop()
252
+ except: pass
253
  raise
254
+
255
+ async def _close_browser(self, playwright, browser, context):
256
+ """关闭浏览器实例"""
257
+ try:
258
+ if context:
259
+ await context.close()
260
+ except: pass
261
+ try:
262
+ if browser:
263
+ await browser.close()
264
+ except: pass
265
+ try:
266
+ if playwright:
267
+ await playwright.stop()
268
+ except: pass
269
+
270
+ async def _execute_captcha(self, context, project_id: str, website_key: str, action: str) -> Optional[str]:
271
+ """在给定 context 中执行打码逻辑"""
272
+ page = None
273
  try:
 
 
 
 
 
 
 
274
  page = await context.new_page()
275
+ await page.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined});")
276
+
277
+ page_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
278
+
279
+ async def handle_route(route):
280
+ if route.request.url.rstrip('/') == page_url.rstrip('/'):
281
+ html = f"""<html><head><script src="https://www.google.com/recaptcha/enterprise.js?render={website_key}"></script></head><body></body></html>"""
282
+ await route.fulfill(status=200, content_type="text/html", body=html)
283
+ elif any(d in route.request.url for d in ["google.com", "gstatic.com", "recaptcha.net"]):
284
+ await route.continue_()
285
+ else:
286
+ await route.abort()
287
+
288
+ await page.route("**/*", handle_route)
289
  try:
290
+ await page.goto(page_url, wait_until="load", timeout=30000)
291
  except Exception as e:
292
+ debug_logger.log_warning(f"[BrowserCaptcha] Token-{self.token_id} page.goto 失败: {type(e).__name__}: {str(e)[:200]}")
293
+ return None
294
+
295
+ try:
296
+ await page.wait_for_function("typeof grecaptcha !== 'undefined'", timeout=15000)
297
+ except Exception as e:
298
+ debug_logger.log_warning(f"[BrowserCaptcha] Token-{self.token_id} grecaptcha 未就绪: {type(e).__name__}: {str(e)[:200]}")
299
+ return None
300
+
301
+ token = await asyncio.wait_for(
302
+ page.evaluate(f"""
303
+ (actionName) => {{
304
+ return new Promise((resolve, reject) => {{
305
+ const timeout = setTimeout(() => reject(new Error('timeout')), 25000);
306
+ grecaptcha.enterprise.execute('{website_key}', {{action: actionName}})
307
+ .then(t => {{ resolve(t); }})
308
+ .catch(e => {{ reject(e); }});
 
 
 
 
 
 
 
 
 
309
  }});
310
  }}
311
+ """, action),
312
+ timeout=30
313
+ )
314
+ return token
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  except Exception as e:
316
+ msg = f"{type(e).__name__}: {str(e)}"
317
+ debug_logger.log_warning(f"[BrowserCaptcha] Token-{self.token_id} 打码失败: {msg[:200]}")
318
  return None
319
  finally:
320
+ if page:
321
+ try: await page.close()
322
+ except: pass
323
+
324
+ async def get_token(self, project_id: str, website_key: str, action: str = "IMAGE_GENERATION") -> Optional[str]:
325
+ """获取 Token:启动新浏览器 -> 打码 -> 关闭浏览器"""
326
+ async with self._semaphore:
327
+ MAX_RETRIES = 3
328
+
329
+ for attempt in range(MAX_RETRIES):
330
+ playwright = None
331
+ browser = None
332
+ context = None
333
  try:
334
+ start_ts = time.time()
335
+
336
+ # 每次都启动新浏览器(新 UA)
337
+ playwright, browser, context = await self._create_browser()
338
+
339
+ # 执行打码
340
+ token = await self._execute_captcha(context, project_id, website_key, action)
341
+
342
+ if token:
343
+ self._solve_count += 1
344
+ debug_logger.log_info(f"[BrowserCaptcha] Token-{self.token_id} 获取成功 ({(time.time()-start_ts)*1000:.0f}ms)")
345
+ return token
346
+
347
+ self._error_count += 1
348
+ debug_logger.log_warning(f"[BrowserCaptcha] Token-{self.token_id} 尝试 {attempt+1}/{MAX_RETRIES} 失败")
349
+
350
  except Exception as e:
351
+ self._error_count += 1
352
+ debug_logger.log_error(f"[BrowserCaptcha] Token-{self.token_id} 浏览器错误: {type(e).__name__}: {str(e)[:200]}")
 
353
  finally:
354
+ # 无论成功失败都关闭浏览器
355
+ await self._close_browser(playwright, browser, context)
356
+
357
+ # 重试前等待
358
+ if attempt < MAX_RETRIES - 1:
359
+ await asyncio.sleep(1)
360
+
361
+ return None
362
+
363
 
364
+ class BrowserCaptchaService:
365
+ """多浏览器轮询打码服务(单例模式)
366
+
367
+ 支持配置浏览器数量,每个浏览器只开 1 个标签页,请求轮询分配
368
+ """
369
+
370
+ _instance: Optional['BrowserCaptchaService'] = None
371
+ _lock = asyncio.Lock()
372
+
373
+ def __init__(self, db=None):
374
+ self.db = db
375
+ self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
376
+ self.base_user_data_dir = os.path.join(os.getcwd(), "browser_data_rt")
377
+ self._browsers: Dict[int, TokenBrowser] = {}
378
+ self._browsers_lock = asyncio.Lock()
379
+
380
+ # 浏览器数量配置
381
+ self._browser_count = 1 # 默认 1 个,会从数���库加载
382
+ self._round_robin_index = 0 # 轮询索引
383
+
384
+ # 统计指标
385
+ self._stats = {
386
+ "req_total": 0,
387
+ "gen_ok": 0,
388
+ "gen_fail": 0,
389
+ "api_403": 0
390
+ }
391
+
392
+ # 并发限制将在 _load_browser_count 中根据配置设置
393
+ self._token_semaphore = None
394
+
395
+ @classmethod
396
+ async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
397
+ if cls._instance is None:
398
+ async with cls._lock:
399
+ if cls._instance is None:
400
+ cls._instance = cls(db)
401
+ # 从数据库加载 browser_count 配置
402
+ await cls._instance._load_browser_count()
403
+ return cls._instance
404
+
405
+ async def _load_browser_count(self):
406
+ """从数据库加载浏览器数量配置"""
407
+ if self.db:
408
+ try:
409
+ captcha_config = await self.db.get_captcha_config()
410
+ self._browser_count = max(1, captcha_config.browser_count)
411
+ debug_logger.log_info(f"[BrowserCaptcha] 浏览器数量配置: {self._browser_count}")
412
+ except Exception as e:
413
+ debug_logger.log_warning(f"[BrowserCaptcha] 加载 browser_count 配置失败: {e},使用默认值 1")
414
+ self._browser_count = 1
415
+ # 并发限制 = 浏览器数量,不再硬编码限制
416
+ self._token_semaphore = asyncio.Semaphore(self._browser_count)
417
+ debug_logger.log_info(f"[BrowserCaptcha] 并发上限: {self._browser_count}")
418
+
419
+ async def reload_browser_count(self):
420
+ """重新加载浏览器数量配置(用于配置更新后热重载)"""
421
+ old_count = self._browser_count
422
+ await self._load_browser_count()
423
+
424
+ # 如果数量减少,移除多余的浏览器实例
425
+ if self._browser_count < old_count:
426
+ async with self._browsers_lock:
427
+ for browser_id in list(self._browsers.keys()):
428
+ if browser_id >= self._browser_count:
429
+ self._browsers.pop(browser_id)
430
+ debug_logger.log_info(f"[BrowserCaptcha] 移除多余浏览器实例 {browser_id}")
431
+
432
+ def _log_stats(self):
433
+ total = self._stats["req_total"]
434
+ gen_fail = self._stats["gen_fail"]
435
+ api_403 = self._stats["api_403"]
436
+ gen_ok = self._stats["gen_ok"]
437
+
438
+ valid_success = gen_ok - api_403
439
+ if valid_success < 0: valid_success = 0
440
+
441
+ rate = (valid_success / total * 100) if total > 0 else 0.0
442
+
443
+
444
+ async def _get_or_create_browser(self, browser_id: int) -> TokenBrowser:
445
+ """获取或创建指定 ID 的浏览器实例"""
446
+ async with self._browsers_lock:
447
+ if browser_id not in self._browsers:
448
+ user_data_dir = os.path.join(self.base_user_data_dir, f"browser_{browser_id}")
449
+ browser = TokenBrowser(browser_id, user_data_dir, db=self.db)
450
+ self._browsers[browser_id] = browser
451
+ debug_logger.log_info(f"[BrowserCaptcha] 创建浏览器实例 {browser_id}")
452
+ return self._browsers[browser_id]
453
+
454
+ def _get_next_browser_id(self) -> int:
455
+ """轮询获取下一个浏览器 ID"""
456
+ browser_id = self._round_robin_index % self._browser_count
457
+ self._round_robin_index += 1
458
+ return browser_id
459
+
460
+ async def get_token(self, project_id: str, action: str = "IMAGE_GENERATION", token_id: int = None) -> tuple[Optional[str], int]:
461
+ """获取 reCAPTCHA Token(轮询分配到不同浏览器)
462
+
463
+ Args:
464
+ project_id: 项目 ID
465
+ action: reCAPTCHA action
466
+ token_id: 忽略,使用轮询分配
467
+
468
+ Returns:
469
+ (token, browser_id) 元组,调用方失败时用 browser_id 调用 report_error
470
+ """
471
+ self._stats["req_total"] += 1
472
+
473
+ # 全局并发限制(如果已配置)
474
+ if self._token_semaphore:
475
+ async with self._token_semaphore:
476
+ # 轮询选择浏览器
477
+ browser_id = self._get_next_browser_id()
478
+ browser = await self._get_or_create_browser(browser_id)
479
+
480
+ token = await browser.get_token(project_id, self.website_key, action)
481
+
482
+ if token:
483
+ self._stats["gen_ok"] += 1
484
+ else:
485
+ self._stats["gen_fail"] += 1
486
+
487
+ self._log_stats()
488
+ return token, browser_id
489
+
490
+ # 无并发限制时直接执行
491
+ browser_id = self._get_next_browser_id()
492
+ browser = await self._get_or_create_browser(browser_id)
493
+
494
+ token = await browser.get_token(project_id, self.website_key, action)
495
+
496
+ if token:
497
+ self._stats["gen_ok"] += 1
498
+ else:
499
+ self._stats["gen_fail"] += 1
500
+
501
+ self._log_stats()
502
+ return token, browser_id
503
+
504
+ async def report_error(self, browser_id: int = None):
505
+ """上层举报:Token 无效(统计用)
506
+
507
+ Args:
508
+ browser_id: 浏览器 ID(当前架构下每次都是新浏览器,此参数仅用于日志)
509
+ """
510
+ async with self._browsers_lock:
511
+ self._stats["api_403"] += 1
512
+ if browser_id is not None:
513
+ debug_logger.log_info(f"[BrowserCaptcha] 浏览器 {browser_id} 的 token 验证失败")
514
 
515
+ async def remove_browser(self, browser_id: int):
516
+ async with self._browsers_lock:
517
+ if browser_id in self._browsers:
518
+ self._browsers.pop(browser_id)
519
+
520
+ async def close(self):
521
+ async with self._browsers_lock:
522
+ self._browsers.clear()
523
+
524
+ async def open_login_browser(self): return {"success": False, "error": "Not implemented"}
525
+ async def create_browser_for_token(self, t, s=None): pass
526
+ def get_stats(self):
527
+ base_stats = {
528
+ "total_solve_count": self._stats["gen_ok"],
529
+ "total_error_count": self._stats["gen_fail"],
530
+ "risk_403_count": self._stats["api_403"],
531
+ "browser_count": len(self._browsers),
532
+ "configured_browser_count": self._browser_count,
533
+ "browsers": []
534
+ }
535
+ return base_stats
src/services/file_cache.py CHANGED
@@ -264,6 +264,41 @@ class FileCache:
264
  )
265
  raise Exception(f"Failed to cache file: {str(e)}")
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  def get_cache_path(self, filename: str) -> Path:
268
  """Get full path to cached file"""
269
  return self.cache_dir / filename
 
264
  )
265
  raise Exception(f"Failed to cache file: {str(e)}")
266
 
267
+ async def cache_base64_image(self, base64_data: str, resolution: str = "") -> str:
268
+ """
269
+ Cache base64 encoded image data to local file
270
+
271
+ Args:
272
+ base64_data: Base64 encoded image data (without data:image/... prefix)
273
+ resolution: Resolution info for filename (e.g., "4K", "2K")
274
+
275
+ Returns:
276
+ Local cache filename
277
+ """
278
+ import base64
279
+ import uuid
280
+
281
+ # Generate unique filename
282
+ unique_id = hashlib.md5(f"{uuid.uuid4()}{time.time()}".encode()).hexdigest()
283
+ suffix = f"_{resolution}" if resolution else ""
284
+ filename = f"{unique_id}{suffix}.jpg"
285
+ file_path = self.cache_dir / filename
286
+
287
+ try:
288
+ # Decode base64 and save to file
289
+ image_data = base64.b64decode(base64_data)
290
+ with open(file_path, 'wb') as f:
291
+ f.write(image_data)
292
+ debug_logger.log_info(f"Base64 image cached: {filename} ({len(image_data)} bytes)")
293
+ return filename
294
+ except Exception as e:
295
+ debug_logger.log_error(
296
+ error_message=f"Failed to cache base64 image: {str(e)}",
297
+ status_code=0,
298
+ response_text=""
299
+ )
300
+ raise Exception(f"Failed to cache base64 image: {str(e)}")
301
+
302
  def get_cache_path(self, filename: str) -> Path:
303
  """Get full path to cached file"""
304
  return self.cache_dir / filename
src/services/flow_client.py CHANGED
@@ -1,4 +1,5 @@
1
  """Flow API Client for VideoFX (Veo)"""
 
2
  import time
3
  import uuid
4
  import random
@@ -21,6 +22,21 @@ class FlowClient:
21
  # 缓存每个账号的 User-Agent
22
  self._user_agent_cache = {}
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  def _generate_user_agent(self, account_id: str = None) -> str:
25
  """基于账号ID生成固定的 User-Agent
26
 
@@ -102,7 +118,8 @@ class FlowClient:
102
  use_st: bool = False,
103
  st_token: Optional[str] = None,
104
  use_at: bool = False,
105
- at_token: Optional[str] = None
 
106
  ) -> Dict[str, Any]:
107
  """统一HTTP请求处理
108
 
@@ -115,8 +132,10 @@ class FlowClient:
115
  st_token: Session Token
116
  use_at: 是否使用AT认证 (Bearer方式)
117
  at_token: Access Token
 
118
  """
119
  proxy_url = await self.proxy_manager.get_proxy_url()
 
120
 
121
  if headers is None:
122
  headers = {}
@@ -142,6 +161,10 @@ class FlowClient:
142
  "User-Agent": self._generate_user_agent(account_id)
143
  })
144
 
 
 
 
 
145
  # Log request
146
  if config.debug_enabled:
147
  debug_logger.log_request(
@@ -161,7 +184,7 @@ class FlowClient:
161
  url,
162
  headers=headers,
163
  proxy=proxy_url,
164
- timeout=self.timeout,
165
  impersonate="chrome110"
166
  )
167
  else: # POST
@@ -170,7 +193,7 @@ class FlowClient:
170
  headers=headers,
171
  json=json_data,
172
  proxy=proxy_url,
173
- timeout=self.timeout,
174
  impersonate="chrome110"
175
  )
176
 
@@ -185,19 +208,45 @@ class FlowClient:
185
  duration_ms=duration_ms
186
  )
187
 
188
- response.raise_for_status()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  return response.json()
190
 
191
  except Exception as e:
192
  duration_ms = (time.time() - start_time) * 1000
193
  error_msg = str(e)
194
 
195
- if config.debug_enabled:
196
- debug_logger.log_error(
197
- error_message=error_msg,
198
- status_code=getattr(e, 'status_code', None),
199
- response_text=getattr(e, 'response_text', None)
200
- )
201
 
202
  raise Exception(f"Flow API request failed: {error_msg}")
203
 
@@ -304,6 +353,60 @@ class FlowClient:
304
 
305
  # ========== 图片上传 (使用AT) ==========
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  async def upload_image(
308
  self,
309
  at: str,
@@ -326,6 +429,9 @@ class FlowClient:
326
  if aspect_ratio.startswith("VIDEO_"):
327
  aspect_ratio = aspect_ratio.replace("VIDEO_", "IMAGE_")
328
 
 
 
 
329
  # 编码为base64 (去掉前缀)
330
  image_base64 = base64.b64encode(image_bytes).decode('utf-8')
331
 
@@ -333,7 +439,7 @@ class FlowClient:
333
  json_data = {
334
  "imageInput": {
335
  "rawImageBytes": image_base64,
336
- "mimeType": "image/jpeg",
337
  "isUserUploaded": True,
338
  "aspectRatio": aspect_ratio
339
  },
@@ -390,42 +496,115 @@ class FlowClient:
390
  """
391
  url = f"{self.api_base_url}/projects/{project_id}/flowMedia:batchGenerateImages"
392
 
393
- # 获取 reCAPTCHA token
394
- recaptcha_token = await self._get_recaptcha_token(project_id) or ""
395
- session_id = self._generate_session_id()
396
-
397
- # 构建请求
398
- request_data = {
399
- "clientContext": {
400
- "recaptchaToken": recaptcha_token,
401
- "projectId": project_id,
 
 
 
 
 
 
 
402
  "sessionId": session_id,
 
403
  "tool": "PINHOLE"
404
- },
405
- "seed": random.randint(1, 99999),
406
- "imageModelName": model_name,
407
- "imageAspectRatio": aspect_ratio,
408
- "prompt": prompt,
409
- "imageInputs": image_inputs or []
410
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
 
412
  json_data = {
 
 
413
  "clientContext": {
414
- "recaptchaToken": recaptcha_token,
415
- "sessionId": session_id
416
- },
417
- "requests": [request_data]
 
 
 
 
418
  }
419
 
 
420
  result = await self._make_request(
421
  method="POST",
422
  url=url,
423
  json_data=json_data,
424
  use_at=True,
425
- at_token=at
 
426
  )
427
 
428
- return result
 
429
 
430
  # ========== 视频生成 (使用AT) - 异步返回 ==========
431
 
@@ -460,41 +639,64 @@ class FlowClient:
460
  """
461
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoText"
462
 
463
- # 获取 reCAPTCHA token
464
- recaptcha_token = await self._get_recaptcha_token(project_id) or ""
465
- session_id = self._generate_session_id()
466
- scene_id = str(uuid.uuid4())
467
-
468
- json_data = {
469
- "clientContext": {
470
- "recaptchaToken": recaptcha_token,
471
- "sessionId": session_id,
472
- "projectId": project_id,
473
- "tool": "PINHOLE",
474
- "userPaygateTier": user_paygate_tier
475
- },
476
- "requests": [{
477
- "aspectRatio": aspect_ratio,
478
- "seed": random.randint(1, 99999),
479
- "textInput": {
480
- "prompt": prompt
 
 
 
481
  },
482
- "videoModelKey": model_key,
483
- "metadata": {
484
- "sceneId": scene_id
485
- }
486
- }]
487
- }
488
-
489
- result = await self._make_request(
490
- method="POST",
491
- url=url,
492
- json_data=json_data,
493
- use_at=True,
494
- at_token=at
495
- )
496
 
497
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
 
499
  async def generate_video_reference_images(
500
  self,
@@ -522,42 +724,65 @@ class FlowClient:
522
  """
523
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoReferenceImages"
524
 
525
- # 获取 reCAPTCHA token
526
- recaptcha_token = await self._get_recaptcha_token(project_id) or ""
527
- session_id = self._generate_session_id()
528
- scene_id = str(uuid.uuid4())
529
-
530
- json_data = {
531
- "clientContext": {
532
- "recaptchaToken": recaptcha_token,
533
- "sessionId": session_id,
534
- "projectId": project_id,
535
- "tool": "PINHOLE",
536
- "userPaygateTier": user_paygate_tier
537
- },
538
- "requests": [{
539
- "aspectRatio": aspect_ratio,
540
- "seed": random.randint(1, 99999),
541
- "textInput": {
542
- "prompt": prompt
 
 
 
543
  },
544
- "videoModelKey": model_key,
545
- "referenceImages": reference_images,
546
- "metadata": {
547
- "sceneId": scene_id
548
- }
549
- }]
550
- }
551
-
552
- result = await self._make_request(
553
- method="POST",
554
- url=url,
555
- json_data=json_data,
556
- use_at=True,
557
- at_token=at
558
- )
559
 
560
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
 
562
  async def generate_video_start_end(
563
  self,
@@ -587,47 +812,70 @@ class FlowClient:
587
  """
588
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
589
 
590
- # 获取 reCAPTCHA token
591
- recaptcha_token = await self._get_recaptcha_token(project_id) or ""
592
- session_id = self._generate_session_id()
593
- scene_id = str(uuid.uuid4())
594
-
595
- json_data = {
596
- "clientContext": {
597
- "recaptchaToken": recaptcha_token,
598
- "sessionId": session_id,
599
- "projectId": project_id,
600
- "tool": "PINHOLE",
601
- "userPaygateTier": user_paygate_tier
602
- },
603
- "requests": [{
604
- "aspectRatio": aspect_ratio,
605
- "seed": random.randint(1, 99999),
606
- "textInput": {
607
- "prompt": prompt
608
- },
609
- "videoModelKey": model_key,
610
- "startImage": {
611
- "mediaId": start_media_id
612
- },
613
- "endImage": {
614
- "mediaId": end_media_id
615
  },
616
- "metadata": {
617
- "sceneId": scene_id
618
- }
619
- }]
620
- }
621
-
622
- result = await self._make_request(
623
- method="POST",
624
- url=url,
625
- json_data=json_data,
626
- use_at=True,
627
- at_token=at
628
- )
 
 
 
 
 
629
 
630
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
 
632
  async def generate_video_start_image(
633
  self,
@@ -655,45 +903,149 @@ class FlowClient:
655
  """
656
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartImage"
657
 
658
- # 获取 reCAPTCHA token
659
- recaptcha_token = await self._get_recaptcha_token(project_id) or ""
660
- session_id = self._generate_session_id()
661
- scene_id = str(uuid.uuid4())
662
-
663
- json_data = {
664
- "clientContext": {
665
- "recaptchaToken": recaptcha_token,
666
- "sessionId": session_id,
667
- "projectId": project_id,
668
- "tool": "PINHOLE",
669
- "userPaygateTier": user_paygate_tier
670
- },
671
- "requests": [{
672
- "aspectRatio": aspect_ratio,
673
- "seed": random.randint(1, 99999),
674
- "textInput": {
675
- "prompt": prompt
676
- },
677
- "videoModelKey": model_key,
678
- "startImage": {
679
- "mediaId": start_media_id
680
  },
681
- # 注意: 没有endImage字段,只用首帧
682
- "metadata": {
683
- "sceneId": scene_id
684
- }
685
- }]
686
- }
 
 
 
 
 
 
 
 
 
 
687
 
688
- result = await self._make_request(
689
- method="POST",
690
- url=url,
691
- json_data=json_data,
692
- use_at=True,
693
- at_token=at
694
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
 
696
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697
 
698
  # ========== 任务轮询 (使用AT) ==========
699
 
@@ -757,6 +1109,31 @@ class FlowClient:
757
 
758
  # ========== 辅助方法 ==========
759
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
  def _generate_session_id(self) -> str:
761
  """生成sessionId: ;timestamp"""
762
  return f";{int(time.time() * 1000)}"
@@ -765,8 +1142,19 @@ class FlowClient:
765
  """生成sceneId: UUID"""
766
  return str(uuid.uuid4())
767
 
768
- async def _get_recaptcha_token(self, project_id: str) -> Optional[str]:
769
- """获取reCAPTCHA token - 支持多种打码方式"""
 
 
 
 
 
 
 
 
 
 
 
770
  captcha_method = config.captcha_method
771
 
772
  # 恒定浏览器打码
@@ -774,25 +1162,26 @@ class FlowClient:
774
  try:
775
  from .browser_captcha_personal import BrowserCaptchaService
776
  service = await BrowserCaptchaService.get_instance(self.db)
777
- return await service.get_token(project_id)
778
  except Exception as e:
779
  debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
780
- return None
781
- # 头浏览器打码
782
  elif captcha_method == "browser":
783
  try:
784
  from .browser_captcha import BrowserCaptchaService
785
  service = await BrowserCaptchaService.get_instance(self.db)
786
- return await service.get_token(project_id)
787
  except Exception as e:
788
  debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
789
- return None
790
  # API打码服务
791
  elif captcha_method in ["yescaptcha", "capmonster", "ezcaptcha", "capsolver"]:
792
- return await self._get_api_captcha_token(captcha_method, project_id)
 
793
  else:
794
- debug_logger.log_error(f"[reCAPTCHA] Unknown captcha method: {captcha_method}")
795
- return None
796
 
797
  async def _get_api_captcha_token(self, method: str, project_id: str) -> Optional[str]:
798
  """通用API打码服务"""
@@ -804,11 +1193,11 @@ class FlowClient:
804
  elif method == "capmonster":
805
  client_key = config.capmonster_api_key
806
  base_url = config.capmonster_base_url
807
- task_type = "RecaptchaV3EnterpriseTask"
808
  elif method == "ezcaptcha":
809
  client_key = config.ezcaptcha_api_key
810
  base_url = config.ezcaptcha_base_url
811
- task_type = "ReCaptchaV3EnterpriseTaskProxyless"
812
  elif method == "capsolver":
813
  client_key = config.capsolver_api_key
814
  base_url = config.capsolver_base_url
 
1
  """Flow API Client for VideoFX (Veo)"""
2
+ import asyncio
3
  import time
4
  import uuid
5
  import random
 
22
  # 缓存每个账号的 User-Agent
23
  self._user_agent_cache = {}
24
 
25
+ # Default "real browser" headers (Android Chrome style) to reduce upstream 4xx/5xx instability.
26
+ # These will be applied as defaults (won't override caller-provided headers).
27
+ self._default_client_headers = {
28
+ "sec-ch-ua-mobile": "?1",
29
+ "sec-ch-ua-platform": "\"Android\"",
30
+ "sec-fetch-dest": "empty",
31
+ "sec-fetch-mode": "cors",
32
+ "sec-fetch-site": "cross-site",
33
+ "x-browser-channel": "stable",
34
+ "x-browser-copyright": "Copyright 2026 Google LLC. All Rights reserved.",
35
+ "x-browser-validation": "UujAs0GAwdnCJ9nvrswZ+O+oco0=",
36
+ "x-browser-year": "2026",
37
+ "x-client-data": "CJS2yQEIpLbJAQipncoBCNj9ygEIlKHLAQiFoM0BGP6lzwE="
38
+ }
39
+
40
  def _generate_user_agent(self, account_id: str = None) -> str:
41
  """基于账号ID生成固定的 User-Agent
42
 
 
118
  use_st: bool = False,
119
  st_token: Optional[str] = None,
120
  use_at: bool = False,
121
+ at_token: Optional[str] = None,
122
+ timeout: Optional[int] = None
123
  ) -> Dict[str, Any]:
124
  """统一HTTP请求处理
125
 
 
132
  st_token: Session Token
133
  use_at: 是否使用AT认证 (Bearer方式)
134
  at_token: Access Token
135
+ timeout: 自定义超时时间(秒),不传则使用默认值
136
  """
137
  proxy_url = await self.proxy_manager.get_proxy_url()
138
+ request_timeout = timeout or self.timeout
139
 
140
  if headers is None:
141
  headers = {}
 
161
  "User-Agent": self._generate_user_agent(account_id)
162
  })
163
 
164
+ # Add default Chromium/Android client headers (do not override explicitly provided values).
165
+ for key, value in self._default_client_headers.items():
166
+ headers.setdefault(key, value)
167
+
168
  # Log request
169
  if config.debug_enabled:
170
  debug_logger.log_request(
 
184
  url,
185
  headers=headers,
186
  proxy=proxy_url,
187
+ timeout=request_timeout,
188
  impersonate="chrome110"
189
  )
190
  else: # POST
 
193
  headers=headers,
194
  json=json_data,
195
  proxy=proxy_url,
196
+ timeout=request_timeout,
197
  impersonate="chrome110"
198
  )
199
 
 
208
  duration_ms=duration_ms
209
  )
210
 
211
+ # 检查HTTP错误
212
+ if response.status_code >= 400:
213
+ # 解析错误响应
214
+ error_reason = f"HTTP Error {response.status_code}"
215
+ try:
216
+ error_body = response.json()
217
+ # 提取 Google API 错误格式中的 reason
218
+ if "error" in error_body:
219
+ error_info = error_body["error"]
220
+ error_message = error_info.get("message", "")
221
+ # 从 details 中提取 reason
222
+ details = error_info.get("details", [])
223
+ for detail in details:
224
+ if detail.get("reason"):
225
+ error_reason = detail.get("reason")
226
+ break
227
+ if error_message:
228
+ error_reason = f"{error_reason}: {error_message}"
229
+ except:
230
+ error_reason = f"HTTP Error {response.status_code}: {response.text[:200]}"
231
+
232
+ # 失败时输出请求体和错误内容到控制台
233
+ debug_logger.log_error(f"[API FAILED] URL: {url}")
234
+ debug_logger.log_error(f"[API FAILED] Request Body: {json_data}")
235
+ debug_logger.log_error(f"[API FAILED] Response: {response.text}")
236
+
237
+ raise Exception(error_reason)
238
+
239
  return response.json()
240
 
241
  except Exception as e:
242
  duration_ms = (time.time() - start_time) * 1000
243
  error_msg = str(e)
244
 
245
+ # 如果不是我们自己抛出的异常,记录日志
246
+ if "HTTP Error" not in error_msg and not any(x in error_msg for x in ["PUBLIC_ERROR", "INVALID_ARGUMENT"]):
247
+ debug_logger.log_error(f"[API FAILED] URL: {url}")
248
+ debug_logger.log_error(f"[API FAILED] Request Body: {json_data}")
249
+ debug_logger.log_error(f"[API FAILED] Exception: {error_msg}")
 
250
 
251
  raise Exception(f"Flow API request failed: {error_msg}")
252
 
 
353
 
354
  # ========== 图片上传 (使用AT) ==========
355
 
356
+ def _detect_image_mime_type(self, image_bytes: bytes) -> str:
357
+ """通过文件头 magic bytes 检测图片 MIME 类型
358
+
359
+ Args:
360
+ image_bytes: 图片字节数据
361
+
362
+ Returns:
363
+ MIME 类型字符串,默认 image/jpeg
364
+ """
365
+ if len(image_bytes) < 12:
366
+ return "image/jpeg"
367
+
368
+ # WebP: RIFF....WEBP
369
+ if image_bytes[:4] == b'RIFF' and image_bytes[8:12] == b'WEBP':
370
+ return "image/webp"
371
+ # PNG: 89 50 4E 47
372
+ if image_bytes[:4] == b'\x89PNG':
373
+ return "image/png"
374
+ # JPEG: FF D8 FF
375
+ if image_bytes[:3] == b'\xff\xd8\xff':
376
+ return "image/jpeg"
377
+ # GIF: GIF87a 或 GIF89a
378
+ if image_bytes[:6] in (b'GIF87a', b'GIF89a'):
379
+ return "image/gif"
380
+ # BMP: BM
381
+ if image_bytes[:2] == b'BM':
382
+ return "image/bmp"
383
+ # JPEG 2000: 00 00 00 0C 6A 50
384
+ if image_bytes[:6] == b'\x00\x00\x00\x0cjP':
385
+ return "image/jp2"
386
+
387
+ return "image/jpeg"
388
+
389
+ def _convert_to_jpeg(self, image_bytes: bytes) -> bytes:
390
+ """将图片转换为 JPEG 格式
391
+
392
+ Args:
393
+ image_bytes: 原始图片字节数据
394
+
395
+ Returns:
396
+ JPEG 格式的图片字节数据
397
+ """
398
+ from io import BytesIO
399
+ from PIL import Image
400
+
401
+ img = Image.open(BytesIO(image_bytes))
402
+ # 如果有透明通道,转换为 RGB
403
+ if img.mode in ('RGBA', 'LA', 'P'):
404
+ img = img.convert('RGB')
405
+
406
+ output = BytesIO()
407
+ img.save(output, format='JPEG', quality=95)
408
+ return output.getvalue()
409
+
410
  async def upload_image(
411
  self,
412
  at: str,
 
429
  if aspect_ratio.startswith("VIDEO_"):
430
  aspect_ratio = aspect_ratio.replace("VIDEO_", "IMAGE_")
431
 
432
+ # 自动检测图片 MIME 类型
433
+ mime_type = self._detect_image_mime_type(image_bytes)
434
+
435
  # 编码为base64 (去掉前缀)
436
  image_base64 = base64.b64encode(image_bytes).decode('utf-8')
437
 
 
439
  json_data = {
440
  "imageInput": {
441
  "rawImageBytes": image_base64,
442
+ "mimeType": mime_type,
443
  "isUserUploaded": True,
444
  "aspectRatio": aspect_ratio
445
  },
 
496
  """
497
  url = f"{self.api_base_url}/projects/{project_id}/flowMedia:batchGenerateImages"
498
 
499
+ # 403/reCAPTCHA 重试逻辑 - 最多重试3次
500
+ max_retries = 3
501
+ last_error = None
502
+
503
+ for retry_attempt in range(max_retries):
504
+ # 每次重试都重新获取 reCAPTCHA token
505
+ recaptcha_token, browser_id = await self._get_recaptcha_token(project_id, action="IMAGE_GENERATION")
506
+ recaptcha_token = recaptcha_token or ""
507
+ session_id = self._generate_session_id()
508
+
509
+ # 构建请求 - clientContext 只在外层,requests 内不重复
510
+ client_context = {
511
+ "recaptchaContext": {
512
+ "token": recaptcha_token,
513
+ "applicationType": "RECAPTCHA_APPLICATION_TYPE_WEB"
514
+ },
515
  "sessionId": session_id,
516
+ "projectId": project_id,
517
  "tool": "PINHOLE"
518
+ }
519
+
520
+ request_data = {
521
+ "seed": random.randint(1, 99999),
522
+ "imageModelName": model_name,
523
+ "imageAspectRatio": aspect_ratio,
524
+ "prompt": prompt,
525
+ "imageInputs": image_inputs or []
526
+ }
527
+
528
+ json_data = {
529
+ "clientContext": client_context,
530
+ "requests": [request_data]
531
+ }
532
+
533
+ try:
534
+ result = await self._make_request(
535
+ method="POST",
536
+ url=url,
537
+ json_data=json_data,
538
+ use_at=True,
539
+ at_token=at
540
+ )
541
+ return result
542
+ except Exception as e:
543
+ error_str = str(e)
544
+ last_error = e
545
+ retry_reason = self._get_retry_reason(error_str)
546
+ if retry_reason and retry_attempt < max_retries - 1:
547
+ debug_logger.log_warning(f"[IMAGE] 生成遇到{retry_reason},正在重新获取验证码重试 ({retry_attempt + 2}/{max_retries})...")
548
+ await self._notify_browser_captcha_error(browser_id)
549
+ await asyncio.sleep(1)
550
+ continue
551
+ else:
552
+ raise e
553
+
554
+ # 所有重试都失败
555
+ raise last_error
556
+
557
+ async def upsample_image(
558
+ self,
559
+ at: str,
560
+ project_id: str,
561
+ media_id: str,
562
+ target_resolution: str = "UPSAMPLE_IMAGE_RESOLUTION_4K"
563
+ ) -> str:
564
+ """放大图片到 2K/4K
565
+
566
+ Args:
567
+ at: Access Token
568
+ project_id: 项目ID
569
+ media_id: 图片的 mediaId (从 batchGenerateImages 返回的 media[0]["name"])
570
+ target_resolution: UPSAMPLE_IMAGE_RESOLUTION_2K 或 UPSAMPLE_IMAGE_RESOLUTION_4K
571
+
572
+ Returns:
573
+ base64 编码的图片数据
574
+ """
575
+ url = f"{self.api_base_url}/flow/upsampleImage"
576
+
577
+ # 获取 reCAPTCHA token - 使用 VIDEO_GENERATION action
578
+ recaptcha_token, _ = await self._get_recaptcha_token(project_id, action="VIDEO_GENERATION")
579
+ recaptcha_token = recaptcha_token or ""
580
+ session_id = self._generate_session_id()
581
 
582
  json_data = {
583
+ "mediaId": media_id,
584
+ "targetResolution": target_resolution,
585
  "clientContext": {
586
+ "recaptchaContext": {
587
+ "token": recaptcha_token,
588
+ "applicationType": "RECAPTCHA_APPLICATION_TYPE_WEB"
589
+ },
590
+ "sessionId": session_id,
591
+ "projectId": project_id,
592
+ "tool": "PINHOLE"
593
+ }
594
  }
595
 
596
+ # 4K/2K 放大使用专用超时,因为返回的 base64 数据量很大
597
  result = await self._make_request(
598
  method="POST",
599
  url=url,
600
  json_data=json_data,
601
  use_at=True,
602
+ at_token=at,
603
+ timeout=config.upsample_timeout
604
  )
605
 
606
+ # 返回 base64 编码的图片
607
+ return result.get("encodedImage", "")
608
 
609
  # ========== 视频生成 (使用AT) - 异步返回 ==========
610
 
 
639
  """
640
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoText"
641
 
642
+ # 403/reCAPTCHA 重试逻辑 - 最多重试3次
643
+ max_retries = 3
644
+ last_error = None
645
+
646
+ for retry_attempt in range(max_retries):
647
+ # 每次重试都重新获取 reCAPTCHA token - 视频使用 VIDEO_GENERATION action
648
+ recaptcha_token, browser_id = await self._get_recaptcha_token(project_id, action="VIDEO_GENERATION")
649
+ recaptcha_token = recaptcha_token or ""
650
+ session_id = self._generate_session_id()
651
+ scene_id = str(uuid.uuid4())
652
+
653
+ json_data = {
654
+ "clientContext": {
655
+ "recaptchaContext": {
656
+ "token": recaptcha_token,
657
+ "applicationType": "RECAPTCHA_APPLICATION_TYPE_WEB"
658
+ },
659
+ "sessionId": session_id,
660
+ "projectId": project_id,
661
+ "tool": "PINHOLE",
662
+ "userPaygateTier": user_paygate_tier
663
  },
664
+ "requests": [{
665
+ "aspectRatio": aspect_ratio,
666
+ "seed": random.randint(1, 99999),
667
+ "textInput": {
668
+ "prompt": prompt
669
+ },
670
+ "videoModelKey": model_key,
671
+ "metadata": {
672
+ "sceneId": scene_id
673
+ }
674
+ }]
675
+ }
 
 
676
 
677
+ try:
678
+ result = await self._make_request(
679
+ method="POST",
680
+ url=url,
681
+ json_data=json_data,
682
+ use_at=True,
683
+ at_token=at
684
+ )
685
+ return result
686
+ except Exception as e:
687
+ error_str = str(e)
688
+ last_error = e
689
+ retry_reason = self._get_retry_reason(error_str)
690
+ if retry_reason and retry_attempt < max_retries - 1:
691
+ debug_logger.log_warning(f"[VIDEO T2V] 生成遇到{retry_reason},正在重新获取验证码重试 ({retry_attempt + 2}/{max_retries})...")
692
+ await self._notify_browser_captcha_error(browser_id)
693
+ await asyncio.sleep(1)
694
+ continue
695
+ else:
696
+ raise e
697
+
698
+ # 所有重试都失败
699
+ raise last_error
700
 
701
  async def generate_video_reference_images(
702
  self,
 
724
  """
725
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoReferenceImages"
726
 
727
+ # 403/reCAPTCHA 重试逻辑 - 最多重试3次
728
+ max_retries = 3
729
+ last_error = None
730
+
731
+ for retry_attempt in range(max_retries):
732
+ # 每次重试都重新获取 reCAPTCHA token - 视频使用 VIDEO_GENERATION action
733
+ recaptcha_token, browser_id = await self._get_recaptcha_token(project_id, action="VIDEO_GENERATION")
734
+ recaptcha_token = recaptcha_token or ""
735
+ session_id = self._generate_session_id()
736
+ scene_id = str(uuid.uuid4())
737
+
738
+ json_data = {
739
+ "clientContext": {
740
+ "recaptchaContext": {
741
+ "token": recaptcha_token,
742
+ "applicationType": "RECAPTCHA_APPLICATION_TYPE_WEB"
743
+ },
744
+ "sessionId": session_id,
745
+ "projectId": project_id,
746
+ "tool": "PINHOLE",
747
+ "userPaygateTier": user_paygate_tier
748
  },
749
+ "requests": [{
750
+ "aspectRatio": aspect_ratio,
751
+ "seed": random.randint(1, 99999),
752
+ "textInput": {
753
+ "prompt": prompt
754
+ },
755
+ "videoModelKey": model_key,
756
+ "referenceImages": reference_images,
757
+ "metadata": {
758
+ "sceneId": scene_id
759
+ }
760
+ }]
761
+ }
 
 
762
 
763
+ try:
764
+ result = await self._make_request(
765
+ method="POST",
766
+ url=url,
767
+ json_data=json_data,
768
+ use_at=True,
769
+ at_token=at
770
+ )
771
+ return result
772
+ except Exception as e:
773
+ error_str = str(e)
774
+ last_error = e
775
+ retry_reason = self._get_retry_reason(error_str)
776
+ if retry_reason and retry_attempt < max_retries - 1:
777
+ debug_logger.log_warning(f"[VIDEO R2V] 生成遇到{retry_reason},正在重新获取验证码重试 ({retry_attempt + 2}/{max_retries})...")
778
+ await self._notify_browser_captcha_error(browser_id)
779
+ await asyncio.sleep(1)
780
+ continue
781
+ else:
782
+ raise e
783
+
784
+ # 所有重试都失败
785
+ raise last_error
786
 
787
  async def generate_video_start_end(
788
  self,
 
812
  """
813
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartAndEndImage"
814
 
815
+ # 403/reCAPTCHA 重试逻辑 - 最多重试3次
816
+ max_retries = 3
817
+ last_error = None
818
+
819
+ for retry_attempt in range(max_retries):
820
+ # 每次重试都重新获取 reCAPTCHA token - 视频使用 VIDEO_GENERATION action
821
+ recaptcha_token, browser_id = await self._get_recaptcha_token(project_id, action="VIDEO_GENERATION")
822
+ recaptcha_token = recaptcha_token or ""
823
+ session_id = self._generate_session_id()
824
+ scene_id = str(uuid.uuid4())
825
+
826
+ json_data = {
827
+ "clientContext": {
828
+ "recaptchaContext": {
829
+ "token": recaptcha_token,
830
+ "applicationType": "RECAPTCHA_APPLICATION_TYPE_WEB"
831
+ },
832
+ "sessionId": session_id,
833
+ "projectId": project_id,
834
+ "tool": "PINHOLE",
835
+ "userPaygateTier": user_paygate_tier
 
 
 
 
836
  },
837
+ "requests": [{
838
+ "aspectRatio": aspect_ratio,
839
+ "seed": random.randint(1, 99999),
840
+ "textInput": {
841
+ "prompt": prompt
842
+ },
843
+ "videoModelKey": model_key,
844
+ "startImage": {
845
+ "mediaId": start_media_id
846
+ },
847
+ "endImage": {
848
+ "mediaId": end_media_id
849
+ },
850
+ "metadata": {
851
+ "sceneId": scene_id
852
+ }
853
+ }]
854
+ }
855
 
856
+ try:
857
+ result = await self._make_request(
858
+ method="POST",
859
+ url=url,
860
+ json_data=json_data,
861
+ use_at=True,
862
+ at_token=at
863
+ )
864
+ return result
865
+ except Exception as e:
866
+ error_str = str(e)
867
+ last_error = e
868
+ retry_reason = self._get_retry_reason(error_str)
869
+ if retry_reason and retry_attempt < max_retries - 1:
870
+ debug_logger.log_warning(f"[VIDEO I2V] 首尾帧生成遇到{retry_reason},正在重新获取验证码重试 ({retry_attempt + 2}/{max_retries})...")
871
+ await self._notify_browser_captcha_error(browser_id)
872
+ await asyncio.sleep(1)
873
+ continue
874
+ else:
875
+ raise e
876
+
877
+ # 所有重试都失败
878
+ raise last_error
879
 
880
  async def generate_video_start_image(
881
  self,
 
903
  """
904
  url = f"{self.api_base_url}/video:batchAsyncGenerateVideoStartImage"
905
 
906
+ # 403/reCAPTCHA 重试逻辑 - 最多重试3次
907
+ max_retries = 3
908
+ last_error = None
909
+
910
+ for retry_attempt in range(max_retries):
911
+ # 每次重试都重新获取 reCAPTCHA token - 视频使用 VIDEO_GENERATION action
912
+ recaptcha_token, browser_id = await self._get_recaptcha_token(project_id, action="VIDEO_GENERATION")
913
+ recaptcha_token = recaptcha_token or ""
914
+ session_id = self._generate_session_id()
915
+ scene_id = str(uuid.uuid4())
916
+
917
+ json_data = {
918
+ "clientContext": {
919
+ "recaptchaContext": {
920
+ "token": recaptcha_token,
921
+ "applicationType": "RECAPTCHA_APPLICATION_TYPE_WEB"
922
+ },
923
+ "sessionId": session_id,
924
+ "projectId": project_id,
925
+ "tool": "PINHOLE",
926
+ "userPaygateTier": user_paygate_tier
 
927
  },
928
+ "requests": [{
929
+ "aspectRatio": aspect_ratio,
930
+ "seed": random.randint(1, 99999),
931
+ "textInput": {
932
+ "prompt": prompt
933
+ },
934
+ "videoModelKey": model_key,
935
+ "startImage": {
936
+ "mediaId": start_media_id
937
+ },
938
+ # 注意: 没有endImage字段,只用首帧
939
+ "metadata": {
940
+ "sceneId": scene_id
941
+ }
942
+ }]
943
+ }
944
 
945
+ try:
946
+ result = await self._make_request(
947
+ method="POST",
948
+ url=url,
949
+ json_data=json_data,
950
+ use_at=True,
951
+ at_token=at
952
+ )
953
+ return result
954
+ except Exception as e:
955
+ error_str = str(e)
956
+ last_error = e
957
+ retry_reason = self._get_retry_reason(error_str)
958
+ if retry_reason and retry_attempt < max_retries - 1:
959
+ debug_logger.log_warning(f"[VIDEO I2V] 首帧生成遇到{retry_reason},正在重新获取验证码重试 ({retry_attempt + 2}/{max_retries})...")
960
+ await self._notify_browser_captcha_error(browser_id)
961
+ await asyncio.sleep(1)
962
+ continue
963
+ else:
964
+ raise e
965
+
966
+ # 所有重试都失败
967
+ raise last_error
968
 
969
+ # ========== 视频放大 (Video Upsampler) ==========
970
+
971
+ async def upsample_video(
972
+ self,
973
+ at: str,
974
+ project_id: str,
975
+ video_media_id: str,
976
+ aspect_ratio: str,
977
+ resolution: str,
978
+ model_key: str
979
+ ) -> dict:
980
+ """视频放大到 4K/1080P,返回 task_id
981
+
982
+ Args:
983
+ at: Access Token
984
+ project_id: 项目ID
985
+ video_media_id: 视频的 mediaId
986
+ aspect_ratio: 视频宽高比 VIDEO_ASPECT_RATIO_PORTRAIT/LANDSCAPE
987
+ resolution: VIDEO_RESOLUTION_4K 或 VIDEO_RESOLUTION_1080P
988
+ model_key: veo_3_1_upsampler_4k 或 veo_3_1_upsampler_1080p
989
+
990
+ Returns:
991
+ 同 generate_video_text
992
+ """
993
+ url = f"{self.api_base_url}/video:batchAsyncUpsampleVideo"
994
+
995
+ # 403/reCAPTCHA 重试逻辑 - 最多重试3次
996
+ max_retries = 3
997
+ last_error = None
998
+
999
+ for retry_attempt in range(max_retries):
1000
+ recaptcha_token, browser_id = await self._get_recaptcha_token(project_id, action="VIDEO_GENERATION")
1001
+ recaptcha_token = recaptcha_token or ""
1002
+ session_id = self._generate_session_id()
1003
+ scene_id = str(uuid.uuid4())
1004
+
1005
+ json_data = {
1006
+ "requests": [{
1007
+ "aspectRatio": aspect_ratio,
1008
+ "resolution": resolution,
1009
+ "seed": random.randint(1, 99999),
1010
+ "videoInput": {
1011
+ "mediaId": video_media_id
1012
+ },
1013
+ "videoModelKey": model_key,
1014
+ "metadata": {
1015
+ "sceneId": scene_id
1016
+ }
1017
+ }],
1018
+ "clientContext": {
1019
+ "recaptchaContext": {
1020
+ "token": recaptcha_token,
1021
+ "applicationType": "RECAPTCHA_APPLICATION_TYPE_WEB"
1022
+ },
1023
+ "sessionId": session_id
1024
+ }
1025
+ }
1026
+
1027
+ try:
1028
+ result = await self._make_request(
1029
+ method="POST",
1030
+ url=url,
1031
+ json_data=json_data,
1032
+ use_at=True,
1033
+ at_token=at
1034
+ )
1035
+ return result
1036
+ except Exception as e:
1037
+ error_str = str(e)
1038
+ last_error = e
1039
+ retry_reason = self._get_retry_reason(error_str)
1040
+ if retry_reason and retry_attempt < max_retries - 1:
1041
+ debug_logger.log_warning(f"[VIDEO UPSAMPLE] 放大遇到{retry_reason},正在重新获取验证码重试 ({retry_attempt + 2}/{max_retries})...")
1042
+ await self._notify_browser_captcha_error(browser_id)
1043
+ await asyncio.sleep(1)
1044
+ continue
1045
+ else:
1046
+ raise e
1047
+
1048
+ raise last_error
1049
 
1050
  # ========== 任务轮询 (使用AT) ==========
1051
 
 
1109
 
1110
  # ========== 辅助方法 ==========
1111
 
1112
+ def _get_retry_reason(self, error_str: str) -> Optional[str]:
1113
+ """判断是否需要重试,返回日志提示内容"""
1114
+ error_lower = error_str.lower()
1115
+ if "403" in error_lower:
1116
+ return "403错误"
1117
+ if "recaptcha evaluation failed" in error_lower:
1118
+ return "reCAPTCHA 验证失败"
1119
+ if "recaptcha" in error_lower:
1120
+ return "reCAPTCHA 错误"
1121
+ return None
1122
+
1123
+ async def _notify_browser_captcha_error(self, browser_id: int = None):
1124
+ """通知有头浏览器打码切换指纹(仅当使用 browser 打码方式时)
1125
+
1126
+ Args:
1127
+ browser_id: 要标记为 bad 的浏览器 ID
1128
+ """
1129
+ if config.captcha_method == "browser":
1130
+ try:
1131
+ from .browser_captcha import BrowserCaptchaService
1132
+ service = await BrowserCaptchaService.get_instance(self.db)
1133
+ await service.report_error(browser_id)
1134
+ except Exception:
1135
+ pass
1136
+
1137
  def _generate_session_id(self) -> str:
1138
  """生成sessionId: ;timestamp"""
1139
  return f";{int(time.time() * 1000)}"
 
1142
  """生成sceneId: UUID"""
1143
  return str(uuid.uuid4())
1144
 
1145
+ async def _get_recaptcha_token(self, project_id: str, action: str = "IMAGE_GENERATION") -> tuple[Optional[str], Optional[int]]:
1146
+ """获取reCAPTCHA token - 支持多种打码方式
1147
+
1148
+ Args:
1149
+ project_id: 项目ID
1150
+ action: reCAPTCHA action类型
1151
+ - IMAGE_GENERATION: 图片生成 (默认)
1152
+ - VIDEO_GENERATION: 视频生成和2K/4K图片放大
1153
+
1154
+ Returns:
1155
+ (token, browser_id) 元组,browser_id 用于失败时调用 report_error
1156
+ 对于非 browser 打码方式,browser_id 为 None
1157
+ """
1158
  captcha_method = config.captcha_method
1159
 
1160
  # 恒定浏览器打码
 
1162
  try:
1163
  from .browser_captcha_personal import BrowserCaptchaService
1164
  service = await BrowserCaptchaService.get_instance(self.db)
1165
+ return await service.get_token(project_id, action), None
1166
  except Exception as e:
1167
  debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
1168
+ return None, None
1169
+ # 头浏览器打码
1170
  elif captcha_method == "browser":
1171
  try:
1172
  from .browser_captcha import BrowserCaptchaService
1173
  service = await BrowserCaptchaService.get_instance(self.db)
1174
+ return await service.get_token(project_id, action)
1175
  except Exception as e:
1176
  debug_logger.log_error(f"[reCAPTCHA Browser] error: {str(e)}")
1177
+ return None, None
1178
  # API打码服务
1179
  elif captcha_method in ["yescaptcha", "capmonster", "ezcaptcha", "capsolver"]:
1180
+ token = await self._get_api_captcha_token(captcha_method, project_id)
1181
+ return token, None
1182
  else:
1183
+ debug_logger.log_info(f"[reCAPTCHA] 未知的打码方式: {captcha_method}")
1184
+ return None, None
1185
 
1186
  async def _get_api_captcha_token(self, method: str, project_id: str) -> Optional[str]:
1187
  """通用API打码服务"""
 
1193
  elif method == "capmonster":
1194
  client_key = config.capmonster_api_key
1195
  base_url = config.capmonster_base_url
1196
+ task_type = "RecaptchaV3TaskProxyless"
1197
  elif method == "ezcaptcha":
1198
  client_key = config.ezcaptcha_api_key
1199
  base_url = config.ezcaptcha_base_url
1200
+ task_type = "ReCaptchaV3TaskProxyless"
1201
  elif method == "capsolver":
1202
  client_key = config.capsolver_api_key
1203
  base_url = config.capsolver_base_url
src/services/generation_handler.py CHANGED
@@ -35,6 +35,85 @@ MODEL_CONFIG = {
35
  "model_name": "GEM_PIX_2",
36
  "aspect_ratio": "IMAGE_ASPECT_RATIO_PORTRAIT"
37
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  # 图片生成 - IMAGEN_3_5 (Imagen 4.0)
40
  "imagen-4.0-generate-preview-landscape": {
@@ -102,25 +181,39 @@ MODEL_CONFIG = {
102
  "supports_images": False
103
  },
104
 
105
- # veo_3_1_t2v_fast_portrait_ultra (竖屏)
106
- "veo_3_1_t2v_fast_portrait_ultra": {
107
  "type": "video",
108
  "video_type": "t2v",
109
  "model_key": "veo_3_1_t2v_fast_portrait_ultra",
110
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
111
  "supports_images": False
112
  },
 
 
 
 
 
 
 
113
 
114
- # veo_3_1_t2v_fast_portrait_ultra_relaxed (竖屏)
115
- "veo_3_1_t2v_fast_portrait_ultra_relaxed": {
116
  "type": "video",
117
  "video_type": "t2v",
118
  "model_key": "veo_3_1_t2v_fast_portrait_ultra_relaxed",
119
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
120
  "supports_images": False
121
  },
 
 
 
 
 
 
 
122
 
123
- # veo_3_1_t2v_portrait (竖屏)
124
  "veo_3_1_t2v_portrait": {
125
  "type": "video",
126
  "video_type": "t2v",
@@ -128,6 +221,13 @@ MODEL_CONFIG = {
128
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
129
  "supports_images": False
130
  },
 
 
 
 
 
 
 
131
 
132
  # ========== 首尾帧模型 (I2V - Image to Video) ==========
133
  # 支持1-2张图片:1张作为首帧,2张作为首尾帧
@@ -192,11 +292,11 @@ MODEL_CONFIG = {
192
  "max_images": 2
193
  },
194
 
195
- # veo_3_1_i2v_s_fast_ultra (需要新增横竖屏)
196
  "veo_3_1_i2v_s_fast_ultra_portrait": {
197
  "type": "video",
198
  "video_type": "i2v",
199
- "model_key": "veo_3_1_i2v_s_fast_ultra",
200
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
201
  "supports_images": True,
202
  "min_images": 1,
@@ -205,7 +305,7 @@ MODEL_CONFIG = {
205
  "veo_3_1_i2v_s_fast_ultra_landscape": {
206
  "type": "video",
207
  "video_type": "i2v",
208
- "model_key": "veo_3_1_i2v_s_fast_ultra",
209
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
210
  "supports_images": True,
211
  "min_images": 1,
@@ -255,64 +355,223 @@ MODEL_CONFIG = {
255
  # ========== 多图生成 (R2V - Reference Images to Video) ==========
256
  # 支持多张图片,不限制数量
257
 
258
- # veo_3_0_r2v_fast (需要新增横竖屏)
259
- "veo_3_0_r2v_fast_portrait": {
260
  "type": "video",
261
  "video_type": "r2v",
262
- "model_key": "veo_3_0_r2v_fast_portrait_ultra_relaxed",
263
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
264
  "supports_images": True,
265
  "min_images": 0,
266
  "max_images": None # 不限制
267
  },
268
- "veo_3_0_r2v_fast_landscape": {
269
  "type": "video",
270
  "video_type": "r2v",
271
- "model_key": "veo_3_0_r2v_fast_ultra_relaxed",
272
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
273
  "supports_images": True,
274
  "min_images": 0,
275
  "max_images": None # 不限制
276
  },
277
 
278
- # veo_3_0_r2v_fast_ultra (需要新增横竖屏)
279
- "veo_3_0_r2v_fast_ultra_portrait": {
280
  "type": "video",
281
  "video_type": "r2v",
282
- "model_key": "veo_3_0_r2v_fast_ultra",
283
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
284
  "supports_images": True,
285
  "min_images": 0,
286
  "max_images": None # 不限制
287
  },
288
- "veo_3_0_r2v_fast_ultra_landscape": {
289
  "type": "video",
290
  "video_type": "r2v",
291
- "model_key": "veo_3_0_r2v_fast_ultra",
292
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
293
  "supports_images": True,
294
  "min_images": 0,
295
  "max_images": None # 不限制
296
  },
297
 
298
- # veo_3_0_r2v_fast_ultra_relaxed (需要新增横竖屏)
299
- "veo_3_0_r2v_fast_ultra_relaxed_portrait": {
300
  "type": "video",
301
  "video_type": "r2v",
302
- "model_key": "veo_3_0_r2v_fast_ultra_relaxed",
303
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
304
  "supports_images": True,
305
  "min_images": 0,
306
  "max_images": None # 不限制
307
  },
308
- "veo_3_0_r2v_fast_ultra_relaxed_landscape": {
309
  "type": "video",
310
  "video_type": "r2v",
311
- "model_key": "veo_3_0_r2v_fast_ultra_relaxed",
312
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
313
  "supports_images": True,
314
  "min_images": 0,
315
  "max_images": None # 不限制
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
317
  }
318
 
@@ -572,13 +831,101 @@ class GenerationHandler:
572
  image_inputs=image_inputs
573
  )
574
 
575
- # 提取URL
576
  media = result.get("media", [])
577
  if not media:
578
  yield self._create_error_response("生成结果为空")
579
  return
580
 
581
  image_url = media[0]["image"]["generatedImage"]["fifeUrl"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
 
583
  # 缓存图片 (如果启用)
584
  local_url = image_url
@@ -835,7 +1182,10 @@ class GenerationHandler:
835
  if stream:
836
  yield self._create_stream_chunk(f"视频生成中...\n")
837
 
838
- async for chunk in self._poll_video_result(token, operations, stream):
 
 
 
839
  yield chunk
840
 
841
  finally:
@@ -846,13 +1196,23 @@ class GenerationHandler:
846
  async def _poll_video_result(
847
  self,
848
  token,
 
849
  operations: List[Dict],
850
- stream: bool
 
851
  ) -> AsyncGenerator:
852
- """轮询视频生成结果"""
 
 
 
 
853
 
854
  max_attempts = config.max_poll_attempts
855
  poll_interval = config.poll_interval
 
 
 
 
856
 
857
  for attempt in range(max_attempts):
858
  await asyncio.sleep(poll_interval)
@@ -879,11 +1239,49 @@ class GenerationHandler:
879
  metadata = operation["operation"].get("metadata", {})
880
  video_info = metadata.get("video", {})
881
  video_url = video_info.get("fifeUrl")
 
 
882
 
883
  if not video_url:
884
  yield self._create_error_response("视频URL为空")
885
  return
886
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
887
  # 缓存视频 (如果启用)
888
  local_url = video_url
889
  if config.cache_enabled:
 
35
  "model_name": "GEM_PIX_2",
36
  "aspect_ratio": "IMAGE_ASPECT_RATIO_PORTRAIT"
37
  },
38
+ "gemini-3.0-pro-image-square": {
39
+ "type": "image",
40
+ "model_name": "GEM_PIX_2",
41
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_SQUARE"
42
+ },
43
+ "gemini-3.0-pro-image-four-three": {
44
+ "type": "image",
45
+ "model_name": "GEM_PIX_2",
46
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_LANDSCAPE_FOUR_THREE"
47
+ },
48
+ "gemini-3.0-pro-image-three-four": {
49
+ "type": "image",
50
+ "model_name": "GEM_PIX_2",
51
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_PORTRAIT_THREE_FOUR"
52
+ },
53
+
54
+ # 图片生成 - GEM_PIX_2 (Gemini 3.0 Pro) 2K 放大版
55
+ "gemini-3.0-pro-image-landscape-2k": {
56
+ "type": "image",
57
+ "model_name": "GEM_PIX_2",
58
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_LANDSCAPE",
59
+ "upsample": "UPSAMPLE_IMAGE_RESOLUTION_2K"
60
+ },
61
+ "gemini-3.0-pro-image-portrait-2k": {
62
+ "type": "image",
63
+ "model_name": "GEM_PIX_2",
64
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_PORTRAIT",
65
+ "upsample": "UPSAMPLE_IMAGE_RESOLUTION_2K"
66
+ },
67
+ "gemini-3.0-pro-image-square-2k": {
68
+ "type": "image",
69
+ "model_name": "GEM_PIX_2",
70
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_SQUARE",
71
+ "upsample": "UPSAMPLE_IMAGE_RESOLUTION_2K"
72
+ },
73
+ "gemini-3.0-pro-image-four-three-2k": {
74
+ "type": "image",
75
+ "model_name": "GEM_PIX_2",
76
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_LANDSCAPE_FOUR_THREE",
77
+ "upsample": "UPSAMPLE_IMAGE_RESOLUTION_2K"
78
+ },
79
+ "gemini-3.0-pro-image-three-four-2k": {
80
+ "type": "image",
81
+ "model_name": "GEM_PIX_2",
82
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_PORTRAIT_THREE_FOUR",
83
+ "upsample": "UPSAMPLE_IMAGE_RESOLUTION_2K"
84
+ },
85
+
86
+ # 图片生成 - GEM_PIX_2 (Gemini 3.0 Pro) 4K 放大版
87
+ "gemini-3.0-pro-image-landscape-4k": {
88
+ "type": "image",
89
+ "model_name": "GEM_PIX_2",
90
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_LANDSCAPE",
91
+ "upsample": "UPSAMPLE_IMAGE_RESOLUTION_4K"
92
+ },
93
+ "gemini-3.0-pro-image-portrait-4k": {
94
+ "type": "image",
95
+ "model_name": "GEM_PIX_2",
96
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_PORTRAIT",
97
+ "upsample": "UPSAMPLE_IMAGE_RESOLUTION_4K"
98
+ },
99
+ "gemini-3.0-pro-image-square-4k": {
100
+ "type": "image",
101
+ "model_name": "GEM_PIX_2",
102
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_SQUARE",
103
+ "upsample": "UPSAMPLE_IMAGE_RESOLUTION_4K"
104
+ },
105
+ "gemini-3.0-pro-image-four-three-4k": {
106
+ "type": "image",
107
+ "model_name": "GEM_PIX_2",
108
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_LANDSCAPE_FOUR_THREE",
109
+ "upsample": "UPSAMPLE_IMAGE_RESOLUTION_4K"
110
+ },
111
+ "gemini-3.0-pro-image-three-four-4k": {
112
+ "type": "image",
113
+ "model_name": "GEM_PIX_2",
114
+ "aspect_ratio": "IMAGE_ASPECT_RATIO_PORTRAIT_THREE_FOUR",
115
+ "upsample": "UPSAMPLE_IMAGE_RESOLUTION_4K"
116
+ },
117
 
118
  # 图片生成 - IMAGEN_3_5 (Imagen 4.0)
119
  "imagen-4.0-generate-preview-landscape": {
 
181
  "supports_images": False
182
  },
183
 
184
+ # veo_3_1_t2v_fast_ultra (竖屏)
185
+ "veo_3_1_t2v_fast_ultra_portrait": {
186
  "type": "video",
187
  "video_type": "t2v",
188
  "model_key": "veo_3_1_t2v_fast_portrait_ultra",
189
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
190
  "supports_images": False
191
  },
192
+ "veo_3_1_t2v_fast_ultra_landscape": {
193
+ "type": "video",
194
+ "video_type": "t2v",
195
+ "model_key": "veo_3_1_t2v_fast_ultra",
196
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
197
+ "supports_images": False
198
+ },
199
 
200
+ # veo_3_1_t2v_fast_ultra_relaxed (竖屏)
201
+ "veo_3_1_t2v_fast_ultra_relaxed_portrait": {
202
  "type": "video",
203
  "video_type": "t2v",
204
  "model_key": "veo_3_1_t2v_fast_portrait_ultra_relaxed",
205
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
206
  "supports_images": False
207
  },
208
+ "veo_3_1_t2v_fast_ultra_relaxed_landscape": {
209
+ "type": "video",
210
+ "video_type": "t2v",
211
+ "model_key": "veo_3_1_t2v_fast_ultra_relaxed",
212
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
213
+ "supports_images": False
214
+ },
215
 
216
+ # veo_3_1_t2v (竖屏)
217
  "veo_3_1_t2v_portrait": {
218
  "type": "video",
219
  "video_type": "t2v",
 
221
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
222
  "supports_images": False
223
  },
224
+ "veo_3_1_t2v_landscape": {
225
+ "type": "video",
226
+ "video_type": "t2v",
227
+ "model_key": "veo_3_1_t2v",
228
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
229
+ "supports_images": False
230
+ },
231
 
232
  # ========== 首尾帧模型 (I2V - Image to Video) ==========
233
  # 支持1-2张图片:1张作为首帧,2张作为首尾帧
 
292
  "max_images": 2
293
  },
294
 
295
+ # veo_3_1_i2v_s_fast_ultra (横竖屏)
296
  "veo_3_1_i2v_s_fast_ultra_portrait": {
297
  "type": "video",
298
  "video_type": "i2v",
299
+ "model_key": "veo_3_1_i2v_s_fast_portrait_ultra_fl",
300
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
301
  "supports_images": True,
302
  "min_images": 1,
 
305
  "veo_3_1_i2v_s_fast_ultra_landscape": {
306
  "type": "video",
307
  "video_type": "i2v",
308
+ "model_key": "veo_3_1_i2v_s_fast_ultra_fl",
309
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
310
  "supports_images": True,
311
  "min_images": 1,
 
355
  # ========== 多图生成 (R2V - Reference Images to Video) ==========
356
  # 支持多张图片,不限制数量
357
 
358
+ # veo_3_1_r2v_fast (横竖屏)
359
+ "veo_3_1_r2v_fast_portrait": {
360
  "type": "video",
361
  "video_type": "r2v",
362
+ "model_key": "veo_3_1_r2v_fast_portrait_ultra_relaxed",
363
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
364
  "supports_images": True,
365
  "min_images": 0,
366
  "max_images": None # 不限制
367
  },
368
+ "veo_3_1_r2v_fast_landscape": {
369
  "type": "video",
370
  "video_type": "r2v",
371
+ "model_key": "veo_3_1_r2v_fast_landscape_ultra_relaxed",
372
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
373
  "supports_images": True,
374
  "min_images": 0,
375
  "max_images": None # 不限制
376
  },
377
 
378
+ # veo_3_1_r2v_fast_ultra (横竖屏)
379
+ "veo_3_1_r2v_fast_ultra_portrait": {
380
  "type": "video",
381
  "video_type": "r2v",
382
+ "model_key": "veo_3_1_r2v_fast_portrait_ultra",
383
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
384
  "supports_images": True,
385
  "min_images": 0,
386
  "max_images": None # 不限制
387
  },
388
+ "veo_3_1_r2v_fast_ultra_landscape": {
389
  "type": "video",
390
  "video_type": "r2v",
391
+ "model_key": "veo_3_1_r2v_fast_landscape_ultra",
392
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
393
  "supports_images": True,
394
  "min_images": 0,
395
  "max_images": None # 不限制
396
  },
397
 
398
+ # veo_3_1_r2v_fast_ultra_relaxed (横竖屏)
399
+ "veo_3_1_r2v_fast_ultra_relaxed_portrait": {
400
  "type": "video",
401
  "video_type": "r2v",
402
+ "model_key": "veo_3_1_r2v_fast_portrait_ultra_relaxed",
403
  "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
404
  "supports_images": True,
405
  "min_images": 0,
406
  "max_images": None # 不限制
407
  },
408
+ "veo_3_1_r2v_fast_ultra_relaxed_landscape": {
409
  "type": "video",
410
  "video_type": "r2v",
411
+ "model_key": "veo_3_1_r2v_fast_landscape_ultra_relaxed",
412
  "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
413
  "supports_images": True,
414
  "min_images": 0,
415
  "max_images": None # 不限制
416
+ },
417
+
418
+ # ========== 视频放大 (Video Upsampler) ==========
419
+ # 仅 3.1 支持,需要先生成视频后再放大,可能需要 30 分钟
420
+
421
+ # T2V 4K 放大版
422
+ "veo_3_1_t2v_fast_portrait_4k": {
423
+ "type": "video",
424
+ "video_type": "t2v",
425
+ "model_key": "veo_3_1_t2v_fast_portrait",
426
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
427
+ "supports_images": False,
428
+ "upsample": {"resolution": "VIDEO_RESOLUTION_4K", "model_key": "veo_3_1_upsampler_4k"}
429
+ },
430
+ "veo_3_1_t2v_fast_landscape_4k": {
431
+ "type": "video",
432
+ "video_type": "t2v",
433
+ "model_key": "veo_3_1_t2v_fast",
434
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
435
+ "supports_images": False,
436
+ "upsample": {"resolution": "VIDEO_RESOLUTION_4K", "model_key": "veo_3_1_upsampler_4k"}
437
+ },
438
+ "veo_3_1_t2v_fast_ultra_portrait_4k": {
439
+ "type": "video",
440
+ "video_type": "t2v",
441
+ "model_key": "veo_3_1_t2v_fast_portrait_ultra",
442
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
443
+ "supports_images": False,
444
+ "upsample": {"resolution": "VIDEO_RESOLUTION_4K", "model_key": "veo_3_1_upsampler_4k"}
445
+ },
446
+ "veo_3_1_t2v_fast_ultra_landscape_4k": {
447
+ "type": "video",
448
+ "video_type": "t2v",
449
+ "model_key": "veo_3_1_t2v_fast_ultra",
450
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
451
+ "supports_images": False,
452
+ "upsample": {"resolution": "VIDEO_RESOLUTION_4K", "model_key": "veo_3_1_upsampler_4k"}
453
+ },
454
+
455
+ # T2V 1080P 放大版
456
+ "veo_3_1_t2v_fast_portrait_1080p": {
457
+ "type": "video",
458
+ "video_type": "t2v",
459
+ "model_key": "veo_3_1_t2v_fast_portrait",
460
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
461
+ "supports_images": False,
462
+ "upsample": {"resolution": "VIDEO_RESOLUTION_1080P", "model_key": "veo_3_1_upsampler_1080p"}
463
+ },
464
+ "veo_3_1_t2v_fast_landscape_1080p": {
465
+ "type": "video",
466
+ "video_type": "t2v",
467
+ "model_key": "veo_3_1_t2v_fast",
468
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
469
+ "supports_images": False,
470
+ "upsample": {"resolution": "VIDEO_RESOLUTION_1080P", "model_key": "veo_3_1_upsampler_1080p"}
471
+ },
472
+ "veo_3_1_t2v_fast_ultra_portrait_1080p": {
473
+ "type": "video",
474
+ "video_type": "t2v",
475
+ "model_key": "veo_3_1_t2v_fast_portrait_ultra",
476
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
477
+ "supports_images": False,
478
+ "upsample": {"resolution": "VIDEO_RESOLUTION_1080P", "model_key": "veo_3_1_upsampler_1080p"}
479
+ },
480
+ "veo_3_1_t2v_fast_ultra_landscape_1080p": {
481
+ "type": "video",
482
+ "video_type": "t2v",
483
+ "model_key": "veo_3_1_t2v_fast_ultra",
484
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
485
+ "supports_images": False,
486
+ "upsample": {"resolution": "VIDEO_RESOLUTION_1080P", "model_key": "veo_3_1_upsampler_1080p"}
487
+ },
488
+
489
+ # I2V 4K 放大版
490
+ "veo_3_1_i2v_s_fast_ultra_portrait_4k": {
491
+ "type": "video",
492
+ "video_type": "i2v",
493
+ "model_key": "veo_3_1_i2v_s_fast_portrait_ultra_fl",
494
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
495
+ "supports_images": True,
496
+ "min_images": 1,
497
+ "max_images": 2,
498
+ "upsample": {"resolution": "VIDEO_RESOLUTION_4K", "model_key": "veo_3_1_upsampler_4k"}
499
+ },
500
+ "veo_3_1_i2v_s_fast_ultra_landscape_4k": {
501
+ "type": "video",
502
+ "video_type": "i2v",
503
+ "model_key": "veo_3_1_i2v_s_fast_ultra_fl",
504
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
505
+ "supports_images": True,
506
+ "min_images": 1,
507
+ "max_images": 2,
508
+ "upsample": {"resolution": "VIDEO_RESOLUTION_4K", "model_key": "veo_3_1_upsampler_4k"}
509
+ },
510
+
511
+ # I2V 1080P 放大版
512
+ "veo_3_1_i2v_s_fast_ultra_portrait_1080p": {
513
+ "type": "video",
514
+ "video_type": "i2v",
515
+ "model_key": "veo_3_1_i2v_s_fast_portrait_ultra_fl",
516
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
517
+ "supports_images": True,
518
+ "min_images": 1,
519
+ "max_images": 2,
520
+ "upsample": {"resolution": "VIDEO_RESOLUTION_1080P", "model_key": "veo_3_1_upsampler_1080p"}
521
+ },
522
+ "veo_3_1_i2v_s_fast_ultra_landscape_1080p": {
523
+ "type": "video",
524
+ "video_type": "i2v",
525
+ "model_key": "veo_3_1_i2v_s_fast_ultra_fl",
526
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
527
+ "supports_images": True,
528
+ "min_images": 1,
529
+ "max_images": 2,
530
+ "upsample": {"resolution": "VIDEO_RESOLUTION_1080P", "model_key": "veo_3_1_upsampler_1080p"}
531
+ },
532
+
533
+ # R2V 4K 放大版
534
+ "veo_3_1_r2v_fast_ultra_portrait_4k": {
535
+ "type": "video",
536
+ "video_type": "r2v",
537
+ "model_key": "veo_3_1_r2v_fast_portrait_ultra",
538
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
539
+ "supports_images": True,
540
+ "min_images": 0,
541
+ "max_images": None,
542
+ "upsample": {"resolution": "VIDEO_RESOLUTION_4K", "model_key": "veo_3_1_upsampler_4k"}
543
+ },
544
+ "veo_3_1_r2v_fast_ultra_landscape_4k": {
545
+ "type": "video",
546
+ "video_type": "r2v",
547
+ "model_key": "veo_3_1_r2v_fast_landscape_ultra",
548
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
549
+ "supports_images": True,
550
+ "min_images": 0,
551
+ "max_images": None,
552
+ "upsample": {"resolution": "VIDEO_RESOLUTION_4K", "model_key": "veo_3_1_upsampler_4k"}
553
+ },
554
+
555
+ # R2V 1080P 放大版
556
+ "veo_3_1_r2v_fast_ultra_portrait_1080p": {
557
+ "type": "video",
558
+ "video_type": "r2v",
559
+ "model_key": "veo_3_1_r2v_fast_portrait_ultra",
560
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_PORTRAIT",
561
+ "supports_images": True,
562
+ "min_images": 0,
563
+ "max_images": None,
564
+ "upsample": {"resolution": "VIDEO_RESOLUTION_1080P", "model_key": "veo_3_1_upsampler_1080p"}
565
+ },
566
+ "veo_3_1_r2v_fast_ultra_landscape_1080p": {
567
+ "type": "video",
568
+ "video_type": "r2v",
569
+ "model_key": "veo_3_1_r2v_fast_landscape_ultra",
570
+ "aspect_ratio": "VIDEO_ASPECT_RATIO_LANDSCAPE",
571
+ "supports_images": True,
572
+ "min_images": 0,
573
+ "max_images": None,
574
+ "upsample": {"resolution": "VIDEO_RESOLUTION_1080P", "model_key": "veo_3_1_upsampler_1080p"}
575
  }
576
  }
577
 
 
831
  image_inputs=image_inputs
832
  )
833
 
834
+ # 提取URL和mediaId
835
  media = result.get("media", [])
836
  if not media:
837
  yield self._create_error_response("生成结果为空")
838
  return
839
 
840
  image_url = media[0]["image"]["generatedImage"]["fifeUrl"]
841
+ media_id = media[0].get("name") # 用于 upsample
842
+
843
+ # 检查是否需要 upsample
844
+ upsample_resolution = model_config.get("upsample")
845
+ if upsample_resolution and media_id:
846
+ resolution_name = "4K" if "4K" in upsample_resolution else "2K"
847
+ if stream:
848
+ yield self._create_stream_chunk(f"正在放大图片到 {resolution_name}...\n")
849
+
850
+ # 4K/2K 图片重试逻辑 - 最多重试3次
851
+ max_retries = 3
852
+ for retry_attempt in range(max_retries):
853
+ try:
854
+ # 调用 upsample API
855
+ encoded_image = await self.flow_client.upsample_image(
856
+ at=token.at,
857
+ project_id=project_id,
858
+ media_id=media_id,
859
+ target_resolution=upsample_resolution
860
+ )
861
+
862
+ if encoded_image:
863
+ debug_logger.log_info(f"[UPSAMPLE] 图片已放大到 {resolution_name}")
864
+
865
+ if stream:
866
+ yield self._create_stream_chunk(f"✅ 图片已放大到 {resolution_name}\n")
867
+
868
+ # 缓存放大后的图片 (如果启用)
869
+ # 日志统一记录原图URL (放大后的base64数据太大,不适合存储)
870
+ self._last_generated_url = image_url
871
+
872
+ if config.cache_enabled:
873
+ try:
874
+ if stream:
875
+ yield self._create_stream_chunk(f"缓存 {resolution_name} 图片中...\n")
876
+ cached_filename = await self.file_cache.cache_base64_image(encoded_image, resolution_name)
877
+ local_url = f"{self._get_base_url()}/tmp/{cached_filename}"
878
+ if stream:
879
+ yield self._create_stream_chunk(f"✅ {resolution_name} 图片缓存成功\n")
880
+ yield self._create_stream_chunk(
881
+ f"![Generated Image]({local_url})",
882
+ finish_reason="stop"
883
+ )
884
+ else:
885
+ yield self._create_completion_response(
886
+ local_url,
887
+ media_type="image"
888
+ )
889
+ return
890
+ except Exception as e:
891
+ debug_logger.log_error(f"Failed to cache {resolution_name} image: {str(e)}")
892
+ if stream:
893
+ yield self._create_stream_chunk(f"⚠️ 缓存失败: {str(e)},返回 base64...\n")
894
+
895
+ # 缓存未启用或缓存失败,返回 base64 格式
896
+ base64_url = f"data:image/jpeg;base64,{encoded_image}"
897
+ if stream:
898
+ yield self._create_stream_chunk(
899
+ f"![Generated Image]({base64_url})",
900
+ finish_reason="stop"
901
+ )
902
+ else:
903
+ yield self._create_completion_response(
904
+ base64_url,
905
+ media_type="image"
906
+ )
907
+ return
908
+ else:
909
+ debug_logger.log_warning("[UPSAMPLE] 返回结果为空")
910
+ if stream:
911
+ yield self._create_stream_chunk(f"⚠️ 放大失败,返回原图...\n")
912
+ break # 空结果不重试
913
+
914
+ except Exception as e:
915
+ error_str = str(e)
916
+ debug_logger.log_error(f"[UPSAMPLE] 放大失败 (尝试 {retry_attempt + 1}/{max_retries}): {error_str}")
917
+
918
+ # 检查是否是403错误,需要重试
919
+ if "403" in error_str and retry_attempt < max_retries - 1:
920
+ if stream:
921
+ yield self._create_stream_chunk(f"⚠️ 放大遇到403错误,正在重新获取验证码重试 ({retry_attempt + 2}/{max_retries})...\n")
922
+ # 等待一小段时间后重试
923
+ await asyncio.sleep(1)
924
+ continue
925
+ else:
926
+ if stream:
927
+ yield self._create_stream_chunk(f"⚠️ 放大失败: {error_str},返回原图...\n")
928
+ break
929
 
930
  # 缓存图片 (如果启用)
931
  local_url = image_url
 
1182
  if stream:
1183
  yield self._create_stream_chunk(f"视频生成中...\n")
1184
 
1185
+ # 检查是否需要放大
1186
+ upsample_config = model_config.get("upsample")
1187
+
1188
+ async for chunk in self._poll_video_result(token, project_id, operations, stream, upsample_config):
1189
  yield chunk
1190
 
1191
  finally:
 
1196
  async def _poll_video_result(
1197
  self,
1198
  token,
1199
+ project_id: str,
1200
  operations: List[Dict],
1201
+ stream: bool,
1202
+ upsample_config: Optional[Dict] = None
1203
  ) -> AsyncGenerator:
1204
+ """轮询视频生成结果
1205
+
1206
+ Args:
1207
+ upsample_config: 放大配置 {"resolution": "VIDEO_RESOLUTION_4K", "model_key": "veo_3_1_upsampler_4k"}
1208
+ """
1209
 
1210
  max_attempts = config.max_poll_attempts
1211
  poll_interval = config.poll_interval
1212
+
1213
+ # 如果需要放大,轮询次数加倍(放大可能需要 30 分钟)
1214
+ if upsample_config:
1215
+ max_attempts = max_attempts * 3 # 放大需要更长时间
1216
 
1217
  for attempt in range(max_attempts):
1218
  await asyncio.sleep(poll_interval)
 
1239
  metadata = operation["operation"].get("metadata", {})
1240
  video_info = metadata.get("video", {})
1241
  video_url = video_info.get("fifeUrl")
1242
+ video_media_id = video_info.get("mediaGenerationId")
1243
+ aspect_ratio = video_info.get("aspectRatio", "VIDEO_ASPECT_RATIO_LANDSCAPE")
1244
 
1245
  if not video_url:
1246
  yield self._create_error_response("视频URL为空")
1247
  return
1248
 
1249
+ # ========== 视频放大处理 ==========
1250
+ if upsample_config and video_media_id:
1251
+ if stream:
1252
+ resolution_name = "4K" if "4K" in upsample_config["resolution"] else "1080P"
1253
+ yield self._create_stream_chunk(f"\n视频生成完成,开始 {resolution_name} 放大处理...(可能需要 30 分钟)\n")
1254
+
1255
+ try:
1256
+ # 提交放大任务
1257
+ upsample_result = await self.flow_client.upsample_video(
1258
+ at=token.at,
1259
+ project_id=project_id,
1260
+ video_media_id=video_media_id,
1261
+ aspect_ratio=aspect_ratio,
1262
+ resolution=upsample_config["resolution"],
1263
+ model_key=upsample_config["model_key"]
1264
+ )
1265
+
1266
+ upsample_operations = upsample_result.get("operations", [])
1267
+ if upsample_operations:
1268
+ if stream:
1269
+ yield self._create_stream_chunk("放大任务已提交,继续轮询...\n")
1270
+
1271
+ # 递归轮询放大结果(不再放大)
1272
+ async for chunk in self._poll_video_result(
1273
+ token, project_id, upsample_operations, stream, None
1274
+ ):
1275
+ yield chunk
1276
+ return
1277
+ else:
1278
+ if stream:
1279
+ yield self._create_stream_chunk("⚠️ 放大任务创建失败,返回原始视频\n")
1280
+ except Exception as e:
1281
+ debug_logger.log_error(f"Video upsample failed: {str(e)}")
1282
+ if stream:
1283
+ yield self._create_stream_chunk(f"⚠️ 放大失败: {str(e)},返回原始视频\n")
1284
+
1285
  # 缓存视频 (如果启用)
1286
  local_url = video_url
1287
  if config.cache_enabled:
static/manage.html CHANGED
@@ -156,9 +156,9 @@
156
  <div id="panelSettings" class="hidden">
157
  <div class="grid gap-6 lg:grid-cols-2">
158
  <!-- 安全配置 -->
159
- <div class="rounded-lg border border-border bg-background p-6">
160
  <h3 class="text-lg font-semibold mb-4">安全配置</h3>
161
- <div class="space-y-4">
162
  <div>
163
  <label class="text-sm font-medium mb-2 block">管理员用户名</label>
164
  <input id="cfgAdminUsername" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
@@ -172,14 +172,14 @@
172
  <label class="text-sm font-medium mb-2 block">新密码</label>
173
  <input id="cfgNewPassword" type="password" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="输入新密码">
174
  </div>
175
- <button onclick="updateAdminPassword()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full">修改密码</button>
176
  </div>
 
177
  </div>
178
 
179
  <!-- API 密钥配置 -->
180
- <div class="rounded-lg border border-border bg-background p-6">
181
  <h3 class="text-lg font-semibold mb-4">API 密钥配置</h3>
182
- <div class="space-y-4">
183
  <div>
184
  <label class="text-sm font-medium mb-2 block">当前 API Key</label>
185
  <input id="cfgCurrentAPIKey" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" readonly disabled>
@@ -190,14 +190,14 @@
190
  <input id="cfgNewAPIKey" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="输入新的 API Key">
191
  <p class="text-xs text-muted-foreground mt-1">用于客户端调用 API 的密钥</p>
192
  </div>
193
- <button onclick="updateAPIKey()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full">更新 API Key</button>
194
  </div>
 
195
  </div>
196
 
197
  <!-- 代理配置 -->
198
- <div class="rounded-lg border border-border bg-background p-6">
199
  <h3 class="text-lg font-semibold mb-4">代理配置</h3>
200
- <div class="space-y-4">
201
  <div>
202
  <label class="inline-flex items-center gap-2 cursor-pointer">
203
  <input type="checkbox" id="cfgProxyEnabled" class="h-4 w-4 rounded border-input">
@@ -209,27 +209,45 @@
209
  <input id="cfgProxyUrl" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="http://127.0.0.1:7890 或 socks5://127.0.0.1:1080">
210
  <p class="text-xs text-muted-foreground mt-1">支持 HTTP 和 SOCKS5 代理</p>
211
  </div>
212
- <button onclick="saveProxyConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full">保存配置</button>
213
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  </div>
215
 
216
  <!-- 错误处理配置 -->
217
- <div class="rounded-lg border border-border bg-background p-6">
218
  <h3 class="text-lg font-semibold mb-4">错误处理配置</h3>
219
- <div class="space-y-4">
220
  <div>
221
  <label class="text-sm font-medium mb-2 block">错误封禁阈值</label>
222
  <input id="cfgErrorBan" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="3">
223
  <p class="text-xs text-muted-foreground mt-1">Token 连续错误达到此次数后自动禁用</p>
224
  </div>
225
- <button onclick="saveAdminConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full">保存配置</button>
226
  </div>
 
227
  </div>
228
 
229
  <!-- 缓存配置 -->
230
- <div class="rounded-lg border border-border bg-background p-6">
231
  <h3 class="text-lg font-semibold mb-4">缓存配置</h3>
232
- <div class="space-y-4">
233
  <div>
234
  <label class="inline-flex items-center gap-2 cursor-pointer">
235
  <input type="checkbox" id="cfgCacheEnabled" class="h-4 w-4 rounded border-input" onchange="toggleCacheOptions()">
@@ -256,15 +274,51 @@
256
  </p>
257
  </div>
258
  </div>
 
 
 
259
 
260
- <button onclick="saveCacheConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full">保存配置</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  </div>
 
262
  </div>
263
 
264
  <!-- 验证码配置 -->
265
- <div class="rounded-lg border border-border bg-background p-6">
266
  <h3 class="text-lg font-semibold mb-4">验证码配置</h3>
267
- <div class="space-y-4">
268
  <div>
269
  <label class="text-sm font-medium mb-2 block">打码方式</label>
270
  <select id="cfgCaptchaMethod" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" onchange="toggleCaptchaOptions()">
@@ -272,7 +326,7 @@
272
  <option value="capmonster">CapMonster打码</option>
273
  <option value="ezcaptcha">EzCaptcha打码</option>
274
  <option value="capsolver">CapSolver打码</option>
275
- <option value="browser">头浏览器打码</option>
276
  <option value="personal">内置浏览器打码</option>
277
  </select>
278
  <p class="text-xs text-muted-foreground mt-1">选择验证码获取方式</p>
@@ -347,7 +401,7 @@
347
  <input type="checkbox" id="cfgBrowserProxyEnabled" class="h-4 w-4 rounded border-input" onchange="toggleBrowserProxyInput()">
348
  <span class="text-sm font-medium">启用代理</span>
349
  </label>
350
- <p class="text-xs text-muted-foreground mt-2">为头浏览器配置独立代理</p>
351
  </div>
352
 
353
  <div id="browserProxyUrlInput" class="hidden">
@@ -358,71 +412,21 @@
358
  示例:<code class="bg-muted px-1 py-0.5 rounded">http://user:pass@proxy.com:8080</code> 或 <code class="bg-muted px-1 py-0.5 rounded">socks5://proxy.com:1080</code>
359
  </p>
360
  </div>
361
- </div>
362
 
363
- <button onclick="saveCaptchaConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full">保存配置</button>
364
- </div>
365
- </div>
366
-
367
- <!-- 插件连接配置 -->
368
- <div class="rounded-lg border border-border bg-background p-6">
369
- <h3 class="text-lg font-semibold mb-4">插件连接配置</h3>
370
- <div class="space-y-4">
371
- <div>
372
- <label class="text-sm font-semibold mb-2 block">连接接口</label>
373
- <div class="flex gap-2">
374
- <input id="cfgPluginConnectionUrl" type="text" readonly class="flex h-9 flex-1 rounded-md border border-input bg-muted px-3 py-2 text-sm" placeholder="加载中...">
375
- <button onclick="copyConnectionUrl()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
376
- </div>
377
- <p class="text-xs text-muted-foreground mt-1">Chrome扩展插件需要配置此接口地址</p>
378
- </div>
379
- <div>
380
- <label class="text-sm font-semibold mb-2 block">连接Token</label>
381
- <div class="flex gap-2">
382
- <input id="cfgPluginConnectionToken" type="text" class="flex h-9 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="留空自动生成">
383
- <button onclick="generateRandomToken()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4">随机</button>
384
- <button onclick="copyConnectionToken()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
385
  </div>
386
- <p class="text-xs text-muted-foreground mt-1">用于验证Chrome扩展插件的身份,留空将自动生成随机token</p>
387
- </div>
388
- <div>
389
- <label class="inline-flex items-center gap-2 cursor-pointer">
390
- <input type="checkbox" id="cfgAutoEnableOnUpdate" class="h-4 w-4 rounded border-input">
391
- <span class="text-sm font-medium">更新token时自动启用</span>
392
- </label>
393
- <p class="text-xs text-muted-foreground mt-2">当插件更新token时,如果该token被禁用,则自动启用它</p>
394
- </div>
395
- <div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
396
- <p class="text-xs text-blue-800 dark:text-blue-200">
397
- ℹ️ <strong>使用说明:</strong>安装Chrome扩展后,将连接接口和Token配置到插件中,插件会自动提取Google Labs的cookie并更新到系统
398
- </p>
399
- </div>
400
- <button onclick="savePluginConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full">保存配置</button>
401
- </div>
402
- </div>
403
-
404
- <!-- 生成超时配置 -->
405
- <div class="rounded-lg border border-border bg-background p-6">
406
- <h3 class="text-lg font-semibold mb-4">生成超时配置</h3>
407
- <div class="space-y-4">
408
- <div>
409
- <label class="text-sm font-medium mb-2 block">图片生成超时时间(秒)</label>
410
- <input id="cfgImageTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="300" min="60" max="3600">
411
- <p class="text-xs text-muted-foreground mt-1">图片生成超时时间,范围:60-3600 秒(1分钟-1小时),超时后自动释放Token锁</p>
412
- </div>
413
- <div>
414
- <label class="text-sm font-medium mb-2 block">视频生成超时时间(秒)</label>
415
- <input id="cfgVideoTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="1500" min="60" max="7200">
416
- <p class="text-xs text-muted-foreground mt-1">视频生成超时时间,范围:60-7200 秒(1分钟-2小时),超时后返回上游API超时错误</p>
417
  </div>
418
- <button onclick="saveGenerationTimeout()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full">保存配置</button>
419
  </div>
 
420
  </div>
421
 
422
  <!-- 调试配置 -->
423
- <div class="rounded-lg border border-border bg-background p-6">
424
  <h3 class="text-lg font-semibold mb-4">调试配置</h3>
425
- <div class="space-y-4">
426
  <div>
427
  <label class="inline-flex items-center gap-2 cursor-pointer">
428
  <input type="checkbox" id="cfgDebugEnabled" class="h-4 w-4 rounded border-input" onchange="toggleDebugMode()">
@@ -436,6 +440,7 @@
436
  </p>
437
  </div>
438
  </div>
 
439
  </div>
440
  </div>
441
  </div>
@@ -746,8 +751,8 @@
746
  saveGenerationTimeout=async()=>{const imageTimeout=parseInt($('cfgImageTimeout').value)||300,videoTimeout=parseInt($('cfgVideoTimeout').value)||1500;console.log('保存生成超时配置:',{imageTimeout,videoTimeout});if(imageTimeout<60||imageTimeout>3600)return showToast('图片超时时间必须在 60-3600 秒之间','error');if(videoTimeout<60||videoTimeout>7200)return showToast('视频超时时间必须在 60-7200 秒之间','error');try{const r=await apiRequest('/api/generation/timeout',{method:'POST',body:JSON.stringify({image_timeout:imageTimeout,video_timeout:videoTimeout})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('生成超时配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadGenerationTimeout()}else{console.error('保存失败:',d);showToast('保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
747
  toggleCaptchaOptions=()=>{const method=$('cfgCaptchaMethod').value;$('yescaptchaOptions').style.display=method==='yescaptcha'?'block':'none';$('capmonsterOptions').classList.toggle('hidden',method!=='capmonster');$('ezcaptchaOptions').classList.toggle('hidden',method!=='ezcaptcha');$('capsolverOptions').classList.toggle('hidden',method!=='capsolver');$('browserCaptchaOptions').classList.toggle('hidden',method!=='browser')},
748
  toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
749
- loadCaptchaConfig=async()=>{try{console.log('开始加载验证码配置...');const r=await apiRequest('/api/captcha/config');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('验证码配置数据:',d);$('cfgCaptchaMethod').value=d.captcha_method||'yescaptcha';$('cfgYescaptchaApiKey').value=d.yescaptcha_api_key||'';$('cfgYescaptchaBaseUrl').value=d.yescaptcha_base_url||'https://api.yescaptcha.com';$('cfgCapmonsterApiKey').value=d.capmonster_api_key||'';$('cfgCapmonsterBaseUrl').value=d.capmonster_base_url||'https://api.capmonster.cloud';$('cfgEzcaptchaApiKey').value=d.ezcaptcha_api_key||'';$('cfgEzcaptchaBaseUrl').value=d.ezcaptcha_base_url||'https://api.ez-captcha.com';$('cfgCapsolverApiKey').value=d.capsolver_api_key||'';$('cfgCapsolverBaseUrl').value=d.capsolver_base_url||'https://api.capsolver.com';$('cfgBrowserProxyEnabled').checked=d.browser_proxy_enabled||false;$('cfgBrowserProxyUrl').value=d.browser_proxy_url||'';toggleCaptchaOptions();toggleBrowserProxyInput();console.log('验证码配置加载成功')}catch(e){console.error('加载验证码配置失败:',e);showToast('加载验证码配置失败: '+e.message,'error')}},
750
- saveCaptchaConfig=async()=>{const method=$('cfgCaptchaMethod').value,yesApiKey=$('cfgYescaptchaApiKey').value.trim(),yesBaseUrl=$('cfgYescaptchaBaseUrl').value.trim(),capApiKey=$('cfgCapmonsterApiKey').value.trim(),capBaseUrl=$('cfgCapmonsterBaseUrl').value.trim(),ezApiKey=$('cfgEzcaptchaApiKey').value.trim(),ezBaseUrl=$('cfgEzcaptchaBaseUrl').value.trim(),solverApiKey=$('cfgCapsolverApiKey').value.trim(),solverBaseUrl=$('cfgCapsolverBaseUrl').value.trim(),browserProxyEnabled=$('cfgBrowserProxyEnabled').checked,browserProxyUrl=$('cfgBrowserProxyUrl').value.trim();console.log('保存验证码配置:',{method,yesApiKey,yesBaseUrl,capApiKey,capBaseUrl,ezApiKey,ezBaseUrl,solverApiKey,solverBaseUrl,browserProxyEnabled,browserProxyUrl});try{const r=await apiRequest('/api/captcha/config',{method:'POST',body:JSON.stringify({captcha_method:method,yescaptcha_api_key:yesApiKey,yescaptcha_base_url:yesBaseUrl,capmonster_api_key:capApiKey,capmonster_base_url:capBaseUrl,ezcaptcha_api_key:ezApiKey,ezcaptcha_base_url:ezBaseUrl,capsolver_api_key:solverApiKey,capsolver_base_url:solverBaseUrl,browser_proxy_enabled:browserProxyEnabled,browser_proxy_url:browserProxyUrl})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('验证码配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadCaptchaConfig()}else{console.error('保存失败:',d);showToast(d.message||'保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
751
  loadPluginConfig=async()=>{try{const r=await apiRequest('/api/plugin/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPluginConnectionUrl').value=d.config.connection_url||'';$('cfgPluginConnectionToken').value=d.config.connection_token||'';$('cfgAutoEnableOnUpdate').checked=d.config.auto_enable_on_update||false}}catch(e){console.error('加载插件配置失败:',e);showToast('加载插件配置失败: '+e.message,'error')}},
752
  savePluginConfig=async()=>{const token=$('cfgPluginConnectionToken').value.trim();const autoEnable=$('cfgAutoEnableOnUpdate').checked;try{const r=await apiRequest('/api/plugin/config',{method:'POST',body:JSON.stringify({connection_token:token,auto_enable_on_update:autoEnable})});if(!r)return;const d=await r.json();if(d.success){showToast('插件配置保存成功','success');await loadPluginConfig()}else{showToast(d.message||'保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}},
753
  copyConnectionUrl=()=>{const url=$('cfgPluginConnectionUrl').value;if(!url){showToast('连接接口为空','error');return}navigator.clipboard.writeText(url).then(()=>showToast('连接接口已复制','success')).catch(()=>showToast('复制失败','error'))},
 
156
  <div id="panelSettings" class="hidden">
157
  <div class="grid gap-6 lg:grid-cols-2">
158
  <!-- 安全配置 -->
159
+ <div class="rounded-lg border border-border bg-background p-6 flex flex-col">
160
  <h3 class="text-lg font-semibold mb-4">安全配置</h3>
161
+ <div class="space-y-4 flex-1">
162
  <div>
163
  <label class="text-sm font-medium mb-2 block">管理员用户名</label>
164
  <input id="cfgAdminUsername" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
 
172
  <label class="text-sm font-medium mb-2 block">新密码</label>
173
  <input id="cfgNewPassword" type="password" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="输入新密码">
174
  </div>
 
175
  </div>
176
+ <button onclick="updateAdminPassword()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full mt-4">修改密码</button>
177
  </div>
178
 
179
  <!-- API 密钥配置 -->
180
+ <div class="rounded-lg border border-border bg-background p-6 flex flex-col">
181
  <h3 class="text-lg font-semibold mb-4">API 密钥配置</h3>
182
+ <div class="space-y-4 flex-1">
183
  <div>
184
  <label class="text-sm font-medium mb-2 block">当前 API Key</label>
185
  <input id="cfgCurrentAPIKey" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" readonly disabled>
 
190
  <input id="cfgNewAPIKey" type="text" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="输入新的 API Key">
191
  <p class="text-xs text-muted-foreground mt-1">用于客户端调用 API 的密钥</p>
192
  </div>
 
193
  </div>
194
+ <button onclick="updateAPIKey()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full mt-4">更新 API Key</button>
195
  </div>
196
 
197
  <!-- 代理配置 -->
198
+ <div class="rounded-lg border border-border bg-background p-6 flex flex-col">
199
  <h3 class="text-lg font-semibold mb-4">代理配置</h3>
200
+ <div class="space-y-4 flex-1">
201
  <div>
202
  <label class="inline-flex items-center gap-2 cursor-pointer">
203
  <input type="checkbox" id="cfgProxyEnabled" class="h-4 w-4 rounded border-input">
 
209
  <input id="cfgProxyUrl" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="http://127.0.0.1:7890 或 socks5://127.0.0.1:1080">
210
  <p class="text-xs text-muted-foreground mt-1">支持 HTTP 和 SOCKS5 代理</p>
211
  </div>
 
212
  </div>
213
+ <button onclick="saveProxyConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full mt-4">保存配置</button>
214
+ </div>
215
+
216
+ <!-- 生成超时配置 -->
217
+ <div class="rounded-lg border border-border bg-background p-6 flex flex-col">
218
+ <h3 class="text-lg font-semibold mb-4">生成超时配置</h3>
219
+ <div class="space-y-4 flex-1">
220
+ <div>
221
+ <label class="text-sm font-medium mb-2 block">图片生成超时时间(秒)</label>
222
+ <input id="cfgImageTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="300" min="60" max="3600">
223
+ <p class="text-xs text-muted-foreground mt-1">图片生成超时时间,范围:60-3600 秒(1分钟-1小时),超时后自动释放Token锁</p>
224
+ </div>
225
+ <div>
226
+ <label class="text-sm font-medium mb-2 block">视频生成超时时间(秒)</label>
227
+ <input id="cfgVideoTimeout" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="1500" min="60" max="7200">
228
+ <p class="text-xs text-muted-foreground mt-1">视频生成超时时间,范围:60-7200 秒(1分钟-2小时),超时后返回上游API超时错误</p>
229
+ </div>
230
+ </div>
231
+ <button onclick="saveGenerationTimeout()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full mt-4">保存配置</button>
232
  </div>
233
 
234
  <!-- 错误处理配置 -->
235
+ <div class="rounded-lg border border-border bg-background p-6 flex flex-col">
236
  <h3 class="text-lg font-semibold mb-4">错误处理配置</h3>
237
+ <div class="space-y-4 flex-1">
238
  <div>
239
  <label class="text-sm font-medium mb-2 block">错误封禁阈值</label>
240
  <input id="cfgErrorBan" type="number" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="3">
241
  <p class="text-xs text-muted-foreground mt-1">Token 连续错误达到此次数后自动禁用</p>
242
  </div>
 
243
  </div>
244
+ <button onclick="saveAdminConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full mt-4">保存配置</button>
245
  </div>
246
 
247
  <!-- 缓存配置 -->
248
+ <div class="rounded-lg border border-border bg-background p-6 flex flex-col">
249
  <h3 class="text-lg font-semibold mb-4">缓存配置</h3>
250
+ <div class="space-y-4 flex-1">
251
  <div>
252
  <label class="inline-flex items-center gap-2 cursor-pointer">
253
  <input type="checkbox" id="cfgCacheEnabled" class="h-4 w-4 rounded border-input" onchange="toggleCacheOptions()">
 
274
  </p>
275
  </div>
276
  </div>
277
+ </div>
278
+ <button onclick="saveCacheConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full mt-4">保存配置</button>
279
+ </div>
280
 
281
+ <!-- 插件连接配置 -->
282
+ <div class="rounded-lg border border-border bg-background p-6 flex flex-col">
283
+ <h3 class="text-lg font-semibold mb-4">插件连接配置</h3>
284
+ <div class="space-y-4 flex-1">
285
+ <div>
286
+ <label class="text-sm font-semibold mb-2 block">连接接口</label>
287
+ <div class="flex gap-2">
288
+ <input id="cfgPluginConnectionUrl" type="text" readonly class="flex h-9 flex-1 rounded-md border border-input bg-muted px-3 py-2 text-sm" placeholder="加载中...">
289
+ <button onclick="copyConnectionUrl()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
290
+ </div>
291
+ <p class="text-xs text-muted-foreground mt-1">Chrome扩展插件需要配置此接口地址</p>
292
+ </div>
293
+ <div>
294
+ <label class="text-sm font-semibold mb-2 block">连接Token</label>
295
+ <div class="flex gap-2">
296
+ <input id="cfgPluginConnectionToken" type="text" class="flex h-9 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="留空自动生成">
297
+ <button onclick="generateRandomToken()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4">随机</button>
298
+ <button onclick="copyConnectionToken()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
299
+ </div>
300
+ <p class="text-xs text-muted-foreground mt-1">用于验证Chrome扩展插件的身份,留空将自动生成随机token</p>
301
+ </div>
302
+ <div>
303
+ <label class="inline-flex items-center gap-2 cursor-pointer">
304
+ <input type="checkbox" id="cfgAutoEnableOnUpdate" class="h-4 w-4 rounded border-input">
305
+ <span class="text-sm font-medium">更新token时自动启用</span>
306
+ </label>
307
+ <p class="text-xs text-muted-foreground mt-2">当插件更新token时,如果该token被禁用,则自动启用它</p>
308
+ </div>
309
+ <div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
310
+ <p class="text-xs text-blue-800 dark:text-blue-200">
311
+ ℹ️ <strong>使用说明:</strong>安装Chrome扩展后,将连接接口和Token配置到插件中,插件会自动提取Google Labs的cookie并更新到系统
312
+ </p>
313
+ </div>
314
  </div>
315
+ <button onclick="savePluginConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full mt-4">保存配置</button>
316
  </div>
317
 
318
  <!-- 验证码配置 -->
319
+ <div class="rounded-lg border border-border bg-background p-6 flex flex-col">
320
  <h3 class="text-lg font-semibold mb-4">验证码配置</h3>
321
+ <div class="space-y-4 flex-1">
322
  <div>
323
  <label class="text-sm font-medium mb-2 block">打码方式</label>
324
  <select id="cfgCaptchaMethod" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" onchange="toggleCaptchaOptions()">
 
326
  <option value="capmonster">CapMonster打码</option>
327
  <option value="ezcaptcha">EzCaptcha打码</option>
328
  <option value="capsolver">CapSolver打码</option>
329
+ <option value="browser">头浏览器打码</option>
330
  <option value="personal">内置浏览器打码</option>
331
  </select>
332
  <p class="text-xs text-muted-foreground mt-1">选择验证码获取方式</p>
 
401
  <input type="checkbox" id="cfgBrowserProxyEnabled" class="h-4 w-4 rounded border-input" onchange="toggleBrowserProxyInput()">
402
  <span class="text-sm font-medium">启用代理</span>
403
  </label>
404
+ <p class="text-xs text-muted-foreground mt-2">为头浏览器配置独立代理</p>
405
  </div>
406
 
407
  <div id="browserProxyUrlInput" class="hidden">
 
412
  示例:<code class="bg-muted px-1 py-0.5 rounded">http://user:pass@proxy.com:8080</code> 或 <code class="bg-muted px-1 py-0.5 rounded">socks5://proxy.com:1080</code>
413
  </p>
414
  </div>
 
415
 
416
+ <div>
417
+ <label class="text-sm font-medium mb-2 block">浏览器数量</label>
418
+ <input id="cfgBrowserCount" type="number" min="1" max="20" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="1" value="1">
419
+ <p class="text-xs text-muted-foreground mt-1">同时启动的浏览器实例数量,每个浏览器只开1个标签页,请求轮询分配</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  </div>
 
422
  </div>
423
+ <button onclick="saveCaptchaConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full mt-4">保存配置</button>
424
  </div>
425
 
426
  <!-- 调试配置 -->
427
+ <div class="rounded-lg border border-border bg-background p-6 flex flex-col">
428
  <h3 class="text-lg font-semibold mb-4">调试配置</h3>
429
+ <div class="space-y-4 flex-1">
430
  <div>
431
  <label class="inline-flex items-center gap-2 cursor-pointer">
432
  <input type="checkbox" id="cfgDebugEnabled" class="h-4 w-4 rounded border-input" onchange="toggleDebugMode()">
 
440
  </p>
441
  </div>
442
  </div>
443
+ <div class="text-xs text-muted-foreground text-center mt-4 h-9 flex items-center justify-center">✅ 开关状态自动保存</div>
444
  </div>
445
  </div>
446
  </div>
 
751
  saveGenerationTimeout=async()=>{const imageTimeout=parseInt($('cfgImageTimeout').value)||300,videoTimeout=parseInt($('cfgVideoTimeout').value)||1500;console.log('保存生成超时配置:',{imageTimeout,videoTimeout});if(imageTimeout<60||imageTimeout>3600)return showToast('图片超时时间必须在 60-3600 秒之间','error');if(videoTimeout<60||videoTimeout>7200)return showToast('视频超时时间必须在 60-7200 秒之间','error');try{const r=await apiRequest('/api/generation/timeout',{method:'POST',body:JSON.stringify({image_timeout:imageTimeout,video_timeout:videoTimeout})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('生成超时配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadGenerationTimeout()}else{console.error('保存失败:',d);showToast('保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
752
  toggleCaptchaOptions=()=>{const method=$('cfgCaptchaMethod').value;$('yescaptchaOptions').style.display=method==='yescaptcha'?'block':'none';$('capmonsterOptions').classList.toggle('hidden',method!=='capmonster');$('ezcaptchaOptions').classList.toggle('hidden',method!=='ezcaptcha');$('capsolverOptions').classList.toggle('hidden',method!=='capsolver');$('browserCaptchaOptions').classList.toggle('hidden',method!=='browser')},
753
  toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
754
+ loadCaptchaConfig=async()=>{try{console.log('开始加载验证码配置...');const r=await apiRequest('/api/captcha/config');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('验证码配置数据:',d);$('cfgCaptchaMethod').value=d.captcha_method||'yescaptcha';$('cfgYescaptchaApiKey').value=d.yescaptcha_api_key||'';$('cfgYescaptchaBaseUrl').value=d.yescaptcha_base_url||'https://api.yescaptcha.com';$('cfgCapmonsterApiKey').value=d.capmonster_api_key||'';$('cfgCapmonsterBaseUrl').value=d.capmonster_base_url||'https://api.capmonster.cloud';$('cfgEzcaptchaApiKey').value=d.ezcaptcha_api_key||'';$('cfgEzcaptchaBaseUrl').value=d.ezcaptcha_base_url||'https://api.ez-captcha.com';$('cfgCapsolverApiKey').value=d.capsolver_api_key||'';$('cfgCapsolverBaseUrl').value=d.capsolver_base_url||'https://api.capsolver.com';$('cfgBrowserProxyEnabled').checked=d.browser_proxy_enabled||false;$('cfgBrowserProxyUrl').value=d.browser_proxy_url||'';$('cfgBrowserCount').value=d.browser_count||1;toggleCaptchaOptions();toggleBrowserProxyInput();console.log('验证码配置加载成功')}catch(e){console.error('加载验证码配置失败:',e);showToast('加载验证码配置失败: '+e.message,'error')}},
755
+ saveCaptchaConfig=async()=>{const method=$('cfgCaptchaMethod').value,yesApiKey=$('cfgYescaptchaApiKey').value.trim(),yesBaseUrl=$('cfgYescaptchaBaseUrl').value.trim(),capApiKey=$('cfgCapmonsterApiKey').value.trim(),capBaseUrl=$('cfgCapmonsterBaseUrl').value.trim(),ezApiKey=$('cfgEzcaptchaApiKey').value.trim(),ezBaseUrl=$('cfgEzcaptchaBaseUrl').value.trim(),solverApiKey=$('cfgCapsolverApiKey').value.trim(),solverBaseUrl=$('cfgCapsolverBaseUrl').value.trim(),browserProxyEnabled=$('cfgBrowserProxyEnabled').checked,browserProxyUrl=$('cfgBrowserProxyUrl').value.trim(),browserCount=parseInt($('cfgBrowserCount').value)||1;console.log('保存验证码配置:',{method,yesApiKey,yesBaseUrl,capApiKey,capBaseUrl,ezApiKey,ezBaseUrl,solverApiKey,solverBaseUrl,browserProxyEnabled,browserProxyUrl,browserCount});try{const r=await apiRequest('/api/captcha/config',{method:'POST',body:JSON.stringify({captcha_method:method,yescaptcha_api_key:yesApiKey,yescaptcha_base_url:yesBaseUrl,capmonster_api_key:capApiKey,capmonster_base_url:capBaseUrl,ezcaptcha_api_key:ezApiKey,ezcaptcha_base_url:ezBaseUrl,capsolver_api_key:solverApiKey,capsolver_base_url:solverBaseUrl,browser_proxy_enabled:browserProxyEnabled,browser_proxy_url:browserProxyUrl,browser_count:browserCount})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('验证码配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadCaptchaConfig()}else{console.error('保存失败:',d);showToast(d.message||'保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
756
  loadPluginConfig=async()=>{try{const r=await apiRequest('/api/plugin/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPluginConnectionUrl').value=d.config.connection_url||'';$('cfgPluginConnectionToken').value=d.config.connection_token||'';$('cfgAutoEnableOnUpdate').checked=d.config.auto_enable_on_update||false}}catch(e){console.error('加载插件配置失败:',e);showToast('加载插件配置失败: '+e.message,'error')}},
757
  savePluginConfig=async()=>{const token=$('cfgPluginConnectionToken').value.trim();const autoEnable=$('cfgAutoEnableOnUpdate').checked;try{const r=await apiRequest('/api/plugin/config',{method:'POST',body:JSON.stringify({connection_token:token,auto_enable_on_update:autoEnable})});if(!r)return;const d=await r.json();if(d.success){showToast('插件配置保存成功','success');await loadPluginConfig()}else{showToast(d.message||'保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}},
758
  copyConnectionUrl=()=>{const url=$('cfgPluginConnectionUrl').value;if(!url){showToast('连接接口为空','error');return}navigator.clipboard.writeText(url).then(()=>showToast('连接接口已复制','success')).catch(()=>showToast('复制失败','error'))},