dasdasd
This commit is contained in:
parent
02d58adbe2
commit
700a04a807
@ -52,6 +52,21 @@ class FillFormAction extends BaseAction {
|
|||||||
|
|
||||||
this.log('debug', ` → 填写字段: ${key}`);
|
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 等复杂表单)
|
// 清空字段(增强清空逻辑,支持 Stripe 等复杂表单)
|
||||||
await element.click({ clickCount: 3 });
|
await element.click({ clickCount: 3 });
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|||||||
@ -131,7 +131,8 @@ class RetryBlockAction extends BaseAction {
|
|||||||
click: require('./click-action'),
|
click: require('./click-action'),
|
||||||
wait: require('./wait-action'),
|
wait: require('./wait-action'),
|
||||||
custom: require('./custom-action'),
|
custom: require('./custom-action'),
|
||||||
scroll: require('./scroll-action')
|
scroll: require('./scroll-action'),
|
||||||
|
verify: require('./verify-action')
|
||||||
};
|
};
|
||||||
|
|
||||||
const ActionClass = actionMap[actionType];
|
const ActionClass = actionMap[actionType];
|
||||||
|
|||||||
@ -21,6 +21,9 @@ class WaitAction extends BaseAction {
|
|||||||
case 'condition':
|
case 'condition':
|
||||||
return await this.waitForCondition();
|
return await this.waitForCondition();
|
||||||
|
|
||||||
|
case 'url':
|
||||||
|
return await this.waitForUrl();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`未知的等待类型: ${type}`);
|
throw new Error(`未知的等待类型: ${type}`);
|
||||||
}
|
}
|
||||||
@ -107,6 +110,59 @@ class WaitAction extends BaseAction {
|
|||||||
|
|
||||||
throw new Error('条件未满足(超时)');
|
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;
|
module.exports = WaitAction;
|
||||||
|
|||||||
@ -142,6 +142,13 @@ workflow:
|
|||||||
timeout: 30000
|
timeout: 30000
|
||||||
waitAfter: 2000
|
waitAfter: 2000
|
||||||
|
|
||||||
|
# 等待跳转到 Stripe 支付页面(通用 URL 等待)
|
||||||
|
- action: wait
|
||||||
|
name: "等待跳转到 Stripe"
|
||||||
|
type: url
|
||||||
|
urlContains: "stripe.com"
|
||||||
|
timeout: 20000
|
||||||
|
|
||||||
# ==================== 步骤 6: 填写支付信息 ====================
|
# ==================== 步骤 6: 填写支付信息 ====================
|
||||||
# 6.1 选择银行卡支付方式
|
# 6.1 选择银行卡支付方式
|
||||||
- action: click
|
- action: click
|
||||||
@ -194,88 +201,56 @@ workflow:
|
|||||||
- css: '#billingName'
|
- css: '#billingName'
|
||||||
value: "{{account.firstName}} {{account.lastName}}"
|
value: "{{account.firstName}} {{account.lastName}}"
|
||||||
|
|
||||||
# 选择澳门地址(动态字段,需要自定义处理)
|
# 选择国家和填写地址
|
||||||
- action: custom
|
- action: fillForm
|
||||||
name: "选择澳门地址"
|
name: "选择国家和填写地址"
|
||||||
handler: "selectBillingAddress"
|
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
|
- action: scroll
|
||||||
name: "滚动到订阅按钮"
|
name: "滚动到订阅按钮"
|
||||||
type: bottom
|
type: bottom
|
||||||
|
|
||||||
# 提交支付
|
# 提交支付(内部会等待按钮变为可点击状态)
|
||||||
- action: click
|
- action: click
|
||||||
name: "点击提交支付"
|
name: "点击提交支付"
|
||||||
selector:
|
selector:
|
||||||
- css: 'button[type="submit"]' # Stripe 订阅按钮
|
- css: 'button[data-testid="hosted-payment-submit-button"]'
|
||||||
timeout: 15000
|
- css: 'button[type="submit"]'
|
||||||
waitAfter: 2000
|
timeout: 30000 # 给足够时间等待按钮从 disabled 变为可点击
|
||||||
|
waitForEnabled: true # 循环等待按钮激活(不是等待出现,而是等待可点击)
|
||||||
|
waitAfter: 5000 # 点击后等待5秒,观察页面反应
|
||||||
|
|
||||||
# 处理 hCaptcha(点击订阅后出现)
|
# 验证点击是否生效(检查按钮状态变化)
|
||||||
- action: custom
|
- action: custom
|
||||||
name: "hCaptcha 验证"
|
name: "验证订阅按钮点击生效"
|
||||||
handler: "handleHCaptcha"
|
handler: "verifySubmitClick"
|
||||||
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
|
|
||||||
|
|
||||||
# 错误处理配置
|
# 错误处理配置
|
||||||
errorHandling:
|
errorHandling:
|
||||||
|
|||||||
@ -252,24 +252,46 @@ class WindsurfAdapter extends SiteAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选择澳门地址(处理动态地址字段)
|
* 验证订阅按钮点击是否生效
|
||||||
*/
|
*/
|
||||||
async selectBillingAddress(params) {
|
async verifySubmitClick() {
|
||||||
// 选择国家/地区
|
this.log('info', '验证订阅按钮点击是否生效...');
|
||||||
await this.page.select('#billingCountry', 'MO');
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
// 填写动态地址字段
|
const buttonState = await this.page.evaluate(() => {
|
||||||
const addressFields = await this.page.$$('input[placeholder*="地址"]');
|
const button = document.querySelector('button[data-testid="hosted-payment-submit-button"]') ||
|
||||||
if (addressFields.length > 0) {
|
document.querySelector('button[type="submit"]');
|
||||||
await addressFields[0].type('Macau', { delay: 100 });
|
|
||||||
if (addressFields[1]) {
|
if (!button) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
return { found: false };
|
||||||
await addressFields[1].type('Macao', { delay: 100 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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}"`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user