fix: align Plus no-payment flow and step5 completion
This commit is contained in:
+194
-13
@@ -719,6 +719,7 @@ const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([
|
||||
const FIVE_SIM_OPERATOR = DEFAULT_FIVE_SIM_OPERATOR;
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||
const PLUS_PAYMENT_METHOD_NONE = 'none';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
|
||||
@@ -823,6 +824,12 @@ function normalizePlusPaymentMethod(value = '') {
|
||||
const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined'
|
||||
? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
|
||||
: 'paypal-hosted';
|
||||
const noneValue = typeof PLUS_PAYMENT_METHOD_NONE !== 'undefined'
|
||||
? PLUS_PAYMENT_METHOD_NONE
|
||||
: 'none';
|
||||
if (normalized === noneValue || normalized === 'no-payment' || normalized === 'skip-payment') {
|
||||
return noneValue;
|
||||
}
|
||||
if (normalized === paypalHostedValue || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return paypalHostedValue;
|
||||
}
|
||||
@@ -1958,6 +1965,12 @@ function normalizePlusPaymentMethod(value = '') {
|
||||
const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined'
|
||||
? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
|
||||
: 'paypal-hosted';
|
||||
const noneValue = typeof PLUS_PAYMENT_METHOD_NONE !== 'undefined'
|
||||
? PLUS_PAYMENT_METHOD_NONE
|
||||
: 'none';
|
||||
if (normalized === noneValue || normalized === 'no-payment' || normalized === 'skip-payment') {
|
||||
return noneValue;
|
||||
}
|
||||
if (normalized === paypalHostedValue || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return paypalHostedValue;
|
||||
}
|
||||
@@ -8608,6 +8621,17 @@ function isLikelyLoggedInChatgptHomeUrl(rawUrl) {
|
||||
return !/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||
}
|
||||
|
||||
function isStep5CompletionChatgptUrl(rawUrl) {
|
||||
const parsed = parseUrlSafely(rawUrl);
|
||||
if (!parsed) return false;
|
||||
const protocol = String(parsed.protocol || '').toLowerCase();
|
||||
const hostname = String(parsed.hostname || '').toLowerCase();
|
||||
if (protocol !== 'https:' || !['chatgpt.com', 'www.chatgpt.com'].includes(hostname)) {
|
||||
return false;
|
||||
}
|
||||
return !/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||
}
|
||||
|
||||
function isSignupPasswordPageUrl(rawUrl) {
|
||||
if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupPasswordPageUrl) {
|
||||
return navigationUtils.isSignupPasswordPageUrl(rawUrl);
|
||||
@@ -9372,13 +9396,9 @@ function isGpcCheckoutRestartRequiredFailure(error) {
|
||||
|
||||
function isPlusCheckoutRestartStep(step, stepExecutionKey = '', state = {}) {
|
||||
const normalizedKey = String(stepExecutionKey || '').trim();
|
||||
if (normalizedKey === 'plus-checkout-create'
|
||||
return normalizedKey === 'plus-checkout-create'
|
||||
|| normalizedKey === 'plus-checkout-billing'
|
||||
|| normalizedKey === 'gopay-subscription-confirm') {
|
||||
return true;
|
||||
}
|
||||
const numericStep = Number(step);
|
||||
return Boolean(state?.plusModeEnabled) && (numericStep === 6 || numericStep === 7);
|
||||
|| normalizedKey === 'gopay-subscription-confirm';
|
||||
}
|
||||
|
||||
function isPlusCheckoutRestartRequiredFailure(error) {
|
||||
@@ -9633,10 +9653,23 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
currentPhoneVerificationCountdownWindowTotal: 0,
|
||||
};
|
||||
}
|
||||
if (step === 5 || step === 6 || step === 7 || step === 8) {
|
||||
const normalizedStepKey = String(stepKey || '').trim();
|
||||
const isEarlyRegistrationNode = [
|
||||
'fill-profile',
|
||||
'wait-registration-success',
|
||||
'plus-checkout-create',
|
||||
].includes(normalizedStepKey);
|
||||
const isBillingNode = normalizedStepKey === 'plus-checkout-billing';
|
||||
const isApprovalNode = normalizedStepKey === 'paypal-approve'
|
||||
|| normalizedStepKey === 'gopay-subscription-confirm'
|
||||
|| normalizedStepKey === 'paypal-hosted-email'
|
||||
|| normalizedStepKey === 'paypal-hosted-card'
|
||||
|| normalizedStepKey === 'paypal-hosted-create-account'
|
||||
|| normalizedStepKey === 'paypal-hosted-review';
|
||||
if (isEarlyRegistrationNode || isBillingNode || isApprovalNode) {
|
||||
return {
|
||||
...(step <= 6 ? plusRuntimeResets : {}),
|
||||
...(step === 7 ? {
|
||||
...(isEarlyRegistrationNode ? plusRuntimeResets : {}),
|
||||
...(isBillingNode ? {
|
||||
plusBillingCountryText: '',
|
||||
plusBillingAddress: null,
|
||||
plusPaypalApprovedAt: null,
|
||||
@@ -9653,7 +9686,7 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
gopayHelperOtpRequestId: '',
|
||||
gopayHelperOtpReferenceId: '',
|
||||
} : {}),
|
||||
...(step === 8 ? {
|
||||
...(isApprovalNode ? {
|
||||
plusPaypalApprovedAt: null,
|
||||
plusGoPayApprovedAt: null,
|
||||
plusReturnUrl: '',
|
||||
@@ -9670,7 +9703,7 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
currentPhoneVerificationCountdownWindowTotal: 0,
|
||||
};
|
||||
}
|
||||
if (step === 9) {
|
||||
if (stepKey === 'plus-checkout-return' || stepKey === 'confirm-oauth') {
|
||||
return {
|
||||
pendingPhoneActivationConfirmation: null,
|
||||
plusReturnUrl: '',
|
||||
@@ -11556,6 +11589,13 @@ async function executeNodeAndWait(nodeId, delayAfter = 2000) {
|
||||
const completionSignalTimeoutMs = getNodeCompletionSignalTimeoutMs(normalizedNodeId, executionState);
|
||||
await addLog(`自动运行:节点 ${normalizedNodeId} 已发起,正在等待完成信号(超时 ${Math.round(completionSignalTimeoutMs / 1000)} 秒)。`, 'info');
|
||||
completionPayload = await executeNodeViaCompletionSignal(normalizedNodeId, completionSignalTimeoutMs);
|
||||
if (normalizedNodeId === 'fill-profile') {
|
||||
await addLog(
|
||||
`步骤 5 [调试] 已收到资料页完成信号 | outcome=${String(completionPayload?.outcome || 'none')} | navigationStarted=${Boolean(completionPayload?.navigationStarted)} | url=${String(completionPayload?.url || '') || 'unknown'}`,
|
||||
'info',
|
||||
{ step: 5, stepKey: 'fill-profile' }
|
||||
);
|
||||
}
|
||||
await addLog(`自动运行:节点 ${normalizedNodeId} 已收到完成信号,准备继续后续节点。`, 'info');
|
||||
} else {
|
||||
await executeNode(normalizedNodeId);
|
||||
@@ -11573,6 +11613,12 @@ async function executeNodeAndWait(nodeId, delayAfter = 2000) {
|
||||
});
|
||||
try {
|
||||
await validateStep5PostCompletion(signupTabId, completionPayload || {});
|
||||
await setNodeStatus(normalizedNodeId, 'completed');
|
||||
await addLog('已完成', 'ok', { nodeId: normalizedNodeId });
|
||||
await addLog('步骤 5 [调试] 资料页完成信号已通过后台复核。', 'ok', {
|
||||
step: 5,
|
||||
stepKey: 'fill-profile',
|
||||
});
|
||||
} catch (step5ValidationError) {
|
||||
await setNodeStatus(normalizedNodeId, 'failed');
|
||||
await addLog(`失败:${getErrorMessage(step5ValidationError)}`, 'error', { nodeId: normalizedNodeId });
|
||||
@@ -13548,6 +13594,7 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
|
||||
resolveSignupEmailForFlow,
|
||||
persistRegistrationEmailState,
|
||||
phoneVerificationHelpers,
|
||||
getStepIdByKeyForState,
|
||||
rerunStep7ForStep8Recovery: (...args) => rerunStep7ForStep8Recovery(...args),
|
||||
resolveSignupMethod,
|
||||
reuseOrCreateTab,
|
||||
@@ -13942,6 +13989,21 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
|
||||
3
|
||||
);
|
||||
},
|
||||
finalizeStep5Completion: async (completionPayload = {}) => {
|
||||
const signupTabId = await getTabId('openai-auth');
|
||||
if (!signupTabId) {
|
||||
throw new Error('步骤 5:缺少认证页标签页,无法确认是否已跳转到 https://chatgpt.com。');
|
||||
}
|
||||
await waitForTabStableComplete(signupTabId, {
|
||||
timeoutMs: 120000,
|
||||
retryDelayMs: 300,
|
||||
stableMs: 1000,
|
||||
initialDelayMs: 800,
|
||||
});
|
||||
await validateStep5PostCompletion(signupTabId, completionPayload || {});
|
||||
await setNodeStatus('fill-profile', 'completed');
|
||||
await addLog('已完成', 'ok', { nodeId: 'fill-profile' });
|
||||
},
|
||||
finalizeIcloudAliasAfterSuccessfulFlow,
|
||||
findHotmailAccount,
|
||||
flushCommand,
|
||||
@@ -14753,18 +14815,69 @@ async function recoverStep5SubmitRetryPageOnTab(options = {}) {
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function addStep5PostCompletionDebugLog(message, details = {}) {
|
||||
const pageState = details?.pageState && typeof details.pageState === 'object'
|
||||
? details.pageState
|
||||
: null;
|
||||
const summary = [
|
||||
`步骤 5 [调试] ${message}`,
|
||||
details?.completionOutcome ? `completionOutcome=${details.completionOutcome}` : null,
|
||||
`navigationStarted=${Boolean(details?.navigationStarted)}`,
|
||||
details?.tabUrl ? `tabUrl=${details.tabUrl}` : null,
|
||||
details?.completionUrl ? `completionUrl=${details.completionUrl}` : null,
|
||||
pageState?.url ? `contentUrl=${pageState.url}` : null,
|
||||
pageState ? `retryPage=${Boolean(pageState.retryPage)}` : null,
|
||||
pageState ? `retryEnabled=${Boolean(pageState.retryEnabled)}` : null,
|
||||
pageState ? `successState=${pageState.successState || 'none'}` : null,
|
||||
pageState ? `profileVisible=${Boolean(pageState.profileVisible)}` : null,
|
||||
pageState ? `unknownAuthPage=${Boolean(pageState.unknownAuthPage)}` : null,
|
||||
pageState ? `maxCheckAttemptsBlocked=${Boolean(pageState.maxCheckAttemptsBlocked)}` : null,
|
||||
pageState ? `userAlreadyExistsBlocked=${Boolean(pageState.userAlreadyExistsBlocked)}` : null,
|
||||
pageState?.errorText ? `errorText=${pageState.errorText}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' | ');
|
||||
|
||||
await addLog(summary, details?.level || 'info', {
|
||||
step: 5,
|
||||
stepKey: 'fill-profile',
|
||||
});
|
||||
}
|
||||
|
||||
async function validateStep5PostCompletion(tabId, completionPayload = {}) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
throw new Error('步骤 5:缺少有效的资料页标签页,无法确认提交后的最终状态。');
|
||||
}
|
||||
const debugLog = typeof addStep5PostCompletionDebugLog === 'function'
|
||||
? addStep5PostCompletionDebugLog
|
||||
: async (message, details = {}) => {
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(`步骤 5 [调试] ${message}`, details?.level || 'info', {
|
||||
step: 5,
|
||||
stepKey: 'fill-profile',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const maxAuthRetryRecoveries = Math.max(1, Number(completionPayload?.maxAuthRetryRecoveries) || 2);
|
||||
let authRetryRecoveryCount = 0;
|
||||
await debugLog('后台已收到资料页完成信号,准备开始最终状态复核。', {
|
||||
completionOutcome: String(completionPayload?.outcome || '').trim(),
|
||||
completionUrl: String(completionPayload?.url || '').trim(),
|
||||
navigationStarted: Boolean(completionPayload?.navigationStarted),
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||||
const currentUrl = String(tab?.url || completionPayload?.url || '').trim();
|
||||
if (currentUrl && isLikelyLoggedInChatgptHomeUrl(currentUrl)) {
|
||||
if (currentUrl && isStep5CompletionChatgptUrl(currentUrl)) {
|
||||
await debugLog('后台直接通过标签页 URL 确认已进入 chatgpt.com,步骤 5 完成。', {
|
||||
completionOutcome: String(completionPayload?.outcome || '').trim(),
|
||||
completionUrl: String(completionPayload?.url || '').trim(),
|
||||
navigationStarted: Boolean(completionPayload?.navigationStarted),
|
||||
tabUrl: currentUrl,
|
||||
level: 'ok',
|
||||
});
|
||||
return {
|
||||
successState: 'logged_in_home',
|
||||
url: currentUrl,
|
||||
@@ -14777,6 +14890,13 @@ async function validateStep5PostCompletion(tabId, completionPayload = {}) {
|
||||
retryDelayMs: 500,
|
||||
logMessage: '步骤 5:资料提交已触发页面跳转,正在确认最终页面状态...',
|
||||
});
|
||||
await debugLog('后台复核当前页面状态。', {
|
||||
completionOutcome: String(completionPayload?.outcome || '').trim(),
|
||||
completionUrl: String(completionPayload?.url || '').trim(),
|
||||
navigationStarted: Boolean(completionPayload?.navigationStarted),
|
||||
tabUrl: currentUrl,
|
||||
pageState,
|
||||
});
|
||||
|
||||
if (pageState.userAlreadyExistsBlocked) {
|
||||
throw new Error('SIGNUP_USER_ALREADY_EXISTS::步骤 5:检测到 user_already_exists,当前轮将直接停止。');
|
||||
@@ -14790,6 +14910,14 @@ async function validateStep5PostCompletion(tabId, completionPayload = {}) {
|
||||
throw new Error(`步骤 5:资料提交后连续进入认证重试页 ${maxAuthRetryRecoveries} 次,页面仍未恢复。URL: ${pageState.url || currentUrl || 'unknown'}`);
|
||||
}
|
||||
authRetryRecoveryCount += 1;
|
||||
await debugLog(`后台复核检测到认证重试页,准备恢复(${authRetryRecoveryCount}/${maxAuthRetryRecoveries})。`, {
|
||||
completionOutcome: String(completionPayload?.outcome || '').trim(),
|
||||
completionUrl: String(completionPayload?.url || '').trim(),
|
||||
navigationStarted: Boolean(completionPayload?.navigationStarted),
|
||||
tabUrl: currentUrl,
|
||||
pageState,
|
||||
level: 'warn',
|
||||
});
|
||||
await addLog(`步骤 5:提交完成信号后检测到认证重试页,正在自动恢复(${authRetryRecoveryCount}/${maxAuthRetryRecoveries})...`, 'warn', {
|
||||
step: 5,
|
||||
stepKey: 'fill-profile',
|
||||
@@ -14808,22 +14936,74 @@ async function validateStep5PostCompletion(tabId, completionPayload = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pageState.successState === 'logged_in_home' || pageState.successState === 'oauth_consent' || pageState.successState === 'add_phone') {
|
||||
if (pageState.successState === 'logged_in_home' && isStep5CompletionChatgptUrl(pageState.url)) {
|
||||
await debugLog(`后台复核确认成功状态:${pageState.successState}`, {
|
||||
completionOutcome: String(completionPayload?.outcome || '').trim(),
|
||||
completionUrl: String(completionPayload?.url || '').trim(),
|
||||
navigationStarted: Boolean(completionPayload?.navigationStarted),
|
||||
tabUrl: currentUrl,
|
||||
pageState,
|
||||
level: 'ok',
|
||||
});
|
||||
return pageState;
|
||||
}
|
||||
|
||||
if (pageState.successState) {
|
||||
await debugLog('后台复核发现非 chatgpt.com 的步骤 5 完成候选,按未完成处理。', {
|
||||
completionOutcome: String(completionPayload?.outcome || '').trim(),
|
||||
completionUrl: String(completionPayload?.url || '').trim(),
|
||||
navigationStarted: Boolean(completionPayload?.navigationStarted),
|
||||
tabUrl: currentUrl,
|
||||
pageState,
|
||||
level: 'error',
|
||||
});
|
||||
throw new Error(`步骤 5:资料提交后尚未跳转到 https://chatgpt.com,不能标记完成。当前状态:${pageState.successState},URL: ${pageState.url || currentUrl || 'unknown'}`);
|
||||
}
|
||||
|
||||
if (pageState.errorText) {
|
||||
await debugLog('后台复核发现页面错误文本,准备按失败结束。', {
|
||||
completionOutcome: String(completionPayload?.outcome || '').trim(),
|
||||
completionUrl: String(completionPayload?.url || '').trim(),
|
||||
navigationStarted: Boolean(completionPayload?.navigationStarted),
|
||||
tabUrl: currentUrl,
|
||||
pageState,
|
||||
level: 'error',
|
||||
});
|
||||
throw new Error(`步骤 5:资料提交后页面返回错误:${pageState.errorText}。URL: ${pageState.url || currentUrl || 'unknown'}`);
|
||||
}
|
||||
|
||||
if (pageState.profileVisible) {
|
||||
await debugLog('后台复核发现页面仍停留在资料页,准备按失败结束。', {
|
||||
completionOutcome: String(completionPayload?.outcome || '').trim(),
|
||||
completionUrl: String(completionPayload?.url || '').trim(),
|
||||
navigationStarted: Boolean(completionPayload?.navigationStarted),
|
||||
tabUrl: currentUrl,
|
||||
pageState,
|
||||
level: 'error',
|
||||
});
|
||||
throw new Error(`步骤 5:资料提交完成信号已收到,但页面仍停留在资料页,当前流程将直接报错。URL: ${pageState.url || currentUrl || 'unknown'}`);
|
||||
}
|
||||
|
||||
if (pageState.unknownAuthPage) {
|
||||
await debugLog('后台复核进入未知认证页,无法确认成功。', {
|
||||
completionOutcome: String(completionPayload?.outcome || '').trim(),
|
||||
completionUrl: String(completionPayload?.url || '').trim(),
|
||||
navigationStarted: Boolean(completionPayload?.navigationStarted),
|
||||
tabUrl: currentUrl,
|
||||
pageState,
|
||||
level: 'error',
|
||||
});
|
||||
throw new Error(`步骤 5:资料提交后进入未识别的认证页,无法确认成功。URL: ${pageState.url || currentUrl || 'unknown'}`);
|
||||
}
|
||||
|
||||
await debugLog('后台复核未识别到成功或明确失败状态,准备按失败结束。', {
|
||||
completionOutcome: String(completionPayload?.outcome || '').trim(),
|
||||
completionUrl: String(completionPayload?.url || '').trim(),
|
||||
navigationStarted: Boolean(completionPayload?.navigationStarted),
|
||||
tabUrl: currentUrl,
|
||||
pageState,
|
||||
level: 'error',
|
||||
});
|
||||
throw new Error(`步骤 5:资料提交后未能确认最终状态。URL: ${pageState.url || currentUrl || 'unknown'}`);
|
||||
}
|
||||
}
|
||||
@@ -15615,6 +15795,7 @@ const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
|
||||
getStep8CallbackUrlFromNavigation,
|
||||
getStep8CallbackUrlFromTabUpdate,
|
||||
getStep8EffectLabel,
|
||||
getStepIdByKeyForState,
|
||||
getTabId,
|
||||
getWebNavCommittedListener,
|
||||
getWebNavListener,
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
testKiroRsConnection,
|
||||
finalizePhoneActivationAfterSuccessfulFlow,
|
||||
finalizeStep3Completion,
|
||||
finalizeStep5Completion = null,
|
||||
finalizeIcloudAliasAfterSuccessfulFlow,
|
||||
findHotmailAccount,
|
||||
findPayPalAccount,
|
||||
@@ -575,6 +576,12 @@
|
||||
|
||||
function normalizePlusPaymentMethodForDisplay(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'none' || normalized === 'no-payment' || normalized === 'skip-payment') {
|
||||
return 'none';
|
||||
}
|
||||
if (normalized === 'paypal-hosted' || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return 'paypal-hosted';
|
||||
}
|
||||
if (normalized === 'gpc-helper') {
|
||||
return 'gpc-helper';
|
||||
}
|
||||
@@ -583,6 +590,12 @@
|
||||
|
||||
function getPlusPaymentMethodLabel(value = '') {
|
||||
const method = normalizePlusPaymentMethodForDisplay(value);
|
||||
if (method === 'none') {
|
||||
return '无需支付';
|
||||
}
|
||||
if (method === 'paypal-hosted') {
|
||||
return 'PayPal 无卡直绑';
|
||||
}
|
||||
if (method === 'gpc-helper') {
|
||||
return 'GPC';
|
||||
}
|
||||
@@ -995,13 +1008,21 @@
|
||||
return { ok: true, error: errorMessage };
|
||||
}
|
||||
|
||||
const deferCompletionUntilBackgroundValidation = nodeId === 'fill-profile';
|
||||
const completionStateCandidate = await getState();
|
||||
const nodeIds = typeof getNodeIdsForState === 'function' ? getNodeIdsForState(completionStateCandidate) : [];
|
||||
const lastNodeId = nodeIds[nodeIds.length - 1] || '';
|
||||
const isFinalNode = nodeId === lastNodeId;
|
||||
const completionState = isFinalNode ? completionStateCandidate : null;
|
||||
await setNodeStatus(nodeId, 'completed');
|
||||
await addLog('已完成', 'ok', { nodeId });
|
||||
if (!deferCompletionUntilBackgroundValidation) {
|
||||
await setNodeStatus(nodeId, 'completed');
|
||||
await addLog('已完成', 'ok', { nodeId });
|
||||
} else {
|
||||
await addLog('步骤 5:已收到资料页完成信号,等待后台最终复核后再标记完成。', 'info', {
|
||||
step: 5,
|
||||
stepKey: nodeId,
|
||||
});
|
||||
}
|
||||
await handleStepData(resolvedStep, message.payload);
|
||||
if (isFinalNode && typeof appendAccountRunRecord === 'function') {
|
||||
await appendAccountRunRecord('success', completionState);
|
||||
@@ -1272,7 +1293,10 @@
|
||||
}
|
||||
const executionState = await getState();
|
||||
if (doesNodeUseCompletionSignal(nodeId, executionState)) {
|
||||
await executeNodeViaCompletionSignal(nodeId);
|
||||
const completionPayload = await executeNodeViaCompletionSignal(nodeId);
|
||||
if (nodeId === 'fill-profile' && typeof finalizeStep5Completion === 'function') {
|
||||
await finalizeStep5Completion(completionPayload || {});
|
||||
}
|
||||
} else {
|
||||
await executeNode(nodeId);
|
||||
}
|
||||
|
||||
@@ -225,8 +225,8 @@
|
||||
);
|
||||
const NORMAL_STEP_DEFINITIONS = STEP_DEFINITIONS;
|
||||
const PLUS_STEP_DEFINITIONS = cloneSteps(
|
||||
defaultWorkflowBuilder?.getVariantStepDefinitions
|
||||
? defaultWorkflowBuilder.getVariantStepDefinitions('plusPaypal')
|
||||
defaultWorkflowBuilder?.getModeStepDefinitions
|
||||
? defaultWorkflowBuilder.getModeStepDefinitions({ plusModeEnabled: true })
|
||||
: [],
|
||||
{ plusModeEnabled: true },
|
||||
DEFAULT_ACTIVE_FLOW_ID
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
setStep8PendingReject,
|
||||
setStep8TabUpdatedListener,
|
||||
shouldDeferStep9CallbackTimeout,
|
||||
getStepIdByKeyForState = null,
|
||||
} = deps;
|
||||
|
||||
const LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS = 240000;
|
||||
@@ -45,7 +46,17 @@
|
||||
}
|
||||
|
||||
function getAuthLoginStepForVisibleStep(visibleStep) {
|
||||
return visibleStep >= 12 ? 10 : 7;
|
||||
return visibleStep >= 12 ? Math.max(1, visibleStep - 3) : 7;
|
||||
}
|
||||
|
||||
function getAuthLoginStepForState(state = {}, visibleStep = 9) {
|
||||
const authStep = typeof getStepIdByKeyForState === 'function'
|
||||
? Number(getStepIdByKeyForState('oauth-login', state))
|
||||
: 0;
|
||||
if (Number.isInteger(authStep) && authStep > 0) {
|
||||
return authStep;
|
||||
}
|
||||
return getAuthLoginStepForVisibleStep(visibleStep);
|
||||
}
|
||||
|
||||
function addStepLog(step, message, level = 'info') {
|
||||
@@ -57,7 +68,7 @@
|
||||
let activeState = state;
|
||||
|
||||
if (!activeState.oauthUrl) {
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
const authLoginStep = getAuthLoginStepForState(activeState, visibleStep);
|
||||
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
|
||||
throwIfStopped,
|
||||
getStepIdByKeyForState = null,
|
||||
} = deps;
|
||||
let activeFetchLoginCodeStep = null;
|
||||
let activeFetchLoginCodeStepKey = 'fetch-login-code';
|
||||
@@ -93,7 +94,17 @@
|
||||
}
|
||||
|
||||
function getAuthLoginStepForVisibleStep(visibleStep) {
|
||||
return visibleStep >= 11 ? 10 : 7;
|
||||
return visibleStep >= 11 ? Math.max(1, visibleStep - 1) : 7;
|
||||
}
|
||||
|
||||
function getAuthLoginStepForState(state = {}, visibleStep = 8) {
|
||||
const authStep = typeof getStepIdByKeyForState === 'function'
|
||||
? Number(getStepIdByKeyForState('oauth-login', state))
|
||||
: 0;
|
||||
if (Number.isInteger(authStep) && authStep > 0) {
|
||||
return authStep;
|
||||
}
|
||||
return getAuthLoginStepForVisibleStep(visibleStep);
|
||||
}
|
||||
|
||||
async function getStep8ReadyTimeoutMs(actionLabel, expectedOauthUrl = '', visibleStep = 8) {
|
||||
@@ -331,7 +342,7 @@
|
||||
}
|
||||
|
||||
async function recoverStep8PollingFailure(currentState, visibleStep) {
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
const authLoginStep = getAuthLoginStepForState(currentState, visibleStep);
|
||||
try {
|
||||
const pageState = await ensureStep8VerificationPageReady({
|
||||
visibleStep,
|
||||
@@ -771,7 +782,7 @@
|
||||
await chrome.tabs.update(authTabId, { active: true });
|
||||
} else {
|
||||
if (!state.oauthUrl) {
|
||||
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`);
|
||||
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${getAuthLoginStepForState(state, visibleStep)}。`);
|
||||
}
|
||||
await reuseOrCreateTab('openai-auth', state.oauthUrl);
|
||||
}
|
||||
@@ -779,7 +790,7 @@
|
||||
throwIfStopped();
|
||||
let pageState = await ensureStep8VerificationPageReady({
|
||||
visibleStep,
|
||||
authLoginStep: getAuthLoginStepForVisibleStep(visibleStep),
|
||||
authLoginStep: getAuthLoginStepForState(state, visibleStep),
|
||||
allowPhoneVerificationPage: true,
|
||||
allowAddEmailPage: true,
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
|
||||
@@ -848,7 +859,7 @@
|
||||
return;
|
||||
} catch (err) {
|
||||
const visibleStep = getVisibleStep(currentState, 8);
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
const authLoginStep = getAuthLoginStepForState(currentState, visibleStep);
|
||||
let currentError = err;
|
||||
let retryWithoutStep7 = false;
|
||||
|
||||
|
||||
@@ -2961,6 +2961,27 @@ function isLikelyLoggedInChatgptHomeUrl(rawUrl = location.href) {
|
||||
}
|
||||
}
|
||||
|
||||
function isStep5CompletionChatgptUrl(rawUrl = location.href) {
|
||||
const url = String(rawUrl || '').trim();
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const protocol = String(parsed.protocol || '').toLowerCase();
|
||||
const host = String(parsed.hostname || '').toLowerCase();
|
||||
if (protocol !== 'https:' || !['chatgpt.com', 'www.chatgpt.com'].includes(host)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const path = String(parsed.pathname || '');
|
||||
return !/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getStep4PostVerificationState(options = {}) {
|
||||
const { ignoreVerificationVisibility = false } = options;
|
||||
// Newer auth flows can briefly render profile fields before the email-verification
|
||||
@@ -6693,43 +6714,13 @@ function getStep5PostSubmitSuccessState() {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLikelyLoggedInChatgptHomeUrl()) {
|
||||
if (isStep5CompletionChatgptUrl()) {
|
||||
return {
|
||||
state: 'logged_in_home',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof isOAuthConsentPage === 'function' && isOAuthConsentPage()) {
|
||||
return {
|
||||
state: 'oauth_consent',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof isAddPhonePageReady === 'function' && isAddPhonePageReady()) {
|
||||
return {
|
||||
state: 'add_phone',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isStep5ProfileStillVisible()) {
|
||||
try {
|
||||
const parsed = new URL(String(location.href || '').trim());
|
||||
const host = String(parsed.hostname || '').toLowerCase();
|
||||
if (['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to the generic "left_profile" success state.
|
||||
}
|
||||
return {
|
||||
state: 'left_profile',
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6764,6 +6755,29 @@ function getStep5SubmitState() {
|
||||
};
|
||||
}
|
||||
|
||||
function logStep5SubmitDebug(message, options = {}) {
|
||||
const resolvedState = options?.state && typeof options.state === 'object'
|
||||
? options.state
|
||||
: getStep5SubmitState();
|
||||
const summary = [
|
||||
`url=${resolvedState?.url || location.href}`,
|
||||
`retryPage=${Boolean(resolvedState?.retryPage)}`,
|
||||
`retryEnabled=${Boolean(resolvedState?.retryEnabled)}`,
|
||||
`successState=${resolvedState?.successState || 'none'}`,
|
||||
`profileVisible=${Boolean(resolvedState?.profileVisible)}`,
|
||||
`unknownAuthPage=${Boolean(resolvedState?.unknownAuthPage)}`,
|
||||
`maxCheckAttemptsBlocked=${Boolean(resolvedState?.maxCheckAttemptsBlocked)}`,
|
||||
`userAlreadyExistsBlocked=${Boolean(resolvedState?.userAlreadyExistsBlocked)}`,
|
||||
resolvedState?.errorText ? `errorText=${resolvedState.errorText}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' | ');
|
||||
log(`步骤 5 [调试] ${message} | ${summary}`, options?.level || 'info', {
|
||||
step: 5,
|
||||
stepKey: 'fill-profile',
|
||||
});
|
||||
}
|
||||
|
||||
async function recoverStep5SubmitRetryPage(payload = {}) {
|
||||
return recoverCurrentAuthRetryPage({
|
||||
...payload,
|
||||
@@ -6780,14 +6794,21 @@ function installStep5NavigationCompletionReporter(completeOnce) {
|
||||
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
|
||||
return () => {};
|
||||
}
|
||||
const debugLog = typeof logStep5SubmitDebug === 'function'
|
||||
? logStep5SubmitDebug
|
||||
: (message, options = {}) => {
|
||||
if (typeof log === 'function') {
|
||||
log(`步骤 5 [调试] ${message}`, options?.level || 'info', {
|
||||
step: 5,
|
||||
stepKey: 'fill-profile',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onNavigationStarted = () => {
|
||||
completeOnce({
|
||||
navigationStarted: true,
|
||||
outcome: {
|
||||
state: 'navigation_started',
|
||||
url: location.href,
|
||||
},
|
||||
const onNavigationStarted = (event) => {
|
||||
const eventType = String(event?.type || 'navigation').trim() || 'navigation';
|
||||
debugLog(`检测到页面开始导航(event=${eventType})。`, {
|
||||
level: 'warn',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6801,6 +6822,16 @@ function installStep5NavigationCompletionReporter(completeOnce) {
|
||||
}
|
||||
|
||||
async function waitForStep5SubmitOutcome(options = {}) {
|
||||
const debugLog = typeof logStep5SubmitDebug === 'function'
|
||||
? logStep5SubmitDebug
|
||||
: (message, logOptions = {}) => {
|
||||
if (typeof log === 'function') {
|
||||
log(`步骤 5 [调试] ${message}`, logOptions?.level || 'info', {
|
||||
step: 5,
|
||||
stepKey: 'fill-profile',
|
||||
});
|
||||
}
|
||||
};
|
||||
const {
|
||||
timeoutMs = 120000,
|
||||
maxAuthRetryRecoveries = 2,
|
||||
@@ -6828,6 +6859,9 @@ async function waitForStep5SubmitOutcome(options = {}) {
|
||||
throw new Error(`步骤 5:资料提交后连续进入认证重试页 ${maxAuthRetryRecoveries} 次,页面仍未恢复。URL: ${location.href}`);
|
||||
}
|
||||
authRetryRecoveryCount += 1;
|
||||
debugLog(`检测到资料提交后的认证重试页,准备执行恢复(${authRetryRecoveryCount}/${maxAuthRetryRecoveries})。`, {
|
||||
level: 'warn',
|
||||
});
|
||||
log(`步骤 5:资料提交后进入认证重试页,正在自动恢复(${authRetryRecoveryCount}/${maxAuthRetryRecoveries})...`, 'warn');
|
||||
await recoverCurrentAuthRetryPage({
|
||||
flow: 'signup',
|
||||
@@ -6837,12 +6871,18 @@ async function waitForStep5SubmitOutcome(options = {}) {
|
||||
step: 5,
|
||||
timeoutMs: 12000,
|
||||
});
|
||||
debugLog('认证重试页恢复动作已完成,准备继续等待最终结果。', {
|
||||
level: 'info',
|
||||
});
|
||||
lastSubmitClickAt = Date.now();
|
||||
continue;
|
||||
}
|
||||
|
||||
const successState = getStep5PostSubmitSuccessState();
|
||||
if (successState) {
|
||||
debugLog(`检测到资料提交成功状态:${successState.state || 'unknown'}`, {
|
||||
level: 'ok',
|
||||
});
|
||||
return successState;
|
||||
}
|
||||
|
||||
@@ -7146,8 +7186,23 @@ async function step5_fillNameBirthday(payload) {
|
||||
}
|
||||
|
||||
let reportedCompletionPayload = null;
|
||||
const debugLog = typeof logStep5SubmitDebug === 'function'
|
||||
? logStep5SubmitDebug
|
||||
: (message, logOptions = {}) => {
|
||||
if (typeof log === 'function') {
|
||||
log(`步骤 5 [调试] ${message}`, logOptions?.level || 'info', {
|
||||
step: 5,
|
||||
stepKey: 'fill-profile',
|
||||
});
|
||||
}
|
||||
};
|
||||
function completeStep5Once(extra = {}) {
|
||||
const completionReason = extra?.outcome?.state
|
||||
|| (extra?.navigationStarted ? `navigation_started:${extra?.navigationEventType || 'unknown'}` : 'direct_completion');
|
||||
if (reportedCompletionPayload) {
|
||||
debugLog(`忽略重复完成信号(reason=${completionReason})。`, {
|
||||
level: 'warn',
|
||||
});
|
||||
return reportedCompletionPayload;
|
||||
}
|
||||
|
||||
@@ -7156,6 +7211,9 @@ async function step5_fillNameBirthday(payload) {
|
||||
navigationStarted: Boolean(extra.navigationStarted),
|
||||
outcome: extra.outcome || null,
|
||||
});
|
||||
debugLog(`准备发送完成信号(reason=${completionReason},isAgeMode=${isAgeMode})。`, {
|
||||
level: extra?.navigationStarted ? 'warn' : 'info',
|
||||
});
|
||||
reportedCompletionPayload = completionPayload;
|
||||
reportComplete(5, completionPayload);
|
||||
return completionPayload;
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
const SIGNUP_METHOD_PHONE = 'phone';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||
const PLUS_PAYMENT_METHOD_NONE = 'none';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const PLUS_PAYMENT_STEP_KEY = 'paypal-approve';
|
||||
const PLUS_REGISTRATION_WAIT_STEP_KEY = 'wait-registration-success';
|
||||
|
||||
function freezeDeep(entry) {
|
||||
if (!entry || typeof entry !== 'object' || Object.isFrozen(entry)) {
|
||||
@@ -3014,12 +3016,87 @@
|
||||
]
|
||||
});
|
||||
|
||||
const PLUS_PAYMENT_CHAIN_STEP_KEYS = Object.freeze([
|
||||
'plus-checkout-create',
|
||||
'plus-checkout-billing',
|
||||
'paypal-approve',
|
||||
'plus-checkout-return',
|
||||
'paypal-hosted-email',
|
||||
'paypal-hosted-card',
|
||||
'paypal-hosted-create-account',
|
||||
'paypal-hosted-review',
|
||||
'gopay-subscription-confirm',
|
||||
]);
|
||||
|
||||
function omitPlusPaymentChainSteps(steps = []) {
|
||||
return steps.filter((step) => !PLUS_PAYMENT_CHAIN_STEP_KEYS.includes(String(step?.key || '').trim()));
|
||||
}
|
||||
|
||||
function reindexModeStepDefinitions(steps = []) {
|
||||
return (Array.isArray(steps) ? steps : []).map((step, index) => ({
|
||||
...step,
|
||||
id: index + 1,
|
||||
order: (index + 1) * 10,
|
||||
}));
|
||||
}
|
||||
|
||||
function getPlusRegistrationWaitStep() {
|
||||
const sourceStep = STEP_VARIANTS.normal.find((step) => step.key === PLUS_REGISTRATION_WAIT_STEP_KEY);
|
||||
return {
|
||||
...(sourceStep || {
|
||||
key: PLUS_REGISTRATION_WAIT_STEP_KEY,
|
||||
title: '等待注册成功',
|
||||
sourceId: 'chatgpt',
|
||||
driverId: null,
|
||||
command: PLUS_REGISTRATION_WAIT_STEP_KEY,
|
||||
flowId: 'openai',
|
||||
}),
|
||||
id: 6,
|
||||
order: 60,
|
||||
};
|
||||
}
|
||||
|
||||
function shiftPlusStepAfterRegistrationWait(step = {}) {
|
||||
const nextStep = { ...step };
|
||||
const id = Number(step.id);
|
||||
const order = Number(step.order);
|
||||
if (Number.isFinite(id)) {
|
||||
nextStep.id = id + 1;
|
||||
}
|
||||
if (Number.isFinite(order)) {
|
||||
nextStep.order = order + 10;
|
||||
}
|
||||
return nextStep;
|
||||
}
|
||||
|
||||
function insertPlusRegistrationWaitStep(steps = []) {
|
||||
if (!Array.isArray(steps) || steps.some((step) => step.key === PLUS_REGISTRATION_WAIT_STEP_KEY)) {
|
||||
return steps;
|
||||
}
|
||||
const fillProfileIndex = steps.findIndex((step) => step.key === 'fill-profile');
|
||||
if (fillProfileIndex < 0) {
|
||||
return steps;
|
||||
}
|
||||
return steps.flatMap((step, index) => {
|
||||
if (index < fillProfileIndex) {
|
||||
return [step];
|
||||
}
|
||||
if (index === fillProfileIndex) {
|
||||
return [step, getPlusRegistrationWaitStep()];
|
||||
}
|
||||
return [shiftPlusStepAfterRegistrationWait(step)];
|
||||
});
|
||||
}
|
||||
|
||||
function isPlusModeEnabled(options = {}) {
|
||||
return Boolean(options?.plusModeEnabled || options?.plusMode);
|
||||
}
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === PLUS_PAYMENT_METHOD_NONE || normalized === 'no-payment' || normalized === 'skip-payment') {
|
||||
return PLUS_PAYMENT_METHOD_NONE;
|
||||
}
|
||||
if (normalized === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
|
||||
}
|
||||
@@ -3118,13 +3195,27 @@
|
||||
}
|
||||
|
||||
function getModeStepDefinitions(options = {}) {
|
||||
return getVariantStepDefinitions(resolveVariantKey(options));
|
||||
const isPlusMode = isPlusModeEnabled(options);
|
||||
let steps = getVariantStepDefinitions(resolveVariantKey(options));
|
||||
if (isPlusMode) {
|
||||
steps = insertPlusRegistrationWaitStep(steps);
|
||||
}
|
||||
if (
|
||||
isPlusMode
|
||||
&& normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod) === PLUS_PAYMENT_METHOD_NONE
|
||||
) {
|
||||
steps = omitPlusPaymentChainSteps(steps);
|
||||
}
|
||||
return reindexModeStepDefinitions(steps);
|
||||
}
|
||||
|
||||
function getAllSteps() {
|
||||
const keyed = new Map();
|
||||
Object.values(STEP_VARIANTS).forEach((steps) => {
|
||||
(Array.isArray(steps) ? steps : []).forEach((step) => {
|
||||
Object.entries(STEP_VARIANTS).forEach(([variantKey, steps]) => {
|
||||
const variantSteps = String(variantKey || '').startsWith('plus')
|
||||
? insertPlusRegistrationWaitStep(steps)
|
||||
: steps;
|
||||
reindexModeStepDefinitions(variantSteps).forEach((step) => {
|
||||
keyed.set(`${step.id}:${step.key}`, step);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createGoPayUtils() {
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||
const PLUS_PAYMENT_METHOD_NONE = 'none';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
@@ -12,6 +13,9 @@
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === PLUS_PAYMENT_METHOD_NONE || normalized === 'no-payment' || normalized === 'skip-payment') {
|
||||
return PLUS_PAYMENT_METHOD_NONE;
|
||||
}
|
||||
if (normalized === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
|
||||
}
|
||||
@@ -419,6 +423,7 @@
|
||||
GPC_HELPER_PHONE_MODE_MANUAL,
|
||||
PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||
PLUS_PAYMENT_METHOD_GOPAY,
|
||||
PLUS_PAYMENT_METHOD_NONE,
|
||||
PLUS_PAYMENT_METHOD_PAYPAL,
|
||||
PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
|
||||
buildGpcCardBalanceUrl,
|
||||
|
||||
@@ -326,6 +326,7 @@
|
||||
<span class="data-label">Plus 支付</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-plus-payment-method" class="data-select">
|
||||
<option value="none">无需支付</option>
|
||||
<option value="paypal-hosted">PayPal 无卡直绑</option>
|
||||
<option value="paypal">PayPal</option>
|
||||
<!--
|
||||
|
||||
@@ -552,6 +552,7 @@ const autoHintText = document.querySelector('.auto-hint');
|
||||
const stepsList = document.querySelector('.steps-list');
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||
const PLUS_PAYMENT_METHOD_NONE = 'none';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
@@ -3075,7 +3076,11 @@ function normalizePlusPaymentMethod(value = '') {
|
||||
const gpcValue = typeof PLUS_PAYMENT_METHOD_GPC_HELPER !== 'undefined' ? PLUS_PAYMENT_METHOD_GPC_HELPER : 'gpc-helper';
|
||||
const paypalValue = typeof PLUS_PAYMENT_METHOD_PAYPAL !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL : 'paypal';
|
||||
const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED : 'paypal-hosted';
|
||||
const noneValue = typeof PLUS_PAYMENT_METHOD_NONE !== 'undefined' ? PLUS_PAYMENT_METHOD_NONE : 'none';
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === noneValue || normalized === 'no-payment' || normalized === 'skip-payment') {
|
||||
return noneValue;
|
||||
}
|
||||
if (normalized === paypalHostedValue || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return paypalHostedValue;
|
||||
}
|
||||
@@ -9502,6 +9507,7 @@ function updatePhoneVerificationSettingsUI() {
|
||||
function updatePlusModeUI() {
|
||||
const paypalValue = typeof PLUS_PAYMENT_METHOD_PAYPAL !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL : 'paypal';
|
||||
const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED : 'paypal-hosted';
|
||||
const noneValue = typeof PLUS_PAYMENT_METHOD_NONE !== 'undefined' ? PLUS_PAYMENT_METHOD_NONE : 'none';
|
||||
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
|
||||
const gpcValue = typeof PLUS_PAYMENT_METHOD_GPC_HELPER !== 'undefined' ? PLUS_PAYMENT_METHOD_GPC_HELPER : 'gpc-helper';
|
||||
const oauthStrategyValue = typeof PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH !== 'undefined'
|
||||
@@ -9655,6 +9661,8 @@ function updatePlusModeUI() {
|
||||
? `GPC ${isGpcAutoMode ? '自动' : '手动'}订阅链路`
|
||||
: method === gopayValue
|
||||
? 'GoPay 印尼订阅链路'
|
||||
: method === noneValue
|
||||
? '已有 Plus,无需配置支付链路'
|
||||
: method === paypalHostedValue
|
||||
? 'PayPal 无卡直绑链路'
|
||||
: 'PayPal 订阅链路';
|
||||
|
||||
@@ -111,12 +111,12 @@ function createRouter(overrides = {}) {
|
||||
deleteIcloudAlias: async () => {},
|
||||
deleteUsedIcloudAliases: async () => {},
|
||||
disableUsedLuckmailPurchases: async () => {},
|
||||
doesNodeUseCompletionSignal: () => false,
|
||||
doesNodeUseCompletionSignal: overrides.doesNodeUseCompletionSignal || (() => false),
|
||||
ensureManualInteractionAllowed: async () => ({}),
|
||||
executeNode: async (nodeId) => {
|
||||
events.executedSteps.push(getStepForNode(nodeId) || nodeId);
|
||||
},
|
||||
executeNodeViaCompletionSignal: async () => {},
|
||||
executeNodeViaCompletionSignal: overrides.executeNodeViaCompletionSignal || (async () => ({})),
|
||||
exportSettingsBundle: async () => ({}),
|
||||
fetchGeneratedEmail: async () => '',
|
||||
finalizePhoneActivationAfterSuccessfulFlow: overrides.finalizePhoneActivationAfterSuccessfulFlow || (async (state) => {
|
||||
@@ -125,6 +125,7 @@ function createRouter(overrides = {}) {
|
||||
finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => {
|
||||
events.finalizePayloads.push(payload);
|
||||
}),
|
||||
finalizeStep5Completion: overrides.finalizeStep5Completion,
|
||||
finalizeIcloudAliasAfterSuccessfulFlow: overrides.finalizeIcloudAliasAfterSuccessfulFlow || (async () => {}),
|
||||
findHotmailAccount: async () => null,
|
||||
flushCommand: async () => {},
|
||||
@@ -752,3 +753,83 @@ test('message router ignores stale step 2 completion while auto-run is already o
|
||||
assert.deepStrictEqual(events.emailStates, []);
|
||||
assert.equal(events.logs.some(({ message }) => /忽略过期的节点 submit-signup-email 完成消息/.test(message)), true);
|
||||
});
|
||||
|
||||
test('message router defers fill-profile completion status until background validation', async () => {
|
||||
const { router, events } = createRouter({
|
||||
state: {
|
||||
currentNodeId: 'fill-profile',
|
||||
nodeStatuses: {
|
||||
'fill-profile': 'running',
|
||||
'wait-registration-success': 'pending',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'NODE_COMPLETE',
|
||||
nodeId: 'fill-profile',
|
||||
payload: {
|
||||
nodeId: 'fill-profile',
|
||||
outcome: 'navigation_started',
|
||||
url: 'https://auth.openai.com/about-you',
|
||||
navigationStarted: true,
|
||||
},
|
||||
}, {});
|
||||
|
||||
assert.deepStrictEqual(response, { ok: true });
|
||||
assert.deepStrictEqual(events.stepStatuses, []);
|
||||
assert.deepStrictEqual(events.nodeStatuses, []);
|
||||
assert.deepStrictEqual(events.notifyCompletions, [
|
||||
{
|
||||
step: 5,
|
||||
nodeId: 'fill-profile',
|
||||
payload: {
|
||||
nodeId: 'fill-profile',
|
||||
outcome: 'navigation_started',
|
||||
url: 'https://auth.openai.com/about-you',
|
||||
navigationStarted: true,
|
||||
step: 5,
|
||||
},
|
||||
},
|
||||
]);
|
||||
assert.equal(events.logs.some(({ message }) => /等待后台最终复核后再标记完成/.test(message)), true);
|
||||
});
|
||||
|
||||
test('message router finalizes manual fill-profile execution through background validation', async () => {
|
||||
const finalizePayloads = [];
|
||||
const { router } = createRouter({
|
||||
state: {
|
||||
currentNodeId: 'fill-profile',
|
||||
nodeStatuses: {
|
||||
'fill-profile': 'pending',
|
||||
},
|
||||
},
|
||||
executeNodeViaCompletionSignal: async (nodeId) => ({
|
||||
nodeId,
|
||||
outcome: 'logged_in_home',
|
||||
url: 'https://chatgpt.com/',
|
||||
}),
|
||||
doesNodeUseCompletionSignal: (nodeId) => nodeId === 'fill-profile',
|
||||
finalizeStep5Completion: async (payload) => {
|
||||
finalizePayloads.push(payload);
|
||||
},
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'EXECUTE_NODE',
|
||||
source: 'sidepanel',
|
||||
nodeId: 'fill-profile',
|
||||
payload: {
|
||||
nodeId: 'fill-profile',
|
||||
},
|
||||
}, {});
|
||||
|
||||
assert.deepStrictEqual(response, { ok: true });
|
||||
assert.deepStrictEqual(finalizePayloads, [
|
||||
{
|
||||
nodeId: 'fill-profile',
|
||||
outcome: 'logged_in_home',
|
||||
url: 'https://chatgpt.com/',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -110,6 +110,7 @@ async function waitForTabStableComplete() {}
|
||||
${extractFunction('parseUrlSafely')}
|
||||
${extractFunction('isSignupEntryHost')}
|
||||
${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
|
||||
${extractFunction('isStep5CompletionChatgptUrl')}
|
||||
${extractFunction('getStep5SubmitStateFromContent')}
|
||||
${extractFunction('recoverStep5SubmitRetryPageOnTab')}
|
||||
${extractFunction('validateStep5PostCompletion')}
|
||||
@@ -138,3 +139,65 @@ return {
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('step 5 post-completion validation rejects non-chatgpt success candidates', async () => {
|
||||
const api = new Function(`
|
||||
const logs = [];
|
||||
const chrome = {
|
||||
tabs: {
|
||||
async get() {
|
||||
return { url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent' };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async function sendToContentScriptResilient(source, message) {
|
||||
if (message.type === 'GET_STEP5_SUBMIT_STATE') {
|
||||
return {
|
||||
retryPage: false,
|
||||
retryEnabled: false,
|
||||
maxCheckAttemptsBlocked: false,
|
||||
userAlreadyExistsBlocked: false,
|
||||
successState: 'oauth_consent',
|
||||
profileVisible: false,
|
||||
errorText: '',
|
||||
unknownAuthPage: false,
|
||||
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||
};
|
||||
}
|
||||
throw new Error('unexpected message type: ' + message.type);
|
||||
}
|
||||
|
||||
async function addLog(message, level, meta) {
|
||||
logs.push({ message, level, meta });
|
||||
}
|
||||
|
||||
async function waitForTabStableComplete() {}
|
||||
|
||||
${extractFunction('parseUrlSafely')}
|
||||
${extractFunction('isSignupEntryHost')}
|
||||
${extractFunction('isLikelyLoggedInChatgptHomeUrl')}
|
||||
${extractFunction('isStep5CompletionChatgptUrl')}
|
||||
${extractFunction('getStep5SubmitStateFromContent')}
|
||||
${extractFunction('recoverStep5SubmitRetryPageOnTab')}
|
||||
${extractFunction('validateStep5PostCompletion')}
|
||||
|
||||
return {
|
||||
run() {
|
||||
return validateStep5PostCompletion(99, {});
|
||||
},
|
||||
snapshot() {
|
||||
return { logs };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
await assert.rejects(
|
||||
api.run(),
|
||||
/尚未跳转到 https:\/\/chatgpt\.com/
|
||||
);
|
||||
assert.equal(
|
||||
api.snapshot().logs.some(({ message }) => /非 chatgpt\.com 的步骤 5 完成候选/.test(message)),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,8 @@ test('GoPay utils keeps GPC helper payment method distinct', () => {
|
||||
const api = loadGoPayUtils();
|
||||
assert.equal(api.normalizePlusPaymentMethod('paypal-hosted'), 'paypal-hosted');
|
||||
assert.equal(api.normalizePlusPaymentMethod('paypal_direct'), 'paypal-hosted');
|
||||
assert.equal(api.normalizePlusPaymentMethod('none'), 'none');
|
||||
assert.equal(api.normalizePlusPaymentMethod('no-payment'), 'none');
|
||||
assert.equal(api.normalizePlusPaymentMethod('gpc-helper'), 'gpc-helper');
|
||||
assert.equal(api.normalizePlusPaymentMethod('gopay'), 'gopay');
|
||||
assert.equal(api.normalizePlusPaymentMethod('unknown'), 'paypal');
|
||||
|
||||
@@ -249,6 +249,66 @@ return {
|
||||
assert.equal(api.rows.rowPlusHostedCheckoutOauthDelay.style.display, 'none');
|
||||
});
|
||||
|
||||
test('sidepanel Plus UI supports no-payment mode without payment-specific rows', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('normalizePlusAccountAccessStrategy'),
|
||||
extractFunction('getSelectedPlusPaymentMethod'),
|
||||
extractFunction('getRequestedPlusAccountAccessStrategy'),
|
||||
extractFunction('normalizeGpcHelperPhoneModeValue'),
|
||||
extractFunction('getGpcHelperAutoModeEnabled'),
|
||||
extractFunction('normalizeGpcAutoModePermissionValue'),
|
||||
extractFunction('getGpcAutoModePermissionFromPayload'),
|
||||
extractFunction('shouldPreserveSelectedGpcAutoMode'),
|
||||
extractFunction('hasGpcAutoModePermissionField'),
|
||||
extractFunction('isGpcAutoModePermissionDenied'),
|
||||
extractFunction('normalizeGpcOtpChannelValue'),
|
||||
extractFunction('updatePlusModeUI'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let latestState = { plusPaymentMethod: 'none' };
|
||||
let currentPlusPaymentMethod = 'none';
|
||||
let currentPlusAccountAccessStrategy = 'sub2api_codex_session';
|
||||
const inputPlusModeEnabled = { checked: true };
|
||||
const selectPlusPaymentMethod = { value: 'none', style: { display: 'none' } };
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const PLUS_PAYMENT_METHOD_NONE = 'none';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
const plusPaymentMethodCaption = { textContent: '' };
|
||||
const rowPayPalAccount = { style: { display: '' } };
|
||||
const rowHostedCheckoutVerificationUrl = { style: { display: '' } };
|
||||
const rowHostedCheckoutPhone = { style: { display: '' } };
|
||||
const rowPlusHostedCheckoutOauthDelay = { style: { display: '' } };
|
||||
const rowGoPayPhone = { style: { display: '' } };
|
||||
const rowGpcHelperApi = { style: { display: '' } };
|
||||
${bundle}
|
||||
return {
|
||||
updatePlusModeUI,
|
||||
plusPaymentMethodCaption,
|
||||
rows: {
|
||||
rowPayPalAccount,
|
||||
rowHostedCheckoutVerificationUrl,
|
||||
rowHostedCheckoutPhone,
|
||||
rowPlusHostedCheckoutOauthDelay,
|
||||
rowGoPayPhone,
|
||||
rowGpcHelperApi,
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
api.updatePlusModeUI();
|
||||
|
||||
assert.match(api.plusPaymentMethodCaption.textContent, /无需配置支付链路/);
|
||||
Object.values(api.rows).forEach((row) => {
|
||||
assert.equal(row.style.display, 'none');
|
||||
});
|
||||
});
|
||||
|
||||
test('sidepanel Plus UI can hide Plus controls when the shared flow capability registry disables them', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
|
||||
@@ -99,6 +99,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
'fill-password',
|
||||
'fetch-signup-code',
|
||||
'fill-profile',
|
||||
'wait-registration-success',
|
||||
'plus-checkout-create',
|
||||
'plus-checkout-billing',
|
||||
'paypal-approve',
|
||||
@@ -110,7 +111,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
'platform-verify',
|
||||
]
|
||||
);
|
||||
assert.equal(plusSteps.some((step) => step.key === 'wait-registration-success'), false);
|
||||
assert.equal(plusSteps[5].title, '等待注册成功');
|
||||
assert.equal(plusSteps.some((step) => step.key === 'fetch-login-code'), true);
|
||||
assert.equal(plusSteps.find((step) => step.key === 'paypal-approve')?.title, 'PayPal 登录与授权');
|
||||
assert.equal(plusPhoneSteps[1].title, '注册并输入手机号');
|
||||
@@ -123,6 +124,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
'fill-password',
|
||||
'fetch-signup-code',
|
||||
'fill-profile',
|
||||
'wait-registration-success',
|
||||
'plus-checkout-create',
|
||||
'plus-checkout-billing',
|
||||
'paypal-approve',
|
||||
@@ -150,14 +152,14 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
]
|
||||
);
|
||||
assert.equal(goPaySteps.some((step) => step.key === 'paypal-approve'), false);
|
||||
assert.equal(api.getStepById(8, { plusModeEnabled: true, plusPaymentMethod: 'gopay' }), null);
|
||||
assert.equal(api.getStepById(9, { plusModeEnabled: true, plusPaymentMethod: 'gopay' })?.key, 'oauth-login');
|
||||
assert.equal(api.getPlusPaymentStepTitle({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), '');
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 14);
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone' }), 15);
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 18);
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 15);
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone' }), 16);
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 19);
|
||||
assert.equal(api.hasFlow('openai'), true);
|
||||
assert.equal(api.hasFlow('kiro'), true);
|
||||
assert.equal(api.hasFlow('site-a'), false);
|
||||
@@ -208,8 +210,8 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
[],
|
||||
]
|
||||
);
|
||||
assert.equal(plusSteps[5].title, '创建 Plus Checkout');
|
||||
assert.equal(plusSteps[7].title, 'PayPal 登录与授权');
|
||||
assert.equal(plusSteps[6].title, '创建 Plus Checkout');
|
||||
assert.equal(plusSteps[8].title, 'PayPal 登录与授权');
|
||||
|
||||
assert.deepStrictEqual(
|
||||
hostedSteps.map((step) => step.key),
|
||||
@@ -219,6 +221,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
'fill-password',
|
||||
'fetch-signup-code',
|
||||
'fill-profile',
|
||||
'wait-registration-success',
|
||||
'plus-checkout-create',
|
||||
'paypal-hosted-email',
|
||||
'paypal-hosted-card',
|
||||
@@ -236,8 +239,8 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
assert.equal(hostedSteps.some((step) => step.key === 'paypal-hosted-openai-checkout'), false);
|
||||
assert.equal(hostedSteps.some((step) => step.key === 'paypal-hosted-verification'), false);
|
||||
assert.equal(hostedSteps.find((step) => step.key === 'paypal-hosted-card')?.title, '无卡直绑填写 PayPal 资料');
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), 14);
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), 15);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
goPaySteps.map((step) => step.key),
|
||||
@@ -247,6 +250,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
'fill-password',
|
||||
'fetch-signup-code',
|
||||
'fill-profile',
|
||||
'wait-registration-success',
|
||||
'plus-checkout-create',
|
||||
'gopay-subscription-confirm',
|
||||
'oauth-login',
|
||||
@@ -256,10 +260,10 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
'platform-verify',
|
||||
]
|
||||
);
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), 14);
|
||||
assert.equal(goPaySteps[5].title, '打开 GoPay 订阅页');
|
||||
assert.equal(goPaySteps[6].title, '等待 GoPay 订阅确认');
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), 13);
|
||||
assert.equal(goPaySteps[6].title, '打开 GoPay 订阅页');
|
||||
assert.equal(goPaySteps[7].title, '等待 GoPay 订阅确认');
|
||||
|
||||
assert.deepStrictEqual(
|
||||
gpcSteps.map((step) => step.key),
|
||||
@@ -269,6 +273,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
'fill-password',
|
||||
'fetch-signup-code',
|
||||
'fill-profile',
|
||||
'wait-registration-success',
|
||||
'plus-checkout-create',
|
||||
'plus-checkout-billing',
|
||||
'oauth-login',
|
||||
@@ -278,10 +283,120 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
'platform-verify',
|
||||
]
|
||||
);
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 14);
|
||||
assert.equal(gpcSteps[5].title, '创建 GPC 订单');
|
||||
assert.equal(gpcSteps[6].title, '等待 GPC 任务完成');
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 13);
|
||||
assert.equal(gpcSteps[6].title, '创建 GPC 订单');
|
||||
assert.equal(gpcSteps[7].title, '等待 GPC 任务完成');
|
||||
});
|
||||
|
||||
test('Plus no-payment mode removes only payment chain nodes', () => {
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${readStepDefinitionsBundle()}; return self.MultiPageStepDefinitions;`)(globalScope);
|
||||
const paymentChainKeys = [
|
||||
'plus-checkout-create',
|
||||
'plus-checkout-billing',
|
||||
'paypal-approve',
|
||||
'plus-checkout-return',
|
||||
'paypal-hosted-email',
|
||||
'paypal-hosted-card',
|
||||
'paypal-hosted-create-account',
|
||||
'paypal-hosted-review',
|
||||
'gopay-subscription-confirm',
|
||||
];
|
||||
|
||||
const oauthSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'none' });
|
||||
const oauthNodes = api.getNodes({ plusModeEnabled: true, plusPaymentMethod: 'none' });
|
||||
const oauthStepKeys = oauthSteps.map((step) => step.key);
|
||||
|
||||
assert.deepStrictEqual(oauthStepKeys, [
|
||||
'open-chatgpt',
|
||||
'submit-signup-email',
|
||||
'fill-password',
|
||||
'fetch-signup-code',
|
||||
'fill-profile',
|
||||
'wait-registration-success',
|
||||
'oauth-login',
|
||||
'fetch-login-code',
|
||||
'post-login-phone-verification',
|
||||
'confirm-oauth',
|
||||
'platform-verify',
|
||||
]);
|
||||
paymentChainKeys.forEach((key) => {
|
||||
assert.equal(oauthStepKeys.includes(key), false, `no-payment OAuth should not keep ${key}`);
|
||||
assert.equal(oauthNodes.some((node) => node.nodeId === key), false, `no-payment OAuth nodes should not keep ${key}`);
|
||||
});
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'none' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
|
||||
assert.equal(api.getPlusPaymentStepTitle({ plusModeEnabled: true, plusPaymentMethod: 'none' }), '');
|
||||
assert.deepStrictEqual(
|
||||
oauthNodes.find((node) => node.nodeId === 'fill-profile')?.next,
|
||||
['wait-registration-success']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
oauthNodes.find((node) => node.nodeId === 'wait-registration-success')?.next,
|
||||
['oauth-login']
|
||||
);
|
||||
|
||||
const sub2apiSteps = api.getSteps({
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'none',
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
});
|
||||
const sub2apiNodes = api.getNodes({
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'none',
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
});
|
||||
assert.deepStrictEqual(sub2apiSteps.map((step) => step.key), [
|
||||
'open-chatgpt',
|
||||
'submit-signup-email',
|
||||
'fill-password',
|
||||
'fetch-signup-code',
|
||||
'fill-profile',
|
||||
'wait-registration-success',
|
||||
'sub2api-session-import',
|
||||
]);
|
||||
paymentChainKeys.forEach((key) => {
|
||||
assert.equal(sub2apiSteps.some((step) => step.key === key), false, `no-payment SUB2API should not keep ${key}`);
|
||||
});
|
||||
assert.deepStrictEqual(api.getStepIds({
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'none',
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
}), [1, 2, 3, 4, 5, 6, 7]);
|
||||
assert.equal(sub2apiNodes.at(-1)?.nodeId, 'sub2api-session-import');
|
||||
assert.deepStrictEqual(sub2apiNodes.find((node) => node.nodeId === 'fill-profile')?.next, ['wait-registration-success']);
|
||||
assert.deepStrictEqual(sub2apiNodes.find((node) => node.nodeId === 'wait-registration-success')?.next, ['sub2api-session-import']);
|
||||
|
||||
const cpaSteps = api.getSteps({
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'none',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
});
|
||||
const cpaNodes = api.getNodes({
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'none',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
});
|
||||
assert.deepStrictEqual(cpaSteps.map((step) => step.key), [
|
||||
'open-chatgpt',
|
||||
'submit-signup-email',
|
||||
'fill-password',
|
||||
'fetch-signup-code',
|
||||
'fill-profile',
|
||||
'wait-registration-success',
|
||||
'cpa-session-import',
|
||||
]);
|
||||
paymentChainKeys.forEach((key) => {
|
||||
assert.equal(cpaSteps.some((step) => step.key === key), false, `no-payment CPA should not keep ${key}`);
|
||||
});
|
||||
assert.deepStrictEqual(api.getStepIds({
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'none',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
}), [1, 2, 3, 4, 5, 6, 7]);
|
||||
assert.equal(cpaNodes.at(-1)?.nodeId, 'cpa-session-import');
|
||||
assert.deepStrictEqual(cpaNodes.find((node) => node.nodeId === 'fill-profile')?.next, ['wait-registration-success']);
|
||||
assert.deepStrictEqual(cpaNodes.find((node) => node.nodeId === 'wait-registration-success')?.next, ['cpa-session-import']);
|
||||
});
|
||||
|
||||
test('Plus session strategy swaps the OAuth tail for a single SUB2API import node', () => {
|
||||
@@ -304,7 +419,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
},
|
||||
previousNodeId: 'plus-checkout-return',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
},
|
||||
{
|
||||
label: 'paypal-hosted',
|
||||
@@ -314,7 +429,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
},
|
||||
previousNodeId: 'paypal-hosted-review',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||
},
|
||||
{
|
||||
label: 'gopay',
|
||||
@@ -324,7 +439,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
},
|
||||
previousNodeId: 'gopay-subscription-confirm',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
},
|
||||
{
|
||||
label: 'gpc-helper',
|
||||
@@ -334,7 +449,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
},
|
||||
previousNodeId: 'plus-checkout-billing',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
},
|
||||
].forEach(({ label, options, previousNodeId, expectedStepIds }) => {
|
||||
const steps = api.getSteps(options);
|
||||
@@ -342,6 +457,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
|
||||
const stepKeys = steps.map((step) => step.key);
|
||||
const nodeIds = nodes.map((node) => node.nodeId);
|
||||
const previousNode = nodes.find((node) => node.nodeId === previousNodeId);
|
||||
const waitNode = nodes.find((node) => node.nodeId === 'wait-registration-success');
|
||||
const sessionImportNode = nodes.find((node) => node.nodeId === 'sub2api-session-import');
|
||||
|
||||
assert.equal(stepKeys.at(-1), 'sub2api-session-import', `${label} should end with session import`);
|
||||
@@ -352,6 +468,7 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
|
||||
});
|
||||
assert.deepStrictEqual(api.getStepIds(options), expectedStepIds, `${label} step ids should follow the new tail`);
|
||||
assert.equal(api.getLastStepId(options), expectedStepIds.at(-1), `${label} last step id should match session import`);
|
||||
assert.deepStrictEqual(waitNode?.next, ['plus-checkout-create'], `${label} wait node should link to checkout chain`);
|
||||
assert.deepStrictEqual(previousNode?.next, ['sub2api-session-import'], `${label} previous node should link to session import`);
|
||||
assert.deepStrictEqual(sessionImportNode?.next, [], `${label} session import should be terminal`);
|
||||
});
|
||||
@@ -393,7 +510,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
},
|
||||
previousNodeId: 'plus-checkout-return',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
},
|
||||
{
|
||||
label: 'paypal-hosted',
|
||||
@@ -403,7 +520,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
},
|
||||
previousNodeId: 'paypal-hosted-review',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||
},
|
||||
{
|
||||
label: 'gopay',
|
||||
@@ -413,7 +530,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
},
|
||||
previousNodeId: 'gopay-subscription-confirm',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
},
|
||||
{
|
||||
label: 'gpc-helper',
|
||||
@@ -423,7 +540,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
},
|
||||
previousNodeId: 'plus-checkout-billing',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 10],
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
},
|
||||
].forEach(({ label, options, previousNodeId, expectedStepIds }) => {
|
||||
const steps = api.getSteps(options);
|
||||
@@ -431,6 +548,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
|
||||
const stepKeys = steps.map((step) => step.key);
|
||||
const nodeIds = nodes.map((node) => node.nodeId);
|
||||
const previousNode = nodes.find((node) => node.nodeId === previousNodeId);
|
||||
const waitNode = nodes.find((node) => node.nodeId === 'wait-registration-success');
|
||||
const sessionImportNode = nodes.find((node) => node.nodeId === 'cpa-session-import');
|
||||
|
||||
assert.equal(stepKeys.at(-1), 'cpa-session-import', `${label} should end with CPA session import`);
|
||||
@@ -441,6 +559,7 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
|
||||
});
|
||||
assert.deepStrictEqual(api.getStepIds(options), expectedStepIds, `${label} step ids should follow the CPA tail`);
|
||||
assert.equal(api.getLastStepId(options), expectedStepIds.at(-1), `${label} last step id should match CPA session import`);
|
||||
assert.deepStrictEqual(waitNode?.next, ['plus-checkout-create'], `${label} wait node should link to checkout chain`);
|
||||
assert.deepStrictEqual(previousNode?.next, ['cpa-session-import'], `${label} previous node should link to CPA session import`);
|
||||
assert.deepStrictEqual(sessionImportNode?.next, [], `${label} CPA session import should be terminal`);
|
||||
});
|
||||
@@ -460,6 +579,7 @@ test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
assert.match(html, /id="input-plus-mode-enabled"/);
|
||||
assert.match(html, /id="select-plus-payment-method"/);
|
||||
assert.match(html, /<option value="none">无需支付<\/option>/);
|
||||
assert.match(html, /id="select-paypal-account"/);
|
||||
assert.match(html, /id="btn-add-paypal-account"/);
|
||||
assert.match(html, /id="input-gopay-phone"/);
|
||||
|
||||
@@ -66,6 +66,7 @@ function getStep5Bundle() {
|
||||
extractFunction('waitForStep5SubmitButton'),
|
||||
extractFunction('isStep5SubmitButtonClickable'),
|
||||
extractFunction('isStep5ProfileStillVisible'),
|
||||
extractFunction('isStep5CompletionChatgptUrl'),
|
||||
extractFunction('getStep5PostSubmitSuccessState'),
|
||||
extractFunction('installStep5NavigationCompletionReporter'),
|
||||
extractFunction('waitForStep5SubmitOutcome'),
|
||||
|
||||
@@ -61,6 +61,7 @@ function getStep5OutcomeBundle() {
|
||||
extractFunction('waitForStep5SubmitButton'),
|
||||
extractFunction('isStep5SubmitButtonClickable'),
|
||||
extractFunction('isStep5ProfileStillVisible'),
|
||||
extractFunction('isStep5CompletionChatgptUrl'),
|
||||
extractFunction('getStep5PostSubmitSuccessState'),
|
||||
extractFunction('installStep5NavigationCompletionReporter'),
|
||||
extractFunction('waitForStep5SubmitOutcome'),
|
||||
@@ -1057,6 +1058,7 @@ function isOAuthConsentPage() { return false; }
|
||||
function isAddPhonePageReady() { return false; }
|
||||
function isStep5ProfileStillVisible() { return false; }
|
||||
|
||||
${extractFunction('isStep5CompletionChatgptUrl')}
|
||||
${extractFunction('getStep5PostSubmitSuccessState')}
|
||||
|
||||
return {
|
||||
@@ -1068,3 +1070,99 @@ return {
|
||||
|
||||
assert.equal(api.run(), null);
|
||||
});
|
||||
|
||||
test('step 5 completion requires https chatgpt.com and rejects auth-success-like pages', () => {
|
||||
const api = new Function(`
|
||||
const location = {
|
||||
href: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||
};
|
||||
|
||||
function getStep5AuthRetryPageState() { return null; }
|
||||
function isStep5ProfileStillVisible() { return false; }
|
||||
|
||||
${extractFunction('isStep5CompletionChatgptUrl')}
|
||||
${extractFunction('getStep5PostSubmitSuccessState')}
|
||||
|
||||
return {
|
||||
run(url) {
|
||||
location.href = url;
|
||||
return getStep5PostSubmitSuccessState();
|
||||
},
|
||||
isCompletion(url) {
|
||||
return isStep5CompletionChatgptUrl(url);
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.deepStrictEqual(api.run('https://chatgpt.com/'), {
|
||||
state: 'logged_in_home',
|
||||
url: 'https://chatgpt.com/',
|
||||
});
|
||||
assert.deepStrictEqual(api.run('https://www.chatgpt.com/c/abc'), {
|
||||
state: 'logged_in_home',
|
||||
url: 'https://www.chatgpt.com/c/abc',
|
||||
});
|
||||
assert.equal(api.run('https://auth.openai.com/sign-in-with-chatgpt/codex/consent'), null);
|
||||
assert.equal(api.run('https://auth.openai.com/add-phone'), null);
|
||||
assert.equal(api.run('https://chat.openai.com/'), null);
|
||||
assert.equal(api.run('http://chatgpt.com/'), null);
|
||||
assert.equal(api.isCompletion('https://chatgpt.com/add-phone'), false);
|
||||
});
|
||||
|
||||
test('step 5 navigation reporter does not complete on beforeunload alone', () => {
|
||||
const api = new Function(`
|
||||
const events = [];
|
||||
const listeners = new Map();
|
||||
const location = {
|
||||
href: 'https://auth.openai.com/about-you',
|
||||
};
|
||||
const window = {
|
||||
addEventListener(type, handler) {
|
||||
listeners.set(type, handler);
|
||||
},
|
||||
removeEventListener(type) {
|
||||
listeners.delete(type);
|
||||
},
|
||||
};
|
||||
|
||||
function log(message, level = 'info') {
|
||||
events.push({ type: 'log', message, level });
|
||||
}
|
||||
function getStep5SubmitState() {
|
||||
return {
|
||||
url: location.href,
|
||||
retryPage: true,
|
||||
retryEnabled: true,
|
||||
successState: '',
|
||||
profileVisible: true,
|
||||
unknownAuthPage: false,
|
||||
maxCheckAttemptsBlocked: false,
|
||||
userAlreadyExistsBlocked: false,
|
||||
errorText: '',
|
||||
};
|
||||
}
|
||||
|
||||
${extractFunction('logStep5SubmitDebug')}
|
||||
${extractFunction('installStep5NavigationCompletionReporter')}
|
||||
|
||||
return {
|
||||
run() {
|
||||
let completionCount = 0;
|
||||
const cleanup = installStep5NavigationCompletionReporter(() => {
|
||||
completionCount += 1;
|
||||
events.push({ type: 'complete' });
|
||||
});
|
||||
const beforeUnload = listeners.get('beforeunload');
|
||||
if (beforeUnload) {
|
||||
beforeUnload({ type: 'beforeunload' });
|
||||
}
|
||||
cleanup();
|
||||
return { completionCount, events };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = api.run();
|
||||
assert.equal(result.completionCount, 0);
|
||||
assert.equal(result.events.some((entry) => entry.type === 'log' && /检测到页面开始导航/.test(entry.message)), true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user