自动测试脚本
This commit is contained in:
parent
93b37907f3
commit
c14a25fbc2
@ -3,10 +3,10 @@ version: '3'
|
||||
services:
|
||||
suno-api:
|
||||
image: registry.cn-shanghai.aliyuncs.com/easyaigc/suno-api:latest
|
||||
# build:
|
||||
# context: .
|
||||
# args:
|
||||
# SUNO_COOKIE: ${SUNO_COOKIE}
|
||||
# build:
|
||||
# context: .
|
||||
# args:
|
||||
# SUNO_COOKIE: ${SUNO_COOKIE}
|
||||
volumes:
|
||||
- ./public:/app/public
|
||||
ports:
|
||||
|
@ -7,7 +7,6 @@ const nextConfig = {
|
||||
});
|
||||
return config;
|
||||
},
|
||||
basePath: '/suno-api', // 👈 应用的统一前缀
|
||||
experimental: {
|
||||
serverMinification: false, // the server minification unfortunately breaks the selector class names
|
||||
},
|
||||
|
@ -303,20 +303,20 @@ class SunoApi {
|
||||
'--disable-setuid-sandbox');
|
||||
try {
|
||||
// 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 browser = await chromium.launch({
|
||||
// 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 browser = await chromium.launch({
|
||||
args,
|
||||
headless: yn(process.env.BROWSER_HEADLESS, { default: true }),
|
||||
...(process.env.PROXY_URL &&{ proxy: {
|
||||
server: process.env.PROXY_URL,
|
||||
}})
|
||||
})
|
||||
const context = await browser.newContext({ userAgent: this.userAgent, locale: process.env.BROWSER_LOCALE, viewport: { width: 1920, height: 1080 } });
|
||||
const cookies = [];
|
||||
const lax: 'Lax' | 'Strict' | 'None' = 'Lax';
|
||||
@ -326,7 +326,6 @@ class SunoApi {
|
||||
domain: '.suno.com',
|
||||
path: '/',
|
||||
sameSite: lax,
|
||||
|
||||
});
|
||||
for (const key in this.cookies) {
|
||||
cookies.push({
|
||||
|
295
test.ts
295
test.ts
@ -1,7 +1,253 @@
|
||||
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 = [
|
||||
@ -22,6 +268,8 @@ const test = async () => {
|
||||
// server: process.env.PROXY_URL,
|
||||
// }})
|
||||
// })
|
||||
const userAgent = new UserAgent(/Macintosh/).random().toString();
|
||||
|
||||
const browser = await chromium.launch({
|
||||
args,
|
||||
headless:false,
|
||||
@ -29,20 +277,53 @@ const test = async () => {
|
||||
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/creat', { waitUntil: 'networkidle' })
|
||||
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']");
|
||||
|
||||
const frame = page
|
||||
.frames()
|
||||
.find((f) => f.url().includes("hcaptcha.com") || f.url().includes("challenges.cloudflare.com"));
|
||||
|
||||
if (frame) {
|
||||
console.log("检测到 Cloudflare 验证组件,开始调用 2Captcha...");
|
||||
}
|
||||
|
||||
console.log('All done, check the screenshot. ✨')
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user