- fix(browser): wrap context lifecycle in try/finally to ensure browser closes on exceptions and KeyboardInterrupt (login, sniffer, generator) - fix(browser): replace time.sleep with Playwright native waits (wait_for, wait_for_timeout) for more reliable element interaction - fix(browser): use parameterized page.evaluate instead of f-string JS injection in generator polling - fix(api): add retry logic in wait_for_completion to survive transient network errors - fix(config): add prepare_profile_dir to copy profile to temp dir, preventing Chromium SingletonLock conflicts when tools run concurrently - fix(sniffer): stream API logs to tempfile instead of unbounded memory list to avoid OOM - fix(api): specify encoding='utf-8' when loading cookies from file Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
156 lines
5.7 KiB
Python
156 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
腾讯混元3D 邮箱验证码登录 CLI 工具 (CloakBrowser 持久化版本)
|
||
第一次输入邮箱回车后自动发送验证码
|
||
第二次输入验证码后回车自动点击登录按钮
|
||
登录状态会自动保存到 ~/.config/hunyuan3dweb/profile,下次运行无需重新登录
|
||
"""
|
||
|
||
import argparse
|
||
import os
|
||
import sys
|
||
import time
|
||
from cloakbrowser import launch_persistent_context
|
||
from ..config import get_profile_dir, get_cookie_path
|
||
|
||
PROFILE_DIR = str(get_profile_dir())
|
||
|
||
|
||
def _extract_and_save_cookies(context):
|
||
"""从浏览器上下文提取 Cookie 并保存到文件"""
|
||
try:
|
||
cookies = context.cookies()
|
||
relevant_names = {"hunyuan_user", "hunyuan_token", "hunyuan_source", "hy_user"}
|
||
cookie_dict = {c["name"]: c["value"] for c in cookies if c["name"] in relevant_names}
|
||
if cookie_dict:
|
||
cookie_str = "; ".join(f"{k}={v}" for k, v in cookie_dict.items())
|
||
cookie_path = get_cookie_path()
|
||
cookie_path.write_text(cookie_str, encoding="utf-8")
|
||
print(f"Cookie 已自动保存到: {cookie_path}")
|
||
return True
|
||
else:
|
||
print("警告: 未找到 hunyuan 相关 Cookie,API 客户端可能无法使用")
|
||
except Exception as e:
|
||
print(f"自动提取 Cookie 失败: {e}")
|
||
return False
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="腾讯混元3D 邮箱验证码登录")
|
||
parser.add_argument(
|
||
"--no-headless",
|
||
action="store_true",
|
||
help="显示浏览器窗口(默认无头模式,适用于服务器)"
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
headless = not args.no_headless
|
||
|
||
# 使用持久化上下文,cookie 和登录状态会保存到 PROFILE_DIR
|
||
context = None
|
||
try:
|
||
context = launch_persistent_context(PROFILE_DIR, headless=headless)
|
||
page = context.new_page()
|
||
|
||
print("正在打开腾讯混元3D...")
|
||
page.goto("https://3d.hunyuan.tencent.com/", timeout=60000)
|
||
page.wait_for_timeout(2000)
|
||
|
||
# 检查是否已经是登录状态(没有登录按钮说明已登录)
|
||
login_btn = page.locator("button").filter(has_text="登录").first
|
||
if login_btn.count() == 0:
|
||
print("检测到已有登录状态,无需重新登录。")
|
||
print(f"当前页面: {page.url}")
|
||
_extract_and_save_cookies(context)
|
||
|
||
if headless:
|
||
return
|
||
else:
|
||
# 未登录,走登录流程
|
||
login_btn.click()
|
||
page.locator("text=邮箱").wait_for(state="visible", timeout=10000)
|
||
|
||
# 切换到邮箱登录
|
||
email_tab = page.locator("text=邮箱").first
|
||
if email_tab.count() > 0:
|
||
email_tab.click()
|
||
email_input.wait_for(state="visible", timeout=10000)
|
||
else:
|
||
print("未找到邮箱登录选项")
|
||
return
|
||
|
||
# 定位元素
|
||
email_input = page.locator('input[placeholder="请输入邮箱地址"]')
|
||
code_input = page.locator('input[type="number"][placeholder="请输入邮箱验证码"]')
|
||
send_code_btn = page.locator("a.hyc-email-login__send-code")
|
||
login_submit_btn = page.locator("button.hyc-email-login__btn")
|
||
checkbox = page.locator(".t-checkbox__former")
|
||
|
||
# 第一次交互:输入邮箱并发送验证码
|
||
print("\n============================================")
|
||
email = input("请输入邮箱地址,按回车发送验证码: ").strip()
|
||
if not email:
|
||
print("邮箱不能为空,退出")
|
||
return
|
||
|
||
email_input.fill(email)
|
||
|
||
# 勾选协议(如果未勾选)
|
||
if checkbox.count() > 0:
|
||
is_checked = checkbox.evaluate("el => el.checked")
|
||
if not is_checked:
|
||
checkbox.evaluate("el => el.click()")
|
||
page.wait_for_timeout(300)
|
||
|
||
if send_code_btn.count() > 0:
|
||
send_code_btn.click()
|
||
print("已点击发送验证码,请查收邮件...")
|
||
else:
|
||
print("未找到发送验证码按钮")
|
||
return
|
||
|
||
# 第二次交互:输入验证码并登录
|
||
print("\n============================================")
|
||
code = input("请输入邮箱验证码,按回车登录: ").strip()
|
||
if not code:
|
||
print("验证码不能为空,退出")
|
||
return
|
||
|
||
code_input.fill(code)
|
||
|
||
# 点击登录
|
||
if login_submit_btn.count() > 0:
|
||
login_submit_btn.click()
|
||
print("已点击登录按钮,等待跳转...")
|
||
else:
|
||
print("未找到登录提交按钮")
|
||
return
|
||
|
||
# 等待登录成功跳转
|
||
try:
|
||
page.wait_for_url(lambda url: "login" not in url, timeout=30000)
|
||
print(f"\n登录成功!当前页面: {page.url}")
|
||
print(f"登录状态已保存到: {os.path.abspath(PROFILE_DIR)}")
|
||
_extract_and_save_cookies(context)
|
||
except Exception:
|
||
print("\n登录可能仍在处理中,或出现错误。请检查浏览器状态。")
|
||
|
||
# 保持浏览器打开(仅非无头模式)
|
||
if not headless:
|
||
print("\n按 Enter 键关闭浏览器并退出...")
|
||
input()
|
||
finally:
|
||
if context is not None:
|
||
try:
|
||
context.close()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
main()
|
||
except KeyboardInterrupt:
|
||
print("\n已取消")
|
||
sys.exit(0)
|