diff --git a/background.js b/background.js
index b56bbbe..efa7aa6 100644
--- a/background.js
+++ b/background.js
@@ -456,6 +456,11 @@ function normalizePlusPaymentMethod(value = '') {
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
+function normalizeGpcHelperPhoneMode(value = '') {
+ const normalized = String(value || '').trim().toLowerCase();
+ return normalized === 'auto' || normalized === 'builtin' ? 'auto' : 'manual';
+}
+
function normalizeContributionModeSource(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === CONTRIBUTION_SOURCE_SUB2API
@@ -634,6 +639,7 @@ const PERSISTED_SETTING_DEFAULTS = {
gopayHelperApiUrl: DEFAULT_GPC_HELPER_API_URL,
gopayHelperApiKey: '',
gopayHelperCardKey: '',
+ gopayHelperPhoneMode: 'manual',
gopayHelperPhoneNumber: '',
gopayHelperCountryCode: '+86',
gopayHelperPin: '',
@@ -665,6 +671,9 @@ const PERSISTED_SETTING_DEFAULTS = {
gopayHelperBalancePayload: null,
gopayHelperBalanceUpdatedAt: 0,
gopayHelperBalanceError: '',
+ gopayHelperRemainingUses: 0,
+ gopayHelperAutoModeEnabled: false,
+ gopayHelperApiKeyStatus: '',
autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0,
oauthFlowTimeoutEnabled: true,
@@ -2331,6 +2340,10 @@ function normalizePersistentSettingValue(key, value) {
return self.GoPayUtils?.normalizeGoPayPin
? self.GoPayUtils.normalizeGoPayPin(value)
: String(value || '');
+ case 'gopayHelperPhoneMode':
+ return self.GoPayUtils?.normalizeGpcHelperPhoneMode
+ ? self.GoPayUtils.normalizeGpcHelperPhoneMode(value)
+ : (String(value || '').trim().toLowerCase() === 'auto' || String(value || '').trim().toLowerCase() === 'builtin' ? 'auto' : 'manual');
case 'gopayHelperPhoneNumber':
return self.GoPayUtils?.normalizeGoPayPhone
? self.GoPayUtils.normalizeGoPayPhone(value)
@@ -2405,6 +2418,7 @@ function normalizePersistentSettingValue(key, value) {
case 'gopayHelperFailureDetail':
case 'gopayHelperBalance':
case 'gopayHelperBalanceError':
+ case 'gopayHelperApiKeyStatus':
return String(value || '').trim();
case 'gopayHelperBalancePayload':
case 'gopayHelperStartPayload':
@@ -2413,10 +2427,12 @@ function normalizePersistentSettingValue(key, value) {
case 'gopayHelperBalanceUpdatedAt':
case 'gopayHelperApiInputWaitSeconds':
case 'gopayHelperOtpInvalidCount':
+ case 'gopayHelperRemainingUses':
return Math.max(0, Number(value) || 0);
case 'autoRunSkipFailures':
case 'oauthFlowTimeoutEnabled':
case 'gopayHelperLocalSmsHelperEnabled':
+ case 'gopayHelperAutoModeEnabled':
case 'autoRunDelayEnabled':
return Boolean(value);
case 'operationDelayEnabled':
@@ -7731,6 +7747,17 @@ function isGpcTaskEndedFailure(error) {
return /GPC_TASK_ENDED::/i.test(message);
}
+function isGpcCheckoutRestartRequiredFailure(error) {
+ const message = getErrorMessage(error);
+ if (/PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格/i.test(message)) {
+ return false;
+ }
+ if (/GPC_TASK_ENDED::/i.test(message)) {
+ return true;
+ }
+ return /GPC\s*API\s*请求超时|步骤\s*[67][\s\S]*GPC[\s\S]*(?:任务轮询超时|请求超时|超时|timeout|timed\s*out|卡死|无响应)|account\s+already\s+linked|GOPAY已经绑了订阅|(?:账号|账户|GoPay|GOPAY)[\s\S]*(?:已绑定|已经绑定|已绑|绑了订阅|绑定了订阅)|创建\s*GPC\s*订单失败[\s\S]*(?:任务已结束|任务结束|failed|expired|discarded|请求超时|timeout|timed\s*out)/i.test(message);
+}
+
function isGoPayCheckoutRestartRequiredFailure(error) {
const message = getErrorMessage(error);
return /GOPAY_RESTART_FROM_STEP6::|GOPAY_RETRY_REQUIRED::/i.test(message);
@@ -9957,12 +9984,27 @@ async function refreshGpcApiKeyBalance(state = {}, options = {}) {
const balancePayload = self.GoPayUtils?.unwrapGpcResponse
? self.GoPayUtils.unwrapGpcResponse(payload)
: (payload?.data && typeof payload === 'object' ? payload.data : payload);
+ const balanceData = balancePayload && typeof balancePayload === 'object' && !Array.isArray(balancePayload)
+ ? balancePayload
+ : {};
+ const remainingUses = self.GoPayUtils?.getGpcBalanceRemainingUses
+ ? self.GoPayUtils.getGpcBalanceRemainingUses(balanceData)
+ : Math.max(0, Number(balanceData.remaining_uses ?? balanceData.remainingUses ?? balanceData.balance ?? balanceData.remaining) || 0);
+ const autoModeEnabled = self.GoPayUtils?.isGpcAutoModeEnabled
+ ? self.GoPayUtils.isGpcAutoModeEnabled(balanceData)
+ : Boolean(balanceData.auto_mode_enabled ?? balanceData.autoModeEnabled);
+ const apiKeyStatus = self.GoPayUtils?.getGpcApiKeyStatus
+ ? self.GoPayUtils.getGpcApiKeyStatus(balanceData)
+ : String(balanceData.status || balanceData.card_status || balanceData.cardStatus || '').trim();
const balanceText = formatGpcApiKeyBalancePayload(payload) || rawText || '未知';
const updates = {
gopayHelperBalance: balanceText,
- gopayHelperBalancePayload: balancePayload && typeof balancePayload === 'object' && !Array.isArray(balancePayload) ? balancePayload : { raw: String(balancePayload || '') },
+ gopayHelperBalancePayload: Object.keys(balanceData).length > 0 ? balanceData : { raw: String(balancePayload || '') },
gopayHelperBalanceUpdatedAt: Date.now(),
gopayHelperBalanceError: '',
+ gopayHelperRemainingUses: Math.max(0, Number(remainingUses) || 0),
+ gopayHelperAutoModeEnabled: Boolean(autoModeEnabled),
+ gopayHelperApiKeyStatus: apiKeyStatus,
};
const flowId = String(balancePayload?.flow_id || balancePayload?.flowId || '').trim();
if (flowId) {
@@ -9991,7 +10033,15 @@ async function refreshGpcApiKeyBalance(state = {}, options = {}) {
: `GPC 余额查询成功:${balanceText}`,
'info'
);
- return { balance: balanceText, payload, updatedAt: updates.gopayHelperBalanceUpdatedAt };
+ return {
+ balance: balanceText,
+ payload,
+ data: updates.gopayHelperBalancePayload,
+ remainingUses: updates.gopayHelperRemainingUses,
+ autoModeEnabled: updates.gopayHelperAutoModeEnabled,
+ apiKeyStatus: updates.gopayHelperApiKeyStatus,
+ updatedAt: updates.gopayHelperBalanceUpdatedAt,
+ };
}
const refreshGpcCardBalance = refreshGpcApiKeyBalance;
@@ -10397,10 +10447,33 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
const { targetRun, totalRuns, attemptRuns, continued = false } = context;
let postStep7RestartCount = 0;
let goPayCheckoutRestartCount = 0;
+ let gpcCheckoutRestartCount = 0;
let step4RestartCount = 0;
let currentStartStep = startStep;
let continueCurrentAttempt = continued;
const resolvedSignupMethod = await ensureResolvedSignupMethodForRun();
+ const normalizePlusPaymentMethodForRun = typeof normalizePlusPaymentMethod === 'function'
+ ? normalizePlusPaymentMethod
+ : (value) => (String(value || '').trim().toLowerCase() === 'gpc-helper' ? 'gpc-helper' : String(value || '').trim().toLowerCase());
+ const plusPaymentMethodGpcHelper = typeof PLUS_PAYMENT_METHOD_GPC_HELPER === 'string'
+ ? PLUS_PAYMENT_METHOD_GPC_HELPER
+ : 'gpc-helper';
+ const attachFailedStep = (error, step) => {
+ const failedStep = Math.floor(Number(step) || 0);
+ if (!error || typeof error !== 'object' || failedStep <= 0) {
+ return error;
+ }
+
+ if (!Number.isInteger(Number(error.failedStep)) || Number(error.failedStep) <= 0) {
+ try {
+ error.failedStep = failedStep;
+ } catch (_err) {
+ // Some host errors may be non-extensible; state-based inference still covers normal paths.
+ }
+ }
+
+ return error;
+ };
while (true) {
@@ -10440,6 +10513,7 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
try {
await executeStepAndWait(3, AUTO_STEP_DELAYS[3]);
} catch (err) {
+ attachFailedStep(err, 3);
if (isStopError(err)) {
throw err;
}
@@ -10486,10 +10560,31 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
await executeStepAndWait(step, AUTO_STEP_DELAYS[step]);
step += 1;
} catch (err) {
+ attachFailedStep(err, step);
if (isStopError(err)) {
throw err;
}
+ const stepExecutionKey = typeof getStepExecutionKeyForState === 'function'
+ ? getStepExecutionKeyForState(step, latestState)
+ : '';
+ const isGpcCheckoutStep = normalizePlusPaymentMethodForRun(latestState?.plusPaymentMethod) === plusPaymentMethodGpcHelper
+ || String(latestState?.plusCheckoutSource || '').trim() === plusPaymentMethodGpcHelper;
+ if (isGpcCheckoutStep
+ && (stepExecutionKey === 'plus-checkout-create' || stepExecutionKey === 'plus-checkout-billing')
+ && isGpcCheckoutRestartRequiredFailure(err)) {
+ gpcCheckoutRestartCount += 1;
+ await addLog(
+ `步骤 ${step}:检测到 GPC 任务失败/卡住,准备回到步骤 6 重新创建 GPC 任务(第 ${gpcCheckoutRestartCount} 次)。原因:${getErrorMessage(err)}`,
+ 'warn'
+ );
+ await invalidateDownstreamAfterStepRestart(5, {
+ logLabel: `步骤 ${step} GPC 任务失败后准备回到步骤 6 重试(第 ${gpcCheckoutRestartCount} 次)`,
+ });
+ step = 6;
+ continue;
+ }
+
if (step === 8 && isGoPayCheckoutRestartRequiredFailure(err)) {
goPayCheckoutRestartCount += 1;
if (goPayCheckoutRestartCount > 3) {
@@ -10733,7 +10828,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
},
isSignupProfilePageUrl: (rawUrl) => {
const parsed = parseUrlSafely(rawUrl);
- return Boolean(parsed && isSignupPageHost(parsed.hostname) && /\/create-account\/profile(?:[/?#]|$)/i.test(parsed.pathname || ''));
+ return Boolean(parsed && isSignupPageHost(parsed.hostname) && /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(parsed.pathname || ''));
},
isRetryableContentScriptTransportError,
isHotmailProvider,
@@ -10861,6 +10956,7 @@ const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({
resolveSignupEmailForFlow,
sendToContentScriptResilient,
SIGNUP_PAGE_INJECT_FILES,
+ waitForTabStableComplete,
});
const step3Executor = self.MultiPageBackgroundStep3?.createStep3Executor({
addLog,
@@ -12070,6 +12166,29 @@ function throwIfStep8SettledOrStopped(isSettled = false) {
}
}
+function isStep9AuthCallbackWaitPageUrl(rawUrl) {
+ if (!rawUrl) return false;
+ try {
+ const parsed = new URL(rawUrl);
+ const hostname = String(parsed.hostname || '').toLowerCase();
+ if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(hostname)) {
+ return false;
+ }
+ const pathname = String(parsed.pathname || '');
+ return /\/api\/oauth\/oauth2\/auth(?:[/?#]|$)/i.test(pathname)
+ || /\/oauth\/oauth2\/auth(?:[/?#]|$)/i.test(pathname);
+ } catch {
+ return false;
+ }
+}
+
+async function shouldDeferStep9CallbackTimeout(details = {}) {
+ const tabId = details?.tabId;
+ if (!Number.isInteger(tabId)) return false;
+ const tab = await chrome.tabs.get(tabId).catch(() => null);
+ return isStep9AuthCallbackWaitPageUrl(tab?.url || '');
+}
+
async function ensureStep8SignupPageReady(tabId, options = {}) {
const visibleStep = Math.floor(Number(options.visibleStep || options.logStep || options.step) || 0);
await ensureContentScriptReadyOnTab('signup-page', tabId, {
@@ -12524,6 +12643,7 @@ const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
setStep8TabUpdatedListener,
setWebNavCommittedListener,
setWebNavListener,
+ shouldDeferStep9CallbackTimeout,
sleepWithStop,
STEP8_CLICK_RETRY_DELAY_MS,
STEP8_MAX_ROUNDS,
diff --git a/background/account-run-history.js b/background/account-run-history.js
index 0a73f29..e80908a 100644
--- a/background/account-run-history.js
+++ b/background/account-run-history.js
@@ -39,7 +39,7 @@
return '';
}
- function extractRecordStep(status = '', detail = '') {
+ function extractRecordStepFromStatus(status = '') {
const normalizedStatus = String(status || '').trim().toLowerCase();
const statusMatch = normalizedStatus.match(/^step(\d+)_(?:failed|stopped)$/);
if (statusMatch) {
@@ -47,6 +47,21 @@
return Number.isInteger(step) && step > 0 ? step : null;
}
+ return null;
+ }
+
+ function extractRecordStepFromDetailPrefix(detail = '') {
+ const text = String(detail || '').trim();
+ const detailMatch = text.match(/^(?:Step\s+(\d+)|步骤\s*(\d+))\s*(?::|:|失败|停止|已|\b)/i);
+ if (!detailMatch) {
+ return null;
+ }
+
+ const step = Number(detailMatch[1] || detailMatch[2]);
+ return Number.isInteger(step) && step > 0 ? step : null;
+ }
+
+ function extractAnyRecordStepFromDetail(detail = '') {
const text = String(detail || '').trim();
const detailMatch = text.match(/(?:Step\s+(\d+)|步骤\s*(\d+))/i);
if (!detailMatch) {
@@ -57,6 +72,76 @@
return Number.isInteger(step) && step > 0 ? step : null;
}
+ function extractRecordStep(status = '', detail = '') {
+ return extractRecordStepFromStatus(status) || extractRecordStepFromDetailPrefix(detail);
+ }
+
+ function parseFailureLabelStep(label = '') {
+ const match = String(label || '').trim().match(/^步骤\s*(\d+)\s*(?:失败|停止)$/);
+ if (!match) {
+ return null;
+ }
+ const step = Number(match[1]);
+ return Number.isInteger(step) && step > 0 ? step : null;
+ }
+
+ function shouldIgnorePersistedFailedStepCandidate(candidate, failureDetail = '') {
+ if (!Number.isInteger(candidate) || candidate <= 0) {
+ return true;
+ }
+
+ const text = String(failureDetail || '').trim();
+ if (!text) {
+ return false;
+ }
+
+ const leadingStep = extractRecordStepFromDetailPrefix(text);
+ if (Number.isInteger(leadingStep) && leadingStep > 0) {
+ return leadingStep !== candidate;
+ }
+
+ const incidentalStep = extractAnyRecordStepFromDetail(text);
+ return incidentalStep === candidate;
+ }
+
+ function resolveNormalizedFailedStep(record = {}, failureDetail = '') {
+ const explicitStatusStep = extractRecordStepFromStatus(record.finalStatus || record.status || '');
+ if (Number.isInteger(explicitStatusStep) && explicitStatusStep > 0) {
+ return explicitStatusStep;
+ }
+
+ const detailStep = extractRecordStepFromDetailPrefix(failureDetail);
+ if (Number.isInteger(detailStep) && detailStep > 0) {
+ return detailStep;
+ }
+
+ const failedStepCandidate = Number(record.failedStep);
+ if (Number.isInteger(failedStepCandidate)
+ && failedStepCandidate > 0
+ && !shouldIgnorePersistedFailedStepCandidate(failedStepCandidate, failureDetail)) {
+ return failedStepCandidate;
+ }
+
+ return null;
+ }
+
+ function resolveFailureLabel(finalStatus, rawFailureLabel = '', computedFailureLabel = '', failedStep = null) {
+ const rawLabel = String(rawFailureLabel || '').trim();
+ if (finalStatus === 'stopped') {
+ return computedFailureLabel;
+ }
+ if (finalStatus !== 'failed') {
+ return rawLabel || computedFailureLabel;
+ }
+
+ const rawStep = parseFailureLabelStep(rawLabel);
+ if (Number.isInteger(rawStep) && rawStep > 0) {
+ return rawStep === failedStep ? rawLabel : computedFailureLabel;
+ }
+
+ return rawLabel || computedFailureLabel;
+ }
+
function isPhoneVerificationFailure(detail = '') {
const text = String(detail || '').trim();
if (!text) {
@@ -247,10 +332,9 @@
const failureDetail = finalStatus === 'failed' || finalStatus === 'stopped'
? String(record.failureDetail || record.reason || '').trim()
: '';
- const failedStepCandidate = Number(record.failedStep);
- const failedStep = Number.isInteger(failedStepCandidate) && failedStepCandidate > 0
- ? failedStepCandidate
- : extractRecordStep(record.finalStatus || record.status || '', failureDetail);
+ const failedStep = finalStatus === 'failed' || finalStatus === 'stopped'
+ ? resolveNormalizedFailedStep(record, failureDetail)
+ : null;
const autoRunContext = normalizeAutoRunContext(record.autoRunContext);
const retryCount = normalizeRetryCount(
record.retryCount !== undefined
@@ -271,9 +355,7 @@
finalStatus,
finishedAt,
retryCount,
- failureLabel: finalStatus === 'stopped'
- ? computedFailureLabel
- : (rawFailureLabel || computedFailureLabel),
+ failureLabel: resolveFailureLabel(finalStatus, rawFailureLabel, computedFailureLabel, failedStep),
failureDetail,
failedStep: Number.isInteger(failedStep) && failedStep > 0 ? failedStep : null,
source,
diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js
index 7c9e063..89a8b1f 100644
--- a/background/auto-run-controller.js
+++ b/background/auto-run-controller.js
@@ -85,6 +85,95 @@
return Math.max(0, Number(summary?.attempts || 0) - 1);
}
+ function normalizeRecordStep(value) {
+ const step = Math.floor(Number(value) || 0);
+ return step > 0 ? step : null;
+ }
+
+ function extractStepFromRecordStatus(status = '') {
+ const match = String(status || '').trim().toLowerCase().match(/^step(\d+)_(?:failed|stopped)$/);
+ if (!match) {
+ return null;
+ }
+ return normalizeRecordStep(match[1]);
+ }
+
+ function getKnownStepIdsFromState(state = {}) {
+ const ids = new Set();
+ for (const key of Object.keys(state?.stepStatuses || {})) {
+ const step = normalizeRecordStep(key);
+ if (step) {
+ ids.add(step);
+ }
+ }
+
+ const currentStep = normalizeRecordStep(state?.currentStep);
+ if (currentStep) {
+ ids.add(currentStep);
+ }
+
+ return Array.from(ids).sort((left, right) => left - right);
+ }
+
+ function inferRecordStepFromState(state = {}, preferredStatuses = []) {
+ const statuses = state?.stepStatuses || {};
+ const preferredStatusSet = new Set(preferredStatuses.map((item) => String(item || '').trim()).filter(Boolean));
+ const stepIds = getKnownStepIdsFromState(state);
+ const currentStep = normalizeRecordStep(state?.currentStep);
+
+ if (currentStep && preferredStatusSet.has(String(statuses[currentStep] || '').trim())) {
+ return currentStep;
+ }
+
+ const matchingSteps = stepIds
+ .filter((step) => preferredStatusSet.has(String(statuses[step] || '').trim()))
+ .sort((left, right) => right - left);
+ if (matchingSteps.length) {
+ return matchingSteps[0];
+ }
+
+ if (currentStep) {
+ const currentStatus = String(statuses[currentStep] || '').trim();
+ if (!['', 'pending', 'completed', 'manual_completed', 'skipped'].includes(currentStatus)) {
+ return currentStep;
+ }
+ }
+
+ return null;
+ }
+
+ function inferRecordStepFromError(errorLike = null) {
+ if (!errorLike || typeof errorLike !== 'object') {
+ return null;
+ }
+
+ return normalizeRecordStep(errorLike.failedStep)
+ || normalizeRecordStep(errorLike.step)
+ || normalizeRecordStep(errorLike.currentStep);
+ }
+
+ function resolveAutoRunAccountRecordStatus(status, state = {}, errorLike = null) {
+ const normalizedStatus = String(status || '').trim().toLowerCase();
+ const explicitStep = extractStepFromRecordStatus(normalizedStatus);
+ if (explicitStep) {
+ return normalizedStatus;
+ }
+
+ if (normalizedStatus === 'failed') {
+ const failedStep = inferRecordStepFromError(errorLike)
+ || inferRecordStepFromState(state, ['failed', 'running']);
+ return failedStep ? `step${failedStep}_failed` : status;
+ }
+
+ if (normalizedStatus === 'stopped') {
+ const stoppedStep = inferRecordStepFromError(errorLike)
+ || inferRecordStepFromState(state, ['stopped', 'running']);
+ return stoppedStep ? `step${stoppedStep}_stopped` : status;
+ }
+
+ return status;
+ }
+
function formatAutoRunFailureReasons(reasons = []) {
if (!Array.isArray(reasons) || !reasons.length) {
return '未知错误';
@@ -456,7 +545,7 @@
forceFreshTabsNextRun = false;
}
- const appendRoundRecordIfNeeded = async (status, reason = '') => {
+ const appendRoundRecordIfNeeded = async (status, reason = '', errorLike = null) => {
if (roundRecordAppended) {
return;
}
@@ -465,7 +554,9 @@
return;
}
- const record = await appendAccountRunRecord(status, null, reason);
+ const recordState = await getState();
+ const recordStatus = resolveAutoRunAccountRecordStatus(status, recordState, errorLike);
+ const record = await appendAccountRunRecord(recordStatus, recordState, reason);
if (record) {
roundRecordAppended = true;
}
@@ -507,7 +598,7 @@
} catch (err) {
if (isStopError(err)) {
stoppedEarly = true;
- await appendRoundRecordIfNeeded('stopped', getErrorMessage(err));
+ await appendRoundRecordIfNeeded('stopped', getErrorMessage(err), err);
await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
@@ -556,7 +647,7 @@
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
- await appendRoundRecordIfNeeded('failed', reason);
+ await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因认证流程进入 add-phone 已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
@@ -591,7 +682,7 @@
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
- await appendRoundRecordIfNeeded('failed', reason);
+ await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因接码号池暂无可用号码已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
@@ -626,7 +717,7 @@
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
- await appendRoundRecordIfNeeded('failed', reason);
+ await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因 Plus 免费试用资格不可用已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
@@ -661,7 +752,7 @@
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
- await appendRoundRecordIfNeeded('failed', reason);
+ await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因 GPC 任务已结束。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
@@ -696,7 +787,7 @@
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
- await appendRoundRecordIfNeeded('failed', reason);
+ await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因 user_already_exists 已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
@@ -731,7 +822,7 @@
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
- await appendRoundRecordIfNeeded('failed', reason);
+ await appendRoundRecordIfNeeded('failed', reason, err);
cancelPendingCommands('当前轮因步骤 4 连续 405 错误已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
@@ -787,7 +878,7 @@
} catch (sleepError) {
if (isStopError(sleepError)) {
stoppedEarly = true;
- await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError));
+ await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError), sleepError);
await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
@@ -811,7 +902,7 @@
} catch (sleepError) {
if (isStopError(sleepError)) {
stoppedEarly = true;
- await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError));
+ await appendRoundRecordIfNeeded('stopped', getErrorMessage(sleepError), sleepError);
await addLog(`第 ${targetRun}/${totalRuns} 轮已被用户停止`, 'warn');
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
@@ -833,7 +924,7 @@
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
- await appendRoundRecordIfNeeded('failed', reason);
+ await appendRoundRecordIfNeeded('failed', reason, err);
if (!autoRunSkipFailures) {
cancelPendingCommands('当前轮执行失败。');
await broadcastStopToContentScripts();
@@ -948,6 +1039,7 @@
handleAutoRunLoopUnhandledError,
logAutoRunFinalSummary,
normalizeAutoRunRoundSummary,
+ resolveAutoRunAccountRecordStatus,
serializeAutoRunRoundSummaries,
skipAutoRunCountdown,
startAutoRunLoop,
diff --git a/background/message-router.js b/background/message-router.js
index ca8b3fb..01c305c 100644
--- a/background/message-router.js
+++ b/background/message-router.js
@@ -167,6 +167,25 @@
return '';
}
+ function isStaleAutoRunStepMessage(step, state = {}) {
+ if (typeof isAutoRunLockedState !== 'function' || !isAutoRunLockedState(state)) {
+ return false;
+ }
+ const normalizedStep = Number(step);
+ if (!Number.isInteger(normalizedStep) || normalizedStep <= 0) {
+ return false;
+ }
+ const currentStatus = String(state?.stepStatuses?.[normalizedStep] || '').trim();
+ if (currentStatus === 'running') {
+ return false;
+ }
+ const currentStep = Number(state?.currentStep) || 0;
+ if (currentStep > 0 && normalizedStep !== currentStep) {
+ return true;
+ }
+ return ['completed', 'manual_completed', 'skipped', 'failed', 'stopped'].includes(currentStatus);
+ }
+
function resolveSignupPhonePayload(payload = {}) {
const directPhone = String(
payload?.signupPhoneNumber
@@ -540,6 +559,11 @@
}
case 'STEP_COMPLETE': {
+ const currentState = await getState();
+ if (isStaleAutoRunStepMessage(message.step, currentState)) {
+ await addLog(`自动运行:忽略过期的步骤 ${message.step} 完成消息,当前流程已在步骤 ${currentState.currentStep || '未知'}。`, 'warn', { step: message.step });
+ return { ok: true, ignored: true };
+ }
if (getStopRequested()) {
await setStepStatus(message.step, 'stopped');
await appendManualAccountRunRecordIfNeeded(`step${message.step}_stopped`, null, '流程已被用户停止。');
@@ -582,6 +606,11 @@
}
case 'STEP_ERROR': {
+ const staleCheckState = await getState();
+ if (isStaleAutoRunStepMessage(message.step, staleCheckState)) {
+ await addLog(`自动运行:忽略过期的步骤 ${message.step} 失败消息,当前流程已在步骤 ${staleCheckState.currentStep || '未知'}。原始错误:${message.error || '未知错误'}`, 'warn', { step: message.step });
+ return { ok: true, ignored: true };
+ }
if (typeof isCloudflareSecurityBlockedError === 'function' && isCloudflareSecurityBlockedError(message.error)) {
const userMessage = typeof handleCloudflareSecurityBlocked === 'function'
? await handleCloudflareSecurityBlocked(message.error)
diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js
index f4a3b19..f3deb95 100644
--- a/background/phone-verification-flow.js
+++ b/background/phone-verification-flow.js
@@ -94,6 +94,7 @@
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::';
const PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX = 'PHONE_RESEND_BANNED_NUMBER::';
+ const PHONE_RESEND_SERVER_ERROR_PREFIX = 'PHONE_RESEND_SERVER_ERROR::';
const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::';
const PHONE_MANUAL_FREE_REUSE_ERROR_PREFIX = 'PHONE_MANUAL_FREE_REUSE::';
const PHONE_AUTO_FREE_REUSE_PREPARE_ERROR_PREFIX = 'PHONE_AUTO_FREE_REUSE_PREPARE::';
@@ -1387,6 +1388,25 @@
return /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|can(?:not|'t)\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number/i.test(message);
}
+ function isPhoneResendServerError(error) {
+ const message = String(error?.message || error || '').trim();
+ if (!message) {
+ return false;
+ }
+ if (message.startsWith(PHONE_RESEND_SERVER_ERROR_PREFIX)) {
+ return true;
+ }
+ return /this\s+page\s+isn['’]?t\s+working|currently\s+unable\s+to\s+handle\s+this\s+request|http\s+error\s+500|500\s+internal\s+server\s+error/i.test(message);
+ }
+
+ function buildPhoneResendServerError(error) {
+ const message = String(error?.message || error || '').trim();
+ if (message.startsWith(PHONE_RESEND_SERVER_ERROR_PREFIX)) {
+ return new Error(message);
+ }
+ return new Error(`${PHONE_RESEND_SERVER_ERROR_PREFIX}${message || 'OpenAI contact-verification page returned HTTP ERROR 500 after resend.'}`);
+ }
+
function shouldTreatResendThrottledAsBanned(state = {}) {
return Boolean(state?.phoneResendThrottledAsBannedEnabled);
}
@@ -4300,6 +4320,13 @@
message: error.message,
};
}
+ if (isPhoneResendServerError(error)) {
+ return {
+ hasError: true,
+ reason: 'resend_server_error',
+ message: error.message,
+ };
+ }
if (isPhoneMaxUsageExceededFlowError(error)) {
return {
hasError: true,
@@ -4971,6 +4998,9 @@
if (pageError?.reason === 'phone_max_usage_exceeded') {
throw buildPhoneMaxUsageExceededError(pageError.message);
}
+ if (pageError?.reason === 'resend_server_error') {
+ throw buildPhoneResendServerError(pageError.message);
+ }
if (pageError?.reason === 'resend_throttled') {
if (shouldTreatResendThrottledAsBanned(state)) {
throw buildHighRiskResendThrottledError(pageError.message);
@@ -5028,6 +5058,18 @@
reason: 'phone_max_usage_exceeded',
};
}
+ if (isPhoneResendServerError(error)) {
+ await addLog(
+ `步骤 9:重发短信后进入 contact-verification 500 页面,立即更换号码。${error.message}`,
+ 'warn'
+ );
+ await clearPhoneRuntimeCountdown();
+ return {
+ code: '',
+ replaceNumber: true,
+ reason: 'resend_server_error',
+ };
+ }
if (isPhoneResendThrottledError(error)) {
if (shouldTreatResendThrottledAsBanned(state)) {
await addLog(
@@ -5144,6 +5186,18 @@
: 'resend_throttled',
};
}
+ if (isPhoneResendServerError(resendError)) {
+ await addLog(
+ `步骤 9:重发短信后进入 contact-verification 500 页面,立即更换号码。${resendError.message}`,
+ 'warn'
+ );
+ await clearPhoneRuntimeCountdown();
+ return {
+ code: '',
+ replaceNumber: true,
+ reason: 'resend_server_error',
+ };
+ }
await addLog(`步骤 9:点击手机验证码页面重发按钮失败。${resendError.message}`, 'warn');
}
continue;
@@ -5382,6 +5436,9 @@
if (isStopRequestedError(resendError)) {
throw resendError;
}
+ if (isPhoneResendServerError(resendError)) {
+ throw buildPhoneResendServerError(resendError);
+ }
await addLog(`步骤 4:注册手机验证码页面重发失败,将继续轮询短信。${resendError.message}`, 'warn', {
step: 4,
stepKey: 'fetch-signup-code',
@@ -5419,6 +5476,9 @@
if (isStopRequestedError(resendError)) {
throw resendError;
}
+ if (isPhoneResendServerError(resendError)) {
+ throw buildPhoneResendServerError(resendError);
+ }
await addLog(`步骤 4:验证码被拒后点击重发失败。${resendError.message}`, 'warn', {
step: 4,
stepKey: 'fetch-signup-code',
@@ -5621,6 +5681,9 @@
if (isStopRequestedError(resendError)) {
throw resendError;
}
+ if (isPhoneResendServerError(resendError)) {
+ throw buildPhoneResendServerError(resendError);
+ }
await addLog(`步骤 ${visibleStep}:登录手机验证码页面重发失败,将继续轮询短信。${resendError.message}`, 'warn', {
step: visibleStep,
stepKey: 'fetch-login-code',
@@ -5656,6 +5719,9 @@
if (isStopRequestedError(resendError)) {
throw resendError;
}
+ if (isPhoneResendServerError(resendError)) {
+ throw buildPhoneResendServerError(resendError);
+ }
await addLog(`步骤 ${visibleStep}:登录手机验证码被拒后点击重发失败。${resendError.message}`, 'warn', {
step: visibleStep,
stepKey: 'fetch-login-code',
diff --git a/background/signup-flow-helpers.js b/background/signup-flow-helpers.js
index b85736e..d3c890e 100644
--- a/background/signup-flow-helpers.js
+++ b/background/signup-flow-helpers.js
@@ -30,12 +30,35 @@
waitForTabUrlMatch,
} = deps;
+ async function waitForSignupEntryTabToSettle(tabId, step = 1) {
+ if (step !== 2 || !Number.isInteger(tabId) || typeof waitForTabStableComplete !== 'function') {
+ return null;
+ }
+
+ if (typeof addLog === 'function') {
+ await addLog(
+ `步骤 ${step}:注册页已打开,正在等待页面加载完成并额外稳定 3 秒...`,
+ 'info',
+ { step, stepKey: 'signup-entry' }
+ );
+ }
+
+ return waitForTabStableComplete(tabId, {
+ timeoutMs: 45000,
+ retryDelayMs: 300,
+ stableMs: 3000,
+ initialDelayMs: 300,
+ });
+ }
+
async function openSignupEntryTab(step = 1) {
const tabId = await reuseOrCreateTab('signup-page', SIGNUP_ENTRY_URL, {
inject: SIGNUP_PAGE_INJECT_FILES,
injectSource: 'signup-page',
});
+ await waitForSignupEntryTabToSettle(tabId, step);
+
await ensureContentScriptReadyOnTab('signup-page', tabId, {
inject: SIGNUP_PAGE_INJECT_FILES,
injectSource: 'signup-page',
@@ -85,7 +108,7 @@
function fallbackSignupProfilePageUrl(rawUrl) {
const parsed = parseUrlSafely(rawUrl);
if (!parsed) return false;
- return /\/create-account\/profile(?:[/?#]|$)/i.test(parsed.pathname || '');
+ return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(parsed.pathname || '');
}
function resolveSignupPostIdentityState(rawUrl) {
diff --git a/background/steps/confirm-oauth.js b/background/steps/confirm-oauth.js
index a2cff0c..52fe3ce 100644
--- a/background/steps/confirm-oauth.js
+++ b/background/steps/confirm-oauth.js
@@ -33,6 +33,7 @@
setWebNavCommittedListener,
setStep8PendingReject,
setStep8TabUpdatedListener,
+ shouldDeferStep9CallbackTimeout,
} = deps;
const LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS = 240000;
@@ -96,6 +97,7 @@
let signupTabId = null;
const callbackWaitStartedAt = Date.now();
let timeoutCheckTimer = null;
+ let timeoutDeferredLogged = false;
const cleanupListener = () => {
if (timeoutCheckTimer) {
@@ -128,11 +130,46 @@
});
};
+ const isCallbackTimeoutDeferred = async (elapsedMs) => {
+ if (typeof shouldDeferStep9CallbackTimeout !== 'function') {
+ return false;
+ }
+ try {
+ const deferred = await shouldDeferStep9CallbackTimeout({
+ tabId: signupTabId,
+ visibleStep,
+ elapsedMs,
+ oauthUrl: activeState?.oauthUrl || '',
+ });
+ if (deferred && !timeoutDeferredLogged) {
+ timeoutDeferredLogged = true;
+ await addStepLog(
+ visibleStep,
+ '检测到认证页仍在安全验证/授权跳转中,暂停本地回调超时判定,继续等待 localhost 回调...',
+ 'info'
+ );
+ }
+ return Boolean(deferred);
+ } catch (error) {
+ await addStepLog(
+ visibleStep,
+ `复核认证页跳转状态失败(${error?.message || error}),继续按原超时规则等待回调。`,
+ 'warn'
+ );
+ return false;
+ }
+ };
+
const checkCallbackTimeout = async () => {
if (resolved) {
return;
}
const elapsedMs = Date.now() - callbackWaitStartedAt;
+ if (await isCallbackTimeoutDeferred(elapsedMs)) {
+ timeoutCheckTimer = setTimeout(checkCallbackTimeout, CALLBACK_TIMEOUT_CHECK_INTERVAL_MS);
+ return;
+ }
+
if (elapsedMs >= LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS) {
rejectStep9(new Error(`${Math.round(LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`));
return;
diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js
index 7ce89e1..a89dfc1 100644
--- a/background/steps/create-plus-checkout.js
+++ b/background/steps/create-plus-checkout.js
@@ -8,6 +8,8 @@
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
+ const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
+ const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
function createPlusCheckoutCreateExecutor(deps = {}) {
const {
@@ -86,6 +88,17 @@
return cleaned;
}
+ function normalizeGpcHelperPhoneMode(value = '') {
+ const rootScope = typeof self !== 'undefined' ? self : globalThis;
+ if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
+ return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
+ }
+ const normalized = String(value || '').trim().toLowerCase();
+ return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
+ ? GPC_HELPER_PHONE_MODE_AUTO
+ : GPC_HELPER_PHONE_MODE_MANUAL;
+ }
+
function normalizeGpcOtpChannel(value = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcOtpChannel) {
@@ -143,6 +156,17 @@
return buildGpcHelperApiUrl(apiUrl, '/api/gp/tasks');
}
+ function buildGpcBalanceUrl(apiUrl = '') {
+ const rootScope = typeof self !== 'undefined' ? self : globalThis;
+ if (rootScope.GoPayUtils?.buildGpcApiKeyBalanceUrl) {
+ return rootScope.GoPayUtils.buildGpcApiKeyBalanceUrl(apiUrl);
+ }
+ if (rootScope.GoPayUtils?.buildGpcCardBalanceUrl) {
+ return rootScope.GoPayUtils.buildGpcCardBalanceUrl(apiUrl);
+ }
+ return buildGpcHelperApiUrl(apiUrl, '/api/gp/balance');
+ }
+
function unwrapGpcResponse(payload = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.unwrapGpcResponse) {
@@ -176,6 +200,55 @@
return payload?.data?.detail || payload?.detail || payload?.message || payload?.error || `HTTP ${status || 0}`;
}
+ function getGpcRemainingUses(payload = {}) {
+ const rootScope = typeof self !== 'undefined' ? self : globalThis;
+ if (rootScope.GoPayUtils?.getGpcBalanceRemainingUses) {
+ return rootScope.GoPayUtils.getGpcBalanceRemainingUses(payload);
+ }
+ const data = unwrapGpcResponse(payload);
+ const numeric = Number(data?.remaining_uses ?? data?.remainingUses ?? data?.balance ?? data?.remaining);
+ return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null;
+ }
+
+ function isGpcAutoModeEnabled(payload = {}) {
+ const rootScope = typeof self !== 'undefined' ? self : globalThis;
+ if (rootScope.GoPayUtils?.isGpcAutoModeEnabled) {
+ return rootScope.GoPayUtils.isGpcAutoModeEnabled(payload);
+ }
+ const data = unwrapGpcResponse(payload);
+ return data?.auto_mode_enabled === true || data?.autoModeEnabled === true;
+ }
+
+ async function assertGpcApiKeyReadyForCreate(state = {}, phoneMode = GPC_HELPER_PHONE_MODE_MANUAL, apiKey = '') {
+ const apiUrl = buildGpcBalanceUrl(state?.gopayHelperApiUrl);
+ if (!apiUrl) {
+ throw new Error('创建 GPC 订单失败:缺少 API 地址。');
+ }
+ const { response, data } = await fetchJsonWithTimeout(apiUrl, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'X-API-Key': apiKey,
+ },
+ }, 30000);
+ if (!response?.ok || !isGpcUnifiedResponseOk(data)) {
+ const detail = getGpcResponseErrorDetail(data, response?.status || 0);
+ throw new Error(`创建 GPC 订单失败:API Key 校验失败:${detail}`);
+ }
+ const balanceData = unwrapGpcResponse(data);
+ const remainingUses = getGpcRemainingUses(balanceData);
+ const status = String(balanceData?.status || balanceData?.card_status || balanceData?.cardStatus || '').trim().toLowerCase();
+ if (status && status !== 'active') {
+ throw new Error(`创建 GPC 订单失败:API Key 状态不可用(${status})。`);
+ }
+ if (remainingUses !== null && remainingUses <= 0) {
+ throw new Error('创建 GPC 订单失败:API Key 剩余次数不足。');
+ }
+ if (phoneMode === GPC_HELPER_PHONE_MODE_AUTO && !isGpcAutoModeEnabled(balanceData)) {
+ throw new Error('创建 GPC 订单失败:当前 GPC API Key 未开通自动模式。');
+ }
+ }
+
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
const fetcher = typeof fetchImpl === 'function'
? fetchImpl
@@ -184,11 +257,34 @@
throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。');
}
const controller = typeof AbortController === 'function' ? new AbortController() : null;
- const timer = controller ? setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 30000)) : null;
+ const effectiveTimeoutMs = Math.max(1000, Number(timeoutMs) || 30000);
+ let didTimeout = false;
+ let timer = null;
+ const buildTimeoutError = () => new Error(`GPC API 请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`);
+ const timeoutPromise = new Promise((_, reject) => {
+ timer = setTimeout(() => {
+ didTimeout = true;
+ reject(buildTimeoutError());
+ if (controller) {
+ controller.abort();
+ }
+ }, effectiveTimeoutMs);
+ });
try {
- const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) });
- const data = await response.json().catch(() => ({}));
+ const response = await Promise.race([
+ fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) }),
+ timeoutPromise,
+ ]);
+ const data = await Promise.race([
+ response.json().catch(() => ({})),
+ timeoutPromise,
+ ]);
return { response, data };
+ } catch (error) {
+ if (didTimeout || error?.name === 'AbortError') {
+ throw buildTimeoutError();
+ }
+ throw error;
} finally {
if (timer) clearTimeout(timer);
}
@@ -226,25 +322,31 @@
if (!apiUrl) {
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
}
+ const phoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode);
+ const isAutoMode = phoneMode === GPC_HELPER_PHONE_MODE_AUTO;
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim();
const countryCode = normalizeHelperCountryCode(state?.gopayHelperCountryCode || '86');
const pin = String(state?.gopayHelperPin || '').trim();
const apiKey = resolveGpcHelperApiKey(state);
- if (!phoneNumber) {
- throw new Error('创建 GPC 订单失败:缺少手机号。');
+ if (!isAutoMode && !phoneNumber) {
+ throw new Error('创建 GPC 订单失败:手动模式缺少手机号。');
}
- if (!pin) {
- throw new Error('创建 GPC 订单失败:缺少 PIN。');
+ if (!isAutoMode && !pin) {
+ throw new Error('创建 GPC 订单失败:手动模式缺少 PIN。');
}
+ throwIfStopped();
+ await assertGpcApiKeyReadyForCreate(state, phoneMode, apiKey);
throwIfStopped();
const payload = {
access_token: token,
- phone_mode: 'manual',
- country_code: countryCode,
- phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode),
- otp_channel: normalizeGpcOtpChannel(state?.gopayHelperOtpChannel),
+ phone_mode: phoneMode,
};
+ if (!isAutoMode) {
+ payload.country_code = countryCode;
+ payload.phone_number = normalizeHelperPhoneNumber(phoneNumber, countryCode);
+ payload.otp_channel = normalizeGpcOtpChannel(state?.gopayHelperOtpChannel);
+ }
const orderCreatedAt = Date.now();
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
@@ -273,6 +375,7 @@
remoteStage: String(taskData?.remote_stage || taskData?.remoteStage || '').trim(),
orderCreatedAt,
responsePayload: taskData && typeof taskData === 'object' && !Array.isArray(taskData) ? taskData : null,
+ phoneMode: normalizeGpcHelperPhoneMode(taskData?.phone_mode || taskData?.phoneMode || phoneMode),
country: 'ID',
currency: 'IDR',
checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
@@ -308,6 +411,7 @@
gopayHelperTaskStatus: result.taskStatus,
gopayHelperStatusText: result.statusText,
gopayHelperRemoteStage: result.remoteStage,
+ gopayHelperPhoneMode: result.phoneMode || normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode),
gopayHelperTaskPayload: result.responsePayload,
gopayHelperReferenceId: '',
gopayHelperGoPayGuid: '',
@@ -318,7 +422,7 @@
gopayHelperStartPayload: null,
gopayHelperOrderCreatedAt: result.orderCreatedAt || Date.now(),
});
- await addLog(`步骤 6:GPC 任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
+ await addLog(`步骤 6:GPC ${result.phoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
await completeStepFromBackground(6, {
plusCheckoutCountry: result.country || 'ID',
plusCheckoutCurrency: result.currency || 'IDR',
diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js
index 6f03bf7..b69ef78 100644
--- a/background/steps/fill-plus-checkout.js
+++ b/background/steps/fill-plus-checkout.js
@@ -11,7 +11,28 @@
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
+ const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
+ const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const GPC_TASK_POLL_INTERVAL_MS = 3000;
+ const GPC_TASK_STALE_STATUS_TIMEOUT_MS = 60000;
+ const GPC_REMOTE_STAGE_LABELS = {
+ auto_otp_wait: '等待自动 OTP',
+ checkout_order_start: '创建订单',
+ checkout_start: '创建订单',
+ completed: '充值完成',
+ gopay_validate_pin: '校验 PIN',
+ otp_ready: '等待 PIN',
+ otp_submitted_local: 'OTP 已提交',
+ payment_processing: '支付处理中',
+ pin_submitted_local: 'PIN 已提交',
+ sms_otp_wait: '等待短信 OTP',
+ whatsapp_otp_wait: '等待 WhatsApp OTP',
+ };
+ const GPC_WAITING_FOR_LABELS = {
+ auto_otp: '自动 OTP',
+ otp: 'OTP',
+ pin: 'PIN',
+ };
const PAYMENT_METHOD_CONFIGS = {
[PLUS_PAYMENT_METHOD_PAYPAL]: {
id: PLUS_PAYMENT_METHOD_PAYPAL,
@@ -112,6 +133,56 @@
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
+ function normalizeGpcHelperPhoneMode(value = '') {
+ const rootScope = typeof self !== 'undefined' ? self : globalThis;
+ if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
+ return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
+ }
+ const normalized = String(value || '').trim().toLowerCase();
+ return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
+ ? GPC_HELPER_PHONE_MODE_AUTO
+ : GPC_HELPER_PHONE_MODE_MANUAL;
+ }
+
+ function formatGpcRemoteStageLabel(stage = '') {
+ const normalized = String(stage || '').trim().toLowerCase();
+ if (!normalized) {
+ return '';
+ }
+ return GPC_REMOTE_STAGE_LABELS[normalized] || normalized;
+ }
+
+ function formatGpcWaitingForLabel(waitingFor = '') {
+ const normalized = String(waitingFor || '').trim().toLowerCase();
+ if (!normalized) {
+ return '';
+ }
+ return GPC_WAITING_FOR_LABELS[normalized] || normalized.toUpperCase();
+ }
+
+ function formatGpcTaskStatusLog(task = {}) {
+ const statusText = String(task?.status_text || task?.statusText || '').trim();
+ const status = String(task?.status || '').trim();
+ const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim();
+ const stageText = formatGpcRemoteStageLabel(remoteStage);
+ const waitingForText = formatGpcWaitingForLabel(task?.api_waiting_for || task?.apiWaitingFor || '');
+ const mainText = stageText || statusText || status || '处理中';
+ const parts = [`步骤 7:GPC 任务状态:${mainText}`];
+ if (waitingForText && !mainText.includes(waitingForText)) {
+ parts.push(`,等待 ${waitingForText}`);
+ }
+ return parts.join('');
+ }
+
+ function getGpcHelperPhoneMode(state = {}, task = null) {
+ return normalizeGpcHelperPhoneMode(
+ task?.phone_mode
+ || task?.phoneMode
+ || state?.gopayHelperPhoneMode
+ || state?.phoneMode
+ );
+ }
+
function getPaymentMethodConfig(method = PLUS_PAYMENT_METHOD_PAYPAL) {
return PAYMENT_METHOD_CONFIGS[normalizePlusPaymentMethod(method)] || PAYMENT_METHOD_CONFIGS[PLUS_PAYMENT_METHOD_PAYPAL];
}
@@ -139,11 +210,34 @@
throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。');
}
const controller = typeof AbortController === 'function' ? new AbortController() : null;
- const timer = controller ? setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 30000)) : null;
+ const effectiveTimeoutMs = Math.max(1000, Number(timeoutMs) || 30000);
+ let didTimeout = false;
+ let timer = null;
+ const buildTimeoutError = () => new Error(`GPC API 请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`);
+ const timeoutPromise = new Promise((_, reject) => {
+ timer = setTimeout(() => {
+ didTimeout = true;
+ reject(buildTimeoutError());
+ if (controller) {
+ controller.abort();
+ }
+ }, effectiveTimeoutMs);
+ });
try {
- const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) });
- const data = await response.json().catch(() => ({}));
+ const response = await Promise.race([
+ fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) }),
+ timeoutPromise,
+ ]);
+ const data = await Promise.race([
+ response.json().catch(() => ({})),
+ timeoutPromise,
+ ]);
return { response, data };
+ } catch (error) {
+ if (didTimeout || error?.name === 'AbortError') {
+ throw buildTimeoutError();
+ }
+ throw error;
} finally {
if (timer) clearTimeout(timer);
}
@@ -299,7 +393,7 @@
task.task_id = String(task.task_id || task.taskId || '').trim();
task.status = String(task.status || '').trim().toLowerCase();
task.status_text = String(task.status_text || task.statusText || '').trim();
- task.phone_mode = String(task.phone_mode || task.phoneMode || '').trim().toLowerCase();
+ task.phone_mode = normalizeGpcHelperPhoneMode(task.phone_mode || task.phoneMode || '');
task.remote_stage = String(task.remote_stage || task.remoteStage || '').trim().toLowerCase();
task.api_waiting_for = String(task.api_waiting_for || task.apiWaitingFor || '').trim().toLowerCase();
task.api_input_deadline_at = String(task.api_input_deadline_at || task.apiInputDeadlineAt || '').trim();
@@ -318,6 +412,7 @@
gopayHelperTaskId: task.task_id,
gopayHelperTaskStatus: task.status,
gopayHelperStatusText: task.status_text,
+ gopayHelperPhoneMode: task.phone_mode,
gopayHelperRemoteStage: task.remote_stage,
gopayHelperApiWaitingFor: task.api_waiting_for,
gopayHelperApiInputDeadlineAt: task.api_input_deadline_at,
@@ -675,12 +770,16 @@
});
}
- function isGpcTaskOtpWait(task = {}) {
- return task?.phone_mode === 'manual' && task?.api_waiting_for === 'otp';
+ function isGpcTaskManualMode(task = {}, state = {}) {
+ return getGpcHelperPhoneMode(state, task) === GPC_HELPER_PHONE_MODE_MANUAL;
}
- function isGpcTaskPinWait(task = {}) {
- return task?.phone_mode === 'manual'
+ function isGpcTaskOtpWait(task = {}, state = {}) {
+ return isGpcTaskManualMode(task, state) && task?.api_waiting_for === 'otp';
+ }
+
+ function isGpcTaskPinWait(task = {}, state = {}) {
+ return isGpcTaskManualMode(task, state)
&& (task?.api_waiting_for === 'pin' || task?.status === 'otp_ready');
}
@@ -688,6 +787,49 @@
return ['completed', 'failed', 'expired', 'discarded'].includes(String(status || '').trim().toLowerCase());
}
+ function buildGpcTaskProgressSignature(task = {}) {
+ return [
+ task?.status,
+ task?.status_text,
+ task?.remote_stage,
+ task?.api_waiting_for,
+ task?.last_input_error,
+ task?.otp_invalid_count,
+ task?.reference_id || task?.referenceId,
+ task?.redirect_url || task?.redirectUrl,
+ task?.flow_id || task?.flowId,
+ task?.challenge_id || task?.challengeId,
+ task?.gopay_guid || task?.gopayGuid,
+ ].map((value) => String(value ?? '').trim()).join('|');
+ }
+
+ function shouldWatchGpcTaskProgress(task = {}, state = {}) {
+ if (!task || isGpcTaskTerminal(task.status)) {
+ return false;
+ }
+ if (isGpcTaskOtpWait(task, state) || isGpcTaskPinWait(task, state)) {
+ return false;
+ }
+ return true;
+ }
+
+ function getGpcTaskStaleStatusTimeoutMs(state = {}) {
+ const configuredSeconds = Number(state?.gopayHelperTaskStaleSeconds);
+ if (Number.isFinite(configuredSeconds) && configuredSeconds > 0) {
+ return Math.max(15000, Math.min(600000, Math.floor(configuredSeconds * 1000)));
+ }
+ return GPC_TASK_STALE_STATUS_TIMEOUT_MS;
+ }
+
+ function buildGpcTaskStaleStatusError(task = {}, staleTimeoutMs = GPC_TASK_STALE_STATUS_TIMEOUT_MS) {
+ const seconds = Math.max(1, Math.round(staleTimeoutMs / 1000));
+ const label = formatGpcRemoteStageLabel(task?.remote_stage)
+ || task?.status_text
+ || task?.status
+ || '未知状态';
+ return new Error(`GPC_TASK_ENDED::GPC 任务状态超过 ${seconds} 秒无进展(${label}),请重新创建任务。`);
+ }
+
function buildGpcTaskTerminalError(task = {}) {
const status = String(task?.status || '').trim().toLowerCase();
const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim();
@@ -829,6 +971,9 @@
let lastSubmittedOtp = '';
let pinSubmitted = false;
let terminalReached = false;
+ let lastProgressSignature = '';
+ let lastProgressAt = Date.now();
+ const staleStatusTimeoutMs = getGpcTaskStaleStatusTimeoutMs(state);
if (!taskId) {
throw new Error('步骤 7:GPC 模式缺少 task_id,请先执行步骤 6。');
@@ -840,27 +985,25 @@
throw new Error('步骤 7:GPC 模式缺少 API Key。');
}
+ const configuredPhoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode || GPC_HELPER_PHONE_MODE_MANUAL);
const rawPin = String(state?.gopayHelperPin || '').trim();
const pinDigits = rawPin.replace(/[^\d]/g, '');
const pin = normalizeSixDigitPin(rawPin);
- if (!pin) {
+ if (configuredPhoneMode === GPC_HELPER_PHONE_MODE_MANUAL && !pin) {
if (taskId && apiUrl && apiKey) {
await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, 'PIN 配置错误');
}
throw new Error(pinDigits
? '步骤 7:GPC PIN 必须是 6 位数字,请检查侧边栏配置。'
- : '步骤 7:GPC 模式缺少 PIN 配置。');
+ : '步骤 7:GPC 手动模式缺少 PIN 配置。');
}
- await addLog(`步骤 7:GPC 模式开始轮询任务(task_id: ${taskId})...`, 'info');
+ await addLog(`步骤 7:GPC ${configuredPhoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式开始轮询任务(task_id: ${taskId})...`, 'info');
try {
while (Date.now() <= deadline) {
throwIfStopped();
const task = await fetchGpcTaskStatus(apiUrl, taskId, apiKey);
- const statusText = task?.status_text || task?.status || '处理中';
- const remoteStage = task?.remote_stage || '';
- const waitingFor = task?.api_waiting_for || '';
- await addLog(`步骤 7:GPC 任务状态:${statusText}${remoteStage ? `(${remoteStage})` : ''}${waitingFor ? `,等待 ${waitingFor.toUpperCase()}` : ''}`, 'info');
+ await addLog(formatGpcTaskStatusLog(task), 'info');
if (task.status === 'completed') {
terminalReached = true;
@@ -879,7 +1022,21 @@
throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。');
}
- if (isGpcTaskOtpWait(task)) {
+ if (shouldWatchGpcTaskProgress(task, state)) {
+ const progressSignature = buildGpcTaskProgressSignature(task);
+ const now = Date.now();
+ if (progressSignature && progressSignature !== lastProgressSignature) {
+ lastProgressSignature = progressSignature;
+ lastProgressAt = now;
+ } else if (progressSignature && now - lastProgressAt >= staleStatusTimeoutMs) {
+ throw buildGpcTaskStaleStatusError(task, staleStatusTimeoutMs);
+ }
+ } else {
+ lastProgressSignature = '';
+ lastProgressAt = Date.now();
+ }
+
+ if (isGpcTaskOtpWait(task, state)) {
if (isGpcTaskInputDeadlineExpired(task)) {
throw buildGpcInputDeadlineError(task, 'OTP');
}
@@ -940,7 +1097,7 @@
otpLastSubmittedAt = Date.now();
lastSubmittedOtp = normalizedOtp;
await addLog('步骤 7:OTP 已提交,继续等待 GPC 任务状态更新。', 'ok');
- } else if (isGpcTaskPinWait(task) && !pinSubmitted) {
+ } else if (isGpcTaskPinWait(task, state) && !pinSubmitted) {
if (isGpcTaskInputDeadlineExpired(task)) {
throw buildGpcInputDeadlineError(task, 'PIN');
}
diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js
index c8f0b02..ba39ca8 100644
--- a/background/steps/oauth-login.js
+++ b/background/steps/oauth-login.js
@@ -58,7 +58,30 @@
&& !Boolean(state?.contributionMode);
}
+ function hasStep7PhoneSignupIdentity(state = {}) {
+ return Boolean(
+ String(state?.signupPhoneNumber || '').trim()
+ || String(state?.signupPhoneCompletedActivation?.phoneNumber || '').trim()
+ || String(state?.signupPhoneActivation?.phoneNumber || '').trim()
+ || (
+ normalizeStep7IdentifierType(state?.accountIdentifierType) === 'phone'
+ && String(state?.accountIdentifier || '').trim()
+ )
+ );
+ }
+
+ function shouldPreferStep7PhoneSignupIdentity(state = {}) {
+ const frozenSignupMethod = normalizeStep7IdentifierType(state?.resolvedSignupMethod);
+ return canUseConfiguredPhoneSignup(state)
+ && frozenSignupMethod !== 'email'
+ && hasStep7PhoneSignupIdentity(state);
+ }
+
function resolveStep7LoginIdentifierType(state = {}, fallbackType = '') {
+ if (shouldPreferStep7PhoneSignupIdentity(state)) {
+ return 'phone';
+ }
+
const explicitIdentifierType = normalizeStep7IdentifierType(state?.accountIdentifierType);
if (explicitIdentifierType) {
return explicitIdentifierType;
diff --git a/background/steps/submit-signup-email.js b/background/steps/submit-signup-email.js
index 5a917f8..44b0a1c 100644
--- a/background/steps/submit-signup-email.js
+++ b/background/steps/submit-signup-email.js
@@ -18,6 +18,7 @@
resolveSignupEmailForFlow,
sendToContentScriptResilient,
SIGNUP_PAGE_INJECT_FILES,
+ waitForTabStableComplete = null,
} = deps;
function getErrorMessage(error) {
@@ -165,6 +166,25 @@
}
}
+ async function waitForStep2SignupTabToSettle(tabId, logMessage) {
+ if (!Number.isInteger(tabId) || typeof waitForTabStableComplete !== 'function') {
+ return null;
+ }
+
+ await addLog(
+ logMessage || '步骤 2:注册页标签已切换,正在等待页面加载完成并额外稳定 3 秒...',
+ 'info',
+ { step: 2, stepKey: 'signup-entry' }
+ );
+
+ return waitForTabStableComplete(tabId, {
+ timeoutMs: 45000,
+ retryDelayMs: 300,
+ stableMs: 3000,
+ initialDelayMs: 300,
+ });
+ }
+
async function ensureSignupPhoneEntryReady(tabId) {
if (!Number.isInteger(tabId)) {
throw new Error('步骤 2:未找到可用的注册页标签,无法切换到手机号注册入口。');
@@ -210,6 +230,10 @@
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
} else {
await chrome.tabs.update(signupTabId, { active: true });
+ await waitForStep2SignupTabToSettle(
+ signupTabId,
+ '步骤 2:已切换到注册页标签,正在等待页面加载完成并额外稳定 3 秒...'
+ );
await ensureContentScriptReadyOnTab('signup-page', signupTabId, {
inject: SIGNUP_PAGE_INJECT_FILES,
injectSource: 'signup-page',
diff --git a/background/verification-flow.js b/background/verification-flow.js
index ffb5158..ad820e6 100644
--- a/background/verification-flow.js
+++ b/background/verification-flow.js
@@ -159,7 +159,7 @@
if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) {
return false;
}
- return /\/create-account\/profile(?:[/?#]|$)/i.test(String(parsed.pathname || ''));
+ return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(String(parsed.pathname || ''));
} catch {
return false;
}
@@ -249,11 +249,21 @@
const authState = String(result?.state || '').trim();
const authUrl = String(result?.url || '').trim();
+ const verificationErrorText = String(result?.verificationErrorText || '').trim();
lastSnapshot = {
state: authState || 'unknown',
url: authUrl,
};
+ if (authState === 'verification_page' && verificationErrorText) {
+ return {
+ success: false,
+ reason: 'invalid_code',
+ invalidCode: true,
+ errorText: verificationErrorText,
+ url: authUrl,
+ };
+ }
if (authState === 'oauth_consent_page') {
return {
success: true,
@@ -1080,7 +1090,8 @@
},
};
let result;
- if (typeof sendToContentScriptResilient === 'function') {
+ const shouldAvoidReplaySubmit = step === 8;
+ if (typeof sendToContentScriptResilient === 'function' && !shouldAvoidReplaySubmit) {
try {
result = await sendToContentScriptResilient('signup-page', message, {
timeoutMs: Math.max(baseResponseTimeoutMs + 15000, 30000),
@@ -1143,6 +1154,56 @@
}
throw err;
}
+ } else if (shouldAvoidReplaySubmit) {
+ try {
+ result = await sendToContentScript('signup-page', message, {
+ responseTimeoutMs: baseResponseTimeoutMs,
+ });
+ } catch (err) {
+ if (isRetryableVerificationTransportError(err)) {
+ await addLog('认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...', 'warn', {
+ step: completionStep,
+ stepKey: 'fetch-login-code',
+ });
+ const fallback = await detectStep8PostSubmitFallback({
+ step,
+ timeoutMs: 9000,
+ pollIntervalMs: 300,
+ });
+ if (fallback.invalidCode) {
+ return {
+ invalidCode: true,
+ errorText: fallback.errorText || '验证码被拒绝。',
+ url: fallback.url || '',
+ };
+ }
+ if (fallback.success) {
+ if (fallback.addPhonePage) {
+ await addLog('验证码提交后通信中断,但页面已进入手机号验证页,按提交成功继续。', 'warn', {
+ step: completionStep,
+ stepKey: 'fetch-login-code',
+ });
+ } else {
+ await addLog('验证码提交后通信中断,但页面已进入 OAuth 授权页,按提交成功继续。', 'warn', {
+ step: completionStep,
+ stepKey: 'fetch-login-code',
+ });
+ }
+ return {
+ success: true,
+ assumed: true,
+ transportRecovered: true,
+ addPhonePage: Boolean(fallback.addPhonePage),
+ url: fallback.url || '',
+ };
+ }
+ if (fallback.restartStep7) {
+ const urlPart = fallback.url ? ` URL: ${fallback.url}` : '';
+ throw new Error(`STEP8_RESTART_STEP7::步骤 ${completionStep}:验证码提交后认证页进入登录超时报错页,请回到步骤 ${authLoginStep} 重新开始。${urlPart}`.trim());
+ }
+ }
+ throw err;
+ }
} else {
result = await sendToContentScript('signup-page', message, {
responseTimeoutMs: baseResponseTimeoutMs,
@@ -1287,20 +1348,8 @@
continue;
}
- const remainingBeforeResendMs = resendIntervalMs > 0 && lastResendAt > 0
- ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
- : 0;
- if (remainingBeforeResendMs > 0) {
- await addLog(
- `步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`,
- 'warn'
- );
- await sleepWithStop(Math.min(remainingBeforeResendMs, 2000));
- continue;
- }
-
if (remainingAutomaticResendCount <= 0) {
- await addLog(`步骤 ${step}:已达到自动重新发送验证码次数上限,将直接使用当前时间窗口继续重试。`, 'warn');
+ await addLog(`步骤 ${step}:已达到自动重新发送验证码次数上限,将排除已拒绝验证码并继续轮询新邮件。`, 'warn');
continue;
}
diff --git a/content/phone-auth.js b/content/phone-auth.js
index 02ec249..268924d 100644
--- a/content/phone-auth.js
+++ b/content/phone-auth.js
@@ -21,6 +21,7 @@
} = deps;
const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::';
const PHONE_RESEND_BANNED_NUMBER_ERROR_PREFIX = 'PHONE_RESEND_BANNED_NUMBER::';
+ const PHONE_RESEND_SERVER_ERROR_PREFIX = 'PHONE_RESEND_SERVER_ERROR::';
const PHONE_MAX_USAGE_EXCEEDED_PATTERN = /phone_max_usage_exceeded/i;
const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::';
const PHONE_ROUTE_405_RECOVERY_COOLDOWN_MS = 6000;
@@ -28,6 +29,7 @@
const PHONE_RESEND_ROUTE_405_MAX_RECOVERY_TOTAL_MS = 12000;
const PHONE_RESEND_THROTTLED_PATTERN = /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试|重试次数过多/i;
const PHONE_RESEND_BANNED_NUMBER_PATTERN = /无法向此电话号码发送短信|无法向此手机号发送短信|无法发送短信到此电话号码|无法发送短信到此手机号|can(?:not|'t)\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number|unable\s+to\s+send\s+(?:an?\s+)?(?:sms|text(?:\s+message)?)\s+to\s+(?:this|that)\s+(?:phone\s+)?number/i;
+ const PHONE_RESEND_SERVER_ERROR_PATTERN = /this\s+page\s+isn['’]?t\s+working|currently\s+unable\s+to\s+handle\s+this\s+request|http\s+error\s+500|500\s+internal\s+server\s+error/i;
const PHONE_ROUTE_405_PATTERN = /405\s+method\s+not\s+allowed|route\s+error.*405|did\s+not\s+provide\s+an?\s+[`'"]?action|post\s+request\s+to\s+["']?\/phone-verification/i;
const PHONE_ROUTE_405_MAX_RECOVERY_CLICKS = 3;
const rootScope = typeof self !== 'undefined' ? self : globalThis;
@@ -550,6 +552,20 @@
return '';
}
+ function getPhoneResendServerErrorText() {
+ const path = String(location?.pathname || '');
+ if (!/\/contact-verification(?:[/?#]|$)/i.test(path)) {
+ return '';
+ }
+ const text = String(getPageTextSnapshot?.() || '').replace(/\s+/g, ' ').trim();
+ const title = String(document?.title || '').replace(/\s+/g, ' ').trim();
+ const combined = `${title} ${text}`.trim();
+ if (!PHONE_RESEND_SERVER_ERROR_PATTERN.test(combined)) {
+ return '';
+ }
+ return combined || 'OpenAI contact-verification page returned HTTP ERROR 500 after resend.';
+ }
+
function checkPhoneResendError() {
const maxUsageText = getAddPhoneErrorText();
if (maxUsageText && PHONE_MAX_USAGE_EXCEEDED_PATTERN.test(maxUsageText)) {
@@ -583,6 +599,17 @@
};
}
+ const serverErrorText = getPhoneResendServerErrorText();
+ if (serverErrorText) {
+ return {
+ hasError: true,
+ reason: 'resend_server_error',
+ prefix: PHONE_RESEND_SERVER_ERROR_PREFIX,
+ message: serverErrorText,
+ url: location.href,
+ };
+ }
+
return {
hasError: false,
reason: '',
@@ -897,6 +924,10 @@
if (throttledText) {
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${throttledText}`);
}
+ const serverErrorText = getPhoneResendServerErrorText();
+ if (serverErrorText) {
+ throw new Error(`${PHONE_RESEND_SERVER_ERROR_PREFIX}${serverErrorText}`);
+ }
const resendButton = getPhoneVerificationResendButton({ allowDisabled: true });
if (resendButton && isActionEnabled(resendButton)) {
const resendInfo = getPhoneVerificationResendActionInfo(resendButton);
@@ -936,6 +967,10 @@
if (afterClickThrottleText) {
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${afterClickThrottleText}`);
}
+ const afterClickServerErrorText = getPhoneResendServerErrorText();
+ if (afterClickServerErrorText) {
+ throw new Error(`${PHONE_RESEND_SERVER_ERROR_PREFIX}${afterClickServerErrorText}`);
+ }
return {
resent: true,
channel: resendInfo.channel || 'sms',
@@ -957,6 +992,11 @@
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${timeoutThrottleText}`);
}
+ const timeoutServerErrorText = getPhoneResendServerErrorText();
+ if (timeoutServerErrorText) {
+ throw new Error(`${PHONE_RESEND_SERVER_ERROR_PREFIX}${timeoutServerErrorText}`);
+ }
+
throw new Error('Timed out waiting for the phone verification resend button.');
})().finally(() => {
activePhoneResendPromise = null;
diff --git a/content/signup-page.js b/content/signup-page.js
index 7bbb7b2..ced1e82 100644
--- a/content/signup-page.js
+++ b/content/signup-page.js
@@ -155,6 +155,8 @@ const LOGIN_EXTERNAL_IDP_PATTERN = /google|microsoft|apple|sso|single\s+sign[-\s
const LOGIN_CODE_ONLY_ACTION_PATTERN = /one[-\s]*time|passcode|use\s+(?:a\s+)?code|验证码|一次性/i;
const RESEND_VERIFICATION_CODE_PATTERN = /重新发送(?:验证码)?|再次发送(?:验证码)?|重发(?:验证码)?|未收到(?:验证码|邮件)|resend(?:\s+code)?|send\s+(?:a\s+)?new\s+code|send\s+(?:it\s+)?again|request\s+(?:a\s+)?new\s+code|didn'?t\s+receive/i;
+const PHONE_RESEND_SERVER_ERROR_PREFIX = 'PHONE_RESEND_SERVER_ERROR::';
+const CONTACT_VERIFICATION_SERVER_ERROR_PATTERN = /this\s+page\s+isn['’]?t\s+working|currently\s+unable\s+to\s+handle\s+this\s+request|http\s+error\s+500|500\s+internal\s+server\s+error/i;
function isVisibleElement(el) {
if (!el) return false;
@@ -258,6 +260,27 @@ function isEmailVerificationPage() {
return /\/email-verification(?:[/?#]|$)/i.test(location.pathname || '');
}
+function getContactVerificationServerErrorText() {
+ const path = String(location?.pathname || '');
+ if (!/\/contact-verification(?:[/?#]|$)/i.test(path)) {
+ return '';
+ }
+ const text = String(getPageTextSnapshot?.() || document?.body?.textContent || '').replace(/\s+/g, ' ').trim();
+ const title = String(document?.title || '').replace(/\s+/g, ' ').trim();
+ const combined = `${title} ${text}`.trim();
+ if (!CONTACT_VERIFICATION_SERVER_ERROR_PATTERN.test(combined)) {
+ return '';
+ }
+ return combined || 'OpenAI contact-verification page returned HTTP ERROR 500 after resend.';
+}
+
+function throwIfContactVerificationServerError() {
+ const serverErrorText = getContactVerificationServerErrorText();
+ if (serverErrorText) {
+ throw new Error(`${PHONE_RESEND_SERVER_ERROR_PREFIX}${serverErrorText}`);
+ }
+}
+
async function resendVerificationCode(step, timeout = 45000) {
const performOperationWithDelay = typeof getOperationDelayRunner === 'function'
? getOperationDelayRunner()
@@ -276,6 +299,7 @@ async function resendVerificationCode(step, timeout = 45000) {
while (Date.now() - start < timeout) {
throwIfStopped();
+ throwIfContactVerificationServerError();
// Check for 405 error page and recover by clicking "Try again"
if (is405MethodNotAllowedPage()) {
@@ -302,6 +326,7 @@ async function resendVerificationCode(step, timeout = 45000) {
loggedWaiting = false;
continue;
}
+ throwIfContactVerificationServerError();
return {
resent: true,
@@ -317,6 +342,7 @@ async function resendVerificationCode(step, timeout = 45000) {
await sleep(250);
}
+ throwIfContactVerificationServerError();
throw new Error('无法点击重新发送验证码按钮。URL: ' + location.href);
}
@@ -986,6 +1012,8 @@ async function waitForSignupEntryState(options = {}) {
logDiagnostics = false,
} = options;
const start = Date.now();
+ const maxSignupEntryClickRetries = 5;
+ const maxSignupEntryClickAttempts = maxSignupEntryClickRetries + 1;
let lastTriggerClickAt = 0;
let clickAttempts = 0;
let lastState = '';
@@ -1042,12 +1070,21 @@ async function waitForSignupEntryState(options = {}) {
}
if (Date.now() - lastTriggerClickAt >= 1500) {
+ if (clickAttempts >= maxSignupEntryClickAttempts) {
+ log(`步骤 ${step}:官网注册入口已完成 ${maxSignupEntryClickRetries} 次重试,页面仍未进入邮箱输入页,停止重试。`, 'warn');
+ return snapshot;
+ }
lastTriggerClickAt = Date.now();
clickAttempts += 1;
+ const retryAttempt = clickAttempts - 1;
if (logDiagnostics) {
- log(`步骤 ${step}:正在点击官网注册入口(第 ${clickAttempts} 次):"${getActionText(snapshot.signupTrigger).slice(0, 80)}"`);
+ log(`步骤 ${step}:正在点击官网注册入口(第 ${clickAttempts}/${maxSignupEntryClickAttempts} 次):"${getActionText(snapshot.signupTrigger).slice(0, 80)}"`);
}
- log('步骤 2:正在点击官网注册入口...');
+ log(retryAttempt > 0
+ ? `步骤 ${step}:上次点击后仍未进入邮箱输入页,等待 3 秒后重试点击官网注册入口(重试 ${retryAttempt}/${maxSignupEntryClickRetries})...`
+ : `步骤 ${step}:已找到官网注册入口,等待 3 秒后点击...`);
+ await sleep(3000);
+ throwIfStopped();
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'signup-entry', kind: 'click', label: 'open-signup-entry' }, async () => {
simulateClick(snapshot.signupTrigger);
@@ -2282,7 +2319,10 @@ async function waitForSignupPhoneEntryState(options = {}) {
step = 2,
} = options;
const start = Date.now();
+ const maxSignupEntryClickRetries = 5;
+ const maxSignupEntryClickAttempts = maxSignupEntryClickRetries + 1;
let lastTriggerClickAt = 0;
+ let clickAttempts = 0;
let lastSwitchToPhoneAt = 0;
let lastMoreOptionsClickAt = 0;
let slowSnapshotLogged = false;
@@ -2328,8 +2368,18 @@ async function waitForSignupPhoneEntryState(options = {}) {
if (snapshot.state === 'entry_home' && snapshot.signupTrigger) {
if (Date.now() - lastTriggerClickAt >= 1500) {
+ if (clickAttempts >= maxSignupEntryClickAttempts) {
+ log(`步骤 ${step}:官网注册入口已完成 ${maxSignupEntryClickRetries} 次重试,页面仍未进入手机号输入页,停止重试。`, 'warn');
+ return snapshot;
+ }
lastTriggerClickAt = Date.now();
- log(`步骤 ${step}:正在点击官网注册入口...`);
+ clickAttempts += 1;
+ const retryAttempt = clickAttempts - 1;
+ log(retryAttempt > 0
+ ? `步骤 ${step}:上次点击后仍未进入手机号输入页,等待 3 秒后重试点击官网注册入口(重试 ${retryAttempt}/${maxSignupEntryClickRetries})...`
+ : `步骤 ${step}:已找到官网注册入口,等待 3 秒后点击...`);
+ await sleep(3000);
+ throwIfStopped();
await humanPause(350, 900);
await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'click', label: 'open-signup-entry' }, async () => {
simulateClick(snapshot.signupTrigger);
@@ -2735,7 +2785,7 @@ function isSignupProfilePageUrl(rawUrl = location.href) {
if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) {
return false;
}
- return /\/create-account\/profile(?:[/?#]|$)/i.test(String(parsed.pathname || ''));
+ return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(String(parsed.pathname || ''));
} catch {
return false;
}
@@ -4109,6 +4159,7 @@ function serializeLoginAuthState(snapshot) {
url: snapshot?.url || location.href,
path: snapshot?.path || location.pathname || '',
displayedEmail: snapshot?.displayedEmail || '',
+ verificationErrorText: getVerificationErrorText(),
retryEnabled: Boolean(snapshot?.retryEnabled),
titleMatched: Boolean(snapshot?.titleMatched),
detailMatched: Boolean(snapshot?.detailMatched),
@@ -6266,14 +6317,23 @@ function getSerializableRect(el) {
// Step 5: Fill Name & Birthday / Age
// ============================================================
-function getStep5DirectCompletionPayload({ isAgeMode = false } = {}) {
+function getStep5DirectCompletionPayload({ isAgeMode = false, navigationStarted = false, outcome = null } = {}) {
const payload = {
- skippedPostSubmitCheck: true,
- directProceedToStep6: true,
+ profileSubmitted: true,
+ postSubmitChecked: true,
};
if (isAgeMode) {
payload.ageMode = true;
}
+ if (navigationStarted) {
+ payload.navigationStarted = true;
+ }
+ if (outcome?.state) {
+ payload.outcome = outcome.state;
+ }
+ if (outcome?.url) {
+ payload.url = outcome.url;
+ }
return payload;
}
@@ -6322,6 +6382,252 @@ async function waitForCombinedSignupVerificationProfilePage(timeout = 2500) {
return isCombinedSignupVerificationProfilePage();
}
+function getStep5ProfilePathPatterns() {
+ return [
+ /\/create-account\/profile(?:[/?#]|$)/i,
+ /\/u\/signup\/profile(?:[/?#]|$)/i,
+ /\/signup\/profile(?:[/?#]|$)/i,
+ ];
+}
+
+function getStep5AuthRetryPathPatterns() {
+ const signupPatterns = typeof getSignupAuthRetryPathPatterns === 'function'
+ ? getSignupAuthRetryPathPatterns()
+ : [];
+ return [
+ ...signupPatterns,
+ ...getStep5ProfilePathPatterns(),
+ ];
+}
+
+function isStep5ProfilePageUrl(rawUrl = location.href) {
+ return isSignupProfilePageUrl(rawUrl);
+}
+
+function getStep5AuthRetryPageState() {
+ if (typeof getAuthTimeoutErrorPageState === 'function') {
+ return getAuthTimeoutErrorPageState({
+ pathPatterns: getStep5AuthRetryPathPatterns(),
+ });
+ }
+
+ if (typeof getCurrentAuthRetryPageState === 'function') {
+ return getCurrentAuthRetryPageState('signup');
+ }
+
+ return null;
+}
+
+function getStep5SubmitButton() {
+ const direct = document.querySelector('button[type="submit"], input[type="submit"]');
+ if (direct && isVisibleElement(direct)) {
+ return direct;
+ }
+
+ const candidates = document.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]');
+ return Array.from(candidates).find((el) => {
+ if (!isVisibleElement(el)) return false;
+ const text = typeof getActionText === 'function'
+ ? getActionText(el)
+ : [
+ el?.textContent,
+ el?.value,
+ el?.getAttribute?.('aria-label'),
+ el?.getAttribute?.('title'),
+ ]
+ .filter(Boolean)
+ .join(' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+ return /完成|创建|create|continue|finish|done|agree/i.test(text);
+ }) || null;
+}
+
+async function waitForStep5SubmitButton(timeout = 5000) {
+ const start = Date.now();
+
+ while (Date.now() - start < timeout) {
+ throwIfStopped();
+ const button = getStep5SubmitButton();
+ if (button) {
+ return button;
+ }
+ await sleep(150);
+ }
+
+ return null;
+}
+
+function isStep5SubmitButtonClickable(button) {
+ return Boolean(button)
+ && isVisibleElement(button)
+ && !button.disabled
+ && button.getAttribute?.('aria-disabled') !== 'true';
+}
+
+function isStep5ProfileStillVisible() {
+ if (isStep5ProfilePageUrl()) {
+ return true;
+ }
+
+ return typeof isStep5Ready === 'function' ? isStep5Ready() : false;
+}
+
+function getStep5PostSubmitSuccessState() {
+ if (getStep5AuthRetryPageState()) {
+ return null;
+ }
+
+ if (isLikelyLoggedInChatgptHomeUrl()) {
+ 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()) {
+ return {
+ state: 'left_profile',
+ url: location.href,
+ };
+ }
+
+ return null;
+}
+
+function installStep5NavigationCompletionReporter(completeOnce) {
+ if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
+ return () => {};
+ }
+
+ const onNavigationStarted = () => {
+ completeOnce({
+ navigationStarted: true,
+ outcome: {
+ state: 'navigation_started',
+ url: location.href,
+ },
+ });
+ };
+
+ window.addEventListener('pagehide', onNavigationStarted, { once: true });
+ window.addEventListener('beforeunload', onNavigationStarted, { once: true });
+
+ return () => {
+ window.removeEventListener('pagehide', onNavigationStarted);
+ window.removeEventListener('beforeunload', onNavigationStarted);
+ };
+}
+
+async function waitForStep5SubmitOutcome(options = {}) {
+ const {
+ timeoutMs = 45000,
+ maxAuthRetryRecoveries = 2,
+ maxSubmitClicks = 3,
+ retryClickIntervalMs = 3500,
+ } = options;
+ const start = Date.now();
+ let authRetryRecoveryCount = 0;
+ let submitClickCount = 1;
+ let lastSubmitClickAt = Date.now();
+ let lastStep5Error = '';
+
+ while (Date.now() - start < timeoutMs) {
+ throwIfStopped();
+
+ const retryState = getStep5AuthRetryPageState();
+ if (retryState?.userAlreadyExistsBlocked) {
+ throw createSignupUserAlreadyExistsError();
+ }
+ if (retryState?.maxCheckAttemptsBlocked) {
+ throw createAuthMaxCheckAttemptsError();
+ }
+ if (retryState) {
+ if (authRetryRecoveryCount >= maxAuthRetryRecoveries) {
+ throw new Error(`步骤 5:资料提交后连续进入认证重试页 ${maxAuthRetryRecoveries} 次,页面仍未恢复。URL: ${location.href}`);
+ }
+ authRetryRecoveryCount += 1;
+ log(`步骤 5:资料提交后进入认证重试页,正在自动恢复(${authRetryRecoveryCount}/${maxAuthRetryRecoveries})...`, 'warn');
+ await recoverCurrentAuthRetryPage({
+ flow: 'signup',
+ logLabel: '步骤 5:资料提交后检测到认证重试页,正在点击“重试”恢复',
+ maxClickAttempts: 2,
+ pathPatterns: getStep5AuthRetryPathPatterns(),
+ step: 5,
+ timeoutMs: 12000,
+ });
+ lastSubmitClickAt = Date.now();
+ continue;
+ }
+
+ const successState = getStep5PostSubmitSuccessState();
+ if (successState) {
+ return successState;
+ }
+
+ const step5Error = typeof getStep5ErrorText === 'function' ? getStep5ErrorText() : '';
+ if (step5Error) {
+ lastStep5Error = step5Error;
+ }
+
+ if (
+ isStep5ProfileStillVisible()
+ && submitClickCount < maxSubmitClicks
+ && Date.now() - lastSubmitClickAt >= retryClickIntervalMs
+ ) {
+ const submitButton = getStep5SubmitButton();
+ if (isStep5SubmitButtonClickable(submitButton)) {
+ submitClickCount += 1;
+ log(`步骤 5:资料提交后仍停留在资料页,正在重新点击“完成帐户创建”(第 ${submitClickCount}/${maxSubmitClicks} 次)...`, 'warn');
+ await humanPause(350, 900);
+ simulateClick(submitButton);
+ lastSubmitClickAt = Date.now();
+ await sleep(1000);
+ continue;
+ }
+ }
+
+ await sleep(250);
+ }
+
+ const finalRetryState = getStep5AuthRetryPageState();
+ if (finalRetryState?.userAlreadyExistsBlocked) {
+ throw createSignupUserAlreadyExistsError();
+ }
+ if (finalRetryState?.maxCheckAttemptsBlocked) {
+ throw createAuthMaxCheckAttemptsError();
+ }
+ if (finalRetryState) {
+ throw new Error(`步骤 5:资料提交后仍停留在认证重试页,自动恢复未完成。URL: ${location.href}`);
+ }
+
+ const finalSuccessState = getStep5PostSubmitSuccessState();
+ if (finalSuccessState) {
+ return finalSuccessState;
+ }
+
+ const finalStep5Error = (typeof getStep5ErrorText === 'function' ? getStep5ErrorText() : '') || lastStep5Error;
+ if (finalStep5Error) {
+ throw new Error(`步骤 5:资料提交后页面返回错误:${finalStep5Error}。URL: ${location.href}`);
+ }
+
+ throw new Error(`步骤 5:资料提交后未检测到页面跳转或恢复成功(已点击提交 ${submitClickCount}/${maxSubmitClicks} 次)。URL: ${location.href}`);
+}
+
async function step5_fillNameBirthday(payload) {
const { firstName, lastName, age, year, month, day, prefillOnly = false } = payload;
if (!firstName || !lastName) throw new Error('未提供姓名数据。');
@@ -6558,7 +6864,7 @@ async function step5_fillNameBirthday(payload) {
// Click "完成帐户创建" button
await sleep(500);
- const completeBtn = document.querySelector('button[type="submit"]')
+ const completeBtn = await waitForStep5SubmitButton(5000)
|| await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
if (!completeBtn) {
throw new Error('未找到“完成帐户创建”按钮。URL: ' + location.href);
@@ -6566,22 +6872,42 @@ async function step5_fillNameBirthday(payload) {
const isAgeMode = !birthdayMode && Boolean(ageInput);
if (isAgeMode) {
- log('步骤 5:当前为年龄输入模式,点击“完成帐户创建”后将直接完成当前步骤。', 'warn');
+ log('步骤 5:当前为年龄输入模式,点击“完成帐户创建”后将等待页面结果。', 'info');
}
+ let reportedCompletionPayload = null;
+ function completeStep5Once(extra = {}) {
+ if (reportedCompletionPayload) {
+ return reportedCompletionPayload;
+ }
+
+ const completionPayload = getStep5DirectCompletionPayload({
+ isAgeMode,
+ navigationStarted: Boolean(extra.navigationStarted),
+ outcome: extra.outcome || null,
+ });
+ reportedCompletionPayload = completionPayload;
+ reportComplete(5, completionPayload);
+ return completionPayload;
+ }
+
+ const cleanupNavigationReporter = installStep5NavigationCompletionReporter(completeStep5Once);
+
await humanPause(500, 1300);
await performOperationWithDelay({ stepKey: 'fill-profile', kind: 'submit', label: 'submit-profile' }, async () => {
simulateClick(completeBtn);
});
+ log('步骤 5:已点击“完成帐户创建”,正在等待页面跳转、重试页或提交结果。');
- const completionPayload = getStep5DirectCompletionPayload({ isAgeMode });
- reportComplete(5, completionPayload);
+ try {
+ const outcome = await waitForStep5SubmitOutcome();
+ cleanupNavigationReporter();
- if (isAgeMode) {
- log('步骤 5:年龄模式已点击“完成帐户创建”,当前步骤直接完成,不再等待页面结果。', 'warn');
+ const completionPayload = completeStep5Once({ outcome });
+ log(`步骤 5:资料提交结果已确认(${outcome.state || 'success'}),准备继续后续步骤。`, 'ok');
return completionPayload;
+ } catch (error) {
+ cleanupNavigationReporter();
+ throw error;
}
-
- log('步骤 5:已点击“完成帐户创建”,当前步骤直接完成,不再等待页面结果。');
- return completionPayload;
}
diff --git a/data/step-definitions.js b/data/step-definitions.js
index 6cc1cae..dd87867 100644
--- a/data/step-definitions.js
+++ b/data/step-definitions.js
@@ -58,7 +58,7 @@
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' },
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' },
{ id: 6, order: 60, key: 'plus-checkout-create', title: '创建 GPC 订单' },
- { id: 7, order: 70, key: 'plus-checkout-billing', title: 'GPC OTP/PIN 验证' },
+ { id: 7, order: 70, key: 'plus-checkout-billing', title: '等待 GPC 任务完成' },
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' },
{ id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码' },
{ id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth' },
diff --git a/gopay-utils.js b/gopay-utils.js
index b2c2eee..0c25a0a 100644
--- a/gopay-utils.js
+++ b/gopay-utils.js
@@ -5,6 +5,8 @@
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
+ const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
+ const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const ALLOWED_GPC_HELPER_REMOTE_HOST = 'gpc.qlhazycoder.top';
function normalizePlusPaymentMethod(value = '') {
@@ -47,6 +49,85 @@
return String(value || '').trim().replace(/[^\d]/g, '');
}
+ function normalizeGpcHelperPhoneMode(value = '') {
+ const normalized = String(value || '').trim().toLowerCase();
+ return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
+ ? GPC_HELPER_PHONE_MODE_AUTO
+ : GPC_HELPER_PHONE_MODE_MANUAL;
+ }
+
+ function normalizeGpcRemainingUses(value) {
+ if (value === undefined || value === null || String(value).trim() === '') {
+ return null;
+ }
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null;
+ }
+
+ function unwrapGpcBalancePayload(payload = {}) {
+ const data = unwrapGpcResponse(payload);
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
+ return data;
+ }
+ const hasBalanceFields = [
+ 'remaining_uses',
+ 'remainingUses',
+ 'balance',
+ 'remaining',
+ 'uses',
+ 'available_uses',
+ 'availableUses',
+ 'auto_mode_enabled',
+ 'autoModeEnabled',
+ 'auto_enabled',
+ 'autoEnabled',
+ 'status',
+ 'card_status',
+ 'cardStatus',
+ ].some((key) => Object.prototype.hasOwnProperty.call(data, key));
+ if (!hasBalanceFields && data.data && typeof data.data === 'object' && !Array.isArray(data.data)) {
+ return data.data;
+ }
+ return data;
+ }
+
+ function getGpcBalanceRemainingUses(payload = {}) {
+ const data = unwrapGpcBalancePayload(payload);
+ if (!data || typeof data !== 'object') {
+ return null;
+ }
+ return normalizeGpcRemainingUses(
+ data.remaining_uses
+ ?? data.remainingUses
+ ?? data.balance
+ ?? data.remaining
+ ?? data.uses
+ ?? data.available_uses
+ ?? data.availableUses
+ );
+ }
+
+ function isGpcAutoModeEnabled(payload = {}) {
+ const data = unwrapGpcBalancePayload(payload);
+ if (!data || typeof data !== 'object') {
+ return false;
+ }
+ const raw = data.auto_mode_enabled ?? data.autoModeEnabled ?? data.auto_enabled ?? data.autoEnabled;
+ if (typeof raw === 'boolean') {
+ return raw;
+ }
+ const normalized = String(raw ?? '').trim().toLowerCase();
+ return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'enabled';
+ }
+
+ function getGpcApiKeyStatus(payload = {}) {
+ const data = unwrapGpcBalancePayload(payload);
+ if (!data || typeof data !== 'object') {
+ return '';
+ }
+ return String(data.status || data.card_status || data.cardStatus || '').trim();
+ }
+
function normalizeGpcOtpChannel(value = '') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'sms') {
@@ -291,7 +372,7 @@
}
function formatGpcBalancePayload(payload = {}) {
- const data = unwrapGpcResponse(payload);
+ const data = unwrapGpcBalancePayload(payload);
if (!data || typeof data !== 'object') {
return '';
}
@@ -330,6 +411,8 @@
return {
DEFAULT_GOPAY_COUNTRY_CODE,
DEFAULT_GPC_HELPER_API_URL,
+ GPC_HELPER_PHONE_MODE_AUTO,
+ GPC_HELPER_PHONE_MODE_MANUAL,
PLUS_PAYMENT_METHOD_GPC_HELPER,
PLUS_PAYMENT_METHOD_GOPAY,
PLUS_PAYMENT_METHOD_PAYPAL,
@@ -348,8 +431,13 @@
buildGpcTaskQueryUrl,
extractGpcResponseErrorDetail,
formatGpcBalancePayload,
+ getGpcApiKeyStatus,
+ getGpcBalanceRemainingUses,
isGpcUnifiedResponseOk,
+ isGpcAutoModeEnabled,
normalizeGpcHelperBaseUrl,
+ normalizeGpcHelperPhoneMode,
+ normalizeGpcRemainingUses,
normalizeGpcTaskId,
normalizeGoPayCountryCode,
normalizeGoPayPhone,
@@ -358,6 +446,7 @@
normalizeGoPayPin,
normalizeGpcOtpChannel,
normalizePlusPaymentMethod,
+ unwrapGpcBalancePayload,
unwrapGpcResponse,
};
});
diff --git a/scripts/__pycache__/gpc_sms_helper_macos.cpython-313.pyc b/scripts/__pycache__/gpc_sms_helper_macos.cpython-313.pyc
new file mode 100644
index 0000000..7f85f82
Binary files /dev/null and b/scripts/__pycache__/gpc_sms_helper_macos.cpython-313.pyc differ
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 50310bb..5337d0f 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -305,6 +305,13 @@
余额未获取
+
+ GPC 模式
+
+
GPC 区号