Gemini bussiness 自动刷新逻辑,接入 yescapture 打码服务,做到了基本 100% 的成功率
【回顾】 在之前的自动化注册或者刷新中会出现验证码发送错误的情况,原因在于获取的谷歌验证分数太低导致发送接口错误
【探索】通过抓包发现,以下接口就是发送邮件的接口
https://accountverification.business.gemini.google/_/IdentityPlatformFrontendUI/data/batchexecute?rpcids=IjXaFf&source-path=%2Fv1%2Fverify-oob-code&bl=boq_cloud-identity-identityplatform-frontend_20251210.10_p1&hl=en&rt=c
请求体为
f.req:[[["IjXaFf",,null,"generic"]]]
里面的 0cAFcWe 就是开头的验证
因此,只需要接入打码服务即可完成再次发送,测试结果为成功率基本 100%
完整 nodejs 代码如下:
const fs = require('fs');
const os = require('os');
const path = require('path');
const axios = require('axios');
const express = require('express');
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const { ImapFlow } = require('imapflow');
const { simpleParser } = require('mailparser');
puppeteer.use(StealthPlugin());
const app = express();
const port = 3000;
// =============================== // 配置项 // =============================== const WEBSITE_KEY = '6Ld8dCcrAAAAAFVbDMVZy8aNRwCjakBVaDEdRUH8';
const WEBSITE_URL = 'https://accountverification.business.gemini.google';
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
function createTempUserDataDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'pptr-profile-'));
}
/**
* 邮件获取类
*/ class GeminiFetcher {
constructor(email, clientId, refreshToken) {
this.email = email;
this.clientId = clientId;
this.refreshToken = refreshToken;
}
async getAccessToken() {
const params = new URLSearchParams({
client_id: this.clientId,
grant_type: 'refresh_token',
refresh_token: this.refreshToken
});
const res = await axios.post('https://login.microsoftonline.com/consumers/oauth2/v2.0/token', params.toString());
return res.data.access_token;
}
async tryFetchOnce() {
const token = await this.getAccessToken();
const client = new ImapFlow({
host: 'outlook.office365.com',
port: 993,
secure: true,
auth: { user: this.email, accessToken: token },
logger: false
});
await client.connect();
let code = null;
try {
for (const mailbox of ['INBOX', 'Junk']) {
let lock;
try {
lock = await client.getMailboxLock(mailbox);
const messages = await client.search({ all: true });
const lastUids = messages.slice(-5).reverse();
const tenMinutesAgo = Date.now() - 10 * 60 * 1000;
for (let uid of lastUids) {
let msg = await client.fetchOne(uid, { source: true });
let parsed = await simpleParser(msg.source);
if (parsed.date.getTime() > tenMinutesAgo &&
parsed.subject && (parsed.subject.includes('Gemini') || parsed.subject.includes('Google'))) {
const content = parsed.text + (parsed.html || "");
const match = content.match(/[A-Z0-9]{6}/);
if (match) { code = match[0]; break; }
}
}
} catch (e) { }
finally { if (lock) lock.release(); }
if (code) break;
}
} finally { await client.logout(); }
return code;
}
}
async function startPollingForCode(email, clientId, refreshToken) {
const fetcher = new GeminiFetcher(email, clientId, refreshToken);
for (let i = 1; i <= 10; i++) {
try {
console.log(`[${email}] 尝试获取邮件验证码 (${i}/10)...`);
const code = await fetcher.tryFetchOnce();
if (code) {
console.log(`[${email}] 成功获取到验证码: ${code}`);
return code;
}
} catch (err) { console.error(`[${email}] 获取验证码报错: ${err.message}`); }
await sleep(4000);
}
return null;
}
async function getCaptchaToken(apiKey) {
try {
console.log('🤖 正在向 YesCaptcha 请求人机识别 Token...');
const createResp = await axios.post('https://api.yescaptcha.com/createTask', {
clientKey: apiKey,
task: {
websiteURL: WEBSITE_URL,
websiteKey: WEBSITE_KEY,
pageAction: 'verify_oob_code',
type: 'RecaptchaV3TaskProxylessM1'
}
});
const taskId = createResp.data.taskId;
for (let i = 0; i < 20; i++) {
await sleep(3000);
const resultResp = await axios.post('https://api.yescaptcha.com/getTaskResult', {
clientKey: apiKey,
taskId: taskId
});
if (resultResp.data.status === 'ready') {
console.log('🤖 YesCaptcha Token 获取成功');
return resultResp.data.solution.gRecaptchaResponse;
}
}
} catch (error) {
console.error(`❌ YesCaptcha 错误: ${error.message}`);
return null;
}
}
function patchPayload(rawBody, newToken) {
const params = new URLSearchParams(rawBody);
let fReq = params.get('f.req');
if (!fReq) return rawBody;
const tokenRegex = /0[3c]AFc[a-zA-Z0-9_\-]{50,}/g;
if (tokenRegex.test(fReq)) {
params.set('f.req', fReq.replace(tokenRegex, newToken));
}
return params.toString();
}
// =============================== // 核心任务逻辑 // =============================== async function runLoginTask(accountData, apiKey) {
const parts = accountData.split('----');
const [email, password, clientId, refreshToken] = parts;
const userDataDir = createTempUserDataDir();
console.log(`🚀 [任务开始] 账号: ${email}`);
let resolveCodeSent;
const codeSentBarrier = new Promise((resolve) => { resolveCodeSent = resolve; });
let resolveAuthBarrier;
const authBarrier = new Promise((resolve) => { resolveAuthBarrier = resolve; });
console.log('🌐 正在启动浏览器...');
const browser = await puppeteer.launch({
headless: "new",
userDataDir,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--window-size=1280,800']
});
try {
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', req => {
if (['image', 'media', 'font'].includes(req.resourceType())) return req.abort().catch(() => {});
req.continue().catch(() => {});
});
page.on('response', async res => {
const url = res.url();
if (url.includes('batchexecute')) {
try {
const text = await res.text();
// 监控验证码发送状态 if (text.includes('LookupVerifiedEmail') || text.includes('SendVerificationCode')) {
console.log('✅ [网络监控] 监听到 SendVerificationCode 响应,服务器已发信!');
resolveCodeSent();
}
// 监控验证码报错 if (text.includes('CAPTCHA_CHECK_FAILED')) {
console.log('⚠️ [网络监控] 检测到人机验证码拦截,准备打码补丁...');
const newToken = await getCaptchaToken(apiKey);
if (newToken) {
const originalRequest = res.request();
const newPostData = patchPayload(originalRequest.postData(), newToken);
console.log('🔄 [补丁] 正在重发带 Token 的 Payload...');
await page.evaluate(async (u, h, b) => {
await fetch(u, { method: 'POST', headers: h, body: b });
}, url, originalRequest.headers(), newPostData);
resolveAuthBarrier();
}
} else if (text.includes('inner_api_status')) {
resolveAuthBarrier();
}
} catch (e) { }
}
});
console.log('🍪 正在设置初始 XSRF Cookie...');
await page.goto('https://auth.business.gemini.google/', { waitUntil: 'domcontentloaded' });
await page.setCookie(
{ name: '__Host-AP_SignInXsrf', value: 'KdLRzKwwBTD5wo8nUollAbY6cW0', domain: 'auth.business.gemini.google', path: '/', secure: true },
{ name: '_GRECAPTCHA', value: '09ABCL...', domain: '.google.com', path: '/', secure: true }
);
const targetUrl = `https://auth.business.gemini.google/login/email?continueUrl=https%3A%2F%2Fbusiness.gemini.google%2F&loginHint=${encodeURIComponent(email)}&xsrfToken=KdLRzKwwBTD5wo8nUollAbY6cW0`;
console.log('🔗 正在加载登录页面...');
await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
console.log('🖱️ 正在点击 [通过电子邮件发送验证码] 按钮...');
await page.waitForSelector('#sign-in-with-email', { visible: true });
await page.click('#sign-in-with-email');
console.log('🔎 等待验证码输入框出现...');
const inputSelector = 'input[jsname="ovqh0b"]';
await page.waitForSelector(inputSelector, { timeout: 30000 });
console.log('⏳ [关键等待] 正在等待发信确认信号 (由网络响应触发)...');
const waitStart = Date.now();
await Promise.race([codeSentBarrier, sleep(12000)]);
console.log(`⏱️ 等待结束,耗时: ${Date.now() - waitStart}ms`);
console.log('📫 开始爬取邮箱验证码...');
const code = await startPollingForCode(email, clientId, refreshToken);
if (!code) throw new Error("获取验证码超时");
console.log('⌨️ 正在输入验证码并提交...');
await page.type(inputSelector, code, { delay: 100 });
await page.click('button[jsname="XooR8e"]');
console.log('🔄 等待登录跳转...');
await sleep(4000);
if (page.url().includes('/admin/create')) {
console.log('📄 进入创建页面,检测是否有 [同意] 按钮...');
const btn = await page.waitForSelector('button.agree-button', { visible: true, timeout: 5000 }).catch(() => null);
if (btn) {
console.log('🖱️ 点击同意按钮');
await btn.click();
await sleep(2000);
}
}
console.log('🍪 正在通过 CDP 协议提取全量 Cookie...');
let hostCoses = '', secureCSes = '';
for (let i = 0; i < 15; i++) {
const client = await page.target().createCDPSession();
const { cookies } = await client.send('Network.getAllCookies');
await client.detach();
for (const c of cookies) {
if (c.name === '__Host-C_OSES') hostCoses = c.value;
if (c.name === '__Secure-C_SES') secureCSes = c.value;
}
if (hostCoses && secureCSes) {
console.log('✨ 核心 Cookie 提取完毕');
break;
}
console.log(`📭 第 ${i+1} 次提取 Cookie 未果,继续轮询...`);
await sleep(1000);
if (i === 5) {
console.log('🔄 提取超时,尝试跳转业务主域名促活 Cookie...');
await page.goto('https://business.gemini.google/', { waitUntil: 'networkidle2' }).catch(() => {});
}
}
if (!hostCoses || !secureCSes) throw new Error('Cookie 提取失败');
console.log('🏁 [任务成功] 正在返回结果');
return `${email}|${password}|${hostCoses.trim()}::${secureCSes.trim()}`;
} finally {
console.log('🧹 正在关闭浏览器并清理临时文件...');
await browser.close();
try { fs.rmSync(userDataDir, { recursive: true, force: true }); } catch (e) {}
}
}
app.get('/login', async (req, res) => {
const { data, apikey } = req.query;
if (!data || !apikey) return res.status(400).send('Missing params');
try {
const result = await runLoginTask(data, apikey);
res.send(result);
} catch (err) {
console.error(`❌ 接口报错: ${err.message}`);
res.status(500).send(`Error: ${err.message}`);
}
});
app.listen(port, () => {
console.log();
console.log(`🚀 服务运行中: http://localhost:${port}`);
console.log(`📝 请求示例: /login?data=email----pass----id----token&apikey=xxx`);
console.log();
});
运行成功后请求示例为:http://localhost:3000/login?data=CharlesHoward4117@outlook.com----rrom1652910----dbc8e03a-b00c-46bd-ae65-b683e7707cb0----M.C539_BL2.0.U.-Cn78r6PhJ0VI48KtRppemAxUI038pVKjd9Kyu*lxTsFW9cyZqou7zcWsKmg0DdlxCURrppr3paaCrZHMIY1gzBcD4p*p*TQHz4DqDF0xeUTOtkTlpy!B6leXglSy3O1Uj9hDUkq3f!75ha4A0VrC7agSXDzUaV!tPIJ7AA6fgdKeehLjzhNaHr5XqvTM39Gr1OR9H8VnYBB!K6yGDAhWWsYd5blEfhqVoAzG3TBDTcdrho3fwU2uR8V8oVtJfQ*aGyw9XwCj*gIRq1SdfWR5h!PSZ4TuFSJVqeLB7qbIOHLfWPtVkz*ya6jxMX4DqJASLQIIILno1lJznm1VAeQJPze!REpnD9L9F9t!dwRwpmd1235SbqnYAEG5rFXKSOqikRjSYSWeCVmZYBkWSWBLEuQ$&apikey=c169a2dbd972acac69b4f30c52c060bfbbe0854f84831
其中 data 就是邮箱的基本信息,这里只适配了微软邮箱apikey 就是 yes 的 apikey
默认如果浏览器可以成功发送则不调用 yes 打码服务,返回示例
CharlesHoward4117@outlook.com|rrom1652910|COS.AW82PoFnmTPemeKaOLiqi59wSPBs5Q6mSkgqGsk2OwOP0c_vBgAo4L3k8IdN-La_tMrJbjbatQy9Uw6QllD2VL0xdhdbG8Ks2tlCGGEfxZgj3FhTP2fNSeEfk3ed1PL_l6oTfMiLL5ivKwKCYE5sbuI::CSE.AdwtfTBBmOWOWWkad3G32S_ut5NlmYWQhJCRohzHwevu9ZpYyKrRHpviThBTJNh8_KdlBLAUbSQT5zD6RGoggxO8air-J0l9qO_XtmbdLhXYDOCmMdNgDFq40G1HmpgeGfUtXEVB-cT2lf33J9xSx_F0w9ZmWrrI96KJk8ijQ_qc5T5nt3grRfI5B9YpHHOww_Uqo14z0Yg1899rHDdNpgg9y_X9xK3nDn8C3lYKerslcKak12VC_Rxq9JJDYNsysUjhfNmyt-tyU9xLaJV_vR_FG7nnkO7bmRL664yEpAzRD0dB9ZnxXrCb45q1Q7HRs3jjxoId1DVYw2aSbU5URDDY-q5bCDbHModdg36rRuICHmxQQRsz39YivNknIDe7kvOnubDYU1M9wmhX7bsLkKawILuj0jwsqNRb0jh-phY3D8DZhydEHROjjba99MAsIvkH8ZytY_x3Aw
COS 与 CSE 的 cookie 都有返回,一个账户大概 30s 左右,可并发,一分钟基本可以刷新或者注册 40 个账户(并发 20)
欢迎更多开发者去完善自动化注册和刷新的逻辑…
评论区(暂无评论)