From 700a04a80743e8e0df5c2188c188d8ea6c028a20 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Wed, 19 Nov 2025 11:02:32 +0800 Subject: [PATCH] dasdasd --- .../actions/fill-form-action.js | 15 +++ .../actions/retry-block-action.js | 3 +- .../actions/wait-action.js | 56 +++++++++ .../configs/sites/windsurf.yaml | 117 +++++++----------- .../sites/windsurf-adapter.js | 46 +++++-- 5 files changed, 153 insertions(+), 84 deletions(-) diff --git a/src/tools/automation-framework/actions/fill-form-action.js b/src/tools/automation-framework/actions/fill-form-action.js index b4da5df..9a32968 100644 --- a/src/tools/automation-framework/actions/fill-form-action.js +++ b/src/tools/automation-framework/actions/fill-form-action.js @@ -52,6 +52,21 @@ class FillFormAction extends BaseAction { this.log('debug', ` → 填写字段: ${key}`); + // 检查字段类型 + const fieldType = fieldConfig.type || 'input'; + + if (fieldType === 'select') { + // 下拉框选择(需要 CSS 选择器) + const cssSelector = selector.css || selector[0]?.css; + if (!cssSelector) { + throw new Error(`select 类型字段需要 css 选择器: ${JSON.stringify(selector)}`); + } + await this.page.select(cssSelector, value); + this.log('debug', ` → 已选择: ${value}`); + return; + } + + // 普通输入框 // 清空字段(增强清空逻辑,支持 Stripe 等复杂表单) await element.click({ clickCount: 3 }); await new Promise(resolve => setTimeout(resolve, 100)); diff --git a/src/tools/automation-framework/actions/retry-block-action.js b/src/tools/automation-framework/actions/retry-block-action.js index 0bf872c..2831350 100644 --- a/src/tools/automation-framework/actions/retry-block-action.js +++ b/src/tools/automation-framework/actions/retry-block-action.js @@ -131,7 +131,8 @@ class RetryBlockAction extends BaseAction { click: require('./click-action'), wait: require('./wait-action'), custom: require('./custom-action'), - scroll: require('./scroll-action') + scroll: require('./scroll-action'), + verify: require('./verify-action') }; const ActionClass = actionMap[actionType]; diff --git a/src/tools/automation-framework/actions/wait-action.js b/src/tools/automation-framework/actions/wait-action.js index 39f4853..0353836 100644 --- a/src/tools/automation-framework/actions/wait-action.js +++ b/src/tools/automation-framework/actions/wait-action.js @@ -21,6 +21,9 @@ class WaitAction extends BaseAction { case 'condition': return await this.waitForCondition(); + case 'url': + return await this.waitForUrl(); + default: throw new Error(`未知的等待类型: ${type}`); } @@ -107,6 +110,59 @@ class WaitAction extends BaseAction { throw new Error('条件未满足(超时)'); } + + /** + * 等待 URL 变化 + */ + async waitForUrl() { + const timeout = this.config.timeout || 20000; + const urlContains = this.config.urlContains; + const urlNotContains = this.config.urlNotContains; + const urlEquals = this.config.urlEquals; + + if (!urlContains && !urlNotContains && !urlEquals) { + throw new Error('需要指定 urlContains、urlNotContains 或 urlEquals'); + } + + this.log('debug', '等待 URL 变化'); + + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + const currentUrl = this.page.url(); + + let matched = false; + + if (urlContains) { + matched = currentUrl.includes(urlContains); + if (matched) { + this.log('debug', `✓ URL 包含 "${urlContains}": ${currentUrl}`); + return { success: true }; + } + } + + if (urlNotContains) { + matched = !currentUrl.includes(urlNotContains); + if (matched) { + this.log('debug', `✓ URL 不包含 "${urlNotContains}": ${currentUrl}`); + return { success: true }; + } + } + + if (urlEquals) { + matched = currentUrl === urlEquals; + if (matched) { + this.log('debug', `✓ URL 等于 "${urlEquals}"`); + return { success: true }; + } + } + + await new Promise(resolve => setTimeout(resolve, 500)); + } + + const finalUrl = this.page.url(); + throw new Error(`URL 条件未满足(超时),当前 URL: ${finalUrl}`); + } } module.exports = WaitAction; diff --git a/src/tools/automation-framework/configs/sites/windsurf.yaml b/src/tools/automation-framework/configs/sites/windsurf.yaml index d14e242..a75851a 100644 --- a/src/tools/automation-framework/configs/sites/windsurf.yaml +++ b/src/tools/automation-framework/configs/sites/windsurf.yaml @@ -141,6 +141,13 @@ workflow: selector: 'button, a' timeout: 30000 waitAfter: 2000 + + # 等待跳转到 Stripe 支付页面(通用 URL 等待) + - action: wait + name: "等待跳转到 Stripe" + type: url + urlContains: "stripe.com" + timeout: 20000 # ==================== 步骤 6: 填写支付信息 ==================== # 6.1 选择银行卡支付方式 @@ -194,88 +201,56 @@ workflow: - css: '#billingName' value: "{{account.firstName}} {{account.lastName}}" - # 选择澳门地址(动态字段,需要自定义处理) - - action: custom - name: "选择澳门地址" - handler: "selectBillingAddress" + # 选择国家和填写地址 + - action: fillForm + name: "选择国家和填写地址" + fields: + billingCountry: + find: + - css: '#billingCountry' + value: "MO" + type: "select" + + # 等待地址字段加载 + - action: wait + type: delay + duration: 1000 + + # 填写地址(动态字段) + - action: fillForm + name: "填写地址" + humanLike: false + fields: + addressLine1: + find: + - css: 'input[placeholder*="地址"]' + - css: 'input[placeholder*="Address"]' + value: "Macau" + addressLine2: + find: + - css: 'input[placeholder*="Address line 2"]' + - css: 'input[name="billingAddressLine2"]' + value: "Macao" # 滚动到页面底部(确保订阅按钮可见) - action: scroll name: "滚动到订阅按钮" type: bottom - # 提交支付 + # 提交支付(内部会等待按钮变为可点击状态) - action: click name: "点击提交支付" selector: - - css: 'button[type="submit"]' # Stripe 订阅按钮 - timeout: 15000 - waitAfter: 2000 + - css: 'button[data-testid="hosted-payment-submit-button"]' + - css: 'button[type="submit"]' + timeout: 30000 # 给足够时间等待按钮从 disabled 变为可点击 + waitForEnabled: true # 循环等待按钮激活(不是等待出现,而是等待可点击) + waitAfter: 5000 # 点击后等待5秒,观察页面反应 - # 处理 hCaptcha(点击订阅后出现) + # 验证点击是否生效(检查按钮状态变化) - action: custom - name: "hCaptcha 验证" - handler: "handleHCaptcha" - params: - timeout: 120000 - - # 验证支付结果(轮询检测成功或失败) - - 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: "卡片被拒绝" - - 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 + name: "验证订阅按钮点击生效" + handler: "verifySubmitClick" # 错误处理配置 errorHandling: diff --git a/src/tools/automation-framework/sites/windsurf-adapter.js b/src/tools/automation-framework/sites/windsurf-adapter.js index bc0964f..87fa373 100644 --- a/src/tools/automation-framework/sites/windsurf-adapter.js +++ b/src/tools/automation-framework/sites/windsurf-adapter.js @@ -252,24 +252,46 @@ class WindsurfAdapter extends SiteAdapter { } /** - * 选择澳门地址(处理动态地址字段) + * 验证订阅按钮点击是否生效 */ - async selectBillingAddress(params) { - // 选择国家/地区 - await this.page.select('#billingCountry', 'MO'); + async verifySubmitClick() { + this.log('info', '验证订阅按钮点击是否生效...'); + await new Promise(resolve => setTimeout(resolve, 1000)); - // 填写动态地址字段 - const addressFields = await this.page.$$('input[placeholder*="地址"]'); - if (addressFields.length > 0) { - await addressFields[0].type('Macau', { delay: 100 }); - if (addressFields[1]) { - await new Promise(resolve => setTimeout(resolve, 300)); - await addressFields[1].type('Macao', { delay: 100 }); + const buttonState = await this.page.evaluate(() => { + const button = document.querySelector('button[data-testid="hosted-payment-submit-button"]') || + document.querySelector('button[type="submit"]'); + + if (!button) { + return { found: false }; } + + const text = button.textContent || button.innerText || ''; + const isDisabled = button.disabled || button.getAttribute('aria-disabled') === 'true'; + const isProcessing = text.includes('处理') || text.includes('Processing') || + text.includes('正在') || text.includes('Loading'); + + return { + found: true, + text: text.trim(), + disabled: isDisabled, + processing: isProcessing, + changed: isDisabled || isProcessing + }; + }); + + if (!buttonState.found) { + throw new Error('订阅按钮已消失(可能页面已跳转)'); } - return { success: true }; + if (buttonState.changed) { + this.log('success', `✓ 按钮状态已变化: "${buttonState.text}" (disabled: ${buttonState.disabled})`); + return { success: true, clicked: true }; + } else { + this.log('error', `✗ 按钮状态未变化: "${buttonState.text}" - 点击可能未生效!`); + throw new Error(`订阅按钮点击未生效,按钮文本: "${buttonState.text}"`); + } } /**