fix(plus): 增强 checkout 免费试用校验

(cherry picked from commit c52f5058a4b5b68902bccb4d1f099d00c471b7bc)
This commit is contained in:
朴圣佑
2026-04-26 22:22:44 +08:00
committed by QLHazyCoder
parent 75e259ae5c
commit aa4e52b9a7
6 changed files with 652 additions and 59 deletions
+39 -6
View File
@@ -23,6 +23,7 @@
getState,
hasSavedProgress,
isAddPhoneAuthFailure,
isPlusCheckoutNonFreeTrialFailure,
isRestartCurrentAttemptError,
isSignupUserAlreadyExistsFailure,
isStopError,
@@ -502,15 +503,12 @@
const reason = getErrorMessage(err);
roundSummary.failureReasons.push(reason);
const blockedByAddPhone = typeof isAddPhoneAuthFailure === 'function' && isAddPhoneAuthFailure(err);
const blockedByPhoneSupplyExhausted = isPhoneNumberSupplyExhaustedFailure(err);
const blockedByPlusNonFreeTrial = typeof isPlusCheckoutNonFreeTrialFailure === 'function'
&& isPlusCheckoutNonFreeTrialFailure(err);
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
&& !keepSameEmailUntilAddPhone
&& isSignupUserAlreadyExistsFailure(err);
const canRetry = !blockedByAddPhone
&& !blockedByPhoneSupplyExhausted
&& !blockedBySignupUserAlreadyExists
&& autoRunSkipFailures
&& attemptRun < maxAttemptsForRound;
const canRetry = !blockedByAddPhone && !blockedByPlusNonFreeTrial && !blockedBySignupUserAlreadyExists && autoRunSkipFailures && attemptRun < maxAttemptsForRound;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
@@ -551,6 +549,41 @@
break;
}
if (blockedByPlusNonFreeTrial) {
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await appendRoundRecordIfNeeded('failed', reason);
cancelPendingCommands('当前轮因 Plus 免费试用资格不可用已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
await addLog(
`${targetRun}/${totalRuns} 轮检测到 Plus 今日应付金额非 0,自动重试未开启,当前自动运行将停止。`,
'warn'
);
stoppedEarly = true;
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
await addLog(`${targetRun}/${totalRuns} 轮没有 Plus 免费试用资格,本轮将直接失败并跳过剩余重试。`, 'warn');
await addLog(
targetRun < totalRuns
? `${targetRun}/${totalRuns} 轮因 Plus 今日应付金额非 0 提前结束,自动流程将继续下一轮。`
: `${targetRun}/${totalRuns} 轮因 Plus 今日应付金额非 0 提前结束,已无后续轮次,本次自动运行结束。`,
'warn'
);
forceFreshTabsNextRun = true;
break;
}
if (blockedBySignupUserAlreadyExists) {
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
+115 -23
View File
@@ -5,6 +5,8 @@
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/plus-checkout.js'];
const PLUS_CHECKOUT_URL_PATTERN = /^https:\/\/chatgpt\.com\/checkout(?:\/|$)/i;
const PLUS_CHECKOUT_FRAME_READY_DELAY_MS = 500;
const PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS = 3;
const PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS = 20000;
const MEIGUODIZHI_ADDRESS_ENDPOINT = 'https://www.meiguodizhi.com/api/v1/dz';
const MEIGUODIZHI_COUNTRY_CONFIG = {
AR: { path: '/ar-address', city: 'Buenos Aires', aliases: ['ar', 'argentina', '阿根廷'] },
@@ -42,10 +44,10 @@
getAddressSeedForCountry,
getTabId,
isTabAlive,
markCurrentRegistrationAccountUsed,
setState,
sleepWithStop,
waitForTabCompleteUntilStopped,
waitForTabUrlMatchUntilStopped,
} = deps;
function isPlusCheckoutUrl(url = '') {
@@ -294,6 +296,28 @@
});
}
async function waitForPayPalRedirectAfterSubmit(tabId) {
const startedAt = Date.now();
while (Date.now() - startedAt < PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS) {
const tab = await chrome.tabs.get(tabId).catch(() => null);
if (!tab) {
throw new Error('步骤 7checkout 标签页已关闭,无法继续等待 PayPal 跳转。');
}
const url = String(tab.url || '');
if (/paypal\./i.test(url)) {
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(1000);
return true;
}
if (url && !isPlusCheckoutUrl(url)) {
await addLog(`步骤 7:点击订阅后页面跳转到非 PayPal 地址:${url}`, 'warn');
return false;
}
await sleepWithStop(500);
}
return false;
}
async function inspectCheckoutFrame(tabId, frame) {
try {
const result = await sendFrameMessage(tabId, frame.frameId, {
@@ -366,6 +390,44 @@
|| null;
}
function findCheckoutAmountInspection(inspections = []) {
return inspections.find((item) => item.result?.checkoutAmountSummary?.hasTodayDue)
|| null;
}
async function inspectCheckoutAmountSummary(tabId) {
const frames = await getReadyCheckoutFrames(tabId);
const inspections = await inspectCheckoutFrames(tabId, frames);
const amountInspection = findCheckoutAmountInspection(inspections);
return amountInspection?.result?.checkoutAmountSummary || null;
}
async function ensureFreeTrialAmount(tabId, state = {}, options = {}) {
const phaseLabel = String(options.phaseLabel || '').trim() || '提交前';
const amountSummary = await inspectCheckoutAmountSummary(tabId);
if (!amountSummary?.hasTodayDue) {
await addLog(`步骤 7${phaseLabel}未能识别 checkout 的“今日应付金额”,为避免误判将继续执行。`, 'warn');
return;
}
if (amountSummary.isZero) {
await addLog(`步骤 7${phaseLabel}已确认今日应付金额为 ${amountSummary.rawAmount || '0'},继续执行。`, 'ok');
return;
}
const amountLabel = amountSummary.rawAmount || (
Number.isFinite(Number(amountSummary.amount)) ? String(amountSummary.amount) : '未知金额'
);
await addLog(`步骤 7${phaseLabel}检测到今日应付金额不是 0${amountLabel}),说明当前账号没有免费试用资格,将跳过 PayPal 提交。`, 'warn');
if (typeof markCurrentRegistrationAccountUsed === 'function') {
await markCurrentRegistrationAccountUsed(state, {
reason: 'plus-checkout-non-free-trial',
logPrefix: 'Plus Checkout:当前账号没有免费试用资格',
});
}
throw new Error(`PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 7:今日应付金额不是 0(${amountLabel}),当前账号没有免费试用资格,已跳过 PayPal 提交。`);
}
async function getReadyCheckoutFrames(tabId) {
return ensurePlusCheckoutFramesReady(tabId, await getCheckoutFrames(tabId));
}
@@ -468,6 +530,9 @@
logMessage: '步骤 7Checkout 页面仍在加载,等待账单填写脚本就绪...',
});
const readyFrames = await getReadyCheckoutFrames(tabId);
await ensureFreeTrialAmount(tabId, state, {
phaseLabel: 'Checkout 页面加载后',
});
const paymentFrame = await resolvePaymentFrame(tabId, readyFrames);
if (paymentFrame.frameId === null) {
const frameSummary = buildFrameSummary(paymentFrame.inspections);
@@ -535,8 +600,9 @@
addressSeed,
},
});
if (suggestionResult?.error) {
throw new Error(suggestionResult.error);
const suggestionError = suggestionResult?.error || '';
if (suggestionError) {
await addLog(`步骤 7:Google 地址推荐不可用,将改用本地地址字段兜底:${suggestionError}`, 'warn');
}
const structuredResult = await sendFrameMessage(tabId, billingFrame.frameId, {
@@ -544,6 +610,7 @@
source: 'background',
payload: {
addressSeed,
overwriteStructuredAddress: Boolean(suggestionError),
},
});
if (structuredResult?.error) {
@@ -552,7 +619,7 @@
result = {
...structuredResult,
selectedAddressText: suggestionResult?.selectedAddressText || '',
selectedAddressText: suggestionError ? '' : (suggestionResult?.selectedAddressText || ''),
};
} else {
result = await sendFrameMessage(tabId, billingFrame.frameId, {
@@ -569,31 +636,56 @@
}
}
await addLog('步骤 7:账单地址已填写完成,正在定位订阅按钮...', 'info');
const subscribeFrame = await waitForSubscribeFrame(tabId, [
{ frameId: 0, url: '' },
{ frameId: paymentFrame.frameId, url: paymentFrame.frameUrl || '' },
{ frameId: billingFrame.frameId, url: billingFrame.frameUrl || '' },
]);
const subscribeResult = await sendFrameMessage(tabId, subscribeFrame.frameId, {
type: 'PLUS_CHECKOUT_CLICK_SUBSCRIBE',
source: 'background',
payload: {},
});
if (subscribeResult?.error) {
throw new Error(subscribeResult.error);
}
await setState({
plusCheckoutTabId: tabId,
plusBillingCountryText: result?.countryText || '',
plusBillingAddress: result?.structuredAddress || null,
});
await ensureFreeTrialAmount(tabId, state, {
phaseLabel: '提交订阅前',
});
await addLog('步骤 7:账单地址已提交,正在等待跳转到 PayPal...', 'info');
await waitForTabUrlMatchUntilStopped(tabId, (url) => /paypal\./i.test(url));
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(1000);
let redirectedToPayPal = false;
let lastSubmitError = '';
for (let attempt = 1; attempt <= PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS; attempt += 1) {
await addLog(
attempt === 1
? '步骤 7:账单地址已填写完成,等待 3 秒让 checkout 完成校验...'
: `步骤 7:准备第 ${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS} 次重新提交账单地址...`,
attempt === 1 ? 'info' : 'warn'
);
await sleepWithStop(3000);
await addLog('步骤 7:正在定位订阅按钮...', 'info');
const subscribeFrame = await waitForSubscribeFrame(tabId, [
{ frameId: 0, url: '' },
{ frameId: paymentFrame.frameId, url: paymentFrame.frameUrl || '' },
{ frameId: billingFrame.frameId, url: billingFrame.frameUrl || '' },
]);
const subscribeResult = await sendFrameMessage(tabId, subscribeFrame.frameId, {
type: 'PLUS_CHECKOUT_CLICK_SUBSCRIBE',
source: 'background',
payload: {
beforeClickDelayMs: attempt === 1 ? 700 : 1200,
},
});
if (subscribeResult?.error) {
lastSubmitError = subscribeResult.error;
await addLog(`步骤 7:点击订阅失败(${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS}):${lastSubmitError}`, 'warn');
continue;
}
await addLog(`步骤 7:账单地址已提交,正在等待跳转到 PayPal(${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS}...`, 'info');
redirectedToPayPal = await waitForPayPalRedirectAfterSubmit(tabId);
if (redirectedToPayPal) {
break;
}
lastSubmitError = `提交后 ${Math.round(PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS / 1000)} 秒内未跳转到 PayPal`;
await addLog(`步骤 7${lastSubmitError},将重试提交。`, 'warn');
}
if (!redirectedToPayPal) {
throw new Error(`步骤 7:多次提交账单地址后仍未跳转到 PayPal。${lastSubmitError}`);
}
await completeStepFromBackground(7, {
plusBillingCountryText: result?.countryText || '',