Merge origin/dev into PR 225 review fix
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user