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:
1 parent
187792d6be
commit
55f56d27c9
19 files changed
+1225
-85
No files matched your search
@@ -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
|
||||
@@ -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
@@ -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/
|
||||
@@ -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. 复制模板并编辑(模板内 `"//…"` 键是注释,加载时忽略):
|
||||
|
||||
+29
-1
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "grok-xai-mail-codes",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"dev": "wrangler dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.99.0"
|
||||
}
|
||||
}
|
||||
@@ -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 (_) {}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+64
-32
@@ -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:
|
||||
|
||||
+252
-17
@@ -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()
|
||||
|
||||
+68
-19
@@ -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"
|
||||
|
||||
Executable
+52
@@ -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"
|
||||
Executable
+82
@@ -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())
|
||||
@@ -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 数量,模拟正常浏览器
|
||||
|
||||
@@ -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"]
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user