This commit is contained in:
dengqichen 2025-11-19 11:02:32 +08:00
parent 02d58adbe2
commit 700a04a807
5 changed files with 153 additions and 84 deletions

View File

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

View File

@ -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];

View File

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

View File

@ -142,6 +142,13 @@ workflow:
timeout: 30000
waitAfter: 2000
# 等待跳转到 Stripe 支付页面(通用 URL 等待)
- action: wait
name: "等待跳转到 Stripe"
type: url
urlContains: "stripe.com"
timeout: 20000
# ==================== 步骤 6: 填写支付信息 ====================
# 6.1 选择银行卡支付方式
- action: click
@ -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:

View File

@ -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 };
}
return { success: true };
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('订阅按钮已消失(可能页面已跳转)');
}
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}"`);
}
}
/**