Files
atomk-hermes-skills/skills/browser-automation/camoufox-google-2fa-login/SKILL.md
T

9.8 KiB
Raw Blame History

name, title, description, category, tags
name title description category tags
camoufox-google-2fa-login CamouFox Google 2FA登录 使用CamouFox反检测浏览器登录Google账号,处理2FA短信验证码。关键:验证码与session绑定,必须保存storage_state在同一会话输入。 browser-automation
camoufox
google
2fa
login
anti-detection

CamouFox Google 2FA 登录

核心问题

Google 2FA短信验证码是session-bound的。每次新的登录会话(新browser context)会自动使之前的验证码失效。因此:

  • 错误做法1:登录到2FA页 → 告诉用户等待 → 用户给码 → 开新session重新登录 → 输入验证码 → "验证码错误"(码已被新session失效)
  • 错误做法2:登录到2FA页 → storage_state保存 → 用户给码 → 新session用storage_state恢复 → Google返回400错误(TL参数已失效,storage_state无法恢复2FA中间状态)
  • 正确做法:登录到2FA页 → 后台脚本保持浏览器会话存活 → 轮询文件等待验证码 → 用户收到短信后写入文件 → 同一会话内读取并输入验证码

依赖

pip install camoufox
playwright install  # camoufox依赖playwright

完整流程

阶段1:登录到2FA验证码页面

from camoufox.sync_api import Camoufox
import time, json

email = "my9commerce@gmail.com"
password = "Bingo2019Cc9"
STATE_FILE = "/tmp/gmail_2fa_state.json"

with Camoufox(headless=True, geoip=True, humanize=True) as browser:
    page = browser.new_page()
    
    # Step 1: Email
    page.goto("https://accounts.google.com/signin", timeout=30000)
    page.wait_for_load_state("networkidle", timeout=15000)
    time.sleep(2)
    page.query_selector("input[type=email]").fill(email)
    time.sleep(1)
    page.query_selector("#identifierNext").click()
    time.sleep(5)
    
    # Step 2: Password
    page.query_selector("input[type=password]").fill(password)
    time.sleep(1)
    page.query_selector("#passwordNext").click()
    time.sleep(6)
    
    # Step 3: 2FA - click SMS option
    page.evaluate('''() => {
        const items = document.querySelectorAll("[role=link]");
        for (const item of items) {
            const t = (item.innerText || "");
            if (t.includes("验证码") || t.includes("verification code") || t.includes("••47")) {
                item.click(); return;
            }
        }
    }''')
    time.sleep(6)
    
    # Verify we're on code entry page
    body = page.evaluate("document.body.innerText")
    if "输入验证码" in body or "Enter the code" in body:
        # 保存完整浏览器状态(关键!)
        page.context.storage_state(path=STATE_FILE)
        print("SMS code sent! Waiting for user to provide the code.")
        print(f"State saved to {STATE_FILE}")
    else:
        print("Failed to reach code entry page")

正确做法:后台脚本保持会话 + 文件轮询

关键:必须在同一个浏览器会话内输入验证码。用后台脚本保持CamouFox进程存活,通过文件信号机制传递验证码。

后台登录脚本(gmail_login_wait.py

#!/usr/bin/env python3
import json, time, os, sys
from camoufox.sync_api import Camoufox

email = "my9commerce@gmail.com"
password = "YOUR_PASSWORD"
CODE_FILE = "/tmp/gmail_code.txt"
SIGNAL_FILE = "/tmp/gmail_signal.txt"

# Clean old files
for f in [CODE_FILE, SIGNAL_FILE]:
    if os.path.exists(f): os.remove(f)

print("STARTING", flush=True)

with Camoufox(headless=True, geoip=True, humanize=True) as browser:
    page = browser.new_page()
    
    # Step 1: Email
    page.goto("https://accounts.google.com/signin", timeout=30000)
    page.wait_for_load_state("networkidle", timeout=15000)
    time.sleep(2)
    page.query_selector("input[type=email]").fill(email)
    time.sleep(1)
    page.query_selector("#identifierNext").click()
    time.sleep(5)
    
    # Step 2: Password
    page.query_selector("input[type=password]").fill(password)
    time.sleep(1)
    page.query_selector("#passwordNext").click()
    time.sleep(6)
    
    # Step 3: 2FA - click SMS option
    page.evaluate('''() => {
        const items = document.querySelectorAll("[role=link]");
        for (const item of items) {
            const t = (item.innerText || "");
            if (t.includes("验证码") || t.includes("verification code") || t.includes("••47")) {
                item.click(); return;
            }
        }
    }''')
    time.sleep(6)
    
    # Verify on code entry page
    body = page.evaluate("document.body.innerText")
    if "输入验证码" not in body and "Enter the code" not in body:
        print(f"NOT_ON_CODE_PAGE", flush=True)
        sys.exit(1)
    
    # Signal ready - SMS has been sent
    print("WAITING_FOR_CODE", flush=True)
    with open(SIGNAL_FILE, "w") as f:
        f.write("READY")
    
    # Poll for code file (max 120 seconds)
    code = None
    for i in range(120):
        if os.path.exists(CODE_FILE):
            with open(CODE_FILE, "r") as f:
                code = f.read().strip()
            if len(code) == 6 and code.isdigit():
                print(f"GOT_CODE: {code}", flush=True)
                break
        time.sleep(1)
    else:
        print("TIMEOUT", flush=True)
        sys.exit(1)
    
    # Step 4: Enter code IN SAME SESSION
    pin = page.query_selector("#idvPin")
    if pin:
        pin.fill(code)
        time.sleep(1)
        
        page.evaluate('''() => {
            const btns = document.querySelectorAll("button");
            for (const b of btns) {
                const t = (b.innerText || "").trim();
                if (t === "下一步" || t === "Next") { b.click(); return; }
            }
        }''')
        time.sleep(10)
        
        body2 = page.evaluate("document.body.innerText")
        if "错误" in body2 or "wrong" in body2.lower():
            print("CODE_REJECTED", flush=True)
            sys.exit(1)
        
        page.goto("https://mail.google.com", timeout=20000)
        time.sleep(5)
        if "mail.google.com" in page.url:
            print("LOGIN_SUCCESS", flush=True)
            cookies = page.context.cookies()
            with open("/home/ubuntu/.hermes/cookies/gmail_cookies.json", "w") as f:
                json.dump(cookies, f)
            print("COOKIES_SAVED", flush=True)
        else:
            print(f"LOGIN_FAILED: {page.url}", flush=True)

启动与交互流程

# 1. 后台启动脚本(注意 -u 参数确保无缓冲输出)
/home/ubuntu/.hermes/hermes-agent/venv/bin/python3 -u gmail_login_wait.py &

# 2. 等待信号文件
while [ ! -f /tmp/gmail_signal.txt ]; do sleep 2; done
echo "Ready for code"

# 3. 用户确认收到短信后,写入验证码
echo "123456" > /tmp/gmail_code.txt

# 4. 检查结果
# 脚本会输出 LOGIN_SUCCESS 或 CODE_REJECTED

重要时序:脚本启动后Google会发送新验证码。用户必须等这个脚本触发的短信,而不是之前任何会话的验证码。

  1. 必须用CamouFox:普通headless Chromium会被Google拦截,重定向到/signin/rejected("此浏览器或应用可能不安全")。CamouFox的指纹伪装+geoip+humanize能绕过检测。

  2. 2FA验证码是session-bound:新登录会话会使旧验证码失效。必须在同一session内输入验证码,或用storage_state保存/恢复session。

  3. 2FA选择页面的点击Google 2FA选择页面用[role="link"]元素,不是按钮。需要通过文本内容("验证码"或手机尾号)定位。

  4. PIN输入框#idvPintype为telname为Pin

  5. storage_state无法恢复2FA中间状态:实测发现用storage_state恢复session后访问2FA页面返回400错误。Google的TL参数是临时的、一次性的。必须保持原始浏览器进程存活,不能关掉后重新打开。

  6. 等待时间:Google的页面切换需要较长等待(5-6秒),网络不稳定时可能需要更长。

常见问题

问题 原因 解决方案
"此浏览器或应用可能不安全" 普通Chromium被Google反自动化检测 使用CamouFox(headless=True, geoip=True, humanize=True)
"验证码错误" 验证码来自不同的登录session 用storage_state保存/恢复session,在同一session输入
2FA选择页面点击无反应 点击了外层容器而非内部链接 [role="link"]定位,通过innerText匹配
页面停留在challenge/selection SMS选项点击未触发跳转 确认JS evaluate中click生效,增加等待时间
storage_state恢复后返回400错误 Google TL参数是临时的一次性的,storage_state无法恢复2FA中间状态 不要用storage_state恢复,用后台脚本保持浏览器进程存活,通过文件轮询传递验证码

CamouFox启动参数

with Camoufox(
    headless=True,    # 无头模式(服务器环境必须)
    geoip=True,       # 自动匹配IP地理位置的指纹
    humanize=True,    # 模拟人类操作模式
) as browser:
    page = browser.new_page()

Python路径

系统上CamouFox安装在Hermes venv中:

/home/ubuntu/.hermes/hermes-agent/venv/bin/python3

注意:/root/路径可能权限不足,用/home/ubuntu/路径。

经验教训(踩坑记录)

  1. 验证码时序陷阱:用户给的是上一轮会话的验证码,但新一轮登录已经使旧码失效。每次新CamouFox会话 = 新登录 = 新验证码。必须等用户确认收到当前会话的短信后再输入

  2. execute_code无法保持会话execute_code每次都是新进程,无法保持浏览器会话。必须用terminal(background=true)启动脚本,通过文件信号机制(SIGNAL_FILE/CODE_FILE)协调。

  3. 脚本输出需要flushPython后台脚本必须用print(..., flush=True)python3 -u参数,否则stdout缓冲导致无法实时看到输出。

  4. CamouFox启动较慢:从启动到2FA页面约需30-40秒,轮询间隔不要太短。