dasdasd
This commit is contained in:
parent
753ed9bda7
commit
331fd8e4bd
1078
src/automation-framework/README.md
Normal file
1078
src/automation-framework/README.md
Normal file
File diff suppressed because it is too large
Load Diff
147
src/automation-framework/actions/extract-action.js
Normal file
147
src/automation-framework/actions/extract-action.js
Normal file
@ -0,0 +1,147 @@
|
||||
const BaseAction = require('../core/base-action');
|
||||
|
||||
/**
|
||||
* 数据提取动作 - 从页面提取数据并保存到上下文
|
||||
*
|
||||
* Example:
|
||||
* - action: extract
|
||||
* name: Extract quota info
|
||||
* selector: p.caption1
|
||||
* extractType: text
|
||||
* regex: (\\d+)\\s*\\/\\s*(\\d+)
|
||||
* saveTo:
|
||||
* used: $1
|
||||
* total: $2
|
||||
* contextKey: quotaInfo
|
||||
*/
|
||||
class ExtractAction extends BaseAction {
|
||||
async execute() {
|
||||
const {
|
||||
selector,
|
||||
extractType = 'text',
|
||||
regex,
|
||||
saveTo,
|
||||
contextKey,
|
||||
filter,
|
||||
multiple = false,
|
||||
required = true
|
||||
} = this.config;
|
||||
|
||||
if (!selector) {
|
||||
throw new Error('Extract action 需要 selector 参数');
|
||||
}
|
||||
|
||||
this.log('debug', `提取数据: ${selector}`);
|
||||
|
||||
try {
|
||||
// 在页面中查找并提取数据
|
||||
const extractedData = await this.page.evaluate((config) => {
|
||||
const { selector, extractType, filter, multiple } = config;
|
||||
|
||||
// 查找元素
|
||||
let elements = Array.from(document.querySelectorAll(selector));
|
||||
|
||||
// 过滤元素
|
||||
if (filter) {
|
||||
if (filter.contains) {
|
||||
elements = elements.filter(el =>
|
||||
el.textContent.includes(filter.contains)
|
||||
);
|
||||
}
|
||||
if (filter.notContains) {
|
||||
elements = elements.filter(el =>
|
||||
!el.textContent.includes(filter.notContains)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 提取数据
|
||||
const extractFrom = (element) => {
|
||||
switch (extractType) {
|
||||
case 'text':
|
||||
return element.textContent.trim();
|
||||
case 'html':
|
||||
return element.innerHTML;
|
||||
case 'attribute':
|
||||
return element.getAttribute(config.attribute);
|
||||
case 'value':
|
||||
return element.value;
|
||||
default:
|
||||
return element.textContent.trim();
|
||||
}
|
||||
};
|
||||
|
||||
if (multiple) {
|
||||
return elements.map(extractFrom);
|
||||
} else {
|
||||
return extractFrom(elements[0]);
|
||||
}
|
||||
}, { selector, extractType, filter, multiple, attribute: this.config.attribute });
|
||||
|
||||
if (extractedData === null) {
|
||||
if (required) {
|
||||
throw new Error(`未找到匹配的元素: ${selector}`);
|
||||
} else {
|
||||
this.log('warn', `未找到元素: ${selector},跳过提取`);
|
||||
return { success: true, data: null };
|
||||
}
|
||||
}
|
||||
|
||||
this.log('debug', `提取到原始数据: ${JSON.stringify(extractedData)}`);
|
||||
|
||||
// 应用正则表达式
|
||||
let processedData = extractedData;
|
||||
if (regex && typeof extractedData === 'string') {
|
||||
const regexObj = new RegExp(regex);
|
||||
const match = extractedData.match(regexObj);
|
||||
|
||||
if (match) {
|
||||
// 如果有 saveTo 配置,使用捕获组
|
||||
if (saveTo && typeof saveTo === 'object') {
|
||||
processedData = {};
|
||||
for (const [key, value] of Object.entries(saveTo)) {
|
||||
// $1, $2 等替换为捕获组
|
||||
if (typeof value === 'string' && value.startsWith('$')) {
|
||||
const groupIndex = parseInt(value.substring(1));
|
||||
processedData[key] = match[groupIndex] || null;
|
||||
} else {
|
||||
processedData[key] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 返回第一个捕获组或整个匹配
|
||||
processedData = match[1] || match[0];
|
||||
}
|
||||
} else if (required) {
|
||||
throw new Error(`正则表达式不匹配: ${regex}`);
|
||||
} else {
|
||||
this.log('warn', `正则表达式不匹配: ${regex}`);
|
||||
processedData = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到上下文
|
||||
if (contextKey && processedData !== null) {
|
||||
if (!this.context.data) {
|
||||
this.context.data = {};
|
||||
}
|
||||
this.context.data[contextKey] = processedData;
|
||||
this.log('info', `✓ 数据已保存到 context.${contextKey}`);
|
||||
}
|
||||
|
||||
this.log('debug', `处理后的数据: ${JSON.stringify(processedData)}`);
|
||||
|
||||
return { success: true, data: processedData };
|
||||
|
||||
} catch (error) {
|
||||
this.log('error', `数据提取失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ExtractAction;
|
||||
240
src/automation-framework/actions/verify-action.js
Normal file
240
src/automation-framework/actions/verify-action.js
Normal file
@ -0,0 +1,240 @@
|
||||
const BaseAction = require('../core/base-action');
|
||||
|
||||
/**
|
||||
* 验证动作 - 检测页面状态并根据结果采取行动
|
||||
*
|
||||
* 用途:验证操作结果(如支付成功/失败),支持轮询检测
|
||||
*
|
||||
* Example:
|
||||
* - action: verify
|
||||
* name: Verify payment result
|
||||
* conditions:
|
||||
* success:
|
||||
* - urlNotContains: stripe.com
|
||||
* - elementExists: .payment-success
|
||||
* failure:
|
||||
* - elementExists: .error-message
|
||||
* - textContains: declined
|
||||
* timeout: 10000
|
||||
* pollInterval: 500
|
||||
* onFailure: throw
|
||||
*/
|
||||
class VerifyAction extends BaseAction {
|
||||
async execute() {
|
||||
const {
|
||||
conditions,
|
||||
timeout = 10000,
|
||||
pollInterval = 500,
|
||||
onSuccess = 'continue',
|
||||
onFailure = 'throw',
|
||||
onTimeout = 'throw'
|
||||
} = this.config;
|
||||
|
||||
if (!conditions) {
|
||||
throw new Error('Verify action 需要 conditions 参数');
|
||||
}
|
||||
|
||||
this.log('debug', '开始验证...');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
// 检查成功条件
|
||||
if (conditions.success) {
|
||||
const successResult = await this.checkConditions(conditions.success);
|
||||
if (successResult.matched) {
|
||||
this.log('success', `✓ 验证成功: ${successResult.reason}`);
|
||||
return this.handleResult('success', onSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查失败条件
|
||||
if (conditions.failure) {
|
||||
const failureResult = await this.checkConditions(conditions.failure);
|
||||
if (failureResult.matched) {
|
||||
this.log('error', `✗ 验证失败: ${failureResult.reason}`);
|
||||
return this.handleResult('failure', onFailure, failureResult.reason);
|
||||
}
|
||||
}
|
||||
|
||||
// 等待后继续轮询
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
|
||||
// 超时
|
||||
this.log('warn', `⚠ 验证超时(${timeout}ms)`);
|
||||
return this.handleResult('timeout', onTimeout, '验证超时');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查条件组(任一满足即可)
|
||||
*/
|
||||
async checkConditions(conditionList) {
|
||||
if (!Array.isArray(conditionList)) {
|
||||
conditionList = [conditionList];
|
||||
}
|
||||
|
||||
for (const condition of conditionList) {
|
||||
const result = await this.checkSingleCondition(condition);
|
||||
if (result.matched) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return { matched: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查单个条件
|
||||
*/
|
||||
async checkSingleCondition(condition) {
|
||||
// 条件类型1: urlContains / urlNotContains
|
||||
if (condition.urlContains !== undefined) {
|
||||
const currentUrl = this.page.url();
|
||||
const matched = currentUrl.includes(condition.urlContains);
|
||||
return {
|
||||
matched,
|
||||
reason: matched ? `URL 包含 "${condition.urlContains}"` : null
|
||||
};
|
||||
}
|
||||
|
||||
if (condition.urlNotContains !== undefined) {
|
||||
const currentUrl = this.page.url();
|
||||
const matched = !currentUrl.includes(condition.urlNotContains);
|
||||
return {
|
||||
matched,
|
||||
reason: matched ? `URL 不包含 "${condition.urlNotContains}"` : null
|
||||
};
|
||||
}
|
||||
|
||||
// 条件类型2: urlEquals
|
||||
if (condition.urlEquals !== undefined) {
|
||||
const currentUrl = this.page.url();
|
||||
const matched = currentUrl === condition.urlEquals;
|
||||
return {
|
||||
matched,
|
||||
reason: matched ? `URL 等于 "${condition.urlEquals}"` : null
|
||||
};
|
||||
}
|
||||
|
||||
// 条件类型3: elementExists / elementNotExists
|
||||
if (condition.elementExists !== undefined) {
|
||||
const element = await this.page.$(condition.elementExists);
|
||||
const matched = !!element;
|
||||
return {
|
||||
matched,
|
||||
reason: matched ? `元素存在: ${condition.elementExists}` : null
|
||||
};
|
||||
}
|
||||
|
||||
if (condition.elementNotExists !== undefined) {
|
||||
const element = await this.page.$(condition.elementNotExists);
|
||||
const matched = !element;
|
||||
return {
|
||||
matched,
|
||||
reason: matched ? `元素不存在: ${condition.elementNotExists}` : null
|
||||
};
|
||||
}
|
||||
|
||||
// 条件类型4: elementVisible / elementHidden
|
||||
if (condition.elementVisible !== undefined) {
|
||||
const visible = await this.page.evaluate((selector) => {
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) return false;
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
||||
}, condition.elementVisible);
|
||||
|
||||
return {
|
||||
matched: visible,
|
||||
reason: visible ? `元素可见: ${condition.elementVisible}` : null
|
||||
};
|
||||
}
|
||||
|
||||
if (condition.elementHidden !== undefined) {
|
||||
const hidden = await this.page.evaluate((selector) => {
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) return true;
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0';
|
||||
}, condition.elementHidden);
|
||||
|
||||
return {
|
||||
matched: hidden,
|
||||
reason: hidden ? `元素隐藏: ${condition.elementHidden}` : null
|
||||
};
|
||||
}
|
||||
|
||||
// 条件类型5: textContains / textNotContains
|
||||
if (condition.textContains !== undefined) {
|
||||
const hasText = await this.page.evaluate((text) => {
|
||||
return document.body.textContent.includes(text);
|
||||
}, condition.textContains);
|
||||
|
||||
return {
|
||||
matched: hasText,
|
||||
reason: hasText ? `页面包含文本: "${condition.textContains}"` : null
|
||||
};
|
||||
}
|
||||
|
||||
if (condition.textNotContains !== undefined) {
|
||||
const hasText = await this.page.evaluate((text) => {
|
||||
return document.body.textContent.includes(text);
|
||||
}, condition.textNotContains);
|
||||
|
||||
return {
|
||||
matched: !hasText,
|
||||
reason: !hasText ? `页面不包含文本: "${condition.textNotContains}"` : null
|
||||
};
|
||||
}
|
||||
|
||||
// 条件类型6: elementTextContains
|
||||
if (condition.elementTextContains !== undefined) {
|
||||
const { selector, text } = condition.elementTextContains;
|
||||
const hasText = await this.page.evaluate((sel, txt) => {
|
||||
const el = document.querySelector(sel);
|
||||
return el && el.textContent.includes(txt);
|
||||
}, selector, text);
|
||||
|
||||
return {
|
||||
matched: hasText,
|
||||
reason: hasText ? `元素 ${selector} 包含文本 "${text}"` : null
|
||||
};
|
||||
}
|
||||
|
||||
// 条件类型7: custom - 自定义 JS 函数
|
||||
if (condition.custom !== undefined) {
|
||||
const matched = await this.page.evaluate(condition.custom);
|
||||
return {
|
||||
matched,
|
||||
reason: matched ? '自定义条件满足' : null
|
||||
};
|
||||
}
|
||||
|
||||
return { matched: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理验证结果
|
||||
*/
|
||||
handleResult(resultType, action, reason = '') {
|
||||
switch (action) {
|
||||
case 'continue':
|
||||
// 继续执行,不做任何事
|
||||
return { success: true, result: resultType };
|
||||
|
||||
case 'throw':
|
||||
// 抛出异常,触发重试或错误处理
|
||||
throw new Error(`验证${resultType}: ${reason}`);
|
||||
|
||||
case 'return':
|
||||
// 返回结果,由调用者处理
|
||||
return { success: resultType === 'success', result: resultType, reason };
|
||||
|
||||
default:
|
||||
return { success: true, result: resultType };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VerifyAction;
|
||||
@ -131,9 +131,32 @@ workflow:
|
||||
- text: "Get started"
|
||||
waitAfter: 2000
|
||||
|
||||
# ==================== 步骤 6: 填写支付信息(带重试) ====================
|
||||
# ==================== 步骤 6: 填写支付信息 ====================
|
||||
# 6.1 选择银行卡支付方式
|
||||
- action: click
|
||||
name: "选择银行卡支付"
|
||||
selector:
|
||||
- css: 'input[type="radio"][value="card"]'
|
||||
waitAfter: 3000
|
||||
|
||||
# 6.2 等待支付表单加载
|
||||
- action: wait
|
||||
name: "等待支付表单"
|
||||
type: element
|
||||
find:
|
||||
- css: '#cardNumber'
|
||||
timeout: 30000
|
||||
|
||||
# 6.3 处理 hCaptcha(只需处理一次)
|
||||
- action: custom
|
||||
name: "hCaptcha 验证"
|
||||
handler: "handleHCaptcha"
|
||||
params:
|
||||
timeout: 120000
|
||||
|
||||
# ==================== 步骤 6.4: 提交支付(带重试) ====================
|
||||
- action: retryBlock
|
||||
name: "支付流程"
|
||||
name: "提交支付并验证"
|
||||
maxRetries: 5
|
||||
retryDelay: 2000
|
||||
onRetryBefore:
|
||||
@ -142,22 +165,7 @@ workflow:
|
||||
handler: "regenerateCard"
|
||||
|
||||
steps:
|
||||
# 6.1 选择银行卡支付方式
|
||||
- action: click
|
||||
name: "选择银行卡支付"
|
||||
selector:
|
||||
- css: 'input[type="radio"][value="card"]'
|
||||
waitAfter: 3000
|
||||
|
||||
# 6.2 等待支付表单加载
|
||||
- action: wait
|
||||
name: "等待支付表单"
|
||||
type: element
|
||||
find:
|
||||
- css: '#cardNumber'
|
||||
timeout: 30000
|
||||
|
||||
# 6.3 填写银行卡信息(使用重新生成的卡号)
|
||||
# 填写银行卡信息(使用重新生成的卡号)
|
||||
- action: fillForm
|
||||
name: "填写银行卡信息"
|
||||
humanLike: false
|
||||
@ -182,27 +190,70 @@ workflow:
|
||||
- css: '#billingName'
|
||||
value: "{{account.firstName}} {{account.lastName}}"
|
||||
|
||||
# 6.4 选择澳门地址(动态字段,需要自定义处理)
|
||||
# 选择澳门地址(动态字段,需要自定义处理)
|
||||
- action: custom
|
||||
name: "选择澳门地址"
|
||||
handler: "selectBillingAddress"
|
||||
|
||||
# 6.5 处理 hCaptcha
|
||||
- action: custom
|
||||
name: "hCaptcha 验证"
|
||||
handler: "handleHCaptcha"
|
||||
params:
|
||||
timeout: 120000
|
||||
# 提交支付
|
||||
- action: click
|
||||
name: "点击提交支付"
|
||||
selector:
|
||||
- css: 'button[type="submit"]'
|
||||
- text: '订阅'
|
||||
- text: 'Subscribe'
|
||||
waitAfter: 2000
|
||||
|
||||
# 6.6 提交支付并检查结果(失败会触发重试)
|
||||
- action: custom
|
||||
name: "提交并验证支付"
|
||||
handler: "submitAndVerifyPayment"
|
||||
# 验证支付结果(轮询检测成功或失败)
|
||||
- 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: "getSubscriptionInfo"
|
||||
name: "处理订阅数据"
|
||||
handler: "processSubscriptionData"
|
||||
optional: true
|
||||
|
||||
# ==================== 步骤 8: 保存到数据库 ====================
|
||||
|
||||
@ -7,6 +7,8 @@ const ClickAction = require('../actions/click-action');
|
||||
const WaitAction = require('../actions/wait-action');
|
||||
const CustomAction = require('../actions/custom-action');
|
||||
const RetryBlockAction = require('../actions/retry-block-action');
|
||||
const ExtractAction = require('../actions/extract-action');
|
||||
const VerifyAction = require('../actions/verify-action');
|
||||
|
||||
/**
|
||||
* 工作流引擎 - 执行配置驱动的自动化流程
|
||||
@ -32,6 +34,8 @@ class WorkflowEngine {
|
||||
this.actionRegistry.register('wait', WaitAction);
|
||||
this.actionRegistry.register('custom', CustomAction);
|
||||
this.actionRegistry.register('retryBlock', RetryBlockAction);
|
||||
this.actionRegistry.register('extract', ExtractAction);
|
||||
this.actionRegistry.register('verify', VerifyAction);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -419,76 +419,31 @@ class WindsurfAdapter extends SiteAdapter {
|
||||
}
|
||||
|
||||
/**
|
||||
* 步骤 7: 获取订阅信息
|
||||
* 处理订阅数据 - 从 extract action 提取的原始数据中构造结构化对象
|
||||
*/
|
||||
async getSubscriptionInfo(params) {
|
||||
this.log('info', '获取订阅信息');
|
||||
async processSubscriptionData(params) {
|
||||
this.log('info', '处理订阅数据...');
|
||||
|
||||
try {
|
||||
// 关闭可能存在的弹窗
|
||||
this.log('info', '关闭可能存在的对话框...');
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
await this.page.keyboard.press('Escape');
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
} catch (e) {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
// 处理配额信息(从多个 span 中提取)
|
||||
const quotaRaw = this.context.data.quotaRaw;
|
||||
let quotaInfo = null;
|
||||
|
||||
// 跳转到订阅使用页面
|
||||
this.log('info', '跳转到订阅使用页面...');
|
||||
await this.page.goto('https://windsurf.com/subscription/usage', {
|
||||
waitUntil: 'networkidle2',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
// 等待页面加载
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
// 获取配额信息
|
||||
this.log('info', '获取配额信息...');
|
||||
const quotaInfo = await this.page.evaluate(() => {
|
||||
const quotaElement = document.querySelector('p.caption1.font-medium.text-sk-black\\/80');
|
||||
if (!quotaElement) return null;
|
||||
|
||||
const spans = quotaElement.querySelectorAll('span.caption3 span');
|
||||
if (spans.length >= 2) {
|
||||
const used = spans[0].textContent.trim();
|
||||
const total = spans[1].textContent.trim().replace('/', '').trim();
|
||||
return { used, total };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (quotaInfo) {
|
||||
this.log('success', `✓ 配额: ${quotaInfo.used} / ${quotaInfo.total} used`);
|
||||
if (quotaRaw && Array.isArray(quotaRaw) && quotaRaw.length >= 2) {
|
||||
quotaInfo = {
|
||||
used: quotaRaw[0].trim(),
|
||||
total: quotaRaw[1].trim().replace('/', '').trim()
|
||||
};
|
||||
this.context.data.quotaInfo = quotaInfo;
|
||||
this.log('success', `✓ 配额: ${quotaInfo.used} / ${quotaInfo.total} used`);
|
||||
} else {
|
||||
this.log('warn', '未找到配额信息');
|
||||
}
|
||||
|
||||
// 获取账单周期信息
|
||||
this.log('info', '获取账单周期信息...');
|
||||
const billingInfo = await this.page.evaluate(() => {
|
||||
const billingElement = Array.from(document.querySelectorAll('p.caption1'))
|
||||
.find(p => p.textContent.includes('Next billing cycle'));
|
||||
|
||||
if (!billingElement) return null;
|
||||
|
||||
const daysMatch = billingElement.textContent.match(/(\d+)\s+days?/);
|
||||
const dateMatch = billingElement.textContent.match(/on\s+([A-Za-z]+\s+\d+,\s+\d{4})/);
|
||||
|
||||
return {
|
||||
days: daysMatch ? daysMatch[1] : null,
|
||||
date: dateMatch ? dateMatch[1] : null,
|
||||
fullText: billingElement.textContent.trim()
|
||||
};
|
||||
});
|
||||
|
||||
// billingInfo 已由 extract action 处理好
|
||||
const billingInfo = this.context.data.billingInfo;
|
||||
if (billingInfo && billingInfo.days) {
|
||||
this.log('success', `✓ 下次账单: ${billingInfo.days} 天后 (${billingInfo.date})`);
|
||||
this.context.data.billingInfo = billingInfo;
|
||||
this.log('success', `✓ 下次账单: ${billingInfo.days} 天后 (${billingInfo.date || 'N/A'})`);
|
||||
} else {
|
||||
this.log('warn', '未找到账单周期信息');
|
||||
}
|
||||
@ -496,11 +451,12 @@ class WindsurfAdapter extends SiteAdapter {
|
||||
// 打印汇总信息
|
||||
this.log('success', `✓ 配额: ${quotaInfo ? `${quotaInfo.used}/${quotaInfo.total}` : 'N/A'} | 下次账单: ${billingInfo?.days || 'N/A'}天后`);
|
||||
|
||||
return { success: true, quotaInfo, billingInfo };
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
this.log('error', `获取订阅信息失败: ${error.message}`);
|
||||
throw error;
|
||||
this.log('error', `处理订阅数据失败: ${error.message}`);
|
||||
// 不抛出异常,允许流程继续
|
||||
return { success: true, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user