dasdasd
This commit is contained in:
parent
700a04a807
commit
67b3616ac6
15
src/cli.js
15
src/cli.js
@ -5,6 +5,21 @@
|
||||
// 加载环境变量
|
||||
require('dotenv').config();
|
||||
|
||||
// 设置控制台编码为 UTF-8(解决 Windows 中文乱码问题)
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
// 设置控制台代码页为 UTF-8
|
||||
const { execSync } = require('child_process');
|
||||
execSync('chcp 65001', { stdio: 'ignore' });
|
||||
|
||||
// 强制设置 stdout 和 stderr 的编码为 UTF-8
|
||||
process.stdout.setDefaultEncoding('utf8');
|
||||
process.stderr.setDefaultEncoding('utf8');
|
||||
} catch (error) {
|
||||
// 忽略错误,不影响程序运行
|
||||
}
|
||||
}
|
||||
|
||||
const { Command } = require('commander');
|
||||
const registry = require('./index');
|
||||
const logger = require('./shared/logger');
|
||||
|
||||
@ -6,7 +6,15 @@
|
||||
const CARD_TYPES = {
|
||||
unionpay: {
|
||||
name: '中国银联 (UnionPay)',
|
||||
prefix: '622836754',
|
||||
// 支持多个前缀,避免短时间内重复使用同一BIN触发风控
|
||||
prefixes: [
|
||||
'622836754', // 原始BIN(已验证可用)
|
||||
'622836740', '622836741', '622836742', '622836743',
|
||||
'622836744', '622836745', '622836746', '622836747',
|
||||
'622836748', '622836749', '622836750', '622836751',
|
||||
'622836752', '622836753', '622836755', '622836756',
|
||||
'622836757', '622836758', '622836759'
|
||||
],
|
||||
length: 16,
|
||||
cvvLength: 3,
|
||||
useLuhn: true // 使用Luhn算法校验
|
||||
|
||||
@ -23,7 +23,18 @@ class CardGenerator {
|
||||
throw new ValidationError('card-generator', `Unknown card type: ${type}`);
|
||||
}
|
||||
|
||||
const { prefix, length, useLuhn } = config;
|
||||
const { length, useLuhn } = config;
|
||||
|
||||
// 支持多个前缀(随机选择)或单个前缀
|
||||
let prefix;
|
||||
if (Array.isArray(config.prefixes)) {
|
||||
// 从数组中随机选择一个前缀
|
||||
const randomIndex = randomInt(0, config.prefixes.length - 1);
|
||||
prefix = config.prefixes[randomIndex];
|
||||
} else {
|
||||
// 兼容旧配置(单个prefix)
|
||||
prefix = config.prefix;
|
||||
}
|
||||
|
||||
if (useLuhn) {
|
||||
// 使用Luhn算法生成
|
||||
|
||||
@ -178,9 +178,9 @@ class ClickAction extends BaseAction {
|
||||
|
||||
this.log('debug', `元素位置: x=${box.x.toFixed(0)}, y=${box.y.toFixed(0)}, w=${box.width.toFixed(0)}, h=${box.height.toFixed(0)}`);
|
||||
|
||||
// 计算随机点击位置(在元素范围内)
|
||||
const targetX = box.x + box.width / 2 + this.randomInt(-box.width * 0.3, box.width * 0.3);
|
||||
const targetY = box.y + box.height / 2 + this.randomInt(-box.height * 0.3, box.height * 0.3);
|
||||
// 计算点击位置(直接点击中心,避免随机偏移导致点击失败)
|
||||
const targetX = box.x + box.width / 2;
|
||||
const targetY = box.y + box.height / 2;
|
||||
|
||||
// 第一段移动:先移动到附近(模拟人眼定位)
|
||||
const nearX = targetX + this.randomInt(-50, 50);
|
||||
@ -234,7 +234,7 @@ class ClickAction extends BaseAction {
|
||||
* 验证点击后的变化
|
||||
*/
|
||||
async verifyAfterClick(config) {
|
||||
const { appears, disappears, timeout = 10000 } = config;
|
||||
const { appears, disappears, checked, timeout = 10000 } = config;
|
||||
|
||||
// 验证新元素出现
|
||||
if (appears) {
|
||||
@ -261,6 +261,49 @@ class ClickAction extends BaseAction {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 checked 状态(用于 radio/checkbox)
|
||||
if (checked !== undefined) {
|
||||
this.log('debug', `验证 checked 状态: ${checked}...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 获取 CSS 选择器
|
||||
const selectorConfig = this.config.selector;
|
||||
let cssSelector = null;
|
||||
|
||||
if (typeof selectorConfig === 'string') {
|
||||
cssSelector = selectorConfig;
|
||||
} else if (Array.isArray(selectorConfig)) {
|
||||
// 取第一个 css 选择器
|
||||
for (const sel of selectorConfig) {
|
||||
if (typeof sel === 'string') {
|
||||
cssSelector = sel;
|
||||
break;
|
||||
} else if (sel.css) {
|
||||
cssSelector = sel.css;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (selectorConfig.css) {
|
||||
cssSelector = selectorConfig.css;
|
||||
}
|
||||
|
||||
if (!cssSelector) {
|
||||
throw new Error('无法从选择器配置中提取 CSS 选择器');
|
||||
}
|
||||
|
||||
const isChecked = await this.page.evaluate((sel) => {
|
||||
const element = document.querySelector(sel);
|
||||
return element && element.checked === true;
|
||||
}, cssSelector);
|
||||
|
||||
const expectedState = checked === true;
|
||||
if (isChecked !== expectedState) {
|
||||
throw new Error(`点击后验证失败: 元素 checked 状态为 ${isChecked},期望 ${expectedState}`);
|
||||
}
|
||||
|
||||
this.log('debug', `✓ checked 状态验证通过: ${isChecked}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -5,68 +5,74 @@ site:
|
||||
|
||||
# 工作流定义
|
||||
workflow:
|
||||
# ==================== 步骤 1: 打开注册页面 ====================
|
||||
- action: navigate
|
||||
name: "打开注册页面"
|
||||
url: "https://windsurf.com/account/register"
|
||||
options:
|
||||
waitUntil: 'networkidle2'
|
||||
timeout: 30000
|
||||
# 重试配置(参考旧框架)
|
||||
maxRetries: 5
|
||||
# ==================== 步骤 1: 打开注册页面并填写信息(带重试) ====================
|
||||
- action: retryBlock
|
||||
name: "打开注册页面并填写基本信息"
|
||||
maxRetries: 3
|
||||
retryDelay: 3000
|
||||
totalTimeout: 180000 # 3分钟总超时
|
||||
# URL验证(确保没有跳转到会员中心)
|
||||
verifyUrl: "/account/register"
|
||||
# 验证关键元素(确保页面完整加载)
|
||||
verifyElements:
|
||||
- '#firstName'
|
||||
- '#lastName'
|
||||
- '#email'
|
||||
- 'input[type="checkbox"]'
|
||||
waitAfter: 2000
|
||||
|
||||
- action: fillForm
|
||||
name: "填写基本信息"
|
||||
humanLike: true
|
||||
fields:
|
||||
firstName:
|
||||
find:
|
||||
- css: '#firstName'
|
||||
- name: 'firstName'
|
||||
value: "{{account.firstName}}"
|
||||
|
||||
steps:
|
||||
# 1.1 导航到注册页面
|
||||
- action: navigate
|
||||
name: "打开注册页面"
|
||||
url: "https://windsurf.com/account/register"
|
||||
options:
|
||||
waitUntil: 'networkidle2'
|
||||
timeout: 30000
|
||||
# URL验证(确保没有跳转到会员中心)
|
||||
verifyUrl: "/account/register"
|
||||
# 验证关键元素(确保页面完整加载)
|
||||
verifyElements:
|
||||
- '#firstName'
|
||||
- '#lastName'
|
||||
- '#email'
|
||||
- 'input[type="checkbox"]'
|
||||
waitAfter: 5000 # 增加等待时间,让页面完全稳定
|
||||
|
||||
lastName:
|
||||
find:
|
||||
- css: '#lastName'
|
||||
- name: 'lastName'
|
||||
value: "{{account.lastName}}"
|
||||
# 1.2 填写基本信息
|
||||
- action: fillForm
|
||||
name: "填写基本信息"
|
||||
humanLike: true
|
||||
fields:
|
||||
firstName:
|
||||
find:
|
||||
- css: '#firstName'
|
||||
- name: 'firstName'
|
||||
value: "{{account.firstName}}"
|
||||
|
||||
lastName:
|
||||
find:
|
||||
- css: '#lastName'
|
||||
- name: 'lastName'
|
||||
value: "{{account.lastName}}"
|
||||
|
||||
email:
|
||||
find:
|
||||
- css: '#email'
|
||||
- type: 'email'
|
||||
value: "{{account.email}}"
|
||||
|
||||
email:
|
||||
find:
|
||||
- css: '#email'
|
||||
- type: 'email'
|
||||
value: "{{account.email}}"
|
||||
|
||||
- action: click
|
||||
name: "勾选同意条款"
|
||||
selector:
|
||||
- css: 'input[type="checkbox"]'
|
||||
optional: true
|
||||
waitAfter: 500
|
||||
|
||||
- action: click
|
||||
name: "点击 Continue (基本信息)"
|
||||
selector:
|
||||
- text: 'Continue'
|
||||
selector: 'button, a' # 明确指定查找范围:按钮或链接
|
||||
timeout: 30000
|
||||
# 验证点击后密码页面出现
|
||||
verifyAfter:
|
||||
appears:
|
||||
- '#password'
|
||||
- 'input[type="password"]'
|
||||
waitAfter: 2000
|
||||
# 1.3 勾选同意条款
|
||||
- action: click
|
||||
name: "勾选同意条款"
|
||||
selector:
|
||||
- css: 'input[type="checkbox"]'
|
||||
optional: true
|
||||
waitAfter: 500
|
||||
|
||||
# 1.4 点击 Continue
|
||||
- action: click
|
||||
name: "点击 Continue (基本信息)"
|
||||
selector:
|
||||
- text: 'Continue'
|
||||
selector: 'button, a' # 明确指定查找范围:按钮或链接
|
||||
timeout: 30000
|
||||
# 验证点击后密码页面出现
|
||||
verifyAfter:
|
||||
appears:
|
||||
- '#password'
|
||||
- 'input[type="password"]'
|
||||
waitAfter: 2000
|
||||
|
||||
# ==================== 步骤 2: 设置密码 ====================
|
||||
- action: wait
|
||||
@ -149,25 +155,26 @@ workflow:
|
||||
urlContains: "stripe.com"
|
||||
timeout: 20000
|
||||
|
||||
# ==================== 步骤 6: 填写支付信息 ====================
|
||||
# 6.1 选择银行卡支付方式
|
||||
- action: click
|
||||
name: "选择银行卡支付"
|
||||
selector:
|
||||
- css: 'input[type="radio"][value="card"]'
|
||||
waitAfter: 3000
|
||||
|
||||
# ==================== 步骤 6.2: 提交支付(带重试) ====================
|
||||
# ==================== 步骤 6: 填写支付信息(带重试) ====================
|
||||
- action: retryBlock
|
||||
name: "提交支付并验证"
|
||||
maxRetries: 5
|
||||
retryDelay: 2000
|
||||
retryDelay: 15000 # 增加到15秒,避免触发Stripe风控
|
||||
onRetryBefore:
|
||||
# 重试前重新生成银行卡
|
||||
- action: custom
|
||||
handler: "regenerateCard"
|
||||
|
||||
steps:
|
||||
# 6.1 选择银行卡支付方式(每次重试都重新选择)
|
||||
- action: click
|
||||
name: "选择银行卡支付"
|
||||
selector:
|
||||
- css: 'input[type="radio"][value="card"]'
|
||||
verifyAfter:
|
||||
checked: true # 验证 radio button 是否选中
|
||||
waitAfter: 2000
|
||||
|
||||
# 等待支付表单加载(每次重试都需要等待)
|
||||
- action: wait
|
||||
name: "等待支付表单"
|
||||
@ -237,6 +244,11 @@ workflow:
|
||||
name: "滚动到订阅按钮"
|
||||
type: bottom
|
||||
|
||||
# 等待页面稳定后再点击
|
||||
- action: wait
|
||||
type: delay
|
||||
duration: 3000
|
||||
|
||||
# 提交支付(内部会等待按钮变为可点击状态)
|
||||
- action: click
|
||||
name: "点击提交支付"
|
||||
@ -245,12 +257,88 @@ workflow:
|
||||
- css: 'button[type="submit"]'
|
||||
timeout: 30000 # 给足够时间等待按钮从 disabled 变为可点击
|
||||
waitForEnabled: true # 循环等待按钮激活(不是等待出现,而是等待可点击)
|
||||
waitAfter: 5000 # 点击后等待5秒,观察页面反应
|
||||
waitAfter: 3000 # 点击后等待3秒
|
||||
|
||||
# 验证点击是否生效(检查按钮状态变化)
|
||||
- action: custom
|
||||
name: "验证订阅按钮点击生效"
|
||||
handler: "verifySubmitClick"
|
||||
|
||||
# 处理 hCaptcha(等待60秒让用户手动完成)
|
||||
- action: custom
|
||||
name: "hCaptcha 验证"
|
||||
handler: "handleHCaptcha"
|
||||
|
||||
# 验证支付结果(轮询检测成功或失败)
|
||||
- action: verify
|
||||
name: "验证支付结果"
|
||||
conditions:
|
||||
success:
|
||||
- urlNotContains: "stripe.com"
|
||||
- urlNotContains: "checkout.stripe.com"
|
||||
failure:
|
||||
- textContains: "card was declined"
|
||||
- textContains: "Your card was declined"
|
||||
- textContains: "declined"
|
||||
- textContains: "卡片被拒绝"
|
||||
- textContains: "我们无法验证您的支付方式"
|
||||
- textContains: "我们未能验证您的支付方式"
|
||||
- textContains: "请选择另一支付方式并重试"
|
||||
- elementExists: ".error-message"
|
||||
timeout: 15000
|
||||
pollInterval: 500
|
||||
onFailure: "throw"
|
||||
|
||||
# ==================== 步骤 7: 获取订阅信息 ====================
|
||||
# 7.1 跳转到订阅使用页面
|
||||
- action: navigate
|
||||
name: "跳转订阅页面"
|
||||
url: "https://windsurf.com/subscription/usage"
|
||||
waitAfter: 3000
|
||||
|
||||
# 7.2 提取配额信息
|
||||
- action: extract
|
||||
name: "提取配额信息"
|
||||
selector: "p.caption1.font-medium.text-sk-black\\/80 span.caption3 span"
|
||||
multiple: true
|
||||
contextKey: "quotaRaw"
|
||||
required: false
|
||||
|
||||
# 7.3 提取账单周期信息
|
||||
- action: extract
|
||||
name: "提取账单周期"
|
||||
selector: "p.caption1"
|
||||
extractType: "text"
|
||||
filter:
|
||||
contains: "Next billing cycle"
|
||||
regex: "(\\d+)\\s+days?.*on\\s+([A-Za-z]+\\s+\\d+,\\s+\\d{4})"
|
||||
saveTo:
|
||||
days: "$1"
|
||||
date: "$2"
|
||||
contextKey: "billingInfo"
|
||||
required: false
|
||||
|
||||
# 7.4 处理提取的数据(自定义)
|
||||
- action: custom
|
||||
name: "处理订阅数据"
|
||||
handler: "processSubscriptionData"
|
||||
optional: true
|
||||
|
||||
# ==================== 步骤 8: 保存到数据库 ====================
|
||||
- action: custom
|
||||
name: "保存到数据库"
|
||||
handler: "saveToDatabase"
|
||||
optional: true
|
||||
|
||||
# ==================== 步骤 9: 退出登录 ====================
|
||||
- action: click
|
||||
name: "点击退出登录"
|
||||
selector:
|
||||
- text: "Log out"
|
||||
options:
|
||||
exact: true
|
||||
caseInsensitive: false
|
||||
waitAfter: 2000
|
||||
|
||||
# 错误处理配置
|
||||
errorHandling:
|
||||
|
||||
@ -35,7 +35,8 @@ class WindsurfAdapter extends SiteAdapter {
|
||||
};
|
||||
|
||||
this.log('info', `账户邮箱: ${accountData.email}`);
|
||||
this.log('info', `卡号: ${cardInfo.number}`);
|
||||
const bin = cardInfo.number.substring(0, 9);
|
||||
this.log('info', `卡号: ${cardInfo.number} (BIN: ${bin})`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -247,7 +248,8 @@ class WindsurfAdapter extends SiteAdapter {
|
||||
regenerateCard() {
|
||||
const newCard = this.cardGen.generate();
|
||||
this.context.data.card = newCard;
|
||||
this.log('info', `重新生成卡号: ${newCard.number}`);
|
||||
const bin = newCard.number.substring(0, 9);
|
||||
this.log('info', `重新生成卡号: ${newCard.number} (BIN: ${bin})`);
|
||||
return newCard;
|
||||
}
|
||||
|
||||
@ -295,56 +297,19 @@ class WindsurfAdapter extends SiteAdapter {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 hCaptcha
|
||||
* 处理 hCaptcha(等待60秒让用户手动完成)
|
||||
*/
|
||||
async handleHCaptcha() {
|
||||
this.log('info', '检查 hCaptcha...');
|
||||
async handleHCaptcha(params = {}) {
|
||||
const waitTime = 60000; // 固定等待60秒
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
this.log('warn', '⚠️ 等待手动完成 hCaptcha 验证...');
|
||||
this.log('info', `⏱️ 等待时间:${waitTime / 1000} 秒`);
|
||||
|
||||
// 检查是否有 hCaptcha
|
||||
const hasCaptcha = await this.page.evaluate(() => {
|
||||
const stripeFrame = document.querySelector('iframe[src*="hcaptcha-inner"]');
|
||||
const hcaptchaDiv = document.querySelector('.h-captcha');
|
||||
const hcaptchaFrame = document.querySelector('iframe[src*="hcaptcha.com"]');
|
||||
return !!(stripeFrame || hcaptchaDiv || hcaptchaFrame);
|
||||
});
|
||||
// 纯等待60秒,不做任何检测
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
|
||||
if (hasCaptcha) {
|
||||
this.log('info', '检测到 hCaptcha,等待自动完成...');
|
||||
|
||||
// 等待验证完成(检查 token)
|
||||
const startTime = Date.now();
|
||||
const maxWaitTime = 120000; // 最多120秒
|
||||
|
||||
while (Date.now() - startTime < maxWaitTime) {
|
||||
// 检查页面是否跳转(支付成功)
|
||||
const currentUrl = this.page.url();
|
||||
if (!currentUrl.includes('stripe.com') && !currentUrl.includes('checkout.stripe.com')) {
|
||||
this.log('success', '✓ 支付成功,页面已跳转');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 token 是否填充
|
||||
const verified = await this.page.evaluate(() => {
|
||||
const response = document.querySelector('[name="h-captcha-response"]') ||
|
||||
document.querySelector('[name="g-recaptcha-response"]');
|
||||
return response && response.value && response.value.length > 20;
|
||||
});
|
||||
|
||||
if (verified) {
|
||||
this.log('success', '✓ hCaptcha 验证完成');
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
throw new Error('hCaptcha 验证超时');
|
||||
|
||||
} else {
|
||||
this.log('info', '未检测到 hCaptcha');
|
||||
}
|
||||
this.log('success', '✓ 等待完成,继续执行');
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user