This commit is contained in:
dengqichen 2025-11-19 13:22:54 +08:00
parent 700a04a807
commit 67b3616ac6
6 changed files with 254 additions and 124 deletions

View File

@ -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');

View File

@ -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算法校验

View File

@ -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算法生成

View File

@ -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}`);
}
}
/**

View File

@ -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:

View File

@ -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 };
}
/**