xiaoyukkkk commited on
Commit
6bf62af
·
verified ·
1 Parent(s): c6b5ea0

Upload 18 files

Browse files
core/config.py CHANGED
@@ -49,7 +49,8 @@ class BasicConfig(BaseModel):
49
  duckmail_base_url: str = Field(default="https://api.duckmail.sbs", description="DuckMail API地址")
50
  duckmail_api_key: str = Field(default="", description="DuckMail API key")
51
  duckmail_verify_ssl: bool = Field(default=True, description="DuckMail SSL校验")
52
- browser_headless: bool = Field(default=True, description="自动化浏览器无头模式")
 
53
  refresh_window_hours: int = Field(default=1, ge=0, le=24, description="过期刷新窗口(小时)")
54
  register_default_count: int = Field(default=1, ge=1, le=30, description="默认注册数量")
55
  register_domain: str = Field(default="", description="默认注册域名(推荐)")
@@ -153,7 +154,8 @@ class ConfigManager:
153
  duckmail_base_url=basic_data.get("duckmail_base_url") or "https://api.duckmail.sbs",
154
  duckmail_api_key=str(duckmail_api_key_raw or "").strip(),
155
  duckmail_verify_ssl=_parse_bool(basic_data.get("duckmail_verify_ssl"), True),
156
- browser_headless=_parse_bool(basic_data.get("browser_headless"), True),
 
157
  refresh_window_hours=int(refresh_window_raw),
158
  register_default_count=int(register_default_raw),
159
  register_domain=str(register_domain_raw or "").strip(),
 
49
  duckmail_base_url: str = Field(default="https://api.duckmail.sbs", description="DuckMail API地址")
50
  duckmail_api_key: str = Field(default="", description="DuckMail API key")
51
  duckmail_verify_ssl: bool = Field(default=True, description="DuckMail SSL校验")
52
+ browser_engine: str = Field(default="dp", description="浏览器引擎:uc (支持无头/有头) 或 dp (仅有头,更稳定)")
53
+ browser_headless: bool = Field(default=False, description="自动化浏览器无头模式(仅 UC 引擎支持)")
54
  refresh_window_hours: int = Field(default=1, ge=0, le=24, description="过期刷新窗口(小时)")
55
  register_default_count: int = Field(default=1, ge=1, le=30, description="默认注册数量")
56
  register_domain: str = Field(default="", description="默认注册域名(推荐)")
 
154
  duckmail_base_url=basic_data.get("duckmail_base_url") or "https://api.duckmail.sbs",
155
  duckmail_api_key=str(duckmail_api_key_raw or "").strip(),
156
  duckmail_verify_ssl=_parse_bool(basic_data.get("duckmail_verify_ssl"), True),
157
+ browser_engine=basic_data.get("browser_engine") or "dp",
158
+ browser_headless=_parse_bool(basic_data.get("browser_headless"), False),
159
  refresh_window_hours=int(refresh_window_raw),
160
  register_default_count=int(register_default_raw),
161
  register_domain=str(register_domain_raw or "").strip(),
core/duckmail_client.py CHANGED
@@ -130,6 +130,7 @@ class DuckMailClient:
130
  return None
131
 
132
  try:
 
133
  # 获取邮件列表
134
  res = self._request(
135
  "GET",
@@ -138,47 +139,19 @@ class DuckMailClient:
138
  )
139
 
140
  if res.status_code != 200:
141
- self._log("warning", f"DuckMail messages request failed: {res.status_code}")
142
  return None
143
 
144
  data = res.json() if res.content else {}
145
  messages = data.get("hydra:member", [])
146
- self._log("info", f"DuckMail messages count: {len(messages)}")
147
 
148
  if not messages:
149
  return None
150
 
151
- # 获取一封邮件的详情(最新的邮件)
152
  msg_id = messages[0].get("id")
153
- msg_created_at = messages[0].get("createdAt", "unknown")
154
  if not msg_id:
155
  return None
156
 
157
- self._log("info", f"DuckMail fetching message: {msg_id} (created: {msg_created_at})")
158
-
159
- # 检查邮件时间是否在 since_time 之后
160
- self._log("info", f"DuckMail since_time check: since_time={since_time}, msg_created_at={msg_created_at}")
161
- if since_time and msg_created_at != "unknown":
162
- try:
163
- from dateutil import parser
164
- email_time = parser.parse(msg_created_at)
165
- # 移除时区信息进行比较
166
- if email_time.tzinfo:
167
- email_time = email_time.replace(tzinfo=None)
168
- if since_time.tzinfo:
169
- since_time = since_time.replace(tzinfo=None)
170
-
171
- self._log("info", f"DuckMail comparing times: email={email_time}, since={since_time}")
172
- if email_time < since_time:
173
- self._log("info", f"DuckMail email too old: {email_time} < {since_time}")
174
- return None
175
- else:
176
- self._log("info", f"DuckMail email is new: {email_time} >= {since_time}")
177
- except Exception as e:
178
- self._log("warning", f"DuckMail time comparison failed: {e}")
179
- else:
180
- self._log("info", f"DuckMail skipping time check (since_time={since_time}, msg_created_at={msg_created_at})")
181
-
182
  detail = self._request(
183
  "GET",
184
  f"{self.base_url}/messages/{msg_id}",
@@ -189,34 +162,24 @@ class DuckMailClient:
189
  return None
190
 
191
  payload = detail.json() if detail.content else {}
192
- subject = payload.get("subject", "")
193
- created_at = payload.get("createdAt", "unknown")
194
- self._log("info", f"DuckMail message subject: {subject} (created: {created_at})")
195
 
196
- # 获取邮件内容(text可能是字符串,html可能是列表)
197
  text_content = payload.get("text") or ""
198
  html_content = payload.get("html") or ""
199
 
200
- # 如果html是列表,转换为字符串
201
  if isinstance(html_content, list):
202
  html_content = "".join(str(item) for item in html_content)
203
  if isinstance(text_content, list):
204
  text_content = "".join(str(item) for item in text_content)
205
 
206
  content = text_content + html_content
207
- self._log("info", f"DuckMail email content length: {len(content)} chars")
208
  code = extract_verification_code(content)
209
  if code:
210
- self._log("info", f"DuckMail extracted code: {code}")
211
- else:
212
- self._log("warning", f"DuckMail no code found in message")
213
- # 打印部分内容用于调试
214
- preview = content[:200] if content else "(empty)"
215
- self._log("warning", f"DuckMail content preview: {preview}")
216
  return code
217
 
218
  except Exception as e:
219
- self._log("error", f"DuckMail fetch code failed: {e}")
220
  return None
221
 
222
  def poll_for_code(
@@ -228,23 +191,19 @@ class DuckMailClient:
228
  """轮询获取验证码"""
229
  if not self.token:
230
  if not self.login():
231
- self._log("error", "DuckMail token missing")
232
  return None
233
 
234
- self._log("info", "DuckMail polling for code")
235
  max_retries = timeout // interval
236
 
237
  for i in range(1, max_retries + 1):
238
- self._log("info", f"DuckMail attempt {i}/{max_retries}")
239
  code = self.fetch_verification_code(since_time=since_time)
240
  if code:
241
- self._log("info", f"DuckMail code found: {code}")
242
  return code
243
 
244
  if i < max_retries:
245
  time.sleep(interval)
246
 
247
- self._log("error", "DuckMail code timeout")
248
  return None
249
 
250
  def _get_domain(self) -> str:
 
130
  return None
131
 
132
  try:
133
+ self._log("info", "fetching verification code")
134
  # 获取邮件列表
135
  res = self._request(
136
  "GET",
 
139
  )
140
 
141
  if res.status_code != 200:
 
142
  return None
143
 
144
  data = res.json() if res.content else {}
145
  messages = data.get("hydra:member", [])
 
146
 
147
  if not messages:
148
  return None
149
 
150
+ # 获取最新一封邮件,不做时间过滤
151
  msg_id = messages[0].get("id")
 
152
  if not msg_id:
153
  return None
154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  detail = self._request(
156
  "GET",
157
  f"{self.base_url}/messages/{msg_id}",
 
162
  return None
163
 
164
  payload = detail.json() if detail.content else {}
 
 
 
165
 
166
+ # 获取邮件内容
167
  text_content = payload.get("text") or ""
168
  html_content = payload.get("html") or ""
169
 
 
170
  if isinstance(html_content, list):
171
  html_content = "".join(str(item) for item in html_content)
172
  if isinstance(text_content, list):
173
  text_content = "".join(str(item) for item in text_content)
174
 
175
  content = text_content + html_content
 
176
  code = extract_verification_code(content)
177
  if code:
178
+ self._log("info", f"code found: {code}")
 
 
 
 
 
179
  return code
180
 
181
  except Exception as e:
182
+ self._log("error", f"fetch code failed: {e}")
183
  return None
184
 
185
  def poll_for_code(
 
191
  """轮询获取验证码"""
192
  if not self.token:
193
  if not self.login():
 
194
  return None
195
 
 
196
  max_retries = timeout // interval
197
 
198
  for i in range(1, max_retries + 1):
 
199
  code = self.fetch_verification_code(since_time=since_time)
200
  if code:
 
201
  return code
202
 
203
  if i < max_retries:
204
  time.sleep(interval)
205
 
206
+ self._log("error", "verification code timeout")
207
  return None
208
 
209
  def _get_domain(self) -> str:
core/gemini_automation.py CHANGED
@@ -132,7 +132,11 @@ class GeminiAutomation:
132
  if has_business_params:
133
  return self._extract_config(page, email)
134
 
135
- # Step 3: 尝试触送验证码
 
 
 
 
136
  if not self._click_send_code_button(page):
137
  self._log("error", "send code button not found")
138
  self._save_screenshot(page, "send_code_button_missing")
@@ -145,11 +149,9 @@ class GeminiAutomation:
145
  self._save_screenshot(page, "code_input_missing")
146
  return {"success": False, "error": "code input not found"}
147
 
148
- time.sleep(3)
149
-
150
- # Step 5: 轮询邮件获取验证码
151
  self._log("info", "polling for verification code")
152
- code = mail_client.poll_for_code(timeout=40, interval=4)
153
 
154
  if not code:
155
  self._log("error", "verification code timeout")
@@ -184,7 +186,8 @@ class GeminiAutomation:
184
  # Step 7: 处理协议页面(如果有)
185
  self._handle_agreement_page(page)
186
 
187
- # Step 8: 导航到业务页面
 
188
  page.get("https://business.gemini.google/", timeout=self.timeout)
189
  time.sleep(3)
190
 
@@ -193,14 +196,20 @@ class GeminiAutomation:
193
  if self._handle_username_setup(page):
194
  time.sleep(3)
195
 
196
- # Step 10: 提取配置
197
- if "cid" in page.url or self._wait_for_cid(page):
198
- self._log("info", "login success")
199
- return self._extract_config(page, email)
200
-
201
- self._log("error", "login failed")
202
- self._save_screenshot(page, "login_failed")
203
- return {"success": False, "error": "login failed"}
 
 
 
 
 
 
204
 
205
  def _click_send_code_button(self, page) -> bool:
206
  """点击发送验证码按钮(如果需要)"""
@@ -284,6 +293,16 @@ class GeminiAutomation:
284
  time.sleep(1)
285
  return False
286
 
 
 
 
 
 
 
 
 
 
 
287
  def _handle_username_setup(self, page) -> bool:
288
  """处理用户名设置页面"""
289
  current_url = page.url
 
132
  if has_business_params:
133
  return self._extract_config(page, email)
134
 
135
+ # Step 3: 记录发送验证码的时间并触发发送
136
+ from datetime import datetime
137
+ send_time = datetime.now()
138
+
139
+ self._log("info", "clicking send verification code button")
140
  if not self._click_send_code_button(page):
141
  self._log("error", "send code button not found")
142
  self._save_screenshot(page, "send_code_button_missing")
 
149
  self._save_screenshot(page, "code_input_missing")
150
  return {"success": False, "error": "code input not found"}
151
 
152
+ # Step 5: 轮询邮件获取验证码(传入发送时间)
 
 
153
  self._log("info", "polling for verification code")
154
+ code = mail_client.poll_for_code(timeout=40, interval=4, since_time=send_time)
155
 
156
  if not code:
157
  self._log("error", "verification code timeout")
 
186
  # Step 7: 处理协议页面(如果有)
187
  self._handle_agreement_page(page)
188
 
189
+ # Step 8: 导航到业务页面并等待参数生成
190
+ self._log("info", "navigating to business page")
191
  page.get("https://business.gemini.google/", timeout=self.timeout)
192
  time.sleep(3)
193
 
 
196
  if self._handle_username_setup(page):
197
  time.sleep(3)
198
 
199
+ # Step 10: 等待 URL 参数生成(csesidx 和 cid)
200
+ self._log("info", "waiting for URL parameters")
201
+ if not self._wait_for_business_params(page):
202
+ self._log("warning", "URL parameters not generated, trying refresh")
203
+ page.refresh()
204
+ time.sleep(3)
205
+ if not self._wait_for_business_params(page):
206
+ self._log("error", "URL parameters generation failed")
207
+ self._save_screenshot(page, "params_missing")
208
+ return {"success": False, "error": "URL parameters not found"}
209
+
210
+ # Step 11: 提取配置
211
+ self._log("info", "login success")
212
+ return self._extract_config(page, email)
213
 
214
  def _click_send_code_button(self, page) -> bool:
215
  """点击发送验证码按钮(如果需要)"""
 
293
  time.sleep(1)
294
  return False
295
 
296
+ def _wait_for_business_params(self, page, timeout: int = 30) -> bool:
297
+ """等待业务页面参数生成(csesidx 和 cid)"""
298
+ for _ in range(timeout):
299
+ url = page.url
300
+ if "csesidx=" in url and "/cid/" in url:
301
+ self._log("info", f"business params ready: {url}")
302
+ return True
303
+ time.sleep(1)
304
+ return False
305
+
306
  def _handle_username_setup(self, page) -> bool:
307
  """处理用户名设置页面"""
308
  current_url = page.url
core/gemini_automation_uc.py CHANGED
@@ -130,6 +130,17 @@ class GeminiAutomationUC:
130
  self._save_screenshot("continue_button_failed")
131
  return {"success": False, "error": f"failed to click continue: {e}"}
132
 
 
 
 
 
 
 
 
 
 
 
 
133
  # 等待验证码输入框出现
134
  code_input = self._wait_for_code_input()
135
  if not code_input:
@@ -137,11 +148,9 @@ class GeminiAutomationUC:
137
  self._save_screenshot("code_input_missing")
138
  return {"success": False, "error": "code input not found"}
139
 
140
- time.sleep(3)
141
-
142
- # 获取验证码
143
  self._log("info", "polling for verification code")
144
- code = mail_client.poll_for_code(timeout=40, interval=4)
145
 
146
  if not code:
147
  self._log("error", "verification code timeout")
@@ -192,7 +201,8 @@ class GeminiAutomationUC:
192
  # 处理协议页面
193
  self._handle_agreement_page()
194
 
195
- # 导航到业务页面
 
196
  self.driver.get("https://business.gemini.google/")
197
  time.sleep(3)
198
 
@@ -201,14 +211,58 @@ class GeminiAutomationUC:
201
  if self._handle_username_setup():
202
  time.sleep(3)
203
 
 
 
 
 
 
 
 
 
 
 
 
204
  # 提取配置
205
- if "cid" in self.driver.current_url or self._wait_for_cid():
206
- self._log("info", "login success")
207
- return self._extract_config(email)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
- self._log("error", "login failed")
210
- self._save_screenshot("login_failed")
211
- return {"success": False, "error": "login failed"}
212
 
213
  def _wait_for_code_input(self, timeout: int = 30):
214
  """等待验证码输入框出现"""
@@ -265,6 +319,16 @@ class GeminiAutomationUC:
265
  time.sleep(1)
266
  return False
267
 
 
 
 
 
 
 
 
 
 
 
268
  def _handle_username_setup(self) -> bool:
269
  """处理用户名设置页面"""
270
  current_url = self.driver.current_url
 
130
  self._save_screenshot("continue_button_failed")
131
  return {"success": False, "error": f"failed to click continue: {e}"}
132
 
133
+ # 记录发送验证码的时间
134
+ from datetime import datetime
135
+ send_time = datetime.now()
136
+
137
+ # 检查是否需要点击"发送验证码"按钮
138
+ self._log("info", "clicking send verification code button")
139
+ if not self._click_send_code_button():
140
+ self._log("error", "send code button not found")
141
+ self._save_screenshot("send_code_button_missing")
142
+ return {"success": False, "error": "send code button not found"}
143
+
144
  # 等待验证码输入框出现
145
  code_input = self._wait_for_code_input()
146
  if not code_input:
 
148
  self._save_screenshot("code_input_missing")
149
  return {"success": False, "error": "code input not found"}
150
 
151
+ # 获取验证码(传入发送时间)
 
 
152
  self._log("info", "polling for verification code")
153
+ code = mail_client.poll_for_code(timeout=40, interval=4, since_time=send_time)
154
 
155
  if not code:
156
  self._log("error", "verification code timeout")
 
201
  # 处理协议页面
202
  self._handle_agreement_page()
203
 
204
+ # 导航到业务页面并等待参数生成
205
+ self._log("info", "navigating to business page")
206
  self.driver.get("https://business.gemini.google/")
207
  time.sleep(3)
208
 
 
211
  if self._handle_username_setup():
212
  time.sleep(3)
213
 
214
+ # 等待 URL 参数生成(csesidx 和 cid)
215
+ self._log("info", "waiting for URL parameters")
216
+ if not self._wait_for_business_params():
217
+ self._log("warning", "URL parameters not generated, trying refresh")
218
+ self.driver.refresh()
219
+ time.sleep(3)
220
+ if not self._wait_for_business_params():
221
+ self._log("error", "URL parameters generation failed")
222
+ self._save_screenshot("params_missing")
223
+ return {"success": False, "error": "URL parameters not found"}
224
+
225
  # 提取配置
226
+ self._log("info", "login success")
227
+ return self._extract_config(email)
228
+
229
+ def _click_send_code_button(self) -> bool:
230
+ """点击发送验证码按钮(如果需要)"""
231
+ time.sleep(2)
232
+
233
+ # 方法1: 直接通过ID查找
234
+ try:
235
+ direct_btn = WebDriverWait(self.driver, 5).until(
236
+ EC.element_to_be_clickable((By.ID, "sign-in-with-email"))
237
+ )
238
+ self.driver.execute_script("arguments[0].click();", direct_btn)
239
+ time.sleep(2)
240
+ return True
241
+ except TimeoutException:
242
+ pass
243
+
244
+ # 方法2: 通过关键词查找按钮
245
+ keywords = ["通过电子邮件发送验证码", "通过电子邮件发送", "email", "Email", "Send code", "Send verification", "Verification code"]
246
+ try:
247
+ buttons = self.driver.find_elements(By.TAG_NAME, "button")
248
+ for btn in buttons:
249
+ text = btn.text.strip() if btn.text else ""
250
+ if text and any(kw in text for kw in keywords):
251
+ self.driver.execute_script("arguments[0].click();", btn)
252
+ time.sleep(2)
253
+ return True
254
+ except Exception:
255
+ pass
256
+
257
+ # 方法3: 检查是否已经在验证码输入页面
258
+ try:
259
+ code_input = self.driver.find_element(By.CSS_SELECTOR, "input[name='pinInput']")
260
+ if code_input:
261
+ return True
262
+ except NoSuchElementException:
263
+ pass
264
 
265
+ return False
 
 
266
 
267
  def _wait_for_code_input(self, timeout: int = 30):
268
  """等待验证码输入框出现"""
 
319
  time.sleep(1)
320
  return False
321
 
322
+ def _wait_for_business_params(self, timeout: int = 30) -> bool:
323
+ """等待业务页面参数生成(csesidx 和 cid)"""
324
+ for _ in range(timeout):
325
+ url = self.driver.current_url
326
+ if "csesidx=" in url and "/cid/" in url:
327
+ self._log("info", f"business params ready: {url}")
328
+ return True
329
+ time.sleep(1)
330
+ return False
331
+
332
  def _handle_username_setup(self) -> bool:
333
  """处理用户名设置页面"""
334
  current_url = self.driver.current_url
core/login_service.py CHANGED
@@ -152,12 +152,24 @@ class LoginService(BaseTaskService[LoginTask]):
152
  else:
153
  return {"success": False, "email": account_id, "error": f"unsupported mail provider: {mail_provider}"}
154
 
155
- automation = GeminiAutomationUC(
156
- user_agent=self.user_agent,
157
- proxy=config.basic.proxy,
158
- headless=config.basic.browser_headless,
159
- log_callback=log_cb,
160
- )
 
 
 
 
 
 
 
 
 
 
 
 
161
  try:
162
  result = automation.login_and_extract(account_id, client)
163
  except Exception as exc:
 
152
  else:
153
  return {"success": False, "email": account_id, "error": f"unsupported mail provider: {mail_provider}"}
154
 
155
+ # 根据配置选择浏览器引擎
156
+ browser_engine = (config.basic.browser_engine or "dp").lower()
157
+ if browser_engine == "dp":
158
+ # DrissionPage 引擎:仅有头模式,更稳定
159
+ automation = GeminiAutomation(
160
+ user_agent=self.user_agent,
161
+ proxy=config.basic.proxy,
162
+ headless=False, # DP 不支持无头模式
163
+ log_callback=log_cb,
164
+ )
165
+ else:
166
+ # undetected-chromedriver 引擎:支持有头和无头
167
+ automation = GeminiAutomationUC(
168
+ user_agent=self.user_agent,
169
+ proxy=config.basic.proxy,
170
+ headless=config.basic.browser_headless,
171
+ log_callback=log_cb,
172
+ )
173
  try:
174
  result = automation.login_and_extract(account_id, client)
175
  except Exception as exc:
core/mail_utils.py CHANGED
@@ -8,7 +8,6 @@ def extract_verification_code(text: str) -> Optional[str]:
8
  return None
9
 
10
  # 策略1: 上下文关键词匹配(中英文冒号)
11
- # 排除 CSS 样式值(如 14px, 16pt 等)
12
  context_pattern = r"(?:验证码|code|verification|passcode|pin).*?[::]\s*([A-Za-z0-9]{4,8})\b"
13
  match = re.search(context_pattern, text, re.IGNORECASE)
14
  if match:
@@ -17,17 +16,14 @@ def extract_verification_code(text: str) -> Optional[str]:
17
  if not re.match(r"^\d+(?:px|pt|em|rem|vh|vw|%)$", candidate, re.IGNORECASE):
18
  return candidate
19
 
20
- # 策略2: 6位数字
 
 
 
 
 
21
  digits = re.findall(r"\b\d{6}\b", text)
22
  if digits:
23
  return digits[0]
24
 
25
- # 策略3: 6位字母数字混合
26
- alphanumeric = re.findall(r"\b[A-Z0-9]{6}\b", text)
27
- for candidate in alphanumeric:
28
- has_letter = any(c.isalpha() for c in candidate)
29
- has_digit = any(c.isdigit() for c in candidate)
30
- if has_letter and has_digit:
31
- return candidate
32
-
33
  return None
 
8
  return None
9
 
10
  # 策略1: 上下文关键词匹配(中英文冒号)
 
11
  context_pattern = r"(?:验证码|code|verification|passcode|pin).*?[::]\s*([A-Za-z0-9]{4,8})\b"
12
  match = re.search(context_pattern, text, re.IGNORECASE)
13
  if match:
 
16
  if not re.match(r"^\d+(?:px|pt|em|rem|vh|vw|%)$", candidate, re.IGNORECASE):
17
  return candidate
18
 
19
+ # 策略2: 6位字母数字混合(与测试代码一致,优先级提高)
20
+ match = re.search(r"[A-Z0-9]{6}", text)
21
+ if match:
22
+ return match.group(0)
23
+
24
+ # 策略3: 6位数字(降级为备选)
25
  digits = re.findall(r"\b\d{6}\b", text)
26
  if digits:
27
  return digits[0]
28
 
 
 
 
 
 
 
 
 
29
  return None
core/microsoft_mail_client.py CHANGED
@@ -54,6 +54,8 @@ class MicrosoftMailClient:
54
  def fetch_verification_code(self, since_time: Optional[datetime] = None) -> Optional[str]:
55
  if not self.email:
56
  return None
 
 
57
  token = self._get_access_token()
58
  if not token:
59
  return None
@@ -63,15 +65,14 @@ class MicrosoftMailClient:
63
  try:
64
  client.authenticate("XOAUTH2", lambda _: auth_string)
65
  except Exception as exc:
66
- self._log("error", f"Microsoft IMAP auth failed: {exc}")
67
  try:
68
  client.logout()
69
  except Exception:
70
  pass
71
  return None
72
 
73
- search_since = since_time or (datetime.utcnow() - timedelta(minutes=5))
74
- since_str = search_since.strftime("%d-%b-%Y")
75
 
76
  try:
77
  for mailbox in ("INBOX", "Junk"):
@@ -82,12 +83,13 @@ class MicrosoftMailClient:
82
  except Exception:
83
  continue
84
 
85
- status, data = client.search(None, "SINCE", since_str)
 
86
  if status != "OK" or not data or not data[0]:
87
  continue
88
 
89
- ids = data[0].split()
90
- ids = ids[-20:]
91
  for msg_id in reversed(ids):
92
  status, msg_data = client.fetch(msg_id, "(RFC822)")
93
  if status != "OK" or not msg_data:
@@ -102,12 +104,17 @@ class MicrosoftMailClient:
102
 
103
  msg = message_from_bytes(raw_bytes)
104
  msg_date = self._parse_message_date(msg.get("Date"))
 
 
105
  if msg_date and msg_date < search_since:
106
  continue
107
 
108
  content = self._message_to_text(msg)
109
- code = extract_verification_code(content)
110
- if code:
 
 
 
111
  return code
112
  finally:
113
  try:
@@ -127,12 +134,15 @@ class MicrosoftMailClient:
127
  return None
128
 
129
  max_retries = max(1, timeout // interval)
130
- for _ in range(max_retries):
 
131
  code = self.fetch_verification_code(since_time=since_time)
132
  if code:
133
  return code
134
- if interval:
135
  time.sleep(interval)
 
 
136
  return None
137
 
138
  @staticmethod
 
54
  def fetch_verification_code(self, since_time: Optional[datetime] = None) -> Optional[str]:
55
  if not self.email:
56
  return None
57
+
58
+ self._log("info", "fetching verification code")
59
  token = self._get_access_token()
60
  if not token:
61
  return None
 
65
  try:
66
  client.authenticate("XOAUTH2", lambda _: auth_string)
67
  except Exception as exc:
68
+ self._log("error", f"IMAP auth failed: {exc}")
69
  try:
70
  client.logout()
71
  except Exception:
72
  pass
73
  return None
74
 
75
+ search_since = since_time or (datetime.now() - timedelta(minutes=5))
 
76
 
77
  try:
78
  for mailbox in ("INBOX", "Junk"):
 
83
  except Exception:
84
  continue
85
 
86
+ # 搜索所有邮件
87
+ status, data = client.search(None, "ALL")
88
  if status != "OK" or not data or not data[0]:
89
  continue
90
 
91
+ ids = data[0].split()[-5:] # 只检查最近 5 封
92
+
93
  for msg_id in reversed(ids):
94
  status, msg_data = client.fetch(msg_id, "(RFC822)")
95
  if status != "OK" or not msg_data:
 
104
 
105
  msg = message_from_bytes(raw_bytes)
106
  msg_date = self._parse_message_date(msg.get("Date"))
107
+
108
+ # 按时间过滤
109
  if msg_date and msg_date < search_since:
110
  continue
111
 
112
  content = self._message_to_text(msg)
113
+ import re
114
+ match = re.search(r'[A-Z0-9]{6}', content)
115
+ if match:
116
+ code = match.group(0)
117
+ self._log("info", f"code found in {mailbox}: {code}")
118
  return code
119
  finally:
120
  try:
 
134
  return None
135
 
136
  max_retries = max(1, timeout // interval)
137
+
138
+ for i in range(1, max_retries + 1):
139
  code = self.fetch_verification_code(since_time=since_time)
140
  if code:
141
  return code
142
+ if i < max_retries:
143
  time.sleep(interval)
144
+
145
+ self._log("error", "verification code timeout")
146
  return None
147
 
148
  @staticmethod
core/register_service.py CHANGED
@@ -116,12 +116,24 @@ class RegisterService(BaseTaskService[RegisterTask]):
116
  if not client.register_account(domain=domain):
117
  return {"success": False, "error": "duckmail register failed"}
118
 
119
- automation = GeminiAutomationUC(
120
- user_agent=self.user_agent,
121
- proxy=config.basic.proxy,
122
- headless=config.basic.browser_headless,
123
- log_callback=log_cb,
124
- )
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  try:
127
  result = automation.login_and_extract(client.email, client)
 
116
  if not client.register_account(domain=domain):
117
  return {"success": False, "error": "duckmail register failed"}
118
 
119
+ # 根据配置选择浏览器引擎
120
+ browser_engine = (config.basic.browser_engine or "dp").lower()
121
+ if browser_engine == "dp":
122
+ # DrissionPage 引擎:仅有头模式,更稳定
123
+ automation = GeminiAutomation(
124
+ user_agent=self.user_agent,
125
+ proxy=config.basic.proxy,
126
+ headless=False, # DP 不支持无头模式
127
+ log_callback=log_cb,
128
+ )
129
+ else:
130
+ # undetected-chromedriver 引擎:支持有头和无头
131
+ automation = GeminiAutomationUC(
132
+ user_agent=self.user_agent,
133
+ proxy=config.basic.proxy,
134
+ headless=config.basic.browser_headless,
135
+ log_callback=log_cb,
136
+ )
137
 
138
  try:
139
  result = automation.login_and_extract(client.email, client)