Spaces:
Sleeping
Sleeping
Upload 18 files
Browse files- core/config.py +4 -2
- core/duckmail_client.py +6 -47
- core/gemini_automation.py +33 -14
- core/gemini_automation_uc.py +75 -11
- core/login_service.py +18 -6
- core/mail_utils.py +6 -10
- core/microsoft_mail_client.py +20 -10
- core/register_service.py +18 -6
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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
# 获取邮件内容
|
| 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"
|
| 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"
|
| 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", "
|
| 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 |
-
|
| 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 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 206 |
-
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
-
|
| 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 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 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"
|
| 67 |
try:
|
| 68 |
client.logout()
|
| 69 |
except Exception:
|
| 70 |
pass
|
| 71 |
return None
|
| 72 |
|
| 73 |
-
search_since = since_time or (datetime.
|
| 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 |
-
|
|
|
|
| 86 |
if status != "OK" or not data or not data[0]:
|
| 87 |
continue
|
| 88 |
|
| 89 |
-
ids = data[0].split()
|
| 90 |
-
|
| 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 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 131 |
code = self.fetch_verification_code(since_time=since_time)
|
| 132 |
if code:
|
| 133 |
return code
|
| 134 |
-
if
|
| 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 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 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)
|