Merge origin/dev into PR 225 review fix

This commit is contained in:
QLHazyCoder
2026-05-10 06:02:53 +08:00
42 changed files with 3710 additions and 175 deletions
+123 -3
View File
@@ -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,
+90 -8
View File
@@ -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,
+104 -12
View File
@@ -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,
+29
View File
@@ -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)
+66
View File
@@ -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',
+24 -1
View File
@@ -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) {
+37
View File
@@ -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;
+116 -12
View File
@@ -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(`步骤 6GPC 任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
await addLog(`步骤 6GPC ${result.phoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
await completeStepFromBackground(6, {
plusCheckoutCountry: result.country || 'ID',
plusCheckoutCurrency: result.currency || 'IDR',
+174 -17
View File
@@ -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 = [`步骤 7GPC 任务状态:${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('步骤 7GPC 模式缺少 task_id,请先执行步骤 6。');
@@ -840,27 +985,25 @@
throw new Error('步骤 7GPC 模式缺少 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
? '步骤 7GPC PIN 必须是 6 位数字,请检查侧边栏配置。'
: '步骤 7GPC 模式缺少 PIN 配置。');
: '步骤 7GPC 手动模式缺少 PIN 配置。');
}
await addLog(`步骤 7:GPC 模式开始轮询任务(task_id: ${taskId}...`, 'info');
await addLog(`步骤 7GPC ${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(`步骤 7GPC 任务状态:${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');
}
+23
View File
@@ -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;
+24
View File
@@ -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',
+64 -15
View File
@@ -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;
}
+40
View File
@@ -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;
+342 -16
View File
@@ -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;
}
+1 -1
View File
@@ -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' },
+90 -1
View File
@@ -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,
};
});
+7
View File
@@ -305,6 +305,13 @@
<span id="display-gpc-helper-balance" class="data-value mono">余额未获取</span>
</div>
</div>
<div class="data-row" id="row-gpc-helper-phone-mode" style="display:none;">
<span class="data-label">GPC 模式</span>
<select id="select-gpc-helper-phone-mode" class="data-select">
<option value="manual">手动模式</option>
<option value="auto">自动模式</option>
</select>
</div>
<div class="data-row" id="row-gpc-helper-country-code" style="display:none;">
<span class="data-label">GPC 区号</span>
<select id="select-gpc-helper-country-code" class="data-select">
+270 -8
View File
@@ -193,6 +193,8 @@ const inputGpcHelperCardKey = document.getElementById('input-gpc-helper-card-key
const btnToggleGpcHelperCardKey = document.getElementById('btn-toggle-gpc-helper-card-key');
const btnGpcHelperBalance = document.getElementById('btn-gpc-helper-balance');
const displayGpcHelperBalance = document.getElementById('display-gpc-helper-balance');
const rowGpcHelperPhoneMode = document.getElementById('row-gpc-helper-phone-mode');
const selectGpcHelperPhoneMode = document.getElementById('select-gpc-helper-phone-mode');
const rowGpcHelperCountryCode = document.getElementById('row-gpc-helper-country-code');
const selectGpcHelperCountryCode = document.getElementById('select-gpc-helper-country-code');
const rowGpcHelperPhone = document.getElementById('row-gpc-helper-phone');
@@ -512,6 +514,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_PORTAL_URL = 'https://gpc.qlhazycoder.top/';
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
@@ -807,6 +811,15 @@ function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
});
}
function getStepIdByKeyForCurrentMode(stepKey = '') {
const normalizedKey = String(stepKey || '').trim();
if (!normalizedKey) {
return 0;
}
const match = (stepDefinitions || []).find((step) => String(step?.key || '') === normalizedKey);
return Number(match?.id) || 0;
}
function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
currentPlusModeEnabled = Boolean(plusModeEnabled);
const defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : 'paypal';
@@ -2122,6 +2135,86 @@ function getSelectedPlusPaymentMethod(state = latestState) {
return normalizePlusPaymentMethod(state?.plusPaymentMethod || currentPlusPaymentMethod || defaultMethod);
}
function normalizeGpcHelperPhoneModeValue(value = '') {
const rootScope = typeof window !== 'undefined' ? window : 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 getGpcHelperAutoModeEnabled(state = latestState) {
return Boolean(state?.gopayHelperAutoModeEnabled);
}
function hasGpcAutoModePermissionField(payload = {}) {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
return false;
}
return payload.auto_mode_enabled !== undefined
|| payload.autoModeEnabled !== undefined
|| payload.auto_enabled !== undefined
|| payload.autoEnabled !== undefined
|| (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data) && hasGpcAutoModePermissionField(payload.data));
}
function isGpcAutoModePermissionDenied(state = latestState) {
if (getGpcHelperAutoModeEnabled(state)) {
return false;
}
return hasGpcAutoModePermissionField(state?.gopayHelperBalancePayload);
}
function normalizeGpcRemainingUsesValue(value) {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcRemainingUses) {
return rootScope.GoPayUtils.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 getGpcBalanceRemainingUsesFromResponse(response = {}) {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (rootScope.GoPayUtils?.getGpcBalanceRemainingUses) {
const remaining = rootScope.GoPayUtils.getGpcBalanceRemainingUses(response?.data || response?.payload || response);
if (remaining !== null && remaining !== undefined) {
return remaining;
}
}
return normalizeGpcRemainingUsesValue(
response?.remainingUses
?? response?.data?.remaining_uses
?? response?.data?.remainingUses
?? response?.payload?.data?.remaining_uses
?? response?.payload?.remaining_uses
?? response?.payload?.remainingUses
);
}
function getGpcAutoModeEnabledFromResponse(response = {}) {
if (typeof response?.autoModeEnabled === 'boolean') {
return response.autoModeEnabled;
}
const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (rootScope.GoPayUtils?.isGpcAutoModeEnabled) {
return rootScope.GoPayUtils.isGpcAutoModeEnabled(response?.data || response?.payload || response);
}
return Boolean(
response?.data?.auto_mode_enabled
?? response?.data?.autoModeEnabled
?? response?.payload?.data?.auto_mode_enabled
?? response?.payload?.auto_mode_enabled
?? response?.payload?.autoModeEnabled
);
}
function normalizeGpcOtpChannelValue(value = '') {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcOtpChannel) {
@@ -3271,12 +3364,23 @@ function collectSettingsPayload() {
: (String(latestState?.signupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'))
);
const plusPaymentMethod = getSelectedPlusPaymentMethod();
const normalizeGpcHelperPhoneModeSafe = typeof normalizeGpcHelperPhoneModeValue === 'function'
? normalizeGpcHelperPhoneModeValue
: ((value = '') => String(value || '').trim().toLowerCase() === 'auto' || String(value || '').trim().toLowerCase() === 'builtin' ? 'auto' : 'manual');
const selectedGpcPhoneMode = normalizeGpcHelperPhoneModeSafe(
typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode
? selectGpcHelperPhoneMode.value
: (latestState?.gopayHelperPhoneMode || 'manual')
);
const effectiveGpcPhoneMode = (typeof isGpcAutoModePermissionDenied === 'function' && isGpcAutoModePermissionDenied(latestState))
? 'manual'
: selectedGpcPhoneMode;
const selectedGpcOtpChannel = normalizeGpcOtpChannelSafe(
typeof selectGpcHelperOtpChannel !== 'undefined' && selectGpcHelperOtpChannel
? selectGpcHelperOtpChannel.value
: (latestState?.gopayHelperOtpChannel || 'whatsapp')
);
const selectedGpcLocalSmsHelperEnabled = Boolean(
const selectedGpcLocalSmsHelperEnabled = effectiveGpcPhoneMode === 'auto' ? false : Boolean(
typeof inputGpcHelperLocalSmsEnabled !== 'undefined' && inputGpcHelperLocalSmsEnabled
? inputGpcHelperLocalSmsEnabled.checked
: latestState?.gopayHelperLocalSmsHelperEnabled
@@ -3391,6 +3495,7 @@ function collectSettingsPayload() {
? String(inputGpcHelperCardKey.value || '').trim()
: String(latestState?.gopayHelperApiKey || latestState?.gopayHelperCardKey || '').trim(),
gopayHelperCardKey: '',
gopayHelperPhoneMode: effectiveGpcPhoneMode,
gopayHelperCountryCode: window.GoPayUtils?.normalizeGoPayCountryCode
? window.GoPayUtils.normalizeGoPayCountryCode(typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode ? selectGpcHelperCountryCode.value : latestState?.gopayHelperCountryCode)
: (typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode
@@ -7241,6 +7346,14 @@ function updatePlusModeUI() {
? Boolean(inputPlusModeEnabled.checked)
: false;
const method = enabled ? getSelectedPlusPaymentMethod() : defaultMethod;
const gpcPhoneMode = normalizeGpcHelperPhoneModeValue(
typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode
? selectGpcHelperPhoneMode.value
: (latestState?.gopayHelperPhoneMode || 'manual')
);
const gpcAutoModeDenied = isGpcAutoModePermissionDenied(latestState);
const gpcAutoModeEnabled = getGpcHelperAutoModeEnabled(latestState);
const isGpcAutoMode = !gpcAutoModeDenied && gpcPhoneMode === GPC_HELPER_PHONE_MODE_AUTO;
const gpcOtpChannel = normalizeGpcOtpChannelValue(
typeof selectGpcHelperOtpChannel !== 'undefined' && selectGpcHelperOtpChannel
? selectGpcHelperOtpChannel.value
@@ -7255,8 +7368,9 @@ function updatePlusModeUI() {
? normalizePlusPaymentMethod(selectPlusPaymentMethod.value)
: method;
const gpcRowsVisible = enabled && selectedMethod === gpcValue;
const localSmsControlsVisible = gpcRowsVisible;
const effectiveLocalSmsEnabled = localSmsEnabled;
const canShowGpcModeSelector = gpcRowsVisible && (gpcAutoModeEnabled || !gpcAutoModeDenied);
const localSmsControlsVisible = gpcRowsVisible && !isGpcAutoMode;
const effectiveLocalSmsEnabled = !isGpcAutoMode && localSmsEnabled;
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) {
selectPlusPaymentMethod.value = method;
if (selectPlusPaymentMethod.style) {
@@ -7265,7 +7379,7 @@ function updatePlusModeUI() {
}
if (typeof plusPaymentMethodCaption !== 'undefined' && plusPaymentMethodCaption) {
plusPaymentMethodCaption.textContent = method === gpcValue
? 'GPC 订阅链路'
? `GPC ${isGpcAutoMode ? '自动' : '手动'}订阅链路`
: method === gopayValue
? 'GoPay 印尼订阅链路'
: 'PayPal 订阅链路';
@@ -7289,6 +7403,19 @@ function updatePlusModeUI() {
[
typeof rowGpcHelperApi !== 'undefined' ? rowGpcHelperApi : null,
typeof rowGpcHelperCardKey !== 'undefined' ? rowGpcHelperCardKey : null,
].forEach((row) => {
if (!row) {
return;
}
row.style.display = gpcRowsVisible ? '' : 'none';
});
if (typeof rowGpcHelperPhoneMode !== 'undefined' && rowGpcHelperPhoneMode) {
rowGpcHelperPhoneMode.style.display = canShowGpcModeSelector ? '' : 'none';
}
if (typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode) {
selectGpcHelperPhoneMode.value = gpcAutoModeDenied ? GPC_HELPER_PHONE_MODE_MANUAL : gpcPhoneMode;
}
[
typeof rowGpcHelperCountryCode !== 'undefined' ? rowGpcHelperCountryCode : null,
typeof rowGpcHelperPhone !== 'undefined' ? rowGpcHelperPhone : null,
typeof rowGpcHelperOtpChannel !== 'undefined' ? rowGpcHelperOtpChannel : null,
@@ -7297,7 +7424,7 @@ function updatePlusModeUI() {
if (!row) {
return;
}
row.style.display = gpcRowsVisible ? '' : 'none';
row.style.display = gpcRowsVisible && !isGpcAutoMode ? '' : 'none';
});
if (typeof selectGpcHelperOtpChannel !== 'undefined' && selectGpcHelperOtpChannel) {
selectGpcHelperOtpChannel.value = gpcOtpChannel;
@@ -7513,6 +7640,101 @@ async function persistSignupPhoneInputForAction() {
await persistSignupPhoneInputValue({ final: true, silent: true });
}
function isGpcHelperCheckoutSelected() {
const gpcValue = typeof PLUS_PAYMENT_METHOD_GPC_HELPER !== 'undefined' ? PLUS_PAYMENT_METHOD_GPC_HELPER : 'gpc-helper';
const plusEnabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
? Boolean(inputPlusModeEnabled.checked)
: Boolean(latestState?.plusModeEnabled);
return plusEnabled && getSelectedPlusPaymentMethod() === gpcValue;
}
function getSelectedGpcHelperPhoneMode() {
return normalizeGpcHelperPhoneModeValue(
typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode
? selectGpcHelperPhoneMode.value
: (latestState?.gopayHelperPhoneMode || GPC_HELPER_PHONE_MODE_MANUAL)
);
}
async function showGpcStartBlockedDialog(message) {
await openConfirmModal({
title: 'GPC 任务无法开启',
message,
confirmLabel: '知道了',
});
}
async function refreshGpcBalanceForStart() {
const response = await chrome.runtime.sendMessage({
type: 'REFRESH_GPC_CARD_BALANCE',
source: 'sidepanel',
payload: {
gopayHelperApiUrl: inputGpcHelperApi?.value || DEFAULT_GPC_HELPER_API_URL,
gopayHelperApiKey: inputGpcHelperCardKey?.value || latestState?.gopayHelperApiKey || '',
reason: 'before_start',
},
});
if (response?.error) {
throw new Error(response.error);
}
const nextState = {
gopayHelperBalance: response?.balance || latestState?.gopayHelperBalance || '',
gopayHelperBalancePayload: response?.data || response?.payload?.data || response?.payload || latestState?.gopayHelperBalancePayload || null,
gopayHelperBalanceUpdatedAt: response?.updatedAt || Date.now(),
gopayHelperBalanceError: '',
gopayHelperRemainingUses: getGpcBalanceRemainingUsesFromResponse(response) ?? 0,
gopayHelperAutoModeEnabled: getGpcAutoModeEnabledFromResponse(response),
gopayHelperApiKeyStatus: response?.apiKeyStatus || response?.data?.status || response?.payload?.data?.status || response?.payload?.status || '',
};
syncLatestState(nextState);
if (displayGpcHelperBalance && nextState.gopayHelperBalance) {
displayGpcHelperBalance.textContent = nextState.gopayHelperBalance;
}
updatePlusModeUI();
return nextState;
}
async function ensureGpcApiKeyReadyForStart(options = {}) {
if (!isGpcHelperCheckoutSelected()) {
return true;
}
const selectedMode = getSelectedGpcHelperPhoneMode();
let balanceState;
try {
balanceState = await refreshGpcBalanceForStart();
} catch (error) {
await showGpcStartBlockedDialog(`API Key 余额校验失败:${error?.message || '未知错误'}。请先确认 API Key 是否正确。`);
return false;
}
const remainingUses = normalizeGpcRemainingUsesValue(balanceState.gopayHelperRemainingUses);
const apiKeyStatus = String(balanceState.gopayHelperApiKeyStatus || '').trim().toLowerCase();
if (apiKeyStatus && apiKeyStatus !== 'active') {
await showGpcStartBlockedDialog(`当前 GPC API Key 状态为 ${balanceState.gopayHelperApiKeyStatus},不能开启任务。`);
return false;
}
if (remainingUses !== null && remainingUses <= 0) {
await showGpcStartBlockedDialog('当前 GPC API Key 剩余次数不足,不能开启任务。');
return false;
}
if (selectedMode === GPC_HELPER_PHONE_MODE_AUTO && !balanceState.gopayHelperAutoModeEnabled) {
if (typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode) {
selectGpcHelperPhoneMode.value = GPC_HELPER_PHONE_MODE_MANUAL;
}
syncLatestState({ gopayHelperPhoneMode: GPC_HELPER_PHONE_MODE_MANUAL });
updatePlusModeUI();
await saveSettings({ silent: true, force: true }).catch(() => {});
await showGpcStartBlockedDialog('当前 GPC API Key 未开通自动模式,已切回手动模式,不能以自动模式开启任务。');
return false;
}
if (options?.notify) {
showToast('GPC API Key 余额和权限校验通过。', 'success', 1800);
}
return true;
}
async function openPlusManualConfirmationDialog(options = {}) {
const method = String(options.method || '').trim().toLowerCase();
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
@@ -8039,6 +8261,9 @@ function applySettingsState(state) {
if (typeof inputGpcHelperCardKey !== 'undefined' && inputGpcHelperCardKey) {
inputGpcHelperCardKey.value = state?.gopayHelperApiKey || state?.gopayHelperCardKey || '';
}
if (typeof selectGpcHelperPhoneMode !== 'undefined' && selectGpcHelperPhoneMode) {
selectGpcHelperPhoneMode.value = normalizeGpcHelperPhoneModeValue(state?.gopayHelperPhoneMode || 'manual');
}
if (typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode) {
const normalizedCountryCode = window.GoPayUtils?.normalizeGoPayCountryCode
? window.GoPayUtils.normalizeGoPayCountryCode(state?.gopayHelperCountryCode)
@@ -10904,6 +11129,10 @@ stepsList?.addEventListener('click', async (event) => {
return;
}
await persistCurrentSettingsForAction();
const gpcCreateStep = getStepIdByKeyForCurrentMode('plus-checkout-create') || 6;
if (step === gpcCreateStep && !(await ensureGpcApiKeyReadyForStart())) {
return;
}
if (step === 3) {
if (inputPassword.value !== (latestState?.customPassword || '')) {
await chrome.runtime.sendMessage({
@@ -11160,6 +11389,10 @@ async function startAutoRunFromCurrentSettings() {
if (typeof persistCurrentSettingsForAction === 'function') {
await persistCurrentSettingsForAction();
}
if (!(await ensureGpcApiKeyReadyForStart())) {
clearPendingAutoRunStartRunCount();
return false;
}
const customEmailPoolEnabled = typeof usesCustomEmailPoolGenerator === 'function'
&& usesCustomEmailPoolGenerator();
@@ -11494,7 +11727,7 @@ btnGpcHelperBalance?.addEventListener('click', async () => {
type: 'REFRESH_GPC_CARD_BALANCE',
source: 'sidepanel',
payload: {
gopayHelperApiUrl: DEFAULT_GPC_HELPER_API_URL,
gopayHelperApiUrl: inputGpcHelperApi?.value || DEFAULT_GPC_HELPER_API_URL,
gopayHelperApiKey: inputGpcHelperCardKey?.value || '',
gopayHelperCountryCode: selectGpcHelperCountryCode?.value || '+86',
reason: 'manual',
@@ -11506,7 +11739,25 @@ btnGpcHelperBalance?.addEventListener('click', async () => {
if (displayGpcHelperBalance) {
displayGpcHelperBalance.textContent = response?.balance || '余额已更新';
}
showToast('GPC 余额已更新。', 'success');
const nextState = {
gopayHelperBalance: response?.balance || latestState?.gopayHelperBalance || '',
gopayHelperBalancePayload: response?.data || response?.payload?.data || response?.payload || latestState?.gopayHelperBalancePayload || null,
gopayHelperBalanceUpdatedAt: response?.updatedAt || Date.now(),
gopayHelperBalanceError: '',
gopayHelperRemainingUses: getGpcBalanceRemainingUsesFromResponse(response) ?? 0,
gopayHelperAutoModeEnabled: getGpcAutoModeEnabledFromResponse(response),
gopayHelperApiKeyStatus: response?.apiKeyStatus || response?.data?.status || response?.payload?.data?.status || response?.payload?.status || '',
};
syncLatestState(nextState);
if (!nextState.gopayHelperAutoModeEnabled && getSelectedGpcHelperPhoneMode() === GPC_HELPER_PHONE_MODE_AUTO) {
selectGpcHelperPhoneMode.value = GPC_HELPER_PHONE_MODE_MANUAL;
syncLatestState({ gopayHelperPhoneMode: GPC_HELPER_PHONE_MODE_MANUAL });
await saveSettings({ silent: true, force: true }).catch(() => {});
showToast('当前 API Key 未开通自动模式,已切回手动模式。', 'warn');
} else {
showToast(nextState.gopayHelperAutoModeEnabled ? 'GPC 余额已更新,自动模式可用。' : 'GPC 余额已更新,当前 API Key 只能使用手动模式。', 'success');
}
updatePlusModeUI();
} catch (error) {
showToast(error?.message || '查询 GPC 余额失败。', 'error');
}
@@ -11525,6 +11776,7 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
[
inputGpcHelperApi,
inputGpcHelperCardKey,
selectGpcHelperPhoneMode,
selectGpcHelperCountryCode,
inputGpcHelperPhone,
selectGpcHelperOtpChannel,
@@ -11541,7 +11793,7 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
scheduleSettingsAutoSave();
});
input?.addEventListener('change', () => {
if (input === selectGpcHelperOtpChannel || input === inputGpcHelperLocalSmsEnabled) {
if (input === selectGpcHelperPhoneMode || input === selectGpcHelperOtpChannel || input === inputGpcHelperLocalSmsEnabled) {
updatePlusModeUI();
}
markSettingsDirty(true);
@@ -13455,6 +13707,14 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.payload.plusPaymentMethod !== undefined && selectPlusPaymentMethod) {
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(message.payload.plusPaymentMethod);
}
if (message.payload.gopayHelperPhoneMode !== undefined && selectGpcHelperPhoneMode) {
selectGpcHelperPhoneMode.value = normalizeGpcHelperPhoneModeValue(message.payload.gopayHelperPhoneMode);
}
if (message.payload.gopayHelperAutoModeEnabled === false && selectGpcHelperPhoneMode?.value === GPC_HELPER_PHONE_MODE_AUTO) {
selectGpcHelperPhoneMode.value = GPC_HELPER_PHONE_MODE_MANUAL;
syncLatestState({ gopayHelperPhoneMode: GPC_HELPER_PHONE_MODE_MANUAL });
showToast('当前 API Key 未开通自动模式,已切回手动模式。', 'warn', 2200);
}
if (message.payload.gopayHelperOtpChannel !== undefined && selectGpcHelperOtpChannel) {
selectGpcHelperOtpChannel.value = normalizeGpcOtpChannelValue(message.payload.gopayHelperOtpChannel);
}
@@ -13476,6 +13736,8 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (
message.payload.plusModeEnabled !== undefined
|| message.payload.plusPaymentMethod !== undefined
|| message.payload.gopayHelperPhoneMode !== undefined
|| message.payload.gopayHelperAutoModeEnabled !== undefined
|| message.payload.gopayHelperOtpChannel !== undefined
|| message.payload.gopayHelperLocalSmsHelperEnabled !== undefined
) {
+120 -16
View File
@@ -56,10 +56,24 @@ const bundle = [
extractFunction('isAddPhoneAuthFailure'),
extractFunction('isAddPhoneAuthUrl'),
extractFunction('isAddPhoneAuthState'),
extractFunction('isGpcCheckoutRestartRequiredFailure'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
].join('\n');
const defaultStepDefinitions = {
1: { key: 'open-signup' },
2: { key: 'prepare-email' },
3: { key: 'fill-password' },
4: { key: 'verify-email' },
5: { key: 'profile-basic' },
6: { key: 'profile-finish' },
7: { key: 'auth-login' },
8: { key: 'auth-email-code' },
9: { key: 'confirm-oauth' },
10: { key: 'platform-verify' },
};
function createHarness(options = {}) {
const {
startStep = 7,
@@ -68,13 +82,18 @@ function createHarness(options = {}) {
failureMessage = '认证失败: Request failed with status code 502',
authState = { state: 'password_page', url: 'https://auth.openai.com/log-in' },
customState = {},
stepDefinitions = defaultStepDefinitions,
stepIds = Object.keys(stepDefinitions).map(Number).sort((a, b) => a - b),
lastStepId = Math.max(...stepIds),
finalOAuthChainStartStep = 7,
} = options;
return new Function(`
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0 };
const LAST_STEP_ID = ${JSON.stringify(lastStepId)};
const FINAL_OAUTH_CHAIN_START_STEP = ${JSON.stringify(finalOAuthChainStartStep)};
const SIGNUP_METHOD_PHONE = 'phone';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const LOG_PREFIX = '[test]';
const chrome = {
tabs: {
@@ -104,21 +123,10 @@ async function getState() {
};
}
function getStepIdsForState() {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
return ${JSON.stringify(stepIds)};
}
function getStepDefinitionForState(step) {
const map = {
1: { key: 'open-signup' },
2: { key: 'prepare-email' },
3: { key: 'fill-password' },
4: { key: 'verify-email' },
5: { key: 'profile-basic' },
6: { key: 'profile-finish' },
7: { key: 'auth-login' },
8: { key: 'auth-email-code' },
9: { key: 'confirm-oauth' },
10: { key: 'platform-verify' },
};
const map = ${JSON.stringify(stepDefinitions)};
return map[Number(step)] || null;
}
function getStepExecutionKeyForState(step, state = {}) {
@@ -149,6 +157,10 @@ function getLoginAuthStateLabel(state) {
function getErrorMessage(error) {
return error?.message || String(error || '');
}
function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
return normalized === PLUS_PAYMENT_METHOD_GPC_HELPER ? PLUS_PAYMENT_METHOD_GPC_HELPER : normalized;
}
function isPhoneSmsPlatformRateLimitFailure(error) {
const message = getErrorMessage(error);
return /FIVE_SIM_RATE_LIMIT::|5sim[\s\S]*(?:限流|rate\s*limit)/i.test(message);
@@ -321,3 +333,95 @@ test('auto-run restarts from confirm-oauth step after transient step10 token_exc
});
assert.ok(events.logs.some(({ message }) => /回到步骤 9 重新开始授权流程/.test(message)));
});
test('auto-run restarts GPC checkout from step 6 when step 7 task polling stalls', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 2,
failureMessage: 'GPC API 请求超时(>30 秒):https://gpc.qlhazycoder.top/api/gp/tasks/task_stalled',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(
events.steps,
[6, 7, 6, 7, 6, 7, 10, 11, 12, 13]
);
assert.deepStrictEqual(
events.invalidations.map((entry) => entry.step),
[5, 5]
);
assert.ok(events.logs.some(({ message }) => /回到步骤 6 重新创建 GPC 任务/.test(message)));
});
test('auto-run treats GPC account binding as recoverable step 6 restart', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
11: { key: 'fetch-login-code' },
12: { key: 'confirm-oauth' },
13: { key: 'platform-verify' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'GPC_TASK_ENDED::GOPAY已经绑了订阅,需要手动解绑',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const events = await harness.run();
assert.deepStrictEqual(events.steps, [6, 7, 6, 7, 10, 11, 12, 13]);
assert.deepStrictEqual(events.invalidations.map((entry) => entry.step), [5]);
});
test('auto-run does not restart GPC checkout when Plus account has no free-trial eligibility', async () => {
const plusGpcSteps = {
6: { key: 'plus-checkout-create' },
7: { key: 'plus-checkout-billing' },
10: { key: 'oauth-login' },
};
const harness = createHarness({
startStep: 6,
failureStep: 7,
failureBudget: 1,
failureMessage: 'PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 7:今日应付金额不是 0(IDR 299000),当前账号没有免费试用资格,已跳过支付提交。',
stepDefinitions: plusGpcSteps,
finalOAuthChainStartStep: 10,
customState: {
stepStatuses: { 3: 'completed' },
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
},
});
const result = await harness.runAndCaptureError();
assert.ok(result?.error);
assert.deepStrictEqual(result.events.steps, [6, 7]);
assert.equal(result.events.invalidations.length, 0);
});
@@ -55,6 +55,7 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeAccountRunHistoryHelperBaseUrl'),
extractFunction('normalizeVerificationResendCount'),
extractFunction('normalizePlusPaymentMethod'),
extractFunction('normalizeGpcHelperPhoneMode'),
extractFunction('normalizePhoneSmsProvider'),
extractFunction('normalizePhoneSmsProviderOrder'),
extractFunction('normalizeSignupMethod'),
@@ -207,6 +208,12 @@ return {
);
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiUrl', ''), 'https://gpc.qlhazycoder.top');
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiKey', ' gpc-123 '), 'gpc-123');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneMode', 'auto'), 'auto');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneMode', 'builtin'), 'auto');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneMode', 'unknown'), 'manual');
assert.equal(api.normalizePersistentSettingValue('gopayHelperRemainingUses', '998'), 998);
assert.equal(api.normalizePersistentSettingValue('gopayHelperAutoModeEnabled', 1), true);
assert.equal(api.normalizePersistentSettingValue('gopayHelperApiKeyStatus', ' active '), 'active');
assert.equal(api.normalizePersistentSettingValue('gopayHelperCountryCode', ' 86 '), '+86');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneNumber', ' +86 138-0013-8000 '), '+8613800138000');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPin', ' 12-34-56 '), '123456');
@@ -198,6 +198,54 @@ test('account run history helper accepts phone-only records without forcing emai
assert.equal(normalized.finalStatus, 'failed');
});
test('account run history does not turn prerequisite guidance into a fake step 2 failure', () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundAccountRunHistory;`)(globalScope);
const helpers = api.createAccountRunHistoryHelpers({
chrome: { storage: { local: { get: async () => ({}), set: async () => {} } } },
getState: async () => ({}),
normalizeAccountRunHistoryHelperBaseUrl: (value) => String(value || '').trim(),
});
const genericFailedRecord = helpers.buildAccountRunHistoryRecord({
email: 'late@example.com',
password: 'secret',
autoRunning: true,
autoRunCurrentRun: 1,
autoRunTotalRuns: 3,
autoRunAttemptRun: 2,
}, 'failed', '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
assert.equal(genericFailedRecord.failedStep, null);
assert.equal(genericFailedRecord.failureLabel, '流程失败');
const explicitFailedRecord = helpers.buildAccountRunHistoryRecord({
email: 'late@example.com',
password: 'secret',
autoRunning: true,
autoRunCurrentRun: 1,
autoRunTotalRuns: 3,
autoRunAttemptRun: 2,
}, 'step10_failed', '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
assert.equal(explicitFailedRecord.failedStep, 10);
assert.equal(explicitFailedRecord.failureLabel, '步骤 10 失败');
const migratedOldRecord = helpers.normalizeAccountRunHistoryRecord({
email: 'old@example.com',
password: 'secret',
finalStatus: 'failed',
failureLabel: '步骤 2 失败',
failureDetail: '缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。',
failedStep: 2,
});
assert.equal(migratedOldRecord.failedStep, null);
assert.equal(migratedOldRecord.failureLabel, '流程失败');
});
test('account run history merges email and phone identities from the same run', async () => {
const source = fs.readFileSync('background/account-run-history.js', 'utf8');
const globalScope = {};
+28
View File
@@ -15,3 +15,31 @@ test('auto-run controller module exposes a factory', () => {
assert.equal(typeof api?.createAutoRunController, 'function');
});
test('auto-run account record status preserves the real failed step instead of parsing guidance text', () => {
const source = fs.readFileSync('background/auto-run-controller.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
const controller = api.createAutoRunController({});
const state = {
currentStep: 11,
stepStatuses: {
2: 'completed',
10: 'completed',
11: 'failed',
},
};
const error = new Error('缺少登录账号:请先完成步骤 2,或在侧栏填写账号后再执行当前步骤。');
assert.equal(
controller.resolveAutoRunAccountRecordStatus('failed', state, error),
'step11_failed'
);
error.failedStep = 13;
assert.equal(
controller.resolveAutoRunAccountRecordStatus('failed', state, error),
'step13_failed'
);
});
@@ -23,13 +23,17 @@ function createRouter(overrides = {}) {
securityBlocks: [],
invalidations: [],
executedSteps: [],
accountRecords: [],
};
const router = api.createMessageRouter({
addLog: async (message, level, options = {}) => {
events.logs.push({ message, level, step: options.step, stepKey: options.stepKey });
},
appendAccountRunRecord: async () => null,
appendAccountRunRecord: overrides.appendAccountRunRecord || (async (status, state, reason) => {
events.accountRecords.push({ status, state, reason });
return null;
}),
batchUpdateLuckmailPurchases: async () => {},
buildLocalhostCleanupPrefix: () => '',
buildLuckmailSessionSettingsPayload: () => ({}),
@@ -89,7 +93,7 @@ function createRouter(overrides = {}) {
events.invalidations.push({ step, options });
},
isCloudflareSecurityBlockedError: overrides.isCloudflareSecurityBlockedError || ((error) => /^CF_SECURITY_BLOCKED::/.test(typeof error === 'string' ? error : error?.message || '')),
isAutoRunLockedState: () => false,
isAutoRunLockedState: overrides.isAutoRunLockedState || (() => false),
isHotmailProvider: () => false,
isLocalhostOAuthCallbackUrl: () => true,
isLuckmailProvider: () => false,
@@ -146,7 +150,7 @@ function createRouter(overrides = {}) {
verifyHotmailAccount: async () => {},
refreshGpcCardBalance: overrides.refreshGpcCardBalance || (async (state, options) => {
events.balanceRefreshes.push({ state, options });
return { balance: '余额 3' };
return { balance: '余额 3', remainingUses: 3, autoModeEnabled: true, apiKeyStatus: 'active' };
}),
});
@@ -556,9 +560,65 @@ test('message router refreshes GPC balance through explicit sidepanel message',
},
}, {});
assert.deepStrictEqual(response, { ok: true, balance: '余额 3' });
assert.deepStrictEqual(response, { ok: true, balance: '余额 3', remainingUses: 3, autoModeEnabled: true, apiKeyStatus: 'active' });
assert.equal(events.balanceRefreshes.length, 1);
assert.equal(events.balanceRefreshes[0].state.gopayHelperApiUrl, 'http://localhost:18473/');
assert.equal(events.balanceRefreshes[0].state.gopayHelperApiKey, 'payload_api_key');
assert.deepStrictEqual(events.balanceRefreshes[0].options, { reason: 'manual' });
});
test('message router ignores stale step 2 errors while auto-run is already on a later step', async () => {
const { router, events } = createRouter({
state: {
autoRunning: true,
autoRunPhase: 'running',
currentStep: 6,
stepStatuses: {
2: 'completed',
6: 'running',
},
},
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
});
const response = await router.handleMessage({
type: 'STEP_ERROR',
step: 2,
error: '步骤 2:旧页面异步失败,不应覆盖当前第 6 步记录。',
}, {});
assert.deepStrictEqual(response, { ok: true, ignored: true });
assert.deepStrictEqual(events.stepStatuses, []);
assert.deepStrictEqual(events.notifyErrors, []);
assert.deepStrictEqual(events.accountRecords, []);
assert.equal(events.logs.some(({ message }) => /忽略过期的步骤 2 失败消息/.test(message)), true);
});
test('message router ignores stale step 2 completion while auto-run is already on a later step', async () => {
const { router, events } = createRouter({
state: {
autoRunning: true,
autoRunPhase: 'running',
currentStep: 6,
stepStatuses: {
2: 'completed',
6: 'running',
},
},
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
});
const response = await router.handleMessage({
type: 'STEP_COMPLETE',
step: 2,
payload: {
email: 'late@example.com',
},
}, {});
assert.deepStrictEqual(response, { ok: true, ignored: true });
assert.deepStrictEqual(events.stepStatuses, []);
assert.deepStrictEqual(events.notifyCompletions, []);
assert.deepStrictEqual(events.emailStates, []);
assert.equal(events.logs.some(({ message }) => /忽略过期的步骤 2 完成消息/.test(message)), true);
});
@@ -428,6 +428,85 @@ test('step 2 does not force auth-entry retry on logged-out chatgpt home when con
assert.equal(logs.some((item) => /已登录 ChatGPT 首页/.test(item.message)), false);
});
test('step 2 waits for the existing signup tab to settle before probing the entry state', async () => {
const completedPayloads = [];
const logs = [];
const events = [];
const executor = step2Api.createStep2Executor({
addLog: async (message, level = 'info', meta = {}) => {
logs.push({ message, level, meta });
},
chrome: {
tabs: {
update: async () => {
events.push('tab-update');
},
get: async () => ({ url: 'https://chatgpt.com/' }),
},
},
completeStepFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {
events.push('content-ready');
},
ensureSignupAuthEntryPageReady: async () => ({ tabId: 17 }),
ensureSignupEntryPageReady: async () => ({ tabId: 17 }),
ensureSignupPostEmailPageReadyInTab: async () => ({
state: 'password_page',
url: 'https://auth.openai.com/create-account/password',
}),
getTabId: async () => 17,
isTabAlive: async () => true,
resolveSignupEmailForFlow: async () => 'user@example.com',
sendToContentScriptResilient: async (_source, message) => {
events.push(message.type);
if (message.type === 'ENSURE_SIGNUP_ENTRY_READY') {
return { ready: true, state: 'entry_home', url: 'https://chatgpt.com/' };
}
return { submitted: true };
},
SIGNUP_PAGE_INJECT_FILES: [],
waitForTabStableComplete: async (_tabId, options) => {
events.push({ type: 'wait-stable', options });
return { id: 17, url: 'https://chatgpt.com/', status: 'complete' };
},
});
await executor.executeStep2({ email: 'user@example.com' });
assert.deepStrictEqual(events.slice(0, 4), [
'tab-update',
{
type: 'wait-stable',
options: {
timeoutMs: 45000,
retryDelayMs: 300,
stableMs: 3000,
initialDelayMs: 300,
},
},
'content-ready',
'ENSURE_SIGNUP_ENTRY_READY',
]);
assert.equal(logs.some((item) => /额外稳定 3 /.test(item.message)), true);
assert.equal(logs.some((item) => item.meta.step === 2 && item.meta.stepKey === 'signup-entry'), true);
assert.deepStrictEqual(completedPayloads, [
{
step: 2,
payload: {
email: 'user@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'user@example.com',
nextSignupState: 'password_page',
nextSignupUrl: 'https://auth.openai.com/create-account/password',
skippedPasswordStep: false,
},
},
]);
});
test('signup flow helper recognizes email verification page as post-email landing page', async () => {
let ensureCalls = 0;
let passwordReadyChecks = 0;
@@ -477,6 +556,71 @@ test('signup flow helper recognizes email verification page as post-email landin
assert.equal(passwordReadyChecks, 0);
});
test('signup flow helper waits for the signup entry tab to settle for step 2 before probing the entry page', async () => {
const logs = [];
const events = [];
const helpers = signupFlowApi.createSignupFlowHelpers({
addLog: async (message, level = 'info', meta = {}) => {
logs.push({ message, level, meta });
},
buildGeneratedAliasEmail: () => '',
ensureContentScriptReadyOnTab: async () => {
events.push('content-ready');
},
ensureHotmailAccountForFlow: async () => ({}),
ensureLuckmailPurchaseForFlow: async () => ({}),
isGeneratedAliasProvider: () => false,
isHotmailProvider: () => false,
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => false,
reuseOrCreateTab: async () => {
events.push('reuse-or-create');
return 23;
},
sendToContentScriptResilient: async () => {
events.push('probe-entry');
return { ready: true, state: 'entry_home', url: 'https://chatgpt.com/' };
},
setEmailState: async () => {},
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
SIGNUP_PAGE_INJECT_FILES: [],
waitForTabStableComplete: async (_tabId, options) => {
events.push({ type: 'wait-stable', options });
return { id: 23, url: 'https://chatgpt.com/', status: 'complete' };
},
waitForTabUrlMatch: async () => null,
});
const result = await helpers.ensureSignupEntryPageReady(2);
assert.deepStrictEqual(events, [
'reuse-or-create',
{
type: 'wait-stable',
options: {
timeoutMs: 45000,
retryDelayMs: 300,
stableMs: 3000,
initialDelayMs: 300,
},
},
'content-ready',
'probe-entry',
]);
assert.equal(logs.some((item) => /额外稳定 3 /.test(item.message)), true);
assert.equal(logs.some((item) => item.meta.step === 2 && item.meta.stepKey === 'signup-entry'), true);
assert.deepStrictEqual(result, {
tabId: 23,
result: {
ready: true,
state: 'entry_home',
url: 'https://chatgpt.com/',
},
});
});
test('signup flow helper accepts phone signup landing on login password page', async () => {
let ensureCalls = 0;
let passwordReadyChecks = 0;
@@ -580,6 +580,62 @@ test('step 7 keeps Plus email login even when phone sms runtime exists', async (
assert.equal(events.payloads[0].accountIdentifier, 'plus.user@example.com');
});
test('step 7 keeps phone login after step 8 stores an unbound email for phone signup', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
const events = {
payloads: [],
};
const phoneSignupState = {
phoneVerificationEnabled: true,
signupMethod: 'phone',
resolvedSignupMethod: 'phone',
email: 'bound.step8@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'bound.step8@example.com',
signupPhoneNumber: '66959916439',
signupPhoneCompletedActivation: {
activationId: 'signup-done',
phoneNumber: '66959916439',
countryId: 52,
countryLabel: 'Thailand',
},
password: 'secret',
};
const executor = api.createStep7Executor({
addLog: async () => {},
completeStepFromBackground: async () => {},
getErrorMessage: (error) => error?.message || String(error || ''),
getLoginAuthStateLabel: (state) => state || 'unknown',
getState: async () => ({ ...phoneSignupState }),
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async (_sourceName, message) => {
events.payloads.push(message.payload);
return {
step6Outcome: 'success',
state: 'phone_verification_page',
loginVerificationRequestedAt: 123456,
};
},
STEP6_MAX_ATTEMPTS: 3,
throwIfStopped: () => {},
});
await executor.executeStep7(phoneSignupState);
assert.equal(events.payloads[0].loginIdentifierType, 'phone');
assert.equal(events.payloads[0].phoneNumber, '66959916439');
assert.equal(events.payloads[0].email, '');
assert.equal(events.payloads[0].accountIdentifier, '66959916439');
});
test('step 7 can infer phone login from an available phone signup configuration before step 2 finishes', async () => {
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
const globalScope = {};
+16 -1
View File
@@ -15,6 +15,10 @@ test('GoPay utils normalize manual OTP input', () => {
assert.equal(api.normalizeGpcOtpChannel('sms'), 'sms');
assert.equal(api.normalizeGpcOtpChannel('wa'), 'whatsapp');
assert.equal(api.normalizeGpcOtpChannel('unknown'), 'whatsapp');
assert.equal(api.normalizeGpcHelperPhoneMode('auto'), 'auto');
assert.equal(api.normalizeGpcHelperPhoneMode('builtin'), 'auto');
assert.equal(api.normalizeGpcHelperPhoneMode('manual'), 'manual');
assert.equal(api.normalizeGpcHelperPhoneMode('unknown'), 'manual');
});
test('GoPay utils keeps GPC helper payment method distinct', () => {
@@ -81,10 +85,21 @@ test('GoPay utils formats balance and maps linked-account errors', () => {
api.formatGpcBalancePayload({
code: 200,
message: 'ok',
data: { remaining_uses: 0, total_uses: 3, used_uses: 3, status: 'active' },
data: { remaining_uses: 0, total_uses: 3, used_uses: 3, status: 'active', auto_mode_enabled: false },
}),
'余额 0/3,已用 3,状态 active'
);
assert.equal(
api.formatGpcBalancePayload({
code: 200,
message: 'ok',
data: { remaining_uses: 998, total_uses: 1000, used_uses: 2, status: 'active', auto_mode_enabled: true },
}),
'余额 998/1000,已用 2,状态 active'
);
assert.equal(api.getGpcBalanceRemainingUses({ data: { remaining_uses: 998 } }), 998);
assert.equal(api.isGpcAutoModeEnabled({ data: { auto_mode_enabled: true } }), true);
assert.equal(api.isGpcAutoModeEnabled({ data: { auto_mode_enabled: false } }), false);
assert.deepEqual(
api.unwrapGpcResponse({ code: 200, message: 'ok', data: { task_id: 'task_1' } }),
{ task_id: 'task_1' }
+63
View File
@@ -5714,6 +5714,69 @@ test('phone verification helper stops when add-phone recovery cannot be verified
}
});
test('signup phone verification cancels activation when resend lands on contact-verification HTTP 500 page', async () => {
const requests = [];
let currentState = {
heroSmsApiKey: 'demo-key',
heroSmsCountryId: 52,
heroSmsCountryLabel: 'Thailand',
verificationResendCount: 0,
phoneCodeWaitSeconds: 60,
phoneCodeTimeoutWindows: 2,
phoneCodePollIntervalSeconds: 1,
phoneCodePollMaxRounds: 1,
signupPhoneActivation: {
activationId: '920001',
phoneNumber: '66953330001',
provider: 'hero-sms',
countryId: 52,
countryLabel: 'Thailand',
},
};
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id');
if (action === 'getStatus') {
return { ok: true, text: async () => 'STATUS_WAIT_CODE' };
}
if (action === 'setStatus') {
return { ok: true, text: async () => `STATUS_UPDATED:${id}` };
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
getState: async () => ({ ...currentState }),
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'RESEND_VERIFICATION_CODE') {
throw new Error(
'PHONE_RESEND_SERVER_ERROR::This page isn\'t working auth.openai.com is currently unable to handle this request. HTTP ERROR 500'
);
}
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
await assert.rejects(
() => helpers.completeSignupPhoneVerificationFlow(1, { state: currentState }),
(error) => {
assert.match(error.message, /^PHONE_RESEND_SERVER_ERROR::This page isn't working/);
assert.equal(error.message.includes('PHONE_RESEND_SERVER_ERROR::PHONE_RESEND_SERVER_ERROR::'), false);
return true;
}
);
assert.equal(currentState.signupPhoneActivation, null);
});
test('phone verification helper skips page resend for 5sim timeouts and rotates number directly', async () => {
const requests = [];
const messages = [];
@@ -85,6 +85,7 @@ function createExecutorHarness({
markCurrentRegistrationAccountUsed = async () => {},
probeIpProxyExit = null,
onSetState = null,
sleepWithStop = null,
submitRedirectUrl = 'https://www.paypal.com/checkoutnow',
}) {
const api = loadPlusCheckoutBillingModule();
@@ -166,7 +167,7 @@ function createExecutorHarness({
await onSetState(updates, events);
}
},
sleepWithStop: async (ms) => events.sleeps.push(ms),
sleepWithStop: sleepWithStop || (async (ms) => events.sleeps.push(ms)),
waitForTabCompleteUntilStopped: async () => checkoutTab,
waitForTabUrlMatchUntilStopped: async (tabId, matcher) => {
events.waitedUrls.push({ tabId });
@@ -903,11 +904,183 @@ test('GPC billing polls queue task, submits WhatsApp OTP then PIN, and waits unt
assert.equal(pinCall.options.headers['X-API-Key'], 'gpc_billing_123');
assert.ok(fetchCalls.findIndex((call) => call.url.endsWith('/api/gp/tasks/task_123/pin')) < fetchCalls.length - 1);
assert.equal(events.states.some((state) => state.gopayHelperTaskId === 'task_123' && state.gopayHelperTaskStatus === 'completed'), true);
assert.equal(events.logs.some((entry) => entry.message === '步骤 7GPC 任务状态:等待 WhatsApp OTP'), true);
assert.equal(events.logs.some((entry) => /whatsapp_otp_wait/.test(entry.message)), false);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
assert.ok(events.sleeps.includes(3000));
});
test('GPC billing auto mode only polls until completed without OTP or PIN submission', async () => {
const fetchCalls = [];
let pollCount = 0;
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_auto') {
pollCount += 1;
if (pollCount === 1) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({ task_id: 'task_auto', phone_mode: 'auto', status: 'queued', status_text: '排队中', api_waiting_for: '' }),
};
}
if (pollCount === 2) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({ task_id: 'task_auto', phone_mode: 'auto', status: 'active', status_text: '处理中', remote_stage: 'auto_otp_wait', api_waiting_for: 'auto_otp' }),
};
}
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({ task_id: 'task_auto', phone_mode: 'auto', status: 'completed', status_text: '充值完成', remote_stage: 'completed', api_waiting_for: '' }),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
await executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperTaskId: 'task_auto',
gopayHelperPhoneMode: 'auto',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperApiKey: 'gpc_auto',
});
assert.equal(fetchCalls.length, 3);
assert.equal(fetchCalls.some((call) => /\/api\/gp\/tasks\/task_auto\/(otp|pin)$/.test(call.url)), false);
assert.equal(events.logs.some((entry) => entry.message === '步骤 7GPC 任务状态:等待自动 OTP'), true);
assert.equal(events.logs.some((entry) => /auto_otp_wait/.test(entry.message)), false);
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
assert.equal(events.states.some((state) => state.gopayHelperTaskId === 'task_auto' && state.gopayHelperPhoneMode === 'auto' && state.gopayHelperTaskStatus === 'completed'), true);
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
});
test('GPC billing logs checkout order stage in Chinese', async () => {
let pollCount = 0;
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
fetchImpl: async (url) => {
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_stage') {
pollCount += 1;
if (pollCount === 1) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({
task_id: 'task_stage',
phone_mode: 'auto',
status: 'active',
status_text: '处理中',
remote_stage: 'checkout_order_start',
api_waiting_for: '',
}),
};
}
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({
task_id: 'task_stage',
phone_mode: 'auto',
status: 'completed',
status_text: '充值完成',
remote_stage: 'completed',
api_waiting_for: '',
}),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
await executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperTaskId: 'task_stage',
gopayHelperPhoneMode: 'auto',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperApiKey: 'gpc_auto',
});
assert.equal(events.logs.some((entry) => entry.message === '步骤 7GPC 任务状态:创建订单'), true);
assert.equal(events.logs.some((entry) => /checkout_order_start/.test(entry.message)), false);
});
test('GPC billing fails repeated checkout stage as stale so auto-run can recreate task', async () => {
const originalNow = Date.now;
let now = 1710000000000;
const fetchCalls = [];
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
sleepWithStop: async (ms) => {
events.sleeps.push(ms);
now += ms;
},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url === 'https://gpc.qlhazycoder.top/api/gp/tasks/task_stale') {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({
task_id: 'task_stale',
phone_mode: 'auto',
status: 'active',
status_text: '处理中',
remote_stage: 'checkout_order_start',
api_waiting_for: '',
}),
};
}
if (url.endsWith('/api/gp/tasks/task_stale/stop')) {
return {
ok: true,
status: 200,
json: async () => createGpcTaskResponse({
task_id: 'task_stale',
status: 'discarded',
status_text: '已停止',
}),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
Date.now = () => now;
try {
await assert.rejects(
() => executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperTaskId: 'task_stale',
gopayHelperPhoneMode: 'auto',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperApiKey: 'gpc_auto',
gopayHelperTaskStaleSeconds: 15,
}),
/GPC_TASK_ENDED::GPC 任务状态超过 15 秒无进展(创建订单),请重新创建任务。/
);
} finally {
Date.now = originalNow;
}
assert.equal(fetchCalls.some((call) => call.url.endsWith('/api/gp/tasks/task_stale/stop')), true);
assert.equal(events.logs.some((entry) => entry.message === '步骤 7GPC 任务状态:创建订单'), true);
});
test('GPC billing reads SMS OTP from local helper for sms_otp_wait', async () => {
const fetchCalls = [];
let pollCount = 0;
+224 -27
View File
@@ -5,7 +5,9 @@ const vm = require('node:vm');
const source = fs.readFileSync('background/steps/create-plus-checkout.js', 'utf8');
const plusCheckoutSource = fs.readFileSync('content/plus-checkout.js', 'utf8');
const gopayUtilsSource = fs.readFileSync('gopay-utils.js', 'utf8');
const globalScope = {};
new Function('self', `${gopayUtilsSource};`)(globalScope);
const api = new Function('self', `${source}; return self.MultiPageBackgroundPlusCheckoutCreate;`)(globalScope);
function createCheckoutContentHarness() {
@@ -154,6 +156,37 @@ function createCheckoutContentHarness() {
return { checkoutEvents, send };
}
function createGpcBalanceResponse(overrides = {}) {
return {
code: 200,
message: 'ok',
data: {
api_key: 'gpc_test',
status: 'active',
auto_mode_enabled: false,
total_uses: 1000,
remaining_uses: 998,
used_uses: 2,
...overrides,
},
};
}
function createGpcTaskResponse(overrides = {}) {
return {
code: 200,
message: 'ok',
data: {
task_id: 'task_123',
status: 'active',
status_text: '处理中',
phone_mode: 'manual',
remote_stage: 'checkout_start',
...overrides,
},
};
}
test('Plus checkout create does not wait 20 seconds after opening checkout page', async () => {
const events = [];
const executor = api.createPlusCheckoutCreateExecutor({
@@ -323,7 +356,7 @@ test('Plus checkout content routes same-frame autocomplete query and suggestion
assert.equal(checkoutEvents.some((event) => event.type === 'delay' && event.ms !== 2000), false);
});
test('GPC checkout injects Plus script before reading ChatGPT session token and sends X-API-Key', async () => {
test('GPC manual checkout injects Plus script before reading ChatGPT session token and sends X-API-Key', async () => {
const events = [];
const fetchCalls = [];
const executor = api.createPlusCheckoutCreateExecutor({
@@ -344,18 +377,9 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
return {
ok: true,
status: 200,
json: async () => ({
code: 200,
message: 'ok',
data: {
task_id: 'task_123',
status: 'active',
status_text: '处理中',
phone_mode: 'manual',
remote_stage: 'checkout_start',
otp_channel: 'whatsapp',
},
}),
json: async () => url.endsWith('/api/gp/balance')
? createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 })
: createGpcTaskResponse({ otp_channel: 'whatsapp' }),
};
},
registerTab: async (source, tabId) => events.push({ type: 'register', source, tabId }),
@@ -371,6 +395,7 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
await executor.executePlusCheckoutCreate({
email: 'Current.Round+GPC@Example.COM',
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'manual',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
gopayHelperPhoneNumber: '+8613800138000',
gopayPhone: '',
@@ -388,9 +413,11 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
includeSession: true,
includeAccessToken: true,
});
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
const helperPayload = JSON.parse(fetchCalls[0].options.body);
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_test_123');
assert.equal(fetchCalls[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
const helperPayload = JSON.parse(fetchCalls[1].options.body);
assert.deepEqual(helperPayload, {
access_token: 'session-access-token',
phone_mode: 'manual',
@@ -398,7 +425,7 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
phone_number: '13800138000',
otp_channel: 'whatsapp',
});
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_test_123');
assert.equal(fetchCalls[1].options.headers['X-API-Key'], 'gpc_test_123');
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'card_key'), false);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'customer_email'), false);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'checkout_ui_mode'), false);
@@ -415,6 +442,165 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
assert.equal(events.find((event) => event.type === 'complete')?.payload?.plusCheckoutSource, 'gpc-helper');
});
test('GPC auto checkout only sends access token and API Key', async () => {
const events = [];
const fetchCalls = [];
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async (message, level = 'info') => events.push({ type: 'log', message, level }),
chrome: {
tabs: {
create: async () => {
throw new Error('should not open token tab when direct access token exists');
},
remove: async () => {},
},
},
completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => url.endsWith('/api/gp/balance')
? createGpcBalanceResponse({ auto_mode_enabled: true, remaining_uses: 998 })
: createGpcTaskResponse({
task_id: 'task_auto',
status: 'queued',
status_text: '排队中',
phone_mode: 'auto',
api_waiting_for: '',
}),
};
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => ({}),
setState: async (payload) => events.push({ type: 'set-state', payload }),
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'auto',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
gopayHelperApiKey: 'gpc_auto_123',
});
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_auto_123');
assert.equal(fetchCalls[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
const helperPayload = JSON.parse(fetchCalls[1].options.body);
assert.deepEqual(helperPayload, {
access_token: 'state-access-token',
phone_mode: 'auto',
});
assert.equal(fetchCalls[1].options.headers['X-API-Key'], 'gpc_auto_123');
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'country_code'), false);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'phone_number'), false);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'otp_channel'), false);
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'pin'), false);
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
assert.equal(statePayload.gopayHelperTaskId, 'task_auto');
assert.equal(statePayload.gopayHelperPhoneMode, 'auto');
assert.equal(statePayload.gopayHelperTaskStatus, 'queued');
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
});
test('GPC auto checkout blocks API Keys without auto mode permission', async () => {
const fetchCalls = [];
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async () => {},
chrome: {
tabs: {
create: async () => {
throw new Error('should not open token tab when direct access token exists');
},
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 }),
};
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'auto',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
gopayHelperApiKey: 'gpc_auto_disabled',
}),
/未开通自动模式/
);
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
});
test('GPC checkout blocks exhausted API Keys before creating task', async () => {
const fetchCalls = [];
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async () => {},
chrome: {
tabs: {
create: async () => {
throw new Error('should not open token tab when direct access token exists');
},
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 0 }),
};
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'manual',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperApiKey: 'gpc_exhausted',
}),
/剩余次数不足/
);
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
});
test('GPC checkout forwards selected SMS OTP channel', async () => {
const fetchCalls = [];
const executor = api.createPlusCheckoutCreateExecutor({
@@ -432,11 +618,9 @@ test('GPC checkout forwards selected SMS OTP channel', async () => {
return {
ok: true,
status: 200,
json: async () => ({
code: 200,
message: 'ok',
data: { task_id: 'task_sms', status: 'active', phone_mode: 'manual', remote_stage: 'checkout_start' },
}),
json: async () => url.endsWith('/api/gp/balance')
? createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 })
: createGpcTaskResponse({ task_id: 'task_sms', status: 'active', phone_mode: 'manual', remote_stage: 'checkout_start' }),
};
},
registerTab: async () => {},
@@ -457,10 +641,13 @@ test('GPC checkout forwards selected SMS OTP channel', async () => {
gopayHelperOtpChannel: 'sms',
});
const helperPayload = JSON.parse(fetchCalls[0].options.body);
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_sms');
const helperPayload = JSON.parse(fetchCalls[1].options.body);
assert.equal(helperPayload.phone_mode, 'manual');
assert.equal(helperPayload.otp_channel, 'sms');
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_sms');
assert.equal(fetchCalls[1].options.headers['X-API-Key'], 'gpc_sms');
assert.equal(Object.prototype.hasOwnProperty.call(helperPayload, 'card_key'), false);
});
@@ -480,6 +667,13 @@ test('GPC checkout surfaces unified queue API errors', async () => {
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url.endsWith('/api/gp/balance')) {
return {
ok: true,
status: 200,
json: async () => createGpcBalanceResponse({ auto_mode_enabled: false, remaining_uses: 998 }),
};
}
return {
ok: false,
status: 400,
@@ -511,9 +705,11 @@ test('GPC checkout surfaces unified queue API errors', async () => {
/创建 GPC 订单失败:access_token 无效/
);
assert.equal(fetchCalls.length, 1);
assert.equal(Object.prototype.hasOwnProperty.call(JSON.parse(fetchCalls[0].options.body), 'card_key'), false);
assert.equal(fetchCalls[0].options.headers['X-API-Key'], 'gpc_paid_456');
assert.equal(fetchCalls.length, 2);
assert.equal(fetchCalls[0].url, 'https://gpc.qlhazycoder.top/api/gp/balance');
assert.equal(fetchCalls[1].url, 'https://gpc.qlhazycoder.top/api/gp/tasks');
assert.equal(Object.prototype.hasOwnProperty.call(JSON.parse(fetchCalls[1].options.body), 'card_key'), false);
assert.equal(fetchCalls[1].options.headers['X-API-Key'], 'gpc_paid_456');
});
test('GPC checkout does not fall back to browser GoPay phone fields', async () => {
@@ -542,6 +738,7 @@ test('GPC checkout does not fall back to browser GoPay phone fields', async () =
await assert.rejects(
() => executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperPhoneMode: 'manual',
gopayHelperApiUrl: 'https://gpc.qlhazycoder.top/',
chatgptAccessToken: 'state-access-token',
email: 'helper-phone-test@example.com',
@@ -113,6 +113,9 @@ async function refreshContributionContentHint() {
events.push({ type: 'refresh' });
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
}
async function ensureGpcApiKeyReadyForStart() {
return true;
}
${bundle}
return {
startAutoRunFromCurrentSettings,
+124 -2
View File
@@ -132,6 +132,10 @@ test('sidepanel Plus UI hides PayPal account selector while GoPay is selected',
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
@@ -141,6 +145,8 @@ let latestState = { plusPaymentMethod: 'gopay' };
let currentPlusPaymentMethod = 'paypal';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gopay', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const rowPayPalAccount = { style: { display: '' } };
${bundle}
return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount };
@@ -209,21 +215,29 @@ test('sidepanel Plus UI shows GPC fields and purchase button only for GPC', () =
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper' };
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperAutoModeEnabled: true };
let currentPlusPaymentMethod = 'paypal';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const plusPaymentMethodCaption = { textContent: '' };
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
const rowPayPalAccount = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
const rowGpcHelperApi = { style: { display: 'none' } };
const rowGpcHelperCardKey = { style: { display: 'none' } };
const rowGpcHelperPhoneMode = { style: { display: 'none' } };
const selectGpcHelperPhoneMode = { value: 'manual' };
const rowGpcHelperCountryCode = { style: { display: 'none' } };
const rowGpcHelperPhone = { style: { display: 'none' } };
const rowGpcHelperOtpChannel = { style: { display: 'none' } };
@@ -240,12 +254,13 @@ ${bundle}
return {
updatePlusModeUI,
selectPlusPaymentMethod,
selectGpcHelperPhoneMode,
selectGpcHelperOtpChannel,
inputGpcHelperLocalSmsEnabled,
btnGpcCardKeyPurchase,
rowPayPalAccount,
plusPaymentMethodCaption,
rows: { rowGpcHelperApi, rowGpcHelperCardKey, rowGpcHelperCountryCode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperLocalSmsEnabled, rowGpcHelperLocalSmsUrl, rowGpcHelperPin },
rows: { rowGpcHelperApi, rowGpcHelperCardKey, rowGpcHelperPhoneMode, rowGpcHelperCountryCode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperLocalSmsEnabled, rowGpcHelperLocalSmsUrl, rowGpcHelperPin },
};
`)();
@@ -255,6 +270,7 @@ return {
assert.equal(api.btnGpcCardKeyPurchase.style.display, '');
assert.equal(api.rows.rowGpcHelperApi.style.display, '');
assert.equal(api.rows.rowGpcHelperCardKey.style.display, '');
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, '');
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
@@ -272,6 +288,15 @@ return {
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
api.selectGpcHelperPhoneMode.value = 'auto';
api.updatePlusModeUI();
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, '');
assert.equal(api.rows.rowGpcHelperPhone.style.display, 'none');
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, 'none');
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
assert.match(api.plusPaymentMethodCaption.textContent, /自动/);
api.selectPlusPaymentMethod.value = 'gopay';
api.updatePlusModeUI();
assert.equal(api.btnGpcCardKeyPurchase.style.display, 'none');
@@ -279,6 +304,103 @@ return {
assert.equal(api.rowPayPalAccount.style.display, 'none');
});
test('sidepanel hides GPC auto mode selector when API Key has no auto permission', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: { auto_mode_enabled: false } };
let currentPlusPaymentMethod = 'gpc-helper';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const plusPaymentMethodCaption = { textContent: '' };
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
const rowPayPalAccount = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
const rowGpcHelperApi = { style: { display: 'none' } };
const rowGpcHelperCardKey = { style: { display: 'none' } };
const rowGpcHelperPhoneMode = { style: { display: 'none' } };
const selectGpcHelperPhoneMode = { value: 'auto' };
const rowGpcHelperCountryCode = { style: { display: 'none' } };
const rowGpcHelperPhone = { style: { display: 'none' } };
const rowGpcHelperOtpChannel = { style: { display: 'none' } };
const selectGpcHelperOtpChannel = { value: 'whatsapp' };
const rowGpcHelperLocalSmsEnabled = { style: { display: 'none' } };
const inputGpcHelperLocalSmsEnabled = { checked: false };
const rowGpcHelperLocalSmsUrl = { style: { display: 'none' } };
const rowGpcHelperPin = { style: { display: 'none' } };
${bundle}
return { updatePlusModeUI, selectGpcHelperPhoneMode, plusPaymentMethodCaption, rows: { rowGpcHelperPhoneMode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperPin } };
`)();
api.updatePlusModeUI();
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, 'none');
assert.equal(api.selectGpcHelperPhoneMode.value, 'manual');
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
assert.equal(api.rows.rowGpcHelperPin.style.display, '');
assert.match(api.plusPaymentMethodCaption.textContent, /手动/);
});
test('sidepanel keeps selected GPC auto mode before permission has been queried', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('normalizeGpcHelperPhoneModeValue'),
extractFunction('getGpcHelperAutoModeEnabled'),
extractFunction('hasGpcAutoModePermissionField'),
extractFunction('isGpcAutoModePermissionDenied'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper', gopayHelperPhoneMode: 'auto', gopayHelperAutoModeEnabled: false, gopayHelperBalancePayload: null };
let currentPlusPaymentMethod = 'gpc-helper';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
const plusPaymentMethodCaption = { textContent: '' };
const rowPayPalAccount = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
const rowGpcHelperApi = { style: { display: 'none' } };
const rowGpcHelperCardKey = { style: { display: 'none' } };
const rowGpcHelperPhoneMode = { style: { display: 'none' } };
const selectGpcHelperPhoneMode = { value: 'auto' };
const rowGpcHelperCountryCode = { style: { display: 'none' } };
const rowGpcHelperPhone = { style: { display: 'none' } };
const rowGpcHelperOtpChannel = { style: { display: 'none' } };
const selectGpcHelperOtpChannel = { value: 'whatsapp' };
const rowGpcHelperLocalSmsEnabled = { style: { display: 'none' } };
const inputGpcHelperLocalSmsEnabled = { checked: false };
const rowGpcHelperLocalSmsUrl = { style: { display: 'none' } };
const rowGpcHelperPin = { style: { display: 'none' } };
${bundle}
return { updatePlusModeUI, selectGpcHelperPhoneMode, plusPaymentMethodCaption, rows: { rowGpcHelperPhoneMode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperPin } };
`)();
api.updatePlusModeUI();
assert.equal(api.rows.rowGpcHelperPhoneMode.style.display, '');
assert.equal(api.selectGpcHelperPhoneMode.value, 'auto');
assert.equal(api.rows.rowGpcHelperPhone.style.display, 'none');
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, 'none');
assert.equal(api.rows.rowGpcHelperPin.style.display, 'none');
assert.match(api.plusPaymentMethodCaption.textContent, /自动/);
});
test('sidepanel resolves pending GoPay manual confirmation from DATA_UPDATED state', async () => {
const bundle = [
extractFunction('openPlusManualConfirmationDialog'),
+252
View File
@@ -440,6 +440,127 @@ return {
assert.equal(api.run()?.kind, 'localized-email');
});
test('waitForSignupEntryState retries the signup entry click five times before giving up', async () => {
const api = new Function(`
const logs = [];
const clicks = [];
let now = 0;
const signupButton = {
textContent: '免费注册',
value: '',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'button';
return '';
},
getBoundingClientRect() {
return { width: 120, height: 36 };
},
};
const document = {
querySelector() {
return null;
},
querySelectorAll(selector) {
if (selector === 'a, button, [role="button"], [role="link"]') {
return [signupButton];
}
return [];
},
};
const location = {
href: 'https://chatgpt.com/',
};
const Date = {
now() {
return now;
},
};
${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')}
${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')}
${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
function isVisibleElement(el) {
return Boolean(el);
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function getSignupPasswordInput() {
return null;
}
function isSignupPasswordPage() {
return false;
}
function getSignupPasswordSubmitButton() {
return null;
}
function getSignupPasswordDisplayedEmail() {
return '';
}
function throwIfStopped() {}
function log(message, level = 'info') {
logs.push({ message, level });
}
async function humanPause() {}
function simulateClick(target) {
clicks.push(getActionText(target));
}
async function sleep(ms) {
now += ms;
}
${extractFunction('getSignupEmailInput')}
${extractFunction('getSignupPhoneInput')}
${extractFunction('getSignupEmailContinueButton')}
${extractFunction('findSignupEntryTrigger')}
${extractFunction('inspectSignupEntryState')}
${extractFunction('waitForSignupEntryState')}
return {
async run() {
return waitForSignupEntryState({ timeout: 30000, autoOpenEntry: true, step: 2 });
},
getClicks() {
return clicks.slice();
},
getLogs() {
return logs.slice();
},
};
`)();
const snapshot = await api.run();
assert.equal(snapshot.state, 'entry_home');
assert.equal(api.getClicks().length, 6);
assert.equal(api.getLogs().some(({ message }) => message.includes('重试 5/5')), true);
assert.equal(api.getLogs().some(({ message, level }) => level === 'warn' && /已完成 5 次重试/.test(message)), true);
});
test('ensureSignupPhoneEntryReady opens free signup before switching to the phone entry', async () => {
const api = new Function(`
const logs = [];
@@ -625,6 +746,137 @@ return {
assert.deepEqual(api.getClicks(), ['免费注册', 'Continue with phone number']);
});
test('waitForSignupPhoneEntryState retries the signup entry click five times before giving up', async () => {
const api = new Function(`
const logs = [];
const clicks = [];
let now = 0;
const signupButton = {
textContent: '免费注册',
value: '',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'button';
return '';
},
getBoundingClientRect() {
return { width: 120, height: 36 };
},
};
const document = {
querySelector() {
return null;
},
querySelectorAll(selector) {
if (selector === 'a, button, [role="button"], [role="link"]') {
return [signupButton];
}
return [];
},
};
const location = {
href: 'https://chatgpt.com/',
};
const Date = {
now() {
return now;
},
};
${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')}
${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')}
${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')}
${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')}
${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')}
${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')}
${extractConst('SIGNUP_WORK_EMAIL_PATTERN')}
${extractConst('SIGNUP_PHONE_ACTION_PATTERN')}
${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')}
${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')}
function isVisibleElement(el) {
return Boolean(el);
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function getSignupPasswordInput() {
return null;
}
function isSignupPasswordPage() {
return false;
}
function getSignupPasswordSubmitButton() {
return null;
}
function getSignupPasswordDisplayedEmail() {
return '';
}
function throwIfStopped() {}
function log(message, level = 'info') {
logs.push({ message, level });
}
function simulateClick(target) {
clicks.push(getActionText(target));
}
async function humanPause() {}
async function sleep(ms) {
now += ms;
}
${extractFunction('getSignupEmailInput')}
${extractFunction('getSignupPhoneInput')}
${extractFunction('findSignupUseEmailTrigger')}
${extractFunction('findSignupUsePhoneTrigger')}
${extractFunction('findSignupMoreOptionsTrigger')}
${extractFunction('getSignupEmailContinueButton')}
${extractFunction('findSignupEntryTrigger')}
${extractFunction('inspectSignupEntryState')}
${extractFunction('waitForSignupPhoneEntryState')}
return {
async run() {
return waitForSignupPhoneEntryState({ timeout: 30000, step: 2 });
},
getClicks() {
return clicks.slice();
},
getLogs() {
return logs.slice();
},
};
`)();
const snapshot = await api.run();
assert.equal(snapshot.state, 'entry_home');
assert.equal(api.getClicks().length, 6);
assert.equal(api.getLogs().some(({ message }) => message.includes('重试 5/5')), true);
assert.equal(api.getLogs().some(({ message, level }) => level === 'warn' && /已完成 5 次重试/.test(message)), true);
});
test('submitSignupPhoneNumberAndContinue auto-switches signup country before filling the local phone number', async () => {
const api = new Function(`
const logs = [];
+4 -1
View File
@@ -111,7 +111,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 13);
assert.equal(gpcSteps[5].title, '创建 GPC 订单');
assert.equal(gpcSteps[6].title, 'GPC OTP/PIN 验证');
assert.equal(gpcSteps[6].title, '等待 GPC 任务完成');
});
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
@@ -142,6 +142,9 @@ test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
assert.match(html, />转换 API Key</);
assert.match(html, /GPC API Key/);
assert.match(html, /id="input-gpc-helper-card-key"/);
assert.match(html, /GPC 模式/);
assert.match(html, /id="select-gpc-helper-phone-mode"/);
assert.match(html, /<option value="auto">自动模式<\/option>/);
assert.match(html, /id="btn-gpc-helper-balance"/);
assert.match(html, /id="input-gpc-helper-phone"/);
assert.match(html, /id="select-gpc-helper-otp-channel"/);
+53
View File
@@ -341,6 +341,59 @@ return {
assert.equal(Object.prototype.hasOwnProperty.call(result, 'invalidCode'), false);
});
test('resendVerificationCode reports contact-verification HTTP 500 after resend click', async () => {
const api = new Function(`
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;
const logs = [];
const location = {
href: 'https://auth.openai.com/email-verification',
pathname: '/email-verification',
};
const document = {
title: 'Verify your email',
body: { textContent: 'Enter the verification code.' },
};
function throwIfStopped() {}
function log(message, level = 'info') { logs.push({ message, level }); }
function findResendVerificationCodeTrigger() { return { textContent: 'Resend code' }; }
function isActionEnabled() { return true; }
async function humanPause() {}
function simulateClick() {
location.href = 'https://auth.openai.com/contact-verification';
location.pathname = '/contact-verification';
document.title = "This page isn't working";
document.body.textContent = 'auth.openai.com is currently unable to handle this request. HTTP ERROR 500';
}
async function sleep() {}
function is405MethodNotAllowedPage() { return false; }
async function handle405ResendError() {}
function getActionText(element) { return element?.textContent || ''; }
function getPageTextSnapshot() { return document.body.textContent; }
${extractFunction('getContactVerificationServerErrorText')}
${extractFunction('throwIfContactVerificationServerError')}
${extractFunction('resendVerificationCode')}
return {
async run() {
try {
await resendVerificationCode(4, 1000);
return { ok: true };
} catch (error) {
return { ok: false, message: String(error?.message || error), logs };
}
},
};
`)();
const result = await api.run();
assert.equal(result.ok, false);
assert.match(result.message, /^PHONE_RESEND_SERVER_ERROR::This page isn't working/);
assert.equal(result.message.includes('PHONE_RESEND_SERVER_ERROR::PHONE_RESEND_SERVER_ERROR::'), false);
});
test('fillVerificationCode does not short-circuit on mixed email-verification profile page before verification exits', async () => {
const api = new Function(`
const logs = [];
+63 -4
View File
@@ -54,9 +54,21 @@ function extractFunction(name) {
function getStep5Bundle() {
return [
extractFunction('getStep5DirectCompletionPayload'),
extractFunction('isSignupProfilePageUrl'),
extractFunction('isStep5AllConsentText'),
extractFunction('findStep5AllConsentCheckbox'),
extractFunction('isStep5CheckboxChecked'),
extractFunction('getStep5ProfilePathPatterns'),
extractFunction('getStep5AuthRetryPathPatterns'),
extractFunction('isStep5ProfilePageUrl'),
extractFunction('getStep5AuthRetryPageState'),
extractFunction('getStep5SubmitButton'),
extractFunction('waitForStep5SubmitButton'),
extractFunction('isStep5SubmitButtonClickable'),
extractFunction('isStep5ProfileStillVisible'),
extractFunction('getStep5PostSubmitSuccessState'),
extractFunction('installStep5NavigationCompletionReporter'),
extractFunction('waitForStep5SubmitOutcome'),
extractFunction('step5_fillNameBirthday'),
].join('\n');
}
@@ -73,6 +85,9 @@ const completeButton = {
tagName: 'BUTTON',
textContent: '\\u5b8c\\u6210\\u8d26\\u6237\\u521b\\u5efa',
hidden: false,
getAttribute() {
return '';
},
};
const allConsentLabel = {
hidden: false,
@@ -110,6 +125,7 @@ const document = {
return null;
case 'input[name="age"]':
return ageInput;
case 'button[type="submit"], input[type="submit"]':
case 'button[type="submit"]':
return completeButton;
default:
@@ -136,6 +152,8 @@ function log(message, level = 'info') {
logs.push({ message, level });
}
function throwIfStopped() {}
async function waitForElement() {
return nameInput;
}
@@ -155,6 +173,14 @@ function isVisibleElement(el) {
return Boolean(el) && !el.hidden;
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute?.('aria-disabled') !== 'true';
}
function getActionText(el) {
return el.textContent || '';
}
async function setReactAriaBirthdaySelect() {
throw new Error('setReactAriaBirthdaySelect should not run in age-mode test');
}
@@ -168,6 +194,9 @@ function simulateClick(el) {
if (el === allConsentLabel || el === allConsentCheckbox) {
allConsentCheckbox.checked = true;
}
if (el === completeButton) {
location.href = 'https://chatgpt.com/';
}
}
function reportComplete(step, payload) {
@@ -178,6 +207,17 @@ function normalizeInlineText(text) {
return String(text || '').replace(/\\s+/g, ' ').trim();
}
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() { return null; }
async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); }
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${getStep5Bundle()}
return {
@@ -205,9 +245,11 @@ return {
const snapshot = api.snapshot();
assert.deepStrictEqual(result, {
skippedPostSubmitCheck: true,
directProceedToStep6: true,
profileSubmitted: true,
postSubmitChecked: true,
ageMode: true,
outcome: 'logged_in_home',
url: 'https://chatgpt.com/',
});
assert.equal(snapshot.nameValue, 'Mia Harris');
assert.equal(snapshot.ageValue, '19');
@@ -220,9 +262,11 @@ return {
{
step: 5,
payload: {
skippedPostSubmitCheck: true,
directProceedToStep6: true,
profileSubmitted: true,
postSubmitChecked: true,
ageMode: true,
outcome: 'logged_in_home',
url: 'https://chatgpt.com/',
},
},
]);
@@ -291,6 +335,7 @@ const document = {
return null;
case 'input[name="age"]':
return ageInput;
case 'button[type="submit"], input[type="submit"]':
case 'button[type="submit"]':
return completeButton;
default:
@@ -311,6 +356,7 @@ const document = {
const location = { href: 'https://auth.openai.com/u/signup/profile' };
function log() {}
function throwIfStopped() {}
async function waitForElement() { return nameInput; }
async function humanPause() {}
async function sleep() {}
@@ -321,9 +367,22 @@ async function setReactAriaBirthdaySelect() { throw new Error('setReactAriaBirth
async function waitForElementByText() { throw new Error('waitForElementByText should not run in this test'); }
function simulateClick(el) {
operationEvents.push(\`simulate-click:\${activeOperationLabel || 'outside'}:\${el.textContent || el.tagName || 'element'}\`);
if (el === completeButton) {
location.href = 'https://chatgpt.com/';
}
}
function reportComplete() {}
function normalizeInlineText(text) { return String(text || '').replace(/\\s+/g, ' ').trim(); }
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() { return null; }
async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); }
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${getStep5Bundle()}
+272 -10
View File
@@ -51,8 +51,26 @@ function extractFunction(name) {
return source.slice(start, end);
}
function getStep5OutcomeBundle() {
return [
extractFunction('getStep5ProfilePathPatterns'),
extractFunction('getStep5AuthRetryPathPatterns'),
extractFunction('isStep5ProfilePageUrl'),
extractFunction('getStep5AuthRetryPageState'),
extractFunction('getStep5SubmitButton'),
extractFunction('waitForStep5SubmitButton'),
extractFunction('isStep5SubmitButtonClickable'),
extractFunction('isStep5ProfileStillVisible'),
extractFunction('getStep5PostSubmitSuccessState'),
extractFunction('installStep5NavigationCompletionReporter'),
extractFunction('waitForStep5SubmitOutcome'),
].join('\n');
}
function getStep5Bundle() {
return [
extractFunction('isSignupProfilePageUrl'),
getStep5OutcomeBundle(),
extractFunction('getStep5DirectCompletionPayload'),
extractFunction('isStep5AllConsentText'),
extractFunction('findStep5AllConsentCheckbox'),
@@ -61,11 +79,11 @@ function getStep5Bundle() {
].join('\n');
}
test('step 5 clicks submit and completes immediately on birthday page', async () => {
test('step 5 clicks submit and completes after confirmed birthday page result', async () => {
const step5Source = extractFunction('step5_fillNameBirthday');
assert.ok(
!step5Source.includes('waitForStep5SubmitOutcome('),
'Step 5 提交后不应再等待页面结果'
step5Source.includes('waitForStep5SubmitOutcome('),
'Step 5 提交后等待页面结果,避免资料提交假完成'
);
const api = new Function(`
@@ -84,6 +102,7 @@ const completeButton = {
tagName: 'BUTTON',
textContent: '完成帐户创建',
hidden: false,
getAttribute() { return ''; },
};
const birthdaySelects = {
@@ -102,6 +121,7 @@ const document = {
return null;
case 'input[name="birthday"]':
return hiddenBirthday;
case 'button[type="submit"], input[type="submit"]':
case 'button[type="submit"]':
return completeButton;
default:
@@ -112,6 +132,12 @@ const document = {
if (selector === 'input[name="allCheckboxes"][type="checkbox"]') {
return [];
}
if (selector === 'input[type="checkbox"]') {
return [];
}
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') {
return [completeButton];
}
return [];
},
execCommand() {},
@@ -130,6 +156,8 @@ function log(message, level = 'info') {
logs.push({ message, level });
}
function throwIfStopped() {}
async function waitForElement() {
return nameInput;
}
@@ -149,6 +177,14 @@ function isVisibleElement(el) {
return Boolean(el) && !el.hidden;
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute?.('aria-disabled') !== 'true';
}
function getActionText(el) {
return el.textContent || '';
}
async function setReactAriaBirthdaySelect(select, value) {
selectedBirthday[select.label] = String(value).padStart(select.label === '年' ? 4 : 2, '0');
if (selectedBirthday['年'] && selectedBirthday['月'] && selectedBirthday['天']) {
@@ -162,6 +198,9 @@ async function waitForElementByText() {
function simulateClick(el) {
clicks.push(el.textContent || el.tagName || 'element');
if (el === completeButton) {
location.href = 'https://chatgpt.com/';
}
}
function reportComplete(step, payload) {
@@ -172,6 +211,17 @@ function reportComplete(step, payload) {
return String(text || '').replace(/\\s+/g, ' ').trim();
}
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() { return null; }
async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); }
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${getStep5Bundle()}
return {
@@ -202,17 +252,21 @@ return {
assert.deepStrictEqual(
result,
{
skippedPostSubmitCheck: true,
directProceedToStep6: true,
profileSubmitted: true,
postSubmitChecked: true,
outcome: 'logged_in_home',
url: 'https://chatgpt.com/',
},
'生日模式点击提交后应直接返回完成载荷'
'生日模式点击提交后应等待并确认页面结果'
);
assert.deepStrictEqual(snapshot.completions, [
{
step: 5,
payload: {
skippedPostSubmitCheck: true,
directProceedToStep6: true,
profileSubmitted: true,
postSubmitChecked: true,
outcome: 'logged_in_home',
url: 'https://chatgpt.com/',
},
},
]);
@@ -220,8 +274,8 @@ return {
assert.equal(snapshot.nameValue, 'Test User');
assert.equal(snapshot.birthdayValue, '2003-06-19');
assert.ok(
snapshot.logs.some(({ message }) => /不再等待页面结果/.test(message)),
'日志应明确说明 Step 5 已直接完成'
snapshot.logs.some(({ message }) => /资料提交结果已确认/.test(message)),
'日志应明确说明 Step 5 已完成提交后检测'
);
});
@@ -237,6 +291,7 @@ const completeButton = {
tagName: 'BUTTON',
textContent: '完成帐户创建',
hidden: false,
getAttribute() { return ''; },
};
const window = {
@@ -261,6 +316,7 @@ const document = {
return null;
case 'input[name="age"]':
return ageInput;
case 'button[type="submit"], input[type="submit"]':
case 'button[type="submit"]':
return completeButton;
default:
@@ -271,6 +327,12 @@ const document = {
if (selector === 'input[name="allCheckboxes"][type="checkbox"]') {
return [];
}
if (selector === 'input[type="checkbox"]') {
return [];
}
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') {
return [completeButton];
}
return [];
},
execCommand() {},
@@ -284,6 +346,8 @@ function log(message, level = 'info') {
logs.push({ message, level });
}
function throwIfStopped() {}
async function waitForElement() {
return nameInput;
}
@@ -304,6 +368,14 @@ function isVisibleElement(el) {
return Boolean(el) && !el.hidden;
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute?.('aria-disabled') !== 'true';
}
function getActionText(el) {
return el.textContent || '';
}
async function setReactAriaBirthdaySelect() {}
async function waitForElementByText() {
@@ -312,6 +384,7 @@ async function waitForElementByText() {
function simulateClick() {
profileEvents.push('click:complete');
location.href = 'https://chatgpt.com/';
}
function reportComplete(step, payload) {
@@ -322,6 +395,17 @@ function normalizeInlineText(text) {
return String(text || '').replace(/\s+/g, ' ').trim();
}
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() { return null; }
async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); }
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${getStep5Bundle()}
return {
@@ -613,3 +697,181 @@ return {
'delay:fill-birthday-day:2000',
]);
});
test('step 5 retries submit while profile page remains visible', async () => {
const api = new Function(`
const realDateNow = Date.now;
let now = 0;
const logs = [];
const completions = [];
const clicks = [];
const nameInput = { value: '', hidden: false };
const ageInput = { value: '', hidden: false };
const completeButton = {
tagName: 'BUTTON',
textContent: '完成帐户创建',
hidden: false,
getAttribute() { return ''; },
};
const location = {
href: 'https://auth.openai.com/u/signup/profile',
};
const document = {
querySelector(selector) {
switch (selector) {
case '[role="spinbutton"][data-type="year"]':
case '[role="spinbutton"][data-type="month"]':
case '[role="spinbutton"][data-type="day"]':
case 'input[name="birthday"]':
return null;
case 'input[name="age"]':
return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href) ? ageInput : null;
case 'button[type="submit"], input[type="submit"]':
case 'button[type="submit"]':
return completeButton;
default:
return null;
}
},
querySelectorAll(selector) {
if (selector === 'input[name="allCheckboxes"][type="checkbox"]') return [];
if (selector === 'input[type="checkbox"]') return [];
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') return [completeButton];
return [];
},
execCommand() {},
};
Date.now = () => now;
function log(message, level = 'info') { logs.push({ message, level }); }
function throwIfStopped() {}
async function waitForElement() { return nameInput; }
async function humanPause() {}
async function sleep(ms = 0) { now += ms || 250; }
function fillInput(input, value) { input.value = value; }
function findBirthdayReactAriaSelect() { return null; }
function isVisibleElement(el) { return Boolean(el) && !el.hidden; }
function isActionEnabled(el) { return Boolean(el) && !el.disabled && el.getAttribute?.('aria-disabled') !== 'true'; }
function getActionText(el) { return el.textContent || ''; }
async function setReactAriaBirthdaySelect() { throw new Error('setReactAriaBirthdaySelect should not run'); }
async function waitForElementByText() { return completeButton; }
function simulateClick(el) {
clicks.push(el.textContent || el.tagName || 'element');
if (el === completeButton && clicks.filter((text) => text === '完成帐户创建').length >= 3) {
location.href = 'https://chatgpt.com/';
}
}
function reportComplete(step, payload) { completions.push({ step, payload }); }
function normalizeInlineText(text) { return String(text || '').replace(/\\s+/g, ' ').trim(); }
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() { return null; }
async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); }
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${getStep5Bundle()}
return {
run() {
return step5_fillNameBirthday({
firstName: 'Mia',
lastName: 'Harris',
age: 19,
});
},
snapshot() {
return { logs, completions, clicks, nameValue: nameInput.value, ageValue: ageInput.value };
},
restore() {
Date.now = realDateNow;
},
};
`)();
let result;
try {
result = await api.run();
} finally {
api.restore();
}
const snapshot = api.snapshot();
assert.deepStrictEqual(result, {
profileSubmitted: true,
postSubmitChecked: true,
ageMode: true,
outcome: 'logged_in_home',
url: 'https://chatgpt.com/',
});
assert.equal(snapshot.nameValue, 'Mia Harris');
assert.equal(snapshot.ageValue, '19');
assert.equal(snapshot.clicks.filter((text) => text === '完成帐户创建').length, 3);
assert.equal(snapshot.logs.some(({ message }) => /仍停留在资料页正在重新点击/.test(message)), true);
});
test('step 5 recovers auth retry page after profile submit', async () => {
const api = new Function(`
let retryVisible = true;
let recoverCalls = 0;
const location = { href: 'https://auth.openai.com/create-account/profile' };
const document = {
querySelector() { return null; },
querySelectorAll() { return []; },
};
function throwIfStopped() {}
function log() {}
async function sleep() {}
async function humanPause() {}
function simulateClick() {}
function isVisibleElement() { return true; }
function isActionEnabled() { return true; }
function getActionText(el) { return el?.textContent || ''; }
function getSignupAuthRetryPathPatterns() { return []; }
function getAuthTimeoutErrorPageState() {
if (!retryVisible) return null;
return {
retryEnabled: true,
userAlreadyExistsBlocked: false,
maxCheckAttemptsBlocked: false,
};
}
async function recoverCurrentAuthRetryPage() {
recoverCalls += 1;
retryVisible = false;
location.href = 'https://chatgpt.com/';
}
function createSignupUserAlreadyExistsError() { return new Error('user already exists'); }
function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); }
function getStep5ErrorText() { return ''; }
function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); }
function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); }
function isOAuthConsentPage() { return false; }
function isAddPhonePageReady() { return false; }
${extractFunction('isSignupProfilePageUrl')}
${getStep5OutcomeBundle()}
return {
run() {
return waitForStep5SubmitOutcome({ timeoutMs: 1000 });
},
snapshot() {
return { recoverCalls };
},
};
`)();
const result = await api.run();
assert.deepStrictEqual(result, {
state: 'logged_in_home',
url: 'https://chatgpt.com/',
});
assert.equal(api.snapshot().recoverCalls, 1);
});
+172
View File
@@ -182,3 +182,175 @@ return {
},
});
});
test('step9 defers callback timeout while auth security verification is still redirecting', async () => {
const api = new Function('step9ModuleSource', `
const self = {};
let webNavListener = null;
let webNavCommittedListener = null;
let step8TabUpdatedListener = null;
let cleanupCalls = 0;
let deferCalls = 0;
let completePayload = null;
const callbackUrl = 'http://localhost:1455/auth/callback?code=abc&state=xyz';
const chrome = {
webNavigation: {
onBeforeNavigate: {
addListener(listener) {
webNavListener = listener;
setTimeout(() => {
if (typeof webNavListener === 'function') {
webNavListener({ tabId: 123, url: callbackUrl });
}
}, 25);
},
removeListener() {},
},
onCommitted: {
addListener(listener) {
webNavCommittedListener = listener;
},
removeListener() {},
},
},
tabs: {
onUpdated: {
addListener(listener) {
step8TabUpdatedListener = listener;
},
removeListener() {},
},
async update() {},
},
};
async function addLog() {}
function cleanupStep8NavigationListeners() {
cleanupCalls += 1;
webNavListener = null;
webNavCommittedListener = null;
step8TabUpdatedListener = null;
}
function throwIfStep8SettledOrStopped(resolved) {
if (resolved) throw new Error('already resolved');
}
async function getTabId() { return 123; }
async function isTabAlive() { return true; }
async function ensureStep8SignupPageReady() {}
async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
if (options.actionLabel === 'OAuth localhost 回调') {
return 1;
}
return defaultTimeoutMs;
}
async function shouldDeferStep9CallbackTimeout(details = {}) {
deferCalls += 1;
return Number(details.tabId) === 123;
}
async function waitForStep8Ready() {
return {
consentReady: true,
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
};
}
async function triggerStep8ContentStrategy() {}
async function waitForStep8ClickEffect() {
return {
progressed: true,
reason: 'url_changed',
url: 'https://auth.openai.com/api/oauth/oauth2/auth?client_id=app_test',
};
}
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
return Number(details?.tabId) === Number(signupTabId) ? details.url : '';
}
function getStep8CallbackUrlFromTabUpdate() { return ''; }
function getStep8EffectLabel() { return 'URL 已变化'; }
async function prepareStep8DebuggerClick() { return { rect: { centerX: 10, centerY: 10 } }; }
async function clickWithDebugger() {}
async function reloadStep8ConsentPage() {}
async function reuseOrCreateTab() { return 123; }
async function sleepWithStop() {}
function setWebNavListener(listener) { webNavListener = listener; }
function getWebNavListener() { return webNavListener; }
function setWebNavCommittedListener(listener) { webNavCommittedListener = listener; }
function getWebNavCommittedListener() { return webNavCommittedListener; }
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
function setStep8PendingReject() {}
async function completeStepFromBackground(step, payload) {
completePayload = { step, payload };
}
const STEP8_CLICK_RETRY_DELAY_MS = 1;
const STEP8_READY_WAIT_TIMEOUT_MS = 5;
const STEP8_MAX_ROUNDS = 1;
const STEP8_STRATEGIES = [
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
];
${step9ModuleSource}
const executor = self.MultiPageBackgroundStep9.createStep9Executor({
addLog,
chrome,
cleanupStep8NavigationListeners,
clickWithDebugger,
completeStepFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowStepTimeoutMs,
getStep8CallbackUrlFromNavigation,
getStep8CallbackUrlFromTabUpdate,
getStep8EffectLabel,
getTabId,
getWebNavCommittedListener,
getWebNavListener,
getStep8TabUpdatedListener,
isTabAlive,
prepareStep8DebuggerClick,
reloadStep8ConsentPage,
reuseOrCreateTab,
setStep8PendingReject,
setStep8TabUpdatedListener,
setWebNavCommittedListener,
setWebNavListener,
shouldDeferStep9CallbackTimeout,
sleepWithStop,
STEP8_CLICK_RETRY_DELAY_MS,
STEP8_MAX_ROUNDS,
STEP8_READY_WAIT_TIMEOUT_MS,
STEP8_STRATEGIES,
throwIfStep8SettledOrStopped,
triggerStep8ContentStrategy,
waitForStep8ClickEffect,
waitForStep8Ready,
});
return {
executeStep9: executor.executeStep9,
snapshot() {
return {
cleanupCalls,
deferCalls,
completePayload,
};
},
};
`)(step9ModuleSource);
await api.executeStep9({
oauthUrl: 'https://auth.openai.com/original-oauth',
visibleStep: 12,
});
const snapshot = api.snapshot();
assert.equal(snapshot.deferCalls >= 1, true);
assert.equal(snapshot.cleanupCalls >= 1, true);
assert.deepEqual(snapshot.completePayload, {
step: 12,
payload: {
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
},
});
});
+151 -1
View File
@@ -547,7 +547,12 @@ test('verification flow keeps step 8 successful when code submit transport fails
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'FILL_CODE') {
throw new Error('message channel is closed before a response was received');
}
return {};
},
sendToContentScriptResilient: async (_source, message) => {
if (message.type === 'FILL_CODE') {
throw new Error('message channel is closed before a response was received');
@@ -1381,6 +1386,151 @@ test('verification flow uses resilient signup-page transport when submitting ver
assert.ok(resilientCalls[0].options.timeoutMs >= 30000);
});
test('verification flow does not replay step 8 code submit after transient auth-page transport failure', async () => {
const directMessages = [];
const resilientMessages = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isRetryableContentScriptTransportError: (error) => /did not respond/i.test(String(error?.message || error || '')),
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
directMessages.push(message.type);
if (message.type === 'FILL_CODE') {
throw new Error('步骤 8:页面通信异常 did not respond in 30s');
}
return {};
},
sendToContentScriptResilient: async (_source, message) => {
resilientMessages.push(message.type);
if (message.type === 'GET_LOGIN_AUTH_STATE') {
return {
state: 'verification_page',
verificationErrorText: '代码不正确',
url: 'https://auth.openai.com/email-verification',
};
}
throw new Error('FILL_CODE should not be replayed through resilient transport');
},
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const result = await helpers.submitVerificationCode(8, '510725', { completionStep: 11 });
assert.deepStrictEqual(result, {
invalidCode: true,
errorText: '代码不正确',
url: 'https://auth.openai.com/email-verification',
});
assert.deepStrictEqual(directMessages, ['FILL_CODE']);
assert.deepStrictEqual(resilientMessages, ['GET_LOGIN_AUTH_STATE']);
});
test('verification flow requests a new code immediately after Cloudflare Temp Email code is rejected', async () => {
const events = [];
const pollPayloads = [];
const completed = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async (_step, payload) => {
completed.push(payload);
},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => {
pollPayloads.push(payload);
const code = pollPayloads.length === 1 ? '111111' : '222222';
events.push(['poll', code]);
return { code, emailTimestamp: pollPayloads.length };
},
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async (_source, message) => {
if (message.type === 'RESEND_VERIFICATION_CODE') {
events.push(['resend', message.step]);
return {};
}
if (message.type === 'FILL_CODE') {
events.push(['submit', message.payload.code]);
return message.payload.code === '111111'
? { invalidCode: true, errorText: '代码不正确' }
: { success: true };
}
return {};
},
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
await helpers.resolveVerificationStep(
8,
{
email: 'user@example.com',
loginVerificationRequestedAt: Date.now(),
verificationResendCount: 1,
},
{ provider: 'cloudflare-temp-email', label: 'Cloudflare Temp Email' },
{
completionStep: 11,
lastResendAt: Date.now(),
resendIntervalMs: 25000,
}
);
assert.deepStrictEqual(events, [
['poll', '111111'],
['submit', '111111'],
['resend', 8],
['poll', '222222'],
['submit', '222222'],
]);
assert.deepStrictEqual(pollPayloads[1].excludeCodes, ['111111']);
assert.equal(completed[0].code, '222222');
});
test('verification flow forwards optional signup profile payload when submitting signup verification code', async () => {
const resilientCalls = [];
+30
View File
@@ -58,6 +58,10 @@ gh auth login
10. 如果是前端改动且没有新增依赖,只提醒开发者自己测试即可;如果新增了依赖并完成安装,可以自行验证能否启动,验证后要关闭启动占用的端口,并提醒开发者重新启动。
11. PR 标题、PR 正文、PR 评论都用自然中文直接表达,不要写“自动回复”“AI 分析结果如下”这种固定机器人腔。
12. 没有开发者明确授权时,AI 不得擅自合并 PR、关闭 PR、删除远端分支。
13. 必须严格区分“同步 PR 分支”和“合并 PR 到 `dev`”:
- `git merge origin/dev``git rebase origin/dev`、把最新 `dev` 推回 PR 源分支,只是让 PR 分支吸收最新基线,不等于合并 PR。
- 只有通过 GitHub PR 合并动作,例如 `gh pr merge <PR_NUMBER> --merge`,并确认 `origin/dev` 已前进到合并提交,才算“合并到 `dev`”。
14. 如果开发者明确说“合并进 dev”“合到 dev”“通过就合”“没问题就合并”“处理完直接合并”这类表达,AI 必须执行阶段 8 的真实 PR 合并流程;不能只更新 PR 分支后声称已经合并。
## 开发者需要提供给 AI 的信息
@@ -308,6 +312,30 @@ git log --oneline HEAD..origin/dev
gh pr merge <PR_NUMBER> --merge --delete-branch
```
注意:
1. 这一步才是“把 PR 合并到 `dev`”。把 `origin/dev` 合并到功能分支、把功能分支推回 PR、让 PR 变成可合并,都不算已经合并到 `dev`
2. 如果不打算删除 PR 源分支,可以去掉 `--delete-branch`,但仍然必须执行 `gh pr merge`
3. `gh pr merge` 成功后,必须立刻验证 PR 和本地 `dev`
```powershell
gh pr view <PR_NUMBER> --json number,state,mergedAt,mergedBy,baseRefName,url,mergeCommit
git fetch origin
git switch dev
git pull --ff-only origin dev
git rev-parse HEAD origin/dev
git log --oneline -1
```
验证要求:
1. `state` 必须是 `MERGED`
2. `baseRefName` 必须是 `dev`
3. `mergeCommit.oid` 必须存在。
4. `git rev-parse HEAD origin/dev` 输出的两个提交必须一致。
5. 本地 `dev` 的最新提交必须是该 PR 的合并提交,或包含该 PR 合并结果的后续 `dev` 提交。
6. 如果本地有无关脏改动导致不能切回或快进 `dev`,必须明确告诉开发者“远端 PR 已合并,但本地 `dev` 尚未更新”,不能说本地也已经完成。
限制:
1. AI 只能把自己的 PR 合并到 `dev`
@@ -352,6 +380,7 @@ gh pr merge <PR_NUMBER> --merge --delete-branch
- PR 标题和正文与真实改动一致
- 如有评论,内容为自然中文,不用固定机器人模板
- 如果未跑测试,已明确提醒开发者测试
- 如果开发者授权合并,已确认 PR 状态为 `MERGED`,且本地 `dev``origin/dev` 指向同一提交
## 最终反馈给开发者时必须说明
@@ -366,6 +395,7 @@ AI 完成后,至少要向开发者明确反馈下面这些信息:
7. 是否改了 SQL 并同步了本地数据库
8. 是否运行过测试;如果没跑,要明确提醒开发者自行测试
9. 是否已经合并;如果已合并,要明确说明是合并到 `dev`
10. 如果已合并,必须说明 PR 状态是否为 `MERGED`、合并提交是什么、本地 `dev``origin/dev` 是否已经一致
## 一句话执行要求
+14 -10
View File
@@ -183,7 +183,7 @@
- Codex2API 配置
- IP 代理持久配置:`ipProxyEnabled`、服务商、模式、API 地址、服务商配置快照、账号列表、固定 Host / Port / Protocol / Username / Password、地区参数、session 与自动切换阈值
- Plus 模式开关 `plusModeEnabled`
- Plus 支付方式 `plusPaymentMethod`GoPay 配置 `gopayPhone / gopayPin`GPC helper 配置 `gopayHelperApiUrl / gopayHelperApiKey / gopayHelperPhoneNumber / gopayHelperOtpChannel / gopayHelperLocalSmsHelperEnabled / gopayHelperLocalSmsHelperUrl / gopayHelperPin`;其中 GPC API 地址固定归一为 `https://gpc.qlhazycoder.top`
- Plus 支付方式 `plusPaymentMethod`GoPay 配置 `gopayPhone / gopayPin`GPC helper 配置 `gopayHelperApiUrl / gopayHelperApiKey / gopayHelperPhoneMode / gopayHelperPhoneNumber / gopayHelperOtpChannel / gopayHelperLocalSmsHelperEnabled / gopayHelperLocalSmsHelperUrl / gopayHelperPin`;其中 GPC API 地址固定归一为 `https://gpc.qlhazycoder.top`
- PayPal 账号池配置 `paypalAccounts / currentPayPalAccountId`,以及供后台步骤兼容读取的 `paypalEmail / paypalPassword`
- 邮箱 provider 配置
- Cloud Mail / SkyMail API 配置:`cloudMailBaseUrl / cloudMailAdminEmail / cloudMailAdminPassword / cloudMailToken / cloudMailReceiveMailbox / cloudMailDomain / cloudMailDomains`
@@ -328,13 +328,15 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
1. 解析本轮应使用的注册方式:默认邮箱注册,开启接码且普通模式下可切到手机号注册
2. 打开或复用注册页
3. 邮箱注册时,如果注册弹窗默认停留在手机号输入模式,会先尝试点击 `继续使用电子邮件地址登录 / Continue using email address` 一类按钮切回邮箱输入模式
4. 邮箱注册分支:提交邮箱;邮箱输入框识别同时兼容本地化占位与 `aria-label`
5. 手机号注册分支:先点击官网“免费注册”并切换到手机号注册入口,必要时展开更多选项;确认手机号输入页就绪后,按“已有 signup activation > 手动填写的运行态手机号 > 接码平台新申请号码”的顺序决定手机号来源,避免重复买号。内容脚本会通过 `content/phone-country-utils.js` 共享工具按号码最长区号和平台国家名切换手机号国家下拉框,兼容 `+(44)` / `(+44)` / `+44` 等页面文案;必须确认可视下拉按钮的区号已同步后才填写并提交,若仍显示旧国家则停止提交
6. 以当前统一账号标识先写入一条“停止(流程尚未完成)”的记录占位;邮箱账号占位邮箱,phone-only 账号占位手机号
7. 等待身份提交后的真实落地页
8. 如果进入密码页,则继续执行 Step 3
9. 如果直接进入邮箱验证码页或手机验证码页,则自动跳过 Step 3 并进入 Step 4
3. 后台打开或切回注册页后,会先等待标签页完成加载并额外稳定 3 秒,再连接内容脚本探测入口;这用于避开官网按钮已在 DOM 中但仍处于隐藏态的异步窗口期
4. 内容脚本识别到官网“免费注册 / Sign up / Register”入口后,会先等待 3 秒再点击;如果点击后仍停留在官网入口页、没有进入邮箱或手机号输入页,会再次等待 3 秒后重试,最多额外重试 5 次,且每次真正点击前都会检查 Stop 状态
5. 邮箱注册时,如果注册弹窗默认停留在手机号输入模式,会先尝试点击 `继续使用电子邮件地址登录 / Continue using email address` 一类按钮切回邮箱输入模式
6. 邮箱注册分支:提交邮箱;邮箱输入框识别同时兼容本地化占位与 `aria-label`
7. 手机号注册分支:先点击官网“免费注册”并切换到手机号注册入口,必要时展开更多选项;确认手机号输入页就绪后,按“已有 signup activation > 手动填写的运行态手机号 > 接码平台新申请号码”的顺序决定手机号来源,避免重复买号。内容脚本会通过 `content/phone-country-utils.js` 共享工具按号码最长区号和平台国家名切换手机号国家下拉框,兼容 `+(44)` / `(+44)` / `+44` 等页面文案;必须确认可视下拉按钮的区号已同步后才填写并提交,若仍显示旧国家则停止提交
8. 以当前统一账号标识先写入一条“停止(流程尚未完成)”的记录占位;邮箱账号占位邮箱,phone-only 账号占位手机号
9. 等待身份提交后的真实落地页
10. 如果进入密码页,则继续执行 Step 3
11. 如果直接进入邮箱验证码页或手机验证码页,则自动跳过 Step 3 并进入 Step 4
### Step 3
@@ -486,6 +488,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
- 对 `custom` provider 而言,Step 8 仍使用手动验证码确认弹窗;弹窗当前额外提供“出现手机号验证”按钮,点击后会直接抛出与真实 `add-phone` 页面一致的 fatal 错误,供 Auto 按既有 add-phone 分支继续下一邮箱。
- 当登录验证码提交后进入 `phone-verification` 页时,内容脚本会显式把该页面识别为“手机验证码页”,避免与邮箱验证码页混淆。
- 手机号注册身份本身不会让 Step 8 自动走短信;只有真实页面状态是 `phone_verification_page` 时才调用短信 helper。手机号注册登录后出现 `add-email` 时,Step 8 走“按需生成邮箱 -> 绑定邮箱 -> 邮箱验证码”链路。
- 如果手机号注册账号在 Step 8 的 `add-email` / 邮箱验证码阶段失败并回退到 Step 7,Step 7 会优先使用当前轮保留的注册手机号身份重新登录,避免误用尚未绑定成功的邮箱登录。
- `email_in_use` 会在 Step 8 当前步骤内清理当前邮箱并重新打开 `add-email` 获取新邮箱;`max_check_attempts` 不继续点击认证页 `重试`,只恢复当前 Step 8。
### Step 9
@@ -512,6 +515,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
- 号码当前最多复用 3 次成功注册;超过上限后会清空可复用激活记录,下次重新申请新号码。
- 5sim 的国家/地区使用字符串 slug(当前开放 `indonesia / thailand / vietnam`),HeroSMS 仍使用数字国家 ID(当前开放印度尼西亚/泰国/越南)。
- 如果同一个号码在重发短信后仍收不到验证码,后台会按当前平台取消或 ban 激活并换号,而不是把当前号码无限重试下去。
- 如果注册手机号验证码页在点击 `resend` 后跳到 `contact-verification``This page isnt working / HTTP ERROR 500`,后台会把当前号码视为不可继续使用并结束当前激活,避免继续停留在坏页面上空轮询。
### Step 10
@@ -551,8 +555,8 @@ Plus 模式通过 `plusModeEnabled` 开启,目标是在普通注册资料完
Plus 模式可见步骤:
1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。
2. 第 6 步 `创建 Plus Checkout`PayPal / GoPay 打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR``https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`GoPay 使用 `ID / IDR``https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`GPC helper 模式改为把 accessToken、手机号、国家区号、`otp_channel` 提交到 `gopayHelperApiUrl``/api/gp/tasks`,并通过 `gopayHelperApiKey` 发送 `X-API-Key` 认证,创建后保存 `task_id`;当前默认 GPC API 地址为 `https://gpc.qlhazycoder.top`
3. 第 7 步 `填写账单并提交订阅`PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是轮询 `/api/gp/tasks/{task_id}`根据远端 `api_waiting_for` 依次向 `/otp``/pin` 提交验证码与 PIN。若侧栏启用本地 OTP helper,后台会轮询 `gopayHelperLocalSmsHelperUrl``/latest-otp?phone=...&consume=1` 接口,按当前 GPC 手机号读取并消费当前 OTP 通道的验证码;未开启本地 helper 时才弹出手动 OTP 输入框。仓库内置的 `scripts/gpc_sms_helper_macos.py` 是 macOS Messages/本地通知兼容读取实现,其他通道需要提供兼容的本地 `/latest-otp``/otp` 接口。
2. 第 6 步 `创建 Plus Checkout`PayPal / GoPay 打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR``https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`GoPay 使用 `ID / IDR``https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`GPC helper 模式改为把 accessToken`gopayHelperPhoneMode` 提交到 `gopayHelperApiUrl``/api/gp/tasks`,并通过 `gopayHelperApiKey` 发送 `X-API-Key` 认证;自动模式只传 `phone_mode=auto`,手动模式额外传手机号、国家区号和 `otp_channel`,创建后保存 `task_id`;当前默认 GPC API 地址为 `https://gpc.qlhazycoder.top`
3. 第 7 步 `填写账单并提交订阅`PayPal / GoPay 仍走 checkout 页面与 Stripe iframe 自动化;GPC helper 模式不再操作 checkout iframe,而是轮询 `/api/gp/tasks/{task_id}`。自动模式只等待远端 `completed` 后继续后续步骤;手动模式根据远端 `api_waiting_for` 依次向 `/otp``/pin` 提交验证码与 PIN。若侧栏启用本地 OTP helper,后台会轮询 `gopayHelperLocalSmsHelperUrl``/latest-otp?phone=...&consume=1` 接口,按当前 GPC 手机号读取并消费当前 OTP 通道的验证码;未开启本地 helper 时才弹出手动 OTP 输入框。仓库内置的 `scripts/gpc_sms_helper_macos.py` 是 macOS Messages/本地通知兼容读取实现,其他通道需要提供兼容的本地 `/latest-otp``/otp` 接口。
4. 仅 PayPal / GoPay 会继续显示第 8 步:该步按当前支付方式显示为 `PayPal 登录与授权``GoPay 手机验证与授权`,底层 step key 仍为 `paypal-approve`
5. 第 9 步 `订阅回跳确认` 仅在 PayPal / GoPay 模式下等待授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。GPC helper 模式在第 7 步任务完成后会直接进入 Plus 可见第 10 步 OAuth 登录。
6. 第 10 步:复用原 Step 7 OAuth 登录执行器,但状态和日志按 Plus 可见第 10 步记录。
+4 -4
View File
@@ -59,7 +59,7 @@
- `background/paypal-account-store.js`:PayPal 账号池持久化模块,负责保存账号列表、切换当前账号,并把当前选中账号同步回兼容字段 `paypalEmail / paypalPassword`
- `background/phone-verification-flow.js`:手机号验证码共享流程模块,负责复用 HeroSMS / 5sim / NexSMS 的取号、复用、轮询、重发、换号与收尾能力;既承接 OAuth 后置 `add-phone / phone-verification` 页面,也承接手机号注册 Step 4 和 Step 8 中真实出现的 `phone-verification` 页面,并保持 `signupPhone*` 运行态与 `currentPhoneActivation` 隔离;后置 add-phone 成功后会把已绑定手机号写入运行态 `phoneNumber`,供账号记录并入同一轮;提交后置手机号验证码时会一并透传可复用的资料页 payload,便于认证页偶发跳回资料页时继续补齐;该流程由调用方传入当前可见步骤号,手机号日志不会再固定显示普通模式 Step 9。
- `background/panel-bridge.js`:来源桥接层;CPA / SUB2API 继续封装页面打开、脚本注入和通信,Codex2API 则直接通过后台协议生成 OAuth 地址。
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
- `background/signup-flow-helpers.js`:注册页辅助层,负责打开注册入口、Step 2 打开入口页后的加载完成与额外 3 秒稳定等待、等待密码页以及解析当前流程所用邮箱;在 Gmail 与 `2925 + provide` 模式下会优先复用已经存在且仍兼容的完整注册邮箱,只有不兼容或为空时才重新生成;当 provider 为 2925 且启用了 provide 模式下的号池时,会先确保账号池中已选中可用账号。
- `background/tab-runtime.js`:标签页与内容脚本运行时基础设施,封装标签注册、冲突清理、消息超时、注入重试与队列;内容脚本恢复等待日志支持 `logStep / logStepKey`,用于保持 Plus 复用步骤的日志标签正确;当前等待标签完成、等待标签完成并短暂稳定、注入后的短暂延迟和内容脚本重试等待都已做 Stop 感知,避免用户停止后后台还继续等待并恢复执行。
- `background/verification-flow.js`:注册/登录验证码共享流程层,封装重发、轮询、提交、失败回退、自定义邮箱跳过、共享验证码自动重发次数配置、Cloud Mail API 轮询以及 2925 长轮询参数;日志步骤号使用 `completionStep`,因此 Plus 登录验证码会显示可见第 11 步而不是内部复用的 Step 8;当前验证码提交重试上限为 15 次,2925 每次重发验证码之间都会固定跑完一轮 15 次邮箱刷新轮询;若 `mail2925Mode = receive`,会额外把目标注册邮箱传给 2925 内容脚本做弱匹配;若登录验证码提交后页面转入 `add-phone / 手机号页`,则会把“后续需要手机号验证”的结果继续传给 OAuth 确认步骤,而不是误判为普通失败;若登录验证码提交后通信中断但认证页已进入 OAuth 同意页或手机号验证页,会按真实页面状态继续后续流程;若 2925 轮询命中“子邮箱已达上限邮箱”,会转入 2925 会话模块执行“记录时间、禁用 24 小时、切下一个账号并重新登录”,然后直接结束当前尝试。
@@ -79,7 +79,7 @@
- `background/steps/platform-verify.js`:平台回调验证实现,普通模式为步骤 10,Plus 模式为可见步骤 13;负责 CPA / SUB2API 回调验证,以及 Codex2API 的协议式 callback code/state 交换,所有平台验证日志和完成信号都按当前可见步骤上报。
- `background/steps/plus-return-confirm.js`:Plus 模式第 9 步实现,负责等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒再完成。
- `background/steps/registry.js`:步骤注册表工厂,负责用稳定的步骤元数据映射到执行器。
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责按 `signupMethod` 在邮箱注册与手机号注册之间分发;邮箱分支会先点击官网“免费注册”再提交邮箱并清理旧手机号注册运行态,手机号分支会先点击官网“免费注册”并切到手机号注册入口,确认手机号输入页就绪后优先复用已有 signup activation,其次使用手动运行态手机号,最后才申请接码平台号码并提交,最后根据落地页判断是否跳过后续密码步骤。
- `background/steps/submit-signup-email.js`:步骤 2 实现,负责按 `signupMethod` 在邮箱注册与手机号注册之间分发;切回已有注册页标签时会先等待加载完成并额外稳定 3 秒,再检查注册入口状态;邮箱分支会先点击官网“免费注册”再提交邮箱并清理旧手机号注册运行态,手机号分支会先点击官网“免费注册”并切到手机号注册入口,确认手机号输入页就绪后优先复用已有 signup activation,其次使用手动运行态手机号,最后才申请接码平台号码并提交,最后根据落地页判断是否跳过后续密码步骤。
## `content/`
@@ -96,7 +96,7 @@
- `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。
- `content/plus-checkout.js`ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。
- `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号国家下拉框同步校验、手机号填写与提交;Step 3 收尾会识别手机号注册密码页的 `Incorrect phone number or password` 并上报当前轮重开信号;Step 7 / 8 登录链路会额外识别手机号输入页、手机号登录入口、`phone-verification` 与真实 `add-email` 页面,避免把手机验证码页或添加邮箱页误判成普通邮箱登录入口;Step 9 后置手机号验证完成后若偶发进入资料页,会复用 Step 5 的姓名生日填写逻辑再继续等待 OAuth 同意页。
- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、官网注册入口点击前固定等待 3 秒、入口点击失败后最多 5 次重试、手机号国家下拉框同步校验、手机号填写与提交;Step 3 收尾会识别手机号注册密码页的 `Incorrect phone number or password` 并上报当前轮重开信号;Step 7 / 8 登录链路会额外识别手机号输入页、手机号登录入口、`phone-verification` 与真实 `add-email` 页面,避免把手机验证码页或添加邮箱页误判成普通邮箱登录入口;Step 9 后置手机号验证完成后若偶发进入资料页,会复用 Step 5 的姓名生日填写逻辑再继续等待 OAuth 同意页。
- `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;平台验证请求会读取 `visibleStep`,普通模式承接步骤 10,Plus 模式承接步骤 13。
- `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。
- `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10 与 Plus 可见步骤 13。
@@ -249,7 +249,7 @@
- `tests/sidepanel-mail2925-mode.test.js`:测试侧边栏保留 `2925 provide / receive` 模式行与独立的号池配置行,并验证 sidepanel 只在 provide 模式下把 2925 视为别名邮箱 provider。
- `tests/sidepanel-paypal-manager.test.js`:测试侧边栏公共表单弹窗脚本与 PayPal manager 的加载顺序,以及 PayPal 账号保存后会立即选中当前账号。
- `tests/signup-entry-diagnostics.test.js`:测试注册入口与密码页诊断快照输出,包括密码页错误文案字段。
- `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式、本地化邮箱输入框识别,以及手机号注册时国家下拉框可视区号同步。
- `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式、本地化邮箱输入框识别、官网注册入口点击等待与最多 5 次重试,以及手机号注册时国家下拉框可视区号同步。
- `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。
- `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。
- `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。