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.
This commit is contained in:
chaos committed 2026-07-11 23:21:23 +08:00
1 parent 187792d6be
commit 55f56d27c9
19 files changed
+1176 -36

No files matched your search

+28
View File
@@ -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
+5
View File
@@ -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.<subdomain>.workers.dev
# GMAIL_WORKER_API_KEY=
# 兼容别名(gmail_code_listener 会回退读取)
# MAIL_USER=
# MAIL_PASSWORD=
+5 -2
View File
@@ -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/
+84
View File
@@ -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://<worker>/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.<subdomain>.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. 复制模板并编辑(模板内 `"//…"` 键是注释,加载时忽略):
+28
View File
@@ -182,12 +182,40 @@ 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):
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()
+98
View File
@@ -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.<subdomain>.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.<subdomain>.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
+11
View File
@@ -0,0 +1,11 @@
{
"name": "grok-xai-mail-codes",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev"
},
"devDependencies": {
"wrangler": "^3.99.0"
}
}
+280
View File
@@ -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 (_) {}
}
}
},
};
+26
View File
@@ -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"
+16
View File
@@ -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.<subdomain>.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 删除该邮件",
+105
View File
@@ -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
}
+4 -1
View File
@@ -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
+34 -2
View File
@@ -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,6 +80,17 @@ def mint_and_export(
if prefer_protocol and sso_val:
log("mint try protocol (SSO HTTP device flow)")
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,
@@ -80,10 +100,20 @@ def mint_and_export(
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: {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,
@@ -92,9 +122,10 @@ def mint_and_export(
"mint_method": "protocol",
}
log("mint fallback → browser")
break
except Exception as e: # noqa: BLE001
protocol_err = str(e)
log(f"mint protocol exception: {e}")
log(f"mint protocol exception (try {attempt}/{max_protocol_attempts}): {e}")
if protocol_only:
return {
"ok": False,
@@ -103,6 +134,7 @@ def mint_and_export(
"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:
+251 -16
View File
@@ -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,7 +656,21 @@ 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):
# 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.
@@ -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()
+57 -8
View File
@@ -57,22 +57,52 @@ def _patched_create_browser_options():
except Exception:
pass
# Prefer Google Chrome for registration reliability; Chromium fallback for extension.
browser_path = None
resolve = getattr(reg, "resolve_browser_path", None)
if callable(resolve):
try:
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",
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
):
if os.path.isfile(cand):
if os.path.isfile(cand) or os.path.islink(cand):
browser_path = cand
break
if browser_path:
try:
opts.set_browser_path(cand)
opts.set_browser_path(browser_path)
print(f"[browser] path={browser_path}", flush=True)
except Exception:
pass
break
# 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")
if os.path.isdir(ext_path):
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:
@@ -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"
+52
View File
@@ -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://<worker>.workers.dev"
echo " gmail_worker_api_key=<你的 CODE_API_KEY>"
echo " 3) 测试: python scripts/test_gmail_worker.py --to test@yourdomain.com"
+82
View File
@@ -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())
+4 -2
View File
@@ -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 {
if (navigator.permissions && navigator.permissions.query) {
var origQuery = navigator.permissions.query.bind(navigator.permissions);
navigator.permissions.query = function (params) {
if (params.name === "notifications") {
if (params && params.name === "notifications") {
return Promise.resolve({ state: Notification.permission });
}
return origQuery(params);
};
}
} catch (e) {}
// 4. 修补 plugin 数量,模拟正常浏览器
+6 -5
View File
@@ -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": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start",
"all_frames": true
"all_frames": true,
"world": "MAIN",
"match_about_blank": true
}
],
"permissions": ["activeTab"]
]
}