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「应用专用密码」
|
# 配合 Cloudflare catch-all 域名使用;密码填 Gmail「应用专用密码」
|
||||||
GMAIL_USER=
|
GMAIL_USER=
|
||||||
GMAIL_PASSWORD=
|
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 会回退读取)
|
# 兼容别名(gmail_code_listener 会回退读取)
|
||||||
# MAIL_USER=
|
# MAIL_USER=
|
||||||
# MAIL_PASSWORD=
|
# MAIL_PASSWORD=
|
||||||
|
|||||||
+5
-2
@@ -6,10 +6,10 @@ __pycache__/
|
|||||||
dist/
|
dist/
|
||||||
|
|
||||||
# secrets / local runtime
|
# secrets / local runtime
|
||||||
config.json
|
# config.json # private repo: tracked intentionally
|
||||||
config.local.json
|
config.local.json
|
||||||
config.json.bak*
|
config.json.bak*
|
||||||
.env
|
# .env # private repo: tracked intentionally
|
||||||
accounts_*.txt
|
accounts_*.txt
|
||||||
emails_used.txt
|
emails_used.txt
|
||||||
emails_error.txt
|
emails_error.txt
|
||||||
@@ -21,3 +21,6 @@ logs/
|
|||||||
*.log
|
*.log
|
||||||
*.bak
|
*.bak
|
||||||
*.pid
|
*.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. 复制模板并编辑(模板内 `"//…"` 键是注释,加载时忽略):
|
1. 复制模板并编辑(模板内 `"//…"` 键是注释,加载时忽略):
|
||||||
|
|||||||
+29
-1
@@ -182,13 +182,41 @@ def save_account(email: str, password: str, sso: str = ""):
|
|||||||
f.write(f"{email}----{password}----{sso}\n")
|
f.write(f"{email}----{password}----{sso}\n")
|
||||||
print(f"[+] Saved: {email}")
|
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:
|
def create_browser_options(headless: bool = False, proxy: str = "") -> ChromiumOptions:
|
||||||
opts = ChromiumOptions()
|
opts = ChromiumOptions()
|
||||||
opts.auto_port()
|
opts.auto_port()
|
||||||
for flag in CHROMIUM_SLIM_FLAGS:
|
for flag in CHROMIUM_SLIM_FLAGS:
|
||||||
opts.set_argument(flag)
|
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):
|
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:
|
if headless:
|
||||||
opts.headless()
|
opts.headless()
|
||||||
if proxy:
|
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_password": "",
|
||||||
"gmail_imap_host": "imap.gmail.com",
|
"gmail_imap_host": "imap.gmail.com",
|
||||||
"gmail_imap_port": 993,
|
"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": "Gmail 取到验证码后是否 IMAP 删除该邮件(失败不影响注册)",
|
||||||
"gmail_delete_after_code": true,
|
"gmail_delete_after_code": true,
|
||||||
"// hotmail_delete_after_code": "Hotmail/Outlook 取到验证码后是否 IMAP 删除该邮件",
|
"// 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
|
pass
|
||||||
log(f"headed browser DISPLAY={os.environ.get('DISPLAY', '')!r}")
|
log(f"headed browser DISPLAY={os.environ.get('DISPLAY', '')!r}")
|
||||||
|
|
||||||
|
# Prefer Chromium: Google Chrome blocks --load-extension (turnstilePatch).
|
||||||
for cand in (
|
for cand in (
|
||||||
|
"/snap/bin/chromium",
|
||||||
"/usr/bin/chromium",
|
"/usr/bin/chromium",
|
||||||
"/usr/bin/chromium-browser",
|
"/usr/bin/chromium-browser",
|
||||||
"/usr/bin/google-chrome",
|
"/usr/bin/google-chrome",
|
||||||
"/usr/bin/google-chrome-stable",
|
"/usr/bin/google-chrome-stable",
|
||||||
):
|
):
|
||||||
if os.path.isfile(cand):
|
if os.path.isfile(cand) or os.path.islink(cand):
|
||||||
try:
|
try:
|
||||||
opts.set_browser_path(cand)
|
opts.set_browser_path(cand)
|
||||||
|
log(f"browser path={cand}")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
break
|
break
|
||||||
|
|||||||
+64
-32
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
@@ -14,6 +17,11 @@ from .writer import write_cpa_xai_auth
|
|||||||
|
|
||||||
LogFn = Callable[[str], None]
|
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:
|
def _noop(_: str) -> None:
|
||||||
return None
|
return None
|
||||||
@@ -51,6 +59,7 @@ def mint_and_export(
|
|||||||
Returns dict with keys: ok, path, email, probe, error?, mint_method?
|
Returns dict with keys: ok, path, email, probe, error?, mint_method?
|
||||||
"""
|
"""
|
||||||
log = log or _noop
|
log = log or _noop
|
||||||
|
global _PROTOCOL_LAST_OK_AT
|
||||||
email = (email or "").strip()
|
email = (email or "").strip()
|
||||||
if not email or not password:
|
if not email or not password:
|
||||||
# Protocol can work with sso alone; password only required for browser fallback
|
# 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:
|
if prefer_protocol and sso_val:
|
||||||
log("mint try protocol (SSO HTTP device flow)")
|
log("mint try protocol (SSO HTTP device flow)")
|
||||||
try:
|
max_protocol_attempts = 3
|
||||||
tokens = mint_with_sso_protocol(
|
for attempt in range(1, max_protocol_attempts + 1):
|
||||||
sso_cookie=sso_val,
|
if cancel and cancel():
|
||||||
email=email,
|
return {"ok": False, "email": email, "error": "cancelled", "mint_method": "protocol"}
|
||||||
proxy=resolved or None,
|
# Throttle protocol calls across mint workers
|
||||||
poll_timeout_sec=protocol_poll_timeout_sec,
|
with _PROTOCOL_MINT_LOCK:
|
||||||
log=log,
|
gap = time.time() - _PROTOCOL_LAST_OK_AT
|
||||||
cancel=cancel,
|
if _PROTOCOL_LAST_OK_AT > 0 and gap < _PROTOCOL_MIN_INTERVAL_SEC:
|
||||||
)
|
wait = _PROTOCOL_MIN_INTERVAL_SEC - gap
|
||||||
log("mint protocol SUCCESS")
|
log(f"protocol throttle sleep {wait:.1f}s")
|
||||||
except ProtocolMintError as e:
|
time.sleep(wait)
|
||||||
protocol_err = str(e)
|
try:
|
||||||
log(f"mint protocol failed: {e}")
|
tokens = mint_with_sso_protocol(
|
||||||
if protocol_only:
|
sso_cookie=sso_val,
|
||||||
return {
|
email=email,
|
||||||
"ok": False,
|
proxy=resolved or None,
|
||||||
"email": email,
|
poll_timeout_sec=protocol_poll_timeout_sec,
|
||||||
"error": f"protocol_only: {e}",
|
log=log,
|
||||||
"mint_method": "protocol",
|
cancel=cancel,
|
||||||
}
|
)
|
||||||
log("mint fallback → browser")
|
with _PROTOCOL_MINT_LOCK:
|
||||||
except Exception as e: # noqa: BLE001
|
_PROTOCOL_LAST_OK_AT = time.time()
|
||||||
protocol_err = str(e)
|
log("mint protocol SUCCESS")
|
||||||
log(f"mint protocol exception: {e}")
|
protocol_err = None
|
||||||
if protocol_only:
|
break
|
||||||
return {
|
except ProtocolMintError as e:
|
||||||
"ok": False,
|
protocol_err = str(e)
|
||||||
"email": email,
|
log(f"mint protocol failed (try {attempt}/{max_protocol_attempts}): {e}")
|
||||||
"error": f"protocol_only: {e}",
|
rate_limited = "rate_limited" in protocol_err.lower()
|
||||||
"mint_method": "protocol",
|
if rate_limited and attempt < max_protocol_attempts:
|
||||||
}
|
backoff = 5.0 * attempt
|
||||||
log("mint fallback → browser")
|
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:
|
elif prefer_protocol and not sso_val:
|
||||||
log("mint protocol skipped (no sso cookie) → browser")
|
log("mint protocol skipped (no sso cookie) → browser")
|
||||||
if protocol_only:
|
if protocol_only:
|
||||||
|
|||||||
+252
-17
@@ -621,6 +621,34 @@ CHROMIUM_SLIM_FLAGS = [
|
|||||||
"--no-first-run",
|
"--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():
|
def create_browser_options():
|
||||||
options = ChromiumOptions()
|
options = ChromiumOptions()
|
||||||
@@ -628,8 +656,22 @@ def create_browser_options():
|
|||||||
options.set_timeouts(base=1)
|
options.set_timeouts(base=1)
|
||||||
for flag in CHROMIUM_SLIM_FLAGS:
|
for flag in CHROMIUM_SLIM_FLAGS:
|
||||||
options.set_argument(flag)
|
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):
|
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
|
# Apply config.json "proxy" to Chromium. Without this, only HTTP helpers
|
||||||
# used get_proxies(); the browser itself fell through to system/env proxy.
|
# used get_proxies(); the browser itself fell through to system/env proxy.
|
||||||
proxy = (config.get("proxy") or "").strip()
|
proxy = (config.get("proxy") or "").strip()
|
||||||
@@ -1833,6 +1875,112 @@ def get_email_provider():
|
|||||||
|
|
||||||
# ──────────────────────── Gmail + Cloudflare catch-all ────────────────────────
|
# ──────────────────────── 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():
|
def gmail_get_email_and_token():
|
||||||
raw = str(config.get("defaultDomains", "") or "")
|
raw = str(config.get("defaultDomains", "") or "")
|
||||||
domains = [x.strip() for x in re.split(r"[,,\s]+", raw) if x.strip()]
|
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,
|
cancel_callback=None,
|
||||||
resend_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".
|
Hybrid path (recommended):
|
||||||
We fetch HEADER only first (no full RFC822 body) and extract from Subject/To.
|
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:
|
if poll_interval is None:
|
||||||
# Gmail catch-all benefits from denser polling; default 1s unless config forces higher.
|
# Gmail catch-all benefits from denser polling; default 1s unless config forces higher.
|
||||||
poll_interval = float(config.get("mail_poll_interval", 1) or 1)
|
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_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()
|
imap_password = str(config.get("gmail_imap_password", "") or os.getenv("GMAIL_PASSWORD", "") or "").strip()
|
||||||
if not imap_user or not imap_password:
|
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")
|
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_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)
|
imap_port = int(config.get("gmail_imap_port", 993) or 993)
|
||||||
delete_after = _config_bool(config.get("gmail_delete_after_code", True), default=True)
|
delete_after = _config_bool(config.get("gmail_delete_after_code", True), default=True)
|
||||||
deadline = time.time() + timeout
|
deadline = time.time() + timeout
|
||||||
# Resend earlier: first mail often arrives in 5-40s; resend at 45s as safety net.
|
# Resend: default 45s for pure IMAP; after worker-miss fallback use sooner resend.
|
||||||
next_resend_at = time.time() + float(config.get("gmail_resend_after_sec", 45) or 45)
|
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
|
imap = None
|
||||||
target_lower = email.lower().strip()
|
target_lower = email.lower().strip()
|
||||||
seen_ids: set[bytes] = set()
|
seen_ids: set[bytes] = set()
|
||||||
@@ -2965,7 +3170,11 @@ return 'clicked';
|
|||||||
raise Exception("验证码已获取,但自动填写/提交失败")
|
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()
|
page = _get_page()
|
||||||
if page is None:
|
if page is None:
|
||||||
raise Exception("页面未就绪,无法执行 Turnstile")
|
raise Exception("页面未就绪,无法执行 Turnstile")
|
||||||
@@ -2977,7 +3186,8 @@ def getTurnstileToken(log_callback=None, cancel_callback=None):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for _ in range(0, 20):
|
rounds = max(2, int(max_rounds or 4))
|
||||||
|
for _ in range(0, rounds):
|
||||||
raise_if_cancelled(cancel_callback)
|
raise_if_cancelled(cancel_callback)
|
||||||
try:
|
try:
|
||||||
token = page.run_js(
|
token = page.run_js(
|
||||||
@@ -3040,7 +3250,7 @@ if (nodes.length && typeof nodes[0].click === 'function') nodes[0].click();
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
human_sleep(1, cancel_callback)
|
human_sleep(0.7, cancel_callback)
|
||||||
|
|
||||||
raise Exception("Turnstile 获取 token 失败")
|
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
|
wait_cf_since = None
|
||||||
last_cf_retry_at = 0.0
|
last_cf_retry_at = 0.0
|
||||||
last_cf_log_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:
|
while time.time() < deadline:
|
||||||
raise_if_cancelled(cancel_callback)
|
raise_if_cancelled(cancel_callback)
|
||||||
@@ -3177,12 +3404,13 @@ return 'filled-no-submit';
|
|||||||
log_callback(f"[*] 资料已填写,等待 Cloudflare... token长度={token_len} waited={waited:.0f}s")
|
log_callback(f"[*] 资料已填写,等待 Cloudflare... token长度={token_len} waited={waited:.0f}s")
|
||||||
last_cf_log_at = now
|
last_cf_log_at = now
|
||||||
# 卡住后自动二次复用 Turnstile 组件
|
# 卡住后自动二次复用 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:
|
if log_callback:
|
||||||
log_callback("[*] Cloudflare 验证卡住,开始二次复用 Turnstile...")
|
log_callback("[*] Cloudflare 验证卡住,开始二次复用 Turnstile...")
|
||||||
try:
|
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:
|
if token:
|
||||||
|
cf_token_fail_streak = 0
|
||||||
synced = page.run_js(
|
synced = page.run_js(
|
||||||
"""
|
"""
|
||||||
const token = String(arguments[0] || '').trim();
|
const token = String(arguments[0] || '').trim();
|
||||||
@@ -3200,8 +3428,11 @@ return String(cfInput.value || '').trim().length;
|
|||||||
if log_callback:
|
if log_callback:
|
||||||
log_callback(f"[*] Turnstile 二次复用完成,回填长度={synced}")
|
log_callback(f"[*] Turnstile 二次复用完成,回填长度={synced}")
|
||||||
except Exception as cf_exc:
|
except Exception as cf_exc:
|
||||||
|
cf_token_fail_streak += 1
|
||||||
if log_callback:
|
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
|
last_cf_retry_at = now
|
||||||
human_sleep(0.8, cancel_callback)
|
human_sleep(0.8, cancel_callback)
|
||||||
continue
|
continue
|
||||||
@@ -3258,12 +3489,13 @@ return 'submitted';
|
|||||||
waited = now - wait_cf_since if wait_cf_since else 0
|
waited = now - wait_cf_since if wait_cf_since else 0
|
||||||
log_callback(f"[*] 等待 Cloudflare 后再提交... token长度={token_len} waited={waited:.0f}s")
|
log_callback(f"[*] 等待 Cloudflare 后再提交... token长度={token_len} waited={waited:.0f}s")
|
||||||
last_cf_log_at = now
|
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:
|
if log_callback:
|
||||||
log_callback("[*] 提交前仍卡住,自动再次复用 Turnstile...")
|
log_callback("[*] 提交前仍卡住,自动再次复用 Turnstile...")
|
||||||
try:
|
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:
|
if token:
|
||||||
|
cf_token_fail_streak = 0
|
||||||
synced = page.run_js(
|
synced = page.run_js(
|
||||||
"""
|
"""
|
||||||
const token = String(arguments[0] || '').trim();
|
const token = String(arguments[0] || '').trim();
|
||||||
@@ -3281,8 +3513,11 @@ return String(cfInput.value || '').trim().length;
|
|||||||
if log_callback:
|
if log_callback:
|
||||||
log_callback(f"[*] Turnstile 二次复用完成,回填长度={synced}")
|
log_callback(f"[*] Turnstile 二次复用完成,回填长度={synced}")
|
||||||
except Exception as cf_exc:
|
except Exception as cf_exc:
|
||||||
|
cf_token_fail_streak += 1
|
||||||
if log_callback:
|
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
|
last_cf_retry_at = now
|
||||||
human_sleep(0.8, cancel_callback)
|
human_sleep(0.8, cancel_callback)
|
||||||
continue
|
continue
|
||||||
@@ -3525,9 +3760,9 @@ return String(cfInput.value || '').trim().length;
|
|||||||
if name:
|
if name:
|
||||||
last_seen_names.add(name)
|
last_seen_names.add(name)
|
||||||
|
|
||||||
if name == "sso" and value:
|
if name in ("sso", "sso-rw") and value:
|
||||||
if log_callback:
|
if log_callback:
|
||||||
log_callback("[*] 已获取到 sso cookie")
|
log_callback(f"[*] 已获取到 {name} cookie")
|
||||||
return value
|
return value
|
||||||
except PageDisconnectedError:
|
except PageDisconnectedError:
|
||||||
refresh_active_page()
|
refresh_active_page()
|
||||||
|
|||||||
+68
-19
@@ -57,26 +57,56 @@ def _patched_create_browser_options():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for cand in (
|
# Prefer Google Chrome for registration reliability; Chromium fallback for extension.
|
||||||
"/snap/bin/chromium",
|
browser_path = None
|
||||||
"/usr/bin/chromium",
|
resolve = getattr(reg, "resolve_browser_path", None)
|
||||||
"/usr/bin/chromium-browser",
|
if callable(resolve):
|
||||||
"/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):
|
|
||||||
try:
|
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:
|
except Exception:
|
||||||
pass
|
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
|
return opts
|
||||||
|
|
||||||
|
|
||||||
@@ -272,14 +302,33 @@ def register_one(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
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. 填写资料")
|
log(worker_id, "4. 填写资料")
|
||||||
profile = reg.fill_profile_and_submit(
|
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, f"资料已填: {profile.get('given_name')} {profile.get('family_name')}")
|
||||||
log(worker_id, "5. 等待 sso cookie")
|
log(worker_id, "5. 等待 sso cookie")
|
||||||
sso = reg.wait_for_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 ""
|
password = profile.get("password", "") or ""
|
||||||
line = f"{email}----{password}----{sso}\n"
|
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 验证
|
// Turnstile Patch - 隐藏自动化标识,加速 Turnstile 验证
|
||||||
// 在 document_start 阶段执行,确保在页面脚本之前生效
|
// MV3 MAIN world + document_start:在页面脚本之前修补真实页面上下文
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
@@ -25,13 +25,15 @@
|
|||||||
|
|
||||||
// 3. 覆盖 permissions.query,隐藏 notifications 权限异常
|
// 3. 覆盖 permissions.query,隐藏 notifications 权限异常
|
||||||
try {
|
try {
|
||||||
var origQuery = navigator.permissions.query.bind(navigator.permissions);
|
if (navigator.permissions && navigator.permissions.query) {
|
||||||
navigator.permissions.query = function (params) {
|
var origQuery = navigator.permissions.query.bind(navigator.permissions);
|
||||||
if (params.name === "notifications") {
|
navigator.permissions.query = function (params) {
|
||||||
return Promise.resolve({ state: Notification.permission });
|
if (params && params.name === "notifications") {
|
||||||
}
|
return Promise.resolve({ state: Notification.permission });
|
||||||
return origQuery(params);
|
}
|
||||||
};
|
return origQuery(params);
|
||||||
|
};
|
||||||
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
// 4. 修补 plugin 数量,模拟正常浏览器
|
// 4. 修补 plugin 数量,模拟正常浏览器
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 3,
|
||||||
"name": "Turnstile Patch",
|
"name": "Turnstile Patch",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "Patch browser automation detection for Turnstile",
|
"description": "Patch browser automation detection for Turnstile",
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["<all_urls>"],
|
"matches": ["<all_urls>"],
|
||||||
"js": ["content.js"],
|
"js": ["content.js"],
|
||||||
"run_at": "document_start",
|
"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