From 55f56d27c96193ad9a7f9fa5abedbef03a4a21aa Mon Sep 17 00:00:00 2001 From: chaos Date: Sat, 11 Jul 2026 23:21:23 +0800 Subject: [PATCH] Add Gmail+Email Worker hybrid mail path and private runtime config Prefer Cloudflare Worker KV for verification codes with optional short IMAP fallback disabled for private deploy; track config.json/.env for private repo. Also add worker project, protocol mint backoff, and fail-fast Turnstile/mail timeouts from batch runs. --- .env | 28 ++++ .env.example | 5 + .gitignore | 7 +- README.md | 84 ++++++++++ auto_register.py | 30 +++- cf-email-worker/README.md | 98 ++++++++++++ cf-email-worker/package.json | 11 ++ cf-email-worker/src/worker.js | 280 ++++++++++++++++++++++++++++++++++ cf-email-worker/wrangler.toml | 26 ++++ config.example.json | 16 ++ config.json | 105 +++++++++++++ cpa_xai/browser_confirm.py | 5 +- cpa_xai/mint.py | 96 ++++++++---- grok_register_ttk.py | 269 +++++++++++++++++++++++++++++--- register_cli.py | 87 ++++++++--- scripts/setup_gmail_worker.sh | 52 +++++++ scripts/test_gmail_worker.py | 82 ++++++++++ turnstilePatch/content.js | 18 ++- turnstilePatch/manifest.json | 11 +- 19 files changed, 1225 insertions(+), 85 deletions(-) create mode 100644 .env create mode 100644 cf-email-worker/README.md create mode 100644 cf-email-worker/package.json create mode 100644 cf-email-worker/src/worker.js create mode 100644 cf-email-worker/wrangler.toml create mode 100644 config.json create mode 100755 scripts/setup_gmail_worker.sh create mode 100755 scripts/test_gmail_worker.py diff --git a/.env b/.env new file mode 100644 index 0000000..fef3b3e --- /dev/null +++ b/.env @@ -0,0 +1,28 @@ +# Grok 注册机 - 本地环境变量(勿提交 git) +# 真实进程环境变量优先于本文件;多数项也可写在 config.json + +# ===== Gmail IMAP(email_provider=gmail)===== +# 密码填 Gmail「应用专用密码」,不是登录密码 +GMAIL_USER=你的Gmail@gmail.com +GMAIL_PASSWORD=xxxx xxxx xxxx xxxx + +# ===== CloudMail(email_provider=cloudmail 时再填)===== +# CLOUDMAIL_URL=https://mail.example.com +# CLOUDMAIL_ADMIN_EMAIL=admin@example.com +# CLOUDMAIL_PASSWORD= + +# ===== grok2api Admin(自动导入 SSO 池时)===== +# GROK2API_APP_KEY= + +# ===== CPA 远程自动导入(Management API)===== +# 1=开启,0=关闭(可覆盖 config.json 的 cpa_auto_import_remote) +CPA_AUTO_IMPORT_REMOTE=1 +# 地址只填根,如 http://127.0.0.1:8317 +CPA_REMOTE_BASE=https://cpaai.qxy1828.com/ +CPA_REMOTE_PASSWORD=sk-fuLgIiBOn6A7rYrMm + +# ===== 可选 ===== +# API_REVERSE_TOOLS= +# CPA_EXPORT=1 +# https_proxy=http://127.0.0.1:7890 +# http_proxy=http://127.0.0.1:7890 diff --git a/.env.example b/.env.example index 73dd85e..3675e42 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,11 @@ # 配合 Cloudflare catch-all 域名使用;密码填 Gmail「应用专用密码」 GMAIL_USER= GMAIL_PASSWORD= + +# ===== Gmail + Cloudflare Email Worker(秒级取码,IMAP 兜底)===== +# 部署见 cf-email-worker/README.md +# GMAIL_WORKER_URL=https://grok-xai-mail-codes..workers.dev +# GMAIL_WORKER_API_KEY= # 兼容别名(gmail_code_listener 会回退读取) # MAIL_USER= # MAIL_PASSWORD= diff --git a/.gitignore b/.gitignore index 875d969..0b7188c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,10 @@ __pycache__/ dist/ # secrets / local runtime -config.json +# config.json # private repo: tracked intentionally config.local.json config.json.bak* -.env +# .env # private repo: tracked intentionally accounts_*.txt emails_used.txt emails_error.txt @@ -21,3 +21,6 @@ logs/ *.log *.bak *.pid + +# local IDE +.zcode/ diff --git a/README.md b/README.md index 45b3147..a7921cd 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,90 @@ uv run python grok_register_ttk.py --- + +## Gmail + Cloudflare Email Worker(秒级取码) + +推荐混合链路(**不需要公网 IP / 内网穿透**): + +``` +xAI 发信 + → Cloudflare Email Routing(域名 catch-all) + → Email Worker:解析验证码 → 写入 KV + → (可选)forward 到 Gmail,保留 IMAP 兜底 + → 注册机:GET https:///code?to=alias@domain&key=... + → 未命中再回退 Gmail IMAP +``` + +### 1. 部署 Worker + +```bash +cd cf-email-worker +# 详见 cf-email-worker/README.md +npx wrangler login +npx wrangler kv namespace create GROK_MAIL_CODES # id 写入 wrangler.toml +npx wrangler secret put CODE_API_KEY +# 可选:保留 Gmail 兜底 +# printf 'you@gmail.com' | npx wrangler secret put GMAIL_FORWARD_TO +npx wrangler deploy +``` + +或用助手脚本: + +```bash +export GMAIL_FORWARD_TO='you@gmail.com' # 可选 +bash scripts/setup_gmail_worker.sh +``` + +### 2. Email Routing + +Dashboard → **Email Routing** → Catch-all → **Send to a Worker** → 选 `grok-xai-mail-codes`。 + +> 若以前 catch-all 只转发 Gmail:改成进 Worker,由 Worker 内 `GMAIL_FORWARD_TO` 再转到 Gmail。 + +### 3. 注册机配置 + +`config.json`(`email_provider` 仍为 `gmail`): + +```json +{ + "email_provider": "gmail", + "defaultDomains": "your-domain.com", + "gmail_imap_user": "you@gmail.com", + "gmail_imap_password": "app-password", + "gmail_worker_enabled": true, + "gmail_worker_url": "https://grok-xai-mail-codes..workers.dev", + "gmail_worker_api_key": "与 CODE_API_KEY 相同", + "gmail_worker_timeout_sec": 25, + "gmail_worker_poll_interval": 0.4, + "gmail_imap_fallback": true +} +``` + +### 4. 自测 + +```bash +# 模拟入库 + 取码 +python scripts/test_gmail_worker.py --to test@your-domain.com --ingest-demo + +# 真实邮件:给 random@your-domain.com 发信后轮询 +python scripts/test_gmail_worker.py --to random@your-domain.com --timeout 60 +``` + +日志里成功时会看到: + +```text +[*] Gmail 取码:优先 Email Worker(timeout=25s),超时回退 IMAP +[*] Gmail Worker 取到验证码: ABC-DEF (elapsed=2.1s ...) +``` + +未命中 Worker 时: + +```text +[*] Gmail Worker 未命中,回退 IMAP(剩余 timeout=...s) +[*] Gmail IMAP 从邮件中提取到验证码: ... +``` + + ## 配置 1. 复制模板并编辑(模板内 `"//…"` 键是注释,加载时忽略): diff --git a/auto_register.py b/auto_register.py index 2c9643a..40d5ad2 100755 --- a/auto_register.py +++ b/auto_register.py @@ -182,13 +182,41 @@ def save_account(email: str, password: str, sso: str = ""): f.write(f"{email}----{password}----{sso}\n") print(f"[+] Saved: {email}") +def resolve_browser_path() -> str | None: + # Prefer Chromium: Google Chrome blocks --load-extension (turnstilePatch). + for cand in ( + "/snap/bin/chromium", + "/usr/bin/chromium", + "/usr/bin/chromium-browser", + "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable", + ): + if os.path.isfile(cand) or os.path.islink(cand): + return cand + return None + + def create_browser_options(headless: bool = False, proxy: str = "") -> ChromiumOptions: opts = ChromiumOptions() opts.auto_port() for flag in CHROMIUM_SLIM_FLAGS: opts.set_argument(flag) + browser_path = resolve_browser_path() + if browser_path: + try: + opts.set_browser_path(browser_path) + except Exception: + pass if os.path.exists(EXTENSION_PATH): - opts.add_extension(EXTENSION_PATH) + is_chrome = bool( + browser_path + and "chrome" in os.path.basename(browser_path) + and "chromium" not in browser_path + ) + if is_chrome: + print("[!] skip turnstilePatch: Google Chrome disallows --load-extension; use Chromium") + else: + opts.add_extension(EXTENSION_PATH) if headless: opts.headless() if proxy: diff --git a/cf-email-worker/README.md b/cf-email-worker/README.md new file mode 100644 index 0000000..c59ce47 --- /dev/null +++ b/cf-email-worker/README.md @@ -0,0 +1,98 @@ +# Grok xAI 验证码 Email Worker + +``` +xAI 发信 + → Cloudflare Email Routing(catch-all) + → 本 Worker:提取验证码写入 KV + → (可选)forward 到 Gmail 作 IMAP 兜底 + → 注册机 HTTP GET Worker /code?to=alias@domain&key=... +``` + +## 1. 前置 + +- 域名 DNS 在 Cloudflare +- 开通 **Email Routing** +- 安装 Node:`npm i -g wrangler` 或用 `npx wrangler` + +## 2. 创建 KV + 配置 + +```bash +cd cf-email-worker +npx wrangler login +npx wrangler kv namespace create GROK_MAIL_CODES +# 把返回的 id 填进 wrangler.toml 的 kv_namespaces.id +``` + +编辑 `wrangler.toml`,或在 Dashboard → Workers → Settings → Variables 设置: + +| 变量 | 含义 | +|------|------| +| `CODE_API_KEY` | 注册机查询密钥(必填) | +| `GMAIL_FORWARD_TO` | 如 `you@gmail.com`,保留 IMAP 兜底 | +| `CODE_TTL_SEC` | KV 过期秒数,默认 600 | + +```bash +npx wrangler secret put CODE_API_KEY +# 可选:也可用 vars;secret 更安全 +``` + +## 3. 部署 + +```bash +npx wrangler deploy +# 记下 workers.dev 地址,例如: +# https://grok-xai-mail-codes..workers.dev +``` + +## 4. Email Routing 绑定 + +Cloudflare Dashboard → **Email** → **Email Routing** → **Routing rules**: + +1. 启用 Catch-all +2. Action = **Send to a Worker** → 选择 `grok-xai-mail-codes` +3. 域名 MX 按 Cloudflare 提示配置(若尚未) + +> 若 catch-all 已转发到 Gmail,可改为:Worker 处理 + Worker 内 `GMAIL_FORWARD_TO` 再转到 Gmail。 +> 不要只转 Gmail 而不进 Worker,否则没有秒级路径。 + +## 5. 注册机配置 + +`config.json`: + +```json +{ + "email_provider": "gmail", + "defaultDomains": "your-domain.com", + "gmail_imap_user": "you@gmail.com", + "gmail_imap_password": "app-password", + "gmail_worker_enabled": true, + "gmail_worker_url": "https://grok-xai-mail-codes..workers.dev", + "gmail_worker_api_key": "与 CODE_API_KEY 相同", + "gmail_worker_timeout_sec": 25, + "gmail_worker_poll_interval": 0.4, + "gmail_imap_fallback": true +} +``` + +## 6. 自测 + +```bash +# 健康检查 +curl -sS "https://YOUR_WORKER/health" + +# 模拟入库 +curl -sS -X POST "https://YOUR_WORKER/ingest?key=YOUR_KEY" \ + -H 'content-type: application/json' \ + -d '{"to":"test@your-domain.com","code":"ABC-DEF","subject":"ABC-DEF xAI confirmation code"}' + +# 取码(consume=1 用后即删) +curl -sS "https://YOUR_WORKER/code?to=test@your-domain.com&key=YOUR_KEY" +``` + +真实链路:给 `random@your-domain.com` 发一封带 `ABC-DEF xAI confirmation code` 主题的信,几秒内应能 GET 到。 + +## 7. 安全 + +- `CODE_API_KEY` 用足够长的随机串 +- 不要把 key 提交进 git(写 config.json / .env,二者已在 .gitignore) +- KV 中验证码默认约 10 分钟过期,且 `/code` 默认 consume-once diff --git a/cf-email-worker/package.json b/cf-email-worker/package.json new file mode 100644 index 0000000..3cb6c73 --- /dev/null +++ b/cf-email-worker/package.json @@ -0,0 +1,11 @@ +{ + "name": "grok-xai-mail-codes", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev" + }, + "devDependencies": { + "wrangler": "^3.99.0" + } +} diff --git a/cf-email-worker/src/worker.js b/cf-email-worker/src/worker.js new file mode 100644 index 0000000..1d87384 --- /dev/null +++ b/cf-email-worker/src/worker.js @@ -0,0 +1,280 @@ +/** + * grok-xai-mail-codes — hardened for clearer errors (no bare 1101 if possible) + * + * Bindings: + * KV name MUST be: CODES + * Secrets/vars: + * CODE_API_KEY (required) + * GMAIL_FORWARD_TO (optional) + * CODE_TTL_SEC (optional, default 600) + */ + +const CODE_PATTERNS = [ + /^([A-Z0-9]{3}-[A-Z0-9]{3})\s+xAI/i, + /\b([A-Z0-9]{3}-[A-Z0-9]{3})\b/i, + /verification\s+code[:\s]+(\d{4,8})/i, + /your\s+code[:\s]+(\d{4,8})/i, + /confirm(?:ation)?\s+code[:\s]+(\d{4,8})/i, + /验证码[::\s]+(\d{4,8})/, +]; + +const KEYWORDS = ["x.ai", "xai", "grok", "verification", "code", "confirm", "验证码", "确认"]; + +function json(data, status = 200) { + return new Response(JSON.stringify(data), { + status, + headers: { "content-type": "application/json; charset=utf-8" }, + }); +} + +function extractCode(text, subject = "") { + const blob = `${subject || ""}\n${text || ""}`; + for (const re of CODE_PATTERNS) { + const m = blob.match(re); + if (m && m[1]) return String(m[1]).trim(); + } + return null; +} + +function emailsFromList(list) { + if (!list) return []; + const arr = Array.isArray(list) ? list : [list]; + const out = []; + for (const item of arr) { + if (!item) continue; + if (typeof item === "string") { + const m = item.match(/[\w.+-]+@[\w.-]+\.\w+/g); + if (m) out.push(...m.map((x) => x.toLowerCase())); + continue; + } + if (item.address) out.push(String(item.address).toLowerCase()); + if (item.email) out.push(String(item.email).toLowerCase()); + } + return [...new Set(out.filter(Boolean))]; +} + +async function parseEmail(message) { + const raw = await new Response(message.raw).arrayBuffer(); + const bytes = new Uint8Array(raw); + let rawText = ""; + try { + rawText = new TextDecoder("utf-8", { fatal: false }).decode(bytes); + } catch { + const n = Math.min(bytes.length, 500000); + let s = ""; + for (let i = 0; i < n; i++) s += String.fromCharCode(bytes[i]); + rawText = s; + } + + const headerBlob = rawText.split(/\r?\n\r?\n/, 1)[0] || ""; + let subject = ""; + const subjMatch = headerBlob.match(/^Subject:\s*(.+)$/im); + if (subjMatch) subject = subjMatch[1].trim(); + + let from = ""; + const fromMatch = headerBlob.match(/^From:\s*(.+)$/im); + if (fromMatch) from = fromMatch[1].trim(); + + const recipients = new Set(); + for (const h of ["To", "Cc", "Delivered-To", "X-Original-To", "Envelope-To"]) { + const re = new RegExp(`^${h}:\\s*(.+)$`, "gim"); + let m; + while ((m = re.exec(headerBlob))) { + const found = m[1].match(/[\w.+-]+@[\w.-]+\.\w+/g) || []; + for (const e of found) recipients.add(e.toLowerCase()); + } + } + try { + for (const e of emailsFromList(message.to)) recipients.add(e); + } catch (_) {} + + return { + subject, + from, + recipients: [...recipients], + rawText: rawText.slice(0, 200000), + }; +} + +function isXaiMail(subject, from, body) { + const blob = `${subject}\n${from}\n${body}`.toLowerCase(); + return KEYWORDS.some((k) => blob.includes(k)); +} + +function kv(env) { + // Accept common mistaken binding names + return env.CODES || env.GROK_MAIL_CODES || env.CODE || null; +} + +async function storeCode(env, toEmail, payload) { + const ns = kv(env); + if (!ns || typeof ns.put !== "function") { + throw new Error("KV binding missing: add KV namespace with variable name CODES"); + } + const ttl = Math.max(60, parseInt(String(env.CODE_TTL_SEC || "600"), 10) || 600); + const key = `code:${String(toEmail).toLowerCase().trim()}`; + await ns.put(key, JSON.stringify(payload), { expirationTtl: ttl }); + try { + const prev = JSON.parse((await ns.get("recent")) || "[]"); + prev.unshift({ + to: toEmail, + code: payload.code, + ts: payload.ts, + subject: payload.subject, + }); + await ns.put("recent", JSON.stringify(prev.slice(0, 30)), { expirationTtl: ttl }); + } catch (_) {} +} + +function checkKey(request, env, url) { + const key = url.searchParams.get("key") || request.headers.get("x-api-key") || ""; + const expected = env.CODE_API_KEY || ""; + return Boolean(expected) && key === expected; +} + +export default { + async fetch(request, env, ctx) { + try { + const url = new URL(request.url); + const path = url.pathname.replace(/\/+$/, "") || "/"; + + if (path === "/health") { + const ns = kv(env); + return json({ + ok: true, + service: "grok-xai-mail-codes", + kv_bound: Boolean(ns && typeof ns.get === "function"), + has_api_key: Boolean(env.CODE_API_KEY), + }); + } + + if (!checkKey(request, env, url)) { + return json({ error: "unauthorized" }, 401); + } + + if (path === "/code" && request.method === "GET") { + const to = (url.searchParams.get("to") || url.searchParams.get("target") || "") + .trim() + .toLowerCase(); + if (!to) return json({ error: "missing to" }, 400); + + const ns = kv(env); + if (!ns || typeof ns.get !== "function") { + return json( + { + error: "kv not bound", + hint: "Workers → grok-xai-mail-codes → Settings → Bindings → add KV, Variable name must be CODES", + }, + 500, + ); + } + + const raw = await ns.get(`code:${to}`); + if (!raw) return json({ to, error: "not found" }, 404); + + let data; + try { + data = JSON.parse(raw); + } catch { + return json({ to, error: "bad kv value" }, 500); + } + + const consume = (url.searchParams.get("consume") || "1") !== "0"; + if (consume) { + try { + await ns.delete(`code:${to}`); + } catch (_) {} + } + + return json({ + ok: true, + to, + code: data.code, + subject: data.subject || "", + from: data.from || "", + ts: data.ts || 0, + source: "worker-kv", + }); + } + + if (path === "/recent" && request.method === "GET") { + const ns = kv(env); + if (!ns) return json({ items: [], kv_bound: false }); + const raw = await ns.get("recent"); + return json({ items: raw ? JSON.parse(raw) : [], kv_bound: true }); + } + + if (path === "/ingest" && request.method === "POST") { + let body; + try { + body = await request.json(); + } catch { + return json({ error: "invalid json" }, 400); + } + const to = String(body.to || body.target || "").toLowerCase().trim(); + const code = String(body.code || "").trim(); + if (!to || !code) return json({ error: "missing to/code" }, 400); + try { + await storeCode(env, to, { + code, + subject: body.subject || `${code} xAI confirmation code`, + from: body.from || "", + ts: Date.now(), + }); + } catch (e) { + return json({ error: String(e && e.message ? e.message : e) }, 500); + } + return json({ ok: true, to, code }); + } + + return json({ error: "not found" }, 404); + } catch (e) { + return json( + { + error: "worker exception", + message: String(e && e.message ? e.message : e), + }, + 500, + ); + } + }, + + async email(message, env, ctx) { + let parsed; + try { + parsed = await parseEmail(message); + } catch (_) { + const forwardTo = String(env.GMAIL_FORWARD_TO || "").trim(); + if (forwardTo) { + try { + await message.forward(forwardTo); + } catch (_) {} + } + return; + } + + const { subject, from, recipients, rawText } = parsed; + const code = extractCode(rawText, subject); + const interesting = isXaiMail(subject, from, rawText) || !!code; + + if (interesting && code && recipients.length) { + const payload = { code, subject, from, ts: Date.now() }; + try { + await Promise.all(recipients.map((to) => storeCode(env, to, payload))); + } catch (_) { + // still forward + } + } + + const forwardTo = String(env.GMAIL_FORWARD_TO || "").trim(); + if (forwardTo) { + try { + await message.forward(forwardTo); + } catch (e) { + try { + message.setReject?.(`forward failed: ${e}`); + } catch (_) {} + } + } + }, +}; diff --git a/cf-email-worker/wrangler.toml b/cf-email-worker/wrangler.toml new file mode 100644 index 0000000..606acaa --- /dev/null +++ b/cf-email-worker/wrangler.toml @@ -0,0 +1,26 @@ +# Cloudflare Email Worker for xAI verification codes +# Deploy: cd cf-email-worker && npx wrangler deploy +# +# Required: +# 1) Cloudflare Email Routing enabled for your domain +# 2) Routing rule: catch-all → this Worker +# 3) Optional Gmail forward destination (keeps IMAP fallback) + +name = "grok-xai-mail-codes" +main = "src/worker.js" +compatibility_date = "2024-11-01" + +# KV namespace for short-lived codes (create once, then bind id) +# npx wrangler kv namespace create GROK_MAIL_CODES +[[kv_namespaces]] +binding = "CODES" +id = "REPLACE_WITH_KV_NAMESPACE_ID" +preview_id = "REPLACE_WITH_KV_NAMESPACE_ID" + +[vars] +# Shared secret for GET /code (also set via dashboard secrets if preferred) +# CODE_API_KEY = "change-me" +# Forward original mail to Gmail so IMAP fallback still works +# GMAIL_FORWARD_TO = "you@gmail.com" +# Code TTL seconds in KV +CODE_TTL_SEC = "600" diff --git a/config.example.json b/config.example.json index 15efce4..00dcb71 100644 --- a/config.example.json +++ b/config.example.json @@ -187,6 +187,22 @@ "gmail_imap_password": "", "gmail_imap_host": "imap.gmail.com", "gmail_imap_port": 993, + "// gmail_worker_enabled": "true=优先用 Cloudflare Email Worker HTTP 取码(秒级);false=仅 IMAP", + "gmail_worker_enabled": false, + "// gmail_worker_url": "Worker 根 URL,如 https://grok-xai-mail-codes..workers.dev", + "gmail_worker_url": "", + "// gmail_worker_api_key": "与 Worker 环境变量 CODE_API_KEY 相同", + "gmail_worker_api_key": "", + "// gmail_worker_timeout_sec": "Worker 轮询最长秒数,超时后若 gmail_imap_fallback=true 则回退 IMAP", + "gmail_worker_timeout_sec": 25, + "// gmail_worker_poll_interval": "Worker HTTP 轮询间隔(秒)", + "gmail_worker_poll_interval": 0.4, + "// gmail_imap_fallback": "Worker 未命中时是否回退 Gmail IMAP", + "gmail_imap_fallback": true, + "// gmail_imap_fallback_timeout_sec": "Worker 未命中后 IMAP 最多等几秒(Catch-all 只进 Worker 时请设小或关闭 fallback)", + "gmail_imap_fallback_timeout_sec": 30, + "// gmail_worker_timeout_sec": "Worker 轮询最长秒数;命中通常 1–3s,过大只会拖慢换号", + "gmail_worker_timeout_sec": 12, "// gmail_delete_after_code": "Gmail 取到验证码后是否 IMAP 删除该邮件(失败不影响注册)", "gmail_delete_after_code": true, "// hotmail_delete_after_code": "Hotmail/Outlook 取到验证码后是否 IMAP 删除该邮件", diff --git a/config.json b/config.json new file mode 100644 index 0000000..3f440ee --- /dev/null +++ b/config.json @@ -0,0 +1,105 @@ +{ + "duckmail_api_key": "", + "cloudflare_api_base": "", + "cloudflare_api_key": "", + "cloudflare_auth_mode": "none", + "cloudflare_path_domains": "/api/domains", + "cloudflare_path_accounts": "/api/new_address", + "cloudflare_path_token": "/api/token", + "cloudflare_path_messages": "/api/mails", + "proxy": "", + "enable_nsfw": true, + "register_count": 1, + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", + "grok2api_auto_add_local": false, + "grok2api_local_token_file": "", + "grok2api_pool_name": "ssoBasic", + "grok2api_auto_add_remote": false, + "grok2api_remote_base": "http://127.0.0.1:8000/admin/api", + "grok2api_remote_app_key": "", + "register_threads": 1, + "thread_start_interval": 0.8, + "show_tutorial_on_start": false, + "cloudmail_url": "", + "cloudmail_admin_email": "", + "cloudmail_password": "", + "cpa_gui_close_mint_browser": true, + "hotmail_accounts_file": "mail_credentials.txt", + "hotmail_alias_mode": "random", + "hotmail_alias_random_length": 8, + "hotmail_alias_random_max_attempts": 200, + "hotmail_max_aliases_per_account": 5, + "hotmail_poll_interval": 5, + "hotmail_recent_seconds": 900, + "hotmail_imap_hosts": "outlook.office365.com,imap-mail.outlook.com", + "hotmail_imap_last_n": 30, + "hotmail_require_recipient_match": true, + "email_provider": "gmail", + "defaultDomains": "zhangyunuo.net", + "yyds_api_key": "", + "yyds_jwt": "", + "register_max_attempts": 30, + "account_hard_timeout": 720, + "nav_email_button_timeout": 12, + "email_form_timeout": 20, + "mail_timeout": 50, + "mail_poll_interval": 1, + "mail_retry_count": 3, + "code_form_timeout": 180, + "profile_timeout": 50, + "turnstile_retry_limit": 3, + "turnstile_stuck_timeout": 150, + "sso_timeout_base": 60, + "sso_timeout_max": 480, + "sso_progress_extension": 120, + "sso_cookie_read_timeout": 20, + "email_submit_confirm_timeout": 30, + "grok2api_import_retries": 5, + "grok2api_import_retry_delay": 2, + "api_reverse_tools": "", + "cpa_export_enabled": true, + "cpa_auth_dir": "./cpa_auths", + "cpa_copy_to_hotload": false, + "cpa_hotload_dir": "", + "cpa_base_url": "https://cli-chat-proxy.grok.com/v1", + "cpa_proxy": "", + "cpa_headless": false, + "cpa_force_standalone": true, + "cpa_mint_timeout_sec": 300, + "cpa_mint_required": false, + "cpa_probe_after_write": true, + "cpa_probe_chat": false, + "cpa_mint_workers": -1, + "cpa_mint_queue_max": 0, + "cpa_prefer_protocol": true, + "cpa_protocol_only": false, + "cpa_protocol_poll_timeout_sec": 90, + "cpa_mint_cookie_inject": true, + "cpa_mint_browser_reuse": true, + "cpa_mint_browser_recycle_every": 15, + "cpa_probe_required": false, + "gmail_imap_user": "nopjcn@gmail.com", + "gmail_imap_password": "fjjp ckmp sonv cjtq", + "gmail_imap_host": "imap.gmail.com", + "gmail_imap_port": 993, + "gmail_resend_after_sec": 45, + "cpa_auto_import_remote": false, + "cpa_remote_base": "", + "cpa_remote_password": "", + "cpa_remote_import_retries": 3, + "cpa_remote_import_retry_delay": 2, + "gmail_delete_after_code": true, + "hotmail_delete_after_code": true, + "gmail_worker_enabled": true, + "gmail_worker_url": "https://grok-xai-mail-codes.nopjcn.workers.dev", + "gmail_worker_api_key": "ZhaoChao19950510", + "gmail_worker_timeout_sec": 20, + "gmail_worker_poll_interval": 0.4, + "gmail_imap_fallback": true, + "gmail_imap_fallback_timeout_sec": 45, + "gmail_imap_fallback_resend_sec": 10, + "turnstile_fast_fail_count": 2, + "turnstile_retry_after_sec": 8, + "turnstile_retry_gap_sec": 5, + "turnstile_max_rounds": 4 +} diff --git a/cpa_xai/browser_confirm.py b/cpa_xai/browser_confirm.py index 33ccdaa..13aad05 100644 --- a/cpa_xai/browser_confirm.py +++ b/cpa_xai/browser_confirm.py @@ -216,15 +216,18 @@ def create_standalone_page( pass log(f"headed browser DISPLAY={os.environ.get('DISPLAY', '')!r}") + # Prefer Chromium: Google Chrome blocks --load-extension (turnstilePatch). for cand in ( + "/snap/bin/chromium", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", ): - if os.path.isfile(cand): + if os.path.isfile(cand) or os.path.islink(cand): try: opts.set_browser_path(cand) + log(f"browser path={cand}") except Exception: pass break diff --git a/cpa_xai/mint.py b/cpa_xai/mint.py index 161de24..055aead 100644 --- a/cpa_xai/mint.py +++ b/cpa_xai/mint.py @@ -2,6 +2,9 @@ from __future__ import annotations +import threading +import time + from pathlib import Path from typing import Any, Callable @@ -14,6 +17,11 @@ from .writer import write_cpa_xai_auth LogFn = Callable[[str], None] +# Serialize/spacing protocol mints to reduce accounts.x.ai rate_limited. +_PROTOCOL_MINT_LOCK = threading.Lock() +_PROTOCOL_LAST_OK_AT = 0.0 +_PROTOCOL_MIN_INTERVAL_SEC = 1.5 + def _noop(_: str) -> None: return None @@ -51,6 +59,7 @@ def mint_and_export( Returns dict with keys: ok, path, email, probe, error?, mint_method? """ log = log or _noop + global _PROTOCOL_LAST_OK_AT email = (email or "").strip() if not email or not password: # Protocol can work with sso alone; password only required for browser fallback @@ -71,38 +80,61 @@ def mint_and_export( if prefer_protocol and sso_val: log("mint try protocol (SSO HTTP device flow)") - try: - tokens = mint_with_sso_protocol( - sso_cookie=sso_val, - email=email, - proxy=resolved or None, - poll_timeout_sec=protocol_poll_timeout_sec, - log=log, - cancel=cancel, - ) - log("mint protocol SUCCESS") - except ProtocolMintError as e: - protocol_err = str(e) - log(f"mint protocol failed: {e}") - if protocol_only: - return { - "ok": False, - "email": email, - "error": f"protocol_only: {e}", - "mint_method": "protocol", - } - log("mint fallback → browser") - except Exception as e: # noqa: BLE001 - protocol_err = str(e) - log(f"mint protocol exception: {e}") - if protocol_only: - return { - "ok": False, - "email": email, - "error": f"protocol_only: {e}", - "mint_method": "protocol", - } - log("mint fallback → browser") + max_protocol_attempts = 3 + for attempt in range(1, max_protocol_attempts + 1): + if cancel and cancel(): + return {"ok": False, "email": email, "error": "cancelled", "mint_method": "protocol"} + # Throttle protocol calls across mint workers + with _PROTOCOL_MINT_LOCK: + gap = time.time() - _PROTOCOL_LAST_OK_AT + if _PROTOCOL_LAST_OK_AT > 0 and gap < _PROTOCOL_MIN_INTERVAL_SEC: + wait = _PROTOCOL_MIN_INTERVAL_SEC - gap + log(f"protocol throttle sleep {wait:.1f}s") + time.sleep(wait) + try: + tokens = mint_with_sso_protocol( + sso_cookie=sso_val, + email=email, + proxy=resolved or None, + poll_timeout_sec=protocol_poll_timeout_sec, + log=log, + cancel=cancel, + ) + with _PROTOCOL_MINT_LOCK: + _PROTOCOL_LAST_OK_AT = time.time() + log("mint protocol SUCCESS") + protocol_err = None + break + except ProtocolMintError as e: + protocol_err = str(e) + log(f"mint protocol failed (try {attempt}/{max_protocol_attempts}): {e}") + rate_limited = "rate_limited" in protocol_err.lower() + if rate_limited and attempt < max_protocol_attempts: + backoff = 5.0 * attempt + log(f"protocol rate_limited → backoff {backoff:.0f}s then retry") + time.sleep(backoff) + continue + if protocol_only: + return { + "ok": False, + "email": email, + "error": f"protocol_only: {e}", + "mint_method": "protocol", + } + log("mint fallback → browser") + break + except Exception as e: # noqa: BLE001 + protocol_err = str(e) + log(f"mint protocol exception (try {attempt}/{max_protocol_attempts}): {e}") + if protocol_only: + return { + "ok": False, + "email": email, + "error": f"protocol_only: {e}", + "mint_method": "protocol", + } + log("mint fallback → browser") + break elif prefer_protocol and not sso_val: log("mint protocol skipped (no sso cookie) → browser") if protocol_only: diff --git a/grok_register_ttk.py b/grok_register_ttk.py index 7b6352d..49deafe 100644 --- a/grok_register_ttk.py +++ b/grok_register_ttk.py @@ -621,6 +621,34 @@ CHROMIUM_SLIM_FLAGS = [ "--no-first-run", ] +# Prefer Google Chrome for registration reliability (email/CF path). +# Chromium is fallback; turnstilePatch only loads on Chromium (Chrome blocks --load-extension). +BROWSER_CANDIDATES = ( + "/usr/bin/google-chrome-stable", + "/usr/bin/google-chrome", + "/snap/bin/chromium", + "/usr/bin/chromium", + "/usr/bin/chromium-browser", +) + + +def _is_google_chrome_path(path: str | None) -> bool: + if not path: + return False + base = os.path.basename(path) + return "chrome" in base and "chromium" not in path + + +def resolve_browser_path(): + # Optional override from config.json "browser_path" + override = str((config.get("browser_path") if isinstance(config, dict) else "") or "").strip() + if override and (os.path.isfile(override) or os.path.islink(override)): + return override + for cand in BROWSER_CANDIDATES: + if os.path.isfile(cand) or os.path.islink(cand): + return cand + return None + def create_browser_options(): options = ChromiumOptions() @@ -628,8 +656,22 @@ def create_browser_options(): options.set_timeouts(base=1) for flag in CHROMIUM_SLIM_FLAGS: options.set_argument(flag) + browser_path = resolve_browser_path() + if browser_path: + try: + options.set_browser_path(browser_path) + print(f" [browser] path={browser_path}", flush=True) + except Exception: + pass if os.path.exists(EXTENSION_PATH): - options.add_extension(EXTENSION_PATH) + # Google Chrome blocks CLI unpacked extension loading; Chromium still allows it. + if _is_google_chrome_path(browser_path): + print( + " [ext] skip turnstilePatch on Google Chrome (--load-extension blocked)", + flush=True, + ) + else: + options.add_extension(EXTENSION_PATH) # Apply config.json "proxy" to Chromium. Without this, only HTTP helpers # used get_proxies(); the browser itself fell through to system/env proxy. proxy = (config.get("proxy") or "").strip() @@ -1833,6 +1875,112 @@ def get_email_provider(): # ──────────────────────── Gmail + Cloudflare catch-all ──────────────────────── +def _gmail_worker_enabled() -> bool: + if not _config_bool(config.get("gmail_worker_enabled", False), default=False): + return False + url = str(config.get("gmail_worker_url", "") or os.getenv("GMAIL_WORKER_URL", "") or "").strip() + key = str(config.get("gmail_worker_api_key", "") or os.getenv("GMAIL_WORKER_API_KEY", "") or "").strip() + return bool(url and key) + + +def _gmail_worker_base_url() -> str: + return str(config.get("gmail_worker_url", "") or os.getenv("GMAIL_WORKER_URL", "") or "").strip().rstrip("/") + + +def _gmail_worker_api_key() -> str: + return str(config.get("gmail_worker_api_key", "") or os.getenv("GMAIL_WORKER_API_KEY", "") or "").strip() + + +def gmail_worker_fetch_code( + email: str, + *, + timeout: float = 25.0, + poll_interval: float | None = None, + log_callback=None, + cancel_callback=None, + consume: bool = True, +) -> str | None: + """Poll Cloudflare Email Worker KV HTTP API for a code. + + Returns code string or None on timeout / not configured. + Does NOT raise on 404; raises only on hard misconfiguration after retries optional. + """ + base = _gmail_worker_base_url() + key = _gmail_worker_api_key() + if not base or not key: + return None + if poll_interval is None: + try: + poll_interval = float(config.get("gmail_worker_poll_interval", 0.4) or 0.4) + except Exception: + poll_interval = 0.4 + poll_interval = max(0.15, min(float(poll_interval), 3.0)) + try: + timeout = float(timeout) + except Exception: + timeout = 25.0 + deadline = time.time() + max(1.0, timeout) + target = (email or "").strip().lower() + if not target: + return None + t0 = time.time() + polls = 0 + last_err = "" + url = f"{base}/code" + while time.time() < deadline: + raise_if_cancelled(cancel_callback) + polls += 1 + try: + # Prefer no proxy for workers.dev / own CF endpoint (local latency) + resp = http_get( + url, + params={ + "to": target, + "key": key, + "consume": "1" if consume else "0", + }, + headers={"x-api-key": key}, + timeout=8, + proxies={}, + ) + if resp.status_code == 200: + data = resp.json() if hasattr(resp, "json") else {} + if not isinstance(data, dict): + data = {} + code = str(data.get("code") or "").strip() + if code: + if log_callback: + log_callback( + f"[*] Gmail Worker 取到验证码: {code} " + f"(elapsed={time.time() - t0:.1f}s polls={polls} source={data.get('source') or 'worker'})" + ) + return code + elif resp.status_code == 404: + last_err = "not found" + elif resp.status_code in (401, 403): + last_err = f"auth {resp.status_code}" + if log_callback and polls <= 2: + log_callback(f"[Debug] Gmail Worker 鉴权失败 HTTP {resp.status_code}(检查 gmail_worker_api_key)") + # auth errors won't recover quickly + if polls >= 3: + return None + else: + last_err = f"HTTP {resp.status_code}" + if log_callback and polls <= 3: + log_callback(f"[Debug] Gmail Worker 响应 {resp.status_code}: {str(getattr(resp, 'text', '') or '')[:120]}") + except Exception as exc: + last_err = str(exc) + if log_callback and polls <= 3: + log_callback(f"[Debug] Gmail Worker 请求失败: {exc}") + sleep_with_cancel(poll_interval, cancel_callback) + if log_callback: + log_callback( + f"[*] Gmail Worker {timeout:.0f}s 内未取到验证码 ({last_err or 'timeout'})," + f"{'将回退 IMAP' if _config_bool(config.get('gmail_imap_fallback', True), default=True) else '结束'}" + ) + return None + + def gmail_get_email_and_token(): raw = str(config.get("defaultDomains", "") or "") domains = [x.strip() for x in re.split(r"[,,\s]+", raw) if x.strip()] @@ -1853,11 +2001,57 @@ def gmail_get_oai_code( cancel_callback=None, resend_callback=None, ): - """Poll Gmail IMAP for xAI code. Optimized for catch-all + subject codes. + """Fetch xAI code: Cloudflare Email Worker first (optional), then Gmail IMAP. - xAI subjects look like: "ABC-DEF xAI confirmation code". - We fetch HEADER only first (no full RFC822 body) and extract from Subject/To. + Hybrid path (recommended): + xAI → CF Email Routing → Worker(KV) → GET /code (seconds) + fallback → Gmail IMAP (if Worker miss / disabled) + + Worker subjects still look like: "ABC-DEF xAI confirmation code". + IMAP path fetches HEADER only first (subject often has the code). """ + # ── 1) Email Worker (KV HTTP) — fast path ── + # Lesson from batch10: hits are ~1.5s; misses used to burn 25s worker + ~155s IMAP + # even when Catch-all only delivers to Worker (IMAP never sees the mail). + worker_on = _gmail_worker_enabled() + imap_fallback = _config_bool(config.get("gmail_imap_fallback", True), default=True) + if worker_on: + try: + # Prefer short wait: real hits arrive in 1–3s; long waits only delay retry. + worker_timeout = float(config.get("gmail_worker_timeout_sec", 12) or 12) + except Exception: + worker_timeout = 12.0 + worker_timeout = max(4.0, min(worker_timeout, float(timeout))) + if log_callback: + log_callback( + f"[*] Gmail 取码:优先 Email Worker(timeout={worker_timeout:.0f}s)" + + (",未命中再短等 IMAP" if imap_fallback else ",未命中即换号") + ) + code = gmail_worker_fetch_code( + email, + timeout=worker_timeout, + log_callback=log_callback, + cancel_callback=cancel_callback, + consume=True, + ) + if code: + return code + if not imap_fallback: + raise Exception( + f"Gmail Worker 在 {worker_timeout:.0f}s 内未收到验证码(gmail_imap_fallback=false): {email}" + ) + # Short IMAP fallback only — Catch-all→Worker often never reaches Gmail. + try: + imap_fb = float(config.get("gmail_imap_fallback_timeout_sec", 35) or 35) + except Exception: + imap_fb = 35.0 + timeout = max(15.0, min(imap_fb, max(0.0, float(timeout) - worker_timeout))) + if log_callback: + log_callback( + f"[*] Gmail Worker 未命中,短回退 IMAP(timeout={timeout:.0f}s;" + f"若邮件只进 Worker 请设 GMAIL_FORWARD_TO 或 gmail_imap_fallback=false)" + ) + if poll_interval is None: # Gmail catch-all benefits from denser polling; default 1s unless config forces higher. poll_interval = float(config.get("mail_poll_interval", 1) or 1) @@ -1865,13 +2059,24 @@ def gmail_get_oai_code( imap_user = str(config.get("gmail_imap_user", "") or os.getenv("GMAIL_USER", "") or "").strip() imap_password = str(config.get("gmail_imap_password", "") or os.getenv("GMAIL_PASSWORD", "") or "").strip() if not imap_user or not imap_password: + if worker_on: + raise Exception( + f"Gmail Worker 未取到验证码,且 IMAP 未配置(gmail_imap_user/password): {email}" + ) raise Exception("Gmail IMAP 未配置:需要在 config.json 设置 gmail_imap_user/gmail_imap_password,或环境变量 GMAIL_USER/GMAIL_PASSWORD") imap_host = str(config.get("gmail_imap_host", "imap.gmail.com") or "imap.gmail.com").strip() imap_port = int(config.get("gmail_imap_port", 993) or 993) delete_after = _config_bool(config.get("gmail_delete_after_code", True), default=True) deadline = time.time() + timeout - # Resend earlier: first mail often arrives in 5-40s; resend at 45s as safety net. - next_resend_at = time.time() + float(config.get("gmail_resend_after_sec", 45) or 45) + # Resend: default 45s for pure IMAP; after worker-miss fallback use sooner resend. + try: + resend_after = float(config.get("gmail_resend_after_sec", 45) or 45) + except Exception: + resend_after = 45.0 + if worker_on: + # fallback window is short; resend once around 12s into IMAP + resend_after = min(resend_after, max(8.0, float(config.get("gmail_imap_fallback_resend_sec", 12) or 12))) + next_resend_at = time.time() + resend_after imap = None target_lower = email.lower().strip() seen_ids: set[bytes] = set() @@ -2965,7 +3170,11 @@ return 'clicked'; raise Exception("验证码已获取,但自动填写/提交失败") -def getTurnstileToken(log_callback=None, cancel_callback=None): +def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 4): + """Try to obtain a Turnstile token quickly. + + max_rounds default 4 (~3-5s) — fail fast and restart browser/account. + """ page = _get_page() if page is None: raise Exception("页面未就绪,无法执行 Turnstile") @@ -2977,7 +3186,8 @@ def getTurnstileToken(log_callback=None, cancel_callback=None): except Exception: pass - for _ in range(0, 20): + rounds = max(2, int(max_rounds or 4)) + for _ in range(0, rounds): raise_if_cancelled(cancel_callback) try: token = page.run_js( @@ -3040,7 +3250,7 @@ if (nodes.length && typeof nodes[0].click === 'function') nodes[0].click(); ) except Exception: pass - human_sleep(1, cancel_callback) + human_sleep(0.7, cancel_callback) raise Exception("Turnstile 获取 token 失败") @@ -3086,6 +3296,23 @@ def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None wait_cf_since = None last_cf_retry_at = 0.0 last_cf_log_at = 0.0 + cf_token_fail_streak = 0 + try: + max_cf_token_fails = max(1, int(config.get("turnstile_fast_fail_count", 2) or 2)) + except Exception: + max_cf_token_fails = 2 + try: + cf_retry_after = float(config.get("turnstile_retry_after_sec", 8) or 8) + except Exception: + cf_retry_after = 8.0 + try: + cf_retry_gap = float(config.get("turnstile_retry_gap_sec", 5) or 5) + except Exception: + cf_retry_gap = 5.0 + try: + turnstile_rounds = max(3, int(config.get("turnstile_max_rounds", 4) or 4)) + except Exception: + turnstile_rounds = 4 while time.time() < deadline: raise_if_cancelled(cancel_callback) @@ -3177,12 +3404,13 @@ return 'filled-no-submit'; log_callback(f"[*] 资料已填写,等待 Cloudflare... token长度={token_len} waited={waited:.0f}s") last_cf_log_at = now # 卡住后自动二次复用 Turnstile 组件 - if now - wait_cf_since >= 12 and now - last_cf_retry_at >= 8: + if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap: if log_callback: log_callback("[*] Cloudflare 验证卡住,开始二次复用 Turnstile...") try: - token = getTurnstileToken(log_callback=log_callback, cancel_callback=cancel_callback) + token = getTurnstileToken(log_callback=log_callback, cancel_callback=cancel_callback, max_rounds=turnstile_rounds) if token: + cf_token_fail_streak = 0 synced = page.run_js( """ const token = String(arguments[0] || '').trim(); @@ -3200,8 +3428,11 @@ return String(cfInput.value || '').trim().length; if log_callback: log_callback(f"[*] Turnstile 二次复用完成,回填长度={synced}") except Exception as cf_exc: + cf_token_fail_streak += 1 if log_callback: - log_callback(f"[Debug] Turnstile 二次复用失败: {cf_exc}") + log_callback(f"[Debug] Turnstile 二次复用失败({cf_token_fail_streak}/{max_cf_token_fails}): {cf_exc}") + if cf_token_fail_streak >= max_cf_token_fails: + raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}") last_cf_retry_at = now human_sleep(0.8, cancel_callback) continue @@ -3258,12 +3489,13 @@ return 'submitted'; waited = now - wait_cf_since if wait_cf_since else 0 log_callback(f"[*] 等待 Cloudflare 后再提交... token长度={token_len} waited={waited:.0f}s") last_cf_log_at = now - if now - wait_cf_since >= 12 and now - last_cf_retry_at >= 8: + if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap: if log_callback: log_callback("[*] 提交前仍卡住,自动再次复用 Turnstile...") try: - token = getTurnstileToken(log_callback=log_callback, cancel_callback=cancel_callback) + token = getTurnstileToken(log_callback=log_callback, cancel_callback=cancel_callback, max_rounds=turnstile_rounds) if token: + cf_token_fail_streak = 0 synced = page.run_js( """ const token = String(arguments[0] || '').trim(); @@ -3281,8 +3513,11 @@ return String(cfInput.value || '').trim().length; if log_callback: log_callback(f"[*] Turnstile 二次复用完成,回填长度={synced}") except Exception as cf_exc: + cf_token_fail_streak += 1 if log_callback: - log_callback(f"[Debug] Turnstile 二次复用失败: {cf_exc}") + log_callback(f"[Debug] Turnstile 二次复用失败({cf_token_fail_streak}/{max_cf_token_fails}): {cf_exc}") + if cf_token_fail_streak >= max_cf_token_fails: + raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}") last_cf_retry_at = now human_sleep(0.8, cancel_callback) continue @@ -3525,9 +3760,9 @@ return String(cfInput.value || '').trim().length; if name: last_seen_names.add(name) - if name == "sso" and value: + if name in ("sso", "sso-rw") and value: if log_callback: - log_callback("[*] 已获取到 sso cookie") + log_callback(f"[*] 已获取到 {name} cookie") return value except PageDisconnectedError: refresh_active_page() diff --git a/register_cli.py b/register_cli.py index 83ce169..1689e7a 100644 --- a/register_cli.py +++ b/register_cli.py @@ -57,26 +57,56 @@ def _patched_create_browser_options(): except Exception: pass - for cand in ( - "/snap/bin/chromium", - "/usr/bin/chromium", - "/usr/bin/chromium-browser", - "/usr/bin/google-chrome", - "/usr/bin/google-chrome-stable", - ): - if os.path.isfile(cand): - try: - opts.set_browser_path(cand) - except Exception: - pass - break - - ext_path = os.path.join(os.path.dirname(os.path.abspath(reg.__file__)), "turnstilePatch") - if os.path.isdir(ext_path): + # Prefer Google Chrome for registration reliability; Chromium fallback for extension. + browser_path = None + resolve = getattr(reg, "resolve_browser_path", None) + if callable(resolve): try: - opts.add_extension(ext_path) + browser_path = resolve() + except Exception: + browser_path = None + if not browser_path: + for cand in ( + "/usr/bin/google-chrome-stable", + "/usr/bin/google-chrome", + "/snap/bin/chromium", + "/usr/bin/chromium", + "/usr/bin/chromium-browser", + ): + if os.path.isfile(cand) or os.path.islink(cand): + browser_path = cand + break + if browser_path: + try: + opts.set_browser_path(browser_path) + print(f"[browser] path={browser_path}", flush=True) except Exception: pass + + # create_browser_options already handles turnstilePatch (skip on Google Chrome). + # Only re-add here for the fallback options path on Chromium. + ext_path = os.path.join(os.path.dirname(os.path.abspath(reg.__file__)), "turnstilePatch") + already = False + try: + already = any( + os.path.abspath(str(p)) == os.path.abspath(ext_path) + for p in (getattr(opts, "extensions", None) or []) + ) + except Exception: + already = False + if os.path.isdir(ext_path) and not already: + is_chrome = bool( + browser_path + and "chrome" in os.path.basename(browser_path) + and "chromium" not in browser_path + ) + if is_chrome: + print("[ext] skip turnstilePatch on Google Chrome (--load-extension blocked)", flush=True) + else: + try: + opts.add_extension(ext_path) + except Exception: + pass return opts @@ -272,14 +302,33 @@ def register_one( return None try: + cfg = getattr(reg, "config", {}) or {} + try: + profile_timeout = int(cfg.get("profile_timeout", 120) or 120) + except Exception: + profile_timeout = 120 + try: + # Prefer faster fail+retry over long dead waits; cap via config. + sso_timeout = int(cfg.get("sso_timeout_base", 120) or 120) + except Exception: + sso_timeout = 120 + # Fast-fail band: keep CF stalls from dominating wall clock. + # batch10 lesson: success profiles finish in ~6–18s; 40–50s CF fail is enough. + profile_timeout = max(35, min(profile_timeout, 70)) + sso_timeout = max(30, min(sso_timeout, 90)) + log(worker_id, "4. 填写资料") profile = reg.fill_profile_and_submit( - log_callback=lambda m: log(worker_id, m), cancel_callback=cancel + timeout=profile_timeout, + log_callback=lambda m: log(worker_id, m), + cancel_callback=cancel, ) log(worker_id, f"资料已填: {profile.get('given_name')} {profile.get('family_name')}") log(worker_id, "5. 等待 sso cookie") sso = reg.wait_for_sso_cookie( - log_callback=lambda m: log(worker_id, m), cancel_callback=cancel + timeout=sso_timeout, + log_callback=lambda m: log(worker_id, m), + cancel_callback=cancel, ) password = profile.get("password", "") or "" line = f"{email}----{password}----{sso}\n" diff --git a/scripts/setup_gmail_worker.sh b/scripts/setup_gmail_worker.sh new file mode 100755 index 0000000..0ddaf2b --- /dev/null +++ b/scripts/setup_gmail_worker.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Helper: create KV + print next steps for Email Worker +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT/cf-email-worker" + +if ! command -v npx >/dev/null 2>&1; then + echo "需要 Node.js / npx。先安装: https://nodejs.org/" + exit 1 +fi + +echo "[1/4] wrangler login (如已登录可跳过)" +npx wrangler login || true + +echo "[2/4] 创建 KV namespace GROK_MAIL_CODES" +OUT=$(npx wrangler kv namespace create GROK_MAIL_CODES 2>&1 || true) +echo "$OUT" +ID=$(echo "$OUT" | sed -n 's/.*id *= *"\([^"]*\)".*/\1/p' | head -1) +if [[ -z "${ID}" ]]; then + ID=$(echo "$OUT" | grep -oE '[a-f0-9]{32}' | head -1 || true) +fi +if [[ -n "${ID}" ]]; then + echo "检测到 KV id=$ID — 写入 wrangler.toml" + if grep -q 'REPLACE_WITH_KV_NAMESPACE_ID' wrangler.toml; then + sed -i "s/REPLACE_WITH_KV_NAMESPACE_ID/${ID}/g" wrangler.toml + fi +else + echo "未能自动解析 KV id,请手动填 wrangler.toml" +fi + +echo "[3/4] 设置 CODE_API_KEY secret" +echo "将提示你输入密钥(与 config gmail_worker_api_key 一致)" +npx wrangler secret put CODE_API_KEY + +if [[ -n "${GMAIL_FORWARD_TO:-}" ]]; then + echo "[3b] 设置 GMAIL_FORWARD_TO=$GMAIL_FORWARD_TO" + printf '%s' "$GMAIL_FORWARD_TO" | npx wrangler secret put GMAIL_FORWARD_TO +else + echo "[3b] 跳过 GMAIL_FORWARD_TO(可 export GMAIL_FORWARD_TO=you@gmail.com 后重跑)" +fi + +echo "[4/4] deploy" +npx wrangler deploy + +echo +echo "完成。接下来:" +echo " 1) Email Routing catch-all → 绑定本 Worker" +echo " 2) config.json 设置:" +echo " gmail_worker_enabled=true" +echo " gmail_worker_url=https://.workers.dev" +echo " gmail_worker_api_key=<你的 CODE_API_KEY>" +echo " 3) 测试: python scripts/test_gmail_worker.py --to test@yourdomain.com" diff --git a/scripts/test_gmail_worker.py b/scripts/test_gmail_worker.py new file mode 100755 index 0000000..2cd1cda --- /dev/null +++ b/scripts/test_gmail_worker.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Test Cloudflare Email Worker code API (and optional local hybrid path).""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +from pathlib import Path +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +ROOT = Path(__file__).resolve().parents[1] + + +def load_cfg(): + p = ROOT / "config.json" + if p.is_file(): + return json.loads(p.read_text(encoding="utf-8")) + return {} + + +def main(): + cfg = load_cfg() + ap = argparse.ArgumentParser(description="Test gmail Email Worker /code API") + ap.add_argument("--url", default=cfg.get("gmail_worker_url") or os.getenv("GMAIL_WORKER_URL") or "") + ap.add_argument("--key", default=cfg.get("gmail_worker_api_key") or os.getenv("GMAIL_WORKER_API_KEY") or "") + ap.add_argument("--to", required=True, help="alias@domain to query") + ap.add_argument("--timeout", type=float, default=20) + ap.add_argument("--interval", type=float, default=0.5) + ap.add_argument("--ingest-demo", action="store_true", help="POST a fake code then GET it") + ap.add_argument("--no-consume", action="store_true") + args = ap.parse_args() + base = (args.url or "").rstrip("/") + key = args.key or "" + if not base or not key: + print("需要 --url/--key 或 config.json 的 gmail_worker_url / gmail_worker_api_key") + return 1 + + def get(path, params=None): + q = dict(params or {}) + q["key"] = key + url = f"{base}{path}?{urlencode(q)}" + req = Request(url, headers={"x-api-key": key}) + with urlopen(req, timeout=10) as r: + return r.status, json.loads(r.read().decode()) + + def post(path, body): + url = f"{base}{path}?{urlencode({'key': key})}" + data = json.dumps(body).encode() + req = Request(url, data=data, headers={"content-type": "application/json", "x-api-key": key}, method="POST") + with urlopen(req, timeout=10) as r: + return r.status, json.loads(r.read().decode()) + + st, health = get("/health") + print("health", st, health) + + if args.ingest_demo: + code = "AB1-CD2" + st, body = post("/ingest", {"to": args.to, "code": code, "subject": f"{code} xAI confirmation code"}) + print("ingest", st, body) + + deadline = time.time() + args.timeout + while time.time() < deadline: + try: + st, body = get( + "/code", + {"to": args.to.lower(), "consume": "0" if args.no_consume else "1"}, + ) + print("code", st, body) + if st == 200 and body.get("code"): + return 0 + except Exception as e: + print("wait", e) + time.sleep(args.interval) + print("timeout — 若是真实邮件,检查 Email Routing 是否绑到 Worker") + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/turnstilePatch/content.js b/turnstilePatch/content.js index 6af4eee..d7ddb20 100644 --- a/turnstilePatch/content.js +++ b/turnstilePatch/content.js @@ -1,5 +1,5 @@ // Turnstile Patch - 隐藏自动化标识,加速 Turnstile 验证 -// 在 document_start 阶段执行,确保在页面脚本之前生效 +// MV3 MAIN world + document_start:在页面脚本之前修补真实页面上下文 (function () { "use strict"; @@ -25,13 +25,15 @@ // 3. 覆盖 permissions.query,隐藏 notifications 权限异常 try { - var origQuery = navigator.permissions.query.bind(navigator.permissions); - navigator.permissions.query = function (params) { - if (params.name === "notifications") { - return Promise.resolve({ state: Notification.permission }); - } - return origQuery(params); - }; + if (navigator.permissions && navigator.permissions.query) { + var origQuery = navigator.permissions.query.bind(navigator.permissions); + navigator.permissions.query = function (params) { + if (params && params.name === "notifications") { + return Promise.resolve({ state: Notification.permission }); + } + return origQuery(params); + }; + } } catch (e) {} // 4. 修补 plugin 数量,模拟正常浏览器 diff --git a/turnstilePatch/manifest.json b/turnstilePatch/manifest.json index f153b00..5ff94c2 100644 --- a/turnstilePatch/manifest.json +++ b/turnstilePatch/manifest.json @@ -1,15 +1,16 @@ { - "manifest_version": 2, + "manifest_version": 3, "name": "Turnstile Patch", - "version": "1.0.0", + "version": "1.1.0", "description": "Patch browser automation detection for Turnstile", "content_scripts": [ { "matches": [""], "js": ["content.js"], "run_at": "document_start", - "all_frames": true + "all_frames": true, + "world": "MAIN", + "match_about_blank": true } - ], - "permissions": ["activeTab"] + ] }