From 67b3616ac6db79c94c2821d989eced39237cf7f3 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Wed, 19 Nov 2025 13:22:54 +0800 Subject: [PATCH] dasdasd --- src/cli.js | 15 ++ src/shared/libs/card-generator/config.js | 10 +- src/shared/libs/card-generator/generator.js | 13 +- .../actions/click-action.js | 51 +++- .../configs/sites/windsurf.yaml | 228 ++++++++++++------ .../sites/windsurf-adapter.js | 61 +---- 6 files changed, 254 insertions(+), 124 deletions(-) diff --git a/src/cli.js b/src/cli.js index fc49be8..b4eb0a7 100644 --- a/src/cli.js +++ b/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'); diff --git a/src/shared/libs/card-generator/config.js b/src/shared/libs/card-generator/config.js index 5b16335..94a703d 100644 --- a/src/shared/libs/card-generator/config.js +++ b/src/shared/libs/card-generator/config.js @@ -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算法校验 diff --git a/src/shared/libs/card-generator/generator.js b/src/shared/libs/card-generator/generator.js index e088406..e96021f 100644 --- a/src/shared/libs/card-generator/generator.js +++ b/src/shared/libs/card-generator/generator.js @@ -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算法生成 diff --git a/src/tools/automation-framework/actions/click-action.js b/src/tools/automation-framework/actions/click-action.js index dcd7e5c..634f859 100644 --- a/src/tools/automation-framework/actions/click-action.js +++ b/src/tools/automation-framework/actions/click-action.js @@ -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}`); + } } /** diff --git a/src/tools/automation-framework/configs/sites/windsurf.yaml b/src/tools/automation-framework/configs/sites/windsurf.yaml index a75851a..feb602c 100644 --- a/src/tools/automation-framework/configs/sites/windsurf.yaml +++ b/src/tools/automation-framework/configs/sites/windsurf.yaml @@ -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: diff --git a/src/tools/automation-framework/sites/windsurf-adapter.js b/src/tools/automation-framework/sites/windsurf-adapter.js index 87fa373..dbdec13 100644 --- a/src/tools/automation-framework/sites/windsurf-adapter.js +++ b/src/tools/automation-framework/sites/windsurf-adapter.js @@ -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 }; } /**