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();
|
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 { Command } = require('commander');
|
||||||
const registry = require('./index');
|
const registry = require('./index');
|
||||||
const logger = require('./shared/logger');
|
const logger = require('./shared/logger');
|
||||||
|
|||||||
@ -6,7 +6,15 @@
|
|||||||
const CARD_TYPES = {
|
const CARD_TYPES = {
|
||||||
unionpay: {
|
unionpay: {
|
||||||
name: '中国银联 (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,
|
length: 16,
|
||||||
cvvLength: 3,
|
cvvLength: 3,
|
||||||
useLuhn: true // 使用Luhn算法校验
|
useLuhn: true // 使用Luhn算法校验
|
||||||
|
|||||||
@ -23,7 +23,18 @@ class CardGenerator {
|
|||||||
throw new ValidationError('card-generator', `Unknown card type: ${type}`);
|
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) {
|
if (useLuhn) {
|
||||||
// 使用Luhn算法生成
|
// 使用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)}`);
|
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 targetX = box.x + box.width / 2;
|
||||||
const targetY = box.y + box.height / 2 + this.randomInt(-box.height * 0.3, box.height * 0.3);
|
const targetY = box.y + box.height / 2;
|
||||||
|
|
||||||
// 第一段移动:先移动到附近(模拟人眼定位)
|
// 第一段移动:先移动到附近(模拟人眼定位)
|
||||||
const nearX = targetX + this.randomInt(-50, 50);
|
const nearX = targetX + this.randomInt(-50, 50);
|
||||||
@ -234,7 +234,7 @@ class ClickAction extends BaseAction {
|
|||||||
* 验证点击后的变化
|
* 验证点击后的变化
|
||||||
*/
|
*/
|
||||||
async verifyAfterClick(config) {
|
async verifyAfterClick(config) {
|
||||||
const { appears, disappears, timeout = 10000 } = config;
|
const { appears, disappears, checked, timeout = 10000 } = config;
|
||||||
|
|
||||||
// 验证新元素出现
|
// 验证新元素出现
|
||||||
if (appears) {
|
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:
|
workflow:
|
||||||
# ==================== 步骤 1: 打开注册页面 ====================
|
# ==================== 步骤 1: 打开注册页面并填写信息(带重试) ====================
|
||||||
- action: navigate
|
- action: retryBlock
|
||||||
name: "打开注册页面"
|
name: "打开注册页面并填写基本信息"
|
||||||
url: "https://windsurf.com/account/register"
|
maxRetries: 3
|
||||||
options:
|
|
||||||
waitUntil: 'networkidle2'
|
|
||||||
timeout: 30000
|
|
||||||
# 重试配置(参考旧框架)
|
|
||||||
maxRetries: 5
|
|
||||||
retryDelay: 3000
|
retryDelay: 3000
|
||||||
totalTimeout: 180000 # 3分钟总超时
|
|
||||||
# URL验证(确保没有跳转到会员中心)
|
steps:
|
||||||
verifyUrl: "/account/register"
|
# 1.1 导航到注册页面
|
||||||
# 验证关键元素(确保页面完整加载)
|
- action: navigate
|
||||||
verifyElements:
|
name: "打开注册页面"
|
||||||
- '#firstName'
|
url: "https://windsurf.com/account/register"
|
||||||
- '#lastName'
|
options:
|
||||||
- '#email'
|
waitUntil: 'networkidle2'
|
||||||
- 'input[type="checkbox"]'
|
timeout: 30000
|
||||||
waitAfter: 2000
|
# URL验证(确保没有跳转到会员中心)
|
||||||
|
verifyUrl: "/account/register"
|
||||||
- action: fillForm
|
# 验证关键元素(确保页面完整加载)
|
||||||
name: "填写基本信息"
|
verifyElements:
|
||||||
humanLike: true
|
- '#firstName'
|
||||||
fields:
|
- '#lastName'
|
||||||
firstName:
|
- '#email'
|
||||||
find:
|
- 'input[type="checkbox"]'
|
||||||
- css: '#firstName'
|
waitAfter: 5000 # 增加等待时间,让页面完全稳定
|
||||||
- name: 'firstName'
|
|
||||||
value: "{{account.firstName}}"
|
|
||||||
|
|
||||||
lastName:
|
# 1.2 填写基本信息
|
||||||
find:
|
- action: fillForm
|
||||||
- css: '#lastName'
|
name: "填写基本信息"
|
||||||
- name: 'lastName'
|
humanLike: true
|
||||||
value: "{{account.lastName}}"
|
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:
|
# 1.3 勾选同意条款
|
||||||
find:
|
- action: click
|
||||||
- css: '#email'
|
name: "勾选同意条款"
|
||||||
- type: 'email'
|
selector:
|
||||||
value: "{{account.email}}"
|
- css: 'input[type="checkbox"]'
|
||||||
|
optional: true
|
||||||
- action: click
|
waitAfter: 500
|
||||||
name: "勾选同意条款"
|
|
||||||
selector:
|
# 1.4 点击 Continue
|
||||||
- css: 'input[type="checkbox"]'
|
- action: click
|
||||||
optional: true
|
name: "点击 Continue (基本信息)"
|
||||||
waitAfter: 500
|
selector:
|
||||||
|
- text: 'Continue'
|
||||||
- action: click
|
selector: 'button, a' # 明确指定查找范围:按钮或链接
|
||||||
name: "点击 Continue (基本信息)"
|
timeout: 30000
|
||||||
selector:
|
# 验证点击后密码页面出现
|
||||||
- text: 'Continue'
|
verifyAfter:
|
||||||
selector: 'button, a' # 明确指定查找范围:按钮或链接
|
appears:
|
||||||
timeout: 30000
|
- '#password'
|
||||||
# 验证点击后密码页面出现
|
- 'input[type="password"]'
|
||||||
verifyAfter:
|
waitAfter: 2000
|
||||||
appears:
|
|
||||||
- '#password'
|
|
||||||
- 'input[type="password"]'
|
|
||||||
waitAfter: 2000
|
|
||||||
|
|
||||||
# ==================== 步骤 2: 设置密码 ====================
|
# ==================== 步骤 2: 设置密码 ====================
|
||||||
- action: wait
|
- action: wait
|
||||||
@ -149,25 +155,26 @@ workflow:
|
|||||||
urlContains: "stripe.com"
|
urlContains: "stripe.com"
|
||||||
timeout: 20000
|
timeout: 20000
|
||||||
|
|
||||||
# ==================== 步骤 6: 填写支付信息 ====================
|
# ==================== 步骤 6: 填写支付信息(带重试) ====================
|
||||||
# 6.1 选择银行卡支付方式
|
|
||||||
- action: click
|
|
||||||
name: "选择银行卡支付"
|
|
||||||
selector:
|
|
||||||
- css: 'input[type="radio"][value="card"]'
|
|
||||||
waitAfter: 3000
|
|
||||||
|
|
||||||
# ==================== 步骤 6.2: 提交支付(带重试) ====================
|
|
||||||
- action: retryBlock
|
- action: retryBlock
|
||||||
name: "提交支付并验证"
|
name: "提交支付并验证"
|
||||||
maxRetries: 5
|
maxRetries: 5
|
||||||
retryDelay: 2000
|
retryDelay: 15000 # 增加到15秒,避免触发Stripe风控
|
||||||
onRetryBefore:
|
onRetryBefore:
|
||||||
# 重试前重新生成银行卡
|
# 重试前重新生成银行卡
|
||||||
- action: custom
|
- action: custom
|
||||||
handler: "regenerateCard"
|
handler: "regenerateCard"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
# 6.1 选择银行卡支付方式(每次重试都重新选择)
|
||||||
|
- action: click
|
||||||
|
name: "选择银行卡支付"
|
||||||
|
selector:
|
||||||
|
- css: 'input[type="radio"][value="card"]'
|
||||||
|
verifyAfter:
|
||||||
|
checked: true # 验证 radio button 是否选中
|
||||||
|
waitAfter: 2000
|
||||||
|
|
||||||
# 等待支付表单加载(每次重试都需要等待)
|
# 等待支付表单加载(每次重试都需要等待)
|
||||||
- action: wait
|
- action: wait
|
||||||
name: "等待支付表单"
|
name: "等待支付表单"
|
||||||
@ -237,6 +244,11 @@ workflow:
|
|||||||
name: "滚动到订阅按钮"
|
name: "滚动到订阅按钮"
|
||||||
type: bottom
|
type: bottom
|
||||||
|
|
||||||
|
# 等待页面稳定后再点击
|
||||||
|
- action: wait
|
||||||
|
type: delay
|
||||||
|
duration: 3000
|
||||||
|
|
||||||
# 提交支付(内部会等待按钮变为可点击状态)
|
# 提交支付(内部会等待按钮变为可点击状态)
|
||||||
- action: click
|
- action: click
|
||||||
name: "点击提交支付"
|
name: "点击提交支付"
|
||||||
@ -245,12 +257,88 @@ workflow:
|
|||||||
- css: 'button[type="submit"]'
|
- css: 'button[type="submit"]'
|
||||||
timeout: 30000 # 给足够时间等待按钮从 disabled 变为可点击
|
timeout: 30000 # 给足够时间等待按钮从 disabled 变为可点击
|
||||||
waitForEnabled: true # 循环等待按钮激活(不是等待出现,而是等待可点击)
|
waitForEnabled: true # 循环等待按钮激活(不是等待出现,而是等待可点击)
|
||||||
waitAfter: 5000 # 点击后等待5秒,观察页面反应
|
waitAfter: 3000 # 点击后等待3秒
|
||||||
|
|
||||||
# 验证点击是否生效(检查按钮状态变化)
|
# 验证点击是否生效(检查按钮状态变化)
|
||||||
- action: custom
|
- action: custom
|
||||||
name: "验证订阅按钮点击生效"
|
name: "验证订阅按钮点击生效"
|
||||||
handler: "verifySubmitClick"
|
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:
|
errorHandling:
|
||||||
|
|||||||
@ -35,7 +35,8 @@ class WindsurfAdapter extends SiteAdapter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.log('info', `账户邮箱: ${accountData.email}`);
|
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() {
|
regenerateCard() {
|
||||||
const newCard = this.cardGen.generate();
|
const newCard = this.cardGen.generate();
|
||||||
this.context.data.card = newCard;
|
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;
|
return newCard;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,56 +297,19 @@ class WindsurfAdapter extends SiteAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理 hCaptcha
|
* 处理 hCaptcha(等待60秒让用户手动完成)
|
||||||
*/
|
*/
|
||||||
async handleHCaptcha() {
|
async handleHCaptcha(params = {}) {
|
||||||
this.log('info', '检查 hCaptcha...');
|
const waitTime = 60000; // 固定等待60秒
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
this.log('warn', '⚠️ 等待手动完成 hCaptcha 验证...');
|
||||||
|
this.log('info', `⏱️ 等待时间:${waitTime / 1000} 秒`);
|
||||||
|
|
||||||
// 检查是否有 hCaptcha
|
// 纯等待60秒,不做任何检测
|
||||||
const hasCaptcha = await this.page.evaluate(() => {
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasCaptcha) {
|
this.log('success', '✓ 等待完成,继续执行');
|
||||||
this.log('info', '检测到 hCaptcha,等待自动完成...');
|
return { success: true };
|
||||||
|
|
||||||
// 等待验证完成(检查 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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user