suno-api/test.ts
2025-09-09 21:50:33 +08:00

333 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { chromium } from 'playwright-extra'; // Import from playwright-extra
import StealthPlugin from 'puppeteer-extra-plugin-stealth'
import yn from 'yn';
import UserAgent from 'user-agents';
// 把字符串转为对象数组
function parseCookies(cookieString:string, domain:string) {
return cookieString.split(';').map(c => {
const [name, ...rest] = c.trim().split('=');
const lax: 'Lax' | 'Strict' | 'None' = 'Lax';
return {
name,
value: rest.join('='), // 防止 value 里有 "=" 的情况
domain, // 必须指定 domain
path: '/', // 一般都是根路径
httpOnly: false,
secure: true,
sameSite: lax,
};
});
}
const API_KEY = "475f30640c5860c432064a2c37f06fd6"; // 你的 2Captcha key
// 等待 Turnstile 渲染出来(最多 60 秒)
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
async function waitForSiteKey(page, { timeout = 90000, pollInterval = 500 } = {}) {
const start = Date.now();
while (Date.now() - start < timeout) {
// 1) 快速在主文档中寻找data-sitekey、iframe src query、script 文本)
const mainCheck = await page.evaluate(() => {
const attrNames = ['data-sitekey','data-key','data-cf-turnstile-sitekey','data-hcaptcha-sitekey','data-captcha'];
for (const a of attrNames) {
const el = document.querySelector(`[${a}]`);
if (el) return { sitekey: el.getAttribute(a), source: 'dom', attr: a };
}
// 收集 iframe src/srcdoc只返回字符串不触碰 frame 内部以避免跨域问题)
const iframes = Array.from(document.querySelectorAll('iframe')).map(f => ({ src: f.src || f.getAttribute('src') || '', srcdoc: f.srcdoc || '' }));
for (const f of iframes) {
if (f.src) {
try {
const url = new URL(f.src, location.href);
const qp = Object.fromEntries(url.searchParams.entries());
const possible = qp.sitekey || qp.k || qp['data-sitekey'] || qp.s || qp.key;
if (possible) return { sitekey: possible, source: 'iframe-src', iframeSrc: f.src };
} catch(e){}
}
if (f.srcdoc && f.srcdoc.includes('sitekey')) {
const m = f.srcdoc.match(/sitekey['"]?\s*[:=]\s*['"]([\w\-]{8,})['"]/i) || f.srcdoc.match(/k=([A-Za-z0-9_-]{8,})/i);
if (m) return { sitekey: m[1], source: 'iframe-srcdoc' };
}
}
// 在内联 script 中查找 sitekey (有些站点把 sitekey 写在脚本里)
for (const s of Array.from(document.scripts)) {
const t = s.textContent || '';
if (!t) continue;
const m = t.match(/sitekey['"]?\s*[:=]\s*['"]([\w\-]{8,})['"]?/i) || t.match(/k=([A-Za-z0-9_-]{8,})/i);
if (m) return { sitekey: m[1], source: 'script' };
}
// window 变量(有些实现会在 window 上挂载)
try {
const candidates = ['turnstile','__turnstile','hcaptcha','__hcaptcha'];
for (const k of candidates) {
// 访问 window[k] 可能为 undefined 或对象
if (window[k] && window[k].sitekey) return { sitekey: window[k].sitekey, source: 'window.' + k };
}
} catch(e){}
return null;
});
// 判断页面是否已经进入登录/应用界面
const isLoginPage = await page.$('input[type="email"], input[name="username"], button:has-text("Sign in"), button:has-text("Log in")');
if (isLoginPage) {
// console.log('已经是登录界面,不需要验证码');
return null;
}
if (mainCheck && mainCheck.sitekey) return mainCheck;
// 2) 逐 frame 检查(对于可访问的 frame 直接 evaluate对于跨域解析 frame.url
const frames = page.frames();
for (const f of frames) {
try {
// 可能跨域evaluate 会抛错,如果可访问就能直接从 frame DOM 找到
const res = await f.evaluate(() => {
const attrNames = ['data-sitekey','data-key','data-cf-turnstile-sitekey','data-hcaptcha-sitekey'];
for (const a of attrNames) {
const el = document.querySelector(`[${a}]`);
if (el) return { sitekey: el.getAttribute(a), source: 'frame-dom', attr: a };
}
for (const s of Array.from(document.scripts)) {
const t = s.textContent || '';
if (!t) continue;
const m = t.match(/sitekey['"]?\s*[:=]\s*['"]([\w\-]{8,})['"]/i) || t.match(/k=([A-Za-z0-9_-]{8,})/i);
if (m) return { sitekey: m[1], source: 'frame-script' };
}
// 也检查 meta 等可见位置(可扩展)
return null;
});
if (res && res.sitekey) return { ...res, frameUrl: f.url() };
} catch (err) {
// 跨域 frame不能 evaluate改为解析 frame.url常见 sitekey 在 query中
try {
const fu = f.url();
if (fu && (fu.includes('turnstile') || fu.includes('hcaptcha') || fu.includes('challenges.cloudflare.com') || fu.includes('hcaptcha.com'))) {
const u = new URL(fu);
const qp = u.searchParams;
const possible = qp.get('sitekey') || qp.get('k') || qp.get('s') || qp.get('key');
if (possible) return { sitekey: possible, source: 'frame-url', frameUrl: fu };
}
} catch(e){}
}
}
// 3) 如果 Cloudflare 仍在“Checking your browser”继续等
const checking = await page.$('text="Checking your browser"') || await page.$('text=Checking') || null;
if (checking) {
// 仅作日志,继续等待
// console.log('Cloudflare still checking your browser...');
}
await sleep(pollInterval);
}
return null; // 超时
}
async function create2captchaTurnstileTask(apiKey, websiteURL, websiteKey) {
const createRes = await fetch('https://api.2captcha.com/createTask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientKey: apiKey,
task: {
type: 'TurnstileTaskProxyless',
websiteURL,
websiteKey
}
})
});
const json = await createRes.json();
if (json.errorId && json.errorId !== 0) throw new Error('createTask error: ' + JSON.stringify(json));
return json.taskId;
}
async function get2captchaResult(apiKey, taskId, { timeout = 120000, pollInterval = 5000 } = {}) {
const start = Date.now();
while (Date.now() - start < timeout) {
await sleep(pollInterval);
const res = await fetch('https://api.2captcha.com/getTaskResult', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clientKey: apiKey, taskId })
});
const json = await res.json();
if (json.errorId && json.errorId !== 0) throw new Error('getTaskResult error: ' + JSON.stringify(json));
if (json.status === 'ready' && json.solution && json.solution.token) return json.solution.token;
// else keep polling
}
throw new Error('2Captcha getTaskResult timeout');
}
async function injectTurnstileToken(page, token) {
// 多种注入方式以提高兼容性
await page.evaluate((t) => {
// 常见隐藏字段
const selectors = [
'textarea[name="cf-turnstile-response"]',
'input[name="cf-turnstile-response"]',
'textarea[name="cf_captcha_token"]',
'input[name="cf_captcha_token"]'
];
let injected = false;
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) {
el.value = t;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
injected = true;
}
}
// 如果没有直接字段,寻找任意 textarea 并设置(降级方案)
if (!injected) {
const ta = document.querySelector('textarea');
if (ta) {
ta.value = t;
ta.dispatchEvent(new Event('input', { bubbles: true }));
injected = true;
}
}
// 调用可能挂载的回调site 端可能传 callback 名称)
try {
if (window.turnstile && typeof window.turnstile.renderResponse === 'function') {
window.turnstile.renderResponse(t);
}
if (window.tsCallback && typeof window.tsCallback === 'function') {
window.tsCallback(t);
}
// 查找 data-callback 属性并触发
document.querySelectorAll('[data-sitekey]').forEach(el => {
const cb = el.getAttribute('data-callback');
if (cb && window[cb] && typeof window[cb] === 'function') {
try { window[cb](t); } catch(e) {}
}
});
} catch(e){}
}, token);
}
async function solveTurnstile(page, apiKey=API_KEY) {
const found = await waitForSiteKey(page, { timeout: 90000, pollInterval: 700 });
if (!found) {
console.warn('⚠️ 超时未检测到 Turnstile sitekey建议开启调试快照screenshot / html进行排查');
// 调试输出(保存快照/HTML
try {
await page.screenshot({ path: 'turnstile-debug.png', fullPage: true });
const html = await page.content();
require('fs').writeFileSync('turnstile-debug.html', html);
console.log('已保存 turnstile-debug.png 和 turnstile-debug.html 用于排查(当前目录)');
} catch(e) { console.error('保存调试文件失败', e); }
return false;
}
console.log('找到 sitekey ->', found);
// 调用 2Captcha
const pageUrl = page.url();
const taskId = await create2captchaTurnstileTask(apiKey, pageUrl, found.sitekey);
console.log('2Captcha createTask 返回 taskId:', taskId);
const token = await get2captchaResult(apiKey, taskId, { timeout: 180000, pollInterval: 5000 });
console.log('2Captcha 返回 token (长度):', token?.length);
await injectTurnstileToken(page, token);
console.log('token 注入完成,等待站点验证或跳转');
// 站点通常会在 token 注入后提交表单或自动验证,给点时间
await page.waitForTimeout(2000);
return true;
}
const test = async () => {
const args = [
'--disable-blink-features=AutomationControlled',
'--disable-web-security',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-features=site-per-process',
'--disable-features=IsolateOrigins',
'--disable-extensions',
'--disable-infobars'
];
chromium.use(StealthPlugin())
// const browser = await this.getBrowserType().launch({
// args,
// headless: yn(process.env.BROWSER_HEADLESS, { default: true }),
// ...(process.env.PROXY_URL &&{ proxy: {
// server: process.env.PROXY_URL,
// }})
// })
const userAgent = new UserAgent(/Macintosh/).random().toString();
const browser = await chromium.launch({
args,
headless:false,
proxy: {
server: 'http://127.0.0.1:12334',
},
})
const context = await browser.newContext({ userAgent, locale:'en', viewport: { width: 1920, height: 1080 } });
const cookiesStr = '_gcl_au=1.1.1122395686.1756802685; _ga=GA1.1.785768328.1756802695; _axwrt=e01097fa-f771-4c63-a49d-78538080b57a; singular_device_id=822605c1-fd96-4312-8e7f-b27653bdcbaf; ajs_anonymous_id=8240937f-6930-4d54-ae11-830db6ef8262; _tt_enable_cookie=1; _ttp=01K44SN90TAWV4SBPRAYF3SEN1_.tt.1; _fbp=fb.1.1756802753737.293820645972563697; afUserId=ef07b62f-6c47-44f3-b92a-269f61235b6f-p; AF_SYNC=1756802770762; _clck=fqr409%5E2%5Efz4%5E0%5E2071; __client=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNsaWVudF8zMkp6djBjV1U1MGozTW1rdHAzUHdnZ1JJbVYiLCJyb3RhdGluZ190b2tlbiI6Im1iZWd1bHc0aHJ2aDA3Z3praGNmaXA0M2o1enVkMDgxYzczNjhwMW0ifQ.GfLgVl0x0OC7yA7HxSneUpsKnvcMvNSqBonmNuxwURAPhBjJx8MoGy5sojC9Kx8zmO36kaahB4uTDiOj-9VTRZaob01iSk7lzciCjW_iEM5gDZfTeQbYW62JmXk8ymr84twVBNy1xmYAQQKb9TTEnCZHbGjgf3yfPnJOndEgRLeMqTLmGTCz0Vi-1OBx-zOSxS0CwPmPUlmRDhTzrw76x9puzh9PQeP3mbVF8uJe4ZvCA-BSWLjURt8VzcpLW0BYWO_yGjuk-kI8dJ6eUmDOfjCXSsh2SLqqwnE_bWirBHM4ce7JLc09iok6LUsDI2O0g2RQxQW0HwfCtuQxtDV7Rg; __client_uat=1757235449; __client_uat_U9tcbTPE=1757235449; __stripe_mid=f1915dd0-feed-4d1e-8402-e52760e3237b4face4; __cf_bm=Wxfi2Ww131iNPv2i27d32fnSJ2ee7aGUtY3Nz7nkpLk-1757392628-1.0.1.1-hwovJvwM_F.ejTX6HkfZd4F5PSugqHgOtI7X56t3p.FS1sUCrUOuGg4YZmwjNBZ4RCZA_HT7j6AAAnF55rF7K9r9T9pVz5FHDy..P6JSBko; _cfuvid=awR2fvj9zdJbloFbbUfGfhWFS51WZtQu_.7_uBwLKJA-1757392628089-0.0.1.1-604800000; __stripe_sid=c848008e-a8a1-423c-9c31-0b74682bd6a3e48eaf; _ga_7B0KEDD7XP=GS2.1.s1757392637$o11$g1$t1757392720$j46$l0$h0; _uetsid=ac2191508d3611f0afd391282bdea7cd|1phqefk|2|fz6|0|2078; ax_visitor=%7B%22firstVisitTs%22%3A1756802706366%2C%22lastVisitTs%22%3A1757317725011%2C%22currentVisitStartTs%22%3A1757392645129%2C%22ts%22%3A1757392720941%2C%22visitCount%22%3A9%7D; ttcsid=1757392640440::ZYVwpHKeEnpgc8y37liU.10.1757392723928; ttcsid_CT67HURC77UB52N3JFBG=1757392640440::rKUF-WAAJUhQ9nYMbwOe.10.1757392724155; _uetvid=1ecc230087d911f08b445db6fd2178ad|yluce0|1757392724319|4|1|bat.bing.com/p/conversions/c/a'
const cookies = parseCookies(cookiesStr, '.suno.com');
// cookies.push({
// name: '__session',
// value: this.currentToken+'',
// domain: '.suno.com',
// path: '/',
// httpOnly: true,
// secure: true,
// sameSite: 'Lax',
// });
// await context.addCookies(cookies);
const page = await browser.newPage();
console.log('Testing the stealth plugin..')
await page.goto('https://www.suno.com/create', { waitUntil: 'networkidle' })
await solveTurnstile(page);
// const frame = page
// .frames()
// .find((f) => f.url().includes("hcaptcha.com") || f.url().includes("challenges.cloudflare.com"));
//
// if (frame) {
// console.log("检测到 Cloudflare 验证组件,开始调用 2Captcha...");
// }
// 点击 Google 登录按钮
const googleBtn = await page.waitForSelector("button.cl-button__google");
await googleBtn.click();
// 等待跳转到 Google 登录页
await page.waitForURL(/accounts\.google\.com/);
// 输入邮箱
await page.fill('input[type="email"]', 'easyai202502@gmail.com');
await page.click('#identifierNext');
// 等待密码输入框
await page.waitForSelector('input[type="password"]', { timeout: 15000 });
await page.fill('input[type="password"]', 'easyai@2025');
await page.click('#passwordNext');
// 登录成功后会跳转回 suno.com
await page.waitForURL(/suno\.com/, { timeout: 60000 });
// const frame = page.mainFrame();
// const captchaEl = await frame.$("iframe[src*='hcaptcha.com'], iframe[src*='challenges.cloudflare.com']");
console.log('All done, check the screenshot. ✨')
};
test()