Merge origin/dev into PR 225 review fix
This commit is contained in:
+123
-3
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
|
||||
function createPlusCheckoutCreateExecutor(deps = {}) {
|
||||
const {
|
||||
@@ -86,6 +88,17 @@
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
function normalizeGpcHelperPhoneMode(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
|
||||
return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
|
||||
? GPC_HELPER_PHONE_MODE_AUTO
|
||||
: GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
|
||||
function normalizeGpcOtpChannel(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcOtpChannel) {
|
||||
@@ -143,6 +156,17 @@
|
||||
return buildGpcHelperApiUrl(apiUrl, '/api/gp/tasks');
|
||||
}
|
||||
|
||||
function buildGpcBalanceUrl(apiUrl = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.buildGpcApiKeyBalanceUrl) {
|
||||
return rootScope.GoPayUtils.buildGpcApiKeyBalanceUrl(apiUrl);
|
||||
}
|
||||
if (rootScope.GoPayUtils?.buildGpcCardBalanceUrl) {
|
||||
return rootScope.GoPayUtils.buildGpcCardBalanceUrl(apiUrl);
|
||||
}
|
||||
return buildGpcHelperApiUrl(apiUrl, '/api/gp/balance');
|
||||
}
|
||||
|
||||
function unwrapGpcResponse(payload = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.unwrapGpcResponse) {
|
||||
@@ -176,6 +200,55 @@
|
||||
return payload?.data?.detail || payload?.detail || payload?.message || payload?.error || `HTTP ${status || 0}`;
|
||||
}
|
||||
|
||||
function getGpcRemainingUses(payload = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.getGpcBalanceRemainingUses) {
|
||||
return rootScope.GoPayUtils.getGpcBalanceRemainingUses(payload);
|
||||
}
|
||||
const data = unwrapGpcResponse(payload);
|
||||
const numeric = Number(data?.remaining_uses ?? data?.remainingUses ?? data?.balance ?? data?.remaining);
|
||||
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : null;
|
||||
}
|
||||
|
||||
function isGpcAutoModeEnabled(payload = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.isGpcAutoModeEnabled) {
|
||||
return rootScope.GoPayUtils.isGpcAutoModeEnabled(payload);
|
||||
}
|
||||
const data = unwrapGpcResponse(payload);
|
||||
return data?.auto_mode_enabled === true || data?.autoModeEnabled === true;
|
||||
}
|
||||
|
||||
async function assertGpcApiKeyReadyForCreate(state = {}, phoneMode = GPC_HELPER_PHONE_MODE_MANUAL, apiKey = '') {
|
||||
const apiUrl = buildGpcBalanceUrl(state?.gopayHelperApiUrl);
|
||||
if (!apiUrl) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
|
||||
}
|
||||
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-API-Key': apiKey,
|
||||
},
|
||||
}, 30000);
|
||||
if (!response?.ok || !isGpcUnifiedResponseOk(data)) {
|
||||
const detail = getGpcResponseErrorDetail(data, response?.status || 0);
|
||||
throw new Error(`创建 GPC 订单失败:API Key 校验失败:${detail}`);
|
||||
}
|
||||
const balanceData = unwrapGpcResponse(data);
|
||||
const remainingUses = getGpcRemainingUses(balanceData);
|
||||
const status = String(balanceData?.status || balanceData?.card_status || balanceData?.cardStatus || '').trim().toLowerCase();
|
||||
if (status && status !== 'active') {
|
||||
throw new Error(`创建 GPC 订单失败:API Key 状态不可用(${status})。`);
|
||||
}
|
||||
if (remainingUses !== null && remainingUses <= 0) {
|
||||
throw new Error('创建 GPC 订单失败:API Key 剩余次数不足。');
|
||||
}
|
||||
if (phoneMode === GPC_HELPER_PHONE_MODE_AUTO && !isGpcAutoModeEnabled(balanceData)) {
|
||||
throw new Error('创建 GPC 订单失败:当前 GPC API Key 未开通自动模式。');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
|
||||
const fetcher = typeof fetchImpl === 'function'
|
||||
? fetchImpl
|
||||
@@ -184,11 +257,34 @@
|
||||
throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。');
|
||||
}
|
||||
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||||
const timer = controller ? setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 30000)) : null;
|
||||
const effectiveTimeoutMs = Math.max(1000, Number(timeoutMs) || 30000);
|
||||
let didTimeout = false;
|
||||
let timer = null;
|
||||
const buildTimeoutError = () => new Error(`GPC API 请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`);
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
didTimeout = true;
|
||||
reject(buildTimeoutError());
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
}
|
||||
}, effectiveTimeoutMs);
|
||||
});
|
||||
try {
|
||||
const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) });
|
||||
const data = await response.json().catch(() => ({}));
|
||||
const response = await Promise.race([
|
||||
fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) }),
|
||||
timeoutPromise,
|
||||
]);
|
||||
const data = await Promise.race([
|
||||
response.json().catch(() => ({})),
|
||||
timeoutPromise,
|
||||
]);
|
||||
return { response, data };
|
||||
} catch (error) {
|
||||
if (didTimeout || error?.name === 'AbortError') {
|
||||
throw buildTimeoutError();
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
@@ -226,25 +322,31 @@
|
||||
if (!apiUrl) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
|
||||
}
|
||||
const phoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode);
|
||||
const isAutoMode = phoneMode === GPC_HELPER_PHONE_MODE_AUTO;
|
||||
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim();
|
||||
const countryCode = normalizeHelperCountryCode(state?.gopayHelperCountryCode || '86');
|
||||
const pin = String(state?.gopayHelperPin || '').trim();
|
||||
const apiKey = resolveGpcHelperApiKey(state);
|
||||
if (!phoneNumber) {
|
||||
throw new Error('创建 GPC 订单失败:缺少手机号。');
|
||||
if (!isAutoMode && !phoneNumber) {
|
||||
throw new Error('创建 GPC 订单失败:手动模式缺少手机号。');
|
||||
}
|
||||
if (!pin) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 PIN。');
|
||||
if (!isAutoMode && !pin) {
|
||||
throw new Error('创建 GPC 订单失败:手动模式缺少 PIN。');
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
await assertGpcApiKeyReadyForCreate(state, phoneMode, apiKey);
|
||||
throwIfStopped();
|
||||
const payload = {
|
||||
access_token: token,
|
||||
phone_mode: 'manual',
|
||||
country_code: countryCode,
|
||||
phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode),
|
||||
otp_channel: normalizeGpcOtpChannel(state?.gopayHelperOtpChannel),
|
||||
phone_mode: phoneMode,
|
||||
};
|
||||
if (!isAutoMode) {
|
||||
payload.country_code = countryCode;
|
||||
payload.phone_number = normalizeHelperPhoneNumber(phoneNumber, countryCode);
|
||||
payload.otp_channel = normalizeGpcOtpChannel(state?.gopayHelperOtpChannel);
|
||||
}
|
||||
|
||||
const orderCreatedAt = Date.now();
|
||||
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
|
||||
@@ -273,6 +375,7 @@
|
||||
remoteStage: String(taskData?.remote_stage || taskData?.remoteStage || '').trim(),
|
||||
orderCreatedAt,
|
||||
responsePayload: taskData && typeof taskData === 'object' && !Array.isArray(taskData) ? taskData : null,
|
||||
phoneMode: normalizeGpcHelperPhoneMode(taskData?.phone_mode || taskData?.phoneMode || phoneMode),
|
||||
country: 'ID',
|
||||
currency: 'IDR',
|
||||
checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||
@@ -308,6 +411,7 @@
|
||||
gopayHelperTaskStatus: result.taskStatus,
|
||||
gopayHelperStatusText: result.statusText,
|
||||
gopayHelperRemoteStage: result.remoteStage,
|
||||
gopayHelperPhoneMode: result.phoneMode || normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode),
|
||||
gopayHelperTaskPayload: result.responsePayload,
|
||||
gopayHelperReferenceId: '',
|
||||
gopayHelperGoPayGuid: '',
|
||||
@@ -318,7 +422,7 @@
|
||||
gopayHelperStartPayload: null,
|
||||
gopayHelperOrderCreatedAt: result.orderCreatedAt || Date.now(),
|
||||
});
|
||||
await addLog(`步骤 6:GPC 任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
|
||||
await addLog(`步骤 6:GPC ${result.phoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式任务已创建(task_id: ${result.taskId}),准备继续下一步。`, 'info');
|
||||
await completeStepFromBackground(6, {
|
||||
plusCheckoutCountry: result.country || 'ID',
|
||||
plusCheckoutCurrency: result.currency || 'IDR',
|
||||
|
||||
@@ -11,7 +11,28 @@
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const GPC_TASK_POLL_INTERVAL_MS = 3000;
|
||||
const GPC_TASK_STALE_STATUS_TIMEOUT_MS = 60000;
|
||||
const GPC_REMOTE_STAGE_LABELS = {
|
||||
auto_otp_wait: '等待自动 OTP',
|
||||
checkout_order_start: '创建订单',
|
||||
checkout_start: '创建订单',
|
||||
completed: '充值完成',
|
||||
gopay_validate_pin: '校验 PIN',
|
||||
otp_ready: '等待 PIN',
|
||||
otp_submitted_local: 'OTP 已提交',
|
||||
payment_processing: '支付处理中',
|
||||
pin_submitted_local: 'PIN 已提交',
|
||||
sms_otp_wait: '等待短信 OTP',
|
||||
whatsapp_otp_wait: '等待 WhatsApp OTP',
|
||||
};
|
||||
const GPC_WAITING_FOR_LABELS = {
|
||||
auto_otp: '自动 OTP',
|
||||
otp: 'OTP',
|
||||
pin: 'PIN',
|
||||
};
|
||||
const PAYMENT_METHOD_CONFIGS = {
|
||||
[PLUS_PAYMENT_METHOD_PAYPAL]: {
|
||||
id: PLUS_PAYMENT_METHOD_PAYPAL,
|
||||
@@ -112,6 +133,56 @@
|
||||
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
}
|
||||
|
||||
function normalizeGpcHelperPhoneMode(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcHelperPhoneMode) {
|
||||
return rootScope.GoPayUtils.normalizeGpcHelperPhoneMode(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === GPC_HELPER_PHONE_MODE_AUTO || normalized === 'builtin'
|
||||
? GPC_HELPER_PHONE_MODE_AUTO
|
||||
: GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
|
||||
function formatGpcRemoteStageLabel(stage = '') {
|
||||
const normalized = String(stage || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
return GPC_REMOTE_STAGE_LABELS[normalized] || normalized;
|
||||
}
|
||||
|
||||
function formatGpcWaitingForLabel(waitingFor = '') {
|
||||
const normalized = String(waitingFor || '').trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
return GPC_WAITING_FOR_LABELS[normalized] || normalized.toUpperCase();
|
||||
}
|
||||
|
||||
function formatGpcTaskStatusLog(task = {}) {
|
||||
const statusText = String(task?.status_text || task?.statusText || '').trim();
|
||||
const status = String(task?.status || '').trim();
|
||||
const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim();
|
||||
const stageText = formatGpcRemoteStageLabel(remoteStage);
|
||||
const waitingForText = formatGpcWaitingForLabel(task?.api_waiting_for || task?.apiWaitingFor || '');
|
||||
const mainText = stageText || statusText || status || '处理中';
|
||||
const parts = [`步骤 7:GPC 任务状态:${mainText}`];
|
||||
if (waitingForText && !mainText.includes(waitingForText)) {
|
||||
parts.push(`,等待 ${waitingForText}`);
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
function getGpcHelperPhoneMode(state = {}, task = null) {
|
||||
return normalizeGpcHelperPhoneMode(
|
||||
task?.phone_mode
|
||||
|| task?.phoneMode
|
||||
|| state?.gopayHelperPhoneMode
|
||||
|| state?.phoneMode
|
||||
);
|
||||
}
|
||||
|
||||
function getPaymentMethodConfig(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
return PAYMENT_METHOD_CONFIGS[normalizePlusPaymentMethod(method)] || PAYMENT_METHOD_CONFIGS[PLUS_PAYMENT_METHOD_PAYPAL];
|
||||
}
|
||||
@@ -139,11 +210,34 @@
|
||||
throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。');
|
||||
}
|
||||
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||||
const timer = controller ? setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 30000)) : null;
|
||||
const effectiveTimeoutMs = Math.max(1000, Number(timeoutMs) || 30000);
|
||||
let didTimeout = false;
|
||||
let timer = null;
|
||||
const buildTimeoutError = () => new Error(`GPC API 请求超时(>${Math.round(effectiveTimeoutMs / 1000)} 秒):${url}`);
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
didTimeout = true;
|
||||
reject(buildTimeoutError());
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
}
|
||||
}, effectiveTimeoutMs);
|
||||
});
|
||||
try {
|
||||
const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) });
|
||||
const data = await response.json().catch(() => ({}));
|
||||
const response = await Promise.race([
|
||||
fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) }),
|
||||
timeoutPromise,
|
||||
]);
|
||||
const data = await Promise.race([
|
||||
response.json().catch(() => ({})),
|
||||
timeoutPromise,
|
||||
]);
|
||||
return { response, data };
|
||||
} catch (error) {
|
||||
if (didTimeout || error?.name === 'AbortError') {
|
||||
throw buildTimeoutError();
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
@@ -299,7 +393,7 @@
|
||||
task.task_id = String(task.task_id || task.taskId || '').trim();
|
||||
task.status = String(task.status || '').trim().toLowerCase();
|
||||
task.status_text = String(task.status_text || task.statusText || '').trim();
|
||||
task.phone_mode = String(task.phone_mode || task.phoneMode || '').trim().toLowerCase();
|
||||
task.phone_mode = normalizeGpcHelperPhoneMode(task.phone_mode || task.phoneMode || '');
|
||||
task.remote_stage = String(task.remote_stage || task.remoteStage || '').trim().toLowerCase();
|
||||
task.api_waiting_for = String(task.api_waiting_for || task.apiWaitingFor || '').trim().toLowerCase();
|
||||
task.api_input_deadline_at = String(task.api_input_deadline_at || task.apiInputDeadlineAt || '').trim();
|
||||
@@ -318,6 +412,7 @@
|
||||
gopayHelperTaskId: task.task_id,
|
||||
gopayHelperTaskStatus: task.status,
|
||||
gopayHelperStatusText: task.status_text,
|
||||
gopayHelperPhoneMode: task.phone_mode,
|
||||
gopayHelperRemoteStage: task.remote_stage,
|
||||
gopayHelperApiWaitingFor: task.api_waiting_for,
|
||||
gopayHelperApiInputDeadlineAt: task.api_input_deadline_at,
|
||||
@@ -675,12 +770,16 @@
|
||||
});
|
||||
}
|
||||
|
||||
function isGpcTaskOtpWait(task = {}) {
|
||||
return task?.phone_mode === 'manual' && task?.api_waiting_for === 'otp';
|
||||
function isGpcTaskManualMode(task = {}, state = {}) {
|
||||
return getGpcHelperPhoneMode(state, task) === GPC_HELPER_PHONE_MODE_MANUAL;
|
||||
}
|
||||
|
||||
function isGpcTaskPinWait(task = {}) {
|
||||
return task?.phone_mode === 'manual'
|
||||
function isGpcTaskOtpWait(task = {}, state = {}) {
|
||||
return isGpcTaskManualMode(task, state) && task?.api_waiting_for === 'otp';
|
||||
}
|
||||
|
||||
function isGpcTaskPinWait(task = {}, state = {}) {
|
||||
return isGpcTaskManualMode(task, state)
|
||||
&& (task?.api_waiting_for === 'pin' || task?.status === 'otp_ready');
|
||||
}
|
||||
|
||||
@@ -688,6 +787,49 @@
|
||||
return ['completed', 'failed', 'expired', 'discarded'].includes(String(status || '').trim().toLowerCase());
|
||||
}
|
||||
|
||||
function buildGpcTaskProgressSignature(task = {}) {
|
||||
return [
|
||||
task?.status,
|
||||
task?.status_text,
|
||||
task?.remote_stage,
|
||||
task?.api_waiting_for,
|
||||
task?.last_input_error,
|
||||
task?.otp_invalid_count,
|
||||
task?.reference_id || task?.referenceId,
|
||||
task?.redirect_url || task?.redirectUrl,
|
||||
task?.flow_id || task?.flowId,
|
||||
task?.challenge_id || task?.challengeId,
|
||||
task?.gopay_guid || task?.gopayGuid,
|
||||
].map((value) => String(value ?? '').trim()).join('|');
|
||||
}
|
||||
|
||||
function shouldWatchGpcTaskProgress(task = {}, state = {}) {
|
||||
if (!task || isGpcTaskTerminal(task.status)) {
|
||||
return false;
|
||||
}
|
||||
if (isGpcTaskOtpWait(task, state) || isGpcTaskPinWait(task, state)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getGpcTaskStaleStatusTimeoutMs(state = {}) {
|
||||
const configuredSeconds = Number(state?.gopayHelperTaskStaleSeconds);
|
||||
if (Number.isFinite(configuredSeconds) && configuredSeconds > 0) {
|
||||
return Math.max(15000, Math.min(600000, Math.floor(configuredSeconds * 1000)));
|
||||
}
|
||||
return GPC_TASK_STALE_STATUS_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
function buildGpcTaskStaleStatusError(task = {}, staleTimeoutMs = GPC_TASK_STALE_STATUS_TIMEOUT_MS) {
|
||||
const seconds = Math.max(1, Math.round(staleTimeoutMs / 1000));
|
||||
const label = formatGpcRemoteStageLabel(task?.remote_stage)
|
||||
|| task?.status_text
|
||||
|| task?.status
|
||||
|| '未知状态';
|
||||
return new Error(`GPC_TASK_ENDED::GPC 任务状态超过 ${seconds} 秒无进展(${label}),请重新创建任务。`);
|
||||
}
|
||||
|
||||
function buildGpcTaskTerminalError(task = {}) {
|
||||
const status = String(task?.status || '').trim().toLowerCase();
|
||||
const remoteStage = String(task?.remote_stage || task?.remoteStage || '').trim();
|
||||
@@ -829,6 +971,9 @@
|
||||
let lastSubmittedOtp = '';
|
||||
let pinSubmitted = false;
|
||||
let terminalReached = false;
|
||||
let lastProgressSignature = '';
|
||||
let lastProgressAt = Date.now();
|
||||
const staleStatusTimeoutMs = getGpcTaskStaleStatusTimeoutMs(state);
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('步骤 7:GPC 模式缺少 task_id,请先执行步骤 6。');
|
||||
@@ -840,27 +985,25 @@
|
||||
throw new Error('步骤 7:GPC 模式缺少 API Key。');
|
||||
}
|
||||
|
||||
const configuredPhoneMode = normalizeGpcHelperPhoneMode(state?.gopayHelperPhoneMode || state?.phoneMode || GPC_HELPER_PHONE_MODE_MANUAL);
|
||||
const rawPin = String(state?.gopayHelperPin || '').trim();
|
||||
const pinDigits = rawPin.replace(/[^\d]/g, '');
|
||||
const pin = normalizeSixDigitPin(rawPin);
|
||||
if (!pin) {
|
||||
if (configuredPhoneMode === GPC_HELPER_PHONE_MODE_MANUAL && !pin) {
|
||||
if (taskId && apiUrl && apiKey) {
|
||||
await stopGpcTaskBestEffort(apiUrl, taskId, apiKey, 'PIN 配置错误');
|
||||
}
|
||||
throw new Error(pinDigits
|
||||
? '步骤 7:GPC PIN 必须是 6 位数字,请检查侧边栏配置。'
|
||||
: '步骤 7:GPC 模式缺少 PIN 配置。');
|
||||
: '步骤 7:GPC 手动模式缺少 PIN 配置。');
|
||||
}
|
||||
|
||||
await addLog(`步骤 7:GPC 模式开始轮询任务(task_id: ${taskId})...`, 'info');
|
||||
await addLog(`步骤 7:GPC ${configuredPhoneMode === GPC_HELPER_PHONE_MODE_AUTO ? '自动' : '手动'}模式开始轮询任务(task_id: ${taskId})...`, 'info');
|
||||
try {
|
||||
while (Date.now() <= deadline) {
|
||||
throwIfStopped();
|
||||
const task = await fetchGpcTaskStatus(apiUrl, taskId, apiKey);
|
||||
const statusText = task?.status_text || task?.status || '处理中';
|
||||
const remoteStage = task?.remote_stage || '';
|
||||
const waitingFor = task?.api_waiting_for || '';
|
||||
await addLog(`步骤 7:GPC 任务状态:${statusText}${remoteStage ? `(${remoteStage})` : ''}${waitingFor ? `,等待 ${waitingFor.toUpperCase()}` : ''}`, 'info');
|
||||
await addLog(formatGpcTaskStatusLog(task), 'info');
|
||||
|
||||
if (task.status === 'completed') {
|
||||
terminalReached = true;
|
||||
@@ -879,7 +1022,21 @@
|
||||
throw buildGpcTaskEndedError(task, 'GPC 任务已结束,请重新创建任务。');
|
||||
}
|
||||
|
||||
if (isGpcTaskOtpWait(task)) {
|
||||
if (shouldWatchGpcTaskProgress(task, state)) {
|
||||
const progressSignature = buildGpcTaskProgressSignature(task);
|
||||
const now = Date.now();
|
||||
if (progressSignature && progressSignature !== lastProgressSignature) {
|
||||
lastProgressSignature = progressSignature;
|
||||
lastProgressAt = now;
|
||||
} else if (progressSignature && now - lastProgressAt >= staleStatusTimeoutMs) {
|
||||
throw buildGpcTaskStaleStatusError(task, staleStatusTimeoutMs);
|
||||
}
|
||||
} else {
|
||||
lastProgressSignature = '';
|
||||
lastProgressAt = Date.now();
|
||||
}
|
||||
|
||||
if (isGpcTaskOtpWait(task, state)) {
|
||||
if (isGpcTaskInputDeadlineExpired(task)) {
|
||||
throw buildGpcInputDeadlineError(task, 'OTP');
|
||||
}
|
||||
@@ -940,7 +1097,7 @@
|
||||
otpLastSubmittedAt = Date.now();
|
||||
lastSubmittedOtp = normalizedOtp;
|
||||
await addLog('步骤 7:OTP 已提交,继续等待 GPC 任务状态更新。', 'ok');
|
||||
} else if (isGpcTaskPinWait(task) && !pinSubmitted) {
|
||||
} else if (isGpcTaskPinWait(task, state) && !pinSubmitted) {
|
||||
if (isGpcTaskInputDeadlineExpired(task)) {
|
||||
throw buildGpcInputDeadlineError(task, 'PIN');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
Binary file not shown.
@@ -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
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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 === '步骤 7:GPC 任务状态:等待 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 === '步骤 7:GPC 任务状态:等待自动 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 === '步骤 7:GPC 任务状态:创建订单'), 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 === '步骤 7:GPC 任务状态:创建订单'), true);
|
||||
});
|
||||
|
||||
test('GPC billing reads SMS OTP from local helper for sms_otp_wait', async () => {
|
||||
const fetchCalls = [];
|
||||
let pollCount = 0;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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"/);
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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()}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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
@@ -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 isn’t 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
@@ -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 在提交前先完成上报,并保留延迟提交回调的可执行性验证。
|
||||
|
||||
Reference in New Issue
Block a user