fix: resolve step 9 profile fallback with latest dev
This commit is contained in:
@@ -90,8 +90,99 @@
|
||||
return '流程失败';
|
||||
}
|
||||
|
||||
function buildRecordId(email = '') {
|
||||
return String(email || '').trim().toLowerCase();
|
||||
function normalizeAccountIdentifierType(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email';
|
||||
}
|
||||
|
||||
function normalizeAccountIdentifierValue(value = '', identifierType = 'email') {
|
||||
const normalizedValue = String(value || '').trim();
|
||||
if (!normalizedValue) {
|
||||
return '';
|
||||
}
|
||||
return normalizeAccountIdentifierType(identifierType) === 'phone'
|
||||
? normalizedValue
|
||||
: normalizedValue.toLowerCase();
|
||||
}
|
||||
|
||||
function getActivationPhoneNumber(activation = null) {
|
||||
if (!activation || typeof activation !== 'object' || Array.isArray(activation)) {
|
||||
return '';
|
||||
}
|
||||
return String(
|
||||
activation.phoneNumber
|
||||
?? activation.number
|
||||
?? activation.phone
|
||||
?? ''
|
||||
).trim();
|
||||
}
|
||||
|
||||
function resolveStatePhoneNumber(state = {}) {
|
||||
const identifierType = String(state?.accountIdentifierType || '').trim().toLowerCase();
|
||||
const accountIdentifierPhone = identifierType === 'phone'
|
||||
? String(state?.accountIdentifier || '').trim()
|
||||
: '';
|
||||
|
||||
return String(
|
||||
state?.phoneNumber
|
||||
|| state?.signupPhoneNumber
|
||||
|| accountIdentifierPhone
|
||||
|| getActivationPhoneNumber(state?.signupPhoneCompletedActivation)
|
||||
|| getActivationPhoneNumber(state?.signupPhoneActivation)
|
||||
|| getActivationPhoneNumber(state?.currentPhoneActivation)
|
||||
|| ''
|
||||
).trim();
|
||||
}
|
||||
|
||||
function normalizePhoneRecordKey(value = '') {
|
||||
const rawValue = String(value || '').trim();
|
||||
const digits = rawValue.replace(/\D+/g, '');
|
||||
return digits || rawValue.toLowerCase();
|
||||
}
|
||||
|
||||
function resolveRecordIdentity(record = {}) {
|
||||
const rawEmail = String(record.email || '').trim().toLowerCase();
|
||||
const rawPhoneNumber = String(record.phoneNumber ?? record.phone ?? record.number ?? '').trim();
|
||||
const rawIdentifierType = String(record.accountIdentifierType || '').trim().toLowerCase();
|
||||
const inferredIdentifierType = rawIdentifierType === 'phone'
|
||||
? 'phone'
|
||||
: (rawIdentifierType === 'email'
|
||||
? 'email'
|
||||
: ((!rawEmail && rawPhoneNumber) ? 'phone' : 'email'));
|
||||
const rawAccountIdentifier = String(
|
||||
record.accountIdentifier
|
||||
|| (inferredIdentifierType === 'phone' ? rawPhoneNumber : rawEmail)
|
||||
|| ''
|
||||
).trim();
|
||||
const accountIdentifierType = rawAccountIdentifier
|
||||
? normalizeAccountIdentifierType(inferredIdentifierType)
|
||||
: (rawEmail ? 'email' : (rawPhoneNumber ? 'phone' : ''));
|
||||
const accountIdentifier = normalizeAccountIdentifierValue(
|
||||
rawAccountIdentifier || (accountIdentifierType === 'phone' ? rawPhoneNumber : rawEmail),
|
||||
accountIdentifierType || inferredIdentifierType
|
||||
);
|
||||
const email = rawEmail || (accountIdentifierType === 'email' ? accountIdentifier : '');
|
||||
const phoneNumber = rawPhoneNumber || (accountIdentifierType === 'phone' ? accountIdentifier : '');
|
||||
|
||||
return {
|
||||
email,
|
||||
phoneNumber,
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRecordId(identifier = '', identifierType = 'email') {
|
||||
const normalizedIdentifierType = normalizeAccountIdentifierType(identifierType);
|
||||
const normalizedIdentifier = normalizeAccountIdentifierValue(identifier, normalizedIdentifierType);
|
||||
if (!normalizedIdentifier) {
|
||||
return '';
|
||||
}
|
||||
if (normalizedIdentifierType === 'phone' && /^phone:/i.test(normalizedIdentifier)) {
|
||||
return normalizedIdentifier.toLowerCase();
|
||||
}
|
||||
return normalizedIdentifierType === 'phone'
|
||||
? `phone:${normalizedIdentifier.toLowerCase()}`
|
||||
: normalizedIdentifier;
|
||||
}
|
||||
|
||||
function normalizeSource(value = '') {
|
||||
@@ -140,11 +231,15 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
const email = String(record.email || '').trim();
|
||||
const password = String(record.password || '').trim();
|
||||
const identity = resolveRecordIdentity(record);
|
||||
const email = identity.email;
|
||||
const phoneNumber = identity.phoneNumber;
|
||||
const accountIdentifierType = identity.accountIdentifierType;
|
||||
const accountIdentifier = identity.accountIdentifier;
|
||||
const password = String(record.password ?? '').trim();
|
||||
const finalStatus = normalizeFinalStatus(record.finalStatus || record.status || '');
|
||||
|
||||
if (!email || !password || !finalStatus) {
|
||||
if (!accountIdentifier || !finalStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -167,8 +262,11 @@
|
||||
const rawFailureLabel = String(record.failureLabel || '').trim();
|
||||
|
||||
return {
|
||||
recordId: String(record.recordId || '').trim() || buildRecordId(email),
|
||||
recordId: String(record.recordId || '').trim() || buildRecordId(accountIdentifier, accountIdentifierType),
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
email,
|
||||
phoneNumber,
|
||||
password,
|
||||
finalStatus,
|
||||
finishedAt,
|
||||
@@ -215,11 +313,20 @@
|
||||
}
|
||||
|
||||
function buildAccountRunHistoryRecord(state = {}, status = '', reason = '') {
|
||||
const email = String(state.email || '').trim();
|
||||
const password = String(state.password || state.customPassword || '').trim() || '无';
|
||||
const identity = resolveRecordIdentity({
|
||||
accountIdentifierType: state.accountIdentifierType,
|
||||
accountIdentifier: state.accountIdentifier,
|
||||
email: state.email,
|
||||
phoneNumber: resolveStatePhoneNumber(state),
|
||||
});
|
||||
const email = identity.email;
|
||||
const phoneNumber = identity.phoneNumber;
|
||||
const accountIdentifierType = identity.accountIdentifierType;
|
||||
const accountIdentifier = identity.accountIdentifier;
|
||||
const password = String(state.password || state.customPassword || '').trim();
|
||||
const finalStatus = normalizeFinalStatus(status);
|
||||
|
||||
if (!email || !finalStatus) {
|
||||
if (!accountIdentifier || !finalStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -233,8 +340,11 @@
|
||||
const finishedAt = new Date().toISOString();
|
||||
|
||||
return {
|
||||
recordId: buildRecordId(email),
|
||||
recordId: buildRecordId(accountIdentifier, accountIdentifierType),
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
email,
|
||||
phoneNumber,
|
||||
password,
|
||||
finalStatus,
|
||||
finishedAt,
|
||||
@@ -257,10 +367,23 @@
|
||||
|
||||
const recordId = String(record.recordId || '').trim();
|
||||
const emailKey = String(record.email || '').trim().toLowerCase();
|
||||
const phoneKey = normalizePhoneRecordKey(record.phoneNumber);
|
||||
const identifierKey = buildRecordId(
|
||||
record.accountIdentifier || record.email || record.phoneNumber,
|
||||
record.accountIdentifierType || (phoneKey && !emailKey ? 'phone' : 'email')
|
||||
);
|
||||
const nextHistory = normalizedHistory.filter((item) => {
|
||||
const itemRecordId = String(item.recordId || '').trim();
|
||||
const itemEmailKey = String(item.email || '').trim().toLowerCase();
|
||||
return itemRecordId !== recordId && itemEmailKey !== emailKey;
|
||||
const itemPhoneKey = normalizePhoneRecordKey(item.phoneNumber);
|
||||
const itemIdentifierKey = buildRecordId(
|
||||
item.accountIdentifier || item.email || item.phoneNumber,
|
||||
item.accountIdentifierType || (itemPhoneKey && !itemEmailKey ? 'phone' : 'email')
|
||||
);
|
||||
return itemRecordId !== recordId
|
||||
&& itemIdentifierKey !== identifierKey
|
||||
&& (!emailKey || itemEmailKey !== emailKey)
|
||||
&& (!phoneKey || itemPhoneKey !== phoneKey);
|
||||
});
|
||||
|
||||
nextHistory.unshift(record);
|
||||
@@ -283,7 +406,12 @@
|
||||
}
|
||||
|
||||
const selectedIds = new Set(normalizedIds);
|
||||
const nextHistory = normalizedHistory.filter((record) => !selectedIds.has(buildRecordId(record.recordId || record.email)));
|
||||
const nextHistory = normalizedHistory.filter((record) => !selectedIds.has(buildRecordId(
|
||||
record.recordId || record.accountIdentifier || record.email || record.phoneNumber,
|
||||
String(record.recordId || '').startsWith('phone:') || String(record.accountIdentifierType || '').trim().toLowerCase() === 'phone'
|
||||
? 'phone'
|
||||
: 'email'
|
||||
)));
|
||||
|
||||
return {
|
||||
deletedCount: normalizedHistory.length - nextHistory.length,
|
||||
|
||||
@@ -23,7 +23,10 @@
|
||||
getState,
|
||||
hasSavedProgress,
|
||||
isAddPhoneAuthFailure,
|
||||
isPhoneSmsPlatformRateLimitFailure,
|
||||
isPlusCheckoutNonFreeTrialFailure,
|
||||
isRestartCurrentAttemptError,
|
||||
isStep4Route405RecoveryLimitFailure,
|
||||
isSignupUserAlreadyExistsFailure,
|
||||
isStopError,
|
||||
launchAutoRunTimerPlan,
|
||||
@@ -123,6 +126,18 @@
|
||||
&& state.customMailProviderPool.length > 0;
|
||||
}
|
||||
|
||||
function isPhoneNumberSupplyExhaustedFailure(error) {
|
||||
const text = String(
|
||||
typeof getErrorMessage === 'function'
|
||||
? getErrorMessage(error)
|
||||
: (error?.message || error || '')
|
||||
).trim();
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
return /no\s+numbers\s+available\s+across|all provider candidates failed to acquire number|no\s+free\s+phones|numbers?\s+not\s+found|no\s+numbers\s+within\s+maxprice|countries\s+are\s+empty|均无可用号码|暂无可用号码|无可用号码|接码号池暂无|\bNO_NUMBERS\b/i.test(text);
|
||||
}
|
||||
|
||||
async function logAutoRunFinalSummary(totalRuns, roundSummaries = []) {
|
||||
const summaries = buildAutoRunRoundSummaries(totalRuns, roundSummaries);
|
||||
const successRounds = summaries.filter((item) => item.status === 'success');
|
||||
@@ -402,6 +417,7 @@
|
||||
autoRunDelayEnabled: prevState.autoRunDelayEnabled,
|
||||
autoRunDelayMinutes: prevState.autoRunDelayMinutes,
|
||||
autoStepDelaySeconds: prevState.autoStepDelaySeconds,
|
||||
signupMethod: prevState.signupMethod,
|
||||
mailProvider: prevState.mailProvider,
|
||||
emailGenerator: prevState.emailGenerator,
|
||||
gmailBaseEmail: prevState.gmailBaseEmail,
|
||||
@@ -501,13 +517,24 @@
|
||||
|
||||
const reason = getErrorMessage(err);
|
||||
roundSummary.failureReasons.push(reason);
|
||||
const blockedByAddPhone = typeof isAddPhoneAuthFailure === 'function' && isAddPhoneAuthFailure(err);
|
||||
const blockedByPhoneSupplyExhausted = isPhoneNumberSupplyExhaustedFailure(err);
|
||||
const blockedByPhoneSmsRateLimit = typeof isPhoneSmsPlatformRateLimitFailure === 'function'
|
||||
&& isPhoneSmsPlatformRateLimitFailure(err);
|
||||
const blockedByPhoneNoSupply = !blockedByPhoneSmsRateLimit
|
||||
&& isPhoneNumberSupplyExhaustedFailure(err);
|
||||
const blockedByAddPhone = !blockedByPhoneSmsRateLimit
|
||||
&& !blockedByPhoneNoSupply
|
||||
&& typeof isAddPhoneAuthFailure === 'function'
|
||||
&& isAddPhoneAuthFailure(err);
|
||||
const blockedByPlusNonFreeTrial = typeof isPlusCheckoutNonFreeTrialFailure === 'function'
|
||||
&& isPlusCheckoutNonFreeTrialFailure(err);
|
||||
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
|
||||
&& !keepSameEmailUntilAddPhone
|
||||
&& isSignupUserAlreadyExistsFailure(err);
|
||||
const blockedByStep4Route405 = typeof isStep4Route405RecoveryLimitFailure === 'function'
|
||||
&& isStep4Route405RecoveryLimitFailure(err);
|
||||
const canRetry = !blockedByAddPhone
|
||||
&& !blockedByPhoneSupplyExhausted
|
||||
&& !blockedByPhoneNoSupply
|
||||
&& !blockedByPlusNonFreeTrial
|
||||
&& !blockedBySignupUserAlreadyExists
|
||||
&& autoRunSkipFailures
|
||||
&& attemptRun < maxAttemptsForRound;
|
||||
@@ -551,6 +578,76 @@
|
||||
break;
|
||||
}
|
||||
|
||||
if (blockedByPhoneNoSupply) {
|
||||
roundSummary.status = 'failed';
|
||||
roundSummary.finalFailureReason = reason;
|
||||
await setState({
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
});
|
||||
await appendRoundRecordIfNeeded('failed', reason);
|
||||
cancelPendingCommands('当前轮因接码号池暂无可用号码已终止。');
|
||||
await broadcastStopToContentScripts();
|
||||
if (!autoRunSkipFailures) {
|
||||
await addLog(
|
||||
`第 ${targetRun}/${totalRuns} 轮接码号池暂无可用号码,自动重试未开启,当前自动运行将停止。`,
|
||||
'warn'
|
||||
);
|
||||
stoppedEarly = true;
|
||||
await broadcastAutoRunStatus('stopped', {
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
sessionId: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`第 ${targetRun}/${totalRuns} 轮接码号池暂无可用号码,本轮将直接失败并跳过剩余重试。`, 'warn');
|
||||
await addLog(
|
||||
targetRun < totalRuns
|
||||
? `第 ${targetRun}/${totalRuns} 轮因接码号池暂无可用号码提前结束,自动流程将继续下一轮。`
|
||||
: `第 ${targetRun}/${totalRuns} 轮因接码号池暂无可用号码提前结束,已无后续轮次,本次自动运行结束。`,
|
||||
'warn'
|
||||
);
|
||||
forceFreshTabsNextRun = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (blockedByPlusNonFreeTrial) {
|
||||
roundSummary.status = 'failed';
|
||||
roundSummary.finalFailureReason = reason;
|
||||
await setState({
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
});
|
||||
await appendRoundRecordIfNeeded('failed', reason);
|
||||
cancelPendingCommands('当前轮因 Plus 免费试用资格不可用已终止。');
|
||||
await broadcastStopToContentScripts();
|
||||
if (!autoRunSkipFailures) {
|
||||
await addLog(
|
||||
`第 ${targetRun}/${totalRuns} 轮检测到 Plus 今日应付金额非 0,自动重试未开启,当前自动运行将停止。`,
|
||||
'warn'
|
||||
);
|
||||
stoppedEarly = true;
|
||||
await broadcastAutoRunStatus('stopped', {
|
||||
currentRun: targetRun,
|
||||
totalRuns,
|
||||
attemptRun,
|
||||
sessionId: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`第 ${targetRun}/${totalRuns} 轮没有 Plus 免费试用资格,本轮将直接失败并跳过剩余重试。`, 'warn');
|
||||
await addLog(
|
||||
targetRun < totalRuns
|
||||
? `第 ${targetRun}/${totalRuns} 轮因 Plus 今日应付金额非 0 提前结束,自动流程将继续下一轮。`
|
||||
: `第 ${targetRun}/${totalRuns} 轮因 Plus 今日应付金额非 0 提前结束,已无后续轮次,本次自动运行结束。`,
|
||||
'warn'
|
||||
);
|
||||
forceFreshTabsNextRun = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (blockedBySignupUserAlreadyExists) {
|
||||
roundSummary.status = 'failed';
|
||||
roundSummary.finalFailureReason = reason;
|
||||
@@ -586,18 +683,18 @@
|
||||
break;
|
||||
}
|
||||
|
||||
if (blockedByPhoneSupplyExhausted) {
|
||||
if (blockedByStep4Route405) {
|
||||
roundSummary.status = 'failed';
|
||||
roundSummary.finalFailureReason = reason;
|
||||
await setState({
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
});
|
||||
await appendRoundRecordIfNeeded('failed', reason);
|
||||
cancelPendingCommands('当前轮因接码号池不可用已终止。');
|
||||
cancelPendingCommands('当前轮因步骤 4 连续 405 错误已终止。');
|
||||
await broadcastStopToContentScripts();
|
||||
if (!autoRunSkipFailures) {
|
||||
await addLog(
|
||||
`第 ${targetRun}/${totalRuns} 轮触发接码号池不可用,自动重试未开启,当前自动运行将停止。`,
|
||||
`第 ${targetRun}/${totalRuns} 轮步骤 4 连续 405 恢复失败,自动重试未开启,当前自动运行将停止。`,
|
||||
'warn'
|
||||
);
|
||||
stoppedEarly = true;
|
||||
@@ -610,11 +707,11 @@
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`第 ${targetRun}/${totalRuns} 轮接码号池暂不可用,本轮将直接失败并跳过剩余重试。`, 'warn');
|
||||
await addLog(`第 ${targetRun}/${totalRuns} 轮步骤 4 连续 405 恢复失败,本轮将直接失败并跳过剩余重试。`, 'warn');
|
||||
await addLog(
|
||||
targetRun < totalRuns
|
||||
? `第 ${targetRun}/${totalRuns} 轮因接码号池不可用提前结束,自动流程将继续下一轮。`
|
||||
: `第 ${targetRun}/${totalRuns} 轮因接码号池不可用提前结束,已无后续轮次,本次自动运行结束。`,
|
||||
? `第 ${targetRun}/${totalRuns} 轮因步骤 4 连续 405 提前结束,自动流程将继续下一轮。`
|
||||
: `第 ${targetRun}/${totalRuns} 轮因步骤 4 连续 405 提前结束,已无后续轮次,本次自动运行结束。`,
|
||||
'warn'
|
||||
);
|
||||
forceFreshTabsNextRun = true;
|
||||
|
||||
+291
-5
@@ -48,6 +48,11 @@ const IP_PROXY_EXIT_PROBE_ENDPOINTS_711_STICKY = [
|
||||
// 与 curl 口径对齐,保持单端点,避免多站点探测引入额外波动与耗时。
|
||||
'https://ipinfo.io/json',
|
||||
];
|
||||
const IP_PROXY_TARGET_REACHABILITY_ENDPOINTS = [
|
||||
// Step 1 的真实目标站点;出口 IP 可用不代表该站点的 CONNECT/TLS 链路可用。
|
||||
'https://chatgpt.com/',
|
||||
];
|
||||
const IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS = 8000;
|
||||
const IP_PROXY_BACKGROUND_PROBE_MAX_ENDPOINTS = 4;
|
||||
const IP_PROXY_BACKGROUND_PROBE_PER_ENDPOINT_TIMEOUT_MS = 3500;
|
||||
const IP_PROXY_PAGE_CONTEXT_PROBE_URL = 'https://example.com/';
|
||||
@@ -1270,6 +1275,7 @@ function buildIpProxyRoutingStatePatch(status = {}) {
|
||||
const exitDetecting = Boolean(status?.exitDetecting);
|
||||
const exitError = String(status?.exitError || '').trim();
|
||||
const exitSource = String(status?.exitSource || '').trim().toLowerCase();
|
||||
const exitEndpoint = String(status?.exitEndpoint || status?.endpoint || '').trim();
|
||||
return {
|
||||
ipProxyApplied: applied,
|
||||
ipProxyAppliedReason: reason,
|
||||
@@ -1286,6 +1292,7 @@ function buildIpProxyRoutingStatePatch(status = {}) {
|
||||
ipProxyAppliedExitDetecting: exitDetecting,
|
||||
ipProxyAppliedExitError: exitError,
|
||||
ipProxyAppliedExitSource: exitSource,
|
||||
ipProxyAppliedExitEndpoint: exitEndpoint,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1317,6 +1324,23 @@ async function setIpProxyLeakGuardEnabled(enabled) {
|
||||
}).catch(() => { });
|
||||
}
|
||||
|
||||
function shouldEnableIpProxyLeakGuardForStatus(status = {}) {
|
||||
if (!status?.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (status?.applied) {
|
||||
return false;
|
||||
}
|
||||
const reason = String(status?.reason || '').trim().toLowerCase();
|
||||
// connectivity_failed 表示 PAC/代理接管已经下发,只是探测或目标站点失败。
|
||||
// 此时继续启用 DNR 会把 chatgpt.com 变成 ERR_BLOCKED_BY_CLIENT,掩盖真实代理链路结果。
|
||||
return reason !== 'connectivity_failed';
|
||||
}
|
||||
|
||||
async function syncIpProxyLeakGuardForStatus(status = {}) {
|
||||
await setIpProxyLeakGuardEnabled(shouldEnableIpProxyLeakGuardForStatus(status));
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = IP_PROXY_FETCH_TIMEOUT_MS) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || IP_PROXY_FETCH_TIMEOUT_MS));
|
||||
@@ -1495,6 +1519,15 @@ function resolveExitProbeEndpoints(options = {}) {
|
||||
return IP_PROXY_EXIT_PROBE_ENDPOINTS.slice();
|
||||
}
|
||||
|
||||
function resolveTargetReachabilityEndpoints(options = {}) {
|
||||
const configured = Array.isArray(options?.targetReachabilityEndpoints)
|
||||
? options.targetReachabilityEndpoints.map((item) => String(item || '').trim()).filter(Boolean)
|
||||
: [];
|
||||
return configured.length
|
||||
? configured
|
||||
: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS.slice();
|
||||
}
|
||||
|
||||
function applyExitRegionExpectation(status = {}, expectedRegion = '') {
|
||||
const exitIp = String(status?.exitIp || '').trim();
|
||||
const exitError = String(status?.exitError || '').trim();
|
||||
@@ -1656,6 +1689,48 @@ function applyExitBaselineExpectation(status = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function shouldVerifyIpProxyTargetReachability(status = {}) {
|
||||
if (!String(status?.exitIp || '').trim()) {
|
||||
return false;
|
||||
}
|
||||
if (status?.applied === false && String(status?.reason || '').trim().toLowerCase() === 'connectivity_failed') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildTargetReachabilityFailureMessage(status = {}, reachability = {}) {
|
||||
const exitIp = String(status?.exitIp || '').trim();
|
||||
const exitRegion = String(status?.exitRegion || '').trim();
|
||||
const endpoint = String(reachability?.endpoint || reachability?.url || IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0] || '').trim();
|
||||
const targetHost = extractProbeHostFromTabUrl(endpoint) || endpoint || 'chatgpt.com';
|
||||
const diagnostic = String(reachability?.error || reachability?.diagnostics || '').trim();
|
||||
const diagnosticSuffix = diagnostic ? ` 诊断:${diagnostic}` : '';
|
||||
const exitRegionSuffix = exitRegion ? ` [${exitRegion}]` : '';
|
||||
return `已检测到出口 IP ${exitIp}${exitRegionSuffix},但真实目标 ${targetHost} 不可达。`
|
||||
+ `这说明代理只通过了 IP 探测,无法打开步骤 1 的 ChatGPT 页面;请更换支持 chatgpt.com CONNECT/TLS 的节点。`
|
||||
+ diagnosticSuffix;
|
||||
}
|
||||
|
||||
function applyTargetReachabilityExpectation(status = {}, reachability = {}) {
|
||||
if (!shouldVerifyIpProxyTargetReachability(status)) {
|
||||
return status;
|
||||
}
|
||||
if (reachability?.reachable === true) {
|
||||
return status;
|
||||
}
|
||||
if (reachability?.skipped === true) {
|
||||
return status;
|
||||
}
|
||||
return {
|
||||
...status,
|
||||
applied: false,
|
||||
reason: 'connectivity_failed',
|
||||
warning: '',
|
||||
error: buildTargetReachabilityFailureMessage(status, reachability),
|
||||
};
|
||||
}
|
||||
|
||||
async function detectProxyExitInfoByBackgroundFetch(options = {}) {
|
||||
const timeoutMs = Number(options?.timeoutMs) > 0 ? Number(options.timeoutMs) : 10000;
|
||||
const errors = Array.isArray(options?.errors) ? options.errors : [];
|
||||
@@ -1954,6 +2029,35 @@ async function readExitProbeFromTabDocument(tabId) {
|
||||
return executionResults?.[0]?.result || null;
|
||||
}
|
||||
|
||||
function appendProbeCacheBuster(rawUrl = '', key = '_multipage_proxy_probe') {
|
||||
const text = String(rawUrl || '').trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(text);
|
||||
parsed.searchParams.set(key, String(Date.now()));
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
const separator = text.includes('?') ? '&' : '?';
|
||||
return `${text}${separator}${key}=${Date.now()}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function readTargetReachabilityFromTabDocument(tabId) {
|
||||
const executionResults = await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
world: 'ISOLATED',
|
||||
func: () => ({
|
||||
href: String(location.href || ''),
|
||||
title: String(document.title || ''),
|
||||
readyState: String(document.readyState || ''),
|
||||
bodyLength: Number(document.body?.innerText?.length || document.documentElement?.innerText?.length || 0),
|
||||
}),
|
||||
});
|
||||
return executionResults?.[0]?.result || null;
|
||||
}
|
||||
|
||||
async function probeExitInfoByTabNavigation(tabId, timeoutMs = 10000, errors = []) {
|
||||
return probeExitInfoByTabNavigationWithEndpoints(tabId, timeoutMs, errors, []);
|
||||
}
|
||||
@@ -2208,6 +2312,101 @@ async function probeExitInfoViaExecuteScript(tabId, timeoutMs = 10000, endpoints
|
||||
return executionResults?.[0]?.result || null;
|
||||
}
|
||||
|
||||
async function detectIpProxyTargetReachabilityByPageContext(options = {}) {
|
||||
const timeoutMs = Number(options?.timeoutMs) > 0
|
||||
? Number(options.timeoutMs)
|
||||
: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS;
|
||||
const errors = Array.isArray(options?.errors) ? options.errors : [];
|
||||
const endpoints = resolveTargetReachabilityEndpoints(options);
|
||||
if (!endpoints.length) {
|
||||
return { reachable: true, skipped: true, source: 'target_page_context', endpoint: '' };
|
||||
}
|
||||
if (!chrome.tabs?.create || !chrome.tabs?.update || !chrome.scripting?.executeScript) {
|
||||
errors.push('target:page_context:unavailable');
|
||||
return {
|
||||
reachable: false,
|
||||
endpoint: endpoints[0],
|
||||
source: 'target_page_context_unavailable',
|
||||
error: 'target:page_context:unavailable',
|
||||
};
|
||||
}
|
||||
|
||||
let tabId = null;
|
||||
let createdTabId = null;
|
||||
const navigationErrorTracker = createProbeNavigationErrorTracker(null);
|
||||
const perEndpointTimeoutMs = Math.max(2500, Math.min(IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS, timeoutMs));
|
||||
|
||||
try {
|
||||
for (let index = 0; index < endpoints.length; index += 1) {
|
||||
const endpoint = String(endpoints[index] || '').trim();
|
||||
if (!endpoint) {
|
||||
continue;
|
||||
}
|
||||
const targetUrl = appendProbeCacheBuster(endpoint, '_multipage_proxy_target');
|
||||
try {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
const tab = await chrome.tabs.create({
|
||||
url: targetUrl,
|
||||
active: false,
|
||||
});
|
||||
tabId = Number(tab?.id) || null;
|
||||
createdTabId = tabId;
|
||||
navigationErrorTracker.setTabId(tabId);
|
||||
} else {
|
||||
await chrome.tabs.update(tabId, {
|
||||
url: targetUrl,
|
||||
active: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (!Number.isInteger(tabId)) {
|
||||
errors.push(`target:page_context:${endpoint}:no_tab`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ready = await waitForPageContextProbeTabReady(tabId, perEndpointTimeoutMs);
|
||||
if (!ready) {
|
||||
errors.push(`target:page_context:${endpoint}:not_ready`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const documentResult = await readTargetReachabilityFromTabDocument(tabId);
|
||||
const href = String(documentResult?.href || '').trim();
|
||||
const host = extractProbeHostFromTabUrl(href);
|
||||
const expectedHost = extractProbeHostFromTabUrl(endpoint);
|
||||
if (host && expectedHost && (host === expectedHost || host.endsWith(`.${expectedHost}`))) {
|
||||
return {
|
||||
reachable: true,
|
||||
endpoint,
|
||||
url: href,
|
||||
source: 'target_page_context',
|
||||
};
|
||||
}
|
||||
errors.push(`target:page_context:${endpoint}:unexpected_url:${href || 'unknown'}`);
|
||||
} catch (error) {
|
||||
errors.push(`target:page_context:${endpoint}:${error?.message || error}`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
navigationErrorTracker.appendDiagnostics(errors, 3);
|
||||
navigationErrorTracker.dispose();
|
||||
if (Number.isInteger(createdTabId)) {
|
||||
try {
|
||||
await chrome.tabs.remove(createdTabId);
|
||||
} catch {
|
||||
// ignore tab close failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reachable: false,
|
||||
endpoint: endpoints[0],
|
||||
source: 'target_page_context',
|
||||
error: buildProbeDiagnosticsSummary(errors, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS),
|
||||
};
|
||||
}
|
||||
|
||||
async function detectProxyExitInfoByPageContext(options = {}) {
|
||||
const timeoutMs = Number(options?.timeoutMs) > 0 ? Number(options.timeoutMs) : 10000;
|
||||
const errors = Array.isArray(options?.errors) ? options.errors : [];
|
||||
@@ -2675,6 +2874,16 @@ function buildIpProxyPacScript(entry) {
|
||||
}
|
||||
const targetPatterns = IP_PROXY_TARGET_HOST_PATTERNS.map((pattern) => `'${String(pattern).replace(/'/g, "\\'")}'`).join(', ');
|
||||
const bypassList = IP_PROXY_BYPASS_LIST.map((pattern) => `'${String(pattern).replace(/'/g, "\\'")}'`).join(', ');
|
||||
const forceDirectPatterns = (typeof IP_PROXY_FORCE_DIRECT_HOST_PATTERNS !== 'undefined' && Array.isArray(IP_PROXY_FORCE_DIRECT_HOST_PATTERNS)
|
||||
? IP_PROXY_FORCE_DIRECT_HOST_PATTERNS
|
||||
: [])
|
||||
.map((pattern) => `'${String(pattern).replace(/'/g, "\\'")}'`)
|
||||
.join(', ');
|
||||
const forceDirectFallback = String(
|
||||
typeof IP_PROXY_FORCE_DIRECT_FALLBACK !== 'undefined' && IP_PROXY_FORCE_DIRECT_FALLBACK
|
||||
? IP_PROXY_FORCE_DIRECT_FALLBACK
|
||||
: 'DIRECT'
|
||||
).replace(/"/g, '\\"');
|
||||
const proxyEndpoint = `${pacScheme} ${host}:${port}`;
|
||||
const routeAllLiteral = (typeof IP_PROXY_ROUTE_ALL_TRAFFIC !== 'undefined' && Boolean(IP_PROXY_ROUTE_ALL_TRAFFIC))
|
||||
? 'true'
|
||||
@@ -2690,6 +2899,22 @@ function FindProxyForURL(url, host) {
|
||||
}
|
||||
}
|
||||
|
||||
var forceDirectPatterns = [${forceDirectPatterns}];
|
||||
for (var fd = 0; fd < forceDirectPatterns.length; fd++) {
|
||||
var directPattern = forceDirectPatterns[fd];
|
||||
if (directPattern.indexOf('*.') === 0) {
|
||||
var directSuffix = directPattern.substring(1);
|
||||
var directHost = directPattern.substring(2);
|
||||
if (dnsDomainIs(host, directSuffix) || host === directHost) {
|
||||
return "${forceDirectFallback}";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (host === directPattern || dnsDomainIs(host, '.' + directPattern)) {
|
||||
return "${forceDirectFallback}";
|
||||
}
|
||||
}
|
||||
|
||||
var routeAllTraffic = ${routeAllLiteral};
|
||||
if (routeAllTraffic) {
|
||||
return "${proxyEndpoint}";
|
||||
@@ -3109,9 +3334,25 @@ async function applyIpProxySettingsFromState(state = {}, options = {}) {
|
||||
const expectedRegion = String(entry?.region || '').trim();
|
||||
let normalizedExitStatus = applyExitRegionExpectation(exitStatus, expectedRegion);
|
||||
normalizedExitStatus = applyExitBaselineExpectation(normalizedExitStatus);
|
||||
if (normalizedExitStatus.reason === 'connectivity_failed') {
|
||||
await setIpProxyLeakGuardEnabled(true);
|
||||
if (shouldVerifyIpProxyTargetReachability(normalizedExitStatus)) {
|
||||
const targetDiagnostics = [];
|
||||
const reachability = await detectIpProxyTargetReachabilityByPageContext({
|
||||
timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS,
|
||||
errors: targetDiagnostics,
|
||||
}).catch((error) => ({
|
||||
reachable: false,
|
||||
endpoint: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0],
|
||||
source: 'target_page_context',
|
||||
error: error?.message || String(error || 'target reachability failed'),
|
||||
}));
|
||||
normalizedExitStatus = applyTargetReachabilityExpectation(normalizedExitStatus, {
|
||||
...reachability,
|
||||
error: reachability?.reachable
|
||||
? ''
|
||||
: (reachability?.error || buildProbeDiagnosticsSummary(targetDiagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS)),
|
||||
});
|
||||
}
|
||||
await syncIpProxyLeakGuardForStatus(normalizedExitStatus);
|
||||
await updateIpProxyRuntimeStatus(normalizedExitStatus);
|
||||
return normalizedExitStatus;
|
||||
}
|
||||
@@ -3425,6 +3666,32 @@ async function probeIpProxyExit(options = {}) {
|
||||
const probePromise = (async () => {
|
||||
const state = options.state || await getState();
|
||||
if (!state?.ipProxyEnabled) {
|
||||
if (options?.detectWhenDisabled) {
|
||||
const diagnostics = [];
|
||||
const exit = await detectProxyExitInfo({
|
||||
timeoutMs: Number(options?.timeoutMs) || 10000,
|
||||
errors: diagnostics,
|
||||
provider: normalizeIpProxyProviderValue(state?.ipProxyService),
|
||||
username: String(state?.ipProxyUsername || '').trim(),
|
||||
preferPageContext: true,
|
||||
allowBackgroundFallback: true,
|
||||
}).catch(() => ({ ip: '', region: '', endpoint: '', source: '' }));
|
||||
const status = {
|
||||
enabled: false,
|
||||
applied: false,
|
||||
reason: 'disabled_probe_only',
|
||||
provider: normalizeIpProxyProviderValue(state?.ipProxyService),
|
||||
exitDetecting: false,
|
||||
exitIp: String(exit?.ip || '').trim(),
|
||||
exitRegion: String(exit?.region || '').trim(),
|
||||
exitBaselineIp: String(exit?.baselineIp || '').trim(),
|
||||
exitEndpoint: String(exit?.endpoint || '').trim(),
|
||||
exitSource: String(exit?.source || '').trim().toLowerCase(),
|
||||
exitError: exit?.ip ? '' : buildProbeDiagnosticsSummary(diagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS),
|
||||
error: '',
|
||||
};
|
||||
return { proxyRouting: status };
|
||||
}
|
||||
const status = {
|
||||
enabled: false,
|
||||
applied: false,
|
||||
@@ -3488,6 +3755,7 @@ async function probeIpProxyExit(options = {}) {
|
||||
exitDetecting: true,
|
||||
exitIp: '',
|
||||
exitRegion: '',
|
||||
exitEndpoint: '',
|
||||
exitError: '',
|
||||
exitSource: '',
|
||||
};
|
||||
@@ -3519,6 +3787,7 @@ async function probeIpProxyExit(options = {}) {
|
||||
exitIp: String(exit?.ip || '').trim(),
|
||||
exitRegion: String(exit?.region || '').trim(),
|
||||
exitBaselineIp: String(exit?.baselineIp || '').trim(),
|
||||
exitEndpoint: String(exit?.endpoint || '').trim(),
|
||||
exitError: exit?.ip ? '' : buildProbeDiagnosticsSummary(diagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS),
|
||||
exitSource: String(exit?.source || '').trim().toLowerCase(),
|
||||
authDiagnostics: statusSeed?.hasAuth ? getIpProxyAuthDiagnosticsSummary() : '',
|
||||
@@ -3526,6 +3795,24 @@ async function probeIpProxyExit(options = {}) {
|
||||
const expectedRegion = String(statusSeed?.region || '').trim();
|
||||
let normalized = applyExitRegionExpectation(finalStatus, expectedRegion);
|
||||
normalized = applyExitBaselineExpectation(normalized);
|
||||
if (shouldVerifyIpProxyTargetReachability(normalized)) {
|
||||
const targetDiagnostics = [];
|
||||
const reachability = await detectIpProxyTargetReachabilityByPageContext({
|
||||
timeoutMs: IP_PROXY_TARGET_REACHABILITY_TIMEOUT_MS,
|
||||
errors: targetDiagnostics,
|
||||
}).catch((error) => ({
|
||||
reachable: false,
|
||||
endpoint: IP_PROXY_TARGET_REACHABILITY_ENDPOINTS[0],
|
||||
source: 'target_page_context',
|
||||
error: error?.message || String(error || 'target reachability failed'),
|
||||
}));
|
||||
normalized = applyTargetReachabilityExpectation(normalized, {
|
||||
...reachability,
|
||||
error: reachability?.reachable
|
||||
? ''
|
||||
: (reachability?.error || buildProbeDiagnosticsSummary(targetDiagnostics, IP_PROXY_DIAGNOSTICS_SUMMARY_MAX_ITEMS)),
|
||||
});
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
@@ -3584,6 +3871,7 @@ async function probeIpProxyExit(options = {}) {
|
||||
exitDetecting: true,
|
||||
exitIp: '',
|
||||
exitRegion: '',
|
||||
exitEndpoint: '',
|
||||
exitError: '',
|
||||
exitSource: '',
|
||||
};
|
||||
@@ -3612,9 +3900,7 @@ async function probeIpProxyExit(options = {}) {
|
||||
normalizedFinalStatus = recovered.status;
|
||||
}
|
||||
}
|
||||
if (normalizedFinalStatus.reason === 'connectivity_failed') {
|
||||
await setIpProxyLeakGuardEnabled(true);
|
||||
}
|
||||
await syncIpProxyLeakGuardForStatus(normalizedFinalStatus);
|
||||
await updateIpProxyRuntimeStatus(normalizedFinalStatus);
|
||||
return { proxyRouting: normalizedFinalStatus };
|
||||
})();
|
||||
|
||||
@@ -31,10 +31,28 @@
|
||||
return labels[source] || source || '未知来源';
|
||||
}
|
||||
|
||||
async function addLog(message, level = 'info') {
|
||||
function normalizeLogStep(value) {
|
||||
const step = Math.floor(Number(value) || 0);
|
||||
return step > 0 ? step : null;
|
||||
}
|
||||
|
||||
function buildLogEntry(message, level = 'info', options = {}) {
|
||||
const normalizedOptions = options && typeof options === 'object' ? options : {};
|
||||
const step = normalizeLogStep(normalizedOptions.step);
|
||||
const stepKey = String(normalizedOptions.stepKey || '').trim();
|
||||
return {
|
||||
message: String(message || ''),
|
||||
level,
|
||||
timestamp: Date.now(),
|
||||
step,
|
||||
stepKey,
|
||||
};
|
||||
}
|
||||
|
||||
async function addLog(message, level = 'info', options = {}) {
|
||||
const state = await getState();
|
||||
const logs = state.logs || [];
|
||||
const entry = { message, level, timestamp: Date.now() };
|
||||
const entry = buildLogEntry(message, level, options);
|
||||
logs.push(entry);
|
||||
if (logs.length > 500) logs.splice(0, logs.length - 500);
|
||||
await setState({ logs });
|
||||
@@ -83,6 +101,8 @@
|
||||
return 'OAuth 授权页';
|
||||
case 'add_phone_page':
|
||||
return '手机号页';
|
||||
case 'add_email_page':
|
||||
return '添加邮箱页';
|
||||
case 'phone_verification_page':
|
||||
return '手机验证码页';
|
||||
default:
|
||||
|
||||
+270
-35
@@ -33,6 +33,7 @@
|
||||
executeStepViaCompletionSignal,
|
||||
exportSettingsBundle,
|
||||
fetchGeneratedEmail,
|
||||
refreshGpcCardBalance,
|
||||
finalizePhoneActivationAfterSuccessfulFlow,
|
||||
finalizeStep3Completion,
|
||||
finalizeIcloudAliasAfterSuccessfulFlow,
|
||||
@@ -48,6 +49,14 @@
|
||||
getStepDefinitionForState,
|
||||
getStepIdsForState,
|
||||
getLastStepIdForState,
|
||||
normalizeSignupMethod = (value = '') => String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email',
|
||||
canUsePhoneSignup = (state = {}) => Boolean(state?.phoneVerificationEnabled)
|
||||
&& !Boolean(state?.plusModeEnabled)
|
||||
&& !Boolean(state?.contributionMode),
|
||||
resolveSignupMethod = (state = {}) => {
|
||||
const method = normalizeSignupMethod(state?.signupMethod);
|
||||
return method === 'phone' && canUsePhoneSignup(state) ? 'phone' : 'email';
|
||||
},
|
||||
getTabId,
|
||||
getStopRequested,
|
||||
handleAutoRunLoopUnhandledError,
|
||||
@@ -66,7 +75,8 @@
|
||||
runIpProxyAutoSync,
|
||||
listIcloudAliases,
|
||||
listLuckmailPurchasesForManagement,
|
||||
refreshIpProxyPool,
|
||||
markCurrentCustomEmailPoolEntryUsed,
|
||||
markCurrentRegistrationAccountUsed,
|
||||
normalizeHotmailAccounts,
|
||||
normalizeMail2925Accounts,
|
||||
normalizePayPalAccounts,
|
||||
@@ -93,6 +103,8 @@
|
||||
setContributionMode,
|
||||
setEmailState,
|
||||
setEmailStateSilently,
|
||||
setSignupPhoneState,
|
||||
setSignupPhoneStateSilently,
|
||||
setIcloudAliasPreservedState,
|
||||
setIcloudAliasUsedState,
|
||||
setLuckmailPurchaseDisabledState,
|
||||
@@ -153,6 +165,74 @@
|
||||
return '';
|
||||
}
|
||||
|
||||
function resolveSignupPhonePayload(payload = {}) {
|
||||
const directPhone = String(
|
||||
payload?.signupPhoneNumber
|
||||
|| payload?.phoneNumber
|
||||
|| ''
|
||||
).trim();
|
||||
if (directPhone) {
|
||||
return directPhone;
|
||||
}
|
||||
return String(payload?.accountIdentifierType || '').trim().toLowerCase() === 'phone'
|
||||
? String(payload?.accountIdentifier || '').trim()
|
||||
: '';
|
||||
}
|
||||
|
||||
function resolveEmailIdentityPayload(payload = {}) {
|
||||
const directEmail = String(payload?.email || '').trim();
|
||||
if (directEmail) {
|
||||
return directEmail;
|
||||
}
|
||||
return String(payload?.accountIdentifierType || '').trim().toLowerCase() === 'email'
|
||||
? String(payload?.accountIdentifier || '').trim()
|
||||
: '';
|
||||
}
|
||||
|
||||
async function syncStepAccountIdentityFromPayload(payload = {}) {
|
||||
const identifierType = String(payload?.accountIdentifierType || '').trim().toLowerCase();
|
||||
const signupPhoneNumber = resolveSignupPhonePayload(payload);
|
||||
if (identifierType === 'phone' || signupPhoneNumber) {
|
||||
if (signupPhoneNumber) {
|
||||
await setSignupPhoneStateSilently(signupPhoneNumber);
|
||||
}
|
||||
const updates = {};
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'signupPhoneActivation')) {
|
||||
updates.signupPhoneActivation = payload.signupPhoneActivation || null;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(payload, 'signupPhoneCompletedActivation')) {
|
||||
updates.signupPhoneCompletedActivation = payload.signupPhoneCompletedActivation || null;
|
||||
}
|
||||
if (Object.keys(updates).length) {
|
||||
await setState(updates);
|
||||
broadcastDataUpdate(updates);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const email = resolveEmailIdentityPayload(payload);
|
||||
if (identifierType === 'email' || email) {
|
||||
if (email) {
|
||||
await setEmailState(email);
|
||||
}
|
||||
const updates = {
|
||||
phoneNumber: '',
|
||||
signupPhoneNumber: '',
|
||||
signupPhoneActivation: null,
|
||||
signupPhoneCompletedActivation: null,
|
||||
signupPhoneVerificationRequestedAt: null,
|
||||
signupPhoneVerificationPurpose: '',
|
||||
...(email ? {
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: email,
|
||||
} : {}),
|
||||
};
|
||||
await setSignupPhoneStateSilently(null);
|
||||
await setState(updates);
|
||||
broadcastDataUpdate(updates);
|
||||
}
|
||||
}
|
||||
|
||||
function isStepProtectedFromAutoSkip(status) {
|
||||
return status === 'running'
|
||||
|| status === 'completed'
|
||||
@@ -178,26 +258,47 @@
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function normalizePlusPaymentMethodForDisplay(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'gpc-helper') {
|
||||
return 'gpc-helper';
|
||||
}
|
||||
return normalized === 'gopay' ? 'gopay' : 'paypal';
|
||||
}
|
||||
|
||||
function getPlusPaymentMethodLabel(value = '') {
|
||||
const method = normalizePlusPaymentMethodForDisplay(value);
|
||||
if (method === 'gpc-helper') {
|
||||
return 'GPC';
|
||||
}
|
||||
return method === 'gopay' ? 'GoPay' : 'PayPal';
|
||||
}
|
||||
|
||||
async function handlePlatformVerifyStepData(payload) {
|
||||
if (payload.localhostUrl) {
|
||||
await closeLocalhostCallbackTabs(payload.localhostUrl);
|
||||
}
|
||||
const latestState = await getState();
|
||||
if (latestState.currentHotmailAccountId && isHotmailProvider(latestState)) {
|
||||
if (typeof markCurrentRegistrationAccountUsed === 'function') {
|
||||
await markCurrentRegistrationAccountUsed(latestState, {
|
||||
logPrefix: '流程完成',
|
||||
level: 'ok',
|
||||
});
|
||||
} else if (latestState.currentHotmailAccountId && isHotmailProvider(latestState)) {
|
||||
await patchHotmailAccount(latestState.currentHotmailAccountId, {
|
||||
used: true,
|
||||
lastUsedAt: Date.now(),
|
||||
});
|
||||
await addLog('当前 Hotmail 账号已自动标记为已用。', 'ok');
|
||||
}
|
||||
if (String(latestState.mailProvider || '').trim().toLowerCase() === '2925' && latestState.currentMail2925AccountId) {
|
||||
if (typeof markCurrentRegistrationAccountUsed !== 'function' && String(latestState.mailProvider || '').trim().toLowerCase() === '2925' && latestState.currentMail2925AccountId) {
|
||||
await patchMail2925Account(latestState.currentMail2925AccountId, {
|
||||
lastUsedAt: Date.now(),
|
||||
lastError: '',
|
||||
});
|
||||
await addLog('当前 2925 账号已记录最近使用时间。', 'ok');
|
||||
}
|
||||
if (isLuckmailProvider(latestState)) {
|
||||
if (typeof markCurrentRegistrationAccountUsed !== 'function' && isLuckmailProvider(latestState)) {
|
||||
const currentPurchase = getCurrentLuckmailPurchase(latestState);
|
||||
if (currentPurchase?.id) {
|
||||
await setLuckmailPurchaseUsedState(currentPurchase.id, true);
|
||||
@@ -213,7 +314,9 @@
|
||||
excludeLocalhostCallbacks: true,
|
||||
});
|
||||
}
|
||||
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
|
||||
if (typeof markCurrentRegistrationAccountUsed !== 'function') {
|
||||
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
|
||||
}
|
||||
if (typeof finalizePhoneActivationAfterSuccessfulFlow === 'function') {
|
||||
await finalizePhoneActivationAfterSuccessfulFlow(latestState);
|
||||
}
|
||||
@@ -253,7 +356,10 @@
|
||||
const currentStatus = latestState.stepStatuses?.[loginCodeStep];
|
||||
if (!isStepProtectedFromAutoSkip(currentStatus)) {
|
||||
await setStepStatus(loginCodeStep, 'skipped');
|
||||
await addLog(`步骤 ${step}:认证页已直接进入 OAuth 授权页,已自动跳过步骤 ${loginCodeStep} 的登录验证码。`, 'warn');
|
||||
await addLog(`认证页已直接进入 OAuth 授权页,已自动跳过步骤 ${loginCodeStep} 的登录验证码。`, 'warn', {
|
||||
step,
|
||||
stepKey: 'oauth-login',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (payload.loginVerificationRequestedAt) {
|
||||
@@ -264,7 +370,13 @@
|
||||
|
||||
if (stepKey === 'fetch-login-code') {
|
||||
await setState({
|
||||
lastEmailTimestamp: payload.emailTimestamp || null,
|
||||
...(payload.phoneVerification || payload.loginPhoneVerification ? {
|
||||
currentPhoneVerificationCode: '',
|
||||
signupPhoneVerificationRequestedAt: null,
|
||||
signupPhoneVerificationPurpose: '',
|
||||
} : {
|
||||
lastEmailTimestamp: payload.emailTimestamp || null,
|
||||
}),
|
||||
loginVerificationRequestedAt: null,
|
||||
});
|
||||
return;
|
||||
@@ -287,10 +399,29 @@
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 2:
|
||||
if (payload.email) {
|
||||
await setEmailState(payload.email);
|
||||
case 1: {
|
||||
const updates = {};
|
||||
if (payload.oauthUrl) {
|
||||
updates.oauthUrl = payload.oauthUrl;
|
||||
broadcastDataUpdate({ oauthUrl: payload.oauthUrl });
|
||||
}
|
||||
if (payload.sub2apiSessionId !== undefined) updates.sub2apiSessionId = payload.sub2apiSessionId || null;
|
||||
if (payload.sub2apiOAuthState !== undefined) updates.sub2apiOAuthState = payload.sub2apiOAuthState || null;
|
||||
if (payload.sub2apiGroupId !== undefined) updates.sub2apiGroupId = payload.sub2apiGroupId || null;
|
||||
if (payload.sub2apiGroupIds !== undefined) updates.sub2apiGroupIds = Array.isArray(payload.sub2apiGroupIds)
|
||||
? payload.sub2apiGroupIds
|
||||
: [];
|
||||
if (payload.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||||
if (payload.codex2apiSessionId !== undefined) updates.codex2apiSessionId = payload.codex2apiSessionId || null;
|
||||
if (payload.codex2apiOAuthState !== undefined) updates.codex2apiOAuthState = payload.codex2apiOAuthState || null;
|
||||
if (Object.keys(updates).length) {
|
||||
await setState(updates);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
await syncStepAccountIdentityFromPayload(payload);
|
||||
if (payload.skipRegistrationFlow) {
|
||||
const latestState = await getState();
|
||||
for (const skipStep of [3, 4, 5]) {
|
||||
@@ -308,22 +439,37 @@
|
||||
const step3Status = latestState.stepStatuses?.[3];
|
||||
if (step3Status !== 'running' && step3Status !== 'completed' && step3Status !== 'manual_completed') {
|
||||
await setStepStatus(3, 'skipped');
|
||||
await addLog('步骤 2:提交邮箱后页面直接进入邮箱验证码页,已自动跳过步骤 3。', 'warn');
|
||||
const identityLabel = payload.accountIdentifierType === 'phone' ? '手机号' : '邮箱';
|
||||
await addLog(`步骤 2:提交${identityLabel}后页面直接进入验证码页,已自动跳过步骤 3。`, 'warn');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
if (payload.email) await setEmailState(payload.email);
|
||||
await syncStepAccountIdentityFromPayload(payload);
|
||||
if (payload.signupVerificationRequestedAt) {
|
||||
await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt });
|
||||
}
|
||||
if (payload.skipProfileStep) {
|
||||
const latestState = await getState();
|
||||
const step5Status = latestState.stepStatuses?.[5];
|
||||
if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') {
|
||||
await setStepStatus(5, 'skipped');
|
||||
await addLog('步骤 3:页面已直接进入已登录态,已自动跳过步骤 5。', 'warn');
|
||||
}
|
||||
}
|
||||
if (payload.loginVerificationRequestedAt) {
|
||||
await setState({ loginVerificationRequestedAt: payload.loginVerificationRequestedAt });
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
await setState({
|
||||
lastEmailTimestamp: payload.emailTimestamp || null,
|
||||
...(payload.phoneVerification ? {
|
||||
currentPhoneVerificationCode: '',
|
||||
signupPhoneVerificationRequestedAt: null,
|
||||
signupPhoneVerificationPurpose: '',
|
||||
} : {
|
||||
lastEmailTimestamp: payload.emailTimestamp || null,
|
||||
}),
|
||||
signupVerificationRequestedAt: null,
|
||||
});
|
||||
if (payload.skipProfileStep) {
|
||||
@@ -339,6 +485,27 @@
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
await setState({
|
||||
...(payload.phoneVerification || payload.loginPhoneVerification ? {
|
||||
currentPhoneVerificationCode: '',
|
||||
signupPhoneVerificationRequestedAt: null,
|
||||
signupPhoneVerificationPurpose: '',
|
||||
} : {
|
||||
lastEmailTimestamp: payload.emailTimestamp || null,
|
||||
}),
|
||||
loginVerificationRequestedAt: null,
|
||||
});
|
||||
break;
|
||||
case 9:
|
||||
if (payload.localhostUrl) {
|
||||
if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) {
|
||||
throw new Error('步骤 9 返回了无效的 localhost OAuth 回调地址。');
|
||||
}
|
||||
await setState({ localhostUrl: payload.localhostUrl });
|
||||
broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -357,8 +524,16 @@
|
||||
}
|
||||
|
||||
case 'LOG': {
|
||||
const { message: msg, level } = message.payload;
|
||||
await addLog(`[${getSourceLabel(message.source)}] ${msg}`, level);
|
||||
const { message: msg, level, step: payloadStep, stepKey } = message.payload;
|
||||
const logStep = Math.floor(Number(message.step || payloadStep) || 0);
|
||||
await addLog(
|
||||
`[${getSourceLabel(message.source)}] ${msg}`,
|
||||
level,
|
||||
{
|
||||
step: logStep > 0 ? logStep : null,
|
||||
stepKey,
|
||||
}
|
||||
);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -383,7 +558,7 @@
|
||||
}
|
||||
const errorMessage = error?.message || String(error || '步骤 3 提交后确认失败');
|
||||
await setStepStatus(message.step, 'failed');
|
||||
await addLog(`步骤 ${message.step} 失败:${errorMessage}`, 'error');
|
||||
await addLog(`失败:${errorMessage}`, 'error', { step: message.step });
|
||||
await appendManualAccountRunRecordIfNeeded(`step${message.step}_failed`, null, errorMessage);
|
||||
notifyStepError(message.step, errorMessage);
|
||||
return { ok: true, error: errorMessage };
|
||||
@@ -395,7 +570,7 @@
|
||||
: 10;
|
||||
const completionState = message.step === lastStepId ? completionStateCandidate : null;
|
||||
await setStepStatus(message.step, 'completed');
|
||||
await addLog(`步骤 ${message.step} 已完成`, 'ok');
|
||||
await addLog('已完成', 'ok', { step: message.step });
|
||||
await handleStepData(message.step, message.payload);
|
||||
if (message.step === lastStepId && typeof appendAccountRunRecord === 'function') {
|
||||
await appendAccountRunRecord('success', completionState);
|
||||
@@ -414,12 +589,12 @@
|
||||
}
|
||||
if (isStopError(message.error)) {
|
||||
await setStepStatus(message.step, 'stopped');
|
||||
await addLog(`步骤 ${message.step} 已被用户停止`, 'warn');
|
||||
await addLog('已被用户停止', 'warn', { step: message.step });
|
||||
await appendManualAccountRunRecordIfNeeded(`step${message.step}_stopped`, null, message.error);
|
||||
notifyStepError(message.step, message.error);
|
||||
} else {
|
||||
await setStepStatus(message.step, 'failed');
|
||||
await addLog(`步骤 ${message.step} 失败:${message.error}`, 'error');
|
||||
await addLog(`失败:${message.error}`, 'error', { step: message.step });
|
||||
await appendManualAccountRunRecordIfNeeded(`step${message.step}_failed`, null, message.error);
|
||||
notifyStepError(message.step, message.error);
|
||||
}
|
||||
@@ -432,6 +607,8 @@
|
||||
const confirmed = Boolean(message.payload?.confirmed);
|
||||
const requestId = String(message.payload?.requestId || '').trim();
|
||||
const currentRequestId = String(currentState?.plusManualConfirmationRequestId || '').trim();
|
||||
const method = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase();
|
||||
const isGpcOtp = method === 'gopay-otp';
|
||||
if (!currentState?.plusManualConfirmationPending) {
|
||||
return { ok: true, ignored: true };
|
||||
}
|
||||
@@ -447,15 +624,31 @@
|
||||
plusManualConfirmationTitle: '',
|
||||
plusManualConfirmationMessage: '',
|
||||
};
|
||||
|
||||
if (isGpcOtp && confirmed) {
|
||||
const otp = String(message.payload?.otp || message.payload?.code || '').trim().replace(/[^\d]/g, '');
|
||||
if (!otp) {
|
||||
throw new Error('请输入 GPC OTP 验证码。');
|
||||
}
|
||||
const otpUpdates = {
|
||||
...clearManualConfirmationState,
|
||||
gopayHelperResolvedOtp: otp,
|
||||
};
|
||||
await setState(otpUpdates);
|
||||
if (typeof broadcastDataUpdate === 'function') {
|
||||
broadcastDataUpdate(otpUpdates);
|
||||
}
|
||||
await addLog(`步骤 ${step}:已收到 GPC OTP,准备提交验证。`, 'ok');
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
await setState(clearManualConfirmationState);
|
||||
if (typeof broadcastDataUpdate === 'function') {
|
||||
broadcastDataUpdate(clearManualConfirmationState);
|
||||
}
|
||||
|
||||
if (confirmed) {
|
||||
const methodLabel = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase() === 'gopay'
|
||||
? 'GoPay'
|
||||
: '手动';
|
||||
const methodLabel = method === 'gopay' ? 'GoPay' : '手动';
|
||||
await addLog(`步骤 ${step}:已确认${methodLabel}订阅完成,准备继续下一步。`, 'ok');
|
||||
await completeStepFromBackground(step, {
|
||||
plusManualConfirmationMethod: currentState?.plusManualConfirmationMethod || '',
|
||||
@@ -464,9 +657,9 @@
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const cancelMessage = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase() === 'gopay'
|
||||
const cancelMessage = method === 'gopay'
|
||||
? '已取消 GoPay 订阅确认'
|
||||
: '已取消当前手动确认';
|
||||
: (isGpcOtp ? '已取消 GPC OTP 输入' : '已取消当前手动确认');
|
||||
await setStepStatus(step, 'failed');
|
||||
await addLog(`步骤 ${step}:${cancelMessage}。`, 'warn');
|
||||
await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, null, cancelMessage);
|
||||
@@ -703,11 +896,23 @@
|
||||
const currentState = await getState();
|
||||
const updates = buildPersistentSettingsPayload(message.payload || {});
|
||||
const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {});
|
||||
const nextSignupState = {
|
||||
...currentState,
|
||||
...updates,
|
||||
resolvedSignupMethod: null,
|
||||
};
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(updates, 'phoneVerificationEnabled')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
|
||||
|| Object.prototype.hasOwnProperty.call(updates, 'signupMethod')
|
||||
) {
|
||||
updates.signupMethod = resolveSignupMethod(nextSignupState);
|
||||
}
|
||||
const modeChanged = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
|
||||
&& Boolean(currentState?.plusModeEnabled) !== Boolean(updates.plusModeEnabled);
|
||||
const plusPaymentChanged = Object.prototype.hasOwnProperty.call(updates, 'plusPaymentMethod')
|
||||
&& String(currentState?.plusPaymentMethod || 'paypal').trim().toLowerCase()
|
||||
!== String(updates.plusPaymentMethod || 'paypal').trim().toLowerCase();
|
||||
&& normalizePlusPaymentMethodForDisplay(currentState?.plusPaymentMethod || 'paypal')
|
||||
!== normalizePlusPaymentMethodForDisplay(updates.plusPaymentMethod || 'paypal');
|
||||
const nextPlusModeEnabled = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
|
||||
? Boolean(updates.plusModeEnabled)
|
||||
: Boolean(currentState?.plusModeEnabled);
|
||||
@@ -782,11 +987,9 @@
|
||||
await setContributionMode(true);
|
||||
}
|
||||
if (modeChanged) {
|
||||
const selectedPlusPaymentMethod = String(
|
||||
(stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal')
|
||||
).trim().toLowerCase() === 'gopay'
|
||||
? 'GoPay'
|
||||
: 'PayPal';
|
||||
const selectedPlusPaymentMethod = getPlusPaymentMethodLabel(
|
||||
stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal'
|
||||
);
|
||||
await addLog(
|
||||
Boolean(updates.plusModeEnabled)
|
||||
? `Plus 模式已开启,已切换为 Plus Checkout 步骤,当前支付方式:${selectedPlusPaymentMethod}。`
|
||||
@@ -794,16 +997,28 @@
|
||||
'info'
|
||||
);
|
||||
} else if (plusPaymentChanged && nextPlusModeEnabled) {
|
||||
const selectedPlusPaymentMethod = String(
|
||||
const selectedPlusPaymentMethod = getPlusPaymentMethodLabel(
|
||||
stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal'
|
||||
).trim().toLowerCase() === 'gopay'
|
||||
? 'GoPay'
|
||||
: 'PayPal';
|
||||
);
|
||||
await addLog(`Plus 支付方式已切换为 ${selectedPlusPaymentMethod},已更新对应的 Plus 步骤。`, 'info');
|
||||
}
|
||||
return { ok: true, state: await getState(), proxyRouting };
|
||||
}
|
||||
|
||||
case 'REFRESH_GPC_CARD_BALANCE': {
|
||||
if (typeof refreshGpcCardBalance !== 'function') {
|
||||
throw new Error('GPC 卡密余额查询能力尚未接入。');
|
||||
}
|
||||
const state = await getState();
|
||||
const result = await refreshGpcCardBalance({
|
||||
...(state || {}),
|
||||
...(message.payload || {}),
|
||||
}, {
|
||||
reason: message.payload?.reason,
|
||||
});
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
|
||||
case 'RUN_IP_PROXY_AUTO_SYNC_NOW': {
|
||||
if (typeof runIpProxyAutoSync !== 'function') {
|
||||
throw new Error('IP 代理自动同步能力尚未接入。');
|
||||
@@ -1066,6 +1281,26 @@
|
||||
return { ok: true, email: message.payload.email };
|
||||
}
|
||||
|
||||
case 'SET_SIGNUP_PHONE_STATE': {
|
||||
const state = await getState();
|
||||
if (isAutoRunLockedState(state)) {
|
||||
throw new Error('自动流程运行中,当前不能手动修改注册手机号。');
|
||||
}
|
||||
const phoneNumber = resolveSignupPhonePayload(message.payload) || null;
|
||||
await setSignupPhoneStateSilently(phoneNumber);
|
||||
return { ok: true, phoneNumber };
|
||||
}
|
||||
|
||||
case 'SAVE_SIGNUP_PHONE': {
|
||||
const state = await getState();
|
||||
if (isAutoRunLockedState(state)) {
|
||||
throw new Error('自动流程运行中,当前不能手动修改注册手机号。');
|
||||
}
|
||||
const phoneNumber = resolveSignupPhonePayload(message.payload) || null;
|
||||
await setSignupPhoneState(phoneNumber);
|
||||
return { ok: true, phoneNumber };
|
||||
}
|
||||
|
||||
case 'FETCH_GENERATED_EMAIL': {
|
||||
clearStopRequest();
|
||||
const state = await getState();
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
const parsed = parseUrlSafely(rawUrl);
|
||||
if (!parsed) return false;
|
||||
return isSignupPageHost(parsed.hostname)
|
||||
&& /\/create-account\/password(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||
&& /\/(?:create-account|log-in)\/password(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||
}
|
||||
|
||||
function isSignupEmailVerificationPageUrl(rawUrl) {
|
||||
|
||||
+1217
-320
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
ensureHotmailAccountForFlow,
|
||||
ensureMail2925AccountForFlow,
|
||||
ensureLuckmailPurchaseForFlow,
|
||||
fetchGeneratedEmail,
|
||||
isGeneratedAliasProvider,
|
||||
isReusableGeneratedAliasEmail,
|
||||
isHotmailProvider,
|
||||
@@ -17,11 +18,15 @@
|
||||
isLuckmailProvider,
|
||||
isSignupEmailVerificationPageUrl,
|
||||
isSignupPasswordPageUrl,
|
||||
isSignupPhoneVerificationPageUrl = null,
|
||||
isSignupProfilePageUrl = null,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScriptResilient,
|
||||
setEmailState,
|
||||
setState,
|
||||
SIGNUP_ENTRY_URL,
|
||||
SIGNUP_PAGE_INJECT_FILES,
|
||||
waitForTabStableComplete = null,
|
||||
waitForTabUrlMatch,
|
||||
} = deps;
|
||||
|
||||
@@ -62,46 +67,95 @@
|
||||
return { tabId, result: result || {} };
|
||||
}
|
||||
|
||||
function resolveSignupPostEmailState(rawUrl) {
|
||||
function parseUrlSafely(rawUrl) {
|
||||
if (!rawUrl) return null;
|
||||
try {
|
||||
return new URL(rawUrl);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackSignupPhoneVerificationPageUrl(rawUrl) {
|
||||
const parsed = parseUrlSafely(rawUrl);
|
||||
if (!parsed) return false;
|
||||
return /\/phone-verification(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||
}
|
||||
|
||||
function fallbackSignupProfilePageUrl(rawUrl) {
|
||||
const parsed = parseUrlSafely(rawUrl);
|
||||
if (!parsed) return false;
|
||||
return /\/create-account\/profile(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||
}
|
||||
|
||||
function resolveSignupPostIdentityState(rawUrl) {
|
||||
if (isSignupPasswordPageUrl(rawUrl)) {
|
||||
return 'password_page';
|
||||
}
|
||||
if (isSignupEmailVerificationPageUrl(rawUrl)) {
|
||||
return 'verification_page';
|
||||
}
|
||||
const isPhoneVerificationUrl = typeof isSignupPhoneVerificationPageUrl === 'function'
|
||||
? isSignupPhoneVerificationPageUrl(rawUrl)
|
||||
: fallbackSignupPhoneVerificationPageUrl(rawUrl);
|
||||
if (isPhoneVerificationUrl) {
|
||||
return 'phone_verification_page';
|
||||
}
|
||||
const isProfileUrl = typeof isSignupProfilePageUrl === 'function'
|
||||
? isSignupProfilePageUrl(rawUrl)
|
||||
: fallbackSignupProfilePageUrl(rawUrl);
|
||||
if (isProfileUrl) {
|
||||
return 'profile_page';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {}) {
|
||||
async function ensureSignupPostIdentityPageReadyInTab(tabId, step = 2, options = {}) {
|
||||
const { skipUrlWait = false } = options;
|
||||
let landingUrl = '';
|
||||
let landingState = '';
|
||||
|
||||
if (!skipUrlWait) {
|
||||
const matchedTab = await waitForTabUrlMatch(tabId, (url) => Boolean(resolveSignupPostEmailState(url)), {
|
||||
const matchedTab = await waitForTabUrlMatch(tabId, (url) => Boolean(resolveSignupPostIdentityState(url)), {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 300,
|
||||
});
|
||||
if (!matchedTab) {
|
||||
throw new Error('等待邮箱提交后的页面跳转超时,请检查页面是否仍停留在邮箱输入页。');
|
||||
throw new Error('等待注册身份提交后的页面跳转超时,请检查页面是否仍停留在输入页。');
|
||||
}
|
||||
|
||||
landingUrl = matchedTab.url || '';
|
||||
landingState = resolveSignupPostEmailState(landingUrl);
|
||||
landingState = resolveSignupPostIdentityState(landingUrl);
|
||||
}
|
||||
|
||||
if (!landingState) {
|
||||
try {
|
||||
const currentTab = await chrome.tabs.get(tabId);
|
||||
landingUrl = landingUrl || currentTab?.url || '';
|
||||
landingState = resolveSignupPostEmailState(landingUrl);
|
||||
landingState = resolveSignupPostIdentityState(landingUrl);
|
||||
} catch {
|
||||
landingUrl = landingUrl || '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!landingState) {
|
||||
throw new Error(`邮箱提交后未能识别当前页面,既不是密码页也不是邮箱验证码页。URL: ${landingUrl || 'unknown'}`);
|
||||
throw new Error(`注册身份提交后未能识别当前页面,既不是密码页、验证码页,也不是资料页。URL: ${landingUrl || 'unknown'}`);
|
||||
}
|
||||
|
||||
if (landingState !== 'password_page' && typeof waitForTabStableComplete === 'function') {
|
||||
const stableTab = await waitForTabStableComplete(tabId, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 300,
|
||||
stableMs: 800,
|
||||
initialDelayMs: 300,
|
||||
});
|
||||
if (stableTab?.url) {
|
||||
const stableState = resolveSignupPostIdentityState(stableTab.url);
|
||||
if (stableState) {
|
||||
landingUrl = stableTab.url;
|
||||
landingState = stableState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||
@@ -109,12 +163,12 @@
|
||||
injectSource: 'signup-page',
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: landingState === 'verification_page'
|
||||
? `步骤 ${step}:邮箱验证码页仍在加载,正在等待页面恢复...`
|
||||
: `步骤 ${step}:密码页仍在加载,正在重试连接内容脚本...`,
|
||||
logMessage: landingState === 'password_page'
|
||||
? `步骤 ${step}:密码页仍在加载,正在重试连接内容脚本...`
|
||||
: `步骤 ${step}:注册后续页面仍在加载,正在等待页面恢复...`,
|
||||
});
|
||||
|
||||
if (landingState === 'verification_page') {
|
||||
if (landingState !== 'password_page') {
|
||||
return {
|
||||
ready: true,
|
||||
state: landingState,
|
||||
@@ -145,6 +199,10 @@
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {}) {
|
||||
return ensureSignupPostIdentityPageReadyInTab(tabId, step, options);
|
||||
}
|
||||
|
||||
async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
|
||||
const result = await ensureSignupPostEmailPageReadyInTab(tabId, step, options);
|
||||
if (result.state !== 'password_page') {
|
||||
@@ -200,8 +258,52 @@
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function resolveSignupEmailForFlow(state) {
|
||||
function getPreservedPhoneIdentityForEmailResolution(state = {}, options = {}) {
|
||||
if (!Boolean(options?.preserveAccountIdentity)) {
|
||||
return null;
|
||||
}
|
||||
const accountIdentifierType = String(state?.accountIdentifierType || '').trim().toLowerCase();
|
||||
const signupPhoneNumber = String(
|
||||
state?.signupPhoneNumber
|
||||
|| (accountIdentifierType === 'phone' ? state?.accountIdentifier : '')
|
||||
|| state?.signupPhoneCompletedActivation?.phoneNumber
|
||||
|| state?.signupPhoneActivation?.phoneNumber
|
||||
|| ''
|
||||
).trim();
|
||||
if (accountIdentifierType !== 'phone' && !signupPhoneNumber) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
accountIdentifierType: 'phone',
|
||||
accountIdentifier: signupPhoneNumber || String(state?.accountIdentifier || '').trim(),
|
||||
signupPhoneNumber,
|
||||
signupPhoneActivation: state?.signupPhoneActivation || null,
|
||||
signupPhoneCompletedActivation: state?.signupPhoneCompletedActivation || null,
|
||||
signupPhoneVerificationRequestedAt: state?.signupPhoneVerificationRequestedAt ?? null,
|
||||
signupPhoneVerificationPurpose: state?.signupPhoneVerificationPurpose || '',
|
||||
};
|
||||
}
|
||||
|
||||
async function persistResolvedSignupEmail(resolvedEmail, state = {}, options = {}) {
|
||||
if (resolvedEmail === state.email && !options?.preserveAccountIdentity) {
|
||||
return;
|
||||
}
|
||||
const preservedPhoneIdentity = getPreservedPhoneIdentityForEmailResolution(state, options);
|
||||
if (preservedPhoneIdentity && typeof setState === 'function') {
|
||||
await setState({
|
||||
email: resolvedEmail,
|
||||
...preservedPhoneIdentity,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (resolvedEmail !== state.email) {
|
||||
await setEmailState(resolvedEmail);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveSignupEmailForFlow(state, options = {}) {
|
||||
let resolvedEmail = state.email;
|
||||
let generatedEmailAlreadyPersisted = false;
|
||||
if (isHotmailProvider(state)) {
|
||||
const account = await ensureHotmailAccountForFlow({
|
||||
allowAllocate: true,
|
||||
@@ -225,14 +327,17 @@
|
||||
if (!isReusableGeneratedAliasEmail?.(state, resolvedEmail)) {
|
||||
resolvedEmail = buildGeneratedAliasEmail(state);
|
||||
}
|
||||
} else if (!resolvedEmail && typeof fetchGeneratedEmail === 'function') {
|
||||
resolvedEmail = await fetchGeneratedEmail(state, options);
|
||||
generatedEmailAlreadyPersisted = true;
|
||||
}
|
||||
|
||||
if (!resolvedEmail) {
|
||||
throw new Error('缺少邮箱地址,请先在侧边栏粘贴邮箱。');
|
||||
}
|
||||
|
||||
if (resolvedEmail !== state.email) {
|
||||
await setEmailState(resolvedEmail);
|
||||
if (!generatedEmailAlreadyPersisted || options?.preserveAccountIdentity) {
|
||||
await persistResolvedSignupEmail(resolvedEmail, state, options);
|
||||
}
|
||||
|
||||
return resolvedEmail;
|
||||
@@ -240,6 +345,7 @@
|
||||
|
||||
return {
|
||||
ensureSignupEntryPageReady,
|
||||
ensureSignupPostIdentityPageReadyInTab,
|
||||
ensureSignupPostEmailPageReadyInTab,
|
||||
finalizeSignupPasswordSubmitInTab,
|
||||
ensureSignupPasswordPageReadyInTab,
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
return visibleStep >= 12 ? 10 : 7;
|
||||
}
|
||||
|
||||
function addStepLog(step, message, level = 'info') {
|
||||
return addLog(message, level, { step, stepKey: 'confirm-oauth' });
|
||||
}
|
||||
|
||||
async function executeStep9(state) {
|
||||
const visibleStep = getVisibleStep(state, 9);
|
||||
let activeState = state;
|
||||
@@ -56,7 +60,7 @@
|
||||
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`);
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在监听 localhost 回调地址...`);
|
||||
await addStepLog(visibleStep, '正在监听 localhost 回调地址...');
|
||||
|
||||
let callbackTimeoutMs = LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS;
|
||||
let timeoutRecoveryAttempted = false;
|
||||
@@ -115,7 +119,7 @@
|
||||
resolved = true;
|
||||
cleanupListener();
|
||||
|
||||
addLog(`步骤 ${visibleStep}:已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => {
|
||||
addStepLog(visibleStep, `已捕获 localhost 地址:${callbackUrl}`, 'ok').then(() => {
|
||||
return completeStepFromBackground(visibleStep, { localhostUrl: callbackUrl });
|
||||
}).then(() => {
|
||||
resolve();
|
||||
@@ -185,10 +189,10 @@
|
||||
|
||||
if (signupTabId && await isTabAlive('signup-page')) {
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await addLog(`步骤 ${visibleStep}:已切回认证页,正在准备调试器点击...`);
|
||||
await addStepLog(visibleStep, '已切回认证页,正在准备调试器点击...');
|
||||
} else {
|
||||
signupTabId = await reuseOrCreateTab('signup-page', activeState.oauthUrl);
|
||||
await addLog(`步骤 ${visibleStep}:已重新打开认证页,正在准备调试器点击...`);
|
||||
await addStepLog(visibleStep, '已重新打开认证页,正在准备调试器点击...');
|
||||
}
|
||||
|
||||
throwIfStep8SettledOrStopped(resolved);
|
||||
@@ -202,7 +206,9 @@
|
||||
actionLabel: '等待 OAuth 同意页内容脚本就绪',
|
||||
})
|
||||
: 15000,
|
||||
logMessage: `步骤 ${visibleStep}:认证页内容脚本尚未就绪,正在等待页面恢复...`,
|
||||
visibleStep,
|
||||
logStepKey: 'confirm-oauth',
|
||||
logMessage: '认证页内容脚本尚未就绪,正在等待页面恢复...',
|
||||
});
|
||||
|
||||
for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) {
|
||||
@@ -214,7 +220,8 @@
|
||||
step: visibleStep,
|
||||
actionLabel: '等待 OAuth 同意页出现',
|
||||
})
|
||||
: STEP8_READY_WAIT_TIMEOUT_MS
|
||||
: STEP8_READY_WAIT_TIMEOUT_MS,
|
||||
{ visibleStep }
|
||||
);
|
||||
if (!pageState?.consentReady) {
|
||||
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
|
||||
@@ -223,7 +230,7 @@
|
||||
|
||||
const strategy = STEP8_STRATEGIES[Math.min(round - 1, STEP8_STRATEGIES.length - 1)];
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`);
|
||||
await addStepLog(visibleStep, `第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label})...`);
|
||||
|
||||
if (strategy.mode === 'debugger') {
|
||||
const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
@@ -235,9 +242,10 @@
|
||||
const clickTarget = await prepareStep8DebuggerClick(signupTabId, {
|
||||
timeoutMs: clickActionTimeoutMs,
|
||||
responseTimeoutMs: clickActionTimeoutMs,
|
||||
visibleStep,
|
||||
});
|
||||
throwIfStep8SettledOrStopped(resolved);
|
||||
await clickWithDebugger(signupTabId, clickTarget?.rect);
|
||||
await clickWithDebugger(signupTabId, clickTarget?.rect, { visibleStep });
|
||||
} else {
|
||||
const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(15000, {
|
||||
@@ -248,6 +256,7 @@
|
||||
await triggerStep8ContentStrategy(signupTabId, strategy.strategy, {
|
||||
timeoutMs: clickActionTimeoutMs,
|
||||
responseTimeoutMs: clickActionTimeoutMs,
|
||||
visibleStep,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,14 +272,15 @@
|
||||
step: visibleStep,
|
||||
actionLabel: '等待 OAuth 同意页点击生效',
|
||||
})
|
||||
: 15000
|
||||
: 15000,
|
||||
{ visibleStep }
|
||||
);
|
||||
if (resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (effect.progressed) {
|
||||
await addLog(`步骤 ${visibleStep}:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info');
|
||||
await addStepLog(visibleStep, `检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info');
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -278,7 +288,7 @@
|
||||
throw new Error(`步骤 ${visibleStep}:连续 ${STEP8_MAX_ROUNDS} 轮点击“继续”后页面仍无反应。`);
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn');
|
||||
await addStepLog(visibleStep, `${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS})...`, 'warn');
|
||||
await reloadStep8ConsentPage(
|
||||
signupTabId,
|
||||
typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
@@ -286,7 +296,8 @@
|
||||
step: visibleStep,
|
||||
actionLabel: '刷新 OAuth 同意页',
|
||||
})
|
||||
: 30000
|
||||
: 30000,
|
||||
{ visibleStep }
|
||||
);
|
||||
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
const PLUS_CHECKOUT_ENTRY_URL = 'https://chatgpt.com/';
|
||||
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/plus-checkout.js'];
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro';
|
||||
|
||||
function createPlusCheckoutCreateExecutor(deps = {}) {
|
||||
const {
|
||||
@@ -11,21 +15,42 @@
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
fetch: fetchImpl = null,
|
||||
markCurrentRegistrationAccountUsed = null,
|
||||
registerTab,
|
||||
sendTabMessageUntilStopped,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
waitForTabCompleteUntilStopped,
|
||||
throwIfStopped = () => {},
|
||||
} = deps;
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal';
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) {
|
||||
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return PLUS_PAYMENT_METHOD_GPC_HELPER;
|
||||
}
|
||||
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
}
|
||||
|
||||
function getCheckoutModeLabel(state = {}) {
|
||||
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay'
|
||||
? 'GoPay 订阅页'
|
||||
: 'Plus Checkout';
|
||||
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return 'GPC 订阅页';
|
||||
}
|
||||
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay 订阅页' : 'Plus Checkout';
|
||||
}
|
||||
|
||||
function getPlusPaymentMethodLabel(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const paymentMethod = normalizePlusPaymentMethod(method);
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return 'GPC';
|
||||
}
|
||||
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay' : 'PayPal';
|
||||
}
|
||||
|
||||
async function openFreshChatGptTabForCheckoutCreate() {
|
||||
@@ -40,7 +65,343 @@
|
||||
return tabId;
|
||||
}
|
||||
|
||||
function normalizeHelperCountryCode(countryCode = '86') {
|
||||
const digits = String(countryCode || '').replace(/\D/g, '');
|
||||
return digits || '86';
|
||||
}
|
||||
|
||||
function normalizeHelperPhoneNumber(phone = '', countryCode = '86') {
|
||||
const cleaned = String(phone || '').replace(/\D/g, '');
|
||||
const countryDigits = normalizeHelperCountryCode(countryCode);
|
||||
if (countryDigits && cleaned.startsWith(countryDigits) && cleaned.length > countryDigits.length) {
|
||||
return cleaned.slice(countryDigits.length);
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
function resolveGpcHelperCardKey(state = {}) {
|
||||
const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim();
|
||||
if (!cardKey) {
|
||||
throw new Error('创建 GPC 订单失败:缺少卡密。');
|
||||
}
|
||||
return cardKey;
|
||||
}
|
||||
|
||||
function resolveGpcHelperCustomerEmail(state = {}) {
|
||||
const email = String(
|
||||
state?.email
|
||||
|| state?.currentEmail
|
||||
|| state?.registrationEmail
|
||||
|| state?.accountEmail
|
||||
|| state?.mailboxEmail
|
||||
|| ''
|
||||
).trim().toLowerCase();
|
||||
if (!email) {
|
||||
throw new Error('创建 GPC 订单失败:缺少当前轮邮箱。');
|
||||
}
|
||||
return email;
|
||||
}
|
||||
|
||||
function parseGpcAmount(value) {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? { amount: value, raw: String(value) } : null;
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw || !/\d/.test(raw)) {
|
||||
return null;
|
||||
}
|
||||
const match = raw.match(/([+-]?\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{1,2})|[+-]?\d+(?:[.,]\d{1,2})?)/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
let numericText = String(match[1] || '').trim();
|
||||
const lastComma = numericText.lastIndexOf(',');
|
||||
const lastDot = numericText.lastIndexOf('.');
|
||||
if (lastComma > -1 && lastDot > -1) {
|
||||
const decimalSeparator = lastComma > lastDot ? ',' : '.';
|
||||
const thousandsSeparator = decimalSeparator === ',' ? '.' : ',';
|
||||
numericText = numericText
|
||||
.replace(new RegExp(`\\${thousandsSeparator}`, 'g'), '')
|
||||
.replace(decimalSeparator, '.');
|
||||
} else if (lastComma > -1) {
|
||||
numericText = numericText.replace(',', '.');
|
||||
}
|
||||
const amount = Number(numericText.replace(/[^\d.+-]/g, ''));
|
||||
return Number.isFinite(amount) ? { amount, raw } : null;
|
||||
}
|
||||
|
||||
function isGpcAmountKey(key = '') {
|
||||
const normalized = String(key || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '_');
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:^|_)(?:id|guid|uuid|phone|country|postal|zip|code|count|status|time|timestamp|created|updated|expires|challenge|client|reference|currency|state)(?:_|$)/i.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return /(?:amount|balance|total|due|payable|gross|subtotal|price|charge)/i.test(normalized);
|
||||
}
|
||||
|
||||
function findGpcNonZeroAmount(payload = {}) {
|
||||
const seen = new Set();
|
||||
function visit(value, path = [], depth = 0) {
|
||||
if (value == null || depth > 10) {
|
||||
return null;
|
||||
}
|
||||
const key = path[path.length - 1] || '';
|
||||
if (isGpcAmountKey(key)) {
|
||||
const parsed = parseGpcAmount(value);
|
||||
if (parsed && Math.abs(parsed.amount) >= 0.005) {
|
||||
return { ...parsed, path: path.join('.') };
|
||||
}
|
||||
}
|
||||
if (typeof value !== 'object') {
|
||||
return null;
|
||||
}
|
||||
if (seen.has(value)) {
|
||||
return null;
|
||||
}
|
||||
seen.add(value);
|
||||
if (Array.isArray(value)) {
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const found = visit(value[index], [...path, String(index)], depth + 1);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
for (const [childKey, childValue] of Object.entries(value)) {
|
||||
const found = visit(childValue, [...path, childKey], depth + 1);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return visit(payload);
|
||||
}
|
||||
|
||||
async function abortGpcNonFreeTrialIfNeeded(data = {}, state = {}) {
|
||||
const nonZeroAmount = findGpcNonZeroAmount(data);
|
||||
if (!nonZeroAmount) {
|
||||
return;
|
||||
}
|
||||
const amountLabel = nonZeroAmount.raw || String(nonZeroAmount.amount);
|
||||
await addLog(`步骤 6:GPC 接口返回余额非 0(${amountLabel}),当前账号没有免费试用资格,将跳过当前账号。`, 'warn');
|
||||
if (typeof markCurrentRegistrationAccountUsed === 'function') {
|
||||
await markCurrentRegistrationAccountUsed(state, {
|
||||
reason: 'plus-checkout-non-free-trial',
|
||||
logPrefix: 'GPC:当前账号没有免费试用资格',
|
||||
});
|
||||
}
|
||||
throw new Error(`PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 6:GPC 接口返回余额非 0(${amountLabel}),当前账号没有免费试用资格,已跳过支付提交。`);
|
||||
}
|
||||
|
||||
function normalizeGpcHelperBaseUrl(apiUrl = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcHelperBaseUrl) {
|
||||
return rootScope.GoPayUtils.normalizeGpcHelperBaseUrl(apiUrl);
|
||||
}
|
||||
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, '');
|
||||
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
|
||||
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, '');
|
||||
normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, '');
|
||||
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
||||
}
|
||||
|
||||
function buildGpcHelperApiUrl(apiUrl = '', path = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.buildGpcHelperApiUrl) {
|
||||
return rootScope.GoPayUtils.buildGpcHelperApiUrl(apiUrl, path);
|
||||
}
|
||||
const baseUrl = normalizeGpcHelperBaseUrl(apiUrl);
|
||||
if (!baseUrl) {
|
||||
return '';
|
||||
}
|
||||
const normalizedPath = String(path || '').startsWith('/') ? String(path || '') : `/${String(path || '')}`;
|
||||
return `${baseUrl}${normalizedPath}`;
|
||||
}
|
||||
|
||||
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
|
||||
const fetcher = typeof fetchImpl === 'function'
|
||||
? fetchImpl
|
||||
: (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
|
||||
if (typeof fetcher !== 'function') {
|
||||
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;
|
||||
try {
|
||||
const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) });
|
||||
const data = await response.json().catch(() => ({}));
|
||||
return { response, data };
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function readAccessTokenFromChatGptSessionTab(tabId) {
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
await sleepWithStop(1000);
|
||||
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
|
||||
inject: PLUS_CHECKOUT_INJECT_FILES,
|
||||
injectSource: PLUS_CHECKOUT_SOURCE,
|
||||
logMessage: '步骤 6:正在等待 ChatGPT 页面完成加载,再继续获取 accessToken...',
|
||||
});
|
||||
|
||||
const sessionResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
||||
type: 'PLUS_CHECKOUT_GET_STATE',
|
||||
source: 'background',
|
||||
payload: {
|
||||
includeSession: true,
|
||||
includeAccessToken: true,
|
||||
},
|
||||
});
|
||||
if (sessionResult?.error) {
|
||||
throw new Error(sessionResult.error);
|
||||
}
|
||||
return String(sessionResult?.accessToken || sessionResult?.session?.accessToken || '').trim();
|
||||
}
|
||||
|
||||
async function generateGpcCheckoutFromApi(accessToken = '', state = {}) {
|
||||
const token = String(accessToken || '').trim();
|
||||
if (!token) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 accessToken。');
|
||||
}
|
||||
const apiUrl = buildGpcHelperApiUrl(state?.gopayHelperApiUrl, '/api/checkout/start');
|
||||
if (!apiUrl) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
|
||||
}
|
||||
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim();
|
||||
const countryCode = normalizeHelperCountryCode(state?.gopayHelperCountryCode || '86');
|
||||
const pin = String(state?.gopayHelperPin || '').trim();
|
||||
const cardKey = resolveGpcHelperCardKey(state);
|
||||
if (!phoneNumber) {
|
||||
throw new Error('创建 GPC 订单失败:缺少手机号。');
|
||||
}
|
||||
if (!pin) {
|
||||
throw new Error('创建 GPC 订单失败:缺少 PIN。');
|
||||
}
|
||||
|
||||
throwIfStopped();
|
||||
const payload = {
|
||||
token,
|
||||
entry_point: 'all_plans_pricing_modal',
|
||||
plan_name: 'chatgptplusplan',
|
||||
billing_details: { country: 'ID', currency: 'IDR' },
|
||||
promo_campaign: {
|
||||
promo_campaign_id: 'plus-1-month-free',
|
||||
is_coupon_from_query_param: false,
|
||||
},
|
||||
checkout_ui_mode: 'custom',
|
||||
proxy: { type: 'direct', url: '' },
|
||||
tax_region: {
|
||||
country: 'US',
|
||||
line1: '1208 Oakdale Street',
|
||||
city: 'Jonesboro',
|
||||
postal_code: '72401',
|
||||
state: 'AR',
|
||||
},
|
||||
customer_email: resolveGpcHelperCustomerEmail(state),
|
||||
card_key: cardKey,
|
||||
gopay_link: {
|
||||
type: 'gopay',
|
||||
country_code: countryCode,
|
||||
phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode),
|
||||
},
|
||||
};
|
||||
|
||||
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
}, 30000);
|
||||
|
||||
const referenceId = String(data?.reference_id || data?.referenceId || '').trim();
|
||||
const gopayGuid = String(data?.gopay_guid || data?.gopayGuid || '').trim();
|
||||
const redirectUrl = String(data?.redirect_url || data?.redirectUrl || '').trim();
|
||||
const nextAction = String(data?.next_action || data?.nextAction || '').trim();
|
||||
const flowId = String(data?.flow_id || data?.flowId || '').trim();
|
||||
const challengeId = String(data?.challenge_id || data?.challengeId || '').trim();
|
||||
|
||||
if (response?.ok) {
|
||||
await abortGpcNonFreeTrialIfNeeded(data, state);
|
||||
}
|
||||
|
||||
if (!response?.ok || !referenceId) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
const detail = rootScope.GoPayUtils?.extractGpcResponseErrorDetail
|
||||
? rootScope.GoPayUtils.extractGpcResponseErrorDetail(data, response?.status || 0)
|
||||
: (data?.detail || data?.message || data?.error || `HTTP ${response?.status || 0}`);
|
||||
throw new Error(`创建 GPC 订单失败:${detail}`);
|
||||
}
|
||||
|
||||
return {
|
||||
referenceId,
|
||||
gopayGuid,
|
||||
redirectUrl,
|
||||
nextAction,
|
||||
flowId,
|
||||
challengeId,
|
||||
responsePayload: data && typeof data === 'object' && !Array.isArray(data) ? data : null,
|
||||
country: 'ID',
|
||||
currency: 'IDR',
|
||||
checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeGpcCheckoutCreate(state = {}) {
|
||||
let accessToken = String(state?.contributionAccessToken || state?.accessToken || state?.chatgptAccessToken || '').trim();
|
||||
if (!accessToken) {
|
||||
await addLog('步骤 6:正在获取 accessToken...', 'info');
|
||||
const tokenTabId = await openFreshChatGptTabForCheckoutCreate();
|
||||
try {
|
||||
accessToken = await readAccessTokenFromChatGptSessionTab(tokenTabId);
|
||||
} finally {
|
||||
if (chrome?.tabs?.remove && Number.isInteger(tokenTabId)) {
|
||||
await chrome.tabs.remove(tokenTabId).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!accessToken) {
|
||||
throw new Error('步骤 6:GPC 模式获取 accessToken 失败。');
|
||||
}
|
||||
|
||||
await addLog('步骤 6:正在调用 GPC 接口创建订单...', 'info');
|
||||
const result = await generateGpcCheckoutFromApi(accessToken, state);
|
||||
await setState({
|
||||
plusCheckoutTabId: null,
|
||||
plusCheckoutUrl: '',
|
||||
plusCheckoutCountry: result.country || 'ID',
|
||||
plusCheckoutCurrency: result.currency || 'IDR',
|
||||
plusCheckoutSource: result.checkoutSource,
|
||||
gopayHelperReferenceId: result.referenceId,
|
||||
gopayHelperGoPayGuid: result.gopayGuid,
|
||||
gopayHelperRedirectUrl: result.redirectUrl,
|
||||
gopayHelperNextAction: result.nextAction,
|
||||
gopayHelperFlowId: result.flowId,
|
||||
gopayHelperChallengeId: result.challengeId,
|
||||
gopayHelperStartPayload: result.responsePayload,
|
||||
});
|
||||
await addLog('步骤 6:GPC 订单已创建,准备继续下一步。', 'info');
|
||||
await completeStepFromBackground(6, {
|
||||
plusCheckoutCountry: result.country || 'ID',
|
||||
plusCheckoutCurrency: result.currency || 'IDR',
|
||||
plusCheckoutSource: result.checkoutSource,
|
||||
});
|
||||
}
|
||||
|
||||
async function executePlusCheckoutCreate(state = {}) {
|
||||
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
await executeGpcCheckoutCreate(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentMethodLabel = getPlusPaymentMethodLabel(paymentMethod);
|
||||
const checkoutModeLabel = getCheckoutModeLabel(state);
|
||||
await addLog(`步骤 6:正在打开新的 ChatGPT 会话,准备创建${checkoutModeLabel}...`, 'info');
|
||||
const tabId = await openFreshChatGptTabForCheckoutCreate();
|
||||
@@ -56,9 +417,7 @@
|
||||
const result = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
||||
type: 'CREATE_PLUS_CHECKOUT',
|
||||
source: 'background',
|
||||
payload: {
|
||||
paymentMethod: normalizePlusPaymentMethod(state?.plusPaymentMethod),
|
||||
},
|
||||
payload: { paymentMethod },
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
@@ -83,9 +442,10 @@
|
||||
plusCheckoutUrl: result.checkoutUrl,
|
||||
plusCheckoutCountry: result.country || 'DE',
|
||||
plusCheckoutCurrency: result.currency || 'EUR',
|
||||
plusCheckoutSource: '',
|
||||
});
|
||||
|
||||
await addLog(`步骤 6:${checkoutModeLabel}已就绪。`, 'info');
|
||||
await addLog(`步骤 6:Plus Checkout 页面已就绪(${paymentMethodLabel} / ${result.country || 'DE'} ${result.currency || 'EUR'}),准备继续下一步。`, 'info');
|
||||
|
||||
await completeStepFromBackground(6, {
|
||||
plusCheckoutCountry: result.country || 'DE',
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
root.MultiPageBackgroundStep8 = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep8Module() {
|
||||
const MAIL_2925_FILTER_LOOKBACK_MS = 10 * 60 * 1000;
|
||||
const STEP8_ADD_EMAIL_URL = 'https://auth.openai.com/add-email';
|
||||
const STEP8_CURRENT_STEP_RECOVERY_MAX_ATTEMPTS = 3;
|
||||
|
||||
function createStep8Executor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
addLog: rawAddLog = async () => {},
|
||||
chrome,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
completeStepFromBackground,
|
||||
@@ -22,9 +24,12 @@
|
||||
isTabAlive,
|
||||
isVerificationMailPollingError,
|
||||
LUCKMAIL_PROVIDER,
|
||||
resolveSignupEmailForFlow,
|
||||
resolveVerificationStep,
|
||||
rerunStep7ForStep8Recovery,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScriptResilient,
|
||||
phoneVerificationHelpers = null,
|
||||
setState,
|
||||
shouldUseCustomRegistrationEmail,
|
||||
sleepWithStop,
|
||||
@@ -32,6 +37,33 @@
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
|
||||
throwIfStopped,
|
||||
} = deps;
|
||||
let activeFetchLoginCodeStep = null;
|
||||
|
||||
function normalizeLogStep(value) {
|
||||
const step = Math.floor(Number(value) || 0);
|
||||
return step > 0 ? step : null;
|
||||
}
|
||||
|
||||
function normalizeStepLogMessage(message) {
|
||||
return String(message || '')
|
||||
.replace(/^步骤\s*\d+\s*[::]\s*/, '')
|
||||
.replace(/^Step\s+\d+\s*[::]\s*/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function addLog(message, level = 'info', options = {}) {
|
||||
const normalizedOptions = options && typeof options === 'object' ? { ...options } : {};
|
||||
const step = normalizeLogStep(normalizedOptions.step || normalizedOptions.visibleStep)
|
||||
|| normalizeLogStep(activeFetchLoginCodeStep);
|
||||
if (step) {
|
||||
normalizedOptions.step = step;
|
||||
if (!normalizedOptions.stepKey) {
|
||||
normalizedOptions.stepKey = 'fetch-login-code';
|
||||
}
|
||||
}
|
||||
delete normalizedOptions.visibleStep;
|
||||
return rawAddLog(normalizeStepLogMessage(message), level, normalizedOptions);
|
||||
}
|
||||
|
||||
function getVisibleStep(state, fallback = 8) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
@@ -70,6 +102,102 @@
|
||||
return String(value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async function getLoginAuthStateFromContent(visibleStep, options = {}) {
|
||||
if (typeof sendToContentScriptResilient !== 'function') {
|
||||
return {};
|
||||
}
|
||||
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 15000);
|
||||
const result = await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
{
|
||||
type: 'GET_LOGIN_AUTH_STATE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
},
|
||||
{
|
||||
timeoutMs,
|
||||
responseTimeoutMs: timeoutMs,
|
||||
retryDelayMs: 600,
|
||||
logMessage: options.logMessage || `步骤 ${visibleStep}:认证页正在切换,等待页面重新就绪...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: 'fetch-login-code',
|
||||
}
|
||||
);
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function submitAddEmailIfNeeded(state, visibleStep, initialPageState = null) {
|
||||
if (typeof resolveSignupEmailForFlow !== 'function' || typeof sendToContentScriptResilient !== 'function') {
|
||||
return { state, pageState: initialPageState };
|
||||
}
|
||||
|
||||
const pageState = initialPageState?.state
|
||||
? initialPageState
|
||||
: await getLoginAuthStateFromContent(visibleStep, {
|
||||
timeoutMs: 15000,
|
||||
logMessage: `步骤 ${visibleStep}:正在确认是否已进入添加邮箱页...`,
|
||||
});
|
||||
if (pageState?.state !== 'add_email_page') {
|
||||
return { state, pageState };
|
||||
}
|
||||
|
||||
const latestState = typeof getState === 'function' ? await getState() : state;
|
||||
const resolvedEmail = await resolveSignupEmailForFlow(latestState, {
|
||||
preserveAccountIdentity: true,
|
||||
});
|
||||
await addLog(`步骤 ${visibleStep}:检测到添加邮箱页,正在添加邮箱 ${resolvedEmail} 并进入邮箱验证码页...`);
|
||||
|
||||
const timeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(60000, {
|
||||
step: visibleStep,
|
||||
actionLabel: '添加邮箱并进入验证码页',
|
||||
oauthUrl: latestState?.oauthUrl || state?.oauthUrl || '',
|
||||
})
|
||||
: 60000;
|
||||
const result = await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
{
|
||||
type: 'SUBMIT_ADD_EMAIL',
|
||||
source: 'background',
|
||||
payload: { email: resolvedEmail },
|
||||
},
|
||||
{
|
||||
timeoutMs,
|
||||
responseTimeoutMs: timeoutMs,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${visibleStep}:添加邮箱页面正在切换,等待邮箱验证码页就绪...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: 'fetch-login-code',
|
||||
}
|
||||
);
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
const displayedEmail = normalizeStep8VerificationTargetEmail(result?.displayedEmail || resolvedEmail);
|
||||
await setState({
|
||||
email: resolvedEmail,
|
||||
step8VerificationTargetEmail: displayedEmail,
|
||||
});
|
||||
|
||||
return {
|
||||
state: {
|
||||
...latestState,
|
||||
email: resolvedEmail,
|
||||
step8VerificationTargetEmail: displayedEmail,
|
||||
},
|
||||
pageState: {
|
||||
state: result?.directOAuthConsentPage ? 'oauth_consent_page' : 'verification_page',
|
||||
displayedEmail,
|
||||
url: result?.url || pageState?.url || '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, options = {}) {
|
||||
await setState({
|
||||
step8VerificationTargetEmail: '',
|
||||
@@ -94,12 +222,69 @@
|
||||
return /add-phone|手机号页面|手机号验证页|phone[\s-_]verification|phone\s+number/i.test(message);
|
||||
}
|
||||
|
||||
function isStep8EmailInUseError(error) {
|
||||
const message = String(error?.message || error || '');
|
||||
return /STEP8_EMAIL_IN_USE::|email_in_use|email\s+(?:address\s+)?already\s+exists|already\s+associated\s+with\s+this\s+email/i.test(message);
|
||||
}
|
||||
|
||||
function isStep8MaxCheckAttemptsError(error) {
|
||||
const message = String(error?.message || error || '');
|
||||
return /AUTH_MAX_CHECK_ATTEMPTS::|max_check_attempts/i.test(message);
|
||||
}
|
||||
|
||||
async function openStep8AddEmailPage(state, visibleStep, reasonLabel = '') {
|
||||
const tabId = typeof getTabId === 'function' ? await getTabId('signup-page') : 0;
|
||||
const url = STEP8_ADD_EMAIL_URL;
|
||||
if (tabId && chrome?.tabs?.update) {
|
||||
await chrome.tabs.update(tabId, { url, active: true });
|
||||
} else if (typeof reuseOrCreateTab === 'function') {
|
||||
await reuseOrCreateTab('signup-page', url);
|
||||
} else {
|
||||
throw new Error(`Step ${visibleStep}: cannot reopen add-email page for Step 8 recovery.`);
|
||||
}
|
||||
if (typeof sleepWithStop === 'function') {
|
||||
await sleepWithStop(1000);
|
||||
}
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:重新打开添加邮箱页面${reasonLabel ? `(${reasonLabel})` : ''}。`,
|
||||
'warn'
|
||||
);
|
||||
return {
|
||||
...(state || {}),
|
||||
oauthUrl: state?.oauthUrl || url,
|
||||
};
|
||||
}
|
||||
|
||||
async function resetStep8AfterEmailInUse(state, visibleStep) {
|
||||
const currentEmail = String(state?.email || '').trim();
|
||||
await setState({
|
||||
email: null,
|
||||
step8VerificationTargetEmail: '',
|
||||
loginVerificationRequestedAt: null,
|
||||
});
|
||||
if (currentEmail) {
|
||||
await addLog(`步骤 ${visibleStep}:检测到邮箱 ${currentEmail} 已被占用,已清理运行态并准备重新获取新邮箱。`, 'warn');
|
||||
} else {
|
||||
await addLog(`步骤 ${visibleStep}:检测到邮箱已被占用,已清理运行态并准备重新获取新邮箱。`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
async function resetStep8AfterMaxCheckAttempts(visibleStep) {
|
||||
await setState({
|
||||
step8VerificationTargetEmail: '',
|
||||
loginVerificationRequestedAt: null,
|
||||
});
|
||||
await addLog(`步骤 ${visibleStep}:检测到 max_check_attempts,将重新开始当前添加邮箱步骤,不继续点击重试。`, 'warn');
|
||||
}
|
||||
|
||||
async function recoverStep8PollingFailure(currentState, visibleStep) {
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
try {
|
||||
const pageState = await ensureStep8VerificationPageReady({
|
||||
visibleStep,
|
||||
authLoginStep,
|
||||
allowPhoneVerificationPage: true,
|
||||
allowAddEmailPage: true,
|
||||
timeoutMs: await getStep8ReadyTimeoutMs(
|
||||
'登录验证码轮询异常后复核认证页状态',
|
||||
currentState?.oauthUrl || '',
|
||||
@@ -110,9 +295,9 @@
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep, { fromRecovery: true });
|
||||
return { outcome: 'completed' };
|
||||
}
|
||||
if (pageState?.state === 'verification_page') {
|
||||
if (pageState?.state === 'verification_page' || pageState?.state === 'phone_verification_page' || pageState?.state === 'add_email_page') {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:检测到邮箱轮询/页面通信异常,但认证页仍在验证码页,先在当前链路重试,不回到步骤 ${authLoginStep}。`,
|
||||
`步骤 ${visibleStep}:检测到邮箱轮询/页面通信异常,但认证页仍在当前登录后续页面,先在当前链路重试,不回到步骤 ${authLoginStep}。`,
|
||||
'warn'
|
||||
);
|
||||
return { outcome: 'retry_without_step7' };
|
||||
@@ -179,21 +364,30 @@
|
||||
return Math.max(0, Number(STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS) || 0);
|
||||
}
|
||||
|
||||
async function executeLoginPhoneCodeStep(state, signupTabId, visibleStep) {
|
||||
if (!Number.isInteger(signupTabId)) {
|
||||
throw new Error(`步骤 ${visibleStep}:认证页面标签页已关闭,无法继续手机号登录验证码流程。`);
|
||||
}
|
||||
if (typeof phoneVerificationHelpers?.completeLoginPhoneVerificationFlow !== 'function') {
|
||||
throw new Error(`步骤 ${visibleStep}:手机号登录验证码流程不可用,接码模块尚未初始化。`);
|
||||
}
|
||||
|
||||
const result = await phoneVerificationHelpers.completeLoginPhoneVerificationFlow(signupTabId, {
|
||||
state,
|
||||
visibleStep,
|
||||
});
|
||||
|
||||
await completeStepFromBackground(visibleStep, {
|
||||
phoneVerification: true,
|
||||
loginPhoneVerification: true,
|
||||
code: result?.code || '',
|
||||
});
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function runStep8Attempt(state, runtime = {}) {
|
||||
const visibleStep = getVisibleStep(state, 8);
|
||||
const mail = getMailConfig(state);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
||||
let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt);
|
||||
const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function'
|
||||
? runtime.onResendRequestedAt
|
||||
: null;
|
||||
|
||||
const stepStartedAt = Date.now();
|
||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: stepStartedAt;
|
||||
const verificationSessionKey = `8:${stepStartedAt}`;
|
||||
activeFetchLoginCodeStep = visibleStep;
|
||||
const authTabId = await getTabId('signup-page');
|
||||
|
||||
if (authTabId) {
|
||||
@@ -205,22 +399,58 @@
|
||||
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||
}
|
||||
|
||||
const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
||||
let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt);
|
||||
const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function'
|
||||
? runtime.onResendRequestedAt
|
||||
: null;
|
||||
|
||||
throwIfStopped();
|
||||
const pageState = await ensureStep8VerificationPageReady({
|
||||
let pageState = await ensureStep8VerificationPageReady({
|
||||
visibleStep,
|
||||
authLoginStep: getAuthLoginStepForVisibleStep(visibleStep),
|
||||
allowPhoneVerificationPage: true,
|
||||
allowAddEmailPage: true,
|
||||
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪', state?.oauthUrl || '', visibleStep),
|
||||
});
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
|
||||
return;
|
||||
}
|
||||
if (pageState?.state === 'phone_verification_page') {
|
||||
return executeLoginPhoneCodeStep(state, authTabId, visibleStep);
|
||||
}
|
||||
|
||||
let preparedState = state;
|
||||
const addEmailPreparation = await submitAddEmailIfNeeded(preparedState, visibleStep, pageState);
|
||||
preparedState = addEmailPreparation?.state || preparedState;
|
||||
pageState = addEmailPreparation?.pageState || pageState;
|
||||
if (pageState?.state === 'oauth_consent_page') {
|
||||
await completeStep8WhenAuthAlreadyOnOauthConsent(visibleStep);
|
||||
return;
|
||||
}
|
||||
if (pageState?.state === 'phone_verification_page') {
|
||||
return executeLoginPhoneCodeStep(preparedState, authTabId, visibleStep);
|
||||
}
|
||||
|
||||
const preparedStateLastResendAt = Number(preparedState?.loginVerificationRequestedAt) || 0;
|
||||
if (preparedStateLastResendAt > 0) {
|
||||
latestResendAt = Math.max(latestResendAt, preparedStateLastResendAt);
|
||||
}
|
||||
|
||||
const mail = getMailConfig(preparedState);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
const stepStartedAt = Date.now();
|
||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: stepStartedAt;
|
||||
const verificationSessionKey = `8:${stepStartedAt}`;
|
||||
const shouldCompareVerificationEmail = mail.provider !== '2925';
|
||||
const displayedVerificationEmail = shouldCompareVerificationEmail
|
||||
? normalizeStep8VerificationTargetEmail(pageState?.displayedEmail)
|
||||
: '';
|
||||
const fixedTargetEmail = shouldCompareVerificationEmail
|
||||
? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(state?.email))
|
||||
? (displayedVerificationEmail || normalizeStep8VerificationTargetEmail(preparedState?.email))
|
||||
: '';
|
||||
|
||||
await setState({
|
||||
@@ -232,7 +462,7 @@
|
||||
await addLog(`步骤 ${visibleStep}:已固定当前验证码页显示邮箱 ${displayedVerificationEmail} 作为后续匹配目标。`, 'info');
|
||||
}
|
||||
|
||||
if (shouldUseCustomRegistrationEmail(state)) {
|
||||
if (shouldUseCustomRegistrationEmail(preparedState)) {
|
||||
await confirmCustomVerificationStepBypass(8, {
|
||||
completionStep: visibleStep,
|
||||
promptStep: visibleStep,
|
||||
@@ -243,7 +473,7 @@
|
||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
||||
await addLog(`步骤 ${visibleStep}:正在确认 iCloud 邮箱登录态...`, 'info');
|
||||
await ensureIcloudMailSession({
|
||||
state,
|
||||
state: preparedState,
|
||||
step: 8,
|
||||
actionLabel: `步骤 ${visibleStep}:确认 iCloud 邮箱登录态`,
|
||||
});
|
||||
@@ -260,10 +490,10 @@
|
||||
await addLog(`步骤 ${visibleStep}:正在打开${mail.label}...`);
|
||||
if (mail.provider === '2925' && typeof ensureMail2925MailboxSession === 'function') {
|
||||
await ensureMail2925MailboxSession({
|
||||
accountId: state.currentMail2925AccountId || null,
|
||||
accountId: preparedState.currentMail2925AccountId || null,
|
||||
forceRelogin: false,
|
||||
allowLoginWhenOnLoginPage: Boolean(state?.mail2925UseAccountPool),
|
||||
expectedMailboxEmail: getExpectedMail2925MailboxEmail(state),
|
||||
allowLoginWhenOnLoginPage: Boolean(preparedState?.mail2925UseAccountPool),
|
||||
expectedMailboxEmail: getExpectedMail2925MailboxEmail(preparedState),
|
||||
actionLabel: `Step ${visibleStep}: ensure 2925 mailbox session`,
|
||||
});
|
||||
} else {
|
||||
@@ -275,14 +505,14 @@
|
||||
}
|
||||
|
||||
await resolveVerificationStep(8, {
|
||||
...state,
|
||||
...preparedState,
|
||||
step8VerificationTargetEmail: displayedVerificationEmail || '',
|
||||
}, mail, {
|
||||
completionStep: visibleStep,
|
||||
filterAfterTimestamp: verificationFilterAfterTimestamp,
|
||||
sessionKey: verificationSessionKey,
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep),
|
||||
getRemainingTimeMs: getStep8RemainingTimeResolver(preparedState?.oauthUrl || '', visibleStep),
|
||||
requestFreshCodeFirst: false,
|
||||
lastResendAt: latestResendAt,
|
||||
onResendRequestedAt: async (requestedAt) => {
|
||||
@@ -318,6 +548,7 @@
|
||||
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
||||
let retryWithoutStep7Streak = 0;
|
||||
const maxRetryWithoutStep7Streak = 3;
|
||||
let currentStepRecoveryAttempt = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
@@ -340,6 +571,28 @@
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
let currentError = err;
|
||||
let retryWithoutStep7 = false;
|
||||
|
||||
if (isStep8EmailInUseError(currentError) || isStep8MaxCheckAttemptsError(currentError)) {
|
||||
currentStepRecoveryAttempt += 1;
|
||||
if (currentStepRecoveryAttempt > STEP8_CURRENT_STEP_RECOVERY_MAX_ATTEMPTS) {
|
||||
throw currentError;
|
||||
}
|
||||
if (isStep8EmailInUseError(currentError)) {
|
||||
await resetStep8AfterEmailInUse(currentState, visibleStep);
|
||||
await openStep8AddEmailPage(currentState, visibleStep, 'email_in_use');
|
||||
} else {
|
||||
await resetStep8AfterMaxCheckAttempts(visibleStep);
|
||||
await openStep8AddEmailPage(currentState, visibleStep, 'max_check_attempts');
|
||||
}
|
||||
const latestState = typeof getState === 'function' ? await getState() : currentState;
|
||||
currentState = {
|
||||
...(currentState || {}),
|
||||
...(latestState || {}),
|
||||
oauthUrl: currentState?.oauthUrl || latestState?.oauthUrl || STEP8_ADD_EMAIL_URL,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
const isMailPollingError = isVerificationMailPollingError(err);
|
||||
if (isMailPollingError && !isStep8RestartStep7Error(err)) {
|
||||
const recovery = await recoverStep8PollingFailure(currentState, visibleStep);
|
||||
@@ -371,7 +624,9 @@
|
||||
'warn'
|
||||
);
|
||||
await rerunStep7ForStep8Recovery({
|
||||
logMessage: `步骤 ${visibleStep}:邮箱通信异常持续未恢复,正在回到步骤 ${authLoginStep} 重新发起登录流程...`,
|
||||
logMessage: `邮箱通信异常持续未恢复,正在回到步骤 ${authLoginStep} 重新发起登录流程...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: 'fetch-login-code',
|
||||
});
|
||||
currentState = await getState();
|
||||
retryWithoutStep7Streak = 0;
|
||||
@@ -415,8 +670,10 @@
|
||||
);
|
||||
await rerunStep7ForStep8Recovery({
|
||||
logMessage: isStep8RestartStep7Error(currentError)
|
||||
? `步骤 ${visibleStep}:认证页进入重试/超时报错状态,正在回到步骤 ${authLoginStep} 重新发起登录流程...`
|
||||
: `步骤 ${visibleStep}:正在回到步骤 ${authLoginStep},重新发起登录验证码流程...`,
|
||||
? `认证页进入重试/超时报错状态,正在回到步骤 ${authLoginStep} 重新发起登录流程...`
|
||||
: `正在回到步骤 ${authLoginStep},重新发起登录验证码流程...`,
|
||||
logStep: visibleStep,
|
||||
logStepKey: 'fetch-login-code',
|
||||
});
|
||||
currentState = await getState();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
shouldUseCustomRegistrationEmail,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
throwIfStopped,
|
||||
waitForTabStableComplete = null,
|
||||
phoneVerificationHelpers = null,
|
||||
resolveSignupMethod = () => 'email',
|
||||
} = deps;
|
||||
|
||||
function buildSignupProfileForVerificationStep() {
|
||||
@@ -58,6 +61,32 @@
|
||||
return String(state?.mail2925BaseEmail || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isPhoneSignupState(state = {}) {
|
||||
return resolveSignupMethod(state) === 'phone'
|
||||
|| state?.accountIdentifierType === 'phone'
|
||||
|| Boolean(state?.signupPhoneActivation);
|
||||
}
|
||||
|
||||
async function executeSignupPhoneCodeStep(state, signupTabId) {
|
||||
if (typeof phoneVerificationHelpers?.completeSignupPhoneVerificationFlow !== 'function') {
|
||||
throw new Error('步骤 4:手机号注册验证码流程不可用,接码模块尚未初始化。');
|
||||
}
|
||||
|
||||
const signupProfile = buildSignupProfileForVerificationStep();
|
||||
const result = await phoneVerificationHelpers.completeSignupPhoneVerificationFlow(signupTabId, {
|
||||
state,
|
||||
signupProfile,
|
||||
});
|
||||
|
||||
await completeStepFromBackground(4, {
|
||||
phoneVerification: true,
|
||||
code: result?.code || '',
|
||||
...(result?.skipProfileStep ? { skipProfileStep: true } : {}),
|
||||
...(result?.skipProfileStepReason ? { skipProfileStepReason: result.skipProfileStepReason } : {}),
|
||||
});
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function focusOrOpenMailTab(mail) {
|
||||
const alive = await isTabAlive(mail.source);
|
||||
if (alive) {
|
||||
@@ -81,13 +110,7 @@
|
||||
}
|
||||
|
||||
async function executeStep4(state) {
|
||||
const mail = getMailConfig(state);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
|
||||
const stepStartedAt = Date.now();
|
||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: stepStartedAt;
|
||||
const verificationSessionKey = `4:${stepStartedAt}`;
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
|
||||
@@ -97,6 +120,16 @@
|
||||
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
throwIfStopped();
|
||||
if (typeof waitForTabStableComplete === 'function') {
|
||||
await addLog('步骤 4:等待注册验证码页面完成加载后再继续...', 'info');
|
||||
await waitForTabStableComplete(signupTabId, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 300,
|
||||
stableMs: 800,
|
||||
initialDelayMs: 300,
|
||||
});
|
||||
}
|
||||
throwIfStopped();
|
||||
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
|
||||
|
||||
const prepareRequest = {
|
||||
@@ -175,11 +208,22 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPhoneSignupState(state)) {
|
||||
return executeSignupPhoneCodeStep(state, signupTabId);
|
||||
}
|
||||
|
||||
if (shouldUseCustomRegistrationEmail(state)) {
|
||||
await confirmCustomVerificationStepBypass(4);
|
||||
return;
|
||||
}
|
||||
|
||||
const mail = getMailConfig(state);
|
||||
if (mail.error) throw new Error(mail.error);
|
||||
|
||||
const verificationFilterAfterTimestamp = mail.provider === '2925'
|
||||
? Math.max(0, stepStartedAt - MAIL_2925_FILTER_LOOKBACK_MS)
|
||||
: stepStartedAt;
|
||||
|
||||
if (mail.source === 'icloud-mail' && typeof ensureIcloudMailSession === 'function') {
|
||||
await addLog('步骤 4:正在确认 iCloud 邮箱登录态...', 'info');
|
||||
await ensureIcloudMailSession({
|
||||
|
||||
@@ -9,16 +9,66 @@
|
||||
generatePassword,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
resolveSignupMethod,
|
||||
sendToContentScript,
|
||||
setPasswordState,
|
||||
setState,
|
||||
SIGNUP_PAGE_INJECT_FILES,
|
||||
} = deps;
|
||||
|
||||
function normalizeSignupMethod(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'phone'
|
||||
? 'phone'
|
||||
: 'email';
|
||||
}
|
||||
|
||||
function getResolvedSignupMethodForStep3(state = {}) {
|
||||
if (typeof resolveSignupMethod === 'function') {
|
||||
return normalizeSignupMethod(resolveSignupMethod(state));
|
||||
}
|
||||
const frozenMethod = String(state?.resolvedSignupMethod || '').trim().toLowerCase();
|
||||
if (frozenMethod === 'phone' || frozenMethod === 'email') {
|
||||
return normalizeSignupMethod(frozenMethod);
|
||||
}
|
||||
return normalizeSignupMethod(state?.signupMethod);
|
||||
}
|
||||
|
||||
function resolveStep3AccountIdentity(state = {}) {
|
||||
const resolvedEmail = String(state?.email || '').trim();
|
||||
const rawAccountIdentifierType = String(state?.accountIdentifierType || '').trim().toLowerCase();
|
||||
const signupPhoneNumber = String(
|
||||
state?.signupPhoneNumber
|
||||
|| (rawAccountIdentifierType === 'phone' ? state?.accountIdentifier : '')
|
||||
|| ''
|
||||
).trim();
|
||||
const explicitEmailIdentity = rawAccountIdentifierType === 'email' && resolvedEmail;
|
||||
const shouldUsePhoneIdentity = !explicitEmailIdentity && (
|
||||
rawAccountIdentifierType === 'phone'
|
||||
|| Boolean(signupPhoneNumber)
|
||||
|| getResolvedSignupMethodForStep3(state) === 'phone'
|
||||
);
|
||||
const accountIdentifierType = shouldUsePhoneIdentity
|
||||
? 'phone'
|
||||
: (resolvedEmail ? 'email' : 'email');
|
||||
const accountIdentifier = accountIdentifierType === 'phone'
|
||||
? signupPhoneNumber
|
||||
: resolvedEmail;
|
||||
|
||||
return {
|
||||
accountIdentifierType,
|
||||
accountIdentifier,
|
||||
email: resolvedEmail,
|
||||
phoneNumber: signupPhoneNumber,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeStep3(state) {
|
||||
const resolvedEmail = state.email;
|
||||
if (!resolvedEmail) {
|
||||
throw new Error('缺少邮箱地址,请先完成步骤 2。');
|
||||
const identity = resolveStep3AccountIdentity(state);
|
||||
if (!identity.accountIdentifier) {
|
||||
if (identity.accountIdentifierType === 'phone') {
|
||||
throw new Error('缺少注册手机号,请先完成步骤 2 或在侧栏填写注册手机号后再执行步骤 3。');
|
||||
}
|
||||
throw new Error('缺少注册账号,请先完成步骤 2。');
|
||||
}
|
||||
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
@@ -30,7 +80,13 @@
|
||||
await setPasswordState(password);
|
||||
|
||||
const accounts = state.accounts || [];
|
||||
accounts.push({ email: resolvedEmail, createdAt: new Date().toISOString() });
|
||||
accounts.push({
|
||||
email: identity.email,
|
||||
phoneNumber: identity.phoneNumber,
|
||||
accountIdentifierType: identity.accountIdentifierType,
|
||||
accountIdentifier: identity.accountIdentifier,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
await setState({ accounts });
|
||||
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
@@ -42,14 +98,23 @@
|
||||
logMessage: '步骤 3:密码页内容脚本未就绪,正在等待页面恢复...',
|
||||
});
|
||||
|
||||
const identityLabel = identity.accountIdentifierType === 'phone'
|
||||
? `注册手机号为 ${identity.accountIdentifier}`
|
||||
: `邮箱为 ${identity.accountIdentifier}`;
|
||||
await addLog(
|
||||
`步骤 3:正在填写密码,邮箱为 ${resolvedEmail},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
|
||||
`步骤 3:正在填写密码,${identityLabel},密码为${state.customPassword ? '自定义' : '自动生成'}(${password.length} 位)`
|
||||
);
|
||||
await sendToContentScript('signup-page', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 3,
|
||||
source: 'background',
|
||||
payload: { email: resolvedEmail, password },
|
||||
payload: {
|
||||
email: identity.email,
|
||||
phoneNumber: identity.phoneNumber,
|
||||
accountIdentifierType: identity.accountIdentifierType,
|
||||
accountIdentifier: identity.accountIdentifier,
|
||||
password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,26 @@
|
||||
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/plus-checkout.js'];
|
||||
const PLUS_CHECKOUT_URL_PATTERN = /^https:\/\/chatgpt\.com\/checkout(?:\/|$)/i;
|
||||
const PLUS_CHECKOUT_FRAME_READY_DELAY_MS = 500;
|
||||
const PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS = 3;
|
||||
const PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS = 20000;
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gopay.hwork.pro';
|
||||
const PAYMENT_METHOD_CONFIGS = {
|
||||
[PLUS_PAYMENT_METHOD_PAYPAL]: {
|
||||
id: PLUS_PAYMENT_METHOD_PAYPAL,
|
||||
label: 'PayPal',
|
||||
selectMessageType: 'PLUS_CHECKOUT_SELECT_PAYPAL',
|
||||
redirectPattern: /paypal\./i,
|
||||
},
|
||||
[PLUS_PAYMENT_METHOD_GOPAY]: {
|
||||
id: PLUS_PAYMENT_METHOD_GOPAY,
|
||||
label: 'GoPay',
|
||||
selectMessageType: 'PLUS_CHECKOUT_SELECT_GOPAY',
|
||||
redirectPattern: /gopay|gojek|midtrans|xendit|stripe|checkout/i,
|
||||
},
|
||||
};
|
||||
const MEIGUODIZHI_ADDRESS_ENDPOINT = 'https://www.meiguodizhi.com/api/v1/dz';
|
||||
const MEIGUODIZHI_COUNTRY_CONFIG = {
|
||||
AR: { path: '/ar-address', city: 'Buenos Aires', aliases: ['ar', 'argentina', '阿根廷'] },
|
||||
@@ -16,6 +36,7 @@
|
||||
FR: { path: '/fr-address', city: 'Paris', aliases: ['fr', 'fra', 'france', '法国'] },
|
||||
GB: { path: '/uk-address', city: 'London', aliases: ['gb', 'uk', 'united kingdom', 'britain', 'england', '英国'] },
|
||||
HK: { path: '/hk-address', city: 'Hong Kong', aliases: ['hk', 'hong kong', '香港'] },
|
||||
ID: { path: '/id-address', city: 'Jakarta', aliases: ['id', 'indonesia', '印度尼西亚', '印尼'] },
|
||||
IT: { path: '/it-address', city: 'Rome', aliases: ['it', 'ita', 'italy', '意大利'] },
|
||||
JP: { path: '/jp-address', city: 'Tokyo', aliases: ['jp', 'jpn', 'japan', '日本', '日本国'] },
|
||||
KR: { path: '/kr-address', city: 'Seoul', aliases: ['kr', 'kor', 'korea', 'south korea', '韩国'] },
|
||||
@@ -34,18 +55,22 @@
|
||||
function createPlusCheckoutBillingExecutor(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
broadcastDataUpdate,
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
fetch: fetchImpl = null,
|
||||
generateRandomName,
|
||||
getAddressSeedForCountry,
|
||||
getState,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
markCurrentRegistrationAccountUsed,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
waitForTabCompleteUntilStopped,
|
||||
waitForTabUrlMatchUntilStopped,
|
||||
probeIpProxyExit = null,
|
||||
throwIfStopped = () => {},
|
||||
} = deps;
|
||||
|
||||
function isPlusCheckoutUrl(url = '') {
|
||||
@@ -56,10 +81,307 @@
|
||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function isGpcHelperCheckout(state = {}) {
|
||||
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GPC_HELPER
|
||||
|| (normalizeText(state?.plusCheckoutSource) === PLUS_PAYMENT_METHOD_GPC_HELPER
|
||||
&& Boolean(state?.gopayHelperReferenceId));
|
||||
}
|
||||
|
||||
function compactCountryText(value = '') {
|
||||
return normalizeText(value).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
|
||||
}
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) {
|
||||
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return PLUS_PAYMENT_METHOD_GPC_HELPER;
|
||||
}
|
||||
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
}
|
||||
|
||||
function getPaymentMethodConfig(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
return PAYMENT_METHOD_CONFIGS[normalizePlusPaymentMethod(method)] || PAYMENT_METHOD_CONFIGS[PLUS_PAYMENT_METHOD_PAYPAL];
|
||||
}
|
||||
|
||||
function normalizeGpcHelperBaseUrl(apiUrl = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizeGpcHelperBaseUrl) {
|
||||
return rootScope.GoPayUtils.normalizeGpcHelperBaseUrl(apiUrl);
|
||||
}
|
||||
let normalized = String(apiUrl || DEFAULT_GPC_HELPER_API_URL).trim().replace(/\/+$/g, '');
|
||||
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
|
||||
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, '');
|
||||
normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, '');
|
||||
return normalized || DEFAULT_GPC_HELPER_API_URL;
|
||||
}
|
||||
|
||||
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
|
||||
const fetcher = typeof fetchImpl === 'function'
|
||||
? fetchImpl
|
||||
: (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
|
||||
if (typeof fetcher !== 'function') {
|
||||
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;
|
||||
try {
|
||||
const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) });
|
||||
const data = await response.json().catch(() => ({}));
|
||||
return { response, data };
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function buildGpcOtpPayload(input = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.buildGpcOtpPayload) {
|
||||
return rootScope.GoPayUtils.buildGpcOtpPayload(input);
|
||||
}
|
||||
const payload = {
|
||||
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
|
||||
otp: String(input.otp ?? input.code ?? '').trim().replace(/[^\d]/g, ''),
|
||||
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
|
||||
};
|
||||
const gopayGuid = String(input.gopay_guid ?? input.gopayGuid ?? '').trim();
|
||||
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
|
||||
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
|
||||
if (flowId) payload.flow_id = flowId;
|
||||
if (gopayGuid) payload.gopay_guid = gopayGuid;
|
||||
if (redirectUrl) payload.redirect_url = redirectUrl;
|
||||
return payload;
|
||||
}
|
||||
|
||||
function buildGpcOtpRetryPayload(input = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.buildGpcOtpRetryPayload) {
|
||||
return rootScope.GoPayUtils.buildGpcOtpRetryPayload(input);
|
||||
}
|
||||
const basePayload = buildGpcOtpPayload(input);
|
||||
return { ...basePayload, code: basePayload.otp };
|
||||
}
|
||||
|
||||
function buildGpcPinPayload(input = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.buildGpcPinPayload) {
|
||||
return rootScope.GoPayUtils.buildGpcPinPayload(input);
|
||||
}
|
||||
const payload = {
|
||||
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
|
||||
challenge_id: String(input.challenge_id ?? input.challengeId ?? '').trim(),
|
||||
gopay_guid: String(input.gopay_guid ?? input.gopayGuid ?? '').trim(),
|
||||
pin: String(input.pin ?? '').trim().replace(/[^\d]/g, ''),
|
||||
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
|
||||
};
|
||||
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
|
||||
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
|
||||
if (flowId) payload.flow_id = flowId;
|
||||
if (redirectUrl) payload.redirect_url = redirectUrl;
|
||||
return payload;
|
||||
}
|
||||
|
||||
function buildGpcPinRetryPayload(input = {}) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.buildGpcPinRetryPayload) {
|
||||
return rootScope.GoPayUtils.buildGpcPinRetryPayload(input);
|
||||
}
|
||||
const basePayload = buildGpcPinPayload(input);
|
||||
return { ...basePayload, challengeId: basePayload.challenge_id };
|
||||
}
|
||||
|
||||
function getGpcResponseErrorDetail(payload = {}, status = 0) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.extractGpcResponseErrorDetail) {
|
||||
return rootScope.GoPayUtils.extractGpcResponseErrorDetail(payload, status);
|
||||
}
|
||||
if (payload && typeof payload === 'object') {
|
||||
return payload.detail || payload.message || payload.error || payload.error_description || payload.reason || `HTTP ${status || 0}`;
|
||||
}
|
||||
return `HTTP ${status || 0}`;
|
||||
}
|
||||
|
||||
async function postGpcJsonWithFallback(apiUrl, endpointPath, primaryPayload, fallbackPayload, timeoutMs = 30000) {
|
||||
const requestUrl = `${apiUrl}${endpointPath}`;
|
||||
const send = (payload) => fetchJsonWithTimeout(requestUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}, timeoutMs);
|
||||
const firstResponse = await send(primaryPayload);
|
||||
if (firstResponse?.response?.ok || !fallbackPayload) {
|
||||
return { ...firstResponse, retried: false, payload: primaryPayload };
|
||||
}
|
||||
const status = Number(firstResponse?.response?.status || 0);
|
||||
if (status !== 400 && status !== 422) {
|
||||
return { ...firstResponse, retried: false, payload: primaryPayload };
|
||||
}
|
||||
const firstDetail = getGpcResponseErrorDetail(firstResponse?.data, status);
|
||||
await addLog(`步骤 7:GPC 接口返回 ${status}(${firstDetail}),使用兼容字段重试。`, 'warn');
|
||||
const secondResponse = await send(fallbackPayload);
|
||||
return {
|
||||
...secondResponse,
|
||||
retried: true,
|
||||
payload: fallbackPayload,
|
||||
firstError: firstDetail,
|
||||
firstStatus: status,
|
||||
};
|
||||
}
|
||||
|
||||
function getStateInternal() {
|
||||
if (typeof getState === 'function') {
|
||||
return getState();
|
||||
}
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
async function requestGpcOtpInput({ title = '', message = '', referenceId = '' }) {
|
||||
const requestId = `otp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const payload = {
|
||||
plusManualConfirmationPending: true,
|
||||
plusManualConfirmationRequestId: requestId,
|
||||
plusManualConfirmationStep: 7,
|
||||
plusManualConfirmationMethod: 'gopay-otp',
|
||||
plusManualConfirmationTitle: title || 'GPC OTP 验证',
|
||||
plusManualConfirmationMessage: message || '请输入 OTP 验证码',
|
||||
gopayHelperOtpRequestId: requestId,
|
||||
gopayHelperOtpReferenceId: referenceId,
|
||||
gopayHelperResolvedOtp: '',
|
||||
};
|
||||
await setState(payload);
|
||||
if (typeof broadcastDataUpdate === 'function') {
|
||||
broadcastDataUpdate(payload);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkInterval = setInterval(async () => {
|
||||
try {
|
||||
throwIfStopped();
|
||||
const currentState = await getStateInternal();
|
||||
if (!currentState?.plusManualConfirmationPending || currentState?.plusManualConfirmationRequestId !== requestId) {
|
||||
clearInterval(checkInterval);
|
||||
const resolvedOtp = String(currentState?.gopayHelperResolvedOtp || '').trim().replace(/[^\d]/g, '');
|
||||
if (resolvedOtp) {
|
||||
resolve(resolvedOtp);
|
||||
} else {
|
||||
reject(new Error('OTP 输入已取消'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(checkInterval);
|
||||
reject(error);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
async function executeGpcHelperBilling(state = {}) {
|
||||
const referenceId = String(state?.gopayHelperReferenceId || '').trim();
|
||||
const apiUrl = normalizeGpcHelperBaseUrl(state?.gopayHelperApiUrl || '');
|
||||
const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim();
|
||||
if (!referenceId) {
|
||||
throw new Error('步骤 7:GPC 模式缺少 reference_id,请先执行步骤 6。');
|
||||
}
|
||||
if (!apiUrl) {
|
||||
throw new Error('步骤 7:GPC 模式缺少 API 地址。');
|
||||
}
|
||||
if (!cardKey) {
|
||||
throw new Error('步骤 7:GPC 模式缺少卡密。');
|
||||
}
|
||||
await addLog(`步骤 7:GPC 模式开始 OTP 验证(reference_id: ${referenceId})...`, 'info');
|
||||
await addLog('步骤 7:等待用户输入 OTP...', 'info');
|
||||
const otp = await requestGpcOtpInput({
|
||||
title: 'GPC OTP 验证',
|
||||
message: `请输入收到的 OTP 验证码(reference_id: ${referenceId})`,
|
||||
referenceId,
|
||||
});
|
||||
|
||||
const flowId = state?.gopayHelperFlowId
|
||||
|| state?.gopayHelperStartPayload?.flow_id
|
||||
|| state?.gopayHelperStartPayload?.flowId
|
||||
|| state?.gopayHelperBalancePayload?.flow_id
|
||||
|| state?.gopayHelperBalancePayload?.flowId
|
||||
|| '';
|
||||
const baseInput = {
|
||||
reference_id: referenceId,
|
||||
otp,
|
||||
card_key: cardKey,
|
||||
gopay_guid: state?.gopayHelperGoPayGuid || '',
|
||||
redirect_url: state?.gopayHelperRedirectUrl || '',
|
||||
flow_id: flowId,
|
||||
};
|
||||
await addLog('步骤 7:正在提交 OTP...', 'info');
|
||||
const otpResponse = await postGpcJsonWithFallback(
|
||||
apiUrl,
|
||||
'/api/gopay/otp',
|
||||
buildGpcOtpPayload(baseInput),
|
||||
buildGpcOtpRetryPayload(baseInput),
|
||||
30000
|
||||
);
|
||||
if (!otpResponse?.response?.ok) {
|
||||
throw new Error(`步骤 7:OTP 验证失败:${getGpcResponseErrorDetail(otpResponse?.data, otpResponse?.response?.status || 0)}`);
|
||||
}
|
||||
|
||||
const otpData = otpResponse?.data || {};
|
||||
const challengeId = String(
|
||||
otpData?.challenge_id
|
||||
|| otpData?.challengeId
|
||||
|| state?.gopayHelperChallengeId
|
||||
|| state?.gopayHelperStartPayload?.challenge_id
|
||||
|| state?.gopayHelperStartPayload?.challengeId
|
||||
|| ''
|
||||
).trim();
|
||||
const nextFlowId = String(otpData?.flow_id || otpData?.flowId || baseInput.flow_id || '').trim();
|
||||
const gopayGuid = String(otpData?.gopay_guid || otpData?.gopayGuid || state?.gopayHelperGoPayGuid || '').trim();
|
||||
const redirectUrl = String(otpData?.redirect_url || otpData?.redirectUrl || state?.gopayHelperRedirectUrl || '').trim();
|
||||
if (!challengeId) {
|
||||
throw new Error('步骤 7:GPC OTP 验证后未返回 challenge_id。');
|
||||
}
|
||||
const pin = String(state?.gopayHelperPin || '').trim().replace(/[^\d]/g, '');
|
||||
if (!pin) {
|
||||
throw new Error('步骤 7:GPC 模式缺少 PIN 配置。');
|
||||
}
|
||||
|
||||
await setState({
|
||||
gopayHelperChallengeId: challengeId,
|
||||
gopayHelperFlowId: nextFlowId,
|
||||
gopayHelperGoPayGuid: gopayGuid,
|
||||
gopayHelperRedirectUrl: redirectUrl,
|
||||
});
|
||||
|
||||
await addLog('步骤 7:正在提交 PIN...', 'info');
|
||||
const pinInput = {
|
||||
reference_id: referenceId,
|
||||
challenge_id: challengeId,
|
||||
gopay_guid: gopayGuid,
|
||||
redirect_url: redirectUrl,
|
||||
flow_id: nextFlowId,
|
||||
pin,
|
||||
card_key: cardKey,
|
||||
};
|
||||
const pinResponse = await postGpcJsonWithFallback(
|
||||
apiUrl,
|
||||
'/api/gopay/pin',
|
||||
buildGpcPinPayload(pinInput),
|
||||
buildGpcPinRetryPayload(pinInput),
|
||||
30000
|
||||
);
|
||||
if (!pinResponse?.response?.ok) {
|
||||
throw new Error(`步骤 7:PIN 验证失败:${getGpcResponseErrorDetail(pinResponse?.data, pinResponse?.response?.status || 0)}`);
|
||||
}
|
||||
|
||||
await setState({
|
||||
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||
gopayHelperPinPayload: pinResponse?.data || null,
|
||||
});
|
||||
await addLog('步骤 7:GPC 支付完成,准备继续下一步。', 'ok');
|
||||
await completeStepFromBackground(7, {
|
||||
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveMeiguodizhiCountryCode(value = '') {
|
||||
const normalized = normalizeText(value);
|
||||
const upper = normalized.toUpperCase();
|
||||
@@ -71,7 +393,7 @@
|
||||
compact === code.toLowerCase()
|
||||
|| (config.aliases || []).some((alias) => {
|
||||
const compactAlias = compactCountryText(alias);
|
||||
return compact === compactAlias || compact.includes(compactAlias);
|
||||
return compact === compactAlias || (compactAlias.length >= 4 && compact.includes(compactAlias));
|
||||
})
|
||||
));
|
||||
return match?.[0] || '';
|
||||
@@ -86,11 +408,31 @@
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePostalCodeForCountry(countryCode, rawPostalCode = '', fallbackPostalCode = '') {
|
||||
const normalizedCountry = resolveMeiguodizhiCountryCode(countryCode) || normalizeText(countryCode).toUpperCase();
|
||||
const postalCode = normalizeText(rawPostalCode);
|
||||
const fallback = normalizeText(fallbackPostalCode);
|
||||
if (normalizedCountry !== 'KR') {
|
||||
return postalCode;
|
||||
}
|
||||
if (/^\d{5}$/.test(postalCode)) {
|
||||
return postalCode;
|
||||
}
|
||||
if (/^\d{5}$/.test(fallback)) {
|
||||
return fallback;
|
||||
}
|
||||
return '04524';
|
||||
}
|
||||
|
||||
function buildDirectAddressSeed(countryCode, apiAddress, fallbackSeed) {
|
||||
const address1 = normalizeText(apiAddress?.Trans_Address || apiAddress?.Address);
|
||||
const city = normalizeText(apiAddress?.City);
|
||||
const region = normalizeText(apiAddress?.State_Full || apiAddress?.State);
|
||||
const postalCode = normalizeText(apiAddress?.Zip_Code);
|
||||
const postalCode = normalizePostalCodeForCountry(
|
||||
countryCode,
|
||||
apiAddress?.Zip_Code,
|
||||
fallbackSeed?.fallback?.postalCode
|
||||
);
|
||||
if (!address1 || !city || !postalCode) {
|
||||
return null;
|
||||
}
|
||||
@@ -168,9 +510,47 @@
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveBillingAddressSeed(state = {}, countryOverride = '') {
|
||||
const requestedCountry = normalizeText(countryOverride || state.plusCheckoutCountry || 'DE');
|
||||
const countryCode = resolveMeiguodizhiCountryCode(requestedCountry) || 'DE';
|
||||
function resolveBillingAddressCountry(state = {}, countryOverride = '', paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const normalizedPaymentMethod = normalizePlusPaymentMethod(paymentMethod || state?.plusPaymentMethod);
|
||||
const checkoutCountry = resolveMeiguodizhiCountryCode(countryOverride);
|
||||
const savedCheckoutCountry = resolveMeiguodizhiCountryCode(state.plusCheckoutCountry);
|
||||
const exitCountry = resolveMeiguodizhiCountryCode(
|
||||
state.ipProxyAppliedExitRegion
|
||||
|| state.ipProxyExitRegion
|
||||
|| ''
|
||||
);
|
||||
|
||||
if (normalizedPaymentMethod === PLUS_PAYMENT_METHOD_GOPAY) {
|
||||
const countryCode = exitCountry || checkoutCountry || savedCheckoutCountry || 'ID';
|
||||
return {
|
||||
countryCode,
|
||||
requestedCountry: exitCountry
|
||||
|| normalizeText(countryOverride)
|
||||
|| normalizeText(state.plusCheckoutCountry)
|
||||
|| 'ID',
|
||||
source: exitCountry ? 'proxy_exit' : (checkoutCountry ? 'checkout_page' : (savedCheckoutCountry ? 'checkout_state' : 'gopay_fallback')),
|
||||
};
|
||||
}
|
||||
|
||||
const countryCode = checkoutCountry || savedCheckoutCountry || exitCountry || 'DE';
|
||||
return {
|
||||
countryCode,
|
||||
requestedCountry: normalizeText(countryOverride)
|
||||
|| normalizeText(state.plusCheckoutCountry)
|
||||
|| exitCountry
|
||||
|| 'DE',
|
||||
source: checkoutCountry ? 'checkout_page' : (savedCheckoutCountry ? 'checkout_state' : (exitCountry ? 'proxy_exit' : 'paypal_fallback')),
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveBillingAddressSeed(state = {}, countryOverride = '', options = {}) {
|
||||
const paymentMethod = normalizePlusPaymentMethod(options.paymentMethod || state?.plusPaymentMethod);
|
||||
const countryResolution = resolveBillingAddressCountry(state, countryOverride, paymentMethod);
|
||||
const countryCode = countryResolution.countryCode;
|
||||
const requestedCountry = countryResolution.requestedCountry;
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY && countryResolution.source === 'proxy_exit') {
|
||||
await addLog(`步骤 7:GoPay 账单地址将按当前代理出口地区 ${countryCode} 填写。`, 'info');
|
||||
}
|
||||
const localSeed = getLocalAddressSeed(countryCode);
|
||||
const lookupSeed = localSeed || buildMeiguodizhiLookupSeed(countryCode);
|
||||
if (!lookupSeed) {
|
||||
@@ -294,6 +674,33 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForPaymentRedirectAfterSubmit(tabId, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const paymentConfig = getPaymentMethodConfig(paymentMethod);
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS) {
|
||||
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||||
if (!tab) {
|
||||
throw new Error(`步骤 7:checkout 标签页已关闭,无法继续等待 ${paymentConfig.label} 跳转。`);
|
||||
}
|
||||
const url = String(tab.url || '');
|
||||
if (paymentConfig.redirectPattern.test(url) && !isPlusCheckoutUrl(url)) {
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
await sleepWithStop(1000);
|
||||
return true;
|
||||
}
|
||||
if (url && !isPlusCheckoutUrl(url)) {
|
||||
await addLog(`步骤 7:点击订阅后页面跳转到非 ${paymentConfig.label} 识别地址:${url}`, 'warn');
|
||||
return false;
|
||||
}
|
||||
await sleepWithStop(500);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function waitForPayPalRedirectAfterSubmit(tabId) {
|
||||
return waitForPaymentRedirectAfterSubmit(tabId, PLUS_PAYMENT_METHOD_PAYPAL);
|
||||
}
|
||||
|
||||
async function inspectCheckoutFrame(tabId, frame) {
|
||||
try {
|
||||
const result = await sendFrameMessage(tabId, frame.frameId, {
|
||||
@@ -329,6 +736,7 @@
|
||||
.map((item) => {
|
||||
const flags = [];
|
||||
if (item.result?.hasPayPal) flags.push('paypal');
|
||||
if (item.result?.hasGoPay) flags.push('gopay');
|
||||
if (item.result?.billingFieldsVisible) flags.push('billing');
|
||||
if (item.result?.hasSubscribeButton) flags.push('subscribe');
|
||||
if (!flags.length && item.error) flags.push(item.error);
|
||||
@@ -348,7 +756,13 @@
|
||||
return inspections;
|
||||
}
|
||||
|
||||
function pickPaymentFrame(inspections) {
|
||||
function pickPaymentFrame(inspections, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const normalizedPaymentMethod = normalizePlusPaymentMethod(paymentMethod);
|
||||
if (normalizedPaymentMethod === PLUS_PAYMENT_METHOD_GOPAY) {
|
||||
return inspections.find((item) => item.result?.hasGoPay || item.result?.gopayCandidates?.length)
|
||||
|| inspections.find((item) => isPaymentFrameUrl(item.frame.url))
|
||||
|| null;
|
||||
}
|
||||
return inspections.find((item) => item.result?.hasPayPal || item.result?.paypalCandidates?.length)
|
||||
|| inspections.find((item) => isPaymentFrameUrl(item.frame.url))
|
||||
|| null;
|
||||
@@ -366,6 +780,44 @@
|
||||
|| null;
|
||||
}
|
||||
|
||||
function findCheckoutAmountInspection(inspections = []) {
|
||||
return inspections.find((item) => item.result?.checkoutAmountSummary?.hasTodayDue)
|
||||
|| null;
|
||||
}
|
||||
|
||||
async function inspectCheckoutAmountSummary(tabId) {
|
||||
const frames = await getReadyCheckoutFrames(tabId);
|
||||
const inspections = await inspectCheckoutFrames(tabId, frames);
|
||||
const amountInspection = findCheckoutAmountInspection(inspections);
|
||||
return amountInspection?.result?.checkoutAmountSummary || null;
|
||||
}
|
||||
|
||||
async function ensureFreeTrialAmount(tabId, state = {}, options = {}) {
|
||||
const phaseLabel = String(options.phaseLabel || '').trim() || '提交前';
|
||||
const amountSummary = await inspectCheckoutAmountSummary(tabId);
|
||||
if (!amountSummary?.hasTodayDue) {
|
||||
await addLog(`步骤 7:${phaseLabel}未能识别 checkout 的“今日应付金额”,为避免误判将继续执行。`, 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
if (amountSummary.isZero) {
|
||||
await addLog(`步骤 7:${phaseLabel}已确认今日应付金额为 ${amountSummary.rawAmount || '0'},继续执行。`, 'ok');
|
||||
return;
|
||||
}
|
||||
|
||||
const amountLabel = amountSummary.rawAmount || (
|
||||
Number.isFinite(Number(amountSummary.amount)) ? String(amountSummary.amount) : '未知金额'
|
||||
);
|
||||
await addLog(`步骤 7:${phaseLabel}检测到今日应付金额不是 0(${amountLabel}),说明当前账号没有免费试用资格,将跳过支付提交。`, 'warn');
|
||||
if (typeof markCurrentRegistrationAccountUsed === 'function') {
|
||||
await markCurrentRegistrationAccountUsed(state, {
|
||||
reason: 'plus-checkout-non-free-trial',
|
||||
logPrefix: 'Plus Checkout:当前账号没有免费试用资格',
|
||||
});
|
||||
}
|
||||
throw new Error(`PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 7:今日应付金额不是 0(${amountLabel}),当前账号没有免费试用资格,已跳过支付提交。`);
|
||||
}
|
||||
|
||||
async function getReadyCheckoutFrames(tabId) {
|
||||
return ensurePlusCheckoutFramesReady(tabId, await getCheckoutFrames(tabId));
|
||||
}
|
||||
@@ -383,9 +835,9 @@
|
||||
};
|
||||
}
|
||||
|
||||
async function resolvePaymentFrame(tabId, frames) {
|
||||
async function resolvePaymentFrame(tabId, frames, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const inspections = await inspectCheckoutFrames(tabId, frames);
|
||||
const picked = pickPaymentFrame(inspections);
|
||||
const picked = pickPaymentFrame(inspections, paymentMethod);
|
||||
if (picked) {
|
||||
return {
|
||||
frameId: picked.frame.frameId,
|
||||
@@ -457,6 +909,12 @@
|
||||
}
|
||||
|
||||
async function executePlusCheckoutBilling(state = {}) {
|
||||
if (isGpcHelperCheckout(state)) {
|
||||
await executeGpcHelperBilling(state);
|
||||
return;
|
||||
}
|
||||
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
|
||||
const paymentConfig = getPaymentMethodConfig(paymentMethod);
|
||||
const tabId = await getCheckoutTabId(state);
|
||||
await addLog('步骤 7:正在等待 Plus Checkout 页面加载完成...', 'info');
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
@@ -468,27 +926,30 @@
|
||||
logMessage: '步骤 7:Checkout 页面仍在加载,等待账单填写脚本就绪...',
|
||||
});
|
||||
const readyFrames = await getReadyCheckoutFrames(tabId);
|
||||
const paymentFrame = await resolvePaymentFrame(tabId, readyFrames);
|
||||
await ensureFreeTrialAmount(tabId, state, {
|
||||
phaseLabel: 'Checkout 页面加载后',
|
||||
});
|
||||
const paymentFrame = await resolvePaymentFrame(tabId, readyFrames, paymentMethod);
|
||||
if (paymentFrame.frameId === null) {
|
||||
const frameSummary = buildFrameSummary(paymentFrame.inspections);
|
||||
throw new Error(`步骤 7:未在主页面或 iframe 中发现 PayPal DOM,无法自动切换付款方式。frame 摘要:${frameSummary}`);
|
||||
throw new Error(`步骤 7:未在主页面或 iframe 中发现 ${paymentConfig.label} DOM,无法自动切换付款方式。frame 摘要:${frameSummary}`);
|
||||
}
|
||||
if (!paymentFrame.ready) {
|
||||
throw new Error(`步骤 7:已定位到 PayPal 所在 iframe(frameId=${paymentFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
|
||||
throw new Error(`步骤 7:已定位到 ${paymentConfig.label} 所在 iframe(frameId=${paymentFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
|
||||
}
|
||||
|
||||
if (paymentFrame.frameId !== 0) {
|
||||
await addLog(`步骤 7:PayPal 位于 checkout iframe(frameId=${paymentFrame.frameId}),将改为在该 frame 内操作。`, 'info');
|
||||
await addLog(`步骤 7:${paymentConfig.label} 位于 checkout iframe(frameId=${paymentFrame.frameId}),将改为在该 frame 内操作。`, 'info');
|
||||
}
|
||||
|
||||
const randomName = generateRandomName();
|
||||
const fullName = [randomName.firstName, randomName.lastName].filter(Boolean).join(' ');
|
||||
|
||||
await addLog('步骤 7:正在切换 PayPal 付款方式...', 'info');
|
||||
await addLog(`步骤 7:正在切换 ${paymentConfig.label} 付款方式...`, 'info');
|
||||
const paymentResult = await sendFrameMessage(tabId, paymentFrame.frameId, {
|
||||
type: 'PLUS_CHECKOUT_SELECT_PAYPAL',
|
||||
type: paymentConfig.selectMessageType,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
payload: { paymentMethod },
|
||||
});
|
||||
if (paymentResult?.error) {
|
||||
throw new Error(paymentResult.error);
|
||||
@@ -502,7 +963,67 @@
|
||||
await addLog(`步骤 7:账单地址位于 checkout iframe(frameId=${billingFrame.frameId}),将改为在该 frame 内填写。`, 'info');
|
||||
}
|
||||
|
||||
const addressSeed = await resolveBillingAddressSeed(state, billingFrame.countryText);
|
||||
let billingState = state;
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY && typeof probeIpProxyExit === 'function') {
|
||||
const staleExitRegion = normalizeText(
|
||||
state?.ipProxyAppliedExitRegion
|
||||
|| state?.ipProxyExitRegion
|
||||
|| ''
|
||||
);
|
||||
try {
|
||||
await addLog('步骤 7:GoPay 账单地址准备按代理出口填写,正在重新检测当前出口地区...', 'info');
|
||||
const probeResult = await probeIpProxyExit({
|
||||
state,
|
||||
timeoutMs: 12000,
|
||||
authRebindRetry: true,
|
||||
detectWhenDisabled: true,
|
||||
});
|
||||
const routing = probeResult?.proxyRouting || {};
|
||||
const probedExitRegion = normalizeText(routing.exitRegion || '');
|
||||
const probedExitIp = normalizeText(routing.exitIp || '');
|
||||
const probedExitSource = normalizeText(routing.exitSource || '');
|
||||
const probeEndpoint = normalizeText(routing.endpoint || routing.exitEndpoint || '');
|
||||
const probeReason = normalizeText(routing.reason || '');
|
||||
const probeError = normalizeText(routing.exitError || routing.error || '');
|
||||
if (probedExitRegion) {
|
||||
billingState = {
|
||||
...(state || {}),
|
||||
ipProxyAppliedExitRegion: probedExitRegion,
|
||||
ipProxyExitRegion: probedExitRegion,
|
||||
ipProxyAppliedExitIp: probedExitIp,
|
||||
ipProxyAppliedExitSource: probedExitSource,
|
||||
};
|
||||
const sourceSuffix = probedExitSource ? `,来源 ${probedExitSource}` : '';
|
||||
const endpointSuffix = probeEndpoint ? `,检测地址 ${probeEndpoint}` : '';
|
||||
await addLog(`步骤 7:当前代理出口复测结果:${probedExitRegion}${probedExitIp ? ` / ${probedExitIp}` : ''}${sourceSuffix}${endpointSuffix}。`, 'info');
|
||||
} else {
|
||||
billingState = {
|
||||
...(state || {}),
|
||||
ipProxyAppliedExitRegion: '',
|
||||
ipProxyExitRegion: '',
|
||||
ipProxyAppliedExitIp: probedExitIp,
|
||||
ipProxyAppliedExitSource: probedExitSource,
|
||||
};
|
||||
await addLog(
|
||||
`步骤 7:代理出口复测没有返回国家/地区代码,已清空旧出口地区${staleExitRegion ? ` ${staleExitRegion}` : ''},不会继续沿用旧地区。${probeReason ? `状态:${probeReason}。` : ''}${probeError ? `诊断:${probeError}` : ''}`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
billingState = {
|
||||
...(state || {}),
|
||||
ipProxyAppliedExitRegion: '',
|
||||
ipProxyExitRegion: '',
|
||||
};
|
||||
await addLog(`步骤 7:代理出口复测失败,已清空旧出口地区${staleExitRegion ? ` ${staleExitRegion}` : ''},不会继续沿用旧地区:${error?.message || String(error || '未知错误')}`, 'warn');
|
||||
}
|
||||
}
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GOPAY
|
||||
&& typeof probeIpProxyExit === 'function'
|
||||
&& !resolveMeiguodizhiCountryCode(billingState?.ipProxyAppliedExitRegion || billingState?.ipProxyExitRegion || '')) {
|
||||
throw new Error('步骤 7:GoPay 账单地址需要当前代理出口国家/地区,但本次复测没有拿到国家码;已停止填写,避免误用旧的 KR/ID 地区。请先点 IP 代理“检测出口”,确认显示 JP 后再继续。');
|
||||
}
|
||||
const addressSeed = await resolveBillingAddressSeed(billingState, billingFrame.countryText, { paymentMethod });
|
||||
if (!addressSeed) {
|
||||
throw new Error('步骤 7:未找到可用的本地账单地址种子。');
|
||||
}
|
||||
@@ -535,8 +1056,9 @@
|
||||
addressSeed,
|
||||
},
|
||||
});
|
||||
if (suggestionResult?.error) {
|
||||
throw new Error(suggestionResult.error);
|
||||
const suggestionError = suggestionResult?.error || '';
|
||||
if (suggestionError) {
|
||||
await addLog(`步骤 7:Google 地址推荐不可用,将改用本地地址字段兜底:${suggestionError}`, 'warn');
|
||||
}
|
||||
|
||||
const structuredResult = await sendFrameMessage(tabId, billingFrame.frameId, {
|
||||
@@ -544,6 +1066,7 @@
|
||||
source: 'background',
|
||||
payload: {
|
||||
addressSeed,
|
||||
overwriteStructuredAddress: Boolean(suggestionError),
|
||||
},
|
||||
});
|
||||
if (structuredResult?.error) {
|
||||
@@ -552,7 +1075,7 @@
|
||||
|
||||
result = {
|
||||
...structuredResult,
|
||||
selectedAddressText: suggestionResult?.selectedAddressText || '',
|
||||
selectedAddressText: suggestionError ? '' : (suggestionResult?.selectedAddressText || ''),
|
||||
};
|
||||
} else {
|
||||
result = await sendFrameMessage(tabId, billingFrame.frameId, {
|
||||
@@ -569,31 +1092,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
await addLog('步骤 7:账单地址已填写完成,正在定位订阅按钮...', 'info');
|
||||
const subscribeFrame = await waitForSubscribeFrame(tabId, [
|
||||
{ frameId: 0, url: '' },
|
||||
{ frameId: paymentFrame.frameId, url: paymentFrame.frameUrl || '' },
|
||||
{ frameId: billingFrame.frameId, url: billingFrame.frameUrl || '' },
|
||||
]);
|
||||
const subscribeResult = await sendFrameMessage(tabId, subscribeFrame.frameId, {
|
||||
type: 'PLUS_CHECKOUT_CLICK_SUBSCRIBE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
if (subscribeResult?.error) {
|
||||
throw new Error(subscribeResult.error);
|
||||
}
|
||||
|
||||
await setState({
|
||||
plusCheckoutTabId: tabId,
|
||||
plusBillingCountryText: result?.countryText || '',
|
||||
plusBillingAddress: result?.structuredAddress || null,
|
||||
});
|
||||
await ensureFreeTrialAmount(tabId, state, {
|
||||
phaseLabel: '提交订阅前',
|
||||
});
|
||||
|
||||
await addLog('步骤 7:账单地址已提交,正在等待跳转到 PayPal...', 'info');
|
||||
await waitForTabUrlMatchUntilStopped(tabId, (url) => /paypal\./i.test(url));
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
await sleepWithStop(1000);
|
||||
let redirectedToPayment = false;
|
||||
let lastSubmitError = '';
|
||||
for (let attempt = 1; attempt <= PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS; attempt += 1) {
|
||||
await addLog(
|
||||
attempt === 1
|
||||
? '步骤 7:账单地址已填写完成,等待 3 秒让 checkout 完成校验...'
|
||||
: `步骤 7:准备第 ${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS} 次重新提交账单地址...`,
|
||||
attempt === 1 ? 'info' : 'warn'
|
||||
);
|
||||
await sleepWithStop(3000);
|
||||
await addLog('步骤 7:正在定位订阅按钮...', 'info');
|
||||
const subscribeFrame = await waitForSubscribeFrame(tabId, [
|
||||
{ frameId: 0, url: '' },
|
||||
{ frameId: paymentFrame.frameId, url: paymentFrame.frameUrl || '' },
|
||||
{ frameId: billingFrame.frameId, url: billingFrame.frameUrl || '' },
|
||||
]);
|
||||
const subscribeResult = await sendFrameMessage(tabId, subscribeFrame.frameId, {
|
||||
type: 'PLUS_CHECKOUT_CLICK_SUBSCRIBE',
|
||||
source: 'background',
|
||||
payload: {
|
||||
beforeClickDelayMs: attempt === 1 ? 700 : 1200,
|
||||
paymentMethod,
|
||||
},
|
||||
});
|
||||
if (subscribeResult?.error) {
|
||||
lastSubmitError = subscribeResult.error;
|
||||
await addLog(`步骤 7:点击订阅失败(${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS}):${lastSubmitError}`, 'warn');
|
||||
continue;
|
||||
}
|
||||
|
||||
await addLog(`步骤 7:账单地址已提交,正在等待跳转到 ${paymentConfig.label}(${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS})...`, 'info');
|
||||
redirectedToPayment = await waitForPaymentRedirectAfterSubmit(tabId, paymentMethod);
|
||||
if (redirectedToPayment) {
|
||||
break;
|
||||
}
|
||||
lastSubmitError = `提交后 ${Math.round(PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS / 1000)} 秒内未跳转到 ${paymentConfig.label}`;
|
||||
await addLog(`步骤 7:${lastSubmitError},将重试提交。`, 'warn');
|
||||
}
|
||||
|
||||
if (!redirectedToPayment) {
|
||||
throw new Error(`步骤 7:多次提交账单地址后仍未跳转到 ${paymentConfig.label}。${lastSubmitError}`);
|
||||
}
|
||||
|
||||
await completeStepFromBackground(7, {
|
||||
plusBillingCountryText: result?.countryText || '',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -41,12 +41,30 @@
|
||||
}
|
||||
|
||||
async function executeStep7(state) {
|
||||
if (!state.email) {
|
||||
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
||||
}
|
||||
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
const completionStep = visibleStep > 0 ? visibleStep : 7;
|
||||
const resolvedIdentifierType = String(
|
||||
state?.accountIdentifierType
|
||||
|| (state?.signupPhoneNumber ? 'phone' : '')
|
||||
|| ''
|
||||
).trim().toLowerCase() === 'phone'
|
||||
? 'phone'
|
||||
: 'email';
|
||||
const phoneNumber = String(
|
||||
state?.signupPhoneNumber
|
||||
|| (resolvedIdentifierType === 'phone' ? state?.accountIdentifier : '')
|
||||
|| state?.signupPhoneCompletedActivation?.phoneNumber
|
||||
|| state?.signupPhoneActivation?.phoneNumber
|
||||
|| ''
|
||||
).trim();
|
||||
const email = String(
|
||||
state?.email
|
||||
|| (resolvedIdentifierType === 'email' ? state?.accountIdentifier : '')
|
||||
|| ''
|
||||
).trim();
|
||||
if (!email && !phoneNumber) {
|
||||
throw new Error('缺少登录账号:请先完成步骤 2,或在侧栏“注册邮箱/注册手机号”中手动填写账号后再执行当前步骤。');
|
||||
}
|
||||
|
||||
let attempt = 0;
|
||||
let lastError = null;
|
||||
@@ -57,25 +75,53 @@
|
||||
try {
|
||||
const currentState = attempt === 1 ? state : await getState();
|
||||
const password = currentState.password || currentState.customPassword || '';
|
||||
const currentIdentifierType = String(
|
||||
currentState?.accountIdentifierType
|
||||
|| (currentState?.signupPhoneNumber ? 'phone' : '')
|
||||
|| resolvedIdentifierType
|
||||
).trim().toLowerCase() === 'phone'
|
||||
? 'phone'
|
||||
: 'email';
|
||||
const currentPhoneNumber = String(
|
||||
currentState?.signupPhoneNumber
|
||||
|| (currentIdentifierType === 'phone' ? currentState?.accountIdentifier : '')
|
||||
|| currentState?.signupPhoneCompletedActivation?.phoneNumber
|
||||
|| currentState?.signupPhoneActivation?.phoneNumber
|
||||
|| phoneNumber
|
||||
).trim();
|
||||
const currentEmail = String(
|
||||
currentState?.email
|
||||
|| (currentIdentifierType === 'email' ? currentState?.accountIdentifier : '')
|
||||
|| email
|
||||
).trim();
|
||||
const accountIdentifier = currentIdentifierType === 'phone'
|
||||
? currentPhoneNumber
|
||||
: currentEmail;
|
||||
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
|
||||
if (typeof startOAuthFlowTimeoutWindow === 'function') {
|
||||
await startOAuthFlowTimeoutWindow({ step: 7, oauthUrl });
|
||||
await startOAuthFlowTimeoutWindow({ step: completionStep, oauthUrl });
|
||||
}
|
||||
const loginTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(180000, {
|
||||
step: 7,
|
||||
step: completionStep,
|
||||
actionLabel: 'OAuth 登录并进入验证码页',
|
||||
oauthUrl,
|
||||
})
|
||||
: 180000;
|
||||
|
||||
if (attempt === 1) {
|
||||
await addLog('步骤 7:正在打开最新 OAuth 链接并登录...');
|
||||
await addLog('正在打开最新 OAuth 链接并登录...', 'info', {
|
||||
step: completionStep,
|
||||
stepKey: 'oauth-login',
|
||||
});
|
||||
} else {
|
||||
await addLog(`步骤 7:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn');
|
||||
await addLog(`上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn', {
|
||||
step: completionStep,
|
||||
stepKey: 'oauth-login',
|
||||
});
|
||||
}
|
||||
|
||||
await reuseOrCreateTab('signup-page', oauthUrl);
|
||||
await reuseOrCreateTab('signup-page', oauthUrl, { forceNew: true });
|
||||
|
||||
const result = await sendToContentScriptResilient(
|
||||
'signup-page',
|
||||
@@ -84,15 +130,29 @@
|
||||
step: 7,
|
||||
source: 'background',
|
||||
payload: {
|
||||
email: currentState.email,
|
||||
email: currentEmail,
|
||||
phoneNumber: currentPhoneNumber,
|
||||
countryId: currentState?.signupPhoneCompletedActivation?.countryId
|
||||
?? currentState?.signupPhoneActivation?.countryId
|
||||
?? null,
|
||||
countryLabel: String(
|
||||
currentState?.signupPhoneCompletedActivation?.countryLabel
|
||||
|| currentState?.signupPhoneActivation?.countryLabel
|
||||
|| ''
|
||||
).trim(),
|
||||
accountIdentifier,
|
||||
loginIdentifierType: currentIdentifierType,
|
||||
password,
|
||||
visibleStep: completionStep,
|
||||
},
|
||||
},
|
||||
{
|
||||
timeoutMs: loginTimeoutMs,
|
||||
responseTimeoutMs: loginTimeoutMs,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续登录...',
|
||||
logMessage: '认证页正在切换,等待页面重新就绪后继续登录...',
|
||||
logStep: completionStep,
|
||||
logStepKey: 'oauth-login',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -117,11 +177,11 @@
|
||||
|
||||
if (isStep6RecoverableResult(result)) {
|
||||
const reasonMessage = result.message
|
||||
|| `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 7。`;
|
||||
|| `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 ${completionStep}。`;
|
||||
throw new Error(reasonMessage);
|
||||
}
|
||||
|
||||
throw new Error('步骤 7:认证页未返回可识别的登录结果。');
|
||||
throw new Error(`步骤 ${completionStep}:认证页未返回可识别的登录结果。`);
|
||||
} catch (err) {
|
||||
throwIfStopped(err);
|
||||
if (isAddPhoneAuthFailure(err)) {
|
||||
@@ -129,8 +189,9 @@
|
||||
}
|
||||
if (isManagementSecretConfigError(err)) {
|
||||
await addLog(
|
||||
`步骤 7:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
|
||||
'error'
|
||||
`检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
|
||||
'error',
|
||||
{ step: completionStep, stepKey: 'oauth-login' }
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
@@ -139,11 +200,14 @@
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`步骤 7:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn');
|
||||
await addLog(`第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn', {
|
||||
step: completionStep,
|
||||
stepKey: 'oauth-login',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`步骤 7:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
|
||||
throw new Error(`步骤 ${completionStep}:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
|
||||
}
|
||||
|
||||
return { executeStep7 };
|
||||
|
||||
@@ -31,18 +31,31 @@
|
||||
return visibleStep >= 10 ? visibleStep : 10;
|
||||
}
|
||||
|
||||
function parseLocalhostCallback(rawUrl) {
|
||||
function resolveConfirmOauthStep(platformVerifyStep = 10) {
|
||||
return Number(platformVerifyStep) >= 13 ? 12 : 9;
|
||||
}
|
||||
|
||||
function resolveAuthLoginStep(platformVerifyStep = 10) {
|
||||
return Number(platformVerifyStep) >= 13 ? 10 : 7;
|
||||
}
|
||||
|
||||
function addStepLog(step, message, level = 'info') {
|
||||
return addLog(message, level, { step, stepKey: 'platform-verify' });
|
||||
}
|
||||
|
||||
function parseLocalhostCallback(rawUrl, platformVerifyStep = 10) {
|
||||
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(rawUrl);
|
||||
} catch {
|
||||
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 9。');
|
||||
throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
|
||||
const code = normalizeString(parsed.searchParams.get('code'));
|
||||
const state = normalizeString(parsed.searchParams.get('state'));
|
||||
if (!code || !state) {
|
||||
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。');
|
||||
throw new Error(`步骤 ${platformVerifyStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -206,18 +219,20 @@
|
||||
|
||||
async function executeCpaStep10(state) {
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep);
|
||||
const authLoginStep = resolveAuthLoginStep(platformVerifyStep);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.vpsUrl) {
|
||||
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
|
||||
}
|
||||
|
||||
if (shouldBypassStep9ForLocalCpa(state)) {
|
||||
await addLog('步骤 10:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。', 'info');
|
||||
await addStepLog(platformVerifyStep, '检测到本地 CPA,且当前策略为“跳过平台回调验证”,本轮不再重复提交回调地址。', 'info');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
localhostUrl: state.localhostUrl,
|
||||
verifiedStatus: 'local-auto',
|
||||
@@ -225,17 +240,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const callback = parseLocalhostCallback(state.localhostUrl);
|
||||
const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep);
|
||||
const expectedState = normalizeString(state.cpaOAuthState);
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error('CPA 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
|
||||
throw new Error(`CPA 回调 state 与当前授权会话不匹配,请重新执行步骤 ${authLoginStep}。`);
|
||||
}
|
||||
const managementKey = normalizeString(state.vpsPassword);
|
||||
if (!managementKey) {
|
||||
throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
await addLog('步骤 10:正在通过 CPA 管理接口提交回调地址...');
|
||||
await addStepLog(platformVerifyStep, '正在通过 CPA 管理接口提交回调地址...');
|
||||
try {
|
||||
const origin = normalizeString(state.cpaManagementOrigin) || deriveCpaManagementOrigin(state.vpsUrl);
|
||||
const result = await fetchCpaManagementJson(origin, '/v0/management/oauth-callback', {
|
||||
@@ -250,43 +265,45 @@
|
||||
const verifiedStatus = normalizeString(result?.message)
|
||||
|| normalizeString(result?.status_message)
|
||||
|| 'CPA 已通过接口提交回调';
|
||||
await addLog(`步骤 10:${verifiedStatus}`, 'ok');
|
||||
await addStepLog(platformVerifyStep, verifiedStatus, 'ok');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
});
|
||||
} catch (error) {
|
||||
const reason = normalizeString(error?.message) || 'unknown error';
|
||||
await addLog(`步骤 10:CPA 接口提交失败:${reason}`, 'error');
|
||||
await addStepLog(platformVerifyStep, `CPA 接口提交失败:${reason}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeCodex2ApiStep10(state) {
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
const confirmOauthStep = resolveConfirmOauthStep(platformVerifyStep);
|
||||
const authLoginStep = resolveAuthLoginStep(platformVerifyStep);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.codex2apiSessionId) {
|
||||
throw new Error('缺少 Codex2API 会话信息,请重新执行步骤 7。');
|
||||
throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${authLoginStep}。`);
|
||||
}
|
||||
if (!normalizeString(state.codex2apiAdminKey)) {
|
||||
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const callback = parseLocalhostCallback(state.localhostUrl);
|
||||
const callback = parseLocalhostCallback(state.localhostUrl, platformVerifyStep);
|
||||
const expectedState = normalizeString(state.codex2apiOAuthState);
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
|
||||
throw new Error(`Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 ${authLoginStep}。`);
|
||||
}
|
||||
|
||||
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
|
||||
const origin = new URL(codex2apiUrl).origin;
|
||||
|
||||
await addLog('步骤 10:正在向 Codex2API 提交回调并创建账号...');
|
||||
await addStepLog(platformVerifyStep, '正在向 Codex2API 提交回调并创建账号...');
|
||||
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', {
|
||||
adminKey: state.codex2apiAdminKey,
|
||||
method: 'POST',
|
||||
@@ -298,7 +315,7 @@
|
||||
});
|
||||
|
||||
const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功';
|
||||
await addLog(`步骤 10:${verifiedStatus}`, 'ok');
|
||||
await addStepLog(platformVerifyStep, verifiedStatus, 'ok');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
@@ -306,12 +323,14 @@
|
||||
}
|
||||
|
||||
async function executeSub2ApiStep10(state) {
|
||||
const visibleStep = resolvePlatformVerifyStep(state);
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
const visibleStep = platformVerifyStep;
|
||||
const confirmOauthStep = resolveConfirmOauthStep(visibleStep);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
throw new Error(`步骤 ${confirmOauthStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmOauthStep}。`);
|
||||
}
|
||||
if (!state.sub2apiSessionId) {
|
||||
throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。');
|
||||
@@ -326,7 +345,7 @@
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||
|
||||
await addLog('步骤 10:正在打开 SUB2API 后台...');
|
||||
await addStepLog(visibleStep, '正在打开 SUB2API 后台...');
|
||||
|
||||
let tabId = await getTabId('sub2api-panel');
|
||||
const alive = tabId && await isTabAlive('sub2api-panel');
|
||||
@@ -348,13 +367,14 @@
|
||||
injectSource: 'sub2api-panel',
|
||||
});
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在向 SUB2API 提交回调并创建账号...`);
|
||||
await addStepLog(visibleStep, '正在向 SUB2API 提交回调并创建账号...');
|
||||
const requestMessage = {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 10,
|
||||
step: platformVerifyStep,
|
||||
source: 'background',
|
||||
payload: {
|
||||
localhostUrl: state.localhostUrl,
|
||||
visibleStep,
|
||||
sub2apiUrl,
|
||||
sub2apiEmail: state.sub2apiEmail,
|
||||
sub2apiPassword: state.sub2apiPassword,
|
||||
@@ -364,6 +384,7 @@
|
||||
sub2apiSessionId: state.sub2apiSessionId,
|
||||
sub2apiOAuthState: state.sub2apiOAuthState,
|
||||
sub2apiGroupId: state.sub2apiGroupId,
|
||||
sub2apiGroupIds: state.sub2apiGroupIds,
|
||||
sub2apiDraftName: state.sub2apiDraftName,
|
||||
},
|
||||
};
|
||||
@@ -384,8 +405,9 @@
|
||||
throw error;
|
||||
}
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:SUB2API 回调交换出现临时网络波动(${error.message}),正在重试 ${attempt + 1}/${maxExchangeAttempts}...`,
|
||||
'warn'
|
||||
`SUB2API 回调交换出现临时网络波动(${error.message}),正在重试 ${attempt + 1}/${maxExchangeAttempts}...`,
|
||||
'warn',
|
||||
{ step: visibleStep, stepKey: 'platform-verify' }
|
||||
);
|
||||
await sleep(1200 * attempt);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
root.MultiPageBackgroundPlusReturnConfirm = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusReturnConfirmModule() {
|
||||
const PAYPAL_SOURCE = 'paypal-flow';
|
||||
const GOPAY_SOURCE = 'gopay-flow';
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
const PLUS_RETURN_SETTLE_WAIT_MS = 20000;
|
||||
|
||||
@@ -21,6 +22,10 @@
|
||||
if (paypalTabId && await isTabAlive(PAYPAL_SOURCE)) {
|
||||
return paypalTabId;
|
||||
}
|
||||
const gopayTabId = await getTabId(GOPAY_SOURCE);
|
||||
if (gopayTabId && await isTabAlive(GOPAY_SOURCE)) {
|
||||
return gopayTabId;
|
||||
}
|
||||
const checkoutTabId = await getTabId(PLUS_CHECKOUT_SOURCE);
|
||||
if (checkoutTabId) {
|
||||
return checkoutTabId;
|
||||
@@ -29,17 +34,17 @@
|
||||
if (storedTabId) {
|
||||
return storedTabId;
|
||||
}
|
||||
throw new Error('步骤 9:未找到 Plus / PayPal 标签页,无法确认订阅回跳。');
|
||||
throw new Error('步骤 9:未找到 Plus / PayPal / GoPay 标签页,无法确认订阅回跳。');
|
||||
}
|
||||
|
||||
function isReturnUrl(url = '') {
|
||||
return /https:\/\/(?:chatgpt\.com|chat\.openai\.com|openai\.com)\//i.test(String(url || ''))
|
||||
&& !/paypal\./i.test(String(url || ''));
|
||||
&& !/paypal\.|gopay|gojek|midtrans|xendit|stripe/i.test(String(url || ''));
|
||||
}
|
||||
|
||||
async function executePlusReturnConfirm(state = {}) {
|
||||
const tabId = await resolveReturnTabId(state);
|
||||
await addLog('步骤 9:正在等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面...', 'info');
|
||||
await addLog('步骤 9:正在等待支付授权后回跳到 ChatGPT / OpenAI 页面...', 'info');
|
||||
const tab = await waitForTabUrlMatchUntilStopped(tabId, isReturnUrl);
|
||||
await addLog('步骤 9:已检测到订阅回跳页面,固定等待 20 秒让页面完成加载。', 'info');
|
||||
await sleepWithStop(PLUS_RETURN_SETTLE_WAIT_MS);
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
ensureSignupAuthEntryPageReady,
|
||||
ensureSignupEntryPageReady,
|
||||
ensureSignupPostEmailPageReadyInTab,
|
||||
ensureSignupPostIdentityPageReadyInTab = ensureSignupPostEmailPageReadyInTab,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
phoneVerificationHelpers = null,
|
||||
resolveSignupMethod = () => 'email',
|
||||
resolveSignupEmailForFlow,
|
||||
sendToContentScriptResilient,
|
||||
SIGNUP_PAGE_INJECT_FILES,
|
||||
@@ -26,6 +29,11 @@
|
||||
return /未找到可用的邮箱输入入口|当前页面没有可用的注册入口,也不在邮箱\/密码页/i.test(message);
|
||||
}
|
||||
|
||||
function isSignupPhoneEntryUnavailableErrorMessage(errorLike) {
|
||||
const message = getErrorMessage(errorLike);
|
||||
return /未找到可用的手机号输入入口|当前页面没有可用的手机号注册入口,也不在密码页/i.test(message);
|
||||
}
|
||||
|
||||
function isRetryableStep2TransportErrorMessage(errorLike) {
|
||||
const message = getErrorMessage(errorLike);
|
||||
return /Content script on signup-page did not respond in \d+s|Receiving end does not exist|message channel closed|A listener indicated an asynchronous response|port closed before a response was received|did not respond in \d+s/i.test(message);
|
||||
@@ -55,17 +63,58 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function shouldForceAuthEntryRetry(tabId) {
|
||||
if (!Number.isInteger(tabId) || typeof chrome?.tabs?.get !== 'function') {
|
||||
return false;
|
||||
function isReadySignupEntryState(state = '') {
|
||||
const normalized = String(state || '').trim().toLowerCase();
|
||||
return normalized === 'entry_home'
|
||||
|| normalized === 'email_entry'
|
||||
|| normalized === 'phone_entry'
|
||||
|| normalized === 'password_page';
|
||||
}
|
||||
|
||||
async function getSignupEntryReadyState(tabId) {
|
||||
if (!Number.isInteger(tabId) || typeof sendToContentScriptResilient !== 'function') {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
return isLikelyLoggedInChatgptHomeUrl(tab?.url);
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'ENSURE_SIGNUP_ENTRY_READY',
|
||||
step: 2,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
}, {
|
||||
timeoutMs: 12000,
|
||||
retryDelayMs: 500,
|
||||
logMessage: '步骤 2:正在检查官网注册入口状态...',
|
||||
});
|
||||
if (result?.error) {
|
||||
return '';
|
||||
}
|
||||
return String(result?.state || '').trim().toLowerCase();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function isLikelyLoggedInChatgptHomeTab(tabId) {
|
||||
if (typeof chrome?.tabs?.get !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const readyState = await getSignupEntryReadyState(tabId);
|
||||
if (isReadySignupEntryState(readyState)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentUrl = await getTabUrl(tabId);
|
||||
return isLikelyLoggedInChatgptHomeUrl(currentUrl);
|
||||
}
|
||||
|
||||
async function shouldForceAuthEntryRetry(tabId) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
return false;
|
||||
}
|
||||
return isLikelyLoggedInChatgptHomeTab(tabId);
|
||||
}
|
||||
|
||||
async function getTabUrl(tabId) {
|
||||
@@ -82,8 +131,7 @@
|
||||
}
|
||||
|
||||
async function failStep2OnLoggedInSession(tabId, reasonMessage = '') {
|
||||
const currentUrl = await getTabUrl(tabId);
|
||||
if (!isLikelyLoggedInChatgptHomeUrl(currentUrl)) {
|
||||
if (!(await isLikelyLoggedInChatgptHomeTab(tabId))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -94,7 +142,7 @@
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
async function submitSignupEmail(resolvedEmail, options = {}) {
|
||||
async function sendSignupIdentity(payload = {}, options = {}) {
|
||||
const {
|
||||
timeoutMs = 35000,
|
||||
retryDelayMs = 700,
|
||||
@@ -106,7 +154,7 @@
|
||||
type: 'EXECUTE_STEP',
|
||||
step: 2,
|
||||
source: 'background',
|
||||
payload: { email: resolvedEmail },
|
||||
payload,
|
||||
}, {
|
||||
timeoutMs,
|
||||
retryDelayMs,
|
||||
@@ -117,9 +165,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function executeStep2(state) {
|
||||
const resolvedEmail = await resolveSignupEmailForFlow(state);
|
||||
async function ensureSignupPhoneEntryReady(tabId) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
throw new Error('步骤 2:未找到可用的注册页标签,无法切换到手机号注册入口。');
|
||||
}
|
||||
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
type: 'ENSURE_SIGNUP_PHONE_ENTRY_READY',
|
||||
step: 2,
|
||||
source: 'background',
|
||||
payload: {},
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:正在打开官网注册入口并切换到手机号注册...',
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function submitSignupEmail(resolvedEmail, options = {}) {
|
||||
return sendSignupIdentity({ email: resolvedEmail }, options);
|
||||
}
|
||||
|
||||
async function submitSignupPhone(phoneNumber, activation, options = {}) {
|
||||
return sendSignupIdentity({
|
||||
signupMethod: 'phone',
|
||||
phoneNumber,
|
||||
countryId: activation?.countryId ?? null,
|
||||
countryLabel: String(activation?.countryLabel || '').trim(),
|
||||
}, {
|
||||
logMessage: '步骤 2:官网注册入口正在切换,等待手机号注册入口恢复...',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureSignupTabForStep2() {
|
||||
let signupTabId = await getTabId('signup-page');
|
||||
if (!signupTabId || !(await isTabAlive('signup-page'))) {
|
||||
await addLog('步骤 2:未发现可用的注册页标签,正在重新打开 ChatGPT 官网...', 'warn');
|
||||
@@ -134,6 +218,162 @@
|
||||
logMessage: '步骤 2:注册入口页内容脚本未就绪,正在等待页面恢复...',
|
||||
});
|
||||
}
|
||||
return signupTabId;
|
||||
}
|
||||
|
||||
function normalizeSignupPhoneActivationForStep2(activation) {
|
||||
if (typeof phoneVerificationHelpers?.normalizeActivation === 'function') {
|
||||
return phoneVerificationHelpers.normalizeActivation(activation);
|
||||
}
|
||||
if (!activation || typeof activation !== 'object' || Array.isArray(activation)) {
|
||||
return null;
|
||||
}
|
||||
const activationId = String(activation.activationId ?? activation.id ?? activation.activation ?? '').trim();
|
||||
const phoneNumber = String(activation.phoneNumber ?? activation.number ?? activation.phone ?? '').trim();
|
||||
if (!activationId || !phoneNumber) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...activation,
|
||||
activationId,
|
||||
phoneNumber,
|
||||
};
|
||||
}
|
||||
|
||||
function getSignupPhoneNumberFromState(state = {}) {
|
||||
return String(
|
||||
state?.signupPhoneNumber
|
||||
|| (String(state?.accountIdentifierType || '').trim().toLowerCase() === 'phone' ? state?.accountIdentifier : '')
|
||||
|| ''
|
||||
).trim();
|
||||
}
|
||||
|
||||
async function resolveSignupPhoneForStep2(state = {}) {
|
||||
const existingActivation = normalizeSignupPhoneActivationForStep2(state?.signupPhoneActivation);
|
||||
if (existingActivation?.phoneNumber) {
|
||||
await addLog(`步骤 2:复用当前注册手机号 ${existingActivation.phoneNumber},不重新获取号码。`);
|
||||
return {
|
||||
phoneNumber: existingActivation.phoneNumber,
|
||||
activation: existingActivation,
|
||||
};
|
||||
}
|
||||
|
||||
const manualPhoneNumber = getSignupPhoneNumberFromState(state);
|
||||
if (manualPhoneNumber) {
|
||||
await addLog(`步骤 2:使用手动填写的注册手机号 ${manualPhoneNumber},本轮不会重新获取号码。`, 'warn');
|
||||
return {
|
||||
phoneNumber: manualPhoneNumber,
|
||||
activation: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof phoneVerificationHelpers?.prepareSignupPhoneActivation !== 'function') {
|
||||
throw new Error('手机号注册流程不可用:接码模块尚未初始化。');
|
||||
}
|
||||
const activation = await phoneVerificationHelpers.prepareSignupPhoneActivation(state);
|
||||
return {
|
||||
phoneNumber: activation.phoneNumber,
|
||||
activation,
|
||||
};
|
||||
}
|
||||
|
||||
async function executeSignupPhoneEntry(state) {
|
||||
let signupTabId = await ensureSignupTabForStep2();
|
||||
if (await shouldForceAuthEntryRetry(signupTabId)) {
|
||||
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交手机号。', 'warn');
|
||||
try {
|
||||
signupTabId = (await ensureSignupAuthEntryPageReady(2)).tabId;
|
||||
} catch (entryError) {
|
||||
const entryErrorMessage = getErrorMessage(entryError);
|
||||
if (await failStep2OnLoggedInSession(signupTabId, entryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
await addLog('步骤 2:切换认证入口失败,正在重新打开官网入口并重试提交手机号...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureSignupPhoneEntryReady(signupTabId);
|
||||
} catch (entryError) {
|
||||
const entryErrorMessage = getErrorMessage(entryError);
|
||||
if (await failStep2OnLoggedInSession(signupTabId, entryErrorMessage)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isSignupPhoneEntryUnavailableErrorMessage(entryErrorMessage)
|
||||
|| isSignupEntryUnavailableErrorMessage(entryErrorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(entryErrorMessage)
|
||||
) {
|
||||
await addLog('步骤 2:手机号注册入口尚未就绪,正在重新打开官网入口后重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
await ensureSignupPhoneEntryReady(signupTabId);
|
||||
} else {
|
||||
throw entryError;
|
||||
}
|
||||
}
|
||||
|
||||
const signupPhone = await resolveSignupPhoneForStep2(state);
|
||||
const { phoneNumber, activation } = signupPhone;
|
||||
let step2Result = await submitSignupPhone(phoneNumber, activation, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:官网注册入口正在切换,等待手机号注册入口恢复...',
|
||||
});
|
||||
|
||||
if (step2Result?.error) {
|
||||
const errorMessage = getErrorMessage(step2Result.error);
|
||||
if (
|
||||
isSignupPhoneEntryUnavailableErrorMessage(errorMessage)
|
||||
|| isSignupEntryUnavailableErrorMessage(errorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(errorMessage)
|
||||
) {
|
||||
await addLog('步骤 2:手机号注册入口不可用或通信超时,正在重新准备手机号注册入口后重试一次...', 'warn');
|
||||
signupTabId = (await ensureSignupEntryPageReady(2)).tabId;
|
||||
await ensureSignupPhoneEntryReady(signupTabId);
|
||||
step2Result = await submitSignupPhone(phoneNumber, activation, {
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: '步骤 2:手机号注册入口已就绪,正在重新提交手机号...',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (step2Result?.error) {
|
||||
const finalErrorMessage = getErrorMessage(step2Result.error);
|
||||
if (
|
||||
(isSignupEntryUnavailableErrorMessage(finalErrorMessage)
|
||||
|| isRetryableStep2TransportErrorMessage(finalErrorMessage))
|
||||
&& await failStep2OnLoggedInSession(signupTabId, finalErrorMessage)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (activation && typeof phoneVerificationHelpers?.cancelSignupPhoneActivation === 'function') {
|
||||
await phoneVerificationHelpers.cancelSignupPhoneActivation(state, activation).catch(() => {});
|
||||
}
|
||||
throw new Error(finalErrorMessage);
|
||||
}
|
||||
|
||||
await addLog(`步骤 2:手机号 ${phoneNumber} 已提交,正在等待页面加载并确认下一步入口...`);
|
||||
const landingResult = await ensureSignupPostIdentityPageReadyInTab(signupTabId, 2, {
|
||||
skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
|
||||
});
|
||||
|
||||
await completeStepFromBackground(2, {
|
||||
accountIdentifierType: 'phone',
|
||||
accountIdentifier: phoneNumber,
|
||||
signupPhoneNumber: phoneNumber,
|
||||
signupPhoneActivation: activation || null,
|
||||
nextSignupState: landingResult?.state || step2Result?.state || 'password_page',
|
||||
nextSignupUrl: landingResult?.url || step2Result?.url || '',
|
||||
skippedPasswordStep: landingResult?.state === 'phone_verification_page' || landingResult?.state === 'profile_page',
|
||||
});
|
||||
}
|
||||
|
||||
async function executeSignupEmailEntry(state) {
|
||||
const resolvedEmail = await resolveSignupEmailForFlow(state);
|
||||
|
||||
let signupTabId = await ensureSignupTabForStep2();
|
||||
|
||||
if (await shouldForceAuthEntryRetry(signupTabId)) {
|
||||
await addLog('步骤 2:检测到当前位于已登录 ChatGPT 首页,先切换认证入口页再提交邮箱。', 'warn');
|
||||
@@ -214,12 +454,21 @@
|
||||
|
||||
await completeStepFromBackground(2, {
|
||||
email: resolvedEmail,
|
||||
accountIdentifierType: 'email',
|
||||
accountIdentifier: resolvedEmail,
|
||||
nextSignupState: landingResult?.state || 'password_page',
|
||||
nextSignupUrl: landingResult?.url || step2Result?.url || '',
|
||||
skippedPasswordStep: landingResult?.state === 'verification_page',
|
||||
});
|
||||
}
|
||||
|
||||
async function executeStep2(state) {
|
||||
if (resolveSignupMethod(state) === 'phone') {
|
||||
return executeSignupPhoneEntry(state);
|
||||
}
|
||||
return executeSignupEmailEntry(state);
|
||||
}
|
||||
|
||||
return { executeStep2 };
|
||||
}
|
||||
|
||||
|
||||
@@ -339,6 +339,8 @@
|
||||
timeoutMs = 30000,
|
||||
retryDelayMs = 700,
|
||||
logMessage = '',
|
||||
logStep = null,
|
||||
logStepKey = '',
|
||||
} = options;
|
||||
|
||||
const start = Date.now();
|
||||
@@ -399,7 +401,10 @@
|
||||
|
||||
if (logMessage && !logged) {
|
||||
console.warn(LOG_PREFIX, `[ensureContentScriptReadyOnTab] ${source} tab=${tabId} still not ready after ${Date.now() - start}ms`);
|
||||
await addLog(logMessage, 'warn');
|
||||
await addLog(logMessage, 'warn', {
|
||||
step: logStep,
|
||||
stepKey: logStepKey,
|
||||
});
|
||||
logged = true;
|
||||
}
|
||||
|
||||
@@ -527,6 +532,31 @@
|
||||
}
|
||||
|
||||
async function reuseOrCreateTab(source, url, options = {}) {
|
||||
if (options.forceNew) {
|
||||
await closeConflictingTabsForSource(source, url);
|
||||
const tab = await chrome.tabs.create({ url, active: true });
|
||||
|
||||
if (options.inject) {
|
||||
await waitForTabUpdateComplete(tab.id);
|
||||
if (options.injectSource) {
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: (injectedSource) => {
|
||||
window.__MULTIPAGE_SOURCE = injectedSource;
|
||||
},
|
||||
args: [options.injectSource],
|
||||
});
|
||||
}
|
||||
await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
files: options.inject,
|
||||
});
|
||||
}
|
||||
|
||||
await rememberSourceLastUrl(source, url);
|
||||
return tab.id;
|
||||
}
|
||||
|
||||
const alive = await isTabAlive(source);
|
||||
if (alive) {
|
||||
const tabId = await getTabId(source);
|
||||
@@ -645,6 +675,8 @@
|
||||
timeoutMs = 30000,
|
||||
retryDelayMs = 600,
|
||||
logMessage = '',
|
||||
logStep = null,
|
||||
logStepKey = '',
|
||||
responseTimeoutMs,
|
||||
} = options;
|
||||
const start = Date.now();
|
||||
@@ -676,7 +708,10 @@
|
||||
|
||||
lastError = err;
|
||||
if (logMessage && !logged) {
|
||||
await addLog(logMessage, 'warn');
|
||||
await addLog(logMessage, 'warn', {
|
||||
step: logStep,
|
||||
stepKey: logStepKey,
|
||||
});
|
||||
logged = true;
|
||||
}
|
||||
|
||||
@@ -691,6 +726,8 @@
|
||||
const {
|
||||
timeoutMs = 45000,
|
||||
maxRecoveryAttempts = 2,
|
||||
logStep = null,
|
||||
logStepKey = '',
|
||||
responseTimeoutMs,
|
||||
} = options;
|
||||
const start = Date.now();
|
||||
@@ -720,7 +757,10 @@
|
||||
|
||||
lastError = err;
|
||||
if (!logged) {
|
||||
await addLog(`步骤 ${message.step}:${mail.label} 页面通信异常,正在尝试让邮箱页重新就绪...`, 'warn');
|
||||
await addLog(`${mail.label} 页面通信异常,正在尝试让邮箱页重新就绪...`, 'warn', {
|
||||
step: logStep,
|
||||
stepKey: logStepKey,
|
||||
});
|
||||
logged = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
function createVerificationFlowHelpers(deps = {}) {
|
||||
const {
|
||||
addLog,
|
||||
addLog: rawAddLog = async () => {},
|
||||
chrome,
|
||||
closeConflictingTabsForSource,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
@@ -34,6 +34,33 @@
|
||||
throwIfStopped,
|
||||
VERIFICATION_POLL_MAX_ROUNDS,
|
||||
} = deps;
|
||||
let activeVerificationLogStep = null;
|
||||
|
||||
function normalizeLogStep(value) {
|
||||
const step = Math.floor(Number(value) || 0);
|
||||
return step > 0 ? step : null;
|
||||
}
|
||||
|
||||
function normalizeVerificationLogMessage(message) {
|
||||
return String(message || '')
|
||||
.replace(/^步骤\s*\d+\s*[::]\s*/, '')
|
||||
.replace(/^Step\s+\d+\s*[::]\s*/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function addLog(message, level = 'info', options = {}) {
|
||||
const normalizedOptions = options && typeof options === 'object' ? { ...options } : {};
|
||||
const step = normalizeLogStep(normalizedOptions.step || normalizedOptions.visibleStep)
|
||||
|| normalizeLogStep(activeVerificationLogStep);
|
||||
if (step) {
|
||||
normalizedOptions.step = step;
|
||||
if (!normalizedOptions.stepKey) {
|
||||
normalizedOptions.stepKey = step === 4 ? 'fetch-signup-code' : 'fetch-login-code';
|
||||
}
|
||||
}
|
||||
delete normalizedOptions.visibleStep;
|
||||
return rawAddLog(normalizeVerificationLogMessage(message), level, normalizedOptions);
|
||||
}
|
||||
|
||||
const isRetryableVerificationTransportError = typeof deps.isRetryableContentScriptTransportError === 'function'
|
||||
? deps.isRetryableContentScriptTransportError
|
||||
@@ -518,6 +545,8 @@
|
||||
timeoutMs: responseTimeoutMs,
|
||||
responseTimeoutMs,
|
||||
maxRecoveryAttempts: 2,
|
||||
logStep: activeVerificationLogStep,
|
||||
logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -583,6 +612,8 @@
|
||||
timeoutMs: 10000,
|
||||
responseTimeoutMs: 5000,
|
||||
maxRecoveryAttempts: 1,
|
||||
logStep: activeVerificationLogStep,
|
||||
logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code',
|
||||
}
|
||||
);
|
||||
} catch (_) {
|
||||
@@ -693,6 +724,8 @@
|
||||
timeoutMs: timeoutWindow.timeoutMs,
|
||||
maxRecoveryAttempts: 2,
|
||||
responseTimeoutMs: timeoutWindow.responseTimeoutMs,
|
||||
logStep: activeVerificationLogStep,
|
||||
logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -959,6 +992,8 @@
|
||||
timeoutMs: timeoutWindow.timeoutMs,
|
||||
maxRecoveryAttempts: 2,
|
||||
responseTimeoutMs: timeoutWindow.responseTimeoutMs,
|
||||
logStep: activeVerificationLogStep,
|
||||
logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1000,6 +1035,8 @@
|
||||
}
|
||||
|
||||
async function submitVerificationCode(step, code, options = {}) {
|
||||
const completionStep = getCompletionStep(step, options);
|
||||
const authLoginStep = completionStep >= 11 ? 10 : 7;
|
||||
const signupTabId = await getTabId('signup-page');
|
||||
if (!signupTabId) {
|
||||
throw new Error('认证页面标签页已关闭,无法填写验证码。');
|
||||
@@ -1028,7 +1065,9 @@
|
||||
timeoutMs: Math.max(baseResponseTimeoutMs + 15000, 30000),
|
||||
retryDelayMs: 700,
|
||||
responseTimeoutMs: baseResponseTimeoutMs,
|
||||
logMessage: `步骤 ${step}:认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...`,
|
||||
logMessage: '认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...',
|
||||
logStep: completionStep,
|
||||
logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code',
|
||||
});
|
||||
} catch (err) {
|
||||
if (step === 4 && isRetryableVerificationTransportError(err)) {
|
||||
@@ -1058,9 +1097,15 @@
|
||||
});
|
||||
if (fallback.success) {
|
||||
if (fallback.addPhonePage) {
|
||||
await addLog('步骤 8:验证码提交后通信中断,但页面已进入手机号验证页,按提交成功继续。', 'warn');
|
||||
await addLog('验证码提交后通信中断,但页面已进入手机号验证页,按提交成功继续。', 'warn', {
|
||||
step: completionStep,
|
||||
stepKey: 'fetch-login-code',
|
||||
});
|
||||
} else {
|
||||
await addLog('步骤 8:验证码提交后通信中断,但页面已进入 OAuth 授权页,按提交成功继续。', 'warn');
|
||||
await addLog('验证码提交后通信中断,但页面已进入 OAuth 授权页,按提交成功继续。', 'warn', {
|
||||
step: completionStep,
|
||||
stepKey: 'fetch-login-code',
|
||||
});
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
@@ -1072,7 +1117,7 @@
|
||||
}
|
||||
if (fallback.restartStep7) {
|
||||
const urlPart = fallback.url ? ` URL: ${fallback.url}` : '';
|
||||
throw new Error(`STEP8_RESTART_STEP7::步骤 8:验证码提交后认证页进入登录超时报错页,请回到步骤 7 重新开始。${urlPart}`.trim());
|
||||
throw new Error(`STEP8_RESTART_STEP7::步骤 ${completionStep}:验证码提交后认证页进入登录超时报错页,请回到步骤 ${authLoginStep} 重新开始。${urlPart}`.trim());
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
@@ -1092,6 +1137,7 @@
|
||||
|
||||
async function resolveVerificationStep(step, state, mail, options = {}) {
|
||||
const completionStep = getCompletionStep(step, options);
|
||||
activeVerificationLogStep = completionStep;
|
||||
const stateKey = getVerificationCodeStateKey(step);
|
||||
const rejectedCodes = new Set();
|
||||
const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER
|
||||
|
||||
Reference in New Issue
Block a user