fix: sync LuckMail PR with dev
This commit is contained in:
@@ -392,6 +392,7 @@
|
||||
inbucketMailbox: prevState.inbucketMailbox,
|
||||
cloudflareDomain: prevState.cloudflareDomain,
|
||||
cloudflareDomains: prevState.cloudflareDomains,
|
||||
reusablePhoneActivation: prevState.reusablePhoneActivation,
|
||||
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
|
||||
autoRunSessionId: sessionId,
|
||||
tabRegistry: {},
|
||||
|
||||
+132
-144
@@ -3,19 +3,15 @@
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundContributionOAuthModule() {
|
||||
const API_BASE_URL = 'https://apikey.qzz.io/oauth/api';
|
||||
const ACTIVE_STATUSES = new Set(['started', 'waiting', 'processing']);
|
||||
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'expired', 'error']);
|
||||
const FINAL_STATUSES = new Set(['auto_approved', 'auto_rejected', 'manual_review_required', 'expired', 'error']);
|
||||
const CALLBACK_FINAL_STATUSES = new Set(['submitted']);
|
||||
const CALLBACK_WAITING_STATUSES = new Set(['idle', 'waiting', 'captured', 'failed', 'submitting']);
|
||||
const CONTRIBUTION_SOURCE_CPA = 'cpa';
|
||||
const CONTRIBUTION_SOURCE_SUB2API = 'sub2api';
|
||||
const CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME = 'codex号池';
|
||||
const CONTRIBUTION_SUB2API_PLUS_GROUP_NAME = 'openai-plus';
|
||||
|
||||
const RUNTIME_DEFAULTS = {
|
||||
contributionMode: false,
|
||||
contributionModeExpected: false,
|
||||
contributionSource: CONTRIBUTION_SOURCE_SUB2API,
|
||||
contributionTargetGroupName: CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME,
|
||||
contributionSource: 'sub2api',
|
||||
contributionTargetGroupName: 'codex号池',
|
||||
contributionNickname: '',
|
||||
contributionQq: '',
|
||||
contributionSessionId: '',
|
||||
@@ -44,7 +40,8 @@
|
||||
} = deps;
|
||||
|
||||
let listenersBound = false;
|
||||
const inFlightCapturedCallbackTasks = new Map();
|
||||
const pendingCallbackSubmissions = new Map();
|
||||
const pendingCapturedCallbacks = new Map();
|
||||
|
||||
function normalizeString(value = '') {
|
||||
return String(value || '').trim();
|
||||
@@ -71,6 +68,9 @@
|
||||
case 'auto_rejected':
|
||||
case 'rejected':
|
||||
return 'auto_rejected';
|
||||
case 'manual_review_required':
|
||||
case 'manual_review':
|
||||
return 'manual_review_required';
|
||||
case 'expired':
|
||||
case 'timeout':
|
||||
return 'expired';
|
||||
@@ -111,63 +111,6 @@
|
||||
return FINAL_STATUSES.has(normalizeContributionStatus(status));
|
||||
}
|
||||
|
||||
function runCapturedCallbackOnce(callbackUrl, executor) {
|
||||
const normalizedUrl = normalizeString(callbackUrl);
|
||||
if (!normalizedUrl) {
|
||||
return Promise.resolve().then(executor);
|
||||
}
|
||||
|
||||
const existingTask = inFlightCapturedCallbackTasks.get(normalizedUrl);
|
||||
if (existingTask) {
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
let task = null;
|
||||
task = Promise.resolve()
|
||||
.then(executor)
|
||||
.finally(() => {
|
||||
if (inFlightCapturedCallbackTasks.get(normalizedUrl) === task) {
|
||||
inFlightCapturedCallbackTasks.delete(normalizedUrl);
|
||||
}
|
||||
});
|
||||
inFlightCapturedCallbackTasks.set(normalizedUrl, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
function normalizeContributionSource(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
return normalized === CONTRIBUTION_SOURCE_SUB2API
|
||||
? CONTRIBUTION_SOURCE_SUB2API
|
||||
: CONTRIBUTION_SOURCE_CPA;
|
||||
}
|
||||
|
||||
function resolveContributionRouting(state = {}) {
|
||||
const currentStatus = normalizeContributionStatus(state.contributionStatus);
|
||||
const currentSource = normalizeContributionSource(state.contributionSource);
|
||||
const hasActiveSession = Boolean(
|
||||
normalizeString(state.contributionSessionId)
|
||||
&& currentStatus
|
||||
&& !FINAL_STATUSES.has(currentStatus)
|
||||
);
|
||||
|
||||
if (hasActiveSession) {
|
||||
return {
|
||||
source: currentSource,
|
||||
targetGroupName: currentSource === CONTRIBUTION_SOURCE_SUB2API
|
||||
? (normalizeString(state.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME)
|
||||
: '',
|
||||
};
|
||||
}
|
||||
|
||||
const source = CONTRIBUTION_SOURCE_SUB2API;
|
||||
return {
|
||||
source,
|
||||
targetGroupName: Boolean(state.plusModeEnabled)
|
||||
? CONTRIBUTION_SUB2API_PLUS_GROUP_NAME
|
||||
: (normalizeString(state.contributionTargetGroupName) || CONTRIBUTION_SUB2API_DEFAULT_GROUP_NAME),
|
||||
};
|
||||
}
|
||||
|
||||
function getStatusLabel(status = '') {
|
||||
switch (normalizeContributionStatus(status)) {
|
||||
case 'started':
|
||||
@@ -175,11 +118,13 @@
|
||||
case 'waiting':
|
||||
return '等待提交回调';
|
||||
case 'processing':
|
||||
return '已提交回调,等待服务端确认';
|
||||
return '已提交回调,等待 CPA 确认';
|
||||
case 'auto_approved':
|
||||
return '贡献成功,服务端已确认';
|
||||
return '贡献成功,CPA 已确认';
|
||||
case 'auto_rejected':
|
||||
return '贡献未通过确认';
|
||||
case 'manual_review_required':
|
||||
return '已提交,等待人工处理';
|
||||
case 'expired':
|
||||
return '贡献会话已超时';
|
||||
case 'error':
|
||||
@@ -315,6 +260,41 @@
|
||||
return qq;
|
||||
}
|
||||
|
||||
function isPlusModeState(state = {}) {
|
||||
return Boolean(state?.plusModeEnabled);
|
||||
}
|
||||
|
||||
function normalizeContributionModeSource(value = '') {
|
||||
const normalized = normalizeString(value).toLowerCase();
|
||||
return normalized === 'sub2api' ? 'sub2api' : 'cpa';
|
||||
}
|
||||
|
||||
function resolveContributionModeRoutingState(state = {}) {
|
||||
const currentStatus = normalizeString(state?.contributionStatus).toLowerCase();
|
||||
const currentSource = normalizeContributionModeSource(state?.contributionSource);
|
||||
const hasActiveSession = Boolean(
|
||||
normalizeString(state?.contributionSessionId)
|
||||
&& currentStatus
|
||||
&& !FINAL_STATUSES.has(currentStatus)
|
||||
);
|
||||
|
||||
if (hasActiveSession) {
|
||||
return {
|
||||
source: currentSource,
|
||||
targetGroupName: currentSource === 'sub2api'
|
||||
? (normalizeString(state?.contributionTargetGroupName) || 'codex号池')
|
||||
: '',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
source: 'sub2api',
|
||||
targetGroupName: isPlusModeState(state)
|
||||
? 'openai-plus'
|
||||
: (normalizeString(state?.contributionTargetGroupName) || 'codex号池'),
|
||||
};
|
||||
}
|
||||
|
||||
function buildStatusMessage(status, payload = {}) {
|
||||
const label = getStatusLabel(status);
|
||||
const details = [
|
||||
@@ -494,68 +474,87 @@
|
||||
return currentState;
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'submitting',
|
||||
contributionCallbackMessage: buildCallbackMessage('submitting'),
|
||||
});
|
||||
|
||||
try {
|
||||
const payload = await fetchContributionJson('/submit-callback', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
callback_url: normalizedUrl,
|
||||
},
|
||||
});
|
||||
|
||||
const nextStatus = 'submitted';
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: nextStatus,
|
||||
contributionCallbackMessage: buildCallbackMessage(nextStatus, payload),
|
||||
});
|
||||
|
||||
if (typeof closeLocalhostCallbackTabs === 'function') {
|
||||
await closeLocalhostCallbackTabs(normalizedUrl).catch(() => {});
|
||||
}
|
||||
|
||||
return await pollContributionStatus({ reason: options.reason || 'submit_callback' });
|
||||
} catch (error) {
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'failed',
|
||||
contributionCallbackMessage: `回调提交失败:${error.message}`,
|
||||
});
|
||||
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(`贡献模式:回调提交失败:${error.message}`, 'warn');
|
||||
}
|
||||
|
||||
throw error;
|
||||
const dedupeKey = `${sessionId}::${normalizedUrl}`;
|
||||
if (pendingCallbackSubmissions.has(dedupeKey)) {
|
||||
return pendingCallbackSubmissions.get(dedupeKey);
|
||||
}
|
||||
|
||||
const task = (async () => {
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'submitting',
|
||||
contributionCallbackMessage: buildCallbackMessage('submitting'),
|
||||
});
|
||||
|
||||
try {
|
||||
const payload = await fetchContributionJson('/submit-callback', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
callback_url: normalizedUrl,
|
||||
},
|
||||
});
|
||||
|
||||
const nextStatus = 'submitted';
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: nextStatus,
|
||||
contributionCallbackMessage: buildCallbackMessage(nextStatus, payload),
|
||||
});
|
||||
|
||||
if (typeof closeLocalhostCallbackTabs === 'function') {
|
||||
await closeLocalhostCallbackTabs(normalizedUrl).catch(() => {});
|
||||
}
|
||||
|
||||
return await pollContributionStatus({ reason: options.reason || 'submit_callback' });
|
||||
} catch (error) {
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'failed',
|
||||
contributionCallbackMessage: `回调提交失败:${error.message}`,
|
||||
});
|
||||
|
||||
if (typeof addLog === 'function') {
|
||||
await addLog(`贡献模式:回调提交失败:${error.message}`, 'warn');
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
pendingCallbackSubmissions.delete(dedupeKey);
|
||||
}
|
||||
})();
|
||||
|
||||
pendingCallbackSubmissions.set(dedupeKey, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
async function handleCapturedCallback(rawUrl, metadata = {}) {
|
||||
const currentState = await getState();
|
||||
if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) {
|
||||
return currentState;
|
||||
}
|
||||
if (!isContributionCallbackUrl(rawUrl, currentState)) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const normalizedUrl = normalizeString(rawUrl);
|
||||
return runCapturedCallbackOnce(normalizedUrl, async () => {
|
||||
const currentState = await getState();
|
||||
if (!normalizeString(currentState.contributionSessionId) || !currentState.contributionMode) {
|
||||
return currentState;
|
||||
}
|
||||
if (!isContributionCallbackUrl(normalizedUrl, currentState)) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
|
||||
if (
|
||||
normalizedUrl
|
||||
&& normalizeString(currentState.contributionCallbackUrl) === normalizedUrl
|
||||
&& (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting')
|
||||
) {
|
||||
return currentState;
|
||||
}
|
||||
const callbackDedupeKey = `${normalizeString(currentState.contributionSessionId)}::${normalizedUrl}`;
|
||||
if (pendingCapturedCallbacks.has(callbackDedupeKey)) {
|
||||
return pendingCapturedCallbacks.get(callbackDedupeKey);
|
||||
}
|
||||
if (pendingCallbackSubmissions.has(callbackDedupeKey)) {
|
||||
return pendingCallbackSubmissions.get(callbackDedupeKey);
|
||||
}
|
||||
const currentCallbackStatus = normalizeContributionCallbackStatus(currentState.contributionCallbackStatus);
|
||||
if (
|
||||
normalizedUrl
|
||||
&& normalizeString(currentState.contributionCallbackUrl) === normalizedUrl
|
||||
&& (CALLBACK_FINAL_STATUSES.has(currentCallbackStatus) || currentCallbackStatus === 'submitting')
|
||||
) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const task = (async () => {
|
||||
await applyRuntimeUpdates({
|
||||
contributionCallbackUrl: normalizedUrl,
|
||||
contributionCallbackStatus: 'captured',
|
||||
@@ -573,8 +572,13 @@
|
||||
});
|
||||
} catch {
|
||||
return getState();
|
||||
} finally {
|
||||
pendingCapturedCallbacks.delete(callbackDedupeKey);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
pendingCapturedCallbacks.set(callbackDedupeKey, task);
|
||||
return task;
|
||||
}
|
||||
|
||||
async function pollContributionStatus(options = {}) {
|
||||
@@ -597,16 +601,6 @@
|
||||
const callbackState = deriveCallbackState(mergedPayload, currentState);
|
||||
const updates = {
|
||||
contributionLastPollAt: Date.now(),
|
||||
contributionSource: normalizeContributionSource(
|
||||
mergedPayload.source
|
||||
|| mergedPayload.source_kind
|
||||
|| currentState.contributionSource
|
||||
),
|
||||
contributionTargetGroupName: normalizeString(
|
||||
mergedPayload.target_group_name
|
||||
|| mergedPayload.group_name
|
||||
|| currentState.contributionTargetGroupName
|
||||
),
|
||||
contributionStatus: normalizedStatus,
|
||||
contributionStatusMessage: buildStatusMessage(normalizedStatus, mergedPayload),
|
||||
contributionCallbackUrl: callbackState.callbackUrl,
|
||||
@@ -648,7 +642,6 @@
|
||||
async function startContributionFlow(options = {}) {
|
||||
const currentState = options.stateOverride || await getState();
|
||||
const shouldOpenAuthTab = options.openAuthTab !== false;
|
||||
const routing = resolveContributionRouting(currentState);
|
||||
if (!currentState.contributionMode) {
|
||||
throw new Error('请先进入贡献模式。');
|
||||
}
|
||||
@@ -666,14 +659,15 @@
|
||||
return pollContributionStatus({ reason: 'resume_existing' });
|
||||
}
|
||||
|
||||
const routingState = resolveContributionModeRoutingState(currentState);
|
||||
const payload = await fetchContributionJson('/start', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
nickname: buildNickname(currentState, options.nickname),
|
||||
qq: buildContributionQq(currentState, options.qq),
|
||||
email: normalizeString(currentState.email),
|
||||
source: routing.source,
|
||||
target_group_name: routing.targetGroupName,
|
||||
source: routingState.source,
|
||||
target_group_name: routingState.targetGroupName,
|
||||
channel: 'codex-extension',
|
||||
},
|
||||
});
|
||||
@@ -686,12 +680,6 @@
|
||||
}
|
||||
|
||||
await applyRuntimeUpdates({
|
||||
contributionSource: normalizeContributionSource(payload.source || routing.source),
|
||||
contributionTargetGroupName: normalizeString(
|
||||
payload.target_group_name
|
||||
|| payload.group_name
|
||||
|| routing.targetGroupName
|
||||
),
|
||||
contributionSessionId: sessionId,
|
||||
contributionAuthUrl: authUrl,
|
||||
contributionAuthState: authState,
|
||||
@@ -722,7 +710,7 @@
|
||||
}
|
||||
|
||||
function onTabUpdated(tabId, changeInfo, tab) {
|
||||
const candidateUrl = normalizeString(changeInfo?.url);
|
||||
const candidateUrl = normalizeString(changeInfo?.url || tab?.url);
|
||||
if (!candidateUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,11 +32,14 @@
|
||||
executeStepViaCompletionSignal,
|
||||
exportSettingsBundle,
|
||||
fetchGeneratedEmail,
|
||||
finalizePhoneActivationAfterSuccessfulFlow,
|
||||
finalizeStep3Completion,
|
||||
finalizeIcloudAliasAfterSuccessfulFlow,
|
||||
findHotmailAccount,
|
||||
findPayPalAccount,
|
||||
flushCommand,
|
||||
getCurrentLuckmailPurchase,
|
||||
getCurrentPayPalAccount,
|
||||
getCurrentMail2925Account,
|
||||
getPendingAutoRunTimerPlan,
|
||||
getSourceLabel,
|
||||
@@ -62,6 +65,7 @@
|
||||
refreshIpProxyPool,
|
||||
normalizeHotmailAccounts,
|
||||
normalizeMail2925Accounts,
|
||||
normalizePayPalAccounts,
|
||||
normalizeRunCount,
|
||||
AUTO_RUN_TIMER_KIND_SCHEDULED_START,
|
||||
notifyStepComplete,
|
||||
@@ -79,6 +83,7 @@
|
||||
selectLuckmailPurchase,
|
||||
switchIpProxy,
|
||||
changeIpProxyExit,
|
||||
setCurrentPayPalAccount,
|
||||
setCurrentMail2925Account,
|
||||
setCurrentHotmailAccount,
|
||||
setContributionMode,
|
||||
@@ -99,7 +104,9 @@
|
||||
deleteMail2925Account,
|
||||
deleteMail2925Accounts,
|
||||
syncHotmailAccounts,
|
||||
syncPayPalAccounts,
|
||||
testHotmailAccountMailAccess,
|
||||
upsertPayPalAccount,
|
||||
upsertMail2925Account,
|
||||
upsertHotmailAccount,
|
||||
verifyHotmailAccount,
|
||||
@@ -203,9 +210,33 @@
|
||||
});
|
||||
}
|
||||
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
|
||||
if (typeof finalizePhoneActivationAfterSuccessfulFlow === 'function') {
|
||||
await finalizePhoneActivationAfterSuccessfulFlow(latestState);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStepData(step, payload) {
|
||||
if (step === 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.sub2apiDraftName !== undefined) updates.sub2apiDraftName = payload.sub2apiDraftName || null;
|
||||
if (payload.sub2apiProxyId !== undefined) updates.sub2apiProxyId = payload.sub2apiProxyId || null;
|
||||
if (payload.cpaOAuthState !== undefined) updates.cpaOAuthState = payload.cpaOAuthState || null;
|
||||
if (payload.cpaManagementOrigin !== undefined) updates.cpaManagementOrigin = payload.cpaManagementOrigin || 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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const stateForStep = await getState();
|
||||
const stepKey = getStepKeyForState(step, stateForStep);
|
||||
|
||||
@@ -252,24 +283,6 @@
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
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.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:
|
||||
if (payload.email) {
|
||||
await setEmailState(payload.email);
|
||||
@@ -779,6 +792,16 @@
|
||||
return { ok: true, account };
|
||||
}
|
||||
|
||||
case 'UPSERT_PAYPAL_ACCOUNT': {
|
||||
const account = await upsertPayPalAccount(message.payload || {});
|
||||
return { ok: true, account };
|
||||
}
|
||||
|
||||
case 'SELECT_PAYPAL_ACCOUNT': {
|
||||
const account = await setCurrentPayPalAccount(String(message.payload?.accountId || ''));
|
||||
return { ok: true, account };
|
||||
}
|
||||
|
||||
case 'DELETE_HOTMAIL_ACCOUNT': {
|
||||
await deleteHotmailAccount(String(message.payload?.accountId || ''));
|
||||
return { ok: true };
|
||||
|
||||
+108
-37
@@ -43,6 +43,78 @@
|
||||
return message || `Codex2API 请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
function deriveCpaManagementOrigin(vpsUrl) {
|
||||
const normalizedUrl = String(vpsUrl || '').trim();
|
||||
if (!normalizedUrl) {
|
||||
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(normalizedUrl);
|
||||
} catch {
|
||||
throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
|
||||
}
|
||||
return parsed.origin;
|
||||
}
|
||||
|
||||
function getCpaApiErrorMessage(payload, responseStatus = 500) {
|
||||
const candidates = [
|
||||
payload?.error,
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.reason,
|
||||
];
|
||||
const message = candidates
|
||||
.map((value) => String(value || '').trim())
|
||||
.find(Boolean);
|
||||
return message || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
async function fetchCpaManagementJson(origin, path, options = {}) {
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const managementKey = String(options.managementKey || '').trim();
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (managementKey) {
|
||||
headers.Authorization = `Bearer ${managementKey}`;
|
||||
headers['X-Management-Key'] = managementKey;
|
||||
}
|
||||
|
||||
const response = await fetch(`${origin}${path}`, {
|
||||
method: options.method || 'POST',
|
||||
headers,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getCpaApiErrorMessage(payload, response.status));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('CPA 管理接口请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCodex2ApiJson(origin, path, options = {}) {
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
|
||||
const controller = new AbortController();
|
||||
@@ -97,49 +169,48 @@
|
||||
if (!state.vpsUrl) {
|
||||
throw new Error('尚未配置 CPA 地址,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
await addLog(`${logLabel}:正在打开 CPA 面板...`);
|
||||
|
||||
const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
|
||||
await closeConflictingTabsForSource('vps-panel', state.vpsUrl);
|
||||
|
||||
const tab = await chrome.tabs.create({ url: state.vpsUrl, active: true });
|
||||
const tabId = tab.id;
|
||||
await rememberSourceLastUrl('vps-panel', state.vpsUrl);
|
||||
|
||||
await addLog(`${logLabel}:CPA 面板已打开,正在等待页面进入目标地址...`);
|
||||
const matchedTab = await waitForTabUrlFamily('vps-panel', tabId, state.vpsUrl, {
|
||||
timeoutMs: 15000,
|
||||
retryDelayMs: 400,
|
||||
});
|
||||
if (!matchedTab) {
|
||||
await addLog(`${logLabel}:CPA 页面尚未完全进入目标地址,继续尝试连接内容脚本...`, 'warn');
|
||||
const managementKey = String(state.vpsPassword || '').trim();
|
||||
if (!managementKey) {
|
||||
throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('vps-panel', tabId, {
|
||||
inject: injectFiles,
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: `${logLabel}:CPA 面板仍在加载,正在重试连接内容脚本...`,
|
||||
const origin = deriveCpaManagementOrigin(state.vpsUrl);
|
||||
|
||||
await addLog(`${logLabel}:正在通过 CPA 管理接口获取 OAuth 授权链接...`);
|
||||
const result = await fetchCpaManagementJson(origin, '/v0/management/codex-auth-url', {
|
||||
method: 'GET',
|
||||
managementKey,
|
||||
});
|
||||
|
||||
const result = await sendToContentScriptResilient('vps-panel', {
|
||||
type: 'REQUEST_OAUTH_URL',
|
||||
source: 'background',
|
||||
payload: {
|
||||
vpsPassword: state.vpsPassword,
|
||||
logStep: 7,
|
||||
},
|
||||
}, {
|
||||
timeoutMs: 30000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `${logLabel}:CPA 面板通信未就绪,正在等待页面恢复...`,
|
||||
});
|
||||
const oauthUrl = String(
|
||||
result?.url
|
||||
|| result?.auth_url
|
||||
|| result?.authUrl
|
||||
|| result?.data?.url
|
||||
|| result?.data?.auth_url
|
||||
|| result?.data?.authUrl
|
||||
|| ''
|
||||
).trim();
|
||||
const oauthState = String(
|
||||
result?.state
|
||||
|| result?.auth_state
|
||||
|| result?.authState
|
||||
|| result?.data?.state
|
||||
|| result?.data?.auth_state
|
||||
|| result?.data?.authState
|
||||
|| ''
|
||||
).trim()
|
||||
|| extractStateFromAuthUrl(oauthUrl);
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
if (!oauthUrl || !oauthUrl.startsWith('http')) {
|
||||
throw new Error('CPA 管理接口未返回有效的 auth_url。');
|
||||
}
|
||||
return result || {};
|
||||
|
||||
return {
|
||||
oauthUrl,
|
||||
cpaOAuthState: oauthState || null,
|
||||
cpaManagementOrigin: origin,
|
||||
};
|
||||
}
|
||||
|
||||
async function requestCodex2ApiOAuthUrl(state, options = {}) {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
(function attachBackgroundPayPalAccountStore(root, factory) {
|
||||
root.MultiPageBackgroundPayPalAccountStore = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPayPalAccountStoreModule() {
|
||||
function createPayPalAccountStore(deps = {}) {
|
||||
const {
|
||||
broadcastDataUpdate,
|
||||
findPayPalAccount,
|
||||
getState,
|
||||
normalizePayPalAccount,
|
||||
normalizePayPalAccounts,
|
||||
setPersistentSettings,
|
||||
setState,
|
||||
upsertPayPalAccountInList,
|
||||
} = deps;
|
||||
|
||||
async function syncSelectedPayPalAccountState(account = null) {
|
||||
const updates = account
|
||||
? {
|
||||
currentPayPalAccountId: account.id,
|
||||
paypalEmail: String(account.email || '').trim(),
|
||||
paypalPassword: String(account.password || ''),
|
||||
}
|
||||
: {
|
||||
currentPayPalAccountId: '',
|
||||
paypalEmail: '',
|
||||
paypalPassword: '',
|
||||
};
|
||||
|
||||
await setPersistentSettings(updates);
|
||||
await setState({
|
||||
currentPayPalAccountId: updates.currentPayPalAccountId || null,
|
||||
paypalEmail: updates.paypalEmail,
|
||||
paypalPassword: updates.paypalPassword,
|
||||
});
|
||||
broadcastDataUpdate({
|
||||
currentPayPalAccountId: updates.currentPayPalAccountId || null,
|
||||
paypalEmail: updates.paypalEmail,
|
||||
paypalPassword: updates.paypalPassword,
|
||||
});
|
||||
}
|
||||
|
||||
async function syncPayPalAccounts(accounts) {
|
||||
const normalized = normalizePayPalAccounts(accounts);
|
||||
await setPersistentSettings({ paypalAccounts: normalized });
|
||||
await setState({ paypalAccounts: normalized });
|
||||
broadcastDataUpdate({ paypalAccounts: normalized });
|
||||
|
||||
const state = await getState();
|
||||
if (state.currentPayPalAccountId && !findPayPalAccount(normalized, state.currentPayPalAccountId)) {
|
||||
await syncSelectedPayPalAccountState(null);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getCurrentPayPalAccount(state = {}) {
|
||||
return findPayPalAccount(state.paypalAccounts, state.currentPayPalAccountId) || null;
|
||||
}
|
||||
|
||||
async function upsertPayPalAccount(input = {}) {
|
||||
const state = await getState();
|
||||
const accounts = normalizePayPalAccounts(state.paypalAccounts);
|
||||
const normalizedEmail = String(input?.email || '').trim().toLowerCase();
|
||||
const existing = input?.id
|
||||
? findPayPalAccount(accounts, input.id)
|
||||
: accounts.find((account) => account.email === normalizedEmail) || null;
|
||||
const normalized = normalizePayPalAccount({
|
||||
...(existing || {}),
|
||||
...input,
|
||||
id: input?.id || existing?.id || crypto.randomUUID(),
|
||||
createdAt: existing?.createdAt || Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
|
||||
const nextAccounts = typeof upsertPayPalAccountInList === 'function'
|
||||
? upsertPayPalAccountInList(accounts, normalized)
|
||||
: accounts.concat(normalized);
|
||||
|
||||
await syncPayPalAccounts(nextAccounts);
|
||||
|
||||
if (state.currentPayPalAccountId === normalized.id) {
|
||||
await syncSelectedPayPalAccountState(normalized);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function setCurrentPayPalAccount(accountId) {
|
||||
const state = await getState();
|
||||
const accounts = normalizePayPalAccounts(state.paypalAccounts);
|
||||
const account = findPayPalAccount(accounts, accountId);
|
||||
if (!account) {
|
||||
throw new Error('未找到对应的 PayPal 账号。');
|
||||
}
|
||||
|
||||
await syncSelectedPayPalAccountState(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrentPayPalAccount,
|
||||
setCurrentPayPalAccount,
|
||||
syncPayPalAccounts,
|
||||
upsertPayPalAccount,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
createPayPalAccountStore,
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
prepareStep8DebuggerClick,
|
||||
recoverOAuthLocalhostTimeout,
|
||||
reloadStep8ConsentPage,
|
||||
reuseOrCreateTab,
|
||||
sleepWithStop,
|
||||
@@ -44,19 +45,43 @@
|
||||
|
||||
async function executeStep9(state) {
|
||||
const visibleStep = getVisibleStep(state, 9);
|
||||
if (!state.oauthUrl) {
|
||||
let activeState = state;
|
||||
|
||||
if (!activeState.oauthUrl) {
|
||||
const authLoginStep = getAuthLoginStepForVisibleStep(visibleStep);
|
||||
throw new Error(`缺少登录用 OAuth 链接,请先完成步骤 ${authLoginStep}。`);
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在监听 localhost 回调地址...`);
|
||||
|
||||
const callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(240000, {
|
||||
step: visibleStep,
|
||||
actionLabel: 'OAuth localhost 回调',
|
||||
})
|
||||
: 240000;
|
||||
let callbackTimeoutMs = 240000;
|
||||
let timeoutRecoveryAttempted = false;
|
||||
while (true) {
|
||||
try {
|
||||
callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(240000, {
|
||||
step: visibleStep,
|
||||
actionLabel: 'OAuth localhost 回调',
|
||||
oauthUrl: activeState?.oauthUrl || '',
|
||||
})
|
||||
: 240000;
|
||||
break;
|
||||
} catch (error) {
|
||||
if (timeoutRecoveryAttempted || typeof recoverOAuthLocalhostTimeout !== 'function') {
|
||||
throw error;
|
||||
}
|
||||
const recoveredState = await recoverOAuthLocalhostTimeout({
|
||||
error,
|
||||
state: activeState,
|
||||
visibleStep,
|
||||
});
|
||||
if (!recoveredState) {
|
||||
throw error;
|
||||
}
|
||||
activeState = recoveredState;
|
||||
timeoutRecoveryAttempted = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let resolved = false;
|
||||
@@ -124,7 +149,7 @@
|
||||
await chrome.tabs.update(signupTabId, { active: true });
|
||||
await addLog(`步骤 ${visibleStep}:已切回认证页,正在准备调试器点击...`);
|
||||
} else {
|
||||
signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||
signupTabId = await reuseOrCreateTab('signup-page', activeState.oauthUrl);
|
||||
await addLog(`步骤 ${visibleStep}:已重新打开认证页,正在准备调试器点击...`);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
reuseOrCreateTab,
|
||||
setState,
|
||||
shouldUseCustomRegistrationEmail,
|
||||
sleepWithStop,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
|
||||
throwIfStopped,
|
||||
@@ -167,10 +168,26 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function runStep8Attempt(state) {
|
||||
function getStep8ResendIntervalMs(state = {}) {
|
||||
const mail = getMailConfig(state);
|
||||
if (mail?.provider === LUCKMAIL_PROVIDER) {
|
||||
return 15000;
|
||||
}
|
||||
if (mail?.provider === HOTMAIL_PROVIDER || mail?.provider === '2925') {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Number(STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS) || 0);
|
||||
}
|
||||
|
||||
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'
|
||||
@@ -267,6 +284,16 @@
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep),
|
||||
requestFreshCodeFirst: false,
|
||||
lastResendAt: latestResendAt,
|
||||
onResendRequestedAt: async (requestedAt) => {
|
||||
const numericRequestedAt = Number(requestedAt) || 0;
|
||||
if (numericRequestedAt > 0) {
|
||||
latestResendAt = Math.max(latestResendAt, numericRequestedAt);
|
||||
}
|
||||
if (notifyResendRequestedAt) {
|
||||
await notifyResendRequestedAt(latestResendAt);
|
||||
}
|
||||
},
|
||||
targetEmail: fixedTargetEmail,
|
||||
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
|
||||
? 15000
|
||||
@@ -274,6 +301,9 @@
|
||||
? 0
|
||||
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
|
||||
});
|
||||
return {
|
||||
lastResendAt: latestResendAt,
|
||||
};
|
||||
}
|
||||
|
||||
function isStep8RestartStep7Error(error) {
|
||||
@@ -285,10 +315,22 @@
|
||||
let currentState = state;
|
||||
let mailPollingAttempt = 1;
|
||||
let lastMailPollingError = null;
|
||||
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
await runStep8Attempt(currentState);
|
||||
const result = await runStep8Attempt(currentState, {
|
||||
stickyLastResendAt,
|
||||
onResendRequestedAt: async (requestedAt) => {
|
||||
const numericRequestedAt = Number(requestedAt) || 0;
|
||||
if (numericRequestedAt > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, numericRequestedAt);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (Number(result?.lastResendAt) > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, Number(result.lastResendAt) || 0);
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
const visibleStep = getVisibleStep(currentState, 8);
|
||||
@@ -323,7 +365,29 @@
|
||||
`步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}。`,
|
||||
'warn'
|
||||
);
|
||||
currentState = await getState();
|
||||
const latestState = await getState();
|
||||
const latestStateResendAt = Number(latestState?.loginVerificationRequestedAt) || 0;
|
||||
if (latestStateResendAt > 0) {
|
||||
stickyLastResendAt = Math.max(stickyLastResendAt, latestStateResendAt);
|
||||
}
|
||||
currentState = latestState;
|
||||
if (stickyLastResendAt > 0 && (!latestStateResendAt || latestStateResendAt < stickyLastResendAt)) {
|
||||
currentState = {
|
||||
...latestState,
|
||||
loginVerificationRequestedAt: stickyLastResendAt,
|
||||
};
|
||||
}
|
||||
const resendIntervalMs = getStep8ResendIntervalMs(currentState);
|
||||
const remainingBeforeRetryMs = stickyLastResendAt > 0 && resendIntervalMs > 0
|
||||
? Math.max(0, resendIntervalMs - (Date.now() - stickyLastResendAt))
|
||||
: 0;
|
||||
if (remainingBeforeRetryMs > 0 && typeof sleepWithStop === 'function') {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:上轮已触发重发验证码,为避免重复重发,先等待 ${Math.ceil(remainingBeforeRetryMs / 1000)} 秒后继续当前链路重试。`,
|
||||
'info'
|
||||
);
|
||||
await sleepWithStop(Math.min(remainingBeforeRetryMs, 3000));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
await addLog(
|
||||
|
||||
@@ -40,17 +40,14 @@
|
||||
return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message);
|
||||
}
|
||||
|
||||
function getVisibleStep(state, fallback = 7) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep > 0 ? visibleStep : fallback;
|
||||
}
|
||||
|
||||
async function executeStep7(state) {
|
||||
const visibleStep = getVisibleStep(state, 7);
|
||||
if (!state.email) {
|
||||
throw new Error('缺少邮箱地址,请先完成步骤 3。');
|
||||
}
|
||||
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
const completionStep = visibleStep > 0 ? visibleStep : 7;
|
||||
|
||||
let attempt = 0;
|
||||
let lastError = null;
|
||||
|
||||
@@ -60,22 +57,22 @@
|
||||
try {
|
||||
const currentState = attempt === 1 ? state : await getState();
|
||||
const password = currentState.password || currentState.customPassword || '';
|
||||
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState, { visibleStep });
|
||||
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
|
||||
if (typeof startOAuthFlowTimeoutWindow === 'function') {
|
||||
await startOAuthFlowTimeoutWindow({ step: visibleStep, oauthUrl });
|
||||
await startOAuthFlowTimeoutWindow({ step: 7, oauthUrl });
|
||||
}
|
||||
const loginTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
|
||||
? await getOAuthFlowStepTimeoutMs(180000, {
|
||||
step: visibleStep,
|
||||
step: 7,
|
||||
actionLabel: 'OAuth 登录并进入验证码页',
|
||||
oauthUrl,
|
||||
})
|
||||
: 180000;
|
||||
|
||||
if (attempt === 1) {
|
||||
await addLog(`步骤 ${visibleStep}:正在打开最新 OAuth 链接并登录...`);
|
||||
await addLog('步骤 7:正在打开最新 OAuth 链接并登录...');
|
||||
} else {
|
||||
await addLog(`步骤 ${visibleStep}:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn');
|
||||
await addLog(`步骤 7:上一轮失败后,正在进行第 ${attempt} 次尝试(最多 ${STEP6_MAX_ATTEMPTS} 次)...`, 'warn');
|
||||
}
|
||||
|
||||
await reuseOrCreateTab('signup-page', oauthUrl);
|
||||
@@ -89,14 +86,13 @@
|
||||
payload: {
|
||||
email: currentState.email,
|
||||
password,
|
||||
visibleStep,
|
||||
},
|
||||
},
|
||||
{
|
||||
timeoutMs: loginTimeoutMs,
|
||||
responseTimeoutMs: loginTimeoutMs,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${visibleStep}:认证页正在切换,等待页面重新就绪后继续登录...`,
|
||||
logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续登录...',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -108,13 +104,14 @@
|
||||
const completionPayload = {
|
||||
loginVerificationRequestedAt: result.loginVerificationRequestedAt || null,
|
||||
};
|
||||
if (result.skipLoginVerificationStep) {
|
||||
completionPayload.skipLoginVerificationStep = true;
|
||||
if (Object.prototype.hasOwnProperty.call(result || {}, 'skipLoginVerificationStep')) {
|
||||
completionPayload.skipLoginVerificationStep = Boolean(result.skipLoginVerificationStep);
|
||||
}
|
||||
if (result.directOAuthConsentPage) {
|
||||
completionPayload.directOAuthConsentPage = true;
|
||||
if (Object.prototype.hasOwnProperty.call(result || {}, 'directOAuthConsentPage')) {
|
||||
completionPayload.directOAuthConsentPage = Boolean(result.directOAuthConsentPage);
|
||||
}
|
||||
await completeStepFromBackground(visibleStep, completionPayload);
|
||||
|
||||
await completeStepFromBackground(completionStep, completionPayload);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,7 +129,7 @@
|
||||
}
|
||||
if (isManagementSecretConfigError(err)) {
|
||||
await addLog(
|
||||
`步骤 ${visibleStep}:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
|
||||
`步骤 7:检测到来源后台管理密钥缺失或错误,不再重试,当前流程停止。原因:${getErrorMessage(err)}`,
|
||||
'error'
|
||||
);
|
||||
throw err;
|
||||
@@ -142,11 +139,11 @@
|
||||
break;
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn');
|
||||
await addLog(`步骤 7:第 ${attempt} 次尝试失败,原因:${getErrorMessage(err)};准备重试...`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`步骤 ${visibleStep}:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
|
||||
throw new Error(`步骤 7:判断失败后已重试 ${STEP6_MAX_ATTEMPTS - 1} 次,仍未成功。最后原因:${getErrorMessage(lastError)}`);
|
||||
}
|
||||
|
||||
return { executeStep7 };
|
||||
|
||||
@@ -99,17 +99,30 @@
|
||||
return result || {};
|
||||
}
|
||||
|
||||
function resolvePayPalCredentials(state = {}) {
|
||||
const currentId = String(state?.currentPayPalAccountId || '').trim();
|
||||
const accounts = Array.isArray(state?.paypalAccounts) ? state.paypalAccounts : [];
|
||||
const selectedAccount = currentId
|
||||
? accounts.find((account) => String(account?.id || '').trim() === currentId) || null
|
||||
: null;
|
||||
return {
|
||||
email: String(selectedAccount?.email || state?.paypalEmail || '').trim(),
|
||||
password: String(selectedAccount?.password || state?.paypalPassword || ''),
|
||||
};
|
||||
}
|
||||
|
||||
async function submitLogin(tabId, state = {}) {
|
||||
if (!state.paypalPassword) {
|
||||
throw new Error('步骤 8:未配置 PayPal 密码,请先在侧边栏填写。');
|
||||
const credentials = resolvePayPalCredentials(state);
|
||||
if (!credentials.password) {
|
||||
throw new Error('步骤 8:未配置可用的 PayPal 账号,请先在侧边栏添加并选择账号。');
|
||||
}
|
||||
await addLog('步骤 8:正在填写 PayPal 登录信息并提交...', 'info');
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_SUBMIT_LOGIN',
|
||||
source: 'background',
|
||||
payload: {
|
||||
email: state.paypalEmail || '',
|
||||
password: state.paypalPassword || '',
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
},
|
||||
});
|
||||
if (result?.error) {
|
||||
|
||||
@@ -26,31 +26,23 @@
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function getVisibleStep(state, fallback = 10) {
|
||||
function resolvePlatformVerifyStep(state = {}) {
|
||||
const visibleStep = Math.floor(Number(state?.visibleStep) || 0);
|
||||
return visibleStep > 0 ? visibleStep : fallback;
|
||||
return visibleStep >= 10 ? visibleStep : 10;
|
||||
}
|
||||
|
||||
function getConfirmStepForVisibleStep(visibleStep) {
|
||||
return visibleStep >= 13 ? 12 : 9;
|
||||
}
|
||||
|
||||
function getAuthLoginStepForVisibleStep(visibleStep) {
|
||||
return visibleStep >= 13 ? 10 : 7;
|
||||
}
|
||||
|
||||
function parseLocalhostCallback(rawUrl, visibleStep = 10, confirmStep = 9) {
|
||||
function parseLocalhostCallback(rawUrl) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(rawUrl);
|
||||
} catch {
|
||||
throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 ${confirmStep}。`);
|
||||
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址格式无效,请重新执行步骤 9。');
|
||||
}
|
||||
|
||||
const code = normalizeString(parsed.searchParams.get('code'));
|
||||
const state = normalizeString(parsed.searchParams.get('state'));
|
||||
if (!code || !state) {
|
||||
throw new Error(`步骤 ${visibleStep} 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 ${confirmStep}。`);
|
||||
throw new Error('步骤 10 捕获到的 localhost OAuth 回调地址缺少 code 或 state,请重新执行步骤 9。');
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -72,6 +64,77 @@
|
||||
return details || `Codex2API 请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
function deriveCpaManagementOrigin(vpsUrl) {
|
||||
const normalizedUrl = normalizeString(vpsUrl);
|
||||
if (!normalizedUrl) {
|
||||
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(normalizedUrl);
|
||||
} catch {
|
||||
throw new Error('CPA 地址格式无效,请先在侧边栏检查。');
|
||||
}
|
||||
return parsed.origin;
|
||||
}
|
||||
|
||||
function getCpaApiErrorMessage(payload, responseStatus = 500) {
|
||||
const details = [
|
||||
payload?.error,
|
||||
payload?.message,
|
||||
payload?.detail,
|
||||
payload?.reason,
|
||||
]
|
||||
.map((value) => normalizeString(value))
|
||||
.find(Boolean);
|
||||
return details || `CPA 管理接口请求失败(HTTP ${responseStatus})。`;
|
||||
}
|
||||
|
||||
async function fetchCpaManagementJson(origin, path, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 20000));
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const managementKey = normalizeString(options.managementKey);
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (managementKey) {
|
||||
headers.Authorization = `Bearer ${managementKey}`;
|
||||
headers['X-Management-Key'] = managementKey;
|
||||
}
|
||||
|
||||
const response = await fetch(`${origin}${path}`, {
|
||||
method: options.method || 'POST',
|
||||
headers,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(getCpaApiErrorMessage(payload, response.status));
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
throw new Error('CPA 管理接口请求超时,请稍后重试。');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCodex2ApiJson(origin, path, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
|
||||
@@ -122,95 +185,88 @@
|
||||
}
|
||||
|
||||
async function executeCpaStep10(state) {
|
||||
const visibleStep = getVisibleStep(state, 10);
|
||||
const confirmStep = getConfirmStepForVisibleStep(visibleStep);
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
|
||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
||||
}
|
||||
if (!state.vpsUrl) {
|
||||
throw new Error('尚未填写 CPA 地址,请先在侧边栏输入。');
|
||||
}
|
||||
|
||||
if (shouldBypassStep9ForLocalCpa(state)) {
|
||||
await addLog(`步骤 ${visibleStep}:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。`, 'info');
|
||||
await completeStepFromBackground(visibleStep, {
|
||||
await addLog('步骤 10:检测到本地 CPA,且当前策略为“跳过第10步”,本轮不再重复提交回调地址。', 'info');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
localhostUrl: state.localhostUrl,
|
||||
verifiedStatus: 'local-auto',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在打开 CPA 面板...`);
|
||||
|
||||
const injectFiles = ['content/activation-utils.js', 'content/utils.js', 'content/vps-panel.js'];
|
||||
let tabId = await getTabId('vps-panel');
|
||||
const alive = tabId && await isTabAlive('vps-panel');
|
||||
|
||||
if (!alive) {
|
||||
tabId = await reuseOrCreateTab('vps-panel', state.vpsUrl, {
|
||||
inject: injectFiles,
|
||||
reloadIfSameUrl: true,
|
||||
});
|
||||
} else {
|
||||
await closeConflictingTabsForSource('vps-panel', state.vpsUrl, { excludeTabIds: [tabId] });
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
await rememberSourceLastUrl('vps-panel', state.vpsUrl);
|
||||
const callback = parseLocalhostCallback(state.localhostUrl);
|
||||
const expectedState = normalizeString(state.cpaOAuthState);
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error('CPA 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
|
||||
}
|
||||
const managementKey = normalizeString(state.vpsPassword);
|
||||
if (!managementKey) {
|
||||
throw new Error('尚未配置 CPA 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
await ensureContentScriptReadyOnTab('vps-panel', tabId, {
|
||||
inject: injectFiles,
|
||||
timeoutMs: 45000,
|
||||
retryDelayMs: 900,
|
||||
logMessage: `步骤 ${visibleStep}:CPA 面板仍在加载,正在重试连接...`,
|
||||
});
|
||||
await addLog('步骤 10:正在通过 CPA 管理接口提交回调地址...');
|
||||
try {
|
||||
const origin = normalizeString(state.cpaManagementOrigin) || deriveCpaManagementOrigin(state.vpsUrl);
|
||||
const result = await fetchCpaManagementJson(origin, '/v0/management/oauth-callback', {
|
||||
method: 'POST',
|
||||
managementKey,
|
||||
body: {
|
||||
provider: 'codex',
|
||||
redirect_url: callback.url,
|
||||
},
|
||||
});
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在填写回调地址...`);
|
||||
const result = await sendToContentScriptResilient('vps-panel', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: visibleStep,
|
||||
source: 'background',
|
||||
payload: { localhostUrl: state.localhostUrl, vpsPassword: state.vpsPassword, visibleStep },
|
||||
}, {
|
||||
timeoutMs: 125000,
|
||||
responseTimeoutMs: 125000,
|
||||
retryDelayMs: 700,
|
||||
logMessage: `步骤 ${visibleStep}:CPA 面板通信未就绪,正在等待页面恢复...`,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
const verifiedStatus = normalizeString(result?.message)
|
||||
|| normalizeString(result?.status_message)
|
||||
|| 'CPA 已通过接口提交回调';
|
||||
await addLog(`步骤 10:${verifiedStatus}`, 'ok');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
});
|
||||
} catch (error) {
|
||||
const reason = normalizeString(error?.message) || 'unknown error';
|
||||
await addLog(`步骤 10:CPA 接口提交失败:${reason}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeCodex2ApiStep10(state) {
|
||||
const visibleStep = getVisibleStep(state, 10);
|
||||
const confirmStep = getConfirmStepForVisibleStep(visibleStep);
|
||||
const platformVerifyStep = resolvePlatformVerifyStep(state);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
|
||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
||||
}
|
||||
if (!state.codex2apiSessionId) {
|
||||
throw new Error(`缺少 Codex2API 会话信息,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`);
|
||||
throw new Error('缺少 Codex2API 会话信息,请重新执行步骤 7。');
|
||||
}
|
||||
if (!normalizeString(state.codex2apiAdminKey)) {
|
||||
throw new Error('尚未配置 Codex2API 管理密钥,请先在侧边栏填写。');
|
||||
}
|
||||
|
||||
const callback = parseLocalhostCallback(state.localhostUrl, visibleStep, confirmStep);
|
||||
const callback = parseLocalhostCallback(state.localhostUrl);
|
||||
const expectedState = normalizeString(state.codex2apiOAuthState);
|
||||
if (expectedState && expectedState !== callback.state) {
|
||||
throw new Error(`Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 ${getAuthLoginStepForVisibleStep(visibleStep)}。`);
|
||||
throw new Error('Codex2API 回调 state 与当前授权会话不匹配,请重新执行步骤 7。');
|
||||
}
|
||||
|
||||
const codex2apiUrl = normalizeCodex2ApiUrl(state.codex2apiUrl);
|
||||
const origin = new URL(codex2apiUrl).origin;
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在向 Codex2API 提交回调并创建账号...`);
|
||||
await addLog('步骤 10:正在向 Codex2API 提交回调并创建账号...');
|
||||
const result = await fetchCodex2ApiJson(origin, '/api/admin/oauth/exchange-code', {
|
||||
adminKey: state.codex2apiAdminKey,
|
||||
method: 'POST',
|
||||
@@ -222,21 +278,19 @@
|
||||
});
|
||||
|
||||
const verifiedStatus = normalizeString(result?.message) || 'Codex2API OAuth 账号添加成功';
|
||||
await addLog(`步骤 ${visibleStep}:${verifiedStatus}`, 'ok');
|
||||
await completeStepFromBackground(visibleStep, {
|
||||
await addLog(`步骤 10:${verifiedStatus}`, 'ok');
|
||||
await completeStepFromBackground(platformVerifyStep, {
|
||||
localhostUrl: callback.url,
|
||||
verifiedStatus,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeSub2ApiStep10(state) {
|
||||
const visibleStep = getVisibleStep(state, 10);
|
||||
const confirmStep = getConfirmStepForVisibleStep(visibleStep);
|
||||
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
|
||||
throw new Error(`步骤 ${confirmStep} 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 ${confirmStep}。`);
|
||||
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
|
||||
}
|
||||
if (!state.localhostUrl) {
|
||||
throw new Error(`缺少 localhost 回调地址,请先完成步骤 ${confirmStep}。`);
|
||||
throw new Error('缺少 localhost 回调地址,请先完成步骤 9。');
|
||||
}
|
||||
if (!state.sub2apiSessionId) {
|
||||
throw new Error('缺少 SUB2API 会话信息,请重新执行步骤 1。');
|
||||
@@ -251,7 +305,7 @@
|
||||
const sub2apiUrl = normalizeSub2ApiUrl(state.sub2apiUrl);
|
||||
const injectFiles = ['content/utils.js', 'content/sub2api-panel.js'];
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在打开 SUB2API 后台...`);
|
||||
await addLog('步骤 10:正在打开 SUB2API 后台...');
|
||||
|
||||
let tabId = await getTabId('sub2api-panel');
|
||||
const alive = tabId && await isTabAlive('sub2api-panel');
|
||||
@@ -273,13 +327,12 @@
|
||||
injectSource: 'sub2api-panel',
|
||||
});
|
||||
|
||||
await addLog(`步骤 ${visibleStep}:正在向 SUB2API 提交回调并创建账号...`);
|
||||
await addLog('步骤 10:正在向 SUB2API 提交回调并创建账号...');
|
||||
const result = await sendToContentScript('sub2api-panel', {
|
||||
type: 'EXECUTE_STEP',
|
||||
step: visibleStep,
|
||||
step: 10,
|
||||
source: 'background',
|
||||
payload: {
|
||||
visibleStep,
|
||||
localhostUrl: state.localhostUrl,
|
||||
sub2apiUrl,
|
||||
sub2apiEmail: state.sub2apiEmail,
|
||||
|
||||
@@ -222,6 +222,28 @@
|
||||
return Math.min(20, Math.max(0, Math.floor(numeric)));
|
||||
}
|
||||
|
||||
function getVerificationRequestedAtStateKey(step) {
|
||||
if (Number(step) === 4) return 'signupVerificationRequestedAt';
|
||||
if (Number(step) === 8) return 'loginVerificationRequestedAt';
|
||||
return '';
|
||||
}
|
||||
|
||||
function resolveInitialVerificationRequestedAt(step, state = {}, fallback = 0) {
|
||||
const stateKey = getVerificationRequestedAtStateKey(step);
|
||||
const candidateValues = [
|
||||
fallback,
|
||||
stateKey ? state?.[stateKey] : 0,
|
||||
];
|
||||
|
||||
for (const value of candidateValues) {
|
||||
const numeric = Number(value);
|
||||
if (Number.isFinite(numeric) && numeric > 0) {
|
||||
return Math.floor(numeric);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getLegacyVerificationResendCountDefault(step, options = {}) {
|
||||
const requestFreshCodeFirst = Boolean(options.requestFreshCodeFirst);
|
||||
const legacyMaxRounds = Math.max(1, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1));
|
||||
@@ -985,9 +1007,23 @@
|
||||
: getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst });
|
||||
const maxSubmitAttempts = mail.provider === LUCKMAIL_PROVIDER ? 3 : 15;
|
||||
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
|
||||
let lastResendAt = Number(options.lastResendAt) || 0;
|
||||
const externalOnResendRequestedAt = typeof options.onResendRequestedAt === 'function'
|
||||
? options.onResendRequestedAt
|
||||
: null;
|
||||
let lastResendAt = resolveInitialVerificationRequestedAt(
|
||||
step,
|
||||
state,
|
||||
Number(options.lastResendAt) || 0
|
||||
);
|
||||
|
||||
const updateFilterAfterTimestampForVerificationStep = async (_requestedAt) => {
|
||||
const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => {
|
||||
if (externalOnResendRequestedAt) {
|
||||
try {
|
||||
await externalOnResendRequestedAt(requestedAt);
|
||||
} catch (_) {
|
||||
// Keep resend flow best-effort; state sync callback failures should not break verification.
|
||||
}
|
||||
}
|
||||
return nextFilterAfterTimestamp;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user