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

feat(browser-captcha): add resident mode for persistent reCAPTCHA token generation

Browse files

- Implement resident mode that maintains a persistent tab for instant token generation
- Add automatic project ID detection from active tokens during initialization
- Support fallback to traditional mode when no active project ID is available
- Add proper sandbox configuration and profile directory handling for nodriver
- Implement reCAPTCHA loading with retry mechanism and error handling
- Add legacy mode as fallback when resident mode fails
- Include comprehensive logging for both resident and legacy modes
- Add browser state management and cleanup functions

Files changed (2) hide show
  1. src/main.py +18 -1
  2. src/services/browser_captcha_personal.py +263 -135
src/main.py CHANGED
@@ -78,8 +78,24 @@ async def lifespan(app: FastAPI):
78
  if captcha_config.captcha_method == "personal":
79
  from .services.browser_captcha_personal import BrowserCaptchaService
80
  browser_service = await BrowserCaptchaService.get_instance(db)
81
- await browser_service.open_login_window()
82
  print("✓ Browser captcha service initialized (nodriver mode)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  elif captcha_config.captcha_method == "browser":
84
  from .services.browser_captcha import BrowserCaptchaService
85
  browser_service = await BrowserCaptchaService.get_instance(db)
@@ -87,6 +103,7 @@ async def lifespan(app: FastAPI):
87
 
88
  # Initialize concurrency manager
89
  tokens = await token_manager.get_all_tokens()
 
90
  await concurrency_manager.initialize(tokens)
91
 
92
  # Start file cache cleanup task
 
78
  if captcha_config.captcha_method == "personal":
79
  from .services.browser_captcha_personal import BrowserCaptchaService
80
  browser_service = await BrowserCaptchaService.get_instance(db)
 
81
  print("✓ Browser captcha service initialized (nodriver mode)")
82
+
83
+ # 启动常驻模式:从第一个可用token获取project_id
84
+ tokens = await token_manager.get_all_tokens()
85
+ resident_project_id = None
86
+ for t in tokens:
87
+ if t.current_project_id and t.is_active:
88
+ resident_project_id = t.current_project_id
89
+ break
90
+
91
+ if resident_project_id:
92
+ # 直接启动常驻模式(会自动导航到项目页面,cookie已持久化)
93
+ await browser_service.start_resident_mode(resident_project_id)
94
+ print(f"✓ Browser captcha resident mode started (project: {resident_project_id[:8]}...)")
95
+ else:
96
+ # 没有可用的project_id时,打开登录窗口供用户手动操作
97
+ await browser_service.open_login_window()
98
+ print("⚠ No active token with project_id found, opened login window for manual setup")
99
  elif captcha_config.captcha_method == "browser":
100
  from .services.browser_captcha import BrowserCaptchaService
101
  browser_service = await BrowserCaptchaService.get_instance(db)
 
103
 
104
  # Initialize concurrency manager
105
  tokens = await token_manager.get_all_tokens()
106
+
107
  await concurrency_manager.initialize(tokens)
108
 
109
  # Start file cache cleanup task
src/services/browser_captcha_personal.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
  浏览器自动化获取 reCAPTCHA token
3
  使用 nodriver (undetected-chromedriver 继任者) 实现反检测浏览器
 
4
  """
5
  import asyncio
6
  import time
@@ -13,7 +14,12 @@ from ..core.logger import debug_logger
13
 
14
 
15
  class BrowserCaptchaService:
16
- """浏览器自动化获取 reCAPTCHA token(nodriver 有头模式)"""
 
 
 
 
 
17
 
18
  _instance: Optional['BrowserCaptchaService'] = None
19
  _lock = asyncio.Lock()
@@ -27,6 +33,12 @@ class BrowserCaptchaService:
27
  self.db = db
28
  # 持久化 profile 目录
29
  self.user_data_dir = os.path.join(os.getcwd(), "browser_data")
 
 
 
 
 
 
30
 
31
  @classmethod
32
  async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
@@ -62,12 +74,14 @@ class BrowserCaptchaService:
62
  self.browser = await uc.start(
63
  headless=self.headless,
64
  user_data_dir=self.user_data_dir,
 
65
  browser_args=[
66
  '--no-sandbox',
67
  '--disable-dev-shm-usage',
68
  '--disable-setuid-sandbox',
69
  '--disable-gpu',
70
  '--window-size=1280,720',
 
71
  ]
72
  )
73
 
@@ -78,8 +92,226 @@ class BrowserCaptchaService:
78
  debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
79
  raise
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  async def get_token(self, project_id: str) -> Optional[str]:
82
  """获取 reCAPTCHA token
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  Args:
85
  project_id: Flow项目ID
@@ -96,13 +328,13 @@ class BrowserCaptchaService:
96
 
97
  try:
98
  website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
99
- debug_logger.log_info(f"[BrowserCaptcha] 访问页面: {website_url}")
100
 
101
  # 新建标签页并访问页面
102
  tab = await self.browser.get(website_url)
103
 
104
  # 等待页面完全加载(增加等待时间)
105
- debug_logger.log_info("[BrowserCaptcha] 等待页面加载...")
106
  await tab.sleep(3)
107
 
108
  # 等待页面 DOM 完成
@@ -112,149 +344,28 @@ class BrowserCaptchaService:
112
  break
113
  await tab.sleep(0.5)
114
 
115
- # 检测 reCAPTCHA 是否已加载
116
- debug_logger.log_info("[BrowserCaptcha] 检测 reCAPTCHA...")
117
-
118
- # 页面使用的是 reCAPTCHA Enterprise,检查 grecaptcha.enterprise.execute
119
- is_enterprise = await tab.evaluate(
120
- "typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
121
- )
122
-
123
- debug_logger.log_info(f"[BrowserCaptcha] 检测结果: is_enterprise={is_enterprise}")
124
-
125
- recaptcha_type = "enterprise" if is_enterprise else None
126
-
127
- # 如果没有检测到 reCAPTCHA,尝试注入脚本
128
- if not recaptcha_type:
129
- debug_logger.log_info("[BrowserCaptcha] 未检测到 reCAPTCHA,注入脚本...")
130
-
131
- # 注入标准版 reCAPTCHA 脚本
132
- await tab.evaluate(f"""
133
- (() => {{
134
- if (document.querySelector('script[src*="recaptcha"]')) return;
135
- const script = document.createElement('script');
136
- script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
137
- script.async = true;
138
- document.head.appendChild(script);
139
- }})()
140
- """)
141
-
142
- # 等待脚本加载
143
- await tab.sleep(3)
144
-
145
- # 轮询等待 reCAPTCHA 加载
146
- for i in range(20):
147
- is_enterprise = await tab.evaluate(
148
- "typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
149
- )
150
-
151
- if is_enterprise:
152
- recaptcha_type = "enterprise"
153
- debug_logger.log_info(f"[BrowserCaptcha] reCAPTCHA Enterprise 已加载(等待了 {i * 0.5} 秒)")
154
- break
155
- await tab.sleep(0.5)
156
- else:
157
- debug_logger.log_warning("[BrowserCaptcha] reCAPTCHA 加载超时")
158
 
159
- if not recaptcha_type:
160
- debug_logger.log_error("[BrowserCaptcha] reCAPTCHA 无法加载")
161
  return None
162
 
163
- # 执行 reCAPTCHA 并获取 token(使用 window 变量传递异步结果)
164
- debug_logger.log_info(f"[BrowserCaptcha] 执行 reCAPTCHA 验证 (类型: {recaptcha_type})...")
165
-
166
- # 生成唯一变量名避免冲突
167
- ts = int(time.time() * 1000)
168
- token_var = f"_recaptcha_token_{ts}"
169
- error_var = f"_recaptcha_error_{ts}"
170
-
171
- # 根据类型��择正确的 API
172
- if recaptcha_type == "enterprise":
173
- execute_script = f"""
174
- (() => {{
175
- window.{token_var} = null;
176
- window.{error_var} = null;
177
-
178
- try {{
179
- grecaptcha.enterprise.ready(function() {{
180
- grecaptcha.enterprise.execute('{self.website_key}', {{action: 'FLOW_GENERATION'}})
181
- .then(function(token) {{
182
- window.{token_var} = token;
183
- }})
184
- .catch(function(err) {{
185
- window.{error_var} = err.message || 'execute failed';
186
- }});
187
- }});
188
- }} catch (e) {{
189
- window.{error_var} = e.message || 'exception';
190
- }}
191
- }})()
192
- """
193
- else:
194
- execute_script = f"""
195
- (() => {{
196
- window.{token_var} = null;
197
- window.{error_var} = null;
198
-
199
- try {{
200
- if (grecaptcha.ready) {{
201
- grecaptcha.ready(function() {{
202
- grecaptcha.execute('{self.website_key}', {{action: 'FLOW_GENERATION'}})
203
- .then(function(token) {{
204
- window.{token_var} = token;
205
- }})
206
- .catch(function(err) {{
207
- window.{error_var} = err.message || 'execute failed';
208
- }});
209
- }});
210
- }} else {{
211
- grecaptcha.execute('{self.website_key}', {{action: 'FLOW_GENERATION'}})
212
- .then(function(token) {{
213
- window.{token_var} = token;
214
- }})
215
- .catch(function(err) {{
216
- window.{error_var} = err.message || 'execute failed';
217
- }});
218
- }}
219
- }} catch (e) {{
220
- window.{error_var} = e.message || 'exception';
221
- }}
222
- }})()
223
- """
224
-
225
- # 注入执行脚本
226
- await tab.evaluate(execute_script)
227
-
228
- # 轮询等待结果(最多 15 秒)
229
- token = None
230
- for i in range(30):
231
- await tab.sleep(0.5)
232
- token = await tab.evaluate(f"window.{token_var}")
233
- if token:
234
- debug_logger.log_info(f"[BrowserCaptcha] Token 已获取(等待了 {i * 0.5} 秒)")
235
- break
236
- error = await tab.evaluate(f"window.{error_var}")
237
- if error:
238
- debug_logger.log_error(f"[BrowserCaptcha] reCAPTCHA 错误: {error}")
239
- break
240
-
241
- # 清理临时变量
242
- try:
243
- await tab.evaluate(f"delete window.{token_var}; delete window.{error_var};")
244
- except:
245
- pass
246
 
247
  duration_ms = (time.time() - start_time) * 1000
248
 
249
  if token:
250
- debug_logger.log_info(f"[BrowserCaptcha] ✅ Token获取成功(耗时 {duration_ms:.0f}ms)")
251
  return token
252
  else:
253
- debug_logger.log_error("[BrowserCaptcha] Token获取失败(返回null)")
254
  return None
255
 
256
  except Exception as e:
257
- debug_logger.log_error(f"[BrowserCaptcha] 获取token异常: {str(e)}")
258
  return None
259
  finally:
260
  # 关闭标签页(但保留浏览器)
@@ -266,6 +377,9 @@ class BrowserCaptchaService:
266
 
267
  async def close(self):
268
  """关闭浏览器"""
 
 
 
269
  try:
270
  if self.browser:
271
  try:
@@ -285,4 +399,18 @@ class BrowserCaptchaService:
285
  await self.initialize()
286
  tab = await self.browser.get("https://accounts.google.com/")
287
  debug_logger.log_info("[BrowserCaptcha] 请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
288
- print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  浏览器自动化获取 reCAPTCHA token
3
  使用 nodriver (undetected-chromedriver 继任者) 实现反检测浏览器
4
+ 支持常驻模式:基于单一 project_id 保持常驻标签页,即时生成 token
5
  """
6
  import asyncio
7
  import time
 
14
 
15
 
16
  class BrowserCaptchaService:
17
+ """浏览器自动化获取 reCAPTCHA token(nodriver 有头模式)
18
+
19
+ 支持两种模式:
20
+ 1. 常驻模式 (Resident Mode): 保持一个常驻标签页,即时生成 token
21
+ 2. 传统模式 (Legacy Mode): 每次请求创建新标签页 (fallback)
22
+ """
23
 
24
  _instance: Optional['BrowserCaptchaService'] = None
25
  _lock = asyncio.Lock()
 
33
  self.db = db
34
  # 持久化 profile 目录
35
  self.user_data_dir = os.path.join(os.getcwd(), "browser_data")
36
+
37
+ # 常驻模式相关属性
38
+ self.resident_project_id: Optional[str] = None # 常驻 project_id
39
+ self.resident_tab = None # 常驻标签页
40
+ self._running = False # 常驻模式运行状态
41
+ self._recaptcha_ready = False # reCAPTCHA 是否已加载
42
 
43
  @classmethod
44
  async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
 
74
  self.browser = await uc.start(
75
  headless=self.headless,
76
  user_data_dir=self.user_data_dir,
77
+ sandbox=False, # nodriver 需要此参数来禁用 sandbox
78
  browser_args=[
79
  '--no-sandbox',
80
  '--disable-dev-shm-usage',
81
  '--disable-setuid-sandbox',
82
  '--disable-gpu',
83
  '--window-size=1280,720',
84
+ '--profile-directory=Default', # 跳过 Profile 选择器页面
85
  ]
86
  )
87
 
 
92
  debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
93
  raise
94
 
95
+ # ========== 常驻模式 API ==========
96
+
97
+ async def start_resident_mode(self, project_id: str):
98
+ """启动常驻模式
99
+
100
+ Args:
101
+ project_id: 用于常驻的项目 ID
102
+ """
103
+ if self._running:
104
+ debug_logger.log_warning("[BrowserCaptcha] 常驻模式已在运行")
105
+ return
106
+
107
+ await self.initialize()
108
+
109
+ self.resident_project_id = project_id
110
+ website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
111
+
112
+ debug_logger.log_info(f"[BrowserCaptcha] 启动常驻模式,访问页面: {website_url}")
113
+
114
+ # 创建一个独立的新标签页(不使用 main_tab,避免被回收)
115
+ self.resident_tab = await self.browser.get(website_url, new_tab=True)
116
+
117
+ debug_logger.log_info("[BrowserCaptcha] 标签页已创建,等待页面加载...")
118
+
119
+ # 等待页面加载完成(带重试机制)
120
+ page_loaded = False
121
+ for retry in range(15):
122
+ try:
123
+ await asyncio.sleep(1)
124
+ ready_state = await self.resident_tab.evaluate("document.readyState")
125
+ debug_logger.log_info(f"[BrowserCaptcha] 页面状态: {ready_state} (重试 {retry + 1}/15)")
126
+ if ready_state == "complete":
127
+ page_loaded = True
128
+ break
129
+ except ConnectionRefusedError as e:
130
+ debug_logger.log_warning(f"[BrowserCaptcha] 标签页连接丢失: {e},尝试重新获取...")
131
+ # 标签页可能已关闭,尝试重新创建
132
+ try:
133
+ self.resident_tab = await self.browser.get(website_url, new_tab=True)
134
+ debug_logger.log_info("[BrowserCaptcha] 已重新创建标签页")
135
+ except Exception as e2:
136
+ debug_logger.log_error(f"[BrowserCaptcha] 重新创建标签页失败: {e2}")
137
+ await asyncio.sleep(2)
138
+ except Exception as e:
139
+ debug_logger.log_warning(f"[BrowserCaptcha] 等待页面异常: {e},重试 {retry + 1}/15...")
140
+ await asyncio.sleep(2)
141
+
142
+ if not page_loaded:
143
+ debug_logger.log_error("[BrowserCaptcha] 页面加载超时,常驻模式启动失败")
144
+ return
145
+
146
+ # 等待 reCAPTCHA 加载
147
+ self._recaptcha_ready = await self._wait_for_recaptcha(self.resident_tab)
148
+
149
+ if not self._recaptcha_ready:
150
+ debug_logger.log_error("[BrowserCaptcha] reCAPTCHA 加载失败,常驻模式启动失败")
151
+ return
152
+
153
+ self._running = True
154
+ debug_logger.log_info(f"[BrowserCaptcha] ✅ 常驻模式已启动 (project: {project_id})")
155
+
156
+ async def stop_resident_mode(self):
157
+ """停止常驻模式"""
158
+ if not self._running:
159
+ return
160
+
161
+ self._running = False
162
+
163
+ # 关闭常驻标签页
164
+ if self.resident_tab:
165
+ try:
166
+ await self.resident_tab.close()
167
+ except Exception:
168
+ pass
169
+ self.resident_tab = None
170
+
171
+ self.resident_project_id = None
172
+ self._recaptcha_ready = False
173
+
174
+ debug_logger.log_info("[BrowserCaptcha] 常驻模式已停止")
175
+
176
+ async def _wait_for_recaptcha(self, tab) -> bool:
177
+ """等待 reCAPTCHA 加载
178
+
179
+ Returns:
180
+ True if reCAPTCHA loaded successfully
181
+ """
182
+ debug_logger.log_info("[BrowserCaptcha] 检测 reCAPTCHA...")
183
+
184
+ # 检查 grecaptcha.enterprise.execute
185
+ is_enterprise = await tab.evaluate(
186
+ "typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
187
+ )
188
+
189
+ if is_enterprise:
190
+ debug_logger.log_info("[BrowserCaptcha] reCAPTCHA Enterprise 已加载")
191
+ return True
192
+
193
+ # 尝试注入脚本
194
+ debug_logger.log_info("[BrowserCaptcha] 未检测到 reCAPTCHA,注入脚本...")
195
+
196
+ await tab.evaluate(f"""
197
+ (() => {{
198
+ if (document.querySelector('script[src*="recaptcha"]')) return;
199
+ const script = document.createElement('script');
200
+ script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
201
+ script.async = true;
202
+ document.head.appendChild(script);
203
+ }})()
204
+ """)
205
+
206
+ # 等待脚本加载
207
+ await tab.sleep(3)
208
+
209
+ # 轮询等待 reCAPTCHA 加载
210
+ for i in range(20):
211
+ is_enterprise = await tab.evaluate(
212
+ "typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
213
+ )
214
+
215
+ if is_enterprise:
216
+ debug_logger.log_info(f"[BrowserCaptcha] reCAPTCHA Enterprise 已加载(等待了 {i * 0.5} 秒)")
217
+ return True
218
+ await tab.sleep(0.5)
219
+
220
+ debug_logger.log_warning("[BrowserCaptcha] reCAPTCHA 加载超时")
221
+ return False
222
+
223
+ async def _execute_recaptcha_on_tab(self, tab) -> Optional[str]:
224
+ """在指定标签页执行 reCAPTCHA 获取 token
225
+
226
+ Args:
227
+ tab: nodriver 标签页对象
228
+
229
+ Returns:
230
+ reCAPTCHA token 或 None
231
+ """
232
+ # 生成唯一变量名避免冲突
233
+ ts = int(time.time() * 1000)
234
+ token_var = f"_recaptcha_token_{ts}"
235
+ error_var = f"_recaptcha_error_{ts}"
236
+
237
+ execute_script = f"""
238
+ (() => {{
239
+ window.{token_var} = null;
240
+ window.{error_var} = null;
241
+
242
+ try {{
243
+ grecaptcha.enterprise.ready(function() {{
244
+ grecaptcha.enterprise.execute('{self.website_key}', {{action: 'FLOW_GENERATION'}})
245
+ .then(function(token) {{
246
+ window.{token_var} = token;
247
+ }})
248
+ .catch(function(err) {{
249
+ window.{error_var} = err.message || 'execute failed';
250
+ }});
251
+ }});
252
+ }} catch (e) {{
253
+ window.{error_var} = e.message || 'exception';
254
+ }}
255
+ }})()
256
+ """
257
+
258
+ # 注入执行脚本
259
+ await tab.evaluate(execute_script)
260
+
261
+ # 轮询等待结果(最多 15 秒)
262
+ token = None
263
+ for i in range(30):
264
+ await tab.sleep(0.5)
265
+ token = await tab.evaluate(f"window.{token_var}")
266
+ if token:
267
+ break
268
+ error = await tab.evaluate(f"window.{error_var}")
269
+ if error:
270
+ debug_logger.log_error(f"[BrowserCaptcha] reCAPTCHA 错误: {error}")
271
+ break
272
+
273
+ # 清理临时变量
274
+ try:
275
+ await tab.evaluate(f"delete window.{token_var}; delete window.{error_var};")
276
+ except:
277
+ pass
278
+
279
+ return token
280
+
281
+ # ========== 主要 API ==========
282
+
283
  async def get_token(self, project_id: str) -> Optional[str]:
284
  """获取 reCAPTCHA token
285
+
286
+ 常驻模式:直接从常驻标签页即时生成 token
287
+ 传统模式:每次创建新标签页 (fallback)
288
+
289
+ Args:
290
+ project_id: Flow项目ID
291
+
292
+ Returns:
293
+ reCAPTCHA token字符串,如果获取失败返回None
294
+ """
295
+ # 如果是常驻模式且 project_id 匹配,直接从常驻标签页生成
296
+ if self._running and self.resident_project_id == project_id:
297
+ if self._recaptcha_ready and self.resident_tab:
298
+ start_time = time.time()
299
+ debug_logger.log_info("[BrowserCaptcha] 从常驻标签页即时生成 token...")
300
+ token = await self._execute_recaptcha_on_tab(self.resident_tab)
301
+ duration_ms = (time.time() - start_time) * 1000
302
+ if token:
303
+ debug_logger.log_info(f"[BrowserCaptcha] ✅ Token生成成功(耗时 {duration_ms:.0f}ms)")
304
+ return token
305
+ else:
306
+ debug_logger.log_warning("[BrowserCaptcha] 常驻模式生成失败,fallback到传统模式")
307
+ else:
308
+ debug_logger.log_warning("[BrowserCaptcha] 常驻标签页未就绪,fallback到传统模式")
309
+
310
+ # Fallback: 使用传统模式
311
+ return await self._get_token_legacy(project_id)
312
+
313
+ async def _get_token_legacy(self, project_id: str) -> Optional[str]:
314
+ """传统模式获取 reCAPTCHA token(每次创建新标签页)
315
 
316
  Args:
317
  project_id: Flow项目ID
 
328
 
329
  try:
330
  website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
331
+ debug_logger.log_info(f"[BrowserCaptcha] [Legacy] 访问页面: {website_url}")
332
 
333
  # 新建标签页并访问页面
334
  tab = await self.browser.get(website_url)
335
 
336
  # 等待页面完全加载(增加等待时间)
337
+ debug_logger.log_info("[BrowserCaptcha] [Legacy] 等待页面加载...")
338
  await tab.sleep(3)
339
 
340
  # 等待页面 DOM 完成
 
344
  break
345
  await tab.sleep(0.5)
346
 
347
+ # 等待 reCAPTCHA 加载
348
+ recaptcha_ready = await self._wait_for_recaptcha(tab)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
+ if not recaptcha_ready:
351
+ debug_logger.log_error("[BrowserCaptcha] [Legacy] reCAPTCHA 无法加载")
352
  return None
353
 
354
+ # 执行 reCAPTCHA
355
+ debug_logger.log_info("[BrowserCaptcha] [Legacy] 执行 reCAPTCHA 验证...")
356
+ token = await self._execute_recaptcha_on_tab(tab)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
 
358
  duration_ms = (time.time() - start_time) * 1000
359
 
360
  if token:
361
+ debug_logger.log_info(f"[BrowserCaptcha] [Legacy] ✅ Token获取成功(耗时 {duration_ms:.0f}ms)")
362
  return token
363
  else:
364
+ debug_logger.log_error("[BrowserCaptcha] [Legacy] Token获取失败(返回null)")
365
  return None
366
 
367
  except Exception as e:
368
+ debug_logger.log_error(f"[BrowserCaptcha] [Legacy] 获取token异常: {str(e)}")
369
  return None
370
  finally:
371
  # 关闭标签页(但保留浏览器)
 
377
 
378
  async def close(self):
379
  """关闭浏览器"""
380
+ # 先停止常驻模式
381
+ await self.stop_resident_mode()
382
+
383
  try:
384
  if self.browser:
385
  try:
 
399
  await self.initialize()
400
  tab = await self.browser.get("https://accounts.google.com/")
401
  debug_logger.log_info("[BrowserCaptcha] 请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
402
+ print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
403
+
404
+ # ========== 状态查询 ==========
405
+
406
+ def is_resident_mode_active(self) -> bool:
407
+ """检查常驻模式是否激活"""
408
+ return self._running
409
+
410
+ def get_queue_size(self) -> int:
411
+ """获取当前缓存队列大小"""
412
+ return self.token_queue.qsize()
413
+
414
+ def get_resident_project_id(self) -> Optional[str]:
415
+ """获取当前常驻的 project_id"""
416
+ return self.resident_project_id